phone: keep-alive foreground service + notification perm; glasses: clear NoConnection on reconnect
- ConnectionKeepAliveService (foreground, PARTIAL_WAKE_LOCK + WIFI_MODE_FULL_HIGH_PERF, started in PhoneVmApp.onCreate): without it HONOR deep-sleeps the phone process on screen-off and the glasses lose the WebSocket link. Same mechanic as view-mate. - MainActivity: request POST_NOTIFICATIONS so the FGS notification is visible. - VmGlassesController.onPhoneConnected(): drop a stuck NoConnection state when the phone (re)connects; returns to Running if a guest is active, else NoAppSelected. - VmService: call it from onConnected. - stopGuest/switch: force-stop the previous guest before releasing its display (releasing first migrates the guest task to display 0 full-screen); VirtualDisplay frame source now destroys its UserService on release. - Mercury status: retry the first pullGeneralStatus until it lands, refresh every 15s and on tab open; GlassesStatusScreen shows 'Ждём статус от очков…' instead of fake zeros. - MainActivity(glasses): HOME candidate flag + finish() on a non-default display (recursion guard). - README: Home-launcher setup/rollback section. - BOOTSTRAP_TEST.md: post-reboot Shizuku bootstrap investigation log.
This commit is contained in:
@@ -3,6 +3,7 @@ package pw.binom.rayneovm.glasses
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.view.Display
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
@@ -27,6 +28,14 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// HOME живёт на display 0. Если систему (или гостя на VirtualDisplay)
|
||||
// угораздит запросить домашний экран внутри чужого дисплея — мы бы
|
||||
// открылись сами внутри себя (зеркальная рекурсия), поэтому выходим.
|
||||
val displayId = display?.displayId ?: Display.DEFAULT_DISPLAY
|
||||
if (displayId != Display.DEFAULT_DISPLAY) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
enableEdgeToEdge()
|
||||
ShizukuBootstrap.attach(this)
|
||||
val controller = VmGlassesController.fromContext(this)
|
||||
|
||||
@@ -108,6 +108,8 @@ class VmService : LifecycleService() {
|
||||
onConnected = { connId ->
|
||||
Log.i(TAG, "Phone connected: $connId")
|
||||
currentConn.set(connId)
|
||||
// Переподключение: снять залипшее «Нет связи с телефоном».
|
||||
controller.onPhoneConnected()
|
||||
},
|
||||
onDisconnected = { connId ->
|
||||
Log.i(TAG, "Phone disconnected: $connId")
|
||||
@@ -203,6 +205,10 @@ class VmService : LifecycleService() {
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
// Прибираем за собой: гость (force-stop + дисплей), страж (флаг + дисплей + UserService).
|
||||
// Не покрывает SIGKILL/reinstall — там наш код не выполняется вообще.
|
||||
runCatching { controller.stopGuest() }
|
||||
runCatching { controller.stopMercuryKillGuard() }
|
||||
unregisterMdns()
|
||||
server?.close()
|
||||
server = null
|
||||
|
||||
+92
-4
@@ -51,6 +51,9 @@ class VmGlassesController {
|
||||
|
||||
private var activeSource: VirtualDisplayFrameSource? = null
|
||||
|
||||
/** Пакет гостя, запущенного на [activeSource] — его и убиваем в [stopGuest]. */
|
||||
@Volatile private var activeGuestPackage: String? = null
|
||||
|
||||
/** Служебный дисплей Mercury-«стража» (см. [ensureMercuryKillGuard]); 0 — не создан. */
|
||||
private var guardDisplayId = 0
|
||||
|
||||
@@ -81,6 +84,9 @@ class VmGlassesController {
|
||||
Log.w(TAG, "acquireFrameSource: appContext not set")
|
||||
return@withLock null
|
||||
}
|
||||
// Смена приложения: прошлого гостя сначала УБИВАЕМ, и только потом рвём его дисплей —
|
||||
// иначе задача переезжает на display 0 и вылезает во весь экран (см. [stopGuest]).
|
||||
killActiveGuest()
|
||||
releaseFrameSource()
|
||||
val surfaceTexture = try {
|
||||
// ВАЖНО: single-buffer нельзя — DisplayManagerService reject'ит
|
||||
@@ -106,6 +112,7 @@ class VmGlassesController {
|
||||
return@withLock null
|
||||
}
|
||||
activeSource = source
|
||||
activeGuestPackage = packageName
|
||||
_frameSource.value = source
|
||||
source
|
||||
}
|
||||
@@ -131,6 +138,18 @@ class VmGlassesController {
|
||||
_state.value = newState
|
||||
}
|
||||
|
||||
/**
|
||||
* Телефон подключился (в том числе переподключился после разрыва). Если на экране висел
|
||||
* [GlassesScreenState.NoConnection], снимаем его — связь-то восстановлена. Гостя не трогаем:
|
||||
* если он запущен, возвращаем [GlassesScreenState.Running], иначе [GlassesScreenState.NoAppSelected].
|
||||
*/
|
||||
fun onPhoneConnected() {
|
||||
if (_state.value !is GlassesScreenState.NoConnection) return
|
||||
val pkg = activeGuestPackage
|
||||
if (pkg != null) setState(GlassesScreenState.Running(pkg))
|
||||
else setState(GlassesScreenState.NoAppSelected)
|
||||
}
|
||||
|
||||
/**
|
||||
* Перемещает виртуальный курсор мыши. Очки рисуют его в обоих глазах, а [clickCursor]
|
||||
* использует последнюю позицию как точку клика.
|
||||
@@ -293,12 +312,48 @@ class VmGlassesController {
|
||||
}
|
||||
}
|
||||
|
||||
/** Остановить гостя и освободить VirtualDisplay (по Shutdown или смене приложения). */
|
||||
/**
|
||||
* Остановить гостя и освободить VirtualDisplay (по Shutdown или смене приложения).
|
||||
*
|
||||
* Порядок критичен: СНАЧАЛА force-stop гостя, потом release дисплея. Если освободить
|
||||
* дисплей первым, система не убивает процесс, а переносит задачу на основной дисплей —
|
||||
* приложение вылезает на очках во весь экран (так было с `free.zona`).
|
||||
*
|
||||
* Всё под [acquireMutex]: параллельный запуск нового гостя не должен поднять дисплей,
|
||||
* пока мы добиваем старого.
|
||||
*/
|
||||
fun stopGuest() {
|
||||
Log.i(TAG, "stopGuest")
|
||||
releaseFrameSource()
|
||||
_cursor.value = null
|
||||
setState(GlassesScreenState.NoAppSelected)
|
||||
if (activeSource == null && activeGuestPackage == null) {
|
||||
setState(GlassesScreenState.NoAppSelected)
|
||||
return
|
||||
}
|
||||
testScope.launch {
|
||||
acquireMutex.withLock {
|
||||
killActiveGuest()
|
||||
releaseFrameSource()
|
||||
_cursor.value = null
|
||||
setState(GlassesScreenState.NoAppSelected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-stop активного гостя, дисплей НЕ трогает. Вызывать под [acquireMutex].
|
||||
*
|
||||
* Без этого процесс гостя остаётся жить (со своим VirtualDisplay он уже не связан), а при
|
||||
* простом release дисплея система переносит задачу на display 0 — гость вылезает во весь
|
||||
* экран очков.
|
||||
*/
|
||||
private suspend fun killActiveGuest() {
|
||||
val src = activeSource
|
||||
val pkg = activeGuestPackage
|
||||
if (src == null || pkg == null) return
|
||||
val killed = ShizukuVdBinder.forceStopPackage(src.shizukuBinder, pkg)
|
||||
Log.i(TAG, "killActiveGuest: forceStop($pkg) -> $killed")
|
||||
activeGuestPackage = null
|
||||
// Даём AMS снять задачу, прежде чем рвать дисплей.
|
||||
if (killed) delay(GUEST_KILL_SETTLE_MS)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -387,6 +442,36 @@ class VmGlassesController {
|
||||
Log.i(TAG, "startGuardOnce: страж поднят на displayId=$displayId")
|
||||
}
|
||||
|
||||
/**
|
||||
* Гасит Mercury-«страж»: watchdog, выключатель, служебный дисплей и его UserService.
|
||||
* Идемпотентно. Без этого после остановки приложения остаются висеть невидимый дисплей
|
||||
* и процесс `:shizuku_vd` (см. правило #4847).
|
||||
*/
|
||||
fun stopMercuryKillGuard() {
|
||||
guardWatchdog?.cancel()
|
||||
guardWatchdog = null
|
||||
val binder = guardBinder
|
||||
val displayId = guardDisplayId
|
||||
val client = guardClient
|
||||
val texture = guardSurfaceTexture
|
||||
guardBinder = null
|
||||
guardDisplayId = 0
|
||||
guardClient = null
|
||||
guardSurfaceTexture = null
|
||||
if (binder == null && displayId <= 0 && client == null && texture == null) return
|
||||
Log.i(TAG, "stopMercuryKillGuard: displayId=$displayId")
|
||||
testScope.launch {
|
||||
if (binder != null) {
|
||||
runCatching { ShizukuVdBinder.stopKillGuard(binder) }
|
||||
if (displayId > 0) runCatching { ShizukuVdBinder.releaseDisplay(binder, displayId) }
|
||||
}
|
||||
runCatching { texture?.release() }
|
||||
// Процесс UserService надо именно убить: unbind(remove=true) его не завершает (#4847).
|
||||
if (binder != null) runCatching { ShizukuVdBinder.destroy(binder) }
|
||||
runCatching { client?.stop() }
|
||||
}
|
||||
}
|
||||
|
||||
/** Watchdog-перезапуск активности стража: если система её прибила, флаг надо вернуть. */
|
||||
private fun launchGuardActivity() {
|
||||
val binder = guardBinder ?: return
|
||||
@@ -436,6 +521,9 @@ class VmGlassesController {
|
||||
/** Пауза перед повторной попыткой поднять стража (например, Shizuku ещё не дал права). */
|
||||
private const val GUARD_RETRY_MS = 3_000L
|
||||
|
||||
/** Пауза после force-stop гостя: AMS должен успеть снять задачу до release дисплея. */
|
||||
private const val GUEST_KILL_SETTLE_MS = 300L
|
||||
|
||||
val Local = staticCompositionLocalOf<VmGlassesController> {
|
||||
error("VmGlassesController not provided")
|
||||
}
|
||||
|
||||
+4
@@ -40,6 +40,10 @@ class VirtualDisplayFrameSource private constructor(
|
||||
runCatching { ShizukuVdBinder.releaseDisplay(binder, displayId) }
|
||||
runCatching { surface.release() }
|
||||
super.release()
|
||||
// Убиваем процесс UserService: `unbindUserService(remove = true)` сам по себе его не
|
||||
// завершает (Shizuku держит UserService живым для переиспользования), и после каждого
|
||||
// запуска гостя оставался висеть процесс-сирота (~13 МБ). `destroy` шлёт System.exit.
|
||||
runCatching { ShizukuVdBinder.destroy(binder) }
|
||||
// Отвязываем UserService — держим его живым ровно на время жизни дисплея.
|
||||
runCatching { client.stop() }
|
||||
Log.i(TAG, "VirtualDisplayFrameSource released: displayId=$displayId")
|
||||
|
||||
Reference in New Issue
Block a user