app: make the Shizuku banner actionable (open Shizuku / request permission)

A third-party app cannot start Shizuku — the wireless-debugging pairing
flow is privileged and lives in Shizuku's own app — so the banner
deep-links there when it is installed but stopped, and fires the
permission request directly when it is running but unauthorised. The hint
line says which.

Verified on-device together with the earlier UX work: progress bar showing
"test 4 of 8 · icmp.ping6 · ~33s left", Cancel beside the disabled Run
button, cutout-safe title, and the banner live-updating from
not-running to needs-permission via the binder listener.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-08-01 09:58:39 +02:00
co-authored by Claude Opus 5
parent d7dda40e4e
commit 1d9e2063bf
4 changed files with 65 additions and 5 deletions
@@ -12,6 +12,7 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.clickable
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
@@ -64,6 +65,19 @@ class MainActivity : ComponentActivity() {
state = vm.state,
onRun = { vm.run() },
onCancel = vm::cancel,
onShizukuAction = {
// Shizuku can only be started from its own app (the pairing flow lives
// there), so send the user straight to it; if it is already running we
// just need permission.
when (vm.state.shizukuState) {
app.echo_lot.shizuku.ShizukuAvailability.State.NEEDS_PERMISSION ->
app.echo_lot.shizuku.ShizukuAvailability.requestPermission()
app.echo_lot.shizuku.ShizukuAvailability.State.INSTALLED_NOT_RUNNING ->
app.echo_lot.shizuku.ShizukuAvailability.launchIntent(this)
?.let { startActivity(it) }
else -> Unit
}
},
onExport = { doc -> startActivity(Intent.createChooser(Report.share(this, doc), "Export Echolot run")) },
)
}
@@ -99,6 +113,7 @@ private fun EcholotScreen(
state: UiState,
onRun: () -> Unit,
onCancel: () -> Unit,
onShizukuAction: () -> Unit,
onExport: (MeasurementDocument) -> Unit,
) {
Column(
@@ -119,15 +134,23 @@ private fun EcholotScreen(
// only users who actually use it get reminded that it must be running.
state.shizukuNotice?.let { notice ->
Card(
Modifier.fillMaxWidth(),
Modifier.fillMaxWidth().let { m ->
if (state.shizukuHint != null) m.clickable { onShizukuAction() } else m
},
colors = CardDefaults.cardColors(
containerColor = if (state.shizukuReady) Color(0xFF14301F) else Color(0xFF3A2E12),
),
) {
Text(
notice, Modifier.padding(10.dp), fontSize = 12.sp,
color = if (state.shizukuReady) Color(0xFF9CCFA8) else Color(0xFFFFD08A),
)
Column(Modifier.padding(10.dp)) {
Text(
notice, fontSize = 12.sp,
color = if (state.shizukuReady) Color(0xFF9CCFA8) else Color(0xFFFFD08A),
)
state.shizukuHint?.let {
Text(it, fontSize = 11.sp, fontWeight = FontWeight.SemiBold,
color = Color(0xFFFFB454))
}
}
}
}
@@ -41,6 +41,8 @@ data class UiState(
/** Shell-tier readiness, shown before a run; null message = say nothing (Shizuku not installed). */
val shizukuNotice: String? = null,
val shizukuReady: Boolean = false,
val shizukuHint: String? = null,
val shizukuState: ShizukuAvailability.State = ShizukuAvailability.State.NOT_INSTALLED,
)
/**
@@ -65,6 +67,8 @@ class RunViewModel(app: Application) : AndroidViewModel(app) {
state = state.copy(
shizukuNotice = ShizukuAvailability.describe(st),
shizukuReady = st == ShizukuAvailability.State.READY,
shizukuHint = ShizukuAvailability.actionHint(st),
shizukuState = st,
)
}
}