1use std::fmt;
18
19use super::ControllerLink;
20use crate::emulator::{self, Endpoint, Options};
21
22#[derive(Clone, Debug, Default, PartialEq, Eq)]
24pub enum ControllerChoice {
25 #[default]
28 Auto,
29 Off,
31 Endpoint(Endpoint),
33}
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum Path {
38 Controller,
40 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#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct Paths {
56 pub emulator: bool,
57 pub input: Path,
59 pub frames: Option<Path>,
62 pub screenshots: Path,
63 pub keys: Path,
65 pub keyboard: Option<bool>,
68 pub endpoint: Option<String>,
70 pub warning: Option<String>,
72}
73
74impl Paths {
75 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 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#[derive(Debug)]
106pub struct Selection {
107 pub link: Option<ControllerLink>,
108 pub paths: Paths,
109}
110
111pub 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#[derive(Clone, Debug, PartialEq, Eq)]
147pub struct Status {
148 pub serial: String,
149 pub agent: String,
151 pub protocol: u16,
152 pub pid: u32,
153 pub socket: String,
154 pub reused: bool,
156 pub paths: Paths,
159 pub inputs: (u64, u64),
161 pub frames_received: u64,
162 pub controller_error: Option<String>,
164 pub frame_error: Option<String>,
166 pub disconnected: Option<String>,
168}
169
170impl Status {
171 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 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 #[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 #[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 #[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}