diff --git a/echolot-app/app/src/main/kotlin/app/echo_lot/app/MainActivity.kt b/echolot-app/app/src/main/kotlin/app/echo_lot/app/MainActivity.kt index 603be7e..972f692 100644 --- a/echolot-app/app/src/main/kotlin/app/echo_lot/app/MainActivity.kt +++ b/echolot-app/app/src/main/kotlin/app/echo_lot/app/MainActivity.kt @@ -149,6 +149,20 @@ class MainActivity : ComponentActivity() { screen = Screen.SETTINGS } } + // Shizuku can be started, stopped or authorised in its own app, where nothing + // calls back into this process. Asking again each time this screen comes + // forward is what makes the banner right after the user has been away to fix + // it — which is exactly the moment they look at it. + val lifecycleOwner = androidx.compose.ui.platform.LocalLifecycleOwner.current + androidx.compose.runtime.DisposableEffect(lifecycleOwner) { + val obs = androidx.lifecycle.LifecycleEventObserver { _, event -> + if (event == androidx.lifecycle.Lifecycle.Event.ON_RESUME) { + vm.refreshShizuku() + } + } + lifecycleOwner.lifecycle.addObserver(obs) + onDispose { lifecycleOwner.lifecycle.removeObserver(obs) } + } androidx.compose.runtime.LaunchedEffect(autorun) { if (autorun) vm.run(devUpload = true) } diff --git a/echolot-app/app/src/main/kotlin/app/echo_lot/app/RunViewModel.kt b/echolot-app/app/src/main/kotlin/app/echo_lot/app/RunViewModel.kt index 2160fcc..05685ce 100644 --- a/echolot-app/app/src/main/kotlin/app/echo_lot/app/RunViewModel.kt +++ b/echolot-app/app/src/main/kotlin/app/echo_lot/app/RunViewModel.kt @@ -103,6 +103,23 @@ class RunViewModel(app: Application) : AndroidViewModel(app) { } } + /** + * Re-reads the shell tier's state, for when it changed somewhere this process cannot see. + * + * Permission can be granted inside Shizuku's own app, and Shizuku can be started or stopped + * there too; none of that calls back here. Asking again on resume is the only way to be right + * after the user has been somewhere else to fix it. + */ + fun refreshShizuku() { + val st = ShizukuAvailability.current(getApplication()) + state = state.copy( + shizukuNotice = ShizukuAvailability.describe(st), + shizukuReady = st == ShizukuAvailability.State.READY, + shizukuHint = ShizukuAvailability.actionHint(st), + shizukuState = st, + ) + } + override fun onCleared() { stopShizukuObserver() super.onCleared() diff --git a/echolot-app/core-shizuku/src/main/kotlin/app/echo_lot/shizuku/ShizukuAvailability.kt b/echolot-app/core-shizuku/src/main/kotlin/app/echo_lot/shizuku/ShizukuAvailability.kt index d634041..42f2c4c 100644 --- a/echolot-app/core-shizuku/src/main/kotlin/app/echo_lot/shizuku/ShizukuAvailability.kt +++ b/echolot-app/core-shizuku/src/main/kotlin/app/echo_lot/shizuku/ShizukuAvailability.kt @@ -98,20 +98,32 @@ object ShizukuAvailability { .addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK) /** - * Reports the state now and on every binder transition. Returns a function that removes the - * listeners again (call it from onCleared). + * Reports the state now, on every binder transition, and when a permission request is + * answered. Returns a function that removes the listeners again (call it from onCleared). + * + * The permission listener matters as much as the binder ones: granting permission does not + * make the binder arrive or die, so without it the banner still read "running but not + * authorised" after the user had just authorised it — the one moment they are looking for + * confirmation that it worked. + * + * It is still not sufficient on its own. Permission can be granted inside Shizuku's own app, + * where nothing calls back into this process at all, so callers should re-check on resume as + * well; see [current]. */ 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)) } + val permission = Shizuku.OnRequestPermissionResultListener { _, _ -> onChange(current(app)) } // "Sticky" fires immediately if the binder already arrived before we registered. runCatching { Shizuku.addBinderReceivedListenerSticky(received) } runCatching { Shizuku.addBinderDeadListener(dead) } + runCatching { Shizuku.addRequestPermissionResultListener(permission) } onChange(current(app)) return { runCatching { Shizuku.removeBinderReceivedListener(received) } runCatching { Shizuku.removeBinderDeadListener(dead) } + runCatching { Shizuku.removeRequestPermissionResultListener(permission) } } } }