api_controller.dart 16 KB

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