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;
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}
37
38impl AndroidDriver {
39 #[must_use]
40 pub fn new(runner: HttpRunnerClient) -> Self {
41 AndroidDriver { runner }
42 }
43
44 pub fn runner(&self) -> &HttpRunnerClient {
45 &self.runner
46 }
47}
48
49async fn resolve_with_implicit_wait(
53 driver: &AndroidDriver,
54 selector: &Selector,
55 include: Option<IncludeScope>,
56) -> Result<(f64, f64), ExpectationFailure> {
57 let start = std::time::Instant::now();
58 let timeout = Duration::from_millis(5000);
59 loop {
60 let tree = driver.tree(include).await?;
61 match resolve_to_norm_coord(&tree, selector) {
62 Ok(coord) => return Ok(coord),
63 Err(HostResolveError::NotFound) => {
64 if start.elapsed() > timeout {
65 return Err(ExpectationFailure::new(FailureInit {
66 code: Some(FailureCode::ElementNotFound),
67 message: format!(
68 "AndroidDriver: element not found: {}",
69 describe_selector(selector)
70 ),
71 selector: Some(selector.clone()),
72 ..Default::default()
73 }));
74 }
75 tokio::time::sleep(Duration::from_millis(250)).await;
76 continue;
77 }
78 Err(e) => {
79 return Err(ExpectationFailure::new(FailureInit {
80 code: Some(FailureCode::DriverError),
81 message: format!("AndroidDriver: resolve error: {e:?}"),
82 selector: Some(selector.clone()),
83 ..Default::default()
84 }));
85 }
86 }
87 }
88}
89
90fn defer_err(method: &str) -> ExpectationFailure {
91 ExpectationFailure::new(FailureInit {
92 code: Some(FailureCode::DriverError),
93 message: format!(
94 "AndroidDriver::{method}: Kotlin runner endpoint not yet shipped (v6.0 c3b earned defer)"
95 ),
96 ..Default::default()
97 })
98}
99
100#[async_trait]
101impl Driver for AndroidDriver {
102 fn platform(&self) -> Platform {
103 Platform::Android
104 }
105
106 async fn tree(&self, include: Option<IncludeScope>) -> Result<A11yNode, ExpectationFailure> {
111 self.runner.get_tree(include).await.map_err(|e| {
115 ExpectationFailure::new(FailureInit {
116 code: Some(FailureCode::DriverError),
117 message: format!("AndroidDriver::tree: {e}"),
118 ..Default::default()
119 })
120 })
121 }
122
123 async fn find(
124 &self,
125 selector: &Selector,
126 include: Option<IncludeScope>,
127 ) -> Result<bool, ExpectationFailure> {
128 let tree = self.tree(include).await?;
131 Ok(resolve_selector(&tree, selector).is_some())
132 }
133
134 async fn find_one(
135 &self,
136 selector: &Selector,
137 include: Option<IncludeScope>,
138 ) -> Result<Option<A11yNode>, ExpectationFailure> {
139 let tree = self.tree(include).await?;
140 Ok(resolve_selector(&tree, selector).cloned())
141 }
142
143 async fn find_all(
144 &self,
145 selector: &Selector,
146 include: Option<IncludeScope>,
147 ) -> Result<Vec<A11yNode>, ExpectationFailure> {
148 let tree = self.tree(include).await?;
149 Ok(resolve_selector_all(&tree, selector)
150 .into_iter()
151 .cloned()
152 .collect())
153 }
154
155 async fn find_norm_coord(
156 &self,
157 selector: &Selector,
158 ) -> Result<Option<(f64, f64)>, ExpectationFailure> {
159 let tree = self.tree(None).await?;
160 match resolve_to_norm_coord(&tree, selector) {
161 Ok(coord) => Ok(Some(coord)),
162 Err(HostResolveError::NotFound | HostResolveError::EmptyMatchedFrame) => Ok(None),
163 Err(HostResolveError::UnknownAppFrame) => Err(ExpectationFailure::new(FailureInit {
164 code: Some(FailureCode::DriverError),
165 message: "AndroidDriver::find_norm_coord: tree bounds w/h ≤ 0 (unknown app frame)"
166 .into(),
167 ..Default::default()
168 })),
169 Err(HostResolveError::CentroidOutOfFrame { .. }) => Ok(None),
170 }
171 }
172
173 async fn find_text_by_ocr(
174 &self,
175 text: &str,
176 locales: &[String],
177 recognition_level: &str,
178 ) -> Result<Option<OcrFrame>, ExpectationFailure> {
179 self.runner
184 .find_text_by_ocr(text, locales, recognition_level)
185 .await
186 .map_err(|e| {
187 ExpectationFailure::new(FailureInit {
188 code: Some(FailureCode::DriverError),
189 message: format!("AndroidDriver::find_text_by_ocr: {e}"),
190 ..Default::default()
191 })
192 })
193 }
194
195 async fn system_popups(
196 &self,
197 include: Option<IncludeScope>,
198 ) -> Result<Vec<SystemPopup>, ExpectationFailure> {
199 self.runner.system_popups(include).await.map_err(|e| {
203 ExpectationFailure::new(FailureInit {
204 code: Some(FailureCode::DriverError),
205 message: format!("AndroidDriver::system_popups: {e}"),
206 ..Default::default()
207 })
208 })
209 }
210
211 async fn system_popup_action(
212 &self,
213 popup_id: &str,
214 button_id: &str,
215 ) -> Result<bool, ExpectationFailure> {
216 self.runner
220 .system_popup_action(popup_id, button_id)
221 .await
222 .map_err(|e| {
223 ExpectationFailure::new(FailureInit {
224 code: Some(FailureCode::DriverError),
225 message: format!("AndroidDriver::system_popup_action: {e}"),
226 ..Default::default()
227 })
228 })
229 }
230
231 async fn wait_for(
232 &self,
233 selector: &Selector,
234 timeout: Duration,
235 include: Option<IncludeScope>,
236 ) -> Result<A11yNode, ExpectationFailure> {
237 let start = std::time::Instant::now();
240 loop {
241 let tree = self.tree(include).await?;
242 if let Some(node) = resolve_selector(&tree, selector) {
243 return Ok(node.clone());
244 }
245 if start.elapsed() >= timeout {
246 return Err(ExpectationFailure::new(FailureInit {
247 code: Some(FailureCode::ElementNotFound),
248 message: format!(
249 "AndroidDriver::wait_for timeout after {}ms: {}",
250 timeout.as_millis(),
251 describe_selector(selector)
252 ),
253 selector: Some(selector.clone()),
254 ..Default::default()
255 }));
256 }
257 tokio::time::sleep(Duration::from_millis(250)).await;
258 }
259 }
260
261 async fn tap(
264 &self,
265 selector: &Selector,
266 include: Option<IncludeScope>,
267 ) -> Result<(), ExpectationFailure> {
268 let (nx, ny) = resolve_with_implicit_wait(self, selector, include).await?;
271 self.runner.tap_at_norm_coord(nx, ny).await.map_err(|e| {
272 ExpectationFailure::new(FailureInit {
273 code: Some(FailureCode::DriverError),
274 message: format!("AndroidDriver::tap: runner.tap_at_norm_coord: {e}"),
275 ..Default::default()
276 })
277 })
278 }
279
280 async fn tap_with_mode(
281 &self,
282 _selector: &Selector,
283 _mode: TapMode,
284 _include: Option<IncludeScope>,
285 ) -> Result<(), ExpectationFailure> {
286 Err(defer_err("tap_with_mode"))
287 }
288
289 async fn tap_at_norm_coord(&self, nx: f64, ny: f64) -> Result<(), ExpectationFailure> {
290 self.runner.tap_at_norm_coord(nx, ny).await.map_err(|e| {
292 ExpectationFailure::new(FailureInit {
293 code: Some(FailureCode::DriverError),
294 message: format!("AndroidDriver::tap_at_norm_coord: {e}"),
295 ..Default::default()
296 })
297 })
298 }
299
300 async fn tap_by_id(&self, id: &str) -> Result<(), ExpectationFailure> {
301 let ok = self.runner.tap_by_id(id).await.map_err(|e| {
304 ExpectationFailure::new(FailureInit {
305 code: Some(FailureCode::DriverError),
306 message: format!("AndroidDriver::tap_by_id: {e}"),
307 ..Default::default()
308 })
309 })?;
310 if !ok {
311 return Err(ExpectationFailure::new(FailureInit {
312 code: Some(FailureCode::ElementNotFound),
313 message: format!("AndroidDriver::tap_by_id: no element with resource-id '{id}'"),
314 ..Default::default()
315 }));
316 }
317 Ok(())
318 }
319
320 async fn double_tap(
321 &self,
322 selector: &Selector,
323 include: Option<IncludeScope>,
324 ) -> Result<(), ExpectationFailure> {
325 let (nx, ny) = resolve_with_implicit_wait(self, selector, include).await?;
328 self.runner
329 .double_tap_at_norm_coord(nx, ny)
330 .await
331 .map_err(|e| {
332 ExpectationFailure::new(FailureInit {
333 code: Some(FailureCode::DriverError),
334 message: format!("AndroidDriver::double_tap: {e}"),
335 ..Default::default()
336 })
337 })
338 }
339
340 async fn long_press(
341 &self,
342 selector: &Selector,
343 duration: Duration,
344 include: Option<IncludeScope>,
345 ) -> Result<(), ExpectationFailure> {
346 let (nx, ny) = resolve_with_implicit_wait(self, selector, include).await?;
350 let duration_ms = duration.as_millis() as u64;
351 self.runner
352 .long_press_at_norm_coord(nx, ny, duration_ms)
353 .await
354 .map_err(|e| {
355 ExpectationFailure::new(FailureInit {
356 code: Some(FailureCode::DriverError),
357 message: format!("AndroidDriver::long_press: {e}"),
358 ..Default::default()
359 })
360 })
361 }
362
363 async fn fill(
364 &self,
365 selector: &Selector,
366 text: &str,
367 include: Option<IncludeScope>,
368 ) -> Result<(), ExpectationFailure> {
369 let (nx, ny) = resolve_with_implicit_wait(self, selector, include).await?;
373 self.runner.tap_at_norm_coord(nx, ny).await.map_err(|e| {
374 ExpectationFailure::new(FailureInit {
375 code: Some(FailureCode::DriverError),
376 message: format!("AndroidDriver::fill: focus tap failed: {e}"),
377 ..Default::default()
378 })
379 })?;
380 self.runner.input_text(text).await.map_err(|e| {
381 ExpectationFailure::new(FailureInit {
382 code: Some(FailureCode::DriverError),
383 message: format!("AndroidDriver::fill: input_text failed: {e}"),
384 ..Default::default()
385 })
386 })
387 }
388
389 async fn clear(
390 &self,
391 selector: &Selector,
392 include: Option<IncludeScope>,
393 ) -> Result<(), ExpectationFailure> {
394 let (nx, ny) = resolve_with_implicit_wait(self, selector, include).await?;
398 self.runner.tap_at_norm_coord(nx, ny).await.map_err(|e| {
399 ExpectationFailure::new(FailureInit {
400 code: Some(FailureCode::DriverError),
401 message: format!("AndroidDriver::clear: focus tap failed: {e}"),
402 ..Default::default()
403 })
404 })?;
405 for _ in 0..50 {
406 self.runner.press_key(KeyName::Delete).await.map_err(|e| {
407 ExpectationFailure::new(FailureInit {
408 code: Some(FailureCode::DriverError),
409 message: format!("AndroidDriver::clear: delete press failed: {e}"),
410 ..Default::default()
411 })
412 })?;
413 }
414 Ok(())
415 }
416
417 async fn press_key(&self, key: KeyName) -> Result<(), ExpectationFailure> {
418 self.runner.press_key(key).await.map(|_| ()).map_err(|e| {
420 ExpectationFailure::new(FailureInit {
421 code: Some(FailureCode::DriverError),
422 message: format!("AndroidDriver::press_key: {e}"),
423 ..Default::default()
424 })
425 })
426 }
427
428 async fn scroll(
429 &self,
430 selector: &Selector,
431 direction: SwipeDirection,
432 ) -> Result<(), ExpectationFailure> {
433 let start = std::time::Instant::now();
436 let timeout = Duration::from_secs(20);
437 const MAX_SWIPES: u32 = 30;
438 for _ in 0..=MAX_SWIPES {
439 let tree = self.tree(None).await?;
440 if resolve_selector(&tree, selector).is_some() {
441 return Ok(());
442 }
443 if start.elapsed() > timeout {
444 break;
445 }
446 self.swipe_once(direction).await?;
447 }
448 Err(ExpectationFailure::new(FailureInit {
449 code: Some(FailureCode::ElementNotFound),
450 message: format!(
451 "AndroidDriver::scroll: element not visible after {} swipes: {}",
452 MAX_SWIPES,
453 describe_selector(selector)
454 ),
455 selector: Some(selector.clone()),
456 ..Default::default()
457 }))
458 }
459
460 async fn swipe_once(&self, direction: SwipeDirection) -> Result<(), ExpectationFailure> {
461 self.runner.swipe_once(direction).await.map_err(|e| {
462 ExpectationFailure::new(FailureInit {
463 code: Some(FailureCode::DriverError),
464 message: format!("AndroidDriver::swipe_once: {e}"),
465 ..Default::default()
466 })
467 })
468 }
469
470 async fn swipe_at_norm_coord(
471 &self,
472 from: (f64, f64),
473 to: (f64, f64),
474 ) -> Result<(), ExpectationFailure> {
475 self.runner
476 .swipe_at_norm_coord(from, to)
477 .await
478 .map_err(|e| {
479 ExpectationFailure::new(FailureInit {
480 code: Some(FailureCode::DriverError),
481 message: format!("AndroidDriver::swipe_at_norm_coord: {e}"),
482 ..Default::default()
483 })
484 })
485 }
486
487 async fn hide_keyboard(&self) -> Result<(), ExpectationFailure> {
488 self.runner.hide_keyboard().await.map_err(|e| {
489 ExpectationFailure::new(FailureInit {
490 code: Some(FailureCode::DriverError),
491 message: format!("AndroidDriver::hide_keyboard: {e}"),
492 ..Default::default()
493 })
494 })
495 }
496
497 async fn back(&self) -> Result<(), ExpectationFailure> {
498 self.runner.back().await.map_err(|e| {
500 ExpectationFailure::new(FailureInit {
501 code: Some(FailureCode::DriverError),
502 message: format!("AndroidDriver::back: {e}"),
503 ..Default::default()
504 })
505 })
506 }
507
508 async fn set_orientation(&self, orientation: Orientation) -> Result<(), ExpectationFailure> {
509 self.runner
510 .set_orientation(orientation.as_wire())
511 .await
512 .map_err(|e| {
513 ExpectationFailure::new(FailureInit {
514 code: Some(FailureCode::DriverError),
515 message: format!("AndroidDriver::set_orientation: {e}"),
516 ..Default::default()
517 })
518 })
519 }
520
521 async fn foreground(&self, bundle_id: &str) -> Result<(), ExpectationFailure> {
522 self.runner.foreground(bundle_id).await.map_err(|e| {
526 ExpectationFailure::new(FailureInit {
527 code: Some(FailureCode::DriverError),
528 message: format!("AndroidDriver::foreground: {e}"),
529 ..Default::default()
530 })
531 })
532 }
533
534 async fn webview_eval(&self, js: &str) -> Result<serde_json::Value, ExpectationFailure> {
535 self.runner.webview_eval(js).await.map_err(|e| {
540 ExpectationFailure::new(FailureInit {
541 code: Some(FailureCode::DriverError),
542 message: format!("AndroidDriver::webview_eval: {e}"),
543 ..Default::default()
544 })
545 })
546 }
547}