экран очков: единый тумблер на «Пульте» + очки шлют реальное состояние экрана

- shared: новое сообщение ScreenState(on) от очков телефону
- glasses: слушаем ACTION_SCREEN_ON/OFF (PowerManager.isInteractive), шлём состояние
  при подключении телефона и на каждое изменение; после ScreenControl — актуализация
- phone: VmPhoneController.screenOn (StateFlow<Boolean?>), оптимистичное обновление в setScreen
- phone: на экране «Пульт» одна кнопка-тумблер «Погасить/Разбудить экран очков»,
  подпись отражает реальное состояние, disabled без Канала 1
This commit is contained in:
2026-10-01 11:26:37 +03:00
parent de6c648c35
commit 4743427756
4 changed files with 111 additions and 0 deletions
@@ -28,6 +28,7 @@ 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.ScreenState
import pw.binom.rayneovm.shared.SetAudioMode
import pw.binom.rayneovm.shared.SetText
import pw.binom.rayneovm.shared.ShizukuStatus
@@ -56,6 +57,10 @@ class VmPhoneController(
private val _glassesShizuku = MutableStateFlow<ShizukuStatus?>(null)
val glassesShizuku: StateFlow<ShizukuStatus?> = _glassesShizuku.asStateFlow()
/** Реальное состояние экрана очков (шлют очки); null — пока неизвестно. */
private val _screenOn = MutableStateFlow<Boolean?>(null)
val screenOn: StateFlow<Boolean?> = _screenOn.asStateFlow()
private var currentTransport: StringTransport? = null
private val audioManager by lazy { AudioPlaybackManager(context) }
@@ -122,6 +127,7 @@ class VmPhoneController(
onDisconnected = {
currentTransport = null
_glassesShizuku.value = null
_screenOn.value = null
// Очки отвалились — аудио-поток всё равно мёртв, глушим трек.
audioManager.stop()
_state.value = ConnectionState.Disconnected
@@ -159,6 +165,9 @@ class VmPhoneController(
is ShizukuStatus -> {
_glassesShizuku.value = msg
}
is ScreenState -> {
_screenOn.value = msg.on
}
is SetAudioMode -> {
// Аудио режим — отдельный поток, не контролируется этим контроллером напрямую.
// AudioCaptureService на очках стартует/останавливается.
@@ -226,6 +235,8 @@ class VmPhoneController(
suspend fun setScreen(sleep: Boolean): Boolean {
if (_state.value !is ConnectionState.Connected) return false
send(ScreenControl(sleep))
// Оптимистично: точное состояние очки пришлют сами (ScreenState) — возможно с задержкой.
_screenOn.value = !sleep
return true
}
@@ -20,10 +20,12 @@ import androidx.compose.material.icons.filled.FastRewind
import androidx.compose.material.icons.filled.Keyboard
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.PowerSettingsNew
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -42,6 +44,7 @@ import androidx.compose.ui.unit.sp
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import pw.binom.rayneovm.phone.protocol.ConnectionState
import pw.binom.rayneovm.phone.protocol.VmPhoneController
import pw.binom.rayneovm.shared.VmDisplay
@@ -71,6 +74,9 @@ fun RemoteScreen(
modifier: Modifier = Modifier,
) {
val scope = rememberCoroutineScope()
// Реальное состояние экрана очков (шлют сами очки) — питает кнопку-тумблер ниже.
val screenOn by controller.screenOn.collectAsState()
val connected = controller.state.collectAsState().value is ConnectionState.Connected
// Позиция курсора в координатах VirtualDisplay гостя.
var cursor by remember { mutableStateOf(Offset(VmDisplay.WIDTH / 2f, VmDisplay.HEIGHT / 2f)) }
// Отложенный клик: два пальца кладут не одновременно, и первый палец успевает «щёлкнуть»
@@ -203,6 +209,23 @@ fun RemoteScreen(
}
}
// Одна кнопка-тумблер: гасит/будит экран очков. Символ отражает текущее состояние
// (его шлют очки), поэтому нажатие всегда чередует вкл/выкл корректно.
Row(modifier = Modifier.fillMaxWidth()) {
RemoteButton(
icon = Icons.Default.PowerSettingsNew,
label = when (screenOn) {
true -> "Погасить экран очков"
false -> "Разбудить экран очков"
null -> "Экран очков: вкл/выкл"
},
modifier = Modifier.weight(1f),
enabled = connected,
) {
scope.launch { controller.setScreen(sleep = screenOn != false) }
}
}
if (showKeyboard) {
KeyboardDialog(
onDismiss = { showKeyboard = false },