core_controller.dart 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:io';
  4. import 'package:device_info_plus/device_info_plus.dart';
  5. import 'package:dio/dio.dart';
  6. import 'package:get/get.dart';
  7. import 'package:nomo/app/constants/api_domains.dart';
  8. import 'package:nomo/app/constants/keys.dart';
  9. import 'package:package_info_plus/package_info_plus.dart';
  10. import 'package:uuid/uuid.dart';
  11. import '../../config/translations/strings_enum.dart';
  12. import '../../pigeons/core_api.g.dart';
  13. import '../../utils/boost_report_manager.dart';
  14. import '../../utils/haptic_feedback_manager.dart';
  15. import '../../utils/log/logger.dart';
  16. import '../../utils/network_helper.dart';
  17. import '../components/ix_snackbar.dart';
  18. import '../constants/enums.dart';
  19. import '../constants/errors.dart';
  20. import '../data/models/api_exception.dart';
  21. import '../data/models/failure.dart';
  22. import '../data/models/vpn_message.dart';
  23. import '../data/sp/ix_sp.dart';
  24. import '../dialog/error_dialog.dart';
  25. import '../dialog/feedback_bottom_sheet.dart';
  26. import 'api_controller.dart';
  27. class CoreController extends GetxService {
  28. final TAG = 'CoreController';
  29. final _apiController = Get.find<ApiController>();
  30. final _state = ConnectionState.disconnected.obs;
  31. ConnectionState get state => _state.value;
  32. set state(ConnectionState value) => _state.value = value;
  33. // 公开状态流供外部监听
  34. Rx<ConnectionState> get stateStream => _state;
  35. final _timer = "00:00:00".obs;
  36. String get timer => _timer.value;
  37. set timer(String value) => _timer.value = value;
  38. // VPN 事件流订阅
  39. StreamSubscription<String>? _eventSubscription;
  40. CancelToken? _cancelToken;
  41. //全局uuid
  42. final _globalUuid = Uuid().v4();
  43. String locationSelectionType = 'auto';
  44. @override
  45. void onInit() {
  46. super.onInit();
  47. _initCheckConnect();
  48. _startListeningToEvents();
  49. }
  50. @override
  51. void onClose() {
  52. super.onClose();
  53. // 取消事件流订阅
  54. _eventSubscription?.cancel();
  55. _eventSubscription = null;
  56. }
  57. void _initCheckConnect() {
  58. CoreApi().isConnected().then((value) {
  59. if (value == true) {
  60. state = ConnectionState.connected;
  61. } else {
  62. state = ConnectionState.disconnected;
  63. }
  64. });
  65. }
  66. void handleConnection() {
  67. if (state == ConnectionState.disconnected) {
  68. // 开始连接 - 轻微震动
  69. state = ConnectionState.connecting;
  70. HapticFeedbackManager.connectionStart();
  71. getDispatchInfo();
  72. } else {
  73. // 断开连接
  74. CoreApi().disconnect();
  75. }
  76. }
  77. void selectLocationConnect() {
  78. if (state != ConnectionState.disconnected) {
  79. CoreApi().disconnect();
  80. // 延迟300ms
  81. Future.delayed(const Duration(milliseconds: 300), () {
  82. log(TAG, 'selectLocationConnect disconnected = $state');
  83. state = ConnectionState.connecting;
  84. getDispatchInfo();
  85. });
  86. } else {
  87. handleConnection();
  88. }
  89. }
  90. Future<void> getDispatchInfo() async {
  91. // 如果正在请求中,取消当前请求
  92. if (_cancelToken != null) {
  93. log(TAG, '取消当前请求,重新发起新请求');
  94. _cancelToken?.cancel('取消旧请求,发起新请求');
  95. }
  96. // 创建新的 CancelToken
  97. final currentToken = CancelToken();
  98. _cancelToken = currentToken;
  99. // 创建一条加速日志
  100. await createBoostLog();
  101. try {
  102. final locationId = IXSP.getSelectedLocation()?['id'];
  103. final locationCode = IXSP.getSelectedLocation()?['code'];
  104. final launch = await _apiController.getDispatchInfo(
  105. locationId,
  106. locationCode,
  107. cancelToken: currentToken,
  108. );
  109. // 只有当前 token 没有被替换时才清空
  110. if (_cancelToken == currentToken) {
  111. _cancelToken = null;
  112. }
  113. final remainTime = launch.userConfig?.remainTime ?? 0;
  114. if (remainTime <= 0) {
  115. _onVpnError(Errors.ERROR_REMAIN_TIME, Strings.remainTimeEnded.tr);
  116. return;
  117. }
  118. if (state == ConnectionState.connecting) {
  119. final sessionId = Uuid().v4();
  120. final socksPort = launch.nodesConfig!.socketPort!;
  121. final tunnelConfig = launch.nodesConfig!.tunnelConfig!;
  122. final configJson = jsonEncode(launch.nodesConfig!);
  123. // 根据分流隧道设置获取 allowVpnApps 和 disallowVpnApps
  124. final splitTunnelingApps = _getSplitTunnelingApps();
  125. final allowVpnApps = splitTunnelingApps['allowVpnApps']!;
  126. final disallowVpnApps = splitTunnelingApps['disallowVpnApps']!;
  127. final accessToken = launch.userConfig?.accessToken ?? '';
  128. final peekTimeInterval = launch.appConfig?.peekTimeInterval ?? 600;
  129. final aesKey = Keys.aesKey;
  130. final aesIv = Keys.aesIv;
  131. final baseUrls = ApiDomains.instance.getAllLogUrls();
  132. final params = jsonEncode(_apiController.fp);
  133. CoreApi().connect(
  134. sessionId,
  135. socksPort,
  136. tunnelConfig,
  137. configJson,
  138. remainTime,
  139. false,
  140. allowVpnApps,
  141. disallowVpnApps,
  142. accessToken,
  143. aesKey,
  144. aesIv,
  145. locationId,
  146. locationCode,
  147. baseUrls,
  148. params,
  149. peekTimeInterval,
  150. );
  151. }
  152. } on DioException catch (e, s) {
  153. // 只有当前 token 没有被替换时才清空
  154. if (_cancelToken == currentToken) {
  155. _cancelToken = null;
  156. }
  157. // 如果是取消错误,不处理
  158. if (e.type == DioExceptionType.cancel) {
  159. log(TAG, '请求已取消');
  160. return;
  161. }
  162. if (state == ConnectionState.connecting) {
  163. state = ConnectionState.disconnected;
  164. }
  165. handleErrorDialog(e, s);
  166. log(TAG, 'getDispatchInfo error: $e');
  167. } catch (e, s) {
  168. // 只有当前 token 没有被替换时才清空
  169. if (_cancelToken == currentToken) {
  170. _cancelToken = null;
  171. }
  172. if (state == ConnectionState.connecting) {
  173. state = ConnectionState.disconnected;
  174. }
  175. handleErrorDialog(e, s);
  176. log(TAG, 'getDispatchInfo error: $e');
  177. }
  178. }
  179. /// 根据分流隧道设置获取 allowVpnApps 和 disallowVpnApps
  180. /// 直接从 IXSP 读取,参考 SplittunnelingController 的逻辑
  181. Map<String, List<String>> _getSplitTunnelingApps() {
  182. List<String> allowVpnApps = [];
  183. List<String> disallowVpnApps = [];
  184. try {
  185. // 读取选中的模式
  186. final modeString = IXSP.getString('splittunneling_selected_mode');
  187. if (modeString == null) {
  188. log(TAG, '分流隧道未设置模式');
  189. return {
  190. 'allowVpnApps': allowVpnApps,
  191. 'disallowVpnApps': disallowVpnApps,
  192. };
  193. }
  194. // 判断模式类型
  195. final isExcludeMode = modeString.contains('exclude');
  196. final isIncludeMode = modeString.contains('include');
  197. if (!isExcludeMode && !isIncludeMode) {
  198. log(TAG, '分流隧道模式为 none');
  199. return {
  200. 'allowVpnApps': allowVpnApps,
  201. 'disallowVpnApps': disallowVpnApps,
  202. };
  203. }
  204. // 根据模式获取对应的应用列表
  205. final key = isExcludeMode
  206. ? 'splittunneling_exclude_selected_apps'
  207. : 'splittunneling_include_selected_apps';
  208. final selectedAppsJson = IXSP.getString(key);
  209. if (selectedAppsJson != null) {
  210. final selectedPackageNames =
  211. (jsonDecode(selectedAppsJson) as List<dynamic>).cast<String>();
  212. if (isIncludeMode) {
  213. // include 模式:只有选中的应用走 VPN
  214. allowVpnApps = selectedPackageNames;
  215. log(TAG, '分流隧道 include 模式,允许的应用: $allowVpnApps');
  216. } else {
  217. // exclude 模式:选中的应用不走 VPN
  218. disallowVpnApps = selectedPackageNames;
  219. log(TAG, '分流隧道 exclude 模式,排除的应用: $disallowVpnApps');
  220. }
  221. }
  222. } catch (e) {
  223. log(TAG, '获取分流隧道设置失败: $e');
  224. }
  225. return {'allowVpnApps': allowVpnApps, 'disallowVpnApps': disallowVpnApps};
  226. }
  227. /// 开始监听来自 Android 的事件
  228. void _startListeningToEvents() {
  229. _eventSubscription = onEventChange().listen(
  230. _handleEventChange,
  231. onError: (error) {
  232. log(TAG, '事件流错误: $error');
  233. },
  234. );
  235. }
  236. // 处理从原生端接收到的消息
  237. void _handleEventChange(String message) {
  238. try {
  239. final Map<String, dynamic> json = jsonDecode(message);
  240. final String type = json['type'] ?? '';
  241. switch (type) {
  242. case 'vpn_status':
  243. _handleVpnStatus(VpnStatusMessage.fromJson(json));
  244. break;
  245. case 'timer_update':
  246. _handleTimerUpdate(TimerUpdateMessage.fromJson(json));
  247. break;
  248. case 'boost_result':
  249. _handleBoostResult(BoostResultMessage.fromJson(json));
  250. break;
  251. default:
  252. log(TAG, '未知消息类型: $type');
  253. }
  254. } catch (e) {
  255. log(TAG, '解析消息失败: $e');
  256. }
  257. }
  258. void _handleVpnStatus(VpnStatusMessage message) {
  259. final vpnError = VpnStatus.fromValue(message.status);
  260. log(
  261. TAG,
  262. 'VPN状态变化: ${vpnError.label}, status=${message.status}, code=${message.code}, message=${message.message}',
  263. );
  264. // 根据状态码处理不同的VPN状态
  265. switch (vpnError) {
  266. case VpnStatus.idle:
  267. // disconnected
  268. _onVpnDisconnected();
  269. break;
  270. case VpnStatus.connecting:
  271. // connecting
  272. _onVpnConnecting();
  273. break;
  274. case VpnStatus.connected:
  275. // connected
  276. _onVpnConnected();
  277. break;
  278. case VpnStatus.error:
  279. // error
  280. _onVpnError(message.code, message.message);
  281. break;
  282. }
  283. }
  284. void _handleTimerUpdate(TimerUpdateMessage message) {
  285. // log(
  286. // TAG,
  287. // '计时更新: time=${message.currentTime}, mode=${message.mode}, running=${message.isRunning}, paused=${message.isPaused}',
  288. // );
  289. timer = _formatTime(message.currentTime);
  290. // 处理计时更新
  291. if (message.isRunning) {
  292. if (message.isPaused) {
  293. _onTimerPaused(message.currentTime, message.mode);
  294. } else {
  295. _onTimerRunning(message.currentTime, message.mode);
  296. }
  297. } else {
  298. _onTimerStopped();
  299. }
  300. }
  301. void _handleBoostResult(BoostResultMessage message) async {
  302. log(TAG, '加速结果: ${message.toString()}');
  303. if (message.success) {
  304. try {
  305. await _apiController.connected({
  306. 'locationCode': message.locationCode,
  307. 'instanceId': message.nodeId,
  308. });
  309. } catch (e) {
  310. log('handleRouterConnected error: $e');
  311. }
  312. }
  313. try {
  314. final json = jsonDecode(message.param);
  315. await _apiController.uploadLogs(json);
  316. } catch (e) {
  317. log('handleBoostResult error: $e');
  318. }
  319. }
  320. // VPN状态处理方法
  321. void _onVpnDisconnected() {
  322. log(TAG, 'VPN已断开连接');
  323. // 更新UI状态
  324. _uninitState();
  325. // FeedbackBottomSheet.show();
  326. }
  327. void _onVpnConnecting() {
  328. log(TAG, 'VPN正在连接');
  329. // 显示连接中状态
  330. state = ConnectionState.connecting;
  331. }
  332. void _onVpnConnected() {
  333. log(TAG, 'VPN已连接');
  334. // 显示已连接状态
  335. state = ConnectionState.connected;
  336. HapticFeedbackManager.connectionSuccess();
  337. }
  338. void _onVpnError(int code, String message) {
  339. log(TAG, 'VPN连接错误: code=$code, message=$message');
  340. // 显示错误信息
  341. _uninitState();
  342. showErrorDialog(code, message);
  343. }
  344. void showErrorDialog(int code, String message) {
  345. var errorMessage = message;
  346. switch (code) {
  347. case Errors.ERROR_NODE_TIMEOUT:
  348. errorMessage = Strings.vpnConnectionTimeoutError.tr;
  349. break;
  350. case Errors.ERROR_NO_NODE:
  351. errorMessage = Strings.vpnNoNodeError.tr;
  352. break;
  353. case Errors.ERROR_INIT:
  354. errorMessage = Strings.vpnInitError.tr;
  355. break;
  356. case Errors.ERROR_KILL:
  357. errorMessage = Strings.vpnKillError.tr;
  358. break;
  359. case Errors.ERROR_REVOKE:
  360. errorMessage = Strings.vpnRevokeError.tr;
  361. break;
  362. case Errors.ERROR_SERVICE_EMPTY:
  363. errorMessage = Strings.vpnServiceEmptyError.tr;
  364. break;
  365. case Errors.ERROR_ROUTER:
  366. errorMessage = Strings.vpnRouterError.tr;
  367. break;
  368. case Errors.ERROR_PERMISSION_DENIED:
  369. errorMessage = Strings.vpnPermissionDeniedError.tr;
  370. break;
  371. }
  372. if (errorMessage.isNotEmpty) {
  373. ErrorDialog.show(message: errorMessage);
  374. }
  375. }
  376. void _uninitState() {
  377. if (state != ConnectionState.disconnected) {
  378. state = ConnectionState.disconnected;
  379. timer = "00:00:00";
  380. HapticFeedbackManager.connectionDisconnected();
  381. }
  382. }
  383. // 计时器状态处理方法
  384. void _onTimerRunning(int currentTime, int mode) {
  385. log(
  386. TAG,
  387. '计时器运行中: ${_formatTime(currentTime)}, 模式: ${mode == 0 ? "普通计时" : "倒计时"}',
  388. );
  389. }
  390. void _onTimerPaused(int currentTime, int mode) {
  391. log(
  392. TAG,
  393. '计时器已暂停: ${_formatTime(currentTime)}, 模式: ${mode == 0 ? "普通计时" : "倒计时"}',
  394. );
  395. }
  396. void _onTimerStopped() {
  397. log(TAG, '计时器已停止');
  398. }
  399. // 格式化时间显示
  400. String _formatTime(int timeMs) {
  401. final totalSeconds = (timeMs / 1000).abs().round();
  402. final hours = totalSeconds ~/ 3600;
  403. final minutes = (totalSeconds % 3600) ~/ 60;
  404. final seconds = totalSeconds % 60;
  405. return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
  406. }
  407. void handleSnackBarError(dynamic error, StackTrace stackTrace) {
  408. if (error is ApiException) {
  409. IXSnackBar.showIXErrorSnackBar(
  410. title: Strings.error.tr,
  411. message: error.message,
  412. );
  413. } else if (error is Failure) {
  414. IXSnackBar.showIXErrorSnackBar(
  415. title: Strings.error.tr,
  416. message: error.message ?? Strings.unknownError.tr,
  417. );
  418. } else if (error is DioException) {
  419. switch (error.type) {
  420. case DioExceptionType.connectionError:
  421. case DioExceptionType.connectionTimeout:
  422. case DioExceptionType.receiveTimeout:
  423. case DioExceptionType.sendTimeout:
  424. IXSnackBar.showIXErrorSnackBar(
  425. title: Strings.error.tr,
  426. message: Strings.unableToConnectNetwork.tr,
  427. );
  428. break;
  429. default:
  430. IXSnackBar.showIXErrorSnackBar(
  431. title: Strings.error.tr,
  432. message: Strings.unableToConnectServer.tr,
  433. );
  434. }
  435. } else {
  436. IXSnackBar.showIXErrorSnackBar(
  437. title: Strings.error.tr,
  438. message: error.toString(),
  439. );
  440. }
  441. }
  442. void handleErrorDialog(dynamic error, StackTrace stackTrace) {
  443. if (error is ApiException) {
  444. ErrorDialog.show(title: Strings.error.tr, message: error.message);
  445. } else if (error is Failure) {
  446. ErrorDialog.show(
  447. title: Strings.error.tr,
  448. message: error.message ?? Strings.unknownError.tr,
  449. );
  450. } else if (error is DioException) {
  451. switch (error.type) {
  452. case DioExceptionType.connectionError:
  453. case DioExceptionType.connectionTimeout:
  454. case DioExceptionType.receiveTimeout:
  455. case DioExceptionType.sendTimeout:
  456. ErrorDialog.show(
  457. title: Strings.error.tr,
  458. message: Strings.unableToConnectNetwork.tr,
  459. );
  460. break;
  461. default:
  462. ErrorDialog.show(
  463. title: Strings.error.tr,
  464. message: Strings.unableToConnectServer.tr,
  465. );
  466. }
  467. } else {
  468. ErrorDialog.show(title: Strings.error.tr, message: error.toString());
  469. }
  470. }
  471. // 创建一条加速日志
  472. Future<void> createBoostLog() async {
  473. await initLog();
  474. await setSessionInfoLog();
  475. await setTargetInfoLog();
  476. }
  477. // 初始化日志
  478. Future<void> initLog() async {
  479. await BoostReportManager().init();
  480. }
  481. // 读取历史日志
  482. Future<void> readHistoryLog() async {
  483. await BoostReportManager().readHistoryLog();
  484. }
  485. // 初始化会话日志
  486. Future<void> setSessionInfoLog() async {
  487. final deviceInfoPlugin = DeviceInfoPlugin();
  488. final appVersion = await PackageInfo.fromPlatform().then(
  489. (value) => value.version,
  490. );
  491. final networkType = await NetworkHelper.instance.getNetworkType();
  492. Map<String, String> deviceInfo = {};
  493. if (Platform.isIOS) {
  494. final iosOsInfo = await deviceInfoPlugin.iosInfo;
  495. deviceInfo = {
  496. 'deviceModel': iosOsInfo.model,
  497. 'osVersion': iosOsInfo.systemVersion,
  498. 'appVersion': appVersion,
  499. 'networkType': networkType,
  500. 'deviceBrand': iosOsInfo.utsname.machine,
  501. };
  502. } else if (Platform.isAndroid) {
  503. final androidOsInfo = await deviceInfoPlugin.androidInfo;
  504. deviceInfo = {
  505. 'deviceModel': androidOsInfo.model,
  506. 'osVersion': androidOsInfo.version.release,
  507. 'appVersion': appVersion,
  508. 'networkType': networkType,
  509. 'deviceBrand': androidOsInfo.brand,
  510. };
  511. }
  512. final boostSessionId = Uuid().v4();
  513. await BoostReportManager().initSessionInfo(
  514. appSessionId: _globalUuid,
  515. boostSessionId: boostSessionId,
  516. deviceInfo: deviceInfo,
  517. );
  518. }
  519. // 初始化目标信息
  520. Future<void> setTargetInfoLog() async {
  521. await BoostReportManager().addTargetInfo(
  522. locationSelectionType: locationSelectionType,
  523. location: IXSP.getSelectedLocation(),
  524. );
  525. }
  526. }