From 3d002318d531b9d6d7be2ee3f98e18a36cc59bf3 Mon Sep 17 00:00:00 2001 From: Kazeia Team Date: Thu, 18 Jun 2026 08:27:41 +0200 Subject: [PATCH] =?UTF-8?q?feat(updates):=20MAJ=20manuelles=20pilot=C3=A9e?= =?UTF-8?q?s=20depuis=20l'admin=20(plus=20d'auto-update)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'app patient ne vérifie/installe plus de MAJ au lancement. OnboardingActivity démarre directement si le cœur est présent (offline) ; le catalogue n'est sollicité que pour la 1ʳᵉ installation. Plus de prompt APK ni de worker d'update en arrière-plan au boot. Côté patient : - UpdateStatus (état live) + UpdateWorker (check / install) réutilisant ProvisioningManager (contenu) + ApkUpdater (APK, PackageInstaller). - ContentProvider : call("update_check"|"update_install") enqueue le worker ; query /updates expose phase + dispo APK + nb composants + progression. Côté admin (Kazeia Admin) : - écran « Mises à jour » (nav) : bouton Vérifier + Installer, état + barre de progression (poll /updates), via KazeiaUpdateClient. - L'install APK ouvre le dialogue système PackageInstaller sur la tablette. Bump 0.2.1 / versionCode 13. Round-trip device validé (check → AVAILABLE, 1 composant GGUF 2.38 Go détecté). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../java/com/kazeia/admin/MainActivity.kt | 3 + .../admin/data/source/KazeiaUpdateClient.kt | 66 ++++++++ .../admin/ui/navigation/AdminDestination.kt | 2 + .../kazeia/admin/ui/updates/UpdatesScreen.kt | 157 ++++++++++++++++++ .../app-admin/src/main/res/values/strings.xml | 1 + kazeia-android/app/build.gradle.kts | 4 +- .../main/java/com/kazeia/dist/UpdateStatus.kt | 30 ++++ .../main/java/com/kazeia/dist/UpdateWorker.kt | 102 ++++++++++++ .../telemetry/KazeiaTelemetryProvider.kt | 47 ++++++ .../java/com/kazeia/ui/OnboardingActivity.kt | 33 ++-- 10 files changed, 425 insertions(+), 20 deletions(-) create mode 100644 kazeia-android/app-admin/src/main/java/com/kazeia/admin/data/source/KazeiaUpdateClient.kt create mode 100644 kazeia-android/app-admin/src/main/java/com/kazeia/admin/ui/updates/UpdatesScreen.kt create mode 100644 kazeia-android/app/src/main/java/com/kazeia/dist/UpdateStatus.kt create mode 100644 kazeia-android/app/src/main/java/com/kazeia/dist/UpdateWorker.kt diff --git a/kazeia-android/app-admin/src/main/java/com/kazeia/admin/MainActivity.kt b/kazeia-android/app-admin/src/main/java/com/kazeia/admin/MainActivity.kt index a9c7b4d..f6c2d6b 100644 --- a/kazeia-android/app-admin/src/main/java/com/kazeia/admin/MainActivity.kt +++ b/kazeia-android/app-admin/src/main/java/com/kazeia/admin/MainActivity.kt @@ -105,6 +105,9 @@ private fun MainShell() { composable(AdminDestination.Telemetry.route) { TelemetryScreen() } + composable(AdminDestination.Updates.route) { + com.kazeia.admin.ui.updates.UpdatesScreen() + } composable(AdminDestination.System.route) { SystemScreen() } diff --git a/kazeia-android/app-admin/src/main/java/com/kazeia/admin/data/source/KazeiaUpdateClient.kt b/kazeia-android/app-admin/src/main/java/com/kazeia/admin/data/source/KazeiaUpdateClient.kt new file mode 100644 index 0000000..21e257e --- /dev/null +++ b/kazeia-android/app-admin/src/main/java/com/kazeia/admin/data/source/KazeiaUpdateClient.kt @@ -0,0 +1,66 @@ +package com.kazeia.admin.data.source + +import android.content.ContentResolver +import android.net.Uri + +/** + * Client des mises à jour à la demande de l'app patient (ContentProvider + * `call(update_check|update_install)` + query `/updates`). Le provisioning + * (download modèles + APK) s'exécute côté patient ; l'admin déclenche + suit. + */ +class KazeiaUpdateClient(private val resolver: ContentResolver) { + + companion object { + private const val AUTHORITY = "com.kazeia.provider" + private val ANY_URI: Uri = Uri.parse("content://$AUTHORITY/updates") + const val CALL_CHECK = "update_check" + const val CALL_INSTALL = "update_install" + } + + /** Reflète [com.kazeia.dist.UpdateStatus] côté patient. */ + data class UpdateInfo( + val phase: String, // IDLE/CHECKING/AVAILABLE/UPTODATE/DOWNLOADING/INSTALLING/DONE/ERROR + val appUpdateAvailable: Boolean, + val appVersionName: String, + val appSize: Long, + val contentCount: Int, + val contentSize: Long, + val progressDone: Long, + val progressTotal: Long, + val label: String, + val error: String, + val lastCheck: Long + ) + + /** True si l'app patient est joignable (provider présent). */ + fun reachable(): Boolean = query() != null + + fun check(): Boolean = call(CALL_CHECK) + fun install(): Boolean = call(CALL_INSTALL) + + private fun call(method: String): Boolean = runCatching { + resolver.call(ANY_URI, method, null, null)?.getBoolean("accepted", false) ?: false + }.getOrDefault(false) + + fun query(): UpdateInfo? = runCatching { + resolver.query(ANY_URI, null, null, null, null)?.use { c -> + if (!c.moveToFirst()) return@use null + fun s(n: String) = c.getString(c.getColumnIndexOrThrow(n)) + fun i(n: String) = c.getInt(c.getColumnIndexOrThrow(n)) + fun l(n: String) = c.getLong(c.getColumnIndexOrThrow(n)) + UpdateInfo( + phase = s("phase"), + appUpdateAvailable = i("app_update_available") == 1, + appVersionName = s("app_version_name"), + appSize = l("app_size"), + contentCount = i("content_count"), + contentSize = l("content_size"), + progressDone = l("progress_done"), + progressTotal = l("progress_total"), + label = s("label"), + error = s("error"), + lastCheck = l("last_check") + ) + } + }.getOrNull() +} diff --git a/kazeia-android/app-admin/src/main/java/com/kazeia/admin/ui/navigation/AdminDestination.kt b/kazeia-android/app-admin/src/main/java/com/kazeia/admin/ui/navigation/AdminDestination.kt index 0c30bd8..083ce62 100644 --- a/kazeia-android/app-admin/src/main/java/com/kazeia/admin/ui/navigation/AdminDestination.kt +++ b/kazeia-android/app-admin/src/main/java/com/kazeia/admin/ui/navigation/AdminDestination.kt @@ -10,6 +10,7 @@ import androidx.compose.material.icons.outlined.History import androidx.compose.material.icons.outlined.People import androidx.compose.material.icons.outlined.Tune import androidx.compose.material.icons.outlined.RecordVoiceOver +import androidx.compose.material.icons.outlined.SystemUpdate import androidx.compose.ui.graphics.vector.ImageVector /** @@ -28,6 +29,7 @@ enum class AdminDestination( Rag( "rag", com.kazeia.admin.R.string.nav_rag, Icons.Outlined.Book), History( "history", com.kazeia.admin.R.string.nav_history, Icons.Outlined.History), Telemetry("telemetry", com.kazeia.admin.R.string.nav_telemetry, Icons.Outlined.Analytics), + Updates( "updates", com.kazeia.admin.R.string.nav_updates, Icons.Outlined.SystemUpdate), System( "system", com.kazeia.admin.R.string.nav_system, Icons.Outlined.Build); companion object { diff --git a/kazeia-android/app-admin/src/main/java/com/kazeia/admin/ui/updates/UpdatesScreen.kt b/kazeia-android/app-admin/src/main/java/com/kazeia/admin/ui/updates/UpdatesScreen.kt new file mode 100644 index 0000000..050392e --- /dev/null +++ b/kazeia-android/app-admin/src/main/java/com/kazeia/admin/ui/updates/UpdatesScreen.kt @@ -0,0 +1,157 @@ +package com.kazeia.admin.ui.updates + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.kazeia.admin.data.source.KazeiaUpdateClient +import kotlinx.coroutines.delay + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun UpdatesScreen() { + val context = LocalContext.current + val client = remember { KazeiaUpdateClient(context.contentResolver) } + var info by remember { mutableStateOf(null) } + var reachable by remember { mutableStateOf(true) } + + // Poll continu de l'état (le worker tourne côté patient). + LaunchedEffect(Unit) { + while (true) { + val q = client.query() + reachable = q != null + info = q + delay(1000) + } + } + + val phase = info?.phase ?: "IDLE" + val busy = phase == "CHECKING" || phase == "DOWNLOADING" || phase == "INSTALLING" + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Mises à jour") }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.background) + ) + } + ) { padding -> + Column( + modifier = Modifier.fillMaxSize().padding(padding).padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + if (!reachable) { + Text("Kazeia patient non joignable.", style = MaterialTheme.typography.titleMedium) + Text("Lance l'app Kazeia puis reviens ici.", color = MaterialTheme.colorScheme.onSurfaceVariant) + return@Column + } + + StatusCard(info, busy) + + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Button(enabled = !busy, onClick = { client.check() }) { + Text("Vérifier les mises à jour") + } + val hasUpdate = info?.let { it.appUpdateAvailable || it.contentCount > 0 } == true + Button(enabled = !busy && hasUpdate, onClick = { client.install() }) { + Text("Installer") + } + } + + Text( + "Les mises à jour ne se font plus automatiquement : elles se déclenchent " + + "uniquement ici. L'installation de l'application affichera un dialogue " + + "système à confirmer sur la tablette.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +@Composable +private fun StatusCard(info: KazeiaUpdateClient.UpdateInfo?, busy: Boolean) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + shape = RoundedCornerShape(12.dp) + ) { + Column(Modifier.padding(16.dp)) { + val phase = info?.phase ?: "IDLE" + val (title, detail) = when (phase) { + "IDLE" -> "Aucune vérification" to "Touchez « Vérifier » pour interroger le serveur." + "CHECKING" -> "Vérification en cours…" to "" + "UPTODATE" -> "Tout est à jour ✓" to "Aucune mise à jour disponible." + "AVAILABLE" -> "Mises à jour disponibles" to buildAvailable(info) + "DOWNLOADING" -> "Téléchargement…" to (info?.label ?: "") + "INSTALLING" -> "Installation…" to (info?.label ?: "") + "DONE" -> "Mise à jour terminée ✓" to "L'application redémarrera si elle a été mise à jour." + "ERROR" -> "Échec" to (info?.error ?: "Erreur inconnue.") + else -> phase to "" + } + Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) + if (detail.isNotBlank()) { + Spacer(Modifier.height(4.dp)) + Text(detail, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + if (busy) { + Spacer(Modifier.height(12.dp)) + val total = info?.progressTotal ?: 0L + val done = info?.progressDone ?: 0L + if (total > 0L) { + LinearProgressIndicator( + progress = { (done.toFloat() / total).coerceIn(0f, 1f) }, + modifier = Modifier.fillMaxWidth() + ) + Spacer(Modifier.height(4.dp)) + Text("${human(done)} / ${human(total)}", style = MaterialTheme.typography.bodySmall) + } else { + LinearProgressIndicator(Modifier.fillMaxWidth()) + } + } + } + } +} + +private fun buildAvailable(info: KazeiaUpdateClient.UpdateInfo?): String { + if (info == null) return "" + val parts = mutableListOf() + if (info.appUpdateAvailable) parts += "Application v${info.appVersionName} (${human(info.appSize)})" + if (info.contentCount > 0) parts += "${info.contentCount} composant(s) modèle/voix (${human(info.contentSize)})" + return parts.joinToString("\n") +} + +private fun human(bytes: Long): String { + if (bytes < 1024) return "$bytes o" + val kb = bytes / 1024.0 + if (kb < 1024) return "%.0f Ko".format(kb) + val mb = kb / 1024.0 + if (mb < 1024) return "%.1f Mo".format(mb) + return "%.2f Go".format(mb / 1024.0) +} diff --git a/kazeia-android/app-admin/src/main/res/values/strings.xml b/kazeia-android/app-admin/src/main/res/values/strings.xml index 5cfe72e..23060fd 100644 --- a/kazeia-android/app-admin/src/main/res/values/strings.xml +++ b/kazeia-android/app-admin/src/main/res/values/strings.xml @@ -7,6 +7,7 @@ Profils patients Prompts Paramètres + Mises à jour Base documentaire Historique Telemetry diff --git a/kazeia-android/app/build.gradle.kts b/kazeia-android/app/build.gradle.kts index e67df17..9c0c98b 100644 --- a/kazeia-android/app/build.gradle.kts +++ b/kazeia-android/app/build.gradle.kts @@ -44,8 +44,8 @@ android { applicationId = "com.kazeia" minSdk = 28 targetSdk = 36 - versionCode = 12 - versionName = "0.2.0" + versionCode = 13 + versionName = "0.2.1" // WebDAV de distribution — injecté depuis local.properties. buildConfigField("String", "WEBDAV_BASE", "\"${localProp("webdav.base")}\"") diff --git a/kazeia-android/app/src/main/java/com/kazeia/dist/UpdateStatus.kt b/kazeia-android/app/src/main/java/com/kazeia/dist/UpdateStatus.kt new file mode 100644 index 0000000..8f6038e --- /dev/null +++ b/kazeia-android/app/src/main/java/com/kazeia/dist/UpdateStatus.kt @@ -0,0 +1,30 @@ +package com.kazeia.dist + +/** + * État live des mises à jour, piloté à la demande depuis l'app admin (plus + * d'update automatique au lancement — cf OnboardingActivity). + * + * Singleton en mémoire : le [UpdateWorker] (process patient) écrit, le + * ContentProvider lit (même process). Si le process meurt entre deux étapes, + * l'état repart à IDLE — l'admin n'a qu'à relancer « Vérifier ». Suffisant + * pour le flux check → poll → install → poll piloté côté admin. + */ +object UpdateStatus { + enum class Phase { IDLE, CHECKING, AVAILABLE, UPTODATE, DOWNLOADING, INSTALLING, DONE, ERROR } + + @Volatile var phase: Phase = Phase.IDLE + @Volatile var appUpdateAvailable: Boolean = false + @Volatile var appVersionName: String = "" + @Volatile var appSize: Long = 0L + @Volatile var contentCount: Int = 0 // composants modèles/voix à (re)télécharger + @Volatile var contentSize: Long = 0L + @Volatile var progressDone: Long = 0L + @Volatile var progressTotal: Long = 0L + @Volatile var label: String = "" + @Volatile var error: String = "" + @Volatile var lastCheck: Long = 0L + + fun resetForRun() { + progressDone = 0L; progressTotal = 0L; label = ""; error = "" + } +} diff --git a/kazeia-android/app/src/main/java/com/kazeia/dist/UpdateWorker.kt b/kazeia-android/app/src/main/java/com/kazeia/dist/UpdateWorker.kt new file mode 100644 index 0000000..cc6c785 --- /dev/null +++ b/kazeia-android/app/src/main/java/com/kazeia/dist/UpdateWorker.kt @@ -0,0 +1,102 @@ +package com.kazeia.dist + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters + +/** + * Worker de mise à jour À LA DEMANDE (déclenché par l'app admin via le + * ContentProvider — `call("update_check")` / `call("update_install")`). + * + * - mode CHECK : récupère le catalogue, calcule ce qui est dispo (APK + + * composants modèles/voix périmés) → publie dans [UpdateStatus]. + * - mode INSTALL : télécharge les composants périmés, puis (si dispo) télécharge + * l'APK et lance l'installation système (PackageInstaller). + * + * Réutilise [ProvisioningManager] (contenu) et [ApkUpdater] (APK). Aucune + * mise à jour automatique au lancement : tout passe par ici. + */ +class UpdateWorker( + private val appContext: Context, + params: WorkerParameters +) : CoroutineWorker(appContext, params) { + + private val manager = ProvisioningManager(appContext) + private val apk = ApkUpdater(appContext) + + override suspend fun doWork(): Result { + val install = inputData.getString(KEY_MODE) == MODE_INSTALL + UpdateStatus.resetForRun() + UpdateStatus.phase = UpdateStatus.Phase.CHECKING + + val catalog = manager.fetchCatalog() ?: run { + UpdateStatus.phase = UpdateStatus.Phase.ERROR + UpdateStatus.error = "Serveur de mise à jour injoignable." + return Result.failure() + } + val voices = manager.fetchVoicesCatalog() + + val appUpd = apk.availableUpdate(catalog) + val pending = manager.pending(catalog) + (voices?.let { manager.pending(it) } ?: emptyList()) + + UpdateStatus.appUpdateAvailable = appUpd != null + UpdateStatus.appVersionName = appUpd?.versionName ?: "" + UpdateStatus.appSize = appUpd?.size ?: 0L + UpdateStatus.contentCount = pending.size + UpdateStatus.contentSize = pending.sumOf { it.totalSize } + UpdateStatus.lastCheck = System.currentTimeMillis() + + if (!install) { + UpdateStatus.phase = if (appUpd == null && pending.isEmpty()) + UpdateStatus.Phase.UPTODATE else UpdateStatus.Phase.AVAILABLE + return Result.success() + } + + // ── INSTALL ────────────────────────────────────────────────────────── + // 1. Contenu (modèles + voix), s'il y en a. + if (pending.isNotEmpty()) { + UpdateStatus.phase = UpdateStatus.Phase.DOWNLOADING + val res = manager.provision(catalog) { p -> + UpdateStatus.label = p.componentLabel + UpdateStatus.progressDone = p.overallDone + UpdateStatus.progressTotal = p.overallTotal + } + if (res is ProvisioningManager.Outcome.Failed) { + UpdateStatus.phase = UpdateStatus.Phase.ERROR + UpdateStatus.error = res.reason + return Result.failure() + } + if (voices != null) manager.provision(voices) { p -> + UpdateStatus.label = p.componentLabel + UpdateStatus.progressDone = p.overallDone + UpdateStatus.progressTotal = p.overallTotal + } + } + + // 2. APK — téléchargement + installation système (dialogue PackageInstaller). + if (appUpd != null) { + UpdateStatus.phase = UpdateStatus.Phase.INSTALLING + UpdateStatus.label = "Téléchargement de l'application v${appUpd.versionName}…" + val file = apk.downloadApk(appUpd) { done, total -> + UpdateStatus.progressDone = done; UpdateStatus.progressTotal = total + } + if (file == null) { + UpdateStatus.phase = UpdateStatus.Phase.ERROR + UpdateStatus.error = "Échec du téléchargement de l'APK." + return Result.failure() + } + UpdateStatus.label = "Installation — confirmez le dialogue système." + apk.install(file) // ouvre le dialogue d'installation sur la tablette + } + + UpdateStatus.phase = UpdateStatus.Phase.DONE + return Result.success() + } + + companion object { + const val NAME = "kazeia_update" + const val KEY_MODE = "mode" + const val MODE_CHECK = "check" + const val MODE_INSTALL = "install" + } +} diff --git a/kazeia-android/app/src/main/java/com/kazeia/telemetry/KazeiaTelemetryProvider.kt b/kazeia-android/app/src/main/java/com/kazeia/telemetry/KazeiaTelemetryProvider.kt index 929674b..0f0203a 100644 --- a/kazeia-android/app/src/main/java/com/kazeia/telemetry/KazeiaTelemetryProvider.kt +++ b/kazeia-android/app/src/main/java/com/kazeia/telemetry/KazeiaTelemetryProvider.kt @@ -6,6 +6,7 @@ import android.content.UriMatcher import android.database.Cursor import android.database.MatrixCursor import android.net.Uri +import android.os.Bundle import android.os.Debug import android.os.Process import java.io.File @@ -42,6 +43,10 @@ class KazeiaTelemetryProvider : ContentProvider() { const val PATH_RAG = "rag" const val PATH_RAG_STATUS = "rag_status" const val PATH_RAG_QUERY = "rag_query" + const val PATH_UPDATES = "updates" + // Méthodes ContentProvider.call() pour les MAJ à la demande (pilotées admin). + const val CALL_UPDATE_CHECK = "update_check" + const val CALL_UPDATE_INSTALL = "update_install" const val ACTION_RELOAD_CONFIG = "com.kazeia.action.RELOAD_CONFIG" const val ACTION_RELOAD_PROFILES = "com.kazeia.action.RELOAD_PROFILES" const val ACTION_RELOAD_RAG = "com.kazeia.action.RELOAD_RAG" @@ -64,6 +69,7 @@ class KazeiaTelemetryProvider : ContentProvider() { private const val CODE_RAG_ITEM = 16 private const val CODE_RAG_STATUS = 17 private const val CODE_RAG_QUERY = 18 + private const val CODE_UPDATES = 19 // Emplacements canoniques — résolus par KazeiaPaths (legacy /data/local/tmp // sur tablette dev, stockage externe app sinon). Non-const. @@ -97,6 +103,7 @@ class KazeiaTelemetryProvider : ContentProvider() { addURI(AUTHORITY, "$PATH_RAG/*", CODE_RAG_ITEM) addURI(AUTHORITY, PATH_RAG_STATUS, CODE_RAG_STATUS) addURI(AUTHORITY, PATH_RAG_QUERY, CODE_RAG_QUERY) + addURI(AUTHORITY, PATH_UPDATES, CODE_UPDATES) } override fun onCreate(): Boolean = true @@ -124,9 +131,49 @@ class KazeiaTelemetryProvider : ContentProvider() { CODE_RAG_ITEM -> ragDocCursor(uri.lastPathSegment ?: "") CODE_RAG_STATUS -> ragStatusCursor() CODE_RAG_QUERY -> ragQueryCursor(selectionArgs?.getOrNull(0) ?: selection) + CODE_UPDATES -> updatesCursor() else -> null } + /** État live des mises à jour (lu par l'admin pendant check/install). */ + private fun updatesCursor(): Cursor { + val s = com.kazeia.dist.UpdateStatus + val cols = arrayOf( + "phase", "app_update_available", "app_version_name", "app_size", + "content_count", "content_size", "progress_done", "progress_total", + "label", "error", "last_check" + ) + return MatrixCursor(cols).apply { + addRow(arrayOf( + s.phase.name, + if (s.appUpdateAvailable) 1 else 0, + s.appVersionName, s.appSize, + s.contentCount, s.contentSize, + s.progressDone, s.progressTotal, + s.label, s.error, s.lastCheck + )) + } + } + + /** RPC pilotées admin : déclenche un check ou une installation de MAJ. */ + override fun call(method: String, arg: String?, extras: Bundle?): Bundle? { + val ctx = context ?: return null + val mode = when (method) { + CALL_UPDATE_CHECK -> com.kazeia.dist.UpdateWorker.MODE_CHECK + CALL_UPDATE_INSTALL -> com.kazeia.dist.UpdateWorker.MODE_INSTALL + else -> return null + } + val req = androidx.work.OneTimeWorkRequestBuilder() + .setInputData(androidx.work.workDataOf(com.kazeia.dist.UpdateWorker.KEY_MODE to mode)) + .build() + androidx.work.WorkManager.getInstance(ctx).enqueueUniqueWork( + com.kazeia.dist.UpdateWorker.NAME, + androidx.work.ExistingWorkPolicy.REPLACE, + req + ) + return Bundle().apply { putBoolean("accepted", true) } + } + private fun ragStatusCursor(): Cursor { val ctx = context ?: return MatrixCursor(arrayOf()) val cols = arrayOf("enabled", "ready", "model", "dim", "doc_count", "chunk_count", "pending") diff --git a/kazeia-android/app/src/main/java/com/kazeia/ui/OnboardingActivity.kt b/kazeia-android/app/src/main/java/com/kazeia/ui/OnboardingActivity.kt index 549f64a..4ec80ac 100644 --- a/kazeia-android/app/src/main/java/com/kazeia/ui/OnboardingActivity.kt +++ b/kazeia-android/app/src/main/java/com/kazeia/ui/OnboardingActivity.kt @@ -78,30 +78,27 @@ class OnboardingActivity : AppCompatActivity() { /** Point d'entrée : récupère le catalogue puis décide de la suite. */ private fun begin() { resetUi() - tvStatus.text = "Vérification du catalogue…" + // Les mises à jour sont désormais MANUELLES (pilotées depuis l'app admin : + // écran « Mises à jour », bouton Vérifier/Installer). Au lancement on ne + // consulte plus le catalogue ni l'APK : si le cœur est présent, on démarre + // directement (offline, zéro auto-update). Le catalogue n'est sollicité que + // pour la PREMIÈRE installation (cœur absent) ou un re-provisionnement forcé. + if (!force && manager.hasCoreModels()) { + Log.i(TAG, "modèles présents → Splash (pas d'auto-update)") + goToSplash() + return + } + tvStatus.text = "Première installation…" lifecycleScope.launch { val catalog = withContext(Dispatchers.IO) { manager.fetchCatalog() } - if (catalog == null) { - // Hors-ligne : un install qui tourne déjà peut continuer. - if (!force && manager.hasCoreModels()) { - Log.i(TAG, "catalogue injoignable mais modèles présents → Splash") - goToSplash() - } else { - showError("Connexion requise pour la première installation.\n" + - "Vérifiez le réseau puis réessayez.") - } + if (!force && manager.hasCoreModels()) goToSplash() + else showError("Connexion requise pour la première installation.\n" + + "Vérifiez le réseau puis réessayez.") return@launch } - - // 1. Mise à jour de l'app elle-même. - val update = apkUpdater.availableUpdate(catalog) - if (update != null) { - promptApkUpdate(update, catalog) - return@launch - } - continueWithModels(catalog) + continueWithModels(catalog) // télécharge les modèles manquants (sans prompt APK) } }