diff --git a/android/app/src/main/kotlin/com/follow/clash/Service.kt b/android/app/src/main/kotlin/com/follow/clash/Service.kt index 05a6589..758818a 100644 --- a/android/app/src/main/kotlin/com/follow/clash/Service.kt +++ b/android/app/src/main/kotlin/com/follow/clash/Service.kt @@ -3,6 +3,7 @@ package com.follow.clash import com.follow.clash.common.ServiceDelegate import com.follow.clash.common.formatString import com.follow.clash.common.intent +import com.follow.clash.service.IAckInterface import com.follow.clash.service.ICallbackInterface import com.follow.clash.service.IEventInterface import com.follow.clash.service.IRemoteInterface @@ -44,8 +45,11 @@ object Service { return delegate.useService { it.invokeAction( data, object : ICallbackInterface.Stub() { - override fun onResult(result: ByteArray?, isSuccess: Boolean) { + override fun onResult( + result: ByteArray?, isSuccess: Boolean, ack: IAckInterface? + ) { res.add(result ?: byteArrayOf()) + ack?.onAck() if (isSuccess) { cb(res.formatString()) } @@ -61,24 +65,24 @@ object Service { return delegate.useService { it.setEventListener( when (cb != null) { - true -> object : IEventInterface.Stub() { - override fun onEvent( - id: String, data: ByteArray?, isSuccess: Boolean - ) { - if (results[id] == null) { - results[id] = mutableListOf() - } - results[id]?.add(data ?: byteArrayOf()) - if (isSuccess) { - cb(results[id]?.formatString()) - results.remove(id) - } + true -> object : IEventInterface.Stub() { + override fun onEvent( + id: String, data: ByteArray?, isSuccess: Boolean, ack: IAckInterface? + ) { + if (results[id] == null) { + results[id] = mutableListOf() + } + results[id]?.add(data ?: byteArrayOf()) + ack?.onAck() + if (isSuccess) { + cb(results[id]?.formatString()) + results.remove(id) } } - - false -> null } - ) + + false -> null + }) } } diff --git a/android/app/src/main/kotlin/com/follow/clash/TileService.kt b/android/app/src/main/kotlin/com/follow/clash/TileService.kt index 84746cb..144be1f 100644 --- a/android/app/src/main/kotlin/com/follow/clash/TileService.kt +++ b/android/app/src/main/kotlin/com/follow/clash/TileService.kt @@ -4,8 +4,9 @@ import android.annotation.SuppressLint import android.os.Build import android.service.quicksettings.Tile import android.service.quicksettings.TileService -import com.follow.clash.common.QuickAction -import com.follow.clash.common.quickIntent +import com.follow.clash.common.Components +import com.follow.clash.common.GlobalState +import com.follow.clash.common.intent import com.follow.clash.common.toPendingIntent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -39,20 +40,22 @@ class TileService : TileService() { } @SuppressLint("StartActivityAndCollapseDeprecated") - private fun handleToggle() { - val intent = QuickAction.TOGGLE.quickIntent + private fun activityTransfer() { + val intent = Components.TEMP_ACTIVITY.intent val pendingIntent = intent.toPendingIntent if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { startActivityAndCollapse(pendingIntent) } else { - @Suppress("DEPRECATION") - startActivityAndCollapse(intent) + @Suppress("DEPRECATION") startActivityAndCollapse(intent) } } override fun onClick() { super.onClick() - handleToggle() + activityTransfer() + GlobalState.launch { + State.handleToggleAction() + } } override fun onStopListening() { diff --git a/android/common/src/main/java/com/follow/clash/common/Ext.kt b/android/common/src/main/java/com/follow/clash/common/Ext.kt index 1d27016..71773d9 100644 --- a/android/common/src/main/java/com/follow/clash/common/Ext.kt +++ b/android/common/src/main/java/com/follow/clash/common/Ext.kt @@ -220,7 +220,6 @@ val Long.formatBytes: String fun String.chunkedForAidl(charset: Charset = Charsets.UTF_8): List { val allBytes = toByteArray(charset) val total = allBytes.size - val maxBytes = when { total <= 100 * 1024 -> total total <= 1024 * 1024 -> 64 * 1024 diff --git a/android/common/src/main/java/com/follow/clash/common/Service.kt b/android/common/src/main/java/com/follow/clash/common/Service.kt index f3e7e68..09433cb 100644 --- a/android/common/src/main/java/com/follow/clash/common/Service.kt +++ b/android/common/src/main/java/com/follow/clash/common/Service.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import java.util.concurrent.atomic.AtomicBoolean @@ -59,7 +60,9 @@ class ServiceDelegate( withTimeout(timeoutMillis) { val state = serviceState.filterNotNull().first() state.first?.let { - block(it) + withContext(Dispatchers.Default) { + block(it) + } } ?: throw Exception(state.second) } } diff --git a/android/service/src/main/aidl/com/follow/clash/service/IAckInterface.aidl b/android/service/src/main/aidl/com/follow/clash/service/IAckInterface.aidl new file mode 100644 index 0000000..11efaa9 --- /dev/null +++ b/android/service/src/main/aidl/com/follow/clash/service/IAckInterface.aidl @@ -0,0 +1,8 @@ +// IAckInterface.aidl +package com.follow.clash.service; + +import com.follow.clash.service.IAckInterface; + +interface IAckInterface { + oneway void onAck(); +} \ No newline at end of file diff --git a/android/service/src/main/aidl/com/follow/clash/service/ICallbackInterface.aidl b/android/service/src/main/aidl/com/follow/clash/service/ICallbackInterface.aidl index 179ad1a..5a8cf24 100644 --- a/android/service/src/main/aidl/com/follow/clash/service/ICallbackInterface.aidl +++ b/android/service/src/main/aidl/com/follow/clash/service/ICallbackInterface.aidl @@ -1,6 +1,8 @@ // ICallbackInterface.aidl package com.follow.clash.service; +import com.follow.clash.service.IAckInterface; + interface ICallbackInterface { - oneway void onResult(in byte[] data,in boolean isSuccess); + oneway void onResult(in byte[] data,in boolean isSuccess, in IAckInterface ack); } \ No newline at end of file diff --git a/android/service/src/main/aidl/com/follow/clash/service/IEventInterface.aidl b/android/service/src/main/aidl/com/follow/clash/service/IEventInterface.aidl index 3c01bdb..87c1897 100644 --- a/android/service/src/main/aidl/com/follow/clash/service/IEventInterface.aidl +++ b/android/service/src/main/aidl/com/follow/clash/service/IEventInterface.aidl @@ -1,6 +1,8 @@ // IEventInterface.aidl package com.follow.clash.service; +import com.follow.clash.service.IAckInterface; + interface IEventInterface { - oneway void onEvent(in String id, in byte[] data,in boolean isSuccess); + oneway void onEvent(in String id, in byte[] data,in boolean isSuccess, in IAckInterface ack); } \ No newline at end of file diff --git a/android/service/src/main/java/com/follow/clash/service/RemoteService.kt b/android/service/src/main/java/com/follow/clash/service/RemoteService.kt index ec9e039..5fbb6e1 100644 --- a/android/service/src/main/java/com/follow/clash/service/RemoteService.kt +++ b/android/service/src/main/java/com/follow/clash/service/RemoteService.kt @@ -17,8 +17,10 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.sync.withLock import java.util.UUID +import kotlin.coroutines.resume class RemoteService : Service(), CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) { @@ -75,11 +77,22 @@ class RemoteService : Service(), private val binder = object : IRemoteInterface.Stub() { override fun invokeAction(data: String, callback: ICallbackInterface) { Core.invokeAction(data) { - runCatching { - val chunks = it?.chunkedForAidl() ?: listOf() - val totalSize = chunks.size - chunks.forEachIndexed { index, chunk -> - callback.onResult(chunk, totalSize - 1 == index) + launch { + runCatching { + val chunks = it?.chunkedForAidl() ?: listOf() + for ((index, chunk) in chunks.withIndex()) { + suspendCancellableCoroutine { cont -> + callback.onResult( + chunk, + index == chunks.lastIndex, + object : IAckInterface.Stub() { + override fun onAck() { + cont.resume(Unit) + } + }, + ) + } + } } } } @@ -89,6 +102,7 @@ class RemoteService : Service(), State.notificationParamsFlow.tryEmit(params) } + override fun startService( options: VpnOptions, runtime: Long, @@ -106,12 +120,24 @@ class RemoteService : Service(), GlobalState.log("RemoveEventListener ${eventListener == null}") when (eventListener != null) { true -> Core.callSetEventListener { - runCatching { - val id = UUID.randomUUID().toString() - val chunks = it?.chunkedForAidl() ?: listOf() - val totalSize = chunks.size - chunks.forEachIndexed { index, chunk -> - eventListener.onEvent(id, chunk, totalSize - 1 == index) + launch { + runCatching { + val id = UUID.randomUUID().toString() + val chunks = it?.chunkedForAidl() ?: listOf() + for ((index, chunk) in chunks.withIndex()) { + suspendCancellableCoroutine { cont -> + eventListener.onEvent( + id, + chunk, + index == chunks.lastIndex, + object : IAckInterface.Stub() { + override fun onAck() { + cont.resume(Unit) + } + }, + ) + } + } } } } diff --git a/android/service/src/main/java/com/follow/clash/service/VpnService.kt b/android/service/src/main/java/com/follow/clash/service/VpnService.kt index 5ffeb65..2194d32 100644 --- a/android/service/src/main/java/com/follow/clash/service/VpnService.kt +++ b/android/service/src/main/java/com/follow/clash/service/VpnService.kt @@ -213,6 +213,7 @@ class VpnService : SystemVpnService(), IBaseService, allowBypass() } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && options.systemProxy) { + GlobalState.log("Open http proxy") setHttpProxy( ProxyInfo.buildDirectProxy( "127.0.0.1", options.port, options.bypassDomain diff --git a/core/action.go b/core/action.go index c392070..c3e200f 100644 --- a/core/action.go +++ b/core/action.go @@ -53,8 +53,8 @@ func handleAction(action *Action, result ActionResult) { result.success(handleShutdown()) return case validateConfigMethod: - data := []byte(action.Data.(string)) - result.success(handleValidateConfig(data)) + path := action.Data.(string) + result.success(handleValidateConfig(path)) return case updateConfigMethod: data := []byte(action.Data.(string)) diff --git a/core/hub.go b/core/hub.go index 3b3175a..449615c 100644 --- a/core/hub.go +++ b/core/hub.go @@ -83,8 +83,9 @@ func handleShutdown() bool { return true } -func handleValidateConfig(bytes []byte) string { - _, err := config.UnmarshalRawConfig(bytes) +func handleValidateConfig(path string) string { + buf, err := readFile(path) + _, err = config.UnmarshalRawConfig(buf) if err != nil { return err.Error() } diff --git a/lib/common/path.dart b/lib/common/path.dart index 018e7c8..119e528 100644 --- a/lib/common/path.dart +++ b/lib/common/path.dart @@ -71,6 +71,11 @@ class AppPath { return join(homeDirPath, 'config.json'); } + Future get validateFilePath async { + final homeDirPath = await appPath.homeDirPath; + return join(homeDirPath, 'temp', 'validate${utils.id}.yaml'); + } + Future get sharedPreferencesPath async { final directory = await dataDir.future; return join(directory.path, 'shared_preferences.json'); diff --git a/lib/core/controller.dart b/lib/core/controller.dart index 0fb0b46..1cf31b6 100644 --- a/lib/core/controller.dart +++ b/lib/core/controller.dart @@ -71,8 +71,20 @@ class CoreController { FutureOr get isInit => _interface.isInit; - FutureOr validateConfig(String data) { - return _interface.validateConfig(data); + Future validateConfig(String data) async { + final path = await appPath.validateFilePath; + await globalState.genValidateFile(path, data); + final res = await _interface.validateConfig(path); + await File(path).delete(); + return res; + } + + Future validateConfigFormBytes(Uint8List bytes) async { + final path = await appPath.validateFilePath; + await globalState.genValidateFileFormBytes(path, bytes); + final res = await _interface.validateConfig(path); + await File(path).delete(); + return res; } Future updateConfig(UpdateParams updateParams) async { diff --git a/lib/core/interface.dart b/lib/core/interface.dart index a619e5f..ed01935 100644 --- a/lib/core/interface.dart +++ b/lib/core/interface.dart @@ -17,7 +17,7 @@ mixin CoreInterface { Future forceGc(); - Future validateConfig(String data); + Future validateConfig(String path); Future getConfig(String path); @@ -125,10 +125,10 @@ abstract class CoreHandlerInterface with CoreInterface { } @override - Future validateConfig(String data) async { + Future validateConfig(String path) async { return await _invoke( method: ActionMethod.validateConfig, - data: data, + data: path, ) ?? ''; } diff --git a/lib/models/profile.dart b/lib/models/profile.dart index a701b48..acbde82 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; @@ -174,7 +173,7 @@ extension ProfileExtension on Profile { } Future saveFile(Uint8List bytes) async { - final message = await coreController.validateConfig(utf8.decode(bytes)); + final message = await coreController.validateConfigFormBytes(bytes); if (message.isNotEmpty) { throw message; } @@ -182,14 +181,4 @@ extension ProfileExtension on Profile { await file.writeAsBytes(bytes); return copyWith(lastUpdateDate: DateTime.now()); } - - Future saveFileWithString(String value) async { - final message = await coreController.validateConfig(value); - if (message.isNotEmpty) { - throw message; - } - final file = await getFile(); - await file.writeAsString(value); - return copyWith(lastUpdateDate: DateTime.now()); - } } diff --git a/lib/state.dart b/lib/state.dart index 0834cd9..53b2646 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -239,7 +239,7 @@ class GlobalState { return VpnOptions( stack: config.patchClashConfig.tun.stack.name, enable: vpnProps.enable, - systemProxy: networkProps.systemProxy, + systemProxy: vpnProps.systemProxy, port: port, ipv6: vpnProps.ipv6, dnsHijacking: vpnProps.dnsHijacking, @@ -323,6 +323,42 @@ class GlobalState { } } + Future genValidateFile(String path, String data) async { + final res = await Isolate.run(() async { + try { + final file = File(path); + if (!await file.exists()) { + await file.create(recursive: true); + } + await file.writeAsString(data); + return ''; + } catch (e) { + return e.toString(); + } + }); + if (res.isNotEmpty) { + throw res; + } + } + + Future genValidateFileFormBytes(String path, Uint8List bytes) async { + final res = await Isolate.run(() async { + try { + final file = File(path); + if (!await file.exists()) { + await file.create(recursive: true); + } + await file.writeAsBytes(bytes); + return ''; + } catch (e) { + return e.toString(); + } + }); + if (res.isNotEmpty) { + throw res; + } + } + AndroidState getAndroidState() { return AndroidState( currentProfileName: config.currentProfile?.label ?? '', diff --git a/pubspec.yaml b/pubspec.yaml index 6c63a8a..21d3c6b 100755 --- 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.88+2025092301 +version: 0.8.89+2025092401 environment: sdk: '>=3.8.0 <4.0.0'