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
@@ -507,3 +507,16 @@ an elvis-operator bug that printed "no UPnP response" even when UPnP data was pr
|
|||||||
- **Insets/cutout**: Android 15 draws edge-to-edge by default, so the title was running under the
|
- **Insets/cutout**: Android 15 draws edge-to-edge by default, so the title was running under the
|
||||||
status-bar clock and the camera cutout. The root column now uses `safeDrawingPadding()`, which
|
status-bar clock and the camera cutout. The root column now uses `safeDrawingPadding()`, which
|
||||||
covers status bar, navigation bar and display cutout.
|
covers status bar, navigation bar and display cutout.
|
||||||
|
|
||||||
|
### Shell-tier readiness shown before a run (2026-08-01)
|
||||||
|
`ShizukuAvailability` distinguishes four states and the UI only speaks when it is actionable:
|
||||||
|
- **NOT_INSTALLED → says nothing.** Users who don't use Shizuku are never nagged.
|
||||||
|
- **INSTALLED_NOT_RUNNING → amber banner** "Shizuku is installed but not running — start it to
|
||||||
|
include shell-tier tests". This is the case worth reminding about: the user has it, but a
|
||||||
|
stopped service silently costs them the whole shell tier.
|
||||||
|
- NEEDS_PERMISSION → "running but not authorised, it will ask on first use".
|
||||||
|
- READY → green "shell-tier tests will run".
|
||||||
|
Detection is listener-based (`addBinderReceivedListenerSticky` + binder-dead), because
|
||||||
|
`pingBinder()` is only truthful once ShizukuProvider has delivered the binder — a one-shot poll at
|
||||||
|
launch would show a false "not running". Installed-vs-not needs the `<queries>` package-visibility
|
||||||
|
entry on Android 11+. Verified on-device: with shizuku_server stopped the banner appears correctly.
|
||||||
|
|||||||
@@ -115,6 +115,22 @@ private fun EcholotScreen(
|
|||||||
Text("Echolot", fontSize = 26.sp, fontWeight = FontWeight.SemiBold)
|
Text("Echolot", fontSize = 26.sp, fontWeight = FontWeight.SemiBold)
|
||||||
Text("measure, don't guess", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 13.sp)
|
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) {
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
Button(onClick = onRun, enabled = !state.running) {
|
Button(onClick = onRun, enabled = !state.running) {
|
||||||
Text(if (state.running) "Running…" else "Run measurement")
|
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.Probe
|
||||||
import app.echo_lot.probe.ProbeIds
|
import app.echo_lot.probe.ProbeIds
|
||||||
import app.echo_lot.probe.RouterIdentityProbe
|
import app.echo_lot.probe.RouterIdentityProbe
|
||||||
|
import app.echo_lot.shizuku.ShizukuAvailability
|
||||||
import app.echo_lot.shizuku.ShizukuProbe
|
import app.echo_lot.shizuku.ShizukuProbe
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -37,6 +38,9 @@ data class UiState(
|
|||||||
val stepsDone: Int = 0,
|
val stepsDone: Int = 0,
|
||||||
val stepsTotal: Int = 0,
|
val stepsTotal: Int = 0,
|
||||||
val etaSeconds: 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 set
|
||||||
|
|
||||||
private var runJob: kotlinx.coroutines.Job? = null
|
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.
|
// Results collected so far. A cancelled run must still be able to show what it measured.
|
||||||
private val collected = mutableListOf<Test>()
|
private val collected = mutableListOf<Test>()
|
||||||
private var runIds: RunIds = RunIds()
|
private var runIds: RunIds = RunIds()
|
||||||
|
|||||||
@@ -5,6 +5,12 @@
|
|||||||
|
|
||||||
<uses-permission android:name="moe.shizuku.manager.permission.API_V23" />
|
<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>
|
<application>
|
||||||
<!-- Shizuku binder provider (merged into the host app). -->
|
<!-- Shizuku binder provider (merged into the host app). -->
|
||||||
<provider
|
<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