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" }