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