diff --git a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/VmService.kt b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/VmService.kt index b2c3929..89dd8b1 100644 --- a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/VmService.kt +++ b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/VmService.kt @@ -60,6 +60,10 @@ class VmService : LifecycleService() { instance = this controller = VmGlassesController.fromContext(this) Log.i(TAG, "VmService onCreate") + // Стража поднимаем сразу при старте сервиса, а не в момент запуска гостя: убийца + // (BackgroundAppManager) успевает убить за ~2 c после смены top_app, а запуск гостя + // сам эту смену и вызывает. Подробности — в VmGlassesController.ensureMercuryKillGuard. + controller.ensureMercuryKillGuard() startForeground() startServer() registerMdns() diff --git a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/protocol/VmGlassesController.kt b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/protocol/VmGlassesController.kt index 314b018..163bf4a 100644 --- a/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/protocol/VmGlassesController.kt +++ b/app-glasses-vm/src/main/kotlin/pw/binom/rayneovm/glasses/protocol/VmGlassesController.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -23,10 +24,12 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import pw.binom.mercury.binocular.FrameSource import pw.binom.mercury.shizuku.ShizukuVdBinder +import pw.binom.mercury.shizuku.ShizukuVdClient import pw.binom.rayneovm.glasses.VmShizukuService import pw.binom.rayneovm.glasses.shizuku.VirtualDisplayFrameSource import pw.binom.rayneovm.glasses.vm.GlassesScreenState import pw.binom.rayneovm.shared.VmDisplay +import kotlin.math.abs /** * Контроллер состояния экрана очков и кадров. @@ -47,14 +50,26 @@ class VmGlassesController { private var activeSource: VirtualDisplayFrameSource? = null - /** Служебный дисплей Mercury-«стража» (см. [startMercuryKillGuard]); 0 — не создан. */ + /** Служебный дисплей Mercury-«стража» (см. [ensureMercuryKillGuard]); 0 — не создан. */ private var guardDisplayId = 0 /** Surface служебного дисплея: без валидной Surface дисплей считается OFF и активность не стартует. */ private var guardSurfaceTexture: SurfaceTexture? = null - /** Watchdog: переподнимает активность стража, если она завершилась сама. */ + /** Отдельный долгоживущий Shizuku-клиент стража — не привязан к жизни гостевого дисплея. */ + private var guardClient: ShizukuVdClient? = null + + /** Binder того же клиента (после успешного bind). */ + private var guardBinder: IBinder? = null + + /** Watchdog: поднимает стража и переподнимает его активность, пока живёт приложение. */ private var guardWatchdog: Job? = null + + /** Дельты скролла от телефона: копим и инжектим пачкой (см. [scrollCursor]). */ + private val scrollChannel = Channel(Channel.UNLIMITED) + + /** Насос скролла: один на приложение. */ + private var scrollPump: Job? = null @Volatile private var appContext: Context? = null /** Сериализует создание/освобождение VirtualDisplay — параллельный acquire оставил бы висеть второй UserService. */ @@ -101,7 +116,6 @@ class VmGlassesController { return } try { - runCatching { stopMercuryKillGuard(src.shizukuBinder) } src.release() } catch (t: Throwable) { Log.w(TAG, "releaseFrameSource failed", t) @@ -152,22 +166,49 @@ class VmGlassesController { * (≈ [SCROLL_FACTOR]px при density 160), поэтому делим на него: смещение становится * 1:1 с движением пальца. */ + /** + * Скролл тачпада: дельты пальца копим и раз в [SCROLL_TICK_MS] отправляем одним + * синтетическим драгом. Инжектить на каждое движение пальца нельзя — жесты перекрываются. + */ fun scrollCursor(dx: Float, dy: Float) { if (dx == 0f && dy == 0f) return - val src = activeSource ?: return - val pos = _cursor.value ?: return - val binder = src.shizukuBinder - val displayId = src.virtualDisplayId - testScope.launch { - val ok = ShizukuVdBinder.scroll( - binder = binder, - displayId = displayId, - x = pos.x, - y = pos.y, - hScroll = dx / SCROLL_FACTOR, - vScroll = dy / SCROLL_FACTOR, - ) - if (!ok) Log.w(TAG, "scrollCursor: scroll failed (displayId=$displayId)") + scrollChannel.trySend(floatArrayOf(dx, dy)) + ensureScrollPump() + } + + /** Один насос на приложение: копит дельты и инжектит пачкой. */ + private fun ensureScrollPump() { + if (scrollPump != null) return + scrollPump = testScope.launch { + var accX = 0f + var accY = 0f + while (isActive) { + delay(SCROLL_TICK_MS) + while (true) { + val r = scrollChannel.tryReceive().getOrNull() ?: break + accX += r[0] + accY += r[1] + } + val src = activeSource + val pos = _cursor.value + if (src == null || pos == null) { + accX = 0f + accY = 0f + continue + } + if (abs(accX) < SCROLL_MIN_PX && abs(accY) < SCROLL_MIN_PX) continue + val ok = ShizukuVdBinder.scroll( + binder = src.shizukuBinder, + displayId = src.virtualDisplayId, + x = pos.x, + y = pos.y, + dx = accX, + dy = accY, + ) + if (!ok) Log.w(TAG, "scrollCursor: scroll failed (displayId=${src.virtualDisplayId})") + accX = 0f + accY = 0f + } } } @@ -221,8 +262,9 @@ class VmGlassesController { val launchIntent = pm.getLaunchIntentForPackage(packageName) val className = launchIntent?.component?.className ?: "" - // Пока гость работает — держим выключенным Mercury-«убийцу» (BackgroundAppManager). - startMercuryKillGuard(src.shizukuBinder) + // Стража обычно поднимает ensureMercuryKillGuard() при старте сервиса; здесь — + // подстраховка, если он ещё не поднялся (идемпотентно). + ensureMercuryKillGuard() val ok = ShizukuVdBinder.launchActivity( binder = src.shizukuBinder, @@ -246,7 +288,7 @@ class VmGlassesController { } /** - * Выключает Mercury-«убийцу» фоновых приложений (`BackgroundAppManager`) на время работы гостя. + * Поднимает Mercury-«страж» и держит его, пока живёт приложение. * * У RayNeo политика «одно приложение на переднем плане»: на каждой смене `Secure.top_app` * лаунчер force-стопит все пакеты из накопительной очереди предыдущих топов. Гость на @@ -255,21 +297,53 @@ class VmGlassesController { * * Легальный выключатель — вендорский `BackgroundAppManager.enableStopBackgroundKill(true)`. * Его ставит `NetConfigActivity.onStart` и снимает в `onStop`, поэтому держим эту активность - * запущенной на отдельном невидимом VirtualDisplay: там её никто не перекрывает, флаг - * остаётся true, и `forceStopApp` отменяется на проверке `tmpStopBackgroundKill`. + * запущенной на отдельном невидимом VirtualDisplay: её никто не перекрывает, флаг остаётся + * true, и `forceStopApp` отменяется на проверке `tmpStopBackgroundKill`. * - * Почему не через `top_app` и не через провайдер: `Settings.Secure.top_app` лаунчер сам - * переписывает по реальному топу, а `ContentProvider.getCallingPackage()` на API 31+ - * сверяет пакет с реальным uid вызывающего — shell (uid 2000) не может представиться - * `com.leiniao.camera`. + * Поднимать надо заранее, до первого запуска гостя: у убийцы есть окно ~2 c между сменой + * top_app и проверкой флага, а активность на скрытом дисплее стартует не мгновенно. Пока + * страж не поднят, любая смена топа (в том числе сам запуск гостя) успевает убить нас. + * + * Почему не через `top_app` и не через провайдер: `Secure.top_app` лаунчер сам переписывает + * по реальному топу, а `ContentProvider.getCallingPackage()` на API 31+ сверяет пакет с + * реальным uid вызывающего — shell (uid 2000) не может представиться `com.leiniao.camera`. */ - private suspend fun startMercuryKillGuard(binder: IBinder) { - if (guardDisplayId > 0) return + fun ensureMercuryKillGuard() { + if (guardWatchdog != null) return + guardWatchdog = testScope.launch { + while (isActive) { + if (guardDisplayId <= 0) { + startGuardOnce() + delay(GUARD_RETRY_MS) + } else { + launchGuardActivity() + delay(GUARD_RELAUNCH_MS) + } + } + } + } + + /** Создаёт служебный дисплей и активность-держатель флага. Вызывается повторно до успеха. */ + private suspend fun startGuardOnce() { + val ctx = appContext ?: return val surfaceTexture = try { - // Дисплей с невалідной Surface считается OFF, и активность на нём не стартует. + // Дисплей с невалидной Surface считается OFF, и активность на нём не стартует. SurfaceTexture(false).also { it.setDefaultBufferSize(GUARD_WIDTH, GUARD_HEIGHT) } } catch (t: Throwable) { - Log.w(TAG, "startMercuryKillGuard: SurfaceTexture failed", t) + Log.w(TAG, "startGuardOnce: SurfaceTexture failed", t) + return + } + val client = ShizukuVdClient(ctx, VmShizukuService::class.java) + val binder = try { + client.bindForResult(timeoutMs = 5000) + } catch (t: Throwable) { + Log.w(TAG, "startGuardOnce: bind failed", t) + null + } + if (binder == null) { + Log.w(TAG, "startGuardOnce: Shizuku binder не получен (нет прав?)") + runCatching { client.stop() } + surfaceTexture.release() return } val displayId = try { @@ -282,31 +356,28 @@ class VmGlassesController { surface = Surface(surfaceTexture), ) } catch (t: Throwable) { - Log.w(TAG, "startMercuryKillGuard: createDisplay failed", t) - surfaceTexture.release() - return + Log.w(TAG, "startGuardOnce: createDisplay failed", t) + 0 } if (displayId <= 0) { - Log.w(TAG, "startMercuryKillGuard: createDisplay вернул displayId=$displayId") + Log.w(TAG, "startGuardOnce: createDisplay вернул displayId=$displayId") + runCatching { client.stop() } surfaceTexture.release() return } + guardClient = client + guardBinder = binder guardDisplayId = displayId guardSurfaceTexture = surfaceTexture - launchGuardActivity(binder, displayId) - // Даём активности дойти до onStart: именно там включается флаг. Иначе первая же - // смена top_app (запуск гостя) успеет попасть в forceStopApp до включения флага. - delay(GUARD_WARMUP_MS) - guardWatchdog = testScope.launch { - while (isActive) { - delay(GUARD_RELAUNCH_MS) - launchGuardActivity(binder, displayId) - } - } - Log.i(TAG, "startMercuryKillGuard: страж поднят на displayId=$displayId") + launchGuardActivity() + Log.i(TAG, "startGuardOnce: страж поднят на displayId=$displayId") } - private fun launchGuardActivity(binder: IBinder, displayId: Int) { + /** Watchdog-перезапуск активности стража: если система её прибила, флаг надо вернуть. */ + private fun launchGuardActivity() { + val binder = guardBinder ?: return + val displayId = guardDisplayId + if (displayId <= 0) return runCatching { ShizukuVdBinder.launchActivity( binder = binder, @@ -317,19 +388,6 @@ class VmGlassesController { }.onFailure { Log.w(TAG, "launchGuardActivity failed (displayId=$displayId)", it) } } - /** Снимает стража: останавливает watchdog и освобождает служебный дисплей. */ - private fun stopMercuryKillGuard(binder: IBinder) { - guardWatchdog?.cancel() - guardWatchdog = null - val displayId = guardDisplayId - guardDisplayId = 0 - guardSurfaceTexture?.release() - guardSurfaceTexture = null - if (displayId > 0) { - runCatching { ShizukuVdBinder.releaseDisplay(binder, displayId) } - } - } - fun setAppContext(context: Context) { this.appContext = context.applicationContext } @@ -340,8 +398,9 @@ class VmGlassesController { private const val VIRTUAL_HEIGHT = VmDisplay.HEIGHT private const val VIRTUAL_DENSITY = VmDisplay.DENSITY - /** `ViewConfiguration.scaledVerticalScrollFactor` при density 160: гость умножает axis на это. */ - private const val SCROLL_FACTOR = 64f + /** Скролл-драг: как часто сливаем накопленные дельты и минимальный порог (px). */ + private const val SCROLL_TICK_MS = 90L + private const val SCROLL_MIN_PX = 16f /** Активность RayNeo-лаунчера, чей `onStart` включает вендорский `tmpStopBackgroundKill`. */ private const val GUARD_PACKAGE = "com.ffalconxr.mercury.launcher" @@ -355,8 +414,8 @@ class VmGlassesController { /** Как часто перепроверяем, что активность стража жива. */ private const val GUARD_RELAUNCH_MS = 5_000L - /** Пауза после старта стража: даём `onStart` выставить флаг до запуска гостя. */ - private const val GUARD_WARMUP_MS = 1_500L + /** Пауза перед повторной попыткой поднять стража (например, Shizuku ещё не дал права). */ + private const val GUARD_RETRY_MS = 3_000L val Local = staticCompositionLocalOf { error("VmGlassesController not provided")