1use std::sync::atomic::Ordering;
10use std::sync::{Arc, PoisonError};
11use std::time::{Duration, Instant};
12
13use mirror::backend::{
14 BackendError, BackendInfo, NodeHandle, ProbedNode, ReadReason, ReadRequest, TreeRead,
15 TreeSource,
16};
17use protocol::event::Stream;
18use protocol::message::{BoundsParams, Call, Reply, SnapshotParams};
19use protocol::snapshot::{Node, Snapshot};
20use protocol::{Capabilities, code};
21
22use super::{INPUT_QUIET, Inner, ReadOptions, ReadStats};
23
24pub const BACKEND: &str = "android-agent";
26
27pub const PROBE_DEADLINE: Duration = Duration::from_millis(500);
31
32pub fn scale_from_density(wm_density: &str) -> Option<f64> {
35 let density = |label: &str| {
36 wm_density
37 .lines()
38 .find_map(|line| line.trim().strip_prefix(label))
39 .and_then(|value| value.trim().parse::<f64>().ok())
40 .filter(|dpi| *dpi > 0.0)
41 };
42 density("Override density:")
43 .or_else(|| density("Physical density:"))
44 .map(|dpi| dpi / 160.0)
45}
46
47pub fn has_event_less_window(snapshot: &Snapshot) -> bool {
51 snapshot
52 .windows
53 .iter()
54 .any(|window| window.emits_events == Some(false))
55}
56
57pub fn snapshot_params(reason: ReadReason, options: &ReadOptions) -> SnapshotParams {
61 let idle_ms = match reason {
62 ReadReason::Attach | ReadReason::Action | ReadReason::Resync => options.settle_idle_ms,
63 ReadReason::Events | ReadReason::Poll | ReadReason::Frames | ReadReason::Moved => {
64 Some(options.idle_ms)
65 }
66 };
67 SnapshotParams {
68 max_depth: options.max_depth,
69 max_nodes: options.max_nodes,
70 idle_ms,
71 ..SnapshotParams::default()
72 }
73}
74
75pub(crate) fn serialized_nodes(snapshot: &Snapshot) -> u32 {
77 fn count(node: &Node) -> u32 {
78 1 + node.children.iter().map(count).sum::<u32>()
79 }
80 snapshot
81 .windows
82 .iter()
83 .filter_map(|w| w.root.as_ref())
84 .map(count)
85 .sum()
86}
87
88pub(crate) struct AgentTree {
90 pub(crate) inner: Arc<Inner>,
91}
92
93impl TreeSource for AgentTree {
94 fn read(&mut self, request: &ReadRequest) -> Result<TreeRead, BackendError> {
95 let inner = &*self.inner;
96 let started = Instant::now();
97 let deadline = inner.options.read.deadline;
98 let failed: Vec<Stream> = inner
101 .failed_resyncs
102 .lock()
103 .unwrap_or_else(PoisonError::into_inner)
104 .drain(..)
105 .collect();
106 for stream in failed {
107 let _ = inner.connection.resync(stream);
108 }
109 let mut cleared_cache = false;
110 let mut params = snapshot_params(request.reason, &inner.options.read);
111 if request.reason == ReadReason::Moved {
112 params.evict = request.windows.clone();
115 cleared_cache = true;
116 }
117 let foreign = inner
118 .foreign_input
119 .lock()
120 .unwrap_or_else(PoisonError::into_inner)
121 .take();
122 if let Some(input) = foreign {
123 let quiet = input + INPUT_QUIET;
128 if let Some(wait) = quiet.checked_duration_since(Instant::now()) {
129 std::thread::sleep(wait);
130 }
131 if inner.event_less.load(Ordering::SeqCst) {
132 params.evict_event_less = Some(true);
133 cleared_cache = true;
134 }
135 }
136 let Reply::Snapshot(snapshot) = inner.connection.call(Call::Snapshot(params), deadline)?
137 else {
138 return Err(BackendError::new(
139 code::PROTOCOL_ERROR,
140 "the agent answered a snapshot request with another result",
141 ));
142 };
143 inner
144 .event_less
145 .store(has_event_less_window(&snapshot), Ordering::SeqCst);
146 inner.reads.fetch_add(1, Ordering::SeqCst);
147 *inner
148 .last_read
149 .lock()
150 .unwrap_or_else(PoisonError::into_inner) = Some(ReadStats {
151 node_count: snapshot.node_count,
152 serialized: serialized_nodes(&snapshot),
153 windows: u32::try_from(snapshot.windows.len()).unwrap_or(u32::MAX),
154 took: started.elapsed(),
155 reason: request.reason,
156 cleared_cache,
157 });
158 Ok(TreeRead {
159 snapshot,
160 backend: BackendInfo::new(BACKEND, false),
161 scale: inner.scale,
162 })
163 }
164
165 fn can_probe(&self) -> bool {
166 self.inner
167 .connection
168 .negotiated()
169 .is_some_and(|n| n.capabilities.contains(Capabilities::BOUNDS))
170 }
171
172 fn probe(&mut self, nodes: &[NodeHandle]) -> Result<Vec<ProbedNode>, BackendError> {
173 let params = BoundsParams {
174 nodes: nodes.iter().map(|handle| handle.node).collect(),
175 };
176 let Reply::Bounds(probed) = self
177 .inner
178 .connection
179 .call(Call::Bounds(params), PROBE_DEADLINE)?
180 else {
181 return Err(BackendError::new(
182 code::PROTOCOL_ERROR,
183 "the agent answered a bounds probe with another result",
184 ));
185 };
186 Ok(probed
188 .nodes
189 .into_iter()
190 .filter_map(|found| {
191 let handle = nodes.iter().find(|h| h.node == found.node)?;
192 Some(ProbedNode {
193 node: *handle,
194 bounds: found.bounds,
195 })
196 })
197 .collect())
198 }
199}
200
201#[cfg(test)]
202pub(crate) mod tests {
203 use super::*;
204 use protocol::snapshot::{Bounds, NodeFlags, Truncation, Window, WindowFlags, WindowType};
205
206 pub(crate) fn node(id: u32, class: u32, children: Vec<Node>) -> Node {
207 Node {
208 id,
209 class,
210 res_pkg: None,
211 res_id: None,
212 text: None,
213 desc: None,
214 bounds: Bounds::new(0, 0, 100, 100),
215 flags: NodeFlags::ENABLED | NodeFlags::VISIBLE,
216 children,
217 scroll: None,
218 }
219 }
220
221 pub(crate) fn window(
222 id: u32,
223 kind: WindowType,
224 layer: i32,
225 package: u32,
226 bounds: Bounds,
227 root: Node,
228 ) -> Window {
229 Window {
230 id,
231 kind,
232 layer,
233 package: Some(package),
234 title: None,
235 bounds,
236 flags: WindowFlags::EMPTY,
237 root: Some(root),
238 emits_events: None,
239 owner: None,
240 modal: false,
241 }
242 }
243
244 pub(crate) fn reported(window: Window, emits_events: bool, owner: Option<u32>) -> Window {
247 Window {
248 emits_events: Some(emits_events),
249 owner,
250 ..window
251 }
252 }
253
254 pub(crate) fn strings() -> Vec<String> {
257 [
258 "android.widget.FrameLayout",
259 "androidx.compose.ui.platform.ComposeView",
260 "com.android.systemui",
261 "dev.mobileagent.fixture.compose",
262 "android.widget.Button",
263 "com.google.android.inputmethod.latin",
264 ]
265 .map(String::from)
266 .to_vec()
267 }
268
269 pub(crate) fn compose_screen(cells: u32) -> Snapshot {
272 let full = Bounds::new(0, 0, 1080, 2400);
273 let buttons = (0..cells).map(|i| node(10 + i, 4, Vec::new())).collect();
274 Snapshot {
275 strings: strings(),
276 windows: vec![
277 reported(
278 window(
279 3,
280 WindowType::INPUT_METHOD,
281 3,
282 5,
283 Bounds::new(0, 1500, 1080, 2400),
284 node(1, 0, Vec::new()),
285 ),
286 true,
287 Some(5),
288 ),
289 reported(
290 window(
291 2,
292 WindowType::SYSTEM,
293 2,
294 2,
295 Bounds::new(0, 0, 1080, 132),
296 node(2, 0, Vec::new()),
297 ),
298 true,
299 Some(2),
300 ),
301 reported(
302 window(
303 1,
304 WindowType::APPLICATION,
305 1,
306 3,
307 full,
308 node(3, 0, vec![node(4, 1, buttons)]),
309 ),
310 false,
311 None,
312 ),
313 ],
314 node_count: 4 + cells,
315 truncated: Truncation::EMPTY,
316 seq: 0,
317 }
318 }
319
320 #[test]
321 fn the_scale_is_the_density_over_160_with_the_override_first() {
322 assert_eq!(scale_from_density("Physical density: 420\n"), Some(2.625));
323 assert_eq!(
324 scale_from_density("Physical density: 420\nOverride density: 480\n"),
325 Some(3.0)
326 );
327 assert_eq!(scale_from_density("Physical density: 160"), Some(1.0));
328 assert_eq!(scale_from_density("Error: no display"), None);
329 }
330
331 #[test]
332 fn event_less_windows_come_from_the_agents_metadata() {
333 let mut snapshot = compose_screen(3);
334 assert!(has_event_less_window(&snapshot));
335 snapshot.windows[2].emits_events = Some(true);
336 assert!(!has_event_less_window(&snapshot));
337 snapshot.windows[2].emits_events = None;
339 assert!(!has_event_less_window(&snapshot));
340 }
341
342 #[test]
343 fn reads_after_an_attach_action_or_resync_wait_for_idle_and_others_do_not() {
344 let options = ReadOptions {
345 max_nodes: Some(1000),
346 max_depth: Some(40),
347 idle_ms: 0,
348 settle_idle_ms: Some(300),
349 ..ReadOptions::default()
350 };
351 for reason in [ReadReason::Attach, ReadReason::Action, ReadReason::Resync] {
352 let params = snapshot_params(reason, &options);
353 assert_eq!(params.idle_ms, Some(300), "{reason:?}");
354 assert_eq!(params.max_nodes, Some(1000));
355 assert_eq!(params.max_depth, Some(40));
356 }
357 for reason in [
358 ReadReason::Events,
359 ReadReason::Poll,
360 ReadReason::Frames,
361 ReadReason::Moved,
362 ] {
363 assert_eq!(
364 snapshot_params(reason, &options).idle_ms,
365 Some(0),
366 "{reason:?}"
367 );
368 }
369 let defaults = snapshot_params(ReadReason::Attach, &ReadOptions::default());
370 assert_eq!(defaults, SnapshotParams::default(), "the agent's defaults");
371 }
372
373 #[test]
374 fn serialized_nodes_count_every_window() {
375 assert_eq!(serialized_nodes(&compose_screen(5)), 9);
376 }
377}