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