Separate android ui and vpn

This commit is contained in:
chen08209
2024-07-13 16:36:08 +08:00
parent 53af86238e
commit c6407984ac
33 changed files with 864 additions and 525 deletions

View File

@@ -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<RunState> = MutableLiveData<RunState>(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<RunState> = MutableLiveData<RunState>(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
}
}

View File

@@ -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())

View File

@@ -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
}
}

View File

@@ -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<Int>("port")
val args = call.argument<String>("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<Int>("port")!!
val args = call.argument<String>("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<Int>("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<String>("title") as String
content = call.argument<String>("content") as String
requestNotificationsPermission()
val title = call.argument<String>("title") as String
val content = call.argument<String>("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
}
}
}

View File

@@ -1,3 +1,4 @@
package com.follow.clash.plugins
import io.flutter.embedding.engine.plugins.FlutterPlugin

View File

@@ -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> { 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()
}
}

View File

@@ -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)
}

View File

@@ -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"`

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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,
})
}

View File

@@ -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)
}
}()
}

77
core/message.go Normal file
View File

@@ -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
}

View File

@@ -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,

View File

@@ -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,
})

View File

@@ -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)
}

View File

@@ -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<Char>();
clashFFI.setCurrentProfileName(
profileNameChar,
);
malloc.free(profileNameChar);
}
getProfileName() {
final currentProfileNameRaw = clashFFI.getCurrentProfileName();
final currentProfileName =
currentProfileNameRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(currentProfileNameRaw);
return currentProfileName;
}
Future<List<Group>> getProxiesGroups() {
final proxiesRaw = clashFFI.getProxies();
final proxiesRawString = proxiesRaw.cast<Utf8>().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<Utf8>().toDartString();
// if (runTimeString.isEmpty) return null;
// return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
// }
DateTime? getRunTime() {
final runTimeRaw = clashFFI.getRunTime();
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(runTimeRaw);
if (runTimeString.isEmpty) return null;
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
}
List<Connection> getConnections() {
final connectionsDataRaw = clashFFI.getConnections();

View File

@@ -5190,6 +5190,30 @@ class ClashFFI {
_lookup<ffi.NativeFunction<ffi.Void Function()>>('forceGc');
late final _forceGc = _forceGcPtr.asFunction<void Function()>();
void setCurrentProfileName(
ffi.Pointer<ffi.Char> s,
) {
return _setCurrentProfileName(
s,
);
}
late final _setCurrentProfileNamePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'setCurrentProfileName');
late final _setCurrentProfileName = _setCurrentProfileNamePtr
.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> getCurrentProfileName() {
return _getCurrentProfileName();
}
late final _getCurrentProfileNamePtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getCurrentProfileName');
late final _getCurrentProfileName =
_getCurrentProfileNamePtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void validateConfig(
ffi.Pointer<ffi.Char> s,
int port,
@@ -5406,20 +5430,30 @@ class ClashFFI {
void initNativeApiBridge(
ffi.Pointer<ffi.Void> api,
int port,
) {
return _initNativeApiBridge(
api,
);
}
late final _initNativeApiBridgePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'initNativeApiBridge');
late final _initNativeApiBridge = _initNativeApiBridgePtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void initMessage(
int port,
) {
return _initMessage(
port,
);
}
late final _initNativeApiBridgePtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<ffi.Void>, ffi.LongLong)>>('initNativeApiBridge');
late final _initNativeApiBridge = _initNativeApiBridgePtr
.asFunction<void Function(ffi.Pointer<ffi.Void>, int)>();
late final _initMessagePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
'initMessage');
late final _initMessage = _initMessagePtr.asFunction<void Function(int)>();
void freeCString(
ffi.Pointer<ffi.Char> s,
@@ -5467,15 +5501,28 @@ class ClashFFI {
void startTUN(
int fd,
int port,
) {
return _startTUN(
fd,
port,
);
}
late final _startTUNPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int)>>('startTUN');
late final _startTUN = _startTUNPtr.asFunction<void Function(int)>();
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int, ffi.LongLong)>>(
'startTUN');
late final _startTUN = _startTUNPtr.asFunction<void Function(int, int)>();
ffi.Pointer<ffi.Char> getRunTime() {
return _getRunTime();
}
late final _getRunTimePtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getRunTime');
late final _getRunTime =
_getRunTimePtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void stopTun() {
return _stopTun();

View File

@@ -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<ClashMessageListener> _listeners =
ObserverList<ClashMessageListener>();
final ObserverList<AppMessageListener> _listeners =
ObserverList<AppMessageListener>();
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);
}
}

