import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:fpdart/fpdart.dart'; import 'package:hiddify/domain/connectivity/connection_status.dart'; import 'package:hiddify/domain/singbox/config_options.dart'; import 'package:hiddify/services/singbox/shared.dart'; import 'package:hiddify/services/singbox/singbox_service.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:rxdart/rxdart.dart'; class MobileSingboxService with ServiceStatus, InfraLogger implements SingboxService { late final _methodChannel = const MethodChannel("com.hiddify.app/method"); late final _statusChannel = const EventChannel("com.hiddify.app/service.status"); late final _alertsChannel = const EventChannel("com.hiddify.app/service.alerts"); late final _logsChannel = const EventChannel("com.hiddify.app/service.logs"); late final ValueStream _connectionStatus; @override Future init() async { loggy.debug("initializing"); final status = _statusChannel.receiveBroadcastStream().map(mapEventToStatus); final alerts = _alertsChannel.receiveBroadcastStream().map(mapEventToStatus); _connectionStatus = ValueConnectableStream(Rx.merge([status, alerts])).autoConnect(); await _connectionStatus.first; } @override TaskEither setup( String baseDir, String workingDir, String tempDir, ) => TaskEither.of(unit); @override TaskEither parseConfig( String path, String tempPath, bool debug, ) { return TaskEither( () async { final message = await _methodChannel.invokeMethod( "parse_config", {"path": path, "tempPath": tempPath, "debug": debug}, ); if (message == null || message.isEmpty) return right(unit); return left(message); }, ); } @override TaskEither changeConfigOptions(ConfigOptions options) { return TaskEither( () async { await _methodChannel.invokeMethod( "change_config_options", jsonEncode(options.toJson()), ); return right(unit); }, ); } @override TaskEither start(String configPath) { return TaskEither( () async { loggy.debug("starting"); await _methodChannel.invokeMethod( "start", {"path": configPath}, ); return right(unit); }, ); } @override TaskEither stop() { return TaskEither( () async { loggy.debug("stopping"); await _methodChannel.invokeMethod("stop"); return right(unit); }, ); } @override TaskEither restart(String configPath) { return TaskEither( () async { loggy.debug("restarting"); await _methodChannel.invokeMethod( "restart", {"path": configPath}, ); return right(unit); }, ); } @override Stream watchOutbounds() { const channel = EventChannel("com.hiddify.app/groups"); loggy.debug("watching outbounds"); return channel.receiveBroadcastStream().map( (event) { if (event case String _) { return event; } loggy.error("[group client] unexpected type, msg: $event"); throw "invalid type"; }, ); } @override Stream watchConnectionStatus() => _connectionStatus; @override Stream watchStats() { // TODO: implement watchStatus return const Stream.empty(); } @override TaskEither selectOutbound(String groupTag, String outboundTag) { return TaskEither( () async { loggy.debug("selecting outbound"); await _methodChannel.invokeMethod( "select_outbound", {"groupTag": groupTag, "outboundTag": outboundTag}, ); return right(unit); }, ); } @override TaskEither urlTest(String groupTag) { return TaskEither( () async { await _methodChannel.invokeMethod( "url_test", {"groupTag": groupTag}, ); return right(unit); }, ); } @override Stream> watchLogs(String path) async* { yield* _logsChannel .receiveBroadcastStream() .map((event) => (event as List).map((e) => e as String).toList()); } @override TaskEither clearLogs() { return TaskEither( () async { await _methodChannel.invokeMethod("clear_logs"); return right(unit); }, ); } }