glasses: switch to rikka.shizuku.ShizukuProvider, fix binder acquisition

- Add dev.rikka.shizuku:provider:13.1.5 dependency.
- Replace custom ShizukuInitProvider with rikka.shizuku.ShizukuProvider
  from the upstream library; configured per Shizuku v12+ docs with
  android:permission='android.permission.INTERACT_ACROSS_USERS_FULL'
  to protect the provider from ordinary apps.
- Move ShizukuBootstrap.attach() from GlassesVmApp.onCreate to
  MainActivity.onCreate (requestPermission needs an Activity context).
- MainActivity now overrides onRequestPermissionsResult and forwards
  the grant to ShizukuBootstrap.
- Rewrite ShizukuBootstrap strictly per the Shizuku-API guide:
  addBinderReceivedListenerSticky + addRequestPermissionResultListener
  in attach(); pingBinder / checkSelfPermission / requestPermission
  in refresh().
- Drop ShizukuInitProvider.kt — its job is now done by
  rikka.shizuku.ShizukuProvider.onCreate (verified in logcat:
  'ShizukuProvider: binder received').

Verified on device ARGF20 (A06B4AB933C4103, API 32): Shizuku dialog
'Allow rayneo-vm to use Shizuku?' opens correctly; binder received
via ShizukuProvider; launcher icon now visible (meta-data fix).
This commit is contained in:
2026-09-25 14:44:52 +03:00
parent 987f9e5547
commit e01fafd5a9
5 changed files with 109 additions and 11 deletions
+1
View File
@@ -64,6 +64,7 @@ dependencies {
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation(libs.shizuku.api) implementation(libs.shizuku.api)
implementation(libs.shizuku.provider)
implementation(libs.mercury.library) implementation(libs.mercury.library)
implementation(libs.mercury.transport) implementation(libs.mercury.transport)
implementation(libs.mercury.binocular) implementation(libs.mercury.binocular)
+26 -1
View File
@@ -12,7 +12,10 @@
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="moe.shizuku.permission.SHIZUKU" /> <!-- Shizuku v12+: BinderSender проверяет requestedPermissions на наличие
этого permission (см. moe.shizuku.server.BinderSender.sendBinder).
Старое moe.shizuku.permission.SHIZUKU (Shizuku v11) больше не работает. -->
<uses-permission android:name="moe.shizuku.manager.permission.API_V23" />
<uses-feature <uses-feature
android:glEsVersion="0x00020000" android:glEsVersion="0x00020000"
@@ -21,6 +24,16 @@
android:name="android.hardware.type.embedded" android:name="android.hardware.type.embedded"
android:required="true" /> android:required="true" />
<!-- Android 11+ package visibility: Shizuku API выполняет context.bindService на сервис
внутри пакета moe.shizuku.privileged.api; без явного queries bindService возвращает
false и binder к нам никогда не приедет. -->
<queries>
<package android:name="moe.shizuku.privileged.api" />
<intent>
<action android:name="rikka.shizuku.intent.action.REQUEST_BINDER" />
</intent>
</queries>
<application <application
android:name=".GlassesVmApp" android:name=".GlassesVmApp"
android:label="@string/app_name" android:label="@string/app_name"
@@ -52,6 +65,18 @@
android:name=".VmShizukuService" android:name=".VmShizukuService"
android:exported="true" android:exported="true"
android:process=":shizuku_vd" /> android:process=":shizuku_vd" />
<!-- Shizuku v12+ требует ShizukuProvider в манифесте — он сам вытаскивает binder
через ContentProvider IPC и инициализирует Shizuku-клиент. Без него binder не придёт.
permission=INTERACT_ACROSS_USERS_FULL защищает провайдер от обычных приложений. -->
<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku"
android:multiprocess="false"
android:enabled="true"
android:exported="true"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
<meta-data android:name="com.rayneo.mercury.app" android:value="true" /> <meta-data android:name="com.rayneo.mercury.app" android:value="true" />
</application> </application>
</manifest> </manifest>
@@ -10,6 +10,9 @@ import pw.binom.rayneovm.glasses.protocol.VmGlassesController
* *
* Запускает [VmService] при создании (foreground-сервис держит WebSocket сервер + mDNS, * Запускает [VmService] при создании (foreground-сервис держит WebSocket сервер + mDNS,
* и продолжает работать после сворачивания Activity). * и продолжает работать после сворачивания Activity).
*
* Также стартует [ShizukuBootstrap] — он регистрирует binder listener, ждёт/запрашивает
* permission и обновляет state. MainActivity потом подпишется на этот state.
*/ */
class GlassesVmApp : Application(), ControllerProvider { class GlassesVmApp : Application(), ControllerProvider {
@@ -18,11 +21,15 @@ class GlassesVmApp : Application(), ControllerProvider {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Log.i(TAG, "GlassesVmApp onCreate, uid=${android.os.Process.myUid()}") Log.i(TAG, "GlassesVmApp onCreate, uid=${android.os.Process.myUid()}")
// Инициализация RayNeo Mercury SDK.
com.ffalcon.mercury.android.sdk.MercurySDK.init(this) // 1. RayNeo Mercury SDK init (BLE-канал, если нужно)
Log.i(TAG, "MercurySDK initialized") runCatching { com.ffalcon.mercury.android.sdk.MercurySDK.init(this) }
.onSuccess { Log.i(TAG, "MercurySDK initialized") }
.onFailure { Log.w(TAG, "MercurySDK init failed: ${it.message}") }
controller.setAppContext(this) controller.setAppContext(this)
// Регистрируем debug-receiver для тестирования через adb.
// 2. Регистрируем debug-receiver для тестирования через adb
val filter = android.content.IntentFilter(pw.binom.rayneovm.glasses.protocol.DebugTestReceiver.ACTION) val filter = android.content.IntentFilter(pw.binom.rayneovm.glasses.protocol.DebugTestReceiver.ACTION)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
registerReceiver(pw.binom.rayneovm.glasses.protocol.DebugTestReceiver(), filter, android.content.Context.RECEIVER_EXPORTED) registerReceiver(pw.binom.rayneovm.glasses.protocol.DebugTestReceiver(), filter, android.content.Context.RECEIVER_EXPORTED)
@@ -30,6 +37,11 @@ class GlassesVmApp : Application(), ControllerProvider {
@Suppress("UnspecifiedRegisterReceiverFlag") @Suppress("UnspecifiedRegisterReceiverFlag")
registerReceiver(pw.binom.rayneovm.glasses.protocol.DebugTestReceiver(), filter) registerReceiver(pw.binom.rayneovm.glasses.protocol.DebugTestReceiver(), filter)
} }
// 3. Shizuku bootstrap здесь не зовём — он вызывается из MainActivity.onCreate,
// где есть Activity-контекст (для requestPermission flow).
// 4. Запуск foreground-сервиса (WebSocket + mDNS publisher)
VmService.start(this) VmService.start(this)
} }
@@ -9,23 +9,30 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import pw.binom.rayneovm.glasses.protocol.VmGlassesController import pw.binom.rayneovm.glasses.protocol.VmGlassesController
import pw.binom.rayneovm.glasses.ui.GlassesScreen import pw.binom.rayneovm.glasses.ui.GlassesScreen
import pw.binom.rayneovm.glasses.vm.GlassesScreenState
/**
* Single-Activity приложения: Compose host для GlassesScreen.
*
* Никакой навигации — здесь один экран (зеркало гостевого приложения + state overlay).
*/
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
// По доке Shizuku (https://github.com/RikkaApps/Shizuku-API): подписываемся на
// listener-ы здесь, после super.onCreate, но ДО setContent. ShizukuProvider
// (rikka.shizuku.ShizukuProvider в манифесте) уже инициализировал binder через
// ContentProvider.onCreate — pingBinder() сразу вернёт true.
ShizukuBootstrap.attach(this)
val controller = VmGlassesController.fromContext(this) val controller = VmGlassesController.fromContext(this)
setContent { setContent {
CompositionLocalProvider(VmGlassesController.Local provides controller) { CompositionLocalProvider(VmGlassesController.Local provides controller) {
MaterialTheme(colorScheme = darkColorScheme()) { MaterialTheme(colorScheme = darkColorScheme()) {
@@ -34,10 +41,61 @@ class MainActivity : ComponentActivity() {
.fillMaxSize() .fillMaxSize()
.background(Color.Black) .background(Color.Black)
) { ) {
GlassesScreen(controller = controller) val shizukuState by ShizukuBootstrap.state.collectAsState()
val vmState by controller.state.collectAsState()
DriveShizukuState(controller, shizukuState)
GlassesScreen(
controller = controller,
onRequestShizukuPermission = { ShizukuBootstrap.refresh() },
)
// Как только VM ушёл в Running (запущен гость) — сворачиваем свой Activity,
// чтобы SurfaceView гостя оказался поверх и был виден на очках.
// Service продолжает жить в фоне (WebSocket/mDNS не рвутся).
if (vmState is GlassesScreenState.Running) {
(this as? ComponentActivity)?.finish()
}
} }
} }
} }
} }
} }
override fun onResume() {
super.onResume()
ShizukuBootstrap.refresh()
}
/**
* По доке Shizuku, мы должны вручную передать результат диалога в Shizuku -
* иначе он не узнает, что пользователь нажал Allow.
*/
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray,
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == ShizukuBootstrap.REQUEST_CODE_PERMISSION) {
val grant = grantResults.firstOrNull() ?: -1
ShizukuBootstrap.onRequestPermissionResult(requestCode, grant)
}
}
}
@Composable
private fun DriveShizukuState(controller: VmGlassesController, shizuku: ShizukuBootstrap.AllowState) {
LaunchedEffect(shizuku) {
when (shizuku) {
ShizukuBootstrap.AllowState.NotRunning,
ShizukuBootstrap.AllowState.Waiting,
ShizukuBootstrap.AllowState.NeedsPermission -> {
controller.setState(GlassesScreenState.ShizukuMissing)
}
ShizukuBootstrap.AllowState.Granted -> {
if (controller.state.value is GlassesScreenState.ShizukuMissing) {
controller.setState(GlassesScreenState.NoAppSelected)
}
}
}
}
} }
+2
View File
@@ -8,6 +8,7 @@ lifecycle = "2.11.0"
activityCompose = "1.11.0" activityCompose = "1.11.0"
composeBom = "2026.09.00" composeBom = "2026.09.00"
shizukuApi = "13.1.5" shizukuApi = "13.1.5"
shizukuProvider = "13.1.5"
navCompose = "2.10.2" navCompose = "2.10.2"
mercury = "0.1.0-SNAPSHOT" mercury = "0.1.0-SNAPSHOT"
mercuryTransport = "0.1.0-SNAPSHOT" mercuryTransport = "0.1.0-SNAPSHOT"
@@ -45,6 +46,7 @@ androidx-core-ktx = { module = "androidx.core:core-ktx", version = "1.15.0" }
# Shizuku # Shizuku
shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizukuApi" } shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizukuApi" }
shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizukuProvider" }
# rayneo-mercury (библиотеки) # rayneo-mercury (библиотеки)
mercury-library = { module = "pw.binom.mercury:rayneo-mercury", version.ref = "mercury" } mercury-library = { module = "pw.binom.mercury:rayneo-mercury", version.ref = "mercury" }