// 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 progress = ValueNotifier(0.0); // 状态流控制器(广播) final StreamController _statusController = StreamController.broadcast(); Stream get statusStream => _statusController.stream; // Dio cancel token 用于取消下载 CancelToken? _cancelToken; final MethodChannel _channel = const MethodChannel(_kInstallChannel); /// 启动下载并在下载完成后尝试安装 /// apkUrl: 下载地址 /// apkFileName: 保存的文件名(可选) Future 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 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 installApk(String apkPath) async { try { _statusController.add(UpdateEvent(UpdateState.installing)); final Map 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 dispose() async { try { _statusController.add(UpdateEvent(UpdateState.idle)); await _statusController.close(); } catch (_) {} try { progress.dispose(); } catch (_) {} } }