Compare commits

..

5 Commits

Author SHA1 Message Date
chen08209
a1edaba5c9 Add android separates the core process
Support core status check and force restart

Update flutter and pub dependencies
2025-08-27 19:13:43 +08:00
chen08209
e956373ef4 Update changelog 2025-07-29 02:57:43 +00:00
chen08209
1154e7b245 Optimize desktop view
Optimize logs, requests, connection pages

Optimize windows tray auto hide

Optimize some details

Update core
2025-07-29 10:43:05 +08:00
chen08209
adb890d763 Update changelog 2025-06-15 10:59:15 +00:00
chen08209
1477f9bd9c Fix windows tun issues
Optimize android get system dns

Optimize more details
2025-06-15 18:44:19 +08:00
279 changed files with 40226 additions and 44915 deletions

View File

@@ -75,7 +75,7 @@ jobs:
with:
channel: 'stable'
cache: true
flutter-version: 3.29.3
# flutter-version: 3.29.3
- name: Get Flutter Dependency
run: flutter pub get

View File

@@ -1,3 +1,27 @@
## v0.8.87
- Optimize desktop view
- Optimize logs, requests, connection pages
- Optimize windows tray auto hide
- Optimize some details
- Update core
- Update changelog
## v0.8.86
- Fix windows tun issues
- Optimize android get system dns
- Optimize more details
- Update changelog
## v0.8.85
- Support override script

View File

