api_controller.dart 13 KB

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