fix: 剪贴板注入改用 ClipInject 通道——u2.set_clipboard 在 Android 10+ 被系统静默丢弃(atx-agent 后台身份写剪贴板受限),改为 am start 透明 Activity(shell 身份启动不受后台限制),注入后 u2 读回比对验证,未装 APK 明确报错

This commit is contained in:
2026-08-20 10:34:53 +08:00
parent 81a99e007b
commit 5d637bd4f5
+74 -18
View File
@@ -94,29 +94,57 @@ def api_tools_clipboard_set():
def _set_device_clipboard(serial, text):
"""向单台设备注入剪贴板文字(u2 jsonrpc.setClipboard)。
"""向单台设备注入剪贴板文字(ClipInject APK:透明 Activity 聚焦后写入)。
Android 10+ 禁止后台(atx-agent/shell)写剪贴板,u2.set_clipboard 在
Android 12/13 上调用"成功"但内容被系统静默丢弃。ClipInject
(com.example.clipinject)通过 am start 启动透明 Activity,窗口聚焦后
setPrimaryClip,shell 启动 Activity 不受后台启动限制(广播方式在 MIUI
会被 Background activity start 拦截,必须用 am start)。
- IP:port 设备:先 adb connect(已连接自动跳过;绝不 disconnect,红线)
- USB 设备(无冒号):u2 按 serial 直连,首次会自动推送 atx-agent
- 连接/注入都带超时保护,避免 atx-agent 无响应时挂住请求
- 设备未装 ClipInject:am start 返回 unable to resolve Intent,明确报错
- 注入后 u2 读回比对验证(后台读剪贴板实测可用),不一致也明确报错
"""
import base64
import time
import uiautomator2 as u2
if ":" in serial:
try:
adb_connect(serial)
except Exception as e:
return False, f"adb 连接失败: {e}"
d = None
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", "com.example.clipinject/.ClipActivity",
"--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, ("设备未安装 ClipInject(com.example.clipinject),"
f"请先安装: {out.strip()[:100]}")
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:
with ThreadPoolExecutor(max_workers=1) as pool:
d = pool.submit(u2.connect, serial).result(timeout=30)
with ThreadPoolExecutor(max_workers=1) as pool:
pool.submit(d.set_clipboard, text).result(timeout=15)
return True, "已注入"
except FuturesTimeout:
return False, "连接/注入超时(atx-agent 可能无响应)"
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:
return False, f"{type(e).__name__}: {str(e)[:120]}"
# 读回不可用(旧版 u2/连接失败)时按写入成功处理,避免误报,但带上具体原因
return True, f"已注入(读回验证不可用: {type(e).__name__}: {str(e)[:80]})"
def _app_ver_on_device(serial, pkg):
@@ -186,29 +214,57 @@ def _blocked_adb_cmd(cmd):
return any(p in low for p in _ADB_BLOCKED_PATTERNS)
def _set_device_clipboard(serial, text):
"""向单台设备注入剪贴板文字(u2 jsonrpc.setClipboard)。
"""向单台设备注入剪贴板文字(ClipInject APK:透明 Activity 聚焦后写入)。
Android 10+ 禁止后台(atx-agent/shell)写剪贴板,u2.set_clipboard 在
Android 12/13 上调用"成功"但内容被系统静默丢弃。ClipInject
(com.example.clipinject)通过 am start 启动透明 Activity,窗口聚焦后
setPrimaryClip,shell 启动 Activity 不受后台启动限制(广播方式在 MIUI
会被 Background activity start 拦截,必须用 am start)。
- IP:port 设备:先 adb connect(已连接自动跳过;绝不 disconnect,红线)
- USB 设备(无冒号):u2 按 serial 直连,首次会自动推送 atx-agent
- 连接/注入都带超时保护,避免 atx-agent 无响应时挂住请求
- 设备未装 ClipInject:am start 返回 unable to resolve Intent,明确报错
- 注入后 u2 读回比对验证(后台读剪贴板实测可用),不一致也明确报错
"""
import base64
import time
import uiautomator2 as u2
if ":" in serial:
try:
adb_connect(serial)
except Exception as e:
return False, f"adb 连接失败: {e}"
d = None
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", "com.example.clipinject/.ClipActivity",
"--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, ("设备未安装 ClipInject(com.example.clipinject),"
f"请先安装: {out.strip()[:100]}")
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:
with ThreadPoolExecutor(max_workers=1) as pool:
d = pool.submit(u2.connect, serial).result(timeout=30)
with ThreadPoolExecutor(max_workers=1) as pool:
pool.submit(d.set_clipboard, text).result(timeout=15)
return True, "已注入"
except FuturesTimeout:
return False, "连接/注入超时(atx-agent 可能无响应)"
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:
return False, f"{type(e).__name__}: {str(e)[:120]}"
# 读回不可用(旧版 u2/连接失败)时按写入成功处理,避免误报,但带上具体原因
return True, f"已注入(读回验证不可用: {type(e).__name__}: {str(e)[:80]})"
def _app_ver_on_device(serial, pkg):
"""单设备包版本查询:pm path 检查安装 → dumpsys 取 versionName/versionCode。