#!/usr/bin/env python3 """TNG 全流程自动化:注册区号 + 登录 PIN/区号;两次冷启动,互不干扰。""" import re import subprocess import sys import time import xml.etree.ElementTree as ET PKG = "my.com.tngdigital.ewallet" SPLASH = f"{PKG}/.ui.SplashActivity" ADB = ["adb"] TNG_LOG = re.compile( r"TngRoot|F HWUI|GraphicBuffer|Runtime abort|signal 6|exited due to signal" r"|UserSearchCallingCode|UserLogin|UserRegistration|SecurityError" r"|Dialog\.show|skip loading|registration flow", re.I, ) CRASH_LOG = re.compile( r"signal 6|Runtime aborting|F HWUI|F GraphicBuffer|gralloc-mapper is missing", re.I, ) def adb(*args, timeout=60): r = subprocess.run(ADB + list(args), capture_output=True, text=True, timeout=timeout, errors="ignore") return r.returncode, (r.stdout or "") + (r.stderr or "") def is_tng_foreground(): _, out = adb("shell", "dumpsys", "activity", "activities") for line in out.splitlines(): if "topResumedActivity=" in line: return PKG in line return False def bring_tng_foreground(): adb("shell", "input", "keyevent", "KEYCODE_WAKEUP") if is_tng_foreground(): return adb("shell", "am", "start", "-n", SPLASH) time.sleep(2) def dump_ui(path="/sdcard/tng_ui.xml", retries=2): adb("shell", "input", "keyevent", "KEYCODE_WAKEUP") for _ in range(retries): adb("shell", "uiautomator", "dump", path) code, xml = adb("shell", "cat", path) if code == 0 and xml.strip().startswith("= 4 and "PIN" in t) ): candidates.append((pt[1], node)) if not candidates: return None candidates.sort(key=lambda x: x[0]) return candidates[0][1] def wait_phone_page(timeout=20): return wait_for(lambda: is_phone_page(dump_ui()), timeout=timeout, interval=1.5, desc="phone page") def find_nodes(root, **kwargs): out = [] for node in root.iter("node"): text = (node.get("text") or "").strip() rid = node.get("resource-id") or "" cls = node.get("class") or "" clickable = node.get("clickable") == "true" ok = True if "text_contains" in kwargs and kwargs["text_contains"] not in text: ok = False if "text_excludes" in kwargs: for ex in kwargs["text_excludes"]: if ex in text: ok = False if "rid_contains" in kwargs and kwargs["rid_contains"] not in rid: ok = False if "class_contains" in kwargs and kwargs["class_contains"] not in cls: ok = False if kwargs.get("clickable") and not clickable: ok = False if ok: out.append(node) return out def tap_node(node, label=""): pt = center(node.get("bounds")) if not pt: print(f" skip tap {label}: bad bounds") return False x, y = pt print(f" tap {label!r} at {x},{y}") adb("shell", "input", "tap", str(x), str(y)) return True def find_country_control(root, min_y=0, max_y=9999): nodes = find_nodes(root, rid_contains="ll_country", clickable=True) for node in nodes: pt = center(node.get("bounds")) if pt and min_y <= pt[1] <= max_y: return node nodes = find_nodes(root, rid_contains="tv_left", clickable=True) for node in nodes: pt = center(node.get("bounds")) if pt and min_y <= pt[1] <= max_y: return node return None def find_register_btn(root): for node in root.iter("node"): if node.get("clickable") != "true": continue t = node.get("text") or "" if ("注册" in t or "Register" in t.lower()) and "已经" not in t and "已注" not in t: return node pt = center(node.get("bounds")) if pt and 1050 < pt[1] < 1220 and 430 < pt[0] < 660: return node return None def is_phone_page(root): return bool(find_nodes(root, rid_contains="userContinueBtn")) def is_register_page(root): return bool( find_nodes(root, rid_contains="ftv_register_title") or find_nodes(root, rid_contains="userRegisterContinueBtn") ) def has_country_list(root): if root is None: return False for node in root.iter("node"): t = node.get("text") or "" if any(k in t for k in ("Malaysia", "Singapore", "Australia", "China", "+86", "+61")): return True return False def pid(): code, out = adb("shell", "pidof", PKG) return out.strip() if code == 0 and out.strip() else "" def top_activity(): _, out = adb("shell", "dumpsys", "activity", "activities") for line in out.splitlines(): if PKG not in line: continue if "topResumedActivity=" in line: m = re.search(r"/([^/}\s]+)", line) return m.group(1) if m else "?" for line in out.splitlines(): if "ResumedActivity:" in line and PKG in line: m = re.search(r"/([^/}\s]+)", line) if m: return m.group(1) return "?" def wait_for(fn, timeout=35, interval=1.5, desc=""): deadline = time.time() + timeout while time.time() < deadline: if fn(): return True time.sleep(interval) print(f" timeout: {desc}") return False def wait_ui_widgets(timeout=30): """等待 Compose 控件出现在 dump(Splash WebView 退场后)。""" def ready(): root = dump_ui() if root is None: return False return bool( find_pin_login_row(root) or find_register_btn(root) or is_phone_page(root) or find_nodes(root, rid_contains="ftv_content") or find_nodes(root, rid_contains="ftv_register_title") ) return wait_for(ready, timeout=timeout, interval=2, desc="login/register widgets") def wait_login_method(timeout=40): def ready(): if not pid(): return False if not is_tng_foreground(): bring_tng_foreground() return "UserLoginActivity" in top_activity() ok = wait_for(ready, timeout=timeout, interval=2, desc="UserLoginActivity ready") if ok: wait_ui_widgets(timeout=25) return ok def wait_login_method_register(timeout=40): return wait_login_method(timeout=timeout) def wait_country_list(timeout=30): def activity_has_country_list(): act = top_activity() if "UserSearchCallingCode" in act: return True _, out = adb("shell", "dumpsys", "activity", "activities") return "UserSearchCallingCodeActivity" in out and PKG in out def ready(): if activity_has_country_list(): return True root = dump_ui() return has_country_list(root) return wait_for(ready, timeout=timeout, interval=1.5, desc="country list") def cold_start(clear_log=False): if clear_log: adb("logcat", "-c") adb("shell", "am", "force-stop", PKG) time.sleep(3) adb("shell", "input", "keyevent", "KEYCODE_WAKEUP") adb("shell", "am", "start", "-W", "-n", SPLASH) ok = wait_for( lambda: bool(pid()) and "UserLoginActivity" in top_activity(), timeout=60, interval=2, desc="UserLoginActivity after cold start", ) if not ok: return False bring_tng_foreground() time.sleep(3) return True def cold_start_with_retry(clear_log=False, attempts=3): for i in range(attempts): if cold_start(clear_log and i == 0): return True print(f" cold start retry {i + 1}/{attempts}") adb("shell", "am", "force-stop", PKG) time.sleep(5) return bool(pid()) and "UserLoginActivity" in top_activity() def tap_pin_row(root): if root is None: print(" PIN fallback (no dump) 561,677") adb("shell", "input", "tap", "561", "677") return pin_text = find_pin_login_row(root) if pin_text is None: if find_nodes(root, rid_contains="ftv_content") or find_nodes(root, rid_contains="ftv_title"): print(" PIN layout fallback 561,677") adb("shell", "input", "tap", "561", "677") return print(" PIN fallback tap 540,677") adb("shell", "input", "tap", "540", "677") return pt = center(pin_text.get("bounds")) print(f" tap PIN at {pt[0]},{pt[1]}") adb("shell", "input", "tap", str(pt[0]), str(pt[1])) def collect_logs(n=3000): _, out = adb("shell", "logcat", "-d", "-t", str(n)) return [ln for ln in out.splitlines() if TNG_LOG.search(ln)] def collect_crashes(n=4000): _, out = adb("shell", "logcat", "-d", "-t", str(n)) return [ln for ln in out.splitlines() if CRASH_LOG.search(ln) and "digital.ewallet" in ln] def step(name, fn): print(f"\n=== {name} ===") bring_tng_foreground() ok = fn() print(f" pid={pid() or 'DEAD'} activity={top_activity()}") return ok and bool(pid()) def wait_register_page(timeout=20): def ready(): root = dump_ui() if root is None or not is_register_page(root): return False ctrl = find_country_control(root, min_y=680) if ctrl is None: return False pt = center(ctrl.get("bounds")) # 注册页 ll_country 中心 y 通常 > 680 return pt is not None and pt[1] >= 680 ok = wait_for(ready, timeout=timeout, interval=1.5, desc="register page settled") if ok: time.sleep(1) return ok def flow_register(): time.sleep(5) if not cold_start_with_retry(): return False if not wait_login_method_register(): print(" register entry not visible") return False root = dump_ui() reg = find_register_btn(root) if reg is None: print(" register fallback 540,1137") adb("shell", "input", "tap", "540", "1137") else: tap_node(reg, "register") if not wait_register_page(): return False root2 = dump_ui() target = find_country_control(root2, min_y=680) if target is None: print(" no country control on register page") return False tap_node(target, "register country") if not wait_country_list(timeout=25): return False root3 = dump_ui() if root3 is not None: for node in root3.iter("node"): t = node.get("text") or "" if "Malaysia" in t or "Singapore" in t: print(f" country UI: {t[:40]}") break logs = collect_logs(300) for ln in logs: if "skip loading" in ln or "UserSearchCallingCode" in ln: print(f" log: {ln[:120]}") break return True def flow_login(): if not cold_start_with_retry(clear_log=True): return False if not wait_login_method(): return False root = dump_ui() if is_phone_page(root): print(" already on phone page") else: for attempt in range(2): root = dump_ui() if root is None: time.sleep(2) continue if is_phone_page(root): print(" phone page ready") break tap_pin_row(root) if wait_phone_page(timeout=15): break print(f" phone page retry {attempt + 1}/2") time.sleep(2) else: return False root2 = dump_ui() target = find_country_control(root2, min_y=450, max_y=650) if target is None: print(" no country control on phone page") return False tap_node(target, "login country") if not wait_country_list(): return False root3 = dump_ui() if root3 is not None: for node in root3.iter("node"): t = node.get("text") or "" if "Malaysia" in t or "Singapore" in t: print(f" country UI: {t[:40]}") break logs = collect_logs(300) for ln in logs: if "skip loading" in ln: print(f" log: {ln[:120]}") break return "UserSearchCallingCode" in top_activity() or has_country_list(root3) def main(): adb("shell", "svc", "power", "stayon", "true") adb("logcat", "-c") adb("shell", "am", "force-stop", PKG) time.sleep(5) # 先登录后注册:首次冷启动最稳定 results = [ ("login_country", step("A. login PIN → country list", flow_login)), ("register_country", step("B. register → country list", flow_register)), ] crashes = collect_crashes() p = pid() print("\n=== SUMMARY ===") for name, ok in results: print(f" {'PASS' if ok else 'FAIL'} {name}") print(f" pid={p or 'DEAD'} activity={top_activity()}") print(f" crashes={len(crashes)}") if crashes: for ln in crashes[-5:]: print(" ", ln[:150]) print("\n=== TngRoot (last 20) ===") for ln in collect_logs(2000)[-20:]: if "TngRoot" in ln: print(ln[:180]) ok = bool(p) and all(r[1] for r in results) if crashes and not ok: print(" (crash lines may include prior process; see pid)") return 0 if ok else 1 if __name__ == "__main__": sys.exit(main())