Skip to main content

android/session/
paths.rs

1//! Path selection (android-driver requirement "Emulator fast path"; design
2//! D7): whether taps, swipes and frames go through the emulator's controller
3//! or the agent, and the status that says which.
4//!
5//! - On an emulator whose control endpoint resolves and answers, taps,
6//!   swipes and the frame stream use the controller.
7//! - Without one, input goes through the agent, there is no frame stream,
8//!   and the status carries the warning that names the launch flag and the
9//!   console command that enables the endpoint without a restart.
10//! - Keys and text always go through the agent: the controller accepts and
11//!   drops keys unless the AVD declares a keyboard, and even then it cannot
12//!   say whether the platform accepted them. The status reports whether the
13//!   AVD declares one.
14//! - Screenshots go through the agent, which returns them upright in every
15//!   display orientation.
16
17use std::fmt;
18
19use super::ControllerLink;
20use crate::emulator::{self, Endpoint, Options};
21
22/// Whether to use the emulator's controller.
23#[derive(Clone, Debug, Default, PartialEq, Eq)]
24pub enum ControllerChoice {
25    /// Resolve the endpoint from the emulator's discovery file and use it
26    /// when it answers.
27    #[default]
28    Auto,
29    /// Never use it.
30    Off,
31    /// Use this endpoint.
32    Endpoint(Endpoint),
33}
34
35/// Where a kind of input or output goes.
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum Path {
38    /// The emulator's gRPC controller: the fast path.
39    Controller,
40    /// The resident agent.
41    Agent,
42}
43
44impl fmt::Display for Path {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        f.write_str(match self {
47            Path::Controller => "emulator controller",
48            Path::Agent => "agent",
49        })
50    }
51}
52
53/// The paths chosen for a target.
54#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct Paths {
56    pub emulator: bool,
57    /// Taps, long presses and swipes.
58    pub input: Path,
59    /// The frame stream; `None` when there is none, as on devices and
60    /// emulators without the controller.
61    pub frames: Option<Path>,
62    pub screenshots: Path,
63    /// Keys and text.
64    pub keys: Path,
65    /// Whether the AVD declares a keyboard (`hw.keyboard`), as the controller
66    /// reports it; `None` without a controller.
67    pub keyboard: Option<bool>,
68    /// The controller's address, when it is used.
69    pub endpoint: Option<String>,
70    /// Why the fast path is not used on an emulator, and how to enable it.
71    pub warning: Option<String>,
72}
73
74impl Paths {
75    /// Everything through the agent; `warning` says why, on an emulator.
76    pub fn agent(emulator: bool, warning: Option<String>) -> Paths {
77        Paths {
78            emulator,
79            input: Path::Agent,
80            frames: None,
81            screenshots: Path::Agent,
82            keys: Path::Agent,
83            keyboard: None,
84            endpoint: None,
85            warning,
86        }
87    }
88
89    /// Taps, swipes and frames through `link`'s controller.
90    pub fn controller(link: &ControllerLink) -> Paths {
91        Paths {
92            emulator: true,
93            input: Path::Controller,
94            frames: Some(Path::Controller),
95            screenshots: Path::Agent,
96            keys: Path::Agent,
97            keyboard: Some(link.keyboard()),
98            endpoint: Some(link.endpoint().to_owned()),
99            warning: None,
100        }
101    }
102}
103
104/// The controller to use, if any, and the paths that follow.
105#[derive(Debug)]
106pub struct Selection {
107    pub link: Option<ControllerLink>,
108    pub paths: Paths,
109}
110
111/// Selects the paths for the target `serial`, an emulator when `emulator`
112/// says so: the controller as `choice` says when it connects within
113/// `options`' deadlines, else the agent, with the warning on an emulator
114/// whose controller was wanted.
115pub fn select_paths(
116    serial: &str,
117    emulator: bool,
118    choice: &ControllerChoice,
119    options: Options,
120) -> Selection {
121    let fallback = |warning: Option<String>| Selection {
122        link: None,
123        paths: Paths::agent(emulator, warning),
124    };
125    if !emulator {
126        return fallback(None);
127    }
128    let endpoint = match choice {
129        ControllerChoice::Off => return fallback(None),
130        ControllerChoice::Endpoint(endpoint) => endpoint.clone(),
131        ControllerChoice::Auto => match emulator::resolve(serial) {
132            Ok(endpoint) => endpoint,
133            Err(unavailable) => return fallback(Some(unavailable.warning())),
134        },
135    };
136    match ControllerLink::connect(&endpoint, options) {
137        Ok(link) => Selection {
138            paths: Paths::controller(&link),
139            link: Some(link),
140        },
141        Err(unavailable) => fallback(Some(unavailable.warning())),
142    }
143}
144
145/// What a session reports about its target.
146#[derive(Clone, Debug, PartialEq, Eq)]
147pub struct Status {
148    pub serial: String,
149    /// The agent name and version its hello carried.
150    pub agent: String,
151    pub protocol: u16,
152    pub pid: u32,
153    pub socket: String,
154    /// Whether the agent was reused rather than launched.
155    pub reused: bool,
156    /// The paths as they are now: input moves to the agent when the
157    /// controller fails during the session.
158    pub paths: Paths,
159    /// Inputs sent through the controller and through the agent.
160    pub inputs: (u64, u64),
161    pub frames_received: u64,
162    /// Why the controller stopped taking input, once it failed.
163    pub controller_error: Option<String>,
164    /// The last failure of the frame stream.
165    pub frame_error: Option<String>,
166    /// Why the connection to the agent closed, once it has.
167    pub disconnected: Option<String>,
168}
169
170impl Status {
171    /// The status as lines of text: the agent, then one line per path, then
172    /// the warnings.
173    pub fn lines(&self) -> Vec<String> {
174        let paths = &self.paths;
175        let how = if self.reused { "reused" } else { "launched" };
176        let mut lines = vec![format!(
177            "{}: {} over protocol {}, pid {}, socket {}, {how}",
178            self.serial, self.agent, self.protocol, self.pid, self.socket
179        )];
180        let mut input = match (paths.input, &paths.endpoint) {
181            (Path::Controller, Some(endpoint)) => {
182                format!("input: emulator controller at {endpoint}")
183            }
184            (path, _) => format!("input: {path}"),
185        };
186        input.push_str(" (taps, long presses and swipes)");
187        if let Some(error) = &self.controller_error {
188            input.push_str(&format!(
189                "; the emulator controller failed ({error}), so input goes through the agent"
190            ));
191        }
192        lines.push(input);
193        lines.push(match paths.frames {
194            Some(path) => format!("frames: {path}, {} received", self.frames_received),
195            None => "frames: none; screenshots come from the agent on demand".to_owned(),
196        });
197        lines.push(format!("screenshots: {}", paths.screenshots));
198        let keyboard = match paths.keyboard {
199            Some(false) => "; the AVD declares no keyboard, so the controller would drop keys",
200            Some(true) => {
201                "; the AVD declares a keyboard, but only the agent says whether a key was accepted"
202            }
203            None => "",
204        };
205        lines.push(format!("keys and text: {}{keyboard}", paths.keys));
206        if let Some(warning) = &paths.warning {
207            lines.push(format!("warning: {warning}"));
208        }
209        if let Some(error) = &self.frame_error {
210            lines.push(format!(
211                "warning: the frame stream failed ({error}); it is subscribed to again"
212            ));
213        }
214        if let Some(reason) = &self.disconnected {
215            lines.push(format!("disconnected: {reason}"));
216        }
217        lines
218    }
219}
220
221impl fmt::Display for Status {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        f.write_str(&self.lines().join("\n"))
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use std::time::{Duration, Instant};
231
232    fn quick() -> Options {
233        Options {
234            deadline: Duration::from_millis(500),
235            connect_deadline: Duration::from_millis(500),
236        }
237    }
238
239    /// An endpoint on a port nothing listens on.
240    fn closed_endpoint(serial: &str) -> Endpoint {
241        let port = {
242            let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
243            listener.local_addr().unwrap().port()
244        };
245        Endpoint {
246            serial: serial.into(),
247            uri: format!("http://127.0.0.1:{port}"),
248            token: Some("token".into()),
249            console_port: Some(5554),
250            discovery_file: None,
251        }
252    }
253
254    fn status(paths: Paths) -> Status {
255        Status {
256            serial: "emulator-5554".into(),
257            agent: "mobile-agent-android 0.1.0".into(),
258            protocol: 1,
259            pid: 812,
260            socket: "mobile-agent-0.1.0-p1-00ff".into(),
261            reused: false,
262            paths,
263            inputs: (0, 0),
264            frames_received: 0,
265            controller_error: None,
266            frame_error: None,
267            disconnected: None,
268        }
269    }
270
271    fn controller_paths(keyboard: bool) -> Paths {
272        Paths {
273            emulator: true,
274            input: Path::Controller,
275            frames: Some(Path::Controller),
276            screenshots: Path::Agent,
277            keys: Path::Agent,
278            keyboard: Some(keyboard),
279            endpoint: Some("http://127.0.0.1:8554".into()),
280            warning: None,
281        }
282    }
283
284    #[test]
285    fn a_device_uses_the_agent_without_a_warning() {
286        let selection = select_paths("R58M123ABC", false, &ControllerChoice::Auto, quick());
287        assert!(selection.link.is_none());
288        assert_eq!(selection.paths, Paths::agent(false, None));
289        assert_eq!(selection.paths.input, Path::Agent);
290        assert_eq!(selection.paths.frames, None);
291        assert_eq!(selection.paths.warning, None);
292    }
293
294    #[test]
295    fn an_emulator_whose_controller_is_turned_off_uses_the_agent_without_a_warning() {
296        let selection = select_paths("emulator-5554", true, &ControllerChoice::Off, quick());
297        assert!(selection.link.is_none());
298        assert_eq!(selection.paths, Paths::agent(true, None));
299    }
300
301    /// Spec scenario "Control endpoint disabled", its selection: a closed
302    /// port stands for an emulator started without the control flag.
303    #[test]
304    fn an_emulator_whose_controller_does_not_answer_falls_back_with_the_launch_flag() {
305        let endpoint = closed_endpoint("emulator-5554");
306        let started = Instant::now();
307        let selection = select_paths(
308            "emulator-5554",
309            true,
310            &ControllerChoice::Endpoint(endpoint),
311            quick(),
312        );
313        assert!(started.elapsed() < Duration::from_secs(2));
314        assert!(selection.link.is_none());
315        let paths = &selection.paths;
316        assert_eq!(
317            (paths.input, paths.frames, paths.keys),
318            (Path::Agent, None, Path::Agent)
319        );
320        let warning = paths.warning.as_deref().expect("a warning");
321        assert!(warning.contains("-grpc 8554 -grpc-use-token"), "{warning}");
322        assert!(warning.contains("`grpc 8554`"), "{warning}");
323        assert!(warning.contains("without a restart"), "{warning}");
324    }
325
326    #[test]
327    fn the_agent_paths_send_everything_to_the_agent() {
328        let paths = Paths::agent(true, Some("no endpoint".into()));
329        assert_eq!(paths.input, Path::Agent);
330        assert_eq!(paths.frames, None);
331        assert_eq!(paths.screenshots, Path::Agent);
332        assert_eq!(paths.keys, Path::Agent);
333        assert_eq!(paths.keyboard, None);
334        assert_eq!(paths.endpoint, None);
335        assert_eq!(paths.warning.as_deref(), Some("no endpoint"));
336    }
337
338    /// Spec scenario "Control endpoint enabled", its status: the fast path
339    /// is named, and keys stay with the agent with the keyboard reported.
340    #[test]
341    fn the_status_names_the_fast_path_and_the_keyboard() {
342        let text = status(controller_paths(false)).to_string();
343        assert!(
344            text.contains("input: emulator controller at http://127.0.0.1:8554"),
345            "{text}"
346        );
347        assert!(text.contains("frames: emulator controller"), "{text}");
348        assert!(text.contains("screenshots: agent"), "{text}");
349        assert!(text.contains("keys and text: agent"), "{text}");
350        assert!(text.contains("declares no keyboard"), "{text}");
351        assert!(!text.contains("warning"), "{text}");
352        let with_keyboard = status(controller_paths(true)).to_string();
353        assert!(
354            with_keyboard.contains("declares a keyboard"),
355            "{with_keyboard}"
356        );
357    }
358
359    /// Spec scenario "Control endpoint disabled", its status: the agent path
360    /// is named and the warning names the launch flag.
361    #[test]
362    fn the_status_names_the_agent_path_and_carries_the_warning() {
363        let warning = emulator::Unavailable::new("emulator-5554", Some(5554), "refused").warning();
364        let text = status(Paths::agent(true, Some(warning))).to_string();
365        assert!(text.contains("input: agent"), "{text}");
366        assert!(text.contains("frames: none"), "{text}");
367        assert!(text.contains("screenshots: agent"), "{text}");
368        assert!(text.contains("warning: "), "{text}");
369        assert!(text.contains("-grpc 8554 -grpc-use-token"), "{text}");
370    }
371
372    #[test]
373    fn the_status_names_the_agent_its_version_and_whether_it_was_reused() {
374        let mut reused = status(Paths::agent(false, None));
375        reused.reused = true;
376        let first = reused.lines().remove(0);
377        assert!(
378            first.starts_with("emulator-5554: mobile-agent-android 0.1.0"),
379            "{first}"
380        );
381        assert!(first.contains("protocol 1"), "{first}");
382        assert!(first.contains("pid 812"), "{first}");
383        assert!(first.contains("reused"), "{first}");
384    }
385
386    #[test]
387    fn a_controller_failure_and_a_lost_agent_are_reported() {
388        let mut failed = status(controller_paths(false));
389        failed.controller_error = Some("sendTouch: no answer within 2s".into());
390        failed.paths.input = Path::Agent;
391        failed.disconnected = Some("the agent is disconnected (eof)".into());
392        let text = failed.to_string();
393        assert!(text.contains("input: agent"), "{text}");
394        assert!(text.contains("sendTouch: no answer within 2s"), "{text}");
395        assert!(text.contains("disconnected"), "{text}");
396    }
397}