glasses: start Mercury kill guard with service; batch scroll deltas into one drag per tick
This commit is contained in:
@@ -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()
|
||||
|
||||
+122
-63
@@ -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 {
|
||||
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<VmGlassesController> {
|
||||
error("VmGlassesController not provided")
|
||||
|
||||
Reference in New Issue
Block a user