Parses Android Accessibility XML and returns a lean list of interactive elements. Calculates center coordinates (x, y) for every clickable element.
(xml_content: str)
| 2 | from typing import List, Dict, Optional |
| 3 | |
| 4 | def get_interactive_elements(xml_content: str) -> List[Dict]: |
| 5 | """ |
| 6 | Parses Android Accessibility XML and returns a lean list of interactive elements. |
| 7 | Calculates center coordinates (x, y) for every clickable element. |
| 8 | """ |
| 9 | try: |
| 10 | root = ET.fromstring(xml_content) |
| 11 | except ET.ParseError: |
| 12 | print("⚠️ Error parsing XML. The screen might be loading.") |
| 13 | return [] |
| 14 | |
| 15 | elements = [] |
| 16 | |
| 17 | # Recursively find all nodes |
| 18 | for node in root.iter(): |
| 19 | # Filter: We only care about elements that are interactive or have information |
| 20 | is_clickable = node.attrib.get("clickable") == "true" |
| 21 | # Check for actual text input fields (not just focusable elements) |
| 22 | element_class = node.attrib.get("class", "") |
| 23 | is_editable = ( |
| 24 | "EditText" in element_class or |
| 25 | "AutoCompleteTextView" in element_class or |
| 26 | node.attrib.get("editable") == "true" |
| 27 | ) |
| 28 | text = node.attrib.get("text", "") |
| 29 | desc = node.attrib.get("content-desc", "") |
| 30 | resource_id = node.attrib.get("resource-id", "") |
| 31 | |
| 32 | # Skip empty layout containers that do nothing |
| 33 | if not is_clickable and not is_editable and not text and not desc: |
| 34 | continue |
| 35 | |
| 36 | # Parse Bounds: "[140,200][400,350]" -> Center X, Y |
| 37 | bounds = node.attrib.get("bounds") |
| 38 | if bounds: |
| 39 | try: |
| 40 | # Extract coordinates |
| 41 | coords = bounds.replace("][", ",").replace("[", "").replace("]", "").split(",") |
| 42 | x1, y1, x2, y2 = map(int, coords) |
| 43 | |
| 44 | center_x = (x1 + x2) // 2 |
| 45 | center_y = (y1 + y2) // 2 |
| 46 | |
| 47 | # Determine suggested action based on element type |
| 48 | if is_editable: |
| 49 | suggested_action = "type" |
| 50 | elif is_clickable: |
| 51 | suggested_action = "tap" |
| 52 | else: |
| 53 | suggested_action = "read" |
| 54 | |
| 55 | element = { |
| 56 | "id": resource_id, |
| 57 | "text": text or desc, # Fallback to content-desc if text is empty |
| 58 | "type": node.attrib.get("class", "").split(".")[-1], |
| 59 | "bounds": bounds, |
| 60 | "center": (center_x, center_y), |
| 61 | "clickable": is_clickable, |
nothing calls this directly
no outgoing calls
no test coverage detected