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
controller = VmGlassesController.fromContext(this)
Log.i(TAG, "VmService onCreate")
// Стража поднимаем сразу при старте сервиса, а не в момент запуска гостя: убийца
// (BackgroundAppManager) успевает убить за ~2 c после смены top_app, а запуск гостя
// сам эту смену и вызывает. Подробности — в VmGlassesController.ensureMercuryKillGuard.
controller.ensureMercuryKillGuard()
startForeground()
startServer()
registerMdns()
@@ -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<FloatArray>(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 {
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 = binder,
displayId = displayId,
binder = src.shizukuBinder,
displayId = src.virtualDisplayId,
x = pos.x,
y = pos.y,
hScroll = dx / SCROLL_FACTOR,
vScroll = dy / SCROLL_FACTOR,
dx = accX,
dy = accY,
)
if (!ok) Log.w(TAG, "scrollCursor: scroll failed (displayId=$displayId)")
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<VmGlassesController> {
error("VmGlassesController not provided")