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