Feat: Android client Phase 0-3 full implementation
This commit was merged in pull request #3.
This commit is contained in:
266
android/PLAN.md
Normal file
266
android/PLAN.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# 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_ACK` RTT 估算)、认证状态机(`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` 路由到 Kotlin
|
||||
- `ScreenHandler.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 构建~~ | ~~Phase 0 阻塞~~ | ✅ 已解决:`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 边缘手势检测有时间和距离双重门槛 | 普通慢速拖拽不会被误识别为系统手势 |
|
||||
|
||||
### 审查流程
|
||||
|
||||
1. `git diff HEAD` 逐文件审查,重点关注非 Android 代码路径的改动
|
||||
2. 确认所有新增条件分支均以 `isAndroidRemote`、`client_type === 'APK'` 等 Android 专有标志守卫
|
||||
3. 共享协议结构体(`commands.h`、`LOGIN_INFOR`、`szReserved`)改动需确认向前/向后兼容
|
||||
4. 服务端 C++ 改动在 Windows 下编译,前端 `index.html` 在桌面和移动浏览器两端验证
|
||||
|
||||
---
|
||||
|
||||
## 七、不在本期范围
|
||||
|
||||
- 音频监听(`AudioRecord` + Opus 编码)
|
||||
- 文件管理(已有 `FileTransferV2`,后期可直接接入)
|
||||
- 摄像头(前/后摄)
|
||||
- Shell 终端(`/system/bin/sh` + PTY,可参考 `PTYHandler.h`)
|
||||
- Root 特权功能(uinput、内核注入)
|
||||
Reference in New Issue
Block a user