Files
pangolin/docs/superpowers/plans/2026-06-22-android-client.md
T

444 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Pangolin Android 客户端 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 把已有的 M2 Android PoC 推到真机能连真实节点出网,并补齐切节点 + KillSwitch(清 TODO 11G)。
**Architecture:** Flutter UI 全平台共享、零改动。Android 走 `VpnService` + 内嵌 sing-box `libbox.aar`gomobile 绑定),经冻结的 `pangolin/vpn` MethodChannel/EventChannel 与 Dart 通信。配置由服务端渲染、客户端原样下发。本质是「先构建 aar → 对着真实 libbox API 修原生代码到能编译运行 → 接线 Dart → 真机端到端 → 补 11G」。
**Tech Stack:** Flutter 3.44 / Dart、Kotlin、Android `VpnService`、sing-box `v1.13.12` libboxgomobile)、Go 1.24.3、Android NDK。
**设计依据:** `docs/android-client-design.html``docs/killswitch-design.html`
## Global Constraints
- sing-box 内核版本 `v1.13.12``app/kernel/VERSION` 锚定,禁止脚本内硬编码)。
- Go 工具链:用官方 `golang.org/dl/go1.24.3` 当启动器(本机 Go 1.26.1 不动);sing-box go.mod 为 `go 1.24.7`GOTOOLCHAIN=auto 会自动下载并切到 **1.24.7** 编译。
- **gomobile = `github.com/sagernet/gomobile@v0.1.12`**SagerNet fork,非上游 `golang.org/x/mobile`;对照 sing-box v1.13.12 go.mod 与 `cmd/internal/build_libbox`)。原 VERSION 里的上游 pin 是失效 revision。
- libbox bind ldflags 必须含 **`-checklinkname=0`**(放行 `badlinkname``//go:linkname`Go 1.23+ 默认 checklinkname=1 会拒 `os.checkPidfdOnce` 等,缺则链接失败);build tags 含 `badlinkname,tfogo_checklinkname0`
- 构建需 **JDK**gomobile bind 末段 javac):用 Android Studio JBROpenJDK 21);macOS `/usr/bin/javac` 仅桩。
- 国内网络:`GOPROXY=https://goproxy.cn,direct` + `GOSUMDB=sum.golang.google.cn`proxy.golang.org 连不上)。
- Android 构建栈(Flutter 3.44 要求):**Gradle 8.7 + 声明式 plugins block + AGP 8.6.0 + Kotlin 2.2.0**(旧脚手架是 Gradle 8.0/AGP 8.1/命令式插件,全需升级迁移)。
- libbox build tags`with_quic,with_utls,with_clash_api,with_gvisor`(脚本默认,勿改)。
- `minSdkVersion 21`libbox `-androidapi 21` 要求),`targetSdkVersion` 跟随 flutter。
- Channel 契约冻结(`pangolin/vpn` + `pangolin/vpn/status` + `pangolin/vpn/stats`),方法签名见 `client/lib/bridge/vpn_bridge.dart`**不得私改**。
- 配置来源:服务端 `POST /v1/nodes/:id/connect`**客户端绝不拼配置**。
- **禁止乐观显示**:连接键 `on` 必须由 `bridge.statusStream`(内核回调)确认,`start()` 返回 ≠ on。
- 测试基线:x86_64 模拟器跑通 → Vivo X200Android 16)主力真机 → 华为 HarmonyOS 4.2 兼容性抽查。
- NDK`/Users/wangjia/Library/Android/sdk/ndk/27.0.12077973`(构建前 export `ANDROID_NDK_HOME`)。
---
### Task 1: Go 1.24.3 工具链就位
**Files:** 无(环境配置)。
**Interfaces:**
- Produces: 可执行 `go1.24.3``$(go env GOPATH)/bin/go1.24.3`),供 Task 2 构建用。
- [ ] **Step 1: 安装官方 Go 1.24.3 多版本 shim**
Run:
```bash
go install golang.org/dl/go1.24.3@latest
go1.24.3 download
```
Expected: `Downloaded ... go1.24.3 ...`,无错误。
- [ ] **Step 2: 验证版本与 NDK**
Run:
```bash
go1.24.3 version
ls -d /Users/wangjia/Library/Android/sdk/ndk/27.0.12077973
```
Expected: `go version go1.24.3 darwin/arm64`NDK 目录存在。
- [ ] **Step 3: 无需 commit**(纯环境,无文件改动)。
---
### Task 2: 构建 libbox.aar(里程碑 A
**Files:**
- 产出: `app/kernel/dist/android/libbox.aar`git 忽略产物,不入库)。
**Interfaces:**
- Consumes: Task 1 的 `go1.24.3`
- Produces: `libbox.aar`,含 `go.libbox.*` 类,供 Task 3 解析、Task 5 链接。
- [ ] **Step 1: 用 Go 1.24.3 跑构建脚本**
`build-android.sh` 内部用 `command -v go` 取 Go,需临时让 `go` 指向 1.24.3。用 PATH 注入 shim 目录的方式(不改脚本):
Run:
```bash
cd /Users/wangjia/code/pangolin/.claude/worktrees/feature+android/app/kernel
export ANDROID_NDK_HOME=/Users/wangjia/Library/Android/sdk/ndk/27.0.12077973
GOROOT_1243="$(go1.24.3 env GOROOT)"
PATH="${GOROOT_1243}/bin:${PATH}" go version # 确认此 shell 内 go=1.24.3
PATH="${GOROOT_1243}/bin:${PATH}" bash build-android.sh
```
Expected: 末尾 `✓ 构建完成: .../dist/android/libbox.aar` + 三个 `.so`arm64-v8a/armeabi-v7a/x86_64)列出。
注意:首次会 `git clone sing-box`(约数分钟)+ `gomobile bind`(数分钟~十几分钟)。
- [ ] **Step 2: 验证产物三 ABI 齐**
Run:
```bash
unzip -l app/kernel/dist/android/libbox.aar | grep -E '\.so$'
```
Expected: 至少 `jni/arm64-v8a/libgojni.so``jni/armeabi-v7a/...``jni/x86_64/...` 三条。
- [ ] **Step 3: 无需 commit**aar 为忽略产物)。若 `gomobile bind` 失败,记录报错——多半是 Go/gomobile 组合问题,回到 Task 1 复核版本。
---
### Task 3: 解析真实 libbox API(里程碑 B 发现)
**Files:**
- 创建: `app/kernel/dist/android/libbox-api.txt`javap 输出存档,便于 Task 5/9 对照;可入库或留作临时参考)。
**Interfaces:**
- Consumes: Task 2 的 `libbox.aar`
- Produces: `go.libbox` 关键类的真实方法签名清单(`Libbox``BoxService``TunOptions``PlatformInterface``CommandClient``CommandClientHandler``CommandClientOptions``StatusMessage``OutboundGroupIterator`)。
- [ ] **Step 1: 解包 aar 取 classes.jarjavap 导出签名**
Run:
```bash
cd /Users/wangjia/code/pangolin/.claude/worktrees/feature+android/app/kernel/dist/android
mkdir -p .inspect && cd .inspect
unzip -o ../libbox.aar classes.jar >/dev/null
for c in Libbox BoxService TunOptions PlatformInterface CommandClient CommandClientHandler CommandClientOptions StatusMessage OutboundGroupIterator BoxService\$Companion; do
echo "===== go.libbox.$c ====="
javap -classpath classes.jar "go.libbox.$c" 2>/dev/null || echo "(类不存在,名称可能不同)"
done | tee ../libbox-api.txt
```
Expected: 各类方法签名打印到屏幕与 `libbox-api.txt`。重点记录:
- 创建 BoxService 的入口(`Libbox.newService` / `new BoxService(...)` / 参数个数与类型)。
- `TunOptions` 的 getter 真名(`getInet4Address()` vs `inet4Address()``getMTU()`/`getMtu()`)。
- `CommandClient` 选 outbound 的方法(如 `selectOutbound(group, tag)`)。
- `StatusMessage` 流量字段(`getUploadTotal()` 等)。
- [ ] **Step 2: 比对现有 Kotlin 的「猜测点」**
Run:
```bash
grep -n "// libbox API" /Users/wangjia/code/pangolin/.claude/worktrees/feature+android/client/android/app/src/main/kotlin/com/pangolin/pangolin_vpn/PangolinVpnService.kt
```
Expected: 列出所有标注「libbox API」的行;逐条对照 `libbox-api.txt` 标记需改的方法名(用于 Task 5)。
- [ ] **Step 3: Commit(存档 API 清单)**
```bash
cd /Users/wangjia/code/pangolin/.claude/worktrees/feature+android
git add app/kernel/dist/android/libbox-api.txt
git commit -m "docs(android): 存档 libbox.aar 真实 API 签名清单(Task 3)"
```
(若 `dist/` 被 .gitignore 忽略,改存到 `docs/` 下并相应调整路径。)
---
### Task 4: Dart provider 接 Android → VpnNativeBridge
**Files:**
- Modify: `client/lib/bridge/vpn_bridge_provider.dart`(约 27 行,加 Android 分派)。
**Interfaces:**
- Consumes: 现有 `VpnNativeBridge``vpn_bridge.dart`,走 `pangolin/vpn` 通道)。
- Produces: Android 运行时返回 `VpnNativeBridge` 而非 mock。
- [ ] **Step 1: 改平台分派**
`vpn_bridge_provider.dart` 中:
```dart
if (Platform.isMacOS || Platform.isLinux || Platform.isWindows) {
return DesktopVpnBridge();
}
```
之前加入 Android 分支:
```dart
if (Platform.isAndroid) {
return VpnNativeBridge();
}
```
并把文件顶部注释「其他平台(iOS / Web / 测试)→ VpnBridgeMock」更新为「iOS / Web / 测试 → VpnBridgeMockAndroid → VpnNativeBridge」。
- [ ] **Step 2: 静态分析**
Run:
```bash
cd /Users/wangjia/code/pangolin/.claude/worktrees/feature+android/client
flutter analyze lib/bridge/vpn_bridge_provider.dart
```
Expected: `No issues found!`
- [ ] **Step 3: Commit**
```bash
cd /Users/wangjia/code/pangolin/.claude/worktrees/feature+android
git add client/lib/bridge/vpn_bridge_provider.dart
git commit -m "feat(android): Dart provider 接 Android→VpnNativeBridge(Task 4)"
```
---
### Task 5: 修原生代码对齐真实 API 并编译通过(里程碑 B)
**Files:**
- Modify: `client/android/app/src/main/kotlin/com/pangolin/pangolin_vpn/PangolinVpnService.kt`(按 Task 3 清单逐处校正方法名)。
- Modify(按需): `MainActivity.kt``VpnEventBus.kt`
**Interfaces:**
- Consumes: Task 2 的 `libbox.aar``build.gradle` 已配 `implementation files(libboxAar)`)、Task 3 的 `libbox-api.txt`
- Produces: 可编译的 debug APK。
- [ ] **Step 1: 按 Task 3 清单校正 `PangolinVpnService.kt` 的 libbox 调用**
对照 `libbox-api.txt`,逐处替换标注 `// libbox API` 的方法名(`Libbox.newBoxService`/`service.start()`/`TunOptions.inet4Address()`/`mtu()`/`CommandClientOptions` 字段/`StatusMessage` getter 等)为真实签名。`TunOptions` getter 已有 try-catch 兜底,优先改成真名以免走兜底默认值。
- [ ] **Step 2: 构建 debug APK(只 arm64 加速首轮)**
Run:
```bash
cd /Users/wangjia/code/pangolin/.claude/worktrees/feature+android/client
flutter build apk --debug --target-platform android-arm64
```
Expected: `✓ Built build/app/outputs/flutter-apk/app-debug.apk`。若报 Kotlin 编译错(未知方法/类型不符)→ 回 Step 1 对照 `libbox-api.txt` 再修。
- [ ] **Step 3: 全量 analyze**
Run:
```bash
flutter analyze
```
Expected: `No issues found!`(或仅既有无关告警)。
- [ ] **Step 4: Commit**
```bash
cd /Users/wangjia/code/pangolin/.claude/worktrees/feature+android
git add client/android/app/src/main/kotlin/com/pangolin/pangolin_vpn/
git commit -m "fix(android): PangolinVpnService 对齐真实 libbox API,编译通过(Task 5)"
```
---
### Task 6: 模拟器冒烟——启动 + 通道连通
**Files:** 无代码改动(验证任务)。
**Interfaces:**
- Consumes: Task 4/5 产物。
- Produces: 确认 app 在 x86_64 模拟器启动、通道注册、初始状态 `off`
- [ ] **Step 1: 起 x86_64 模拟器并装运行**
Run:
```bash
adb devices # 确认有 emulator-xxxx;无则先在 Android Studio 启一个 x86_64 AVD
cd /Users/wangjia/code/pangolin/.claude/worktrees/feature+android/client
flutter run -d emulator --debug # 或 flutter run 选模拟器
```
Expected: app 启动到登录/主页,无崩溃。
- [ ] **Step 2: 验证通道初始状态**
观察 logcat
```bash
adb logcat -s PangolinMainActivity PangolinVpnService flutter | head -40
```
Expected: 见 `MethodChannel: getStatus`、status channel `onListen``off`UI 连接键为 off 态。
- [ ] **Step 3: 无需 commit。**
---
### Task 7: 模拟器端到端连通(里程碑 C)
**Files:** 无代码改动(验证任务;若发现 openTun/DNS 问题则回 Task 5 修)。
**Interfaces:**
- Consumes: 服务端真实 config(经登录→选节点→`fetchConfig` 自动获取)。
- Produces: 真隧道连通、可访问被墙站点。
- [ ] **Step 1: 登录并连接**
在 app 内登录(真账号)→ 选 RackNerd 节点 → 点连接。授予 VPN 授权弹窗。
观察:
```bash
adb logcat -s PangolinVpnService | grep -E "openTun|BoxService started|postStatus"
```
Expected: `openTun: fd=...``BoxService started` → status `on`;连接键转 on(计时开始)。
- [ ] **Step 2: 验证真出网 + DNS(铁律)**
在模拟器浏览器或 `adb shell` 访问被墙站点:
```bash
adb shell curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" https://www.google.com
```
Expected: `200 ...`。若超时但能连其它国内站 → 多半 DNS 劫持规则缺失,检查服务端下发 config 的 `route.rules` 首条是否 `hijack-dns`
- [ ] **Step 3: 断开验证**
点断开 → status `off`、连接键复位、`doStop` 日志干净(无重入崩溃)。
- [ ] **Step 4: 无代码则无 commit;若 Task 5 文件有修复则 commit。**
---
### Task 8: 统计走字(里程碑 D)
**Files:** 验证为主;若字段不准则 Modify `PangolinVpnService.kt``writeStatus` 映射)。
**Interfaces:**
- Consumes: libbox `CommandClient` STATUS(或 TrafficStats 兜底)。
- Produces: `pangolin/vpn/stats` 每秒一帧,UI 统计页跳动。
- [ ] **Step 1: 观察 stats 帧**
连接状态下:
```bash
adb logcat -s PangolinVpnService | grep -E "CommandClient connected|TrafficStats fallback"
```
Expected: 见 `CommandClient connected`(优先)或兜底日志;UI 统计页上/下行字节与速率每秒更新。
- [ ] **Step 2: 校正字段(如需)**
若速率恒 0 或字节不增 → 对照 Task 3 的 `StatusMessage` getter 真名修 `writeStatus` 映射,重编重测。
- [ ] **Step 3: Commit(如有修复)**
```bash
git add client/android/app/src/main/kotlin/com/pangolin/pangolin_vpn/PangolinVpnService.kt
git commit -m "fix(android): 校正统计字段映射(Task 8)"
```
---
### Task 9: 切节点 selectOutbound + getActiveOutbound(里程碑 E / 11G
**Files:**
- Modify: `PangolinVpnService.kt`(加 libbox CommandClient group 选择 + 查询)。
- Modify: `MainActivity.kt``selectOutbound`/`getActiveOutbound` 从 stub 改为转发到 Service)。
**Interfaces:**
- Consumes: Task 3 的 CommandClient 选 outbound API 签名;服务端 config 的出口组名(`auto` urltest + `reality-out`/`hy2-out`)。
- Produces: `selectOutbound(tag)` 切换不断连;`getActiveOutbound()` 返回当前出口 tag。语义复刻桌面 `desktop_vpn_bridge.dart``selectProxy`/`getProxies`
- [ ] **Step 1: 确认组名**
```bash
grep -n "\"tag\"\|selector\|urltest\|proxyTags" /Users/wangjia/code/pangolin/.claude/worktrees/feature+android/server/internal/httpapi/clientconfig.go
```
Expected: 确认可选组名(当前为 urltest `auto`;如需手动选需服务端含 `selector` 组——若没有则 selectOutbound 作用于 `auto` 组内成员或触发服务端补 selector,按实际 config 决定)。
- [ ] **Step 2: 实现 Service 侧 selectOutbound/getActiveOutbound**
`PangolinVpnService` 暴露静态/单例方法,用 libbox `CommandClient`(按 Task 3 真名)执行 group 选择与查询;`MainActivity` 的对应 MethodChannel 分支从 stub 改为调用之。
- [ ] **Step 3: 编译 + 真机/模拟器验证**
Run:
```bash
cd client && flutter build apk --debug --target-platform android-arm64
```
连接后在节点页切换出口:观察不断连、`getActiveOutbound` 返回值随之变、UI 高亮正确。
- [ ] **Step 4: Commit**
```bash
git add client/android/app/src/main/kotlin/com/pangolin/pangolin_vpn/
git commit -m "feat(android): selectOutbound/getActiveOutbound 经 libbox CommandClient 切节点(Task 9,11G)"
```
---
### Task 10: KillSwitch L1 + 引导系统 Always-on(里程碑 F / 11G
**Files:**
- Modify: `MainActivity.kt``setKillSwitch` 从 stub 改为:缓存偏好 + 触发以 `strict_route` 重启隧道;并暴露跳系统 Always-on 设置的方法)。
- Modify: `PangolinVpnService.kt`(启动时按偏好将 config TUN `strict_route` 置位——可复用桌面 `applyKillSwitchToConfig` 的等价 JSON 改写逻辑,在 Kotlin 侧实现)。
- ModifyUI 文案): `client/lib/l10n/strings_zh.dart` + `strings_en.dart`KillSwitch 旁诚实标注「彻底防泄漏需到系统设置开启 Always-on」)。
**Interfaces:**
- Consumes: 设计依据 `docs/killswitch-design.html`L1 语义 + 引导 L3)。
- Produces: `setKillSwitch(on)``strict_route`(与 Windows 一致)+ 系统 Always-on 引导入口。
- [ ] **Step 1: Kotlin 侧实现 strict_route 改写**
在 Service 启动前对 configJson 做 JSON 改写:`inbounds``type==tun` 的项设 `strict_route = on``setKillSwitch(on)` 若隧道在跑则以新偏好重启(短暂重连,与桌面一致)。
- [ ] **Step 2: Always-on 引导入口**
`setKillSwitch(on)` 为 true 时(或 UI 按钮)跳转系统 VPN 设置:
```kotlin
startActivity(Intent("android.settings.VPN_SETTINGS"))
```
(部分 ROM 落不到「Always-on」子页,落到 VPN 列表即可;华为/Vivo 单独适配留 Task 11。)
- [ ] **Step 3: UI 文案诚实标注**
在 KillSwitch 开关旁加说明文案(走 l10n,**无红线词**),明确 app 内 = L1、彻底防泄漏需系统 Always-on。
- [ ] **Step 4: 编译 + analyze + 验证**
Run:
```bash
cd client && flutter build apk --debug --target-platform android-arm64 && flutter analyze
```
连接下开 KillSwitch:观察重连后 `strict_route` 生效(停内核即断网);点引导跳到系统 VPN 设置。
- [ ] **Step 5: Commit**
```bash
git add client/android/app/src/main/kotlin/com/pangolin/pangolin_vpn/ client/lib/l10n/
git commit -m "feat(android): KillSwitch L1(strict_route)+引导系统 Always-on(Task 10,11G)"
```
---
### Task 11: 真机验证(Vivo X200 + 华为 4.2
**Files:** 验证为主;厂商保活引导若需适配则 Modify `MainActivity.kt`
**Interfaces:**
- Consumes: 全部前序产物。
- Produces: 真机端到端连通 + 后台保活验证 + 厂商电池优化引导适配。
- [ ] **Step 1: Vivo X200Android 16,主力)全链路**
插线 `adb devices` 确认 → `flutter run -d <vivo> --release` 或装 release APK。跑通:登录→连接→访问被墙站→切节点→KillSwitch→断开。重点验证 **targetSdk 高版本下前台服务 specialUse 不被限**、后台挂 30 分钟不被杀(需在 Vivo 设置允许后台+自启动)。
- [ ] **Step 2: 华为 HarmonyOS 4.2(兼容抽查)**
装 APK 跑通连接主链路。华为后台保活更激进——验证电池优化引导 Intent 是否落对页面;若落不到,加华为定制 Intent 兜底(`Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` 之外的 ROM 专属页)。
- [ ] **Step 3: Commit(如有厂商适配)**
```bash
git add client/android/app/src/main/kotlin/com/pangolin/pangolin_vpn/MainActivity.kt
git commit -m "fix(android): 厂商(Vivo/华为)后台保活引导适配(Task 11)"
```
- [ ] **Step 4: 标记 todo #2 完成待验收**
```bash
node ~/.claude/skills/todo/todo.mjs status 2 done
```
---
## 验收总览(对应里程碑)
| 里程碑 | Task | 完成判据 |
|---|---|---|
| A 构建内核 | 2 | libbox.aar 三 ABI 齐 |
| B 编译链接 | 35 | debug APK 构建成功、analyze 零警告 |
| C 端到端连通 | 6–7 | 模拟器实测科学上网、UI 三态正确 |
| D 统计走字 | 8 | stats 每秒跳动且数值合理 |
| E 切节点 | 9 | 切出口不断连、当前出口正确 |
| F KillSwitch | 10 | strict_route 生效 + Always-on 引导 + 诚实文案 |
| 真机 | 11 | Vivo X200 全链路 + 华为兼容抽查 + 后台保活 |