import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:http/http.dart' as http; import '../../../data/models/appconfig.dart'; import '../../../data/models/appconfig_model.dart'; import '../../../data/models/user_Info.dart'; import '../../../routes/app_routes.dart'; import '../../../data/services/network/api.dart'; import '../../../data/services/network/dio_manager.dart'; import '../../../data/models/channel_app_model.dart'; import '../../../data/services/version_update_service.dart'; import '../../../modules/common/widgets/version_update_dialog.dart'; import '../../../modules/common/widgets/privacy_policy_dialog.dart'; import '../../../core/utils/logger.dart'; import '../../../data/services/db/sqflite_api.dart'; class SplashController extends GetxController { final GetStorage _storage = GetStorage(); static const String _tag = 'SplashCtrl'; final versionService = VersionUpdateService.to; // 进度条动画控制 final _loadingProgress = 0.0.obs; double get loadingProgress => _loadingProgress.value; // 隐私政策同意状态 - 新增 final _hasAcceptedPrivacy = false.obs; RxBool get hasAcceptedPrivacy => _hasAcceptedPrivacy; // 使用一个反应式变量来决定是否可以导航,替代所有布尔标记 final _canNavigate = false.obs; @override void onInit() { super.onInit(); // 初始化隐私政策状态 _hasAcceptedPrivacy.value = _storage.read('hasAcceptedPrivacy') ?? false; // 监听 _canNavigate 状态,一旦为 true,立即且仅执行一次导航 once(_canNavigate, (_) => _navigateToTargetPage()); // 启动整个初始化流程 _initializeApp(); } /// 启动并协调所有初始化任务 Future _initializeApp() async { // 步骤 1: 检查并等待隐私政策同意 final hasAccepted = await _checkAndRequestPrivacy(); if (!hasAccepted) { // 如果用户拒绝隐私政策,应用会退出,后续代码不执行 return; } // 步骤 3: 并行执行所有启动任务,并等待它们全部完成 // Future.wait 会等待数组中所有的 Future 都完成后才继续 await Future.wait([ _startLoadingAnimation(), // 任务1: 最小动画时长 _performParallelTasks(), // 任务2: 其他所有后台任务 ]); // 步骤 4: 所有任务(动画和后台)都已完成,触发导航 _canNavigate.value = true; } /// 切换隐私政策同意状态 - 新增方法 // void togglePrivacyAcceptance() { // _hasAcceptedPrivacy.value = !_hasAcceptedPrivacy.value; // _storage.write('hasAcceptedPrivacy', _hasAcceptedPrivacy.value); // } /// 检查隐私政策,如果未同意,则显示弹窗并等待用户操作 /// 返回用户是否同意 Future _checkAndRequestPrivacy() async { if (_hasAcceptedPrivacy.value) { return true; } final completer = Completer(); WidgetsBinding.instance.addPostFrameCallback((_) { PrivacyPolicyDialog.show( onAccept: () { _storage.write('hasAcceptedPrivacy', true); _hasAcceptedPrivacy.value = true; Get.back(); // 关闭弹窗 if (!completer.isCompleted) completer.complete(true); }, onReject: () { if (!completer.isCompleted) completer.complete(false); exit(0); // 退出应用 }, canDismiss: false, ); }); return completer.future; } /// 网络连接检查 - 阻塞版本 Future _checkNetworkStatus() async { while (true) { // 无限循环直到网络检查成功 try { final connectivityResult = await Connectivity().checkConnectivity(); final hasConnectivity = !connectivityResult.contains(ConnectivityResult.none); // 条件变更:满足“有网络连接”或“能访问至少一个测试地址”之一即通过 if (hasConnectivity) { Logger.d(_tag, '网络检查通过:检测到设备网络连接'); break; } // 若设备层面无网络标识,再尝试外网连通性 final hasInternet = await _checkInternetConnection(); if (hasInternet) { Logger.d(_tag, '网络检查通过:可访问外网测试地址'); break; } // 两个条件都不满足,提示并重试 await _showNoNetworkDialogAndWait(); continue; } catch (e) { Logger.e(_tag, '网络检查失败: $e'); Logger.e(_tag, '网络检查异常: $e'); await _showNoNetworkDialogAndWait(); continue; } } } /// 检查实际的网络连通性 Future _checkInternetConnection() async { // 业务服务器探测跟随 .env 的 SERVER_URL(同 doc_url_helper 的做法), // 别再写死域名 —— 原先写死的是上一代服务器的两个地址,换服务器后必然超时, // 每次启动白等两轮 5s 才 fallback 到 baidu/google。 final testUrls = [ '${DioManager().baseUrl}/ping', // 当前业务服务器 'https://www.baidu.com', // 百度 'https://www.google.com', // Google ]; for (final url in testUrls) { try { final response = await http.get( Uri.parse(url.trim()), // 去除空格 headers: {'User-Agent': 'Eaimar-App'}, // 有助于后端识别 ).timeout(const Duration(seconds: 5)); if (response.statusCode == 200) { Logger.d(_tag, '网络连接检查成功: $url'); return true; } } on TimeoutException { Logger.w(_tag, '网络连接超时: $url'); continue; } on Exception catch (e) { Logger.w(_tag, '网络连接失败: $url, 错误: $e'); continue; } } Logger.e(_tag, '所有网络连接检查均失败'); return false; } /// 动画任务:返回一个在动画结束时完成的 Future Future _startLoadingAnimation() { const totalDuration = 1500; // 动画总时长1.5秒 const steps = totalDuration ~/ 30; const stepValue = 1.0 / steps; final completer = Completer(); Timer.periodic(const Duration(milliseconds: 30), (timer) { if (_loadingProgress.value < 1.0) { _loadingProgress.value += stepValue; } else { _loadingProgress.value = 1.0; timer.cancel(); if (!completer.isCompleted) completer.complete(); } }); return completer.future; } /// 将所有并行的后台任务打包成一个 Future Future _performParallelTasks() async { //步骤2:检查网络连接状态 await _checkNetworkStatus(); // 同样使用 Future.wait,确保此函数在所有子任务完成后才结束 // 这里原先还会先弹 ATT 追踪授权框(app_tracking_transparency)。已移除: // 本 App 从不读取 IDFA,也没有任何广告/归因 SDK,弹框纯粹劝退用户, // 还会让 App Store Connect 要求申报「用于追踪的数据类型」。详见 CLAUDE.md。 await _performVersionCheckAsync(); await _validateTokenAsync(); //await Future.wait([ //_validateTokenAsync(), //_performVersionCheckAsync(), //]); Logger.d(_tag, '所有后台任务已完成'); } /// 异步验证Token和加载应用配置 Future _validateTokenAsync() async { try { final token = _storage.read('logintoken'); if (token != null && token.isNotEmpty) { final isLoggedIn = await getLoginToken(); if (isLoggedIn) { // 只有登录成功,才获取App配置 await getAppConfig(); // ⚠️ 会议模板**不放在启动关键路径上**:它只被会议模板页读取 // (落 sqlite,由 SqfliteApi.getMeetingTemplateList 消费), // 启动时没有任何地方要用。原来 await 它等于让每次冷启动的白屏 // 多背一次网络往返——而 _initializeApp 要等这一整串跑完才允许导航。 // 改成后台拉:模板照常更新,启动不再等它。 unawaited(_initMeetingTemplate()); } else { await User.clearUserSession(); // } } } catch (e) { Logger.e(_tag, 'Token验证或配置加载失败: $e'); await User.clearUserSession(); } } /// 异步执行版本检查 Future _performVersionCheckAsync() async { try { final versionService = VersionUpdateService.to; final updateInfo = await versionService.checkForUpdate(); if (updateInfo.hasUpdate) { Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}'); // 推荐更新被用户「稍后提醒」过:同一版本 24 小时内不再弹。强更不受影响。 if (updateInfo.updateType == UpdateType.optional && versionService.isSnoozed(updateInfo.latestVersion)) { Logger.i(_tag, '该版本已被稍后提醒,本次不弹'); return; } // 等待到下一微任务,降低与其它 UI 的并发竞争 await Future.delayed(Duration.zero); try { await VersionUpdateDialog.show(updateInfo); } catch (e) { Logger.e(_tag, '更新弹窗显示失败: $e'); } } else { Logger.i(_tag, '当前已是最新版本'); } } catch (e) { Logger.e(_tag, '版本检查失败: $e'); } } /// 根据最终状态跳转到目标页面 Future _navigateToTargetPage() async { // 导航时直接检查最终的数据状态,而不是依赖中间变量 final token = _storage.read('logintoken'); final isAppConfigInitialized = AppConfig.isInitialized(); // 检查登录状态和配置初始化状态 if (token != null && token.isNotEmpty && isAppConfigInitialized) { // 登录成功 → 直接进入首页 Get.offAllNamed(Routes.home); } else { _storage.remove('meeting_upload_list'); _storage.remove('operation_records'); _storage.remove('operation_delids'); Get.offAllNamed(Routes.login); } } /// 异步获取登录Token Future getLoginToken() async { try { final appVersion = versionService.cachedAppVersion; final channel = versionService.cachedChannel; Logger.i(_tag, 'Token登录请求: version=$appVersion, channel=$channel'); final response = await Api.tokenlogin({ 'version': appVersion, 'channel': channel, }); if (response?['user'] != null && response['user'] != '') { final user = User.fromJson(response); await _storage.write('user_info', user.toJson()); return true; } return false; } catch (e) { Logger.e(_tag, 'Token获取失败: $e'); return false; } } Future getAppConfig() async { try { final response = await Api.getappconfig(); if (response == null) return false; final resp = UserGetAppConfigResp.fromJson(response); AppConfig.initialize(resp); return true; } catch (e) { Logger.e(_tag, 'getAppConfig 获取失败: $e'); return false; } } Future _initMeetingTemplate() async { try { var response = await Api.getEchomeetTemplates(); if (response != null) { await SqfliteApi.initMeetingTemplate(response['templates']); } } catch (e) { Logger.e(_tag, 'initMeetingTemplate 获取失败: $e'); } } /// 处理QQ音乐登录回调 void handleQQMusicLogin() { try { Logger.i(_tag, '处理QQ音乐登录回调'); // 这里可以添加具体的QQ音乐登录处理逻辑 // 例如:验证登录状态、更新用户信息、导航到相应页面等 // 如果需要导航到特定页面,可以使用: // Get.toNamed(Routes.qqmusic); } catch (e) { Logger.e(_tag, '处理QQ音乐登录回调失败: $e'); } } } /// 显示无网络连接对话框并等待用户操作(阻塞版本) Future _showNoNetworkDialogAndWait() async { final completer = Completer(); WidgetsBinding.instance.addPostFrameCallback((_) { Get.dialog( AlertDialog( title: Row( children: [ const Icon(Icons.wifi_off, color: Colors.orange), const SizedBox(width: 8), Text('networkError'.tr), // 无网络连接 ], ), content: Text('networkConnectionFailed'.tr), // 网络连接失败,请检查网络设置 actions: [ TextButton( onPressed: () { Get.back(); // 关闭弹窗 completer.complete(); // 完成Future,继续执行 }, child: Text('retry'.tr), // 重试 ), ], ), barrierDismissible: false, // 不允许点击外部关闭 ); }); // 等待用户点击重试按钮 return completer.future; }