@@ -1 +1,10 @@
include: package:flutter_lints/flutter.yaml
analyzer:
plugins:
- custom_lint
exclude:
- lib/l10n/intl/**
linter:
rules:
prefer_single_quotes: true

View File

@@ -1,3 +1,4 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.util.Properties
plugins {
@@ -24,22 +25,19 @@ val isRelease = mStoreFile.exists()
android {
namespace = "com.follow.clash"
compileSdk = 35
ndkVersion = "28.0.13004108"
compileSdk = libs.versions.compileSdk.get().toInt()
ndkVersion = libs.versions.ndkVersion.get()
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
applicationId = "com.follow.clash"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = flutter.versionCode
versionName = flutter.versionName
}
@@ -63,8 +61,7 @@ android {
release {
isMinifyEnabled = true
isDebuggable = false
isShrinkResources = true
signingConfig = if (isRelease) {
signingConfigs.getByName("release")
} else {
@@ -79,15 +76,22 @@ android {
}
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}
flutter {
source = "../.."
}
dependencies {
implementation(project(":core"))
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("com.google.code.gson:gson:2.10.1")
implementation("com.android.tools.smali:smali-dexlib2:3.0.9") {
implementation(project(":service"))
implementation(project(":common"))
implementation(libs.core.splashscreen)
implementation(libs.gson)
implementation(libs.smali.dexlib2) {
exclude(group = "com.google.guava", module = "guava")
}
}

View File

@@ -1,2 +1,4 @@
-keep class com.follow.clash.models.**{ *; }
-keep class com.follow.clash.models.**{ *; }
-keep class com.follow.clash.service.models.**{ *; }

View File

@@ -9,7 +9,7 @@
android:label="FlClash Debug"
tools:replace="android:label">
<service
android:name=".services.FlClashTileService"
android:name=".TileService"
android:label="FlClash Debug"
tools:replace="android:label"
tools:targetApi="24" />

View File

@@ -1,6 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<permission
android:name="${applicationId}.permission.RECEIVE_BROADCASTS"
android:protectionLevel="signature" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
@@ -13,19 +18,19 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:name=".FlClashApplication"
android:name=".Application"
android:banner="@mipmap/ic_banner"
android:extractNativeLibs="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="FlClash">
@@ -47,6 +52,7 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
@@ -67,12 +73,9 @@
</intent-filter>
</activity>
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
<activity
android:name=".TempActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@style/TransparentTheme">
<intent-filter>
@@ -85,17 +88,16 @@
</intent-filter>
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="${applicationId}.action.CHANGE" />
<action android:name="${applicationId}.action.TOGGLE" />
</intent-filter>
</activity>
<service
android:name=".services.FlClashTileService"
android:name=".TileService"
android:exported="true"
android:icon="@drawable/ic"
android:label="FlClash"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
tools:targetApi="n">
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
@@ -104,6 +106,19 @@
android:value="true" />
</service>
<receiver
android:name=".BroadcastReceiver"
android:enabled="true"
android:exported="true"
android:permission="${applicationId}.permission.RECEIVE_BROADCASTS">
<intent-filter>
<action android:name="${applicationId}.intent.action.START" />
<action android:name="${applicationId}.intent.action.STOP" />
<action android:name="${applicationId}.intent.action.TOGGLE" />
</intent-filter>
</receiver>
<provider
android:name=".FilesProvider"
android:authorities="${applicationId}.files"
@@ -126,28 +141,6 @@
android:resource="@xml/file_paths" />
</provider>
<service
android:name=".services.FlClashVpnService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="vpn" />
</service>
<service
android:name=".services.FlClashService"
android:exported="false"
android:foregroundServiceType="dataSync">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="service" />
</service>
<meta-data
android:name="flutterEmbedding"
android:value="2" />

View File

@@ -0,0 +1,13 @@
package com.follow.clash
import android.app.Application
import android.content.Context
import com.follow.clash.common.GlobalState
class Application : Application() {
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
GlobalState.init(this)
}
}

View File

@@ -0,0 +1,34 @@
package com.follow.clash
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.follow.clash.common.BroadcastAction
import com.follow.clash.common.action
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
class BroadcastReceiver : BroadcastReceiver(),
CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
BroadcastAction.START.action -> {
launch {
State.handleStartServiceAction()
}
}
BroadcastAction.STOP.action -> {
State.handleStopServiceAction()
}
BroadcastAction.TOGGLE.action -> {
launch {
State.handleToggleAction()
}
}
}
}
}

View File

@@ -0,0 +1,76 @@
package com.follow.clash
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.os.Handler
import android.os.Looper
import android.util.Base64
import androidx.core.graphics.drawable.toBitmap
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
import kotlin.coroutines.resume
suspend fun Drawable.getBase64(): String {
val drawable = this
return withContext(Dispatchers.IO) {
val bitmap = drawable.toBitmap()
val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)
}
}
suspend fun <T> MethodChannel.awaitResult(
method: String, arguments: Any? = null
): T? = withContext(Dispatchers.Main) {
suspendCancellableCoroutine { continuation ->
invokeMethod(method, arguments, object : MethodChannel.Result {
override fun success(result: Any?) {
@Suppress("UNCHECKED_CAST") continuation.resume(result as T?)
}
override fun error(code: String, message: String?, details: Any?) {
continuation.resume(null)
}
override fun notImplemented() {
continuation.resume(null)
}
})
}
}
inline fun <reified T : FlutterPlugin> FlutterEngine.plugin(): T? {
return plugins.get(T::class.java) as T?
}
fun <T> MethodChannel.invokeMethodOnMainThread(
method: String,
arguments: Any? = null,
callback: ((Result<T>) -> Unit)? = null
) {
Handler(Looper.getMainLooper()).post {
invokeMethod(method, arguments, object : MethodChannel.Result {
override fun success(result: Any?) {
@Suppress("UNCHECKED_CAST")
callback?.invoke(Result.success(result as T))
}
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
val exception = Exception("MethodChannel error: $errorCode - $errorMessage")
callback?.invoke(Result.failure(exception))
}
override fun notImplemented() {
val exception = NotImplementedError("Method not implemented: $method")
callback?.invoke(Result.failure(exception))
}
})
}
}

View File

@@ -43,7 +43,7 @@ class FilesProvider : DocumentsProvider() {
add(Root.COLUMN_ROOT_ID, DEFAULT_ROOT_ID)
add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY)
add(Root.COLUMN_ICON, R.mipmap.ic_launcher)
add(Root.COLUMN_TITLE, context!!.getString(R.string.fl_clash))
add(Root.COLUMN_TITLE, "FlClash")
add(Root.COLUMN_SUMMARY, "Data")
add(Root.COLUMN_DOCUMENT_ID, "/")
}

View File

@@ -1,18 +0,0 @@
package com.follow.clash;
import android.app.Application
import android.content.Context
class FlClashApplication : Application() {
companion object {
private lateinit var instance: FlClashApplication
fun getAppContext(): Context {
return instance.applicationContext
}
}
override fun onCreate() {
super.onCreate()
instance = this
}
}

View File

@@ -1,125 +0,0 @@
package com.follow.clash
import androidx.lifecycle.MutableLiveData
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.TilePlugin
import com.follow.clash.plugins.VpnPlugin
import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
enum class RunState {
START,
PENDING,
STOP
}
object GlobalState {
val runLock = ReentrantLock()
const val NOTIFICATION_CHANNEL = "FlClash"
const val NOTIFICATION_ID = 1
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
var flutterEngine: FlutterEngine? = null
private var serviceEngine: FlutterEngine? = null
fun getCurrentAppPlugin(): AppPlugin? {
val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
}
fun syncStatus() {
CoroutineScope(Dispatchers.Default).launch {
val status = getCurrentVPNPlugin()?.getStatus() ?: false
withContext(Dispatchers.Main){
runState.value = if (status) RunState.START else RunState.STOP
}
}
}
suspend fun getText(text: String): String {
return getCurrentAppPlugin()?.getText(text) ?: ""
}
fun getCurrentTilePlugin(): TilePlugin? {
val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine
return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
}
fun getCurrentVPNPlugin(): VpnPlugin? {
return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
}
fun handleToggle() {
val starting = handleStart()
if (!starting) {
handleStop()
}
}
fun handleStart(): Boolean {
if (runState.value == RunState.STOP) {
runState.value = RunState.PENDING
runLock.lock()
val tilePlugin = getCurrentTilePlugin()
if (tilePlugin != null) {
tilePlugin.handleStart()
} else {
initServiceEngine()
}
return true
}
return false
}
fun handleStop() {
if (runState.value == RunState.START) {
runState.value = RunState.PENDING
runLock.lock()
getCurrentTilePlugin()?.handleStop()
}
}
fun handleTryDestroy() {
if (flutterEngine == null) {
destroyServiceEngine()
}
}
fun destroyServiceEngine() {
runLock.withLock {
serviceEngine?.destroy()
serviceEngine = null
}
}
fun initServiceEngine() {
if (serviceEngine != null) return
destroyServiceEngine()
runLock.withLock {
serviceEngine = FlutterEngine(FlClashApplication.getAppContext())
serviceEngine?.plugins?.add(VpnPlugin)
serviceEngine?.plugins?.add(AppPlugin())
serviceEngine?.plugins?.add(TilePlugin())
val vpnService = DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"_service"
)
serviceEngine?.dartExecutor?.executeDartEntrypoint(
vpnService,
if (flutterEngine == null) listOf("quick") else null
)
}
}
}

View File

@@ -1,23 +1,37 @@
package com.follow.clash
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.TilePlugin
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
class MainActivity : FlutterActivity(),
CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
State.destroyServiceEngine()
}
}
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
flutterEngine.plugins.add(AppPlugin())
flutterEngine.plugins.add(ServicePlugin)
flutterEngine.plugins.add(ServicePlugin())
flutterEngine.plugins.add(TilePlugin())
GlobalState.flutterEngine = flutterEngine
State.flutterEngine = flutterEngine
}
override fun onDestroy() {
GlobalState.flutterEngine = null
GlobalState.runState.value = RunState.STOP
State.flutterEngine = null
super.onDestroy()
}
}

View File

@@ -0,0 +1,77 @@
package com.follow.clash
import com.follow.clash.common.ServiceDelegate
import com.follow.clash.common.intent
import com.follow.clash.service.ICallbackInterface
import com.follow.clash.service.IRemoteInterface
import com.follow.clash.service.RemoteService
import com.follow.clash.service.models.NotificationParams
import com.follow.clash.service.models.VpnOptions
import java.util.concurrent.atomic.AtomicBoolean
object Service {
private val delegate by lazy {
ServiceDelegate<IRemoteInterface>(
RemoteService::class.intent, ::handleOnServiceCrash
) {
IRemoteInterface.Stub.asInterface(it)
}
}
var onServiceCrash: (() -> Unit)? = null
private fun handleOnServiceCrash() {
bindingState.set(false)
onServiceCrash?.let {
it()
}
}
private val bindingState = AtomicBoolean(false)
fun bind() {
if (bindingState.compareAndSet(false, true)) {
delegate.bind()
}
}
suspend fun invokeAction(
data: String, cb: (result: String?) -> Unit
) {
delegate.useService {
it.invokeAction(data, object : ICallbackInterface.Stub() {
override fun onResult(result: String?) {
cb(result)
}
})
}
}
suspend fun updateNotificationParams(
params: NotificationParams
) {
delegate.useService {
it.updateNotificationParams(params)
}
}
suspend fun setMessageCallback(
cb: (result: String?) -> Unit
) {
delegate.useService {
it.setMessageCallback(object : ICallbackInterface.Stub() {
override fun onResult(result: String?) {
cb(result)
}
})
}
}
suspend fun startService(options: VpnOptions, inApp: Boolean) {
delegate.useService { it.startService(options, inApp) }
}
suspend fun stopService() {
delegate.useService { it.stopService() }
}
}

View File

@@ -0,0 +1,148 @@
package com.follow.clash
import com.follow.clash.common.GlobalState
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.TilePlugin
import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
enum class RunState {
START, PENDING, STOP
}
object State {
val runLock = Mutex()
var runTime: Long = 0
val runStateFlow: MutableStateFlow<RunState> = MutableStateFlow(RunState.STOP)
var flutterEngine: FlutterEngine? = null
var serviceFlutterEngine: FlutterEngine? = null
val appPlugin: AppPlugin?
get() = flutterEngine?.plugin<AppPlugin>() ?: serviceFlutterEngine?.plugin<AppPlugin>()
val servicePlugin: ServicePlugin?
get() = flutterEngine?.plugin<ServicePlugin>()
?: serviceFlutterEngine?.plugin<ServicePlugin>()
val tilePlugin: TilePlugin?
get() = flutterEngine?.plugin<TilePlugin>() ?: serviceFlutterEngine?.plugin<TilePlugin>()
suspend fun handleToggleAction() {
var action: (suspend () -> Unit)?
runLock.withLock {
action = when (runStateFlow.value) {
RunState.PENDING -> null
RunState.START -> ::handleStopServiceAction
RunState.STOP -> ::handleStartServiceAction
}
}
action?.invoke()
}
suspend fun handleStartServiceAction() {
tilePlugin?.handleStart()
if (flutterEngine != null) {
return
}
startServiceWithEngine()
}
fun handleStopServiceAction() {
tilePlugin?.handleStop()
if (flutterEngine != null || serviceFlutterEngine != null) {
return
}
handleStopService()
}
fun handleStartService() {
if (appPlugin != null) {
appPlugin?.requestNotificationsPermission {
startService()
}
return
}
startService()
}
suspend fun destroyServiceEngine() {
runLock.withLock {
withContext(Dispatchers.Main) {
runCatching {
serviceFlutterEngine?.destroy()
serviceFlutterEngine = null
}
}
}
}
suspend fun startServiceWithEngine() {
runLock.withLock {
withContext(Dispatchers.Main) {
serviceFlutterEngine = FlutterEngine(GlobalState.application)
serviceFlutterEngine?.plugins?.add(ServicePlugin())
serviceFlutterEngine?.plugins?.add(AppPlugin())
serviceFlutterEngine?.plugins?.add(TilePlugin())
val dartEntrypoint = DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(), "_service"
)
serviceFlutterEngine?.dartExecutor?.executeDartEntrypoint(dartEntrypoint)
}
}
}
private fun startService() {
GlobalState.launch {
runLock.withLock {
if (runStateFlow.value == RunState.PENDING || runStateFlow.value == RunState.START) {
return@launch
}
runStateFlow.tryEmit(RunState.PENDING)
if (servicePlugin == null) {
return@launch
}
val options = servicePlugin?.handleGetVpnOptions()
if (options == null) {
return@launch
}
appPlugin?.prepare(options.enable) {
Service.startService(options, true)
runStateFlow.tryEmit(RunState.START)
runTime = System.currentTimeMillis()
}
}
}
}
fun handleStopService() {
GlobalState.launch {
runLock.withLock {
if (runStateFlow.value == RunState.PENDING || runStateFlow.value == RunState.STOP) {
return@launch
}
runStateFlow.tryEmit(RunState.PENDING)
Service.stopService()
runStateFlow.tryEmit(RunState.STOP)
runTime = 0
}
destroyServiceEngine()
}
}
}

View File

@@ -2,24 +2,34 @@ package com.follow.clash
import android.app.Activity
import android.os.Bundle
import com.follow.clash.extensions.wrapAction
import com.follow.clash.common.QuickAction
import com.follow.clash.common.action
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
class TempActivity : Activity() {
class TempActivity : Activity(),
CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
when (intent.action) {
wrapAction("START") -> {
GlobalState.handleStart()
QuickAction.START.action -> {
launch {
State.handleStartServiceAction()
}
}
wrapAction("STOP") -> {
GlobalState.handleStop()
QuickAction.STOP.action -> {
State.handleStopServiceAction()
}
wrapAction("CHANGE") -> {
GlobalState.handleToggle()
QuickAction.TOGGLE.action -> {
launch {
State.handleToggleAction()
}
}
}
finishAndRemoveTask()
finish()
}
}

View File

@@ -0,0 +1,61 @@
package com.follow.clash
import android.annotation.SuppressLint
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.follow.clash.common.QuickAction
import com.follow.clash.common.quickIntent
import com.follow.clash.common.toPendingIntent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
class TileService : TileService() {
private var scope: CoroutineScope? = null
private fun updateTile(runState: RunState) {
if (qsTile != null) {
qsTile.state = when (runState) {
RunState.START -> Tile.STATE_ACTIVE
RunState.PENDING -> Tile.STATE_UNAVAILABLE
RunState.STOP -> Tile.STATE_INACTIVE
}
qsTile.updateTile()
}
}
override fun onStartListening() {
super.onStartListening()
scope?.cancel()
scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
scope?.launch {
State.runStateFlow.collect {
updateTile(it)
}
}
}
@SuppressLint("StartActivityAndCollapseDeprecated")
private fun handleToggle() {
val intent = QuickAction.TOGGLE.quickIntent
val pendingIntent = intent.toPendingIntent
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startActivityAndCollapse(pendingIntent)
} else {
@Suppress("DEPRECATION")
startActivityAndCollapse(intent)
}
}
override fun onClick() {
super.onClick()
handleToggle()
}
override fun onStopListening() {
scope?.cancel()
super.onStopListening()
}
}

View File

@@ -1,200 +0,0 @@
package com.follow.clash.extensions
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.ConnectivityManager
import android.net.Network
import android.os.Build
import android.system.OsConstants.IPPROTO_TCP
import android.system.OsConstants.IPPROTO_UDP
import android.util.Base64
import androidx.core.graphics.drawable.toBitmap
import com.follow.clash.TempActivity
import com.follow.clash.models.CIDR
import com.follow.clash.models.Metadata
import com.follow.clash.models.VpnOptions
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import java.util.concurrent.locks.ReentrantLock
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
suspend fun Drawable.getBase64(): String {
val drawable = this
return withContext(Dispatchers.IO) {
val bitmap = drawable.toBitmap()
val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)
}
}
fun Metadata.getProtocol(): Int? {
if (network.startsWith("tcp")) return IPPROTO_TCP
if (network.startsWith("udp")) return IPPROTO_UDP
return null
}
fun VpnOptions.getIpv4RouteAddress(): List<CIDR> {
return routeAddress.filter {
it.isIpv4()
}.map {
it.toCIDR()
}
}
fun VpnOptions.getIpv6RouteAddress(): List<CIDR> {
return routeAddress.filter {
it.isIpv6()
}.map {
it.toCIDR()
}
}
fun String.isIpv4(): Boolean {
val parts = split("/")
if (parts.size != 2) {
throw IllegalArgumentException("Invalid CIDR format")
}
val address = InetAddress.getByName(parts[0])
return address.address.size == 4
}
fun String.isIpv6(): Boolean {
val parts = split("/")
if (parts.size != 2) {
throw IllegalArgumentException("Invalid CIDR format")
}
val address = InetAddress.getByName(parts[0])
return address.address.size == 16
}
fun String.toCIDR(): CIDR {
val parts = split("/")
if (parts.size != 2) {
throw IllegalArgumentException("Invalid CIDR format")
}
val ipAddress = parts[0]
val prefixLength = parts[1].toIntOrNull()
?: throw IllegalArgumentException("Invalid prefix length")
val address = InetAddress.getByName(ipAddress)
val maxPrefix = if (address.address.size == 4) 32 else 128
if (prefixLength < 0 || prefixLength > maxPrefix) {
throw IllegalArgumentException("Invalid prefix length for IP version")
}
return CIDR(address, prefixLength)
}
fun ConnectivityManager.resolveDns(network: Network?): List<String> {
val properties = getLinkProperties(network) ?: return listOf()
return properties.dnsServers.map { it.asSocketAddressText(53) }
}
fun InetAddress.asSocketAddressText(port: Int): String {
return when (this) {
is Inet6Address ->
"[${numericToTextFormat(this)}]:$port"
is Inet4Address ->
"${this.hostAddress}:$port"
else -> throw IllegalArgumentException("Unsupported Inet type ${this.javaClass}")
}
}
fun Context.wrapAction(action: String): String {
return "${this.packageName}.action.$action"
}
fun Context.getActionIntent(action: String): Intent {
val actionIntent = Intent(this, TempActivity::class.java)
actionIntent.action = wrapAction(action)
return actionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
}
fun Context.getActionPendingIntent(action: String): PendingIntent {
return if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this,
0,
getActionIntent(action),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this,
0,
getActionIntent(action),
PendingIntent.FLAG_UPDATE_CURRENT
)
}
}
private fun numericToTextFormat(address: Inet6Address): String {
val src = address.address
val sb = StringBuilder(39)
for (i in 0 until 8) {
sb.append(
Integer.toHexString(
src[i shl 1].toInt() shl 8 and 0xff00
or (src[(i shl 1) + 1].toInt() and 0xff)
)
)
if (i < 7) {
sb.append(":")
}
}
if (address.scopeId > 0) {
sb.append("%")
sb.append(address.scopeId)
}
return sb.toString()
}
suspend fun <T> MethodChannel.awaitResult(
method: String,
arguments: Any? = null
): T? = withContext(Dispatchers.Main) { // 切换到主线程
suspendCoroutine { continuation ->
invokeMethod(method, arguments, object : MethodChannel.Result {
override fun success(result: Any?) {
@Suppress("UNCHECKED_CAST")
continuation.resume(result as T)
}
override fun error(code: String, message: String?, details: Any?) {
continuation.resume(null)
}
override fun notImplemented() {
continuation.resume(null)
}
})
}
}
fun ReentrantLock.safeLock() {
if (this.isLocked) {
return
}
this.lock()
}
fun ReentrantLock.safeUnlock() {
if (!this.isLocked) {
return
}
this.unlock()
}

View File

@@ -1,15 +0,0 @@
package com.follow.clash.models
data class Process(
val id: String,
val metadata: Metadata,
)
data class Metadata(
val network: String,
val sourceIP: String,
val sourcePort: Int,
val destinationIP: String,
val destinationPort: Int,
val host: String
)

View File

@@ -1,34 +0,0 @@
package com.follow.clash.models
import java.net.InetAddress
enum class AccessControlMode {
acceptSelected, rejectSelected,
}
data class AccessControl(
val enable: Boolean,
val mode: AccessControlMode,
val acceptList: List<String>,
val rejectList: List<String>,
)
data class CIDR(val address: InetAddress, val prefixLength: Int)
data class VpnOptions(
val enable: Boolean,
val port: Int,
val accessControl: AccessControl,
val allowBypass: Boolean,
val systemProxy: Boolean,
val bypassDomain: List<String>,
val routeAddress: List<String>,
val ipv4Address: String,
val ipv6Address: String,
val dnsServerAddress: String,
)
data class StartForegroundParams(
val title: String,
val content: String,
)

View File

@@ -0,0 +1,8 @@
package com.follow.clash.models
data class AppState(
val currentProfileName: String,
val stopText: String,
val onlyStatisticsProxy: Boolean,
)

View File

@@ -13,17 +13,16 @@ import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.FileProvider
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
import com.follow.clash.FlClashApplication
import com.follow.clash.GlobalState
import com.follow.clash.R
import com.follow.clash.extensions.awaitResult
import com.follow.clash.extensions.getActionIntent
import com.follow.clash.extensions.getBase64
import com.follow.clash.common.Components
import com.follow.clash.common.GlobalState
import com.follow.clash.common.QuickAction
import com.follow.clash.common.quickIntent
import com.follow.clash.getBase64
import com.follow.clash.models.Package
import com.google.gson.Gson
import io.flutter.embedding.android.FlutterActivity
@@ -44,13 +43,20 @@ import java.util.zip.ZipFile
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
companion object {
const val VPN_PERMISSION_REQUEST_CODE = 1001
const val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
}
private var activityRef: WeakReference<Activity>? = null
private lateinit var channel: MethodChannel
private lateinit var scope: CoroutineScope
private var vpnCallBack: (() -> Unit)? = null
private var vpnPrepareCallback: (suspend () -> Unit)? = null
private var requestNotificationCallback: (() -> Unit)? = null
private val iconMap = mutableMapOf<String, String?>()
@@ -111,46 +117,8 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex()
}
val VPN_PERMISSION_REQUEST_CODE = 1001
val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
private var isBlockNotification: Boolean = false
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
scope = CoroutineScope(Dispatchers.Default)
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
channel.setMethodCallHandler(this)
}
private fun initShortcuts(label: String) {
val shortcut = ShortcutInfoCompat.Builder(FlClashApplication.getAppContext(), "toggle")
.setShortLabel(label)
.setIcon(
IconCompat.createWithResource(
FlClashApplication.getAppContext(),
R.mipmap.ic_launcher_round
)
)
.setIntent(FlClashApplication.getAppContext().getActionIntent("CHANGE"))
.build()
ShortcutManagerCompat.setDynamicShortcuts(
FlClashApplication.getAppContext(),
listOf(shortcut)
)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
scope.cancel()
}
private fun tip(message: String?) {
if (GlobalState.flutterEngine == null) {
Toast.makeText(FlClashApplication.getAppContext(), message, Toast.LENGTH_LONG).show()
}
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"moveTaskToBack" -> {
@@ -196,7 +164,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
if (iconMap["default"] == null) {
iconMap["default"] =
FlClashApplication.getAppContext().packageManager?.defaultActivityIcon?.getBase64()
GlobalState.application.packageManager?.defaultActivityIcon?.getBase64()
}
result.success(iconMap["default"])
return@launch
@@ -210,56 +178,36 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
result.success(true)
}
"openFile" -> {
val path = call.argument<String>("path")!!
openFile(path)
result.success(true)
}
else -> {
result.notImplemented()
}
}
}
private fun openFile(path: String) {
val file = File(path)
val uri = FileProvider.getUriForFile(
FlClashApplication.getAppContext(),
"${FlClashApplication.getAppContext().packageName}.fileProvider",
file
)
val intent = Intent(Intent.ACTION_VIEW).setDataAndType(
uri,
"text/plain"
)
val flags =
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
val resInfoList = FlClashApplication.getAppContext().packageManager.queryIntentActivities(
intent, PackageManager.MATCH_DEFAULT_ONLY
)
for (resolveInfo in resInfoList) {
val packageName = resolveInfo.activityInfo.packageName
FlClashApplication.getAppContext().grantUriPermission(
packageName,
uri,
flags
private fun initShortcuts(label: String) {
val shortcut = with(ShortcutInfoCompat.Builder(GlobalState.application, "toggle")) {
setShortLabel(label)
setIcon(
IconCompat.createWithResource(
GlobalState.application,
R.mipmap.ic_launcher_round,
)
)
setIntent(QuickAction.TOGGLE.quickIntent)
build()
}
try {
activityRef?.get()?.startActivity(intent)
} catch (e: Exception) {
println(e)
}
ShortcutManagerCompat.setDynamicShortcuts(
GlobalState.application, listOf(shortcut)
)
}
private fun tip(message: String?) {
Toast.makeText(GlobalState.application, message, Toast.LENGTH_LONG).show()
}
@Suppress("DEPRECATION")
private fun updateExcludeFromRecents(value: Boolean?) {
val am = getSystemService(FlClashApplication.getAppContext(), ActivityManager::class.java)
val am = getSystemService(GlobalState.application, ActivityManager::class.java)
val task = am?.appTasks?.firstOrNull {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
it.taskInfo.taskId == activityRef?.get()?.taskId
@@ -276,7 +224,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
private suspend fun getPackageIcon(packageName: String): String? {
val packageManager = FlClashApplication.getAppContext().packageManager
val packageManager = GlobalState.application.packageManager
if (iconMap[packageName] == null) {
iconMap[packageName] = try {
packageManager?.getApplicationIcon(packageName)?.getBase64()
@@ -289,11 +237,11 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
private fun getPackages(): List<Package> {
val packageManager = FlClashApplication.getAppContext().packageManager
val packageManager = GlobalState.application.packageManager
if (packages.isNotEmpty()) return packages
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA or PackageManager.GET_PERMISSIONS)
?.filter {
it.packageName != FlClashApplication.getAppContext().packageName || it.packageName == "android"
it.packageName != GlobalState.application.packageName || it.packageName == "android"
}?.map {
Package(
@@ -321,52 +269,66 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
}
fun requestVpnPermission(callBack: () -> Unit) {
vpnCallBack = callBack
val intent = VpnService.prepare(FlClashApplication.getAppContext())
fun requestNotificationsPermission(callBack: () -> Unit) {
requestNotificationCallback = callBack
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permission = ContextCompat.checkSelfPermission(
GlobalState.application, Manifest.permission.POST_NOTIFICATIONS
)
if (permission == PackageManager.PERMISSION_GRANTED || isBlockNotification) {
invokeRequestNotificationCallback()
return
}
activityRef?.get()?.let {
ActivityCompat.requestPermissions(
it,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_REQUEST_CODE
)
}
return
} else {
invokeRequestNotificationCallback()
}
}
fun invokeRequestNotificationCallback() {
requestNotificationCallback?.invoke()
requestNotificationCallback = null
}
fun prepare(needPrepare: Boolean, callBack: (suspend () -> Unit)) {
vpnPrepareCallback = callBack
if (!needPrepare) {
invokeVpnPrepareCallback()
return
}
val intent = VpnService.prepare(GlobalState.application)
if (intent != null) {
activityRef?.get()?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
return
}
vpnCallBack?.invoke()
invokeVpnPrepareCallback()
}
fun requestNotificationsPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permission = ContextCompat.checkSelfPermission(
FlClashApplication.getAppContext(),
Manifest.permission.POST_NOTIFICATIONS
)
if (permission != PackageManager.PERMISSION_GRANTED) {
if (isBlockNotification) return
if (activityRef?.get() == null) return
activityRef?.get()?.let {
ActivityCompat.requestPermissions(
it,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_REQUEST_CODE
)
return
}
}
fun invokeVpnPrepareCallback() {
GlobalState.launch {
vpnPrepareCallback?.invoke()
vpnPrepareCallback = null
}
}
suspend fun getText(text: String): String? {
return withContext(Dispatchers.Default) {
channel.awaitResult<String>("getText", text)
}
}
@Suppress("DEPRECATION")
private fun isChinaPackage(packageName: String): Boolean {
val packageManager = FlClashApplication.getAppContext().packageManager ?: return false
val packageManager = GlobalState.application.packageManager ?: return false
skipPrefixList.forEach {
if (packageName == it || packageName.startsWith("$it.")) return false
}
val packageManagerFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
PackageManager.MATCH_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
} else {
@Suppress("DEPRECATION")
PackageManager.GET_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
}
if (packageName.matches(chinaAppRegex)) {
@@ -375,8 +337,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
try {
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackageInfo(
packageName,
PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
packageName, PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
)
} else {
packageManager.getPackageInfo(
@@ -427,6 +388,18 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
return false
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
scope = CoroutineScope(Dispatchers.Default)
channel =
MethodChannel(flutterPluginBinding.binaryMessenger, "${Components.PACKAGE_NAME}/app")
channel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
scope.cancel()
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activityRef = WeakReference(binding.activity)
binding.addActivityResultListener(::onActivityResult)
@@ -449,21 +422,19 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
if (resultCode == FlutterActivity.RESULT_OK) {
GlobalState.initServiceEngine()
vpnCallBack?.invoke()
invokeVpnPrepareCallback()
}
}
return true
}
private fun onRequestPermissionsResultListener(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
requestCode: Int, permissions: Array<String>, grantResults: IntArray
): Boolean {
if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
isBlockNotification = true
}
invokeRequestNotificationCallback()
return true
}
}

View File

@@ -1,19 +1,33 @@
package com.follow.clash.plugins
import com.follow.clash.GlobalState
import com.follow.clash.models.VpnOptions
import com.follow.clash.RunState
import com.follow.clash.Service
import com.follow.clash.State
import com.follow.clash.awaitResult
import com.follow.clash.common.Components
import com.follow.clash.invokeMethodOnMainThread
import com.follow.clash.models.AppState
import com.follow.clash.service.models.NotificationParams
import com.follow.clash.service.models.VpnOptions
import com.google.gson.Gson
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
data object ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
private lateinit var flutterMethodChannel: MethodChannel
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "service")
flutterMethodChannel = MethodChannel(
flutterPluginBinding.binaryMessenger, "${Components.PACKAGE_NAME}/service"
)
flutterMethodChannel.setMethodCallHandler(this)
}
@@ -22,28 +36,28 @@ data object ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
"startVpn" -> {
val data = call.argument<String>("data")
val options = Gson().fromJson(data, VpnOptions::class.java)
GlobalState.getCurrentVPNPlugin()?.handleStart(options)
result.success(true)
}
"stopVpn" -> {
GlobalState.getCurrentVPNPlugin()?.handleStop()
result.success(true)
}
"init" -> {
GlobalState.getCurrentAppPlugin()
?.requestNotificationsPermission()
GlobalState.initServiceEngine()
result.success(true)
handleInit(result)
}
"destroy" -> {
handleDestroy()
result.success(true)
"invokeAction" -> {
handleInvokeAction(call, result)
}
"getRunTime" -> {
handleGetRunTime(result)
}
"syncState" -> {
handleSyncState(call, result)
}
"start" -> {
handleStart(result)
}
"stop" -> {
handleStop(result)
}
else -> {
@@ -51,8 +65,73 @@ data object ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}
}
private fun handleInvokeAction(call: MethodCall, result: MethodChannel.Result) {
launch {
val data = call.arguments<String>()!!
Service.invokeAction(data) {
result.success(it)
}
}
}
private fun handleDestroy() {
GlobalState.destroyServiceEngine()
private fun handleStart(result: MethodChannel.Result) {
State.handleStartService()
result.success(true)
}
private fun handleStop(result: MethodChannel.Result) {
State.handleStopService()
result.success(true)
}
suspend fun handleGetVpnOptions(): VpnOptions? {
val res = flutterMethodChannel.awaitResult<String>("getVpnOptions", null)
return Gson().fromJson(res, VpnOptions::class.java)
}
val semaphore = Semaphore(10)
fun handleSendEvent(value: String?) {
launch(Dispatchers.Main) {
semaphore.withPermit {
flutterMethodChannel.invokeMethod("event", value)
}
}
}
private fun onServiceCrash() {
State.runStateFlow.tryEmit(RunState.STOP)
flutterMethodChannel.invokeMethodOnMainThread<Any>("crash", null)
}
private fun handleSyncState(call: MethodCall, result: MethodChannel.Result) {
launch {
val data = call.arguments<String>()!!
val params = Gson().fromJson(data, AppState::class.java)
Service.updateNotificationParams(
NotificationParams(
title = params.currentProfileName,
stopText = params.stopText,
onlyStatisticsProxy = params.onlyStatisticsProxy
)
)
result.success(true)
}
}
fun handleInit(result: MethodChannel.Result) {
Service.bind()
launch {
Service.setMessageCallback {
handleSendEvent(it)
}
result.success(true)
}
Service.onServiceCrash = ::onServiceCrash
}
private fun handleGetRunTime(result: MethodChannel.Result) {
return result.success(State.runTime)
}
}

View File

@@ -1,5 +1,7 @@
package com.follow.clash.plugins
import com.follow.clash.common.Components
import com.follow.clash.invokeMethodOnMainThread
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
@@ -9,25 +11,21 @@ class TilePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var channel: MethodChannel
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "tile")
channel =
MethodChannel(flutterPluginBinding.binaryMessenger, "${Components.PACKAGE_NAME}/tile")
channel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
handleDetached()
channel.setMethodCallHandler(null)
}
fun handleStart() {
channel.invokeMethod("start", null)
channel.invokeMethodOnMainThread<Any>("start", null)
}
fun handleStop() {
channel.invokeMethod("stop", null)
}
private fun handleDetached() {
channel.invokeMethod("detached", null)
channel.invokeMethodOnMainThread<Any>("stop", null)
}

View File

@@ -1,276 +0,0 @@
package com.follow.clash.plugins
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import android.os.IBinder
import androidx.core.content.getSystemService
import com.follow.clash.FlClashApplication
import com.follow.clash.GlobalState
import com.follow.clash.RunState
import com.follow.clash.core.Core
import com.follow.clash.extensions.awaitResult
import com.follow.clash.extensions.resolveDns
import com.follow.clash.models.StartForegroundParams
import com.follow.clash.models.VpnOptions
import com.follow.clash.services.BaseServiceInterface
import com.follow.clash.services.FlClashService
import com.follow.clash.services.FlClashVpnService
import com.google.gson.Gson
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.InetSocketAddress
import kotlin.concurrent.withLock
data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var flutterMethodChannel: MethodChannel
private var flClashService: BaseServiceInterface? = null
private var options: VpnOptions? = null
private var isBind: Boolean = false
private lateinit var scope: CoroutineScope
private var lastStartForegroundParams: StartForegroundParams? = null
private var timerJob: Job? = null
private val uidPageNameMap = mutableMapOf<Int, String>()
private val connectivity by lazy {
FlClashApplication.getAppContext().getSystemService<ConnectivityManager>()
}
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
isBind = true
flClashService = when (service) {
is FlClashVpnService.LocalBinder -> service.getService()
is FlClashService.LocalBinder -> service.getService()
else -> throw Exception("invalid binder")
}
handleStartService()
}
override fun onServiceDisconnected(arg: ComponentName) {
isBind = false
flClashService = null
}
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
scope = CoroutineScope(Dispatchers.Default)
scope.launch {
registerNetworkCallback()
}
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "vpn")
flutterMethodChannel.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
unRegisterNetworkCallback()
flutterMethodChannel.setMethodCallHandler(null)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"start" -> {
val data = call.argument<String>("data")
result.success(handleStart(Gson().fromJson(data, VpnOptions::class.java)))
}
"stop" -> {
handleStop()
result.success(true)
}
else -> {
result.notImplemented()
}
}
}
fun handleStart(options: VpnOptions): Boolean {
onUpdateNetwork();
if (options.enable != this.options?.enable) {
this.flClashService = null
}
this.options = options
when (options.enable) {
true -> handleStartVpn()
false -> handleStartService()
}
return true
}
private fun handleStartVpn() {
GlobalState.getCurrentAppPlugin()?.requestVpnPermission {
handleStartService()
}
}
fun requestGc() {
flutterMethodChannel.invokeMethod("gc", null)
}
val networks = mutableSetOf<Network>()
fun onUpdateNetwork() {
val dns = networks.flatMap { network ->
connectivity?.resolveDns(network) ?: emptyList()
}.toSet().joinToString(",")
scope.launch {
withContext(Dispatchers.Main) {
flutterMethodChannel.invokeMethod("dnsChanged", dns)
}
}
}
private val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
networks.add(network)
onUpdateNetwork()
}
override fun onLost(network: Network) {
networks.remove(network)
onUpdateNetwork()
}
}
private val request = NetworkRequest.Builder().apply {
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
}.build()
private fun registerNetworkCallback() {
networks.clear()
connectivity?.registerNetworkCallback(request, callback)
}
private fun unRegisterNetworkCallback() {
connectivity?.unregisterNetworkCallback(callback)
networks.clear()
onUpdateNetwork()
}
private suspend fun startForeground() {
GlobalState.runLock.lock()
try {
if (GlobalState.runState.value != RunState.START) return
val data = flutterMethodChannel.awaitResult<String>("getStartForegroundParams")
val startForegroundParams = if (data != null) Gson().fromJson(
data, StartForegroundParams::class.java
) else StartForegroundParams(
title = "", content = ""
)
if (lastStartForegroundParams != startForegroundParams) {
lastStartForegroundParams = startForegroundParams
flClashService?.startForeground(
startForegroundParams.title,
startForegroundParams.content,
)
}
} finally {
GlobalState.runLock.unlock()
}
}
private fun startForegroundJob() {
stopForegroundJob()
timerJob = CoroutineScope(Dispatchers.Main).launch {
while (isActive) {
startForeground()
delay(1000)
}
}
}
private fun stopForegroundJob() {
timerJob?.cancel()
timerJob = null
}
suspend fun getStatus(): Boolean? {
return withContext(Dispatchers.Default) {
flutterMethodChannel.awaitResult<Boolean>("status", null)
}
}
private fun handleStartService() {
if (flClashService == null) {
bindService()
return
}
GlobalState.runLock.withLock {
if (GlobalState.runState.value == RunState.START) return
GlobalState.runState.value = RunState.START
val fd = flClashService?.start(options!!)
Core.startTun(
fd = fd ?: 0,
protect = this::protect,
resolverProcess = this::resolverProcess,
)
startForegroundJob()
}
}
private fun protect(fd: Int): Boolean {
return (flClashService as? FlClashVpnService)?.protect(fd) == true
}
private fun resolverProcess(
protocol: Int,
source: InetSocketAddress,
target: InetSocketAddress,
uid: Int,
): String {
val nextUid = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
connectivity?.getConnectionOwnerUid(protocol, source, target) ?: -1
} else {
uid
}
if (nextUid == -1) {
return ""
}
if (!uidPageNameMap.containsKey(nextUid)) {
uidPageNameMap[nextUid] =
FlClashApplication.getAppContext().packageManager?.getPackagesForUid(nextUid)
?.first() ?: ""
}
return uidPageNameMap[nextUid] ?: ""
}
fun handleStop() {
GlobalState.runLock.withLock {
if (GlobalState.runState.value == RunState.STOP) return
GlobalState.runState.value = RunState.STOP
flClashService?.stop()
stopForegroundJob()
Core.stopTun()
GlobalState.handleTryDestroy()
}
}
private fun bindService() {
if (isBind) {
FlClashApplication.getAppContext().unbindService(connection)
}
val intent = when (options?.enable == true) {
true -> Intent(FlClashApplication.getAppContext(), FlClashVpnService::class.java)
false -> Intent(FlClashApplication.getAppContext(), FlClashService::class.java)
}
FlClashApplication.getAppContext().bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
}

View File

@@ -1,96 +0,0 @@
package com.follow.clash.services
import android.annotation.SuppressLint
import android.app.Notification
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.os.Build
import androidx.core.app.NotificationCompat
import com.follow.clash.GlobalState
import com.follow.clash.MainActivity
import com.follow.clash.R
import com.follow.clash.extensions.getActionPendingIntent
import com.follow.clash.models.VpnOptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
interface BaseServiceInterface {
fun start(options: VpnOptions): Int
fun stop()
suspend fun startForeground(title: String, content: String)
}
fun Service.createFlClashNotificationBuilder(): Deferred<NotificationCompat.Builder> =
CoroutineScope(Dispatchers.Main).async {
val stopText = GlobalState.getText("stop")
val intent = Intent(this@createFlClashNotificationBuilder, MainActivity::class.java)
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this@createFlClashNotificationBuilder,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this@createFlClashNotificationBuilder, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT
)
}
with(
NotificationCompat.Builder(
this@createFlClashNotificationBuilder, GlobalState.NOTIFICATION_CHANNEL
)
) {
setSmallIcon(R.drawable.ic)
setContentTitle("FlClash")
setContentIntent(pendingIntent)
setCategory(NotificationCompat.CATEGORY_SERVICE)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
}
setOngoing(true)
addAction(
0, stopText, getActionPendingIntent("STOP")
)
setShowWhen(false)
setOnlyAlertOnce(true)
}
}
@SuppressLint("ForegroundServiceType")
fun Service.startForeground(notification: Notification) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
var channel = manager?.getNotificationChannel(GlobalState.NOTIFICATION_CHANNEL)
if (channel == null) {
channel = NotificationChannel(
GlobalState.NOTIFICATION_CHANNEL, "SERVICE_CHANNEL", NotificationManager.IMPORTANCE_LOW
)
manager?.createNotificationChannel(channel)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
try {
startForeground(
GlobalState.NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
} catch (_: Exception) {
startForeground(GlobalState.NOTIFICATION_ID, notification)
}
} else {
startForeground(GlobalState.NOTIFICATION_ID, notification)
}
}

View File

@@ -1,67 +0,0 @@
package com.follow.clash.services
import android.annotation.SuppressLint
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.follow.clash.GlobalState
import com.follow.clash.models.VpnOptions
class FlClashService : Service(), BaseServiceInterface {
override fun start(options: VpnOptions) = 0
override fun stop() {
stopSelf()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
stopForeground(STOP_FOREGROUND_REMOVE)
}
}
private var cachedBuilder: NotificationCompat.Builder? = null
private suspend fun notificationBuilder(): NotificationCompat.Builder {
if (cachedBuilder == null) {
cachedBuilder = createFlClashNotificationBuilder().await()
}
return cachedBuilder!!
}
@SuppressLint("ForegroundServiceType")
override suspend fun startForeground(title: String, content: String) {
startForeground(
notificationBuilder()
.setContentTitle(title)
.setContentText(content).build()
)
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
GlobalState.getCurrentVPNPlugin()?.requestGc()
}
private val binder = LocalBinder()
inner class LocalBinder : Binder() {
fun getService(): FlClashService = this@FlClashService
}
override fun onBind(intent: Intent): IBinder {
return binder
}
override fun onUnbind(intent: Intent?): Boolean {
return super.onUnbind(intent)
}
override fun onDestroy() {
stop()
super.onDestroy()
}
}

View File

@@ -1,77 +0,0 @@
package com.follow.clash.services
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.annotation.RequiresApi
import androidx.lifecycle.Observer
import com.follow.clash.GlobalState
import com.follow.clash.RunState
import com.follow.clash.TempActivity
@RequiresApi(Build.VERSION_CODES.N)
class FlClashTileService : TileService() {
private val observer = Observer<RunState> { runState ->
updateTile(runState)
}
private fun updateTile(runState: RunState) {
if (qsTile != null) {
qsTile.state = when (runState) {
RunState.START -> Tile.STATE_ACTIVE
RunState.PENDING -> Tile.STATE_UNAVAILABLE
RunState.STOP -> Tile.STATE_INACTIVE
}
qsTile.updateTile()
}
}
override fun onStartListening() {
super.onStartListening()
GlobalState.syncStatus()
GlobalState.runState.value?.let { updateTile(it) }
GlobalState.runState.observeForever(observer)
}
@SuppressLint("StartActivityAndCollapseDeprecated")
private fun activityTransfer() {
val intent = Intent(this, TempActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startActivityAndCollapse(pendingIntent)
} else {
startActivityAndCollapse(intent)
}
}
override fun onClick() {
super.onClick()
activityTransfer()
GlobalState.handleToggle()
}
override fun onDestroy() {
GlobalState.runState.removeObserver(observer)
super.onDestroy()
}
}

View File

@@ -1,193 +0,0 @@
package com.follow.clash.services
import android.annotation.SuppressLint
import android.content.Intent
import android.net.ProxyInfo
import android.net.VpnService
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.os.Parcel
import android.os.RemoteException
import android.util.Log
import androidx.core.app.NotificationCompat
import com.follow.clash.GlobalState
import com.follow.clash.extensions.getIpv4RouteAddress
import com.follow.clash.extensions.getIpv6RouteAddress
import com.follow.clash.extensions.toCIDR
import com.follow.clash.models.AccessControlMode
import com.follow.clash.models.VpnOptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class FlClashVpnService : VpnService(), BaseServiceInterface {
override fun onCreate() {
super.onCreate()
GlobalState.initServiceEngine()
}
override fun start(options: VpnOptions): Int {
return with(Builder()) {
if (options.ipv4Address.isNotEmpty()) {
val cidr = options.ipv4Address.toCIDR()
addAddress(cidr.address, cidr.prefixLength)
Log.d(
"addAddress",
"address: ${cidr.address} prefixLength:${cidr.prefixLength}"
)
val routeAddress = options.getIpv4RouteAddress()
if (routeAddress.isNotEmpty()) {
try {
routeAddress.forEach { i ->
Log.d(
"addRoute4",
"address: ${i.address} prefixLength:${i.prefixLength}"
)
addRoute(i.address, i.prefixLength)
}
} catch (_: Exception) {
addRoute("0.0.0.0", 0)
}
} else {
addRoute("0.0.0.0", 0)
}
} else {
addRoute("0.0.0.0", 0)
}
try {
if (options.ipv6Address.isNotEmpty()) {
val cidr = options.ipv6Address.toCIDR()
Log.d(
"addAddress6",
"address: ${cidr.address} prefixLength:${cidr.prefixLength}"
)
addAddress(cidr.address, cidr.prefixLength)
val routeAddress = options.getIpv6RouteAddress()
if (routeAddress.isNotEmpty()) {
try {
routeAddress.forEach { i ->
Log.d(
"addRoute6",
"address: ${i.address} prefixLength:${i.prefixLength}"
)
addRoute(i.address, i.prefixLength)
}
} catch (_: Exception) {
addRoute("::", 0)
}
} else {
addRoute("::", 0)
}
}
}catch (_:Exception){
Log.d(
"addAddress6",
"IPv6 is not supported."
)
}
addDnsServer(options.dnsServerAddress)
setMtu(9000)
options.accessControl.let { accessControl ->
if (accessControl.enable) {
when (accessControl.mode) {
AccessControlMode.acceptSelected -> {
(accessControl.acceptList + packageName).forEach {
addAllowedApplication(it)
}
}
AccessControlMode.rejectSelected -> {
(accessControl.rejectList - packageName).forEach {
addDisallowedApplication(it)
}
}
}
}
}
setSession("FlClash")
setBlocking(false)
if (Build.VERSION.SDK_INT >= 29) {
setMetered(false)
}
if (options.allowBypass) {
allowBypass()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && options.systemProxy) {
setHttpProxy(
ProxyInfo.buildDirectProxy(
"127.0.0.1",
options.port,
options.bypassDomain
)
)
}
establish()?.detachFd()
?: throw NullPointerException("Establish VPN rejected by system")
}
}
override fun stop() {
stopSelf()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
stopForeground(STOP_FOREGROUND_REMOVE)
}
}
private var cachedBuilder: NotificationCompat.Builder? = null
private suspend fun notificationBuilder(): NotificationCompat.Builder {
if (cachedBuilder == null) {
cachedBuilder = createFlClashNotificationBuilder().await()
}
return cachedBuilder!!
}
@SuppressLint("ForegroundServiceType")
override suspend fun startForeground(title: String, content: String) {
startForeground(
notificationBuilder()
.setContentTitle(title)
.setContentText(content).build()
)
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
GlobalState.getCurrentVPNPlugin()?.requestGc()
}
private val binder = LocalBinder()
inner class LocalBinder : Binder() {
fun getService(): FlClashVpnService = this@FlClashVpnService
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
try {
val isSuccess = super.onTransact(code, data, reply, flags)
if (!isSuccess) {
CoroutineScope(Dispatchers.Main).launch {
GlobalState.getCurrentTilePlugin()?.handleStop()
}
}
return isSuccess
} catch (e: RemoteException) {
throw e
}
}
}
override fun onBind(intent: Intent): IBinder {
return binder
}
override fun onUnbind(intent: Intent?): Boolean {
return super.onUnbind(intent)
}
override fun onDestroy() {
stop()
super.onDestroy()
}
}

View File

@@ -1,3 +1,12 @@
buildscript {
dependencies {
classpath(libs.build.kotlin)
}
}
plugins {
id("com.android.library") apply false
}
allprojects {
repositories {
@@ -5,8 +14,10 @@ allprojects {
mavenCentral()
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
@@ -20,3 +31,4 @@ subprojects {
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

1
android/common/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,42 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.follow.clash.common"
compileSdk = libs.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = 21
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}
dependencies {
implementation(libs.androidx.core)
implementation(libs.gson)
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="${applicationId}.permission.RECEIVE_BROADCASTS" />
</manifest>

View File

@@ -0,0 +1,16 @@
package com.follow.clash.common
import android.content.ComponentName
object Components {
const val PACKAGE_NAME = "com.follow.clash"
val MAIN_ACTIVITY =
ComponentName(GlobalState.packageName, "${PACKAGE_NAME}.MainActivity")
val TEMP_ACTIVITY =
ComponentName(GlobalState.packageName, "${PACKAGE_NAME}.TempActivity")
val BROADCAST_RECEIVER =
ComponentName(GlobalState.packageName, "${PACKAGE_NAME}.BroadcastReceiver")
}

View File

@@ -0,0 +1,24 @@
package com.follow.clash.common
import com.google.gson.annotations.SerializedName
enum class QuickAction {
STOP,
START,
TOGGLE,
}
enum class BroadcastAction {
START,
STOP,
TOGGLE,
}
enum class AccessControlMode {
@SerializedName("acceptSelected")
ACCEPT_SELECTED,
@SerializedName("rejectSelected")
REJECT_SELECTED,
}

View File

@@ -0,0 +1,204 @@
package com.follow.clash.common
import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Context.RECEIVER_NOT_EXPORTED
import android.content.Intent
import android.content.IntentFilter
import android.content.ServiceConnection
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.os.Build
import android.os.IBinder
import android.os.RemoteException
import android.util.Log
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlin.reflect.KClass
//fun Context.startForegroundServiceCompat(intent: Intent?) {
// if (Build.VERSION.SDK_INT >= 26) {
// startForegroundService(intent)
// } else {
// startService(intent)
// }
//}
val KClass<*>.intent: Intent
get() = Intent(GlobalState.application, this.java)
fun Service.startForegroundCompat(id: Int, notification: Notification) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(id, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} else {
startForeground(id, notification)
}
}
val QuickAction.action: String
get() = "${GlobalState.application.packageName}.action.${this.name}"
val QuickAction.quickIntent: Intent
get() = Intent().apply {
setComponent(Components.TEMP_ACTIVITY)
setPackage(GlobalState.packageName)
action = this@quickIntent.action
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
}
val BroadcastAction.action: String
get() = "${GlobalState.application.packageName}.intent.action.${this.name}"
val BroadcastAction.quickIntent: Intent
get() = Intent().apply {
setComponent(Components.BROADCAST_RECEIVER)
setPackage(GlobalState.packageName)
action = this@quickIntent.action
}
fun BroadcastAction.sendBroadcast() {
val intent = Intent().apply {
action = this@sendBroadcast.action
Log.d("[sendBroadcast]", "$action")
setPackage(GlobalState.packageName)
}
GlobalState.application.sendBroadcast(
intent, GlobalState.RECEIVE_BROADCASTS_PERMISSIONS
)
}
val Intent.toPendingIntent: PendingIntent
get() = PendingIntent.getActivity(
GlobalState.application,
0,
this,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
fun Service.startForeground(notification: Notification) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
var channel = manager?.getNotificationChannel(GlobalState.NOTIFICATION_CHANNEL)
if (channel == null) {
channel = NotificationChannel(
GlobalState.NOTIFICATION_CHANNEL,
"SERVICE_CHANNEL",
NotificationManager.IMPORTANCE_LOW
)
manager?.createNotificationChannel(channel)
}
}
startForegroundCompat(GlobalState.NOTIFICATION_ID, notification)
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
fun Context.registerReceiverCompat(
receiver: BroadcastReceiver,
filter: IntentFilter,
) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(receiver, filter, RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(receiver, filter)
}
fun Context.receiveBroadcastFlow(
configure: IntentFilter.() -> Unit,
): Flow<Intent> = callbackFlow {
val filter = IntentFilter().apply(configure)
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (context == null || intent == null) return
trySend(intent)
}
}
registerReceiverCompat(receiver, filter)
awaitClose { unregisterReceiver(receiver) }
}
sealed class BindServiceEvent<out T : IBinder> {
data class Connected<T : IBinder>(val binder: T) : BindServiceEvent<T>()
object Disconnected : BindServiceEvent<Nothing>()
object Crashed : BindServiceEvent<Nothing>()
}
inline fun <reified T : IBinder> Context.bindServiceFlow(
intent: Intent,
flags: Int = Context.BIND_AUTO_CREATE,
): Flow<BindServiceEvent<T>> = callbackFlow {
var currentBinder: IBinder? = null
val deathRecipient = IBinder.DeathRecipient {
trySend(BindServiceEvent.Crashed)
}
val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
if (binder != null) {
try {
binder.linkToDeath(deathRecipient, 0)
currentBinder = binder
@Suppress("UNCHECKED_CAST") val casted = binder as? T
if (casted != null) {
trySend(BindServiceEvent.Connected(casted))
} else {
GlobalState.log("Binder is not of type ${T::class.java}")
trySend(BindServiceEvent.Disconnected)
}
} catch (e: RemoteException) {
GlobalState.log("Failed to link to death: ${e.message}")
binder.unlinkToDeath(deathRecipient, 0)
trySend(BindServiceEvent.Disconnected)
}
} else {
trySend(BindServiceEvent.Disconnected)
}
}
override fun onServiceDisconnected(name: ComponentName?) {
GlobalState.log("Service disconnected")
currentBinder?.unlinkToDeath(deathRecipient, 0)
currentBinder = null
trySend(BindServiceEvent.Disconnected)
}
}
if (!bindService(intent, connection, flags)) {
GlobalState.log("Failed to bind service")
trySend(BindServiceEvent.Disconnected)
close()
return@callbackFlow
}
awaitClose {
currentBinder?.unlinkToDeath(deathRecipient, 0)
unbindService(connection)
}
}
val Long.formatBytes: String
get() {
val units = arrayOf("B", "KB", "MB", "GB", "TB")
var size = this.toDouble()
var unitIndex = 0
while (size >= 1024 && unitIndex < units.size - 1) {
size /= 1024
unitIndex++
}
return if (unitIndex == 0) {
"${size.toLong()}${units[unitIndex]}"
} else {
"%.1f${units[unitIndex]}".format(size)
}
}

View File

@@ -0,0 +1,34 @@
package com.follow.clash.common
import android.app.Application
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
object GlobalState : CoroutineScope by CoroutineScope(Dispatchers.Default) {
const val NOTIFICATION_CHANNEL = "FlClash"
const val NOTIFICATION_ID = 1
val packageName: String
get() = _application.packageName
val RECEIVE_BROADCASTS_PERMISSIONS: String
get() = "${packageName}.permission.RECEIVE_BROADCASTS"
private lateinit var _application: Application
val application: Application
get() = _application
fun log(text: String) {
Log.d("[FlClash]", text)
}
fun init(application: Application) {
_application = application
}
}

View File

@@ -0,0 +1,79 @@
package com.follow.clash.common
import android.content.Intent
import android.os.IBinder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.retryWhen
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
class ServiceDelegate<T>(
private val intent: Intent,
private val onServiceDisconnected: (() -> Unit)? = null,
private val onServiceCrash: (() -> Unit)? = null,
private val interfaceCreator: (IBinder) -> T,
) : CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
private val _service = MutableStateFlow<T?>(null)
val service: StateFlow<T?> = _service
private var bindJob: Job? = null
private fun handleBindEvent(event: BindServiceEvent<IBinder>) {
when (event) {
is BindServiceEvent.Connected -> {
_service.value = event.binder.let(interfaceCreator)
}
is BindServiceEvent.Disconnected -> {
_service.value = null
onServiceDisconnected?.invoke()
}
is BindServiceEvent.Crashed -> {
_service.value = null
onServiceCrash?.invoke()
}
}
}
fun bind() {
unbind()
bindJob = launch {
GlobalState.application.bindServiceFlow<IBinder>(intent).collect { it ->
handleBindEvent(it)
}
}
}
suspend inline fun <R> useService(
crossinline block: (T) -> R
): Result<R> {
return withTimeoutOrNull(10_000) {
service.filterNotNull().retryWhen { _, _ ->
delay(200)
true
}.first()
}?.let { s ->
try {
Result.success(block(s))
} catch (e: Exception) {
Result.failure(e)
}
} ?: Result.failure(Exception("Service connection timeout"))
}
fun unbind() {
_service.value = null
bindJob?.cancel()
bindJob = null
}
}

View File

@@ -0,0 +1,14 @@
package com.follow.clash.common
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
fun tickerFlow(delayMillis: Long, initialDelayMillis: Long = delayMillis): Flow<Unit> = flow {
delay(initialDelayMillis)
while (true) {
emit(Unit)
delay(delayMillis)
}
}

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="fl_clash">FlClash</string>
<string name="FlClash">FlClash</string>
</resources>

View File

@@ -1,4 +1,4 @@
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.argumentsWithVarargAsSingleArray
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id("com.android.library")
@@ -7,22 +7,13 @@ plugins {
android {
namespace = "com.follow.clash.core"
compileSdk = 35
ndkVersion = "28.0.13004108"
compileSdk = libs.versions.compileSdk.get().toInt()
ndkVersion = libs.versions.ndkVersion.get()
defaultConfig {
minSdk = 21
minSdk = libs.versions.minSdk.get().toInt()
}
buildTypes {
release {
isJniDebuggable = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
sourceSets {
getByName("main") {
@@ -37,17 +28,30 @@ android {
}
}
kotlinOptions {
jvmTarget = "17"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildTypes {
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}
dependencies {
implementation("androidx.annotation:annotation-jvm:1.9.1")
implementation(libs.annotation.jvm)
}
val copyNativeLibs by tasks.register<Copy>("copyNativeLibs") {
@@ -56,6 +60,18 @@ val copyNativeLibs by tasks.register<Copy>("copyNativeLibs") {
}
from("../../libclash/android")
into("src/main/jniLibs")
doLast {
val includesDir = file("src/main/jniLibs/includes")
val targetDir = file("src/main/cpp/includes")
if (includesDir.exists()) {
copy {
from(includesDir)
into(targetDir)
}
delete(includesDir)
}
}
}
afterEvaluate {

View File

@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@@ -6,7 +6,9 @@ message("CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}")
message("CMAKE_BUILD_TYPE ${CMAKE_BUILD_TYPE}")
if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
# set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
add_compile_options(-O3)
add_compile_options(-flto)
@@ -33,7 +35,7 @@ message("LIB_CLASH_PATH ${LIB_CLASH_PATH}")
if (EXISTS ${LIB_CLASH_PATH})
message("Found libclash.so for ABI ${ANDROID_ABI}")
add_compile_definitions(LIBCLASH)
include_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
include_directories(${CMAKE_SOURCE_DIR}/../cpp/includes/${ANDROID_ABI})
link_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
add_library(${CMAKE_PROJECT_NAME} SHARED
jni_helper.cpp

View File

@@ -1,13 +1,19 @@
#ifdef LIBCLASH
#include <jni.h>
#ifdef LIBCLASH
#include "jni_helper.h"
#include "libclash.h"
#include "bride.h"
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject, const jint fd, jobject cb) {
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject thiz, jint fd, jobject cb,
jstring address, jstring dns) {
const auto interface = new_global(cb);
startTUN(fd, interface);
scoped_string addressChar = get_string(address);
scoped_string dnsChar = get_string(dns);
startTUN(interface, fd, addressChar, dnsChar);
}
extern "C"
@@ -16,9 +22,60 @@ Java_com_follow_clash_core_Core_stopTun(JNIEnv *) {
stopTun();
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_forceGC(JNIEnv *env, jobject thiz) {
forceGC();
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_updateDNS(JNIEnv *env, jobject thiz, jstring dns) {
scoped_string dnsChar = get_string(dns);
updateDns(dnsChar);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_invokeAction(JNIEnv *env, jobject thiz, jstring data, jobject cb) {
const auto interface = new_global(cb);
scoped_string dataChar = get_string(data);
invokeAction(interface, dataChar);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_setMessageCallback(JNIEnv *env, jobject thiz, jobject cb) {
const auto interface = new_global(cb);
setMessageCallback(interface);
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_follow_clash_core_Core_getTraffic(JNIEnv *env, jobject thiz,
const jboolean only_statistics_proxy) {
scoped_string res = getTraffic(only_statistics_proxy);
return new_string(res);
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_follow_clash_core_Core_getTotalTraffic(JNIEnv *env, jobject thiz,
const jboolean only_statistics_proxy) {
scoped_string res = getTotalTraffic(only_statistics_proxy);
return new_string(res);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_suspended(JNIEnv *env, jobject thiz, jboolean suspended) {
suspend(suspended);
}
static jmethodID m_tun_interface_protect;
static jmethodID m_tun_interface_resolve_process;
static jmethodID m_invoke_interface_result;
static void release_jni_object_impl(void *obj) {
@@ -33,19 +90,28 @@ static void call_tun_interface_protect_impl(void *tun_interface, const int fd) {
fd);
}
static const char *
call_tun_interface_resolve_process_impl(void *tun_interface, int protocol,
static char *
call_tun_interface_resolve_process_impl(void *tun_interface, const int protocol,
const char *source,
const char *target,
const int uid) {
ATTACH_JNI();
const auto packageName = reinterpret_cast<jstring>(env->CallObjectMethod(static_cast<jobject>(tun_interface),
m_tun_interface_resolve_process,
protocol,
new_string(source),
new_string(target),
uid));
return get_string(packageName);
const auto packageName = reinterpret_cast<jstring>(env->CallObjectMethod(
static_cast<jobject>(tun_interface),
m_tun_interface_resolve_process,
protocol,
new_string(source),
new_string(target),
uid));
scoped_string packageNameChar = get_string(packageName);
return packageNameChar;
}
static void call_invoke_interface_result_impl(void *invoke_interface, const char *data) {
ATTACH_JNI();
env->CallVoidMethod(static_cast<jobject>(invoke_interface),
m_invoke_interface_result,
new_string(data));
}
extern "C"
@@ -60,13 +126,67 @@ JNI_OnLoad(JavaVM *vm, void *) {
const auto c_tun_interface = find_class("com/follow/clash/core/TunInterface");
const auto c_invoke_interface = find_class("com/follow/clash/core/InvokeInterface");
m_tun_interface_protect = find_method(c_tun_interface, "protect", "(I)V");
m_tun_interface_resolve_process = find_method(c_tun_interface, "resolverProcess",
"(ILjava/lang/String;Ljava/lang/String;I)Ljava/lang/String;");
m_invoke_interface_result = find_method(c_invoke_interface, "onResult",
"(Ljava/lang/String;)V");
protect_func = &call_tun_interface_protect_impl;
resolve_process_func = &call_tun_interface_resolve_process_impl;
result_func = &call_invoke_interface_result_impl;
release_object_func = &release_jni_object_impl;
registerCallbacks(&call_tun_interface_protect_impl,
&call_tun_interface_resolve_process_impl,
&release_jni_object_impl);
return JNI_VERSION_1_6;
}
#else
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject thiz, jint fd, jobject cb,
jstring address, jstring dns) {
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_stopTun(JNIEnv *env, jobject thiz) {
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_invokeAction(JNIEnv *env, jobject thiz, jstring data, jobject cb) {
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_forceGC(JNIEnv *env, jobject thiz) {
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_updateDNS(JNIEnv *env, jobject thiz, jstring dns) {
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_setMessageCallback(JNIEnv *env, jobject thiz, jobject cb) {
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_follow_clash_core_Core_getTraffic(JNIEnv *env, jobject thiz,
const jboolean only_statistics_proxy) {
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_follow_clash_core_Core_getTotalTraffic(JNIEnv *env, jobject thiz,
const jboolean only_statistics_proxy) {
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_suspended(JNIEnv *env, jobject thiz, jboolean suspended) {
}
#endif

View File

@@ -19,7 +19,7 @@ extern void jni_attach_thread(scoped_jni *jni);
extern void jni_detach_thread(const scoped_jni *env);
extern void release_string(char **str);
extern void release_string( char **str);
#define ATTACH_JNI() __attribute__((unused, cleanup(jni_detach_thread))) \
scoped_jni _jni{}; \

View File

@@ -7,7 +7,16 @@ import java.net.URL
data object Core {
private external fun startTun(
fd: Int,
cb: TunInterface
cb: TunInterface,
address: String,
dns: String,
)
external fun forceGC(
)
external fun updateDNS(
dns: String,
)
private fun parseInetSocketAddress(address: String): InetSocketAddress {
@@ -19,31 +28,79 @@ data object Core {
fun startTun(
fd: Int,
protect: (Int) -> Boolean,
resolverProcess: (protocol: Int, source: InetSocketAddress, target: InetSocketAddress, uid: Int) -> String
resolverProcess: (protocol: Int, source: InetSocketAddress, target: InetSocketAddress, uid: Int) -> String,
address: String,
dns: String,
) {
startTun(fd, object : TunInterface {
override fun protect(fd: Int) {
protect(fd)
}
startTun(
fd,
object : TunInterface {
override fun protect(fd: Int) {
protect(fd)
}
override fun resolverProcess(
protocol: Int,
source: String,
target: String,
uid: Int
): String {
return resolverProcess(
protocol,
parseInetSocketAddress(source),
parseInetSocketAddress(target),
uid,
)
}
});
override fun resolverProcess(
protocol: Int,
source: String,
target: String,
uid: Int
): String {
return resolverProcess(
protocol,
parseInetSocketAddress(source),
parseInetSocketAddress(target),
uid,
)
}
},
address,
dns
)
}
external fun suspended(
suspended: Boolean,
)
private external fun invokeAction(
data: String,
cb: InvokeInterface
)
fun invokeAction(
data: String,
cb: (result: String?) -> Unit
) {
invokeAction(
data,
object : InvokeInterface {
override fun onResult(result: String?) {
cb(result)
}
},
)
}
private external fun setMessageCallback(cb: InvokeInterface)
fun setMessageCallback(
cb: (result: String?) -> Unit
) {
setMessageCallback(
object : InvokeInterface {
override fun onResult(result: String?) {
cb(result)
}
},
)
}
external fun stopTun()
external fun getTraffic(onlyStatisticsProxy: Boolean): String
external fun getTotalTraffic(onlyStatisticsProxy: Boolean): String
init {
System.loadLibrary("core")
}

View File

@@ -0,0 +1,8 @@
package com.follow.clash.core
import androidx.annotation.Keep
@Keep
interface InvokeInterface {
fun onResult(result: String?)
}

View File

@@ -1,5 +1,3 @@
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true
kotlin_version=1.9.22
agp_version=8.9.2

View File

@@ -0,0 +1,20 @@
[versions]
#agp = "8.10.1"
minSdk = "23"
targetSdk = "36"
compileSdk = "36"
ndkVersion = "28.0.13004108"
coreKtx = "1.17.0"
annotationJvm = "1.9.1"
coreSplashscreen = "1.0.1"
gson = "2.13.1"
kotlin = "2.2.10"
smaliDexlib2 = "3.0.9"
[libraries]
build-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
androidx-core = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
annotation-jvm = { module = "androidx.annotation:annotation-jvm", version.ref = "annotationJvm" }
core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
smali-dexlib2 = { module = "com.android.tools.smali:smali-dexlib2", version.ref = "smaliDexlib2" }

View File

@@ -2,5 +2,5 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip

1
android/service/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,48 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("kotlin-parcelize")
}
android {
namespace = "com.follow.clash.service"
compileSdk = 36
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
}
buildFeatures {
aidl = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildTypes {
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}
dependencies {
implementation(project(":core"))
implementation(project(":common"))
implementation(libs.gson)
implementation(libs.androidx.core)
}

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application>
<service
android:name="com.follow.clash.service.VpnService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:permission="android.permission.BIND_VPN_SERVICE"
android:process=":background">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="vpn" />
</service>
<service
android:name="com.follow.clash.service.CommonService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:process=":background">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="service" />
</service>
<service
android:name="com.follow.clash.service.RemoteService"
android:exported="false"
android:process=":background" />
</application>
</manifest>

View File

@@ -0,0 +1,6 @@
// ICallbackInterface.aidl
package com.follow.clash.service;
interface ICallbackInterface {
void onResult(String result);
}

View File

@@ -0,0 +1,14 @@
// IRemoteInterface.aidl
package com.follow.clash.service;
import com.follow.clash.service.ICallbackInterface;
import com.follow.clash.service.models.VpnOptions;
import com.follow.clash.service.models.NotificationParams;
interface IRemoteInterface {
void invokeAction(in String data, in ICallbackInterface callback);
void updateNotificationParams(in NotificationParams params);
void startService(in VpnOptions options,in boolean inApp);
void stopService();
void setMessageCallback(in ICallbackInterface messageCallback);
}

View File

@@ -0,0 +1,4 @@
//AccessControl.aidl
package com.follow.clash.service.models;
parcelable AccessControl;

View File

@@ -0,0 +1,4 @@
//NotificationParams.aidl
package com.follow.clash.service.models;
parcelable NotificationParams;

View File

@@ -0,0 +1,6 @@
//VpnOptions.aidl
package com.follow.clash.service.models;
import com.follow.clash.service.models.AccessControl;
parcelable VpnOptions;

View File

@@ -0,0 +1,55 @@
package com.follow.clash.service
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import com.follow.clash.core.Core
import com.follow.clash.service.modules.NetworkObserveModule
import com.follow.clash.service.modules.NotificationModule
import com.follow.clash.service.modules.SuspendModule
import com.follow.clash.service.modules.moduleLoader
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
class CommonService : Service(), IBaseService,
CoroutineScope by CoroutineScope(Dispatchers.Default) {
private val self: CommonService
get() = this
private val loader = moduleLoader {
install(NetworkObserveModule(self))
install(NotificationModule(self))
install(SuspendModule(self))
}
override fun onCreate() {
super.onCreate()
handleCreate()
}
override fun onLowMemory() {
Core.forceGC()
super.onLowMemory()
}
private val binder = LocalBinder()
inner class LocalBinder : Binder() {
fun getService(): CommonService = this@CommonService
}
override fun onBind(intent: Intent): IBinder {
return binder
}
override fun start() {
loader.load()
}
override fun stop() {
loader.cancel()
stopSelf()
}
}

View File

@@ -0,0 +1,18 @@
package com.follow.clash.service
import com.follow.clash.common.BroadcastAction
import com.follow.clash.common.sendBroadcast
interface IBaseService {
fun handleCreate() {
if (!State.inApp) {
BroadcastAction.START.sendBroadcast()
} else {
State.inApp = false
}
}
fun start()
fun stop()
}

View File

@@ -0,0 +1,91 @@
package com.follow.clash.service
import android.app.Service
import android.content.Intent
import android.os.IBinder
import com.follow.clash.common.ServiceDelegate
import com.follow.clash.common.intent
import com.follow.clash.core.Core
import com.follow.clash.service.models.NotificationParams
import com.follow.clash.service.models.VpnOptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
class RemoteService : Service(),
CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
private var delegate: ServiceDelegate<IBaseService>? = null
private var intent: Intent? = null
private fun handleStopService() {
launch {
delegate?.useService { service ->
service.stop()
}
delegate?.unbind()
}
}
fun onServiceDisconnected() {
handleStopService()
}
private fun handleStartService() {
launch {
val nextIntent = when (State.options?.enable == true) {
true -> VpnService::class.intent
false -> CommonService::class.intent
}
if (intent != nextIntent) {
delegate?.unbind()
delegate = ServiceDelegate(nextIntent, ::onServiceDisconnected) { binder ->
when (binder) {
is VpnService.LocalBinder -> binder.getService()
is CommonService.LocalBinder -> binder.getService()
else -> throw IllegalArgumentException("Invalid binder type")
}
}
intent = nextIntent
delegate?.bind()
}
delegate?.useService { service ->
service.start()
}
}
}
private val binder: IRemoteInterface.Stub = object : IRemoteInterface.Stub() {
override fun invokeAction(data: String, callback: ICallbackInterface) {
Core.invokeAction(data, callback::onResult)
}
override fun updateNotificationParams(params: NotificationParams?) {
State.notificationParamsFlow.tryEmit(params)
}
override fun startService(
options: VpnOptions, inApp: Boolean
) {
State.options = options
State.inApp = inApp
handleStartService()
}
override fun stopService() {
handleStopService()
}
override fun setMessageCallback(messageCallback: ICallbackInterface) {
setMessageCallback(messageCallback::onResult)
}
}
private fun setMessageCallback(cb: (result: String?) -> Unit) {
Core.setMessageCallback(cb)
}
override fun onBind(intent: Intent?): IBinder {
return binder
}
}

View File

@@ -0,0 +1,13 @@
package com.follow.clash.service
import com.follow.clash.service.models.NotificationParams
import com.follow.clash.service.models.VpnOptions
import kotlinx.coroutines.flow.MutableStateFlow
object State {
var options: VpnOptions? = null
var inApp: Boolean = false
var notificationParamsFlow: MutableStateFlow<NotificationParams?> = MutableStateFlow(
NotificationParams()
)
}

View File

@@ -0,0 +1,251 @@
package com.follow.clash.service
import android.content.Intent
import android.net.ConnectivityManager
import android.net.ProxyInfo
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.os.Parcel
import android.os.RemoteException
import android.util.Log
import androidx.core.content.getSystemService
import com.follow.clash.common.AccessControlMode
import com.follow.clash.common.BroadcastAction
import com.follow.clash.common.GlobalState
import com.follow.clash.common.sendBroadcast
import com.follow.clash.core.Core
import com.follow.clash.service.models.VpnOptions
import com.follow.clash.service.models.getIpv4RouteAddress
import com.follow.clash.service.models.getIpv6RouteAddress
import com.follow.clash.service.models.toCIDR
import com.follow.clash.service.modules.NetworkObserveModule
import com.follow.clash.service.modules.NotificationModule
import com.follow.clash.service.modules.SuspendModule
import com.follow.clash.service.modules.moduleLoader
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import java.net.InetSocketAddress
import android.net.VpnService as SystemVpnService
class VpnService : SystemVpnService(), IBaseService,
CoroutineScope by CoroutineScope(Dispatchers.Default) {
private val self: VpnService
get() = this
private val loader = moduleLoader {
install(NetworkObserveModule(self))
install(NotificationModule(self))
install(SuspendModule(self))
}
override fun onCreate() {
super.onCreate()
handleCreate()
}
private val connectivity by lazy {
getSystemService<ConnectivityManager>()
}
private val uidPageNameMap = mutableMapOf<Int, String>()
private fun resolverProcess(
protocol: Int,
source: InetSocketAddress,
target: InetSocketAddress,
uid: Int,
): String {
val nextUid = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
connectivity?.getConnectionOwnerUid(protocol, source, target) ?: -1
} else {
uid
}
if (nextUid == -1) {
return ""
}
if (!uidPageNameMap.containsKey(nextUid)) {
uidPageNameMap[nextUid] = this.packageManager?.getPackagesForUid(nextUid)?.first() ?: ""
}
return uidPageNameMap[nextUid] ?: ""
}
val VpnOptions.address
get(): String = buildString {
append(IPV4_ADDRESS)
if (ipv6) {
append(",")
append(IPV6_ADDRESS)
}
}
val VpnOptions.dns
get(): String {
if (dnsHijacking) {
return NET_ANY
}
return buildString {
append(DNS)
if (ipv6) {
append(",")
append(DNS6)
}
}
}
override fun onLowMemory() {
Core.forceGC()
super.onLowMemory()
}
private val binder = LocalBinder()
inner class LocalBinder : Binder() {
fun getService(): VpnService = this@VpnService
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
try {
val isSuccess = super.onTransact(code, data, reply, flags)
if (!isSuccess) {
GlobalState.log("onTransact error ===>")
BroadcastAction.STOP.sendBroadcast()
}
return isSuccess
} catch (e: RemoteException) {
throw e
}
}
}
override fun onBind(intent: Intent): IBinder {
return binder
}
private fun handleStart(options: VpnOptions) {
val fd = with(Builder()) {
val cidr = IPV4_ADDRESS.toCIDR()
addAddress(cidr.address, cidr.prefixLength)
Log.d(
"addAddress", "address: ${cidr.address} prefixLength:${cidr.prefixLength}"
)
val routeAddress = options.getIpv4RouteAddress()
if (routeAddress.isNotEmpty()) {
try {
routeAddress.forEach { i ->
Log.d(
"addRoute4", "address: ${i.address} prefixLength:${i.prefixLength}"
)
addRoute(i.address, i.prefixLength)
}
} catch (_: Exception) {
addRoute(NET_ANY, 0)
}
} else {
addRoute(NET_ANY, 0)
}
if (options.ipv6) {
try {
val cidr = IPV6_ADDRESS.toCIDR()
Log.d(
"addAddress6", "address: ${cidr.address} prefixLength:${cidr.prefixLength}"
)
addAddress(cidr.address, cidr.prefixLength)
} catch (_: Exception) {
Log.d(
"addAddress6", "IPv6 is not supported."
)
}
try {
val routeAddress = options.getIpv6RouteAddress()
if (routeAddress.isNotEmpty()) {
try {
routeAddress.forEach { i ->
Log.d(
"addRoute6",
"address: ${i.address} prefixLength:${i.prefixLength}"
)
addRoute(i.address, i.prefixLength)
}
} catch (_: Exception) {
addRoute("::", 0)
}
} else {
addRoute(NET_ANY6, 0)
}
} catch (_: Exception) {
addRoute(NET_ANY6, 0)
}
}
addDnsServer(DNS)
if (options.ipv6) {
addDnsServer(DNS6)
}
setMtu(9000)
options.accessControl.let { accessControl ->
if (accessControl.enable) {
when (accessControl.mode) {
AccessControlMode.ACCEPT_SELECTED -> {
(accessControl.acceptList + packageName).forEach {
addAllowedApplication(it)
}
}
AccessControlMode.REJECT_SELECTED -> {
(accessControl.rejectList - packageName).forEach {
addDisallowedApplication(it)
}
}
}
}
}
setSession("FlClash")
setBlocking(false)
if (Build.VERSION.SDK_INT >= 29) {
setMetered(false)
}
if (options.allowBypass) {
allowBypass()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && options.systemProxy) {
setHttpProxy(
ProxyInfo.buildDirectProxy(
"127.0.0.1", options.port, options.bypassDomain
)
)
}
establish()?.detachFd()
?: throw NullPointerException("Establish VPN rejected by system")
}
Core.startTun(
fd,
protect = this::protect,
resolverProcess = this::resolverProcess,
options.address,
options.dns
)
}
override fun start() {
loader.load()
State.options?.let {
handleStart(it)
}
}
override fun stop() {
loader.cancel()
Core.stopTun()
stopSelf()
}
companion object {
private const val IPV4_ADDRESS = "172.19.0.1/30"
private const val IPV6_ADDRESS = "fdfe:dcba:9876::1/126"
private const val DNS = "172.19.0.2"
private const val DNS6 = "fdfe:dcba:9876::2"
private const val NET_ANY = "0.0.0.0"
private const val NET_ANY6 = "::"
}
}

View File

@@ -0,0 +1,11 @@
package com.follow.clash.service.models
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class NotificationParams(
val title: String = "FlClash",
val stopText: String = "STOP",
val onlyStatisticsProxy: Boolean = false,
) : Parcelable

View File

@@ -0,0 +1,19 @@
package com.follow.clash.service.models
import com.follow.clash.common.formatBytes
import com.follow.clash.core.Core
import com.google.gson.Gson
data class Traffic(
val up: Long,
val down: Long,
)
val Traffic.speedText: String
get() = "${up.formatBytes}/s↑ ${down.formatBytes}/s↓"
fun Core.getSpeedTrafficText(onlyStatisticsProxy: Boolean): String {
val res = getTraffic(onlyStatisticsProxy)
val traffic = Gson().fromJson(res, Traffic::class.java)
return traffic.speedText
}

View File

@@ -0,0 +1,82 @@
package com.follow.clash.service.models
import android.os.Parcelable
import com.follow.clash.common.AccessControlMode
import kotlinx.parcelize.Parcelize
import java.net.InetAddress
@Parcelize
data class AccessControl(
val enable: Boolean,
val mode: AccessControlMode,
val acceptList: List<String>,
val rejectList: List<String>,
) : Parcelable
@Parcelize
data class VpnOptions(
val enable: Boolean,
val port: Int,
val ipv6: Boolean,
val dnsHijacking: Boolean,
val accessControl: AccessControl,
val allowBypass: Boolean,
val systemProxy: Boolean,
val bypassDomain: List<String>,
val routeAddress: List<String>,
) : Parcelable
data class CIDR(val address: InetAddress, val prefixLength: Int)
fun VpnOptions.getIpv4RouteAddress(): List<CIDR> {
return routeAddress.filter {
it.isIpv4()
}.map {
it.toCIDR()
}
}
fun VpnOptions.getIpv6RouteAddress(): List<CIDR> {
return routeAddress.filter {
it.isIpv6()
}.map {
it.toCIDR()
}
}
fun String.isIpv4(): Boolean {
val parts = split("/")
if (parts.size != 2) {
throw IllegalArgumentException("Invalid CIDR format")
}
val address = InetAddress.getByName(parts[0])
return address.address.size == 4
}
fun String.isIpv6(): Boolean {
val parts = split("/")
if (parts.size != 2) {
throw IllegalArgumentException("Invalid CIDR format")
}
val address = InetAddress.getByName(parts[0])
return address.address.size == 16
}
fun String.toCIDR(): CIDR {
val parts = split("/")
if (parts.size != 2) {
throw IllegalArgumentException("Invalid CIDR format")
}
val ipAddress = parts[0]
val prefixLength =
parts[1].toIntOrNull() ?: throw IllegalArgumentException("Invalid prefix length")
val address = InetAddress.getByName(ipAddress)
val maxPrefix = if (address.address.size == 4) 32 else 128
if (prefixLength < 0 || prefixLength > maxPrefix) {
throw IllegalArgumentException("Invalid prefix length for IP version")
}
return CIDR(address, prefixLength)
}

View File

@@ -0,0 +1,19 @@
package com.follow.clash.service.modules
abstract class Module {
private var isInstall: Boolean = false
protected abstract fun onInstall()
protected abstract fun onUninstall()
fun install() {
isInstall = true
onInstall()
}
fun uninstall() {
onUninstall()
isInstall = false
}
}

View File

@@ -0,0 +1,51 @@
package com.follow.clash.service.modules
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
interface ModuleLoaderScope {
fun <T : Module> install(module: T): T
}
interface ModuleLoader {
fun load()
fun cancel()
}
private val mutex = Mutex()
fun CoroutineScope.moduleLoader(block: suspend ModuleLoaderScope.() -> Unit): ModuleLoader {
val modules = mutableListOf<Module>()
var job: Job? = null
return object : ModuleLoader {
override fun load() {
job = launch(Dispatchers.IO) {
mutex.withLock {
val scope = object : ModuleLoaderScope {
override fun <T : Module> install(module: T): T {
modules.add(module)
module.install()
return module
}
}
scope.block()
}
}
}
override fun cancel() {
launch(Dispatchers.IO) {
job?.cancel()
mutex.withLock {
modules.asReversed().forEach { it.uninstall() }
modules.clear()
}
}
}
}
}

View File

@@ -0,0 +1,143 @@
package com.follow.clash.service.modules
import android.app.Service
import android.net.ConnectivityManager
import android.net.LinkProperties
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.TRANSPORT_SATELLITE
import android.net.NetworkCapabilities.TRANSPORT_USB
import android.net.NetworkRequest
import android.os.Build
import androidx.core.content.getSystemService
import com.follow.clash.core.Core
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import java.util.concurrent.ConcurrentHashMap
private data class NetworkInfo(
@Volatile var losingMs: Long = 0, @Volatile var dnsList: List<InetAddress> = emptyList()
) {
fun isAvailable(): Boolean = losingMs < System.currentTimeMillis()
}
class NetworkObserveModule(private val service: Service) : Module() {
private val networkInfos = ConcurrentHashMap<Network, NetworkInfo>()
private val connectivity by lazy {
service.getSystemService<ConnectivityManager>()
}
private val request = NetworkRequest.Builder().apply {
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
addCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND)
}
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
}.build()
private val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
networkInfos[network] = NetworkInfo()
onUpdateNetwork()
super.onAvailable(network)
}
override fun onLosing(network: Network, maxMsToLive: Int) {
networkInfos[network]?.losingMs = System.currentTimeMillis() + maxMsToLive
onUpdateNetwork()
setUnderlyingNetworks(network)
super.onLosing(network, maxMsToLive)
}
override fun onLost(network: Network) {
networkInfos.remove(network)
onUpdateNetwork()
setUnderlyingNetworks(network)
super.onLost(network)
}
override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {
networkInfos[network]?.dnsList = linkProperties.dnsServers
setUnderlyingNetworks(network)
super.onLinkPropertiesChanged(network, linkProperties)
}
}
override fun onInstall() {
onUpdateNetwork()
connectivity?.registerNetworkCallback(request, callback)
}
private fun networkToInt(entry: Map.Entry<Network, NetworkInfo>): Int {
val capabilities = connectivity?.getNetworkCapabilities(entry.key)
return when {
capabilities == null -> 100
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) -> 90
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> 0
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> 1
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && capabilities.hasTransport(
TRANSPORT_USB
) -> 2
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> 3
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> 4
Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && capabilities.hasTransport(
TRANSPORT_SATELLITE
) -> 5
else -> 20
} + (if (entry.value.isAvailable()) 0 else 10)
}
fun onUpdateNetwork() {
val dnsList = (networkInfos.asSequence().minByOrNull { networkToInt(it) }?.value?.dnsList
?: emptyList()).map { x -> x.asSocketAddressText(53) }
Core.updateDNS(dnsList.joinToString { "," })
}
fun setUnderlyingNetworks(network: Network) {
// if (service is VpnService && Build.VERSION.SDK_INT in 22..28) {
// service.setUnderlyingNetworks(arrayOf(network))
// }
}
override fun onUninstall() {
connectivity?.unregisterNetworkCallback(callback)
networkInfos.clear()
onUpdateNetwork()
}
}
fun InetAddress.asSocketAddressText(port: Int): String {
return when (this) {
is Inet6Address -> "[${numericToTextFormat(this)}]:$port"
is Inet4Address -> "${this.hostAddress}:$port"
else -> throw IllegalArgumentException("Unsupported Inet type ${this.javaClass}")
}
}
private fun numericToTextFormat(address: Inet6Address): String {
val src = address.address
val sb = StringBuilder(39)
for (i in 0 until 8) {
sb.append(
Integer.toHexString(
src[i shl 1].toInt() shl 8 and 0xff00 or (src[(i shl 1) + 1].toInt() and 0xff)
)
)
if (i < 7) {
sb.append(":")
}
}
if (address.scopeId > 0) {
sb.append("%")
sb.append(address.scopeId)
}
return sb.toString()
}

View File

@@ -0,0 +1,114 @@
package com.follow.clash.service.modules
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
import android.app.Service
import android.app.Service.STOP_FOREGROUND_REMOVE
import android.content.Intent
import android.os.Build
import android.os.PowerManager
import androidx.core.app.NotificationCompat
import androidx.core.content.getSystemService
import com.follow.clash.common.Components
import com.follow.clash.common.GlobalState
import com.follow.clash.common.QuickAction
import com.follow.clash.common.quickIntent
import com.follow.clash.common.receiveBroadcastFlow
import com.follow.clash.common.startForeground
import com.follow.clash.common.tickerFlow
import com.follow.clash.common.toPendingIntent
import com.follow.clash.core.Core
import com.follow.clash.service.R
import com.follow.clash.service.State
import com.follow.clash.service.models.NotificationParams
import com.follow.clash.service.models.getSpeedTrafficText
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.zip
import kotlinx.coroutines.launch
class NotificationModule(private val service: Service) : Module() {
private val scope = CoroutineScope(Dispatchers.Default)
override fun onInstall() {
State.notificationParamsFlow.value?.let {
update(it)
}
scope.launch {
val screenFlow = service.receiveBroadcastFlow {
addAction(Intent.ACTION_SCREEN_ON)
addAction(Intent.ACTION_SCREEN_OFF)
}.map { intent ->
intent.action == Intent.ACTION_SCREEN_ON
}.onStart {
emit(isScreenOn())
}
tickerFlow(1000, 0)
.combine(State.notificationParamsFlow.zip(screenFlow) { params, screenOn ->
params to screenOn
}) { _, (params, screenOn) -> params to screenOn }
.filter { (params, screenOn) -> params != null && screenOn }
.collect { (params, _) ->
update(params!!)
}
}
}
private fun isScreenOn(): Boolean {
val pm = service.getSystemService<PowerManager>()
return when (pm != null) {
true -> pm.isInteractive
false -> true
}
}
private val notificationBuilder: NotificationCompat.Builder by lazy {
val intent = Intent().setComponent(Components.MAIN_ACTIVITY)
with(
NotificationCompat.Builder(
service, GlobalState.NOTIFICATION_CHANNEL
)
) {
setSmallIcon(R.drawable.ic)
setContentTitle("FlClash")
setContentIntent(intent.toPendingIntent)
setCategory(NotificationCompat.CATEGORY_SERVICE)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
}
setOngoing(true)
setShowWhen(false)
setOnlyAlertOnce(true)
}
}
private fun update(params: NotificationParams) {
val contentText = Core.getSpeedTrafficText(params.onlyStatisticsProxy)
service.startForeground(
with(notificationBuilder) {
setContentTitle(params.title)
setContentText(contentText)
setPriority(NotificationCompat.PRIORITY_HIGH)
clearActions()
addAction(
0,
params.stopText,
QuickAction.STOP.quickIntent.toPendingIntent
).build()
})
}
override fun onUninstall() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
service.stopForeground(STOP_FOREGROUND_REMOVE)
} else {
service.stopForeground(true)
}
scope.cancel()
}
}

View File

@@ -0,0 +1,61 @@
package com.follow.clash.service.modules
import android.app.Service
import android.content.Intent
import android.os.PowerManager
import androidx.core.content.getSystemService
import com.follow.clash.common.receiveBroadcastFlow
import com.follow.clash.core.Core
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
class SuspendModule(private val service: Service) : Module() {
private val scope = CoroutineScope(Dispatchers.Default)
private fun isScreenOn(): Boolean {
val pm = service.getSystemService<PowerManager>()
return when (pm != null) {
true -> pm.isInteractive
false -> true
}
}
val isDeviceIdleMode: Boolean
get() {
return service.getSystemService<PowerManager>()?.isDeviceIdleMode ?: true
}
private fun onUpdate(isScreenOn: Boolean) {
if (isScreenOn) {
Core.suspended(false)
return
}
Core.suspended(isDeviceIdleMode)
}
override fun onInstall() {
scope.launch {
val screenFlow = service.receiveBroadcastFlow {
addAction(Intent.ACTION_SCREEN_ON)
addAction(Intent.ACTION_SCREEN_OFF)
}.map { intent ->
intent.action == Intent.ACTION_SCREEN_ON
}.onStart {
emit(isScreenOn())
}
screenFlow.collect {
onUpdate(it)
}
}
}
override fun onUninstall() {
scope.cancel()
}
}

View File

@@ -16,14 +16,14 @@ pluginManagement {
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.2" apply false
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
id("com.android.application") version "8.12.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.10" apply false
}
include(":app")
include(":core")
include(":service")
include(":common")

View File

@@ -406,5 +406,27 @@
"import": "Import",
"importFile": "Import from file",
"importUrl": "Import from URL",
"autoSetSystemDns": "Auto set system DNS"
"autoSetSystemDns": "Auto set system DNS",
"details": "{label} details",
"creationTime": "Creation time",
"progress": "Progress",
"host": "Host",
"destination": "Destination",
"destinationGeoIP": "Destination GeoIP",
"destinationIPASN": "Destination IPASN",
"specialProxy": "Special proxy",
"specialRules": "special rules",
"remoteDestination": "Remote destination",
"networkType": "Network type",
"proxyChains": "Proxy chains",
"log": "Log",
"connection": "Connection",
"request": "Request",
"connected": "Connected",
"disconnected": "Disconnected",
"connecting": "Connecting...",
"restartCoreTip": "Are you sure you want to restart the core?",
"forceRestartCoreTip": "Are you sure you want to force restart the core?",
"dnsHijacking": "DNS hijacking",
"coreStatus": "Core status"
}

View File

@@ -407,5 +407,27 @@
"import": "インポート",
"importFile": "ファイルからインポート",
"importUrl": "URLからインポート",
"autoSetSystemDns": "オートセットシステムDNS"
"autoSetSystemDns": "オートセットシステムDNS",
"details": "{label}詳細",
"creationTime": "作成時間",
"progress": "進捗",
"host": "ホスト",
"destination": "宛先",
"destinationGeoIP": "宛先地理情報",
"destinationIPASN": "宛先IP ASN",
"specialProxy": "特殊プロキシ",
"specialRules": "特殊ルール",
"remoteDestination": "リモート宛先",
"networkType": "ネットワーク種別",
"proxyChains": "プロキシチェーン",
"log": "ログ",
"connection": "接続",
"request": "リクエスト",
"connected": "接続済み",
"disconnected": "切断済み",
"connecting": "接続中...",
"restartCoreTip": "コアを再起動してもよろしいですか?",
"forceRestartCoreTip": "コアを強制再起動してもよろしいですか?",
"dnsHijacking": "DNSハイジャッキング",
"coreStatus": "コアステータス"
}

View File

@@ -407,5 +407,27 @@
"import": "Импорт",
"importFile": "Импорт из файла",
"importUrl": "Импорт по URL",
"autoSetSystemDns": "Автоматическая настройка системного DNS"
"autoSetSystemDns": "Автоматическая настройка системного DNS",
"details": "Детали {}",
"creationTime": "Время создания",
"progress": "Прогресс",
"host": "Хост",
"destination": "Назначение",
"destinationGeoIP": "Геолокация назначения",
"destinationIPASN": "ASN назначения",
"specialProxy": "Специальный прокси",
"specialRules": "Специальные правила",
"remoteDestination": "Удалённое назначение",
"networkType": "Тип сети",
"proxyChains": "Цепочки прокси",
"log": "Журнал",
"connection": "Соединение",
"request": "Запрос",
"connected": "Подключено",
"disconnected": "Отключено",
"connecting": "Подключение...",
"restartCoreTip": "Вы уверены, что хотите перезапустить ядро?",
"forceRestartCoreTip": "Вы уверены, что хотите принудительно перезапустить ядро?",
"dnsHijacking": "DNS-перехват",
"coreStatus": "Основной статус"
}

View File

@@ -407,5 +407,27 @@
"import": "导入",
"importFile": "通过文件导入",
"importUrl": "通过URL导入",
"autoSetSystemDns": "自动设置系统DNS"
"autoSetSystemDns": "自动设置系统DNS",
"details": "{label}详情",
"creationTime": "创建时间",
"progress": "进度",
"host": "主机",
"destination": "目标地址",
"destinationGeoIP": "目标地理定位",
"destinationIPASN": "目标IP ASN",
"specialProxy": "特殊代理",
"specialRules": "特殊规则",
"remoteDestination": "远程目标",
"networkType": "网络类型",
"proxyChains": "代理链",
"log": "日志",
"connection": "连接",
"request": "请求",
"connected": "已连接",
"disconnected": "已断开",
"connecting": "连接中...",
"restartCoreTip": "您确定要重启核心吗?",
"forceRestartCoreTip": "您确定要强制重启核心吗?",
"dnsHijacking": "DNS劫持",
"coreStatus": "核心状态"
}

View File

@@ -2,6 +2,7 @@ package main
import (
"encoding/json"
"unsafe"
)
type Action struct {
@@ -11,11 +12,11 @@ type Action struct {
}
type ActionResult struct {
Id string `json:"id"`
Method Method `json:"method"`
Data interface{} `json:"data"`
Code int `json:"code"`
Port int64
Id string `json:"id"`
Method Method `json:"method"`
Data interface{} `json:"data"`
Code int `json:"code"`
callback unsafe.Pointer
}
func (result ActionResult) Json() ([]byte, error) {
@@ -45,7 +46,7 @@ func handleAction(action *Action, result ActionResult) {
result.success(handleGetIsInit())
return
case forceGcMethod:
handleForceGc()
handleForceGC()
result.success(true)
return
case shutdownMethod:
@@ -73,10 +74,12 @@ func handleAction(action *Action, result ActionResult) {
})
return
case getTrafficMethod:
result.success(handleGetTraffic())
data := action.Data.(bool)
result.success(handleGetTraffic(data))
return
case getTotalTrafficMethod:
result.success(handleGetTotalTraffic())
data := action.Data.(bool)
result.success(handleGetTotalTraffic(data))
return
case resetTrafficMethod:
handleResetTraffic()
@@ -175,10 +178,6 @@ func handleAction(action *Action, result ActionResult) {
result.success(value)
})
return
case setStateMethod:
data := action.Data.(string)
handleSetState(data)
result.success(true)
case crashMethod:
result.success(true)
handleCrash()

View File

@@ -1,77 +0,0 @@
//go:build android && cgo
package main
/*
#include <stdlib.h>
typedef void (*release_object_func)(void *obj);
typedef void (*protect_func)(void *tun_interface, int fd);
typedef const char* (*resolve_process_func)(void *tun_interface, int protocol, const char *source, const char *target, int uid);
static void protect(protect_func fn, void *tun_interface, int fd) {
if (fn) {
fn(tun_interface, fd);
}
}
static const char* resolve_process(resolve_process_func fn, void *tun_interface, int protocol, const char *source, const char *target, int uid) {
if (fn) {
return fn(tun_interface, protocol, source, target, uid);
}
return "";
}
static void release_object(release_object_func fn, void *obj) {
if (fn) {
return fn(obj);
}
}
*/
import "C"
import (
"unsafe"
)
var (
globalCallbacks struct {
releaseObjectFunc C.release_object_func
protectFunc C.protect_func
resolveProcessFunc C.resolve_process_func
}
)
func Protect(callback unsafe.Pointer, fd int) {
if globalCallbacks.protectFunc != nil {
C.protect(globalCallbacks.protectFunc, callback, C.int(fd))
}
}
func ResolveProcess(callback unsafe.Pointer, protocol int, source, target string, uid int) string {
if globalCallbacks.resolveProcessFunc == nil {
return ""
}
s := C.CString(source)
defer C.free(unsafe.Pointer(s))
t := C.CString(target)
defer C.free(unsafe.Pointer(t))
res := C.resolve_process(globalCallbacks.resolveProcessFunc, callback, C.int(protocol), s, t, C.int(uid))
defer C.free(unsafe.Pointer(res))
return C.GoString(res)
}
func releaseObject(callback unsafe.Pointer) {
if globalCallbacks.releaseObjectFunc == nil {
return
}
C.release_object(globalCallbacks.releaseObjectFunc, callback)
}
//export registerCallbacks
func registerCallbacks(markSocketFunc C.protect_func, resolveProcessFunc C.resolve_process_func, releaseObjectFunc C.release_object_func) {
globalCallbacks.protectFunc = markSocketFunc
globalCallbacks.resolveProcessFunc = resolveProcessFunc
globalCallbacks.releaseObjectFunc = releaseObjectFunc
}

