From ad4e458f5c8dbab629e43306e73b53c7e1a1fd3c Mon Sep 17 00:00:00 2001 From: subochev Date: Sun, 27 Sep 2026 11:22:04 +0300 Subject: [PATCH] =?UTF-8?q?#5=20AccessibilityProvisioner:=20=D0=B0=D0=B2?= =?UTF-8?q?=D1=82=D0=BE-=D0=B2=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20VmAccessibilityService=20=D0=BD=D0=B0=20=D0=BE=D1=87?= =?UTF-8?q?=D0=BA=D0=B0=D1=85=20=D0=BF=D1=80=D0=B8=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D0=BD=D0=B5=D0=BA=D1=82=D0=B5=20(=D0=BF=D0=BE=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D0=B8=20ShizukuProvisioner)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pw/binom/rayneovm/phone/PhoneVmApp.kt | 36 ++++- .../bootstrap/AccessibilityProvisioner.kt | 138 ++++++++++++++++++ 2 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/bootstrap/AccessibilityProvisioner.kt diff --git a/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/PhoneVmApp.kt b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/PhoneVmApp.kt index 46a94fd..f82b335 100644 --- a/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/PhoneVmApp.kt +++ b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/PhoneVmApp.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import pw.binom.rayneovm.phone.adb.AdbController +import pw.binom.rayneovm.phone.bootstrap.AccessibilityProvisioner import pw.binom.rayneovm.phone.bootstrap.ShizukuProvisioner import pw.binom.rayneovm.phone.mercury.MercuryController import pw.binom.rayneovm.phone.mercury.MercuryControllerProvider @@ -24,7 +25,7 @@ import pw.binom.rayneovm.phone.protocol.VmPhoneController * и [MercuryController] (второй канал — RayNeo Mercury BLE). */ class PhoneVmApp : Application(), ControllerProvider, MercuryControllerProvider, - ShizukuProvisionerProvider, AdbControllerProvider { + ShizukuProvisionerProvider, AdbControllerProvider, AccessibilityProvisionerProvider { private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) @@ -34,6 +35,9 @@ class PhoneVmApp : Application(), ControllerProvider, MercuryControllerProvider, private val provisioner: ShizukuProvisioner by lazy { ShizukuProvisioner(this, mercuryController, controller, adbController) } + private val accessibilityProvisioner: AccessibilityProvisioner by lazy { + AccessibilityProvisioner(controller, adbController) + } override fun onCreate() { super.onCreate() @@ -77,7 +81,28 @@ class PhoneVmApp : Application(), ControllerProvider, MercuryControllerProvider, // Разрыв BLE — новая сессия: при следующем подключении проверяем Shizuku заново. appScope.launch { mercuryController.state.collect { st -> - if (st !is MercuryState.Connected) provisioner.onBleDisconnected() + if (st !is MercuryState.Connected) { + provisioner.onBleDisconnected() + accessibilityProvisioner.onBleDisconnected() + } + } + } + + // Accessibility — отдельный provisioner, гоняется по тому же расписанию, что и Shizuku. + appScope.launch { + controller.state.collect { st -> + if (st is ConnectionState.Connected && mercuryController.state.value is MercuryState.Connected) { + Log.i(TAG, "WiFi подключение к очкам — проверяю Accessibility") + accessibilityProvisioner.ensure() + } + } + } + appScope.launch { + while (true) { + delay(PROVISION_RETRY_MS) + if (mercuryController.state.value is MercuryState.Connected) { + accessibilityProvisioner.ensure() + } } } } @@ -86,6 +111,7 @@ class PhoneVmApp : Application(), ControllerProvider, MercuryControllerProvider, super.onTerminate() ConnectionKeepAliveService.stop(this) provisioner.close() + accessibilityProvisioner.close() controller.stop() mercuryController.close() } @@ -94,6 +120,7 @@ class PhoneVmApp : Application(), ControllerProvider, MercuryControllerProvider, override fun provideMercuryController(): MercuryController = mercuryController override fun provideShizukuProvisioner(): ShizukuProvisioner = provisioner override fun provideAdbController(): AdbController = adbController + override fun provideAccessibilityProvisioner(): AccessibilityProvisioner = accessibilityProvisioner companion object { private const val TAG = "PhoneVmApp" @@ -106,6 +133,11 @@ interface ShizukuProvisionerProvider { fun provideShizukuProvisioner(): ShizukuProvisioner } +/** Провайдер автовключения Accessibility-сервиса на очках для UI. */ +interface AccessibilityProvisionerProvider { + fun provideAccessibilityProvisioner(): AccessibilityProvisioner +} + /** Провайдер adb-контроллера (единая точка входа для adb-команд) для UI. */ interface AdbControllerProvider { fun provideAdbController(): AdbController diff --git a/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/bootstrap/AccessibilityProvisioner.kt b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/bootstrap/AccessibilityProvisioner.kt new file mode 100644 index 0000000..fc94389 --- /dev/null +++ b/app-phone-vm/src/main/kotlin/pw/binom/rayneovm/phone/bootstrap/AccessibilityProvisioner.kt @@ -0,0 +1,138 @@ +package pw.binom.rayneovm.phone.bootstrap + +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import pw.binom.rayneovm.phone.adb.AdbController +import pw.binom.rayneovm.phone.protocol.VmPhoneController + +/** + * Гарантирует, что VmAccessibilityService на очках включён. + * + * Используется ровно тот же путь, что и [ShizukuProvisioner] (через + * [AdbController] → беспроводной adb → `settings put secure`). Если у + * `AdbController` нет соединения, мы НЕ поднимаем adb силой — это работа + * бутстрапа Shizuku; accessibility всё равно будет включен при следующем + * успешном коннекте. + * + * Вызывается на каждый BLE-коннект (см. [VmPhoneController] и + * `ShizukuProvisioner.ensure()`). Так ловим случаи `am force-stop` / + * обновления APK, которые сбрасывают accessibility-флаг. + */ +class AccessibilityProvisioner( + private val controller: VmPhoneController, + private val adb: AdbController, +) { + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private val mutex = Mutex() + + private val _state = MutableStateFlow(AccessibilityState.Idle) + val state: StateFlow = _state.asStateFlow() + + /** + * Проверить, что VmAccessibilityService есть в `enabled_accessibility_services`. + * Если нет — добавить. Один раз за BLE-сессию. + */ + fun ensure() { + Log.i(TAG, "ensure(): состояние=${_state.value}") + if (mutex.isLocked) { + Log.i(TAG, "ensure(): уже идёт проверка") + return + } + scope.launch { + mutex.withLock { ensureLocked() } + } + } + + /** + * BLE-сессия разорвана — при следующем коннекте снова проверим. + * (См. проверенное: `am force-stop` снимает accessibility-флаг, + * поэтому перепроверяем каждый раз.) + */ + fun onBleDisconnected() { + _state.value = AccessibilityState.Idle + } + + private suspend fun ensureLocked() { + if (!adb.available) { + // Без adb мы ничего не сделаем — но состояние не сбрасываем, чтобы UI не моргал. + Log.i(TAG, "ensureLocked(): adb недоступен, пропуск") + return + } + // Шарим соединение с ShizukuProvisioner (если оно поднято). + val serial = adb.ensureConnected() ?: run { + Log.i(TAG, "ensureLocked(): нет adb-соединения") + return + } + Log.i(TAG, "ensureLocked(): adb=$serial, проверяю accessibility") + + val current = readEnabledServices().getOrDefault("") + Log.i(TAG, "ensureLocked(): enabled_accessibility_services='$current'") + if (current.contains(SERVICE_NAME)) { + _state.value = AccessibilityState.Enabled + return + } + + _state.value = AccessibilityState.Enabling + val append = if (current.isBlank()) SERVICE_FQDN else "$current:$SERVICE_FQDN" + val put = adb.shell( + "settings put secure enabled_accessibility_services '$append'", + timeoutMs = 15_000, + ) + val enable = adb.shell( + "settings put secure accessibility_enabled 1", + timeoutMs = 15_000, + ) + if (put.isSuccess && enable.isSuccess) { + Log.i(TAG, "ensureLocked(): accessibility включён") + _state.value = AccessibilityState.Enabled + } else { + val msg = "settings put failed: put=${put.isSuccess}, enable=${enable.isSuccess}" + Log.w(TAG, "ensureLocked(): $msg") + _state.value = AccessibilityState.Failed(msg) + } + } + + private suspend fun readEnabledServices(): Result = runCatching { + val out = adb.shell("settings get secure enabled_accessibility_services", timeoutMs = 15_000) + .getOrElse { error("shell failed: ${it.message}") } + // Если значение равно «null» — это дефолтное состояние, без включённых сервисов. + out.trim().takeIf { it.isNotEmpty() && it != "null" } ?: "" + } + + fun close() { + scope.coroutineContext[kotlinx.coroutines.Job]?.cancel() + } + + private companion object { + const val TAG = "AccessibilityProvisioner" + const val GLASSES_PACKAGE = "pw.binom.rayneovm.glasses" + const val SERVICE_NAME = "VmAccessibilityService" + // settings put ожидает формат ComponentName через package/class (НЕ .FQCN, а /FQCN). + const val SERVICE_FQDN = "$GLASSES_PACKAGE/$GLASSES_PACKAGE.$SERVICE_NAME" + } +} + +/** Состояние автовключения accessibility на очках (для UI). */ +sealed interface AccessibilityState { + data object Idle : AccessibilityState + data object Enabling : AccessibilityState + data object Enabled : AccessibilityState + data class Failed(val message: String) : AccessibilityState + + val label: String + get() = when (this) { + Idle -> "не проверялся" + Enabling -> "включаем…" + Enabled -> "включён" + is Failed -> "ошибка: $message" + } +}