安卓无线调试提权与虚拟副屏自动化
前言
想让一个普通 Android App 在后台“自己操作”另一个应用,比如截图、找图、点击,会遇到一堵墙:普通应用没有权限截取其他应用的画面,也不能向其他应用注入触控。常见解法是借助 ADB 的 shell 身份(uid=2000)。
本文拆解一套完整的落地流程,分成两个视角:
- 用户视角:交互尽量简单,把使用门槛降到最低
- 技术实现:底层 Socket、mDNS 与特权进程注入
流程全景
[阶段 1: 首次启动检查] ──> [阶段 2: 引导开启环境] ──> [阶段 3: 一次性配对提权]
│
[阶段 6: 脚本高速循环] <── [阶段 5: 创建虚拟副屏] <── [阶段 4: 注入特权守护进程]阶段 1:首次启动与前置检查
用户视角
- 首次打开 App,进入主界面。
- 弹出权限弹窗,请求通知权限(Android 13+ 必需,并告知用户“用于后续快捷输入验证码”),用户点击允许。
- 主界面显示状态仪表盘:
[√] Wi-Fi 网络已连接(未连接 Wi-Fi 时提示“请先连接 Wi-Fi 后继续”)[ ] 开发者模式:未开启,旁边亮起按钮 【第一步:开启开发者模式】
技术实现
- 网络检查:通过
ConnectivityManager监听,确认手机连接了有效的局域网 Wi-Fi(无线调试的物理前提)。 - 状态检测:读取系统设置判断开发者模式是否开启:
int devOptions = Settings.Global.getInt(getContentResolver(),
Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0);
boolean isDevEnabled = (devOptions == 1);阶段 2:引导开启开发者模式
仅在用户没开过时触发。
用户视角
- 用户点击 【第一步:开启开发者模式】。
- App 弹出图文提示:“页面即将跳转,请连续点击‘版本号’ 7 次,提示开启后返回”。
- 点击“前往”,页面自动切到系统的“关于手机”页面。
- 用户连点版本号,系统弹出 Toast:“您现在处于开发者模式!”,按返回键回到 App。
- 主界面状态刷新为
[√] 开发者模式:已就绪,下一步按钮解锁:【第二步:无线调试配对】。
技术实现
触发系统“关于”页的 Intent:
Intent intent = new Intent(Settings.ACTION_DEVICE_INFO_SETTINGS);
startActivity(intent);App 在 onResume() 里重新检测一次 DEVELOPMENT_SETTINGS_ENABLED,值变为 1 就自动刷新 UI。
阶段 3:无线调试配对提权
这是整套流程的核心难点,但只需要做一次。
用户视角
- 用户点击 【第二步:无线调试配对】。
- 页面直接跳到系统的“无线调试”设置页,跳过繁琐的开发者菜单列表。
- 手机通知栏出现 App 发出的常驻通知:“正在监听调试端口,请打开开关并点击配对码……”
- 用户打开顶部的“无线调试”总开关,再点击“使用配对码配对设备”。
- 系统弹窗显示 6 位配对码。
- 用户不需要切回 App,直接下拉通知栏,App 的通知卡片上已经变成了输入框,输入这 6 位数字,点击右侧的 【发送】。
- 系统弹窗自动消失(配对成功),通知提示:“配对成功!特权已激活”。
- 用户返回 App,主界面变为全绿就绪状态。
技术实现
1. 精准直达
Intent intent = new Intent("android.settings.WIRELESS_DEBUGGING_SETTINGS");
startActivity(intent);2. 后台 mDNS 嗅探
跳转的同时,App 在后台启动 NsdManager(网络服务发现),监听服务名 _adb-tls-pairing._tcp。
当用户在系统设置里点开“使用配对码配对设备”时,系统会在本地广播一个临时端口,App 能立刻捕获这个随机的配对端口号,用户完全不用看端口,也不用输端口。
3. 通知栏快捷回复(RemoteInput)
捕获到端口后,App 立即更新 Notification,挂载 RemoteInput 输入行为。
4. SPAKE2 + TLS 握手
- 用户在通知栏点击发送,
BroadcastReceiver收到这 6 位字符串。 - App 连接本机回环地址上的配对端口,以这 6 位字符串作为预共享密钥运行 SPAKE2 握手,把本地预先生成的 TLS 客户端公钥写入系统信任库。
- 配对完成后,将证书与私钥持久化到 App 私有目录(
context.getFilesDir())。
阶段 4:常驻连接与特权进程注入
整个过程对用户完全静默。
用户视角
- 用户回到 App,点击主界面的 【启动并开始运行】。
- 屏幕显示加载条:“正在启动特权引擎(约 1 秒)……”
技术实现
1. 自动获取连接端口
通过 NsdManager 监听系统的主调试广播 _adb-tls-connect._tcp,秒级拿到真实的通信端口。
2. mTLS 隧道建立
使用阶段 3 存下的客户端证书连接本机回环地址上的该端口,双向 TLS 握手通过后,进入底层 ADB 协议层。
3. 释放并启动 DroidCast 守护进程
- 把 App Assets 里的
droidcast.dex写到设备临时目录/data/local/tmp/droidcast.dex。 - 通过 ADB 连接发送唯一的启动指令:
CLASSPATH=/data/local/tmp/droidcast.dex app_process / com.rayworks.droidcast.Main &- 这个
app_process进程启动后,天然继承 ADB 的uid=2000 (shell)身份,拥有调用SurfaceControl.screenshot()和注入触控的权限。 - DroidCast 在本机暴露一个高速 Socket/HTTP 端口,供主 App 访问。
阶段 5:创建虚拟副屏,并把目标应用放进去
用户视角
- 屏幕闪一下,主屏幕依然停留在你的 App 上(用户也可以切到微信、抖音,或者锁屏),主屏幕不会被目标应用的画面霸占。
- App 内显示一个悬浮小预览窗(或画中画),可以看到目标应用正在后台默默加载。
技术实现
1. 创建私有虚拟屏幕
App 调用 DisplayManager.createVirtualDisplay(...),创建一个例如 1280x720 的虚拟屏幕,拿到它的 virtualDisplayId。
2. 把目标 Activity 启动到该副屏
普通 App 没有权限调用 setLaunchDisplayId,必须通过刚刚建好的特权通道发送命令:
am start -n <包名>/<入口 Activity> --display <virtualDisplayId>此时目标应用在操作系统眼里是一个正常渲染的窗口,只不过画面渲染在“隐形”的副屏上,不受主屏前台干扰。
阶段 6:脚本高速工作循环
用户视角
- 脚本平稳运行,自动点击、自动寻路、自动操作。
- 用户可以在主屏自由聊微信,几乎感觉不到后台的存在。
技术实现
不再用低效的命令行,App 本地开一个循环线程,所有交互全部走 DroidCast 的特权 Socket:
┌────────────────────────────────────────┐
│ 主 App 的自动化核心控制逻辑 │
└────┬───────────────────────────────▲───┘
1. 请求截图 │ (HTTP GET / Socket) │ 3. 发送触控
(二进制流) │ │ tap(x, y, displayId)
▼────┴───────────────────────────────│───┐
│ DroidCast(uid 2000 后台守护进程) │
└────┬───────────────────────────────▲───┘
│ 反射直调 │ Binder 直调
▼ │
[ SurfaceControl ] [ InputManager ]
(抓取副屏 DisplayID 的帧) (向副屏 DisplayID 派发事件)- 找图:主 App 向 DroidCast 发送读取指令,DroidCast 毫秒级反射抓取副屏帧缓冲,以字节数组直接返回,交给主 App 做模板匹配或 OpenCV 处理。
- 点击:主 App 算出坐标后发指令给 DroidCast,DroidCast 以特权身份调用底层的
InputManager.injectInputEvent,把触控事件派发给指定的virtualDisplayId。
补充:第二天再次使用(含手机重启后)
阶段 3 的配对长期有效,之后的体验会简单很多:
- 用户打开 App,点击【开始运行】;
- App 后台通过 mDNS 重新扫描连接端口(每次开关无线调试,端口都可能变化);
- App 拿出上次保存的 TLS 密钥,静默完成握手;
- 重新拉起 DroidCast,创建虚拟屏,直接开始工作。
全程约 1 秒,用户没有多余操作,也不用再输入配对码。
