#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.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import pw.binom.rayneovm.phone.adb.AdbController
|
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.bootstrap.ShizukuProvisioner
|
||||||
import pw.binom.rayneovm.phone.mercury.MercuryController
|
import pw.binom.rayneovm.phone.mercury.MercuryController
|
||||||
import pw.binom.rayneovm.phone.mercury.MercuryControllerProvider
|
import pw.binom.rayneovm.phone.mercury.MercuryControllerProvider
|
||||||
@@ -24,7 +25,7 @@ import pw.binom.rayneovm.phone.protocol.VmPhoneController
|
|||||||
* и [MercuryController] (второй канал — RayNeo Mercury BLE).
|
* и [MercuryController] (второй канал — RayNeo Mercury BLE).
|
||||||
*/
|
*/
|
||||||
class PhoneVmApp : Application(), ControllerProvider, MercuryControllerProvider,
|
class PhoneVmApp : Application(), ControllerProvider, MercuryControllerProvider,
|
||||||
ShizukuProvisionerProvider, AdbControllerProvider {
|
ShizukuProvisionerProvider, AdbControllerProvider, AccessibilityProvisionerProvider {
|
||||||
|
|
||||||
private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
@@ -34,6 +35,9 @@ class PhoneVmApp : Application(), ControllerProvider, MercuryControllerProvider,
|
|||||||
private val provisioner: ShizukuProvisioner by lazy {
|
private val provisioner: ShizukuProvisioner by lazy {
|
||||||
ShizukuProvisioner(this, mercuryController, controller, adbController)
|
ShizukuProvisioner(this, mercuryController, controller, adbController)
|
||||||
}
|
}
|
||||||
|
private val accessibilityProvisioner: AccessibilityProvisioner by lazy {
|
||||||
|
AccessibilityProvisioner(controller, adbController)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
@@ -77,7 +81,28 @@ class PhoneVmApp : Application(), ControllerProvider, MercuryControllerProvider,
|
|||||||
// Разрыв BLE — новая сессия: при следующем подключении проверяем Shizuku заново.
|
// Разрыв BLE — новая сессия: при следующем подключении проверяем Shizuku заново.
|
||||||
appScope.launch {
|
appScope.launch {
|
||||||
mercuryController.state.collect { st ->
|
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()
|
super.onTerminate()
|
||||||
ConnectionKeepAliveService.stop(this)
|
ConnectionKeepAliveService.stop(this)
|
||||||
provisioner.close()
|
provisioner.close()
|
||||||
|
accessibilityProvisioner.close()
|
||||||
controller.stop()
|
controller.stop()
|
||||||
mercuryController.close()
|
mercuryController.close()
|
||||||
}
|
}
|
||||||
@@ -94,6 +120,7 @@ class PhoneVmApp : Application(), ControllerProvider, MercuryControllerProvider,
|
|||||||
override fun provideMercuryController(): MercuryController = mercuryController
|
override fun provideMercuryController(): MercuryController = mercuryController
|
||||||
override fun provideShizukuProvisioner(): ShizukuProvisioner = provisioner
|
override fun provideShizukuProvisioner(): ShizukuProvisioner = provisioner
|
||||||
override fun provideAdbController(): AdbController = adbController
|
override fun provideAdbController(): AdbController = adbController
|
||||||
|
override fun provideAccessibilityProvisioner(): AccessibilityProvisioner = accessibilityProvisioner
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "PhoneVmApp"
|
private const val TAG = "PhoneVmApp"
|
||||||
@@ -106,6 +133,11 @@ interface ShizukuProvisionerProvider {
|
|||||||
fun provideShizukuProvisioner(): ShizukuProvisioner
|
fun provideShizukuProvisioner(): ShizukuProvisioner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Провайдер автовключения Accessibility-сервиса на очках для UI. */
|
||||||
|
interface AccessibilityProvisionerProvider {
|
||||||
|
fun provideAccessibilityProvisioner(): AccessibilityProvisioner
|
||||||
|
}
|
||||||
|
|
||||||
/** Провайдер adb-контроллера (единая точка входа для adb-команд) для UI. */
|
/** Провайдер adb-контроллера (единая точка входа для adb-команд) для UI. */
|
||||||
interface AdbControllerProvider {
|
interface AdbControllerProvider {
|
||||||
fun provideAdbController(): AdbController
|
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