1use async_trait::async_trait;
18use std::time::Duration;
19
20use smix_error::{ExpectationFailure, FailureCode, FailureInit};
21use smix_host_coord_resolver::{HostResolveError, resolve_to_norm_coord};
22use smix_input::{KeyName, SwipeDirection};
23use smix_runner_client::{HttpRunnerClient, IncludeScope, OcrFrame, SystemPopup, TapMode};
24use smix_screen::{A11yNode, DEFAULT_VISIBLE_LIMIT, collect_visible_summaries};
25use smix_selector::{Selector, describe_selector};
26use smix_selector_resolver::{resolve_selector, resolve_selector_all};
27
28use crate::Orientation;
29use crate::traits::{Driver, Platform};
30
31pub struct AndroidDriver {
35 runner: HttpRunnerClient,
36 force_key_events: bool,
41}
42
43impl AndroidDriver {
44 #[must_use]
45 pub fn new(runner: HttpRunnerClient) -> Self {
46 AndroidDriver {
47 runner,
48 force_key_events: false,
49 }
50 }
51
52 pub fn runner(&self) -> &HttpRunnerClient {
53 &self.runner
54 }
55}
56
57async fn resolve_with_implicit_wait(
61 driver: &AndroidDriver,
62 selector: &Selector,
63 include: Option<IncludeScope>,
64) -> Result<(f64, f64), ExpectationFailure> {
65 let start = std::time::Instant::now();
66 let timeout = Duration::from_millis(5000);
67 loop {
68 let tree = driver.tree(include).await?;
69 match resolve_to_norm_coord(&tree, selector) {
70 Ok(coord) => return Ok(coord),
71 Err(HostResolveError::NotFound) => {
72 if start.elapsed() > timeout {
73 return Err(ExpectationFailure::new(FailureInit {
74 code: Some(FailureCode::ElementNotFound),
75 message: format!(
76 "AndroidDriver: element not found: {}",
77 describe_selector(selector)
78 ),
79 selector: Some(selector.clone()),
80 ..Default::default()
81 }));
82 }
83 tokio::time::sleep(Duration::from_millis(250)).await;
84 continue;
85 }
86 Err(e) => {
87 return Err(ExpectationFailure::new(FailureInit {
88 code: Some(FailureCode::DriverError),
89 message: format!("AndroidDriver: resolve error: {e:?}"),
90 selector: Some(selector.clone()),
91 ..Default::default()
92 }));
93 }
94 }
95 }
96}
97
98fn dispatch_unsupported_err() -> ExpectationFailure {
107 ExpectationFailure::new(FailureInit {
108 code: Some(FailureCode::DriverError),
109 message: "tapOn `dispatch:` is an iOS-runner mechanism and has no \
110 meaning on Android"
111 .to_string(),
112 hint: Some(
113 "remove `dispatch:` from this step — Android taps already use \
114 native event synthesis, which is what the override selects on iOS"
115 .to_string(),
116 ),
117 ..Default::default()
118 })
119}
120
121#[async_trait]
122impl Driver for AndroidDriver {
123 fn platform(&self) -> Platform {
124 Platform::Android
125 }
126
127 fn set_target_bundle_id(&mut self, bundle: &str) {
138 self.runner.set_target_bundle_id(bundle);
139 }
140
141 fn set_session_id(&mut self, id: Option<String>) {
150 match id {
151 Some(sid) => self.runner.set_session_id(sid),
152 None => self.runner.clear_session_id(),
153 }
154 }
155
156 async fn tree(&self, include: Option<IncludeScope>) -> Result<A11yNode, ExpectationFailure> {
159 self.runner.get_tree(include).await.map_err(|e| {
163 ExpectationFailure::new(FailureInit {
164 code: Some(FailureCode::DriverError),
165 message: format!("AndroidDriver::tree: {e}"),
166 ..Default::default()
167 })
168 })
169 }
170
171 async fn find(
172 &self,
173 selector: &Selector,
174 include: Option<IncludeScope>,
175 ) -> Result<bool, ExpectationFailure> {
176 let tree = self.tree(include).await?;
179 Ok(resolve_selector(&tree, selector).is_some())
180 }
181
182 async fn find_one(
183 &self,
184 selector: &Selector,
185 include: Option<IncludeScope>,
186 ) -> Result<Option<A11yNode>, ExpectationFailure> {
187 let tree = self.tree(include).await?;
188 Ok(resolve_selector(&tree, selector).cloned())
189 }
190
191 async fn find_all(
192 &self,
193 selector: &Selector,
194 include: Option<IncludeScope>,
195 ) -> Result<Vec<A11yNode>, ExpectationFailure> {
196 let tree = self.tree(include).await?;
197 Ok(resolve_selector_all(&tree, selector)
198 .into_iter()
199 .cloned()
200 .collect())
201 }
202
203 async fn find_norm_coord(
204 &self,
205 selector: &Selector,
206 ) -> Result<Option<(f64, f64)>, ExpectationFailure> {
207 let tree = self.tree(None).await?;
208 match resolve_to_norm_coord(&tree, selector) {
209 Ok(coord) => Ok(Some(coord)),
210 Err(HostResolveError::NotFound | HostResolveError::EmptyMatchedFrame) => Ok(None),
211 Err(HostResolveError::UnknownAppFrame) => Err(ExpectationFailure::new(FailureInit {
212 code: Some(FailureCode::DriverError),
213 message: "AndroidDriver::find_norm_coord: tree bounds w/h ≤ 0 (unknown app frame)"
214 .into(),
215 ..Default::default()
216 })),
217 Err(HostResolveError::CentroidOutOfFrame { .. }) => Ok(None),
218 }
219 }
220
221 async fn find_text_by_ocr(
222 &self,
223 text: &str,
224 locales: &[String],
225 recognition_level: &str,
226 ) -> Result<Option<OcrFrame>, ExpectationFailure> {
227 self.runner
232 .find_text_by_ocr(text, locales, recognition_level)
233 .await
234 .map_err(|e| {
235 ExpectationFailure::new(FailureInit {
236 code: Some(FailureCode::DriverError),
237 message: format!("AndroidDriver::find_text_by_ocr: {e}"),
238 ..Default::default()
239 })
240 })
241 }
242
243 async fn system_popups(
244 &self,
245 include: Option<IncludeScope>,
246 ) -> Result<Vec<SystemPopup>, ExpectationFailure> {
247 self.runner.system_popups(include).await.map_err(|e| {
251 ExpectationFailure::new(FailureInit {
252 code: Some(FailureCode::DriverError),
253 message: format!("AndroidDriver::system_popups: {e}"),
254 ..Default::default()
255 })
256 })
257 }
258
259 async fn system_popup_action(
260 &self,
261 popup_id: &str,
262 button_id: &str,
263 ) -> Result<bool, ExpectationFailure> {
264 self.runner
268 .system_popup_action(popup_id, button_id)
269 .await
270 .map_err(|e| {
271 ExpectationFailure::new(FailureInit {
272 code: Some(FailureCode::DriverError),
273 message: format!("AndroidDriver::system_popup_action: {e}"),
274 ..Default::default()
275 })
276 })
277 }
278
279 async fn wait_for(
280 &self,
281 selector: &Selector,
282 timeout: Duration,
283 include: Option<IncludeScope>,
284 ) -> Result<A11yNode, ExpectationFailure> {
285 let start = std::time::Instant::now();
288 loop {
289 let tree = self.tree(include).await?;
290 if let Some(node) = resolve_selector(&tree, selector) {
291 return Ok(node.clone());
292 }
293 if start.elapsed() >= timeout {
294 let candidates = collect_visible_summaries(&tree, DEFAULT_VISIBLE_LIMIT);
303 let target = crate::base_text_or_id(selector);
304 let suggestions = smix_error::build_suggestions(target.as_deref(), &candidates);
305 let visible = collect_visible_summaries(&tree, 10);
306 return Err(ExpectationFailure::new(FailureInit {
307 code: Some(FailureCode::ElementNotFound),
308 message: format!(
309 "AndroidDriver::wait_for timeout after {}ms: {}",
310 timeout.as_millis(),
311 describe_selector(selector)
312 ),
313 selector: Some(selector.clone()),
314 visible_elements: visible,
315 suggestions,
316 ..Default::default()
317 }));
318 }
319 tokio::time::sleep(Duration::from_millis(250)).await;
320 }
321 }
322
323 async fn tap(
326 &self,
327 selector: &Selector,
328 include: Option<IncludeScope>,
329 ) -> Result<crate::ActOutcome, ExpectationFailure> {
330 let (nx, ny) = resolve_with_implicit_wait(self, selector, include).await?;
332 self.runner
337 .tap_at_norm_coord(nx, ny)
338 .await
339 .map(|_| crate::ActOutcome::unjudged())
340 .map_err(|e| {
341 ExpectationFailure::new(FailureInit {
342 code: Some(FailureCode::DriverError),
343 message: format!("AndroidDriver::tap: runner.tap_at_norm_coord: {e}"),
344 ..Default::default()
345 })
346 })
347 }
348
349 async fn tap_with_mode(
350 &self,
351 _selector: &Selector,
352 _mode: TapMode,
353 _include: Option<IncludeScope>,
354 ) -> Result<(), ExpectationFailure> {
355 Err(dispatch_unsupported_err())
356 }
357
358 async fn tap_at_norm_coord(&self, nx: f64, ny: f64) -> Result<(), ExpectationFailure> {
359 self.runner
361 .tap_at_norm_coord(nx, ny)
362 .await
363 .map(|_| ())
364 .map_err(|e| {
365 ExpectationFailure::new(FailureInit {
366 code: Some(FailureCode::DriverError),
367 message: format!("AndroidDriver::tap_at_norm_coord: {e}"),
368 ..Default::default()
369 })
370 })
371 }
372
373 async fn tap_by_id(&self, id: &str) -> Result<(), ExpectationFailure> {
374 let ok = self.runner.tap_by_id(id).await.map_err(|e| {
377 ExpectationFailure::new(FailureInit {
378 code: Some(FailureCode::DriverError),
379 message: format!("AndroidDriver::tap_by_id: {e}"),
380 ..Default::default()
381 })
382 })?;
383 if !ok {
384 return Err(ExpectationFailure::new(FailureInit {
385 code: Some(FailureCode::ElementNotFound),
386 message: format!("AndroidDriver::tap_by_id: no element with resource-id '{id}'"),
387 ..Default::default()
388 }));
389 }
390 Ok(())
391 }
392
393 async fn double_tap(
394 &self,
395 selector: &Selector,
396 include: Option<IncludeScope>,
397 ) -> Result<(), ExpectationFailure> {
398 let (nx, ny) = resolve_with_implicit_wait(self, selector, include).await?;
401 self.runner
402 .double_tap_at_norm_coord(nx, ny)
403 .await
404 .map_err(|e| {
405 ExpectationFailure::new(FailureInit {
406 code: Some(FailureCode::DriverError),
407 message: format!("AndroidDriver::double_tap: {e}"),
408 ..Default::default()
409 })
410 })
411 }
412
413 async fn long_press(
414 &self,
415 selector: &Selector,
416 duration: Duration,
417 include: Option<IncludeScope>,
418 ) -> Result<crate::PressTiming, ExpectationFailure> {
419 let (nx, ny) = resolve_with_implicit_wait(self, selector, include).await?;
423 let duration_ms = duration.as_millis() as u64;
424 self.runner
429 .long_press_at_norm_coord(nx, ny, duration_ms)
430 .await
431 .map(|()| crate::PressTiming::unplaceable())
432 .map_err(|e| {
433 ExpectationFailure::new(FailureInit {
434 code: Some(FailureCode::DriverError),
435 message: format!("AndroidDriver::long_press: {e}"),
436 ..Default::default()
437 })
438 })
439 }
440
441 fn set_force_key_events(&mut self, force: bool) {
449 self.force_key_events = force;
450 }
451
452 async fn fill(
453 &self,
454 selector: &Selector,
455 text: &str,
456 include: Option<IncludeScope>,
457 ) -> Result<(), ExpectationFailure> {
458 if self.force_key_events {
459 return self.runner.input_text(text).await.map_err(|e| {
464 ExpectationFailure::new(FailureInit {
465 code: Some(FailureCode::DriverError),
466 message: format!("AndroidDriver::fill (key-events): {e}"),
467 ..Default::default()
468 })
469 });
470 }
471 let (nx, ny) = resolve_with_implicit_wait(self, selector, include).await?;
475 self.runner.tap_at_norm_coord(nx, ny).await.map_err(|e| {
476 ExpectationFailure::new(FailureInit {
477 code: Some(FailureCode::DriverError),
478 message: format!("AndroidDriver::fill: focus tap failed: {e}"),
479 ..Default::default()
480 })
481 })?;
482 self.runner.input_text(text).await.map_err(|e| {
483 ExpectationFailure::new(FailureInit {
484 code: Some(FailureCode::DriverError),
485 message: format!("AndroidDriver::fill: input_text failed: {e}"),
486 ..Default::default()
487 })
488 })
489 }
490
491 async fn clear(
492 &self,
493 selector: &Selector,
494 include: Option<IncludeScope>,
495 ) -> Result<(), ExpectationFailure> {
496 let (nx, ny) = resolve_with_implicit_wait(self, selector, include).await?;
500 self.runner.tap_at_norm_coord(nx, ny).await.map_err(|e| {
501 ExpectationFailure::new(FailureInit {
502 code: Some(FailureCode::DriverError),
503 message: format!("AndroidDriver::clear: focus tap failed: {e}"),
504 ..Default::default()
505 })
506 })?;
507 for _ in 0..50 {
508 self.runner.press_key(KeyName::Delete).await.map_err(|e| {
509 ExpectationFailure::new(FailureInit {
510 code: Some(FailureCode::DriverError),
511 message: format!("AndroidDriver::clear: delete press failed: {e}"),
512 ..Default::default()
513 })
514 })?;
515 }
516 Ok(())
517 }
518
519 async fn press_key(&self, key: KeyName) -> Result<(), ExpectationFailure> {
520 self.runner.press_key(key).await.map(|_| ()).map_err(|e| {
522 ExpectationFailure::new(FailureInit {
523 code: Some(FailureCode::DriverError),
524 message: format!("AndroidDriver::press_key: {e}"),
525 ..Default::default()
526 })
527 })
528 }
529
530 async fn scroll(
531 &self,
532 selector: &Selector,
533 direction: SwipeDirection,
534 ) -> Result<(), ExpectationFailure> {
535 let start = std::time::Instant::now();
538 let timeout = Duration::from_secs(20);
539 const MAX_SWIPES: u32 = 30;
540 for _ in 0..=MAX_SWIPES {
541 let tree = self.tree(None).await?;
542 if resolve_selector(&tree, selector).is_some() {
543 return Ok(());
544 }
545 if start.elapsed() > timeout {
546 break;
547 }
548 self.swipe_once(direction).await?;
549 }
550 Err(ExpectationFailure::new(FailureInit {
551 code: Some(FailureCode::ElementNotFound),
552 message: format!(
553 "AndroidDriver::scroll: element not visible after {} swipes: {}",
554 MAX_SWIPES,
555 describe_selector(selector)
556 ),
557 selector: Some(selector.clone()),
558 ..Default::default()
559 }))
560 }
561
562 async fn swipe_once(&self, direction: SwipeDirection) -> Result<(), ExpectationFailure> {
563 self.runner.swipe_once(direction).await.map_err(|e| {
564 ExpectationFailure::new(FailureInit {
565 code: Some(FailureCode::DriverError),
566 message: format!("AndroidDriver::swipe_once: {e}"),
567 ..Default::default()
568 })
569 })
570 }
571
572 async fn swipe_at_norm_coord(
573 &self,
574 from: (f64, f64),
575 to: (f64, f64),
576 ) -> Result<(), ExpectationFailure> {
577 self.runner
578 .swipe_at_norm_coord(from, to)
579 .await
580 .map_err(|e| {
581 ExpectationFailure::new(FailureInit {
582 code: Some(FailureCode::DriverError),
583 message: format!("AndroidDriver::swipe_at_norm_coord: {e}"),
584 ..Default::default()
585 })
586 })
587 }
588
589 async fn hide_keyboard(&self) -> Result<(), ExpectationFailure> {
590 self.runner.hide_keyboard().await.map_err(|e| {
591 ExpectationFailure::new(FailureInit {
592 code: Some(FailureCode::DriverError),
593 message: format!("AndroidDriver::hide_keyboard: {e}"),
594 ..Default::default()
595 })
596 })
597 }
598
599 async fn back(&self) -> Result<(), ExpectationFailure> {
600 self.runner.back().await.map_err(|e| {
602 ExpectationFailure::new(FailureInit {
603 code: Some(FailureCode::DriverError),
604 message: format!("AndroidDriver::back: {e}"),
605 ..Default::default()
606 })
607 })
608 }
609
610 async fn set_orientation(&self, orientation: Orientation) -> Result<(), ExpectationFailure> {
611 self.runner
612 .set_orientation(orientation.as_wire())
613 .await
614 .map_err(|e| {
615 ExpectationFailure::new(FailureInit {
616 code: Some(FailureCode::DriverError),
617 message: format!("AndroidDriver::set_orientation: {e}"),
618 ..Default::default()
619 })
620 })
621 }
622
623 async fn foreground(&self, bundle_id: &str) -> Result<(), ExpectationFailure> {
624 self.runner.foreground(bundle_id).await.map_err(|e| {
628 ExpectationFailure::new(FailureInit {
629 code: Some(FailureCode::DriverError),
630 message: format!("AndroidDriver::foreground: {e}"),
631 ..Default::default()
632 })
633 })
634 }
635
636 async fn webview_eval(&self, js: &str) -> Result<serde_json::Value, ExpectationFailure> {
637 self.runner.webview_eval_via_runner(js).await.map_err(|e| {
643 ExpectationFailure::new(FailureInit {
644 code: Some(FailureCode::DriverError),
645 message: format!("AndroidDriver::webview_eval: {e}"),
646 ..Default::default()
647 })
648 })
649 }
650}