Skip to content

安卓无线调试提权与虚拟副屏自动化

约 1951 字大约 7 分钟

Android

2026-10-04

统计数据加载中...

前言

想让一个普通 Android App 在后台“自己操作”另一个应用,比如截图、找图、点击,会遇到一堵墙:普通应用没有权限截取其他应用的画面,也不能向其他应用注入触控。常见解法是借助 ADB 的 shell 身份(uid=2000)。

本文拆解一套完整的落地流程,分成两个视角:

  • 用户视角:交互尽量简单,把使用门槛降到最低
  • 技术实现:底层 Socket、mDNS 与特权进程注入

流程全景

[阶段 1: 首次启动检查] ──> [阶段 2: 引导开启环境] ──> [阶段 3: 一次性配对提权]
                                                              │
[阶段 6: 脚本高速循环] <── [阶段 5: 创建虚拟副屏] <── [阶段 4: 注入特权守护进程]

阶段 1:首次启动与前置检查

用户视角

  • 首次打开 App,进入主界面。
  • 弹出权限弹窗,请求通知权限(Android 13+ 必需,并告知用户“用于后续快捷输入验证码”),用户点击允许。
  • 主界面显示状态仪表盘:
    • [√] Wi-Fi 网络已连接(未连接 Wi-Fi 时提示“请先连接 Wi-Fi 后继续”)
    • [ ] 开发者模式:未开启,旁边亮起按钮 【第一步:开启开发者模式】

技术实现

  1. 网络检查:通过 ConnectivityManager 监听,确认手机连接了有效的局域网 Wi-Fi(无线调试的物理前提)。
  2. 状态检测:读取系统设置判断开发者模式是否开启:
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:无线调试配对提权

这是整套流程的核心难点,但只需要做一次。

用户视角

  1. 用户点击 【第二步:无线调试配对】。
  2. 页面直接跳到系统的“无线调试”设置页,跳过繁琐的开发者菜单列表。
  3. 手机通知栏出现 App 发出的常驻通知:“正在监听调试端口,请打开开关并点击配对码……”
  4. 用户打开顶部的“无线调试”总开关,再点击“使用配对码配对设备”。
  5. 系统弹窗显示 6 位配对码。
  6. 用户不需要切回 App,直接下拉通知栏,App 的通知卡片上已经变成了输入框,输入这 6 位数字,点击右侧的 【发送】。
  7. 系统弹窗自动消失(配对成功),通知提示:“配对成功!特权已激活”。
  8. 用户返回 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 派发事件)
  1. 找图:主 App 向 DroidCast 发送读取指令,DroidCast 毫秒级反射抓取副屏帧缓冲,以字节数组直接返回,交给主 App 做模板匹配或 OpenCV 处理。
  2. 点击:主 App 算出坐标后发指令给 DroidCast,DroidCast 以特权身份调用底层的 InputManager.injectInputEvent,把触控事件派发给指定的 virtualDisplayId。

补充:第二天再次使用(含手机重启后)

阶段 3 的配对长期有效,之后的体验会简单很多:

  1. 用户打开 App,点击【开始运行】;
  2. App 后台通过 mDNS 重新扫描连接端口(每次开关无线调试,端口都可能变化);
  3. App 拿出上次保存的 TLS 密钥,静默完成握手;
  4. 重新拉起 DroidCast,创建虚拟屏,直接开始工作。

全程约 1 秒,用户没有多余操作,也不用再输入配对码。