From c6407984acc129a44b98e518489c092e283426e1 Mon Sep 17 00:00:00 2001 From: chen08209 Date: Sat, 13 Jul 2024 16:36:08 +0800 Subject: [PATCH] Separate android ui and vpn --- .../kotlin/com/follow/clash/GlobalState.kt | 48 +++- .../kotlin/com/follow/clash/MainActivity.kt | 1 - .../com/follow/clash/plugins/AppPlugin.kt | 28 +- .../com/follow/clash/plugins/ProxyPlugin.kt | 154 +++++------ .../com/follow/clash/plugins/TilePlugin.kt | 1 + .../clash/services/FlClashTileService.kt | 42 +-- .../clash/services/FlClashVpnService.kt | 13 +- core/common.go | 15 -- core/dart-bridge/lib.go | 5 +- core/dart-bridge/message.go | 39 --- core/hub.go | 32 ++- core/log.go | 7 +- core/message.go | 77 ++++++ core/process.go | 5 +- core/tun.go | 41 ++- core/tun/tun.go | 22 -- lib/clash/core.dart | 40 ++- lib/clash/generated/clash_ffi.dart | 65 ++++- lib/clash/message.dart | 51 +--- lib/common/android.dart | 6 +- lib/common/proxy.dart | 14 - lib/controller.dart | 11 +- lib/enum/enum.dart | 14 +- lib/main.dart | 116 +++++---- lib/models/ffi.dart | 47 +++- lib/models/generated/ffi.freezed.dart | 243 +++++++++++++++--- lib/models/generated/ffi.g.dart | 45 +++- lib/plugins/app.dart | 48 ++-- lib/plugins/proxy.dart | 92 ++++--- lib/state.dart | 27 +- lib/widgets/android_container.dart | 5 +- lib/widgets/clash_message_container.dart | 33 +-- pubspec.yaml | 2 +- 33 files changed, 864 insertions(+), 525 deletions(-) delete mode 100644 core/dart-bridge/message.go create mode 100644 core/message.go diff --git a/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt b/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt index 9ad6611..9592019 100644 --- a/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt +++ b/android/app/src/main/kotlin/com/follow/clash/GlobalState.kt @@ -1,9 +1,14 @@ package com.follow.clash +import android.app.Activity +import android.content.Context import androidx.lifecycle.MutableLiveData import com.follow.clash.plugins.AppPlugin +import com.follow.clash.plugins.ProxyPlugin import com.follow.clash.plugins.TilePlugin +import io.flutter.FlutterInjector import io.flutter.embedding.engine.FlutterEngine +import io.flutter.embedding.engine.dart.DartExecutor import java.util.Date enum class RunState { @@ -12,16 +17,41 @@ enum class RunState { STOP } -class GlobalState { - companion object { - val runState: MutableLiveData = MutableLiveData(RunState.STOP) - var runTime: Date? = null - var flutterEngine: FlutterEngine? = null - fun getCurrentTilePlugin(): TilePlugin? = - flutterEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin? +object GlobalState { + val runState: MutableLiveData = MutableLiveData(RunState.STOP) + var flutterEngine: FlutterEngine? = null + private var serviceEngine: FlutterEngine? = null - fun getCurrentAppPlugin(): AppPlugin? = - flutterEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin? + fun getCurrentAppPlugin(): AppPlugin? { + val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine + return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin? + } + + fun getCurrentTitlePlugin(): TilePlugin? { + val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine + return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin? + } + + fun destroyServiceEngine() { + serviceEngine?.destroy() + serviceEngine = null + } + + fun initServiceEngine(context: Context) { + if (serviceEngine != null) return + val serviceEngine = FlutterEngine(context) + serviceEngine.plugins.add(ProxyPlugin()) + serviceEngine.plugins.add(AppPlugin()) + serviceEngine.plugins.add(TilePlugin()) + val vpnService = DartExecutor.DartEntrypoint( + FlutterInjector.instance().flutterLoader().findAppBundlePath(), + "vpnService" + ) + serviceEngine.dartExecutor.executeDartEntrypoint( + vpnService, + listOf("${flutterEngine == null}") + ) + GlobalState.serviceEngine = serviceEngine } } diff --git a/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt b/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt index c3a0fd7..bd50766 100644 --- a/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt +++ b/android/app/src/main/kotlin/com/follow/clash/MainActivity.kt @@ -10,7 +10,6 @@ import io.flutter.embedding.engine.FlutterEngine class MainActivity : FlutterActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { - GlobalState.flutterEngine?.destroy() super.configureFlutterEngine(flutterEngine) flutterEngine.plugins.add(AppPlugin()) flutterEngine.plugins.add(ProxyPlugin()) diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt index c7507cc..5c1d1aa 100644 --- a/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt +++ b/android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt @@ -11,6 +11,7 @@ import android.os.Build import android.widget.Toast import androidx.core.content.ContextCompat.getSystemService import androidx.core.content.getSystemService +import com.follow.clash.GlobalState import com.follow.clash.extensions.getBase64 import com.follow.clash.extensions.getProtocol import com.follow.clash.models.Package @@ -21,6 +22,7 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.Result import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel @@ -56,14 +58,16 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware } private fun tip(message: String?) { - if (toast != null) { - toast!!.cancel() + if(GlobalState.flutterEngine == null){ + if (toast != null) { + toast!!.cancel() + } + toast = Toast.makeText(context, message, Toast.LENGTH_SHORT) + toast!!.show() } - toast = Toast.makeText(context, message, Toast.LENGTH_SHORT) - toast!!.show() } - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + override fun onMethodCall(call: MethodCall, result: Result) { when (call.method) { "moveTaskToBack" -> { activity?.moveTaskToBack(true) @@ -163,13 +167,17 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware } } - private fun updateExcludeFromRecents(value: Boolean?) { if (context == null) return val am = getSystemService(context!!, ActivityManager::class.java) - val task = am?.appTasks?.firstOrNull { task -> - task.taskInfo.baseIntent.component?.packageName == context!!.packageName + val task = am?.appTasks?.firstOrNull { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + it.taskInfo.taskId == activity?.taskId + } else { + it.taskInfo.id == activity?.taskId + } } + when (value) { true -> task?.setExcludeFromRecents(value) false -> task?.setExcludeFromRecents(value) @@ -220,7 +228,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware } override fun onDetachedFromActivityForConfigChanges() { - activity = null; + activity = null } override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { @@ -230,6 +238,6 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware override fun onDetachedFromActivity() { channel.invokeMethod("exit", null) scope.cancel() - activity = null; + activity = null } } diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/ProxyPlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/ProxyPlugin.kt index 30fcd5f..b630173 100644 --- a/android/app/src/main/kotlin/com/follow/clash/plugins/ProxyPlugin.kt +++ b/android/app/src/main/kotlin/com/follow/clash/plugins/ProxyPlugin.kt @@ -1,7 +1,6 @@ package com.follow.clash.plugins import android.Manifest -import android.annotation.SuppressLint import android.app.Activity import android.content.ComponentName import android.content.Context @@ -9,8 +8,7 @@ import android.content.Intent import android.content.ServiceConnection import android.content.pm.PackageManager import android.net.VpnService -import android.os.Build.VERSION -import android.os.Build.VERSION_CODES +import android.os.Build import android.os.IBinder import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat @@ -25,38 +23,36 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel -import java.util.Date class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware { + private lateinit var flutterMethodChannel: MethodChannel val VPN_PERMISSION_REQUEST_CODE = 1001 val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002 - private lateinit var flutterMethodChannel: MethodChannel - private var activity: Activity? = null private var context: Context? = null private var flClashVpnService: FlClashVpnService? = null - private var isBound = false - private var port: Int? = null + private var port: Int = 7890 private var props: Props? = null - private lateinit var title: String - private lateinit var content: String - var isBlockNotification: Boolean = false + private var isBlockNotification: Boolean = false + private var isStart: Boolean = false private val connection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, service: IBinder) { val binder = service as FlClashVpnService.LocalBinder flClashVpnService = binder.getService() - port?.let { startVpn(it) } - isBound = true + if (isStart) { + startVpn() + } else { + flClashVpnService?.initServiceEngine() + } } - override fun onServiceDisconnected(arg0: ComponentName) { + override fun onServiceDisconnected(arg: ComponentName) { flClashVpnService = null - isBound = false } } @@ -71,21 +67,29 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar } override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) { - "StartProxy" -> { - port = call.argument("port") - val args = call.argument("args") - props = - if (args != null) Gson().fromJson(args, Props::class.java) else null - handleStartVpn() + "initService" -> { + isStart = false + initService() + requestNotificationsPermission() result.success(true) } - "StopProxy" -> { + "startProxy" -> { + isStart = true + port = call.argument("port")!! + val args = call.argument("args") + props = + if (args != null) Gson().fromJson(args, Props::class.java) else null + startVpn() + result.success(true) + } + + "stopProxy" -> { stopVpn() result.success(true) } - "SetProtect" -> { + "setProtect" -> { val fd = call.argument("fd") if (fd != null) { flClashVpnService?.protect(fd) @@ -95,14 +99,10 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar } } - "GetRunTimeStamp" -> { - result.success(GlobalState.runTime?.time) - } - "startForeground" -> { - title = call.argument("title") as String - content = call.argument("content") as String - requestNotificationsPermission() + val title = call.argument("title") as String + val content = call.argument("content") as String + startForeground(title, content) result.success(true) } @@ -111,65 +111,41 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar } } - private fun handleStartVpn() { + private fun initService() { val intent = VpnService.prepare(context) if (intent != null) { activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE) } else { - bindService() + if (flClashVpnService != null) { + flClashVpnService!!.initServiceEngine() + } else { + bindService() + } } } - private fun startVpn(port: Int) { - if (GlobalState.runState.value == RunState.START) return; - flClashVpnService?.start(port, props) + private fun startVpn() { + if (flClashVpnService == null) { + bindService() + return + } + if (GlobalState.runState.value == RunState.START) return GlobalState.runState.value = RunState.START - GlobalState.runTime = Date() - startAfter() + flutterMethodChannel.invokeMethod("started", flClashVpnService?.start(port, props)) } private fun stopVpn() { if (GlobalState.runState.value == RunState.STOP) return + GlobalState.runState.value = RunState.STOP flClashVpnService?.stop() - unbindService() - GlobalState.runState.value = RunState.STOP; - GlobalState.runTime = null; + GlobalState.destroyServiceEngine() } - private fun startForeground() { + private fun startForeground(title: String, content: String) { if (GlobalState.runState.value != RunState.START) return flClashVpnService?.startForeground(title, content) } - private fun requestNotificationsPermission() { - if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { - val permission = context?.let { - ContextCompat.checkSelfPermission( - it, - Manifest.permission.POST_NOTIFICATIONS - ) - } - if (permission == PackageManager.PERMISSION_GRANTED) { - startForeground() - } else { - if (isBlockNotification) return - if (activity == null) return - ActivityCompat.requestPermissions( - activity!!, - arrayOf(Manifest.permission.POST_NOTIFICATIONS), - NOTIFICATION_PERMISSION_REQUEST_CODE - ) - - } - } else { - startForeground() - } - } - - private fun startAfter() { - flutterMethodChannel.invokeMethod("startAfter", flClashVpnService?.fd) - } - override fun onAttachedToActivity(binding: ActivityPluginBinding) { activity = binding.activity binding.addActivityResultListener(::onActivityResult) @@ -184,7 +160,7 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar stopVpn() } } - return true; + return true } private fun onRequestPermissionsResultListener( @@ -194,18 +170,32 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar ): Boolean { if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) { isBlockNotification = true - if (grantResults.isNotEmpty()) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - startForeground() - } - } } - return false; + return false } + private fun requestNotificationsPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val permission = context?.let { + ContextCompat.checkSelfPermission( + it, + Manifest.permission.POST_NOTIFICATIONS + ) + } + if (permission != PackageManager.PERMISSION_GRANTED) { + if (isBlockNotification) return + if (activity == null) return + ActivityCompat.requestPermissions( + activity!!, + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + NOTIFICATION_PERMISSION_REQUEST_CODE + ) + } + } + } override fun onDetachedFromActivityForConfigChanges() { - activity = null; + activity = null } override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { @@ -213,7 +203,6 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar } override fun onDetachedFromActivity() { - stopVpn() activity = null } @@ -221,11 +210,4 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar val intent = Intent(context, FlClashVpnService::class.java) context?.bindService(intent, connection, Context.BIND_AUTO_CREATE) } - - private fun unbindService() { - if (isBound) { - context?.unbindService(connection) - isBound = false - } - } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/TilePlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/TilePlugin.kt index e6233b2..6dc17c0 100644 --- a/android/app/src/main/kotlin/com/follow/clash/plugins/TilePlugin.kt +++ b/android/app/src/main/kotlin/com/follow/clash/plugins/TilePlugin.kt @@ -1,3 +1,4 @@ + package com.follow.clash.plugins import io.flutter.embedding.engine.plugins.FlutterPlugin diff --git a/android/app/src/main/kotlin/com/follow/clash/services/FlClashTileService.kt b/android/app/src/main/kotlin/com/follow/clash/services/FlClashTileService.kt index a998f08..49de9ec 100644 --- a/android/app/src/main/kotlin/com/follow/clash/services/FlClashTileService.kt +++ b/android/app/src/main/kotlin/com/follow/clash/services/FlClashTileService.kt @@ -1,9 +1,9 @@ package com.follow.clash.services -import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Intent import android.os.Build +import android.os.IBinder import android.service.quicksettings.Tile import android.service.quicksettings.TileService import androidx.annotation.RequiresApi @@ -11,14 +11,9 @@ import androidx.lifecycle.Observer import com.follow.clash.GlobalState import com.follow.clash.RunState import com.follow.clash.TempActivity -import com.follow.clash.plugins.AppPlugin -import com.follow.clash.plugins.ProxyPlugin -import com.follow.clash.plugins.TilePlugin -import io.flutter.FlutterInjector -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.embedding.engine.dart.DartExecutor +@RequiresApi(Build.VERSION_CODES.N) class FlClashTileService : TileService() { private val observer = Observer { runState -> @@ -62,42 +57,25 @@ class FlClashTileService : TileService() { } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { startActivityAndCollapse(pendingIntent) - }else{ + } else { startActivityAndCollapse(intent) } } - private var flutterEngine: FlutterEngine? = null; - - private fun initFlutterEngine() { - flutterEngine = FlutterEngine(this) - flutterEngine?.plugins?.add(ProxyPlugin()) - flutterEngine?.plugins?.add(TilePlugin()) - flutterEngine?.plugins?.add(AppPlugin()) - GlobalState.flutterEngine = flutterEngine - if (flutterEngine?.dartExecutor?.isExecutingDart != true) { - val vpnService = DartExecutor.DartEntrypoint( - FlutterInjector.instance().flutterLoader().findAppBundlePath(), - "vpnService" - ) - flutterEngine?.dartExecutor?.executeDartEntrypoint(vpnService) - } - } - override fun onClick() { super.onClick() activityTransfer() - val currentTilePlugin = GlobalState.getCurrentTilePlugin() if (GlobalState.runState.value == RunState.STOP) { GlobalState.runState.value = RunState.PENDING - if(currentTilePlugin == null){ - initFlutterEngine() - }else{ - currentTilePlugin.handleStart() + val titlePlugin = GlobalState.getCurrentTitlePlugin() + if (titlePlugin != null) { + titlePlugin.handleStart() + } else { + GlobalState.initServiceEngine(this) } - } else if(GlobalState.runState.value == RunState.START){ + } else if (GlobalState.runState.value == RunState.START) { GlobalState.runState.value = RunState.PENDING - currentTilePlugin?.handleStop() + GlobalState.getCurrentTitlePlugin()?.handleStop() } } diff --git a/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt b/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt index 5de2168..87d477d 100644 --- a/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt +++ b/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt @@ -20,11 +20,8 @@ import com.follow.clash.models.Props class FlClashVpnService : VpnService() { - - private val CHANNEL = "FlClash" - var fd: Int? = null private val notificationId: Int = 1 private val passList = listOf( @@ -47,8 +44,8 @@ class FlClashVpnService : VpnService() { "192.168.*" ) - fun start(port: Int, props: Props?) { - fd = with(Builder()) { + fun start(port: Int, props: Props?): Int? { + return with(Builder()) { addAddress("172.16.0.1", 30) setMtu(9000) addRoute("0.0.0.0", 0) @@ -94,7 +91,6 @@ class FlClashVpnService : VpnService() { stopForeground() } - private val notificationBuilder: NotificationCompat.Builder by lazy { val intent = Intent(this, MainActivity::class.java) @@ -130,6 +126,10 @@ class FlClashVpnService : VpnService() { } } + fun initServiceEngine() { + GlobalState.initServiceEngine(this) + } + override fun onTrimMemory(level: Int) { super.onTrimMemory(level) GlobalState.getCurrentAppPlugin()?.requestGc() @@ -171,7 +171,6 @@ class FlClashVpnService : VpnService() { } override fun onUnbind(intent: Intent?): Boolean { - GlobalState.getCurrentTilePlugin()?.handleStop(); return super.onUnbind(intent) } diff --git a/core/common.go b/core/common.go index 4f6dcf4..bca8f9d 100644 --- a/core/common.go +++ b/core/common.go @@ -84,26 +84,11 @@ type TestDelayParams struct { Timeout int64 `json:"timeout"` } -type Delay struct { - Name string `json:"name"` - Value int32 `json:"value"` -} - -type Process struct { - Id int64 `json:"id"` - Metadata *constant.Metadata `json:"metadata"` -} - type ProcessMapItem struct { Id int64 `json:"id"` Value string `json:"value"` } -type Now struct { - Name string `json:"name"` - Value string `json:"value"` -} - type ExternalProvider struct { Name string `json:"name"` Type string `json:"type"` diff --git a/core/dart-bridge/lib.go b/core/dart-bridge/lib.go index 934d50d..28ed41c 100644 --- a/core/dart-bridge/lib.go +++ b/core/dart-bridge/lib.go @@ -25,7 +25,7 @@ func InitDartApi(api unsafe.Pointer) { } } -func SendToPort(port int64, msg string) { +func SendToPort(port int64, msg string) bool { var obj C.Dart_CObject obj._type = C.Dart_CObject_kString msgString := C.CString(msg) @@ -34,6 +34,7 @@ func SendToPort(port int64, msg string) { *(**C.char)(ptr) = msgString isSuccess := C.GoDart_PostCObject(C.Dart_Port_DL(port), &obj) if !isSuccess { - fmt.Println("ERROR: post to port ", port, " failed", msg) + return false } + return true } diff --git a/core/dart-bridge/message.go b/core/dart-bridge/message.go deleted file mode 100644 index 72694ee..0000000 --- a/core/dart-bridge/message.go +++ /dev/null @@ -1,39 +0,0 @@ -package dart_bridge - -import "encoding/json" - -var Port *int64 - -type MessageType string - -const ( - Log MessageType = "log" - Tun MessageType = "tun" - Delay MessageType = "delay" - Now MessageType = "now" - Process MessageType = "process" - Request MessageType = "request" - Run MessageType = "run" - Loaded MessageType = "loaded" -) - -type Message struct { - Type MessageType `json:"type"` - Data interface{} `json:"data"` -} - -func (message *Message) Json() (string, error) { - data, err := json.Marshal(message) - return string(data), err -} - -func SendMessage(message Message) { - if Port == nil { - return - } - s, err := message.Json() - if err != nil { - return - } - SendToPort(*Port, s) -} diff --git a/core/hub.go b/core/hub.go index 62ba74c..4e8f6ac 100644 --- a/core/hub.go +++ b/core/hub.go @@ -36,6 +36,8 @@ var configParams = ConfigExtendedParams{} var isInit = false +var currentProfileName = "" + //export initClash func initClash(homeDirStr *C.char) bool { if !isInit { @@ -74,6 +76,16 @@ func forceGc() { }() } +//export setCurrentProfileName +func setCurrentProfileName(s *C.char) { + currentProfileName = C.GoString(s) +} + +//export getCurrentProfileName +func getCurrentProfileName() *C.char { + return C.CString(currentProfileName) +} + //export validateConfig func validateConfig(s *C.char, port C.longlong) { i := int64(port) @@ -415,10 +427,14 @@ func updateExternalProvider(providerName *C.char, providerType *C.char, port C.l } //export initNativeApiBridge -func initNativeApiBridge(api unsafe.Pointer, port C.longlong) { +func initNativeApiBridge(api unsafe.Pointer) { bridge.InitDartApi(api) +} + +//export initMessage +func initMessage(port C.longlong) { i := int64(port) - bridge.Port = &i + Port = i } //export freeCString @@ -436,20 +452,20 @@ func init() { } else { delayData.Value = int32(delay) } - bridge.SendMessage(bridge.Message{ - Type: bridge.Delay, + SendMessage(Message{ + Type: DelayMessage, Data: delayData, }) } statistic.DefaultRequestNotify = func(c statistic.Tracker) { - bridge.SendMessage(bridge.Message{ - Type: bridge.Request, + SendMessage(Message{ + Type: RequestMessage, Data: c, }) } executor.DefaultProxyProviderLoadedHook = func(providerName string) { - bridge.SendMessage(bridge.Message{ - Type: bridge.Loaded, + SendMessage(Message{ + Type: LoadedMessage, Data: providerName, }) } diff --git a/core/log.go b/core/log.go index 11225af..d9520b4 100644 --- a/core/log.go +++ b/core/log.go @@ -2,7 +2,6 @@ package main import "C" import ( - bridge "core/dart-bridge" "github.com/metacubex/mihomo/common/observable" "github.com/metacubex/mihomo/log" ) @@ -21,11 +20,11 @@ func startLog() { if logData.LogLevel < log.Level() { continue } - message := &bridge.Message{ - Type: bridge.Log, + message := &Message{ + Type: LogMessage, Data: logData, } - bridge.SendMessage(*message) + SendMessage(*message) } }() } diff --git a/core/message.go b/core/message.go new file mode 100644 index 0000000..caa2519 --- /dev/null +++ b/core/message.go @@ -0,0 +1,77 @@ +package main + +import ( + bridge "core/dart-bridge" + "encoding/json" + "github.com/metacubex/mihomo/constant" +) + +var Port int64 +var ServicePort int64 + +type MessageType string + +const ( + LogMessage MessageType = "log" + ProtectMessage MessageType = "protect" + DelayMessage MessageType = "delay" + ProcessMessage MessageType = "process" + RequestMessage MessageType = "request" + StartedMessage MessageType = "started" + LoadedMessage MessageType = "loaded" +) + +type Delay struct { + Name string `json:"name"` + Value int32 `json:"value"` +} + +type Process struct { + Id int64 `json:"id"` + Metadata *constant.Metadata `json:"metadata"` +} + +type Message struct { + Type MessageType `json:"type"` + Data interface{} `json:"data"` +} + +func (message *Message) Json() (string, error) { + data, err := json.Marshal(message) + return string(data), err +} + +func SendMessage(message Message) { + s, err := message.Json() + if err != nil { + return + } + if handler, ok := messageHandlers[message.Type]; ok { + handler(s) + } else { + sendToPort(s) + } +} + +var messageHandlers = map[MessageType]func(string) bool{ + ProtectMessage: sendToServicePort, + ProcessMessage: sendToServicePort, + StartedMessage: conditionalSend, + LoadedMessage: conditionalSend, +} + +func sendToPort(s string) bool { + return bridge.SendToPort(Port, s) +} + +func sendToServicePort(s string) bool { + return bridge.SendToPort(ServicePort, s) +} + +func conditionalSend(s string) bool { + isSuccess := sendToPort(s) + if !isSuccess { + return sendToServicePort(s) + } + return isSuccess +} diff --git a/core/process.go b/core/process.go index f0aa83f..5044c88 100644 --- a/core/process.go +++ b/core/process.go @@ -4,7 +4,6 @@ package main import "C" import ( - bridge "core/dart-bridge" "encoding/json" "errors" "github.com/metacubex/mihomo/component/process" @@ -43,8 +42,8 @@ func init() { timeout := time.After(200 * time.Millisecond) - bridge.SendMessage(bridge.Message{ - Type: bridge.Process, + SendMessage(Message{ + Type: ProcessMessage, Data: Process{ Id: id, Metadata: metadata, diff --git a/core/tun.go b/core/tun.go index a2dbadf..d8cde58 100644 --- a/core/tun.go +++ b/core/tun.go @@ -10,6 +10,7 @@ import ( "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/log" "golang.org/x/sync/semaphore" + "strconv" "sync" "sync/atomic" "syscall" @@ -18,6 +19,7 @@ import ( var tunLock sync.Mutex var tun *t.Tun +var runTime *time.Time type FdMap struct { m sync.Map @@ -35,7 +37,9 @@ func (cm *FdMap) Load(key int64) bool { var fdMap FdMap //export startTUN -func startTUN(fd C.int) { +func startTUN(fd C.int, port C.longlong) { + i := int64(port) + ServicePort = i go func() { tunLock.Lock() defer tunLock.Unlock() @@ -44,6 +48,7 @@ func startTUN(fd C.int) { tun.Close() tun = nil } + f := int(fd) gateway := "172.16.0.1/30" portal := "172.16.0.2" @@ -61,15 +66,34 @@ func startTUN(fd C.int) { tempTun.Closer = closer tun = tempTun + + now := time.Now() + + runTime = &now + + SendMessage(Message{ + Type: StartedMessage, + Data: strconv.FormatInt(runTime.UnixMilli(), 10), + }) }() } +//export getRunTime +func getRunTime() *C.char { + if runTime == nil { + return C.CString("") + } + return C.CString(strconv.FormatInt(runTime.UnixMilli(), 10)) +} + //export stopTun func stopTun() { go func() { tunLock.Lock() defer tunLock.Unlock() + runTime = nil + if tun != nil { tun.Close() tun = nil @@ -87,6 +111,18 @@ func setFdMap(fd C.long) { }() } +type Fd struct { + Id int64 `json:"id"` + Value int64 `json:"value"` +} + +func markSocket(fd Fd) { + SendMessage(Message{ + Type: ProtectMessage, + Data: fd, + }) +} + var fdCounter int64 = 0 func init() { @@ -102,7 +138,8 @@ func init() { fdInt := int64(fd) timeout := time.After(100 * time.Millisecond) id := atomic.AddInt64(&fdCounter, 1) - tun.MarkSocket(t.Fd{ + + markSocket(Fd{ Id: id, Value: fdInt, }) diff --git a/core/tun/tun.go b/core/tun/tun.go index 0c95630..5b4c07a 100644 --- a/core/tun/tun.go +++ b/core/tun/tun.go @@ -5,7 +5,6 @@ package tun import "C" import ( "context" - bridge "core/dart-bridge" "encoding/binary" "github.com/Kr328/tun2socket" "github.com/Kr328/tun2socket/nat" @@ -185,24 +184,3 @@ func Start(fd int, gateway, portal, dns string) (io.Closer, error) { return stack, nil } - -type Fd struct { - Id int64 `json:"id"` - Value int64 `json:"value"` -} - -func (t *Tun) MarkSocket(fd Fd) { - _ = t.Limit.Acquire(context.Background(), 1) - defer t.Limit.Release(1) - - if t.Closed { - return - } - - message := &bridge.Message{ - Type: bridge.Tun, - Data: fd, - } - - bridge.SendMessage(*message) -} diff --git a/lib/clash/core.dart b/lib/clash/core.dart index b5620d6..a920ee2 100644 --- a/lib/clash/core.dart +++ b/lib/clash/core.dart @@ -35,7 +35,6 @@ class ClashCore { clashFFI = ClashFFI(lib); clashFFI.initNativeApiBridge( NativeApi.initializeApiDLData, - receiver.sendPort.nativePort, ); } @@ -95,6 +94,28 @@ class ClashCore { return completer.future; } + initMessage() { + clashFFI.initMessage( + receiver.sendPort.nativePort, + ); + } + + setProfileName(String profileName) { + final profileNameChar = profileName.toNativeUtf8().cast(); + clashFFI.setCurrentProfileName( + profileNameChar, + ); + malloc.free(profileNameChar); + } + + getProfileName() { + final currentProfileNameRaw = clashFFI.getCurrentProfileName(); + final currentProfileName = + currentProfileNameRaw.cast().toDartString(); + clashFFI.freeCString(currentProfileNameRaw); + return currentProfileName; + } + Future> getProxiesGroups() { final proxiesRaw = clashFFI.getProxies(); final proxiesRawString = proxiesRaw.cast().toDartString(); @@ -242,8 +263,9 @@ class ClashCore { clashFFI.stopLog(); } - startTun(int fd) { - clashFFI.startTUN(fd); + startTun(int fd, int port) { + if (!Platform.isAndroid) return; + clashFFI.startTUN(fd, port); } requestGc() { @@ -265,11 +287,13 @@ class ClashCore { clashFFI.setFdMap(fd); } - // DateTime? getRunTime() { - // final runTimeString = clashFFI.getRunTime().cast().toDartString(); - // if (runTimeString.isEmpty) return null; - // return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString)); - // } + DateTime? getRunTime() { + final runTimeRaw = clashFFI.getRunTime(); + final runTimeString = runTimeRaw.cast().toDartString(); + clashFFI.freeCString(runTimeRaw); + if (runTimeString.isEmpty) return null; + return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString)); + } List getConnections() { final connectionsDataRaw = clashFFI.getConnections(); diff --git a/lib/clash/generated/clash_ffi.dart b/lib/clash/generated/clash_ffi.dart index 6d766fb..b537b11 100644 --- a/lib/clash/generated/clash_ffi.dart +++ b/lib/clash/generated/clash_ffi.dart @@ -5190,6 +5190,30 @@ class ClashFFI { _lookup>('forceGc'); late final _forceGc = _forceGcPtr.asFunction(); + void setCurrentProfileName( + ffi.Pointer s, + ) { + return _setCurrentProfileName( + s, + ); + } + + late final _setCurrentProfileNamePtr = + _lookup)>>( + 'setCurrentProfileName'); + late final _setCurrentProfileName = _setCurrentProfileNamePtr + .asFunction)>(); + + ffi.Pointer getCurrentProfileName() { + return _getCurrentProfileName(); + } + + late final _getCurrentProfileNamePtr = + _lookup Function()>>( + 'getCurrentProfileName'); + late final _getCurrentProfileName = + _getCurrentProfileNamePtr.asFunction Function()>(); + void validateConfig( ffi.Pointer s, int port, @@ -5406,20 +5430,30 @@ class ClashFFI { void initNativeApiBridge( ffi.Pointer api, - int port, ) { return _initNativeApiBridge( api, + ); + } + + late final _initNativeApiBridgePtr = + _lookup)>>( + 'initNativeApiBridge'); + late final _initNativeApiBridge = _initNativeApiBridgePtr + .asFunction)>(); + + void initMessage( + int port, + ) { + return _initMessage( port, ); } - late final _initNativeApiBridgePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, ffi.LongLong)>>('initNativeApiBridge'); - late final _initNativeApiBridge = _initNativeApiBridgePtr - .asFunction, int)>(); + late final _initMessagePtr = + _lookup>( + 'initMessage'); + late final _initMessage = _initMessagePtr.asFunction(); void freeCString( ffi.Pointer s, @@ -5467,15 +5501,28 @@ class ClashFFI { void startTUN( int fd, + int port, ) { return _startTUN( fd, + port, ); } late final _startTUNPtr = - _lookup>('startTUN'); - late final _startTUN = _startTUNPtr.asFunction(); + _lookup>( + 'startTUN'); + late final _startTUN = _startTUNPtr.asFunction(); + + ffi.Pointer getRunTime() { + return _getRunTime(); + } + + late final _getRunTimePtr = + _lookup Function()>>( + 'getRunTime'); + late final _getRunTime = + _getRunTimePtr.asFunction Function()>(); void stopTun() { return _stopTun(); diff --git a/lib/clash/message.dart b/lib/clash/message.dart index 78d5605..bcfba40 100644 --- a/lib/clash/message.dart +++ b/lib/clash/message.dart @@ -7,24 +7,6 @@ import 'package:flutter/foundation.dart'; import 'core.dart'; -abstract mixin class ClashMessageListener { - void onLog(Log log) {} - - void onTun(Fd fd) {} - - void onDelay(Delay delay) {} - - void onProcess(Process process) {} - - void onRequest(Connection connection) {} - - void onNow(Now now) {} - - void onRun(String runTime) {} - - void onLoaded(String groupName) {} -} - class ClashMessage { StreamSubscription? subscription; @@ -34,31 +16,22 @@ class ClashMessage { subscription = null; } subscription = ClashCore.receiver.listen((message) { - final m = Message.fromJson(json.decode(message)); - for (final ClashMessageListener listener in _listeners) { + final m = AppMessage.fromJson(json.decode(message)); + for (final AppMessageListener listener in _listeners) { switch (m.type) { - case MessageType.log: + case AppMessageType.log: listener.onLog(Log.fromJson(m.data)); break; - case MessageType.tun: - listener.onTun(Fd.fromJson(m.data)); - break; - case MessageType.delay: + case AppMessageType.delay: listener.onDelay(Delay.fromJson(m.data)); break; - case MessageType.process: - listener.onProcess(Process.fromJson(m.data)); - break; - case MessageType.now: - listener.onNow(Now.fromJson(m.data)); - break; - case MessageType.request: + case AppMessageType.request: listener.onRequest(Connection.fromJson(m.data)); break; - case MessageType.run: - listener.onRun(m.data); + case AppMessageType.started: + listener.onStarted(m.data); break; - case MessageType.loaded: + case AppMessageType.loaded: listener.onLoaded(m.data); break; } @@ -68,18 +41,18 @@ class ClashMessage { static final ClashMessage instance = ClashMessage._(); - final ObserverList _listeners = - ObserverList(); + final ObserverList _listeners = + ObserverList(); bool get hasListeners { return _listeners.isNotEmpty; } - void addListener(ClashMessageListener listener) { + void addListener(AppMessageListener listener) { _listeners.add(listener); } - void removeListener(ClashMessageListener listener) { + void removeListener(AppMessageListener listener) { _listeners.remove(listener); } } diff --git a/lib/common/android.dart b/lib/common/android.dart index 143403b..fdf351b 100644 --- a/lib/common/android.dart +++ b/lib/common/android.dart @@ -1,14 +1,10 @@ import 'dart:io'; -import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/plugins/app.dart'; class Android { init() async { - app?.onExit = () { - clashCore.shutdown(); - exit(0); - }; + app?.onExit = () {}; } } diff --git a/lib/common/proxy.dart b/lib/common/proxy.dart index e507dd7..552923c 100644 --- a/lib/common/proxy.dart +++ b/lib/common/proxy.dart @@ -28,20 +28,6 @@ class ProxyManager { return await (_proxy as Proxy).updateStartTime(); } - Future setProtect(int fd) async { - if (_proxy is! Proxy) return null; - return await (_proxy as Proxy).setProtect(fd); - } - - Future startForeground({ - required String title, - required String content, - }) async { - if (_proxy is! Proxy) return null; - return await (_proxy as Proxy) - .startForeground(title: title, content: content); - } - factory ProxyManager() { _instance ??= ProxyManager._internal(); return _instance!; diff --git a/lib/controller.dart b/lib/controller.dart index 683954d..17af747 100644 --- a/lib/controller.dart +++ b/lib/controller.dart @@ -26,7 +26,7 @@ class AppController { updateClashConfigDebounce = debounce(() async { await updateClashConfig(); }); - addCheckIpNumDebounce = debounce((){ + addCheckIpNumDebounce = debounce(() { appState.checkIpNum++; }); measure = Measure.of(context); @@ -70,7 +70,6 @@ class AppController { updateTraffic() { globalState.updateTraffic( - config: config, appState: appState, ); } @@ -121,6 +120,14 @@ class AppController { }); } + Future rawApplyProfile() async { + await globalState.applyProfile( + appState: appState, + config: config, + clashConfig: clashConfig, + ); + } + changeProfile(String? value) async { if (value == config.currentProfileId) return; config.currentProfileId = value; diff --git a/lib/enum/enum.dart b/lib/enum/enum.dart index 2c7cb84..22de9d7 100644 --- a/lib/enum/enum.dart +++ b/lib/enum/enum.dart @@ -56,14 +56,18 @@ enum ProfileType { file, url } enum ResultType { success, error } -enum MessageType { +enum AppMessageType { log, - tun, delay, - process, - now, request, - run, + started, + loaded, +} + +enum ServiceMessageType { + protect, + process, + started, loaded, } diff --git a/lib/main.dart b/lib/main.dart index dfd4f18..9e3913e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,8 @@ import 'dart:async'; -import 'dart:io'; import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/plugins/app.dart'; +import 'package:fl_clash/plugins/proxy.dart'; import 'package:fl_clash/plugins/tile.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; @@ -16,6 +16,7 @@ Future main() async { WidgetsFlutterBinding.ensureInitialized(); await android?.init(); await window?.init(); + clashCore.initMessage(); globalState.packageInfo = await PackageInfo.fromPlatform(); final config = await preferences.getConfig() ?? Config(); final clashConfig = await preferences.getClashConfig() ?? ClashConfig(); @@ -44,6 +45,7 @@ Future main() async { @pragma('vm:entry-point') Future vpnService() async { WidgetsFlutterBinding.ensureInitialized(); + globalState.isVpnService = true; final config = await preferences.getConfig() ?? Config(); final clashConfig = await preferences.getClashConfig() ?? ClashConfig(); final appState = AppState( @@ -51,12 +53,34 @@ Future vpnService() async { isCompatible: config.isCompatible, selectedMap: config.currentSelectedMap, ); - clashMessage.addListener( - ClashMessageListenerWithVpn( - onTun: (Fd fd) async { - await proxyManager.setProtect(fd.value); + await globalState.init( + appState: appState, + config: config, + clashConfig: clashConfig, + ); + + proxy?.setServiceMessageHandler( + ServiceMessageHandler( + onProtect: (Fd fd) async { + await proxy?.setProtect(fd.value); clashCore.setFdMap(fd.id); }, + onProcess: (Process process) async { + var packageName = await app?.resolverProcess(process); + clashCore.setProcessMap( + ProcessMapItem( + id: process.id, + value: packageName ?? "", + ), + ); + }, + onStarted: (String runTime) { + globalState.applyProfile( + appState: appState, + config: config, + clashConfig: clashConfig, + ); + }, onLoaded: (String groupName) { final currentSelectedMap = config.currentSelectedMap; final proxyName = currentSelectedMap[groupName]; @@ -70,70 +94,68 @@ Future vpnService() async { }, ), ); - - await globalState.init( - appState: appState, - config: config, - clashConfig: clashConfig, - ); - - globalState.applyProfile( - appState: appState, - config: config, - clashConfig: clashConfig, - ); - final appLocalizations = await AppLocalizations.load( other.getLocaleForString(config.locale) ?? WidgetsBinding.instance.platformDispatcher.locale, ); - - handleStart() async { - await app?.tip(appLocalizations.startVpn); - await globalState.startSystemProxy( - appState: appState, - config: config, - clashConfig: clashConfig, - ); - globalState.updateTraffic(config: config); - globalState.updateFunctionLists = [ - () { - globalState.updateTraffic(config: config); - } - ]; - } - - handleStart(); + await app?.tip(appLocalizations.startVpn); + await globalState.startSystemProxy( + appState: appState, + config: config, + clashConfig: clashConfig, + ); tile?.addListener( TileListenerWithVpn( onStop: () async { await app?.tip(appLocalizations.stopVpn); await globalState.stopSystemProxy(); - clashCore.shutdown(); - exit(0); }, ), ); + + globalState.updateTraffic(); + globalState.updateFunctionLists = [ + () { + globalState.updateTraffic(); + } + ]; } -class ClashMessageListenerWithVpn with ClashMessageListener { - final Function(Fd fd) _onTun; - final Function(String) _onLoaded; +@immutable +class ServiceMessageHandler with ServiceMessageListener { + final Function(Fd fd) _onProtect; + final Function(Process process) _onProcess; + final Function(String runTime) _onStarted; + final Function(String groupName) _onLoaded; - ClashMessageListenerWithVpn({ - required Function(Fd fd) onTun, - required Function(String) onLoaded, - }) : _onTun = onTun, + const ServiceMessageHandler({ + required Function(Fd fd) onProtect, + required Function(Process process) onProcess, + required Function(String runTime) onStarted, + required Function(String groupName) onLoaded, + }) : _onProtect = onProtect, + _onProcess = onProcess, + _onStarted = onStarted, _onLoaded = onLoaded; @override - void onTun(Fd fd) { - _onTun(fd); + onProtect(Fd fd) { + _onProtect(fd); } @override - void onLoaded(String groupName) { + onProcess(Process process) { + _onProcess(process); + } + + @override + onStarted(String runTime) { + _onStarted(runTime); + } + + @override + onLoaded(String groupName) { _onLoaded(groupName); } } diff --git a/lib/models/ffi.dart b/lib/models/ffi.dart index 42ea7c1..86fb1a5 100644 --- a/lib/models/ffi.dart +++ b/lib/models/ffi.dart @@ -47,14 +47,25 @@ class ChangeProxyParams with _$ChangeProxyParams { } @freezed -class Message with _$Message { - const factory Message({ - required MessageType type, +class AppMessage with _$AppMessage { + const factory AppMessage({ + required AppMessageType type, dynamic data, - }) = _Message; + }) = _AppMessage; - factory Message.fromJson(Map json) => - _$MessageFromJson(json); + factory AppMessage.fromJson(Map json) => + _$AppMessageFromJson(json); +} + +@freezed +class ServiceMessage with _$ServiceMessage { + const factory ServiceMessage({ + required ServiceMessageType type, + dynamic data, + }) = _ServiceMessage; + + factory ServiceMessage.fromJson(Map json) => + _$ServiceMessageFromJson(json); } @freezed @@ -121,3 +132,27 @@ class ExternalProvider with _$ExternalProvider { factory ExternalProvider.fromJson(Map json) => _$ExternalProviderFromJson(json); } + +abstract mixin class AppMessageListener { + void onLog(Log log) {} + + void onDelay(Delay delay) {} + + void onRequest(Connection connection) {} + + void onStarted(String runTime) {} + + void onLoaded(String groupName) {} +} + +abstract mixin class ServiceMessageListener { + onProtect(Fd fd) {} + + onProcess(Process process) {} + + onStarted(String runTime) {} + + onLoaded(String groupName) {} +} + + diff --git a/lib/models/generated/ffi.freezed.dart b/lib/models/generated/ffi.freezed.dart index 169b682..346eb60 100644 --- a/lib/models/generated/ffi.freezed.dart +++ b/lib/models/generated/ffi.freezed.dart @@ -610,32 +610,34 @@ abstract class _ChangeProxyParams implements ChangeProxyParams { throw _privateConstructorUsedError; } -Message _$MessageFromJson(Map json) { - return _Message.fromJson(json); +AppMessage _$AppMessageFromJson(Map json) { + return _AppMessage.fromJson(json); } /// @nodoc -mixin _$Message { - MessageType get type => throw _privateConstructorUsedError; +mixin _$AppMessage { + AppMessageType get type => throw _privateConstructorUsedError; dynamic get data => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) - $MessageCopyWith get copyWith => throw _privateConstructorUsedError; + $AppMessageCopyWith get copyWith => + throw _privateConstructorUsedError; } /// @nodoc -abstract class $MessageCopyWith<$Res> { - factory $MessageCopyWith(Message value, $Res Function(Message) then) = - _$MessageCopyWithImpl<$Res, Message>; +abstract class $AppMessageCopyWith<$Res> { + factory $AppMessageCopyWith( + AppMessage value, $Res Function(AppMessage) then) = + _$AppMessageCopyWithImpl<$Res, AppMessage>; @useResult - $Res call({MessageType type, dynamic data}); + $Res call({AppMessageType type, dynamic data}); } /// @nodoc -class _$MessageCopyWithImpl<$Res, $Val extends Message> - implements $MessageCopyWith<$Res> { - _$MessageCopyWithImpl(this._value, this._then); +class _$AppMessageCopyWithImpl<$Res, $Val extends AppMessage> + implements $AppMessageCopyWith<$Res> { + _$AppMessageCopyWithImpl(this._value, this._then); // ignore: unused_field final $Val _value; @@ -652,7 +654,7 @@ class _$MessageCopyWithImpl<$Res, $Val extends Message> type: null == type ? _value.type : type // ignore: cast_nullable_to_non_nullable - as MessageType, + as AppMessageType, data: freezed == data ? _value.data : data // ignore: cast_nullable_to_non_nullable @@ -662,21 +664,22 @@ class _$MessageCopyWithImpl<$Res, $Val extends Message> } /// @nodoc -abstract class _$$MessageImplCopyWith<$Res> implements $MessageCopyWith<$Res> { - factory _$$MessageImplCopyWith( - _$MessageImpl value, $Res Function(_$MessageImpl) then) = - __$$MessageImplCopyWithImpl<$Res>; +abstract class _$$AppMessageImplCopyWith<$Res> + implements $AppMessageCopyWith<$Res> { + factory _$$AppMessageImplCopyWith( + _$AppMessageImpl value, $Res Function(_$AppMessageImpl) then) = + __$$AppMessageImplCopyWithImpl<$Res>; @override @useResult - $Res call({MessageType type, dynamic data}); + $Res call({AppMessageType type, dynamic data}); } /// @nodoc -class __$$MessageImplCopyWithImpl<$Res> - extends _$MessageCopyWithImpl<$Res, _$MessageImpl> - implements _$$MessageImplCopyWith<$Res> { - __$$MessageImplCopyWithImpl( - _$MessageImpl _value, $Res Function(_$MessageImpl) _then) +class __$$AppMessageImplCopyWithImpl<$Res> + extends _$AppMessageCopyWithImpl<$Res, _$AppMessageImpl> + implements _$$AppMessageImplCopyWith<$Res> { + __$$AppMessageImplCopyWithImpl( + _$AppMessageImpl _value, $Res Function(_$AppMessageImpl) _then) : super(_value, _then); @pragma('vm:prefer-inline') @@ -685,11 +688,11 @@ class __$$MessageImplCopyWithImpl<$Res> Object? type = null, Object? data = freezed, }) { - return _then(_$MessageImpl( + return _then(_$AppMessageImpl( type: null == type ? _value.type : type // ignore: cast_nullable_to_non_nullable - as MessageType, + as AppMessageType, data: freezed == data ? _value.data : data // ignore: cast_nullable_to_non_nullable @@ -700,27 +703,27 @@ class __$$MessageImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$MessageImpl implements _Message { - const _$MessageImpl({required this.type, this.data}); +class _$AppMessageImpl implements _AppMessage { + const _$AppMessageImpl({required this.type, this.data}); - factory _$MessageImpl.fromJson(Map json) => - _$$MessageImplFromJson(json); + factory _$AppMessageImpl.fromJson(Map json) => + _$$AppMessageImplFromJson(json); @override - final MessageType type; + final AppMessageType type; @override final dynamic data; @override String toString() { - return 'Message(type: $type, data: $data)'; + return 'AppMessage(type: $type, data: $data)'; } @override bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$MessageImpl && + other is _$AppMessageImpl && (identical(other.type, type) || other.type == type) && const DeepCollectionEquality().equals(other.data, data)); } @@ -733,30 +736,188 @@ class _$MessageImpl implements _Message { @JsonKey(ignore: true) @override @pragma('vm:prefer-inline') - _$$MessageImplCopyWith<_$MessageImpl> get copyWith => - __$$MessageImplCopyWithImpl<_$MessageImpl>(this, _$identity); + _$$AppMessageImplCopyWith<_$AppMessageImpl> get copyWith => + __$$AppMessageImplCopyWithImpl<_$AppMessageImpl>(this, _$identity); @override Map toJson() { - return _$$MessageImplToJson( + return _$$AppMessageImplToJson( this, ); } } -abstract class _Message implements Message { - const factory _Message( - {required final MessageType type, final dynamic data}) = _$MessageImpl; +abstract class _AppMessage implements AppMessage { + const factory _AppMessage( + {required final AppMessageType type, + final dynamic data}) = _$AppMessageImpl; - factory _Message.fromJson(Map json) = _$MessageImpl.fromJson; + factory _AppMessage.fromJson(Map json) = + _$AppMessageImpl.fromJson; @override - MessageType get type; + AppMessageType get type; @override dynamic get data; @override @JsonKey(ignore: true) - _$$MessageImplCopyWith<_$MessageImpl> get copyWith => + _$$AppMessageImplCopyWith<_$AppMessageImpl> get copyWith => + throw _privateConstructorUsedError; +} + +ServiceMessage _$ServiceMessageFromJson(Map json) { + return _ServiceMessage.fromJson(json); +} + +/// @nodoc +mixin _$ServiceMessage { + ServiceMessageType get type => throw _privateConstructorUsedError; + dynamic get data => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $ServiceMessageCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ServiceMessageCopyWith<$Res> { + factory $ServiceMessageCopyWith( + ServiceMessage value, $Res Function(ServiceMessage) then) = + _$ServiceMessageCopyWithImpl<$Res, ServiceMessage>; + @useResult + $Res call({ServiceMessageType type, dynamic data}); +} + +/// @nodoc +class _$ServiceMessageCopyWithImpl<$Res, $Val extends ServiceMessage> + implements $ServiceMessageCopyWith<$Res> { + _$ServiceMessageCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? data = freezed, + }) { + return _then(_value.copyWith( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as ServiceMessageType, + data: freezed == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as dynamic, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ServiceMessageImplCopyWith<$Res> + implements $ServiceMessageCopyWith<$Res> { + factory _$$ServiceMessageImplCopyWith(_$ServiceMessageImpl value, + $Res Function(_$ServiceMessageImpl) then) = + __$$ServiceMessageImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ServiceMessageType type, dynamic data}); +} + +/// @nodoc +class __$$ServiceMessageImplCopyWithImpl<$Res> + extends _$ServiceMessageCopyWithImpl<$Res, _$ServiceMessageImpl> + implements _$$ServiceMessageImplCopyWith<$Res> { + __$$ServiceMessageImplCopyWithImpl( + _$ServiceMessageImpl _value, $Res Function(_$ServiceMessageImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? type = null, + Object? data = freezed, + }) { + return _then(_$ServiceMessageImpl( + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as ServiceMessageType, + data: freezed == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as dynamic, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ServiceMessageImpl implements _ServiceMessage { + const _$ServiceMessageImpl({required this.type, this.data}); + + factory _$ServiceMessageImpl.fromJson(Map json) => + _$$ServiceMessageImplFromJson(json); + + @override + final ServiceMessageType type; + @override + final dynamic data; + + @override + String toString() { + return 'ServiceMessage(type: $type, data: $data)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ServiceMessageImpl && + (identical(other.type, type) || other.type == type) && + const DeepCollectionEquality().equals(other.data, data)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, type, const DeepCollectionEquality().hash(data)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ServiceMessageImplCopyWith<_$ServiceMessageImpl> get copyWith => + __$$ServiceMessageImplCopyWithImpl<_$ServiceMessageImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$ServiceMessageImplToJson( + this, + ); + } +} + +abstract class _ServiceMessage implements ServiceMessage { + const factory _ServiceMessage( + {required final ServiceMessageType type, + final dynamic data}) = _$ServiceMessageImpl; + + factory _ServiceMessage.fromJson(Map json) = + _$ServiceMessageImpl.fromJson; + + @override + ServiceMessageType get type; + @override + dynamic get data; + @override + @JsonKey(ignore: true) + _$$ServiceMessageImplCopyWith<_$ServiceMessageImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/models/generated/ffi.g.dart b/lib/models/generated/ffi.g.dart index d3d061f..ee6d372 100644 --- a/lib/models/generated/ffi.g.dart +++ b/lib/models/generated/ffi.g.dart @@ -55,27 +55,44 @@ Map _$$ChangeProxyParamsImplToJson( 'proxy-name': instance.proxyName, }; -_$MessageImpl _$$MessageImplFromJson(Map json) => - _$MessageImpl( - type: $enumDecode(_$MessageTypeEnumMap, json['type']), +_$AppMessageImpl _$$AppMessageImplFromJson(Map json) => + _$AppMessageImpl( + type: $enumDecode(_$AppMessageTypeEnumMap, json['type']), data: json['data'], ); -Map _$$MessageImplToJson(_$MessageImpl instance) => +Map _$$AppMessageImplToJson(_$AppMessageImpl instance) => { - 'type': _$MessageTypeEnumMap[instance.type]!, + 'type': _$AppMessageTypeEnumMap[instance.type]!, 'data': instance.data, }; -const _$MessageTypeEnumMap = { - MessageType.log: 'log', - MessageType.tun: 'tun', - MessageType.delay: 'delay', - MessageType.process: 'process', - MessageType.now: 'now', - MessageType.request: 'request', - MessageType.run: 'run', - MessageType.loaded: 'loaded', +const _$AppMessageTypeEnumMap = { + AppMessageType.log: 'log', + AppMessageType.delay: 'delay', + AppMessageType.request: 'request', + AppMessageType.started: 'started', + AppMessageType.loaded: 'loaded', +}; + +_$ServiceMessageImpl _$$ServiceMessageImplFromJson(Map json) => + _$ServiceMessageImpl( + type: $enumDecode(_$ServiceMessageTypeEnumMap, json['type']), + data: json['data'], + ); + +Map _$$ServiceMessageImplToJson( + _$ServiceMessageImpl instance) => + { + 'type': _$ServiceMessageTypeEnumMap[instance.type]!, + 'data': instance.data, + }; + +const _$ServiceMessageTypeEnumMap = { + ServiceMessageType.protect: 'protect', + ServiceMessageType.process: 'process', + ServiceMessageType.started: 'started', + ServiceMessageType.loaded: 'loaded', }; _$DelayImpl _$$DelayImplFromJson(Map json) => _$DelayImpl( diff --git a/lib/plugins/app.dart b/lib/plugins/app.dart index 89ae3f4..3e844de 100644 --- a/lib/plugins/app.dart +++ b/lib/plugins/app.dart @@ -5,32 +5,30 @@ import 'dart:isolate'; import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:intl/intl.dart'; class App { static App? _instance; - MethodChannel? methodChannel; + late MethodChannel methodChannel; Function()? onExit; App._internal() { - if (Platform.isAndroid) { - methodChannel = const MethodChannel("app"); - methodChannel!.setMethodCallHandler((call) async { - switch (call.method) { - case "exit": - if (onExit != null) { - await onExit!(); - } - break; - case "gc": - clashCore.requestGc(); - break; - default: - throw MissingPluginException(); - } - }); - } + methodChannel = const MethodChannel("app"); + methodChannel.setMethodCallHandler((call) async { + switch (call.method) { + case "exit": + if (onExit != null) { + await onExit!(); + } + case "gc": + clashCore.requestGc(); + default: + throw MissingPluginException(); + } + }); } factory App() { @@ -39,21 +37,21 @@ class App { } Future moveTaskToBack() async { - return await methodChannel?.invokeMethod("moveTaskToBack"); + return await methodChannel.invokeMethod("moveTaskToBack"); } Future> getPackages() async { final packagesString = - await methodChannel?.invokeMethod("getPackages"); + await methodChannel.invokeMethod("getPackages"); return Isolate.run>(() { final List packagesRaw = - packagesString != null ? json.decode(packagesString) : []; + packagesString != null ? json.decode(packagesString) : []; return packagesRaw.map((e) => Package.fromJson(e)).toList(); }); } Future getPackageIcon(String packageName) async { - final base64 = await methodChannel?.invokeMethod("getPackageIcon", { + final base64 = await methodChannel.invokeMethod("getPackageIcon", { "packageName": packageName, }); if (base64 == null) { @@ -63,19 +61,19 @@ class App { } Future tip(String? message) async { - return await methodChannel?.invokeMethod("tip", { + return await methodChannel.invokeMethod("tip", { "message": "$message", }); } Future updateExcludeFromRecents(bool value) async { - return await methodChannel?.invokeMethod("updateExcludeFromRecents", { + return await methodChannel.invokeMethod("updateExcludeFromRecents", { "value": value, }); } Future resolverProcess(Process process) async { - return await methodChannel?.invokeMethod("resolverProcess", { + return await methodChannel.invokeMethod("resolverProcess", { "data": json.encode(process), }); } diff --git a/lib/plugins/proxy.dart b/lib/plugins/proxy.dart index deb9728..7832a77 100644 --- a/lib/plugins/proxy.dart +++ b/lib/plugins/proxy.dart @@ -1,29 +1,29 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; -import 'package:fl_clash/clash/core.dart'; +import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; import 'package:flutter/services.dart'; import 'package:proxy/proxy_platform_interface.dart'; class Proxy extends ProxyPlatform { static Proxy? _instance; late MethodChannel methodChannel; - late ReceivePort receiver; + ReceivePort? receiver; + ServiceMessageListener? _serviceMessageHandler; Proxy._internal() { methodChannel = const MethodChannel("proxy"); - receiver = ReceivePort() - ..listen( - (message) { - setProtect(int.parse(message)); - }, - ); methodChannel.setMethodCallHandler((call) async { switch (call.method) { - case "startAfter": - int fd = call.arguments; - startAfterHook(fd); + case "started": + final fd = call.arguments; + onStarted(fd); break; default: throw MissingPluginException(); @@ -36,16 +36,27 @@ class Proxy extends ProxyPlatform { return _instance!; } + Future _initService() async { + return await methodChannel.invokeMethod("initService"); + } + + handleStop() { + globalState.stopSystemProxy(); + } + @override - Future startProxy(int port, String? args) async { + Future startProxy(port, args) async { + if (!globalState.isVpnService) { + return await _initService(); + } return await methodChannel - .invokeMethod("StartProxy", {'port': port, 'args': args}); + .invokeMethod("startProxy", {'port': port, 'args': args}); } @override Future stopProxy() async { clashCore.stopTun(); - final isStop = await methodChannel.invokeMethod("StopProxy"); + final isStop = await methodChannel.invokeMethod("stopProxy"); if (isStop == true) { startTime = null; } @@ -53,11 +64,7 @@ class Proxy extends ProxyPlatform { } Future setProtect(int fd) async { - return await methodChannel.invokeMethod("SetProtect", {'fd': fd}); - } - - Future getRunTimeStamp() async { - return await methodChannel.invokeMethod("GetRunTimeStamp"); + return await methodChannel.invokeMethod("setProtect", {'fd': fd}); } Future startForeground({ @@ -72,26 +79,43 @@ class Proxy extends ProxyPlatform { bool get isStart => startTime != null && startTime!.isBeforeNow; - startAfterHook(int? fd) { - if (!isStart && fd != null) { - clashCore.startTun(fd); - updateStartTime(); + onStarted(int? fd) { + if (fd == null) return; + if (receiver != null) { + receiver!.close(); + receiver == null; } + receiver = ReceivePort(); + receiver!.listen((message) { + _handleServiceMessage(message); + }); + clashCore.startTun(fd, receiver!.sendPort.nativePort); } - // updateStartTime() async { - // startTime = clashCore.getRunTime(); - // } - - updateStartTime() async { - startTime = await getRunTime(); + updateStartTime() { + startTime = clashCore.getRunTime(); } - Future getRunTime() async { - final runTimeStamp = await getRunTimeStamp(); - return runTimeStamp != null - ? DateTime.fromMillisecondsSinceEpoch(runTimeStamp) - : null; + setServiceMessageHandler(ServiceMessageListener serviceMessageListener) { + _serviceMessageHandler = serviceMessageListener; + } + + _handleServiceMessage(String message) { + final m = ServiceMessage.fromJson(json.decode(message)); + switch (m.type) { + case ServiceMessageType.protect: + _serviceMessageHandler?.onProtect(Fd.fromJson(m.data)); + break; + case ServiceMessageType.process: + _serviceMessageHandler?.onProcess(Process.fromJson(m.data)); + break; + case ServiceMessageType.started: + _serviceMessageHandler?.onStarted(m.data); + break; + case ServiceMessageType.loaded: + _serviceMessageHandler?.onLoaded(m.data); + break; + } } } diff --git a/lib/state.dart b/lib/state.dart index 6c4ac9b..0ea9409 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -5,17 +5,20 @@ import 'dart:io'; import 'package:animations/animations.dart'; import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/plugins/proxy.dart'; import 'package:fl_clash/widgets/scaffold.dart'; import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'controller.dart'; + import 'models/models.dart'; import 'common/common.dart'; class GlobalState { Timer? timer; Timer? groupsUpdateTimer; + var isVpnService = false; late PackageInfo packageInfo; Function? updateCurrentDelayDebounce; PageController? pageController; @@ -82,12 +85,15 @@ class GlobalState { args: args, ); startListenUpdate(); + if (Platform.isAndroid) { + return; + } applyProfile( appState: appState, config: config, clashConfig: clashConfig, ).then((_){ - appController.addCheckIpNumDebounce(); + globalState.appController.addCheckIpNumDebounce(); }); } @@ -106,6 +112,7 @@ class GlobalState { config: config, isPatch: false, ); + clashCore.setProfileName(config.currentProfile?.label ?? ''); await updateGroups(appState); } @@ -182,20 +189,18 @@ class GlobalState { updateTraffic({ AppState? appState, - required Config config, }) { final traffic = clashCore.getTraffic(); - if (appState != null) { - appState.addTraffic(traffic); - appState.totalTraffic = clashCore.getTotalTraffic(); - } - if (Platform.isAndroid) { - final currentProfile = config.currentProfile; - if (currentProfile == null) return; - proxyManager.startForeground( - title: currentProfile.label ?? currentProfile.id, + if (Platform.isAndroid && isVpnService == true) { + proxy?.startForeground( + title: clashCore.getProfileName(), content: "$traffic", ); + } else { + if (appState != null) { + appState.addTraffic(traffic); + appState.totalTraffic = clashCore.getTotalTraffic(); + } } } diff --git a/lib/widgets/android_container.dart b/lib/widgets/android_container.dart index 8870066..48f8057 100644 --- a/lib/widgets/android_container.dart +++ b/lib/widgets/android_container.dart @@ -19,6 +19,7 @@ class AndroidContainer extends StatefulWidget { class _AndroidContainerState extends State with WidgetsBindingObserver { + _excludeContainer(Widget child) { return Selector( selector: (_, config) => config.isExclude, @@ -47,9 +48,7 @@ class _AndroidContainerState extends State @override Widget build(BuildContext context) { - return _excludeContainer( - widget.child, - ); + return _excludeContainer(widget.child); } @override diff --git a/lib/widgets/clash_message_container.dart b/lib/widgets/clash_message_container.dart index d79618f..17bf474 100644 --- a/lib/widgets/clash_message_container.dart +++ b/lib/widgets/clash_message_container.dart @@ -1,6 +1,6 @@ import 'package:fl_clash/clash/clash.dart'; -import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/plugins/proxy.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; import 'package:fl_clash/plugins/app.dart'; @@ -18,7 +18,7 @@ class ClashMessageContainer extends StatefulWidget { } class _ClashMessageContainerState extends State - with ClashMessageListener { + with AppMessageListener { @override Widget build(BuildContext context) { return widget.child; @@ -49,25 +49,6 @@ class _ClashMessageContainerState extends State super.onLog(log); } - @override - Future onTun(Fd fd) async { - await proxyManager.setProtect(fd.value); - clashCore.setFdMap(fd.id); - super.onTun(fd); - } - - @override - void onProcess(Process process) async { - var packageName = await app?.resolverProcess(process); - clashCore.setProcessMap( - ProcessMapItem( - id: process.id, - value: packageName ?? "", - ), - ); - super.onProcess(process); - } - @override void onRequest(Connection connection) async { globalState.appController.appState.addRequest(connection); @@ -89,4 +70,14 @@ class _ClashMessageContainerState extends State appController.addCheckIpNumDebounce(); super.onLoaded(proxyName); } + + @override + void onStarted(String runTime) { + super.onStarted(runTime); + proxy?.updateStartTime(); + final appController = globalState.appController; + appController.rawApplyProfile().then((_) { + appController.addCheckIpNumDebounce(); + }); + } } diff --git a/pubspec.yaml b/pubspec.yaml index bc509ae..ac43129 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: fl_clash description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. publish_to: 'none' -version: 0.8.36+202407081 +version: 0.8.36+202407131 environment: sdk: '>=3.1.0 <4.0.0'