#1,#3,#4+#5 keyboards: Binocular Compose, cursor watchdog 3s, SetText + VmNoopIme + sendText

This commit is contained in:
2026-09-27 11:18:52 +03:00
parent 3ac76daca1
commit 65f3f5d228
11 changed files with 238 additions and 94 deletions
@@ -111,8 +111,18 @@ class ShizukuProvisioner(
Log.i(TAG, "применяю системные гранты на очках ($serial)")
val grant = adb.shell("pm grant $GLASSES_PACKAGE android.permission.RECORD_AUDIO", timeoutMs = 15_000)
val appop = adb.shell("cmd appops set $GLASSES_PACKAGE PROJECT_MEDIA allow", timeoutMs = 15_000)
Log.i(TAG, "гранты: RECORD_AUDIO=${grant.isSuccess}, PROJECT_MEDIA=${appop.isSuccess}")
if (grant.isSuccess && appop.isSuccess) grantsAppliedInSession = true
// #4.1: назначаем наш VmNoopImeService дефолтным IME навсегда (см. [VmNoopImeService]).
// Подавляет системный IME, который гость (Zona) поднимает при фокусе в поле ввода.
val ime = adb.shell(
"settings put secure default_input_method " +
"$GLASSES_PACKAGE/.VmNoopImeService",
timeoutMs = 15_000,
)
Log.i(
TAG,
"гранты: RECORD_AUDIO=${grant.isSuccess}, PROJECT_MEDIA=${appop.isSuccess}, IME=${ime.isSuccess}",
)
if (grant.isSuccess && appop.isSuccess && ime.isSuccess) grantsAppliedInSession = true
}
}
@@ -28,6 +28,7 @@ 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.SetText
import pw.binom.rayneovm.shared.ShizukuStatus
import pw.binom.rayneovm.shared.VmMessage
import pw.binom.rayneovm.shared.VmProtocol
@@ -225,6 +226,17 @@ class VmPhoneController(
send(VmKeyEvent(keyCode, VmKeyEvent.ACTION_UP))
}
/**
* Инжектит [text] в активное поле ввода гостя на дисплее [displayId] очков.
*
* Работает по [SetText]: очки получают команду, через Shizuku-binder достают InputConnection
* сфокусированного окна на нашем VirtualDisplay и делают `commitText`. Unicode-корректно
* (русский/эмодзи).
*/
suspend fun sendText(text: String, displayId: Int = 0) {
send(SetText(displayId, text))
}
/**
* Переключает аудио режим: 0 = ON_GLASSES, 1 = ON_PHONE.
* Шлёт SetAudioMode очкам, и на телефоне запускает/останавливает AudioPlaybackManager.
@@ -51,6 +51,8 @@ private const val KEYCODE_DPAD_LEFT = 21
private const val KEYCODE_DPAD_RIGHT = 22
private const val KEYCODE_MEDIA_PLAY = 126
private const val KEYCODE_MEDIA_PAUSE = 127
private const val KEYCODE_DEL = 67
private const val KEYCODE_ENTER = 66
/** Сколько ждать перед кликом-тапом: если за это время коснулись снова — это был скролл. */
private const val CLICK_GRACE_MS = 100L
@@ -74,6 +76,7 @@ fun RemoteScreen(
// Отложенный клик: два пальца кладут не одновременно, и первый палец успевает «щёлкнуть»
// до начала скролла. Клик ждёт [CLICK_GRACE_MS]; касание за это время его отменяет.
val pendingClick = remember { mutableStateOf<Job?>(null) }
var showKeyboard by remember { mutableStateOf(false) }
Column(
modifier = modifier.fillMaxSize().background(Color.Black).padding(16.dp),
@@ -195,8 +198,21 @@ fun RemoteScreen(
RemoteButton(Icons.Default.Pause, "Пауза", Modifier.weight(1f)) {
scope.launch { controller.sendKey(KEYCODE_MEDIA_PAUSE) }
}
// TODO: клавиатура — отдельная задача.
RemoteButton(Icons.Default.Keyboard, "Клавиатура", Modifier.weight(1f), enabled = false) {}
RemoteButton(Icons.Default.Keyboard, "Клавиатура", Modifier.weight(1f)) {
showKeyboard = true
}
}
if (showKeyboard) {
KeyboardDialog(
onDismiss = { showKeyboard = false },
onSend = { text ->
scope.launch { controller.sendText(text) }
showKeyboard = false
},
onBackspace = { scope.launch { controller.sendKey(KEYCODE_DEL) } },
onEnter = { scope.launch { controller.sendKey(KEYCODE_ENTER) } },
)
}
}
}
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
@@ -127,40 +128,59 @@ fun ZonaProfile(
KeyboardDialog(
onDismiss = { showKeyboard = false },
onSend = { text ->
scope.launch {
text.forEach { ch ->
val keyCode = keyCodeForChar(ch)
if (keyCode != -1) {
controller.send(VmKeyEvent(keyCode = keyCode, action = 1))
}
}
}
scope.launch { controller.sendText(text) }
showKeyboard = false
},
onBackspace = { scope.launch { controller.send(VmKeyEvent(keyCode = 67, action = 1)) } },
onEnter = { scope.launch { controller.send(VmKeyEvent(keyCode = 66, action = 1)) } },
)
}
}
@Composable
private fun KeyboardDialog(
fun KeyboardDialog(
onDismiss: () -> Unit,
onSend: (String) -> Unit,
onBackspace: (() -> Unit)? = null,
onEnter: (() -> Unit)? = null,
) {
var input by remember { mutableStateOf("") }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Ввод текста") },
text = {
OutlinedTextField(
value = input,
onValueChange = { input = it },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Done,
),
)
Column {
OutlinedTextField(
value = input,
onValueChange = { input = it },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Done,
),
)
if (onBackspace != null || onEnter != null) {
androidx.compose.foundation.layout.Spacer(Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
if (onBackspace != null) {
androidx.compose.material3.OutlinedButton(
onClick = onBackspace,
modifier = Modifier.weight(1f),
) { Text("⌫ Backspace") }
}
if (onEnter != null) {
androidx.compose.material3.OutlinedButton(
onClick = onEnter,
modifier = Modifier.weight(1f),
) { Text("⏎ Enter") }
}
}
}
}
},
confirmButton = {
TextButton(onClick = { onSend(input) }) { Text("Отправить") }
@@ -171,25 +191,6 @@ private fun KeyboardDialog(
)
}
/**
* ASCII char → Android keyCode. Простая таблица, без юникода и без спец.клавиш.
* Возвращает -1 для символов, которые нельзя передать через KEYCODE.
*/
private fun keyCodeForChar(ch: Char): Int {
return when (ch) {
in 'a'..'z' -> android.view.KeyEvent.KEYCODE_A + (ch - 'a')
in 'A'..'Z' -> android.view.KeyEvent.KEYCODE_A + (ch - 'A')
in '0'..'9' -> android.view.KeyEvent.KEYCODE_0 + (ch - '0')
' ' -> android.view.KeyEvent.KEYCODE_SPACE
'\n' -> android.view.KeyEvent.KEYCODE_ENTER
'.' -> android.view.KeyEvent.KEYCODE_PERIOD
',' -> android.view.KeyEvent.KEYCODE_COMMA
'!' -> android.view.KeyEvent.KEYCODE_1 // shift+1 — отдаём как KEYCODE_1 + shift, но тут упрощённо
'?' -> android.view.KeyEvent.KEYCODE_SLASH
else -> -1
}
}
@Composable
private fun Spacer(modifier: Modifier) {
androidx.compose.foundation.layout.Spacer(modifier = modifier)