glasses: start Mercury kill guard with service; batch scroll deltas into one drag per tick

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