core_controller.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'package:dio/dio.dart';
  4. import 'package:get/get.dart';
  5. import 'package:uuid/uuid.dart';
  6. import '../../config/translations/strings_enum.dart';
  7. import '../../pigeons/core_api.g.dart';
  8. import '../../utils/haptic_feedback_manager.dart';
  9. import '../../utils/log/logger.dart';
  10. import '../components/ix_snackbar.dart';
  11. import '../constants/enums.dart';
  12. import '../data/models/api_exception.dart';
  13. import '../data/models/failure.dart';
  14. import '../data/models/vpn_message.dart';
  15. import '../data/sp/ix_sp.dart';
  16. import '../dialog/error_dialog.dart';
  17. import '../dialog/feedback_bottom_sheet.dart';
  18. import 'api_controller.dart';
  19. class CoreController extends GetxService {
  20. final TAG = 'CoreController';
  21. final _apiController = Get.find<ApiController>();
  22. final _state = ConnectionState.disconnected.obs;
  23. ConnectionState get state => _state.value;
  24. set state(ConnectionState value) => _state.value = value;
  25. // 公开状态流供外部监听
  26. Rx<ConnectionState> get stateStream => _state;
  27. final _timer = "00:00:00".obs;
  28. String get timer => _timer.value;
  29. set timer(String value) => _timer.value = value;
  30. // VPN 事件流订阅
  31. StreamSubscription<String>? _eventSubscription;
  32. CancelToken? _cancelToken;
  33. @override
  34. void onInit() {
  35. super.onInit();
  36. _initCheckConnect();
  37. _startListeningToEvents();
  38. }
  39. @override
  40. void onClose() {
  41. super.onClose();
  42. // 取消事件流订阅
  43. _eventSubscription?.cancel();
  44. _eventSubscription = null;
  45. }
  46. void _initCheckConnect() {
  47. CoreApi().isConnected().then((value) {
  48. if (value == true) {
  49. state = ConnectionState.connected;
  50. CoreApi().reconnect();
  51. } else {
  52. state = ConnectionState.disconnected;
  53. }
  54. });
  55. }
  56. void handleConnection() {
  57. if (state == ConnectionState.disconnected) {
  58. // 开始连接 - 轻微震动
  59. state = ConnectionState.connecting;
  60. HapticFeedbackManager.connectionStart();
  61. getDispatchInfo();
  62. } else {
  63. // 断开连接
  64. CoreApi().disconnect();
  65. }
  66. }
  67. void selectLocationConnect() {
  68. if (state != ConnectionState.disconnected) {
  69. CoreApi().disconnect();
  70. // 延迟300ms
  71. Future.delayed(const Duration(milliseconds: 300), () {
  72. state = ConnectionState.connecting;
  73. getDispatchInfo();
  74. });
  75. } else {
  76. handleConnection();
  77. }
  78. }
  79. Future<void> getDispatchInfo() async {
  80. // 如果正在请求中,取消当前请求
  81. if (_cancelToken != null) {
  82. log(TAG, '取消当前请求,重新发起新请求');
  83. _cancelToken?.cancel('取消旧请求,发起新请求');
  84. }
  85. // 创建新的 CancelToken
  86. final currentToken = CancelToken();
  87. _cancelToken = currentToken;
  88. try {
  89. final locationId = IXSP.getSelectedLocation()?['id'];
  90. final locationCode = IXSP.getSelectedLocation()?['code'];
  91. final launch = await _apiController.getDispatchInfo(
  92. locationId,
  93. locationCode,
  94. cancelToken: currentToken,
  95. );
  96. // 只有当前 token 没有被替换时才清空
  97. if (_cancelToken == currentToken) {
  98. _cancelToken = null;
  99. }
  100. if (state == ConnectionState.connecting) {
  101. final sessionId = Uuid().v4();
  102. final socksPort = launch.nodesConfig!.socketPort!;
  103. final tunnelConfig = launch.nodesConfig!.tunnelConfig!;
  104. final configJson = jsonEncode(launch.nodesConfig!);
  105. CoreApi().connect(sessionId, socksPort, tunnelConfig, configJson);
  106. }
  107. } on DioException catch (e, s) {
  108. // 只有当前 token 没有被替换时才清空
  109. if (_cancelToken == currentToken) {
  110. _cancelToken = null;
  111. }
  112. // 如果是取消错误,不处理
  113. if (e.type == DioExceptionType.cancel) {
  114. log(TAG, '请求已取消');
  115. return;
  116. }
  117. if (state == ConnectionState.connecting) {
  118. state = ConnectionState.disconnected;
  119. }
  120. handleErrorDialog(e, s);
  121. log(TAG, 'getDispatchInfo error: $e');
  122. } catch (e, s) {
  123. // 只有当前 token 没有被替换时才清空
  124. if (_cancelToken == currentToken) {
  125. _cancelToken = null;
  126. }
  127. if (state == ConnectionState.connecting) {
  128. state = ConnectionState.disconnected;
  129. }
  130. handleErrorDialog(e, s);
  131. log(TAG, 'getDispatchInfo error: $e');
  132. }
  133. }
  134. /// 开始监听来自 Android 的事件
  135. void _startListeningToEvents() {
  136. _eventSubscription = onEventChange().listen(
  137. _handleEventChange,
  138. onError: (error) {
  139. log(TAG, '事件流错误: $error');
  140. },
  141. );
  142. }
  143. // 处理从原生端接收到的消息
  144. void _handleEventChange(String message) {
  145. try {
  146. final Map<String, dynamic> json = jsonDecode(message);
  147. final String type = json['type'] ?? '';
  148. switch (type) {
  149. case 'vpn_status':
  150. _handleVpnStatus(VpnStatusMessage.fromJson(json));
  151. break;
  152. case 'timer_update':
  153. _handleTimerUpdate(TimerUpdateMessage.fromJson(json));
  154. break;
  155. default:
  156. log(TAG, '未知消息类型: $type');
  157. }
  158. } catch (e) {
  159. log(TAG, '解析消息失败: $e');
  160. }
  161. }
  162. void _handleVpnStatus(VpnStatusMessage message) {
  163. final vpnError = VpnStatus.fromValue(message.status);
  164. log(
  165. TAG,
  166. 'VPN状态变化: ${vpnError.label}, status=${message.status}, message=${message.message}',
  167. );
  168. // 根据状态码处理不同的VPN状态
  169. switch (vpnError) {
  170. case VpnStatus.idle:
  171. // disconnected
  172. _onVpnDisconnected();
  173. break;
  174. case VpnStatus.connecting:
  175. // connecting
  176. _onVpnConnecting();
  177. break;
  178. case VpnStatus.connected:
  179. // connected
  180. _onVpnConnected();
  181. break;
  182. case VpnStatus.error:
  183. // error
  184. _onVpnError();
  185. break;
  186. case VpnStatus.serviceDisconnected:
  187. // service disconnected
  188. _onVpnServiceDisconnected();
  189. break;
  190. case VpnStatus.permissionDenied:
  191. // permission denied
  192. _onVpnPermissionDenied();
  193. break;
  194. }
  195. }
  196. void _handleTimerUpdate(TimerUpdateMessage message) {
  197. log(
  198. TAG,
  199. '计时更新: time=${message.currentTime}, mode=${message.mode}, running=${message.isRunning}, paused=${message.isPaused}',
  200. );
  201. timer = _formatTime(message.currentTime);
  202. // 处理计时更新
  203. if (message.isRunning) {
  204. if (message.isPaused) {
  205. _onTimerPaused(message.currentTime, message.mode);
  206. } else {
  207. _onTimerRunning(message.currentTime, message.mode);
  208. }
  209. } else {
  210. _onTimerStopped();
  211. }
  212. }
  213. // VPN状态处理方法
  214. void _onVpnDisconnected() {
  215. log(TAG, 'VPN已断开连接');
  216. // 更新UI状态
  217. state = ConnectionState.disconnected;
  218. timer = "00:00:00";
  219. HapticFeedbackManager.connectionDisconnected();
  220. FeedbackBottomSheet.show();
  221. }
  222. void _onVpnConnecting() {
  223. log(TAG, 'VPN正在连接');
  224. // 显示连接中状态
  225. state = ConnectionState.connecting;
  226. }
  227. void _onVpnConnected() {
  228. log(TAG, 'VPN已连接');
  229. // 显示已连接状态
  230. state = ConnectionState.connected;
  231. HapticFeedbackManager.connectionSuccess();
  232. }
  233. void _onVpnError() {
  234. log(TAG, 'VPN连接错误');
  235. // 显示错误信息
  236. state = ConnectionState.disconnected;
  237. timer = "00:00:00";
  238. HapticFeedbackManager.connectionDisconnected();
  239. ErrorDialog.show(message: Strings.vpnConnectionError.tr);
  240. }
  241. void _onVpnServiceDisconnected() {
  242. log(TAG, 'VPN服务异常断开连接');
  243. // 显示错误信息
  244. state = ConnectionState.disconnected;
  245. timer = "00:00:00";
  246. HapticFeedbackManager.connectionDisconnected();
  247. // 可以显示错误提示
  248. ErrorDialog.show(message: Strings.vpnServiceDisconnected.tr);
  249. }
  250. void _onVpnPermissionDenied() {
  251. log(TAG, 'VPN权限拒绝');
  252. // 显示权限拒绝状态
  253. state = ConnectionState.disconnected;
  254. HapticFeedbackManager.connectionDisconnected();
  255. // 可以显示错误提示
  256. ErrorDialog.show(message: '权限拒绝');
  257. }
  258. // 计时器状态处理方法
  259. void _onTimerRunning(int currentTime, int mode) {
  260. log(
  261. TAG,
  262. '计时器运行中: ${_formatTime(currentTime)}, 模式: ${mode == 0 ? "普通计时" : "倒计时"}',
  263. );
  264. }
  265. void _onTimerPaused(int currentTime, int mode) {
  266. log(
  267. TAG,
  268. '计时器已暂停: ${_formatTime(currentTime)}, 模式: ${mode == 0 ? "普通计时" : "倒计时"}',
  269. );
  270. }
  271. void _onTimerStopped() {
  272. log(TAG, '计时器已停止');
  273. }
  274. // 格式化时间显示
  275. String _formatTime(int timeMs) {
  276. final totalSeconds = (timeMs / 1000).abs().round();
  277. final days = totalSeconds ~/ 86400; // 86400 = 24 * 3600
  278. final hours = (totalSeconds % 86400) ~/ 3600;
  279. final minutes = (totalSeconds % 3600) ~/ 60;
  280. final seconds = totalSeconds % 60;
  281. if (days > 0) {
  282. return '$days days ${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
  283. } else if (hours > 0) {
  284. return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
  285. } else {
  286. return '00:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
  287. }
  288. }
  289. void handleSnackBarError(dynamic error, StackTrace stackTrace) {
  290. if (error is ApiException) {
  291. IXSnackBar.showIXErrorSnackBar(
  292. title: Strings.error.tr,
  293. message: error.message,
  294. );
  295. } else if (error is Failure) {
  296. IXSnackBar.showIXErrorSnackBar(
  297. title: Strings.error.tr,
  298. message: error.message ?? Strings.unknownError.tr,
  299. );
  300. } else if (error is DioException) {
  301. switch (error.type) {
  302. case DioExceptionType.connectionError:
  303. case DioExceptionType.connectionTimeout:
  304. case DioExceptionType.receiveTimeout:
  305. case DioExceptionType.sendTimeout:
  306. IXSnackBar.showIXErrorSnackBar(
  307. title: Strings.error.tr,
  308. message: Strings.unableToConnectNetwork.tr,
  309. );
  310. break;
  311. default:
  312. IXSnackBar.showIXErrorSnackBar(
  313. title: Strings.error.tr,
  314. message: Strings.unableToConnectServer.tr,
  315. );
  316. }
  317. } else {
  318. IXSnackBar.showIXErrorSnackBar(
  319. title: Strings.error.tr,
  320. message: error.toString(),
  321. );
  322. }
  323. }
  324. void handleErrorDialog(dynamic error, StackTrace stackTrace) {
  325. if (error is ApiException) {
  326. ErrorDialog.show(title: Strings.error.tr, message: error.message);
  327. } else if (error is Failure) {
  328. ErrorDialog.show(
  329. title: Strings.error.tr,
  330. message: error.message ?? Strings.unknownError.tr,
  331. );
  332. } else if (error is DioException) {
  333. switch (error.type) {
  334. case DioExceptionType.connectionError:
  335. case DioExceptionType.connectionTimeout:
  336. case DioExceptionType.receiveTimeout:
  337. case DioExceptionType.sendTimeout:
  338. ErrorDialog.show(
  339. title: Strings.error.tr,
  340. message: Strings.unableToConnectNetwork.tr,
  341. );
  342. break;
  343. default:
  344. ErrorDialog.show(
  345. title: Strings.error.tr,
  346. message: Strings.unableToConnectServer.tr,
  347. );
  348. }
  349. } else {
  350. ErrorDialog.show(title: Strings.error.tr, message: error.toString());
  351. }
  352. }
  353. }