api_controller.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. import 'dart:convert';
  2. import 'dart:io';
  3. import 'package:device_info_plus/device_info_plus.dart';
  4. import 'package:dio/dio.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:get/get.dart';
  7. import 'package:nomo/app/api/router/api_router.dart';
  8. import 'package:nomo/app/data/sp/ix_sp.dart';
  9. import 'package:package_info_plus/package_info_plus.dart';
  10. import 'package:play_install_referrer/play_install_referrer.dart';
  11. import '../../config/translations/localization_service.dart';
  12. import '../../config/translations/strings_enum.dart';
  13. import '../../pigeons/core_api.g.dart';
  14. import '../../utils/device_manager.dart';
  15. import '../../utils/geo_downloader.dart';
  16. import '../../utils/log/logger.dart';
  17. import '../../utils/network_helper.dart';
  18. import '../../utils/system_helper.dart';
  19. import '../api/core/api_core.dart';
  20. import '../components/country_restricted_overlay.dart';
  21. import '../components/ix_snackbar.dart';
  22. import '../constants/api_domains.dart';
  23. import '../constants/configs.dart';
  24. import '../constants/enums.dart';
  25. import '../constants/errors.dart';
  26. import '../constants/platforms.dart';
  27. import '../data/models/api_exception.dart';
  28. import '../data/models/failure.dart';
  29. import '../data/models/fingerprint.dart';
  30. import '../data/models/launch/groups.dart';
  31. import '../data/models/launch/launch.dart';
  32. import '../dialog/update_dailog.dart';
  33. class ApiController extends GetxService {
  34. final TAG = 'ApiController';
  35. // 记录是否已经显示禁用弹窗
  36. bool isShowDisabled = false;
  37. //是否是游客
  38. final _isGuest = false.obs;
  39. bool get isGuest => _isGuest.value;
  40. set isGuest(bool value) => _isGuest.value = value;
  41. //全部节点列表
  42. final _nodesList = <LocationList>[].obs;
  43. List<LocationList> get nodesList => _nodesList.value;
  44. set nodesList(List<LocationList> value) => _nodesList.value = value;
  45. //初始化fingerprint
  46. Fingerprint fp = Fingerprint.empty();
  47. Future<Fingerprint> initFingerprint() async {
  48. // 读取app发布渠道
  49. if (Platform.isIOS) {
  50. fp.channel = 'apple';
  51. } else if (Platform.isAndroid) {
  52. try {
  53. final channel = await CoreApi().getChannel();
  54. fp.channel = channel ?? 'unknown';
  55. } catch (e) {
  56. log(TAG, 'read app channel error: $e');
  57. fp.channel = '';
  58. }
  59. try {
  60. final advertisingId = await CoreApi().getAdvertisingId();
  61. fp.googleId = advertisingId ?? '';
  62. } catch (e) {
  63. log(TAG, 'read app googleId error: $e');
  64. fp.googleId = '';
  65. }
  66. try {
  67. ReferrerDetails referrerDetails =
  68. await PlayInstallReferrer.installReferrer;
  69. fp.refer = referrerDetails.installReferrer ?? '';
  70. } catch (e) {
  71. log(TAG, 'get install referrer error: $e');
  72. fp.refer = '';
  73. }
  74. }
  75. // 读取应用信息
  76. final info = await PackageInfo.fromPlatform();
  77. fp.appVersionCode = int.tryParse(info.buildNumber) ?? 0;
  78. fp.appVersionName = info.version;
  79. // 读取设备信息
  80. final deviceInfo = DeviceInfoPlugin();
  81. if (Platform.isIOS) {
  82. fp.platform = Platforms.iOS;
  83. final iosOsInfo = await deviceInfo.iosInfo;
  84. fp.deviceModel = iosOsInfo.model;
  85. fp.deviceOs = iosOsInfo.systemVersion;
  86. fp.deviceBrand = iosOsInfo.utsname.machine;
  87. } else if (Platform.isAndroid) {
  88. fp.platform = Platforms.android;
  89. final androidOsInfo = await deviceInfo.androidInfo;
  90. fp.deviceModel = androidOsInfo.model;
  91. fp.deviceOs = androidOsInfo.version.release;
  92. fp.deviceBrand = androidOsInfo.brand;
  93. fp.androidId = androidOsInfo.id;
  94. }
  95. //获取设备尺寸
  96. fp.deviceHeight = Get.height.toInt();
  97. fp.deviceWidth = Get.width.toInt();
  98. // 读取设备ID
  99. fp.deviceId = DeviceManager.getCacheDeviceId();
  100. fp.isNewInstall = IXSP.getIsNewInstall();
  101. await updateFingerprintData();
  102. return fp;
  103. }
  104. // 更新部分数据
  105. Future<void> updateFingerprintData() async {
  106. fp.lang = IXSP.getCurrentLocal().languageCode;
  107. fp.phoneCountryIso = LocalizationService.getSystemCountry();
  108. fp.isVpn = await CoreApi().isConnected() ?? false;
  109. if (!fp.isVpn) {
  110. fp.isConnectedVpn = false;
  111. }
  112. try {
  113. final simInfo = await CoreApi().getSimInfo();
  114. // 解析sim
  115. final sim = jsonDecode(simInfo ?? '{}');
  116. fp.simReady = sim['simReady'];
  117. fp.carrierName = sim['carrierName'];
  118. fp.mcc = sim['mcc'];
  119. fp.mnc = sim['mnc'];
  120. fp.countryIso = sim['countryIso'];
  121. fp.networkCarrierName = sim['networkCarrierName'];
  122. fp.networkMcc = sim['networkMcc'];
  123. fp.networkMnc = sim['networkMnc'];
  124. fp.networkCountryIso = sim['networkCountryIso'];
  125. } catch (e) {
  126. log(TAG, 'read app sim error: $e');
  127. }
  128. }
  129. Future<void> initData(Launch? launch) async {
  130. // 初始化是否第一次安装
  131. IXSP.setIsNewInstall(false);
  132. fp.userUuid = '';
  133. fp.isNewInstall = false;
  134. await initLaunch(launch);
  135. }
  136. Future<void> initLaunch(Launch? launch) async {
  137. try {
  138. if (launch != null) {
  139. // 初始化用户状态
  140. isGuest = launch.userConfig?.memberLevel == MemberLevel.guest.level;
  141. // 设置路由和节点
  142. nodesList = launch.groups?.normal?.list ?? [];
  143. // 设置资源url
  144. if (launch.appConfig?.assetUrls != null &&
  145. launch.appConfig!.assetUrls!.isNotEmpty) {
  146. Configs.assetUrl = launch.appConfig!.assetUrls![0];
  147. }
  148. // 设置官网url
  149. if (launch.appConfig?.websiteUrl != null &&
  150. launch.appConfig!.websiteUrl!.isNotEmpty) {
  151. Configs.websiteUrl = launch.appConfig!.websiteUrl!;
  152. }
  153. }
  154. } catch (e) {
  155. log(TAG, 'initLaunch error: $e');
  156. }
  157. }
  158. // 发送分析事件, 后续可以发送到firebase
  159. Future<void> sendAnalytics(FirebaseEvent event) async {
  160. try {} catch (e) {
  161. log('sendAnalytics error: $e');
  162. }
  163. }
  164. Future<Launch> launch({bool isCache = false}) async {
  165. sendAnalytics(isCache ? FirebaseEvent.launchCache : FirebaseEvent.launch);
  166. while (true) {
  167. try {
  168. ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl());
  169. final request = fp.toJson();
  170. if (IXSP.getDisconnectDomains().isNotEmpty) {
  171. final disconnectDomainList = IXSP
  172. .getDisconnectDomains()
  173. .map((e) => e.toJson())
  174. .toList();
  175. request['disconnectDomainList'] = disconnectDomainList;
  176. }
  177. final result = await ApiCore().launch(request);
  178. if (!result.success) {
  179. throw Failure(
  180. code: result.errorCode ?? '',
  181. message: result.errorMessage ?? '',
  182. );
  183. }
  184. sendAnalytics(
  185. isCache
  186. ? FirebaseEvent.launchCacheSuccess
  187. : FirebaseEvent.launchSuccess,
  188. );
  189. if (IXSP.getDisconnectDomains().isNotEmpty) {
  190. IXSP.clearDisconnectDomains();
  191. }
  192. // 重置禁用状态
  193. IXSP.setLastIsRegionDisabled(false);
  194. IXSP.setLastIsUserDisabled(false);
  195. final launchData = Launch.fromJson(result.data);
  196. // 设置扩展数据
  197. fp.exData = launchData.exData;
  198. // 更新URL列表
  199. await ApiDomains.instance.updateFromLaunch(launchData);
  200. // 保存Launch数据
  201. await IXSP.saveLaunch(launchData);
  202. // 初始化Launch
  203. await initData(launchData);
  204. return launchData;
  205. } on ApiException catch (e) {
  206. final url = await ApiDomains.instance.getNextApiUrl();
  207. log(TAG, 'Launch request failed for URL $url: $e');
  208. if (url.isEmpty) {
  209. rethrow;
  210. }
  211. ApiCore().setbaseUrl(url);
  212. } on Failure catch (_) {
  213. rethrow;
  214. } on DioException catch (e) {
  215. if (e.response?.statusCode == Errors.eRegionNotAvailable ||
  216. e.response?.statusCode == Errors.eUserDisabled ||
  217. e.response?.statusCode == Errors.eTokenExpired) {
  218. rethrow;
  219. } else {
  220. if (await NetworkHelper.instance.isNetworkAvailable()) {
  221. final url = await ApiDomains.instance.getNextApiUrl();
  222. log(TAG, 'Launch request failed for URL $url: $e');
  223. if (url.isEmpty) {
  224. rethrow;
  225. }
  226. ApiCore().setbaseUrl(url);
  227. } else {
  228. rethrow;
  229. }
  230. }
  231. } catch (e) {
  232. final url = await ApiDomains.instance.getNextApiUrl();
  233. log(TAG, 'Launch request failed for URL $url: $e');
  234. if (url.isEmpty) {
  235. rethrow;
  236. }
  237. ApiCore().setbaseUrl(url);
  238. }
  239. }
  240. }
  241. Future<void> asyncHandleLaunch() async {
  242. try {
  243. final data = await launch(isCache: true);
  244. final isVpnRunning = await CoreApi().isConnected() ?? false;
  245. if (!isVpnRunning) {
  246. await checkUpdate();
  247. // 下载smartgeo文件
  248. GeoDownloader().downloadSmartGeo(smartGeo: data.appConfig!.smartGeo!);
  249. }
  250. } catch (e, s) {
  251. if (IXSP.getLastIsUserDisabled()) {
  252. if (!isShowDisabled) {
  253. Get.offAll(
  254. () => CountryRestrictedOverlay(
  255. type: RestrictedType.user,
  256. onPressed: () async {
  257. // 清除LaunchData
  258. await IXSP.clearLaunchData();
  259. // 清除禁用状态
  260. IXSP.setLastIsUserDisabled(false);
  261. // 发送事件
  262. },
  263. ),
  264. transition: Transition.fadeIn,
  265. );
  266. }
  267. return;
  268. } else if (IXSP.getLastIsRegionDisabled()) {
  269. if (!isShowDisabled) {
  270. Get.offAll(
  271. () => const CountryRestrictedOverlay(type: RestrictedType.region),
  272. transition: Transition.fadeIn,
  273. );
  274. }
  275. return;
  276. } else if (IXSP.getLastIsDeviceDisabled()) {
  277. if (!isShowDisabled) {
  278. Get.offAll(
  279. () => const CountryRestrictedOverlay(type: RestrictedType.device),
  280. transition: Transition.fadeIn,
  281. );
  282. }
  283. return;
  284. }
  285. final isVpnRunning = await CoreApi().isConnected() ?? false;
  286. if (!isVpnRunning) {
  287. await checkUpdate();
  288. }
  289. handleSnackBarError(e, s);
  290. }
  291. }
  292. void handleSnackBarError(dynamic error, StackTrace stackTrace) {
  293. if (error is ApiException) {
  294. IXSnackBar.showIXErrorSnackBar(
  295. title: Strings.error.tr,
  296. message: error.message,
  297. );
  298. } else if (error is Failure) {
  299. IXSnackBar.showIXErrorSnackBar(
  300. title: Strings.error.tr,
  301. message: error.message ?? Strings.unknownError.tr,
  302. );
  303. } else if (error is DioException) {
  304. switch (error.type) {
  305. case DioExceptionType.connectionError:
  306. case DioExceptionType.connectionTimeout:
  307. case DioExceptionType.receiveTimeout:
  308. case DioExceptionType.sendTimeout:
  309. IXSnackBar.showIXErrorSnackBar(
  310. title: Strings.error.tr,
  311. message: Strings.unableToConnectNetwork.tr,
  312. );
  313. break;
  314. default:
  315. IXSnackBar.showIXErrorSnackBar(
  316. title: Strings.error.tr,
  317. message: Strings.unableToConnectServer.tr,
  318. );
  319. }
  320. } else {
  321. IXSnackBar.showIXErrorSnackBar(
  322. title: Strings.error.tr,
  323. message: Strings.unknownError.tr,
  324. );
  325. }
  326. }
  327. // 更新检查 - 智能时间控制版本
  328. Future<bool> checkUpdate({bool isClickCheck = false}) async {
  329. try {
  330. final upgrade = IXSP.getUpgrade();
  331. var hasUpdate = false;
  332. var hasForceUpdate = false;
  333. if (upgrade != null) {
  334. if (upgrade.upgradeType == 1) {
  335. hasUpdate = true;
  336. }
  337. if (upgrade.forced == true) {
  338. hasForceUpdate = true;
  339. }
  340. }
  341. if (hasUpdate) {
  342. Get.dialog(
  343. WillPopScope(
  344. onWillPop: () async => false,
  345. child: UpdateDialog(
  346. upgrade: upgrade,
  347. onUpdate: () =>
  348. SystemHelper.openGooglePlayUrl(upgrade?.appStoreUrl ?? ''),
  349. onLater: hasForceUpdate
  350. ? null
  351. : () {
  352. Navigator.of(Get.context!).pop();
  353. },
  354. ),
  355. ),
  356. barrierDismissible: false,
  357. ).then((_) {
  358. log('UpdateDialog closed');
  359. });
  360. }
  361. return hasUpdate;
  362. } catch (e) {
  363. log(TAG, 'checkUpdate error: $e');
  364. }
  365. return false;
  366. }
  367. Future<Launch> getDispatchInfo(
  368. int locationId,
  369. String locationCode, {
  370. CancelToken? cancelToken,
  371. }) async {
  372. while (true) {
  373. try {
  374. ApiRouter().setbaseUrl(ApiDomains.instance.getRouterUrl());
  375. final request = fp.toJson();
  376. if (IXSP.getDisconnectDomains().isNotEmpty) {
  377. final disconnectDomainList = IXSP
  378. .getDisconnectDomains()
  379. .map((e) => e.toJson())
  380. .toList();
  381. request['disconnectDomainList'] = disconnectDomainList;
  382. }
  383. request['locationId'] = locationId;
  384. request['locationCode'] = locationCode;
  385. final result = await ApiRouter().getDispatchInfo(
  386. request,
  387. cancelToken: cancelToken,
  388. );
  389. if (!result.success) {
  390. throw Failure(
  391. code: result.errorCode ?? '',
  392. message: result.errorMessage ?? '',
  393. );
  394. }
  395. if (IXSP.getDisconnectDomains().isNotEmpty) {
  396. IXSP.clearDisconnectDomains();
  397. }
  398. // 重置禁用状态
  399. IXSP.setLastIsRegionDisabled(false);
  400. IXSP.setLastIsUserDisabled(false);
  401. final launchData = Launch.fromJson(result.data);
  402. // 更新URL列表
  403. await ApiDomains.instance.updateFromLaunch(launchData);
  404. // 保存app配置
  405. await IXSP.saveAppConfig(launchData.appConfig!);
  406. // 保存vpn配置
  407. // await IXSP.saveVpnConfig(launchData.vpnConfig!);
  408. return launchData;
  409. } on ApiException catch (_) {
  410. rethrow;
  411. } on Failure catch (_) {
  412. rethrow;
  413. } on DioException catch (e) {
  414. if (e.response?.statusCode == Errors.eRegionNotAvailable ||
  415. e.response?.statusCode == Errors.eUserDisabled ||
  416. e.response?.statusCode == Errors.eTokenExpired) {
  417. rethrow;
  418. } else {
  419. if (await NetworkHelper.instance.isNetworkAvailable()) {
  420. final url = await ApiDomains.instance.getNextRouterUrl();
  421. log(TAG, 'Launch request failed for URL $url: $e');
  422. if (url.isEmpty) {
  423. rethrow;
  424. }
  425. ApiRouter().setbaseUrl(url);
  426. } else {
  427. rethrow;
  428. }
  429. }
  430. } catch (e) {
  431. final url = await ApiDomains.instance.getNextRouterUrl();
  432. log(TAG, 'Launch request failed for URL $url: $e');
  433. if (url.isEmpty) {
  434. rethrow;
  435. }
  436. ApiRouter().setbaseUrl(url);
  437. }
  438. }
  439. }
  440. }