Skip to content

Commit 58ab9fa

Browse files
committed
Fix Android keyboard input mapping
1 parent ebf3f72 commit 58ab9fa

3 files changed

Lines changed: 234 additions & 61 deletions

File tree

server/src/android.rs

Lines changed: 230 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ const ANDROID_TOUCH_IDENTIFIER: i32 = 1;
2121
const RUNNING_EMULATOR_CACHE_TTL: Duration = Duration::from_secs(2);
2222
const AVD_GRPC_PORT_CACHE_TTL: Duration = Duration::from_secs(60);
2323
const SCREEN_SIZE_CACHE_TTL: Duration = Duration::from_secs(60);
24+
const MODIFIER_SHIFT: u32 = 1 << 0;
25+
const MODIFIER_CONTROL: u32 = 1 << 1;
26+
const MODIFIER_OPTION: u32 = 1 << 2;
27+
const MODIFIER_COMMAND: u32 = 1 << 3;
28+
const MODIFIER_CAPS_LOCK: u32 = 1 << 4;
2429

2530
type TimedMap<T> = Option<(Instant, HashMap<String, T>)>;
2631
type ScreenSizeCache = HashMap<String, (Instant, (f64, f64))>;
@@ -316,39 +321,60 @@ impl AndroidBridge {
316321
Ok(())
317322
}
318323

