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:
co-authored by
Claude Opus 5
parent
d7dda40e4e
commit
1d9e2063bf
@@ -520,3 +520,14 @@ Detection is listener-based (`addBinderReceivedListenerSticky` + binder-dead), b
|
|||||||
`pingBinder()` is only truthful once ShizukuProvider has delivered the binder — a one-shot poll at
|
`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
|
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.
|
entry on Android 11+. Verified on-device: with shizuku_server stopped the banner appears correctly.
|
||||||
|
|
||||||
|
### Shizuku banner is actionable; progress + cancel verified on-device (2026-08-01)
|
||||||
|
Tapping the shell-tier banner now does the right thing per state: **installed-but-stopped** →
|
||||||
|
deep-links into the Shizuku app (a third-party app *cannot* start Shizuku itself; the wireless-
|
||||||
|
debugging pairing flow is privileged and lives in that app, so taking the user there in one tap is
|
||||||
|
the best available), **running-but-unauthorised** → fires the Shizuku permission request directly.
|
||||||
|
The hint line states which action the tap performs.
|
||||||
|
Verified on-device in one screenshot: progress bar at "test 4 of 8 · icmp.ping6 · ~33s left",
|
||||||
|
Cancel button beside the disabled Run button, title clear of the status bar/cutout, and the banner
|
||||||
|
having live-switched from "installed but not running" to "running but not authorised" via the
|
||||||
|
binder listener when Shizuku was started mid-session.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.activity.ComponentActivity
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
@@ -64,6 +65,19 @@ class MainActivity : ComponentActivity() {
|
|||||||
state = vm.state,
|
state = vm.state,
|
||||||
onRun = { vm.run() },
|
onRun = { vm.run() },
|
||||||
onCancel = vm::cancel,
|
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")) },
|
onExport = { doc -> startActivity(Intent.createChooser(Report.share(this, doc), "Export Echolot run")) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -99,6 +113,7 @@ private fun EcholotScreen(
|
|||||||
state: UiState,
|
state: UiState,
|
||||||
onRun: () -> Unit,
|
onRun: () -> Unit,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
|
onShizukuAction: () -> Unit,
|
||||||
onExport: (MeasurementDocument) -> Unit,
|
onExport: (MeasurementDocument) -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@@ -119,15 +134,23 @@ private fun EcholotScreen(
|
|||||||
// only users who actually use it get reminded that it must be running.
|
// only users who actually use it get reminded that it must be running.
|
||||||
state.shizukuNotice?.let { notice ->
|
state.shizukuNotice?.let { notice ->
|
||||||
Card(
|
Card(
|
||||||
Modifier.fillMaxWidth(),
|
Modifier.fillMaxWidth().let { m ->
|
||||||
|
if (state.shizukuHint != null) m.clickable { onShizukuAction() } else m
|
||||||
|
},
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = if (state.shizukuReady) Color(0xFF14301F) else Color(0xFF3A2E12),
|
containerColor = if (state.shizukuReady) Color(0xFF14301F) else Color(0xFF3A2E12),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Text(
|
Column(Modifier.padding(10.dp)) {
|
||||||
notice, Modifier.padding(10.dp), fontSize = 12.sp,
|
Text(
|
||||||
color = if (state.shizukuReady) Color(0xFF9CCFA8) else Color(0xFFFFD08A),
|
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). */
|
/** Shell-tier readiness, shown before a run; null message = say nothing (Shizuku not installed). */
|
||||||
val shizukuNotice: String? = null,
|
val shizukuNotice: String? = null,
|
||||||
val shizukuReady: Boolean = false,
|
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(
|
state = state.copy(
|
||||||
shizukuNotice = ShizukuAvailability.describe(st),
|
shizukuNotice = ShizukuAvailability.describe(st),
|
||||||
shizukuReady = st == ShizukuAvailability.State.READY,
|
shizukuReady = st == ShizukuAvailability.State.READY,
|
||||||
|
shizukuHint = ShizukuAvailability.actionHint(st),
|
||||||
|
shizukuState = st,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,28 @@ object ShizukuAvailability {
|
|||||||
State.NOT_INSTALLED -> null
|
State.NOT_INSTALLED -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intent that opens the Shizuku manager, where the user starts the service ("Start with
|
||||||
|
* wireless debugging" / root). A third-party app cannot start Shizuku itself — the pairing
|
||||||
|
* flow lives inside that app — so the best we can do is take the user straight there.
|
||||||
|
* Null when Shizuku isn't installed or exposes no launcher activity.
|
||||||
|
*/
|
||||||
|
fun launchIntent(context: Context): android.content.Intent? =
|
||||||
|
context.packageManager.getLaunchIntentForPackage(SHIZUKU_PACKAGE)
|
||||||
|
?.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
|
||||||
|
/** Asks Shizuku for permission (only meaningful while it is running). */
|
||||||
|
fun requestPermission(requestCode: Int = 0xE1) {
|
||||||
|
runCatching { Shizuku.requestPermission(requestCode) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What tapping the notice should do, so the UI can label it honestly. */
|
||||||
|
fun actionHint(s: State): String? = when (s) {
|
||||||
|
State.INSTALLED_NOT_RUNNING -> "Tap to open Shizuku and start it"
|
||||||
|
State.NEEDS_PERMISSION -> "Tap to grant permission"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reports the state now and on every binder transition. Returns a function that removes the
|
* Reports the state now and on every binder transition. Returns a function that removes the
|
||||||
* listeners again (call it from onCleared).
|
* listeners again (call it from onCleared).
|
||||||
|
|||||||
Reference in New Issue
Block a user