25
core/bride.c Normal file
View File

@@ -0,0 +1,25 @@
#include "bride.h"
void (*release_object_func)(void *obj);
void (*protect_func)(void *tun_interface, int fd);
char* (*resolve_process_func)(void *tun_interface,int protocol, const char *source, const char *target, int uid);
void (*result_func)(void *invoke_Interface, const char *data);
void protect(void *tun_interface, int fd) {
protect_func(tun_interface, fd);
}
char* resolve_process(void *tun_interface, int protocol, const char *source, const char *target, int uid) {
return resolve_process_func(tun_interface, protocol, source, target, uid);
}
void release_object(void *obj) {
release_object_func(obj);
}
void result(void *invoke_Interface, const char *data) {
return result_func(invoke_Interface, data);
}

35
core/bride.go Normal file
View File

@@ -0,0 +1,35 @@
//go:build android && cgo
package main
//#include "bride.h"
import "C"
import "unsafe"
func protect(callback unsafe.Pointer, fd int) {
C.protect(callback, C.int(fd))
}
func resolveProcess(callback unsafe.Pointer, protocol int, source, target string, uid int) string {
s := C.CString(source)
defer C.free(unsafe.Pointer(s))
t := C.CString(target)
defer C.free(unsafe.Pointer(t))
res := C.resolve_process(callback, C.int(protocol), s, t, C.int(uid))
return parseCString(res)
}
func invokeResult(callback unsafe.Pointer, data string) {
s := C.CString(data)
defer C.free(unsafe.Pointer(s))
C.result(callback, s)
}
func releaseObject(callback unsafe.Pointer) {
C.release_object(callback)
}
func parseCString(s *C.char) string {
//defer C.free(unsafe.Pointer(s))
return C.GoString(s)
}

19
core/bride.h Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include <stdlib.h>
extern void (*release_object_func)(void *obj);
extern void (*protect_func)(void *tun_interface, int fd);
extern char* (*resolve_process_func)(void *tun_interface, int protocol, const char *source, const char *target, int uid);
extern void (*result_func)(void *invoke_Interface, const char *data);
extern void protect(void *tun_interface, int fd);
extern char* resolve_process(void *tun_interface, int protocol, const char *source, const char *target, int uid);
extern void release_object(void *obj);
extern void result(void *invoke_Interface, const char *data);

View File

@@ -98,10 +98,6 @@ const (
startListenerMethod Method = "startListener"
stopListenerMethod Method = "stopListener"
updateDnsMethod Method = "updateDns"
setStateMethod Method = "setState"
getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions"
getRunTimeMethod Method = "getRunTime"
getCurrentProfileNameMethod Method = "getCurrentProfileName"
crashMethod Method = "crash"
setupConfigMethod Method = "setupConfig"
getConfigMethod Method = "getConfig"

File diff suppressed because it is too large Load Diff

View File

@@ -1,59 +0,0 @@
/*
* Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
* for details. All rights reserved. Use of this source code is governed by a
* BSD-style license that can be found in the LICENSE file.
*/
#include "dart_api_dl.h" /* NOLINT */
#include "dart_version.h" /* NOLINT */
#include "internal/dart_api_dl_impl.h" /* NOLINT */
#include <string.h>
#define DART_API_DL_DEFINITIONS(name, R, A) name##_Type name##_DL = NULL;
DART_API_ALL_DL_SYMBOLS(DART_API_DL_DEFINITIONS)
#undef DART_API_DL_DEFINITIONS
typedef void* DartApiEntry_function;
DartApiEntry_function FindFunctionPointer(const DartApiEntry* entries,
const char* name) {
while (entries->name != NULL) {
if (strcmp(entries->name, name) == 0) return entries->function;
entries++;
}
return NULL;
}
intptr_t Dart_InitializeApiDL(void* data) {
DartApi* dart_api_data = (DartApi*)data;
if (dart_api_data->major != DART_API_DL_MAJOR_VERSION) {
// If the DartVM we're running on does not have the same version as this
// file was compiled against, refuse to initialize. The symbols are not
// compatible.
return -1;
}
// Minor versions are allowed to be different.
// If the DartVM has a higher minor version, it will provide more symbols
// than we initialize here.
// If the DartVM has a lower minor version, it will not provide all symbols.
// In that case, we leave the missing symbols un-initialized. Those symbols
// should not be used by the Dart and native code. The client is responsible
// for checking the minor version number himself based on which symbols it
// is using.
// (If we would error out on this case, recompiling native code against a
// newer SDK would break all uses on older SDKs, which is too strict.)
const DartApiEntry* dart_api_function_pointers = dart_api_data->functions;
#define DART_API_DL_INIT(name, R, A) \
name##_DL = \
(name##_Type)(FindFunctionPointer(dart_api_function_pointers, #name));
DART_API_ALL_DL_SYMBOLS(DART_API_DL_INIT)
#undef DART_API_DL_INIT
return 0;
}

View File

@@ -1,156 +0,0 @@
/*
* Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
* for details. All rights reserved. Use of this source code is governed by a
* BSD-style license that can be found in the LICENSE file.
*/
#ifndef RUNTIME_INCLUDE_DART_API_DL_H_
#define RUNTIME_INCLUDE_DART_API_DL_H_
#include "dart_api.h" /* NOLINT */
#include "dart_native_api.h" /* NOLINT */
/** \mainpage Dynamically Linked Dart API
*
* This exposes a subset of symbols from dart_api.h and dart_native_api.h
* available in every Dart embedder through dynamic linking.
*
* All symbols are postfixed with _DL to indicate that they are dynamically
* linked and to prevent conflicts with the original symbol.
*
* Link `dart_api_dl.c` file into your library and invoke
* `Dart_InitializeApiDL` with `NativeApi.initializeApiDLData`.
*/
DART_EXPORT intptr_t Dart_InitializeApiDL(void* data);
// ============================================================================
// IMPORTANT! Never update these signatures without properly updating
// DART_API_DL_MAJOR_VERSION and DART_API_DL_MINOR_VERSION.
//
// Verbatim copy of `dart_native_api.h` and `dart_api.h` symbol names and types
// to trigger compile-time errors if the symbols in those files are updated
// without updating these.
//
// Function return and argument types, and typedefs are carbon copied. Structs
// are typechecked nominally in C/C++, so they are not copied, instead a
// comment is added to their definition.
typedef int64_t Dart_Port_DL;
typedef void (*Dart_NativeMessageHandler_DL)(Dart_Port_DL dest_port_id,
Dart_CObject* message);
// dart_native_api.h symbols can be called on any thread.
#define DART_NATIVE_API_DL_SYMBOLS(F) \
/***** dart_native_api.h *****/ \
/* Dart_Port */ \
F(Dart_PostCObject, bool, (Dart_Port_DL port_id, Dart_CObject * message)) \
F(Dart_PostInteger, bool, (Dart_Port_DL port_id, int64_t message)) \
F(Dart_NewNativePort, Dart_Port_DL, \
(const char* name, Dart_NativeMessageHandler_DL handler, \
bool handle_concurrently)) \
F(Dart_CloseNativePort, bool, (Dart_Port_DL native_port_id))
// dart_api.h symbols can only be called on Dart threads.
#define DART_API_DL_SYMBOLS(F) \
/***** dart_api.h *****/ \
/* Errors */ \
F(Dart_IsError, bool, (Dart_Handle handle)) \
F(Dart_IsApiError, bool, (Dart_Handle handle)) \
F(Dart_IsUnhandledExceptionError, bool, (Dart_Handle handle)) \
F(Dart_IsCompilationError, bool, (Dart_Handle handle)) \
F(Dart_IsFatalError, bool, (Dart_Handle handle)) \
F(Dart_GetError, const char*, (Dart_Handle handle)) \
F(Dart_ErrorHasException, bool, (Dart_Handle handle)) \
F(Dart_ErrorGetException, Dart_Handle, (Dart_Handle handle)) \
F(Dart_ErrorGetStackTrace, Dart_Handle, (Dart_Handle handle)) \
F(Dart_NewApiError, Dart_Handle, (const char* error)) \
F(Dart_NewCompilationError, Dart_Handle, (const char* error)) \
F(Dart_NewUnhandledExceptionError, Dart_Handle, (Dart_Handle exception)) \
F(Dart_PropagateError, void, (Dart_Handle handle)) \
/* Dart_Handle, Dart_PersistentHandle, Dart_WeakPersistentHandle */ \
F(Dart_HandleFromPersistent, Dart_Handle, (Dart_PersistentHandle object)) \
F(Dart_HandleFromWeakPersistent, Dart_Handle, \
(Dart_WeakPersistentHandle object)) \
F(Dart_NewPersistentHandle, Dart_PersistentHandle, (Dart_Handle object)) \
F(Dart_SetPersistentHandle, void, \
(Dart_PersistentHandle obj1, Dart_Handle obj2)) \
F(Dart_DeletePersistentHandle, void, (Dart_PersistentHandle object)) \
F(Dart_NewWeakPersistentHandle, Dart_WeakPersistentHandle, \
(Dart_Handle object, void* peer, intptr_t external_allocation_size, \
Dart_HandleFinalizer callback)) \
F(Dart_DeleteWeakPersistentHandle, void, (Dart_WeakPersistentHandle object)) \
F(Dart_UpdateExternalSize, void, \
(Dart_WeakPersistentHandle object, intptr_t external_allocation_size)) \
F(Dart_NewFinalizableHandle, Dart_FinalizableHandle, \
(Dart_Handle object, void* peer, intptr_t external_allocation_size, \
Dart_HandleFinalizer callback)) \
F(Dart_DeleteFinalizableHandle, void, \
(Dart_FinalizableHandle object, Dart_Handle strong_ref_to_object)) \
F(Dart_UpdateFinalizableExternalSize, void, \
(Dart_FinalizableHandle object, Dart_Handle strong_ref_to_object, \
intptr_t external_allocation_size)) \
/* Isolates */ \
F(Dart_CurrentIsolate, Dart_Isolate, (void)) \
F(Dart_ExitIsolate, void, (void)) \
F(Dart_EnterIsolate, void, (Dart_Isolate)) \
/* Dart_Port */ \
F(Dart_Post, bool, (Dart_Port_DL port_id, Dart_Handle object)) \
F(Dart_NewSendPort, Dart_Handle, (Dart_Port_DL port_id)) \
F(Dart_SendPortGetId, Dart_Handle, \
(Dart_Handle port, Dart_Port_DL * port_id)) \
/* Scopes */ \
F(Dart_EnterScope, void, (void)) \
F(Dart_ExitScope, void, (void)) \
/* Objects */ \
F(Dart_IsNull, bool, (Dart_Handle))
#define DART_API_ALL_DL_SYMBOLS(F) \
DART_NATIVE_API_DL_SYMBOLS(F) \
DART_API_DL_SYMBOLS(F)
// IMPORTANT! Never update these signatures without properly updating
// DART_API_DL_MAJOR_VERSION and DART_API_DL_MINOR_VERSION.
//
// End of verbatim copy.
// ============================================================================
// Copy of definition of DART_EXPORT without 'used' attribute.
//
// The 'used' attribute cannot be used with DART_API_ALL_DL_SYMBOLS because
// they are not function declarations, but variable declarations with a
// function pointer type.
//
// The function pointer variables are initialized with the addresses of the
// functions in the VM. If we were to use function declarations instead, we
// would need to forward the call to the VM adding indirection.
#if defined(__CYGWIN__)
#error Tool chain and platform not supported.
#elif defined(_WIN32)
#if defined(DART_SHARED_LIB)
#define DART_EXPORT_DL DART_EXTERN_C __declspec(dllexport)
#else
#define DART_EXPORT_DL DART_EXTERN_C
#endif
#else
#if __GNUC__ >= 4
#if defined(DART_SHARED_LIB)
#define DART_EXPORT_DL DART_EXTERN_C __attribute__((visibility("default")))
#else
#define DART_EXPORT_DL DART_EXTERN_C
#endif
#else
#error Tool chain not supported.
#endif
#endif
#define DART_API_DL_DECLARATIONS(name, R, A) \
typedef R(*name##_Type) A; \
DART_EXPORT_DL name##_Type name##_DL;
DART_API_ALL_DL_SYMBOLS(DART_API_DL_DECLARATIONS)
#undef DART_API_DL_DECLARATIONS
#undef DART_EXPORT_DL
#endif /* RUNTIME_INCLUDE_DART_API_DL_H_ */ /* NOLINT */

View File

@@ -1,207 +0,0 @@
/*
* Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
* for details. All rights reserved. Use of this source code is governed by a
* BSD-style license that can be found in the LICENSE file.
*/
#ifndef RUNTIME_INCLUDE_DART_NATIVE_API_H_
#define RUNTIME_INCLUDE_DART_NATIVE_API_H_
#include "dart_api.h" /* NOLINT */
/*
* ==========================================
* Message sending/receiving from native code
* ==========================================
*/
/**
* A Dart_CObject is used for representing Dart objects as native C
* data outside the Dart heap. These objects are totally detached from
* the Dart heap. Only a subset of the Dart objects have a
* representation as a Dart_CObject.
*
* The string encoding in the 'value.as_string' is UTF-8.
*
* All the different types from dart:typed_data are exposed as type
* kTypedData. The specific type from dart:typed_data is in the type
* field of the as_typed_data structure. The length in the
* as_typed_data structure is always in bytes.
*
* The data for kTypedData is copied on message send and ownership remains with
* the caller. The ownership of data for kExternalTyped is passed to the VM on
* message send and returned when the VM invokes the
* Dart_HandleFinalizer callback; a non-NULL callback must be provided.
*
* Note that Dart_CObject_kNativePointer is intended for internal use by
* dart:io implementation and has no connection to dart:ffi Pointer class.
* It represents a pointer to a native resource of a known type.
* The receiving side will only see this pointer as an integer and will not
* see the specified finalizer.
* The specified finalizer will only be invoked if the message is not delivered.
*/
typedef enum {
Dart_CObject_kNull = 0,
Dart_CObject_kBool,
Dart_CObject_kInt32,
Dart_CObject_kInt64,
Dart_CObject_kDouble,
Dart_CObject_kString,
Dart_CObject_kArray,
Dart_CObject_kTypedData,
Dart_CObject_kExternalTypedData,
Dart_CObject_kSendPort,
Dart_CObject_kCapability,
Dart_CObject_kNativePointer,
Dart_CObject_kUnsupported,
Dart_CObject_kUnmodifiableExternalTypedData,
Dart_CObject_kNumberOfTypes
} Dart_CObject_Type;
// This enum is versioned by DART_API_DL_MAJOR_VERSION, only add at the end
// and bump the DART_API_DL_MINOR_VERSION.
typedef struct _Dart_CObject {
Dart_CObject_Type type;
union {
bool as_bool;
int32_t as_int32;
int64_t as_int64;
double as_double;
const char* as_string;
struct {
Dart_Port id;
Dart_Port origin_id;
} as_send_port;
struct {
int64_t id;
} as_capability;
struct {
intptr_t length;
struct _Dart_CObject** values;
} as_array;
struct {
Dart_TypedData_Type type;
intptr_t length; /* in elements, not bytes */
const uint8_t* values;
} as_typed_data;
struct {
Dart_TypedData_Type type;
intptr_t length; /* in elements, not bytes */
uint8_t* data;
void* peer;
Dart_HandleFinalizer callback;
} as_external_typed_data;
struct {
intptr_t ptr;
intptr_t size;
Dart_HandleFinalizer callback;
} as_native_pointer;
} value;
} Dart_CObject;
// This struct is versioned by DART_API_DL_MAJOR_VERSION, bump the version when
// changing this struct.
/**
* Posts a message on some port. The message will contain the Dart_CObject
* object graph rooted in 'message'.
*
* While the message is being sent the state of the graph of Dart_CObject
* structures rooted in 'message' should not be accessed, as the message
* generation will make temporary modifications to the data. When the message
* has been sent the graph will be fully restored.
*
* If true is returned, the message was enqueued, and finalizers for external
* typed data will eventually run, even if the receiving isolate shuts down
* before processing the message. If false is returned, the message was not
* enqueued and ownership of external typed data in the message remains with the
* caller.
*
* This function may be called on any thread when the VM is running (that is,
* after Dart_Initialize has returned and before Dart_Cleanup has been called).
*
* \param port_id The destination port.
* \param message The message to send.
*
* \return True if the message was posted.
*/
DART_EXPORT bool Dart_PostCObject(Dart_Port port_id, Dart_CObject* message);
/**
* Posts a message on some port. The message will contain the integer 'message'.
*
* \param port_id The destination port.
* \param message The message to send.
*
* \return True if the message was posted.
*/
DART_EXPORT bool Dart_PostInteger(Dart_Port port_id, int64_t message);
/**
* A native message handler.
*
* This handler is associated with a native port by calling
* Dart_NewNativePort.
*
* The message received is decoded into the message structure. The
* lifetime of the message data is controlled by the caller. All the
* data references from the message are allocated by the caller and
* will be reclaimed when returning to it.
*/
typedef void (*Dart_NativeMessageHandler)(Dart_Port dest_port_id,
Dart_CObject* message);
/**
* Creates a new native port. When messages are received on this
* native port, then they will be dispatched to the provided native
* message handler.
*
* \param name The name of this port in debugging messages.
* \param handler The C handler to run when messages arrive on the port.
* \param handle_concurrently Is it okay to process requests on this
* native port concurrently?
*
* \return If successful, returns the port id for the native port. In
* case of error, returns ILLEGAL_PORT.
*/
DART_EXPORT Dart_Port Dart_NewNativePort(const char* name,
Dart_NativeMessageHandler handler,
bool handle_concurrently);
/* TODO(turnidge): Currently handle_concurrently is ignored. */
/**
* Closes the native port with the given id.
*
* The port must have been allocated by a call to Dart_NewNativePort.
*
* \param native_port_id The id of the native port to close.
*
* \return Returns true if the port was closed successfully.
*/
DART_EXPORT bool Dart_CloseNativePort(Dart_Port native_port_id);
/*
* ==================
* Verification Tools
* ==================
*/
/**
* Forces all loaded classes and functions to be compiled eagerly in
* the current isolate..
*
* TODO(turnidge): Document.
*/
DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_CompileAll(void);
/**
* Finalizes all classes.
*/
DART_EXPORT DART_WARN_UNUSED_RESULT Dart_Handle Dart_FinalizeAllClasses(void);
/* This function is intentionally undocumented.
*
* It should not be used outside internal tests.
*/
DART_EXPORT void* Dart_ExecuteInternalCommand(const char* command, void* arg);
#endif /* INCLUDE_DART_NATIVE_API_H_ */ /* NOLINT */

View File

@@ -1,658 +0,0 @@
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
#ifndef RUNTIME_INCLUDE_DART_TOOLS_API_H_
#define RUNTIME_INCLUDE_DART_TOOLS_API_H_
#include "dart_api.h" /* NOLINT */
/** \mainpage Dart Tools Embedding API Reference
*
* This reference describes the Dart embedding API for tools. Tools include
* a debugger, service protocol, and timeline.
*
* NOTE: The APIs described in this file are unstable and subject to change.
*
* This reference is generated from the header include/dart_tools_api.h.
*/
/*
* ========
* Debugger
* ========
*/
/**
* ILLEGAL_ISOLATE_ID is a number guaranteed never to be associated with a
* valid isolate.
*/
#define ILLEGAL_ISOLATE_ID ILLEGAL_PORT
/**
* ILLEGAL_ISOLATE_GROUP_ID is a number guaranteed never to be associated with a
* valid isolate group.
*/
#define ILLEGAL_ISOLATE_GROUP_ID 0
/*
* =======
* Service
* =======
*/
/**
* A service request callback function.
*
* These callbacks, registered by the embedder, are called when the VM receives
* a service request it can't handle and the service request command name
* matches one of the embedder registered handlers.
*
* The return value of the callback indicates whether the response
* should be used as a regular result or an error result.
* Specifically, if the callback returns true, a regular JSON-RPC
* response is built in the following way:
*
* {
* "jsonrpc": "2.0",
* "result": <json_object>,
* "id": <some sequence id>,
* }
*
* If the callback returns false, a JSON-RPC error is built like this:
*
* {
* "jsonrpc": "2.0",
* "error": <json_object>,
* "id": <some sequence id>,
* }
*
* \param method The rpc method name.
* \param param_keys Service requests can have key-value pair parameters. The
* keys and values are flattened and stored in arrays.
* \param param_values The values associated with the keys.
* \param num_params The length of the param_keys and param_values arrays.
* \param user_data The user_data pointer registered with this handler.
* \param result A C string containing a valid JSON object. The returned
* pointer will be freed by the VM by calling free.
*
* \return True if the result is a regular JSON-RPC response, false if the
* result is a JSON-RPC error.
*/
typedef bool (*Dart_ServiceRequestCallback)(const char* method,
const char** param_keys,
const char** param_values,
intptr_t num_params,
void* user_data,
const char** json_object);
/**
* Register a Dart_ServiceRequestCallback to be called to handle
* requests for the named rpc on a specific isolate. The callback will
* be invoked with the current isolate set to the request target.
*
* \param method The name of the method that this callback is responsible for.
* \param callback The callback to invoke.
* \param user_data The user data passed to the callback.
*
* NOTE: If multiple callbacks with the same name are registered, only
* the last callback registered will be remembered.
*/
DART_EXPORT void Dart_RegisterIsolateServiceRequestCallback(
const char* method,
Dart_ServiceRequestCallback callback,
void* user_data);
/**
* Register a Dart_ServiceRequestCallback to be called to handle
* requests for the named rpc. The callback will be invoked without a
* current isolate.
*
* \param method The name of the command that this callback is responsible for.
* \param callback The callback to invoke.
* \param user_data The user data passed to the callback.
*
* NOTE: If multiple callbacks with the same name are registered, only
* the last callback registered will be remembered.
*/
DART_EXPORT void Dart_RegisterRootServiceRequestCallback(
const char* method,
Dart_ServiceRequestCallback callback,
void* user_data);
/**
* Embedder information which can be requested by the VM for internal or
* reporting purposes.
*
* The pointers in this structure are not going to be cached or freed by the VM.
*/
#define DART_EMBEDDER_INFORMATION_CURRENT_VERSION (0x00000001)
typedef struct {
int32_t version;
const char* name; // [optional] The name of the embedder
int64_t current_rss; // [optional] the current RSS of the embedder
int64_t max_rss; // [optional] the maximum RSS of the embedder
} Dart_EmbedderInformation;
/**
* Callback provided by the embedder that is used by the VM to request
* information.
*
* \return Returns a pointer to a Dart_EmbedderInformation structure.
* The embedder keeps the ownership of the structure and any field in it.
* The embedder must ensure that the structure will remain valid until the
* next invocation of the callback.
*/
typedef void (*Dart_EmbedderInformationCallback)(
Dart_EmbedderInformation* info);
/**
* Register a Dart_ServiceRequestCallback to be called to handle
* requests for the named rpc. The callback will be invoked without a
* current isolate.
*
* \param method The name of the command that this callback is responsible for.
* \param callback The callback to invoke.
* \param user_data The user data passed to the callback.
*
* NOTE: If multiple callbacks are registered, only the last callback registered
* will be remembered.
*/
DART_EXPORT void Dart_SetEmbedderInformationCallback(
Dart_EmbedderInformationCallback callback);
/**
* Invoke a vm-service method and wait for its result.
*
* \param request_json The utf8-encoded json-rpc request.
* \param request_json_length The length of the json-rpc request.
*
* \param response_json The returned utf8-encoded json response, must be
* free()ed by caller.
* \param response_json_length The length of the returned json response.
* \param error An optional error, must be free()ed by caller.
*
* \return Whether the call was successfully performed.
*
* NOTE: This method does not need a current isolate and must not have the
* vm-isolate being the current isolate. It must be called after
* Dart_Initialize() and before Dart_Cleanup().
*/
DART_EXPORT bool Dart_InvokeVMServiceMethod(uint8_t* request_json,
intptr_t request_json_length,
uint8_t** response_json,
intptr_t* response_json_length,
char** error);
/*
* ========
* Event Streams
* ========
*/
/**
* A callback invoked when the VM service gets a request to listen to
* some stream.
*
* \return Returns true iff the embedder supports the named stream id.
*/
typedef bool (*Dart_ServiceStreamListenCallback)(const char* stream_id);
/**
* A callback invoked when the VM service gets a request to cancel
* some stream.
*/
typedef void (*Dart_ServiceStreamCancelCallback)(const char* stream_id);
/**
* Adds VM service stream callbacks.
*
* \param listen_callback A function pointer to a listen callback function.
* A listen callback function should not be already set when this function
* is called. A NULL value removes the existing listen callback function
* if any.
*
* \param cancel_callback A function pointer to a cancel callback function.
* A cancel callback function should not be already set when this function
* is called. A NULL value removes the existing cancel callback function
* if any.
*
* \return Success if the callbacks were added. Otherwise, returns an
* error handle.
*/
DART_EXPORT char* Dart_SetServiceStreamCallbacks(
Dart_ServiceStreamListenCallback listen_callback,
Dart_ServiceStreamCancelCallback cancel_callback);
/**
* Sends a data event to clients of the VM Service.
*
* A data event is used to pass an array of bytes to subscribed VM
* Service clients. For example, in the standalone embedder, this is
* function used to provide WriteEvents on the Stdout and Stderr
* streams.
*
* If the embedder passes in a stream id for which no client is
* subscribed, then the event is ignored.
*
* \param stream_id The id of the stream on which to post the event.
*
* \param event_kind A string identifying what kind of event this is.
* For example, 'WriteEvent'.
*
* \param bytes A pointer to an array of bytes.
*
* \param bytes_length The length of the byte array.
*
* \return NULL if the arguments are well formed. Otherwise, returns an
* error string. The caller is responsible for freeing the error message.
*/
DART_EXPORT char* Dart_ServiceSendDataEvent(const char* stream_id,
const char* event_kind,
const uint8_t* bytes,
intptr_t bytes_length);
/*
* ========
* Reload support
* ========
*
* These functions are used to implement reloading in the Dart VM.
* This is an experimental feature, so embedders should be prepared
* for these functions to change.
*/
/**
* A callback which determines whether the file at some url has been
* modified since some time. If the file cannot be found, true should
* be returned.
*/
typedef bool (*Dart_FileModifiedCallback)(const char* url, int64_t since);
DART_EXPORT char* Dart_SetFileModifiedCallback(
Dart_FileModifiedCallback file_modified_callback);
/**
* Returns true if isolate is currently reloading.
*/
DART_EXPORT bool Dart_IsReloading();
/*
* ========
* Timeline
* ========
*/
/**
* Enable tracking of specified timeline category. This is operational
* only when systrace timeline functionality is turned on.
*
* \param categories A comma separated list of categories that need to
* be enabled, the categories are
* "all" : All categories
* "API" - Execution of Dart C API functions
* "Compiler" - Execution of Dart JIT compiler
* "CompilerVerbose" - More detailed Execution of Dart JIT compiler
* "Dart" - Execution of Dart code
* "Debugger" - Execution of Dart debugger
* "Embedder" - Execution of Dart embedder code
* "GC" - Execution of Dart Garbage Collector
* "Isolate" - Dart Isolate lifecycle execution
* "VM" - Execution in Dart VM runtime code
* "" - None
*
* When "all" is specified all the categories are enabled.
* When a comma separated list of categories is specified, the categories
* that are specified will be enabled and the rest will be disabled.
* When "" is specified all the categories are disabled.
* The category names are case sensitive.
* eg: Dart_EnableTimelineCategory("all");
* Dart_EnableTimelineCategory("GC,API,Isolate");
* Dart_EnableTimelineCategory("GC,Debugger,Dart");
*
* \return True if the categories were successfully enabled, False otherwise.
*/
DART_EXPORT bool Dart_SetEnabledTimelineCategory(const char* categories);
/**
* Returns a timestamp in microseconds. This timestamp is suitable for
* passing into the timeline system, and uses the same monotonic clock
* as dart:developer's Timeline.now.
*
* \return A timestamp that can be passed to the timeline system.
*/
DART_EXPORT int64_t Dart_TimelineGetMicros();
/**
* Returns a raw timestamp in from the monotonic clock.
*
* \return A raw timestamp from the monotonic clock.
*/
DART_EXPORT int64_t Dart_TimelineGetTicks();
/**
* Returns the frequency of the monotonic clock.
*
* \return The frequency of the monotonic clock.
*/
DART_EXPORT int64_t Dart_TimelineGetTicksFrequency();
typedef enum {
Dart_Timeline_Event_Begin, // Phase = 'B'.
Dart_Timeline_Event_End, // Phase = 'E'.
Dart_Timeline_Event_Instant, // Phase = 'i'.
Dart_Timeline_Event_Duration, // Phase = 'X'.
Dart_Timeline_Event_Async_Begin, // Phase = 'b'.
Dart_Timeline_Event_Async_End, // Phase = 'e'.
Dart_Timeline_Event_Async_Instant, // Phase = 'n'.
Dart_Timeline_Event_Counter, // Phase = 'C'.
Dart_Timeline_Event_Flow_Begin, // Phase = 's'.
Dart_Timeline_Event_Flow_Step, // Phase = 't'.
Dart_Timeline_Event_Flow_End, // Phase = 'f'.
} Dart_Timeline_Event_Type;
/**
* Add a timeline event to the embedder stream.
*
* DEPRECATED: this function will be removed in Dart SDK v3.2.
*
* \param label The name of the event. Its lifetime must extend at least until
* Dart_Cleanup.
* \param timestamp0 The first timestamp of the event.
* \param timestamp1_or_id When reporting an event of type
* |Dart_Timeline_Event_Duration|, the second (end) timestamp of the event
* should be passed through |timestamp1_or_id|. When reporting an event of
* type |Dart_Timeline_Event_Async_Begin|, |Dart_Timeline_Event_Async_End|,
* or |Dart_Timeline_Event_Async_Instant|, the async ID associated with the
* event should be passed through |timestamp1_or_id|. When reporting an
* event of type |Dart_Timeline_Event_Flow_Begin|,
* |Dart_Timeline_Event_Flow_Step|, or |Dart_Timeline_Event_Flow_End|, the
* flow ID associated with the event should be passed through
* |timestamp1_or_id|. When reporting an event of type
* |Dart_Timeline_Event_Begin| or |Dart_Timeline_Event_End|, the event ID
* associated with the event should be passed through |timestamp1_or_id|.
* Note that this event ID will only be used by the MacOS recorder. The
* argument to |timestamp1_or_id| will not be used when reporting events of
* other types.
* \param argument_count The number of argument names and values.
* \param argument_names An array of names of the arguments. The lifetime of the
* names must extend at least until Dart_Cleanup. The array may be reclaimed
* when this call returns.
* \param argument_values An array of values of the arguments. The values and
* the array may be reclaimed when this call returns.
*/
DART_EXPORT void Dart_TimelineEvent(const char* label,
int64_t timestamp0,
int64_t timestamp1_or_id,
Dart_Timeline_Event_Type type,
intptr_t argument_count,
const char** argument_names,
const char** argument_values);
/**
* Add a timeline event to the embedder stream.
*
* Note regarding flow events: events must be associated with flow IDs in two
* different ways to allow flow events to be serialized correctly in both
* Chrome's JSON trace event format and Perfetto's proto trace format. Events
* of type |Dart_Timeline_Event_Flow_Begin|, |Dart_Timeline_Event_Flow_Step|,
* and |Dart_Timeline_Event_Flow_End| must be reported to support serialization
* in Chrome's trace format. The |flow_ids| argument must be supplied when
* reporting events of type |Dart_Timeline_Event_Begin|,
* |Dart_Timeline_Event_Duration|, |Dart_Timeline_Event_Instant|,
* |Dart_Timeline_Event_Async_Begin|, and |Dart_Timeline_Event_Async_Instant| to
* support serialization in Perfetto's proto format.
*
* \param label The name of the event. Its lifetime must extend at least until
* Dart_Cleanup.
* \param timestamp0 The first timestamp of the event.
* \param timestamp1_or_id When reporting an event of type
* |Dart_Timeline_Event_Duration|, the second (end) timestamp of the event
* should be passed through |timestamp1_or_id|. When reporting an event of
* type |Dart_Timeline_Event_Async_Begin|, |Dart_Timeline_Event_Async_End|,
* or |Dart_Timeline_Event_Async_Instant|, the async ID associated with the
* event should be passed through |timestamp1_or_id|. When reporting an
* event of type |Dart_Timeline_Event_Flow_Begin|,
* |Dart_Timeline_Event_Flow_Step|, or |Dart_Timeline_Event_Flow_End|, the
* flow ID associated with the event should be passed through
* |timestamp1_or_id|. When reporting an event of type
* |Dart_Timeline_Event_Begin| or |Dart_Timeline_Event_End|, the event ID
* associated with the event should be passed through |timestamp1_or_id|.
* Note that this event ID will only be used by the MacOS recorder. The
* argument to |timestamp1_or_id| will not be used when reporting events of
* other types.
* \param flow_id_count The number of flow IDs associated with this event.
* \param flow_ids An array of flow IDs associated with this event. The array
* may be reclaimed when this call returns.
* \param argument_count The number of argument names and values.
* \param argument_names An array of names of the arguments. The lifetime of the
* names must extend at least until Dart_Cleanup. The array may be reclaimed
* when this call returns.
* \param argument_values An array of values of the arguments. The values and
* the array may be reclaimed when this call returns.
*/
DART_EXPORT void Dart_RecordTimelineEvent(const char* label,
int64_t timestamp0,
int64_t timestamp1_or_id,
intptr_t flow_id_count,
const int64_t* flow_ids,
Dart_Timeline_Event_Type type,
intptr_t argument_count,
const char** argument_names,
const char** argument_values);
/**
* Associates a name with the current thread. This name will be used to name
* threads in the timeline. Can only be called after a call to Dart_Initialize.
*
* \param name The name of the thread.
*/
DART_EXPORT void Dart_SetThreadName(const char* name);
typedef struct {
const char* name;
const char* value;
} Dart_TimelineRecorderEvent_Argument;
#define DART_TIMELINE_RECORDER_CURRENT_VERSION (0x00000002)
typedef struct {
/* Set to DART_TIMELINE_RECORDER_CURRENT_VERSION */
int32_t version;
/* The event's type / phase. */
Dart_Timeline_Event_Type type;
/* The event's timestamp according to the same clock as
* Dart_TimelineGetMicros. For a duration event, this is the beginning time.
*/
int64_t timestamp0;
/**
* For a duration event, this is the end time. For an async event, this is the
* async ID. For a flow event, this is the flow ID. For a begin or end event,
* this is the event ID (which is only referenced by the MacOS recorder).
*/
int64_t timestamp1_or_id;
/* The current isolate of the event, as if by Dart_GetMainPortId, or
* ILLEGAL_PORT if the event had no current isolate. */
Dart_Port isolate;
/* The current isolate group of the event, as if by
* Dart_CurrentIsolateGroupId, or ILLEGAL_PORT if the event had no current
* isolate group. */
Dart_IsolateGroupId isolate_group;
/* The callback data associated with the isolate if any. */
void* isolate_data;
/* The callback data associated with the isolate group if any. */
void* isolate_group_data;
/* The name / label of the event. */
const char* label;
/* The stream / category of the event. */
const char* stream;
intptr_t argument_count;
Dart_TimelineRecorderEvent_Argument* arguments;
} Dart_TimelineRecorderEvent;
/**
* Callback provided by the embedder to handle the completion of timeline
* events.
*
* \param event A timeline event that has just been completed. The VM keeps
* ownership of the event and any field in it (i.e., the embedder should copy
* any values it needs after the callback returns).
*/
typedef void (*Dart_TimelineRecorderCallback)(
Dart_TimelineRecorderEvent* event);
/**
* Register a `Dart_TimelineRecorderCallback` to be called as timeline events
* are completed.
*
* The callback will be invoked without a current isolate.
*
* The callback will be invoked on the thread completing the event. Because
* `Dart_TimelineEvent` may be called by any thread, the callback may be called
* on any thread.
*
* The callback may be invoked at any time after `Dart_Initialize` is called and
* before `Dart_Cleanup` returns.
*
* If multiple callbacks are registered, only the last callback registered
* will be remembered. Providing a NULL callback will clear the registration
* (i.e., a NULL callback produced a no-op instead of a crash).
*
* Setting a callback is insufficient to receive events through the callback. The
* VM flag `timeline_recorder` must also be set to `callback`.
*/
DART_EXPORT void Dart_SetTimelineRecorderCallback(
Dart_TimelineRecorderCallback callback);
/*
* =======
* Metrics
* =======
*/
/**
* Return metrics gathered for the VM and individual isolates.
*/
DART_EXPORT int64_t
Dart_IsolateGroupHeapOldUsedMetric(Dart_IsolateGroup group); // Byte
DART_EXPORT int64_t
Dart_IsolateGroupHeapOldCapacityMetric(Dart_IsolateGroup group); // Byte
DART_EXPORT int64_t
Dart_IsolateGroupHeapOldExternalMetric(Dart_IsolateGroup group); // Byte
DART_EXPORT int64_t
Dart_IsolateGroupHeapNewUsedMetric(Dart_IsolateGroup group); // Byte
DART_EXPORT int64_t
Dart_IsolateGroupHeapNewCapacityMetric(Dart_IsolateGroup group); // Byte
DART_EXPORT int64_t
Dart_IsolateGroupHeapNewExternalMetric(Dart_IsolateGroup group); // Byte
/*
* ========
* UserTags
* ========
*/
/*
* Gets the current isolate's currently set UserTag instance.
*
* \return The currently set UserTag instance.
*/
DART_EXPORT Dart_Handle Dart_GetCurrentUserTag();
/*
* Gets the current isolate's default UserTag instance.
*
* \return The default UserTag with label 'Default'
*/
DART_EXPORT Dart_Handle Dart_GetDefaultUserTag();
/*
* Creates a new UserTag instance.
*
* \param label The name of the new UserTag.
*
* \return The newly created UserTag instance or an error handle.
*/
DART_EXPORT Dart_Handle Dart_NewUserTag(const char* label);
/*
* Updates the current isolate's UserTag to a new value.
*
* \param user_tag The UserTag to be set as the current UserTag.
*
* \return The previously set UserTag instance or an error handle.
*/
DART_EXPORT Dart_Handle Dart_SetCurrentUserTag(Dart_Handle user_tag);
/*
* Returns the label of a given UserTag instance.
*
* \param user_tag The UserTag from which the label will be retrieved.
*
* \return The UserTag's label. NULL if the user_tag is invalid. The caller is
* responsible for freeing the returned label.
*/
DART_EXPORT DART_WARN_UNUSED_RESULT char* Dart_GetUserTagLabel(
Dart_Handle user_tag);
/*
* =======
* Heap Snapshot
* =======
*/
/**
* Callback provided by the caller of `Dart_WriteHeapSnapshot` which is
* used to write out chunks of the requested heap snapshot.
*
* \param context An opaque context which was passed to `Dart_WriteHeapSnapshot`
* together with this callback.
*
* \param buffer Pointer to the buffer containing a chunk of the snapshot.
* The callback owns the buffer and needs to `free` it.
*
* \param size Number of bytes in the `buffer` to be written.
*
* \param is_last Set to `true` for the last chunk. The callback will not
* be invoked again after it was invoked once with `is_last` set to `true`.
*/
typedef void (*Dart_HeapSnapshotWriteChunkCallback)(void* context,
uint8_t* buffer,
intptr_t size,
bool is_last);
/**
* Generate heap snapshot of the current isolate group and stream it into the
* given `callback`. VM would produce snapshot in chunks and send these chunks
* one by one back to the embedder by invoking the provided `callback`.
*
* This API enables embedder to stream snapshot into a file or socket without
* allocating a buffer to hold the whole snapshot in memory.
*
* The isolate group will be paused for the duration of this operation.
*
* \param write Callback used to write chunks of the heap snapshot.
*
* \param context Opaque context which would be passed on each invocation of
* `write` callback.
*
* \returns `nullptr` if the operation is successful otherwise error message.
* Caller owns error message string and needs to `free` it.
*/
DART_EXPORT char* Dart_WriteHeapSnapshot(
Dart_HeapSnapshotWriteChunkCallback write,
void* context);
#endif // RUNTIME_INCLUDE_DART_TOOLS_API_H_

View File

@@ -1,16 +0,0 @@
/*
* Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
* for details. All rights reserved. Use of this source code is governed by a
* BSD-style license that can be found in the LICENSE file.
*/
#ifndef RUNTIME_INCLUDE_DART_VERSION_H_
#define RUNTIME_INCLUDE_DART_VERSION_H_
// On breaking changes the major version is increased.
// On backwards compatible changes the minor version is increased.
// The versioning covers the symbols exposed in dart_api_dl.h
#define DART_API_DL_MAJOR_VERSION 2
#define DART_API_DL_MINOR_VERSION 3
#endif /* RUNTIME_INCLUDE_DART_VERSION_H_ */ /* NOLINT */

View File

@@ -1,21 +0,0 @@
/*
* Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
* for details. All rights reserved. Use of this source code is governed by a
* BSD-style license that can be found in the LICENSE file.
*/
#ifndef RUNTIME_INCLUDE_INTERNAL_DART_API_DL_IMPL_H_
#define RUNTIME_INCLUDE_INTERNAL_DART_API_DL_IMPL_H_
typedef struct {
const char* name;
void (*function)(void);
} DartApiEntry;
typedef struct {
const int major;
const int minor;
const DartApiEntry* const functions;
} DartApi;
#endif /* RUNTIME_INCLUDE_INTERNAL_DART_API_DL_IMPL_H_ */ /* NOLINT */

View File

@@ -1,42 +0,0 @@
//go:build cgo
package dart_bridge
/*
#include <stdlib.h>
#include "stdint.h"
#include "include/dart_api_dl.h"
#include "include/dart_api_dl.c"
#include "include/dart_native_api.h"
bool GoDart_PostCObject(Dart_Port_DL port, Dart_CObject* obj) {
return Dart_PostCObject_DL(port, obj);
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func InitDartApi(api unsafe.Pointer) {
if C.Dart_InitializeApiDL(api) != 0 {
panic("failed to create dart bridge")
} else {
fmt.Println("Dart Api DL is initialized")
}
}
func SendToPort(port int64, msg string) bool {
var obj C.Dart_CObject
obj._type = C.Dart_CObject_kString
msgString := C.CString(msg)
defer C.free(unsafe.Pointer(msgString))
ptr := unsafe.Pointer(&obj.value[0])
*(**C.char)(ptr) = msgString
isSuccess := C.GoDart_PostCObject(C.Dart_Port_DL(port), &obj)
if !isSuccess {
return false
}
return true
}

View File

@@ -1,7 +0,0 @@
//go:build !cgo
package dart_bridge
func SendToPort(port int64, msg string) bool {
return false
}

View File

@@ -17,18 +17,17 @@ require (
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/ebitengine/purego v0.8.3 // indirect
github.com/enfein/mieru/v3 v3.13.0 // indirect
github.com/enfein/mieru/v3 v3.16.1 // indirect
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect
github.com/go-chi/chi/v5 v5.2.1 // indirect
github.com/go-chi/chi/v5 v5.2.2 // indirect
github.com/go-chi/render v1.0.3 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
@@ -51,26 +50,26 @@ require (
github.com/mdlayher/socket v0.4.1 // indirect
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
github.com/metacubex/bart v0.20.5 // indirect
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
github.com/metacubex/chacha v0.1.2 // indirect
github.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b // indirect
github.com/metacubex/chacha v0.1.5 // indirect
github.com/metacubex/fswatch v0.1.1 // indirect
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 // indirect
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639 // indirect
github.com/metacubex/quic-go v0.53.1-0.20250628094454-fda5262d1d9c // indirect
github.com/metacubex/randv2 v0.2.0 // indirect
github.com/metacubex/sing v0.5.3 // indirect
github.com/metacubex/sing v0.5.4 // indirect
github.com/metacubex/sing-mux v0.3.2 // indirect
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f // indirect
github.com/metacubex/sing-shadowsocks v0.2.10 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.4 // indirect
github.com/metacubex/sing-quic v0.0.0-20250718154553-1b193bec4cbb // indirect
github.com/metacubex/sing-shadowsocks v0.2.11 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.5 // indirect
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 // indirect
github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c // indirect
github.com/metacubex/sing-vmess v0.2.2 // indirect
github.com/metacubex/sing-tun v0.4.7-0.20250721020617-8e7c37ed3d97 // indirect
github.com/metacubex/sing-vmess v0.2.3 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f // indirect
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee // indirect
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 // indirect
github.com/metacubex/utls v1.7.3 // indirect
github.com/metacubex/utls v1.8.0 // indirect
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
github.com/miekg/dns v1.1.63 // indirect
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
@@ -80,11 +79,10 @@ require (
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/sagernet/cors v1.2.1 // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/samber/lo v1.50.0 // indirect
github.com/samber/lo v1.51.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect

View File

@@ -17,8 +17,6 @@ github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx2
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=
github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -28,8 +26,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/enfein/mieru/v3 v3.16.1 h1:CfIt1pQCCQbohkw+HBD2o8V9tnhZvB5yuXGGQIXTLOs=
github.com/enfein/mieru/v3 v3.16.1/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
@@ -43,8 +41,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
@@ -84,7 +82,6 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
@@ -99,10 +96,10 @@ github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
github.com/metacubex/bart v0.20.5 h1:XkgLZ17QxfxkqKdGsojoM2Zu01mmHyyQSFzt2/calTM=
github.com/metacubex/bart v0.20.5/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
github.com/metacubex/chacha v0.1.2 h1:QulCq3eVm3TO6+4nVIWJtmSe7BT2GMrgVHuAoqRQnlc=
github.com/metacubex/chacha v0.1.2/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b h1:j7dadXD8I2KTmMt8jg1JcaP1ANL3JEObJPdANKcSYPY=
github.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b/go.mod h1:+WmP0VJZDkDszvpa83HzfUp6QzARl/IKkMorH4+nODw=
github.com/metacubex/chacha v0.1.5 h1:fKWMb/5c7ZrY8Uoqi79PPFxl+qwR7X/q0OrsAubyX2M=
github.com/metacubex/chacha v0.1.5/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQuxhU=
github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
@@ -111,42 +108,41 @@ github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo=
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639 h1:L+1brQNzBhCCxWlicwfK1TlceemCRmrDE4HmcVHc29w=
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639/go.mod h1:Kc6h++Q/zf3AxcUCevJhJwgrskJumv+pZdR8g/E/10k=
github.com/metacubex/quic-go v0.53.1-0.20250628094454-fda5262d1d9c h1:ABQzmOaZddM3q0OYeoZEc0XF+KW+dUdPNvY/c5rsunI=
github.com/metacubex/quic-go v0.53.1-0.20250628094454-fda5262d1d9c/go.mod h1:eWlAK3zsKI0P8UhYpXlIsl3mtW4D6MpMNuYLIu8CKWI=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing v0.5.3 h1:QWdN16WFKMk06x4nzkc8SvZ7y2x+TLQrpkPoHs+WSVM=
github.com/metacubex/sing v0.5.3/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing v0.5.4 h1:a4kAOZmF+OXosbzPEcrSc5QD35/ex+MNuZsrcuWskHk=
github.com/metacubex/sing v0.5.4/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7ncw=
github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw=
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f h1:mP3vIm+9hRFI0C0Vl3pE0NESF/L85FDbuB0tGgUii6I=
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f/go.mod h1:JPTpf7fpnojsSuwRJExhSZSy63pVbp3VM39+zj+sAJM=
github.com/metacubex/sing-shadowsocks v0.2.10 h1:Pr7LDbjMANIQHl07zWgl1vDuhpsfDQUpZ8cX6DPabfg=
github.com/metacubex/sing-shadowsocks v0.2.10/go.mod h1:MtRM0ZZjR0kaDOzy9zWSt6/4/UlrnsNBq+1FNAF4vBk=
github.com/metacubex/sing-shadowsocks2 v0.2.4 h1:Ec0x3hHR7xkld5Z09IGh16wtUUpBb2HgqZ9DExd8Q7s=
github.com/metacubex/sing-shadowsocks2 v0.2.4/go.mod h1:WP8+S0kqtnSbX1vlIpo5i8Irm/ijZITEPBcZ26B5unY=
github.com/metacubex/sing-quic v0.0.0-20250718154553-1b193bec4cbb h1:U/m3h8lp/j7i8zFgfvScLdZa1/Y8dd74oO7iZaQq80s=
github.com/metacubex/sing-quic v0.0.0-20250718154553-1b193bec4cbb/go.mod h1:B60FxaPHjR1SeQB0IiLrgwgvKsaoASfOWdiqhLjmMGA=
github.com/metacubex/sing-shadowsocks v0.2.11 h1:p2NGNOdF95e6XvdDKipLj1FRRqR8dnbfC/7pw2CCTlw=
github.com/metacubex/sing-shadowsocks v0.2.11/go.mod h1:bT1PCTV316zFnlToRMk5zt9HmIQYRBveiT71mplYPfc=
github.com/metacubex/sing-shadowsocks2 v0.2.5 h1:MnPn0hbcDkSJt6TlpI15XImHKK6IqaOwBUGPKyMnJnE=
github.com/metacubex/sing-shadowsocks2 v0.2.5/go.mod h1:Zyh+rAQRyevYfG/COCvDs1c/YMhGqCuknn7QrGmoQIw=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c h1:Y6jk7AH5BEg9Dsvczrf/KokYsvxeKSZZlCLHg+hC4ro=
github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c/go.mod h1:HDaHDL6onAX2ZGbAGUXKp++PohRdNb7Nzt6zxzhox+U=
github.com/metacubex/sing-vmess v0.2.2 h1:nG6GIKF1UOGmlzs+BIetdGHkFZ20YqFVIYp5Htqzp+4=
github.com/metacubex/sing-vmess v0.2.2/go.mod h1:CVDNcdSLVYFgTHQlubr88d8CdqupAUDqLjROos+H9xk=
github.com/metacubex/sing-tun v0.4.7-0.20250721020617-8e7c37ed3d97 h1:YYpc60UZE2G0pUeHbRw9erDrUDZrPQy8QzWFqA3kHsk=
github.com/metacubex/sing-tun v0.4.7-0.20250721020617-8e7c37ed3d97/go.mod h1:2YywXPWW8Z97kTH7RffOeykKzU+l0aiKlglWV1PAS64=
github.com/metacubex/sing-vmess v0.2.3 h1:QKLdIk5A2FcR3Y7m2/JO1XhfzgDA8tF4W9/ffsH9opo=
github.com/metacubex/sing-vmess v0.2.3/go.mod h1:21R5R1u90uUvBQF0owoooEu96/SAYYD56nDrwm6nFaM=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f/go.mod h1:jpAkVLPnCpGSfNyVmj6Cq4YbuZsFepm/Dc+9BAOcR80=
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee h1:lp6hJ+4wCLZu113awp7P6odM2okB5s60HUyF0FMqKmo=
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE=
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 h1:j1VRTiC9JLR4nUbSikx9OGdu/3AgFDqgcLj4GoqyQkc=
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.7.3 h1:yDcMEWojFh+t8rU9X0HPcZDPAoFze/rIIyssqivzj8A=
github.com/metacubex/utls v1.7.3/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU=
github.com/metacubex/utls v1.8.0 h1:mSYi6FMnmc5riARl5UZDmWVy710z+P5b7xuGW0lV9ac=
github.com/metacubex/utls v1.8.0/go.mod h1:FdjYzVfCtgtna19hX0ER1Xsa5uJInwdQ4IcaaI98lEQ=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
@@ -166,16 +162,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
@@ -272,8 +266,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -2,7 +2,6 @@ package main
import (
"context"
"core/state"
"encoding/json"
"fmt"
"github.com/metacubex/mihomo/adapter"
@@ -68,7 +67,7 @@ func handleGetIsInit() bool {
return isInit
}
func handleForceGc() {
func handleForceGC() {
go func() {
log.Infoln("[APP] request force GC")
runtime.GC()
@@ -136,8 +135,8 @@ func handleChangeProxy(data string, fn func(string string)) {
}()
}
func handleGetTraffic() string {
up, down := statistic.DefaultManager.Current(state.CurrentState.OnlyStatisticsProxy)
func handleGetTraffic(onlyStatisticsProxy bool) string {
up, down := statistic.DefaultManager.Current(onlyStatisticsProxy)
traffic := map[string]int64{
"up": up,
"down": down,
@@ -150,8 +149,8 @@ func handleGetTraffic() string {
return string(data)
}
func handleGetTotalTraffic() string {
up, down := statistic.DefaultManager.Total(state.CurrentState.OnlyStatisticsProxy)
func handleGetTotalTraffic(onlyStatisticsProxy bool) string {
up, down := statistic.DefaultManager.Total(onlyStatisticsProxy)
traffic := map[string]int64{
"up": up,
"down": down,
@@ -374,6 +373,15 @@ func handleSideLoadExternalProvider(providerName string, data []byte, fn func(va
}()
}
func handleSuspend(suspended bool) bool {
if suspended {
tunnel.OnSuspend()
} else {
tunnel.OnRunning()
}
return true
}
func handleStartLog() {
if logSubscriber != nil {
log.UnSubscribe(logSubscriber)
@@ -420,10 +428,6 @@ func handleGetMemory(fn func(value string)) {
}()
}
func handleSetState(params string) {
_ = json.Unmarshal([]byte(params), state.CurrentState)
}
func handleGetConfig(path string) (*config.RawConfig, error) {
bytes, err := readFile(path)
if err != nil {

View File

@@ -6,37 +6,155 @@ package main
#include <stdlib.h>
*/
import "C"
import (
bridge "core/dart-bridge"
"context"
"core/platform"
t "core/tun"
"encoding/json"
"errors"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/process"
"github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/dns"
"github.com/metacubex/mihomo/listener/sing_tun"
"github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore"
"net"
"strings"
"sync"
"syscall"
"unsafe"
)
var messagePort int64 = -1
var messageCallback unsafe.Pointer
//export initNativeApiBridge
func initNativeApiBridge(api unsafe.Pointer) {
bridge.InitDartApi(api)
type TunHandler struct {
listener *sing_tun.Listener
callback unsafe.Pointer
limit *semaphore.Weighted
}
//export attachMessagePort
func attachMessagePort(mPort C.longlong) {
messagePort = int64(mPort)
func (th *TunHandler) start(fd int, address, dns string) {
_ = th.limit.Acquire(context.TODO(), 4)
defer th.limit.Release(4)
th.initHook()
tunListener := t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack, address, dns)
if tunListener != nil {
log.Infoln("TUN address: %v", tunListener.Address())
th.listener = tunListener
return
}
th.clear()
}
//export getTraffic
func getTraffic() *C.char {
return C.CString(handleGetTraffic())
func (th *TunHandler) close() {
_ = th.limit.Acquire(context.TODO(), 4)
defer th.limit.Release(4)
th.clear()
}
//export getTotalTraffic
func getTotalTraffic() *C.char {
return C.CString(handleGetTotalTraffic())
func (th *TunHandler) clear() {
th.removeHook()
if th.listener != nil {
_ = th.listener.Close()
}
if th.callback != nil {
releaseObject(th.callback)
}
th.callback = nil
th.listener = nil
}
//export freeCString
func freeCString(s *C.char) {
C.free(unsafe.Pointer(s))
func (th *TunHandler) handleProtect(fd int) {
_ = th.limit.Acquire(context.Background(), 1)
defer th.limit.Release(1)
if th.listener == nil {
return
}
protect(th.callback, fd)
}
func (th *TunHandler) handleResolveProcess(source, target net.Addr) string {
_ = th.limit.Acquire(context.Background(), 1)
defer th.limit.Release(1)
if th.listener == nil {
return ""
}
var protocol int
uid := -1
switch source.Network() {
case "udp", "udp4", "udp6":
protocol = syscall.IPPROTO_UDP
case "tcp", "tcp4", "tcp6":
protocol = syscall.IPPROTO_TCP
}
if version < 29 {
uid = platform.QuerySocketUidFromProcFs(source, target)
}
return resolveProcess(th.callback, protocol, source.String(), target.String(), uid)
}
func (th *TunHandler) initHook() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() {
return errBlocked
}
return conn.Control(func(fd uintptr) {
tunHandler.handleProtect(int(fd))
})
}
process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) {
src, dst := metadata.RawSrcAddr, metadata.RawDstAddr
if src == nil || dst == nil {
return "", process.ErrInvalidNetwork
}
return tunHandler.handleResolveProcess(src, dst), nil
}
}
func (th *TunHandler) removeHook() {
dialer.DefaultSocketHook = nil
process.DefaultPackageNameResolver = nil
}
var (
tunLock sync.Mutex
errBlocked = errors.New("blocked")
tunHandler *TunHandler
)
func handleStopTun() {
tunLock.Lock()
defer tunLock.Unlock()
if tunHandler != nil {
tunHandler.close()
}
}
func handleStartTun(callback unsafe.Pointer, fd int, address, dns string) {
handleStopTun()
tunLock.Lock()
defer tunLock.Unlock()
if fd != 0 {
tunHandler = &TunHandler{
callback: callback,
limit: semaphore.NewWeighted(4),
}
tunHandler.start(fd, address, dns)
}
}
func handleUpdateDns(value string) {
go func() {
log.Infoln("[DNS] updateDns %s", value)
dns.UpdateSystemDNS(strings.Split(value, ","))
dns.FlushCacheWithDefaultResolver()
}()
}
func (result ActionResult) send() {
@@ -44,59 +162,92 @@ func (result ActionResult) send() {
if err != nil {
return
}
bridge.SendToPort(result.Port, string(data))
invokeResult(result.callback, string(data))
if result.Method != messageMethod {
releaseObject(result.callback)
}
}
func nextHandle(action *Action, result ActionResult) bool {
switch action.Method {
case updateDnsMethod:
data := action.Data.(string)
handleUpdateDns(data)
result.success(true)
return true
}
return false
}
//export invokeAction
func invokeAction(paramsChar *C.char, port C.longlong) {
params := C.GoString(paramsChar)
i := int64(port)
func invokeAction(callback unsafe.Pointer, paramsChar *C.char) {
params := parseCString(paramsChar)
var action = &Action{}
err := json.Unmarshal([]byte(params), action)
if err != nil {
bridge.SendToPort(i, err.Error())
invokeResult(callback, err.Error())
return
}
result := ActionResult{
Id: action.Id,
Method: action.Method,
Port: i,
Id: action.Id,
Method: action.Method,
callback: callback,
}
go handleAction(action, result)
}
//export startTUN
func startTUN(callback unsafe.Pointer, fd C.int, addressChar, dnsChar *C.char) bool {
handleStartTun(callback, int(fd), parseCString(addressChar), parseCString(dnsChar))
return true
}
//export setMessageCallback
func setMessageCallback(callback unsafe.Pointer) {
if messageCallback != nil {
releaseObject(messageCallback)
}
messageCallback = callback
}
//export getTotalTraffic
func getTotalTraffic(onlyStatisticsProxy bool) *C.char {
return C.CString(handleGetTotalTraffic(onlyStatisticsProxy))
}
//export getTraffic
func getTraffic(onlyStatisticsProxy bool) *C.char {
return C.CString(handleGetTraffic(onlyStatisticsProxy))
}
func sendMessage(message Message) {
if messagePort == -1 {
if messageCallback == nil {
return
}
result := ActionResult{
Method: messageMethod,
Port: messagePort,
Data: message,
Method: messageMethod,
callback: messageCallback,
Data: message,
}
result.send()
}
//export getConfig
func getConfig(s *C.char) *C.char {
path := C.GoString(s)
config, err := handleGetConfig(path)
if err != nil {
return C.CString("")
}
marshal, err := json.Marshal(config)
if err != nil {
return C.CString("")
}
return C.CString(string(marshal))
//export stopTun
func stopTun() {
handleStopTun()
}
//export startListener
func startListener() {
handleStartListener()
//export suspend
func suspend(suspended bool) {
handleSuspend(suspended)
}
//export stopListener
func stopListener() {
handleStopListener()
//export forceGC
func forceGC() {
handleForceGC()
}
//export updateDns
func updateDns(s *C.char) {
handleUpdateDns(parseCString(s))
}

View File

@@ -1,267 +0,0 @@
//go:build android && cgo
package main
import "C"
import (
"context"
bridge "core/dart-bridge"
"core/platform"
"core/state"
t "core/tun"
"encoding/json"
"errors"
"fmt"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/process"
"github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/dns"
"github.com/metacubex/mihomo/listener/sing_tun"
"github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore"
"net"
"strconv"
"strings"
"sync"
"syscall"
"time"
"unsafe"
)
type TunHandler struct {
listener *sing_tun.Listener
callback unsafe.Pointer
limit *semaphore.Weighted
}
func (t *TunHandler) close() {
_ = t.limit.Acquire(context.TODO(), 4)
defer t.limit.Release(4)
removeTunHook()
if t.listener != nil {
_ = t.listener.Close()
}
if t.callback != nil {
releaseObject(t.callback)
}
t.callback = nil
t.listener = nil
}
func (t *TunHandler) handleProtect(fd int) {
_ = t.limit.Acquire(context.Background(), 1)
defer t.limit.Release(1)
if t.listener == nil {
return
}
Protect(t.callback, fd)
}
func (t *TunHandler) handleResolveProcess(source, target net.Addr) string {
_ = t.limit.Acquire(context.Background(), 1)
defer t.limit.Release(1)
if t.listener == nil {
return ""
}
var protocol int
uid := -1
switch source.Network() {
case "udp", "udp4", "udp6":
protocol = syscall.IPPROTO_UDP
case "tcp", "tcp4", "tcp6":
protocol = syscall.IPPROTO_TCP
}
if version < 29 {
uid = platform.QuerySocketUidFromProcFs(source, target)
}
return ResolveProcess(t.callback, protocol, source.String(), target.String(), uid)
}
var (
tunLock sync.Mutex
runTime *time.Time
errBlocked = errors.New("blocked")
tunHandler *TunHandler
)
func handleStopTun() {
tunLock.Lock()
defer tunLock.Unlock()
runTime = nil
if tunHandler != nil {
tunHandler.close()
}
}
func handleStartTun(fd int, callback unsafe.Pointer) {
handleStopTun()
tunLock.Lock()
defer tunLock.Unlock()
now := time.Now()
runTime = &now
if fd != 0 {
tunHandler = &TunHandler{
callback: callback,
limit: semaphore.NewWeighted(4),
}
initTunHook()
tunListener, _ := t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
if tunListener != nil {
log.Infoln("TUN address: %v", tunListener.Address())
tunHandler.listener = tunListener
} else {
removeTunHook()
}
}
}
func handleGetRunTime() string {
if runTime == nil {
return ""
}
return strconv.FormatInt(runTime.UnixMilli(), 10)
}
func initTunHook() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() {
return errBlocked
}
return conn.Control(func(fd uintptr) {
tunHandler.handleProtect(int(fd))
})
}
process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) {
src, dst := metadata.RawSrcAddr, metadata.RawDstAddr
if src == nil || dst == nil {
return "", process.ErrInvalidNetwork
}
return tunHandler.handleResolveProcess(src, dst), nil
}
}
func removeTunHook() {
dialer.DefaultSocketHook = nil
process.DefaultPackageNameResolver = nil
}
func handleGetAndroidVpnOptions() string {
tunLock.Lock()
defer tunLock.Unlock()
options := state.AndroidVpnOptions{
Enable: state.CurrentState.VpnProps.Enable,
Port: currentConfig.General.MixedPort,
Ipv4Address: state.DefaultIpv4Address,
Ipv6Address: state.GetIpv6Address(),
AccessControl: state.CurrentState.VpnProps.AccessControl,
SystemProxy: state.CurrentState.VpnProps.SystemProxy,
AllowBypass: state.CurrentState.VpnProps.AllowBypass,
RouteAddress: currentConfig.General.Tun.RouteAddress,
BypassDomain: state.CurrentState.BypassDomain,
DnsServerAddress: state.GetDnsServerAddress(),
}
data, err := json.Marshal(options)
if err != nil {
fmt.Println("Error:", err)
return ""
}
return string(data)
}
func handleUpdateDns(value string) {
go func() {
log.Infoln("[DNS] updateDns %s", value)
dns.UpdateSystemDNS(strings.Split(value, ","))
dns.FlushCacheWithDefaultResolver()
}()
}
func handleGetCurrentProfileName() string {
if state.CurrentState == nil {
return ""
}
return state.CurrentState.CurrentProfileName
}
func nextHandle(action *Action, result ActionResult) bool {
switch action.Method {
case getAndroidVpnOptionsMethod:
result.success(handleGetAndroidVpnOptions())
return true
case updateDnsMethod:
data := action.Data.(string)
handleUpdateDns(data)
result.success(true)
return true
case getRunTimeMethod:
result.success(handleGetRunTime())
return true
case getCurrentProfileNameMethod:
result.success(handleGetCurrentProfileName())
return true
}
return false
}
//export quickStart
func quickStart(initParamsChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) {
i := int64(port)
paramsString := C.GoString(initParamsChar)
bytes := []byte(C.GoString(paramsChar))
stateParams := C.GoString(stateParamsChar)
go func() {
res := handleInitClash(paramsString)
if res == false {
bridge.SendToPort(i, "init error")
}
handleSetState(stateParams)
bridge.SendToPort(i, handleSetupConfig(bytes))
}()
}
//export startTUN
func startTUN(fd C.int, callback unsafe.Pointer) bool {
go func() {
handleStartTun(int(fd), callback)
}()
return true
}
//export getRunTime
func getRunTime() *C.char {
return C.CString(handleGetRunTime())
}
//export stopTun
func stopTun() {
go func() {
handleStopTun()
}()
}
//export getCurrentProfileName
func getCurrentProfileName() *C.char {
return C.CString(handleGetCurrentProfileName())
}
//export getAndroidVpnOptions
func getAndroidVpnOptions() *C.char {
return C.CString(handleGetAndroidVpnOptions())
}
//export setState
func setState(s *C.char) {
paramsString := C.GoString(s)
handleSetState(paramsString)
}
//export updateDns
func updateDns(s *C.char) {
dnsList := C.GoString(s)
handleUpdateDns(dnsList)
}

Some files were not shown because too many files have changed in this diff Show More