diff --git a/app-glasses-vm/build.gradle.kts b/app-glasses-vm/build.gradle.kts
index 35a65b3..7b39740 100644
--- a/app-glasses-vm/build.gradle.kts
+++ b/app-glasses-vm/build.gradle.kts
@@ -64,6 +64,7 @@ dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.shizuku.api)
+ implementation(libs.shizuku.provider)
implementation(libs.mercury.library)
implementation(libs.mercury.transport)
implementation(libs.mercury.binocular)
diff --git a/app-glasses-vm/src/main/AndroidManifest.xml b/app-glasses-vm/src/main/AndroidManifest.xml
index a9888e5..9448b57 100644
--- a/app-glasses-vm/src/main/AndroidManifest.xml
+++ b/app-glasses-vm/src/main/AndroidManifest.xml
@@ -12,7 +12,10 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/GlassesVmApp.kt b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/GlassesVmApp.kt
index 1a6f7a1..cc3fe5a 100644
--- a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/GlassesVmApp.kt
+++ b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/GlassesVmApp.kt
@@ -10,6 +10,9 @@ import pw.binom.rayneovm.glasses.protocol.VmGlassesController
*
* Запускает [VmService] при создании (foreground-сервис держит WebSocket сервер + mDNS,
* и продолжает работать после сворачивания Activity).
+ *
+ * Также стартует [ShizukuBootstrap] — он регистрирует binder listener, ждёт/запрашивает
+ * permission и обновляет state. MainActivity потом подпишется на этот state.
*/
class GlassesVmApp : Application(), ControllerProvider {
@@ -18,11 +21,15 @@ class GlassesVmApp : Application(), ControllerProvider {
override fun onCreate() {
super.onCreate()
Log.i(TAG, "GlassesVmApp onCreate, uid=${android.os.Process.myUid()}")
- // Инициализация RayNeo Mercury SDK.
- com.ffalcon.mercury.android.sdk.MercurySDK.init(this)
- Log.i(TAG, "MercurySDK initialized")
+
+ // 1. RayNeo Mercury SDK init (BLE-канал, если нужно)
+ 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)
- // Регистрируем debug-receiver для тестирования через adb.
+
+ // 2. Регистрируем debug-receiver для тестирования через adb
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) {
registerReceiver(pw.binom.rayneovm.glasses.protocol.DebugTestReceiver(), filter, android.content.Context.RECEIVER_EXPORTED)
@@ -30,6 +37,11 @@ class GlassesVmApp : Application(), ControllerProvider {
@Suppress("UnspecifiedRegisterReceiverFlag")
registerReceiver(pw.binom.rayneovm.glasses.protocol.DebugTestReceiver(), filter)
}
+
+ // 3. Shizuku bootstrap здесь не зовём — он вызывается из MainActivity.onCreate,
+ // где есть Activity-контекст (для requestPermission flow).
+
+ // 4. Запуск foreground-сервиса (WebSocket + mDNS publisher)
VmService.start(this)
}
diff --git a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/MainActivity.kt b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/MainActivity.kt
index a039235..e708f2c 100644
--- a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/MainActivity.kt
+++ b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/MainActivity.kt
@@ -9,23 +9,30 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
+import androidx.compose.runtime.Composable
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.graphics.Color
import pw.binom.rayneovm.glasses.protocol.VmGlassesController
import pw.binom.rayneovm.glasses.ui.GlassesScreen
+import pw.binom.rayneovm.glasses.vm.GlassesScreenState
-/**
- * Single-Activity приложения: Compose host для GlassesScreen.
- *
- * Никакой навигации — здесь один экран (зеркало гостевого приложения + state overlay).
- */
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
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)
+
setContent {
CompositionLocalProvider(VmGlassesController.Local provides controller) {
MaterialTheme(colorScheme = darkColorScheme()) {
@@ -34,10 +41,61 @@ class MainActivity : ComponentActivity() {
.fillMaxSize()
.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,
+ 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)
+ }
+ }
+ }
+ }
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 77cb29c..5f7f6b2 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -8,6 +8,7 @@ lifecycle = "2.11.0"
activityCompose = "1.11.0"
composeBom = "2026.09.00"
shizukuApi = "13.1.5"
+shizukuProvider = "13.1.5"
navCompose = "2.10.2"
mercury = "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-api = { module = "dev.rikka.shizuku:api", version.ref = "shizukuApi" }
+shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizukuProvider" }
# rayneo-mercury (библиотеки)
mercury-library = { module = "pw.binom.mercury:rayneo-mercury", version.ref = "mercury" }