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
50 changes: 37 additions & 13 deletions android/app/src/main/java/com/skohscripts/kairos/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import android.app.AlertDialog;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.ViewTreeObserver;
import android.webkit.JsResult;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
Expand Down Expand Up @@ -36,25 +38,23 @@ 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.
// WebView (voir `onPageFinished` ci-dessous). Sans mécanisme de retenue, le
// splash (natif API 31+, ou simple fond `windowBackground` en dessous) se
// ferme/laisse place à la première frame dessinée dès `setContentView`
// ci-dessous, donc avant même que Python/uvicorn n'ait démarré — l'utilisateur
// voit alors une WebView blanche pendant toute l'attente du serveur. Technique
// retenue : `ViewTreeObserver.OnPreDrawListener` (voir onCreate) — reporter la
// toute première frame reporte de fait la disparition du splash, quelle que
// soit l'API, sans dépendre d'une classe spécifique à l'API 31+ ni d'AndroidX
// (`androidx.core.splashscreen.SplashScreen.setKeepOnScreenCondition`, la
// seule à porter ce nom, est hors périmètre — voir docs/ANDROID_PACKAGING.md).
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 @@ -74,7 +74,12 @@ protected void onCreate(Bundle savedInstanceState) {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
uiReady.set(true); // libère le splash natif (voir champ `uiReady`)
uiReady.set(true);
// Force une nouvelle passe de dessin pour que le pre-draw listener
// (voir onCreate) soit ré-évalué maintenant que `uiReady` est vrai —
// sans ça, rien ne garantit qu'une invalidation survienne d'elle-même
// pendant que la WebView est restée vide en attendant le serveur.
view.invalidate();
}
});
webView.setWebChromeClient(new WebChromeClient() {
Expand Down Expand Up @@ -109,6 +114,25 @@ public boolean onJsConfirm(WebView view, String url, String message, JsResult re
webView.addJavascriptInterface(notificationBridge, "KairosAndroid");

setContentView(webView);

// Reporte le dessin de la toute première frame tant que `uiReady` est faux
// (voir champ `uiReady` et `onPageFinished` ci-dessus) : technique native
// documentée par Android pour ce cas précis (attente d'un chargement
// asynchrone avant la première frame), sans AndroidX ni dépendance de
// version — fonctionne identiquement sur toutes les API, contrairement à
// une éventuelle API dédiée à l'écran de démarrage natif (API 31+ seulement).
final View content = findViewById(android.R.id.content);
content.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
if (!uiReady.get()) {
return false;
}
content.getViewTreeObserver().removeOnPreDrawListener(this);
return true;
}
});

registerPredictiveBackCallback();

loadWhenServerReady();
Expand Down
34 changes: 24 additions & 10 deletions docs/ANDROID_PACKAGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,17 +98,31 @@ Points notables :
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
blanche pendant toute l'attente). **Piège documenté ici pour ne pas le
retrancher deux fois** : une première tentative a utilisé
`Activity.getSplashScreen().setKeepOnScreenCondition(...)` — cette méthode
**n'existe pas** sur `android.window.SplashScreen` (la classe **native**,
seule autorisée par la contrainte « pas d'AndroidX » ci-dessus) ; elle
n'existe que sur `androidx.core.splashscreen.SplashScreen`, la bibliothèque
de compatibilité, hors périmètre. Erreur de compilation constatée en CI
(`cannot find symbol: method setKeepOnScreenCondition(...)`), corrigée
avant tout usage réel. Mécanisme retenu à la place :
`ViewTreeObserver.OnPreDrawListener` sur la vue de contenu
(`findViewById(android.R.id.content)`, posé juste après `setContentView`) —
reporter le dessin de la toute première frame de l'activité reporte de
fait la disparition du splash (natif API 31+, ou simplement l'affichage du
contenu sous `windowBackground` en dessous), puisque c'est justement ce
dessin qui déclenche cette disparition. La condition de report est 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).
`WebViewClient.onPageFinished` (+ `view.invalidate()` pour forcer une
nouvelle passe de dessin et faire réévaluer le listener) — 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`). Technique
documentée par Android pour ce cas d'usage précis (attente d'un
chargement asynchrone avant la première frame) ; ni classe ni attribut
spécifiques à l'API 31+, fonctionne identiquement à toutes les API sans
garde de version.
- **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é
Expand Down
8 changes: 5 additions & 3 deletions docs/spec/packaging-lancement.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,9 +330,11 @@ cette spec (pas de duplication du reste) :
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`). 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
`MainActivity` par un `ViewTreeObserver.OnPreDrawListener` qui reporte la
toute première frame de l'activité (technique native standard, pas de classe
spécifique à l'API 31+) : sans ce report, 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
Expand Down
Loading