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()