phone: AdbController — единственная точка входа для adb-команд; screen on/off через Канал 1
- AdbController: shell/push/ensureConnected/invalidate; перед каждой командой проверяет соединение (echo ok) и сам поднимает wireless-adb (Mercury -> mDNS -> connect). Мьютекс только на подъём, длинный бутстрап не блокирует команды. - ShizukuProvisioner переведён на AdbController (adb-код вынесен). - Убрана неработающая UI-консоль adb (нужен только программный API). - ScreenControl(sleep) в shared: гашение/пробуждение экрана очков их же Shizuku-UserService (инжект 223/224 как shell), не зависит от Mercury/adb. - VmPhoneController.setScreen + кнопки «Экран очков» на Канале 1. - DebugReceiver: DEBUG_ADB_SHELL (+ SET_DEEP_SUSPEND) как отладочный инструмент. - MercuryController/AdbClient: сопутствующие правки.
This commit is contained in:
@@ -60,6 +60,10 @@
|
||||
android:permission="android.permission.DUMP">
|
||||
<intent-filter>
|
||||
<action android:name="pw.binom.rayneovm.phone.DEBUG_PROVISION" />
|
||||
<action android:name="pw.binom.rayneovm.phone.DEBUG_SET_BRIGHTNESS" />
|
||||
<action android:name="pw.binom.rayneovm.phone.DEBUG_SET_DEEP_SUSPEND" />
|
||||
<action android:name="pw.binom.rayneovm.phone.DEBUG_ADB_SHELL" />
|
||||
<action android:name="pw.binom.rayneovm.phone.DEBUG_REFRESH_STATUS" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
|
||||
@@ -8,6 +8,7 @@ import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import pw.binom.rayneovm.phone.adb.AdbController
|
||||
import pw.binom.rayneovm.phone.bootstrap.ShizukuProvisioner
|
||||
import pw.binom.rayneovm.phone.mercury.MercuryController
|
||||
import pw.binom.rayneovm.phone.mercury.MercuryControllerProvider
|
||||
@@ -22,14 +23,16 @@ import pw.binom.rayneovm.phone.protocol.VmPhoneController
|
||||
* При старте подготавливает контроллер [VmPhoneController] (auto-reconnect к очкам через mDNS)
|
||||
* и [MercuryController] (второй канал — RayNeo Mercury BLE).
|
||||
*/
|
||||
class PhoneVmApp : Application(), ControllerProvider, MercuryControllerProvider, ShizukuProvisionerProvider {
|
||||
class PhoneVmApp : Application(), ControllerProvider, MercuryControllerProvider,
|
||||
ShizukuProvisionerProvider, AdbControllerProvider {
|
||||
|
||||
private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
private val controller: VmPhoneController by lazy { VmPhoneController(this) }
|
||||
private val mercuryController: MercuryController by lazy { MercuryController(this) }
|
||||
private val adbController: AdbController by lazy { AdbController(this, mercuryController) }
|
||||
private val provisioner: ShizukuProvisioner by lazy {
|
||||
ShizukuProvisioner(this, mercuryController, controller)
|
||||
ShizukuProvisioner(this, mercuryController, controller, adbController)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -90,6 +93,7 @@ class PhoneVmApp : Application(), ControllerProvider, MercuryControllerProvider,
|
||||
override fun provideController(): VmPhoneController = controller
|
||||
override fun provideMercuryController(): MercuryController = mercuryController
|
||||
override fun provideShizukuProvisioner(): ShizukuProvisioner = provisioner
|
||||
override fun provideAdbController(): AdbController = adbController
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PhoneVmApp"
|
||||
@@ -101,3 +105,8 @@ class PhoneVmApp : Application(), ControllerProvider, MercuryControllerProvider,
|
||||
interface ShizukuProvisionerProvider {
|
||||
fun provideShizukuProvisioner(): ShizukuProvisioner
|
||||
}
|
||||
|
||||
/** Провайдер adb-контроллера (единая точка входа для adb-команд) для UI. */
|
||||
interface AdbControllerProvider {
|
||||
fun provideAdbController(): AdbController
|
||||
}
|
||||
|
||||
@@ -86,8 +86,8 @@ class AdbClient(private val context: Context) {
|
||||
exec(listOf("-s", serial, "push", local.absolutePath, remote), timeoutMs)
|
||||
|
||||
/** Устройство реально отвечает по serial. */
|
||||
fun isConnected(serial: String): Boolean =
|
||||
shell(serial, "echo ok", timeoutMs = 10_000).output.contains("ok")
|
||||
fun isConnected(serial: String, timeoutMs: Long = 10_000): Boolean =
|
||||
shell(serial, "echo ok", timeoutMs = timeoutMs).output.contains("ok")
|
||||
|
||||
private fun exec(args: List<String>, timeoutMs: Long): Result {
|
||||
val cmd = listOf(adbPath) + args
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
package pw.binom.rayneovm.phone.adb
|
||||
|
||||
import android.content.Context
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import pw.binom.rayneovm.phone.mercury.MercuryController
|
||||
import java.io.File
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
/**
|
||||
* Единая точка входа для всех adb-вызовов к очкам.
|
||||
*
|
||||
* Зачем: wireless-adb на очках **сам гаснет**, как только они теряют WiFi-сеть, к которой
|
||||
* он был привязан (`adb_wifi_enabled` → 0, порт → -1), и не переживает ребут. Поэтому
|
||||
* хранить «готовое» соединение бессмысленно — его надо каждый раз проверять и при
|
||||
* необходимости поднимать заново.
|
||||
*
|
||||
* Контракт: **все** команды идут через [shell] / [push]; каждый вызов сначала
|
||||
* [ensureConnected] — если соединение живо, оно переиспользуется (кэш `ip:port` + дешёвый
|
||||
* `echo ok`), если мертво — wireless-adb включается через Mercury, адрес при
|
||||
* необходимости ищется по mDNS `_adb-tls-connect._tcp`, и adb подключается заново.
|
||||
*
|
||||
* Мьютекс держится **только на время подъёма** соединения, поэтому длинный бутстрап
|
||||
* Shizuku не блокирует пользовательские команды (кнопки в UI).
|
||||
*/
|
||||
class AdbController(
|
||||
context: Context,
|
||||
private val mercury: MercuryController,
|
||||
) {
|
||||
|
||||
private val appContext = context.applicationContext
|
||||
private val adb = AdbClient(appContext)
|
||||
|
||||
/** Только на подъём соединения: сами команды после него идут без блокировки. */
|
||||
private val connectMutex = Mutex()
|
||||
|
||||
@Volatile
|
||||
private var address: String? = null
|
||||
|
||||
private val _state = MutableStateFlow<AdbConnectionState>(AdbConnectionState.Disconnected)
|
||||
val state: StateFlow<AdbConnectionState> = _state.asStateFlow()
|
||||
|
||||
/** Бинарник adb и ключ встроены в сборку. */
|
||||
val available: Boolean get() = adb.available
|
||||
|
||||
/**
|
||||
* Живой serial очков (`ip:port`), поднимая соединение при необходимости.
|
||||
*
|
||||
* Быстрый путь: сохранённый адрес + `echo ok` (≤ [PROBE_TIMEOUT_MS]). Если не отвечает —
|
||||
* полный подъём: Mercury включает wireless-adb → адрес от Mercury или mDNS → `adb connect`.
|
||||
*
|
||||
* @return serial для [AdbClient] или null, если поднять не удалось (см. [state]).
|
||||
*/
|
||||
suspend fun ensureConnected(): String? = withContext(Dispatchers.IO) {
|
||||
if (!adb.available) {
|
||||
fail("adb-бинарник или ключ не встроены в сборку")
|
||||
return@withContext null
|
||||
}
|
||||
address?.let { cached ->
|
||||
if (adb.isConnected(cached, timeoutMs = PROBE_TIMEOUT_MS)) {
|
||||
_state.value = AdbConnectionState.Connected(cached)
|
||||
return@withContext cached
|
||||
}
|
||||
Log.i(TAG, "соединение с $cached протухло — поднимаем заново")
|
||||
address = null
|
||||
}
|
||||
connectMutex.withLock {
|
||||
// Пока ждали мьютекс, подъём мог уже случиться в другом вызове.
|
||||
address?.let { cached ->
|
||||
if (adb.isConnected(cached, timeoutMs = PROBE_TIMEOUT_MS)) {
|
||||
_state.value = AdbConnectionState.Connected(cached)
|
||||
return@withLock cached
|
||||
}
|
||||
address = null
|
||||
}
|
||||
bringUp()?.also { address = it }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполнить shell-команду на очках (`adb -s <serial> shell ...`). Соединение проверяется
|
||||
* и при необходимости поднимается автоматически.
|
||||
*/
|
||||
suspend fun shell(command: String, timeoutMs: Long = DEFAULT_SHELL_TIMEOUT_MS): Result<String> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val serial = ensureConnected()
|
||||
?: return@withContext Result.failure(
|
||||
IllegalStateException("нет adb-соединения с очками (wireless-adb не поднялся?)"),
|
||||
)
|
||||
val result = adb.shell(serial, command, timeoutMs = timeoutMs)
|
||||
Log.i(TAG, "shell($serial): $command -> exit=${result.exitCode} ${result.output.take(200)}")
|
||||
if (result.ok) {
|
||||
Result.success(result.output)
|
||||
} else {
|
||||
Result.failure(IllegalStateException(result.output.ifBlank { "exit=${result.exitCode}" }))
|
||||
}
|
||||
}
|
||||
|
||||
/** Залить файл на очки (`adb push`). Соединение поднимается автоматически. */
|
||||
suspend fun push(
|
||||
local: File,
|
||||
remote: String,
|
||||
timeoutMs: Long = DEFAULT_PUSH_TIMEOUT_MS,
|
||||
): Result<String> = withContext(Dispatchers.IO) {
|
||||
val serial = ensureConnected()
|
||||
?: return@withContext Result.failure(
|
||||
IllegalStateException("нет adb-соединения с очками (wireless-adb не поднялся?)"),
|
||||
)
|
||||
val result = adb.push(serial, local, remote, timeoutMs = timeoutMs)
|
||||
Log.i(TAG, "push($serial): ${local.name} -> $remote exit=${result.exitCode} ${result.output.take(200)}")
|
||||
if (result.ok) {
|
||||
Result.success(result.output)
|
||||
} else {
|
||||
Result.failure(IllegalStateException(result.output.ifBlank { "exit=${result.exitCode}" }))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Диагностика: подключиться к заданному `ip:port` в обход Mercury/mDNS.
|
||||
* Нужно, чтобы проверять adb-часть, когда телефон и очки в разных сетях (через релей).
|
||||
*/
|
||||
suspend fun ensureConnectedTo(hostPort: String): String? = withContext(Dispatchers.IO) {
|
||||
if (!adb.available) {
|
||||
fail("adb-бинарник или ключ не встроены в сборку")
|
||||
return@withContext null
|
||||
}
|
||||
connectMutex.withLock {
|
||||
_state.value = AdbConnectionState.BringingUp
|
||||
adb.installKey()
|
||||
adb.startServer(timeoutMs = 15_000)
|
||||
if (!adb.isConnected(hostPort, timeoutMs = PROBE_TIMEOUT_MS)) {
|
||||
adb.disconnect(hostPort, timeoutMs = 5_000)
|
||||
adb.connect(hostPort, timeoutMs = 12_000)
|
||||
}
|
||||
if (!adb.isConnected(hostPort, timeoutMs = PROBE_TIMEOUT_MS)) {
|
||||
fail("adb connect $hostPort не удался")
|
||||
return@withLock null
|
||||
}
|
||||
address = hostPort
|
||||
_state.value = AdbConnectionState.Connected(hostPort)
|
||||
hostPort
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сбросить кэш соединения — вызывать при разрыве BLE/WiFi: адрес почти наверняка мёртв,
|
||||
* а wireless-adb очки уже погасили сами.
|
||||
*/
|
||||
fun invalidate() {
|
||||
Log.i(TAG, "invalidate(): забываем adb-соединение")
|
||||
address = null
|
||||
_state.value = AdbConnectionState.Disconnected
|
||||
}
|
||||
|
||||
/** Полный подъём: wireless-adb через Mercury → адрес (Mercury/mDNS) → `adb connect`. */
|
||||
private suspend fun bringUp(): String? {
|
||||
_state.value = AdbConnectionState.BringingUp
|
||||
adb.installKey()
|
||||
adb.startServer(timeoutMs = 15_000)
|
||||
|
||||
// Mercury почти никогда не отдаёт адрес (flow не срабатывает), а mDNS находит за ~20мс —
|
||||
// поэтому ждём Mercury недолго, чтобы UI не висел «включаем ADB over WiFi».
|
||||
val found = mercury.enableWirelessAdbAndAwaitAddress(timeoutMs = MERCURY_ADDRESS_WAIT_MS)
|
||||
?: discoverWirelessAdb(timeoutMs = MDNS_TIMEOUT_MS)
|
||||
if (found == null) {
|
||||
fail("не удалось получить адрес ADB over WiFi на очках")
|
||||
return null
|
||||
}
|
||||
Log.i(TAG, "адрес wireless-adb: $found")
|
||||
|
||||
if (!adb.isConnected(found, timeoutMs = PROBE_TIMEOUT_MS)) {
|
||||
// Возможен «already connected» на протухшую сессию (adb-сервер переживает
|
||||
// перезапуск процесса) — рвём соединение и подключаемся заново.
|
||||
adb.disconnect(found, timeoutMs = 5_000)
|
||||
val connect = adb.connect(found, timeoutMs = 12_000)
|
||||
Log.i(TAG, "adb connect $found -> ${connect.output}")
|
||||
}
|
||||
if (!adb.isConnected(found, timeoutMs = PROBE_TIMEOUT_MS)) {
|
||||
fail("adb connect $found не удался")
|
||||
return null
|
||||
}
|
||||
_state.value = AdbConnectionState.Connected(found)
|
||||
Log.i(TAG, "adb подключён: $found")
|
||||
return found
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback: если Mercury не отдал `ip:port`, ищем сервис `_adb-tls-connect._tcp`,
|
||||
* который adbd очков публикует в mDNS при включённом wireless debugging.
|
||||
*/
|
||||
private suspend fun discoverWirelessAdb(timeoutMs: Long): String? =
|
||||
withTimeoutOrNull(timeoutMs) {
|
||||
suspendCancellableCoroutine<String?> { cont ->
|
||||
val nsd = appContext.getSystemService(Context.NSD_SERVICE) as? NsdManager ?: run {
|
||||
cont.resume(null); return@suspendCancellableCoroutine
|
||||
}
|
||||
val discovery = object : NsdManager.DiscoveryListener {
|
||||
override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) {
|
||||
cont.resume(null)
|
||||
}
|
||||
|
||||
override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) = Unit
|
||||
|
||||
override fun onDiscoveryStarted(serviceType: String?) = Unit
|
||||
override fun onDiscoveryStopped(serviceType: String?) = Unit
|
||||
override fun onServiceLost(serviceInfo: NsdServiceInfo?) = Unit
|
||||
|
||||
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
|
||||
nsd.resolveService(serviceInfo, object : NsdManager.ResolveListener {
|
||||
override fun onResolveFailed(info: NsdServiceInfo?, errorCode: Int) = Unit
|
||||
override fun onServiceResolved(info: NsdServiceInfo) {
|
||||
val host = info.host?.hostAddress ?: return
|
||||
if (cont.isActive) cont.resume("$host:${info.port}")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
nsd.discoverServices("_adb-tls-connect._tcp", NsdManager.PROTOCOL_DNS_SD, discovery)
|
||||
cont.invokeOnCancellation {
|
||||
runCatching { nsd.stopServiceDiscovery(discovery) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fail(message: String) {
|
||||
Log.w(TAG, "adb: $message")
|
||||
_state.value = AdbConnectionState.Failed(message)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TAG = "AdbController"
|
||||
|
||||
/** Быстрая проверка «жив ли serial» — короткий таймаут, чтобы не ждать мёртвое соединение. */
|
||||
const val PROBE_TIMEOUT_MS = 4_000L
|
||||
|
||||
/** Сколько ждём адрес от Mercury, прежде чем искать по mDNS. */
|
||||
const val MERCURY_ADDRESS_WAIT_MS = 5_000L
|
||||
|
||||
const val MDNS_TIMEOUT_MS = 8_000L
|
||||
|
||||
const val DEFAULT_SHELL_TIMEOUT_MS = 15_000L
|
||||
const val DEFAULT_PUSH_TIMEOUT_MS = 60_000L
|
||||
}
|
||||
}
|
||||
|
||||
/** Состояние adb-соединения с очками (для UI). */
|
||||
sealed interface AdbConnectionState {
|
||||
data object Disconnected : AdbConnectionState
|
||||
data object BringingUp : AdbConnectionState
|
||||
data class Connected(val address: String) : AdbConnectionState
|
||||
data class Failed(val message: String) : AdbConnectionState
|
||||
|
||||
val label: String
|
||||
get() = when (this) {
|
||||
Disconnected -> "нет соединения"
|
||||
BringingUp -> "подключаемся…"
|
||||
is Connected -> address
|
||||
is Failed -> "ошибка: $message"
|
||||
}
|
||||
}
|
||||
+32
-135
@@ -1,8 +1,6 @@
|
||||
package pw.binom.rayneovm.phone.bootstrap
|
||||
|
||||
import android.content.Context
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -11,19 +9,15 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import pw.binom.rayneovm.phone.adb.AdbClient
|
||||
import pw.binom.rayneovm.phone.adb.AdbConnectionState
|
||||
import pw.binom.rayneovm.phone.adb.AdbController
|
||||
import pw.binom.rayneovm.phone.mercury.MercuryController
|
||||
import pw.binom.rayneovm.phone.protocol.ConnectionState
|
||||
import pw.binom.rayneovm.phone.protocol.VmPhoneController
|
||||
import java.io.File
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
/**
|
||||
* Автономный подъём Shizuku на очках с телефона.
|
||||
@@ -33,15 +27,17 @@ import kotlin.coroutines.resume
|
||||
* adb по wireless-debugging (TLS, ключ уже авторизован) → заливаем starter →
|
||||
* запускаем `shizuku_server` → (опционально) просим Mercury запустить наше
|
||||
* приложение. Всё без ПК и без ручных кнопок.
|
||||
*
|
||||
* Сам adb (подключение и команды) живёт в [AdbController] — здесь только домен Shizuku.
|
||||
*/
|
||||
class ShizukuProvisioner(
|
||||
context: Context,
|
||||
private val mercury: MercuryController,
|
||||
private val controller: VmPhoneController,
|
||||
private val adb: AdbController,
|
||||
) {
|
||||
|
||||
private val appContext = context.applicationContext
|
||||
private val adb = AdbClient(appContext)
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val mutex = Mutex()
|
||||
|
||||
@@ -59,10 +55,6 @@ class ShizukuProvisioner(
|
||||
@Volatile
|
||||
private var provisionedInSession = false
|
||||
|
||||
/** Последний живой adb-адрес очков (`ip:port`) — переиспользуем, не поднимая всё заново. */
|
||||
@Volatile
|
||||
private var lastAddress: String? = null
|
||||
|
||||
/**
|
||||
* Проверить Shizuku и, если не запущен, поднять его. Безопасно вызывать часто:
|
||||
* дешёвая проверка через WiFi-статус, полный adb-цикл — максимум один раз за BLE-сессию.
|
||||
@@ -103,6 +95,8 @@ class ShizukuProvisioner(
|
||||
/** BLE-сессия разорвана: при следующем подключении проверяем Shizuku заново. */
|
||||
fun onBleDisconnected() {
|
||||
provisionedInSession = false
|
||||
// wireless-adb очки гасят сами при потере сети — соединение считать мёртвым.
|
||||
adb.invalidate()
|
||||
}
|
||||
|
||||
/** Принудительно прогнать весь сценарий (диагностика/кнопка). */
|
||||
@@ -152,39 +146,17 @@ class ShizukuProvisioner(
|
||||
return try {
|
||||
_state.value = ProvisionState.Checking
|
||||
|
||||
var address = addressOverride
|
||||
if (address == null) {
|
||||
_state.value = ProvisionState.EnablingAdb
|
||||
// Mercury почти не отдаёт адрес (null), а mDNS находит за ~20мс — не ждём
|
||||
// полный таймаут, иначе «включаем ADB over WiFi…» висит полминуты.
|
||||
address = mercury.enableWirelessAdbAndAwaitAddress(timeoutMs = 5_000)
|
||||
Log.i(TAG, "адрес от Mercury: $address")
|
||||
if (address == null) {
|
||||
Log.i(TAG, "Mercury не прислал адрес — ищу wireless-adb по mDNS")
|
||||
address = discoverWirelessAdb()
|
||||
Log.i(TAG, "адрес от mDNS: $address")
|
||||
}
|
||||
_state.value = ProvisionState.EnablingAdb
|
||||
val serial = if (addressOverride != null) {
|
||||
adb.ensureConnectedTo(addressOverride)
|
||||
} else {
|
||||
Log.i(TAG, "адрес задан вручную: $address")
|
||||
}
|
||||
if (address == null) return "не удалось получить адрес ADB over WiFi на очках"
|
||||
adb.ensureConnected()
|
||||
} ?: return (adb.state.value as? AdbConnectionState.Failed)?.message
|
||||
?: "нет adb-соединения с очками"
|
||||
|
||||
_state.value = ProvisionState.Connecting(address)
|
||||
adb.installKey()
|
||||
adb.startServer()
|
||||
if (!adb.isConnected(address)) {
|
||||
// Возможен «already connected» на протухшую сессию (adb-сервер переживает
|
||||
// перезапуск процесса) — рвём соединение и подключаемся заново.
|
||||
adb.disconnect(address)
|
||||
val connectResult = adb.connect(address)
|
||||
if (!adb.isConnected(address)) {
|
||||
return "adb connect $address не удался: ${connectResult.output}"
|
||||
}
|
||||
}
|
||||
Log.i(TAG, "adb подключён к $address")
|
||||
lastAddress = address
|
||||
_state.value = ProvisionState.Connecting(serial)
|
||||
|
||||
if (isShizukuRunning(address)) {
|
||||
if (isShizukuRunning()) {
|
||||
Log.i(TAG, "Shizuku уже запущен на очках")
|
||||
_state.value = ProvisionState.Running
|
||||
launchGlassesAppIfNeeded()
|
||||
@@ -194,29 +166,29 @@ class ShizukuProvisioner(
|
||||
_state.value = ProvisionState.StartingShizuku
|
||||
val starter = copyStarterToCache()
|
||||
Log.i(TAG, "push starter ${starter.length()}b -> $REMOTE_STARTER")
|
||||
val push = adb.push(address, starter, REMOTE_STARTER)
|
||||
val push = adb.push(starter, REMOTE_STARTER)
|
||||
// push может «провалиться» из-за уже существующего файла — проверяем наличие.
|
||||
if (!push.ok && adb.shell(address, "test -f $REMOTE_STARTER").exitCode != 0) {
|
||||
return "push starter не удался: ${push.output}"
|
||||
if (push.isFailure && adb.shell("test -f $REMOTE_STARTER").isFailure) {
|
||||
return "push starter не удался: ${push.exceptionOrNull()?.message}"
|
||||
}
|
||||
adb.shell(address, "chmod 755 $REMOTE_STARTER")
|
||||
val apkPath = shizukuApkPath(address)
|
||||
adb.shell("chmod 755 $REMOTE_STARTER")
|
||||
val apkPath = shizukuApkPath()
|
||||
Log.i(TAG, "shizuku apk: $apkPath")
|
||||
val args = buildString {
|
||||
append(REMOTE_STARTER)
|
||||
if (apkPath != null) append(" --apk=").append(apkPath)
|
||||
}
|
||||
val run = adb.shell(address, args, timeoutMs = 90_000)
|
||||
Log.i(TAG, "starter exit=${run.exitCode}: ${run.output}")
|
||||
val run = adb.shell(args, timeoutMs = 90_000)
|
||||
Log.i(TAG, "starter -> ${run.getOrNull() ?: "ошибка: ${run.exceptionOrNull()?.message}"}")
|
||||
|
||||
delay(START_VERIFY_DELAY_MS)
|
||||
if (isShizukuRunning(address)) {
|
||||
if (isShizukuRunning()) {
|
||||
Log.i(TAG, "Shizuku поднят")
|
||||
_state.value = ProvisionState.Running
|
||||
launchGlassesAppIfNeeded()
|
||||
null
|
||||
} else {
|
||||
"стартер не поднял shizuku_server: ${run.output.take(300)}"
|
||||
"стартер не поднял shizuku_server: ${run.exceptionOrNull()?.message?.take(300).orEmpty()}"
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Log.w(TAG, "tryProvision(): исключение", t)
|
||||
@@ -224,55 +196,18 @@ class ShizukuProvisioner(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполнить shell-команду на очках по wireless-adb (TLS). При необходимости сначала
|
||||
* поднимает «ADB over WiFi» через Mercury и подключается. Используется кнопками UI
|
||||
* (гашение/пробуждение экрана и т.п.).
|
||||
*/
|
||||
suspend fun runShell(command: String): Result<String> = mutex.withLock {
|
||||
if (!adb.available) {
|
||||
return@withLock Result.failure(IllegalStateException("adb-бинарник или ключ не встроены"))
|
||||
}
|
||||
val address = ensureAdbConnection()
|
||||
?: return@withLock Result.failure(IllegalStateException("не удалось подключиться к очкам по adb"))
|
||||
val result = adb.shell(address, command, timeoutMs = 20_000)
|
||||
Log.i(TAG, "runShell($address): $command -> exit=${result.exitCode} ${result.output.take(200)}")
|
||||
if (result.ok) {
|
||||
Result.success(result.output)
|
||||
} else {
|
||||
Result.failure(IllegalStateException(result.output.ifBlank { "exit=${result.exitCode}" }))
|
||||
}
|
||||
}
|
||||
|
||||
/** Живое adb-соединение, поднимая wireless-adb при необходимости. null — не вышло. */
|
||||
private suspend fun ensureAdbConnection(): String? {
|
||||
lastAddress?.let { if (adb.isConnected(it)) return it }
|
||||
adb.installKey()
|
||||
adb.startServer()
|
||||
val address = mercury.enableWirelessAdbAndAwaitAddress(timeoutMs = 5_000)
|
||||
?: discoverWirelessAdb()
|
||||
?: return null
|
||||
if (!adb.isConnected(address)) {
|
||||
adb.disconnect(address)
|
||||
adb.connect(address)
|
||||
}
|
||||
if (!adb.isConnected(address)) return null
|
||||
lastAddress = address
|
||||
return address
|
||||
}
|
||||
|
||||
private fun isShizukuRunning(serial: String): Boolean {
|
||||
val pidof = adb.shell(serial, "pidof shizuku_server", timeoutMs = 15_000).output
|
||||
private suspend fun isShizukuRunning(): Boolean {
|
||||
val pidof = adb.shell("pidof shizuku_server", timeoutMs = 15_000).getOrDefault("")
|
||||
Log.i(TAG, "pidof shizuku_server -> '$pidof'")
|
||||
return pidof.isNotBlank()
|
||||
}
|
||||
|
||||
private fun shizukuApkPath(serial: String): String? =
|
||||
adb.shell(serial, "pm path $SHIZUKU_PACKAGE", timeoutMs = 15_000)
|
||||
.output
|
||||
.lineSequence()
|
||||
.map { it.removePrefix("package:").trim() }
|
||||
.firstOrNull { it.endsWith(".apk") }
|
||||
private suspend fun shizukuApkPath(): String? =
|
||||
adb.shell("pm path $SHIZUKU_PACKAGE", timeoutMs = 15_000)
|
||||
.getOrNull()
|
||||
?.lineSequence()
|
||||
?.map { it.removePrefix("package:").trim() }
|
||||
?.firstOrNull { it.endsWith(".apk") }
|
||||
|
||||
private fun copyStarterToCache(): File {
|
||||
val out = File(appContext.cacheDir, "shizuku_starter")
|
||||
@@ -307,44 +242,6 @@ class ShizukuProvisioner(
|
||||
launchGlassesApp()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback: если Mercury не отдал `ip:port`, ищем сервис `_adb-tls-connect._tcp`,
|
||||
* который adbd очков публикует в mDNS при включённом wireless debugging.
|
||||
*/
|
||||
private suspend fun discoverWirelessAdb(timeoutMs: Long = 15_000): String? =
|
||||
withTimeoutOrNull(timeoutMs) {
|
||||
suspendCancellableCoroutine<String?> { cont ->
|
||||
val nsd = appContext.getSystemService(Context.NSD_SERVICE) as? NsdManager ?: run {
|
||||
cont.resume(null); return@suspendCancellableCoroutine
|
||||
}
|
||||
val discovery = object : NsdManager.DiscoveryListener {
|
||||
override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) {
|
||||
cont.resume(null)
|
||||
}
|
||||
|
||||
override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) = Unit
|
||||
|
||||
override fun onDiscoveryStarted(serviceType: String?) = Unit
|
||||
override fun onDiscoveryStopped(serviceType: String?) = Unit
|
||||
override fun onServiceLost(serviceInfo: NsdServiceInfo?) = Unit
|
||||
|
||||
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
|
||||
nsd.resolveService(serviceInfo, object : NsdManager.ResolveListener {
|
||||
override fun onResolveFailed(info: NsdServiceInfo?, errorCode: Int) = Unit
|
||||
override fun onServiceResolved(info: NsdServiceInfo) {
|
||||
val host = info.host?.hostAddress ?: return
|
||||
if (cont.isActive) cont.resume("$host:${info.port}")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
nsd.discoverServices("_adb-tls-connect._tcp", NsdManager.PROTOCOL_DNS_SD, discovery)
|
||||
cont.invokeOnCancellation {
|
||||
runCatching { nsd.stopServiceDiscovery(discovery) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fail(message: String) {
|
||||
Log.w(TAG, "Shizuku bootstrap: $message")
|
||||
_state.value = ProvisionState.Failed(message)
|
||||
|
||||
@@ -4,27 +4,83 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import pw.binom.rayneovm.phone.AdbControllerProvider
|
||||
import pw.binom.rayneovm.phone.ShizukuProvisionerProvider
|
||||
import pw.binom.rayneovm.phone.mercury.MercuryControllerProvider
|
||||
|
||||
/**
|
||||
* Диагностический приёмник: `adb shell am broadcast -a pw.binom.rayneovm.phone.DEBUG_PROVISION`
|
||||
* принудительно запускает автономный подъём Shizuku (кнопка без UI).
|
||||
* Диагностический приёмник (shell/system, `android.permission.DUMP`):
|
||||
*
|
||||
* Защищён `android.permission.DUMP` — им обладает shell/system, обычные приложения нет.
|
||||
* - `-a pw.binom.rayneovm.phone.DEBUG_PROVISION` — принудительный подъём Shizuku
|
||||
* (`--es address ip:port` — с заданным адресом, в обход Mercury/mDNS);
|
||||
* - `-a pw.binom.rayneovm.phone.DEBUG_SET_BRIGHTNESS --ei value 42` — выставить яркость
|
||||
* очков через Mercury BLE (проверка слайдера без тыканья в UI);
|
||||
* - `-a pw.binom.rayneovm.phone.DEBUG_REFRESH_STATUS` — перезапросить статус очков;
|
||||
* - `-a pw.binom.rayneovm.phone.DEBUG_ADB_SHELL --es cmd '...'` — adb-shell команда на очки
|
||||
* через [pw.binom.rayneovm.phone.adb.AdbController] (сам поднимает wireless-adb).
|
||||
*/
|
||||
class DebugReceiver : BroadcastReceiver() {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val app = context.applicationContext as? ShizukuProvisionerProvider ?: return
|
||||
Log.i(TAG, "получен ${intent.action} — запускаю бутстрап Shizuku")
|
||||
val address = intent.getStringExtra("address")
|
||||
if (address.isNullOrBlank()) {
|
||||
app.provideShizukuProvisioner().force()
|
||||
} else {
|
||||
app.provideShizukuProvisioner().forceWithAddress(address)
|
||||
val app = context.applicationContext
|
||||
when (intent.action) {
|
||||
ACTION_SET_BRIGHTNESS -> {
|
||||
val mc = (app as? MercuryControllerProvider)?.provideMercuryController() ?: return
|
||||
val value = intent.getIntExtra("value", -1)
|
||||
Log.i(TAG, "DEBUG_SET_BRIGHTNESS value=$value")
|
||||
mc.setBrightness(value)
|
||||
}
|
||||
|
||||
ACTION_SET_DEEP_SUSPEND -> {
|
||||
val mc = (app as? MercuryControllerProvider)?.provideMercuryController() ?: return
|
||||
val sleep = intent.getIntExtra("on", 0) != 0
|
||||
Log.i(TAG, "DEBUG_SET_DEEP_SUSPEND on=$sleep")
|
||||
mc.setDeepSuspendAsync(sleep)
|
||||
}
|
||||
|
||||
ACTION_REFRESH_STATUS -> {
|
||||
val mc = (app as? MercuryControllerProvider)?.provideMercuryController() ?: return
|
||||
Log.i(TAG, "DEBUG_REFRESH_STATUS")
|
||||
mc.refreshStatus()
|
||||
}
|
||||
|
||||
ACTION_ADB_SHELL -> {
|
||||
val adb = (app as? AdbControllerProvider)?.provideAdbController() ?: return
|
||||
val cmd = intent.getStringExtra("cmd")
|
||||
if (cmd.isNullOrBlank()) {
|
||||
Log.w(TAG, "DEBUG_ADB_SHELL без --es cmd")
|
||||
return
|
||||
}
|
||||
Log.i(TAG, "DEBUG_ADB_SHELL: $cmd")
|
||||
scope.launch {
|
||||
val result = adb.shell(cmd)
|
||||
Log.i(TAG, "DEBUG_ADB_SHELL результат: ${result.getOrNull() ?: result.exceptionOrNull()}")
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
val provider = app as? ShizukuProvisionerProvider ?: return
|
||||
Log.i(TAG, "получен ${intent.action} — запускаю бутстрап Shizuku")
|
||||
val address = intent.getStringExtra("address")
|
||||
if (address.isNullOrBlank()) {
|
||||
provider.provideShizukuProvisioner().force()
|
||||
} else {
|
||||
provider.provideShizukuProvisioner().forceWithAddress(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TAG = "DebugReceiver"
|
||||
const val ACTION_SET_BRIGHTNESS = "pw.binom.rayneovm.phone.DEBUG_SET_BRIGHTNESS"
|
||||
const val ACTION_SET_DEEP_SUSPEND = "pw.binom.rayneovm.phone.DEBUG_SET_DEEP_SUSPEND"
|
||||
const val ACTION_REFRESH_STATUS = "pw.binom.rayneovm.phone.DEBUG_REFRESH_STATUS"
|
||||
const val ACTION_ADB_SHELL = "pw.binom.rayneovm.phone.DEBUG_ADB_SHELL"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,6 +336,26 @@ class MercuryController(private val context: Context) {
|
||||
mercury.setAutoBrightness(enable)
|
||||
}
|
||||
|
||||
/**
|
||||
* Погасить ([enable] = true) / разбудить экран очков по BLE (deepSuspend).
|
||||
*
|
||||
* Suspend-вариант с реальным результатом — кнопка в UI показывает ошибку, если
|
||||
* команда не ушла (раньше «не работало» из-за баги hasStatus, см. library).
|
||||
*/
|
||||
suspend fun setDeepSuspend(enable: Boolean): Boolean = try {
|
||||
mercury.setDeepSuspend(enable).also {
|
||||
android.util.Log.i(TAG, "setDeepSuspend($enable) -> $it")
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
android.util.Log.w(TAG, "setDeepSuspend($enable) failed: ${t.message}")
|
||||
false
|
||||
}
|
||||
|
||||
/** Fire-and-forget вариант [setDeepSuspend] (диагностика/DebugReceiver). */
|
||||
fun setDeepSuspendAsync(enable: Boolean) = launchMercury("setDeepSuspend($enable)") {
|
||||
mercury.setDeepSuspend(enable)
|
||||
}
|
||||
|
||||
/** Включить/выключить WiFi-радио очков. */
|
||||
fun setWifiEnabled(enable: Boolean) = launchMercury("setWifiEnabled($enable)") {
|
||||
mercury.wifiToggle(enable)
|
||||
@@ -415,18 +435,23 @@ class MercuryController(private val context: Context) {
|
||||
mercury.wifiRemove(ssid)
|
||||
}
|
||||
|
||||
private fun launchMercury(action: String, block: suspend () -> Unit) {
|
||||
private fun launchMercury(action: String, block: suspend () -> Any?) {
|
||||
scope.launch {
|
||||
try {
|
||||
val result = try {
|
||||
block()
|
||||
} catch (t: Throwable) {
|
||||
android.util.Log.w(TAG, "$action failed: ${t.message}")
|
||||
"исключение"
|
||||
}
|
||||
// Логируем результат: библиотека молча возвращает false, если статус ещё не
|
||||
// получен (тогда команда никуда не уходит — «слайдер не работает»).
|
||||
android.util.Log.i(TAG, "$action -> $result")
|
||||
}
|
||||
}
|
||||
|
||||
/** Сбрасывает данные, которые живут только внутри BLE-сессии. */
|
||||
private fun clearGlassesData() {
|
||||
android.util.Log.i(TAG, "clearGlassesData(): данные BLE-сессии сброшены (статус=null)")
|
||||
statusRefreshJob?.cancel()
|
||||
statusRefreshJob = null
|
||||
_generalStatus.value = null
|
||||
@@ -477,6 +502,12 @@ class MercuryController(private val context: Context) {
|
||||
}
|
||||
}
|
||||
is RayNeoMercury.Event.GeneralStatusUpdate -> {
|
||||
android.util.Log.i(
|
||||
TAG,
|
||||
"статус очков: brightness=${event.status.brightness} " +
|
||||
"autoBright=${event.status.automaticBrightness} " +
|
||||
"wifi=${event.status.wifiEnabled} battery=${event.status.battery}",
|
||||
)
|
||||
_generalStatus.value = event.status
|
||||
val cur = _state.value
|
||||
if (cur is MercuryState.Connected) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import pw.binom.rayneovm.shared.GlassesStatus
|
||||
import pw.binom.rayneovm.shared.KeyEvent as VmKeyEvent
|
||||
import pw.binom.rayneovm.shared.Pong
|
||||
import pw.binom.rayneovm.shared.RequestShizukuStatus
|
||||
import pw.binom.rayneovm.shared.ScreenControl
|
||||
import pw.binom.rayneovm.shared.SetAudioMode
|
||||
import pw.binom.rayneovm.shared.ShizukuStatus
|
||||
import pw.binom.rayneovm.shared.VmMessage
|
||||
@@ -195,6 +196,17 @@ class VmPhoneController(
|
||||
/** Скролл в текущей позиции курсора: dx/dy — смещение пальцев в пикселях гостя. */
|
||||
suspend fun scrollCursor(dx: Float, dy: Float) = send(CursorScroll(dx, dy))
|
||||
|
||||
/**
|
||||
* Погасить ([sleep] = true) / разбудить экран очков их же Shizuku-сервисом (Канал 1).
|
||||
*
|
||||
* @return false, если канал не подключён — вызывающий покажет ошибку, а не «0 реакции»
|
||||
*/
|
||||
suspend fun setScreen(sleep: Boolean): Boolean {
|
||||
if (_state.value !is ConnectionState.Connected) return false
|
||||
send(ScreenControl(sleep))
|
||||
return true
|
||||
}
|
||||
|
||||
/** Клавиша/медиа-команда гостю (Back, Play, Pause, ...): DOWN + UP. */
|
||||
suspend fun sendKey(keyCode: Int) {
|
||||
send(VmKeyEvent(keyCode, VmKeyEvent.ACTION_DOWN))
|
||||
|
||||
@@ -140,7 +140,8 @@ fun GlassesStatusScreen(
|
||||
)
|
||||
|
||||
ScreenPowerSection(
|
||||
onShell = { command -> provisioner.runShell(command) },
|
||||
enabled = connection is ConnectionState.Connected,
|
||||
onSetScreen = { sleep -> controller.setScreen(sleep) },
|
||||
)
|
||||
|
||||
GlassesWifiSection(
|
||||
@@ -331,42 +332,44 @@ private fun ScreenAndBrightnessSection(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Гашение/пробуждение экрана очков через adb (`input keyevent 223/224`).
|
||||
* Работает по wireless-debugging: если очки ушли в глубокий сон и WiFi/adb отвалились —
|
||||
* команда не дойдёт (Mercury setDeepSuspend мы выпилили, т.к. он не работал).
|
||||
*/
|
||||
@Composable
|
||||
private fun ScreenPowerSection(
|
||||
onShell: suspend (String) -> Result<String>,
|
||||
enabled: Boolean,
|
||||
onSetScreen: suspend (Boolean) -> Boolean,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var busy by remember { mutableStateOf(false) }
|
||||
var message by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
fun send(command: String, okText: String) {
|
||||
if (busy) return
|
||||
fun send(sleep: Boolean, okText: String) {
|
||||
if (busy || !enabled) return
|
||||
busy = true
|
||||
message = null
|
||||
scope.launch {
|
||||
val r = onShell(command)
|
||||
message = if (r.isSuccess) okText else "Ошибка: ${r.exceptionOrNull()?.message}"
|
||||
busy = false
|
||||
try {
|
||||
val ok = onSetScreen(sleep)
|
||||
message = if (ok) okText else "Нет Канала 1 — команда не ушла"
|
||||
} catch (t: Throwable) {
|
||||
message = "Ошибка: ${t.message}"
|
||||
} finally {
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionCard(title = "Экран очков") {
|
||||
Hint("Через adb (wireless-debugging). Если очки ушли в глубокий сон и WiFi отвалился — команда не дойдёт.")
|
||||
Hint("Очки гасят/будят себя сами своим Shizuku-сервисом (Канал 1). Нужен Shizuku на очках.")
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Button(
|
||||
enabled = !busy,
|
||||
onClick = { send("input keyevent 223", "Экран погашен") },
|
||||
enabled = !busy && enabled,
|
||||
onClick = { send(true, "Экран погашен") },
|
||||
) { Text("Погасить") }
|
||||
OutlinedButton(
|
||||
enabled = !busy,
|
||||
onClick = { send("input keyevent 224", "Экран разбужен") },
|
||||
enabled = !busy && enabled,
|
||||
onClick = { send(false, "Экран разбужен") },
|
||||
) { Text("Разбудить") }
|
||||
}
|
||||
if (!enabled) Hint("Нужен Канал 1 (WebSocket) — приложение на очках не подключено.")
|
||||
message?.let { Hint(it) }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user