Compare commits
1 Commits
main
...
v0.8.89-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
582e8e34d5 |
@@ -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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// IAckInterface.aidl
|
||||||
|
package com.follow.clash.service;
|
||||||
|
|
||||||
|
import com.follow.clash.service.IAckInterface;
|
||||||
|
|
||||||
|
interface IAckInterface {
|
||||||
|
oneway void onAck();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
) ??
|
) ??
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ?? '',
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user