"""剪贴板注入公共模块(ClipInject APK 通道)。 背景:Android 10+ 禁止后台(atx-agent/shell)写剪贴板——u2.set_clipboard 调用"成功"但内容被系统静默丢弃(Android 12/13 实测)。ClipInject (com.example.clipinject,用户自研 APK)通过 am start 启动透明 Activity, 窗口聚焦后 setPrimaryClip;shell 身份启动 Activity 不受后台启动限制 (广播方式在 MIUI 会被 Background activity start 拦截,必须用 am start)。 工具页(web/tools_api.py)与任务执行器(tasks/generic/task.py)共用本模块。 """ import base64 import subprocess import time from concurrent.futures import ThreadPoolExecutor from config import ADB_PATH from core.adb_helper import adb_connect from core.logger import get_logger _log = get_logger("core.clip") _CLIP_PACKAGE = "com.example.clipinject" _CLIP_ACTIVITY = "com.example.clipinject/.ClipActivity" def inject_clipboard(serial, text, d=None): """向设备剪贴板注入文字,返回 (ok, msg)。 - d:可选,任务执行器已持有 u2 Device 时传入复用(跳过重复连接), 读回验证直接用它;不传则临时 u2.connect(工具页场景,带超时保护) - IP:port 设备先 adb connect(已连接自动跳过;绝不 disconnect,红线) - 未装 ClipInject / 读回不一致都会明确报错(不再静默失败) """ import uiautomator2 as u2 if ":" in serial: try: adb_connect(serial) except Exception as e: return False, f"adb 连接失败: {e}" b64 = base64.b64encode(text.encode("utf-8")).decode() # 1. am start 透明 Activity 写剪贴板(shell 身份,不受后台启动限制) try: r = subprocess.run( [ADB_PATH, "-s", serial, "shell", "am", "start", "-n", _CLIP_ACTIVITY, "--es", "text_b64", b64], capture_output=True, timeout=30) out = ((r.stdout or b"") + (r.stderr or b"")).decode(errors="replace") if "does not exist" in out or "unable to resolve Intent" in out: return False, f"设备未安装 ClipInject({_CLIP_PACKAGE}),请先安装" if "Error" in out or ("Starting" not in out and r.returncode != 0): return False, f"am start 失败: {out.strip()[:120]}" except subprocess.TimeoutExpired: return False, "am start 超时(adb 无响应)" except Exception as e: return False, f"{type(e).__name__}: {str(e)[:120]}" # 2. 读回验证(等透明 Activity 完成写入;u2 读剪贴板在 Android 12 实测可用) time.sleep(0.5) try: if d is None: with ThreadPoolExecutor(max_workers=1) as pool: d = pool.submit(u2.connect, serial).result(timeout=30) with ThreadPoolExecutor(max_workers=1) as pool: got = pool.submit(lambda: d.clipboard).result(timeout=15) if got == text: return True, "已注入" return False, f"剪贴板读回不一致(写入可能被拒): {str(got)[:60]!r}" except Exception as e: # 读回不可用(旧版 u2/连接失败)时按写入成功处理,避免误报,但带上具体原因 return True, f"已注入(读回验证不可用: {type(e).__name__}: {str(e)[:80]})"