Skip to main content

droidrun_core/ui/
provider.rs

1/// StateProvider — orchestrates fetching and parsing device state.
2use tracing::{debug, warn};
3
4use crate::driver::DeviceDriver;
5use crate::error::{DroidrunError, Result};
6use crate::ui::filter::TreeFilter;
7use crate::ui::formatter::TreeFormatter;
8use crate::ui::state::{ScreenDimensions, UIState};
9
10/// Fetches state from an Android device, applies filters and formatters.
11pub struct AndroidStateProvider<F: TreeFilter, M: TreeFormatter> {
12    filter: F,
13    formatter: M,
14    use_normalized: bool,
15}
16
17impl<F: TreeFilter, M: TreeFormatter> AndroidStateProvider<F, M> {
18    pub fn new(filter: F, formatter: M, use_normalized: bool) -> Self {
19        Self {
20            filter,
21            formatter,
22            use_normalized,
23        }
24    }
25
26    /// Fetch and process the current UI state.
27    ///
28    /// Includes retry logic (3 attempts).
29    pub async fn get_state(&self, driver: &dyn DeviceDriver) -> Result<UIState> {
30        let max_retries = 3;
31        let mut last_error = None;
32
33        for attempt in 1..=max_retries {
34            debug!("getting state (attempt {attempt}/{max_retries})");
35
36            match self.get_state_inner(driver).await {
37                Ok(state) => return Ok(state),
38                Err(e) => {
39                    warn!("get_state attempt {attempt} failed: {e}");
40                    last_error = Some(e);
41                    if attempt < max_retries {
42                        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
43                    }
44                }
45            }
46        }
47
48        Err(last_error.unwrap_or_else(|| {
49            DroidrunError::PortalCommError("get_state failed after retries".into())
50        }))
51    }
52
53    async fn get_state_inner(&self, driver: &dyn DeviceDriver) -> Result<UIState> {
54        let combined = driver.get_ui_tree().await?;
55
56        // Check for error response
57        if combined.get("error").is_some() {
58            let msg = combined
59                .get("message")
60                .and_then(|v| v.as_str())
61                .unwrap_or("Unknown error");
62            return Err(DroidrunError::PortalCommError(format!(
63                "Portal returned error: {msg}"
64            )));
65        }
66
67        // Validate required keys
68        for key in &["a11y_tree", "phone_state", "device_context"] {
69            if combined.get(*key).is_none() {
70                return Err(DroidrunError::Parse(format!("Missing data in state: {key}")));
71            }
72        }
73
74        let device_context = &combined["device_context"];
75        let screen_bounds = device_context
76            .get("screen_bounds")
77            .cloned()
78            .unwrap_or_default();
79        let screen_width = screen_bounds
80            .get("width")
81            .and_then(|v| v.as_i64())
82            .unwrap_or(1080) as i32;
83        let screen_height = screen_bounds
84            .get("height")
85            .and_then(|v| v.as_i64())
86            .unwrap_or(2400) as i32;
87
88        // Filter tree
89        let filtered = self
90            .filter
91            .filter(&combined["a11y_tree"], device_context);
92
93        // Format
94        let (formatted_text, focused_text, elements, phone_state) = self.formatter.format(
95            filtered.as_ref(),
96            &combined["phone_state"],
97            screen_width,
98            screen_height,
99            self.use_normalized,
100        );
101
102        Ok(UIState::new(
103            elements,
104            formatted_text,
105            focused_text,
106            phone_state,
107            ScreenDimensions {
108                width: screen_width,
109                height: screen_height,
110            },
111            self.use_normalized,
112        ))
113    }
114}