View File

@@ -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 = () {};
}
}

View File

@@ -28,20 +28,6 @@ class ProxyManager {
return await (_proxy as Proxy).updateStartTime();
}
Future<bool?> setProtect(int fd) async {
if (_proxy is! Proxy) return null;
return await (_proxy as Proxy).setProtect(fd);
}
Future<bool?> 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!;

View File

@@ -26,7 +26,7 @@ class AppController {
updateClashConfigDebounce = debounce<Function()>(() 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;

View File

@@ -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,
}

View File

@@ -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<void> 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<void> main() async {
@pragma('vm:entry-point')
Future<void> 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<void> 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<void> 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);
}
}

View File

@@ -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<String, Object?> json) =>
_$MessageFromJson(json);
factory AppMessage.fromJson(Map<String, Object?> json) =>
_$AppMessageFromJson(json);
}
@freezed
class ServiceMessage with _$ServiceMessage {
const factory ServiceMessage({
required ServiceMessageType type,
dynamic data,
}) = _ServiceMessage;
factory ServiceMessage.fromJson(Map<String, Object?> json) =>
_$ServiceMessageFromJson(json);
}
@freezed
@@ -121,3 +132,27 @@ class ExternalProvider with _$ExternalProvider {
factory ExternalProvider.fromJson(Map<String, Object?> 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) {}
}

View File

