From dd5904f0a7324f8534ccb9d6518095e467ad984d Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Tue, 25 Aug 2026 14:53:25 +0100 Subject: [PATCH 1/2] refacto Makefiles and MainView setup --- Makefile | 171 +------------------------------- src/PostIt/Makefile | 173 ++++++++++++++++++++++++++++++++- src/PostIt/PostIt/App.axaml.cs | 39 +++++--- 3 files changed, 197 insertions(+), 186 deletions(-) diff --git a/Makefile b/Makefile index 9f1eca7f..fa9d4ecf 100644 --- a/Makefile +++ b/Makefile @@ -121,173 +121,4 @@ release: git push -u origin "$$BRANCH"; \ echo "==> Terminé. Branche $$BRANCH live sur origin." -# Cibles pour installer PostIt.Android en Debug sur l'AVD qemu. -# -# Usage typique : -# make qemu # lance l'AVD, attend le boot, build l'APK, l'installe -# make android-install # (re)build l'APK et l'installe (AVD doit tourner) -# make android-build # build l'APK seul (sans install) -# make qemu-run # démarre l'AVD en background -# make qemu-stop # arrête l'émulateur -# make qemu-wait-boot # attend que l'AVD ait fini de booter -# -# Variables surchargeables (make VAR=valeur) : -# AVD_NAME default: postit_test_avd -# (l'AVD doit être listé par `avdmanager list avd`) -# ADB_SERIAL default: emulator-5554 -# (port standard du premier émulateur lancé) -# ANDROID_HOME default: /opt/android-sdk -# (le SDK Android local; doit contenir -# emulator/emulator et platform-tools/adb) -# POSTIT_RID default: android-x64 -# (doit matcher l'ABI de l'AVD; `avdmanager list avd` -# affiche la ligne Tag/ABI) -# EMU_HEADLESS default: 0 -# (1 = lancer l'émulateur sans fenêtre, pour scripter) -# CONFIG surcharge la variable CONFIG globale (Debug par -# défaut dans ce Makefile). Passer à Release pour -# un APK optimisé et signé release. -# LOGCAT_LINES default: 200 -# (nombre de lignes dumpées par `make qemu-logcat`) -# LOGCAT_FOLLOW default: 0 -# (1 = stream live via `make logcat`, -# sinon dump one-shot des N dernières lignes) -# LOGCAT_BOOT_WAIT default: 30 -# (secondes d'attente entre le clear du buffer, -# le `am start`, et le dump final dans -# `make qemu-logcat-boot`) -AVD_NAME ?= postit_test_avd -ADB_SERIAL ?= emulator-5554 -ANDROID_HOME ?= /opt/android-sdk -POSTIT_RID ?= android-x64 -EMU_HEADLESS ?= 0 -LOGCAT_LINES ?= 600 -LOGCAT_FOLLOW ?= 0 -LOGCAT_BOOT_WAIT ?= 30 - -ANDROID_PACKAGE_NAME = fr.pschneider.postit -POSTIT_ANDROID_CSPROJ := src/PostIt/PostIt.Android/PostIt.Android.csproj -POSTIT_APK_DIR := src/PostIt/PostIt.Android/bin/$(CONFIG)/net10.0-android/$(POSTIT_RID) -POSTIT_APK := $(POSTIT_APK_DIR)/$(ANDROID_PACKAGE_NAME)-Signed.apk - -qemu-run: - @echo " Starting AVD $(AVD_NAME) on $(ADB_SERIAL)..." - @mkdir -p /tmp/yavsc-emu - @EMU_ARGS=""; \ - if [ "$(EMU_HEADLESS)" = "1" ]; then EMU_ARGS="-no-window -no-audio"; fi; \ - $(ANDROID_HOME)/emulator/emulator -avd $(AVD_NAME) $$EMU_ARGS \ - >/tmp/yavsc-emu/$(AVD_NAME).log 2>&1 & \ - echo " emulator PID: $$!" - -qemu-stop: - adb -s $(ADB_SERIAL) emu kill - -qemu-wait-boot: - @echo " Waiting for $(ADB_SERIAL) to finish booting..." - adb -s $(ADB_SERIAL) wait-for-device - @for i in $$(seq 1 180); do \ - BOOTED=$$(adb -s $(ADB_SERIAL) shell getprop sys.boot_completed 2>/dev/null | tr -d '\r\n'); \ - if [ "$$BOOTED" = "1" ]; then \ - echo " ✓ booted in $${i}s"; \ - exit 0; \ - fi; \ - sleep 1; \ - done; \ - echo " ERROR: device did not boot within 180s." >&2; \ - echo " Logs: /tmp/yavsc-emu/$(AVD_NAME).log" >&2; \ - exit 1 - -android-build: - # EmbedAssembliesIntoApk=true: without this, the Debug APK ships - # without the managed assemblies in it (they are pushed at runtime - # via `adb push`, "Fast Deployment"). On the qemu emulator, the - # runtime cannot find them in `files/.__override__//` and - # aborts at startup with "No assemblies found in '.__override__'" - # (monodroid-glue.cc:757, SIGABRT). Forcing this property on - # packages the .dlls into the APK as `assemblies//` so the - # runtime reads them directly. - # - # The Xamarin.Android SDK property is `EmbedAssembliesIntoApk`, - # not `AndroidEnableFastDeployment` (which exists in older - # templates but is a no-op in the .NET 10 SDK). - dotnet build $(POSTIT_ANDROID_CSPROJ) \ - -c $(CONFIG) \ - -p:RuntimeIdentifier=$(POSTIT_RID) \ - -p:EmbedAssembliesIntoApk=true \ - --nologo - @if [ ! -f "$(POSTIT_APK)" ]; then \ - echo " APK not found at $(POSTIT_APK)." >&2; \ - echo " Files in $(POSTIT_APK_DIR):" >&2; \ - ls -la "$(POSTIT_APK_DIR)" 2>/dev/null || echo " (directory does not exist)" >&2; \ - exit 1; \ - fi - - -android-install: android-build - @echo " Installing $(POSTIT_APK) on $(ADB_SERIAL)..." - adb -s $(ADB_SERIAL) install -r "$(POSTIT_APK)" -r - -qemu-uninstall: - adb -s $(ADB_SERIAL) uninstall $(ANDROID_PACKAGE_NAME) - -# Dump recent logcat output for the running PostIt.Android process. -# By default, prints the last $(LOGCAT_LINES) lines (one-shot, with -# `-d`). Set LOGCAT_FOLLOW=1 to follow the stream live instead. -# Filtering is by PID (pidof $(ANDROID_PACKAGE_NAME)), not by tag, -# because Mono/Xamarin can emit logs under several tags -# (mono, PostIt.Android, Avalonia.Android) and tag-based filtering -# would miss the ones not matching. PID-based filtering is exact. -# If the app is not running, pidof returns empty and logcat exits -# silently with no output; that is the expected behaviour for -# "no logs yet". -logcat: - @PID=$$(adb -s $(ADB_SERIAL) shell pidof $(ANDROID_PACKAGE_NAME) 2>/dev/null | tr -d '\r\n'); \ - if [ -z "$$PID" ]; then \ - echo " $(ANDROID_PACKAGE_NAME) is not running on $(ADB_SERIAL)."; \ - echo " Start the app first (am start -n $(ANDROID_PACKAGE_NAME)/PostIt.Android.PostItMainActivity)"; \ - exit 1; \ - fi; \ - echo " Following PID $$PID (LOGCAT_FOLLOW=$(LOGCAT_FOLLOW), LOGCAT_LINES=$(LOGCAT_LINES))"; \ - if [ "$(LOGCAT_FOLLOW)" = "1" ]; then \ - adb -s $(ADB_SERIAL) logcat -v time --pid=$$PID $(ANDROID_PACKAGE_NAME); \ - else \ - adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES) --pid=$$PID $(ANDROID_PACKAGE_NAME); \ - fi - -# Clear logcat, launch PostIt.Android, then dump everything that was -# emitted during the startup window. Targets the "démarrage KO" case -# where the process starts but Avalonia never renders a frame — the -# logcat trace from process start to first frame is what diagnoses it. -# -# Override LOGCAT_BOOT_WAIT to extend the post-launch wait -# (default 15s; raise to 30+ if the device is slow to boot Avalonia). -LOGCAT_BOOT_WAIT ?= 15 - - -android-start: - @echo " Clearing logcat buffer..." - adb -s $(ADB_SERIAL) logcat -c - @echo " Launching $(ANDROID_PACKAGE_NAME)..." - adb -s $(ADB_SERIAL) shell am start \ - -n $(ANDROID_PACKAGE_NAME)/PostIt.Android.PostItMainActivity - -qemu-logcat-boot: android-start - @echo " Waiting $(LOGCAT_BOOT_WAIT)s for the app to start rendering..." - @sleep $(LOGCAT_BOOT_WAIT) - - @echo " Dumping logcat (PostIt PID + system buffer):" - @PID=$$(adb -s $(ADB_SERIAL) shell pidof $(ANDROID_PACKAGE_NAME) 2>/dev/null | tr -d '\r\n'); \ - if [ -n "$$PID" ]; then \ - echo " ✅ (PID $$PID at dump time)"; \ - sleep 10; \ - adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES) --pid=$$PID; \ - else \ - echo " 👿 (PostIt process not running at dump time — dumping last $(LOGCAT_LINES) lines unfiltered)"; \ - adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES); \ - exit 1; \ - fi - -qemu: qemu-run qemu-wait-boot android-install - @echo " ✓ PostIt.Android installed on $(ADB_SERIAL)" - -.PHONY: test release qemu qemu-run qemu-stop qemu-wait-boot android-build android-install logcat qemu-logcat-boot +.PHONY: test release diff --git a/src/PostIt/Makefile b/src/PostIt/Makefile index 72158bfa..1217976b 100644 --- a/src/PostIt/Makefile +++ b/src/PostIt/Makefile @@ -1,7 +1,178 @@ +# Cibles pour installer PostIt.Android en Debug sur l'AVD qemu. +# +# Usage typique : +# make qemu # lance l'AVD, attend le boot, build l'APK, l'installe +# make android-install # (re)build l'APK et l'installe (AVD doit tourner) +# make android-build # build l'APK seul (sans install) +# make qemu-run # démarre l'AVD en background +# make qemu-stop # arrête l'émulateur +# make qemu-wait-boot # attend que l'AVD ait fini de booter +# +# Variables surchargeables (make VAR=valeur) : +# AVD_NAME default: postit_test_avd +# (l'AVD doit être listé par `avdmanager list avd`) +# ADB_SERIAL default: emulator-5554 +# (port standard du premier émulateur lancé) +# ANDROID_HOME default: /opt/android-sdk +# (le SDK Android local; doit contenir +# emulator/emulator et platform-tools/adb) +# POSTIT_RID default: android-x64 +# (doit matcher l'ABI de l'AVD; `avdmanager list avd` +# affiche la ligne Tag/ABI) +# EMU_HEADLESS default: 0 +# (1 = lancer l'émulateur sans fenêtre, pour scripter) +# CONFIG surcharge la variable CONFIG globale (Debug par +# défaut dans ce Makefile). Passer à Release pour +# un APK optimisé et signé release. +# LOGCAT_LINES default: 200 +# (nombre de lignes dumpées par `make qemu-logcat`) +# LOGCAT_FOLLOW default: 0 +# (1 = stream live via `make logcat`, +# sinon dump one-shot des N dernières lignes) +# LOGCAT_BOOT_WAIT default: 30 +# (secondes d'attente entre le clear du buffer, +# le `am start`, et le dump final dans +# `make qemu-logcat-boot`) +AVD_NAME ?= postit_test_avd +ADB_SERIAL ?= emulator-5554 +ANDROID_HOME ?= /opt/android-sdk +POSTIT_RID ?= android-x64 +EMU_HEADLESS ?= 0 +LOGCAT_LINES ?= 600 +LOGCAT_FOLLOW ?= 0 +LOGCAT_BOOT_WAIT ?= 30 + +ANDROID_PACKAGE_NAME = fr.pschneider.postit +POSTIT_ANDROID_CSPROJ := PostIt.Android/PostIt.Android.csproj +POSTIT_APK_DIR := PostIt.Android/bin/$(CONFIG)/net10.0-android/$(POSTIT_RID) +POSTIT_APK := $(POSTIT_APK_DIR)/$(ANDROID_PACKAGE_NAME)-Signed.apk + clean: clean-PostIt clean-PostIt.Android clean-PostIt.Desktop clean-%: rm -rf $*/obj $*/bin -.PHONY: clean +qemu-run: + @echo " Starting AVD $(AVD_NAME) on $(ADB_SERIAL)..." + @mkdir -p /tmp/yavsc-emu + @EMU_ARGS=""; \ + if [ "$(EMU_HEADLESS)" = "1" ]; then EMU_ARGS="-no-window -no-audio"; fi; \ + $(ANDROID_HOME)/emulator/emulator -avd $(AVD_NAME) $$EMU_ARGS \ + >/tmp/yavsc-emu/$(AVD_NAME).log 2>&1 & \ + echo " ✅ Started emulator PID: $$!" + +qemu-stop: + adb -s $(ADB_SERIAL) emu kill + echo " ✅ Stopped emulator" + +qemu-wait-boot: + @echo " Waiting for $(ADB_SERIAL) to finish booting..." + adb -s $(ADB_SERIAL) wait-for-device + @for i in $$(seq 1 180); do \ + BOOTED=$$(adb -s $(ADB_SERIAL) shell getprop sys.boot_completed 2>/dev/null | tr -d '\r\n'); \ + if [ "$$BOOTED" = "1" ]; then \ + echo " ✓ booted in $${i}s"; \ + exit 0; \ + fi; \ + sleep 1; \ + done; \ + echo " 👿 ERROR: device did not boot within 180s." >&2; \ + echo " Logs: /tmp/yavsc-emu/$(AVD_NAME).log" >&2; \ + exit 1 + +android-build: + # EmbedAssembliesIntoApk=true: without this, the Debug APK ships + # without the managed assemblies in it (they are pushed at runtime + # via `adb push`, "Fast Deployment"). On the qemu emulator, the + # runtime cannot find them in `files/.__override__//` and + # aborts at startup with "No assemblies found in '.__override__'" + # (monodroid-glue.cc:757, SIGABRT). Forcing this property on + # packages the .dlls into the APK as `assemblies//` so the + # runtime reads them directly. + # + # The Xamarin.Android SDK property is `EmbedAssembliesIntoApk`, + # not `AndroidEnableFastDeployment` (which exists in older + # templates but is a no-op in the .NET 10 SDK). + dotnet build $(POSTIT_ANDROID_CSPROJ) \ + -c $(CONFIG) \ + -p:RuntimeIdentifier=$(POSTIT_RID) \ + -p:EmbedAssembliesIntoApk=true \ + --nologo + @if [ ! -f "$(POSTIT_APK)" ]; then \ + echo " APK not found at $(POSTIT_APK)." >&2; \ + echo " Files in $(POSTIT_APK_DIR):" >&2; \ + ls -la "$(POSTIT_APK_DIR)" 2>/dev/null || echo " (directory does not exist)" >&2; \ + exit 1; \ + fi + + +android-install: android-build + @echo " Installing $(POSTIT_APK) on $(ADB_SERIAL)..." + adb -s $(ADB_SERIAL) install -r "$(POSTIT_APK)" -r + @echo " ✅ PostIt.Android installed on $(ADB_SERIAL)" + +qemu-uninstall: + adb -s $(ADB_SERIAL) uninstall $(ANDROID_PACKAGE_NAME) + +# Dump recent logcat output for the running PostIt.Android process. +# By default, prints the last $(LOGCAT_LINES) lines (one-shot, with +# `-d`). Set LOGCAT_FOLLOW=1 to follow the stream live instead. +# Filtering is by PID (pidof $(ANDROID_PACKAGE_NAME)), not by tag, +# because Mono/Xamarin can emit logs under several tags +# (mono, PostIt.Android, Avalonia.Android) and tag-based filtering +# would miss the ones not matching. PID-based filtering is exact. +# If the app is not running, pidof returns empty and logcat exits +# silently with no output; that is the expected behaviour for +# "no logs yet". +logcat: + @PID=$$(adb -s $(ADB_SERIAL) shell pidof $(ANDROID_PACKAGE_NAME) 2>/dev/null | tr -d '\r\n'); \ + if [ -z "$$PID" ]; then \ + echo " $(ANDROID_PACKAGE_NAME) is not running on $(ADB_SERIAL)."; \ + echo " Start the app first (am start -n $(ANDROID_PACKAGE_NAME)/PostIt.Android.PostItMainActivity)"; \ + exit 1; \ + fi; \ + echo " Following PID $$PID (LOGCAT_FOLLOW=$(LOGCAT_FOLLOW), LOGCAT_LINES=$(LOGCAT_LINES))"; \ + if [ "$(LOGCAT_FOLLOW)" = "1" ]; then \ + adb -s $(ADB_SERIAL) logcat -v time --pid=$$PID $(ANDROID_PACKAGE_NAME); \ + else \ + adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES) --pid=$$PID $(ANDROID_PACKAGE_NAME); \ + fi + +# Clear logcat, launch PostIt.Android, then dump everything that was +# emitted during the startup window. Targets the "démarrage KO" case +# where the process starts but Avalonia never renders a frame — the +# logcat trace from process start to first frame is what diagnoses it. +# +# Override LOGCAT_BOOT_WAIT to extend the post-launch wait +# (default 15s; raise to 30+ if the device is slow to boot Avalonia). +LOGCAT_BOOT_WAIT ?= 15 + + +android-start: + @echo " Clearing logcat buffer..." + adb -s $(ADB_SERIAL) logcat -c + @echo " Launching $(ANDROID_PACKAGE_NAME)..." + adb -s $(ADB_SERIAL) shell am start \ + -n $(ANDROID_PACKAGE_NAME)/PostIt.Android.PostItMainActivity + @echo " ✅ $(ANDROID_PACKAGE_NAME) started on $(ADB_SERIAL)" + +qemu-logcat-boot: android-start + @echo " Waiting $(LOGCAT_BOOT_WAIT)s for the app to start rendering..." + @sleep $(LOGCAT_BOOT_WAIT) + + @echo " Dumping logcat (PostIt PID + system buffer):" + @PID=$$(adb -s $(ADB_SERIAL) shell pidof $(ANDROID_PACKAGE_NAME) 2>/dev/null | tr -d '\r\n'); \ + if [ -n "$$PID" ]; then \ + echo " ✅ (PID $$PID at dump time)"; \ + sleep 10; \ + adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES) --pid=$$PID; \ + else \ + echo " 👿 (PostIt process not running at dump time — dumping last $(LOGCAT_LINES) lines unfiltered)"; \ + adb -s $(ADB_SERIAL) logcat -d -v time -t $(LOGCAT_LINES); \ + exit 1; \ + fi + +qemu: qemu-run qemu-wait-boot android-install + +.PHONY: clean qemu qemu-run qemu-stop qemu-wait-boot android-build android-install logcat qemu-logcat-boot diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index 67d6dac2..f8430347 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -44,27 +44,15 @@ public partial class App : Application this.ServiceProvider = new ServiceCollection().BuildServices(); var settings = ServiceProvider.GetRequiredService(); - var sessionStatus = ServiceProvider!.GetRequiredService(); - sessionStatus.LogoutCompleted += () => - { - View.NavRoot.PopToRootAsync(); - }; - sessionStatus.LoginSucceeded += () => - { - PushMainPageAsync(); - }; if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { var window = ServiceProvider.GetRequiredService(); desktop.MainWindow = window; var api = ServiceProvider!.GetRequiredService(); desktop.MainWindow.Opened += async (_, _) => await BootAsync(this.ServiceProvider!, api); - View = window.MainView; - - View.SessionBanner.DataContext = sessionStatus; - // FIXME Window.Opened += async (_, _) => await BootAsync(this.ServiceProvider!, api); + this.ConfigureRootView(window.MainView); ApplyDarkMode(settings); } @@ -74,15 +62,15 @@ public partial class App : Application () => { View = ServiceProvider.GetRequiredService(); + this.ConfigureRootView(View); ApplyDarkMode(settings); - View.SessionBanner.DataContext = sessionStatus; return View; }; } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) { singleViewPlatform.MainView = View = ServiceProvider.GetRequiredService(); - View.SessionBanner.DataContext = sessionStatus; + ConfigureRootView(View); ApplyDarkMode(settings); } var mainVm = ServiceProvider!.GetRequiredService(); @@ -91,6 +79,27 @@ public partial class App : Application this.PushPageAsync(mainVm); } +private void ConfigureRootView(MainView rootView) +{ + var api = ServiceProvider!.GetRequiredService(); + + // Déclencher le Boot lors du chargement du contrôle à l'écran + rootView.AttachedToVisualTree += async (_, _) => await BootAsync(this.ServiceProvider!, api); + + var sessionStatus = ServiceProvider!.GetRequiredService(); + sessionStatus.LogoutCompleted += () => + { + // Remplacer Window.NavRoot par rootView.NavRoot + rootView.NavRoot.PopToRootAsync(); + }; + + sessionStatus.LoginSucceeded += () => + { + PushMainPageAsync(); + }; + + rootView.SessionBanner.DataContext = sessionStatus; +} /// /// Test-only hook: bind a concrete so From 75b298b0f80c1c1768290c248efdbb810025004c Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Tue, 25 Aug 2026 19:20:03 +0100 Subject: [PATCH 2/2] Fixes the Android Login process --- .vscode/launch.json | 14 +++---- .vscode/settings.json | 2 + .vscode/tasks.json | 4 +- src/PostIt/PostIt.Android/MainActivity.cs | 16 +++++--- .../PostIt.Android/PlatformBootstrap.cs | 7 +--- .../Properties/AndroidManifest.xml | 26 ------------- .../Services/AndroidSystemBrowser.cs | 10 ++++- .../Services/OidcCallbackManager.cs | 21 ++++++++++ .../WebAuthenticationCallbackActivity.cs | 35 +++++++++++++++++ .../PostIt.Desktop/PlatformBootstrap.cs | 29 -------------- src/PostIt/PostIt.Desktop/Program.cs | 2 - src/PostIt/PostIt.Tests/SettingsLoadTests.cs | 2 +- src/PostIt/PostIt/App.axaml.cs | 10 ++--- src/PostIt/PostIt/Services/Platform.cs | 6 +-- .../PostIt/Settings/AuthenticationSettings.cs | 12 ++++-- src/PostIt/PostIt/ViewModels/Settings.cs | 39 ++----------------- 16 files changed, 106 insertions(+), 129 deletions(-) create mode 100644 src/PostIt/PostIt.Android/Services/OidcCallbackManager.cs create mode 100644 src/PostIt/PostIt.Android/WebAuthenticationCallbackActivity.cs delete mode 100644 src/PostIt/PostIt.Desktop/PlatformBootstrap.cs diff --git a/.vscode/launch.json b/.vscode/launch.json index 36dc17a2..dc8d3c68 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,19 +5,19 @@ "version": "0.2.0", "configurations": [ { - "name": "Android", + "name": "Android Debug", "type": "mono", "preLaunchTask": "run-debug-android", "request": "attach", "address": "localhost", - "port": 10000 + "port": 55555 }, { - "name": "Attach - Android", + "name": "Android Attach - Debug", "type": "mono", "request": "attach", "address": "localhost", - "port": 10000 + "port": 55555 }, { "name": "API", @@ -26,19 +26,19 @@ "projectPath": "${workspaceFolder}/src/Api/Api.csproj" }, { - "name": "Yavsc.Org", + "name": "Yavsc Org", "type": "dotnet", "request": "launch", "projectPath": "${workspaceFolder}/src/Yavsc.Org/Yavsc.Org.csproj", }, { - "name": "Yavsc.Blogs", + "name": "Yavsc Blogs", "type": "dotnet", "request": "launch", "projectPath": "${workspaceFolder}/src/Yavsc.Blogs/Yavsc.Blogs.csproj" }, { - "name": "PostIt", + "name": "PostIt Desktop", "type": "dotnet", "request": "launch", "projectPath": "${workspaceFolder}/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj", diff --git a/.vscode/settings.json b/.vscode/settings.json index 16bbe483..915683fc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ "appsettings", "asciidoctor", "ASPNETCORE", + "Avalonia", "Configurabilité", "Cratie", "DESTDIR", @@ -15,6 +16,7 @@ "Hsts", "Newtonsoft", "Npgsql", + "Oidc", "PKCE", "postit", "pschneider", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 9fe9caaf..3c0c0bf2 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -18,8 +18,8 @@ "-p:TargetFramework=net10.0-android", "-p:Configuration=Debug", "-p:AndroidAttachDebugger=true", - "-p:AndroidSdbHostPort=10000", - "-p:AndroidSdbTargetPort=10000" + "-p:AndroidSdbHostPort=55555", + "-p:AndroidSdbTargetPort=55555" ], "problemMatcher": [ { diff --git a/src/PostIt/PostIt.Android/MainActivity.cs b/src/PostIt/PostIt.Android/MainActivity.cs index 4f788550..ad8455ef 100644 --- a/src/PostIt/PostIt.Android/MainActivity.cs +++ b/src/PostIt/PostIt.Android/MainActivity.cs @@ -1,13 +1,11 @@ -using Android; + using Android.App; using Android.Content; using Android.Content.PM; using AndroidX.Core.Provider; using AndroidX.Emoji2.Text; -using Avalonia; using Avalonia.Android; -using AndroidX.Core.Provider; -using AndroidX.Emoji2.Text; +using PostIt.Droid.Services; namespace PostIt.Android; @@ -34,7 +32,7 @@ public class MainActivity : AvaloniaMainActivity Yavsc.Resource.Array.com_google_android_gms_fonts_certs); //com_google_android_gms_fonts_certs EmojiCompat.Config config = new FontRequestEmojiCompatConfig(this, fontRequest); EmojiCompat.Init(config); - PlatformBootstrap.EnsureInitialized(); + PlatformBootstrap.InitPlatform(); base.OnCreate(savedInstanceState); Current = this; } @@ -50,7 +48,13 @@ public class MainActivity : AvaloniaMainActivity protected override void OnNewIntent(Intent? intent) { base.OnNewIntent(intent); - if (intent is not null) AndroidOidcCallbackSink.Handle(intent); + + var url = intent?.DataString; + if (!string.IsNullOrEmpty(url) && url.StartsWith("postit://callback")) + { + OidcCallbackManager.SetResult(url); + } + } internal static class AndroidOidcCallbackSink diff --git a/src/PostIt/PostIt.Android/PlatformBootstrap.cs b/src/PostIt/PostIt.Android/PlatformBootstrap.cs index d59b154f..f208f9ce 100644 --- a/src/PostIt/PostIt.Android/PlatformBootstrap.cs +++ b/src/PostIt/PostIt.Android/PlatformBootstrap.cs @@ -12,14 +12,9 @@ namespace PostIt.Android; /// internal static class PlatformBootstrap { - private static int _initialized; - - internal static void EnsureInitialized() + internal static void InitPlatform() { - if (System.Threading.Interlocked.Exchange(ref _initialized, 1) != 0) - return; - Platform.DefaultRedirectUri = ViewModels.Settings.AndroidRedirectUri; Platform.CreateBrowser = () => { var activity = MainActivity.Current; diff --git a/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml b/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml index 91b61d05..8793aae8 100644 --- a/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml +++ b/src/PostIt/PostIt.Android/Properties/AndroidManifest.xml @@ -2,31 +2,5 @@ - - - - - - - - - - diff --git a/src/PostIt/PostIt.Android/Services/AndroidSystemBrowser.cs b/src/PostIt/PostIt.Android/Services/AndroidSystemBrowser.cs index b6716e22..bb10b364 100644 --- a/src/PostIt/PostIt.Android/Services/AndroidSystemBrowser.cs +++ b/src/PostIt/PostIt.Android/Services/AndroidSystemBrowser.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Android.App; using AndroidX.Browser.CustomTabs; using IdentityModel.OidcClient.Browser; +using PostIt.Droid.Services; namespace PostIt.Android.Services; @@ -35,9 +36,14 @@ public sealed class AndroidSystemBrowser : IBrowser }; } - var uri = global::Android.Net.Uri.Parse(options.StartUrl)!; + // 1. Enregistrez la tâche avant de lancer le Custom Tab + var callbackTask = OidcCallbackManager.RegisterCallback(cancellationToken); - var callbackTask = MainActivity.AndroidOidcCallbackSink.AwaitNextCallbackAsync(); + // 2. LANCEZ VOTRE CUSTOM TAB ICI (via AndroidX.Browser.CustomTabs) + // ... code pour ouvrir l'URL d'authentification ... + + + var uri = global::Android.Net.Uri.Parse(options.StartUrl)!; var tabsIntent = new CustomTabsIntent.Builder() .SetShowTitle(true)! diff --git a/src/PostIt/PostIt.Android/Services/OidcCallbackManager.cs b/src/PostIt/PostIt.Android/Services/OidcCallbackManager.cs new file mode 100644 index 00000000..30f8fa18 --- /dev/null +++ b/src/PostIt/PostIt.Android/Services/OidcCallbackManager.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace PostIt.Droid.Services; + +public static class OidcCallbackManager +{ + private static TaskCompletionSource? _tcs; + + public static Task RegisterCallback(CancellationToken cancellationToken) + { + _tcs = new TaskCompletionSource(); + cancellationToken.Register(() => _tcs.TrySetCanceled()); + return _tcs.Task; + } + + public static void SetResult(string url) + { + _tcs?.TrySetResult(url); + } +} diff --git a/src/PostIt/PostIt.Android/WebAuthenticationCallbackActivity.cs b/src/PostIt/PostIt.Android/WebAuthenticationCallbackActivity.cs new file mode 100644 index 00000000..9ed2eb18 --- /dev/null +++ b/src/PostIt/PostIt.Android/WebAuthenticationCallbackActivity.cs @@ -0,0 +1,35 @@ +using Android.App; +using Android.Content; +using Android.Content.PM; +using Android.OS; +using PostIt.Droid.Services; + +namespace PostIt.Android; + +[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop, Exported = true)] +[IntentFilter(new[] { Intent.ActionView }, + Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable }, + DataScheme = "postit", // Remplacez par votre schéma personnalisé (ex: yavsc ou postit) + DataHost = "callback")] // Correspond à postit://callback +public class WebAuthenticationCallbackActivity : Activity +{ + protected override void OnCreate(Bundle? savedInstanceState) + { + base.OnCreate(savedInstanceState); + + // Capturer l'URL de redirection OIDC + var url = Intent?.DataString; + + if (!string.IsNullOrEmpty(url)) + { + // Transmettre l'URL au gestionnaire partagé pour compléter la Task + OidcCallbackManager.SetResult(url); + } + + // Fermer cette activité transparente et ramener l'application au premier plan + var intent = new Intent(this, typeof(MainActivity)); + intent.AddFlags(ActivityFlags.ClearTop | ActivityFlags.SingleTop); + StartActivity(intent); + Finish(); + } +} diff --git a/src/PostIt/PostIt.Desktop/PlatformBootstrap.cs b/src/PostIt/PostIt.Desktop/PlatformBootstrap.cs deleted file mode 100644 index ff9ca7f6..00000000 --- a/src/PostIt/PostIt.Desktop/PlatformBootstrap.cs +++ /dev/null @@ -1,29 +0,0 @@ -using PostIt.Services; - -namespace PostIt.Desktop; - -/// -/// One-shot platform bootstrap. Called from Program.Main so that -/// the shared OIDC login path sees a working IBrowser — the -/// custom-scheme browser that hands the OIDC callback off to the -/// running instance through the named pipe. Desktop builds do NOT use -/// a loopback HTTP listener: the postit:// scheme is registered -/// with the OS at install time and the browser is whatever the user -/// has configured to open it. -/// -internal static class PlatformBootstrap -{ - private static int _initialized; - - internal static void EnsureInitialized() - { - if (System.Threading.Interlocked.Exchange(ref _initialized, 1) != 0) - return; - - // Use the custom-scheme redirect on Desktop. Loopback is only - // a fallback for platforms that cannot register postit:// - // (see Settings.DefaultLoopbackRedirectUri for that path). - Platform.DefaultRedirectUri = AuthenticationSettings.DefaultDesktopRedirectUri; - Platform.CustomScheme = "postit"; - } -} diff --git a/src/PostIt/PostIt.Desktop/Program.cs b/src/PostIt/PostIt.Desktop/Program.cs index f22f79b0..0de3bd69 100644 --- a/src/PostIt/PostIt.Desktop/Program.cs +++ b/src/PostIt/PostIt.Desktop/Program.cs @@ -12,8 +12,6 @@ sealed class Program [STAThread] public static void Main(string[] args) { - PlatformBootstrap.EnsureInitialized(); - // Short-circuit 2nd-instance launches (OS handing us the // postit://callback URL) BEFORE Avalonia spins up a window. // If we let Avalonia initialise, the new MainWindow flashes diff --git a/src/PostIt/PostIt.Tests/SettingsLoadTests.cs b/src/PostIt/PostIt.Tests/SettingsLoadTests.cs index dc58e9fc..43021caa 100644 --- a/src/PostIt/PostIt.Tests/SettingsLoadTests.cs +++ b/src/PostIt/PostIt.Tests/SettingsLoadTests.cs @@ -85,7 +85,7 @@ public class SettingsLoadTests bool flip = ((workerId + i) & 1) == 0; settings.DarkMode = flip; settings.Authentication.RedirectUri = - global::AuthenticationSettings.DefaultDesktopRedirectUri; + global::AuthenticationSettings.DesktopRedirectUri; settings.BusinessApiUrl = flip ? "https://a.example.test/api/v1/" diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index f8430347..1b019f6e 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; @@ -73,10 +72,8 @@ public partial class App : Application ConfigureRootView(View); ApplyDarkMode(settings); } - var mainVm = ServiceProvider!.GetRequiredService(); base.OnFrameworkInitializationCompleted(); - this.PushPageAsync(mainVm); } private void ConfigureRootView(MainView rootView) @@ -93,9 +90,9 @@ private void ConfigureRootView(MainView rootView) rootView.NavRoot.PopToRootAsync(); }; - sessionStatus.LoginSucceeded += () => + sessionStatus.LoginSucceeded += async () => { - PushMainPageAsync(); + await PushMainPageAsync(); }; rootView.SessionBanner.DataContext = sessionStatus; @@ -133,6 +130,9 @@ private void ConfigureRootView(MainView rootView) var refreshed = await api.TrySilentLoginAsync().ConfigureAwait(true); var sessionStatus = provider.GetRequiredService(); sessionStatus.Refresh(); + var homePage = provider.GetRequiredService(); + var app = (App)Current!; + await app.PushPageAsync(homePage); if (!refreshed) return; await PushMainPageAsync().ConfigureAwait(true); diff --git a/src/PostIt/PostIt/Services/Platform.cs b/src/PostIt/PostIt/Services/Platform.cs index c867c63c..8e5f7e25 100644 --- a/src/PostIt/PostIt/Services/Platform.cs +++ b/src/PostIt/PostIt/Services/Platform.cs @@ -21,14 +21,14 @@ public static class Platform /// override this property at startup (e.g. PostIt.Android sets /// it to android://postit-signin). /// - public static string DefaultRedirectUri { get; set; } = "postit://callback"; + public const string RedirectUri = "postit://callback"; /// /// Scheme prefix the matches /// against BrowserOptions.EndUrl. Overridable for apps /// that want to register their own scheme. /// - public static string CustomScheme { get; set; } = "postit"; + public const string CustomScheme = "postit"; /// /// Constructs a fresh for the running platform. @@ -37,4 +37,4 @@ public static class Platform /// public static System.Func? CreateBrowser { get; set; } = () => new CustomSchemeBrowser(CustomScheme); -} \ No newline at end of file +} diff --git a/src/PostIt/PostIt/Settings/AuthenticationSettings.cs b/src/PostIt/PostIt/Settings/AuthenticationSettings.cs index 99358cfb..9ebeaebd 100644 --- a/src/PostIt/PostIt/Settings/AuthenticationSettings.cs +++ b/src/PostIt/PostIt/Settings/AuthenticationSettings.cs @@ -10,7 +10,7 @@ public partial class AuthenticationSettings : ObservableObject /// hand-off in /// (RFC 8252 §7.1). Production Desktop builds use this. /// - public const string DefaultDesktopRedirectUri = "postit://callback"; + public const string DesktopRedirectUri = "postit://callback"; /// /// Redirect URI used by the Android app. The corresponding IntentFilter @@ -34,15 +34,19 @@ public partial class AuthenticationSettings : ObservableObject [ObservableProperty] public partial string[] Scopes { get; set; } - /// - /// OAuth redirect URI. Defaults to + /// OAuth redirect URI. Defaults to /// (custom URI scheme) which is the right answer for desktop /// production builds. Mobile platforms must set this to /// before calling LoginAsync. /// [ObservableProperty] - public partial string RedirectUri { get; set; } = DefaultDesktopRedirectUri; + public partial string RedirectUri { get; set; } +#if ANDROID + = AndroidRedirectUri; +#else + = DesktopRedirectUri; +#endif /// /// Space-separated view of . Exists for the diff --git a/src/PostIt/PostIt/ViewModels/Settings.cs b/src/PostIt/PostIt/ViewModels/Settings.cs index 2115a0cd..fd87d772 100644 --- a/src/PostIt/PostIt/ViewModels/Settings.cs +++ b/src/PostIt/PostIt/ViewModels/Settings.cs @@ -2,13 +2,11 @@ using System.Runtime.CompilerServices; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using IdentityModel.OidcClient; -using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Text.Json; -using System.Threading; [assembly: InternalsVisibleTo("PostIt.Tests")] @@ -18,37 +16,6 @@ public partial class Settings : ViewModelBase { const string SettingsFileName = "postit-settings.json"; - /// - /// Redirect URI used by the Android app. The corresponding IntentFilter - /// in PostIt.Android/Properties/AndroidManifest.xml must match. - /// - public const string AndroidRedirectUri = "android://postit-signin"; - - - - /// - /// Process-wide canonical instance, wired up - /// at application boot by - /// through . The hybrid pattern: - /// - /// The static Current reference gives - /// ViewModels a non-DI way to reach the same instance (and lets - /// the framework bindings push notifications through one stable - /// ). - /// Tests that want to exercise a clean - /// instance still call new Settings(); Current - /// stays null in those contexts because - /// is never invoked. - /// Reads () are - /// thread-safe and never allocate; mutations always go through - /// the DI-resolved singleton so two threads cannot each register - /// a different "current" Settings. - /// - /// - private static Settings? s_current; - - - [ObservableProperty] public partial AuthenticationSettings Authentication { get; set; } = new(); @@ -66,7 +33,7 @@ public partial class Settings : ViewModelBase /// setters above all funnel through here, and we flip /// in lock-step. Sub-property mutations /// (e.g. Authentication.Authority) are caught by the - /// subscription wired up in + /// subscription wired up in /// below. disables the flag during bulk /// hydration so the disk load itself does not count as a user /// edit. @@ -336,7 +303,7 @@ public partial class Settings : ViewModelBase this.Authentication.ClientId = string.IsNullOrWhiteSpace(settings.Authentication.ClientId) ? AuthenticationSettings.DefaultClientId : settings.Authentication.ClientId; this.Authentication.RedirectUri = string.IsNullOrWhiteSpace(settings.Authentication.RedirectUri) ? - AuthenticationSettings.DefaultDesktopRedirectUri : settings.Authentication.RedirectUri; + AuthenticationSettings.DesktopRedirectUri : settings.Authentication.RedirectUri; if (settings.Authentication.Scopes is null || settings.Authentication.Scopes.Length == 0) { settings.Authentication.Scopes = AuthenticationSettings.DefaultScopes; @@ -377,7 +344,7 @@ public partial class Settings : ViewModelBase { Authority = AuthenticationSettings.DefaultAuthority, ClientId = AuthenticationSettings.DefaultClientId, - RedirectUri = AuthenticationSettings.DefaultDesktopRedirectUri, + RedirectUri = AuthenticationSettings.DesktopRedirectUri, Scopes = AuthenticationSettings.DefaultScopes }; this.DarkMode = false;