core_controller.dart 16 KB

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