15 KiB
15 KiB
Android 客户端开发计划书
目标功能:屏幕浏览 + 操作控制(触控/键盘输入注入) 参考实现:
linux/与macos/,两者已验证的共用策略同样适用于 Android
一、代码复用分析
1.1 可 100% 直接复用(NDK 无改动)
| 文件 | 用途 | 依赖 |
|---|---|---|
common/ikcp.c/.h |
KCP 可靠 UDP 传输 | 纯 C,无 OS 依赖 |
common/aes.c/.h |
AES 加密 | 纯 C |
common/commands.h |
协议定义(全平台共享) | 无 |
common/client_auth_state.h |
认证状态机 | 无 |
common/posix_net_helpers.h |
POSIX socket 辅助函数 | POSIX(Android NDK 支持) |
common/sub_conn_thread.h |
子连接线程 | POSIX |
common/rtt_estimator.h |
RTT 估计器 | 无 |
common/logger.h |
日志 | 无 |
common/locker.h |
互斥锁 | std::mutex |
common/FileTransferV2.h |
V2 文件传输协议 | 无 |
common/xxhash.h |
xxHash 校验 | 纯头文件 |
client/IOCPClient.cpp/.h |
TCP/KCP 连接管理(Linux 已复用) | POSIX socket |
client/Buffer.cpp/.h |
数据缓冲区 | 无 |
client/sign_shim_unix.cpp |
签名垫片(Linux/macOS 已复用) | libsign.a |
1.2 可参考逻辑、重新实现 Android 版本
| 现有文件 | Android 对应实现 | 原因 |
|---|---|---|
linux/ScreenHandler.h |
android/cpp/ScreenHandler.h |
捕获 API 不同(MediaProjection vs X11) |
macos/InputHandler.mm |
ControlService.kt + main.cpp::DispatchControlEvent |
注入方式不同(AccessibilityService vs CGEvent) |
linux/SystemManager.h |
android/cpp/SystemManager.cpp(Phase 4) |
系统 API 不同 |
macos/H264Encoder.mm |
Android MediaCodec 路径(Java 侧) | 硬件编码器 API 不同 |
linux/main.cpp |
android/cpp/main.cpp |
参考连接逻辑,逐段移植 |
1.3 不复用(Windows 专属)
client/ScreenCapturerDXGI.h、client/IOCPBase.h、client/ScreenSpy.cpp(Windows GDI/DXGI)client/KeyboardManager.cpp(SendInput API)client/KernelManager.cpp、client/ServicesManager.cpp
二、技术选型
2.1 屏幕捕获
方案:MediaProjection API(Android 5.0+)
MediaProjection
└── VirtualDisplay (Surface)
└── MediaCodec (Surface input,零拷贝)
└── H.264 NALU → JNI → C++ ScreenHandler → 网络发送
- 无需 root,官方公开 API
- 通过
ForegroundService+FOREGROUND_SERVICE_MEDIA_PROJECTION维持后台运行 - 实际采用零拷贝路径(Path A):VirtualDisplay 的 Surface 直接绑定
MediaCodec输入 Surface,无 ImageReader 中间环节
2.2 视频编码
已采用:Android MediaCodec(硬件 H.264 加速)✅
- VirtualDisplay Surface → MediaCodec Surface input → H.264 NALU,零拷贝
- 关键帧前自动拼接 SPS+PPS,确保解码器可初始化
- 每 2 秒强制触发一次 IDR(兼容忽略
KEY_I_FRAME_INTERVAL的软编码器) - 编码分辨率:长边限制 1080,保持宽高比,宽高各自 2 对齐(H.264 要求)
编码流水线:
VirtualDisplay → MediaCodec(Surface input) → CODEC_CONFIG(SPS/PPS) + IDR+P 帧
→ onOutputBufferAvailable → JNI nativeOnH264Frame → C++ g_screenHandlers (broadcast) → TCP
2.3 输入控制
已采用:AccessibilityService(ControlService.kt)✅
| 事件 | 映射 |
|---|---|
WM_LBUTTONDOWN/MOVE/UP |
累积路径,UP 时判断:位移 < 10px → 单击(dispatchTap),否则 → 拖拽(dispatchTouchGesture) |
WM_LBUTTONDBLCLK |
双击:两次 80ms 手势,间隔 200ms |
WM_RBUTTONDOWN |
长按 600ms |
WM_MOUSEWHEEL |
delta > 0(滚轮上)→ 手指下划 +400px;delta < 0 → 手指上划 -400px |
WM_KEYDOWN Backspace/ESC |
GLOBAL_ACTION_BACK |
WM_KEYDOWN VK_HOME (0x24) |
GLOBAL_ACTION_HOME |
WM_KEYDOWN VK_APPS (0x5D) |
GLOBAL_ACTION_RECENTS |
WM_KEYDOWN PrtSc (0x2C) |
GLOBAL_ACTION_TAKE_SCREENSHOT |
关键实现细节:
- 协议:
COMMAND_SCREEN_CONTROL+ MSG64(固定 48 字节),坐标从 lParam(offset 24)读取,与 Windows/Linux/macOS 客户端完全一致;不读 pt.x/pt.y(64-bit MSG 与 MSG64 的 pt 偏移不同,直接读会得到错误 Y 坐标) - 坐标映射:
phys = enc * physSize / encSize(编码空间 → 物理屏幕空间) - 单击路径非退化:
moveTo(x,y); lineTo(x+1,y),避免 Android 静默拒绝零长度手势路径 - C++ → Kotlin 回调:
DispatchControlEvent()全局函数通过 JNI AttachCurrentThread →ControlService.onControlEvent(@JvmStatic)→ post 到主线程 Handler
2.4 网络传输
与 Linux 完全相同的 POSIX socket 路径:
- TCP 控制通道(
IOCPClient.cpp已在 Linux 验证) - KCP over UDP 视频流(
ikcp.c纯 C,NDK 直接编译,当前未启用) - TLS/AES 加密复用
common/aes.c
三、目录结构(实际)
android/
├── PLAN.md # 本文件
├── README.md # 编译与安装说明
├── build_apk.sh # 编译脚本(release/debug/clean)
├── yama-release.jks # 签名密钥库(密码通过 YAMA_PWD 环境变量传入)
├── app/
│ ├── build.gradle
│ ├── proguard-rules.pro # 保留 JNI 方法名
│ └── src/main/
│ ├── AndroidManifest.xml
│ ├── java/com/yama/client/
│ │ ├── MainActivity.kt # 权限申请 + 启动 Service
│ │ ├── CaptureService.kt # ForegroundService:MediaProjection + MediaCodec
│ │ ├── ControlService.kt # AccessibilityService:手势注入 + 全局键 + 活动窗口上报
│ │ └── YamaBridge.kt # JNI 声明(nativeInit/Stop/SetScreenSize/OnH264Frame)
│ ├── cpp/
│ │ ├── CMakeLists.txt
│ │ ├── main.cpp # NDK 入口:连接/心跳/DataProcess/DispatchControlEvent
│ │ ├── ScreenHandler.h # 子连接管理:发送 BitmapInfo/H264 帧,接收 SCREEN_CONTROL
│ │ ├── android_compat.h # 平台兼容头(强制注入替代修改共用源文件)
│ │ ├── common/ → ../../common/ # 符号链接(commands.h 等)
│ │ ├── client/ → ../../client/ # 符号链接(IOCPClient 等)
│ │ └── lib/
│ │ ├── arm64-v8a/libsign.a
│ │ ├── arm64-v8a/libzstd.a
│ │ ├── armeabi-v7a/libsign.a
│ │ └── armeabi-v7a/libzstd.a
│ └── res/
│ ├── mipmap-*/ic_launcher.png # 传统图标(各密度)
│ ├── mipmap-*/ic_launcher_foreground.png # Adaptive Icon 前景层
│ ├── mipmap-anydpi-v26/ic_launcher.xml # Adaptive Icon 定义
│ ├── values/colors.xml # ic_launcher_background (#F2F2F2)
│ ├── values/strings.xml
│ └── xml/accessibility_service_config.xml
├── build.gradle (project-level)
└── settings.gradle
注:原计划中的
InputHandler.cpp已合并至main.cpp(DispatchControlEvent全局函数)和ControlService.kt;SystemManager.cpp留待 Phase 4。
四、开发阶段
Phase 0:工程初始化 ✅ 已完成
- 创建 Android Studio 项目,配置 NDK + CMake
CMakeLists.txt引入common/、client/源文件(与linux/CMakeLists.txt同构)- 编译验证
IOCPClient.cpp + Buffer.cpp + sign_shim_unix.cpp + ikcp.c在 NDK 下无错误 SimplePlugins/sign_lib/新增sha256_portable.h(纯 C,零外部依赖)和build_android.sh,在 WSL + NDK r27c 下成功交叉编译,产物已复制至android/app/src/main/cpp/lib/{arm64-v8a,armeabi-v7a}/libsign.a
Phase 1:连接与协议 ✅ 已完成
- 移植
linux/main.cpp的连接逻辑到android/cpp/main.cpp CaptureService.kt通过YamaBridge.nativeInit()启动 C++ 网络线程- 实现登录握手(
LOGIN_INFOR)、心跳(TOKEN_HEARTBEAT+CMD_HEARTBEAT_ACKRTT 估算)、认证状态机(client_auth_state.h) - 设备信息上报:ANDROID_ID(XXH64 作为 ClientID)、型号、OS 版本、分辨率
- 心跳日志限流:60 秒最多打印一次,避免 logcat 刷屏
Phase 2:屏幕捕获与编码 ✅ 已完成
CaptureService.kt:申请MediaProjection,建立VirtualDisplay,Surface 直连MediaCodec- SPS/PPS 缓存,IDR 帧发送前自动拼接,子连接
COMMAND_NEXT后开始推流 ScreenHandler.h:发送TOKEN_BITMAPINFO(含ScreenSettings.QualityLevel=H264),SendH264封装帧头- 服务端 H264 解码器正常显示 Android 屏幕 ✅
- 服务端适配:
ScreenSpyDlg.cpp新增ComputeAdaptiveLayout()自适应缩放和信箱黑边,m_offsetX/Y修正坐标映射(已合入main分支)
Phase 3:操作控制 ✅ 已完成(branch: feature/android-remote-control)
ControlService.kt(AccessibilityService):手势注入、全局键、双击/长按/滚轮main.cpp::DispatchControlEvent():JNI 反向调用,将COMMAND_SCREEN_CONTROL路由到 KotlinScreenHandler.h::OnReceive:子连接也可接收COMMAND_SCREEN_CONTROL,统一调用DispatchControlEvent- 坐标 Bug 修复:64-bit MSG 与 MSG64 的
pt.x/pt.y偏移不同(MSG: offset 36,MSG64: offset 40);改为从 lParam(两者均在 offset 24)提取坐标,与其他所有客户端保持一致 - 单击无效 Bug 修复:零长度手势路径被 Android 静默拒绝;tap 改用
lineTo(x+1, y)非退化路径 - 代码审查修复:JNI
ExceptionClear()
Phase 3 后期修复 ✅ 已完成
发现并修复的生产问题:
| 问题 | 根因 | 修复文件 |
|---|---|---|
| 静止屏幕首帧黑屏 10-60 秒 | SurfaceFlinger 空闲优化:无脏区时不向 VirtualDisplay 推帧,REQUEST_SYNC_FRAME 永远排队等不到输入帧 |
CaptureService.forceFirstFrame():VirtualDisplay 临时 resize +2px 触发强制合成;main.cpp 在 SendBitmapInfo 后调用 ForceFirstFrameFromJava() |
| 部分硬件编码器 IDR 帧未被识别 | 高通等 SoC 对强制 IDR 不设置 BUFFER_FLAG_KEY_FRAME,SendLoop 将其当作 P 帧丢弃,首帧永远发不出 |
CaptureService.isNaluKeyframe():扫描 Annex-B NALU type 5/7/8 补充判断 |
H.264 推流在 SendBitmapInfo 前卡住 |
推送模式下服务端 COMMAND_NEXT 到达时 setManagerCallBack 尚未注册消息被丢弃,m_started 永远为 false |
ScreenHandler.h::SendBitmapInfo() 末尾直接 m_started=true; m_cond.notify_all() |
| 多用户不能同时观看/控制 | g_screenHandler 单指针 + g_screenSpyRunning 互斥锁,只允许一条子连接 |
main.cpp:改为 std::set<AndroidScreenHandler*> g_screenHandlers,nativeOnH264Frame 广播给全部 handler;每个 COMMAND_SCREEN_SPY 独立建立子连接,对齐 Windows/Linux/macOS 客户端行为 |
Phase 4:系统信息与稳定性(进行中)
已完成:
| 功能 | 说明 |
|---|---|
| 活动窗口上报 | ControlService 监听 TYPE_WINDOW_STATE_CHANGED,通过 JNI getActiveWindow() 上报当前前台包名;锁屏时上报 "Locked" |
| 心跳间隔下限 | Android 侧将服务端下发的 ReportInterval 强制限制为 max(收到值, 30),防止 5s 高频心跳耗电 |
| 服务端心跳超时延长 | CheckHeartbeat() 超时阈值从 max(60, interval*3) 改为 max(120, interval*3),与 30s 心跳间隔留出 4 倍余量 |
| CPU 频率上报 | GetCpuMHz() 优先读 /sys/devices/system/cpu/*/cpufreq/cpuinfo_max_freq,虚拟机无此节点时回退读 /proc/cpuinfo 的 BogoMIPS |
| 后台保活 | ForegroundService + 持久通知(Phase 2 已完成),豁免 Doze 模式网络限制 |
| 应用图标 | 自定义眼睛图标,支持 Android 8+ Adaptive Icon(前景层 + 浅灰背景),兼容各厂商形状裁切 |
| Release 签名 | yama-release.jks 随仓库分发,密码通过 YAMA_PWD 环境变量或交互输入,保证多机一致签名 |
待完成:
- 处理
MediaProjection被用户撤销的情况(弹出通知,引导重授权) - 连接断线自动重连后子连接同步重建(目前
ScreenSpyThread有 20 次重试,但主连接重连后子连接未同步恢复) WM_MBUTTONDOWN(中键)手势映射(目前静默丢弃)SystemManager封装:将 Java 层已上报的设备信息(型号、版本、IP)统一封装为独立模块
五、关键约束与风险
| 风险 | 影响 | 状态 |
|---|---|---|
libsign.a 无 ARM 构建 |
✅ 已解决:sha256_portable.h + build_android.sh |
|
Android 12+ MediaProjection 需每次重新申请 |
后台录屏被中断 | ForegroundService 保活 + 通知引导重授权 |
AccessibilityService 用户需手动开启 |
操作控制功能受限 | UI 引导流程,说明开启步骤 |
| 某些厂商 ROM 限制后台 Service | 连接断开 | 电池优化白名单申请 |
| H.264 Baseline Level 对齐 | 服务端解码兼容性 | MediaCodec 输出指定 profile=Baseline,与 x264 现有配置一致 |
AccessibilityService 无法向系统设置页面注入手势 |
设置界面无法远程操作 | Android 安全限制,无解,提示用户 |
六、代码审查注意事项
最高原则:既有功能无破坏
增加 Android 客户端支持时,Windows/Linux/macOS 客户端的全部既有功能必须保持正常。每次改动上线前须验证:
| 检查项 | 说明 |
|---|---|
isAndroidRemote 初始值为 false |
非 Android 客户端走原有代码路径,不受影响 |
updateUIForOrientation() Android 分支有 isAndroidRemote && 守卫 |
非 Android 客户端不会进入沉浸模式分支 |
NotifyResolutionChange 新参数有默认值 = "" |
现有调用点即便漏传也能正常编译和运行 |
FindHostByClientID 与 FindHostByIP 一致的指针返回约定 |
ScreenSpyDlg 在 UI 线程(OnInitDialog)调用,OnReceiveComplete 在 IO 线程调用,后者读 additonalInfo[] 为只读操作,低概率竞争 |
Web 端坐标映射 getTouchPos 使用 getBoundingClientRect() |
动态取 canvas 实际显示尺寸,CSS 拉伸不影响坐标精度 |
| Android 边缘手势检测有时间和距离双重门槛 | 普通慢速拖拽不会被误识别为系统手势 |
审查流程
git diff HEAD逐文件审查,重点关注非 Android 代码路径的改动- 确认所有新增条件分支均以
isAndroidRemote、client_type === 'APK'等 Android 专有标志守卫 - 共享协议结构体(
commands.h、LOGIN_INFOR、szReserved)改动需确认向前/向后兼容 - 服务端 C++ 改动在 Windows 下编译,前端
index.html在桌面和移动浏览器两端验证
七、不在本期范围
- 音频监听(
AudioRecord+ Opus 编码) - 文件管理(已有
FileTransferV2,后期可直接接入) - 摄像头(前/后摄)
- Shell 终端(
/system/bin/sh+ PTY,可参考PTYHandler.h) - Root 特权功能(uinput、内核注入)