清洁代码
This commit is contained in:
parent
00fc47c653
commit
2f0cd3ef37
|
|
@ -16,6 +16,7 @@ export 'core/http_config.dart';
|
||||||
export 'core/permission_helper.dart';
|
export 'core/permission_helper.dart';
|
||||||
export 'models/install_strategy.dart';
|
export 'models/install_strategy.dart';
|
||||||
export 'models/upgrade_info.dart';
|
export 'models/upgrade_info.dart';
|
||||||
|
// 导出市场选择对话框(其他对话框已整合到简化API中)
|
||||||
export 'widgets/widgets.dart';
|
export 'widgets/widgets.dart';
|
||||||
|
|
||||||
class AppUpgradePlugin {
|
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');
|
debugPrint('响应数据: $responseData');
|
||||||
|
|
||||||
// TODO 解析更新信息
|
// 解析更新信息
|
||||||
// final upgradeInfo = UpgradeInfo.fromJson(responseData);
|
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 (可选,用于校验)
|
|
||||||
});
|
|
||||||
|
|
||||||
// 比较版本
|
// 比较版本
|
||||||
if (_compareVersion(upgradeInfo.versionCode, currentBuildNumber) > 0) {
|
if (_compareVersion(upgradeInfo.versionCode, currentBuildNumber) > 0) {
|
||||||
|
|
|
||||||
|
|
@ -73,25 +73,26 @@ class AppUpgradeSimple {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('检查更新失败: $e');
|
debugPrint('检查更新失败: $e');
|
||||||
|
|
||||||
String errorMessage = '检查更新失败';
|
final errorString = e.toString();
|
||||||
|
final String errorMessage;
|
||||||
|
|
||||||
if (e.toString().contains('无网络连接')) {
|
if (errorString.contains('无网络连接')) {
|
||||||
errorMessage = '无网络连接,请检查网络设置';
|
errorMessage = '无网络连接,请检查网络设置';
|
||||||
} else if (e.toString().contains('Failed host lookup')) {
|
} else if (errorString.contains('Failed host lookup')) {
|
||||||
errorMessage = '无法连接到服务器,请检查网络或稍后重试';
|
errorMessage = '无法连接到服务器,请检查网络或稍后重试';
|
||||||
} else if (e.toString().contains('Connection refused')) {
|
} else if (errorString.contains('Connection refused')) {
|
||||||
errorMessage = '服务器拒绝连接,请稍后重试';
|
errorMessage = '服务器拒绝连接,请稍后重试';
|
||||||
} else if (e.toString().contains('timeout')) {
|
} else if (errorString.contains('timeout')) {
|
||||||
errorMessage = '连接超时,请检查网络';
|
errorMessage = '连接超时,请检查网络';
|
||||||
} else {
|
} else {
|
||||||
errorMessage = '检查更新失败: ${e.toString().split(':').first}';
|
errorMessage = '检查更新失败: ${errorString.split(':').first}';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
_showToast(errorMessage);
|
_showToast(errorMessage);
|
||||||
|
|
||||||
// 如果是网络问题,显示网络诊断建议
|
// 如果是网络问题,显示网络诊断建议
|
||||||
if (e.toString().contains('Failed host lookup') || e.toString().contains('无网络连接')) {
|
if (errorString.contains('Failed host lookup') || errorString.contains('无网络连接')) {
|
||||||
debugPrint('建议: 请检查网络连接或尝试使用网络诊断功能');
|
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({
|
Future<UpgradeInfo?> checkUpdateSilent({
|
||||||
required String url,
|
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提示
|
/// 显示Toast提示
|
||||||
void _showToast(String message) {
|
void _showToast(String message) {
|
||||||
Fluttertoast.showToast(msg: message);
|
Fluttertoast.showToast(msg: message);
|
||||||
|
|
@ -206,20 +156,6 @@ class AppUpgradeSimple {
|
||||||
return false;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
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();
|
info.updateContent.split(RegExp(r'\r?\n')).map((e) => e.trim()).where((e) => e.isNotEmpty).toList();
|
||||||
|
|
||||||
return Column(
|
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() {
|
void _handleAction() {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
_handleAndroidAction();
|
_handleAndroidAction();
|
||||||
|
|
@ -550,14 +356,10 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
Future<void> _handleAndroidAction() async {
|
Future<void> _handleAndroidAction() async {
|
||||||
// On Android, we always assume a market option is available,
|
// On Android, we always assume a market option is available,
|
||||||
// because we can fall back to a generic market:// intent.
|
// because we can fall back to a generic market:// intent.
|
||||||
const bool hasMarketOption = true;
|
|
||||||
final bool hasDownloadOption = info.downloadUrl != null;
|
final bool hasDownloadOption = info.downloadUrl != null;
|
||||||
|
|
||||||
// This case is unlikely on Android, but kept for robustness.
|
// 在Android上,我们总是假设市场选项可用,因为可以回退到通用的 market:// intent
|
||||||
if (!hasMarketOption && !hasDownloadOption) {
|
// 因此这里不需要检查无更新方法的情况
|
||||||
showToast('No update method available.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a download option is not available, going to the market is the only choice.
|
// If a download option is not available, going to the market is the only choice.
|
||||||
if (!hasDownloadOption) {
|
if (!hasDownloadOption) {
|
||||||
|
|
@ -587,7 +389,6 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
final pkg = appInfo['packageName'] ?? '';
|
final pkg = appInfo['packageName'] ?? '';
|
||||||
if (pkg.isNotEmpty) {
|
if (pkg.isNotEmpty) {
|
||||||
_plugin.goToAppStore('market://details?id=$pkg');
|
_plugin.goToAppStore('market://details?id=$pkg');
|
||||||
// _plugin.goToAppStore('market://details?id=$pkg');
|
|
||||||
} else {
|
} else {
|
||||||
showToast('Could not determine app package name.');
|
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.
|
/// Pops the current dialog and then performs the market action.
|
||||||
Future<void> _handleMarketAction() async {
|
Future<void> _handleMarketAction() async {
|
||||||
// Pop the upgrade dialog before proceeding.
|
// 对于强制更新,不关闭对话框
|
||||||
if (Navigator.canPop(context)) {
|
if (!info.isForceUpdate && Navigator.canPop(context)) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
@ -606,8 +407,6 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showDownloadChoiceSheet() async {
|
Future<void> _showDownloadChoiceSheet() async {
|
||||||
final bool hasDownload = info.downloadUrl != null;
|
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
final choice = await showModalBottomSheet<String>(
|
final choice = await showModalBottomSheet<String>(
|
||||||
|
|
@ -662,7 +461,6 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
),
|
),
|
||||||
|
|
||||||
// Option 2: Direct Download
|
// Option 2: Direct Download
|
||||||
if (hasDownload)
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.download_for_offline_outlined),
|
leading: const Icon(Icons.download_for_offline_outlined),
|
||||||
title: const Text('直接下载安装包'),
|
title: const Text('直接下载安装包'),
|
||||||
|
|
@ -699,12 +497,17 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (choice == 'market') {
|
if (choice == 'market') {
|
||||||
|
// 对于强制更新,不调用 onComplete,保持对话框打开状态
|
||||||
|
if (!info.isForceUpdate) {
|
||||||
await _performMarketAction();
|
await _performMarketAction();
|
||||||
onComplete?.call();
|
onComplete?.call();
|
||||||
|
} else {
|
||||||
|
await _performMarketAction();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (choice == 'download' && hasDownload && !_isDownloading) {
|
if (choice == 'download' && !_isDownloading) {
|
||||||
await _startDownloadAndInstall();
|
await _startDownloadAndInstall();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -744,8 +547,6 @@ class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _Upgrad
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
|
@ -756,7 +557,7 @@ class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _Upgrad
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.system_update,
|
Icons.system_update,
|
||||||
color: theme.colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Text('发现新版本'),
|
const Text('发现新版本'),
|
||||||
|
|
@ -772,7 +573,6 @@ class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _Upgrad
|
||||||
actions: _isDownloading
|
actions: _isDownloading
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
if (!widget.info.isForceUpdate)
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|
@ -799,78 +599,20 @@ class _ForceUpgradeDialog extends StatefulWidget {
|
||||||
State<_ForceUpgradeDialog> createState() => _ForceUpgradeDialogState();
|
State<_ForceUpgradeDialog> createState() => _ForceUpgradeDialogState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> {
|
class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> with _UpgradeDialogLogic {
|
||||||
final _plugin = AppUpgradePlugin();
|
|
||||||
bool _isDownloading = false;
|
|
||||||
double _downloadProgress = 0;
|
|
||||||
String _statusText = '';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
UpgradeInfo get info => widget.info;
|
||||||
super.initState();
|
@override
|
||||||
// 强制更新也需用户交互后再开始下载
|
void Function(String) get showToast => (message) => AppUpgradeSimple.instance._showToast(message);
|
||||||
}
|
@override
|
||||||
|
VoidCallback? get onComplete => null;
|
||||||
Future<void> _startDownload() async {
|
@override
|
||||||
// 下载前申请权限 (Just-in-Time)
|
bool get autoDownload => false;
|
||||||
if (Platform.isAndroid) {
|
@override
|
||||||
final hasStorage = await PermissionHelper.checkAndRequestStoragePermission(context: context);
|
bool get autoInstall => true;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: () async => false, // 强制更新,不允许返回
|
onWillPop: () async => false, // 强制更新,不允许返回
|
||||||
child: AlertDialog(
|
child: AlertDialog(
|
||||||
|
|
@ -883,7 +625,7 @@ class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> {
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.system_update,
|
Icons.system_update,
|
||||||
color: theme.colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Text('发现新版本 (强制)'),
|
const Text('发现新版本 (强制)'),
|
||||||
|
|
@ -900,253 +642,11 @@ class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> {
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: _handleAction,
|
||||||
// 强制更新下也先让用户选择安装方式
|
child: Text(Platform.isAndroid ? '立即更新' : '前往更新'),
|
||||||
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('立即更新'),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 当 [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';
|
// 所有对话框都已整合到 app_upgrade_simple.dart 中
|
||||||
export 'upgrade_dialog.dart';
|
// 这里保留市场选择对话框的导出
|
||||||
|
export 'market_selection_dialog.dart';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue