feat(android): libbox 隧道重写对接真实 1.13.12 API,APK 编译通过(Task 5)
原生层照 PoC 旧/想象 API 写,与真实 sing-box 1.13.12 libbox 差距是架构级: - 包名 libbox(非 go.libbox);无 BoxService,改 Libbox.setup + CommandServer 模型 - PangolinVpnService 重写:实现完整 PlatformInterface(15法)+CommandServerHandler - 新增 DefaultNetworkMonitor(上报默认接口,缺则报 no available network) - openTun 适配真实 TunOptions(RoutePrefixIterator/getMTU/StringBox DNS) - 统计/切节点经 CommandClient(uplinkTotal/selectOutbound);MainActivity 接线 构建环境矩阵打通:Gradle 8.7(Java21)+声明式插件迁移+AGP 8.6+Kotlin 2.2.0+占位图标 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
@@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
|
||||
}
|
||||
}
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
@@ -21,10 +22,6 @@ if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
namespace "com.pangolin.pangolin_vpn"
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
@@ -57,17 +54,6 @@ android {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
// libbox.aar 包含 arm64-v8a / armeabi-v7a / x86_64 三个 ABI;
|
||||
// 若只需调试,可缩减 abiFilters 以加快构建速度。
|
||||
// splits {
|
||||
// abi {
|
||||
// enable true
|
||||
// reset()
|
||||
// include 'arm64-v8a', 'x86_64'
|
||||
// universalApk true
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
flutter {
|
||||
@@ -75,21 +61,15 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
// ── sing-box libbox(gomobile AAR)──────────────────────────────
|
||||
// 产物由 app/kernel/build-android.sh 生成:
|
||||
// cd app/kernel && ./build-android.sh
|
||||
// 产物路径(相对于本 build.gradle):../../../app/kernel/dist/android/libbox.aar
|
||||
//
|
||||
// 若文件不存在,构建会报错:请先运行 build-android.sh 生成产物。
|
||||
// 路径说明:client/android/app → ../../.. → repo root → app/kernel/dist/android/
|
||||
// 路径:client/android/app → ../../.. → repo root → app/kernel/dist/android/
|
||||
// Kotlin stdlib 由 kotlin-android 插件自动引入,无需显式声明。
|
||||
def libboxAar = file("${projectDir}/../../../app/kernel/dist/android/libbox.aar")
|
||||
if (libboxAar.exists()) {
|
||||
implementation files(libboxAar)
|
||||
} else {
|
||||
// AAR 尚未构建 — 保留占位;IDE 会报红线,但不影响非 libbox 代码编辑。
|
||||
// 运行 `app/kernel/build-android.sh` 后重新 sync 即可。
|
||||
logger.warn("⚠ libbox.aar not found at ${libboxAar.absolutePath}")
|
||||
logger.warn(" Run: cd app/kernel && ./build-android.sh")
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<application
|
||||
android:label="穿山甲"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@drawable/ic_launcher">
|
||||
|
||||
<!-- 主 Activity -->
|
||||
<activity
|
||||
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package com.pangolin.pangolin_vpn
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.LinkProperties
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.util.Log
|
||||
import libbox.InterfaceUpdateListener
|
||||
import java.net.NetworkInterface
|
||||
|
||||
/**
|
||||
* DefaultNetworkMonitor — 上报「底层默认网络」给 sing-box 内核。
|
||||
*
|
||||
* sing-box 在 Android 上不自己枚举接口(PlatformInterface.usePlatformAutoDetectInterfaceControl=true),
|
||||
* 它通过 [InterfaceUpdateListener.updateDefaultInterface] 得知当前默认出口接口(名字+index),
|
||||
* 据此把出站 socket 绑定到底层物理网络(避免 TUN 路由环路)。
|
||||
*
|
||||
* **缺这个监听 → 内核启动期拿不到默认接口 → 报 "no available network interface",隧道连不上。**
|
||||
* 故 [start] 必须在 CommandServer.startOrReloadService 之前调用,并把首个默认网络同步上报。
|
||||
*
|
||||
* 自包含实现:直接用 ConnectivityManager.registerNetworkCallback(API 21+),
|
||||
* 不依赖 SFA 的 Application/协程框架。
|
||||
*/
|
||||
object DefaultNetworkMonitor {
|
||||
|
||||
private const val TAG = "PangolinNetMonitor"
|
||||
|
||||
@Volatile private var listener: InterfaceUpdateListener? = null
|
||||
@Volatile private var defaultNetwork: Network? = null
|
||||
|
||||
private var connectivity: ConnectivityManager? = null
|
||||
private var callback: ConnectivityManager.NetworkCallback? = null
|
||||
|
||||
/** 注册默认网络回调,并立即同步一次当前网络。幂等。 */
|
||||
fun start(context: Context) {
|
||||
if (callback != null) return
|
||||
val cm = context.applicationContext
|
||||
.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
connectivity = cm
|
||||
|
||||
val cb = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
defaultNetwork = network
|
||||
notifyUpdate(network)
|
||||
}
|
||||
|
||||
override fun onLinkPropertiesChanged(network: Network, lp: LinkProperties) {
|
||||
defaultNetwork = network
|
||||
notifyUpdate(network)
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
if (defaultNetwork == network) {
|
||||
defaultNetwork = null
|
||||
notifyUpdate(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
callback = cb
|
||||
|
||||
// 要求一个具备 INTERNET 能力的网络(等价于默认网络)。registerNetworkCallback 支持 API 21+。
|
||||
val request = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.build()
|
||||
try {
|
||||
cm.registerNetworkCallback(request, cb)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "registerNetworkCallback failed: $e")
|
||||
}
|
||||
|
||||
// 同步当前活动网络(回调可能不会立刻触发 onAvailable)。
|
||||
val active = cm.activeNetwork
|
||||
if (active != null) {
|
||||
defaultNetwork = active
|
||||
notifyUpdate(active)
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
val cm = connectivity
|
||||
val cb = callback
|
||||
if (cm != null && cb != null) {
|
||||
try { cm.unregisterNetworkCallback(cb) } catch (_: Exception) {}
|
||||
}
|
||||
callback = null
|
||||
connectivity = null
|
||||
defaultNetwork = null
|
||||
}
|
||||
|
||||
/** 内核注册/注销监听(PlatformInterface.start/closeDefaultInterfaceMonitor 调用)。 */
|
||||
fun setListener(l: InterfaceUpdateListener?) {
|
||||
listener = l
|
||||
if (l != null) notifyUpdate(defaultNetwork)
|
||||
}
|
||||
|
||||
/** 把网络的接口名 + index 上报内核;网络为 null 时上报空("",-1)。 */
|
||||
private fun notifyUpdate(network: Network?) {
|
||||
val l = listener ?: return
|
||||
if (network == null) {
|
||||
l.updateDefaultInterface("", -1, false, false)
|
||||
return
|
||||
}
|
||||
val cm = connectivity ?: return
|
||||
// LinkProperties 可能短暂为 null,重试几次拿接口名。
|
||||
for (i in 0 until 10) {
|
||||
val lp = cm.getLinkProperties(network)
|
||||
val name = lp?.interfaceName
|
||||
if (name == null) {
|
||||
try { Thread.sleep(100) } catch (_: InterruptedException) {}
|
||||
continue
|
||||
}
|
||||
val index = try {
|
||||
NetworkInterface.getByName(name)?.index ?: -1
|
||||
} catch (_: Exception) {
|
||||
-1
|
||||
}
|
||||
l.updateDefaultInterface(name, index, false, false)
|
||||
return
|
||||
}
|
||||
Log.w(TAG, "notifyUpdate: interface name unavailable after retries")
|
||||
}
|
||||
}
|
||||
@@ -68,14 +68,14 @@ class MainActivity : FlutterActivity() {
|
||||
}
|
||||
"selectOutbound" -> {
|
||||
val tag = call.arguments as? String ?: "auto"
|
||||
Log.d(TAG, "selectOutbound: tag=$tag (stub — 11G)")
|
||||
// TODO(11G): 通过 libbox CommandClient 切换出口
|
||||
PangolinVpnService.instance?.selectOutbound(tag)
|
||||
Log.d(TAG, "selectOutbound: tag=$tag")
|
||||
result.success(null)
|
||||
}
|
||||
"getActiveOutbound" -> {
|
||||
Log.d(TAG, "getActiveOutbound (stub — 11G)")
|
||||
// TODO(11G): 从 libbox CommandClient 查询当前出口 tag
|
||||
result.success("auto")
|
||||
val active = PangolinVpnService.instance?.currentActiveOutbound() ?: "auto"
|
||||
Log.d(TAG, "getActiveOutbound -> $active")
|
||||
result.success(active)
|
||||
}
|
||||
"setKillSwitch" -> {
|
||||
val on = call.arguments as? Boolean ?: false
|
||||
|
||||
+325
-324
@@ -6,57 +6,69 @@ import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.TrafficStats
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.system.OsConstants
|
||||
import android.util.Log
|
||||
import go.libbox.BoxService
|
||||
import go.libbox.CommandClient
|
||||
import go.libbox.CommandClientHandler
|
||||
import go.libbox.CommandClientOptions
|
||||
import go.libbox.Libbox
|
||||
import go.libbox.OutboundGroupIterator
|
||||
import go.libbox.PlatformInterface
|
||||
import go.libbox.ProcessInfo
|
||||
import go.libbox.StatusMessage
|
||||
import go.libbox.StringIterator
|
||||
import go.libbox.TunOptions
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.TimeUnit
|
||||
import go.Seq
|
||||
import libbox.CommandClient
|
||||
import libbox.CommandClientHandler
|
||||
import libbox.CommandClientOptions
|
||||
import libbox.CommandServer
|
||||
import libbox.CommandServerHandler
|
||||
import libbox.ConnectionEvents
|
||||
import libbox.ConnectionOwner
|
||||
import libbox.InterfaceUpdateListener
|
||||
import libbox.Libbox
|
||||
import libbox.LocalDNSTransport
|
||||
import libbox.LogIterator
|
||||
import libbox.NetworkInterfaceIterator
|
||||
import libbox.Notification as LibboxNotification
|
||||
import libbox.OutboundGroupIterator
|
||||
import libbox.OverrideOptions
|
||||
import libbox.PlatformInterface
|
||||
import libbox.SetupOptions
|
||||
import libbox.StatusMessage
|
||||
import libbox.StringIterator
|
||||
import libbox.SystemProxyStatus
|
||||
import libbox.TunOptions
|
||||
import libbox.WIFIState
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* PangolinVpnService — VPN 前台服务,封装 sing-box libbox 隧道。
|
||||
*
|
||||
* ## 生命周期
|
||||
* START → handleStart() → 后台线程 → startLibbox() → [BoxService.start()]
|
||||
* → VpnEventBus.postStatus("on")
|
||||
* STOP → doStop() → [BoxService.close()] → 关 TUN fd → postStatus("off")
|
||||
* REVOKE → onRevoke() → postStatus("error") → doStop()
|
||||
* ## 架构(sing-box 1.13.x libbox,CommandServer 模型)
|
||||
* Libbox.setup(SetupOptions) —— 进程级一次,设可写目录 + 命令 socket
|
||||
* CommandServer(handler, platformInterface).start()
|
||||
* commandServer.startOrReloadService(configJson, OverrideOptions()) —— 起内核,回调 openTun
|
||||
* 停止:closeService() → close()
|
||||
*
|
||||
* ## 通信
|
||||
* Dart 通过 [MainActivity] MethodChannel 发送 start/stop intent。
|
||||
* 状态与统计经 [VpnEventBus] 推回 Flutter EventChannel。
|
||||
* 本类同时实现 [PlatformInterface](openTun/protect/接口监听…)与 [CommandServerHandler]。
|
||||
* 统计与切节点经独立的 [CommandClient](同进程连内核命令 socket)。
|
||||
*
|
||||
* ## 注意:libbox.aar API
|
||||
* 依赖 app/kernel/dist/android/libbox.aar(由 build-android.sh 产出)。
|
||||
* 方法签名注释标注 "// libbox API" 处若编译失败,请对照实际 libbox.aar 中
|
||||
* go.libbox.* 的 javadoc 微调参数名;整体架构不变。
|
||||
* ## 与 Dart 通信
|
||||
* 状态/统计经 [VpnEventBus] 推回 Flutter EventChannel(契约见 lib/bridge/vpn_bridge.dart)。
|
||||
* selectOutbound/getActiveOutbound 由 [MainActivity] 通过 [instance] 调用。
|
||||
*/
|
||||
class PangolinVpnService : VpnService() {
|
||||
class PangolinVpnService : VpnService(), PlatformInterface, CommandServerHandler {
|
||||
|
||||
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 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
|
||||
|
||||
/** 同进程内供 MainActivity 调用 selectOutbound/getActiveOutbound。 */
|
||||
@Volatile var instance: PangolinVpnService? = null
|
||||
private set
|
||||
|
||||
fun startVpn(context: Context, configJson: String) {
|
||||
val intent = Intent(context, PangolinVpnService::class.java).apply {
|
||||
action = ACTION_START
|
||||
@@ -67,7 +79,6 @@ class PangolinVpnService : VpnService() {
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
Log.d(TAG, "startVpn: intent dispatched, configLen=${configJson.length}")
|
||||
}
|
||||
|
||||
fun stopVpn(context: Context) {
|
||||
@@ -75,37 +86,35 @@ class PangolinVpnService : VpnService() {
|
||||
action = ACTION_STOP
|
||||
}
|
||||
context.startService(intent)
|
||||
Log.d(TAG, "stopVpn: intent dispatched")
|
||||
}
|
||||
}
|
||||
|
||||
// ── 内核与 TUN 状态 ───────────────────────────────────────────
|
||||
@Volatile private var commandServer: CommandServer? = null
|
||||
@Volatile private var statsClient: CommandClient? = null
|
||||
@Volatile private var tunPfd: ParcelFileDescriptor? = null
|
||||
|
||||
/** libbox BoxService 内核实例(主线程以外创建,通过 volatile 保证可见性) */
|
||||
@Volatile private var boxService: BoxService? = null
|
||||
/** 最近一次 writeGroups 缓存的「代理组当前出口」与 urltest 延迟,供 getActiveOutbound/stats 用。 */
|
||||
@Volatile private var activeOutbound: String = "auto"
|
||||
@Volatile private var latestUrltest: List<Pair<String, Int>> = emptyList()
|
||||
|
||||
/** TUN 文件描述符持有者(VpnService.Builder.establish() 返回值) */
|
||||
@Volatile var currentTunPfd: ParcelFileDescriptor? = null
|
||||
/** 缓存的「可手动选择的出口组」tag(selector 组)。selectOutbound 据此切换。 */
|
||||
@Volatile private var selectableGroup: String = ""
|
||||
|
||||
/** libbox CommandClient(统计与状态轮询) */
|
||||
@Volatile private var commandClient: CommandClient? = null
|
||||
|
||||
/** 兜底统计定时器(CommandClient 不可用时启用 TrafficStats 方案) */
|
||||
private var statsExecutor: ScheduledExecutorService? = null
|
||||
|
||||
/** 防止 doStop 重入 */
|
||||
private val stopping = AtomicBoolean(false)
|
||||
private var libboxSetupDone = false
|
||||
|
||||
// ── 生命周期 ────────────────────────────────────────────────
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.i(TAG, "onCreate")
|
||||
instance = this
|
||||
// gomobile 绑定需要 Android Context(否则部分调用崩溃)。
|
||||
Seq.setContext(applicationContext)
|
||||
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) ?: "{}"
|
||||
@@ -116,23 +125,19 @@ class PangolinVpnService : VpnService() {
|
||||
doStop()
|
||||
START_NOT_STICKY
|
||||
}
|
||||
else -> {
|
||||
Log.w(TAG, "onStartCommand: unknown action, falling through")
|
||||
START_NOT_STICKY
|
||||
}
|
||||
else -> START_NOT_STICKY
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRevoke() {
|
||||
// 系统撤销 VPN 权限(如用户在设置里关闭)→ 推 error 再清理
|
||||
Log.w(TAG, "onRevoke: permission revoked by system")
|
||||
VpnEventBus.postStatus("error")
|
||||
doStop()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.i(TAG, "onDestroy")
|
||||
doStop()
|
||||
instance = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -143,12 +148,12 @@ class PangolinVpnService : VpnService() {
|
||||
startForeground(NOTIFICATION_ID, buildNotification("正在连接…"))
|
||||
VpnEventBus.postStatus("connecting")
|
||||
|
||||
// libbox 内核启动必须在后台线程(不能阻塞 onStartCommand)
|
||||
// 内核启动必须在后台线程(不能阻塞 onStartCommand;openTun 回调会同步等 TUN 建好)。
|
||||
Thread {
|
||||
try {
|
||||
startLibbox(configJson)
|
||||
startKernel(configJson)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "startLibbox failed: $e", e)
|
||||
Log.e(TAG, "startKernel failed: $e", e)
|
||||
VpnEventBus.postStatus("error")
|
||||
updateNotification("连接失败")
|
||||
doStop()
|
||||
@@ -156,305 +161,311 @@ class PangolinVpnService : VpnService() {
|
||||
}.apply { name = "pangolin-libbox-start" }.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* 在后台线程内:
|
||||
* 1. 构造 PlatformInterface(inner class,持有 VpnService 引用)
|
||||
* 2. 新建 BoxService 并 start()(内核会回调 openTun 建 TUN 接口)
|
||||
* 3. 启动 CommandClient 轮询统计
|
||||
* 4. 推送 "on" 状态
|
||||
*/
|
||||
private fun startLibbox(configJson: String) {
|
||||
val platform = PangolinPlatformInterface()
|
||||
private fun startKernel(configJson: String) {
|
||||
ensureLibboxSetup()
|
||||
|
||||
// ── libbox API: Libbox.newBoxService(platformInterface, configJson, needBuildConfig)
|
||||
// 如果编译报错,可能 API 为: new BoxService(platform, configJson)
|
||||
// 或: BoxService.newService(platform, configJson)
|
||||
val service = Libbox.newBoxService(platform, configJson, false) // libbox API
|
||||
service.start() // libbox API
|
||||
boxService = service
|
||||
Log.i(TAG, "BoxService started")
|
||||
// 先起默认接口监听,再起内核——否则内核启动期拿不到默认出口,报 no available network interface。
|
||||
DefaultNetworkMonitor.start(applicationContext)
|
||||
|
||||
// 启动统计客户端
|
||||
startCommandClientOrFallback()
|
||||
val server = Libbox.newCommandServer(this, this)
|
||||
server.start()
|
||||
commandServer = server
|
||||
|
||||
server.startOrReloadService(configJson, OverrideOptions())
|
||||
Log.i(TAG, "sing-box service started")
|
||||
|
||||
startStatsClient()
|
||||
|
||||
// 更新通知与状态
|
||||
updateNotification("加速已开启")
|
||||
VpnEventBus.postStatus("on")
|
||||
}
|
||||
|
||||
// ── 统计:CommandClient + TrafficStats 兜底 ───────────────────
|
||||
private fun ensureLibboxSetup() {
|
||||
if (libboxSetupDone) return
|
||||
val base = filesDir.absolutePath
|
||||
val work = filesDir.absolutePath
|
||||
val temp = cacheDir.absolutePath
|
||||
val options = SetupOptions().apply {
|
||||
basePath = base
|
||||
workingPath = work
|
||||
tempPath = temp
|
||||
}
|
||||
Libbox.setup(options)
|
||||
libboxSetupDone = true
|
||||
Log.i(TAG, "Libbox.setup done (base=$base)")
|
||||
}
|
||||
|
||||
/**
|
||||
* 优先使用 libbox CommandClient 获取精确统计;若连接失败则退回 Android TrafficStats。
|
||||
*/
|
||||
private fun startCommandClientOrFallback() {
|
||||
// ── 统计 + 切节点:CommandClient ──────────────────────────────
|
||||
|
||||
private fun startStatsClient() {
|
||||
try {
|
||||
startCommandClient()
|
||||
val options = CommandClientOptions().apply {
|
||||
statusInterval = 1_000_000_000L // 1s(纳秒)
|
||||
addCommand(Libbox.CommandStatus)
|
||||
addCommand(Libbox.CommandGroup) // 取出口组 → getActiveOutbound + urltest 延迟
|
||||
}
|
||||
val client = Libbox.newCommandClient(StatsHandler(), options)
|
||||
client.connect()
|
||||
statsClient = client
|
||||
Log.i(TAG, "stats CommandClient connected")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "CommandClient unavailable ($e), falling back to TrafficStats")
|
||||
startTrafficStatsFallback()
|
||||
Log.w(TAG, "stats CommandClient failed: $e")
|
||||
}
|
||||
}
|
||||
|
||||
private fun startCommandClient() {
|
||||
val options = CommandClientOptions()
|
||||
// Command 0 = STATUS(含流量统计);间隔 1 秒(纳秒)
|
||||
// libbox API: options.Command / options.StatusInterval 为 gomobile 暴露的字段
|
||||
options.command = 0 // libbox API field
|
||||
options.statusInterval = 1_000_000_000L // libbox API field: nanoseconds
|
||||
|
||||
val handler = object : CommandClientHandler {
|
||||
override fun connected() {
|
||||
Log.d(TAG, "CommandClient connected")
|
||||
}
|
||||
override fun disconnected(message: String) {
|
||||
Log.d(TAG, "CommandClient disconnected: $message")
|
||||
// 断开后不改变 VPN 状态——隧道仍在运行,仅统计中断
|
||||
}
|
||||
override fun writeLog(message: String) {
|
||||
Log.v("libbox", message)
|
||||
}
|
||||
override fun writeStatus(status: StatusMessage) {
|
||||
// libbox API: StatusMessage 提供累计流量与瞬时速率
|
||||
val stats = mapOf<String, Any>(
|
||||
"uploadBytes" to (status.uploadTotal()), // libbox API
|
||||
"downloadBytes" to (status.downloadTotal()), // libbox API
|
||||
"uploadSpeed" to (status.uploadSpeed().toDouble()), // libbox API
|
||||
"downloadSpeed" to (status.downloadSpeed().toDouble()), // libbox API
|
||||
"urltestResults" to emptyList<Any>()
|
||||
)
|
||||
VpnEventBus.postStats(stats)
|
||||
}
|
||||
override fun writeGroups(groups: OutboundGroupIterator) { /* 暂不处理 urltest 延迟 */ }
|
||||
override fun initializeClashMode(modeList: StringIterator, currentMode: String) {}
|
||||
override fun updateClashMode(newMode: String) {}
|
||||
/** 由 MainActivity(同进程)调用:切换出口节点到 [tag](在缓存的可选组内)。 */
|
||||
fun selectOutbound(tag: String) {
|
||||
val group = selectableGroup
|
||||
if (group.isEmpty()) {
|
||||
Log.w(TAG, "selectOutbound: 无可选出口组(config 可能只有 urltest,无 selector)")
|
||||
return
|
||||
}
|
||||
try {
|
||||
statsClient?.selectOutbound(group, tag)
|
||||
Log.i(TAG, "selectOutbound(group=$group, tag=$tag)")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "selectOutbound failed: $e")
|
||||
}
|
||||
|
||||
// libbox API: Libbox.newCommandClient(handler, options)
|
||||
val client = Libbox.newCommandClient(handler, options) // libbox API
|
||||
client.connect() // libbox API
|
||||
commandClient = client
|
||||
Log.i(TAG, "CommandClient started")
|
||||
}
|
||||
|
||||
/**
|
||||
* 兜底:用 Android TrafficStats API 按 UID 统计网络流量。
|
||||
* 由于所有流量都走 VPN,UID 统计近似等于隧道流量。
|
||||
*/
|
||||
private fun startTrafficStatsFallback() {
|
||||
val uid = android.os.Process.myUid()
|
||||
var lastRx = TrafficStats.getUidRxBytes(uid)
|
||||
var lastTx = TrafficStats.getUidTxBytes(uid)
|
||||
var totalRx = 0L
|
||||
var totalTx = 0L
|
||||
fun currentActiveOutbound(): String = activeOutbound
|
||||
|
||||
statsExecutor = Executors.newSingleThreadScheduledExecutor().also { exec ->
|
||||
exec.scheduleAtFixedRate({
|
||||
if (stopping.get()) return@scheduleAtFixedRate
|
||||
try {
|
||||
val rx = TrafficStats.getUidRxBytes(uid).let { if (it < 0) 0L else it }
|
||||
val tx = TrafficStats.getUidTxBytes(uid).let { if (it < 0) 0L else it }
|
||||
val rxDiff = (rx - lastRx).coerceAtLeast(0)
|
||||
val txDiff = (tx - lastTx).coerceAtLeast(0)
|
||||
totalRx += rxDiff
|
||||
totalTx += txDiff
|
||||
lastRx = rx
|
||||
lastTx = tx
|
||||
private inner class StatsHandler : CommandClientHandler {
|
||||
override fun connected() { Log.d(TAG, "stats client connected") }
|
||||
override fun disconnected(message: String?) { Log.d(TAG, "stats client disconnected: $message") }
|
||||
override fun clearLogs() {}
|
||||
override fun writeLogs(messageList: LogIterator?) {}
|
||||
override fun setDefaultLogLevel(level: Int) {}
|
||||
override fun initializeClashMode(modeList: StringIterator?, currentMode: String?) {}
|
||||
override fun updateClashMode(newMode: String?) {}
|
||||
override fun writeConnectionEvents(message: ConnectionEvents?) {}
|
||||
|
||||
val stats = mapOf<String, Any>(
|
||||
"uploadBytes" to totalTx,
|
||||
"downloadBytes" to totalRx,
|
||||
"uploadSpeed" to txDiff.toDouble(),
|
||||
"downloadSpeed" to rxDiff.toDouble(),
|
||||
"urltestResults" to emptyList<Any>()
|
||||
)
|
||||
VpnEventBus.postStats(stats)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "TrafficStats error: $e")
|
||||
override fun writeStatus(message: StatusMessage) {
|
||||
val stats = mapOf<String, Any>(
|
||||
"uploadBytes" to message.uplinkTotal,
|
||||
"downloadBytes" to message.downlinkTotal,
|
||||
"uploadSpeed" to message.uplink.toDouble(),
|
||||
"downloadSpeed" to message.downlink.toDouble(),
|
||||
"urltestResults" to latestUrltest.map {
|
||||
mapOf<String, Any>("tag" to it.first, "delayMs" to it.second)
|
||||
}
|
||||
}, 1L, 1L, TimeUnit.SECONDS)
|
||||
)
|
||||
VpnEventBus.postStats(stats)
|
||||
}
|
||||
|
||||
override fun writeGroups(groups: OutboundGroupIterator?) {
|
||||
if (groups == null) return
|
||||
val urltest = mutableListOf<Pair<String, Int>>()
|
||||
var selected = activeOutbound
|
||||
try {
|
||||
while (groups.hasNext()) {
|
||||
val group = groups.next()
|
||||
// 记录可选组的当前出口(如 selector / urltest 的 selected)。
|
||||
if (group.selected.isNotEmpty()) selected = group.selected
|
||||
if (group.selectable) selectableGroup = group.tag
|
||||
val items = group.items
|
||||
while (items.hasNext()) {
|
||||
val item = items.next()
|
||||
urltest.add(item.tag to item.getURLTestDelay())
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "writeGroups parse error: $e")
|
||||
}
|
||||
latestUrltest = urltest
|
||||
activeOutbound = selected
|
||||
}
|
||||
Log.i(TAG, "TrafficStats fallback started, uid=$uid")
|
||||
}
|
||||
|
||||
// ── 停止流程 ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 幂等停止:关闭统计 → 关闭 BoxService → 关闭 TUN fd → 停前台通知。
|
||||
* 由多个入口(ACTION_STOP / onRevoke / onDestroy)调用,通过 [stopping] 防止重入。
|
||||
*/
|
||||
private fun doStop() {
|
||||
if (!stopping.compareAndSet(false, true)) {
|
||||
Log.d(TAG, "doStop: already stopping, skip")
|
||||
return
|
||||
}
|
||||
Log.i(TAG, "doStop: shutting down tunnel")
|
||||
if (!stopping.compareAndSet(false, true)) return
|
||||
Log.i(TAG, "doStop")
|
||||
|
||||
// 1. 停统计(先于 BoxService,避免最后一帧读到已关闭的 fd)
|
||||
statsExecutor?.apply {
|
||||
shutdownNow()
|
||||
try { awaitTermination(500, TimeUnit.MILLISECONDS) } catch (_: InterruptedException) {}
|
||||
}
|
||||
statsExecutor = null
|
||||
try { statsClient?.disconnect() } catch (_: Exception) {}
|
||||
statsClient = null
|
||||
|
||||
try {
|
||||
commandClient?.disconnect() // libbox API
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "commandClient.disconnect failed: $e")
|
||||
}
|
||||
commandClient = null
|
||||
try { commandServer?.closeService() } catch (e: Exception) { Log.w(TAG, "closeService: $e") }
|
||||
try { commandServer?.close() } catch (e: Exception) { Log.w(TAG, "close: $e") }
|
||||
commandServer = null
|
||||
|
||||
// 2. 关闭 BoxService 内核(会在内部关闭它持有的 TUN fd 引用)
|
||||
try {
|
||||
boxService?.close() // libbox API
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "boxService.close failed: $e")
|
||||
}
|
||||
boxService = null
|
||||
DefaultNetworkMonitor.stop()
|
||||
|
||||
// 3. 关闭 ParcelFileDescriptor(持有底层 fd;必须在 BoxService.close 之后)
|
||||
try {
|
||||
currentTunPfd?.close()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "tunPfd.close failed: $e")
|
||||
}
|
||||
currentTunPfd = null
|
||||
try { tunPfd?.close() } catch (_: Exception) {}
|
||||
tunPfd = null
|
||||
|
||||
// 4. 停前台 + 自我停止
|
||||
@Suppress("DEPRECATION")
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
|
||||
VpnEventBus.postStatus("off")
|
||||
Log.i(TAG, "doStop: done")
|
||||
}
|
||||
|
||||
// ── PlatformInterface(inner class,持有 VpnService.Builder 访问权)────
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// PlatformInterface 实现(1.13.12 共 15 法)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* sing-box libbox 平台回调实现。
|
||||
*
|
||||
* [openTun]: 用 TunOptions 配置 VpnService.Builder,调用 establish() 取 TUN fd。
|
||||
* [autoDetectInterfaceControl]: 调用 VpnService.protect(fd) 防止 TUN 流量形成环路。
|
||||
*/
|
||||
private inner class PangolinPlatformInterface : PlatformInterface {
|
||||
override fun openTun(options: TunOptions): Int {
|
||||
if (prepare(this) != null) throw Exception("android: missing vpn permission")
|
||||
|
||||
/**
|
||||
* libbox 回调 — 创建 TUN 接口并返回 fd。
|
||||
*
|
||||
* 注:TunOptions 的具体 getter 方法名(inet4Address / mtu 等)
|
||||
* 需与实际 libbox.aar 中 go.libbox.TunOptions 的 javadoc 对齐。
|
||||
* 若方法不存在,请使用 options 对应的实际方法名替换。
|
||||
*/
|
||||
override fun openTun(options: TunOptions): Int {
|
||||
val builder = Builder() // VpnService.Builder(inner class,此处可直接调用)
|
||||
val builder = Builder()
|
||||
.setSession("Pangolin")
|
||||
.setMtu(options.getMTU())
|
||||
|
||||
// ── 地址配置 ──────────────────────────────────────────
|
||||
// libbox API: TunOptions.inet4Address() 返回 CIDR 字符串(如 "172.19.0.1/30")
|
||||
// 若 API 名不同(如 getInet4Address())请对应修改
|
||||
val inet4 = tryGetInet4(options)
|
||||
if (inet4.isNotEmpty()) {
|
||||
val slash = inet4.indexOf('/')
|
||||
if (slash > 0) {
|
||||
builder.addAddress(inet4.substring(0, slash), inet4.substring(slash + 1).toInt())
|
||||
} else {
|
||||
builder.addAddress(inet4, 30)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) builder.setMetered(false)
|
||||
|
||||
// 地址(RoutePrefixIterator)
|
||||
val inet4 = options.inet4Address
|
||||
while (inet4.hasNext()) {
|
||||
val a = inet4.next(); builder.addAddress(a.address(), a.prefix())
|
||||
}
|
||||
val inet6 = options.inet6Address
|
||||
while (inet6.hasNext()) {
|
||||
val a = inet6.next(); builder.addAddress(a.address(), a.prefix())
|
||||
}
|
||||
|
||||
if (options.autoRoute) {
|
||||
// DNS(1.13.12:单个 StringBox)
|
||||
try {
|
||||
val dns = options.getDNSServerAddress()?.getValue()
|
||||
if (!dns.isNullOrEmpty()) builder.addDnsServer(dns)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "addDnsServer: $e")
|
||||
}
|
||||
|
||||
// 路由:优先 inet4RouteAddress;为空则全量 0.0.0.0/0。
|
||||
val r4 = options.inet4RouteAddress
|
||||
if (r4.hasNext()) {
|
||||
while (r4.hasNext()) { val a = r4.next(); builder.addRoute(a.address(), a.prefix()) }
|
||||
} else if (options.inet4Address.hasNext()) {
|
||||
builder.addRoute("0.0.0.0", 0)
|
||||
}
|
||||
val r6 = options.inet6RouteAddress
|
||||
if (r6.hasNext()) {
|
||||
while (r6.hasNext()) { val a = r6.next(); builder.addRoute(a.address(), a.prefix()) }
|
||||
} else if (options.inet6Address.hasNext()) {
|
||||
builder.addRoute("::", 0)
|
||||
}
|
||||
|
||||
// 分应用(include/exclude package)
|
||||
val inc = options.includePackage
|
||||
while (inc.hasNext()) {
|
||||
try { builder.addAllowedApplication(inc.next()) } catch (e: Exception) { Log.w(TAG, "allow app: $e") }
|
||||
}
|
||||
val exc = options.excludePackage
|
||||
while (exc.hasNext()) {
|
||||
try { builder.addDisallowedApplication(exc.next()) } catch (e: Exception) { Log.w(TAG, "disallow app: $e") }
|
||||
}
|
||||
}
|
||||
|
||||
val pfd = builder.establish() ?: throw Exception("establish() returned null — 未授权或已被撤销")
|
||||
tunPfd = pfd
|
||||
Log.i(TAG, "openTun: fd=${pfd.fd}, mtu=${options.mtu}")
|
||||
return pfd.fd
|
||||
}
|
||||
|
||||
override fun autoDetectInterfaceControl(fd: Int) {
|
||||
if (!protect(fd)) throw Exception("VpnService.protect(fd=$fd) failed")
|
||||
}
|
||||
|
||||
override fun usePlatformAutoDetectInterfaceControl(): Boolean = true
|
||||
|
||||
override fun useProcFS(): Boolean = Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
|
||||
|
||||
override fun startDefaultInterfaceMonitor(listener: InterfaceUpdateListener) {
|
||||
DefaultNetworkMonitor.setListener(listener)
|
||||
}
|
||||
|
||||
override fun closeDefaultInterfaceMonitor(listener: InterfaceUpdateListener) {
|
||||
DefaultNetworkMonitor.setListener(null)
|
||||
}
|
||||
|
||||
override fun getInterfaces(): NetworkInterfaceIterator {
|
||||
val list = mutableListOf<libbox.NetworkInterface>()
|
||||
try {
|
||||
for (ni in Collections.list(java.net.NetworkInterface.getNetworkInterfaces())) {
|
||||
val box = libbox.NetworkInterface()
|
||||
box.name = ni.name
|
||||
box.index = ni.index
|
||||
runCatching { box.setMTU(ni.mtu) }
|
||||
box.addresses = StringArray(
|
||||
ni.interfaceAddresses.mapNotNull { ia ->
|
||||
val host = ia.address?.hostAddress ?: return@mapNotNull null
|
||||
"$host/${ia.networkPrefixLength}"
|
||||
}.iterator()
|
||||
)
|
||||
var flags = 0
|
||||
runCatching {
|
||||
if (ni.isUp) flags = flags or OsConstants.IFF_UP or OsConstants.IFF_RUNNING
|
||||
if (ni.isLoopback) flags = flags or OsConstants.IFF_LOOPBACK
|
||||
if (ni.isPointToPoint) flags = flags or OsConstants.IFF_POINTOPOINT
|
||||
if (ni.supportsMulticast()) flags = flags or OsConstants.IFF_MULTICAST
|
||||
}
|
||||
} else {
|
||||
// 兜底:使用 PoC 静态配置的地址
|
||||
builder.addAddress("172.19.0.1", 30)
|
||||
box.flags = flags
|
||||
box.type = Libbox.InterfaceTypeOther
|
||||
list.add(box)
|
||||
}
|
||||
|
||||
val inet6 = tryGetInet6(options)
|
||||
if (inet6.isNotEmpty()) {
|
||||
val slash = inet6.indexOf('/')
|
||||
if (slash > 0) {
|
||||
builder.addAddress(inet6.substring(0, slash), inet6.substring(slash + 1).toInt())
|
||||
}
|
||||
}
|
||||
|
||||
// ── MTU ────────────────────────────────────────────────
|
||||
val mtu = tryGetMtu(options)
|
||||
builder.setMtu(mtu)
|
||||
|
||||
// ── 路由:将所有流量导入 TUN(sing-box auto_route = true)────
|
||||
builder.addRoute("0.0.0.0", 0) // IPv4 全部流量
|
||||
builder.addRoute("::", 0) // IPv6 全部流量
|
||||
|
||||
// ── DNS:sing-box 内置 Fake-IP 地址 ───────────────────
|
||||
// sing-box 在 172.18.0.0/15 运行 Fake-IP,设置匹配 DNS 服务器
|
||||
builder.addDnsServer("198.18.0.2")
|
||||
|
||||
builder.setSession("Pangolin")
|
||||
builder.setBlocking(true)
|
||||
|
||||
val pfd = builder.establish()
|
||||
?: throw Exception("VpnService.Builder.establish() returned null — 可能未授权")
|
||||
currentTunPfd = pfd
|
||||
|
||||
Log.i(TAG, "openTun: fd=${pfd.fd}, inet4=$inet4, mtu=$mtu")
|
||||
return pfd.fd
|
||||
}
|
||||
|
||||
/**
|
||||
* libbox 回调 — 调用 VpnService.protect() 使 fd 跳过 TUN,防止路由环路。
|
||||
*
|
||||
* 注:gomobile 将 Go `error` 返回翻译为抛出 Exception(无返回值)。
|
||||
*/
|
||||
override fun autoDetectInterfaceControl(fd: Int) {
|
||||
if (!protect(fd)) {
|
||||
throw Exception("VpnService.protect(fd=$fd) failed")
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeLog(message: String) {
|
||||
Log.v("libbox", message)
|
||||
}
|
||||
|
||||
/** 使用 Android VpnService 的 protect() 机制,不需要平台独立的路由探测 */
|
||||
override fun usePlatformAutoDetectInterfaceControl(): Boolean = true
|
||||
|
||||
/** 使用 Android 系统的网络变化监听,不使用 libbox 内置监听 */
|
||||
override fun usePlatformDefaultInterfaceMonitor(): Boolean = false
|
||||
|
||||
/** 不使用 libbox 内置接口枚举 */
|
||||
override fun usePlatformInterfaceGetter(): Boolean = false
|
||||
|
||||
/**
|
||||
* 进程信息查询(用于 sing-box 的应用规则)。
|
||||
* PoC 阶段不需要,返回 null 即可(libbox 会跳过进程匹配规则)。
|
||||
*/
|
||||
override fun findProcessInfo(
|
||||
networkType: Int,
|
||||
srcIP: String,
|
||||
srcPort: Int,
|
||||
destIP: String,
|
||||
destPort: Int,
|
||||
): ProcessInfo? = null
|
||||
|
||||
// ── TunOptions 安全 getter(兜底处理 API 名不匹配问题)──
|
||||
|
||||
/** 安全读取 inet4Address,若 API 不匹配返回空串,由调用方用兜底值 */
|
||||
private fun tryGetInet4(options: TunOptions): String = try {
|
||||
// libbox API: options.inet4Address() — 若名称不同请修改
|
||||
options.inet4Address()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "TunOptions.inet4Address() failed: $e")
|
||||
""
|
||||
Log.w(TAG, "getInterfaces: $e")
|
||||
}
|
||||
return InterfaceArray(list.iterator())
|
||||
}
|
||||
|
||||
private fun tryGetInet6(options: TunOptions): String = try {
|
||||
options.inet6Address()
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
override fun findConnectionOwner(
|
||||
ipProtocol: Int, sourceAddress: String, sourcePort: Int,
|
||||
destinationAddress: String, destinationPort: Int,
|
||||
): ConnectionOwner {
|
||||
// 我们的配置不含进程匹配规则,核心不会调用;保守抛出。
|
||||
throw Exception("findConnectionOwner not supported")
|
||||
}
|
||||
|
||||
private fun tryGetMtu(options: TunOptions): Int = try {
|
||||
// libbox API: options.mtu() returns Int32/Int
|
||||
options.mtu().toInt().coerceIn(576, 65535)
|
||||
} catch (e: Exception) {
|
||||
9000 // 兜底:与 PoC 配置对齐
|
||||
}
|
||||
override fun includeAllNetworks(): Boolean = false
|
||||
|
||||
override fun clearDNSCache() {}
|
||||
|
||||
override fun readWIFIState(): WIFIState? = null
|
||||
|
||||
override fun localDNSTransport(): LocalDNSTransport? = null
|
||||
|
||||
override fun systemCertificates(): StringIterator = StringArray(emptyList<String>().iterator())
|
||||
|
||||
override fun underNetworkExtension(): Boolean = false
|
||||
|
||||
override fun sendNotification(notification: LibboxNotification) {
|
||||
Log.d(TAG, "libbox notification: ${notification.title} / ${notification.body}")
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// CommandServerHandler 实现(5 法)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
override fun serviceReload() { Log.d(TAG, "serviceReload") }
|
||||
|
||||
override fun serviceStop() { doStop() }
|
||||
|
||||
override fun getSystemProxyStatus(): SystemProxyStatus = SystemProxyStatus().apply {
|
||||
available = false
|
||||
enabled = false
|
||||
}
|
||||
|
||||
override fun setSystemProxyEnabled(isEnabled: Boolean) {}
|
||||
|
||||
override fun writeDebugMessage(message: String?) { Log.d("sing-box", message ?: "") }
|
||||
|
||||
// ── gomobile 迭代器适配 ───────────────────────────────────────
|
||||
|
||||
private class StringArray(private val it: Iterator<String>) : StringIterator {
|
||||
override fun len(): Int = 0 // 内核不依赖精确长度
|
||||
override fun hasNext(): Boolean = it.hasNext()
|
||||
override fun next(): String = it.next()
|
||||
}
|
||||
|
||||
private class InterfaceArray(
|
||||
private val it: Iterator<libbox.NetworkInterface>
|
||||
) : NetworkInterfaceIterator {
|
||||
override fun hasNext(): Boolean = it.hasNext()
|
||||
override fun next(): libbox.NetworkInterface = it.next()
|
||||
}
|
||||
|
||||
// ── 通知 ─────────────────────────────────────────────────────
|
||||
@@ -462,46 +473,36 @@ class PangolinVpnService : VpnService() {
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_ID,
|
||||
"穿山甲加速",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
NOTIFICATION_CHANNEL_ID, "穿山甲加速", NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
description = "穿山甲加速状态"
|
||||
setShowBadge(false)
|
||||
}
|
||||
getSystemService(NotificationManager::class.java)
|
||||
.createNotificationChannel(channel)
|
||||
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNotification(contentText: String) {
|
||||
getSystemService(NotificationManager::class.java)
|
||||
.notify(NOTIFICATION_ID, buildNotification(contentText))
|
||||
private fun updateNotification(text: String) {
|
||||
getSystemService(NotificationManager::class.java).notify(NOTIFICATION_ID, buildNotification(text))
|
||||
}
|
||||
|
||||
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 {
|
||||
@Suppress("DEPRECATION")
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
@Suppress("DEPRECATION") PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
val openIntent = PendingIntent.getActivity(
|
||||
this, 0,
|
||||
Intent(this, MainActivity::class.java),
|
||||
pendingFlags
|
||||
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)
|
||||
@Suppress("DEPRECATION") Notification.Builder(this)
|
||||
}
|
||||
|
||||
return builder
|
||||
.setContentTitle("穿山甲")
|
||||
.setContentText(contentText) // "加速已开启" / "正在连接…" / "连接失败"
|
||||
.setContentText(contentText)
|
||||
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
||||
.setContentIntent(openIntent)
|
||||
.setOngoing(true)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<!-- 占位启动图标(vector,全 API 可用)。正式图标待设计接入后替换。 -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#0F1117"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#E0884F"
|
||||
android:pathData="M54,28 L80,82 L28,82 Z" />
|
||||
<path
|
||||
android:fillColor="#0F1117"
|
||||
android:pathData="M54,52 L66,76 L42,76 Z" />
|
||||
</vector>
|
||||
@@ -1,16 +1,3 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.9.0'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.1.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
@@ -18,12 +5,12 @@ allprojects {
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = '../build'
|
||||
rootProject.buildDir = "../build"
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete) {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||
android.builtInKotlin=false
|
||||
# This newDsl flag was added automatically by Flutter migrator
|
||||
android.newDsl=false
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
@@ -1,11 +1,25 @@
|
||||
include ':app'
|
||||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}()
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "8.6.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.2.0" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
||||
Reference in New Issue
Block a user