#5 AccessibilityProvisioner: авто-включение VmAccessibilityService на очках при коннекте (по модели ShizukuProvisioner)
This commit is contained in:
@@ -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
|
||||
|
||||
+138
@@ -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>(AccessibilityState.Idle)
|
||||
val state: StateFlow<AccessibilityState> = _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<String> = 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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user