# adb_ui.py - 一键dump+解析Android UI (u2优先,原生fallback) # u2 (uiautomator2) 不受idle限制,适合动画密集app(美团等) # 弹窗检测: ui(clickable_only=True, raw=True) 找全屏FrameLayout+底部小ImageView(关闭X) # 已知包名: 美团外卖=com.sankuai.meituan.takeoutnew 淘宝=com.taobao.taobao import subprocess, xml.etree.ElementTree as ET, os, re, shutil ADB = shutil.which("adb") or "adb" LOCAL_XML = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ui_mt.xml") def _dump_u2(): """用uiautomator2 dump,不受idle限制""" try: import uiautomator2 as u2 d = u2.connect() xml_str = d.dump_hierarchy() if xml_str and len(xml_str) > 100: return xml_str except Exception as e: print(f"[u2 fallback] {e}") return None def _dump_native(): """原生uiautomator dump(需idle状态)""" subprocess.run([ADB, "shell", "rm", "-f", "/sdcard/ui.xml"], capture_output=True) r = subprocess.run([ADB, "shell", "uiautomator", "dump", "--compressed", "/sdcard/ui.xml"], capture_output=True, text=True, timeout=15) if "dumped" not in r.stdout.lower() and "dumped" not in r.stderr.lower(): print(f"dump failed: {r.stdout}{r.stderr}") return None subprocess.run([ADB, "pull", "/sdcard/ui.xml", LOCAL_XML], capture_output=True, timeout=10) with open(LOCAL_XML, "r", encoding="utf-8") as f: return f.read() def _parse_xml(xml_str, keyword=None, clickable_only=False, raw=False): """解析XML字符串为节点列表""" root = ET.fromstring(xml_str) nodes = [] for n in root.iter("node"): text = n.get("text", "") desc = n.get("content-desc", "") bounds = n.get("bounds", "") click = n.get("clickable") == "true" cls = n.get("class", "").split(".")[-1] rid = n.get("resource-id", "") label = text or desc if not label and not raw: continue if clickable_only and not click: continue if keyword and keyword.lower() not in (label or "").lower(): continue cx, cy = 0, 0 if bounds: m = re.findall(r'\[(\d+),(\d+)\]', bounds) if len(m) == 2: cx = (int(m[0][0]) + int(m[1][0])) // 2 cy = (int(m[0][1]) + int(m[1][1])) // 2 nodes.append({"label": label, "click": click, "bounds": bounds, "cx": cx, "cy": cy, "class": cls, "id": rid}) return nodes def ui(keyword=None, clickable_only=False, raw=False): """一键dump+解析Android UI (u2优先) keyword: 过滤含关键词的节点 clickable_only: 只显示可点击节点 raw: 返回原始节点列表而非打印 """ xml_str = _dump_u2() or _dump_native() if not xml_str: print("dump failed (both u2 and native)") return [] nodes = _parse_xml(xml_str, keyword, clickable_only, raw) if not raw: for n in nodes: flag = "Y" if n["click"] else " " coord = f"({n['cx']},{n['cy']})" if n['cx'] else "" print(f"[{flag}] {n['label']} {coord} {n['bounds']}") print(f"\ntotal: {len(nodes)} nodes") return nodes def tap(x, y): subprocess.run([ADB, "shell", "input", "tap", str(x), str(y)], capture_output=True) print(f"tap({x},{y}) ok") if __name__ == "__main__": ui()