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:
@@ -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) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user