清洁代码
This commit is contained in:
parent
00fc47c653
commit
2f0cd3ef37
|
|
@ -16,6 +16,7 @@ export 'core/http_config.dart';
|
|||
export 'core/permission_helper.dart';
|
||||
export 'models/install_strategy.dart';
|
||||
export 'models/upgrade_info.dart';
|
||||
// 导出市场选择对话框(其他对话框已整合到简化API中)
|
||||
export 'widgets/widgets.dart';
|
||||
|
||||
class AppUpgradePlugin {
|
||||
|
|
|
|||
|
|
@ -1,638 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart'; // Added for MethodChannel
|
||||
import 'package:flutter/widgets.dart'; // Added for WidgetsBinding
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'app_upgrade_plugin_platform_interface.dart';
|
||||
import 'core/cache_manager.dart';
|
||||
import 'core/download_manager.dart';
|
||||
import 'core/network_monitor.dart';
|
||||
import 'core/notification_helper.dart';
|
||||
import 'core/upgrade_config.dart';
|
||||
import 'core/version_comparator.dart';
|
||||
import 'models/upgrade_info.dart';
|
||||
|
||||
export 'core/cache_manager.dart';
|
||||
export 'core/download_manager.dart';
|
||||
export 'core/network_monitor.dart';
|
||||
export 'core/upgrade_config.dart';
|
||||
export 'core/version_comparator.dart';
|
||||
export 'models/upgrade_info.dart';
|
||||
export 'widgets/widgets.dart';
|
||||
|
||||
/// 升级回调
|
||||
typedef UpgradeCallback = void Function(UpgradeInfo info);
|
||||
typedef DownloadCallback = void Function(DownloadTask task);
|
||||
typedef ErrorCallback = void Function(String error);
|
||||
|
||||
/// 增强版App升级插件
|
||||
class AppUpgradePluginEnhanced {
|
||||
static AppUpgradePluginEnhanced? _instance;
|
||||
|
||||
/// 获取单例实例
|
||||
static AppUpgradePluginEnhanced get instance {
|
||||
_instance ??= AppUpgradePluginEnhanced._();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
AppUpgradePluginEnhanced._() {
|
||||
_init();
|
||||
}
|
||||
// 核心组件
|
||||
|
||||
final UpgradeConfig _config = UpgradeConfig.instance;
|
||||
final DownloadManager _downloadManager = DownloadManager.instance;
|
||||
final NetworkMonitor _networkMonitor = NetworkMonitor.instance;
|
||||
final CacheManager _cacheManager = CacheManager.instance;
|
||||
final NotificationHelper _notificationHelper = NotificationHelper.instance;
|
||||
late VersionComparator _versionComparator;
|
||||
final Dio _dio = Dio();
|
||||
|
||||
// 回调
|
||||
final List<UpgradeCallback> _upgradeCallbacks = [];
|
||||
final List<DownloadCallback> _downloadCallbacks = [];
|
||||
final List<ErrorCallback> _errorCallbacks = [];
|
||||
|
||||
// 状态
|
||||
UpgradeInfo? _currentUpgradeInfo;
|
||||
String? _currentDownloadTaskId;
|
||||
Timer? _autoCheckTimer;
|
||||
StreamSubscription<NetworkStatus>? _networkSubscription;
|
||||
|
||||
// 内存管理
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
final Map<String, WeakReference<Object>> _weakRefs = {};
|
||||
|
||||
/// 初始化
|
||||
void _init() {
|
||||
_versionComparator = VersionComparator(
|
||||
strategy: _config.debugMode ? VersionCompareStrategy.buildNumber : VersionCompareStrategy.semantic,
|
||||
);
|
||||
|
||||
// 延迟初始化Flutter相关服务,直到绑定完全初始化
|
||||
_scheduleFlutterServicesInit();
|
||||
|
||||
// 监听网络状态 - 这个可以立即初始化,因为不依赖Flutter绑定
|
||||
_networkSubscription = _networkMonitor.statusStream.listen(_onNetworkStatusChanged);
|
||||
_subscriptions.add(_networkSubscription!);
|
||||
|
||||
// 启动自动检查
|
||||
if (_config.autoCheck) {
|
||||
_startAutoCheck();
|
||||
}
|
||||
}
|
||||
|
||||
/// 延迟初始化Flutter相关服务
|
||||
void _scheduleFlutterServicesInit() {
|
||||
// 如果绑定已经初始化,直接执行
|
||||
_initFlutterServices();
|
||||
}
|
||||
|
||||
/// 初始化Flutter相关服务
|
||||
void _initFlutterServices() {
|
||||
try {
|
||||
// 初始化通知,并设置点击回调
|
||||
_notificationHelper.initialize(_onNotificationClick);
|
||||
|
||||
// 监听原生渠道的安装请求
|
||||
const MethodChannel('app_upgrade_plugin_channel').setMethodCallHandler((call) async {
|
||||
if (call.method == 'installApkFromNotification') {
|
||||
final filePath = call.arguments as String?;
|
||||
if (filePath != null) {
|
||||
await installApkSmart(filePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('Failed to initialize Flutter services: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 配置插件
|
||||
void configure({
|
||||
bool? debugMode,
|
||||
int? checkIntervalHours,
|
||||
bool? autoCheck,
|
||||
bool? wifiOnly,
|
||||
int? downloadTimeout,
|
||||
int? connectTimeout,
|
||||
int? maxRetryCount,
|
||||
bool? supportBreakpoint,
|
||||
bool? verifyIntegrity,
|
||||
VersionCompareStrategy? versionStrategy,
|
||||
Map<String, String>? customHeaders,
|
||||
}) {
|
||||
_config.updateConfig(
|
||||
debugMode: debugMode,
|
||||
checkIntervalHours: checkIntervalHours,
|
||||
autoCheck: autoCheck,
|
||||
wifiOnly: wifiOnly,
|
||||
downloadTimeout: downloadTimeout,
|
||||
connectTimeout: connectTimeout,
|
||||
maxRetryCount: maxRetryCount,
|
||||
supportBreakpoint: supportBreakpoint,
|
||||
verifyIntegrity: verifyIntegrity,
|
||||
customHeaders: customHeaders,
|
||||
);
|
||||
|
||||
if (versionStrategy != null) {
|
||||
_versionComparator = VersionComparator(strategy: versionStrategy);
|
||||
}
|
||||
|
||||
// 重启自动检查
|
||||
if (autoCheck != null) {
|
||||
if (autoCheck) {
|
||||
_startAutoCheck();
|
||||
} else {
|
||||
_stopAutoCheck();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取平台版本(无需平台接口)
|
||||
Future<String?> getPlatformVersion() async {
|
||||
try {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
return info.version;
|
||||
} catch (e) {
|
||||
debugPrint('getPlatformVersion failed: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取当前App信息
|
||||
Future<Map<String, String>> getAppInfo() async {
|
||||
return await _cacheManager.get<Map<String, String>>(
|
||||
'app_info',
|
||||
strategy: CacheStrategy.cacheFirst,
|
||||
networkFetcher: _getPackageInfoMap,
|
||||
) ??
|
||||
{};
|
||||
}
|
||||
|
||||
/// 检查更新(智能版)
|
||||
Future<UpgradeInfo?> checkUpdateSmart(
|
||||
String url, {
|
||||
Map<String, dynamic>? params,
|
||||
bool forceRefresh = false,
|
||||
Duration? cacheDuration,
|
||||
}) async {
|
||||
try {
|
||||
// 检查网络状态
|
||||
if (!_networkMonitor.isConnected) {
|
||||
_notifyError('No network connection');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查是否可以下载
|
||||
if (_config.wifiOnly && !_networkMonitor.isWifi) {
|
||||
_notifyError('WiFi required for update check');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 使用缓存策略
|
||||
final strategy = forceRefresh ? CacheStrategy.networkFirst : CacheStrategy.cacheFirst;
|
||||
|
||||
final upgradeInfo = await _cacheManager.get<UpgradeInfo>(
|
||||
'upgrade_info_$url',
|
||||
strategy: strategy,
|
||||
networkFetcher: () async {
|
||||
final info = await _fetchUpgradeInfo(url, params: params);
|
||||
if (info != null) {
|
||||
// 智能版本比较
|
||||
final appInfo = await getAppInfo();
|
||||
final currentVersion = appInfo['version'] ?? '0.0.0';
|
||||
|
||||
if (_versionComparator.isUpdateAvailable(currentVersion, info.versionName)) {
|
||||
// 判断更新类型
|
||||
final isMajor = _versionComparator.isMajorUpdate(currentVersion, info.versionName);
|
||||
|
||||
// 根据更新类型调整策略
|
||||
if (isMajor && !info.isForceUpdate) {
|
||||
// 主要版本更新建议强制
|
||||
debugPrint('Major update detected, suggesting force update');
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
if (upgradeInfo != null) {
|
||||
_currentUpgradeInfo = upgradeInfo;
|
||||
_notifyUpgrade(upgradeInfo);
|
||||
|
||||
// 预下载(如果配置了静默下载)
|
||||
if (_config.silentDownload && upgradeInfo.downloadUrl != null && _networkMonitor.isWifi) {
|
||||
_startSilentDownload(upgradeInfo.downloadUrl!);
|
||||
}
|
||||
}
|
||||
|
||||
return upgradeInfo;
|
||||
} catch (e) {
|
||||
_notifyError('Check update failed: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _getPackageInfoMap() async {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
return {
|
||||
'appName': packageInfo.appName,
|
||||
'packageName': packageInfo.packageName,
|
||||
'version': packageInfo.version,
|
||||
'buildNumber': packageInfo.buildNumber,
|
||||
};
|
||||
}
|
||||
|
||||
/// 直接通过网络请求拉取升级信息,避免依赖平台接口实现
|
||||
Future<UpgradeInfo?> _fetchUpgradeInfo(
|
||||
String url, {
|
||||
Map<String, dynamic>? params,
|
||||
}) async {
|
||||
try {
|
||||
final appInfo = await getAppInfo();
|
||||
final requestParams = <String, dynamic>{
|
||||
'version': appInfo['version'],
|
||||
'buildNumber': appInfo['buildNumber'],
|
||||
'platform': Platform.isAndroid ? 'android' : 'ios',
|
||||
...?params,
|
||||
};
|
||||
|
||||
final response = await _dio.get(url, queryParameters: requestParams);
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
return UpgradeInfo.fromJson(response.data);
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('fetch upgrade info failed: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 智能下载APK
|
||||
Future<String?> downloadApkSmart(
|
||||
String url, {
|
||||
String? versionName, // 新增版本名,用于标准化文件名
|
||||
Function(DownloadProgress)? onProgress,
|
||||
String? savePath,
|
||||
String? md5,
|
||||
String? sha256,
|
||||
bool resumeIfExists = true,
|
||||
}) async {
|
||||
if (!Platform.isAndroid) {
|
||||
throw UnsupportedError('downloadApk only supports Android platform');
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查网络状态
|
||||
final networkStatus = _networkMonitor.currentStatus;
|
||||
if (networkStatus == null || !networkStatus.isConnected) {
|
||||
_notifyError('No network connection for download');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 创建下载任务
|
||||
_currentDownloadTaskId = await _downloadManager.createTask(
|
||||
url: url,
|
||||
savePath: savePath,
|
||||
md5: md5,
|
||||
sha256: sha256,
|
||||
versionName: versionName ?? _currentUpgradeInfo?.versionName,
|
||||
);
|
||||
|
||||
// 监听下载进度
|
||||
final progressSubscription = _downloadManager.getProgressStream(_currentDownloadTaskId!)?.listen((task) {
|
||||
_notifyDownload(task);
|
||||
|
||||
// 更新通知栏进度
|
||||
_updateNotificationForTask(task);
|
||||
|
||||
if (onProgress != null && task.totalSize != null) {
|
||||
onProgress(DownloadProgress(received: task.downloadedSize, total: task.totalSize!));
|
||||
}
|
||||
});
|
||||
|
||||
if (progressSubscription != null) {
|
||||
_subscriptions.add(progressSubscription);
|
||||
}
|
||||
|
||||
// 开始下载
|
||||
final success = await _downloadManager.startDownload(_currentDownloadTaskId!);
|
||||
|
||||
if (success) {
|
||||
final task = _downloadManager.getTask(_currentDownloadTaskId!);
|
||||
return task?.savePath;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
_notifyError('Download failed: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 安装APK(带权限检查)
|
||||
Future<bool> installApkSmart(String filePath) async {
|
||||
if (!Platform.isAndroid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查文件是否存在
|
||||
final file = File(filePath);
|
||||
if (!await file.exists()) {
|
||||
_notifyError('APK file not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查文件完整性(如果有MD5)
|
||||
if (_currentUpgradeInfo?.apkMd5 != null) {
|
||||
final isValid = await DownloadManager.verifyFileInIsolate({
|
||||
'filePath': filePath,
|
||||
'md5': _currentUpgradeInfo!.apkMd5,
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
_notifyError('APK file integrity check failed');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return await AppUpgradePluginPlatform.instance.installApk(filePath);
|
||||
} catch (e) {
|
||||
_notifyError('Install failed: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 跳转到应用商店(直接使用 url_launcher,避免依赖平台通道)
|
||||
Future<bool> goToAppStore(String url) async {
|
||||
try {
|
||||
final uri = Uri.parse(url);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
return await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
_notifyError('Failed to open app store: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 暂停下载
|
||||
void pauseDownload() {
|
||||
if (_currentDownloadTaskId != null) {
|
||||
_downloadManager.pauseDownload(_currentDownloadTaskId!);
|
||||
}
|
||||
}
|
||||
|
||||
/// 恢复下载
|
||||
Future<bool> resumeDownload() async {
|
||||
if (_currentDownloadTaskId != null) {
|
||||
return await _downloadManager.resumeDownload(_currentDownloadTaskId!);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 取消下载
|
||||
void cancelDownload() {
|
||||
if (_currentDownloadTaskId != null) {
|
||||
_downloadManager.cancelDownload(_currentDownloadTaskId!);
|
||||
_notificationHelper.cancelNotification(); // 取消时移除通知
|
||||
_currentDownloadTaskId = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 重试下载
|
||||
Future<bool> retryDownload() async {
|
||||
if (_currentDownloadTaskId != null) {
|
||||
return await _downloadManager.retryDownload(_currentDownloadTaskId!);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 获取下载任务
|
||||
DownloadTask? getCurrentDownloadTask() {
|
||||
if (_currentDownloadTaskId != null) {
|
||||
return _downloadManager.getTask(_currentDownloadTaskId!);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 添加升级回调
|
||||
void addUpgradeCallback(UpgradeCallback callback) {
|
||||
_upgradeCallbacks.add(callback);
|
||||
}
|
||||
|
||||
/// 移除升级回调
|
||||
void removeUpgradeCallback(UpgradeCallback callback) {
|
||||
_upgradeCallbacks.remove(callback);
|
||||
}
|
||||
|
||||
/// 添加下载回调
|
||||
void addDownloadCallback(DownloadCallback callback) {
|
||||
_downloadCallbacks.add(callback);
|
||||
}
|
||||
|
||||
/// 移除下载回调
|
||||
void removeDownloadCallback(DownloadCallback callback) {
|
||||
_downloadCallbacks.remove(callback);
|
||||
}
|
||||
|
||||
/// 添加错误回调
|
||||
void addErrorCallback(ErrorCallback callback) {
|
||||
_errorCallbacks.add(callback);
|
||||
}
|
||||
|
||||
/// 移除错误回调
|
||||
void removeErrorCallback(ErrorCallback callback) {
|
||||
_errorCallbacks.remove(callback);
|
||||
}
|
||||
|
||||
/// 开始自动检查
|
||||
void _startAutoCheck() {
|
||||
_stopAutoCheck();
|
||||
|
||||
_autoCheckTimer = Timer.periodic(Duration(hours: _config.checkIntervalHours), (_) async {
|
||||
// 自动检查需要在WiFi环境下
|
||||
if (_config.wifiOnly && !_networkMonitor.isWifi) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行检查(使用缓存的URL)
|
||||
final lastCheckUrl = await _cacheManager.get<String>('last_check_url');
|
||||
if (lastCheckUrl != null) {
|
||||
await checkUpdateSmart(lastCheckUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 停止自动检查
|
||||
void _stopAutoCheck() {
|
||||
_autoCheckTimer?.cancel();
|
||||
_autoCheckTimer = null;
|
||||
}
|
||||
|
||||
/// 静默下载
|
||||
void _startSilentDownload(String url) async {
|
||||
if (!_config.silentDownload) return;
|
||||
if (!_networkMonitor.isWifi) return;
|
||||
|
||||
try {
|
||||
final taskId = await _downloadManager.createTask(url: url);
|
||||
await _downloadManager.startDownload(taskId);
|
||||
} catch (e) {
|
||||
debugPrint('Silent download failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 网络状态变化处理
|
||||
void _onNetworkStatusChanged(NetworkStatus status) {
|
||||
if (_config.debugMode) {
|
||||
debugPrint('Network status changed: ${status.toJson()}');
|
||||
}
|
||||
|
||||
// 网络恢复时恢复下载
|
||||
if (status.isConnected && _currentDownloadTaskId != null) {
|
||||
final task = _downloadManager.getTask(_currentDownloadTaskId!);
|
||||
if (task != null && task.status == DownloadStatus.paused) {
|
||||
resumeDownload();
|
||||
}
|
||||
}
|
||||
|
||||
// WiFi环境下启动静默下载
|
||||
if (status.type == NetworkType.wifi && _config.silentDownload && _currentUpgradeInfo?.downloadUrl != null) {
|
||||
_startSilentDownload(_currentUpgradeInfo!.downloadUrl!);
|
||||
}
|
||||
}
|
||||
|
||||
/// 通知升级信息
|
||||
void _notifyUpgrade(UpgradeInfo info) {
|
||||
for (final callback in _upgradeCallbacks) {
|
||||
callback(info);
|
||||
}
|
||||
}
|
||||
|
||||
/// 通知下载进度
|
||||
void _notifyDownload(DownloadTask task) {
|
||||
for (final callback in _downloadCallbacks) {
|
||||
callback(task);
|
||||
}
|
||||
}
|
||||
|
||||
/// 通知错误
|
||||
void _notifyError(String error) {
|
||||
if (_config.debugMode) {
|
||||
debugPrint('AppUpgrade Error: $error');
|
||||
}
|
||||
|
||||
for (final callback in _errorCallbacks) {
|
||||
callback(error);
|
||||
}
|
||||
}
|
||||
|
||||
/// 通知点击回调
|
||||
Future<void> _onNotificationClick(NotificationResponse response) async {
|
||||
if (response.payload != null && response.payload!.startsWith('download_complete:')) {
|
||||
final filePath = response.payload!.split(':').last;
|
||||
await installApkSmart(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据下载任务状态更新通知
|
||||
void _updateNotificationForTask(DownloadTask task) async {
|
||||
final progress = (task.progress * 100).toInt();
|
||||
final appInfo = await _cacheManager.get<Map<String, String>>('app_info', strategy: CacheStrategy.cacheFirst);
|
||||
final appName = appInfo?['appName'] ?? '应用';
|
||||
|
||||
switch (task.status) {
|
||||
case DownloadStatus.downloading:
|
||||
_notificationHelper.showDownloadProgressNotification(
|
||||
progress: progress,
|
||||
title: '$appName 下载中...',
|
||||
body: '${(task.progress * 100).toStringAsFixed(1)}%',
|
||||
);
|
||||
break;
|
||||
case DownloadStatus.completed:
|
||||
_notificationHelper.showDownloadCompleteNotification(
|
||||
title: '$appName 下载完成',
|
||||
body: '点击安装新版本',
|
||||
filePath: task.savePath,
|
||||
);
|
||||
break;
|
||||
case DownloadStatus.failed:
|
||||
_notificationHelper.showDownloadFailedNotification(
|
||||
title: '$appName 下载失败',
|
||||
body: task.errorMessage ?? '请检查网络后重试',
|
||||
);
|
||||
break;
|
||||
case DownloadStatus.cancelled:
|
||||
_notificationHelper.cancelNotification();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取缓存统计
|
||||
Future<Map<String, dynamic>> getCacheStats() {
|
||||
return _cacheManager.getCacheStats();
|
||||
}
|
||||
|
||||
/// 清空缓存
|
||||
Future<void> clearCache() {
|
||||
return _cacheManager.clear();
|
||||
}
|
||||
|
||||
/// 获取网络状态
|
||||
NetworkStatus? get networkStatus => _networkMonitor.currentStatus;
|
||||
|
||||
/// 刷新网络状态
|
||||
Future<void> refreshNetworkStatus() async {
|
||||
await _networkMonitor.refreshNetworkStatus();
|
||||
}
|
||||
|
||||
/// 获取当前配置
|
||||
UpgradeConfig get config => _config;
|
||||
|
||||
/// 获取版本比较器
|
||||
VersionComparator get versionComparator => _versionComparator;
|
||||
|
||||
/// 释放资源(优化内存)
|
||||
void dispose() {
|
||||
// 停止自动检查
|
||||
_stopAutoCheck();
|
||||
|
||||
// 取消所有订阅
|
||||
for (final subscription in _subscriptions) {
|
||||
subscription.cancel();
|
||||
}
|
||||
_subscriptions.clear();
|
||||
|
||||
// 清理回调
|
||||
_upgradeCallbacks.clear();
|
||||
_downloadCallbacks.clear();
|
||||
_errorCallbacks.clear();
|
||||
|
||||
// 释放组件资源
|
||||
_downloadManager.dispose();
|
||||
_networkMonitor.dispose();
|
||||
_cacheManager.dispose();
|
||||
|
||||
// 清理弱引用
|
||||
_weakRefs.clear();
|
||||
|
||||
// 清空当前状态
|
||||
_currentUpgradeInfo = null;
|
||||
_currentDownloadTaskId = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -167,20 +167,8 @@ class MethodChannelAppUpgradePlugin extends AppUpgradePluginPlatform {
|
|||
|
||||
debugPrint('响应数据: $responseData');
|
||||
|
||||
// TODO 解析更新信息
|
||||
// final upgradeInfo = UpgradeInfo.fromJson(responseData);
|
||||
final upgradeInfo = UpgradeInfo.fromJson({
|
||||
"hasUpdate": true,
|
||||
"isForceUpdate": true,
|
||||
"versionCode": "101", // Android buildNumber, 必须是数字字符串
|
||||
"versionName": "1.0.1", // 显示的版本名
|
||||
"updateContent": "1. 修复了xxx Bug。\n2. 优化了用户体验。",
|
||||
"downloadUrl":
|
||||
"https://dpc-job-oss.23544.com/infra-app/making_school_asignment_app/1.0.5/1/app-release.apk", // APK 下载地址
|
||||
"appStoreUrl": "https://itunes.apple.com/app/id123456", // iOS App Store 地址
|
||||
"apkSize": 20971520, // APK 文件大小 (单位: byte)
|
||||
"apkMd5": "b10a8db164e0754105b7a99be72e3fe5" // APK 的 MD5 (可选,用于校验)
|
||||
});
|
||||
// 解析更新信息
|
||||
final upgradeInfo = UpgradeInfo.fromJson(responseData);
|
||||
|
||||
// 比较版本
|
||||
if (_compareVersion(upgradeInfo.versionCode, currentBuildNumber) > 0) {
|
||||
|
|
|
|||
|
|
@ -73,25 +73,26 @@ class AppUpgradeSimple {
|
|||
} catch (e) {
|
||||
debugPrint('检查更新失败: $e');
|
||||
|
||||
String errorMessage = '检查更新失败';
|
||||
final errorString = e.toString();
|
||||
final String errorMessage;
|
||||
|
||||
if (e.toString().contains('无网络连接')) {
|
||||
if (errorString.contains('无网络连接')) {
|
||||
errorMessage = '无网络连接,请检查网络设置';
|
||||
} else if (e.toString().contains('Failed host lookup')) {
|
||||
} else if (errorString.contains('Failed host lookup')) {
|
||||
errorMessage = '无法连接到服务器,请检查网络或稍后重试';
|
||||
} else if (e.toString().contains('Connection refused')) {
|
||||
} else if (errorString.contains('Connection refused')) {
|
||||
errorMessage = '服务器拒绝连接,请稍后重试';
|
||||
} else if (e.toString().contains('timeout')) {
|
||||
} else if (errorString.contains('timeout')) {
|
||||
errorMessage = '连接超时,请检查网络';
|
||||
} else {
|
||||
errorMessage = '检查更新失败: ${e.toString().split(':').first}';
|
||||
errorMessage = '检查更新失败: ${errorString.split(':').first}';
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
_showToast(errorMessage);
|
||||
|
||||
// 如果是网络问题,显示网络诊断建议
|
||||
if (e.toString().contains('Failed host lookup') || e.toString().contains('无网络连接')) {
|
||||
if (errorString.contains('Failed host lookup') || errorString.contains('无网络连接')) {
|
||||
debugPrint('建议: 请检查网络连接或尝试使用网络诊断功能');
|
||||
}
|
||||
}
|
||||
|
|
@ -99,26 +100,6 @@ class AppUpgradeSimple {
|
|||
}
|
||||
}
|
||||
|
||||
/// 在无法显示对话框时的后备逻辑:显示Toast并尝试后台下载/安装
|
||||
Future<void> _showToastAndDownloadInBackground({
|
||||
required BuildContext context,
|
||||
required UpgradeInfo info,
|
||||
required bool autoDownload,
|
||||
required bool autoInstall,
|
||||
}) async {
|
||||
if (info.isForceUpdate) {
|
||||
_showToast('有重要更新,但无法显示对话框。请在 MaterialApp 环境内重试。');
|
||||
} else {
|
||||
_showToast('发现新版本: ${info.versionName}');
|
||||
if (autoDownload && Platform.isAndroid && info.downloadUrl != null) {
|
||||
final filePath = await _plugin.downloadApk(info.downloadUrl!, onProgress: (_) {});
|
||||
if (filePath != null && autoInstall) {
|
||||
await _installApkHeadless(context, filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 静默检查更新(不显示无更新提示)
|
||||
Future<UpgradeInfo?> checkUpdateSilent({
|
||||
required String url,
|
||||
|
|
@ -159,37 +140,6 @@ class AppUpgradeSimple {
|
|||
);
|
||||
}
|
||||
|
||||
/// 显示备用升级对话框(当MaterialApp环境不可用时)
|
||||
void _showFallbackUpgradeDialog({
|
||||
required BuildContext context,
|
||||
required UpgradeInfo info,
|
||||
required bool autoDownload,
|
||||
required bool autoInstall,
|
||||
VoidCallback? onComplete,
|
||||
}) {
|
||||
showGeneralDialog(
|
||||
context: context,
|
||||
barrierDismissible: !info.isForceUpdate,
|
||||
barrierLabel: 'Dismiss', // Provide a non-localized label
|
||||
pageBuilder: (buildContext, animation, secondaryAnimation) {
|
||||
// 使用一个包装器来提供基本的文本样式和方向,这对于独立于Material的对话框是必需的
|
||||
return Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: _FallbackUpgradeDialog(
|
||||
info: info,
|
||||
autoDownload: autoDownload,
|
||||
autoInstall: autoInstall,
|
||||
onComplete: onComplete,
|
||||
showToast: (message) => _showToast(message),
|
||||
),
|
||||
);
|
||||
},
|
||||
).then((_) {
|
||||
// The dialog has been dismissed.
|
||||
onComplete?.call();
|
||||
});
|
||||
}
|
||||
|
||||
/// 显示Toast提示
|
||||
void _showToast(String message) {
|
||||
Fluttertoast.showToast(msg: message);
|
||||
|
|
@ -206,20 +156,6 @@ class AppUpgradeSimple {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 共享的安装逻辑 (无UI)
|
||||
Future<void> _installApkHeadless(BuildContext context, String filePath) async {
|
||||
if (!Platform.isAndroid) return;
|
||||
final hasPermission = await PermissionHelper.checkAndRequestInstallPermission(context: context);
|
||||
if (!hasPermission) {
|
||||
_showToast('未授予安装权限,无法完成更新');
|
||||
return;
|
||||
}
|
||||
final success = await _plugin.installApk(filePath);
|
||||
if (!success) {
|
||||
_showToast('安装失败,请手动安装');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 共享的升级对话框内容构建器
|
||||
|
|
@ -238,7 +174,7 @@ class _UpgradeDialogContent extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<String> changeItems =
|
||||
final changeItems =
|
||||
info.updateContent.split(RegExp(r'\r?\n')).map((e) => e.trim()).where((e) => e.isNotEmpty).toList();
|
||||
|
||||
return Column(
|
||||
|
|
@ -392,136 +328,6 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _showInstallMethodChooser() async {
|
||||
final bool hasMarkets = info.appMarkets != null && info.appMarkets!.isNotEmpty;
|
||||
final bool hasDownload = info.downloadUrl != null;
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// 始终提供“应用市场”入口;无具体列表则用通用 market:// 链接
|
||||
final choice = await showModalBottomSheet<String>(
|
||||
context: context,
|
||||
isScrollControlled: false,
|
||||
useRootNavigator: true,
|
||||
// UI Beautification: Rounded corners
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (ctx) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Draggable handle
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
// Title and Close Button
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
const Text('选择更新方式', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
Positioned(
|
||||
right: -12,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Option 1: App Market
|
||||
ListTile(
|
||||
leading: const Icon(Icons.storefront_outlined),
|
||||
title: const Text('前往应用市场更新'),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
onTap: () => Navigator.of(ctx).pop('market'),
|
||||
),
|
||||
|
||||
// Option 2: Direct Download
|
||||
if (hasDownload)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.download_for_offline_outlined),
|
||||
title: const Text('直接下载安装包'),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
onTap: () => Navigator.of(ctx).pop('download'),
|
||||
),
|
||||
|
||||
const Divider(height: 24),
|
||||
|
||||
// Cancel Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
elevation: 2,
|
||||
shadowColor: Colors.grey.withOpacity(0.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
),
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: const Text('取消', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (choice == 'market') {
|
||||
if (hasMarkets) {
|
||||
await MarketSelectionDialog.show(
|
||||
context,
|
||||
markets: info.appMarkets!,
|
||||
onSelected: (market) async {
|
||||
if (market.url != null && market.url!.isNotEmpty) {
|
||||
_plugin.goToAppStore(market.url!);
|
||||
} else {
|
||||
final appInfo = await _plugin.getAppInfo();
|
||||
final pkg = appInfo['packageName'] ?? '';
|
||||
if (pkg.isNotEmpty) {
|
||||
_plugin.goToAppStore('market://details?id=$pkg');
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
final appInfo = await _plugin.getAppInfo();
|
||||
final pkg = appInfo['packageName'] ?? '';
|
||||
if (pkg.isNotEmpty) {
|
||||
_plugin.goToAppStore('market://details?id=$pkg');
|
||||
}
|
||||
}
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
onComplete?.call();
|
||||
return;
|
||||
}
|
||||
|
||||
if (choice == 'download' && hasDownload && !_isDownloading) {
|
||||
await _startDownloadAndInstall();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleAction() {
|
||||
if (Platform.isAndroid) {
|
||||
_handleAndroidAction();
|
||||
|
|
@ -550,14 +356,10 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
Future<void> _handleAndroidAction() async {
|
||||
// On Android, we always assume a market option is available,
|
||||
// because we can fall back to a generic market:// intent.
|
||||
const bool hasMarketOption = true;
|
||||
final bool hasDownloadOption = info.downloadUrl != null;
|
||||
|
||||
// This case is unlikely on Android, but kept for robustness.
|
||||
if (!hasMarketOption && !hasDownloadOption) {
|
||||
showToast('No update method available.');
|
||||
return;
|
||||
}
|
||||
// 在Android上,我们总是假设市场选项可用,因为可以回退到通用的 market:// intent
|
||||
// 因此这里不需要检查无更新方法的情况
|
||||
|
||||
// If a download option is not available, going to the market is the only choice.
|
||||
if (!hasDownloadOption) {
|
||||
|
|
@ -587,7 +389,6 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
final pkg = appInfo['packageName'] ?? '';
|
||||
if (pkg.isNotEmpty) {
|
||||
_plugin.goToAppStore('market://details?id=$pkg');
|
||||
// _plugin.goToAppStore('market://details?id=$pkg');
|
||||
} else {
|
||||
showToast('Could not determine app package name.');
|
||||
}
|
||||
|
|
@ -596,8 +397,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
|
||||
/// Pops the current dialog and then performs the market action.
|
||||
Future<void> _handleMarketAction() async {
|
||||
// Pop the upgrade dialog before proceeding.
|
||||
if (Navigator.canPop(context)) {
|
||||
// 对于强制更新,不关闭对话框
|
||||
if (!info.isForceUpdate && Navigator.canPop(context)) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
if (!mounted) return;
|
||||
|
|
@ -606,8 +407,6 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
}
|
||||
|
||||
Future<void> _showDownloadChoiceSheet() async {
|
||||
final bool hasDownload = info.downloadUrl != null;
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
final choice = await showModalBottomSheet<String>(
|
||||
|
|
@ -662,13 +461,12 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
),
|
||||
|
||||
// Option 2: Direct Download
|
||||
if (hasDownload)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.download_for_offline_outlined),
|
||||
title: const Text('直接下载安装包'),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
onTap: () => Navigator.of(ctx).pop('download'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.download_for_offline_outlined),
|
||||
title: const Text('直接下载安装包'),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
onTap: () => Navigator.of(ctx).pop('download'),
|
||||
),
|
||||
|
||||
const Divider(height: 24),
|
||||
|
||||
|
|
@ -699,12 +497,17 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
);
|
||||
|
||||
if (choice == 'market') {
|
||||
await _performMarketAction();
|
||||
onComplete?.call();
|
||||
// 对于强制更新,不调用 onComplete,保持对话框打开状态
|
||||
if (!info.isForceUpdate) {
|
||||
await _performMarketAction();
|
||||
onComplete?.call();
|
||||
} else {
|
||||
await _performMarketAction();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (choice == 'download' && hasDownload && !_isDownloading) {
|
||||
if (choice == 'download' && !_isDownloading) {
|
||||
await _startDownloadAndInstall();
|
||||
}
|
||||
}
|
||||
|
|
@ -744,8 +547,6 @@ class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _Upgrad
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
|
|
@ -756,7 +557,7 @@ class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _Upgrad
|
|||
children: [
|
||||
Icon(
|
||||
Icons.system_update,
|
||||
color: theme.colorScheme.primary,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text('发现新版本'),
|
||||
|
|
@ -772,14 +573,13 @@ class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _Upgrad
|
|||
actions: _isDownloading
|
||||
? []
|
||||
: [
|
||||
if (!widget.info.isForceUpdate)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
widget.onComplete?.call();
|
||||
},
|
||||
child: const Text('稍后更新'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
widget.onComplete?.call();
|
||||
},
|
||||
child: const Text('稍后更新'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _handleAction,
|
||||
child: Text(Platform.isAndroid ? '立即更新' : '前往更新'),
|
||||
|
|
@ -799,78 +599,20 @@ class _ForceUpgradeDialog extends StatefulWidget {
|
|||
State<_ForceUpgradeDialog> createState() => _ForceUpgradeDialogState();
|
||||
}
|
||||
|
||||
class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> {
|
||||
final _plugin = AppUpgradePlugin();
|
||||
bool _isDownloading = false;
|
||||
double _downloadProgress = 0;
|
||||
String _statusText = '';
|
||||
|
||||
class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> with _UpgradeDialogLogic {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 强制更新也需用户交互后再开始下载
|
||||
}
|
||||
|
||||
Future<void> _startDownload() async {
|
||||
// 下载前申请权限 (Just-in-Time)
|
||||
if (Platform.isAndroid) {
|
||||
final hasStorage = await PermissionHelper.checkAndRequestStoragePermission(context: context);
|
||||
if (!hasStorage) {
|
||||
setState(() {
|
||||
_statusText = '缺少存储权限,无法下载';
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 通知权限仅为辅助
|
||||
await PermissionHelper.checkAndRequestNotificationPermission(context: context);
|
||||
}
|
||||
|
||||
if (widget.info.downloadUrl == null) {
|
||||
setState(() {
|
||||
_statusText = '下载地址无效';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isDownloading = true;
|
||||
_statusText = '准备下载...';
|
||||
});
|
||||
|
||||
final filePath = await _plugin.downloadApk(
|
||||
widget.info.downloadUrl!,
|
||||
onProgress: (p) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_downloadProgress = p.progress;
|
||||
_statusText = '下载中 ${p.percentage}%';
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if (filePath != null) {
|
||||
await _installApk(context, filePath);
|
||||
}
|
||||
}
|
||||
|
||||
// 进度已在 downloadApk 的回调中更新
|
||||
|
||||
Future<void> _installApk(BuildContext context, String filePath) async {
|
||||
final hasPermission = await PermissionHelper.checkAndRequestInstallPermission(context: context);
|
||||
if (!hasPermission) {
|
||||
if (context.mounted) {
|
||||
setState(() {
|
||||
_statusText = '未授予安装权限,请手动授权后重试';
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
await _plugin.installApk(filePath);
|
||||
}
|
||||
UpgradeInfo get info => widget.info;
|
||||
@override
|
||||
void Function(String) get showToast => (message) => AppUpgradeSimple.instance._showToast(message);
|
||||
@override
|
||||
VoidCallback? get onComplete => null;
|
||||
@override
|
||||
bool get autoDownload => false;
|
||||
@override
|
||||
bool get autoInstall => true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return WillPopScope(
|
||||
onWillPop: () async => false, // 强制更新,不允许返回
|
||||
child: AlertDialog(
|
||||
|
|
@ -883,7 +625,7 @@ class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> {
|
|||
children: [
|
||||
Icon(
|
||||
Icons.system_update,
|
||||
color: theme.colorScheme.primary,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text('发现新版本 (强制)'),
|
||||
|
|
@ -900,253 +642,11 @@ class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> {
|
|||
? []
|
||||
: [
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
// 强制更新下也先让用户选择安装方式
|
||||
final hasMarkets = widget.info.appMarkets != null && widget.info.appMarkets!.isNotEmpty;
|
||||
final hasDownload = widget.info.downloadUrl != null;
|
||||
|
||||
if (hasMarkets && hasDownload) {
|
||||
final choice = await showModalBottomSheet<String>(
|
||||
context: context,
|
||||
isScrollControlled: false,
|
||||
useRootNavigator: true,
|
||||
// UI Beautification: Rounded corners
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (ctx) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Draggable handle
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
// Title and Close Button
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
const Text('选择更新方式',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
Positioned(
|
||||
right: -12,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Option 1: App Market
|
||||
ListTile(
|
||||
leading: const Icon(Icons.storefront_outlined),
|
||||
title: const Text('前往应用市场更新'),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
onTap: () => Navigator.of(ctx).pop('market'),
|
||||
),
|
||||
|
||||
// Option 2: Direct Download
|
||||
if (hasDownload)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.download_for_offline_outlined),
|
||||
title: const Text('直接下载安装包'),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
onTap: () => Navigator.of(ctx).pop('download'),
|
||||
),
|
||||
|
||||
const Divider(height: 24),
|
||||
|
||||
// Cancel Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
elevation: 2,
|
||||
shadowColor: Colors.grey.withOpacity(0.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
),
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child:
|
||||
const Text('取消', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (choice == 'market') {
|
||||
Navigator.of(context).pop();
|
||||
MarketSelectionDialog.show(
|
||||
context,
|
||||
markets: widget.info.appMarkets!,
|
||||
onSelected: (market) {
|
||||
_plugin.goToAppStore(market.url ?? market.packageName ?? '');
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (choice == 'download') {
|
||||
await _startDownload();
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasMarkets) {
|
||||
Navigator.of(context).pop();
|
||||
MarketSelectionDialog.show(
|
||||
context,
|
||||
markets: widget.info.appMarkets!,
|
||||
onSelected: (market) {
|
||||
_plugin.goToAppStore(market.url ?? market.packageName ?? '');
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasDownload) {
|
||||
await _startDownload();
|
||||
return;
|
||||
}
|
||||
},
|
||||
child: const Text('立即更新'),
|
||||
onPressed: _handleAction,
|
||||
child: Text(Platform.isAndroid ? '立即更新' : '前往更新'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 当 [MaterialApp] 不可用时,提供一个基础的回退升级对话框
|
||||
class _FallbackUpgradeDialog extends StatefulWidget {
|
||||
final UpgradeInfo info;
|
||||
final bool autoDownload;
|
||||
final bool autoInstall;
|
||||
final VoidCallback? onComplete;
|
||||
final void Function(String) showToast;
|
||||
|
||||
const _FallbackUpgradeDialog({
|
||||
required this.info,
|
||||
required this.autoDownload,
|
||||
required this.autoInstall,
|
||||
this.onComplete,
|
||||
required this.showToast,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_FallbackUpgradeDialog> createState() => _FallbackUpgradeDialogState();
|
||||
}
|
||||
|
||||
class _FallbackUpgradeDialogState extends State<_FallbackUpgradeDialog> with _UpgradeDialogLogic {
|
||||
@override
|
||||
UpgradeInfo get info => widget.info;
|
||||
@override
|
||||
void Function(String) get showToast => widget.showToast;
|
||||
@override
|
||||
VoidCallback? get onComplete => widget.onComplete;
|
||||
@override
|
||||
bool get autoDownload => widget.autoDownload;
|
||||
@override
|
||||
bool get autoInstall => widget.autoInstall;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final title = '发现新版本${widget.info.isForceUpdate ? " (强制)" : ""}';
|
||||
|
||||
final body = Center(
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(24.0),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DefaultTextStyle(
|
||||
style: const TextStyle(color: Colors.black87, fontSize: 14),
|
||||
child: _UpgradeDialogContent(
|
||||
info: widget.info,
|
||||
isDownloading: _isDownloading,
|
||||
downloadProgress: _downloadProgress,
|
||||
statusText: _statusText,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (!widget.info.isForceUpdate && !_isDownloading) ...[
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
widget.onComplete?.call();
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Text('稍后更新', style: TextStyle(color: Colors.blue)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
GestureDetector(
|
||||
onTap: _isDownloading ? null : _handleAction,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: _isDownloading ? Colors.grey : Colors.blue,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
_isDownloading
|
||||
? '下载中...'
|
||||
: Platform.isAndroid
|
||||
? '立即更新'
|
||||
: '前往更新',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return widget.info.isForceUpdate ? WillPopScope(onWillPop: () async => false, child: body) : body;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,495 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart'; // Added for WidgetsBinding
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// 缓存条目
|
||||
class CacheEntry<T> {
|
||||
final String key;
|
||||
final T data;
|
||||
final DateTime createdAt;
|
||||
final DateTime? expiresAt;
|
||||
final Map<String, dynamic>? metadata;
|
||||
|
||||
CacheEntry({required this.key, required this.data, required this.createdAt, this.expiresAt, this.metadata});
|
||||
|
||||
bool get isExpired {
|
||||
if (expiresAt == null) return false;
|
||||
return DateTime.now().isAfter(expiresAt!);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'key': key,
|
||||
'data': data is Map || data is List ? data : data.toString(),
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'expiresAt': expiresAt?.toIso8601String(),
|
||||
'metadata': metadata,
|
||||
};
|
||||
|
||||
factory CacheEntry.fromJson(Map<String, dynamic> json) {
|
||||
return CacheEntry<T>(
|
||||
key: json['key'],
|
||||
data: json['data'] as T,
|
||||
createdAt: DateTime.parse(json['createdAt']),
|
||||
expiresAt: json['expiresAt'] != null ? DateTime.parse(json['expiresAt']) : null,
|
||||
metadata: json['metadata'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 缓存策略
|
||||
enum CacheStrategy {
|
||||
/// 优先使用缓存,缓存过期后才请求网络
|
||||
cacheFirst,
|
||||
|
||||
/// 优先使用网络,网络失败时使用缓存
|
||||
networkFirst,
|
||||
|
||||
/// 只使用缓存
|
||||
cacheOnly,
|
||||
|
||||
/// 只使用网络
|
||||
networkOnly,
|
||||
|
||||
/// 同时请求网络和缓存,返回最快的结果
|
||||
fastest,
|
||||
}
|
||||
|
||||
/// 智能缓存管理器
|
||||
class CacheManager {
|
||||
static CacheManager? _instance;
|
||||
static CacheManager get instance {
|
||||
_instance ??= CacheManager._();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
CacheManager._() {
|
||||
_scheduleInit();
|
||||
}
|
||||
|
||||
late SharedPreferences _prefs;
|
||||
late Directory _cacheDir;
|
||||
final Map<String, CacheEntry> _memoryCache = {};
|
||||
final Map<String, Timer> _autoCleanTimers = {};
|
||||
|
||||
static const String _cachePrefix = 'app_upgrade_cache_';
|
||||
static const String _cacheMetaKey = 'cache_metadata';
|
||||
static const int _maxMemoryCacheSize = 50; // 最大内存缓存条目数
|
||||
static const int _maxDiskCacheSize = 100 * 1024 * 1024; // 100MB
|
||||
|
||||
/// 延迟初始化
|
||||
void _scheduleInit() {
|
||||
// 如果绑定已经初始化,直接执行
|
||||
_init().catchError((e) {
|
||||
debugPrint('Failed to initialize CacheManager: $e');
|
||||
});
|
||||
}
|
||||
|
||||
/// 初始化
|
||||
Future<void> _init() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
_cacheDir = await _getCacheDirectory();
|
||||
_startAutoClean();
|
||||
await _cleanExpiredCache();
|
||||
}
|
||||
|
||||
/// 获取缓存目录
|
||||
Future<Directory> _getCacheDirectory() async {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final cacheDir = Directory('${tempDir.path}/app_upgrade_cache');
|
||||
if (!await cacheDir.exists()) {
|
||||
await cacheDir.create(recursive: true);
|
||||
}
|
||||
return cacheDir;
|
||||
}
|
||||
|
||||
/// 保存到缓存
|
||||
Future<void> save<T>({
|
||||
required String key,
|
||||
required T data,
|
||||
Duration? expiration,
|
||||
CacheStrategy strategy = CacheStrategy.cacheFirst,
|
||||
Map<String, dynamic>? metadata,
|
||||
}) async {
|
||||
final fullKey = '$_cachePrefix$key';
|
||||
final expiresAt = expiration != null ? DateTime.now().add(expiration) : null;
|
||||
|
||||
final entry = CacheEntry<T>(
|
||||
key: fullKey,
|
||||
data: data,
|
||||
createdAt: DateTime.now(),
|
||||
expiresAt: expiresAt,
|
||||
metadata: metadata,
|
||||
);
|
||||
|
||||
// 保存到内存缓存
|
||||
_saveToMemory(fullKey, entry);
|
||||
|
||||
// 保存到磁盘缓存
|
||||
await _saveToDisk(fullKey, entry);
|
||||
|
||||
// 更新缓存元数据
|
||||
await _updateCacheMetadata(fullKey, entry);
|
||||
}
|
||||
|
||||
/// 从缓存获取
|
||||
Future<T?> get<T>(
|
||||
String key, {
|
||||
CacheStrategy strategy = CacheStrategy.cacheFirst,
|
||||
Future<T?> Function()? networkFetcher,
|
||||
}) async {
|
||||
final fullKey = '$_cachePrefix$key';
|
||||
|
||||
switch (strategy) {
|
||||
case CacheStrategy.cacheFirst:
|
||||
return await _getCacheFirst<T>(fullKey, networkFetcher);
|
||||
case CacheStrategy.networkFirst:
|
||||
return await _getNetworkFirst<T>(fullKey, networkFetcher);
|
||||
case CacheStrategy.cacheOnly:
|
||||
return await _getCacheOnly<T>(fullKey);
|
||||
case CacheStrategy.networkOnly:
|
||||
return await _getNetworkOnly<T>(networkFetcher);
|
||||
case CacheStrategy.fastest:
|
||||
return await _getFastest<T>(fullKey, networkFetcher);
|
||||
}
|
||||
}
|
||||
|
||||
/// 缓存优先策略
|
||||
Future<T?> _getCacheFirst<T>(String key, Future<T?> Function()? fetcher) async {
|
||||
// 先从内存缓存获取
|
||||
final memoryEntry = _memoryCache[key];
|
||||
if (memoryEntry != null && !memoryEntry.isExpired) {
|
||||
return memoryEntry.data as T?;
|
||||
}
|
||||
|
||||
// 从磁盘缓存获取
|
||||
final diskEntry = await _getFromDisk<T>(key);
|
||||
if (diskEntry != null && !diskEntry.isExpired) {
|
||||
_saveToMemory(key, diskEntry);
|
||||
return diskEntry.data;
|
||||
}
|
||||
|
||||
// 缓存无效,从网络获取
|
||||
if (fetcher != null) {
|
||||
try {
|
||||
final data = await fetcher();
|
||||
if (data != null) {
|
||||
await save(key: key.replaceFirst(_cachePrefix, ''), data: data);
|
||||
}
|
||||
return data;
|
||||
} catch (e) {
|
||||
// 网络失败,返回过期的缓存
|
||||
if (diskEntry != null) {
|
||||
return diskEntry.data;
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 网络优先策略
|
||||
Future<T?> _getNetworkFirst<T>(String key, Future<T?> Function()? fetcher) async {
|
||||
if (fetcher != null) {
|
||||
try {
|
||||
final data = await fetcher();
|
||||
if (data != null) {
|
||||
await save(key: key.replaceFirst(_cachePrefix, ''), data: data);
|
||||
}
|
||||
return data;
|
||||
} catch (e) {
|
||||
// 网络失败,使用缓存
|
||||
final entry = await _getFromDisk<T>(key) ?? _memoryCache[key] as CacheEntry<T>?;
|
||||
if (entry != null) {
|
||||
return entry.data;
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 仅缓存策略
|
||||
Future<T?> _getCacheOnly<T>(String key) async {
|
||||
final memoryEntry = _memoryCache[key];
|
||||
if (memoryEntry != null) {
|
||||
return memoryEntry.data as T?;
|
||||
}
|
||||
|
||||
final diskEntry = await _getFromDisk<T>(key);
|
||||
if (diskEntry != null) {
|
||||
_saveToMemory(key, diskEntry);
|
||||
return diskEntry.data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 仅网络策略
|
||||
Future<T?> _getNetworkOnly<T>(Future<T?> Function()? fetcher) async {
|
||||
if (fetcher != null) {
|
||||
return await fetcher();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 最快响应策略
|
||||
Future<T?> _getFastest<T>(String key, Future<T?> Function()? fetcher) async {
|
||||
final futures = <Future<T?>>[];
|
||||
|
||||
// 添加缓存获取
|
||||
futures.add(_getCacheOnly<T>(key));
|
||||
|
||||
// 添加网络获取
|
||||
if (fetcher != null) {
|
||||
futures.add(fetcher());
|
||||
}
|
||||
|
||||
// 返回最快的结果
|
||||
try {
|
||||
final result = await Future.any(futures);
|
||||
|
||||
// 如果是网络结果,保存到缓存
|
||||
if (result != null && fetcher != null) {
|
||||
final cacheResult = await _getCacheOnly<T>(key);
|
||||
if (cacheResult == null || cacheResult != result) {
|
||||
await save(key: key.replaceFirst(_cachePrefix, ''), data: result);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存到内存缓存
|
||||
void _saveToMemory<T>(String key, CacheEntry<T> entry) {
|
||||
_memoryCache[key] = entry;
|
||||
|
||||
// 限制内存缓存大小
|
||||
if (_memoryCache.length > _maxMemoryCacheSize) {
|
||||
// 移除最旧的条目
|
||||
final oldestKey =
|
||||
_memoryCache.entries.reduce((a, b) => a.value.createdAt.isBefore(b.value.createdAt) ? a : b).key;
|
||||
_memoryCache.remove(oldestKey);
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存到磁盘缓存
|
||||
Future<void> _saveToDisk<T>(String key, CacheEntry<T> entry) async {
|
||||
try {
|
||||
final file = File('${_cacheDir.path}/${_encodeKey(key)}.json');
|
||||
final json = jsonEncode(entry.toJson());
|
||||
await file.writeAsString(json);
|
||||
|
||||
// 检查磁盘缓存大小
|
||||
await _checkDiskCacheSize();
|
||||
} catch (e) {
|
||||
debugPrint('Failed to save to disk cache: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 从磁盘缓存获取
|
||||
Future<CacheEntry<T>?> _getFromDisk<T>(String key) async {
|
||||
try {
|
||||
final file = File('${_cacheDir.path}/${_encodeKey(key)}.json');
|
||||
if (!await file.exists()) return null;
|
||||
|
||||
final json = await file.readAsString();
|
||||
final data = jsonDecode(json);
|
||||
return CacheEntry<T>.fromJson(data);
|
||||
} catch (e) {
|
||||
debugPrint('Failed to get from disk cache: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新缓存元数据
|
||||
Future<void> _updateCacheMetadata(String key, CacheEntry entry) async {
|
||||
final metadata = _prefs.getString(_cacheMetaKey);
|
||||
final metaMap = metadata != null ? jsonDecode(metadata) : {};
|
||||
|
||||
metaMap[key] = {
|
||||
'createdAt': entry.createdAt.toIso8601String(),
|
||||
'expiresAt': entry.expiresAt?.toIso8601String(),
|
||||
'size': entry.toJson().toString().length,
|
||||
};
|
||||
|
||||
await _prefs.setString(_cacheMetaKey, jsonEncode(metaMap));
|
||||
}
|
||||
|
||||
/// 删除缓存
|
||||
Future<void> delete(String key) async {
|
||||
final fullKey = '$_cachePrefix$key';
|
||||
|
||||
// 从内存缓存删除
|
||||
_memoryCache.remove(fullKey);
|
||||
|
||||
// 从磁盘缓存删除
|
||||
final file = File('${_cacheDir.path}/${_encodeKey(fullKey)}.json');
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
|
||||
// 更新元数据
|
||||
final metadata = _prefs.getString(_cacheMetaKey);
|
||||
if (metadata != null) {
|
||||
final metaMap = jsonDecode(metadata);
|
||||
metaMap.remove(fullKey);
|
||||
await _prefs.setString(_cacheMetaKey, jsonEncode(metaMap));
|
||||
}
|
||||
}
|
||||
|
||||
/// 清空所有缓存
|
||||
Future<void> clear() async {
|
||||
// 清空内存缓存
|
||||
_memoryCache.clear();
|
||||
|
||||
// 清空磁盘缓存
|
||||
if (await _cacheDir.exists()) {
|
||||
await _cacheDir.delete(recursive: true);
|
||||
await _cacheDir.create();
|
||||
}
|
||||
|
||||
// 清空元数据
|
||||
await _prefs.remove(_cacheMetaKey);
|
||||
}
|
||||
|
||||
/// 清理过期缓存
|
||||
Future<void> _cleanExpiredCache() async {
|
||||
// 清理内存缓存
|
||||
_memoryCache.removeWhere((key, entry) => entry.isExpired);
|
||||
|
||||
// 清理磁盘缓存
|
||||
final metadata = _prefs.getString(_cacheMetaKey);
|
||||
if (metadata != null) {
|
||||
final metaMap = jsonDecode(metadata) as Map<String, dynamic>;
|
||||
final keysToRemove = <String>[];
|
||||
|
||||
for (final entry in metaMap.entries) {
|
||||
final expiresAt = entry.value['expiresAt'];
|
||||
if (expiresAt != null) {
|
||||
final expireTime = DateTime.parse(expiresAt);
|
||||
if (DateTime.now().isAfter(expireTime)) {
|
||||
keysToRemove.add(entry.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final key in keysToRemove) {
|
||||
await delete(key.replaceFirst(_cachePrefix, ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查磁盘缓存大小
|
||||
Future<void> _checkDiskCacheSize() async {
|
||||
int totalSize = 0;
|
||||
final files = await _cacheDir.list().toList();
|
||||
|
||||
for (final file in files) {
|
||||
if (file is File) {
|
||||
totalSize += await file.length();
|
||||
}
|
||||
}
|
||||
|
||||
if (totalSize > _maxDiskCacheSize) {
|
||||
// 删除最旧的文件
|
||||
final fileStats = <MapEntry<File, FileStat>>[];
|
||||
for (final file in files) {
|
||||
if (file is File) {
|
||||
fileStats.add(MapEntry(file, await file.stat()));
|
||||
}
|
||||
}
|
||||
|
||||
fileStats.sort((a, b) => a.value.accessed.compareTo(b.value.accessed));
|
||||
|
||||
// 删除文件直到大小合适
|
||||
for (final entry in fileStats) {
|
||||
final fileSize = await entry.key.length();
|
||||
await entry.key.delete();
|
||||
totalSize -= fileSize;
|
||||
if (totalSize <= (_maxDiskCacheSize * 0.8).toInt()) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 编码缓存键
|
||||
String _encodeKey(String key) {
|
||||
return base64Encode(utf8.encode(key)).replaceAll('/', '_');
|
||||
}
|
||||
|
||||
/// 启动自动清理
|
||||
void _startAutoClean() {
|
||||
// 每小时清理一次过期缓存
|
||||
_autoCleanTimers['expired'] = Timer.periodic(const Duration(hours: 1), (_) => _cleanExpiredCache());
|
||||
}
|
||||
|
||||
/// 获取缓存统计信息
|
||||
Future<Map<String, dynamic>> getCacheStats() async {
|
||||
int memoryCount = _memoryCache.length;
|
||||
int memorySize = 0;
|
||||
|
||||
for (final entry in _memoryCache.values) {
|
||||
memorySize += entry.toJson().toString().length;
|
||||
}
|
||||
|
||||
int diskCount = 0;
|
||||
int diskSize = 0;
|
||||
|
||||
final files = await _cacheDir.list().toList();
|
||||
for (final file in files) {
|
||||
if (file is File) {
|
||||
diskCount++;
|
||||
diskSize += await file.length();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'memoryCache': {'count': memoryCount, 'size': memorySize, 'sizeFormatted': _formatBytes(memorySize)},
|
||||
'diskCache': {'count': diskCount, 'size': diskSize, 'sizeFormatted': _formatBytes(diskSize)},
|
||||
'total': {
|
||||
'count': memoryCount + diskCount,
|
||||
'size': memorySize + diskSize,
|
||||
'sizeFormatted': _formatBytes(memorySize + diskSize),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// 格式化字节数
|
||||
String _formatBytes(int bytes) {
|
||||
if (bytes < 1024) return '$bytes B';
|
||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(2)} KB';
|
||||
if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB';
|
||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';
|
||||
}
|
||||
|
||||
/// 预加载缓存
|
||||
Future<void> preload(List<String> keys) async {
|
||||
for (final key in keys) {
|
||||
final fullKey = '$_cachePrefix$key';
|
||||
if (!_memoryCache.containsKey(fullKey)) {
|
||||
final entry = await _getFromDisk(fullKey);
|
||||
if (entry != null && !entry.isExpired) {
|
||||
_saveToMemory(fullKey, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 释放资源
|
||||
void dispose() {
|
||||
for (final timer in _autoCleanTimers.values) {
|
||||
timer.cancel();
|
||||
}
|
||||
_autoCleanTimers.clear();
|
||||
_memoryCache.clear();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,474 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import 'upgrade_config.dart';
|
||||
|
||||
/// 下载任务状态
|
||||
enum DownloadStatus { pending, downloading, paused, completed, failed, cancelled }
|
||||
|
||||
/// 下载任务信息
|
||||
class DownloadTask {
|
||||
final String id;
|
||||
final String url;
|
||||
final String savePath;
|
||||
final Map<String, String>? headers;
|
||||
final String? md5;
|
||||
final String? sha256;
|
||||
int? totalSize;
|
||||
|
||||
DownloadStatus status;
|
||||
int downloadedSize;
|
||||
double progress;
|
||||
String? errorMessage;
|
||||
int retryCount;
|
||||
DateTime? startTime;
|
||||
DateTime? endTime;
|
||||
|
||||
DownloadTask({
|
||||
required this.id,
|
||||
required this.url,
|
||||
required this.savePath,
|
||||
this.headers,
|
||||
this.md5,
|
||||
this.sha256,
|
||||
this.totalSize,
|
||||
this.status = DownloadStatus.pending,
|
||||
this.downloadedSize = 0,
|
||||
this.progress = 0.0,
|
||||
this.errorMessage,
|
||||
this.retryCount = 0,
|
||||
this.startTime,
|
||||
this.endTime,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'url': url,
|
||||
'savePath': savePath,
|
||||
'headers': headers,
|
||||
'md5': md5,
|
||||
'sha256': sha256,
|
||||
'totalSize': totalSize,
|
||||
'status': status.toString(),
|
||||
'downloadedSize': downloadedSize,
|
||||
'progress': progress,
|
||||
'errorMessage': errorMessage,
|
||||
'retryCount': retryCount,
|
||||
'startTime': startTime?.toIso8601String(),
|
||||
'endTime': endTime?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 高性能下载管理器
|
||||
class DownloadManager {
|
||||
static DownloadManager? _instance;
|
||||
static DownloadManager get instance {
|
||||
_instance ??= DownloadManager._();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
DownloadManager._() {
|
||||
_initializeDio();
|
||||
}
|
||||
|
||||
final Dio _dio = Dio();
|
||||
final Map<String, DownloadTask> _tasks = {};
|
||||
final Map<String, CancelToken> _cancelTokens = {};
|
||||
final Map<String, StreamController<DownloadTask>> _progressControllers = {};
|
||||
final UpgradeConfig _config = UpgradeConfig.instance;
|
||||
|
||||
Timer? _speedCalculatorTimer;
|
||||
final Map<String, List<int>> _speedSamples = {};
|
||||
|
||||
/// 初始化Dio配置
|
||||
void _initializeDio() {
|
||||
_dio.options.connectTimeout = Duration(seconds: _config.connectTimeout);
|
||||
_dio.options.receiveTimeout = Duration(seconds: _config.downloadTimeout);
|
||||
|
||||
if (_config.proxyUrl != null) {
|
||||
(_dio.httpClientAdapter as dynamic).onHttpClientCreate = (client) {
|
||||
client.findProxy = (uri) {
|
||||
return 'PROXY ${_config.proxyUrl}';
|
||||
};
|
||||
return client;
|
||||
};
|
||||
}
|
||||
|
||||
// 添加拦截器
|
||||
_dio.interceptors.add(LogInterceptor(request: _config.debugMode, responseBody: false, error: _config.debugMode));
|
||||
|
||||
_dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
// 添加自定义请求头
|
||||
options.headers.addAll(_config.customHeaders);
|
||||
handler.next(options);
|
||||
},
|
||||
onError: (error, handler) {
|
||||
if (_config.debugMode) {
|
||||
debugPrint('Download error: ${error.message}');
|
||||
}
|
||||
handler.next(error);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 创建下载任务
|
||||
Future<String> createTask({
|
||||
required String url,
|
||||
String? savePath,
|
||||
Map<String, String>? headers,
|
||||
String? md5,
|
||||
String? sha256,
|
||||
int? expectedSize,
|
||||
String? versionName, // 新增版本名参数
|
||||
}) async {
|
||||
final taskId = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
|
||||
if (savePath == null) {
|
||||
final dir = await _getDownloadDirectory();
|
||||
// 如果提供了版本名,则使用标准化的文件名
|
||||
final fileName = versionName != null ? 'app-upgrade-$versionName.apk' : url.split('/').last.split('?').first;
|
||||
savePath = '${dir.path}/$fileName';
|
||||
}
|
||||
|
||||
final task = DownloadTask(
|
||||
id: taskId,
|
||||
url: url,
|
||||
savePath: savePath,
|
||||
headers: headers,
|
||||
md5: md5,
|
||||
sha256: sha256,
|
||||
totalSize: expectedSize,
|
||||
);
|
||||
|
||||
_tasks[taskId] = task;
|
||||
_progressControllers[taskId] = StreamController<DownloadTask>.broadcast();
|
||||
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/// 开始下载
|
||||
Future<bool> startDownload(String taskId) async {
|
||||
final task = _tasks[taskId];
|
||||
if (task == null) return false;
|
||||
|
||||
task.status = DownloadStatus.downloading;
|
||||
task.startTime = DateTime.now();
|
||||
|
||||
final cancelToken = CancelToken();
|
||||
_cancelTokens[taskId] = cancelToken;
|
||||
|
||||
// 启动速度计算定时器
|
||||
_startSpeedCalculator(taskId);
|
||||
|
||||
try {
|
||||
// 检查是否支持断点续传
|
||||
if (_config.supportBreakpoint && await _checkBreakpointSupport(task.url)) {
|
||||
await _downloadWithBreakpoint(task, cancelToken);
|
||||
} else {
|
||||
await _downloadNormal(task, cancelToken);
|
||||
}
|
||||
|
||||
// 校验文件完整性
|
||||
if (_config.verifyIntegrity && (task.md5 != null || task.sha256 != null)) {
|
||||
final isValid = await _verifyFileIntegrity(task);
|
||||
if (!isValid) {
|
||||
throw Exception('File integrity check failed');
|
||||
}
|
||||
}
|
||||
|
||||
task.status = DownloadStatus.completed;
|
||||
task.endTime = DateTime.now();
|
||||
task.progress = 1.0;
|
||||
_notifyProgress(task);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) {
|
||||
task.status = DownloadStatus.cancelled;
|
||||
} else {
|
||||
task.status = DownloadStatus.failed;
|
||||
task.errorMessage = e.toString();
|
||||
|
||||
// 自动重试
|
||||
if (task.retryCount < _config.maxRetryCount) {
|
||||
task.retryCount++;
|
||||
await Future.delayed(Duration(seconds: _config.retryDelay));
|
||||
return startDownload(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
_notifyProgress(task);
|
||||
return false;
|
||||
} finally {
|
||||
_stopSpeedCalculator(taskId);
|
||||
_cancelTokens.remove(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
/// 普通下载
|
||||
Future<void> _downloadNormal(DownloadTask task, CancelToken cancelToken) async {
|
||||
final response = await _dio.download(
|
||||
task.url,
|
||||
task.savePath,
|
||||
cancelToken: cancelToken,
|
||||
options: Options(headers: task.headers),
|
||||
onReceiveProgress: (received, total) {
|
||||
task.downloadedSize = received;
|
||||
task.totalSize ??= total;
|
||||
task.progress = total > 0 ? received / total : 0.0;
|
||||
_notifyProgress(task);
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 206) {
|
||||
throw Exception('Download failed with status: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
/// 断点续传下载
|
||||
Future<void> _downloadWithBreakpoint(DownloadTask task, CancelToken cancelToken) async {
|
||||
final file = File(task.savePath);
|
||||
int downloadedBytes = 0;
|
||||
|
||||
// 检查文件是否已存在
|
||||
if (await file.exists()) {
|
||||
downloadedBytes = await file.length();
|
||||
task.downloadedSize = downloadedBytes;
|
||||
}
|
||||
|
||||
// 如果文件已下载完成
|
||||
if (task.totalSize != null && downloadedBytes >= task.totalSize!) {
|
||||
task.progress = 1.0;
|
||||
_notifyProgress(task);
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置Range头
|
||||
final headers = Map<String, dynamic>.from(task.headers ?? {});
|
||||
headers['Range'] = 'bytes=$downloadedBytes-';
|
||||
|
||||
// 创建文件流
|
||||
final raf = await file.open(mode: FileMode.append);
|
||||
|
||||
try {
|
||||
final response = await _dio.get<ResponseBody>(
|
||||
task.url,
|
||||
cancelToken: cancelToken,
|
||||
options: Options(headers: headers, responseType: ResponseType.stream),
|
||||
);
|
||||
|
||||
final total = int.tryParse(response.headers.value('content-length') ?? '0') ?? 0;
|
||||
|
||||
task.totalSize ??= total + downloadedBytes;
|
||||
|
||||
// 写入文件
|
||||
await for (final chunk in response.data!.stream) {
|
||||
await raf.writeFrom(chunk);
|
||||
task.downloadedSize += chunk.length;
|
||||
task.progress = task.totalSize! > 0 ? task.downloadedSize / task.totalSize! : 0.0;
|
||||
_notifyProgress(task);
|
||||
}
|
||||
} finally {
|
||||
await raf.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查是否支持断点续传
|
||||
Future<bool> _checkBreakpointSupport(String url) async {
|
||||
try {
|
||||
final response = await _dio.head(url);
|
||||
final acceptRanges = response.headers.value('accept-ranges');
|
||||
return acceptRanges == 'bytes';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 暂停下载
|
||||
void pauseDownload(String taskId) {
|
||||
final cancelToken = _cancelTokens[taskId];
|
||||
if (cancelToken != null && !cancelToken.isCancelled) {
|
||||
cancelToken.cancel('User paused');
|
||||
final task = _tasks[taskId];
|
||||
if (task != null) {
|
||||
task.status = DownloadStatus.paused;
|
||||
_notifyProgress(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 恢复下载
|
||||
Future<bool> resumeDownload(String taskId) async {
|
||||
final task = _tasks[taskId];
|
||||
if (task != null && task.status == DownloadStatus.paused) {
|
||||
return startDownload(taskId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 取消下载
|
||||
void cancelDownload(String taskId) {
|
||||
final cancelToken = _cancelTokens[taskId];
|
||||
if (cancelToken != null && !cancelToken.isCancelled) {
|
||||
cancelToken.cancel('User cancelled');
|
||||
}
|
||||
|
||||
final task = _tasks[taskId];
|
||||
if (task != null) {
|
||||
task.status = DownloadStatus.cancelled;
|
||||
_notifyProgress(task);
|
||||
|
||||
// 删除未完成的文件
|
||||
final file = File(task.savePath);
|
||||
if (file.existsSync()) {
|
||||
file.deleteSync();
|
||||
}
|
||||
}
|
||||
|
||||
_cleanupTask(taskId);
|
||||
}
|
||||
|
||||
/// 重试下载
|
||||
Future<bool> retryDownload(String taskId) async {
|
||||
final task = _tasks[taskId];
|
||||
if (task != null && task.status == DownloadStatus.failed) {
|
||||
task.retryCount = 0;
|
||||
task.errorMessage = null;
|
||||
return startDownload(taskId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 获取任务信息
|
||||
DownloadTask? getTask(String taskId) => _tasks[taskId];
|
||||
|
||||
/// 获取所有任务
|
||||
List<DownloadTask> getAllTasks() => _tasks.values.toList();
|
||||
|
||||
/// 获取任务进度流
|
||||
Stream<DownloadTask>? getProgressStream(String taskId) {
|
||||
return _progressControllers[taskId]?.stream;
|
||||
}
|
||||
|
||||
/// 清理任务
|
||||
void _cleanupTask(String taskId) {
|
||||
_tasks.remove(taskId);
|
||||
_cancelTokens.remove(taskId);
|
||||
_progressControllers[taskId]?.close();
|
||||
_progressControllers.remove(taskId);
|
||||
_speedSamples.remove(taskId);
|
||||
}
|
||||
|
||||
/// 通知进度更新
|
||||
void _notifyProgress(DownloadTask task) {
|
||||
_progressControllers[task.id]?.add(task);
|
||||
}
|
||||
|
||||
/// 启动速度计算器
|
||||
void _startSpeedCalculator(String taskId) {
|
||||
_speedSamples[taskId] = [];
|
||||
_speedCalculatorTimer?.cancel();
|
||||
_speedCalculatorTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
final task = _tasks[taskId];
|
||||
if (task != null && task.status == DownloadStatus.downloading) {
|
||||
final samples = _speedSamples[taskId]!;
|
||||
samples.add(task.downloadedSize);
|
||||
|
||||
// 保留最近10个采样点
|
||||
if (samples.length > 10) {
|
||||
samples.removeAt(0);
|
||||
}
|
||||
|
||||
// 计算平均速度
|
||||
if (samples.length >= 2) {
|
||||
final speed = samples.last - samples.first;
|
||||
final timeSpan = samples.length - 1;
|
||||
final avgSpeed = speed / timeSpan;
|
||||
|
||||
// 可以在这里发送速度信息
|
||||
if (_config.debugMode) {
|
||||
debugPrint('Download speed: ${_formatBytes(avgSpeed.toInt())}/s');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 停止速度计算器
|
||||
void _stopSpeedCalculator(String taskId) {
|
||||
_speedSamples.remove(taskId);
|
||||
if (_speedSamples.isEmpty) {
|
||||
_speedCalculatorTimer?.cancel();
|
||||
_speedCalculatorTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证文件完整性
|
||||
Future<bool> _verifyFileIntegrity(DownloadTask task) async {
|
||||
return compute(verifyFileInIsolate, {'filePath': task.savePath, 'md5': task.md5, 'sha256': task.sha256});
|
||||
}
|
||||
|
||||
/// 在Isolate中验证文件
|
||||
static Future<bool> verifyFileInIsolate(Map<String, dynamic> params) async {
|
||||
final filePath = params['filePath'] as String;
|
||||
final expectedMd5 = params['md5'] as String?;
|
||||
final expectedSha256 = params['sha256'] as String?;
|
||||
|
||||
final file = File(filePath);
|
||||
if (!await file.exists()) return false;
|
||||
|
||||
final bytes = await file.readAsBytes();
|
||||
|
||||
if (expectedMd5 != null) {
|
||||
final md5Hash = md5.convert(bytes).toString();
|
||||
if (md5Hash != expectedMd5) return false;
|
||||
}
|
||||
|
||||
if (expectedSha256 != null) {
|
||||
final sha256Hash = sha256.convert(bytes).toString();
|
||||
if (sha256Hash != expectedSha256) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// 获取下载目录
|
||||
Future<Directory> _getDownloadDirectory() async {
|
||||
if (Platform.isAndroid) {
|
||||
return (await getExternalStorageDirectory())!;
|
||||
} else {
|
||||
return getApplicationDocumentsDirectory();
|
||||
}
|
||||
}
|
||||
|
||||
/// 格式化字节数
|
||||
String _formatBytes(int bytes) {
|
||||
if (bytes < 1024) return '$bytes B';
|
||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(2)} KB';
|
||||
if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB';
|
||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';
|
||||
}
|
||||
|
||||
/// 清理所有任务
|
||||
void clearAllTasks() {
|
||||
for (final taskId in _tasks.keys.toList()) {
|
||||
cancelDownload(taskId);
|
||||
}
|
||||
_tasks.clear();
|
||||
}
|
||||
|
||||
/// 释放资源
|
||||
void dispose() {
|
||||
clearAllTasks();
|
||||
_speedCalculatorTimer?.cancel();
|
||||
_dio.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,516 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart'; // Added for WidgetsBinding
|
||||
|
||||
/// 网络类型
|
||||
enum NetworkType { none, mobile, wifi, ethernet, bluetooth, vpn, other }
|
||||
|
||||
/// 网络质量
|
||||
enum NetworkQuality {
|
||||
unknown,
|
||||
poor, // 差 (<100KB/s)
|
||||
moderate, // 中等 (100KB/s - 500KB/s)
|
||||
good, // 良好 (500KB/s - 2MB/s)
|
||||
excellent, // 优秀 (>2MB/s)
|
||||
}
|
||||
|
||||
/// 网络状态信息
|
||||
class NetworkStatus {
|
||||
final NetworkType type;
|
||||
final NetworkQuality quality;
|
||||
final bool isConnected;
|
||||
final bool isMetered;
|
||||
final double? downloadSpeed;
|
||||
final double? uploadSpeed;
|
||||
final int? ping;
|
||||
final DateTime timestamp;
|
||||
|
||||
NetworkStatus({
|
||||
required this.type,
|
||||
required this.quality,
|
||||
required this.isConnected,
|
||||
required this.isMetered,
|
||||
this.downloadSpeed,
|
||||
this.uploadSpeed,
|
||||
this.ping,
|
||||
DateTime? timestamp,
|
||||
}) : timestamp = timestamp ?? DateTime.now();
|
||||
|
||||
/// 是否适合下载大文件
|
||||
bool get isSuitableForLargeDownload {
|
||||
return isConnected && (type == NetworkType.wifi || type == NetworkType.ethernet) && quality != NetworkQuality.poor;
|
||||
}
|
||||
|
||||
/// 是否应该限速
|
||||
bool get shouldLimitSpeed {
|
||||
return type == NetworkType.mobile || quality == NetworkQuality.poor;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'type': type.toString(),
|
||||
'quality': quality.toString(),
|
||||
'isConnected': isConnected,
|
||||
'isMetered': isMetered,
|
||||
'downloadSpeed': downloadSpeed,
|
||||
'uploadSpeed': uploadSpeed,
|
||||
'ping': ping,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 网络监测器
|
||||
class NetworkMonitor {
|
||||
static NetworkMonitor? _instance;
|
||||
static NetworkMonitor get instance {
|
||||
_instance ??= NetworkMonitor._();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
NetworkMonitor._() {
|
||||
_init().catchError((e) {
|
||||
debugPrint('NetworkMonitor initialization failed: $e');
|
||||
});
|
||||
}
|
||||
|
||||
final Connectivity _connectivity = Connectivity();
|
||||
NetworkStatus? _currentStatus;
|
||||
|
||||
final _statusController = StreamController<NetworkStatus>.broadcast();
|
||||
Stream<NetworkStatus> get statusStream => _statusController.stream;
|
||||
|
||||
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
|
||||
Timer? _qualityCheckTimer;
|
||||
|
||||
final List<double> _speedSamples = [];
|
||||
static const int _maxSamples = 10;
|
||||
|
||||
/// 初始化
|
||||
Future<void> _init() async {
|
||||
debugPrint('NetworkMonitor: Starting initialization');
|
||||
|
||||
// 立即开始网络监听,不等待Flutter绑定
|
||||
_startMonitoring();
|
||||
|
||||
// 检查初始网络状态
|
||||
await _checkInitialStatus();
|
||||
|
||||
// 启动质量检查
|
||||
_startQualityCheck();
|
||||
|
||||
debugPrint('NetworkMonitor: Initialization completed');
|
||||
}
|
||||
|
||||
/// 检查初始状态
|
||||
Future<void> _checkInitialStatus() async {
|
||||
try {
|
||||
debugPrint('NetworkMonitor: Checking initial connectivity...');
|
||||
final result = await _connectivity.checkConnectivity();
|
||||
debugPrint('NetworkMonitor: Initial connectivity result: $result');
|
||||
|
||||
// 强制触发网络状态更新
|
||||
await _updateStatus(result);
|
||||
} catch (e) {
|
||||
debugPrint('NetworkMonitor: Failed to check initial network status: $e');
|
||||
// 如果检查失败,设置默认状态
|
||||
await _updateStatus(ConnectivityResult.none);
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始监听网络变化
|
||||
void _startMonitoring() {
|
||||
debugPrint('NetworkMonitor: Starting connectivity monitoring...');
|
||||
_connectivitySubscription?.cancel(); // 确保之前的监听器被取消
|
||||
|
||||
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(
|
||||
(List<ConnectivityResult> results) async {
|
||||
debugPrint('NetworkMonitor: Connectivity changed: $results');
|
||||
await _updateStatus(results);
|
||||
},
|
||||
onError: (error) {
|
||||
debugPrint('NetworkMonitor: Connectivity monitoring error: $error');
|
||||
},
|
||||
cancelOnError: false,
|
||||
);
|
||||
}
|
||||
|
||||
/// 更新网络状态
|
||||
Future<void> _updateStatus(dynamic results) async {
|
||||
List<ConnectivityResult> connectivityResults;
|
||||
|
||||
// 处理不同类型的输入
|
||||
if (results is ConnectivityResult) {
|
||||
connectivityResults = [results];
|
||||
} else if (results is List<ConnectivityResult>) {
|
||||
connectivityResults = results;
|
||||
} else {
|
||||
debugPrint('NetworkMonitor: Invalid results type: ${results.runtimeType}');
|
||||
return;
|
||||
}
|
||||
debugPrint('NetworkMonitor: Updating status with results: $results');
|
||||
|
||||
NetworkType type = NetworkType.none;
|
||||
bool isConnected = false;
|
||||
bool isMetered = false;
|
||||
|
||||
// 检查是否有有效的网络连接
|
||||
if (results.isNotEmpty && !results.contains(ConnectivityResult.none)) {
|
||||
isConnected = true;
|
||||
|
||||
// 确定网络类型(优先级:WiFi > Ethernet > Mobile > VPN > Bluetooth > Other)
|
||||
if (results.contains(ConnectivityResult.wifi)) {
|
||||
type = NetworkType.wifi;
|
||||
isMetered = false;
|
||||
debugPrint('NetworkMonitor: Detected WiFi connection');
|
||||
} else if (results.contains(ConnectivityResult.ethernet)) {
|
||||
type = NetworkType.ethernet;
|
||||
isMetered = false;
|
||||
debugPrint('NetworkMonitor: Detected Ethernet connection');
|
||||
} else if (results.contains(ConnectivityResult.mobile)) {
|
||||
type = NetworkType.mobile;
|
||||
isMetered = true;
|
||||
debugPrint('NetworkMonitor: Detected Mobile connection');
|
||||
} else if (results.contains(ConnectivityResult.vpn)) {
|
||||
type = NetworkType.vpn;
|
||||
isMetered = false;
|
||||
debugPrint('NetworkMonitor: Detected VPN connection');
|
||||
} else if (results.contains(ConnectivityResult.bluetooth)) {
|
||||
type = NetworkType.bluetooth;
|
||||
isMetered = true;
|
||||
debugPrint('NetworkMonitor: Detected Bluetooth connection');
|
||||
} else {
|
||||
type = NetworkType.other;
|
||||
isMetered = true;
|
||||
debugPrint('NetworkMonitor: Detected Other connection type');
|
||||
}
|
||||
} else {
|
||||
debugPrint('NetworkMonitor: No network connection detected');
|
||||
type = NetworkType.none;
|
||||
isConnected = false;
|
||||
}
|
||||
|
||||
// 更新当前状态
|
||||
final previousStatus = _currentStatus;
|
||||
_currentStatus = NetworkStatus(
|
||||
type: type,
|
||||
quality: _currentStatus?.quality ?? NetworkQuality.unknown,
|
||||
isConnected: isConnected,
|
||||
isMetered: isMetered,
|
||||
downloadSpeed: _currentStatus?.downloadSpeed,
|
||||
uploadSpeed: _currentStatus?.uploadSpeed,
|
||||
ping: _currentStatus?.ping,
|
||||
);
|
||||
|
||||
debugPrint('NetworkMonitor: Status updated - Type: $type, Connected: $isConnected, Metered: $isMetered');
|
||||
|
||||
// 只有在状态真正改变时才发送通知
|
||||
if (previousStatus == null ||
|
||||
previousStatus.type != type ||
|
||||
previousStatus.isConnected != isConnected ||
|
||||
previousStatus.isMetered != isMetered) {
|
||||
_statusController.add(_currentStatus!);
|
||||
debugPrint('NetworkMonitor: Status change detected, notifying listeners');
|
||||
}
|
||||
|
||||
// 如果已连接,开始网络质量测试
|
||||
if (isConnected && (type == NetworkType.wifi || type == NetworkType.ethernet)) {
|
||||
_testNetworkQuality();
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始网络质量检查
|
||||
void _startQualityCheck() {
|
||||
_qualityCheckTimer?.cancel();
|
||||
_qualityCheckTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||
if (_currentStatus?.isConnected == true) {
|
||||
_testNetworkQuality();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 测试网络质量
|
||||
Future<void> _testNetworkQuality() async {
|
||||
try {
|
||||
// 测试延迟
|
||||
final ping = await _testPing();
|
||||
|
||||
// 测试下载速度
|
||||
final downloadSpeed = await _testDownloadSpeed();
|
||||
|
||||
// 计算网络质量
|
||||
final quality = _calculateQuality(downloadSpeed, ping);
|
||||
|
||||
_currentStatus = NetworkStatus(
|
||||
type: _currentStatus?.type ?? NetworkType.other,
|
||||
quality: quality,
|
||||
isConnected: _currentStatus?.isConnected ?? true,
|
||||
isMetered: _currentStatus?.isMetered ?? false,
|
||||
downloadSpeed: downloadSpeed,
|
||||
uploadSpeed: _currentStatus?.uploadSpeed,
|
||||
ping: ping,
|
||||
);
|
||||
|
||||
_statusController.add(_currentStatus!);
|
||||
} catch (e) {
|
||||
debugPrint('Failed to test network quality: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试延迟
|
||||
Future<int> _testPing() async {
|
||||
try {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final socket = await Socket.connect('8.8.8.8', 53, timeout: const Duration(seconds: 5));
|
||||
socket.destroy();
|
||||
stopwatch.stop();
|
||||
return stopwatch.elapsedMilliseconds;
|
||||
} catch (e) {
|
||||
return 9999; // 超时或失败
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试下载速度
|
||||
Future<double> _testDownloadSpeed() async {
|
||||
try {
|
||||
// 使用小文件测试速度
|
||||
const testUrl = 'https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png';
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
final client = HttpClient();
|
||||
client.connectionTimeout = const Duration(seconds: 5);
|
||||
|
||||
final request = await client.getUrl(Uri.parse(testUrl));
|
||||
final response = await request.close();
|
||||
|
||||
double bytes = 0;
|
||||
await for (final chunk in response) {
|
||||
bytes += chunk.length;
|
||||
}
|
||||
|
||||
stopwatch.stop();
|
||||
final seconds = stopwatch.elapsedMilliseconds / 1000;
|
||||
final speed = bytes / seconds; // bytes per second
|
||||
|
||||
// 添加到采样列表
|
||||
_speedSamples.add(speed);
|
||||
if (_speedSamples.length > _maxSamples) {
|
||||
_speedSamples.removeAt(0);
|
||||
}
|
||||
|
||||
// 返回平均速度
|
||||
final avgSpeed = _speedSamples.reduce((a, b) => a + b) / _speedSamples.length;
|
||||
return avgSpeed;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// 计算网络质量
|
||||
NetworkQuality _calculateQuality(double downloadSpeed, int ping) {
|
||||
// 基于下载速度和延迟综合评估
|
||||
if (downloadSpeed == 0 || ping > 1000) {
|
||||
return NetworkQuality.poor;
|
||||
}
|
||||
|
||||
// 速度权重70%,延迟权重30%
|
||||
double score = 0;
|
||||
|
||||
// 速度评分 (0-70)
|
||||
if (downloadSpeed > 2 * 1024 * 1024) {
|
||||
// > 2MB/s
|
||||
score += 70;
|
||||
} else if (downloadSpeed > 500 * 1024) {
|
||||
// > 500KB/s
|
||||
score += 50;
|
||||
} else if (downloadSpeed > 100 * 1024) {
|
||||
// > 100KB/s
|
||||
score += 30;
|
||||
} else {
|
||||
score += 10;
|
||||
}
|
||||
|
||||
// 延迟评分 (0-30)
|
||||
if (ping < 50) {
|
||||
score += 30;
|
||||
} else if (ping < 100) {
|
||||
score += 20;
|
||||
} else if (ping < 200) {
|
||||
score += 10;
|
||||
} else {
|
||||
score += 5;
|
||||
}
|
||||
|
||||
// 根据总分判断质量
|
||||
if (score >= 80) {
|
||||
return NetworkQuality.excellent;
|
||||
} else if (score >= 60) {
|
||||
return NetworkQuality.good;
|
||||
} else if (score >= 40) {
|
||||
return NetworkQuality.moderate;
|
||||
} else {
|
||||
return NetworkQuality.poor;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取当前网络状态
|
||||
NetworkStatus? get currentStatus => _currentStatus;
|
||||
|
||||
/// 手动刷新网络状态
|
||||
Future<void> refreshNetworkStatus() async {
|
||||
debugPrint('NetworkMonitor: Manual refresh requested');
|
||||
try {
|
||||
final result = await _connectivity.checkConnectivity();
|
||||
debugPrint('NetworkMonitor: Manual refresh result: $result');
|
||||
await _updateStatus(result);
|
||||
} catch (e) {
|
||||
debugPrint('NetworkMonitor: Manual refresh failed: $e');
|
||||
await _updateStatus(ConnectivityResult.none);
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否已连接
|
||||
bool get isConnected => _currentStatus?.isConnected ?? false;
|
||||
|
||||
/// 是否WiFi连接
|
||||
bool get isWifi => _currentStatus?.type == NetworkType.wifi;
|
||||
|
||||
/// 是否移动网络
|
||||
bool get isMobile => _currentStatus?.type == NetworkType.mobile;
|
||||
|
||||
/// 是否计费网络
|
||||
bool get isMetered => _currentStatus?.isMetered ?? false;
|
||||
|
||||
/// 等待网络连接
|
||||
Future<bool> waitForConnection({Duration? timeout}) async {
|
||||
if (isConnected) return true;
|
||||
|
||||
final completer = Completer<bool>();
|
||||
StreamSubscription<NetworkStatus>? subscription;
|
||||
Timer? timer;
|
||||
|
||||
subscription = statusStream.listen((status) {
|
||||
if (status.isConnected) {
|
||||
completer.complete(true);
|
||||
subscription?.cancel();
|
||||
timer?.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
if (timeout != null) {
|
||||
timer = Timer(timeout, () {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(false);
|
||||
subscription?.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/// 等待WiFi连接
|
||||
Future<bool> waitForWifi({Duration? timeout}) async {
|
||||
if (isWifi) return true;
|
||||
|
||||
final completer = Completer<bool>();
|
||||
StreamSubscription<NetworkStatus>? subscription;
|
||||
Timer? timer;
|
||||
|
||||
subscription = statusStream.listen((status) {
|
||||
if (status.type == NetworkType.wifi) {
|
||||
completer.complete(true);
|
||||
subscription?.cancel();
|
||||
timer?.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
if (timeout != null) {
|
||||
timer = Timer(timeout, () {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(false);
|
||||
subscription?.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/// 检查是否可以下载
|
||||
bool canDownload({required bool wifiOnly, required bool allowCellular}) {
|
||||
if (!isConnected) return false;
|
||||
|
||||
if (wifiOnly && !isWifi) return false;
|
||||
|
||||
if (!allowCellular && isMobile) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// 获取建议的下载策略
|
||||
Map<String, dynamic> getSuggestedDownloadStrategy() {
|
||||
if (!isConnected) {
|
||||
return {'canDownload': false, 'reason': 'No network connection'};
|
||||
}
|
||||
|
||||
final quality = _currentStatus?.quality ?? NetworkQuality.unknown;
|
||||
final type = _currentStatus?.type ?? NetworkType.other;
|
||||
|
||||
return {
|
||||
'canDownload': true,
|
||||
'useParallelDownload': quality == NetworkQuality.excellent,
|
||||
'maxConcurrentChunks': _getSuggestedChunks(quality),
|
||||
'chunkSize': _getSuggestedChunkSize(quality),
|
||||
'shouldCompress': type == NetworkType.mobile,
|
||||
'recommendedTimeout': _getSuggestedTimeout(quality),
|
||||
};
|
||||
}
|
||||
|
||||
int _getSuggestedChunks(NetworkQuality quality) {
|
||||
switch (quality) {
|
||||
case NetworkQuality.excellent:
|
||||
return 5;
|
||||
case NetworkQuality.good:
|
||||
return 3;
|
||||
case NetworkQuality.moderate:
|
||||
return 2;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
int _getSuggestedChunkSize(NetworkQuality quality) {
|
||||
switch (quality) {
|
||||
case NetworkQuality.excellent:
|
||||
return 2 * 1024 * 1024; // 2MB
|
||||
case NetworkQuality.good:
|
||||
return 1024 * 1024; // 1MB
|
||||
case NetworkQuality.moderate:
|
||||
return 512 * 1024; // 512KB
|
||||
default:
|
||||
return 256 * 1024; // 256KB
|
||||
}
|
||||
}
|
||||
|
||||
int _getSuggestedTimeout(NetworkQuality quality) {
|
||||
switch (quality) {
|
||||
case NetworkQuality.excellent:
|
||||
return 30;
|
||||
case NetworkQuality.good:
|
||||
return 60;
|
||||
case NetworkQuality.moderate:
|
||||
return 120;
|
||||
default:
|
||||
return 300;
|
||||
}
|
||||
}
|
||||
|
||||
/// 释放资源
|
||||
void dispose() {
|
||||
_connectivitySubscription?.cancel();
|
||||
_qualityCheckTimer?.cancel();
|
||||
_statusController.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
|
||||
/// 通知助手类,用于显示下载进度
|
||||
class NotificationHelper {
|
||||
static final NotificationHelper _instance = NotificationHelper._();
|
||||
static NotificationHelper get instance => _instance;
|
||||
NotificationHelper._();
|
||||
|
||||
final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
|
||||
/// 初始化
|
||||
Future<void> initialize(Future<void> Function(NotificationResponse)? onDidReceiveNotificationResponse) async {
|
||||
const AndroidInitializationSettings initializationSettingsAndroid =
|
||||
AndroidInitializationSettings('@mipmap/ic_launcher'); // 使用App图标
|
||||
|
||||
const InitializationSettings initializationSettings = InitializationSettings(
|
||||
android: initializationSettingsAndroid,
|
||||
);
|
||||
|
||||
await _flutterLocalNotificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
onDidReceiveNotificationResponse: onDidReceiveNotificationResponse,
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示下载进度通知
|
||||
Future<void> showDownloadProgressNotification({
|
||||
required int progress,
|
||||
required String title,
|
||||
required String body,
|
||||
}) async {
|
||||
final AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails(
|
||||
'app_upgrade_download_channel',
|
||||
'下载通知',
|
||||
channelDescription: '显示应用更新的下载进度',
|
||||
importance: Importance.low, // 设置为 low,避免下载过程中声音干扰
|
||||
priority: Priority.low,
|
||||
showProgress: true,
|
||||
maxProgress: 100,
|
||||
progress: progress,
|
||||
onlyAlertOnce: true, // 只在首次显示时响铃
|
||||
);
|
||||
final NotificationDetails platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics);
|
||||
|
||||
await _flutterLocalNotificationsPlugin.show(
|
||||
0, // 使用固定的 ID,以便更新通知
|
||||
title,
|
||||
body,
|
||||
platformChannelSpecifics,
|
||||
payload: 'download_progress',
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示下载完成通知
|
||||
Future<void> showDownloadCompleteNotification({
|
||||
required String title,
|
||||
required String body,
|
||||
required String filePath,
|
||||
}) async {
|
||||
final AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails(
|
||||
'app_upgrade_download_channel', // 与进度通知使用相同渠道
|
||||
'下载通知',
|
||||
channelDescription: '显示应用更新的下载进度',
|
||||
importance: Importance.high, // 完成时使用高优先级,以便用户看到
|
||||
priority: Priority.high,
|
||||
autoCancel: true,
|
||||
);
|
||||
final NotificationDetails platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics);
|
||||
|
||||
await _flutterLocalNotificationsPlugin.show(
|
||||
0, // ID 仍然为 0,覆盖之前的进度通知
|
||||
title,
|
||||
body,
|
||||
platformChannelSpecifics,
|
||||
payload: 'download_complete:$filePath', // payload 中带上文件路径
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示下载失败通知
|
||||
Future<void> showDownloadFailedNotification({
|
||||
required String title,
|
||||
required String body,
|
||||
}) async {
|
||||
// 与完成通知类似,但内容不同
|
||||
final AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails(
|
||||
'app_upgrade_download_channel',
|
||||
'下载通知',
|
||||
channelDescription: '显示应用更新的下载进度',
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
autoCancel: true,
|
||||
);
|
||||
final NotificationDetails platformChannelSpecifics = NotificationDetails(android: androidPlatformChannelSpecifics);
|
||||
|
||||
await _flutterLocalNotificationsPlugin.show(
|
||||
0,
|
||||
title,
|
||||
body,
|
||||
platformChannelSpecifics,
|
||||
payload: 'download_failed',
|
||||
);
|
||||
}
|
||||
|
||||
/// 取消通知
|
||||
Future<void> cancelNotification() async {
|
||||
await _flutterLocalNotificationsPlugin.cancel(0);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,232 +0,0 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// 升级配置类
|
||||
class UpgradeConfig {
|
||||
/// 单例实例
|
||||
static UpgradeConfig? _instance;
|
||||
|
||||
/// 获取单例实例
|
||||
static UpgradeConfig get instance {
|
||||
_instance ??= UpgradeConfig._();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
UpgradeConfig._();
|
||||
|
||||
/// 是否启用调试模式
|
||||
bool debugMode = kDebugMode;
|
||||
|
||||
/// 检查更新的间隔时间(小时)
|
||||
int checkIntervalHours = 24;
|
||||
|
||||
/// 是否自动检查更新
|
||||
bool autoCheck = true;
|
||||
|
||||
/// 是否只在WiFi下自动下载
|
||||
bool wifiOnly = true;
|
||||
|
||||
/// 下载超时时间(秒)
|
||||
int downloadTimeout = 300;
|
||||
|
||||
/// 连接超时时间(秒)
|
||||
int connectTimeout = 30;
|
||||
|
||||
/// 最大重试次数
|
||||
int maxRetryCount = 3;
|
||||
|
||||
/// 重试延迟时间(秒)
|
||||
int retryDelay = 5;
|
||||
|
||||
/// 是否使用缓存
|
||||
bool useCache = true;
|
||||
|
||||
/// 缓存有效期(小时)
|
||||
int cacheValidHours = 1;
|
||||
|
||||
/// 是否显示下载通知(Android)
|
||||
bool showNotification = true;
|
||||
|
||||
/// 是否支持断点续传
|
||||
bool supportBreakpoint = true;
|
||||
|
||||
/// 下载块大小(字节)
|
||||
int chunkSize = 1024 * 1024; // 1MB
|
||||
|
||||
/// 最大并发下载数
|
||||
int maxConcurrentDownloads = 3;
|
||||
|
||||
/// 是否校验文件完整性
|
||||
bool verifyIntegrity = true;
|
||||
|
||||
/// 是否允许移动网络下载
|
||||
bool allowCellular = false;
|
||||
|
||||
/// 是否静默下载(后台下载)
|
||||
bool silentDownload = false;
|
||||
|
||||
/// 是否保存下载历史
|
||||
bool saveDownloadHistory = true;
|
||||
|
||||
/// 自定义请求头
|
||||
Map<String, String> customHeaders = {};
|
||||
|
||||
/// 代理设置
|
||||
String? proxyUrl;
|
||||
|
||||
/// 更新配置
|
||||
void updateConfig({
|
||||
bool? debugMode,
|
||||
int? checkIntervalHours,
|
||||
bool? autoCheck,
|
||||
bool? wifiOnly,
|
||||
int? downloadTimeout,
|
||||
int? connectTimeout,
|
||||
int? maxRetryCount,
|
||||
int? retryDelay,
|
||||
bool? useCache,
|
||||
int? cacheValidHours,
|
||||
bool? showNotification,
|
||||
bool? supportBreakpoint,
|
||||
int? chunkSize,
|
||||
int? maxConcurrentDownloads,
|
||||
bool? verifyIntegrity,
|
||||
bool? allowCellular,
|
||||
bool? silentDownload,
|
||||
bool? saveDownloadHistory,
|
||||
Map<String, String>? customHeaders,
|
||||
String? proxyUrl,
|
||||
}) {
|
||||
if (debugMode != null) this.debugMode = debugMode;
|
||||
if (checkIntervalHours != null) this.checkIntervalHours = checkIntervalHours;
|
||||
if (autoCheck != null) this.autoCheck = autoCheck;
|
||||
if (wifiOnly != null) this.wifiOnly = wifiOnly;
|
||||
if (downloadTimeout != null) this.downloadTimeout = downloadTimeout;
|
||||
if (connectTimeout != null) this.connectTimeout = connectTimeout;
|
||||
if (maxRetryCount != null) this.maxRetryCount = maxRetryCount;
|
||||
if (retryDelay != null) this.retryDelay = retryDelay;
|
||||
if (useCache != null) this.useCache = useCache;
|
||||
if (cacheValidHours != null) this.cacheValidHours = cacheValidHours;
|
||||
if (showNotification != null) this.showNotification = showNotification;
|
||||
if (supportBreakpoint != null) this.supportBreakpoint = supportBreakpoint;
|
||||
if (chunkSize != null) this.chunkSize = chunkSize;
|
||||
if (maxConcurrentDownloads != null) this.maxConcurrentDownloads = maxConcurrentDownloads;
|
||||
if (verifyIntegrity != null) this.verifyIntegrity = verifyIntegrity;
|
||||
if (allowCellular != null) this.allowCellular = allowCellular;
|
||||
if (silentDownload != null) this.silentDownload = silentDownload;
|
||||
if (saveDownloadHistory != null) this.saveDownloadHistory = saveDownloadHistory;
|
||||
if (customHeaders != null) this.customHeaders = customHeaders;
|
||||
if (proxyUrl != null) this.proxyUrl = proxyUrl;
|
||||
}
|
||||
|
||||
/// 重置为默认配置
|
||||
void reset() {
|
||||
debugMode = kDebugMode;
|
||||
checkIntervalHours = 24;
|
||||
autoCheck = true;
|
||||
wifiOnly = true;
|
||||
downloadTimeout = 300;
|
||||
connectTimeout = 30;
|
||||
maxRetryCount = 3;
|
||||
retryDelay = 5;
|
||||
useCache = true;
|
||||
cacheValidHours = 1;
|
||||
showNotification = true;
|
||||
supportBreakpoint = true;
|
||||
chunkSize = 1024 * 1024;
|
||||
maxConcurrentDownloads = 3;
|
||||
verifyIntegrity = true;
|
||||
allowCellular = false;
|
||||
silentDownload = false;
|
||||
saveDownloadHistory = true;
|
||||
customHeaders = {};
|
||||
proxyUrl = null;
|
||||
}
|
||||
|
||||
/// 导出配置为Map
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'debugMode': debugMode,
|
||||
'checkIntervalHours': checkIntervalHours,
|
||||
'autoCheck': autoCheck,
|
||||
'wifiOnly': wifiOnly,
|
||||
'downloadTimeout': downloadTimeout,
|
||||
'connectTimeout': connectTimeout,
|
||||
'maxRetryCount': maxRetryCount,
|
||||
'retryDelay': retryDelay,
|
||||
'useCache': useCache,
|
||||
'cacheValidHours': cacheValidHours,
|
||||
'showNotification': showNotification,
|
||||
'supportBreakpoint': supportBreakpoint,
|
||||
'chunkSize': chunkSize,
|
||||
'maxConcurrentDownloads': maxConcurrentDownloads,
|
||||
'verifyIntegrity': verifyIntegrity,
|
||||
'allowCellular': allowCellular,
|
||||
'silentDownload': silentDownload,
|
||||
'saveDownloadHistory': saveDownloadHistory,
|
||||
'customHeaders': customHeaders,
|
||||
'proxyUrl': proxyUrl,
|
||||
};
|
||||
}
|
||||
|
||||
/// 从Map导入配置
|
||||
void fromMap(Map<String, dynamic> map) {
|
||||
updateConfig(
|
||||
debugMode: map['debugMode'],
|
||||
checkIntervalHours: map['checkIntervalHours'],
|
||||
autoCheck: map['autoCheck'],
|
||||
wifiOnly: map['wifiOnly'],
|
||||
downloadTimeout: map['downloadTimeout'],
|
||||
connectTimeout: map['connectTimeout'],
|
||||
maxRetryCount: map['maxRetryCount'],
|
||||
retryDelay: map['retryDelay'],
|
||||
useCache: map['useCache'],
|
||||
cacheValidHours: map['cacheValidHours'],
|
||||
showNotification: map['showNotification'],
|
||||
supportBreakpoint: map['supportBreakpoint'],
|
||||
chunkSize: map['chunkSize'],
|
||||
maxConcurrentDownloads: map['maxConcurrentDownloads'],
|
||||
verifyIntegrity: map['verifyIntegrity'],
|
||||
allowCellular: map['allowCellular'],
|
||||
silentDownload: map['silentDownload'],
|
||||
saveDownloadHistory: map['saveDownloadHistory'],
|
||||
customHeaders: map['customHeaders'] != null ? Map<String, String>.from(map['customHeaders']) : null,
|
||||
proxyUrl: map['proxyUrl'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 升级策略
|
||||
enum UpgradeStrategy {
|
||||
/// 立即升级
|
||||
immediate,
|
||||
|
||||
/// 空闲时升级
|
||||
idle,
|
||||
|
||||
/// 定时升级
|
||||
scheduled,
|
||||
|
||||
/// 静默升级
|
||||
silent,
|
||||
|
||||
/// 灰度升级
|
||||
grayScale,
|
||||
}
|
||||
|
||||
/// 版本比较策略
|
||||
enum VersionCompareStrategy {
|
||||
/// 数字比较(如:1.2.3)
|
||||
numeric,
|
||||
|
||||
/// 语义化版本比较(如:1.2.3-beta.1)
|
||||
semantic,
|
||||
|
||||
/// 时间戳比较
|
||||
timestamp,
|
||||
|
||||
/// 构建号比较
|
||||
buildNumber,
|
||||
|
||||
/// 自定义比较
|
||||
custom,
|
||||
}
|
||||
|
|
@ -1,350 +0,0 @@
|
|||
import 'upgrade_config.dart';
|
||||
|
||||
/// 版本信息
|
||||
class Version {
|
||||
final String raw;
|
||||
final int? major;
|
||||
final int? minor;
|
||||
final int? patch;
|
||||
final String? preRelease;
|
||||
final String? buildMetadata;
|
||||
final int? buildNumber;
|
||||
final DateTime? timestamp;
|
||||
|
||||
Version({
|
||||
required this.raw,
|
||||
this.major,
|
||||
this.minor,
|
||||
this.patch,
|
||||
this.preRelease,
|
||||
this.buildMetadata,
|
||||
this.buildNumber,
|
||||
this.timestamp,
|
||||
});
|
||||
|
||||
/// 从字符串解析版本
|
||||
factory Version.parse(String version) {
|
||||
final raw = version.trim();
|
||||
|
||||
// 尝试解析语义化版本 (1.2.3-beta.1+build.123)
|
||||
final semanticRegex = RegExp(r'^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9\.\-]+))?(?:\+([a-zA-Z0-9\.\-]+))?$');
|
||||
|
||||
final match = semanticRegex.firstMatch(raw);
|
||||
if (match != null) {
|
||||
return Version(
|
||||
raw: raw,
|
||||
major: int.tryParse(match.group(1)!),
|
||||
minor: int.tryParse(match.group(2)!),
|
||||
patch: int.tryParse(match.group(3)!),
|
||||
preRelease: match.group(4),
|
||||
buildMetadata: match.group(5),
|
||||
);
|
||||
}
|
||||
|
||||
// 先尝试解析为纯数字构建号
|
||||
final buildNumber = int.tryParse(raw);
|
||||
if (buildNumber != null && !raw.contains('.')) {
|
||||
return Version(raw: raw, buildNumber: buildNumber);
|
||||
}
|
||||
|
||||
// 尝试解析简单版本 (1.2.3)
|
||||
final simpleRegex = RegExp(r'^(\d+)(?:\.(\d+))?(?:\.(\d+))?$');
|
||||
final simpleMatch = simpleRegex.firstMatch(raw);
|
||||
if (simpleMatch != null) {
|
||||
return Version(
|
||||
raw: raw,
|
||||
major: int.tryParse(simpleMatch.group(1)!),
|
||||
minor: simpleMatch.group(2) != null ? int.tryParse(simpleMatch.group(2)!) : null,
|
||||
patch: simpleMatch.group(3) != null ? int.tryParse(simpleMatch.group(3)!) : null,
|
||||
);
|
||||
}
|
||||
|
||||
// 尝试解析为时间戳
|
||||
final timestamp = DateTime.tryParse(raw);
|
||||
if (timestamp != null) {
|
||||
return Version(raw: raw, timestamp: timestamp);
|
||||
}
|
||||
|
||||
// 无法解析,返回原始字符串
|
||||
return Version(raw: raw);
|
||||
}
|
||||
|
||||
/// 转换为字符串
|
||||
@override
|
||||
String toString() => raw;
|
||||
|
||||
/// 是否是预发布版本
|
||||
bool get isPreRelease => preRelease != null && preRelease!.isNotEmpty;
|
||||
|
||||
/// 获取版本数组
|
||||
List<int> get versionArray {
|
||||
final arr = <int>[];
|
||||
if (major != null) arr.add(major!);
|
||||
if (minor != null) arr.add(minor!);
|
||||
if (patch != null) arr.add(patch!);
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
|
||||
/// 版本比较器
|
||||
class VersionComparator {
|
||||
final VersionCompareStrategy strategy;
|
||||
final Function(Version, Version)? customComparator;
|
||||
|
||||
VersionComparator({this.strategy = VersionCompareStrategy.semantic, this.customComparator});
|
||||
|
||||
/// 比较两个版本
|
||||
/// 返回: 1 表示 v1 > v2, 0 表示相等, -1 表示 v1 < v2
|
||||
int compare(String version1, String version2) {
|
||||
if (version1 == version2) return 0;
|
||||
|
||||
final v1 = Version.parse(version1);
|
||||
final v2 = Version.parse(version2);
|
||||
|
||||
switch (strategy) {
|
||||
case VersionCompareStrategy.numeric:
|
||||
return _compareNumeric(v1, v2);
|
||||
case VersionCompareStrategy.semantic:
|
||||
return _compareSemantic(v1, v2);
|
||||
case VersionCompareStrategy.timestamp:
|
||||
return _compareTimestamp(v1, v2);
|
||||
case VersionCompareStrategy.buildNumber:
|
||||
return _compareBuildNumber(v1, v2);
|
||||
case VersionCompareStrategy.custom:
|
||||
if (customComparator != null) {
|
||||
return customComparator!(v1, v2);
|
||||
}
|
||||
return _compareSemantic(v1, v2);
|
||||
}
|
||||
}
|
||||
|
||||
/// 数字比较
|
||||
int _compareNumeric(Version v1, Version v2) {
|
||||
final arr1 = v1.versionArray;
|
||||
final arr2 = v2.versionArray;
|
||||
|
||||
if (arr1.isEmpty && arr2.isEmpty) {
|
||||
return v1.raw.compareTo(v2.raw);
|
||||
}
|
||||
|
||||
if (arr1.isEmpty) return -1;
|
||||
if (arr2.isEmpty) return 1;
|
||||
|
||||
final maxLength = arr1.length > arr2.length ? arr1.length : arr2.length;
|
||||
|
||||
for (int i = 0; i < maxLength; i++) {
|
||||
final num1 = i < arr1.length ? arr1[i] : 0;
|
||||
final num2 = i < arr2.length ? arr2[i] : 0;
|
||||
|
||||
if (num1 > num2) return 1;
|
||||
if (num1 < num2) return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// 语义化版本比较
|
||||
int _compareSemantic(Version v1, Version v2) {
|
||||
// 先比较主版本号
|
||||
final majorCompare = _compareInt(v1.major, v2.major);
|
||||
if (majorCompare != 0) return majorCompare;
|
||||
|
||||
// 比较次版本号
|
||||
final minorCompare = _compareInt(v1.minor, v2.minor);
|
||||
if (minorCompare != 0) return minorCompare;
|
||||
|
||||
// 比较修订号
|
||||
final patchCompare = _compareInt(v1.patch, v2.patch);
|
||||
if (patchCompare != 0) return patchCompare;
|
||||
|
||||
// 比较预发布版本
|
||||
if (v1.preRelease != null && v2.preRelease == null) return -1;
|
||||
if (v1.preRelease == null && v2.preRelease != null) return 1;
|
||||
if (v1.preRelease != null && v2.preRelease != null) {
|
||||
return _comparePreRelease(v1.preRelease!, v2.preRelease!);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// 比较预发布版本
|
||||
int _comparePreRelease(String pre1, String pre2) {
|
||||
final parts1 = pre1.split('.');
|
||||
final parts2 = pre2.split('.');
|
||||
|
||||
final maxLength = parts1.length > parts2.length ? parts1.length : parts2.length;
|
||||
|
||||
for (int i = 0; i < maxLength; i++) {
|
||||
if (i >= parts1.length) return -1;
|
||||
if (i >= parts2.length) return 1;
|
||||
|
||||
final part1 = parts1[i];
|
||||
final part2 = parts2[i];
|
||||
|
||||
// 尝试数字比较
|
||||
final num1 = int.tryParse(part1);
|
||||
final num2 = int.tryParse(part2);
|
||||
|
||||
if (num1 != null && num2 != null) {
|
||||
if (num1 > num2) return 1;
|
||||
if (num1 < num2) return -1;
|
||||
} else {
|
||||
// 字符串比较
|
||||
final compare = part1.compareTo(part2);
|
||||
if (compare != 0) return compare;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// 时间戳比较
|
||||
int _compareTimestamp(Version v1, Version v2) {
|
||||
if (v1.timestamp == null && v2.timestamp == null) {
|
||||
return _compareSemantic(v1, v2);
|
||||
}
|
||||
|
||||
if (v1.timestamp == null) return -1;
|
||||
if (v2.timestamp == null) return 1;
|
||||
|
||||
return v1.timestamp!.compareTo(v2.timestamp!);
|
||||
}
|
||||
|
||||
/// 构建号比较
|
||||
int _compareBuildNumber(Version v1, Version v2) {
|
||||
if (v1.buildNumber == null && v2.buildNumber == null) {
|
||||
return _compareSemantic(v1, v2);
|
||||
}
|
||||
|
||||
if (v1.buildNumber == null) return -1;
|
||||
if (v2.buildNumber == null) return 1;
|
||||
|
||||
if (v1.buildNumber! > v2.buildNumber!) return 1;
|
||||
if (v1.buildNumber! < v2.buildNumber!) return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// 比较两个可空整数
|
||||
int _compareInt(int? a, int? b) {
|
||||
if (a == null && b == null) return 0;
|
||||
if (a == null) return -1;
|
||||
if (b == null) return 1;
|
||||
if (a > b) return 1;
|
||||
if (a < b) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// 检查是否需要更新
|
||||
bool isUpdateAvailable(String currentVersion, String remoteVersion) {
|
||||
return compare(remoteVersion, currentVersion) > 0;
|
||||
}
|
||||
|
||||
/// 检查是否是主要版本更新
|
||||
bool isMajorUpdate(String currentVersion, String remoteVersion) {
|
||||
final v1 = Version.parse(currentVersion);
|
||||
final v2 = Version.parse(remoteVersion);
|
||||
|
||||
if (v1.major != null && v2.major != null) {
|
||||
return v2.major! > v1.major!;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 检查是否是次要版本更新
|
||||
bool isMinorUpdate(String currentVersion, String remoteVersion) {
|
||||
final v1 = Version.parse(currentVersion);
|
||||
final v2 = Version.parse(remoteVersion);
|
||||
|
||||
if (v1.major != null && v2.major != null && v1.minor != null && v2.minor != null) {
|
||||
return v2.major! == v1.major! && v2.minor! > v1.minor!;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 检查是否是修订版本更新
|
||||
bool isPatchUpdate(String currentVersion, String remoteVersion) {
|
||||
final v1 = Version.parse(currentVersion);
|
||||
final v2 = Version.parse(remoteVersion);
|
||||
|
||||
if (v1.major != null &&
|
||||
v2.major != null &&
|
||||
v1.minor != null &&
|
||||
v2.minor != null &&
|
||||
v1.patch != null &&
|
||||
v2.patch != null) {
|
||||
return v2.major! == v1.major! && v2.minor! == v1.minor! && v2.patch! > v1.patch!;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 获取版本差异描述
|
||||
String getVersionDifference(String currentVersion, String remoteVersion) {
|
||||
if (isMajorUpdate(currentVersion, remoteVersion)) {
|
||||
return '主要版本更新';
|
||||
} else if (isMinorUpdate(currentVersion, remoteVersion)) {
|
||||
return '功能更新';
|
||||
} else if (isPatchUpdate(currentVersion, remoteVersion)) {
|
||||
return 'Bug修复';
|
||||
} else if (isUpdateAvailable(currentVersion, remoteVersion)) {
|
||||
return '新版本';
|
||||
} else {
|
||||
return '已是最新版本';
|
||||
}
|
||||
}
|
||||
|
||||
/// 批量比较版本,返回最新版本
|
||||
String? getLatestVersion(List<String> versions) {
|
||||
if (versions.isEmpty) return null;
|
||||
if (versions.length == 1) return versions.first;
|
||||
|
||||
String latest = versions.first;
|
||||
for (final version in versions.skip(1)) {
|
||||
if (compare(version, latest) > 0) {
|
||||
latest = version;
|
||||
}
|
||||
}
|
||||
|
||||
return latest;
|
||||
}
|
||||
|
||||
/// 按版本排序
|
||||
List<String> sortVersions(List<String> versions, {bool descending = false}) {
|
||||
final sorted = List<String>.from(versions);
|
||||
sorted.sort((a, b) => descending ? compare(b, a) : compare(a, b));
|
||||
return sorted;
|
||||
}
|
||||
|
||||
/// 过滤出符合条件的版本
|
||||
List<String> filterVersions(
|
||||
List<String> versions, {
|
||||
String? minVersion,
|
||||
String? maxVersion,
|
||||
bool includePreRelease = false,
|
||||
}) {
|
||||
return versions.where((version) {
|
||||
// 检查最小版本
|
||||
if (minVersion != null && compare(version, minVersion) < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查最大版本
|
||||
if (maxVersion != null && compare(version, maxVersion) > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查预发布版本
|
||||
if (!includePreRelease) {
|
||||
final v = Version.parse(version);
|
||||
if (v.isPreRelease) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../app_upgrade_plugin.dart';
|
||||
import '../core/upgrade_utils.dart';
|
||||
|
||||
/// 下载进度对话框
|
||||
class DownloadProgressDialog extends StatefulWidget {
|
||||
final String downloadUrl;
|
||||
final Color? primaryColor;
|
||||
|
||||
const DownloadProgressDialog({super.key, required this.downloadUrl, this.primaryColor});
|
||||
|
||||
/// 显示下载进度对话框
|
||||
static Future<String?> show(BuildContext context, {required String downloadUrl, Color? primaryColor}) {
|
||||
return showDialog<String?>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => DownloadProgressDialog(downloadUrl: downloadUrl, primaryColor: primaryColor),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<DownloadProgressDialog> createState() => _DownloadProgressDialogState();
|
||||
}
|
||||
|
||||
class _DownloadProgressDialogState extends State<DownloadProgressDialog> {
|
||||
final AppUpgradePlugin _plugin = AppUpgradePlugin();
|
||||
|
||||
double _progress = 0.0;
|
||||
String _progressText = '准备下载...';
|
||||
int _received = 0;
|
||||
int _total = 0;
|
||||
bool _isDownloading = false;
|
||||
String? _errorMessage;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startDownload();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final primaryColor = widget.primaryColor ?? Theme.of(context).primaryColor;
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async => false,
|
||||
child: Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 图标
|
||||
Icon(
|
||||
_errorMessage != null ? Icons.error_outline : Icons.download_rounded,
|
||||
size: 48,
|
||||
color: _errorMessage != null ? Colors.red : primaryColor,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 标题
|
||||
Text(
|
||||
_errorMessage != null ? '下载失败' : '正在下载更新',
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (_errorMessage != null) ...[
|
||||
// 错误信息
|
||||
Text(
|
||||
_errorMessage!,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 按钮
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(null);
|
||||
},
|
||||
child: const Text('取消'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_errorMessage = null;
|
||||
_progress = 0.0;
|
||||
_progressText = '准备下载...';
|
||||
});
|
||||
_startDownload();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(backgroundColor: primaryColor),
|
||||
child: const Text('重试'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
] else ...[
|
||||
// 进度条
|
||||
LinearProgressIndicator(
|
||||
value: _isDownloading ? _progress : null,
|
||||
backgroundColor: Colors.grey[300],
|
||||
valueColor: AlwaysStoppedAnimation<Color>(primaryColor),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 进度文本
|
||||
Text(_progressText, style: TextStyle(fontSize: 14, color: Colors.grey[600])),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 文件大小信息
|
||||
if (_total > 0)
|
||||
Text(
|
||||
'${formatBytes(_received)} / ${formatBytes(_total)}',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _startDownload() async {
|
||||
setState(() {
|
||||
_isDownloading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final filePath = await _plugin.downloadApk(
|
||||
widget.downloadUrl,
|
||||
onProgress: (progress) {
|
||||
setState(() {
|
||||
_progress = progress.progress;
|
||||
_received = progress.received;
|
||||
_total = progress.total;
|
||||
_progressText = '下载中 ${progress.percentage}%';
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if (filePath != null) {
|
||||
setState(() {
|
||||
_progressText = '下载完成';
|
||||
});
|
||||
|
||||
// 延迟一下让用户看到完成状态
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
// 返回文件路径
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop(filePath);
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
_errorMessage = '下载失败,请检查网络连接';
|
||||
_isDownloading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_errorMessage = '下载出错:${e.toString()}';
|
||||
_isDownloading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../app_upgrade_plugin.dart';
|
||||
import '../core/upgrade_utils.dart';
|
||||
|
||||
/// App升级对话框
|
||||
class UpgradeDialog extends StatefulWidget {
|
||||
final UpgradeInfo upgradeInfo;
|
||||
final VoidCallback? onCancel;
|
||||
final VoidCallback? onConfirm;
|
||||
final Color? primaryColor;
|
||||
|
||||
const UpgradeDialog({super.key, required this.upgradeInfo, this.onCancel, this.onConfirm, this.primaryColor});
|
||||
|
||||
/// 显示升级对话框
|
||||
static Future<bool?> show(BuildContext context, {required UpgradeInfo upgradeInfo, Color? primaryColor}) {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: !upgradeInfo.isForceUpdate,
|
||||
builder: (BuildContext context) {
|
||||
return UpgradeDialog(
|
||||
upgradeInfo: upgradeInfo,
|
||||
primaryColor: primaryColor,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<UpgradeDialog> createState() => _UpgradeDialogState();
|
||||
}
|
||||
|
||||
class _UpgradeDialogState extends State<UpgradeDialog> {
|
||||
final AppUpgradePlugin _plugin = AppUpgradePlugin();
|
||||
bool _isDownloading = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final primaryColor = widget.primaryColor ?? Theme.of(context).primaryColor;
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async => !widget.upgradeInfo.isForceUpdate && !_isDownloading,
|
||||
child: Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 顶部图片或图标
|
||||
Container(
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.system_update, size: 48, color: Colors.white),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'发现新版本 ${widget.upgradeInfo.versionName}',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 更新内容
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('更新内容:', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
Text(widget.upgradeInfo.updateContent, style: const TextStyle(fontSize: 14, height: 1.5)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 文件大小提示(如果有)
|
||||
if (widget.upgradeInfo.apkSize != null && Platform.isAndroid)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'新版本大小:${formatBytes(widget.upgradeInfo.apkSize!)}',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 按钮
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
if (!widget.upgradeInfo.isForceUpdate) ...[
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: _isDownloading
|
||||
? null
|
||||
: () {
|
||||
widget.onCancel?.call();
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text('稍后更新', style: TextStyle(color: Colors.grey[600])),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: _isDownloading ? null : () => _handleConfirm(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: primaryColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
),
|
||||
child: const Text('立即更新'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleConfirm() async {
|
||||
widget.onConfirm?.call();
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
// Android平台:下载并安装APK
|
||||
if (widget.upgradeInfo.downloadUrl != null) {
|
||||
setState(() {
|
||||
_isDownloading = true;
|
||||
});
|
||||
|
||||
// 显示下载进度对话框
|
||||
final filePath = await DownloadProgressDialog.show(
|
||||
context,
|
||||
downloadUrl: widget.upgradeInfo.downloadUrl!,
|
||||
primaryColor: widget.primaryColor,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_isDownloading = false;
|
||||
});
|
||||
|
||||
if (filePath != null) {
|
||||
// 安装前再次检查并请求权限
|
||||
final hasInstallPermission = await PermissionHelper.checkAndRequestInstallPermission(context: context);
|
||||
if (!hasInstallPermission) {
|
||||
_showError('未授予安装权限,无法完成更新');
|
||||
return;
|
||||
}
|
||||
|
||||
// 安装APK
|
||||
final success = await _plugin.installApk(filePath);
|
||||
if (success) {
|
||||
if (!widget.upgradeInfo.isForceUpdate) {
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
} else {
|
||||
_showError('安装失败,请检查权限设置');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (Platform.isIOS) {
|
||||
// iOS平台:跳转到App Store
|
||||
if (widget.upgradeInfo.appStoreUrl != null) {
|
||||
final success = await _plugin.goToAppStore(widget.upgradeInfo.appStoreUrl!);
|
||||
if (success) {
|
||||
Navigator.of(context).pop(true);
|
||||
} else {
|
||||
_showError('跳转App Store失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showError(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message), backgroundColor: Colors.red));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
export 'download_progress_dialog.dart';
|
||||
export 'upgrade_dialog.dart';
|
||||
// 所有对话框都已整合到 app_upgrade_simple.dart 中
|
||||
// 这里保留市场选择对话框的导出
|
||||
export 'market_selection_dialog.dart';
|
||||
|
|
|
|||
Loading…
Reference in New Issue