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/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 939b0ee..cca7102 100644 --- a/docs/ANDROID_PACKAGING.md +++ b/docs/ANDROID_PACKAGING.md @@ -94,6 +94,44 @@ 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). + - **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 24aaec9..23a967b 100644 --- a/docs/spec/packaging-lancement.md +++ b/docs/spec/packaging-lancement.md @@ -329,7 +329,14 @@ 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. 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