|
|
|
@ -1,15 +1,14 @@ |
|
|
|
import 'dart:async'; |
|
|
|
import 'dart:io'; |
|
|
|
import 'package:app_tracking_transparency/app_tracking_transparency.dart'; |
|
|
|
|
|
|
|
import '../../../data/models/appconfig.dart'; |
|
|
|
import '../../../data/models/appconfig_model.dart'; |
|
|
|
import 'package:flutter/material.dart'; |
|
|
|
import 'package:get/get.dart'; |
|
|
|
import 'package:get_storage/get_storage.dart'; |
|
|
|
|
|
|
|
import '../../../data/models/appconfig.dart'; |
|
|
|
import '../../../data/models/appconfig_model.dart'; |
|
|
|
import '../../../data/models/user_Info.dart'; |
|
|
|
import '../../../routes/app_routes.dart'; |
|
|
|
import '../../../core/themes/theme_service.dart'; |
|
|
|
import '../../../data/services/network/api.dart'; |
|
|
|
import '../../../data/services/version_update_service.dart'; |
|
|
|
import '../../../modules/common/widgets/version_update_dialog.dart'; |
|
|
|
@ -24,191 +23,174 @@ class SplashController extends GetxController { |
|
|
|
final _loadingProgress = 0.0.obs; |
|
|
|
double get loadingProgress => _loadingProgress.value; |
|
|
|
|
|
|
|
// 各项任务完成状态 |
|
|
|
final _isTrackingPermissionRequested = false.obs; // 追踪权限请求完成 |
|
|
|
final _isVersionCheckCompleted = false.obs; // 版本检查完成 |
|
|
|
final _isTokenValidated = false.obs; // Token验证完成 |
|
|
|
final _isAppConfigLoaded = false.obs; // 应用配置加载完成 |
|
|
|
final _isAnimationCompleted = false.obs; // 动画完成 |
|
|
|
|
|
|
|
// 登录状态 |
|
|
|
final _isLoggedIn = false.obs; |
|
|
|
bool get isLoggedIn => _isLoggedIn.value; |
|
|
|
|
|
|
|
final _hasAcceptedPrivacy = false.obs; //是否同意隐私政策 |
|
|
|
|
|
|
|
// 版本号 |
|
|
|
//final String version = 'V1.0.0'; |
|
|
|
|
|
|
|
// 主题服务 |
|
|
|
final ThemeService _themeService = ThemeService(); |
|
|
|
|
|
|
|
// 会话级别的版本检查标记(静态变量,整个应用会话期间保持) |
|
|
|
static bool _hasCheckedVersionInSession = false; |
|
|
|
|
|
|
|
// 版本检查相关常量 |
|
|
|
static const String _lastVersionCheckKey = 'last_version_check_timestamp'; |
|
|
|
static const int _versionCheckIntervalHours = 24; // 24小时间隔 |
|
|
|
// 使用一个反应式变量来决定是否可以导航,替代所有布尔标记 |
|
|
|
final _canNavigate = false.obs; |
|
|
|
|
|
|
|
@override |
|
|
|
void onInit() { |
|
|
|
super.onInit(); |
|
|
|
|
|
|
|
// 首先检查隐私政策状态 |
|
|
|
final hasAcceptedPrivacy = _storage.read('hasAcceptedPrivacy') ?? false; |
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) { |
|
|
|
if (!hasAcceptedPrivacy) { |
|
|
|
// 优先显示隐私政策弹窗,暂停其他初始化 |
|
|
|
_showPrivacyDialog(); |
|
|
|
} else { |
|
|
|
// 已同意隐私政策,正常启动流程 |
|
|
|
_startNormalFlow(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 注意:追踪权限请求将在用户同意隐私政策后执行 |
|
|
|
} |
|
|
|
|
|
|
|
/// 异步请求追踪权限 |
|
|
|
Future<void> _requestTrackingPermissionAsync() async { |
|
|
|
try { |
|
|
|
await AppTrackingTransparency.requestTrackingAuthorization(); |
|
|
|
Logger.d(_tag, '用户追踪权限请求完成'); |
|
|
|
} catch (e) { |
|
|
|
Logger.e(_tag, '用户追踪权限请求失败: $e'); |
|
|
|
} finally { |
|
|
|
_isTrackingPermissionRequested.value = true; |
|
|
|
// 监听 _canNavigate 状态,一旦为 true,立即且仅执行一次导航 |
|
|
|
once(_canNavigate, (_) => _navigateToTargetPage()); |
|
|
|
// 启动整个初始化流程 |
|
|
|
_initializeApp(); |
|
|
|
} |
|
|
|
|
|
|
|
/// 启动并协调所有初始化任务 |
|
|
|
Future<void> _initializeApp() async { |
|
|
|
// 步骤 1: 检查并等待隐私政策同意 |
|
|
|
final hasAccepted = await _checkAndRequestPrivacy(); |
|
|
|
if (!hasAccepted) { |
|
|
|
// 如果用户拒绝隐私政策,应用会退出,后续代码不执行 |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
void _startNormalFlow() { |
|
|
|
_startLoadingAnimation(); |
|
|
|
_startParallelTasks(); |
|
|
|
} |
|
|
|
// 步骤 2: 并行执行所有启动任务,并等待它们全部完成 |
|
|
|
// Future.wait 会等待数组中所有的 Future 都完成后才继续 |
|
|
|
await Future.wait([ |
|
|
|
_startLoadingAnimation(), // 任务1: 最小动画时长 |
|
|
|
_performParallelTasks(), // 任务2: 其他所有后台任务 |
|
|
|
]); |
|
|
|
|
|
|
|
/// 启动并行任务:在1.5秒动画期间同时执行多个任务 |
|
|
|
void _startParallelTasks() { |
|
|
|
// 并行执行所有任务 |
|
|
|
_requestTrackingPermissionAsync(); |
|
|
|
_performVersionCheckAsync(); |
|
|
|
_validateTokenAsync(); |
|
|
|
// 步骤 3: 所有任务(动画和后台)都已完成,触发导航 |
|
|
|
_canNavigate.value = true; |
|
|
|
} |
|
|
|
|
|
|
|
/// 异步执行版本检查 |
|
|
|
Future<void> _performVersionCheckAsync() async { |
|
|
|
try { |
|
|
|
Logger.d(_tag, '开始执行版本检查'); |
|
|
|
await _checkVersionUpdateIfNeeded(); |
|
|
|
} catch (e) { |
|
|
|
Logger.e(_tag, '版本检查失败: $e'); |
|
|
|
/// 检查隐私政策,如果未同意,则显示弹窗并等待用户操作 |
|
|
|
/// 返回用户是否同意 |
|
|
|
Future<bool> _checkAndRequestPrivacy() async { |
|
|
|
if (_storage.read<bool>('hasAcceptedPrivacy') ?? false) { |
|
|
|
return true; |
|
|
|
} |
|
|
|
//finally { |
|
|
|
// _isVersionCheckCompleted.value = true; |
|
|
|
// } |
|
|
|
} |
|
|
|
|
|
|
|
void _onPrivacyAccepted() { |
|
|
|
_storage.write('hasAcceptedPrivacy', true); |
|
|
|
Get.back(); // 关闭弹窗 |
|
|
|
_startNormalFlow(); // 开始正常流程(已包含版本检查) |
|
|
|
} |
|
|
|
|
|
|
|
void _onPrivacyRejected() { |
|
|
|
exit(0); // 退出应用 |
|
|
|
} |
|
|
|
final completer = Completer<bool>(); |
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) { |
|
|
|
PrivacyPolicyDialog.show( |
|
|
|
onAccept: () { |
|
|
|
_storage.write('hasAcceptedPrivacy', true); |
|
|
|
Get.back(); // 关闭弹窗 |
|
|
|
if (!completer.isCompleted) completer.complete(true); |
|
|
|
}, |
|
|
|
onReject: () { |
|
|
|
if (!completer.isCompleted) completer.complete(false); |
|
|
|
exit(0); // 退出应用 |
|
|
|
}, |
|
|
|
canDismiss: false, |
|
|
|
); |
|
|
|
}); |
|
|
|
|
|
|
|
// 显示隐私政策弹窗 |
|
|
|
void _showPrivacyDialog() { |
|
|
|
PrivacyPolicyDialog.show( |
|
|
|
onAccept: () { |
|
|
|
_onPrivacyAccepted(); |
|
|
|
_hasAcceptedPrivacy.value = true; |
|
|
|
}, |
|
|
|
onReject: _onPrivacyRejected, |
|
|
|
canDismiss: false, // 禁止通过返回键关闭弹窗 |
|
|
|
); |
|
|
|
return completer.future; |
|
|
|
} |
|
|
|
|
|
|
|
// 进度条动画(持续1.5秒) |
|
|
|
void _startLoadingAnimation() { |
|
|
|
/// 动画任务:返回一个在动画结束时完成的 Future |
|
|
|
Future<void> _startLoadingAnimation() { |
|
|
|
const totalDuration = 1500; // 动画总时长1.5秒 |
|
|
|
const steps = totalDuration ~/ 30; // 30ms间隔,更平滑的动画 |
|
|
|
const steps = totalDuration ~/ 30; |
|
|
|
const stepValue = 1.0 / steps; |
|
|
|
final completer = Completer<void>(); |
|
|
|
|
|
|
|
Timer.periodic(const Duration(milliseconds: 30), (timer) { |
|
|
|
if (_loadingProgress.value < 1.0) { |
|
|
|
_loadingProgress.value += stepValue; |
|
|
|
} else { |
|
|
|
_loadingProgress.value = 1.0; |
|
|
|
timer.cancel(); |
|
|
|
_isAnimationCompleted.value = true; |
|
|
|
_checkNavigateCondition(); |
|
|
|
if (!completer.isCompleted) completer.complete(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
return completer.future; |
|
|
|
} |
|
|
|
|
|
|
|
/// 将所有并行的后台任务打包成一个 Future |
|
|
|
Future<void> _performParallelTasks() async { |
|
|
|
// 同样使用 Future.wait,确保此函数在所有子任务完成后才结束 |
|
|
|
await Future.wait([ |
|
|
|
_requestTrackingPermissionAsync(), |
|
|
|
_validateTokenAsync(), |
|
|
|
_performVersionCheckAsync(), |
|
|
|
]); |
|
|
|
Logger.d(_tag, '所有后台任务已完成'); |
|
|
|
} |
|
|
|
|
|
|
|
/// 异步请求追踪权限 |
|
|
|
Future<void> _requestTrackingPermissionAsync() async { |
|
|
|
try { |
|
|
|
await AppTrackingTransparency.requestTrackingAuthorization(); |
|
|
|
Logger.d(_tag, '用户追踪权限请求完成'); |
|
|
|
} catch (e) { |
|
|
|
Logger.e(_tag, '用户追踪权限请求失败: $e'); |
|
|
|
// 即使失败,也视为完成,不阻塞主流程 |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 异步验证Token和加载应用配置 |
|
|
|
Future<void> _validateTokenAsync() async { |
|
|
|
try { |
|
|
|
bool isLoggedIn = false; |
|
|
|
final token = _storage.read('logintoken'); |
|
|
|
Logger.d(_tag, 'Token验证开始: $token'); |
|
|
|
|
|
|
|
if (token != null && token.isNotEmpty) { |
|
|
|
isLoggedIn = await getLoginToken(); |
|
|
|
final isLoggedIn = await getLoginToken(); |
|
|
|
if (isLoggedIn) { |
|
|
|
// 只有登录成功,才获取App配置 |
|
|
|
await getAppConfig(); |
|
|
|
_isAppConfigLoaded.value = true; |
|
|
|
} else { |
|
|
|
await User.clearUserSession(); // |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
_isLoggedIn.value = isLoggedIn; |
|
|
|
Logger.d(_tag, 'Token验证完成,登录状态: $isLoggedIn'); |
|
|
|
} catch (e) { |
|
|
|
Logger.e(_tag, 'Token验证失败: $e'); |
|
|
|
_isLoggedIn.value = false; |
|
|
|
} finally { |
|
|
|
_isTokenValidated.value = true; |
|
|
|
Logger.e(_tag, 'Token验证或配置加载失败: $e'); |
|
|
|
await User.clearUserSession(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 检查跳转条件:所有任务都完成后才跳转 |
|
|
|
void _checkNavigateCondition() { |
|
|
|
if (_isAnimationCompleted.value && |
|
|
|
_isTrackingPermissionRequested.value && |
|
|
|
_isVersionCheckCompleted.value && |
|
|
|
_isTokenValidated.value) { |
|
|
|
_navigateToTargetPage(); |
|
|
|
/// 异步执行版本检查 |
|
|
|
Future<void> _performVersionCheckAsync() async { |
|
|
|
try { |
|
|
|
final versionService = VersionUpdateService.to; |
|
|
|
final updateInfo = await versionService.checkForUpdate(); |
|
|
|
|
|
|
|
if (updateInfo.hasUpdate) { |
|
|
|
Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}'); |
|
|
|
final completer = Completer<void>(); |
|
|
|
// 在主线程中显示更新对话框 |
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) { |
|
|
|
// 使用 whenComplete 确保无论弹窗如何关闭,Future 都会完成 |
|
|
|
VersionUpdateDialog.show(updateInfo).whenComplete(() { |
|
|
|
if (!completer.isCompleted) completer.complete(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
// 等待弹窗交互结束 |
|
|
|
await completer.future; |
|
|
|
} else { |
|
|
|
Logger.i(_tag, '当前已是最新版本'); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
Logger.e(_tag, '版本检查失败: $e'); |
|
|
|
// 即使检查失败,也视为任务完成 |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 根据登录状态跳转到目标页面 |
|
|
|
/// 根据最终状态跳转到目标页面 |
|
|
|
void _navigateToTargetPage() { |
|
|
|
if (_isLoggedIn.value && _isAppConfigLoaded.value) { |
|
|
|
// 导航时直接检查最终的数据状态,而不是依赖中间变量 |
|
|
|
final token = _storage.read('logintoken'); |
|
|
|
final isAppConfigInitialized = AppConfig.isInitialized(); |
|
|
|
// 检查登录状态和配置初始化状态 |
|
|
|
if (token != null && token.isNotEmpty && isAppConfigInitialized) { |
|
|
|
Get.offAllNamed(Routes.pairing); |
|
|
|
} else { |
|
|
|
Get.offAllNamed(Routes.login); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 异步获取登录Token |
|
|
|
Future<bool> getLoginToken() async { |
|
|
|
try { |
|
|
|
Logger.d(_tag, 'getLoginToken:进来了'); |
|
|
|
//if (User.getname() != "") { |
|
|
|
final response = await Api.tokenlogin(); |
|
|
|
Logger.d(_tag, 'getLoginToken: $response'); |
|
|
|
if (response == null) return false; |
|
|
|
|
|
|
|
if (response['user'] != '') { |
|
|
|
Logger.d(_tag, 'getLoginToken:进来了'); |
|
|
|
// 根据API设计调整 |
|
|
|
if (response?['user'] != null && response['user'] != '') { |
|
|
|
final user = User.fromJson(response); |
|
|
|
|
|
|
|
// 存储用户信息(示例) |
|
|
|
await _storage.write('user_info', user.toJson()); |
|
|
|
return true; |
|
|
|
} |
|
|
|
//return true; |
|
|
|
//} |
|
|
|
return false; |
|
|
|
} catch (e) { |
|
|
|
Logger.e(_tag, 'Token获取失败: $e'); |
|
|
|
@ -218,140 +200,14 @@ class SplashController extends GetxController { |
|
|
|
|
|
|
|
Future<bool> getAppConfig() async { |
|
|
|
try { |
|
|
|
Logger.d(_tag, 'getAppConfig:进来了'); |
|
|
|
//if (User.getname() != "") { |
|
|
|
final response = await Api.getappconfig(); |
|
|
|
Logger.d(_tag, 'getAppConfig: $response'); |
|
|
|
if (response == null) return false; |
|
|
|
// 根据API设计调整 |
|
|
|
final resp = UserGetAppConfigResp.fromJson(response); |
|
|
|
Logger.d(_tag, 'getAppConfig:进来了'); |
|
|
|
// 存储用户信息(示例) |
|
|
|
// _storage.write('ENV', resp.env); |
|
|
|
// _storage.write('AGENTS', resp.agents); |
|
|
|
// _storage.write('MCPS', resp.mcps); |
|
|
|
AppConfig.initialize(resp); |
|
|
|
return true; |
|
|
|
} catch (e) { |
|
|
|
Logger.e(_tag, 'getAppConfig 取失败: $e'); |
|
|
|
Logger.e(_tag, 'getAppConfig 获取失败: $e'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 混合检查机制:判断是否需要进行版本检查 |
|
|
|
/// 检查条件:1. 会话内未检查过 OR 2. 距离上次检查超过24小时 |
|
|
|
Future<void> _checkVersionUpdateIfNeeded() async { |
|
|
|
try { |
|
|
|
final now = DateTime.now(); |
|
|
|
final lastCheckTimestamp = _storage.read(_lastVersionCheckKey); |
|
|
|
|
|
|
|
bool shouldCheck = false; |
|
|
|
String reason = ''; |
|
|
|
|
|
|
|
// 条件1:会话内未检查过 |
|
|
|
if (!_hasCheckedVersionInSession) { |
|
|
|
shouldCheck = true; |
|
|
|
reason = '会话内首次检查'; |
|
|
|
} |
|
|
|
// 条件2:距离上次检查超过24小时 |
|
|
|
else if (lastCheckTimestamp != null) { |
|
|
|
final lastCheckTime = |
|
|
|
DateTime.fromMillisecondsSinceEpoch(lastCheckTimestamp); |
|
|
|
final timeDifference = now.difference(lastCheckTime); |
|
|
|
|
|
|
|
if (timeDifference.inHours >= _versionCheckIntervalHours) { |
|
|
|
shouldCheck = true; |
|
|
|
reason = '距离上次检查已超过${_versionCheckIntervalHours}小时'; |
|
|
|
} |
|
|
|
} |
|
|
|
// 条件3:从未检查过(首次使用应用) |
|
|
|
else { |
|
|
|
shouldCheck = true; |
|
|
|
reason = '首次使用应用'; |
|
|
|
} |
|
|
|
|
|
|
|
if (shouldCheck) { |
|
|
|
Logger.i(_tag, '开始版本检查 - 原因: $reason'); |
|
|
|
await _performVersionCheck(); |
|
|
|
} else { |
|
|
|
Logger.i(_tag, '跳过版本检查 - 会话内已检查且未超过时间间隔'); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
Logger.e(_tag, '版本检查判断失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 执行版本检查(在后台进行,不阻塞启动流程) |
|
|
|
Future<void> _performVersionCheck() async { |
|
|
|
try { |
|
|
|
final versionService = VersionUpdateService.to; |
|
|
|
// 异步执行,不等待结果 |
|
|
|
versionService.checkForUpdate().then((updateInfo) { |
|
|
|
if (updateInfo.hasUpdate) { |
|
|
|
Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}'); |
|
|
|
// 在主线程中显示更新对话框 |
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) { |
|
|
|
VersionUpdateDialog.show(updateInfo).then((_) { |
|
|
|
// 无论用户选择更新还是稍后提醒,都标记版本检查完成 |
|
|
|
_isVersionCheckCompleted.value = true; |
|
|
|
_checkNavigateCondition(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
} else { |
|
|
|
_isVersionCheckCompleted.value = true; |
|
|
|
Logger.i(_tag, '当前已是最新版本: ${updateInfo.currentVersion}'); |
|
|
|
} |
|
|
|
|
|
|
|
// 更新检查记录 |
|
|
|
_updateVersionCheckRecord(); |
|
|
|
}).catchError((e) { |
|
|
|
Logger.e(_tag, '版本检查失败: $e'); |
|
|
|
// 即使检查失败,也标记版本检查完成,让流程继续 |
|
|
|
_isVersionCheckCompleted.value = true; |
|
|
|
// 更新检查记录,避免频繁重试 |
|
|
|
_updateVersionCheckRecord(); |
|
|
|
}); |
|
|
|
} catch (e) { |
|
|
|
Logger.e(_tag, '版本检查失败: $e'); |
|
|
|
// 即使检查失败,也标记版本检查完成,让流程继续 |
|
|
|
_isVersionCheckCompleted.value = true; |
|
|
|
// 更新检查记录,避免频繁重试 |
|
|
|
_updateVersionCheckRecord(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 更新版本检查记录 |
|
|
|
void _updateVersionCheckRecord() { |
|
|
|
try { |
|
|
|
// 更新时间戳 |
|
|
|
final now = DateTime.now(); |
|
|
|
_storage.write(_lastVersionCheckKey, now.millisecondsSinceEpoch); |
|
|
|
|
|
|
|
// 标记本次会话已检查过版本 |
|
|
|
_hasCheckedVersionInSession = true; |
|
|
|
|
|
|
|
Logger.i(_tag, '版本检查记录已更新: ${now.toIso8601String()}'); |
|
|
|
} catch (e) { |
|
|
|
Logger.e(_tag, '更新版本检查记录失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 重置版本检查标记(可用于强制重新检查) |
|
|
|
/// 注意:这会清除会话标记,但不会清除时间戳记录 |
|
|
|
static void resetVersionCheckFlag() { |
|
|
|
_hasCheckedVersionInSession = false; |
|
|
|
Logger.i('SplashCtrl', '版本检查会话标记已重置'); |
|
|
|
} |
|
|
|
|
|
|
|
/// 完全重置版本检查状态(包括时间戳) |
|
|
|
/// 用于测试或特殊情况下强制重新检查 |
|
|
|
void forceResetVersionCheck() { |
|
|
|
try { |
|
|
|
_hasCheckedVersionInSession = false; |
|
|
|
_storage.remove(_lastVersionCheckKey); |
|
|
|
Logger.i(_tag, '版本检查状态已完全重置'); |
|
|
|
} catch (e) { |
|
|
|
Logger.e(_tag, '重置版本检查状态失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|