glasses: hold Mercury kill switch on hidden VirtualDisplay; phone: brightness/WiFi panel, two-finger touchpad scroll
This commit is contained in:
@@ -32,6 +32,7 @@ import pw.binom.rayneovm.shared.VmProtocol
|
||||
import pw.binom.rayneovm.shared.GetAppList
|
||||
import pw.binom.rayneovm.shared.AppEntry
|
||||
import pw.binom.rayneovm.shared.AppListResponse
|
||||
import pw.binom.rayneovm.shared.CursorScroll
|
||||
import pw.binom.rayneovm.shared.CursorClick
|
||||
import pw.binom.rayneovm.shared.CursorMove
|
||||
|
||||
@@ -84,6 +85,14 @@ class VmService : LifecycleService() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Текущая активная сессия телефона.
|
||||
*
|
||||
* Нужна, чтобы отключение «чужой»/устаревшей сессии (например, пробного WebSocket-хендшейка
|
||||
* из subnet-фоллбека при поиске очков) не сбрасывало экран в NoConnection, пока жива настоящая.
|
||||
*/
|
||||
private val currentConn = java.util.concurrent.atomic.AtomicReference<String?>(null)
|
||||
|
||||
private fun startServer() {
|
||||
server = Transports.wifiServer(port = WS_PORT, path = WS_PATH)
|
||||
lifecycleScope.launch(kotlinx.coroutines.Dispatchers.IO) {
|
||||
@@ -94,10 +103,15 @@ class VmService : LifecycleService() {
|
||||
},
|
||||
onConnected = { connId ->
|
||||
Log.i(TAG, "Phone connected: $connId")
|
||||
currentConn.set(connId)
|
||||
},
|
||||
onDisconnected = { connId ->
|
||||
Log.i(TAG, "Phone disconnected: $connId")
|
||||
controller.setState(GlassesScreenState.NoConnection)
|
||||
if (currentConn.compareAndSet(connId, null)) {
|
||||
controller.setState(GlassesScreenState.NoConnection)
|
||||
} else {
|
||||
Log.i(TAG, "Ignoring disconnect of stale session $connId")
|
||||
}
|
||||
},
|
||||
)
|
||||
} catch (t: Throwable) {
|
||||
@@ -129,6 +143,10 @@ class VmService : LifecycleService() {
|
||||
Log.d(TAG, "CursorClick")
|
||||
controller.clickCursor()
|
||||
}
|
||||
is CursorScroll -> {
|
||||
Log.d(TAG, "CursorScroll: ${msg.dx},${msg.dy}")
|
||||
controller.scrollCursor(msg.dx, msg.dy)
|
||||
}
|
||||
is VmKeyEvent -> {
|
||||
Log.d(TAG, "Key: ${msg.keyCode} action=${msg.action}")
|
||||
controller.sendKey(msg.keyCode, msg.action)
|
||||
|
||||
+147
-8
@@ -5,14 +5,19 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import android.view.Surface
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
@@ -41,6 +46,15 @@ class VmGlassesController {
|
||||
val cursor: StateFlow<CursorPosition?> = _cursor.asStateFlow()
|
||||
|
||||
private var activeSource: VirtualDisplayFrameSource? = null
|
||||
|
||||
/** Служебный дисплей Mercury-«стража» (см. [startMercuryKillGuard]); 0 — не создан. */
|
||||
private var guardDisplayId = 0
|
||||
|
||||
/** Surface служебного дисплея: без валидной Surface дисплей считается OFF и активность не стартует. */
|
||||
private var guardSurfaceTexture: SurfaceTexture? = null
|
||||
|
||||
/** Watchdog: переподнимает активность стража, если она завершилась сама. */
|
||||
private var guardWatchdog: Job? = null
|
||||
@Volatile private var appContext: Context? = null
|
||||
|
||||
/** Сериализует создание/освобождение VirtualDisplay — параллельный acquire оставил бы висеть второй UserService. */
|
||||
@@ -87,7 +101,7 @@ class VmGlassesController {
|
||||
return
|
||||
}
|
||||
try {
|
||||
runCatching { ShizukuVdBinder.stopKillGuard(src.shizukuBinder) }
|
||||
runCatching { stopMercuryKillGuard(src.shizukuBinder) }
|
||||
src.release()
|
||||
} catch (t: Throwable) {
|
||||
Log.w(TAG, "releaseFrameSource failed", t)
|
||||
@@ -130,6 +144,33 @@ class VmGlassesController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Скролл в текущей позиции курсора: [dx]/[dy] — смещение пальцев в пикселях координат
|
||||
* гостя (два пальца по тачпаду). Инжектим колесо мыши (`ACTION_SCROLL`) в точку курсора.
|
||||
*
|
||||
* Гость умножает axis-значение на `ViewConfiguration.scaledVerticalScrollFactor`
|
||||
* (≈ [SCROLL_FACTOR]px при density 160), поэтому делим на него: смещение становится
|
||||
* 1:1 с движением пальца.
|
||||
*/
|
||||
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)")
|
||||
}
|
||||
}
|
||||
|
||||
/** Клавиатурное/медиа-событие гостю на его VirtualDisplay (Back, Play, Pause, ...). */
|
||||
fun sendKey(keyCode: Int, action: Int) {
|
||||
val src = activeSource ?: return
|
||||
@@ -180,13 +221,8 @@ class VmGlassesController {
|
||||
val launchIntent = pm.getLaunchIntentForPackage(packageName)
|
||||
val className = launchIntent?.component?.className ?: ""
|
||||
|
||||
// Пока гость работает, держим «страж»: Settings.Secure.top_app = наш пакет.
|
||||
// Mercury-лаунчер по первой же проверке в forceStopApp (pkgName == mCurrentTopPkg)
|
||||
// отменяет убийство. Иначе нас/гостя force-стопит через ~2 c после смены top-app.
|
||||
// Подробный разбор — в ShizukuVdBinder.startKillGuard.
|
||||
if (!ShizukuVdBinder.startKillGuard(src.shizukuBinder, ctx2.packageName)) {
|
||||
Log.w(TAG, "launchGuest: не удалось запустить Mercury kill guard")
|
||||
}
|
||||
// Пока гость работает — держим выключенным Mercury-«убийцу» (BackgroundAppManager).
|
||||
startMercuryKillGuard(src.shizukuBinder)
|
||||
|
||||
val ok = ShizukuVdBinder.launchActivity(
|
||||
binder = src.shizukuBinder,
|
||||
@@ -209,6 +245,91 @@ class VmGlassesController {
|
||||
setState(GlassesScreenState.NoAppSelected)
|
||||
}
|
||||
|
||||
/**
|
||||
* Выключает Mercury-«убийцу» фоновых приложений (`BackgroundAppManager`) на время работы гостя.
|
||||
*
|
||||
* У RayNeo политика «одно приложение на переднем плане»: на каждой смене `Secure.top_app`
|
||||
* лаунчер force-стопит все пакеты из накопительной очереди предыдущих топов. Гость на
|
||||
* VirtualDisplay тоже считается топом, поэтому лаунчер убивал то наше приложение, то гостя
|
||||
* (снаружи выглядело как «клик работает ровно один раз»).
|
||||
*
|
||||
* Легальный выключатель — вендорский `BackgroundAppManager.enableStopBackgroundKill(true)`.
|
||||
* Его ставит `NetConfigActivity.onStart` и снимает в `onStop`, поэтому держим эту активность
|
||||
* запущенной на отдельном невидимом VirtualDisplay: там её никто не перекрывает, флаг
|
||||
* остаётся true, и `forceStopApp` отменяется на проверке `tmpStopBackgroundKill`.
|
||||
*
|
||||
* Почему не через `top_app` и не через провайдер: `Settings.Secure.top_app` лаунчер сам
|
||||
* переписывает по реальному топу, а `ContentProvider.getCallingPackage()` на API 31+
|
||||
* сверяет пакет с реальным uid вызывающего — shell (uid 2000) не может представиться
|
||||
* `com.leiniao.camera`.
|
||||
*/
|
||||
private suspend fun startMercuryKillGuard(binder: IBinder) {
|
||||
if (guardDisplayId > 0) return
|
||||
val surfaceTexture = try {
|
||||
// Дисплей с невалідной Surface считается OFF, и активность на нём не стартует.
|
||||
SurfaceTexture(false).also { it.setDefaultBufferSize(GUARD_WIDTH, GUARD_HEIGHT) }
|
||||
} catch (t: Throwable) {
|
||||
Log.w(TAG, "startMercuryKillGuard: SurfaceTexture failed", t)
|
||||
return
|
||||
}
|
||||
val displayId = try {
|
||||
ShizukuVdBinder.createDisplay(
|
||||
binder = binder,
|
||||
name = GUARD_DISPLAY_NAME,
|
||||
width = GUARD_WIDTH,
|
||||
height = GUARD_HEIGHT,
|
||||
density = VIRTUAL_DENSITY,
|
||||
surface = Surface(surfaceTexture),
|
||||
)
|
||||
} catch (t: Throwable) {
|
||||
Log.w(TAG, "startMercuryKillGuard: createDisplay failed", t)
|
||||
surfaceTexture.release()
|
||||
return
|
||||
}
|
||||
if (displayId <= 0) {
|
||||
Log.w(TAG, "startMercuryKillGuard: createDisplay вернул displayId=$displayId")
|
||||
surfaceTexture.release()
|
||||
return
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
private fun launchGuardActivity(binder: IBinder, displayId: Int) {
|
||||
runCatching {
|
||||
ShizukuVdBinder.launchActivity(
|
||||
binder = binder,
|
||||
packageName = GUARD_PACKAGE,
|
||||
className = GUARD_ACTIVITY,
|
||||
displayId = displayId,
|
||||
)
|
||||
}.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
|
||||
}
|
||||
@@ -219,6 +340,24 @@ 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
|
||||
|
||||
/** Активность RayNeo-лаунчера, чей `onStart` включает вендорский `tmpStopBackgroundKill`. */
|
||||
private const val GUARD_PACKAGE = "com.ffalconxr.mercury.launcher"
|
||||
private const val GUARD_ACTIVITY =
|
||||
"com.ffalconxr.mercury.launcher.wizard.netconfig.NetConfigActivity"
|
||||
|
||||
private const val GUARD_DISPLAY_NAME = "VmKillGuard"
|
||||
private const val GUARD_WIDTH = 320
|
||||
private const val GUARD_HEIGHT = 240
|
||||
|
||||
/** Как часто перепроверяем, что активность стража жива. */
|
||||
private const val GUARD_RELAUNCH_MS = 5_000L
|
||||
|
||||
/** Пауза после старта стража: даём `onStart` выставить флаг до запуска гостя. */
|
||||
private const val GUARD_WARMUP_MS = 1_500L
|
||||
|
||||
val Local = staticCompositionLocalOf<VmGlassesController> {
|
||||
error("VmGlassesController not provided")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user