Support proxies search Support svg display Optimize config persistence Add some scenes auto close connections Update core Optimize more details
294 lines
7.3 KiB
Dart
294 lines
7.3 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:ffi';
|
|
import 'dart:io';
|
|
import 'dart:isolate';
|
|
import 'dart:ui';
|
|
|
|
import 'package:ffi/ffi.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/plugins/service.dart';
|
|
import 'package:fl_clash/state.dart';
|
|
|
|
import 'generated/clash_ffi.dart';
|
|
import 'interface.dart';
|
|
|
|
class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
|
|
static ClashLib? _instance;
|
|
Completer<bool> _canSendCompleter = Completer();
|
|
SendPort? sendPort;
|
|
final receiverPort = ReceivePort();
|
|
|
|
ClashLib._internal() {
|
|
_initService();
|
|
}
|
|
|
|
@override
|
|
preload() {
|
|
return _canSendCompleter.future;
|
|
}
|
|
|
|
_initService() async {
|
|
await service?.destroy();
|
|
_registerMainPort(receiverPort.sendPort);
|
|
receiverPort.listen((message) {
|
|
if (message is SendPort) {
|
|
if (_canSendCompleter.isCompleted) {
|
|
sendPort = null;
|
|
_canSendCompleter = Completer();
|
|
}
|
|
sendPort = message;
|
|
_canSendCompleter.complete(true);
|
|
} else {
|
|
handleResult(
|
|
ActionResult.fromJson(json.decode(
|
|
message,
|
|
)),
|
|
);
|
|
}
|
|
});
|
|
await service?.init();
|
|
}
|
|
|
|
_registerMainPort(SendPort sendPort) {
|
|
IsolateNameServer.removePortNameMapping(mainIsolate);
|
|
IsolateNameServer.registerPortWithName(sendPort, mainIsolate);
|
|
}
|
|
|
|
factory ClashLib() {
|
|
_instance ??= ClashLib._internal();
|
|
return _instance!;
|
|
}
|
|
|
|
@override
|
|
destroy() async {
|
|
await service?.destroy();
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
reStart() {
|
|
_initService();
|
|
}
|
|
|
|
@override
|
|
Future<bool> shutdown() async {
|
|
await super.shutdown();
|
|
destroy();
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
sendMessage(String message) async {
|
|
await _canSendCompleter.future;
|
|
sendPort?.send(message);
|
|
}
|
|
|
|
// @override
|
|
// Future<bool> stopTun() {
|
|
// return invoke<bool>(
|
|
// method: ActionMethod.stopTun,
|
|
// );
|
|
// }
|
|
|
|
@override
|
|
Future<AndroidVpnOptions?> getAndroidVpnOptions() async {
|
|
final res = await invoke<String>(
|
|
method: ActionMethod.getAndroidVpnOptions,
|
|
);
|
|
if (res.isEmpty) {
|
|
return null;
|
|
}
|
|
return AndroidVpnOptions.fromJson(json.decode(res));
|
|
}
|
|
|
|
@override
|
|
Future<bool> updateDns(String value) {
|
|
return invoke<bool>(
|
|
method: ActionMethod.updateDns,
|
|
data: value,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<DateTime?> getRunTime() async {
|
|
final runTimeString = await invoke<String>(
|
|
method: ActionMethod.getRunTime,
|
|
);
|
|
if (runTimeString.isEmpty) {
|
|
return null;
|
|
}
|
|
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
|
|
}
|
|
|
|
@override
|
|
Future<String> getCurrentProfileName() {
|
|
return invoke<String>(
|
|
method: ActionMethod.getCurrentProfileName,
|
|
);
|
|
}
|
|
}
|
|
|
|
class ClashLibHandler {
|
|
static ClashLibHandler? _instance;
|
|
|
|
late final ClashFFI clashFFI;
|
|
|
|
late final DynamicLibrary lib;
|
|
|
|
ClashLibHandler._internal() {
|
|
lib = DynamicLibrary.open("libclash.so");
|
|
clashFFI = ClashFFI(lib);
|
|
clashFFI.initNativeApiBridge(
|
|
NativeApi.initializeApiDLData,
|
|
);
|
|
}
|
|
|
|
factory ClashLibHandler() {
|
|
_instance ??= ClashLibHandler._internal();
|
|
return _instance!;
|
|
}
|
|
|
|
Future<String> invokeAction(String actionParams) {
|
|
final completer = Completer<String>();
|
|
final receiver = ReceivePort();
|
|
receiver.listen((message) {
|
|
if (!completer.isCompleted) {
|
|
completer.complete(message);
|
|
receiver.close();
|
|
}
|
|
});
|
|
final actionParamsChar = actionParams.toNativeUtf8().cast<Char>();
|
|
clashFFI.invokeAction(
|
|
actionParamsChar,
|
|
receiver.sendPort.nativePort,
|
|
);
|
|
malloc.free(actionParamsChar);
|
|
return completer.future;
|
|
}
|
|
|
|
attachMessagePort(int messagePort) {
|
|
clashFFI.attachMessagePort(
|
|
messagePort,
|
|
);
|
|
}
|
|
|
|
updateDns(String dns) {
|
|
final dnsChar = dns.toNativeUtf8().cast<Char>();
|
|
clashFFI.updateDns(dnsChar);
|
|
malloc.free(dnsChar);
|
|
}
|
|
|
|
setState(CoreState state) {
|
|
final stateChar = json.encode(state).toNativeUtf8().cast<Char>();
|
|
clashFFI.setState(stateChar);
|
|
malloc.free(stateChar);
|
|
}
|
|
|
|
String getCurrentProfileName() {
|
|
final currentProfileRaw = clashFFI.getCurrentProfileName();
|
|
final currentProfile = currentProfileRaw.cast<Utf8>().toDartString();
|
|
clashFFI.freeCString(currentProfileRaw);
|
|
return currentProfile;
|
|
}
|
|
|
|
AndroidVpnOptions getAndroidVpnOptions() {
|
|
final vpnOptionsRaw = clashFFI.getAndroidVpnOptions();
|
|
final vpnOptions = json.decode(vpnOptionsRaw.cast<Utf8>().toDartString());
|
|
clashFFI.freeCString(vpnOptionsRaw);
|
|
return AndroidVpnOptions.fromJson(vpnOptions);
|
|
}
|
|
|
|
Traffic getTraffic() {
|
|
final trafficRaw = clashFFI.getTraffic();
|
|
final trafficString = trafficRaw.cast<Utf8>().toDartString();
|
|
clashFFI.freeCString(trafficRaw);
|
|
if (trafficString.isEmpty) {
|
|
return Traffic();
|
|
}
|
|
return Traffic.fromMap(json.decode(trafficString));
|
|
}
|
|
|
|
Traffic getTotalTraffic(bool value) {
|
|
final trafficRaw = clashFFI.getTotalTraffic();
|
|
final trafficString = trafficRaw.cast<Utf8>().toDartString();
|
|
clashFFI.freeCString(trafficRaw);
|
|
if (trafficString.isEmpty) {
|
|
return Traffic();
|
|
}
|
|
return Traffic.fromMap(json.decode(trafficString));
|
|
}
|
|
|
|
startListener() async {
|
|
clashFFI.startListener();
|
|
return true;
|
|
}
|
|
|
|
stopListener() async {
|
|
clashFFI.stopListener();
|
|
return true;
|
|
}
|
|
|
|
DateTime? getRunTime() {
|
|
final runTimeRaw = clashFFI.getRunTime();
|
|
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
|
|
if (runTimeString.isEmpty) {
|
|
return null;
|
|
}
|
|
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
|
|
}
|
|
|
|
Future<Map<String, dynamic>> getConfig(String id) async {
|
|
final path = await appPath.getProfilePath(id);
|
|
final pathChar = path.toNativeUtf8().cast<Char>();
|
|
final configRaw = clashFFI.getConfig(pathChar);
|
|
final configString = configRaw.cast<Utf8>().toDartString();
|
|
if (configString.isEmpty) {
|
|
return {};
|
|
}
|
|
final config = json.decode(configString);
|
|
malloc.free(pathChar);
|
|
clashFFI.freeCString(configRaw);
|
|
return config;
|
|
}
|
|
|
|
Future<String> quickStart(
|
|
InitParams initParams,
|
|
SetupParams setupParams,
|
|
CoreState state,
|
|
) {
|
|
final completer = Completer<String>();
|
|
final receiver = ReceivePort();
|
|
receiver.listen((message) {
|
|
if (!completer.isCompleted) {
|
|
completer.complete(message);
|
|
receiver.close();
|
|
}
|
|
});
|
|
final params = json.encode(setupParams);
|
|
final initValue = json.encode(initParams);
|
|
final stateParams = json.encode(state);
|
|
final initParamsChar = initValue.toNativeUtf8().cast<Char>();
|
|
final paramsChar = params.toNativeUtf8().cast<Char>();
|
|
final stateParamsChar = stateParams.toNativeUtf8().cast<Char>();
|
|
clashFFI.quickStart(
|
|
initParamsChar,
|
|
paramsChar,
|
|
stateParamsChar,
|
|
receiver.sendPort.nativePort,
|
|
);
|
|
malloc.free(initParamsChar);
|
|
malloc.free(paramsChar);
|
|
malloc.free(stateParamsChar);
|
|
return completer.future;
|
|
}
|
|
}
|
|
|
|
ClashLib? get clashLib =>
|
|
Platform.isAndroid && !globalState.isService ? ClashLib() : null;
|
|
|
|
ClashLibHandler? get clashLibHandler =>
|
|
Platform.isAndroid && globalState.isService ? ClashLibHandler() : null;
|