diff --git a/scripts/and_controller.py b/scripts/and_controller.py
index 2cde0bb..f2b3ada 100644
--- a/scripts/and_controller.py
+++ b/scripts/and_controller.py
@@ -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}"
@@ -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:
diff --git a/tests/test_and_controller.py b/tests/test_and_controller.py
new file mode 100644
index 0000000..320bab1
--- /dev/null
+++ b/tests/test_and_controller.py
@@ -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(
+ ''
+ ''
+ ''
+ )
+
+ 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(
+ ''
+ ''
+ ''
+ ''
+ ''
+ )
+
+ self.assertEqual(len(elements), 1)
+ self.assertEqual(elements[0].uid, "android.widget.Button_50_30_2")
+
+
+if __name__ == "__main__":
+ unittest.main()