merge: maestro/tsk_iP5-_Ztq5THx [桥接 API 面 Channel 契约 + 三端原生骨架] [tsk_rcNKc3GQHBUd]

合并冲突补救:原分支从 merge base 7871512 分叉,仅修改 client/ 目录,
与 main 新增的 server/(apierr/idgen/CONVENTIONS.md) 完全不重叠,
无真实文件冲突,手动应用 feature branch 的全部 client/ 改动。

client 侧新增:
- lib/bridge/vpn_bridge.dart      — Dart↔原生通道契约(冻结)
- lib/bridge/vpn_bridge_mock.dart — 假内核,UI 联调用
- lib/bridge/kernel_process.dart  — 桌面子进程管理接口
- ios/PacketTunnel/{Info.plist,PacketTunnelProvider.swift} — NEPacketTunnel 骨架
- ios/Runner/VpnManager.swift     — NETunnelProviderManager 封装
- android/.../PangolinVpnService.kt — VpnService 骨架
- android/.../VpnEventBus.kt      — Application 级状态总线
- test/bridge/vpn_bridge_mock_test.dart — 桥接层单元测试

client 侧修改:
- android/.../MainActivity.kt    — 注册三通道(MethodChannel + 2×EventChannel)
- android/.../AndroidManifest.xml — 声明 FOREGROUND_SERVICE + PangolinVpnService
- ios/Runner/AppDelegate.swift   — 注册通道 + VpnManager 初始化
- ios/Runner/Info.plist          — 新增 NSVPNUsageDescription
- ios/Runner.xcodeproj/project.pbxproj — 添加 PacketTunnel extension target
- lib/widgets/connect_button.dart — VpnStatus 迁移到 bridge/vpn_bridge.dart
- lib/widgets/home_shell.dart    — error 状态 UI 分支补全
This commit is contained in:
wangjia
2026-06-13 14:58:38 +08:00
parent 30e73b31c2
commit c84b6ea7da
16 changed files with 1677 additions and 11 deletions
@@ -1,9 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 网络权限(实际隧道流量) -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 前台服务(VPN 持续运行所需,API 28+) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:label="穿山甲"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- 主 Activity -->
<activity
android:name=".MainActivity"
android:exported="true"
@@ -14,13 +22,28 @@
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!--
VPN 服务(骨架)。
android:permission="android.permission.BIND_VPN_SERVICE" 确保只有系统可 bind。
intent-filter android.net.VpnService 是 VpnService 规范要求。
TODO(11E): 启用后在设备上需用户授权 VPN 权限(VpnService.prepare())。
-->
<service
android:name=".PangolinVpnService"
android:exported="false"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
@@ -1,5 +1,122 @@
package com.pangolin.pangolin_vpn
import android.util.Log
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterActivity()
/**
* MainActivity — Flutter ↔ 原生通道注册
*
* 通道契约(见 lib/bridge/vpn_bridge.dart 头部注释):
* MethodChannel : pangolin/vpn
* EventChannel : pangolin/vpn/status
* EventChannel : pangolin/vpn/stats
*
* MethodChannel 路由到 PangolinVpnService(通过 Intent)。
* EventChannel 由 VpnEventBus 驱动(Service → EventBus → EventSink → Flutter)。
*/
class MainActivity : FlutterActivity() {
companion object {
private const val TAG = "PangolinMainActivity"
private const val VPN_CHANNEL = "pangolin/vpn"
private const val STATUS_CHANNEL = "pangolin/vpn/status"
private const val STATS_CHANNEL = "pangolin/vpn/stats"
}
// 持有 EventSink 引用,以便 VpnEventBus 回调时转发
private var statusEventSink: EventChannel.EventSink? = null
private var statsEventSink: EventChannel.EventSink? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val messenger = flutterEngine.dartExecutor.binaryMessenger
// ── MethodChannel ────────────────────────────────────────
MethodChannel(messenger, VPN_CHANNEL).setMethodCallHandler { call, result ->
Log.d(TAG, "MethodChannel: ${call.method}")
when (call.method) {
"start" -> {
val configJson = call.arguments as? String ?: "{}"
PangolinVpnService.startVpn(this, configJson)
result.success(null)
}
"stop" -> {
PangolinVpnService.stopVpn(this)
result.success(null)
}
"getStatus" -> {
// TODO(11E): 从 VpnService 查询真实状态
result.success("off")
}
"selectOutbound" -> {
val tag = call.arguments as? String ?: "auto"
Log.d(TAG, "selectOutbound: tag=$tag")
// TODO(11E): 发送 IPC 到 VpnService → sing-box outbound selector
result.success(null)
}
"getActiveOutbound" -> {
// TODO(11E): 从 VpnService 查询当前出口 tag
result.success("auto")
}
"setKillSwitch" -> {
val on = call.arguments as? Boolean ?: false
Log.d(TAG, "setKillSwitch: on=$on")
// TODO(11E): 配置 VpnService allowedApplications / blockingMode
result.success(null)
}
else -> result.notImplemented()
}
}
// ── EventChannel: status ─────────────────────────────────
EventChannel(messenger, STATUS_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
Log.d(TAG, "status channel: onListen")
statusEventSink = events
VpnEventBus.setStatusListener(object : VpnEventBus.StatusListener {
override fun onStatus(status: String) {
statusEventSink?.success(status)
}
})
}
override fun onCancel(arguments: Any?) {
Log.d(TAG, "status channel: onCancel")
statusEventSink = null
VpnEventBus.setStatusListener(null)
}
}
)
// ── EventChannel: stats ──────────────────────────────────
EventChannel(messenger, STATS_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
Log.d(TAG, "stats channel: onListen")
statsEventSink = events
VpnEventBus.setStatsListener(object : VpnEventBus.StatsListener {
override fun onStats(stats: Map<String, Any>) {
statsEventSink?.success(stats)
}
})
}
override fun onCancel(arguments: Any?) {
Log.d(TAG, "stats channel: onCancel")
statsEventSink = null
VpnEventBus.setStatsListener(null)
}
}
)
}
override fun onDestroy() {
statusEventSink = null
statsEventSink = null
VpnEventBus.setStatusListener(null)
VpnEventBus.setStatsListener(null)
super.onDestroy()
}
}
@@ -0,0 +1,157 @@
package com.pangolin.pangolin_vpn
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.VpnService
import android.os.Build
import android.util.Log
/**
* PangolinVpnService — VPN 服务骨架(空壳)
*
* 生命周期占位日志 + 前台通知;实际隧道建立由 11E 完成。
*
* 通信:
* - [MainActivity] 通过 [startVpn] / [stopVpn] 发送 Intent 启动/停止本服务
* - 状态回调通过 [VpnEventBus] (Application 级 LiveData) 传到 MainActivity EventSink
*
* TODO(11E): 在 onStartCommand 中初始化 libbox / sing-box TUN 隧道。
*/
class PangolinVpnService : VpnService() {
companion object {
private const val TAG = "PangolinVpnService"
const val ACTION_START = "com.pangolin.vpn.START"
const val ACTION_STOP = "com.pangolin.vpn.STOP"
const val EXTRA_CONFIG = "config_json"
private const val NOTIFICATION_CHANNEL_ID = "pangolin_vpn_channel"
private const val NOTIFICATION_ID = 1001
fun startVpn(context: Context, configJson: String) {
val intent = Intent(context, PangolinVpnService::class.java).apply {
action = ACTION_START
putExtra(EXTRA_CONFIG, configJson)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
Log.d(TAG, "startVpn: intent dispatched")
}
fun stopVpn(context: Context) {
val intent = Intent(context, PangolinVpnService::class.java).apply {
action = ACTION_STOP
}
context.startService(intent)
Log.d(TAG, "stopVpn: intent dispatched")
}
}
// ── 生命周期 ────────────────────────────────────────────────
override fun onCreate() {
super.onCreate()
Log.i(TAG, "onCreate")
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(TAG, "onStartCommand: action=${intent?.action}")
return when (intent?.action) {
ACTION_START -> {
val configJson = intent.getStringExtra(EXTRA_CONFIG) ?: "{}"
handleStart(configJson)
START_STICKY
}
ACTION_STOP -> {
handleStop()
START_NOT_STICKY
}
else -> {
Log.w(TAG, "onStartCommand: unknown action=${intent?.action}")
START_NOT_STICKY
}
}
}
override fun onDestroy() {
Log.i(TAG, "onDestroy")
handleStop()
super.onDestroy()
}
override fun onRevoke() {
Log.w(TAG, "onRevoke: VPN permission revoked by system")
// TODO(11E): 通知 Dart 侧 status=error
handleStop()
}
// ── 内部处理 ────────────────────────────────────────────────
private fun handleStart(configJson: String) {
Log.i(TAG, "handleStart: config=${configJson.take(80)}")
startForeground(NOTIFICATION_ID, buildNotification("正在连接…"))
// TODO(11E): 解析 configJson → 构建 VPN 接口 → 启动 sing-box 内核
// 状态变化后通过 VpnEventBus.postStatus("connecting") / ("on") 通知 MainActivity
}
private fun handleStop() {
Log.i(TAG, "handleStop")
stopForeground(true)
stopSelf()
// TODO(11E): 停止 sing-box 内核 → 关闭 TUN fd
}
// ── 前台通知 ────────────────────────────────────────────────
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
"穿山甲 VPN",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "穿山甲 VPN 隧道状态"
setShowBadge(false)
}
val nm = getSystemService(NotificationManager::class.java)
nm.createNotificationChannel(channel)
}
}
private fun buildNotification(contentText: String): Notification {
val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
val openIntent = PendingIntent.getActivity(
this, 0,
Intent(this, MainActivity::class.java),
pendingFlags
)
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
} else {
@Suppress("DEPRECATION")
Notification.Builder(this)
}
return builder
.setContentTitle("穿山甲")
.setContentText(contentText)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentIntent(openIntent)
.setOngoing(true)
.build()
}
}
@@ -0,0 +1,42 @@
package com.pangolin.pangolin_vpn
import android.os.Handler
import android.os.Looper
/**
* VpnEventBus — Application 级状态总线
*
* 解耦 PangolinVpnService 与 MainActivity
* - Service 调用 postStatus / postStats 推送事件
* - MainActivity 注册 StatusListener / StatsListener 接收事件并转发给 Flutter EventSink
*
* 线程安全:内部切换到主线程再回调(VPN 内核可能在工作线程回调)。
*/
object VpnEventBus {
interface StatusListener {
fun onStatus(status: String)
}
interface StatsListener {
fun onStats(stats: Map<String, Any>)
}
private val mainHandler = Handler(Looper.getMainLooper())
@Volatile private var statusListener: StatusListener? = null
@Volatile private var statsListener: StatsListener? = null
fun setStatusListener(l: StatusListener?) { statusListener = l }
fun setStatsListener(l: StatsListener?) { statsListener = l }
/** 由 PangolinVpnService(可能在工作线程)调用 */
fun postStatus(status: String) {
mainHandler.post { statusListener?.onStatus(status) }
}
/** 由 PangolinVpnService(可能在工作线程)调用 */
fun postStats(stats: Map<String, Any>) {
mainHandler.post { statsListener?.onStats(stats) }
}
}