Skip to main content

android/emulator/
hardware.rs

1//! What the running emulator reports about its virtual hardware, and the frame
2//! size to request from it.
3
4use std::collections::BTreeMap;
5
6use super::proto;
7
8/// Size of the emulator's display in pixels, in its natural orientation.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct DisplaySize {
11    pub width: u32,
12    pub height: u32,
13}
14
15/// The virtual hardware the driver needs to know about.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct Hardware {
18    /// From `hw.lcd.width` and `hw.lcd.height`.
19    pub display: DisplaySize,
20    /// Whether the AVD declares a keyboard (`hw.keyboard=yes`). Without one the
21    /// controller's `sendKey` returns OK and the guest never sees the key (S7),
22    /// so keys and text go through the agent unless this is true.
23    pub keyboard: bool,
24}
25
26impl Hardware {
27    /// Reads the hardware keys from `(key, value)` entries.
28    pub fn from_entries<'a>(
29        entries: impl IntoIterator<Item = (&'a str, &'a str)>,
30    ) -> Result<Self, String> {
31        let entries: BTreeMap<&str, &str> = entries.into_iter().collect();
32        let pixels = |key: &str| -> Result<u32, String> {
33            let value = entries
34                .get(key)
35                .ok_or_else(|| format!("the emulator reports no {key}"))?;
36            match value.trim().parse() {
37                Ok(n) if n > 0 => Ok(n),
38                _ => Err(format!("{key} is {value:?}, not a positive pixel count")),
39            }
40        };
41        Ok(Self {
42            display: DisplaySize {
43                width: pixels("hw.lcd.width")?,
44                height: pixels("hw.lcd.height")?,
45            },
46            keyboard: entries
47                .get("hw.keyboard")
48                .is_some_and(|v| v.trim().eq_ignore_ascii_case("yes")),
49        })
50    }
51
52    /// Reads the hardware from `getStatus`. Emulator 37.1.11 leaves
53    /// `platformConfig` empty and reports the display size only in the
54    /// deprecated `hardwareConfig` (S7), so a key missing from the former is
55    /// taken from the latter.
56    #[allow(deprecated)]
57    pub fn from_status(status: &proto::EmulatorStatus) -> Result<Self, String> {
58        let hardware = status
59            .hardware_config
60            .iter()
61            .flat_map(|list| &list.entry)
62            .map(|e| (e.key.as_str(), e.value.as_str()));
63        let platform = status
64            .platform_config
65            .iter()
66            .filter(|(_, v)| !v.is_empty())
67            .map(|(k, v)| (k.as_str(), v.as_str()));
68        // Later entries win when collected, so platformConfig goes last.
69        Self::from_entries(hardware.chain(platform))
70    }
71}
72
73/// The frame size to request for frames `working_width` pixels wide.
74///
75/// The controller treats a zero in either dimension as "display size", so a
76/// width alone does not scale (S7); both are set, the height following the
77/// display's aspect ratio and rounded up. A working width of 0, or one at least
78/// as wide as the display, requests the display size as `(0, 0)`.
79pub fn scaled_size(display: DisplaySize, working_width: u32) -> (u32, u32) {
80    if working_width == 0 || working_width >= display.width {
81        return (0, 0);
82    }
83    let height =
84        (u64::from(working_width) * u64::from(display.height)).div_ceil(u64::from(display.width));
85    (working_width, u32::try_from(height).unwrap_or(u32::MAX))
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    const PIXEL_8: DisplaySize = DisplaySize {
93        width: 1080,
94        height: 2400,
95    };
96
97    #[test]
98    fn scaling_sets_both_width_and_height() {
99        assert_eq!(scaled_size(PIXEL_8, 540), (540, 1200));
100        assert_eq!(scaled_size(PIXEL_8, 270), (270, 600));
101    }
102
103    #[test]
104    fn the_height_keeps_the_aspect_ratio_rounded_up() {
105        assert_eq!(scaled_size(PIXEL_8, 500), (500, 1112));
106        let tall = DisplaySize {
107            width: 1080,
108            height: 2340,
109        };
110        assert_eq!(scaled_size(tall, 540), (540, 1170));
111    }
112
113    #[test]
114    fn no_scaling_requests_the_display_size_as_zeros() {
115        assert_eq!(scaled_size(PIXEL_8, 0), (0, 0));
116        assert_eq!(scaled_size(PIXEL_8, 1080), (0, 0));
117        assert_eq!(scaled_size(PIXEL_8, 4000), (0, 0));
118    }
119
120    #[test]
121    fn the_display_size_comes_from_the_lcd_keys() {
122        let hw = Hardware::from_entries([
123            ("hw.lcd.density", "420"),
124            ("hw.lcd.width", "1080"),
125            ("hw.lcd.height", "2400"),
126            ("hw.keyboard", "no"),
127        ])
128        .unwrap();
129        assert_eq!(hw.display, PIXEL_8);
130        assert!(!hw.keyboard);
131    }
132
133    #[test]
134    fn a_missing_or_zero_display_size_is_an_error() {
135        let missing = Hardware::from_entries([("hw.lcd.width", "1080")]).unwrap_err();
136        assert!(missing.contains("hw.lcd.height"), "{missing}");
137        let zero =
138            Hardware::from_entries([("hw.lcd.width", "0"), ("hw.lcd.height", "2400")]).unwrap_err();
139        assert!(zero.contains("hw.lcd.width"), "{zero}");
140        let garbage = Hardware::from_entries([("hw.lcd.width", "wide"), ("hw.lcd.height", "2400")])
141            .unwrap_err();
142        assert!(garbage.contains("wide"), "{garbage}");
143    }
144
145    #[test]
146    fn keys_go_through_the_controller_only_with_a_declared_keyboard() {
147        let with = |value: &'static str| {
148            Hardware::from_entries([
149                ("hw.lcd.width", "1080"),
150                ("hw.lcd.height", "2400"),
151                ("hw.keyboard", value),
152            ])
153            .unwrap()
154            .keyboard
155        };
156        assert!(with("yes"));
157        assert!(with("YES"));
158        assert!(!with("no"));
159        assert!(!with(""));
160        let absent =
161            Hardware::from_entries([("hw.lcd.width", "1080"), ("hw.lcd.height", "2400")]).unwrap();
162        assert!(!absent.keyboard);
163    }
164
165    #[test]
166    #[allow(deprecated)]
167    fn the_status_falls_back_to_the_deprecated_hardware_config() {
168        let entry = |key: &str, value: &str| proto::Entry {
169            key: key.into(),
170            value: value.into(),
171        };
172        let mut status = proto::EmulatorStatus {
173            hardware_config: Some(proto::EntryList {
174                entry: vec![
175                    entry("hw.lcd.width", "1080"),
176                    entry("hw.lcd.height", "2400"),
177                    entry("hw.keyboard", "no"),
178                ],
179            }),
180            ..Default::default()
181        };
182        let hw = Hardware::from_status(&status).unwrap();
183        assert_eq!(hw.display, PIXEL_8);
184        assert!(!hw.keyboard);
185
186        // platformConfig, where a newer emulator fills it, takes precedence.
187        status
188            .platform_config
189            .insert("hw.keyboard".into(), "yes".into());
190        assert!(Hardware::from_status(&status).unwrap().keyboard);
191
192        let empty = proto::EmulatorStatus::default();
193        assert!(Hardware::from_status(&empty).is_err());
194    }
195}