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:
mrambossek
2026-08-01 09:51:01 +02:00
co-authored by Claude Opus 5
parent 217818f7b3
commit d7dda40e4e
5 changed files with 135 additions and 0 deletions
@@ -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) }
}
}
}