app: tell the user before the run when Shizuku is installed but not started
Distinguishes not-installed (say nothing — don't nag users who don't use Shizuku) from installed-but-stopped (amber banner: start it to include shell-tier tests), plus running-unauthorised and ready. Detection is listener-based since pingBinder() only becomes truthful once ShizukuProvider delivers the binder; a launch-time poll would show a false "not running". Installed-vs-not needs the <queries> entry on Android 11+. Verified on-device: with shizuku_server stopped, the banner shows before pressing Run; the title also now clears the status bar/cutout after the safeDrawingPadding fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
217818f7b3
commit
d7dda40e4e
@@ -115,6 +115,22 @@ private fun EcholotScreen(
|
||||
Text("Echolot", fontSize = 26.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text("measure, don't guess", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 13.sp)
|
||||
|
||||
// Shell-tier readiness, before the run. Nothing is shown when Shizuku isn't installed —
|
||||
// only users who actually use it get reminded that it must be running.
|
||||
state.shizukuNotice?.let { notice ->
|
||||
Card(
|
||||
Modifier.fillMaxWidth(),
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Button(onClick = onRun, enabled = !state.running) {
|
||||
Text(if (state.running) "Running…" else "Run measurement")
|
||||
|
||||
@@ -20,6 +20,7 @@ import app.echo_lot.probe.NetworkInventory
|
||||
import app.echo_lot.probe.Probe
|
||||
import app.echo_lot.probe.ProbeIds
|
||||
import app.echo_lot.probe.RouterIdentityProbe
|
||||
import app.echo_lot.shizuku.ShizukuAvailability
|
||||
import app.echo_lot.shizuku.ShizukuProbe
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -37,6 +38,9 @@ data class UiState(
|
||||
val stepsDone: Int = 0,
|
||||
val stepsTotal: Int = 0,
|
||||
val etaSeconds: Int = 0,
|
||||
/** Shell-tier readiness, shown before a run; null message = say nothing (Shizuku not installed). */
|
||||
val shizukuNotice: String? = null,
|
||||
val shizukuReady: Boolean = false,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -51,6 +55,24 @@ class RunViewModel(app: Application) : AndroidViewModel(app) {
|
||||
private set
|
||||
|
||||
private var runJob: kotlinx.coroutines.Job? = null
|
||||
private val stopShizukuObserver: () -> Unit
|
||||
|
||||
init {
|
||||
// Report shell-tier readiness up front. The binder arrives asynchronously, so this is a
|
||||
// listener, not a one-shot poll — otherwise a running Shizuku would look "not running"
|
||||
// for the first moment after launch.
|
||||
stopShizukuObserver = ShizukuAvailability.observe(app) { st ->
|
||||
state = state.copy(
|
||||
shizukuNotice = ShizukuAvailability.describe(st),
|
||||
shizukuReady = st == ShizukuAvailability.State.READY,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
stopShizukuObserver()
|
||||
super.onCleared()
|
||||
}
|
||||
// Results collected so far. A cancelled run must still be able to show what it measured.
|
||||
private val collected = mutableListOf<Test>()
|
||||
private var runIds: RunIds = RunIds()
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
|
||||
<uses-permission android:name="moe.shizuku.manager.permission.API_V23" />
|
||||
|
||||
<!-- Android 11+ package visibility: needed to tell "Shizuku installed but stopped"
|
||||
(worth a reminder) apart from "not installed" (say nothing). -->
|
||||
<queries>
|
||||
<package android:name="moe.shizuku.privileged.api" />
|
||||
</queries>
|
||||
|
||||
<application>
|
||||
<!-- Shizuku binder provider (merged into the host app). -->
|
||||
<provider
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// SPDX-FileCopyrightText: 2026 Echolot contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app.echo_lot.shizuku
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import rikka.shizuku.Shizuku
|
||||
|
||||
/**
|
||||
* Tells the UI whether the shell tier is usable **before** a run starts, so the app can say
|
||||
* "Shizuku isn't running, shell-tier tests will be skipped" instead of silently producing an
|
||||
* UNSUPPORTED result minutes later.
|
||||
*
|
||||
* Detection is reliable but *asynchronous*: `Shizuku.pingBinder()` answers truthfully only once
|
||||
* ShizukuProvider has delivered the binder to this process, which happens shortly after start.
|
||||
* [observe] therefore reports the current state immediately and again whenever the binder
|
||||
* arrives or dies — a poll at t=0 alone would show a false "not running".
|
||||
*/
|
||||
object ShizukuAvailability {
|
||||
|
||||
const val SHIZUKU_PACKAGE = "moe.shizuku.privileged.api"
|
||||
|
||||
enum class State {
|
||||
/** Running and this app may use it — shell tier will execute. */
|
||||
READY,
|
||||
/** Running, but the user hasn't granted this app permission yet (we can ask). */
|
||||
NEEDS_PERMISSION,
|
||||
/** Shizuku IS installed but its service isn't started — worth reminding this user. */
|
||||
INSTALLED_NOT_RUNNING,
|
||||
/** Shizuku isn't installed at all — say nothing; this user doesn't use the shell tier. */
|
||||
NOT_INSTALLED,
|
||||
}
|
||||
|
||||
/** Is the Shizuku manager installed? Needs the <queries> entry on Android 11+. */
|
||||
fun isInstalled(context: Context): Boolean = runCatching {
|
||||
context.packageManager.getPackageInfo(SHIZUKU_PACKAGE, 0); true
|
||||
}.getOrDefault(false)
|
||||
|
||||
fun current(context: Context): State = when {
|
||||
runCatching { Shizuku.pingBinder() }.getOrDefault(false) ->
|
||||
if (runCatching { Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED }
|
||||
.getOrDefault(false)
|
||||
) State.READY else State.NEEDS_PERMISSION
|
||||
isInstalled(context) -> State.INSTALLED_NOT_RUNNING
|
||||
else -> State.NOT_INSTALLED
|
||||
}
|
||||
|
||||
/**
|
||||
* UI one-liner, or null when nothing should be said. Users without Shizuku get no nag; users
|
||||
* who have it installed but stopped get the reminder that makes the difference between a
|
||||
* full run and a silently skipped shell tier.
|
||||
*/
|
||||
fun describe(s: State): String? = when (s) {
|
||||
State.READY -> "Shizuku ready — shell-tier tests will run"
|
||||
State.NEEDS_PERMISSION -> "Shizuku is running but not authorised — it will ask on first use"
|
||||
State.INSTALLED_NOT_RUNNING -> "Shizuku is installed but not running — start it to include shell-tier tests"
|
||||
State.NOT_INSTALLED -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports the state now and on every binder transition. Returns a function that removes the
|
||||
* listeners again (call it from onCleared).
|
||||
*/
|
||||
fun observe(context: Context, onChange: (State) -> Unit): () -> Unit {
|
||||
val app = context.applicationContext
|
||||
val received = Shizuku.OnBinderReceivedListener { onChange(current(app)) }
|
||||
val dead = Shizuku.OnBinderDeadListener { onChange(current(app)) }
|
||||
// "Sticky" fires immediately if the binder already arrived before we registered.
|
||||
runCatching { Shizuku.addBinderReceivedListenerSticky(received) }
|
||||
runCatching { Shizuku.addBinderDeadListener(dead) }
|
||||
onChange(current(app))
|
||||
return {
|
||||
runCatching { Shizuku.removeBinderReceivedListener(received) }
|
||||
runCatching { Shizuku.removeBinderDeadListener(dead) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user