#1,#3,#4+#5 keyboards: Binocular Compose, cursor watchdog 3s, SetText + VmNoopIme + sendText
This commit is contained in:
+12
-2
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user