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:
@@ -0,0 +1,4 @@
|
|||||||
|
1. [ ] Стартовый экран (тот когда не запущино ничего на вирутальных мониторах) отображается криво, не бинокулдярно. Он тоже должен быть бинокулярным.
|
||||||
|
2. [ ] Надо добавить возможность проброса звука с виртуального дисплея на телефон. Должен быть переключатель через что выводить звук: через телефон или с очков. то есть надо сделать саму механику проброса, а потом кнопку переключатиль в мобильном приложении
|
||||||
|
3. [ ] Когда приложение на виртуальном дисплее запущено отображается положение мышки (жолтая точка). Когда она долго не двигается надо её скрывать на очках
|
||||||
|
4. [ ] Доработать функционал клавиатуры. 1. когда на самих очках поле ввода в фокусе НЕ нужно отображать клавиатуру на очках. 2. когда на телефоне тыкаю на кнопку клавиатуры нужно отобразить клавиатуру, и чтобы все что там вводится вводилось на очках
|
||||||
@@ -36,6 +36,7 @@ import pw.binom.rayneovm.shared.CursorScroll
|
|||||||
import pw.binom.rayneovm.shared.CursorClick
|
import pw.binom.rayneovm.shared.CursorClick
|
||||||
import pw.binom.rayneovm.shared.CursorMove
|
import pw.binom.rayneovm.shared.CursorMove
|
||||||
import pw.binom.rayneovm.shared.RequestShizukuStatus
|
import pw.binom.rayneovm.shared.RequestShizukuStatus
|
||||||
|
import pw.binom.rayneovm.shared.ScreenControl
|
||||||
import pw.binom.rayneovm.shared.ShizukuStatus
|
import pw.binom.rayneovm.shared.ShizukuStatus
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -167,6 +168,10 @@ class VmService : LifecycleService() {
|
|||||||
controller.sendKey(msg.keyCode, msg.action)
|
controller.sendKey(msg.keyCode, msg.action)
|
||||||
}
|
}
|
||||||
is SetAudioMode -> Log.d(TAG, "SetAudioMode: ${msg.mode}")
|
is SetAudioMode -> Log.d(TAG, "SetAudioMode: ${msg.mode}")
|
||||||
|
is ScreenControl -> {
|
||||||
|
Log.i(TAG, "ScreenControl: sleep=${msg.sleep}")
|
||||||
|
controller.setScreen(msg.sleep)
|
||||||
|
}
|
||||||
is Shutdown -> {
|
is Shutdown -> {
|
||||||
Log.i(TAG, "Shutdown")
|
Log.i(TAG, "Shutdown")
|
||||||
controller.stopGuest()
|
controller.stopGuest()
|
||||||
|
|||||||
+21
@@ -245,6 +245,27 @@ class VmGlassesController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Гасит/будит экран самих очков (дисплей 0) через Shizuku-UserService.
|
||||||
|
*
|
||||||
|
* Идёт через инжект `KEYCODE_SLEEP`/`KEYCODE_WAKEUP` от shell UID, поэтому не зависит
|
||||||
|
* ни от Mercury BLE, ни от wireless-adb. Биндер берём у «стража» (он всегда поднят, пока
|
||||||
|
* живёт приложение), иначе — у активного гостевого дисплея.
|
||||||
|
*/
|
||||||
|
fun setScreen(sleep: Boolean) {
|
||||||
|
val binder = guardBinder ?: activeSource?.shizukuBinder
|
||||||
|
if (binder == null) {
|
||||||
|
Log.w(TAG, "setScreen(sleep=$sleep): нет Shizuku-биндера (Shizuku не поднят?)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
testScope.launch {
|
||||||
|
val ok = runCatching { ShizukuVdBinder.setScreen(binder, !sleep) }
|
||||||
|
.onFailure { Log.w(TAG, "setScreen: binder мёртв", it) }
|
||||||
|
.getOrDefault(false)
|
||||||
|
Log.i(TAG, "setScreen(sleep=$sleep) -> $ok")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Клавиатурное/медиа-событие гостю на его VirtualDisplay (Back, Play, Pause, ...). */
|
/** Клавиатурное/медиа-событие гостю на его VirtualDisplay (Back, Play, Pause, ...). */
|
||||||
fun sendKey(keyCode: Int, action: Int) {
|
fun sendKey(keyCode: Int, action: Int) {
|
||||||
val src = activeSource ?: return
|
val src = activeSource ?: return
|
||||||
|
|||||||
@@ -60,6 +60,10 @@
|
|||||||
android:permission="android.permission.DUMP">
|
android:permission="android.permission.DUMP">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="pw.binom.rayneovm.phone.DEBUG_PROVISION" />
|
<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>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import kotlinx.coroutines.SupervisorJob
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import pw.binom.rayneovm.phone.adb.AdbController
|
||||||
import pw.binom.rayneovm.phone.bootstrap.ShizukuProvisioner
|
import pw.binom.rayneovm.phone.bootstrap.ShizukuProvisioner
|
||||||
import pw.binom.rayneovm.phone.mercury.MercuryController
|
import pw.binom.rayneovm.phone.mercury.MercuryController
|
||||||
import pw.binom.rayneovm.phone.mercury.MercuryControllerProvider
|
import pw.binom.rayneovm.phone.mercury.MercuryControllerProvider
|
||||||
@@ -22,14 +23,16 @@ import pw.binom.rayneovm.phone.protocol.VmPhoneController
|
|||||||
* При старте подготавливает контроллер [VmPhoneController] (auto-reconnect к очкам через mDNS)
|
* При старте подготавливает контроллер [VmPhoneController] (auto-reconnect к очкам через mDNS)
|
||||||
* и [MercuryController] (второй канал — RayNeo Mercury BLE).
|
* и [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 appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
private val controller: VmPhoneController by lazy { VmPhoneController(this) }
|
private val controller: VmPhoneController by lazy { VmPhoneController(this) }
|
||||||
private val mercuryController: MercuryController by lazy { MercuryController(this) }
|
private val mercuryController: MercuryController by lazy { MercuryController(this) }
|
||||||
|
private val adbController: AdbController by lazy { AdbController(this, mercuryController) }
|
||||||
private val provisioner: ShizukuProvisioner by lazy {
|
private val provisioner: ShizukuProvisioner by lazy {
|
||||||
ShizukuProvisioner(this, mercuryController, controller)
|
ShizukuProvisioner(this, mercuryController, controller, adbController)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@@ -90,6 +93,7 @@ class PhoneVmApp : Application(), ControllerProvider, MercuryControllerProvider,
|
|||||||
override fun provideController(): VmPhoneController = controller
|
override fun provideController(): VmPhoneController = controller
|
||||||
override fun provideMercuryController(): MercuryController = mercuryController
|
override fun provideMercuryController(): MercuryController = mercuryController
|
||||||
override fun provideShizukuProvisioner(): ShizukuProvisioner = provisioner
|
override fun provideShizukuProvisioner(): ShizukuProvisioner = provisioner
|
||||||
|
override fun provideAdbController(): AdbController = adbController
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "PhoneVmApp"
|
private const val TAG = "PhoneVmApp"
|
||||||
@@ -101,3 +105,8 @@ class PhoneVmApp : Application(), ControllerProvider, MercuryControllerProvider,
|
|||||||
interface ShizukuProvisionerProvider {
|
interface ShizukuProvisionerProvider {
|
||||||
fun provideShizukuProvisioner(): ShizukuProvisioner
|
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)
|
exec(listOf("-s", serial, "push", local.absolutePath, remote), timeoutMs)
|
||||||
|
|
||||||
/** Устройство реально отвечает по serial. */
|
/** Устройство реально отвечает по serial. */
|
||||||
fun isConnected(serial: String): Boolean =
|
fun isConnected(serial: String, timeoutMs: Long = 10_000): Boolean =
|
||||||
shell(serial, "echo ok", timeoutMs = 10_000).output.contains("ok")
|
shell(serial, "echo ok", timeoutMs = timeoutMs).output.contains("ok")
|
||||||
|
|
||||||
private fun exec(args: List<String>, timeoutMs: Long): Result {
|
private fun exec(args: List<String>, timeoutMs: Long): Result {
|
||||||
val cmd = listOf(adbPath) + args
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
-134
@@ -1,8 +1,6 @@
|
|||||||
package pw.binom.rayneovm.phone.bootstrap
|
package pw.binom.rayneovm.phone.bootstrap
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.nsd.NsdManager
|
|
||||||
import android.net.nsd.NsdServiceInfo
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -11,19 +9,15 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import pw.binom.rayneovm.phone.adb.AdbConnectionState
|
||||||
import pw.binom.rayneovm.phone.adb.AdbClient
|
import pw.binom.rayneovm.phone.adb.AdbController
|
||||||
import pw.binom.rayneovm.phone.mercury.MercuryController
|
import pw.binom.rayneovm.phone.mercury.MercuryController
|
||||||
import pw.binom.rayneovm.phone.protocol.ConnectionState
|
import pw.binom.rayneovm.phone.protocol.ConnectionState
|
||||||
import pw.binom.rayneovm.phone.protocol.VmPhoneController
|
import pw.binom.rayneovm.phone.protocol.VmPhoneController
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.coroutines.resume
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Автономный подъём Shizuku на очках с телефона.
|
* Автономный подъём Shizuku на очках с телефона.
|
||||||
@@ -33,15 +27,17 @@ import kotlin.coroutines.resume
|
|||||||
* adb по wireless-debugging (TLS, ключ уже авторизован) → заливаем starter →
|
* adb по wireless-debugging (TLS, ключ уже авторизован) → заливаем starter →
|
||||||
* запускаем `shizuku_server` → (опционально) просим Mercury запустить наше
|
* запускаем `shizuku_server` → (опционально) просим Mercury запустить наше
|
||||||
* приложение. Всё без ПК и без ручных кнопок.
|
* приложение. Всё без ПК и без ручных кнопок.
|
||||||
|
*
|
||||||
|
* Сам adb (подключение и команды) живёт в [AdbController] — здесь только домен Shizuku.
|
||||||
*/
|
*/
|
||||||
class ShizukuProvisioner(
|
class ShizukuProvisioner(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val mercury: MercuryController,
|
private val mercury: MercuryController,
|
||||||
private val controller: VmPhoneController,
|
private val controller: VmPhoneController,
|
||||||
|
private val adb: AdbController,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val appContext = context.applicationContext
|
private val appContext = context.applicationContext
|
||||||
private val adb = AdbClient(appContext)
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
||||||
@@ -59,10 +55,6 @@ class ShizukuProvisioner(
|
|||||||
@Volatile
|
@Volatile
|
||||||
private var provisionedInSession = false
|
private var provisionedInSession = false
|
||||||
|
|
||||||
/** Последний живой adb-адрес очков (`ip:port`) — переиспользуем, не поднимая всё заново. */
|
|
||||||
@Volatile
|
|
||||||
private var lastAddress: String? = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверить Shizuku и, если не запущен, поднять его. Безопасно вызывать часто:
|
* Проверить Shizuku и, если не запущен, поднять его. Безопасно вызывать часто:
|
||||||
* дешёвая проверка через WiFi-статус, полный adb-цикл — максимум один раз за BLE-сессию.
|
* дешёвая проверка через WiFi-статус, полный adb-цикл — максимум один раз за BLE-сессию.
|
||||||
@@ -103,6 +95,8 @@ class ShizukuProvisioner(
|
|||||||
/** BLE-сессия разорвана: при следующем подключении проверяем Shizuku заново. */
|
/** BLE-сессия разорвана: при следующем подключении проверяем Shizuku заново. */
|
||||||
fun onBleDisconnected() {
|
fun onBleDisconnected() {
|
||||||
provisionedInSession = false
|
provisionedInSession = false
|
||||||
|
// wireless-adb очки гасят сами при потере сети — соединение считать мёртвым.
|
||||||
|
adb.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Принудительно прогнать весь сценарий (диагностика/кнопка). */
|
/** Принудительно прогнать весь сценарий (диагностика/кнопка). */
|
||||||
@@ -152,39 +146,17 @@ class ShizukuProvisioner(
|
|||||||
return try {
|
return try {
|
||||||
_state.value = ProvisionState.Checking
|
_state.value = ProvisionState.Checking
|
||||||
|
|
||||||
var address = addressOverride
|
|
||||||
if (address == null) {
|
|
||||||
_state.value = ProvisionState.EnablingAdb
|
_state.value = ProvisionState.EnablingAdb
|
||||||
// Mercury почти не отдаёт адрес (null), а mDNS находит за ~20мс — не ждём
|
val serial = if (addressOverride != null) {
|
||||||
// полный таймаут, иначе «включаем ADB over WiFi…» висит полминуты.
|
adb.ensureConnectedTo(addressOverride)
|
||||||
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")
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Log.i(TAG, "адрес задан вручную: $address")
|
adb.ensureConnected()
|
||||||
}
|
} ?: return (adb.state.value as? AdbConnectionState.Failed)?.message
|
||||||
if (address == null) return "не удалось получить адрес ADB over WiFi на очках"
|
?: "нет adb-соединения с очками"
|
||||||
|
|
||||||
_state.value = ProvisionState.Connecting(address)
|
_state.value = ProvisionState.Connecting(serial)
|
||||||
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
|
|
||||||
|
|
||||||
if (isShizukuRunning(address)) {
|
if (isShizukuRunning()) {
|
||||||
Log.i(TAG, "Shizuku уже запущен на очках")
|
Log.i(TAG, "Shizuku уже запущен на очках")
|
||||||
_state.value = ProvisionState.Running
|
_state.value = ProvisionState.Running
|
||||||
launchGlassesAppIfNeeded()
|
launchGlassesAppIfNeeded()
|
||||||
@@ -194,29 +166,29 @@ class ShizukuProvisioner(
|
|||||||
_state.value = ProvisionState.StartingShizuku
|
_state.value = ProvisionState.StartingShizuku
|
||||||
val starter = copyStarterToCache()
|
val starter = copyStarterToCache()
|
||||||
Log.i(TAG, "push starter ${starter.length()}b -> $REMOTE_STARTER")
|
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 может «провалиться» из-за уже существующего файла — проверяем наличие.
|
// push может «провалиться» из-за уже существующего файла — проверяем наличие.
|
||||||
if (!push.ok && adb.shell(address, "test -f $REMOTE_STARTER").exitCode != 0) {
|
if (push.isFailure && adb.shell("test -f $REMOTE_STARTER").isFailure) {
|
||||||
return "push starter не удался: ${push.output}"
|
return "push starter не удался: ${push.exceptionOrNull()?.message}"
|
||||||
}
|
}
|
||||||
adb.shell(address, "chmod 755 $REMOTE_STARTER")
|
adb.shell("chmod 755 $REMOTE_STARTER")
|
||||||
val apkPath = shizukuApkPath(address)
|
val apkPath = shizukuApkPath()
|
||||||
Log.i(TAG, "shizuku apk: $apkPath")
|
Log.i(TAG, "shizuku apk: $apkPath")
|
||||||
val args = buildString {
|
val args = buildString {
|
||||||
append(REMOTE_STARTER)
|
append(REMOTE_STARTER)
|
||||||
if (apkPath != null) append(" --apk=").append(apkPath)
|
if (apkPath != null) append(" --apk=").append(apkPath)
|
||||||
}
|
}
|
||||||
val run = adb.shell(address, args, timeoutMs = 90_000)
|
val run = adb.shell(args, timeoutMs = 90_000)
|
||||||
Log.i(TAG, "starter exit=${run.exitCode}: ${run.output}")
|
Log.i(TAG, "starter -> ${run.getOrNull() ?: "ошибка: ${run.exceptionOrNull()?.message}"}")
|
||||||
|
|
||||||
delay(START_VERIFY_DELAY_MS)
|
delay(START_VERIFY_DELAY_MS)
|
||||||
if (isShizukuRunning(address)) {
|
if (isShizukuRunning()) {
|
||||||
Log.i(TAG, "Shizuku поднят")
|
Log.i(TAG, "Shizuku поднят")
|
||||||
_state.value = ProvisionState.Running
|
_state.value = ProvisionState.Running
|
||||||
launchGlassesAppIfNeeded()
|
launchGlassesAppIfNeeded()
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
"стартер не поднял shizuku_server: ${run.output.take(300)}"
|
"стартер не поднял shizuku_server: ${run.exceptionOrNull()?.message?.take(300).orEmpty()}"
|
||||||
}
|
}
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.w(TAG, "tryProvision(): исключение", t)
|
Log.w(TAG, "tryProvision(): исключение", t)
|
||||||
@@ -224,55 +196,18 @@ class ShizukuProvisioner(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private suspend fun isShizukuRunning(): Boolean {
|
||||||
* Выполнить shell-команду на очках по wireless-adb (TLS). При необходимости сначала
|
val pidof = adb.shell("pidof shizuku_server", timeoutMs = 15_000).getOrDefault("")
|
||||||
* поднимает «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
|
|
||||||
Log.i(TAG, "pidof shizuku_server -> '$pidof'")
|
Log.i(TAG, "pidof shizuku_server -> '$pidof'")
|
||||||
return pidof.isNotBlank()
|
return pidof.isNotBlank()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shizukuApkPath(serial: String): String? =
|
private suspend fun shizukuApkPath(): String? =
|
||||||
adb.shell(serial, "pm path $SHIZUKU_PACKAGE", timeoutMs = 15_000)
|
adb.shell("pm path $SHIZUKU_PACKAGE", timeoutMs = 15_000)
|
||||||
.output
|
.getOrNull()
|
||||||
.lineSequence()
|
?.lineSequence()
|
||||||
.map { it.removePrefix("package:").trim() }
|
?.map { it.removePrefix("package:").trim() }
|
||||||
.firstOrNull { it.endsWith(".apk") }
|
?.firstOrNull { it.endsWith(".apk") }
|
||||||
|
|
||||||
private fun copyStarterToCache(): File {
|
private fun copyStarterToCache(): File {
|
||||||
val out = File(appContext.cacheDir, "shizuku_starter")
|
val out = File(appContext.cacheDir, "shizuku_starter")
|
||||||
@@ -307,44 +242,6 @@ class ShizukuProvisioner(
|
|||||||
launchGlassesApp()
|
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) {
|
private fun fail(message: String) {
|
||||||
Log.w(TAG, "Shizuku bootstrap: $message")
|
Log.w(TAG, "Shizuku bootstrap: $message")
|
||||||
_state.value = ProvisionState.Failed(message)
|
_state.value = ProvisionState.Failed(message)
|
||||||
|
|||||||
@@ -4,27 +4,83 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.util.Log
|
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.ShizukuProvisionerProvider
|
||||||
|
import pw.binom.rayneovm.phone.mercury.MercuryControllerProvider
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Диагностический приёмник: `adb shell am broadcast -a pw.binom.rayneovm.phone.DEBUG_PROVISION`
|
* Диагностический приёмник (shell/system, `android.permission.DUMP`):
|
||||||
* принудительно запускает автономный подъём Shizuku (кнопка без UI).
|
|
||||||
*
|
*
|
||||||
* Защищён `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() {
|
class DebugReceiver : BroadcastReceiver() {
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
val app = context.applicationContext as? ShizukuProvisionerProvider ?: return
|
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")
|
Log.i(TAG, "получен ${intent.action} — запускаю бутстрап Shizuku")
|
||||||
val address = intent.getStringExtra("address")
|
val address = intent.getStringExtra("address")
|
||||||
if (address.isNullOrBlank()) {
|
if (address.isNullOrBlank()) {
|
||||||
app.provideShizukuProvisioner().force()
|
provider.provideShizukuProvisioner().force()
|
||||||
} else {
|
} else {
|
||||||
app.provideShizukuProvisioner().forceWithAddress(address)
|
provider.provideShizukuProvisioner().forceWithAddress(address)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val TAG = "DebugReceiver"
|
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)
|
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-радио очков. */
|
/** Включить/выключить WiFi-радио очков. */
|
||||||
fun setWifiEnabled(enable: Boolean) = launchMercury("setWifiEnabled($enable)") {
|
fun setWifiEnabled(enable: Boolean) = launchMercury("setWifiEnabled($enable)") {
|
||||||
mercury.wifiToggle(enable)
|
mercury.wifiToggle(enable)
|
||||||
@@ -415,18 +435,23 @@ class MercuryController(private val context: Context) {
|
|||||||
mercury.wifiRemove(ssid)
|
mercury.wifiRemove(ssid)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchMercury(action: String, block: suspend () -> Unit) {
|
private fun launchMercury(action: String, block: suspend () -> Any?) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
val result = try {
|
||||||
block()
|
block()
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
android.util.Log.w(TAG, "$action failed: ${t.message}")
|
android.util.Log.w(TAG, "$action failed: ${t.message}")
|
||||||
|
"исключение"
|
||||||
}
|
}
|
||||||
|
// Логируем результат: библиотека молча возвращает false, если статус ещё не
|
||||||
|
// получен (тогда команда никуда не уходит — «слайдер не работает»).
|
||||||
|
android.util.Log.i(TAG, "$action -> $result")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Сбрасывает данные, которые живут только внутри BLE-сессии. */
|
/** Сбрасывает данные, которые живут только внутри BLE-сессии. */
|
||||||
private fun clearGlassesData() {
|
private fun clearGlassesData() {
|
||||||
|
android.util.Log.i(TAG, "clearGlassesData(): данные BLE-сессии сброшены (статус=null)")
|
||||||
statusRefreshJob?.cancel()
|
statusRefreshJob?.cancel()
|
||||||
statusRefreshJob = null
|
statusRefreshJob = null
|
||||||
_generalStatus.value = null
|
_generalStatus.value = null
|
||||||
@@ -477,6 +502,12 @@ class MercuryController(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
is RayNeoMercury.Event.GeneralStatusUpdate -> {
|
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
|
_generalStatus.value = event.status
|
||||||
val cur = _state.value
|
val cur = _state.value
|
||||||
if (cur is MercuryState.Connected) {
|
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.KeyEvent as VmKeyEvent
|
||||||
import pw.binom.rayneovm.shared.Pong
|
import pw.binom.rayneovm.shared.Pong
|
||||||
import pw.binom.rayneovm.shared.RequestShizukuStatus
|
import pw.binom.rayneovm.shared.RequestShizukuStatus
|
||||||
|
import pw.binom.rayneovm.shared.ScreenControl
|
||||||
import pw.binom.rayneovm.shared.SetAudioMode
|
import pw.binom.rayneovm.shared.SetAudioMode
|
||||||
import pw.binom.rayneovm.shared.ShizukuStatus
|
import pw.binom.rayneovm.shared.ShizukuStatus
|
||||||
import pw.binom.rayneovm.shared.VmMessage
|
import pw.binom.rayneovm.shared.VmMessage
|
||||||
@@ -195,6 +196,17 @@ class VmPhoneController(
|
|||||||
/** Скролл в текущей позиции курсора: dx/dy — смещение пальцев в пикселях гостя. */
|
/** Скролл в текущей позиции курсора: dx/dy — смещение пальцев в пикселях гостя. */
|
||||||
suspend fun scrollCursor(dx: Float, dy: Float) = send(CursorScroll(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. */
|
/** Клавиша/медиа-команда гостю (Back, Play, Pause, ...): DOWN + UP. */
|
||||||
suspend fun sendKey(keyCode: Int) {
|
suspend fun sendKey(keyCode: Int) {
|
||||||
send(VmKeyEvent(keyCode, VmKeyEvent.ACTION_DOWN))
|
send(VmKeyEvent(keyCode, VmKeyEvent.ACTION_DOWN))
|
||||||
|
|||||||
@@ -140,7 +140,8 @@ fun GlassesStatusScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
ScreenPowerSection(
|
ScreenPowerSection(
|
||||||
onShell = { command -> provisioner.runShell(command) },
|
enabled = connection is ConnectionState.Connected,
|
||||||
|
onSetScreen = { sleep -> controller.setScreen(sleep) },
|
||||||
)
|
)
|
||||||
|
|
||||||
GlassesWifiSection(
|
GlassesWifiSection(
|
||||||
@@ -331,42 +332,44 @@ private fun ScreenAndBrightnessSection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Гашение/пробуждение экрана очков через adb (`input keyevent 223/224`).
|
|
||||||
* Работает по wireless-debugging: если очки ушли в глубокий сон и WiFi/adb отвалились —
|
|
||||||
* команда не дойдёт (Mercury setDeepSuspend мы выпилили, т.к. он не работал).
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ScreenPowerSection(
|
private fun ScreenPowerSection(
|
||||||
onShell: suspend (String) -> Result<String>,
|
enabled: Boolean,
|
||||||
|
onSetScreen: suspend (Boolean) -> Boolean,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var busy by remember { mutableStateOf(false) }
|
var busy by remember { mutableStateOf(false) }
|
||||||
var message by remember { mutableStateOf<String?>(null) }
|
var message by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
fun send(command: String, okText: String) {
|
fun send(sleep: Boolean, okText: String) {
|
||||||
if (busy) return
|
if (busy || !enabled) return
|
||||||
busy = true
|
busy = true
|
||||||
message = null
|
message = null
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val r = onShell(command)
|
try {
|
||||||
message = if (r.isSuccess) okText else "Ошибка: ${r.exceptionOrNull()?.message}"
|
val ok = onSetScreen(sleep)
|
||||||
|
message = if (ok) okText else "Нет Канала 1 — команда не ушла"
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
message = "Ошибка: ${t.message}"
|
||||||
|
} finally {
|
||||||
busy = false
|
busy = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SectionCard(title = "Экран очков") {
|
SectionCard(title = "Экран очков") {
|
||||||
Hint("Через adb (wireless-debugging). Если очки ушли в глубокий сон и WiFi отвалился — команда не дойдёт.")
|
Hint("Очки гасят/будят себя сами своим Shizuku-сервисом (Канал 1). Нужен Shizuku на очках.")
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
Button(
|
Button(
|
||||||
enabled = !busy,
|
enabled = !busy && enabled,
|
||||||
onClick = { send("input keyevent 223", "Экран погашен") },
|
onClick = { send(true, "Экран погашен") },
|
||||||
) { Text("Погасить") }
|
) { Text("Погасить") }
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
enabled = !busy,
|
enabled = !busy && enabled,
|
||||||
onClick = { send("input keyevent 224", "Экран разбужен") },
|
onClick = { send(false, "Экран разбужен") },
|
||||||
) { Text("Разбудить") }
|
) { Text("Разбудить") }
|
||||||
}
|
}
|
||||||
|
if (!enabled) Hint("Нужен Канал 1 (WebSocket) — приложение на очках не подключено.")
|
||||||
message?.let { Hint(it) }
|
message?.let { Hint(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import kotlinx.serialization.Serializable
|
|||||||
* (настраивается в [VmProtocol]).
|
* (настраивается в [VmProtocol]).
|
||||||
*
|
*
|
||||||
* Потоки:
|
* Потоки:
|
||||||
* - Phone → Glasses: [TouchEvent], [KeyEvent], [CursorMove], [CursorClick], [LaunchAppCommand], [SetAudioMode], [Shutdown]
|
* - Phone → Glasses: [TouchEvent], [KeyEvent], [CursorMove], [CursorClick], [LaunchAppCommand], [SetAudioMode], [ScreenControl], [Shutdown]
|
||||||
* - Glasses → Phone: [GlassesStatus], [AppCrashedEvent], [AppLaunchedAck]
|
* - Glasses → Phone: [GlassesStatus], [AppCrashedEvent], [AppLaunchedAck]
|
||||||
* - Двунаправленные: [Ping], [Pong]
|
* - Двунаправленные: [Ping], [Pong]
|
||||||
*/
|
*/
|
||||||
@@ -112,6 +112,18 @@ enum class AudioMode { ON_GLASSES, ON_PHONE }
|
|||||||
@SerialName("shutdown")
|
@SerialName("shutdown")
|
||||||
data object Shutdown : VmMessage
|
data object Shutdown : VmMessage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Погасить/разбудить экран очков ([sleep] = true — погасить).
|
||||||
|
*
|
||||||
|
* Выполняется на очках их же Shizuku-UserService (инжект `KEYCODE_SLEEP`/`KEYCODE_WAKEUP`
|
||||||
|
* от shell UID), поэтому не зависит ни от Mercury BLE, ни от wireless-adb.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
@SerialName("screen_control")
|
||||||
|
data class ScreenControl(
|
||||||
|
val sleep: Boolean,
|
||||||
|
) : VmMessage
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("get_app_list")
|
@SerialName("get_app_list")
|
||||||
data object GetAppList : VmMessage
|
data object GetAppList : VmMessage
|
||||||
|
|||||||
Reference in New Issue
Block a user