flutter_integrated_whb/lib/services/update_service.dart

169 lines
5.5 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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