Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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));
}
Expand All @@ -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
Expand Down
27 changes: 27 additions & 0 deletions android/app/src/main/res/animator/kairos_splash_wedge_sweep.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Anime le secteur du cadran (path "wedge" de kairos_splash_icon_base.xml) d'un
sweep nul (aiguille collapsée à midi, aucune aire visible) jusqu'au tracé
final identique au logo statique (secteur 80° depuis midi, coordonnées
reprises telles quelles de ic_launcher_foreground.xml). Morph de `pathData`
par 5 images-clés (0/20/40/60/80°, coordonnées calculées sur le cercle de
rayon 16 — cx+16·sinθ, cy-16·cosθ) plutôt qu'un simple `valueFrom`/`valueTo`
à deux points : l'interpolation d'un `pathData` bouge les coordonnées de
l'arc en ligne droite (corde), jamais l'angle réel — sur un seul segment de
0 à 80°, la corde coupe jusqu'à ~3.7 sur un rayon de 16 au milieu du geste
(secteur visiblement "aplati" à mi-course). Avec des images-clés tous les
20°, l'écart tombe à ~0.24, imperceptible. -->
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:propertyName="pathData"
android:duration="700"
android:valueType="pathType"
android:interpolator="@android:interpolator/fast_out_slow_in">
<propertyValuesHolder android:propertyName="pathData">
<keyframe android:fraction="0" android:value="M20,20 L20,4 A16,16 0 0 1 20,4 Z" />
<keyframe android:fraction="0.25" android:value="M20,20 L20,4 A16,16 0 0 1 25.47,4.96 Z" />
<keyframe android:fraction="0.5" android:value="M20,20 L20,4 A16,16 0 0 1 30.28,7.74 Z" />
<keyframe android:fraction="0.75" android:value="M20,20 L20,4 A16,16 0 0 1 33.86,12.00 Z" />
<keyframe android:fraction="1" android:value="M20,20 L20,4 A16,16 0 0 1 35.76,17.22 Z" />
</propertyValuesHolder>
</objectAnimator>
</set>
12 changes: 12 additions & 0 deletions android/app/src/main/res/drawable/kairos_splash_icon.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- AnimatedVectorDrawable natif (android.graphics.drawable, API 21+, pas
AndroidX) posé sur android:windowSplashScreenAnimatedIcon (themes.xml,
API 31+ seulement — voir docs/ANDROID_PACKAGING.md). Le système détecte
que ce drawable implémente Animatable et joue l'animation automatiquement
à l'affichage du splash, sans code Java dédié. -->
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/kairos_splash_icon_base">
<target
android:name="wedge"
android:animation="@animator/kairos_splash_wedge_sweep" />
</animated-vector>
31 changes: 31 additions & 0 deletions android/app/src/main/res/drawable/kairos_splash_icon_base.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Icône du splash screen (API 31+, voir themes.xml) : même tracé que le logo
Kairos (mire/cadran solaire — static/favicon.svg, templates/base.html,
ic_launcher_foreground.xml), viewBox 40×40 identique pour réutiliser les
mêmes coordonnées sans recalcul. Contrairement à ic_launcher_foreground.xml
(juste le premier plan, pensé pour un masque adaptatif), ce drawable porte
aussi son propre fond ivoire + anneau : il est affiché tel quel par le
splash, sans masque système. Le secteur terracotta (`android:name="wedge"`)
est la seule pièce animée (voir kairos_splash_icon.xml +
animator/kairos_splash_wedge_sweep.xml) : les autres restent statiques. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="160dp"
android:height="160dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:fillColor="#FBEEDF"
android:pathData="M1.5,20 A18.5,18.5 0 1 1 38.5,20 A18.5,18.5 0 1 1 1.5,20 Z" />
<path
android:strokeColor="#E7C4A2"
android:strokeWidth="1.6"
android:fillColor="#00000000"
android:pathData="M1.5,20 A18.5,18.5 0 1 1 38.5,20 A18.5,18.5 0 1 1 1.5,20 Z" />
<path
android:name="wedge"
android:fillColor="#D9713C"
android:pathData="M20,20 L20,4 A16,16 0 0 1 20,4 Z" />
<path
android:fillColor="#2B241E"
android:pathData="M17.4,20 A2.6,2.6 0 1 1 22.6,20 A2.6,2.6 0 1 1 17.4,20 Z" />
</vector>
16 changes: 12 additions & 4 deletions android/app/src/main/res/values/themes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,19 @@
<item name="android:windowLightNavigationBar" tools:targetApi="27">true</item>
<item name="android:windowOptOutEdgeToEdgeEnforcement" tools:targetApi="35">true</item>
<!-- Splash natif (API 31+, sans androidx.core:splashscreen — voir "pas
d'AndroidX" dans docs/ANDROID_PACKAGING.md) : reprend l'icône de
lanceur adaptative existante par défaut, pas d'asset dédié. 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. -->
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. -->
<item name="android:windowSplashScreenBackground" tools:targetApi="31">@color/kairos_bg</item>
<item name="android:windowBackground">@color/kairos_bg</item>
<!-- Icône animée dédiée (kairos_splash_icon.xml, AnimatedVectorDrawable
natif) plutôt que l'icône de lanceur adaptative par défaut : le
secteur du cadran « balaie » depuis midi jusqu'à sa position finale
(même tracé que le logo statique) au lieu d'apparaître figé. Durée
plafonnée à 1000ms par le système ; alignée sur celle de l'animator
(voir animator/kairos_splash_wedge_sweep.xml) pour que le système ne
considère pas l'icône « terminée » avant la fin réelle du sweep. -->
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="31">@drawable/kairos_splash_icon</item>
<item name="android:windowSplashScreenAnimationDuration" tools:targetApi="31">700</item>
</style>
</resources>
38 changes: 38 additions & 0 deletions docs/ANDROID_PACKAGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<application>`
(impératif — sans lui, tout enregistrement de callback reste sans effet même
Expand Down
9 changes: 8 additions & 1 deletion docs/spec/packaging-lancement.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading