清洁代码

This commit is contained in:
DESKTOP-I3JPKHK\wy 2025-09-18 10:21:30 +08:00
parent 00fc47c653
commit 2f0cd3ef37
13 changed files with 54 additions and 3750 deletions

View File

@ -16,6 +16,7 @@ export 'core/http_config.dart';
export 'core/permission_helper.dart';
export 'models/install_strategy.dart';
export 'models/upgrade_info.dart';
// API中
export 'widgets/widgets.dart';
class AppUpgradePlugin {

View File

@ -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;
}
}

View File

@ -167,20 +167,8 @@ class MethodChannelAppUpgradePlugin extends AppUpgradePluginPlatform {
debugPrint('响应数据: $responseData');
// TODO
// final upgradeInfo = UpgradeInfo.fromJson(responseData);
final upgradeInfo = UpgradeInfo.fromJson({
"hasUpdate": true,
"isForceUpdate": true,
"versionCode": "101", // Android buildNumber,
"versionName": "1.0.1", //
"updateContent": "1. 修复了xxx Bug。\n2. 优化了用户体验。",
"downloadUrl":
"https://dpc-job-oss.23544.com/infra-app/making_school_asignment_app/1.0.5/1/app-release.apk", // APK
"appStoreUrl": "https://itunes.apple.com/app/id123456", // iOS App Store
"apkSize": 20971520, // APK (: byte)
"apkMd5": "b10a8db164e0754105b7a99be72e3fe5" // APK MD5 ()
});
//
final upgradeInfo = UpgradeInfo.fromJson(responseData);
//
if (_compareVersion(upgradeInfo.versionCode, currentBuildNumber) > 0) {

View File

@ -73,25 +73,26 @@ class AppUpgradeSimple {
} catch (e) {
debugPrint('检查更新失败: $e');
String errorMessage = '检查更新失败';
final errorString = e.toString();
final String errorMessage;
if (e.toString().contains('无网络连接')) {
if (errorString.contains('无网络连接')) {
errorMessage = '无网络连接,请检查网络设置';
} else if (e.toString().contains('Failed host lookup')) {
} else if (errorString.contains('Failed host lookup')) {
errorMessage = '无法连接到服务器,请检查网络或稍后重试';
} else if (e.toString().contains('Connection refused')) {
} else if (errorString.contains('Connection refused')) {
errorMessage = '服务器拒绝连接,请稍后重试';
} else if (e.toString().contains('timeout')) {
} else if (errorString.contains('timeout')) {
errorMessage = '连接超时,请检查网络';
} else {
errorMessage = '检查更新失败: ${e.toString().split(':').first}';
errorMessage = '检查更新失败: ${errorString.split(':').first}';
}
if (context.mounted) {
_showToast(errorMessage);
//
if (e.toString().contains('Failed host lookup') || e.toString().contains('无网络连接')) {
if (errorString.contains('Failed host lookup') || errorString.contains('无网络连接')) {
debugPrint('建议: 请检查网络连接或尝试使用网络诊断功能');
}
}
@ -99,26 +100,6 @@ class AppUpgradeSimple {
}
}
/// Toast并尝试后台下载/
Future<void> _showToastAndDownloadInBackground({
required BuildContext context,
required UpgradeInfo info,
required bool autoDownload,
required bool autoInstall,
}) async {
if (info.isForceUpdate) {
_showToast('有重要更新,但无法显示对话框。请在 MaterialApp 环境内重试。');
} else {
_showToast('发现新版本: ${info.versionName}');
if (autoDownload && Platform.isAndroid && info.downloadUrl != null) {
final filePath = await _plugin.downloadApk(info.downloadUrl!, onProgress: (_) {});
if (filePath != null && autoInstall) {
await _installApkHeadless(context, filePath);
}
}
}
}
///
Future<UpgradeInfo?> checkUpdateSilent({
required String url,
@ -159,37 +140,6 @@ class AppUpgradeSimple {
);
}
/// MaterialApp环境不可用时
void _showFallbackUpgradeDialog({
required BuildContext context,
required UpgradeInfo info,
required bool autoDownload,
required bool autoInstall,
VoidCallback? onComplete,
}) {
showGeneralDialog(
context: context,
barrierDismissible: !info.isForceUpdate,
barrierLabel: 'Dismiss', // Provide a non-localized label
pageBuilder: (buildContext, animation, secondaryAnimation) {
// 使Material的对话框是必需的
return Directionality(
textDirection: TextDirection.ltr,
child: _FallbackUpgradeDialog(
info: info,
autoDownload: autoDownload,
autoInstall: autoInstall,
onComplete: onComplete,
showToast: (message) => _showToast(message),
),
);
},
).then((_) {
// The dialog has been dismissed.
onComplete?.call();
});
}
/// Toast提示
void _showToast(String message) {
Fluttertoast.showToast(msg: message);
@ -206,20 +156,6 @@ class AppUpgradeSimple {
return false;
}
}
/// (UI)
Future<void> _installApkHeadless(BuildContext context, String filePath) async {
if (!Platform.isAndroid) return;
final hasPermission = await PermissionHelper.checkAndRequestInstallPermission(context: context);
if (!hasPermission) {
_showToast('未授予安装权限,无法完成更新');
return;
}
final success = await _plugin.installApk(filePath);
if (!success) {
_showToast('安装失败,请手动安装');
}
}
}
///
@ -238,7 +174,7 @@ class _UpgradeDialogContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
final List<String> changeItems =
final changeItems =
info.updateContent.split(RegExp(r'\r?\n')).map((e) => e.trim()).where((e) => e.isNotEmpty).toList();
return Column(
@ -392,136 +328,6 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
}
}
Future<void> _showInstallMethodChooser() async {
final bool hasMarkets = info.appMarkets != null && info.appMarkets!.isNotEmpty;
final bool hasDownload = info.downloadUrl != null;
if (!mounted) return;
// market://
final choice = await showModalBottomSheet<String>(
context: context,
isScrollControlled: false,
useRootNavigator: true,
// UI Beautification: Rounded corners
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (ctx) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Draggable handle
Container(
width: 40,
height: 4,
margin: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(2),
),
),
// Title and Close Button
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Stack(
alignment: Alignment.center,
children: [
const Text('选择更新方式', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Positioned(
right: -12,
child: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(ctx).pop(),
),
),
],
),
),
// Option 1: App Market
ListTile(
leading: const Icon(Icons.storefront_outlined),
title: const Text('前往应用市场更新'),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onTap: () => Navigator.of(ctx).pop('market'),
),
// Option 2: Direct Download
if (hasDownload)
ListTile(
leading: const Icon(Icons.download_for_offline_outlined),
title: const Text('直接下载安装包'),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onTap: () => Navigator.of(ctx).pop('download'),
),
const Divider(height: 24),
// Cancel Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
backgroundColor: Colors.white,
foregroundColor: Theme.of(context).textTheme.bodyLarge?.color,
elevation: 2,
shadowColor: Colors.grey.withOpacity(0.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Colors.grey.shade300),
),
),
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('取消', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
),
),
],
),
),
);
},
);
if (choice == 'market') {
if (hasMarkets) {
await MarketSelectionDialog.show(
context,
markets: info.appMarkets!,
onSelected: (market) async {
if (market.url != null && market.url!.isNotEmpty) {
_plugin.goToAppStore(market.url!);
} else {
final appInfo = await _plugin.getAppInfo();
final pkg = appInfo['packageName'] ?? '';
if (pkg.isNotEmpty) {
_plugin.goToAppStore('market://details?id=$pkg');
}
}
},
);
} else {
final appInfo = await _plugin.getAppInfo();
final pkg = appInfo['packageName'] ?? '';
if (pkg.isNotEmpty) {
_plugin.goToAppStore('market://details?id=$pkg');
}
}
if (!mounted) return;
Navigator.of(context).pop();
onComplete?.call();
return;
}
if (choice == 'download' && hasDownload && !_isDownloading) {
await _startDownloadAndInstall();
}
}
void _handleAction() {
if (Platform.isAndroid) {
_handleAndroidAction();
@ -550,14 +356,10 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
Future<void> _handleAndroidAction() async {
// On Android, we always assume a market option is available,
// because we can fall back to a generic market:// intent.
const bool hasMarketOption = true;
final bool hasDownloadOption = info.downloadUrl != null;
// This case is unlikely on Android, but kept for robustness.
if (!hasMarketOption && !hasDownloadOption) {
showToast('No update method available.');
return;
}
// Android上退 market:// intent
//
// If a download option is not available, going to the market is the only choice.
if (!hasDownloadOption) {
@ -587,7 +389,6 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
final pkg = appInfo['packageName'] ?? '';
if (pkg.isNotEmpty) {
_plugin.goToAppStore('market://details?id=$pkg');
// _plugin.goToAppStore('market://details?id=$pkg');
} else {
showToast('Could not determine app package name.');
}
@ -596,8 +397,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
/// Pops the current dialog and then performs the market action.
Future<void> _handleMarketAction() async {
// Pop the upgrade dialog before proceeding.
if (Navigator.canPop(context)) {
//
if (!info.isForceUpdate && Navigator.canPop(context)) {
Navigator.of(context).pop();
}
if (!mounted) return;
@ -606,8 +407,6 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
}
Future<void> _showDownloadChoiceSheet() async {
final bool hasDownload = info.downloadUrl != null;
if (!mounted) return;
final choice = await showModalBottomSheet<String>(
@ -662,13 +461,12 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
),
// Option 2: Direct Download
if (hasDownload)
ListTile(
leading: const Icon(Icons.download_for_offline_outlined),
title: const Text('直接下载安装包'),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onTap: () => Navigator.of(ctx).pop('download'),
),
ListTile(
leading: const Icon(Icons.download_for_offline_outlined),
title: const Text('直接下载安装包'),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onTap: () => Navigator.of(ctx).pop('download'),
),
const Divider(height: 24),
@ -699,12 +497,17 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
);
if (choice == 'market') {
await _performMarketAction();
onComplete?.call();
// onComplete
if (!info.isForceUpdate) {
await _performMarketAction();
onComplete?.call();
} else {
await _performMarketAction();
}
return;
}
if (choice == 'download' && hasDownload && !_isDownloading) {
if (choice == 'download' && !_isDownloading) {
await _startDownloadAndInstall();
}
}
@ -744,8 +547,6 @@ class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _Upgrad
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
@ -756,7 +557,7 @@ class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _Upgrad
children: [
Icon(
Icons.system_update,
color: theme.colorScheme.primary,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
const Text('发现新版本'),
@ -772,14 +573,13 @@ class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _Upgrad
actions: _isDownloading
? []
: [
if (!widget.info.isForceUpdate)
TextButton(
onPressed: () {
Navigator.of(context).pop();
widget.onComplete?.call();
},
child: const Text('稍后更新'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
widget.onComplete?.call();
},
child: const Text('稍后更新'),
),
ElevatedButton(
onPressed: _handleAction,
child: Text(Platform.isAndroid ? '立即更新' : '前往更新'),
@ -799,78 +599,20 @@ class _ForceUpgradeDialog extends StatefulWidget {
State<_ForceUpgradeDialog> createState() => _ForceUpgradeDialogState();
}
class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> {
final _plugin = AppUpgradePlugin();
bool _isDownloading = false;
double _downloadProgress = 0;
String _statusText = '';
class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> with _UpgradeDialogLogic {
@override
void initState() {
super.initState();
//
}
Future<void> _startDownload() async {
// (Just-in-Time)
if (Platform.isAndroid) {
final hasStorage = await PermissionHelper.checkAndRequestStoragePermission(context: context);
if (!hasStorage) {
setState(() {
_statusText = '缺少存储权限,无法下载';
});
return;
}
//
await PermissionHelper.checkAndRequestNotificationPermission(context: context);
}
if (widget.info.downloadUrl == null) {
setState(() {
_statusText = '下载地址无效';
});
return;
}
setState(() {
_isDownloading = true;
_statusText = '准备下载...';
});
final filePath = await _plugin.downloadApk(
widget.info.downloadUrl!,
onProgress: (p) {
if (!mounted) return;
setState(() {
_downloadProgress = p.progress;
_statusText = '下载中 ${p.percentage}%';
});
},
);
if (filePath != null) {
await _installApk(context, filePath);
}
}
// downloadApk
Future<void> _installApk(BuildContext context, String filePath) async {
final hasPermission = await PermissionHelper.checkAndRequestInstallPermission(context: context);
if (!hasPermission) {
if (context.mounted) {
setState(() {
_statusText = '未授予安装权限,请手动授权后重试';
});
}
return;
}
await _plugin.installApk(filePath);
}
UpgradeInfo get info => widget.info;
@override
void Function(String) get showToast => (message) => AppUpgradeSimple.instance._showToast(message);
@override
VoidCallback? get onComplete => null;
@override
bool get autoDownload => false;
@override
bool get autoInstall => true;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return WillPopScope(
onWillPop: () async => false, //
child: AlertDialog(
@ -883,7 +625,7 @@ class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> {
children: [
Icon(
Icons.system_update,
color: theme.colorScheme.primary,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
const Text('发现新版本 (强制)'),
@ -900,253 +642,11 @@ class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> {
? []
: [
ElevatedButton(
onPressed: () async {
//
final hasMarkets = widget.info.appMarkets != null && widget.info.appMarkets!.isNotEmpty;
final hasDownload = widget.info.downloadUrl != null;
if (hasMarkets && hasDownload) {
final choice = await showModalBottomSheet<String>(
context: context,
isScrollControlled: false,
useRootNavigator: true,
// UI Beautification: Rounded corners
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (ctx) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Draggable handle
Container(
width: 40,
height: 4,
margin: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(2),
),
),
// Title and Close Button
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Stack(
alignment: Alignment.center,
children: [
const Text('选择更新方式',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Positioned(
right: -12,
child: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(ctx).pop(),
),
),
],
),
),
// Option 1: App Market
ListTile(
leading: const Icon(Icons.storefront_outlined),
title: const Text('前往应用市场更新'),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onTap: () => Navigator.of(ctx).pop('market'),
),
// Option 2: Direct Download
if (hasDownload)
ListTile(
leading: const Icon(Icons.download_for_offline_outlined),
title: const Text('直接下载安装包'),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onTap: () => Navigator.of(ctx).pop('download'),
),
const Divider(height: 24),
// Cancel Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
backgroundColor: Colors.white,
foregroundColor: Theme.of(context).textTheme.bodyLarge?.color,
elevation: 2,
shadowColor: Colors.grey.withOpacity(0.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Colors.grey.shade300),
),
),
onPressed: () => Navigator.of(ctx).pop(),
child:
const Text('取消', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
),
),
],
),
),
);
},
);
if (choice == 'market') {
Navigator.of(context).pop();
MarketSelectionDialog.show(
context,
markets: widget.info.appMarkets!,
onSelected: (market) {
_plugin.goToAppStore(market.url ?? market.packageName ?? '');
},
);
return;
}
if (choice == 'download') {
await _startDownload();
return;
}
return;
}
if (hasMarkets) {
Navigator.of(context).pop();
MarketSelectionDialog.show(
context,
markets: widget.info.appMarkets!,
onSelected: (market) {
_plugin.goToAppStore(market.url ?? market.packageName ?? '');
},
);
return;
}
if (hasDownload) {
await _startDownload();
return;
}
},
child: const Text('立即更新'),
onPressed: _handleAction,
child: Text(Platform.isAndroid ? '立即更新' : '前往更新'),
),
],
),
);
}
}
/// [MaterialApp] 退
class _FallbackUpgradeDialog extends StatefulWidget {
final UpgradeInfo info;
final bool autoDownload;
final bool autoInstall;
final VoidCallback? onComplete;
final void Function(String) showToast;
const _FallbackUpgradeDialog({
required this.info,
required this.autoDownload,
required this.autoInstall,
this.onComplete,
required this.showToast,
});
@override
State<_FallbackUpgradeDialog> createState() => _FallbackUpgradeDialogState();
}
class _FallbackUpgradeDialogState extends State<_FallbackUpgradeDialog> with _UpgradeDialogLogic {
@override
UpgradeInfo get info => widget.info;
@override
void Function(String) get showToast => widget.showToast;
@override
VoidCallback? get onComplete => widget.onComplete;
@override
bool get autoDownload => widget.autoDownload;
@override
bool get autoInstall => widget.autoInstall;
@override
Widget build(BuildContext context) {
final title = '发现新版本${widget.info.isForceUpdate ? " (强制)" : ""}';
final body = Center(
child: Material(
type: MaterialType.transparency,
child: Container(
margin: const EdgeInsets.all(24.0),
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.0),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
DefaultTextStyle(
style: const TextStyle(color: Colors.black87, fontSize: 14),
child: _UpgradeDialogContent(
info: widget.info,
isDownloading: _isDownloading,
downloadProgress: _downloadProgress,
statusText: _statusText,
),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (!widget.info.isForceUpdate && !_isDownloading) ...[
GestureDetector(
onTap: () {
Navigator.of(context).pop();
widget.onComplete?.call();
},
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Text('稍后更新', style: TextStyle(color: Colors.blue)),
),
),
const SizedBox(width: 8),
],
GestureDetector(
onTap: _isDownloading ? null : _handleAction,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: _isDownloading ? Colors.grey : Colors.blue,
borderRadius: BorderRadius.circular(8),
),
child: Text(
_isDownloading
? '下载中...'
: Platform.isAndroid
? '立即更新'
: '前往更新',
style: const TextStyle(color: Colors.white),
),
),
),
],
),
],
),
),
),
);
return widget.info.isForceUpdate ? WillPopScope(onWillPop: () async => false, child: body) : body;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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,
}

View File

@ -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();
}
}

View File

@ -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;
});
}
}
}

View File

@ -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));
}
}

View File

@ -1,2 +1,3 @@
export 'download_progress_dialog.dart';
export 'upgrade_dialog.dart';
// app_upgrade_simple.dart
//
export 'market_selection_dialog.dart';