通用安卓弹窗点击方法

封装的方法
  1 import contextlib
  2 import logging
  3 import re
  4 import threading
  5 import time
  6 from dataclasses import dataclass
  7 from airtest.core.android import Android
  8 from airtest.core.ios import IOS 10 
 11 logger = logging.getLogger(__name__)
 12 
 20 
 21 class DeviceSession:
 22     """
 23     线程内设备会话:
 24     - dev 固定 26     """
 27     def __init__(self, host: str, serialno: str, adb_port: int = 5039,
 28                  policy: PocoBuildPolicy | None = None):
 29         self.host = host
 30         self.serialno = serialno 32         self._lock = threading.RLock()
 33 
 34         # 远程测试农场用 5039,本地 USB 设备用标准端口 5037
 35         self.dev = Android(serialno=self.serialno, host=(self.host, adb_port)) 37         self._generation = 0  # 游戏重启后 +1,触发重建
 3 92 
 93     @contextlib.contextmanager
 94     def alert_watch_and_click(self, buttons: list, interval: float = 3.0):
 95         """
 96         后台监控 Android 系统弹窗并自动点击,用法与 iOS 的 wda alert_watch_and_click 一致。
 97 
 98         原理:
 99             启动一个 daemon 后台线程,每隔 interval 秒用 adb shell 执行 uiautomator dump,
100             解析当前界面上的可点击文字,如果匹配到 buttons 中的任意一个就点击它。
101 
102         Args:
103             buttons: 需要点击的按钮文字列表,按优先级排列,匹配到第一个就点击
104                      例如:["允许", "同意", "确定", "好"]
105             interval: 检查间隔(秒),默认 3 秒
106 
107         Examples:
108             with self.device_init.session.alert_watch_and_click(["允许", "同意", "确定"], interval=3):
109                 self.device_init.dev.adb.shell("pm install xxx.apk")
110                 time.sleep(30)
111         """
112         stop_event = threading.Event()
113 
114         def _watch_worker():
115             while not stop_event.is_set():
116                 try:
117                     self._check_and_click_alert(buttons)
118                 except Exception as e:
119                     logger.debug(f"[alert_watch] 检查弹窗异常(可忽略): {e}")
120                 # 用 wait 代替 sleep,这样 stop_event 被 set 后能立即退出,不用等满 interval
121                 stop_event.wait(interval)
122 
123         t = threading.Thread(target=_watch_worker, name="android-alert-watcher", daemon=True)
124         t.start()
125         logger.info(f"[alert_watch] 开始监控弹窗,目标按钮: {buttons},间隔: {interval}s")
126         try:
127             yield
128         finally:
129             stop_event.set()
130             t.join(timeout=interval + 1)
131             logger.info("[alert_watch] 弹窗监控已停止")
132 
133     def _dump_ui_xml(self, max_retry: int = 3) -> str:
134         """
135         dump 当前界面层级并返回完整 XML,失败返回空串。
136 
137         内存紧张时(前台游戏占满内存),uiautomator dump 新起的进程会被系统 OOM 杀掉
138         (回显为 "Killed"),文件写不出来。这里做两件事缓解:
139         1. --compressed:只保留对无障碍有意义的节点,层级树更小、占内存更少,弹窗按钮通常会保留。
140         2. 单次检测内重试若干次:OOM 多为瞬时,重试期间内存可能已回收。
141         判据:cat 出的内容必须是含结尾 </hierarchy> 的完整 XML(前面已 rm,杜绝读到旧残留)。
142         """
143         dump_out = ""
144         for i in range(1, max_retry + 1):
145             # 先删旧文件,避免本次失败时 cat 到上一次成功的残留 XML(明明有弹窗却读到旧界面)。
146             self.dev.adb.shell("rm -f /sdcard/alert_dump.xml")
147             # 成功/失败提示都走 stderr,用 2>&1 重定向回 stdout 以便记录真正原因。
148             dump_out = self.dev.adb.shell("uiautomator dump --compressed /sdcard/alert_dump.xml 2>&1") or ""
149             xml_content = self.dev.adb.shell("cat /sdcard/alert_dump.xml") or ""
150             if "<?xml" in xml_content and "</hierarchy>" in xml_content:
151                 return xml_content
152             if i < max_retry:
153                 time.sleep(0.5)
154         logger.debug(f"[alert_watch] dump 连续 {max_retry} 次失败,跳过本次检测,最后一次 dump 输出: {dump_out.strip()!r}")
155         return ""
156 
157     def _check_and_click_alert(self, buttons: list):
158         """
159         执行一次弹窗检测和点击。
160         dump UI 只做一次,避免在 _watch_worker 里对每个按钮文字各 dump 一次,
161         造成多次 adb 调用和 XML 不一致的问题。
162         """
163         xml_content = self._dump_ui_xml()
164         if not xml_content:
165             return
166         for btn_text in buttons:
167             # 匹配完整的 <node .../> 标签,同时兼容 text 和 content-desc 属性
168             # 前瞻断言确认节点包含目标文字后,再匹配完整标签,避免跨节点误匹配 bounds
169             node_match = re.search(
170                 rf'<node(?=[^>]*(?:text|content-desc)="{re.escape(btn_text)}")[^>]*/>',
171                 xml_content
172             )
173             if not node_match:
174                 continue
175             node = node_match.group()
176 
177             # 复选框已勾选时跳过,不重复点击
178             checkable = 'checkable="true"' in node
179             checked = 'checked="true"' in node
180             if checkable and checked:
181                 logger.debug(f"[alert_watch] '{btn_text}' 复选框已勾选,跳过")
182                 continue
183 
184             # 从匹配到的 node 串中提取 bounds
185             bounds_match = re.search(r'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"', node)
186             if not bounds_match:
187                 continue
188             x1, y1, x2, y2 = (int(bounds_match.group(1)), int(bounds_match.group(2)),
189                                int(bounds_match.group(3)), int(bounds_match.group(4)))
190             cx = (x1 + x2) // 2
191             cy = (y1 + y2) // 2
192             logger.info(f"[alert_watch] 点击 '{btn_text}'  checkable={checkable} checked={checked},坐标: ({cx}, {cy})")
193             self.dev.adb.shell(f"input tap {cx} {cy}")
194             # 每轮只处理一个,避免误点多个按钮
195             return
196 
197         logger.debug(f"[alert_watch] 未检测到目标弹窗按钮: {buttons}")

使用方法

import time
from DeviceSession import DeviceSession

session = DeviceSession(host="127.0.0.1", serialno="10.30.244.58:20012", adb_port=5037)
with session.alert_watch_and_click([
    "已了解应用的风险检测结果",
    "继续安装",
    "安装",
    "稍后",
    "稍后提醒",
    "允许",
    "始终允许",
    "同意",
    "同意并继续",
    "传输文件",
    "传输文件(MTP)",
    "忽略本次",
    "登录",
    "完成",
    "退出"
], interval=3):
    time.sleep(200)
    # 放入装包的命令
    time.sleep(3)
    print("继续开始检测")

 

posted @ 2026-07-01 14:33  ShineLeem  阅读(8)  评论(0)    收藏  举报