@@ -610,32 +610,34 @@ abstract class _ChangeProxyParams implements ChangeProxyParams {
throw _privateConstructorUsedError;
}
Message _$MessageFromJson(Map<String, dynamic> json) {
return _Message.fromJson(json);
AppMessage _$AppMessageFromJson(Map<String, dynamic> 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<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$MessageCopyWith<Message> get copyWith => throw _privateConstructorUsedError;
$AppMessageCopyWith<AppMessage> 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<String, dynamic> json) =>
_$$MessageImplFromJson(json);
factory _$AppMessageImpl.fromJson(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) = _$MessageImpl.fromJson;
factory _AppMessage.fromJson(Map<String, dynamic> 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<String, dynamic> json) {
return _ServiceMessage.fromJson(json);
}
/// @nodoc
mixin _$ServiceMessage {
ServiceMessageType get type => throw _privateConstructorUsedError;
dynamic get data => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ServiceMessageCopyWith<ServiceMessage> 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<String, dynamic> 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<String, dynamic> toJson() {
return _$$ServiceMessageImplToJson(
this,
);
}
}
abstract class _ServiceMessage implements ServiceMessage {
const factory _ServiceMessage(
{required final ServiceMessageType type,
final dynamic data}) = _$ServiceMessageImpl;
factory _ServiceMessage.fromJson(Map<String, dynamic> json) =
_$ServiceMessageImpl.fromJson;
@override
ServiceMessageType get type;
@override
dynamic get data;
@override
@JsonKey(ignore: true)
_$$ServiceMessageImplCopyWith<_$ServiceMessageImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -55,27 +55,44 @@ Map<String, dynamic> _$$ChangeProxyParamsImplToJson(
'proxy-name': instance.proxyName,
};
_$MessageImpl _$$MessageImplFromJson(Map<String, dynamic> json) =>
_$MessageImpl(
type: $enumDecode(_$MessageTypeEnumMap, json['type']),
_$AppMessageImpl _$$AppMessageImplFromJson(Map<String, dynamic> json) =>
_$AppMessageImpl(
type: $enumDecode(_$AppMessageTypeEnumMap, json['type']),
data: json['data'],
);
Map<String, dynamic> _$$MessageImplToJson(_$MessageImpl instance) =>
Map<String, dynamic> _$$AppMessageImplToJson(_$AppMessageImpl instance) =>
<String, dynamic>{
'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<String, dynamic> json) =>
_$ServiceMessageImpl(
type: $enumDecode(_$ServiceMessageTypeEnumMap, json['type']),
data: json['data'],
);
Map<String, dynamic> _$$ServiceMessageImplToJson(
_$ServiceMessageImpl instance) =>
<String, dynamic>{
'type': _$ServiceMessageTypeEnumMap[instance.type]!,
'data': instance.data,
};
const _$ServiceMessageTypeEnumMap = {
ServiceMessageType.protect: 'protect',
ServiceMessageType.process: 'process',
ServiceMessageType.started: 'started',
ServiceMessageType.loaded: 'loaded',
};
_$DelayImpl _$$DelayImplFromJson(Map<String, dynamic> json) => _$DelayImpl(

View File

@@ -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<bool?> moveTaskToBack() async {
return await methodChannel?.invokeMethod<bool>("moveTaskToBack");
return await methodChannel.invokeMethod<bool>("moveTaskToBack");
}
Future<List<Package>> getPackages() async {
final packagesString =
await methodChannel?.invokeMethod<String>("getPackages");
await methodChannel.invokeMethod<String>("getPackages");
return Isolate.run<List<Package>>(() {
final List<dynamic> packagesRaw =
packagesString != null ? json.decode(packagesString) : [];
packagesString != null ? json.decode(packagesString) : [];
return packagesRaw.map((e) => Package.fromJson(e)).toList();
});
}
Future<ImageProvider?> getPackageIcon(String packageName) async {
final base64 = await methodChannel?.invokeMethod<String>("getPackageIcon", {
final base64 = await methodChannel.invokeMethod<String>("getPackageIcon", {
"packageName": packageName,
});
if (base64 == null) {
@@ -63,19 +61,19 @@ class App {
}
Future<bool?> tip(String? message) async {
return await methodChannel?.invokeMethod<bool>("tip", {
return await methodChannel.invokeMethod<bool>("tip", {
"message": "$message",
});
}
Future<bool?> updateExcludeFromRecents(bool value) async {
return await methodChannel?.invokeMethod<bool>("updateExcludeFromRecents", {
return await methodChannel.invokeMethod<bool>("updateExcludeFromRecents", {
"value": value,
});
}
Future<String?> resolverProcess(Process process) async {
return await methodChannel?.invokeMethod<String>("resolverProcess", {
return await methodChannel.invokeMethod<String>("resolverProcess", {
"data": json.encode(process),
});
}

View File

@@ -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<bool?> _initService() async {
return await methodChannel.invokeMethod<bool>("initService");
}
handleStop() {
globalState.stopSystemProxy();
}
@override
Future<bool?> startProxy(int port, String? args) async {
Future<bool?> startProxy(port, args) async {
if (!globalState.isVpnService) {
return await _initService();
}
return await methodChannel
.invokeMethod<bool>("StartProxy", {'port': port, 'args': args});
.invokeMethod<bool>("startProxy", {'port': port, 'args': args});
}
@override
Future<bool?> stopProxy() async {
clashCore.stopTun();
final isStop = await methodChannel.invokeMethod<bool>("StopProxy");
final isStop = await methodChannel.invokeMethod<bool>("stopProxy");
if (isStop == true) {
startTime = null;
}
@@ -53,11 +64,7 @@ class Proxy extends ProxyPlatform {
}
Future<bool?> setProtect(int fd) async {
return await methodChannel.invokeMethod<bool?>("SetProtect", {'fd': fd});
}
Future<int?> getRunTimeStamp() async {
return await methodChannel.invokeMethod<int?>("GetRunTimeStamp");
return await methodChannel.invokeMethod<bool?>("setProtect", {'fd': fd});
}
Future<bool?> 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<DateTime?> 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;
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -19,6 +19,7 @@ class AndroidContainer extends StatefulWidget {
class _AndroidContainerState extends State<AndroidContainer>
with WidgetsBindingObserver {
_excludeContainer(Widget child) {
return Selector<Config, bool>(
selector: (_, config) => config.isExclude,
@@ -47,9 +48,7 @@ class _AndroidContainerState extends State<AndroidContainer>
@override
Widget build(BuildContext context) {
return _excludeContainer(
widget.child,
);
return _excludeContainer(widget.child);
}
@override

View File

@@ -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<ClashMessageContainer>
with ClashMessageListener {
with AppMessageListener {
@override
Widget build(BuildContext context) {
return widget.child;
@@ -49,25 +49,6 @@ class _ClashMessageContainerState extends State<ClashMessageContainer>
super.onLog(log);
}
@override
Future<void> 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<ClashMessageContainer>
appController.addCheckIpNumDebounce();
super.onLoaded(proxyName);
}
@override
void onStarted(String runTime) {
super.onStarted(runTime);
proxy?.updateStartTime();
final appController = globalState.appController;
appController.rawApplyProfile().then((_) {
appController.addCheckIpNumDebounce();
});
}
}

View File

@@ -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'