Compare commits

...

1 Commits

Author SHA1 Message Date
chen08209
582e8e34d5 Fix some issues 2025-09-24 10:12:43 +08:00
17 changed files with 152 additions and 61 deletions

View File

@@ -3,6 +3,7 @@ package com.follow.clash
import com.follow.clash.common.ServiceDelegate import com.follow.clash.common.ServiceDelegate
import com.follow.clash.common.formatString import com.follow.clash.common.formatString
import com.follow.clash.common.intent import com.follow.clash.common.intent
import com.follow.clash.service.IAckInterface
import com.follow.clash.service.ICallbackInterface import com.follow.clash.service.ICallbackInterface
import com.follow.clash.service.IEventInterface import com.follow.clash.service.IEventInterface
import com.follow.clash.service.IRemoteInterface import com.follow.clash.service.IRemoteInterface
@@ -44,8 +45,11 @@ object Service {
return delegate.useService { return delegate.useService {
it.invokeAction( it.invokeAction(
data, object : ICallbackInterface.Stub() { data, object : ICallbackInterface.Stub() {
override fun onResult(result: ByteArray?, isSuccess: Boolean) { override fun onResult(
result: ByteArray?, isSuccess: Boolean, ack: IAckInterface?
) {
res.add(result ?: byteArrayOf()) res.add(result ?: byteArrayOf())
ack?.onAck()
if (isSuccess) { if (isSuccess) {
cb(res.formatString()) cb(res.formatString())
} }
@@ -61,24 +65,24 @@ object Service {
return delegate.useService { return delegate.useService {
it.setEventListener( it.setEventListener(
when (cb != null) { when (cb != null) {
true -> object : IEventInterface.Stub() { true -> object : IEventInterface.Stub() {
override fun onEvent( override fun onEvent(
id: String, data: ByteArray?, isSuccess: Boolean id: String, data: ByteArray?, isSuccess: Boolean, ack: IAckInterface?
) { ) {
if (results[id] == null) { if (results[id] == null) {
results[id] = mutableListOf() results[id] = mutableListOf()
} }
results[id]?.add(data ?: byteArrayOf()) results[id]?.add(data ?: byteArrayOf())
if (isSuccess) { ack?.onAck()
cb(results[id]?.formatString()) if (isSuccess) {
results.remove(id) cb(results[id]?.formatString())
} results.remove(id)
} }
} }
false -> null
} }
)
false -> null
})
} }
} }

View File

@@ -4,8 +4,9 @@ import android.annotation.SuppressLint
import android.os.Build import android.os.Build
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import com.follow.clash.common.QuickAction import com.follow.clash.common.Components
import com.follow.clash.common.quickIntent import com.follow.clash.common.GlobalState
import com.follow.clash.common.intent
import com.follow.clash.common.toPendingIntent import com.follow.clash.common.toPendingIntent
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -39,20 +40,22 @@ class TileService : TileService() {
} }
@SuppressLint("StartActivityAndCollapseDeprecated") @SuppressLint("StartActivityAndCollapseDeprecated")
private fun handleToggle() { private fun activityTransfer() {
val intent = QuickAction.TOGGLE.quickIntent val intent = Components.TEMP_ACTIVITY.intent
val pendingIntent = intent.toPendingIntent val pendingIntent = intent.toPendingIntent
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startActivityAndCollapse(pendingIntent) startActivityAndCollapse(pendingIntent)
} else { } else {
@Suppress("DEPRECATION") @Suppress("DEPRECATION") startActivityAndCollapse(intent)
startActivityAndCollapse(intent)
} }
} }
override fun onClick() { override fun onClick() {
super.onClick() super.onClick()
handleToggle() activityTransfer()
GlobalState.launch {
State.handleToggleAction()
}
} }
override fun onStopListening() { override fun onStopListening() {

View File

@@ -220,7 +220,6 @@ val Long.formatBytes: String
fun String.chunkedForAidl(charset: Charset = Charsets.UTF_8): List<ByteArray> { fun String.chunkedForAidl(charset: Charset = Charsets.UTF_8): List<ByteArray> {
val allBytes = toByteArray(charset) val allBytes = toByteArray(charset)
val total = allBytes.size val total = allBytes.size
val maxBytes = when { val maxBytes = when {
total <= 100 * 1024 -> total total <= 100 * 1024 -> total
total <= 1024 * 1024 -> 64 * 1024 total <= 1024 * 1024 -> 64 * 1024

View File

@@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@@ -59,7 +60,9 @@ class ServiceDelegate<T>(
withTimeout(timeoutMillis) { withTimeout(timeoutMillis) {
val state = serviceState.filterNotNull().first() val state = serviceState.filterNotNull().first()
state.first?.let { state.first?.let {
block(it) withContext(Dispatchers.Default) {
block(it)
}
} ?: throw Exception(state.second) } ?: throw Exception(state.second)
} }
} }

View File

@@ -0,0 +1,8 @@
// IAckInterface.aidl
package com.follow.clash.service;
import com.follow.clash.service.IAckInterface;
interface IAckInterface {
oneway void onAck();
}

View File

@@ -1,6 +1,8 @@
// ICallbackInterface.aidl // ICallbackInterface.aidl
package com.follow.clash.service; package com.follow.clash.service;
import com.follow.clash.service.IAckInterface;
interface ICallbackInterface { interface ICallbackInterface {
oneway void onResult(in byte[] data,in boolean isSuccess); oneway void onResult(in byte[] data,in boolean isSuccess, in IAckInterface ack);
} }

View File

@@ -1,6 +1,8 @@
// IEventInterface.aidl // IEventInterface.aidl
package com.follow.clash.service; package com.follow.clash.service;
import com.follow.clash.service.IAckInterface;
interface IEventInterface { 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);
} }

View File

@@ -17,8 +17,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import java.util.UUID import java.util.UUID
import kotlin.coroutines.resume
class RemoteService : Service(), class RemoteService : Service(),
CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) { CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
@@ -75,11 +77,22 @@ class RemoteService : Service(),
private val binder = object : IRemoteInterface.Stub() { private val binder = object : IRemoteInterface.Stub() {
override fun invokeAction(data: String, callback: ICallbackInterface) { override fun invokeAction(data: String, callback: ICallbackInterface) {
Core.invokeAction(data) { Core.invokeAction(data) {
runCatching { launch {
val chunks = it?.chunkedForAidl() ?: listOf() runCatching {
val totalSize = chunks.size val chunks = it?.chunkedForAidl() ?: listOf()
chunks.forEachIndexed { index, chunk -> for ((index, chunk) in chunks.withIndex()) {
callback.onResult(chunk, totalSize - 1 == index) 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) State.notificationParamsFlow.tryEmit(params)
} }
override fun startService( override fun startService(
options: VpnOptions, options: VpnOptions,
runtime: Long, runtime: Long,
@@ -106,12 +120,24 @@ class RemoteService : Service(),
GlobalState.log("RemoveEventListener ${eventListener == null}") GlobalState.log("RemoveEventListener ${eventListener == null}")
when (eventListener != null) { when (eventListener != null) {
true -> Core.callSetEventListener { true -> Core.callSetEventListener {
runCatching { launch {
val id = UUID.randomUUID().toString() runCatching {
val chunks = it?.chunkedForAidl() ?: listOf() val id = UUID.randomUUID().toString()
val totalSize = chunks.size val chunks = it?.chunkedForAidl() ?: listOf()
chunks.forEachIndexed { index, chunk -> for ((index, chunk) in chunks.withIndex()) {
eventListener.onEvent(id, chunk, totalSize - 1 == index) suspendCancellableCoroutine { cont ->
eventListener.onEvent(
id,
chunk,
index == chunks.lastIndex,
object : IAckInterface.Stub() {
override fun onAck() {
cont.resume(Unit)
}
},
)
}
}
} }
} }
} }

View File

@@ -213,6 +213,7 @@ class VpnService : SystemVpnService(), IBaseService,
allowBypass() allowBypass()
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && options.systemProxy) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && options.systemProxy) {
GlobalState.log("Open http proxy")
setHttpProxy( setHttpProxy(
ProxyInfo.buildDirectProxy( ProxyInfo.buildDirectProxy(
"127.0.0.1", options.port, options.bypassDomain "127.0.0.1", options.port, options.bypassDomain

View File

@@ -53,8 +53,8 @@ func handleAction(action *Action, result ActionResult) {
result.success(handleShutdown()) result.success(handleShutdown())
return return
case validateConfigMethod: case validateConfigMethod:
data := []byte(action.Data.(string)) path := action.Data.(string)
result.success(handleValidateConfig(data)) result.success(handleValidateConfig(path))
return return
case updateConfigMethod: case updateConfigMethod:
data := []byte(action.Data.(string)) data := []byte(action.Data.(string))

View File

@@ -83,8 +83,9 @@ func handleShutdown() bool {
return true return true
} }
func handleValidateConfig(bytes []byte) string { func handleValidateConfig(path string) string {
_, err := config.UnmarshalRawConfig(bytes) buf, err := readFile(path)
_, err = config.UnmarshalRawConfig(buf)
if err != nil { if err != nil {
return err.Error() return err.Error()
} }

View File

@@ -71,6 +71,11 @@ class AppPath {
return join(homeDirPath, 'config.json'); return join(homeDirPath, 'config.json');
} }
Future<String> get validateFilePath async {
final homeDirPath = await appPath.homeDirPath;
return join(homeDirPath, 'temp', 'validate${utils.id}.yaml');
}
Future<String> get sharedPreferencesPath async { Future<String> get sharedPreferencesPath async {
final directory = await dataDir.future; final directory = await dataDir.future;
return join(directory.path, 'shared_preferences.json'); return join(directory.path, 'shared_preferences.json');

View File

@@ -71,8 +71,20 @@ class CoreController {
FutureOr<bool> get isInit => _interface.isInit; FutureOr<bool> get isInit => _interface.isInit;
FutureOr<String> validateConfig(String data) { Future<String> validateConfig(String data) async {
return _interface.validateConfig(data); final path = await appPath.validateFilePath;
await globalState.genValidateFile(path, data);
final res = await _interface.validateConfig(path);
await File(path).delete();
return res;
}
Future<String> 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<String> updateConfig(UpdateParams updateParams) async { Future<String> updateConfig(UpdateParams updateParams) async {

View File

@@ -17,7 +17,7 @@ mixin CoreInterface {
Future<bool> forceGc(); Future<bool> forceGc();
Future<String> validateConfig(String data); Future<String> validateConfig(String path);
Future<Result> getConfig(String path); Future<Result> getConfig(String path);
@@ -125,10 +125,10 @@ abstract class CoreHandlerInterface with CoreInterface {
} }
@override @override
Future<String> validateConfig(String data) async { Future<String> validateConfig(String path) async {
return await _invoke<String>( return await _invoke<String>(
method: ActionMethod.validateConfig, method: ActionMethod.validateConfig,
data: data, data: path,
) ?? ) ??
''; '';
} }

View File

@@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
@@ -174,7 +173,7 @@ extension ProfileExtension on Profile {
} }
Future<Profile> saveFile(Uint8List bytes) async { Future<Profile> saveFile(Uint8List bytes) async {
final message = await coreController.validateConfig(utf8.decode(bytes)); final message = await coreController.validateConfigFormBytes(bytes);
if (message.isNotEmpty) { if (message.isNotEmpty) {
throw message; throw message;
} }
@@ -182,14 +181,4 @@ extension ProfileExtension on Profile {
await file.writeAsBytes(bytes); await file.writeAsBytes(bytes);
return copyWith(lastUpdateDate: DateTime.now()); return copyWith(lastUpdateDate: DateTime.now());
} }
Future<Profile> 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());
}
} }

View File

@@ -239,7 +239,7 @@ class GlobalState {
return VpnOptions( return VpnOptions(
stack: config.patchClashConfig.tun.stack.name, stack: config.patchClashConfig.tun.stack.name,
enable: vpnProps.enable, enable: vpnProps.enable,
systemProxy: networkProps.systemProxy, systemProxy: vpnProps.systemProxy,
port: port, port: port,
ipv6: vpnProps.ipv6, ipv6: vpnProps.ipv6,
dnsHijacking: vpnProps.dnsHijacking, dnsHijacking: vpnProps.dnsHijacking,
@@ -323,6 +323,42 @@ class GlobalState {
} }
} }
Future<void> genValidateFile(String path, String data) async {
final res = await Isolate.run<String>(() 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<void> genValidateFileFormBytes(String path, Uint8List bytes) async {
final res = await Isolate.run<String>(() 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() { AndroidState getAndroidState() {
return AndroidState( return AndroidState(
currentProfileName: config.currentProfile?.label ?? '', currentProfileName: config.currentProfile?.label ?? '',

View File

@@ -1,7 +1,7 @@
name: fl_clash name: fl_clash
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
publish_to: 'none' publish_to: 'none'
version: 0.8.88+2025092301 version: 0.8.89+2025092401
environment: environment:
sdk: '>=3.8.0 <4.0.0' sdk: '>=3.8.0 <4.0.0'