1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
|
#!/usr/bin/env python3
"""Parse a uiautomator dump into labelled, tappable nodes.
Kept in its own file rather than inlined into the shell script: the first version was a
python -c embedded in a bash case block, and the nested quoting silently mangled the regexes
so the dump reported one element and a tap landed on the wrong one.
Usage:
uinodes.py list < dump.xml
uinodes.py find <label> < dump.xml
"""
import re
import sys
NODE = re.compile(r"<node\b[^>]*>")
DESC = re.compile(r'content-desc="([^"]*)"')
TEXT = re.compile(r'text="([^"]*)"')
BOUNDS = re.compile(r'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"')
CLICKABLE = re.compile(r'clickable="true"')
def nodes(raw):
for match in NODE.findall(raw):
bounds = BOUNDS.search(match)
if not bounds:
continue
x1, y1, x2, y2 = map(int, bounds.groups())
if x2 <= x1 or y2 <= y1:
continue
desc = (DESC.search(match) or [None, ""])[1] if DESC.search(match) else ""
text = (TEXT.search(match) or [None, ""])[1] if TEXT.search(match) else ""
label = desc.strip() or text.strip()
if not label:
continue
yield {
"label": label,
"kind": "desc" if desc.strip() else "text",
"x": (x1 + x2) // 2,
"y": (y1 + y2) // 2,
"clickable": bool(CLICKABLE.search(match)),
}
def main():
if len(sys.argv) < 2:
print("usage: uinodes.py list|find <label>", file=sys.stderr)
return 2
raw = sys.stdin.read()
found = list(nodes(raw))
if sys.argv[1] == "list":
if not found:
print(" (no labelled nodes found -- is anything on screen?)")
seen = set()
for node in found:
key = (node["label"], node["x"], node["y"])
if key in seen:
continue
seen.add(key)
tap = "tappable" if node["clickable"] else " "
print(f" [{node['kind']:4}] {tap} {node['label']!r} @ ({node['x']},{node['y']})")
return 0
if sys.argv[1] == "find":
target = sys.argv[2]
hits = [n for n in found if n["label"] == target]
# Prefer a clickable node: a label and the button carrying it are often separate nodes
# at different coordinates, and tapping the label does nothing.
clickable = [n for n in hits if n["clickable"]]
if clickable:
hits = clickable
# Collapse nodes that resolve to the same point.
unique = {(n["x"], n["y"]) for n in hits}
if not unique:
print("NONE")
elif len(unique) > 1:
print(f"AMBIGUOUS {len(unique)}")
else:
x, y = unique.pop()
print(f"{x} {y}")
return 0
print(f"unknown command {sys.argv[1]}", file=sys.stderr)
return 2
if __name__ == "__main__":
sys.exit(main())
|