Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions scripts/and_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,32 @@ def list_all_devices():
return device_list


def get_bounds_from_element(elem):
raw_bounds = elem.attrib.get("bounds")
if not raw_bounds:
return None
try:
bounds = raw_bounds[1:-1].split("][")
if len(bounds) != 2:
return None
x1, y1 = map(int, bounds[0].split(","))
x2, y2 = map(int, bounds[1].split(","))
except (TypeError, ValueError):
return None
return (x1, y1), (x2, y2)


def get_id_from_element(elem):
bounds = elem.attrib["bounds"][1:-1].split("][")
x1, y1 = map(int, bounds[0].split(","))
x2, y2 = map(int, bounds[1].split(","))
elem_w, elem_h = x2 - x1, y2 - y1
bounds = get_bounds_from_element(elem)
if bounds:
(x1, y1), (x2, y2) = bounds
elem_w, elem_h = x2 - x1, y2 - y1
if "resource-id" in elem.attrib and elem.attrib["resource-id"]:
elem_id = elem.attrib["resource-id"].replace(":", ".").replace("/", "_")
elif bounds:
elem_id = f"{elem.attrib.get('class', 'element')}_{elem_w}_{elem_h}"
else:
elem_id = f"{elem.attrib['class']}_{elem_w}_{elem_h}"
return ""
if "content-desc" in elem.attrib and elem.attrib["content-desc"] and len(elem.attrib["content-desc"]) < 20:
content_desc = elem.attrib['content-desc'].replace("/", "_").replace(" ", "").replace(":", "_")
elem_id += f"_{content_desc}"
Expand All @@ -59,12 +76,13 @@ def traverse_tree(xml_path, elem_list, attrib, add_index=False):
if event == 'start':
path.append(elem)
if attrib in elem.attrib and elem.attrib[attrib] == "true":
bounds = get_bounds_from_element(elem)
if not bounds:
continue
parent_prefix = ""
if len(path) > 1:
parent_prefix = get_id_from_element(path[-2])
bounds = elem.attrib["bounds"][1:-1].split("][")
x1, y1 = map(int, bounds[0].split(","))
x2, y2 = map(int, bounds[1].split(","))
(x1, y1), (x2, y2) = bounds
center = (x1 + x2) // 2, (y1 + y2) // 2
elem_id = get_id_from_element(elem)
if parent_prefix:
Expand Down
54 changes: 54 additions & 0 deletions tests/test_and_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import sys
import tempfile
import types
import unittest
from pathlib import Path


scripts_dir = Path(__file__).resolve().parents[1] / "scripts"
sys.path.insert(0, str(scripts_dir))

utils_stub = types.ModuleType("utils")
utils_stub.print_with_color = lambda *_args, **_kwargs: None
sys.modules["utils"] = utils_stub

from and_controller import traverse_tree


class TraverseTreeTest(unittest.TestCase):
def traverse(self, xml):
with tempfile.TemporaryDirectory() as temp_dir:
xml_path = Path(temp_dir) / "window.xml"
xml_path.write_text(xml, encoding="utf-8")
elements = []
traverse_tree(xml_path, elements, "focusable", True)
return elements

def test_handles_root_without_bounds(self):
elements = self.traverse(
'<hierarchy rotation="0">'
'<node index="0" class="android.widget.EditText" focusable="true" '
'bounds="[10,20][110,60]" />'
'</hierarchy>'
)

self.assertEqual(len(elements), 1)
self.assertEqual(elements[0].uid, "android.widget.EditText_100_40_0")
self.assertEqual(elements[0].bbox, ((10, 20), (110, 60)))

def test_skips_interactive_elements_without_valid_bounds(self):
elements = self.traverse(
'<hierarchy rotation="0">'
'<node index="0" class="android.widget.EditText" focusable="true" />'
'<node index="1" class="android.widget.Button" focusable="true" bounds="invalid" />'
'<node index="2" class="android.widget.Button" focusable="true" '
'bounds="[0,0][50,30]" />'
'</hierarchy>'
)

self.assertEqual(len(elements), 1)
self.assertEqual(elements[0].uid, "android.widget.Button_50_30_2")


if __name__ == "__main__":
unittest.main()