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 (_) {} | |||
|  |   } | |||
|  | } |