Files
SimpleRemoter/android/PLAN.md
2026-06-21 23:00:05 +02:00

15 KiB
Raw Permalink Blame History

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 辅助函数 POSIXAndroid 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.cppPhase 4 系统 API 不同
macos/H264Encoder.mm Android MediaCodec 路径Java 侧) 硬件编码器 API 不同
linux/main.cpp android/cpp/main.cpp 参考连接逻辑,逐段移植

1.3 不复用Windows 专属)

  • client/ScreenCapturerDXGI.hclient/IOCPBase.hclient/ScreenSpy.cppWindows GDI/DXGI
  • client/KeyboardManager.cppSendInput API
  • client/KernelManager.cppclient/ServicesManager.cpp

二、技术选型

2.1 屏幕捕获

方案MediaProjection APIAndroid 5.0+

MediaProjection
  └── VirtualDisplay (Surface)
        └── MediaCodec (Surface input零拷贝)
              └── H.264 NALU → JNI → C++ ScreenHandler → 网络发送
  • 无需 root官方公开 API
  • 通过 ForegroundService + FOREGROUND_SERVICE_MEDIA_PROJECTION 维持后台运行
  • 实际采用零拷贝路径Path AVirtualDisplay 的 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 输入控制

已采用AccessibilityServiceControlService.kt

事件 映射
WM_LBUTTONDOWN/MOVE/UP 累积路径UP 时判断:位移 < 10px → 单击dispatchTap否则 → 拖拽dispatchTouchGesture
WM_LBUTTONDBLCLK 双击:两次 80ms 手势,间隔 200ms
WM_RBUTTONDOWN 长按 600ms
WM_MOUSEWHEEL delta > 0滚轮上→ 手指下划 +400pxdelta < 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 字节),坐标从 lParamoffset 24读取,与 Windows/Linux/macOS 客户端完全一致;不读 pt.x/pt.y64-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 纯 CNDK 直接编译,当前未启用)
  • 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        # ForegroundServiceMediaProjection + 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.cppDispatchControlEvent 全局函数)和 ControlService.ktSystemManager.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_IDXXH64 作为 ClientID、型号、OS 版本、分辨率
  • 心跳日志限流60 秒最多打印一次,避免 logcat 刷屏

Phase 2屏幕捕获与编码 已完成

  • CaptureService.kt:申请 MediaProjection,建立 VirtualDisplaySurface 直连 MediaCodec
  • SPS/PPS 缓存IDR 帧发送前自动拼接,子连接 COMMAND_NEXT 后开始推流
  • ScreenHandler.h:发送 TOKEN_BITMAPINFO(含 ScreenSettings.QualityLevel=H264SendH264 封装帧头
  • 服务端 H264 解码器正常显示 Android 屏幕
  • 服务端适配:ScreenSpyDlg.cpp 新增 ComputeAdaptiveLayout() 自适应缩放和信箱黑边,m_offsetX/Y 修正坐标映射(已合入 main 分支)

Phase 3操作控制 已完成branch: feature/android-remote-control

  • ControlService.ktAccessibilityService):手势注入、全局键、双击/长按/滚轮
  • 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 36MSG64: 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.cppSendBitmapInfo 后调用 ForceFirstFrameFromJava()
部分硬件编码器 IDR 帧未被识别 高通等 SoC 对强制 IDR 不设置 BUFFER_FLAG_KEY_FRAMESendLoop 将其当作 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_screenHandlersnativeOnH264Frame 广播给全部 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/cpuinfoBogoMIPS
后台保活 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 新参数有默认值 = "" 现有调用点即便漏传也能正常编译和运行
FindHostByClientIDFindHostByIP 一致的指针返回约定 ScreenSpyDlg 在 UI 线程OnInitDialog调用OnReceiveComplete 在 IO 线程调用,后者读 additonalInfo[] 为只读操作,低概率竞争
Web 端坐标映射 getTouchPos 使用 getBoundingClientRect() 动态取 canvas 实际显示尺寸CSS 拉伸不影响坐标精度
Android 边缘手势检测有时间和距离双重门槛 普通慢速拖拽不会被误识别为系统手势

审查流程

  1. git diff HEAD 逐文件审查,重点关注非 Android 代码路径的改动
  2. 确认所有新增条件分支均以 isAndroidRemoteclient_type === 'APK' 等 Android 专有标志守卫
  3. 共享协议结构体(commands.hLOGIN_INFORszReserved)改动需确认向前/向后兼容
  4. 服务端 C++ 改动在 Windows 下编译,前端 index.html 在桌面和移动浏览器两端验证

七、不在本期范围

  • 音频监听(AudioRecord + Opus 编码)
  • 文件管理(已有 FileTransferV2,后期可直接接入)
  • 摄像头(前/后摄)
  • Shell 终端(/system/bin/sh + PTY可参考 PTYHandler.h
  • Root 特权功能uinput、内核注入