"""uiautomator2 通用辅助函数。 所有任务都可复用:应用启动保障、等待首页、安全点击、随机操作等。 具体业务逻辑(如抖音首页特征元素判断)不放这里,放各自 task 文件。 """ import time import random import re from core.logger import get_logger _log = get_logger("core.u2") def ensure_app_running(d, package, max_restart=3, home_check=None): """确保 app 在前台运行,被人为退出/崩溃则自动重启。 d: u2.Device package: 包名 max_restart: 最大重启次数 home_check: 可选回调 d->bool,判断是否进入主页。 默认只看包名(包名对就返回 True,和原抖音逻辑一致)。 返回 True 表示 app 在前台;连续重启失败返回 False。 """ home_check = home_check or (lambda d: True) for attempt in range(1, max_restart + 1): try: cur = d.app_current() except Exception: cur = {} if cur.get("package") == package and home_check(d): return True _log.info(f"{package} 不在前台(当前:{cur}),第 {attempt}/{max_restart} 次重启...") try: d.app_start(package, wait=True) time.sleep(3) if home_check(d): return True except Exception as e: _log.warning(f"重启异常: {e}") time.sleep(3) return False def wait_for_app_home(d, package, home_check, timeout=40): """等待 app 进入主页。 home_check: d->bool 回调,返回 True 表示已进入主页。 抖音主页 Activity 名仍叫 SplashActivity,不能靠 Activity 名判断, 要用界面特征元素(由调用方通过 home_check 传入)。 """ deadline = time.time() + timeout while time.time() < deadline: try: cur = d.app_current() except Exception: cur = {} if cur.get("package") == package and home_check(d): return True time.sleep(2) return False def random_sleep(min_s, max_s): """随机睡眠(模拟人类操作间隔)。""" time.sleep(random.uniform(min_s, max_s)) def screen_is_on(d): """屏幕是否亮着。返回 True/False,取不到返回 None(三态,别当 False 用)。 为什么不用 `d.info`:它在部分设备上一次要**十几秒**(见 doc/backlog/TODO.md); `dumpsys power` 经 atx-agent 跑只要 ~0.3s,且 `mWakefulness` 字段全机型都有 (MIUI 的 `dumpsys display` 反而没有 mScreenState)。 """ try: out = d.shell("dumpsys power") out = getattr(out, "output", None) or str(out) m = re.search(r"mWakefulness=(\w+)", out) if m: return m.group(1).lower() != "asleep" except Exception as e: _log.warning(f"读取屏幕状态失败: {e}") return None def current_package(d): """当前前台 App 包名;取不到返回空串。""" try: cur = d.app_current() or {} return cur.get("package") or "" except Exception as e: _log.warning(f"读取前台 App 失败: {e}") return "" def safe_click(el, timeout=1): """安全点击:元素存在才点,不抛异常。返回是否点击成功。""" try: if el.exists(timeout=timeout): el.click() return True except Exception: pass return False def safe_set_text(el, text, timeout=1): """安全输入文本:元素存在才输入,不抛异常。返回是否成功。""" try: if el.exists(timeout=timeout): el.set_text(text) return True except Exception: pass return False def find_and_click(d, timeout=1, **selectors): """按定位器找元素并点击,找不到不抛异常。返回是否成功。 用法:find_and_click(d, text="首页") 或 find_and_click(d, description="搜索") """ try: el = d(**selectors) if el.exists(timeout=timeout): el.click() return True except Exception: pass return False def random_swipe_up(d, width_ratio=0.5, height_start=0.8, height_end=0.2, duration=None): """随机上滑(刷视频常用)。坐标按屏幕比例算,适配不同分辨率。 duration: 滑动时长(秒),None 则随机 0.25~0.50s。 """ info = d.info w, h = info["displayWidth"], info["displayHeight"] dur = duration or random.uniform(0.25, 0.50) d.swipe(int(w * width_ratio), int(h * height_start), int(w * width_ratio), int(h * height_end), dur)