319-
pub fn send_key(&self, id: &str, key_code: u16, _modifiers: u32) -> Result<(), AppError> {
320-
if self
321-
.send_key_grpc(id, grpc::KeyboardEvent::usb_keypress(i32::from(key_code)))
322-
.is_ok()
323-
{
324-
return Ok(());
324+
pub fn send_key(&self, id: &str, key_code: u16, modifiers: u32) -> Result<(), AppError> {
325+
if let Some(text) = hid_text_for_key(key_code, modifiers) {
326+
return self.type_text_adb(id, &text);
325327
}
328+
326329
let serial = self.serial_for_id(id)?;
327330
let android_key = android_key_code(key_code);
328-
self.run_adb([
329-
"-s",
330-
&serial,
331-
"shell",
332-
"input",
333-
"keyevent",
334-
&android_key.to_string(),
335-
])?;
336-
Ok(())
331+
if has_android_key_modifiers(modifiers) {
332+
return self.press_android_key_combination(&serial, android_key, modifiers);
333+
}
334+
self.press_android_key(&serial, android_key)
337335
}
338336

339337
pub fn type_text(&self, id: &str, text: &str) -> Result<(), AppError> {
340-
if self
341-
.send_key_grpc(id, grpc::KeyboardEvent::text(text.to_owned()))
342-
.is_ok()
343-
{
344-
return Ok(());
345-
}
338+
self.type_text_adb(id, text)
339+
}
340+
341+
pub fn dismiss_keyboard(&self, id: &str) -> Result<(), AppError> {
346342
let serial = self.serial_for_id(id)?;
347-
let escaped = text.replace('%', "%25").replace(' ', "%s");
343+
self.press_android_key(&serial, 4)
344+
}
345+
346+
fn type_text_adb(&self, id: &str, text: &str) -> Result<(), AppError> {
347+
let serial = self.serial_for_id(id)?;
348+
let escaped = android_input_text_arg(text);
348349
self.run_adb(["-s", &serial, "shell", "input", "text", &escaped])?;
349350
Ok(())
350351
}
351352

353+
fn press_android_key(&self, serial: &str, key_code: u16) -> Result<(), AppError> {
354+
let key_code = key_code.to_string();
355+
self.run_adb(["-s", serial, "shell", "input", "keyevent", &key_code])?;
356+
Ok(())
357+
}
358+
359+
fn press_android_key_combination(
360+
&self,
361+
serial: &str,
362+
key_code: u16,
363+
modifiers: u32,
364+
) -> Result<(), AppError> {
365+
let mut parts = vec!["input".to_owned(), "keycombination".to_owned()];
366+
parts.extend(
367+
android_modifier_key_codes(modifiers)
368+
.into_iter()
369+
.map(|key| key.to_string()),
370+
);
371+
parts.push(key_code.to_string());
372+
match self.run_adb_shell(serial, &parts.join(" ")) {
373+
Ok(_) => Ok(()),
374+
Err(_) => self.press_android_key(serial, key_code),
375+
}
376+
}
377+
352378
pub fn press_home(&self, id: &str) -> Result<(), AppError> {
353379
let serial = self.serial_for_id(id)?;
354380
self.run_adb(["-s", &serial, "shell", "input", "keyevent", "3"])?;
@@ -610,19 +636,6 @@ impl AndroidBridge {
610636
self.send_touch_grpc(id, end_x, end_y, "ended")
611637
}
612638

613-
fn send_key_grpc(&self, id: &str, event: grpc::KeyboardEvent) -> Result<(), AppError> {
614-
self.block_on_grpc(async {
615-
let avd_name = avd_from_id(id)?;
616-
self.grpc_unary_for_avd::<grpc::KeyboardEvent, grpc::Empty>(
617-
&avd_name,
618-
"/android.emulation.control.EmulatorController/sendKey",
619-
event,
620-
)
621-
.await?;
622-
Ok(())
623-
})
624-
}
625-
626639
fn block_on_grpc<F, T>(&self, future: F) -> Result<T, AppError>
627640
where
628641
F: Future<Output = Result<T, AppError>>,
@@ -1189,11 +1202,41 @@ fn android_role(node: roxmltree::Node<'_, '_>, class_name: &str) -> &'static str
11891202

11901203
fn android_key_code(hid: u16) -> u16 {
11911204
match hid {
1205+
4..=29 => 29 + (hid - 4),
1206+
30 => 8,
1207+
31 => 9,
1208+
32 => 10,
1209+
33 => 11,
1210+
34 => 12,
1211+
35 => 13,
1212+
36 => 14,
1213+
37 => 15,
1214+
38 => 16,
1215+
39 => 7,
11921216
40 => 66,
11931217
41 => 111,
11941218
42 => 67,
11951219
43 => 61,
11961220
44 => 62,
1221+
45 => 69,
1222+
46 => 70,
1223+
47 => 71,
1224+
48 => 72,
1225+
49 => 73,
1226+
51 => 74,
1227+
52 => 75,
1228+
53 => 68,
1229+
54 => 55,
1230+
55 => 56,
1231+
56 => 76,
1232+
57 => 115,
1233+
58..=69 => 131 + (hid - 58),
1234+
73 => 124,
1235+
74 => 122,
1236+
75 => 92,
1237+
76 => 112,
1238+
77 => 123,
1239+
78 => 93,
11971240
79 => 22,
11981241
80 => 21,
11991242
81 => 20,
@@ -1202,6 +1245,106 @@ fn android_key_code(hid: u16) -> u16 {
12021245
}
12031246
}
12041247

1248+
fn hid_text_for_key(hid: u16, modifiers: u32) -> Option<String> {
1249+
if modifiers & (MODIFIER_CONTROL | MODIFIER_OPTION | MODIFIER_COMMAND) != 0 {
1250+
return None;
1251+
}
1252+
1253+
let shifted = modifiers & MODIFIER_SHIFT != 0;
1254+
let caps_locked = modifiers & MODIFIER_CAPS_LOCK != 0;
1255+
1256+
if (4..=29).contains(&hid) {
1257+
let offset = (hid - 4) as u8;
1258+
let base = if shifted ^ caps_locked { b'A' } else { b'a' };
1259+
return Some(char::from(base + offset).to_string());
1260+
}
1261+
1262+
let text = match (hid, shifted) {
1263+
(30, false) => "1",
1264+
(30, true) => "!",
1265+
(31, false) => "2",
1266+
(31, true) => "@",
1267+
(32, false) => "3",
1268+
(32, true) => "#",
1269+
(33, false) => "4",
1270+
(33, true) => "$",
1271+
(34, false) => "5",
1272+
(34, true) => "%",
1273+
(35, false) => "6",
1274+
(35, true) => "^",
1275+
(36, false) => "7",
1276+
(36, true) => "&",
1277+
(37, false) => "8",
1278+
(37, true) => "*",
1279+
(38, false) => "9",
1280+
(38, true) => "(",
1281+
(39, false) => "0",
1282+
(39, true) => ")",
1283+
(44, _) => " ",
1284+
(45, false) => "-",
1285+
(45, true) => "_",
1286+
(46, false) => "=",
1287+
(46, true) => "+",
1288+
(47, false) => "[",
1289+
(47, true) => "{",
1290+
(48, false) => "]",
1291+
(48, true) => "}",
1292+
(49, false) => "\\",
1293+
(49, true) => "|",
1294+
(51, false) => ";",
1295+
(51, true) => ":",
1296+
(52, false) => "'",
1297+
(52, true) => "\"",
1298+
(53, false) => "`",
1299+
(53, true) => "~",
1300+
(54, false) => ",",
1301+
(54, true) => "<",
1302+
(55, false) => ".",
1303+
(55, true) => ">",
1304+
(56, false) => "/",
1305+
(56, true) => "?",
1306+
_ => return None,
1307+
};
1308+
Some(text.to_owned())
1309+
}
1310+
1311+
fn android_input_text_arg(text: &str) -> String {
1312+
let mut escaped = String::new();
1313+
for character in text.chars() {
1314+
match character {
1315+
' ' => escaped.push_str("%s"),
1316+
'%' => escaped.push_str("%25"),
1317+
'&' | '(' | ')' | '<' | '>' | ';' | '|' | '*' | '\\' | '"' | '\'' | '`' | '$' => {
1318+
escaped.push('\\');
1319+
escaped.push(character);
1320+
}
1321+
_ => escaped.push(character),
1322+
}
1323+
}
1324+
escaped
1325+
}
1326+
1327+
fn has_android_key_modifiers(modifiers: u32) -> bool {
1328+
modifiers & (MODIFIER_SHIFT | MODIFIER_CONTROL | MODIFIER_OPTION | MODIFIER_COMMAND) != 0
1329+
}
1330+
1331+
fn android_modifier_key_codes(modifiers: u32) -> Vec<u16> {
1332+
let mut keys = Vec::new();
1333+
if modifiers & MODIFIER_CONTROL != 0 {
1334+
keys.push(113);
1335+
}
1336+
if modifiers & MODIFIER_OPTION != 0 {
1337+
keys.push(57);
1338+
}
1339+
if modifiers & MODIFIER_SHIFT != 0 {
1340+
keys.push(59);
1341+
}
1342+
if modifiers & MODIFIER_COMMAND != 0 {
1343+
keys.push(117);
1344+
}
1345+
keys
1346+
}
1347+
12051348
fn android_log_level(line: &str) -> &'static str {
12061349
if line.contains(" E ") {
12071350
"error"
@@ -1251,6 +1394,58 @@ mod tests {
12511394
assert_eq!(value["type"], "CustomTile");
12521395
assert_eq!(value["role"], "button");
12531396
}
1397+
1398+
#[test]
1399+
fn android_key_code_maps_usb_hid_keyboard_usages() {
1400+
assert_eq!(android_key_code(4), 29);
1401+
assert_eq!(android_key_code(29), 54);
1402+
assert_eq!(android_key_code(30), 8);
1403+
assert_eq!(android_key_code(39), 7);
1404+
assert_eq!(android_key_code(40), 66);
1405+
assert_eq!(android_key_code(42), 67);
1406+
assert_eq!(android_key_code(58), 131);
1407+
assert_eq!(android_key_code(69), 142);
1408+
assert_eq!(android_key_code(73), 124);
1409+
assert_eq!(android_key_code(79), 22);
1410+
}
1411+
1412+
#[test]
1413+
fn hid_text_for_key_uses_shift_and_caps_for_printable_input() {
1414+
assert_eq!(hid_text_for_key(4, 0).as_deref(), Some("a"));
1415+
assert_eq!(hid_text_for_key(4, MODIFIER_SHIFT).as_deref(), Some("A"));
1416+
assert_eq!(
1417+
hid_text_for_key(4, MODIFIER_CAPS_LOCK).as_deref(),
1418+
Some("A")
1419+
);
1420+
assert_eq!(
1421+
hid_text_for_key(4, MODIFIER_SHIFT | MODIFIER_CAPS_LOCK).as_deref(),
1422+
Some("a")
1423+
);
1424+
assert_eq!(hid_text_for_key(30, 0).as_deref(), Some("1"));
1425+
assert_eq!(hid_text_for_key(30, MODIFIER_SHIFT).as_deref(), Some("!"));
1426+
assert_eq!(hid_text_for_key(56, MODIFIER_SHIFT).as_deref(), Some("?"));
1427+
assert_eq!(hid_text_for_key(80, 0), None);
1428+
assert_eq!(hid_text_for_key(4, MODIFIER_COMMAND), None);
1429+
}
1430+
1431+
#[test]
1432+
fn android_input_text_arg_escapes_adb_shell_text() {
1433+
assert_eq!(android_input_text_arg("hello world"), "hello%sworld");
1434+
assert_eq!(android_input_text_arg("100%"), "100%25");
1435+
assert_eq!(android_input_text_arg("a&b"), "a\\&b");
1436+
}
1437+
1438+
#[test]
1439+
fn android_modifier_key_codes_match_android_meta_keys() {
1440+
assert_eq!(android_modifier_key_codes(MODIFIER_CONTROL), vec![113]);
1441+
assert_eq!(android_modifier_key_codes(MODIFIER_OPTION), vec![57]);
1442+
assert_eq!(android_modifier_key_codes(MODIFIER_SHIFT), vec![59]);
1443+
assert_eq!(android_modifier_key_codes(MODIFIER_COMMAND), vec![117]);
1444+
assert_eq!(
1445+
android_modifier_key_codes(MODIFIER_CONTROL | MODIFIER_SHIFT),
1446+
vec![113, 59]
1447+
);
1448+
}
12541449
}
12551450

12561451
#[allow(dead_code)]
@@ -1319,28 +1514,6 @@ mod grpc {
13191514
pub text: String,
13201515
}
13211516

1322-
impl KeyboardEvent {
1323-
pub fn usb_keypress(key_code: i32) -> Self {
1324-
Self {
1325-
code_type: keyboard_event::KeyCodeType::Usb as i32,
1326-
event_type: keyboard_event::KeyEventType::Keypress as i32,
1327-
key_code,
1328-
key: String::new(),
1329-
text: String::new(),
1330-
}
1331-
}
1332-
1333-
pub fn text(text: String) -> Self {
1334-
Self {
1335-
code_type: keyboard_event::KeyCodeType::Usb as i32,
1336-
event_type: keyboard_event::KeyEventType::Keypress as i32,
1337-
key_code: 0,
1338-
key: String::new(),
1339-
text,
1340-
}
1341-
}
1342-
}
1343-
13441517
pub mod keyboard_event {
13451518
#[derive(
13461519
Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration,

server/src/api/routes.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1856,7 +1856,7 @@ async fn run_android_control_message(
18561856
"`phase` must be `down`, `up`, `began`, `ended`, or `cancelled`.",
18571857
)),
18581858
},
1859-
ControlMessage::DismissKeyboard => android.send_key(&udid, 41, 0),
1859+
ControlMessage::DismissKeyboard => android.dismiss_keyboard(&udid),
18601860
ControlMessage::Home => android.press_home(&udid),
18611861
ControlMessage::AppSwitcher => android.open_app_switcher(&udid),
18621862
ControlMessage::RotateLeft | ControlMessage::RotateRight => {
@@ -2602,7 +2602,7 @@ async fn dismiss_keyboard(
26022602
Path(udid): Path<String>,
26032603
) -> Result<Json<Value>, AppError> {
26042604
if android::is_android_id(&udid) {
2605-
run_android_action(state, move |android| android.send_key(&udid, 41, 0)).await?;
2605+
run_android_action(state, move |android| android.dismiss_keyboard(&udid)).await?;
26062606
return Ok(json(json_value!({ "ok": true })));
26072607
}
26082608
run_bridge_action(state, move |bridge| bridge.send_key(&udid, 41, 0)).await?;
@@ -3484,7 +3484,7 @@ async fn run_batch_step(state: AppState, udid: String, step: BatchStep) -> Resul
34843484
}
34853485
BatchStep::DismissKeyboard => {
34863486
if android::is_android_id(&udid) {
3487-
run_android_action(state, move |android| android.send_key(&udid, 41, 0)).await?;
3487+
run_android_action(state, move |android| android.dismiss_keyboard(&udid)).await?;
34883488
return Ok(json_value!({ "action": "dismissKeyboard" }));
34893489
}
34903490
run_bridge_action(state, move |bridge| bridge.send_key(&udid, 41, 0)).await?;

server/src/transport/webrtc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,7 @@ async fn run_android_webrtc_control_message(
857857
"`phase` must be `down`, `up`, `began`, `ended`, or `cancelled`.",
858858
)),
859859
},
860-
ControlMessage::DismissKeyboard => state.android.send_key(&udid, 41, 0),
860+
ControlMessage::DismissKeyboard => state.android.dismiss_keyboard(&udid),
861861
ControlMessage::Home => state.android.press_home(&udid),
862862
ControlMessage::AppSwitcher => state.android.open_app_switcher(&udid),
863863
ControlMessage::RotateLeft | ControlMessage::RotateRight => {

0 commit comments

Comments
 (0)