feat(updates): MAJ manuelles pilotées depuis l'admin (plus d'auto-update)
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) <noreply@anthropic.com>
This commit is contained in:
parent
a2f82f69f9
commit
3d002318d5
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<KazeiaUpdateClient.UpdateInfo?>(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<String>()
|
||||
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)
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
<string name="nav_profiles">Profils patients</string>
|
||||
<string name="nav_prompts">Prompts</string>
|
||||
<string name="nav_parameters">Paramètres</string>
|
||||
<string name="nav_updates">Mises à jour</string>
|
||||
<string name="nav_rag">Base documentaire</string>
|
||||
<string name="nav_history">Historique</string>
|
||||
<string name="nav_telemetry">Telemetry</string>
|
||||
|
|
|
|||
|
|
@ -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")}\"")
|
||||
|
|
|
|||
|
|
@ -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 = ""
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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<com.kazeia.dist.UpdateWorker>()
|
||||
.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<String>())
|
||||
val cols = arrayOf("enabled", "ready", "model", "dim", "doc_count", "chunk_count", "pending")
|
||||
|
|
|
|||
|
|
@ -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" +
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue