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
|
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()
|
||||||
|
|||||||
+122
-63
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user