You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
214 lines
6.9 KiB
214 lines
6.9 KiB
3 weeks ago
|
import 'dart:io';
|
||
|
import 'package:dio/dio.dart';
|
||
|
import 'package:flutter/material.dart';
|
||
|
import 'package:get/get.dart';
|
||
|
import 'package:path_provider/path_provider.dart';
|
||
|
import 'package:open_file/open_file.dart';
|
||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||
|
import 'package:permission_handler/permission_handler.dart';
|
||
|
|
||
|
class UpgraderService extends GetxService {
|
||
|
final Dio _dio = Dio();
|
||
|
final String _versionUrl =
|
||
|
'http://xhota.anxincloud.cn/problem/version.json'; // 您的版本信息 JSON 地址
|
||
|
|
||
|
var isUpdateAvailable = false.obs;
|
||
|
var updateInfo = {}.obs;
|
||
|
var downloadProgress = '0'.obs;
|
||
|
var totalSize = '0'.obs;
|
||
|
var isDownloading = false.obs;
|
||
|
|
||
|
@override
|
||
|
void onInit() {
|
||
|
super.onInit();
|
||
|
checkForUpdate();
|
||
|
}
|
||
|
|
||
|
// 检查更新
|
||
|
Future<void> checkForUpdate() async {
|
||
|
try {
|
||
|
// 获取当前应用版本信息
|
||
|
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||
|
String currentVersion = packageInfo.version;
|
||
|
|
||
|
final response = await _dio.get(_versionUrl);
|
||
|
if (response.statusCode == 200) {
|
||
|
final latestVersion = response.data['version'];
|
||
|
if (latestVersion.compareTo(currentVersion) > 0) {
|
||
|
isUpdateAvailable.value = true;
|
||
|
updateInfo.value = response.data;
|
||
|
showUpdateDialog();
|
||
|
}
|
||
|
}
|
||
|
} catch (e) {
|
||
|
Get.log('检查更新失败: $e');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 显示更新提示对话框
|
||
|
void showUpdateDialog() {
|
||
|
Get.dialog(
|
||
|
AlertDialog(
|
||
|
title: Text('发现新版本'),
|
||
|
content: Text(updateInfo['description'] ?? '有新的更新可用。'),
|
||
|
actions: [
|
||
|
TextButton(onPressed: () => Get.back(), child: Text('稍后')),
|
||
|
TextButton(
|
||
|
onPressed: () {
|
||
|
Get.back();
|
||
|
startDownload();
|
||
|
},
|
||
|
child: Text('立即更新'),
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
barrierDismissible: false,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// 开始下载
|
||
|
Future<void> startDownload() async {
|
||
|
isDownloading.value = true;
|
||
|
showDownloadProgressDialog();
|
||
|
|
||
|
try {
|
||
|
Directory? dir = await getExternalStorageDirectory();
|
||
|
|
||
|
if (dir == null) {
|
||
|
Get.back(); // 关闭进度对话框
|
||
|
Get.snackbar('错误', '无法获取外部存储目录。');
|
||
|
print('错误: getExternalStorageDirectory() 返回 null');
|
||
|
isDownloading.value = false;
|
||
|
return; // 提前退出
|
||
|
}
|
||
|
|
||
|
String savePath = '${dir.path}/app-release.apk';
|
||
|
print('--- 调试信息 ---');
|
||
|
print('目标下载路径: $savePath');
|
||
|
print('--- 调试信息 ---');
|
||
|
|
||
|
// 确保父目录存在
|
||
|
final file = File(savePath);
|
||
|
if (!await file.parent.exists()) {
|
||
|
await file.parent.create(recursive: true);
|
||
|
print('--- 调试信息 ---');
|
||
|
print('已尝试创建父目录: ${file.parent.path}');
|
||
|
print('--- 调试信息 ---');
|
||
|
}
|
||
|
|
||
|
await _dio.download(
|
||
|
updateInfo['url'],
|
||
|
savePath,
|
||
|
onReceiveProgress: (received, total) {
|
||
|
if (total != -1) {
|
||
|
downloadProgress.value = (received / 1024 / 1024).toStringAsFixed(
|
||
|
2,
|
||
|
);
|
||
|
totalSize.value = (total / 1024 / 1024).toStringAsFixed(2);
|
||
|
// 在进度回调中也打印路径,确保一直有效
|
||
|
// print('下载进度 - 目标路径: $savePath');
|
||
|
}
|
||
|
},
|
||
|
options: Options(
|
||
|
receiveTimeout: const Duration(seconds: 30), // 增加超时时间
|
||
|
sendTimeout: const Duration(seconds: 30),
|
||
|
),
|
||
|
);
|
||
|
|
||
|
isDownloading.value = false;
|
||
|
Get.back(); // 关闭下载进度对话框
|
||
|
|
||
|
// 再次验证文件是否存在
|
||
|
if (await file.exists()) {
|
||
|
final fileSize = await file.length();
|
||
|
Get.log('文件验证成功!路径: $savePath');
|
||
|
Get.log('文件大小: ${(fileSize / 1024 / 1024).toStringAsFixed(2)} MB');
|
||
|
installApk(savePath);
|
||
|
} else {
|
||
|
Get.snackbar('下载失败', '文件下载完毕但未找到,请联系管理员。');
|
||
|
Get.log('错误:文件下载完毕但未找到!');
|
||
|
}
|
||
|
} on DioException catch (e) {
|
||
|
// 使用 DioException 捕获更具体的 Dio 错误
|
||
|
isDownloading.value = false;
|
||
|
Get.back();
|
||
|
String errorMessage = '下载失败: 网络或服务器问题。';
|
||
|
if (e.type == DioExceptionType.badResponse) {
|
||
|
errorMessage = '下载失败: 服务器响应错误 (${e.response?.statusCode})';
|
||
|
Get.log('Dio响应错误: ${e.response?.data}');
|
||
|
} else if (e.type == DioExceptionType.connectionError) {
|
||
|
errorMessage = '下载失败: 连接错误,请检查网络。';
|
||
|
} else if (e.type == DioExceptionType.unknown) {
|
||
|
// 可能是文件系统错误、权限错误等
|
||
|
errorMessage = '下载失败: 未知错误。请检查存储权限或设备空间。';
|
||
|
Get.log('Dio未知错误: $e');
|
||
|
}
|
||
|
Get.snackbar('下载失败', errorMessage);
|
||
|
Get.log('下载失败 (DioException): $e');
|
||
|
} catch (e) {
|
||
|
isDownloading.value = false;
|
||
|
Get.back();
|
||
|
Get.snackbar('下载失败', '发生未知错误。');
|
||
|
Get.log('下载失败 (通用异常): $e');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 显示下载进度对话框
|
||
|
void showDownloadProgressDialog() {
|
||
|
Get.dialog(
|
||
|
AlertDialog(
|
||
|
title: Text('正在下载更新...'),
|
||
|
content: Obx(
|
||
|
() => Column(
|
||
|
mainAxisSize: MainAxisSize.min,
|
||
|
children: [
|
||
|
LinearProgressIndicator(
|
||
|
value: totalSize.value == '0'
|
||
|
? null
|
||
|
: double.parse(downloadProgress.value) /
|
||
|
double.parse(totalSize.value),
|
||
|
),
|
||
|
SizedBox(height: 16),
|
||
|
Text('${downloadProgress.value} MB / ${totalSize.value} MB'),
|
||
|
],
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
barrierDismissible: false,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// 安装 APK
|
||
|
Future<void> installApk(String path) async {
|
||
|
// 检查安装权限
|
||
|
var status = await Permission.requestInstallPackages.status;
|
||
|
if (status.isGranted) {
|
||
|
// 权限已授予,直接安装
|
||
|
_openApkFile(path);
|
||
|
} else {
|
||
|
// 权限未授予,发起请求
|
||
|
// 这会打开系统设置页面,让用户手动授权
|
||
|
status = await Permission.requestInstallPackages.request();
|
||
|
if (status.isGranted) {
|
||
|
_openApkFile(path);
|
||
|
} else {
|
||
|
// 如果用户拒绝了权限,给出提示
|
||
|
Get.snackbar(
|
||
|
'权限被拒绝',
|
||
|
'需要授权才能安装更新。请在系统设置中为本应用开启“安装未知应用”的权限。',
|
||
|
duration: Duration(seconds: 5),
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 抽离出打开文件的逻辑
|
||
|
Future<void> _openApkFile(String path) async {
|
||
|
final result = await OpenFile.open(path);
|
||
|
print('OpenFile result: ${result.type}, message: ${result.message}');
|
||
|
if (result.type != ResultType.done) {
|
||
|
Get.snackbar('安装失败', '无法启动安装程序: ${result.message}');
|
||
|
}
|
||
|
}
|
||
|
}
|