Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions src/emulator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,54 @@ pub fn parse_image_sysdir(config_ini: &str) -> Option<String> {
.map(|v| v.trim().trim_end_matches('/').to_string())
}

/// Ensure an AVD `config.ini` enables the emulated hardware keyboard so the host
/// (Mac) keyboard types into the guest. avdmanager's device profiles default
/// `hw.keyboard=no`, which silently drops host key events — you tap a field, the
/// on-screen keyboard shows, but typing on the Mac does nothing. Flipping it to
/// `yes` forwards the physical keyboard; the soft IME still appears because the
/// system images default `show_ime_with_hard_keyboard=1`.
///
/// Pure string transform: returns the rewritten file when a change is needed, or
/// `None` if it already reads `hw.keyboard=yes` (so the caller can skip writing).
/// Only the exact `hw.keyboard=` key is touched — `hw.keyboard.charmap`/`.lid`
/// are left alone.
pub fn with_hw_keyboard_enabled(config_ini: &str) -> Option<String> {
fn is_hw_keyboard(line: &str) -> bool {
line.trim_start()
.strip_prefix("hw.keyboard")
.is_some_and(|rest| rest.trim_start().starts_with('='))
}
let mut found = false;
let mut changed = false;
let mut out: Vec<String> = Vec::new();
for line in config_ini.lines() {
if is_hw_keyboard(line) {
found = true;
let value = line.split_once('=').map_or("", |(_, v)| v.trim());
if value == "yes" {
out.push(line.to_string());
} else {
out.push("hw.keyboard=yes".to_string());
changed = true;
}
} else {
out.push(line.to_string());
}
}
if !found {
out.push("hw.keyboard=yes".to_string());
changed = true;
}
if !changed {
return None;
}
let mut result = out.join("\n");
if config_ini.ends_with('\n') {
result.push('\n');
}
Some(result)
}

/// System images that are installed but referenced by no live AVD — i.e. safe
/// to prune. Both lists are relative image paths (`system-images/<api>/<tag>/<abi>`).
pub fn unreferenced_images(installed: &[String], referenced: &[String]) -> Vec<String> {
Expand Down Expand Up @@ -178,6 +226,45 @@ mod tests {
assert_eq!(parse_image_sysdir(""), None);
}

#[test]
fn hw_keyboard_flips_no_to_yes_and_leaves_siblings() {
let ini = "hw.dPad=no\n\
hw.keyboard=no\n\
hw.keyboard.charmap=qwerty2\n\
hw.keyboard.lid=yes\n\
hw.mainKeys=no\n";
let out = with_hw_keyboard_enabled(ini).expect("should rewrite");
assert!(out.contains("hw.keyboard=yes"));
// siblings untouched
assert!(out.contains("hw.keyboard.charmap=qwerty2"));
assert!(out.contains("hw.keyboard.lid=yes"));
// the old value is gone and the file still ends in a newline
assert!(!out.contains("hw.keyboard=no"));
assert!(out.ends_with('\n'));
}

#[test]
fn hw_keyboard_noop_when_already_yes() {
let ini = "hw.keyboard=yes\nhw.mainKeys=no\n";
assert_eq!(with_hw_keyboard_enabled(ini), None);
}

#[test]
fn hw_keyboard_appended_when_absent() {
let ini = "hw.dPad=no\nhw.mainKeys=no\n";
let out = with_hw_keyboard_enabled(ini).expect("should append");
assert!(out.contains("hw.keyboard=yes"));
assert!(out.ends_with('\n'));
}

#[test]
fn hw_keyboard_noop_does_not_match_charmap_only() {
// a config that has the sibling keys but no bare hw.keyboard= must append.
let ini = "hw.keyboard.charmap=qwerty2\nhw.keyboard.lid=yes\n";
let out = with_hw_keyboard_enabled(ini).expect("should append a bare key");
assert!(out.lines().any(|l| l == "hw.keyboard=yes"));
}

#[test]
fn unreferenced_images_is_set_difference() {
let installed = vec![
Expand Down
19 changes: 19 additions & 0 deletions src/provision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ pub fn boot(sdk: &Sdk, cfg: &Config) -> Result<()> {
}
if !is_running(sdk) {
clear_stale_locks(sdk, cfg);
enable_hw_keyboard(sdk, cfg);
eprintln!("🚀 booting emulator ({})…", cfg.profile.avd_name());
let log = sdk.home().join("emulator.log");
let mut c = sdk.command(&sdk.emulator_bin());
Expand Down Expand Up @@ -222,6 +223,24 @@ fn boot_args(cfg: &Config) -> Vec<String> {
args
}

/// Make the AVD type from the host (Mac) keyboard. avdmanager's device profiles
/// leave `hw.keyboard=no` in the generated `config.ini`, which drops physical
/// key events; patch it to `yes` in place before booting. Idempotent and applied
/// on every cold boot so AVDs created by an older andro heal automatically. The
/// on-screen keyboard is unaffected — the images keep `show_ime_with_hard_keyboard=1`.
/// Best-effort: a missing/unwritable config.ini just leaves the AVD as-is.
fn enable_hw_keyboard(sdk: &Sdk, cfg: &Config) {
let config = sdk
.avd_home()
.join(format!("{}.avd", cfg.profile.avd_name()))
.join("config.ini");
if let Ok(text) = fs::read_to_string(&config)
&& let Some(patched) = emulator::with_hw_keyboard_enabled(&text)
{
let _ = fs::write(&config, patched);
}
}

/// Remove stale AVD lock files left by a crashed emulator. Only safe because the
/// caller already checked the emulator is NOT running; clears the spurious
/// "AVD is already in use" error so the next boot just works.
Expand Down
Loading