169 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Dart
		
	
	
			
		
		
	
	
			169 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Dart
		
	
	
| // update_service.dart
 | ||
| import 'dart:async';
 | ||
| import 'dart:io';
 | ||
| 
 | ||
| import 'package:dio/dio.dart';
 | ||
| import 'package:flutter/foundation.dart';
 | ||
| import 'package:flutter/services.dart';
 | ||
| import 'package:path_provider/path_provider.dart';
 | ||
| 
 | ||
| /// 安装 MethodChannel 名称(与原生保持一致)
 | ||
| const String _kInstallChannel = 'app.install';
 | ||
| 
 | ||
| /// 更新事件类型
 | ||
| enum UpdateState { idle, starting, downloading, completed, installing, installed, failed, canceled }
 | ||
| 
 | ||
| class UpdateEvent {
 | ||
|   final UpdateState state;
 | ||
|   final String? message; // 可选的错误/提示消息
 | ||
| 
 | ||
|   UpdateEvent(this.state, {this.message});
 | ||
| }
 | ||
| 
 | ||
| /// 单例服务类:管理下载、进度与安装
 | ||
| class UpdateService {
 | ||
|   UpdateService._internal();
 | ||
| 
 | ||
|   static final UpdateService _instance = UpdateService._internal();
 | ||
| 
 | ||
|   factory UpdateService() => _instance;
 | ||
| 
 | ||
|   // 进度通知器(0.0 ~ 1.0)
 | ||
|   final ValueNotifier<double> progress = ValueNotifier<double>(0.0);
 | ||
| 
 | ||
|   // 状态流控制器(广播)
 | ||
|   final StreamController<UpdateEvent> _statusController =
 | ||
|   StreamController<UpdateEvent>.broadcast();
 | ||
| 
 | ||
|   Stream<UpdateEvent> get statusStream => _statusController.stream;
 | ||
| 
 | ||
|   // Dio cancel token 用于取消下载
 | ||
|   CancelToken? _cancelToken;
 | ||
| 
 | ||
|   final MethodChannel _channel = const MethodChannel(_kInstallChannel);
 | ||
| 
 | ||
|   /// 启动下载并在下载完成后尝试安装
 | ||
|   /// apkUrl: 下载地址
 | ||
|   /// apkFileName: 保存的文件名(可选)
 | ||
|   Future<void> downloadAndInstall({
 | ||
|     required String apkUrl,
 | ||
|     String? apkFileName,
 | ||
|   }) async {
 | ||
|     // 防止重复下载
 | ||
|     if (_cancelToken != null && !_cancelToken!.isCancelled) {
 | ||
|       _statusController.add(UpdateEvent(UpdateState.failed, message: '已有下载进行中'));
 | ||
|       return;
 | ||
|     }
 | ||
| 
 | ||
|     _statusController.add(UpdateEvent(UpdateState.starting));
 | ||
|     progress.value = 0.0;
 | ||
| 
 | ||
|     _cancelToken = CancelToken();
 | ||
| 
 | ||
|     // 准备保存路径(app 专属外部目录)
 | ||
|     String savePath;
 | ||
|     try {
 | ||
|       final extDir = await getExternalStorageDirectory();
 | ||
|       if (extDir == null) {
 | ||
|         _statusController.add(UpdateEvent(UpdateState.failed, message: '无法获取存储目录'));
 | ||
|         return;
 | ||
|       }
 | ||
|       apkFileName ??= 'app_update_${DateTime.now().millisecondsSinceEpoch}.apk';
 | ||
|       savePath = '${extDir.path}/$apkFileName';
 | ||
|     } catch (e) {
 | ||
|       _statusController.add(UpdateEvent(UpdateState.failed, message: '获取存储路径失败: $e'));
 | ||
|       return;
 | ||
|     }
 | ||
| 
 | ||
|     final dio = Dio();
 | ||
| 
 | ||
|     try {
 | ||
|       _statusController.add(UpdateEvent(UpdateState.downloading));
 | ||
|       await dio.download(
 | ||
|         apkUrl,
 | ||
|         savePath,
 | ||
|         cancelToken: _cancelToken,
 | ||
|         onReceiveProgress: (received, total) {
 | ||
|           if (total > 0) {
 | ||
|             final p = received / total;
 | ||
|             progress.value = p;
 | ||
|           }
 | ||
|         },
 | ||
|         options: Options(
 | ||
|           responseType: ResponseType.stream,
 | ||
|           followRedirects: true,
 | ||
|           // 不设置超时(可能大文件下载)
 | ||
|           receiveTimeout: Duration(seconds: 0),
 | ||
|           headers: {"Accept": "application/vnd.android.package-archive"},
 | ||
|         ),
 | ||
|       );
 | ||
| 
 | ||
|       // 下载完成
 | ||
|       progress.value = 1.0;
 | ||
|       _statusController.add(UpdateEvent(UpdateState.completed));
 | ||
| 
 | ||
|       // 延迟让 UI 显示 100%
 | ||
|       await Future.delayed(const Duration(milliseconds: 200));
 | ||
| 
 | ||
|       // 调用原生安装
 | ||
|       _statusController.add(UpdateEvent(UpdateState.installing));
 | ||
|       try {
 | ||
|         final Map<String, dynamic> args = {'path': savePath};
 | ||
|         await _channel.invokeMethod('installApk', args);
 | ||
|         // 成功发起安装(系统会弹出安装界面),我们不能保证用户安装成功,但这里当做已触发安装
 | ||
|         _statusController.add(UpdateEvent(UpdateState.installed));
 | ||
|       } on PlatformException catch (e) {
 | ||
|         _statusController.add(UpdateEvent(UpdateState.failed, message: '安装失败: ${e.message}'));
 | ||
|       } catch (e) {
 | ||
|         _statusController.add(UpdateEvent(UpdateState.failed, message: '安装异常: $e'));
 | ||
|       }
 | ||
|     } on DioError catch (e) {
 | ||
|       if (CancelToken.isCancel(e)) {
 | ||
|         progress.value = 0.0;
 | ||
|         _statusController.add(UpdateEvent(UpdateState.canceled));
 | ||
|       } else {
 | ||
|         progress.value = 0.0;
 | ||
|         _statusController.add(UpdateEvent(UpdateState.failed, message: e.message));
 | ||
|       }
 | ||
|     } catch (e) {
 | ||
|       progress.value = 0.0;
 | ||
|       _statusController.add(UpdateEvent(UpdateState.failed, message: e.toString()));
 | ||
|     } finally {
 | ||
|       // 清理 token
 | ||
|       _cancelToken = null;
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   /// 取消当前下载(若有)
 | ||
|   void cancelDownload() {
 | ||
|     if (_cancelToken != null && !_cancelToken!.isCancelled) {
 | ||
|       _cancelToken!.cancel();
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   /// 仅触发安装(如果你已经手动下载好了文件),传入本地 apk 路径
 | ||
|   Future<void> installApk(String apkPath) async {
 | ||
|     try {
 | ||
|       _statusController.add(UpdateEvent(UpdateState.installing));
 | ||
|       final Map<String, dynamic> args = {'path': apkPath};
 | ||
|       await _channel.invokeMethod('installApk', args);
 | ||
|       _statusController.add(UpdateEvent(UpdateState.installed));
 | ||
|     } on PlatformException catch (e) {
 | ||
|       _statusController.add(UpdateEvent(UpdateState.failed, message: '安装失败: ${e.message}'));
 | ||
|     } catch (e) {
 | ||
|       _statusController.add(UpdateEvent(UpdateState.failed, message: '安装异常: $e'));
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   /// 释放资源(页面销毁或 app 退出时调用)
 | ||
|   Future<void> dispose() async {
 | ||
|     try {
 | ||
|       _statusController.add(UpdateEvent(UpdateState.idle));
 | ||
|       await _statusController.close();
 | ||
|     } catch (_) {}
 | ||
|     try {
 | ||
|       progress.dispose();
 | ||
|     } catch (_) {}
 | ||
|   }
 | ||
| }
 |