From 483ce7f260978ba05db14c4bc57db2d5790c5c57 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 21:43:00 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Retient=20le=20splash=20screen=20Android=20?= =?UTF-8?q?jusqu'au=20premier=20rendu=20r=C3=A9el=20de=20la=20WebView?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vérification manuelle post-merge (PR #24) : la bottom nav et le geste retour prédictif fonctionnent, mais le splash screen natif n'apparaît pas visiblement. Cause : android:windowSplashScreenBackground/windowBackground seuls ne couvrent que la fenêtre de thème — le splash se ferme dès la première frame dessinée par setContentView(webView), bien avant que Python/uvicorn n'ait fini de démarrer, laissant place à une WebView blanche pendant toute l'attente réelle (jusqu'à 30s, sonde /favicon.ico). Fix : MainActivity retient le splash via Activity.getSplashScreen().setKeepOnScreenCondition(...) (natif, android.window, API 31+, pas AndroidX), condition liée à un AtomicBoolean mis à true par WebViewClient.onPageFinished — donc jusqu'à ce que la première page ait réellement fini de charger, pas seulement jusqu'à la réponse du serveur. Sans effet en dessous de l'API 31 (méthode absente du framework, appel gardé par un contrôle de version explicite). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_012rz3jgonqVHP4fzQsJh8oP --- .../com/skohscripts/kairos/MainActivity.java | 23 ++++++++++++++++++- docs/ANDROID_PACKAGING.md | 15 ++++++++++++ docs/spec/packaging-lancement.md | 6 ++++- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/java/com/skohscripts/kairos/MainActivity.java b/android/app/src/main/java/com/skohscripts/kairos/MainActivity.java index 9f6bc50..96e4fb5 100644 --- a/android/app/src/main/java/com/skohscripts/kairos/MainActivity.java +++ b/android/app/src/main/java/com/skohscripts/kairos/MainActivity.java @@ -17,6 +17,7 @@ import java.net.HttpURLConnection; import java.net.URL; +import java.util.concurrent.atomic.AtomicBoolean; /** * Unique activité de Kairos : démarre le serveur local (CPython + uvicorn via @@ -34,12 +35,26 @@ public class MainActivity extends Activity { private WebView webView; private KairosNotificationBridge notificationBridge; + // Tenu à `false` jusqu'à ce que la première page ait fini de charger dans la + // WebView (voir `onPageFinished` ci-dessous) : sans ce verrou, le splash natif + // se ferme dès la première frame dessinée par `setContentView` ci-dessous, donc + // avant même que Python/uvicorn n'ait démarré — l'utilisateur voit alors une + // WebView blanche à la place du splash pendant toute l'attente du serveur. + private final AtomicBoolean uiReady = new AtomicBoolean(false); @SuppressLint("SetJavaScriptEnabled") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // Doit être appelé avant `setContentView` (voir plus bas) pour pouvoir + // retenir le splash natif — sans effet sur API < 31 (pas d'AndroidX, cf. + // docs/ANDROID_PACKAGING.md : `getSplashScreen()` n'existe pas avant cette + // API, d'où la garde explicite). + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + getSplashScreen().setKeepOnScreenCondition(() -> !uiReady.get()); + } + if (!Python.isStarted()) { Python.start(new AndroidPlatform(this)); } @@ -55,7 +70,13 @@ protected void onCreate(Bundle savedInstanceState) { webView = new WebView(this); webView.getSettings().setJavaScriptEnabled(true); // chrono vivant, alertes webView.getSettings().setDomStorageEnabled(true); - webView.setWebViewClient(new WebViewClient()); // navigation interne, pas de navigateur externe + webView.setWebViewClient(new WebViewClient() { // navigation interne, pas de navigateur externe + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + uiReady.set(true); // libère le splash natif (voir champ `uiReady`) + } + }); webView.setWebChromeClient(new WebChromeClient() { // Sans WebChromeClient, le WebView système n'affiche jamais les dialogues // JS confirm()/alert() : confirm() résout silencieusement à false, donc les diff --git a/docs/ANDROID_PACKAGING.md b/docs/ANDROID_PACKAGING.md index 939b0ee..e789135 100644 --- a/docs/ANDROID_PACKAGING.md +++ b/docs/ANDROID_PACKAGING.md @@ -94,6 +94,21 @@ Points notables : générique pendant le démarrage de Python+uvicorn ; `windowBackground` seul couvre aussi les appareils API 24-30 (minSdk 24, en dessous du seuil splash-screen natif). + - **Durée du splash retenue jusqu'au premier rendu réel** (constaté sur + appareil : le thème seul ne suffisait pas — le splash disparaissait dès la + première frame dessinée par `setContentView(webView)`, bien avant que + Python/uvicorn n'ait fini de démarrer, laissant place à une WebView + blanche pendant toute l'attente). `MainActivity` retient le splash via + `Activity.getSplashScreen().setKeepOnScreenCondition(...)` (natif, + `android.window`, API 31+ seulement, pas AndroidX), condition liée à un + champ `uiReady` (`AtomicBoolean`) mis à `true` par + `WebViewClient.onPageFinished` — donc jusqu'à ce que la première page ait + réellement fini de charger dans la WebView, pas seulement jusqu'à la + réponse du serveur (`loadWhenServerReady`/sonde `/favicon.ico`, qui ne + fait que déclencher le `loadUrl`). Appelé avant `setContentView`, seule + séquence valide pour que la condition prenne effet. En dessous de l'API + 31, `windowBackground` reste le seul mécanisme (pas de retenue possible, + l'API `getSplashScreen()` n'existe pas sur ces versions). - **Geste retour prédictif** (Android 13+/15, même revue) : `AndroidManifest.xml` pose `android:enableOnBackInvokedCallback="true"` au niveau `` (impératif — sans lui, tout enregistrement de callback reste sans effet même diff --git a/docs/spec/packaging-lancement.md b/docs/spec/packaging-lancement.md index 24aaec9..dc500f8 100644 --- a/docs/spec/packaging-lancement.md +++ b/docs/spec/packaging-lancement.md @@ -329,7 +329,11 @@ cette spec (pas de duplication du reste) : API 31+) et `android:windowBackground` (toutes API) posés à la couleur de fond de l'app — évite le flash blanc générique pendant le démarrage de Python+uvicorn, sans dépendance `androidx.core:splashscreen` (voir « pas d'AndroidX » dans - `docs/ANDROID_PACKAGING.md`). + `docs/ANDROID_PACKAGING.md`). Sa durée réelle d'affichage est contrôlée depuis + `MainActivity` (`getSplashScreen().setKeepOnScreenCondition(...)`, natif, API + 31+) : sans ce contrôle, le thème seul ne suffit pas à couvrir l'attente du + serveur, le splash se ferme dès la première frame dessinée. Détail complet dans + `docs/ANDROID_PACKAGING.md`. - **`AndroidManifest.xml`** / **`MainActivity.java`** : geste retour prédictif Android 13+ (`android:enableOnBackInvokedCallback="true"` + `OnBackInvokedDispatcher` natif, `android.window`, pas AndroidX) — chemin From 80aa8406d92b1e4e6df91198f64841505dbbdce8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 22:11:40 +0000 Subject: [PATCH 2/2] =?UTF-8?q?Anime=20l'ic=C3=B4ne=20du=20splash=20screen?= =?UTF-8?q?=20:=20le=20cadran=20balaie=20depuis=20midi=20jusqu'au=20logo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Suite au fix de durée du splash (même PR) : l'icône était jusqu'ici statique (icône de lanceur adaptative reprise telle quelle). Ajoute une icône dédiée animée (AnimatedVectorDrawable natif, android.graphics.drawable, API 21+, toujours sans AndroidX) qui reprend le tracé exact du logo Kairos et anime le secteur terracotta d'un balayage nul (aiguille collapsée à midi) jusqu'à sa position finale (secteur de 80° depuis midi, coordonnées identiques à ic_launcher_foreground.xml). Le morph de pathData se fait par 5 images-clés (0/20/40/60/80°) plutôt qu'un simple départ/arrivée à deux points : l'interpolation d'un pathData déplace les coordonnées de l'arc en ligne droite (corde), jamais l'angle réel — sur un seul segment de 80° la corde coupe jusqu'à ~3.7 sur un rayon de 16 au plus fort du geste (secteur visiblement aplati à mi-course, vérifié par calcul) ; des images-clés tous les 20° ramènent cet écart à ~0.24, imperceptible. Posée sur android:windowSplashScreenAnimatedIcon (API 31+) : le système détecte l'Animatable et joue l'animation automatiquement à l'affichage du splash, sans code Java dédié. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_012rz3jgonqVHP4fzQsJh8oP --- .../animator/kairos_splash_wedge_sweep.xml | 27 ++++++++++++++++ .../main/res/drawable/kairos_splash_icon.xml | 12 +++++++ .../res/drawable/kairos_splash_icon_base.xml | 31 +++++++++++++++++++ android/app/src/main/res/values/themes.xml | 16 +++++++--- docs/ANDROID_PACKAGING.md | 23 ++++++++++++++ docs/spec/packaging-lancement.md | 7 +++-- 6 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 android/app/src/main/res/animator/kairos_splash_wedge_sweep.xml create mode 100644 android/app/src/main/res/drawable/kairos_splash_icon.xml create mode 100644 android/app/src/main/res/drawable/kairos_splash_icon_base.xml diff --git a/android/app/src/main/res/animator/kairos_splash_wedge_sweep.xml b/android/app/src/main/res/animator/kairos_splash_wedge_sweep.xml new file mode 100644 index 0000000..b3ce341 --- /dev/null +++ b/android/app/src/main/res/animator/kairos_splash_wedge_sweep.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/kairos_splash_icon.xml b/android/app/src/main/res/drawable/kairos_splash_icon.xml new file mode 100644 index 0000000..57bc5f0 --- /dev/null +++ b/android/app/src/main/res/drawable/kairos_splash_icon.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/android/app/src/main/res/drawable/kairos_splash_icon_base.xml b/android/app/src/main/res/drawable/kairos_splash_icon_base.xml new file mode 100644 index 0000000..1946b4d --- /dev/null +++ b/android/app/src/main/res/drawable/kairos_splash_icon_base.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml index 289c6db..d7b99d8 100644 --- a/android/app/src/main/res/values/themes.xml +++ b/android/app/src/main/res/values/themes.xml @@ -11,11 +11,19 @@ true true + d'AndroidX" dans docs/ANDROID_PACKAGING.md). En dessous de l'API 31, + `windowBackground` seul unifie déjà le flash de démarrage avec le + fond de l'app plutôt que le blanc générique. --> @color/kairos_bg @color/kairos_bg + + @drawable/kairos_splash_icon + 700 diff --git a/docs/ANDROID_PACKAGING.md b/docs/ANDROID_PACKAGING.md index e789135..cca7102 100644 --- a/docs/ANDROID_PACKAGING.md +++ b/docs/ANDROID_PACKAGING.md @@ -109,6 +109,29 @@ Points notables : séquence valide pour que la condition prenne effet. En dessous de l'API 31, `windowBackground` reste le seul mécanisme (pas de retenue possible, l'API `getSplashScreen()` n'existe pas sur ces versions). + - **Icône animée** (`android:windowSplashScreenAnimatedIcon`, API 31+) : + plutôt que l'icône de lanceur adaptative statique par défaut, un + `AnimatedVectorDrawable` dédié + (`res/drawable/kairos_splash_icon.xml` + `kairos_splash_icon_base.xml` + + `res/animator/kairos_splash_wedge_sweep.xml`, natif + `android.graphics.drawable`, API 21+, pas AndroidX) anime le secteur + terracotta du cadran d'un balayage nul (« aiguille » collapsée à midi) + jusqu'à sa position finale identique au logo statique (secteur de 80° + depuis midi, mêmes coordonnées que `ic_launcher_foreground.xml`). Le + système détecte l'`Animatable` et joue l'animation automatiquement à + l'affichage du splash, sans code Java dédié — `windowSplashScreenAnimationDuration` + (700ms, plafond système 1000ms) doit couvrir la durée réelle de + l'animator pour que le système ne considère pas l'icône « terminée » + trop tôt. Morph de `pathData` par 5 images-clés (0/20/40/60/80°) plutôt + qu'un simple `valueFrom`/`valueTo` à deux points : l'interpolation d'un + `pathData` déplace les coordonnées de l'arc en ligne droite (corde), + jamais l'angle réel — sur un seul segment de 80°, la corde couperait + jusqu'à ~3.7 sur un rayon de 16 au plus fort du geste (secteur + visiblement aplati à mi-course) ; des images-clés tous les 20° ramènent + cet écart à ~0.24, imperceptible. Le drawable de base porte son propre + fond ivoire + anneau (contrairement à `ic_launcher_foreground.xml`, + pensé pour un masque adaptatif) : affiché tel quel par le splash, sans + masque système. - **Geste retour prédictif** (Android 13+/15, même revue) : `AndroidManifest.xml` pose `android:enableOnBackInvokedCallback="true"` au niveau `` (impératif — sans lui, tout enregistrement de callback reste sans effet même diff --git a/docs/spec/packaging-lancement.md b/docs/spec/packaging-lancement.md index dc500f8..23a967b 100644 --- a/docs/spec/packaging-lancement.md +++ b/docs/spec/packaging-lancement.md @@ -332,8 +332,11 @@ cette spec (pas de duplication du reste) : `docs/ANDROID_PACKAGING.md`). Sa durée réelle d'affichage est contrôlée depuis `MainActivity` (`getSplashScreen().setKeepOnScreenCondition(...)`, natif, API 31+) : sans ce contrôle, le thème seul ne suffit pas à couvrir l'attente du - serveur, le splash se ferme dès la première frame dessinée. Détail complet dans - `docs/ANDROID_PACKAGING.md`. + serveur, le splash se ferme dès la première frame dessinée. Son icône est un + `AnimatedVectorDrawable` dédié (`res/drawable/kairos_splash_icon*.xml` + + `res/animator/kairos_splash_wedge_sweep.xml`, natif, API 21+) : le secteur du + logo balaie depuis midi jusqu'à sa position finale plutôt que d'apparaître + figé. Détail complet dans `docs/ANDROID_PACKAGING.md`. - **`AndroidManifest.xml`** / **`MainActivity.java`** : geste retour prédictif Android 13+ (`android:enableOnBackInvokedCallback="true"` + `OnBackInvokedDispatcher` natif, `android.window`, pas AndroidX) — chemin