Merge pull request #552 from CyberMind-FR/feature/551-android-apk-zero-tap-auto-run-silent-onb
Some checks are pending
License Headers / check (push) Waiting to run

Android: zero-tap auto-run silent onboarding on root (#551)
This commit is contained in:
CyberMind 2026-06-13 13:16:59 +02:00 committed by GitHub
commit dc6505a2f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 54 additions and 30 deletions

View File

@ -12,10 +12,13 @@ import the WireGuard profile, verify the tunnel, then open the live
4. **Verify** — polls `/wg/r3-check` → "Tunnel R3 actif ✓".
5. **Live metrics** — opens `/social/me` (cartographie sociale).
## Root path — fully-automated silent onboarding (#538)
When the device is **rooted**, the Discover step shows an extra
**⚡ Installation automatique (root)** button. Tapping it runs the whole
onboarding with zero further interaction (`RootAuto` step, streaming log):
## Root path — fully-automated silent onboarding (#538, #551)
When the device is **rooted**, the app runs the whole onboarding with **zero
taps**: on launch it auto-detects root and, if this cabine host hasn't been
onboarded yet, starts the silent sequence automatically (`RootAuto` step,
streaming log). The **⚡ Installation automatique (root)** button stays for
re-runs. The "already onboarded" flag is persisted per host (SharedPreferences)
so reopening the app doesn't redo it. Steps:
1. **System CA install** — downloads `/wg/ca.pem`, computes the OpenSSL
`subject_hash_old` in pure Kotlin, and bind-mounts a populated copy of

View File

@ -12,8 +12,8 @@ android {
applicationId = "in.secubox.toolbox"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "0.1.0"
versionCode = 2
versionName = "0.2.0"
}
buildTypes {

View File

@ -64,8 +64,50 @@ fun OnboardApp() {
val api = remember(host) { ToolboxApi(host) }
var rootAvail by remember { mutableStateOf(false) }
val rootLog = remember { mutableStateListOf<String>() }
val prefs = remember {
ctx.getSharedPreferences("secubox-toolbox", android.content.Context.MODE_PRIVATE)
}
var autoTried by remember { mutableStateOf(false) }
// The whole root-mode silent run, reused by the ⚡ button AND the
// zero-tap auto-launch (#551). Persists an onboarded flag per host on
// success so reopening the app doesn't redo it.
val runRootAuto: () -> Unit = {
busy = true; status = ""; rootLog.clear()
scope.launch {
val ok = withContext(Dispatchers.IO) { api.reachable() }
if (!ok) {
busy = false; status = "Borne injoignable — vérifie le réseau."
} else {
step = Step.RootAuto
val onb = RootOnboard(api, ctx.cacheDir)
val out = withContext(Dispatchers.IO) {
onb.runSilent { line -> scope.launch(Dispatchers.Main) { rootLog.add(line) } }
}
busy = false
onTunnel = out.verified
if (out.verified) prefs.edit().putBoolean("onboarded:$host", true).apply()
when {
out.verified -> step = Step.Done
out.wgViaApp -> { step = Step.ImportProfile
status = "CA installé en root ✓ — termine le tunnel via l'app WireGuard." }
else -> { step = Step.Verify
status = "Active le tunnel puis vérifie." }
}
}
}
}
// Detect root once, off the main thread.
LaunchedEffect(Unit) { rootAvail = withContext(Dispatchers.IO) { RootShell.available() } }
// Zero-tap (#551): on a rooted device, auto-run the silent onboarding
// once on launch — unless this host was already onboarded.
LaunchedEffect(rootAvail) {
if (rootAvail && !autoTried && step == Step.Discover) {
autoTried = true
if (!prefs.getBoolean("onboarded:$host", false)) runRootAuto()
}
}
MaterialTheme(colorScheme = darkColorScheme(
primary = Gold, secondary = Cyan, background = Cosmos, surface = Cosmos,
@ -105,32 +147,11 @@ fun OnboardApp() {
}
if (rootAvail) {
Spacer(Modifier.height(10.dp))
Text("🔓 Root détecté — installation 100% automatique possible.",
Text("🔓 Root détecté — l'installation se lance automatiquement. " +
"Tu peux aussi la relancer ici.",
color = Matrix, fontSize = 12.sp)
Spacer(Modifier.height(6.dp))
OutlinedButton(onClick = {
busy = true; status = ""; rootLog.clear()
scope.launch {
val ok = withContext(Dispatchers.IO) { api.reachable() }
if (!ok) { busy = false; status = "Borne injoignable."; return@launch }
step = Step.RootAuto
val onb = RootOnboard(api, ctx.cacheDir)
val out = withContext(Dispatchers.IO) {
onb.runSilent { line ->
scope.launch(Dispatchers.Main) { rootLog.add(line) }
}
}
busy = false
onTunnel = out.verified
when {
out.verified -> step = Step.Done
out.wgViaApp -> { step = Step.ImportProfile
status = "CA installé en root ✓ — termine le tunnel via l'app WireGuard." }
else -> { step = Step.Verify
status = "Active le tunnel puis vérifie." }
}
}
}, modifier = Modifier.fillMaxWidth(),
OutlinedButton(onClick = runRootAuto, modifier = Modifier.fillMaxWidth(),
border = BorderStroke(1.dp, Matrix),
colors = ButtonDefaults.outlinedButtonColors(contentColor = Matrix)) {
Text("⚡ Installation automatique (root)", fontWeight = FontWeight.Bold)