phone: autonomous Shizuku revive over wireless-adb; glasses screen on/off buttons

AdbClient wraps the bundled libadb.so (useLegacyPackaging -> exec from nativeLibraryDir, HOME/TMPDIR overridden, adb key injected into BuildConfig from ~/.android at build time and never committed). ShizukuProvisioner: on each BLE+WiFi connect it enables ADB-over-WiFi via Mercury, connects with mDNS fallback, pushes the Shizuku starter and runs it; runs once per BLE session, does not relaunch GlassesApp when the WS channel is already up, and exposes runShell() for arbitrary commands. DebugReceiver (DUMP) + am start --ez provision for diagnostics.

Protocol: ShizukuStatus(running/granted) + RequestShizukuStatus; glasses push it on connect and on Shizuku state change, using a passive probe() that never shows a permission dialog. Glasses kill-guard now recreates its UserService when the Shizuku binder dies instead of spamming DeadObjectException. Mercury: brightness disables auto-brightness first (was silently ignored), BLE auto-connect loop retries every 6s (fixes 'searches then stops'). New phone section 'Экран очков' with wake/sleep via adb (input keyevent 224/223).
This commit is contained in:
2026-09-27 03:52:05 +03:00
parent 59069a741e
commit 8a0b7832c0
18 changed files with 1112 additions and 44 deletions
@@ -112,6 +112,23 @@ object ShizukuBootstrap {
fun isGranted(): Boolean = _state.value == AllowState.Granted
/** Снимок состояния без побочных эффектов (не показывает диалог разрешения). */
data class Snapshot(val running: Boolean, val granted: Boolean)
/**
* Пассивная проверка Shizuku: жив ли сервер и выдан ли нам permission.
*
* Зовётся из сервиса (не из Activity), поэтому не имеет права запрашивать разрешение —
* только читает. Нужна, чтобы телефон узнал, поднимать ли Shizuku через ADB.
*/
fun probe(): Snapshot {
val running = try { Shizuku.pingBinder() } catch (_: Throwable) { false }
val granted = if (!running) false else try {
Shizuku.checkSelfPermission() == 0 /* PERMISSION_GRANTED */
} catch (_: Throwable) { false }
return Snapshot(running = running, granted = granted)
}
/**
* Хук для Activity.onRequestPermissionResult: перенаправить в Shizuku.
*
@@ -35,6 +35,8 @@ import pw.binom.rayneovm.shared.AppListResponse
import pw.binom.rayneovm.shared.CursorScroll
import pw.binom.rayneovm.shared.CursorClick
import pw.binom.rayneovm.shared.CursorMove
import pw.binom.rayneovm.shared.RequestShizukuStatus
import pw.binom.rayneovm.shared.ShizukuStatus
/**
* Foreground service на стороне очков.
@@ -67,6 +69,11 @@ class VmService : LifecycleService() {
startForeground()
startServer()
registerMdns()
// Сообщаем телефону о Shizuku, как только меняется его состояние (сервер поднялся
// или нам выдали permission) — чтобы телефон не опрашивал вручную.
lifecycleScope.launch {
ShizukuBootstrap.state.collect { pushShizukuStatus() }
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -110,6 +117,8 @@ class VmService : LifecycleService() {
currentConn.set(connId)
// Переподключение: снять залипшее «Нет связи с телефоном».
controller.onPhoneConnected()
// И сразу отдать телефoну состояние Shizuku (для ADB-бутстрапа).
lifecycleScope.launch { sendShizukuStatus(connId) }
},
onDisconnected = { connId ->
Log.i(TAG, "Phone disconnected: $connId")
@@ -174,11 +183,32 @@ class VmService : LifecycleService() {
Log.e(TAG, "Failed to send app list", t)
}
}
is RequestShizukuStatus -> sendShizukuStatus(connId)
is Ping -> server?.send(connId, VmProtocol.encode(Pong))
else -> Log.d(TAG, "Unhandled: $msg")
}
}
/** Отправляет текущее состояние Shizuku конкретной сессии (ответ на запрос). */
private suspend fun sendShizukuStatus(connId: String) {
val snapshot = ShizukuBootstrap.probe()
val json = VmProtocol.encode(
ShizukuStatus(running = snapshot.running, granted = snapshot.granted),
)
try {
server?.send(connId, json)
} catch (t: Throwable) {
Log.w(TAG, "Не удалось отправить ShizukuStatus: ${t.message}")
}
Log.i(TAG, "ShizukuStatus: running=${snapshot.running} granted=${snapshot.granted}")
}
/** Отправляет состояние Shizuku текущей активной сессии (если есть). */
private suspend fun pushShizukuStatus() {
val connId = currentConn.get() ?: return
sendShizukuStatus(connId)
}
private fun createNotificationChannel(): String {
val channelId = "vm_service_channel"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -477,14 +477,44 @@ class VmGlassesController {
val binder = guardBinder ?: return
val displayId = guardDisplayId
if (displayId <= 0) return
runCatching {
try {
ShizukuVdBinder.launchActivity(
binder = binder,
packageName = GUARD_PACKAGE,
className = GUARD_ACTIVITY,
displayId = displayId,
)
}.onFailure { Log.w(TAG, "launchGuardActivity failed (displayId=$displayId)", it) }
} catch (t: Throwable) {
// Binder умер (Shizuku-сервер перезапускался/упал): старый UserService бесполезен,
// его надо выбросить и поднять новый — иначе watchdog вечно долбит мёртвый binder
// (DeadObjectException каждые 5с), а стража на экране нет.
if (t is android.os.RemoteException || t is IllegalStateException) {
Log.w(TAG, "launchGuardActivity: binder мёртв (displayId=$displayId) — пересоздаю стража", t)
resetGuardState()
} else {
Log.w(TAG, "launchGuardActivity failed (displayId=$displayId)", t)
}
}
}
/** Сбрасывает состояние стража (без остановки watchdog), чтобы тот поднял его заново. */
private fun resetGuardState() {
val binder = guardBinder
val displayId = guardDisplayId
val client = guardClient
val texture = guardSurfaceTexture
guardBinder = null
guardDisplayId = 0
guardClient = null
guardSurfaceTexture = null
testScope.launch {
runCatching { texture?.release() }
if (binder != null) {
if (displayId > 0) runCatching { ShizukuVdBinder.releaseDisplay(binder, displayId) }
runCatching { ShizukuVdBinder.destroy(binder) }
}
runCatching { client?.stop() }
}
}
fun setAppContext(context: Context) {