894 lines
30 KiB
Dart
894 lines
30 KiB
Dart
import 'dart:convert';
|
||
import 'dart:io';
|
||
|
||
import 'package:dio/dio.dart';
|
||
import 'package:dio/io.dart';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:fluttertoast/fluttertoast.dart';
|
||
import 'package:package_info_plus/package_info_plus.dart';
|
||
import 'package:path_provider/path_provider.dart';
|
||
import 'package:permission_handler/permission_handler.dart';
|
||
import 'package:url_launcher/url_launcher.dart';
|
||
|
||
import 'app_upgrade_plugin_platform_interface.dart';
|
||
import 'core/http_config.dart';
|
||
import 'core/permission_helper.dart';
|
||
import 'models/install_strategy.dart';
|
||
import 'models/upgrade_info.dart';
|
||
|
||
/// An implementation of [AppUpgradePluginPlatform] that uses method channels.
|
||
class MethodChannelAppUpgradePlugin extends AppUpgradePluginPlatform {
|
||
/// The method channel used to interact with the native platform.
|
||
@visibleForTesting
|
||
final methodChannel = const MethodChannel('app_upgrade_plugin');
|
||
|
||
late Dio _dio;
|
||
HttpConfig _httpConfig = const HttpConfig();
|
||
|
||
MethodChannelAppUpgradePlugin() {
|
||
// 默认配置:根据环境自动决定
|
||
if (!const bool.fromEnvironment('dart.vm.product')) {
|
||
// 开发环境:自动使用开发配置(绕过证书)
|
||
_httpConfig = HttpConfig.development;
|
||
} else {
|
||
// 生产环境:使用生产配置(严格验证)
|
||
_httpConfig = HttpConfig.production;
|
||
}
|
||
|
||
// 初始化Dio
|
||
_initializeDio(_httpConfig);
|
||
}
|
||
|
||
/// 初始化Dio实例
|
||
void _initializeDio(HttpConfig config) {
|
||
_httpConfig = config;
|
||
|
||
// 配置 Dio 实例
|
||
_dio = Dio(BaseOptions(
|
||
connectTimeout: Duration(seconds: config.connectTimeout),
|
||
receiveTimeout: Duration(seconds: config.receiveTimeout),
|
||
sendTimeout: Duration(seconds: config.connectTimeout),
|
||
headers: {
|
||
'User-Agent': 'AppUpgradePlugin/1.0',
|
||
'Accept': 'application/json',
|
||
'Content-Type': 'application/json',
|
||
...?config.headers,
|
||
},
|
||
// 允许所有状态码,我们自己处理
|
||
validateStatus: (status) => true,
|
||
));
|
||
|
||
// 配置HTTPS支持
|
||
(_dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
|
||
final client = HttpClient();
|
||
if (config.ignoreCertificate) {
|
||
client.badCertificateCallback = (cert, host, port) => true;
|
||
}
|
||
return client;
|
||
};
|
||
|
||
// 清除旧的拦截器
|
||
_dio.interceptors.clear();
|
||
|
||
// 添加拦截器
|
||
if (config.enableLog) {
|
||
_dio.interceptors.add(InterceptorsWrapper(
|
||
onRequest: (options, handler) {
|
||
debugPrint('==== HTTP请求 ====');
|
||
debugPrint('${options.method} ${options.uri}');
|
||
debugPrint('请求头: ${options.headers}');
|
||
if (options.data != null) {
|
||
debugPrint('请求体: ${options.data}');
|
||
}
|
||
handler.next(options);
|
||
},
|
||
onResponse: (response, handler) {
|
||
debugPrint('==== HTTP响应 ====');
|
||
debugPrint('状态码: ${response.statusCode}');
|
||
debugPrint('URL: ${response.requestOptions.uri}');
|
||
handler.next(response);
|
||
},
|
||
onError: (DioException e, handler) {
|
||
_logDioError(e);
|
||
handler.next(e);
|
||
},
|
||
));
|
||
}
|
||
}
|
||
|
||
@override
|
||
void configureHttp(HttpConfig config) {
|
||
_initializeDio(config);
|
||
}
|
||
|
||
@override
|
||
Future<String?> getPlatformVersion() async {
|
||
final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
|
||
return version;
|
||
}
|
||
|
||
@override
|
||
Future<UpgradeInfo?> checkUpdate(String url, {Map<String, dynamic>? params}) async {
|
||
try {
|
||
final packageInfo = await PackageInfo.fromPlatform();
|
||
final currentVersionName = packageInfo.version;
|
||
final currentBuildNumber = int.parse(packageInfo.buildNumber);
|
||
|
||
// 准备请求参数
|
||
final requestParams = {
|
||
'version': currentVersionName,
|
||
'buildNumber': currentBuildNumber,
|
||
'platform': Platform.isAndroid ? 'android' : 'ios',
|
||
...?params,
|
||
};
|
||
|
||
debugPrint('==== 检查更新 ====');
|
||
debugPrint('当前版本: $currentVersionName (Build: $currentBuildNumber)');
|
||
debugPrint('请求URL: $url');
|
||
|
||
Response? response;
|
||
|
||
// 根据配置或参数决定请求方式
|
||
final usePost = _httpConfig.defaultMethod.toUpperCase() == 'POST' ||
|
||
(params != null && params.containsKey('_method') && params['_method'] == 'POST');
|
||
|
||
if (usePost) {
|
||
// POST请求
|
||
debugPrint('使用POST请求');
|
||
response = await _dio.post(
|
||
url,
|
||
data: requestParams,
|
||
);
|
||
} else {
|
||
// GET请求
|
||
debugPrint('使用GET请求');
|
||
response = await _dio.get(
|
||
url,
|
||
queryParameters: requestParams,
|
||
);
|
||
}
|
||
|
||
debugPrint('响应状态: ${response.statusCode}');
|
||
|
||
if (response.statusCode == 200) {
|
||
// 处理响应数据
|
||
dynamic responseData = response.data;
|
||
|
||
// 如果响应是字符串,尝试解析为JSON
|
||
if (responseData is String) {
|
||
if (responseData.isEmpty) {
|
||
throw Exception('服务器返回空内容');
|
||
}
|
||
try {
|
||
responseData = json.decode(responseData);
|
||
} catch (_) {
|
||
throw Exception('响应数据格式错误');
|
||
}
|
||
}
|
||
|
||
debugPrint('响应数据: $responseData');
|
||
|
||
// 解析更新信息
|
||
final upgradeInfo = UpgradeInfo.fromJson(
|
||
responseData,
|
||
currentBuildNumber: currentBuildNumber,
|
||
currentVersionName: currentVersionName,
|
||
);
|
||
|
||
// 比较版本
|
||
if (upgradeInfo.hasUpdate) {
|
||
debugPrint('✅ 发现新版本: ${upgradeInfo.versionName}');
|
||
return upgradeInfo;
|
||
} else {
|
||
debugPrint('✅ 当前已是最新版本');
|
||
return null;
|
||
}
|
||
} else {
|
||
throw Exception('服务器响应错误: ${response.statusCode}');
|
||
}
|
||
} on DioException catch (e) {
|
||
if (e.type == DioExceptionType.connectionError && e.error is SocketException) {
|
||
final socketError = e.error as SocketException;
|
||
if (socketError.message.contains('Failed host lookup')) {
|
||
debugPrint('❌ DNS解析失败,请检查网络连接或域名配置');
|
||
}
|
||
}
|
||
_handleNetworkError(e);
|
||
|
||
return null;
|
||
} catch (e) {
|
||
debugPrint('❌ 检查更新失败: $e');
|
||
return null;
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<int?> getAndroidSdkVersion() async {
|
||
if (!Platform.isAndroid) return null;
|
||
return await methodChannel.invokeMethod<int>('getAndroidSdkVersion');
|
||
}
|
||
|
||
@override
|
||
Future<bool> openInstallPermissionSettings() async {
|
||
if (!Platform.isAndroid) return false;
|
||
|
||
try {
|
||
final result = await methodChannel.invokeMethod<bool>('openInstallPermissionSettings');
|
||
return result ?? false;
|
||
} catch (e) {
|
||
debugPrint('Failed to open install permission settings: $e');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<Map<String, dynamic>?> getDeviceInfo() async {
|
||
if (!Platform.isAndroid) return null;
|
||
|
||
try {
|
||
final result = await methodChannel.invokeMethod<Map<dynamic, dynamic>>('getDeviceInfo');
|
||
return result?.cast<String, dynamic>();
|
||
} catch (e) {
|
||
debugPrint('Failed to get device info: $e');
|
||
return null;
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<Map<String, String>> getAppInfo() async {
|
||
final packageInfo = await PackageInfo.fromPlatform();
|
||
return {
|
||
'appName': packageInfo.appName,
|
||
'packageName': packageInfo.packageName,
|
||
'version': packageInfo.version,
|
||
'buildNumber': packageInfo.buildNumber,
|
||
};
|
||
}
|
||
|
||
@override
|
||
Future<String?> downloadApk(String url, {Function(DownloadProgress)? onProgress, String? savePath}) async {
|
||
if (!Platform.isAndroid) {
|
||
throw PlatformException(code: 'PLATFORM_NOT_SUPPORTED', message: 'downloadApk only supports Android platform');
|
||
}
|
||
|
||
debugPrint('开始下载APK: $url');
|
||
|
||
// 先测试URL连接性
|
||
final errorMessage = await testDownloadUrlWithError(url);
|
||
if (errorMessage != null) {
|
||
debugPrint('错误: $errorMessage');
|
||
throw Exception(errorMessage);
|
||
}
|
||
|
||
try {
|
||
// 获取保存路径
|
||
savePath ??= await getDownloadPath();
|
||
if (savePath == null) {
|
||
debugPrint('错误: 无法获取下载路径');
|
||
throw Exception('无法获取下载路径');
|
||
}
|
||
debugPrint('下载路径: $savePath');
|
||
|
||
// 创建文件名
|
||
final fileName = 'app_${DateTime.now().millisecondsSinceEpoch}.apk';
|
||
final filePath = '$savePath/$fileName';
|
||
debugPrint('文件将保存到: $filePath');
|
||
|
||
// 配置下载选项
|
||
final options = Options(
|
||
responseType: ResponseType.bytes,
|
||
followRedirects: true,
|
||
validateStatus: (status) {
|
||
return status != null && status < 500;
|
||
},
|
||
headers: {
|
||
'Accept': '*/*',
|
||
'Accept-Encoding': 'gzip, deflate, br',
|
||
'Connection': 'keep-alive',
|
||
},
|
||
);
|
||
|
||
// 下载文件
|
||
final response = await _dio.download(
|
||
url,
|
||
filePath,
|
||
onReceiveProgress: (received, total) {
|
||
if (total != -1) {
|
||
final progress = (received / total * 100).toStringAsFixed(0);
|
||
debugPrint('下载进度: $progress% ($received/$total)');
|
||
}
|
||
if (onProgress != null) {
|
||
onProgress(DownloadProgress(received: received, total: total));
|
||
}
|
||
},
|
||
options: options,
|
||
deleteOnError: true,
|
||
);
|
||
|
||
debugPrint('下载完成,响应状态码: ${response.statusCode}');
|
||
|
||
// 检查文件是否存在
|
||
final file = File(filePath);
|
||
if (await file.exists()) {
|
||
final fileSize = await file.length();
|
||
debugPrint('文件保存成功,大小: $fileSize 字节');
|
||
return filePath;
|
||
} else {
|
||
debugPrint('错误: 文件保存失败');
|
||
return null;
|
||
}
|
||
} on DioException catch (e) {
|
||
debugPrint('Dio下载异常:');
|
||
debugPrint(' 类型: ${e.type}');
|
||
debugPrint(' 消息: ${e.message}');
|
||
debugPrint(' 错误: ${e.error}');
|
||
if (e.response != null) {
|
||
debugPrint(' 响应状态码: ${e.response?.statusCode}');
|
||
debugPrint(' 响应数据: ${e.response?.data}');
|
||
}
|
||
return null;
|
||
} catch (e, stackTrace) {
|
||
debugPrint('下载APK失败: $e');
|
||
debugPrint('堆栈跟踪: $stackTrace');
|
||
return null;
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<bool> installApk(String filePath) async {
|
||
if (!Platform.isAndroid) {
|
||
debugPrint('installApk: 非Android平台');
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
debugPrint('开始安装APK: $filePath');
|
||
|
||
// Check if file exists
|
||
final file = File(filePath);
|
||
if (!await file.exists()) {
|
||
debugPrint('安装失败: APK文件不存在 - $filePath');
|
||
return false;
|
||
}
|
||
|
||
final result = await methodChannel.invokeMethod<bool>('installApk', {'filePath': filePath});
|
||
debugPrint('安装APK结果: $result');
|
||
return result ?? false;
|
||
} catch (e) {
|
||
debugPrint('安装APK异常: $e');
|
||
|
||
// Provide more specific error messages
|
||
if (e.toString().contains('PERMISSION_DENIED')) {
|
||
debugPrint('安装失败: 缺少安装未知应用权限');
|
||
} else if (e.toString().contains('FILE_NOT_FOUND')) {
|
||
debugPrint('安装失败: APK文件未找到');
|
||
} else if (e.toString().contains('FILEPROVIDER_ERROR')) {
|
||
debugPrint('安装失败: FileProvider配置错误');
|
||
} else if (e.toString().contains('INTENT_ERROR')) {
|
||
debugPrint('安装失败: 无法启动安装程序');
|
||
}
|
||
|
||
return false;
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<bool> installApkWithSystemFlow(String filePath) async {
|
||
if (!Platform.isAndroid) {
|
||
debugPrint('installApkWithSystemFlow: 非Android平台');
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
debugPrint('开始使用系统流程安装APK: $filePath');
|
||
|
||
// Check if file exists
|
||
final file = File(filePath);
|
||
if (!await file.exists()) {
|
||
debugPrint('安装失败: APK文件不存在 - $filePath');
|
||
return false;
|
||
}
|
||
|
||
final result = await methodChannel.invokeMethod<bool>('installApkWithSystemFlow', {'filePath': filePath});
|
||
debugPrint('系统流程安装APK结果: $result');
|
||
return result ?? false;
|
||
} catch (e) {
|
||
debugPrint('系统流程安装APK异常: $e');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<bool> installApkWithConfig(String filePath, InstallConfig config) async {
|
||
if (!Platform.isAndroid) {
|
||
debugPrint('installApkWithConfig: 非Android平台');
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
debugPrint('开始使用配置策略安装APK: $filePath, 策略: ${config.strategy}');
|
||
|
||
// Check if file exists
|
||
final file = File(filePath);
|
||
if (!await file.exists()) {
|
||
debugPrint('安装失败: APK文件不存在 - $filePath');
|
||
return false;
|
||
}
|
||
|
||
switch (config.strategy) {
|
||
case InstallStrategy.systemFlow:
|
||
return await _installWithSystemFlow(filePath);
|
||
|
||
case InstallStrategy.preCheckPermission:
|
||
return await _installWithPreCheck(filePath, config);
|
||
|
||
case InstallStrategy.smart:
|
||
return await _installWithSmartStrategy(filePath, config);
|
||
}
|
||
} catch (e) {
|
||
debugPrint('配置策略安装APK异常: $e');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
Future<bool> _installWithSystemFlow(String filePath) async {
|
||
final result = await methodChannel.invokeMethod<bool>('installApkWithSystemFlow', {'filePath': filePath});
|
||
return result ?? false;
|
||
}
|
||
|
||
Future<bool> _installWithPreCheck(String filePath, InstallConfig config) async {
|
||
// 先检查权限
|
||
final permissionStatus = await PermissionHelper.checkInstallPermission();
|
||
|
||
if (permissionStatus != InstallPermissionStatus.granted) {
|
||
debugPrint('安装权限未授予,状态: $permissionStatus');
|
||
|
||
if (config.autoOpenSettings) {
|
||
// 自动跳转到权限设置
|
||
final opened = await openInstallPermissionSettings();
|
||
if (opened) {
|
||
debugPrint('已跳转到权限设置页面');
|
||
// 等待用户操作
|
||
await Future.delayed(const Duration(seconds: 2));
|
||
// 重新检查权限
|
||
final newStatus = await PermissionHelper.checkInstallPermission();
|
||
if (newStatus != InstallPermissionStatus.granted) {
|
||
debugPrint('用户未授予权限');
|
||
return false;
|
||
}
|
||
} else {
|
||
debugPrint('无法跳转到权限设置页面');
|
||
return false;
|
||
}
|
||
} else {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 有权限时使用传统安装方式
|
||
final result = await methodChannel.invokeMethod<bool>('installApk', {'filePath': filePath});
|
||
return result ?? false;
|
||
}
|
||
|
||
Future<bool> _installWithSmartStrategy(String filePath, InstallConfig config) async {
|
||
// 智能策略:先检查权限状态
|
||
final permissionStatus = await PermissionHelper.checkInstallPermission();
|
||
|
||
if (permissionStatus == InstallPermissionStatus.granted) {
|
||
// 有权限时使用预检查方式
|
||
debugPrint('智能策略: 检测到已有权限,使用预检查方式');
|
||
return await _installWithPreCheck(filePath, config);
|
||
} else {
|
||
// 无权限时使用系统流程
|
||
debugPrint('智能策略: 检测到无权限,使用系统流程');
|
||
return await _installWithSystemFlow(filePath);
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<bool> goToAppStore(String url) async {
|
||
try {
|
||
final uri = Uri.parse(url);
|
||
final bool flag = await canLaunchUrl(uri);
|
||
if (flag) {
|
||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||
return true;
|
||
}
|
||
Fluttertoast.showToast(msg: '当前APP没有上架当前设备对应的应用市场');
|
||
return false;
|
||
} catch (e) {
|
||
debugPrint('跳转应用商店失败: $e');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<String?> getDownloadPath({bool checkPermission = true}) async {
|
||
if (Platform.isAndroid && checkPermission) {
|
||
// 检查存储权限状态
|
||
final sdkVersion = await _getAndroidSdkVersion();
|
||
|
||
// Android 9 及以下需要 WRITE_EXTERNAL_STORAGE 权限来写入公共目录
|
||
if (sdkVersion <= 28) {
|
||
final permission = await Permission.storage.status;
|
||
if (!permission.isGranted) {
|
||
debugPrint('存储权限未授予,使用应用私有目录');
|
||
// 权限未授予,直接使用应用私有目录
|
||
return await _getAppPrivateDownloadPath();
|
||
}
|
||
}
|
||
}
|
||
|
||
try {
|
||
// 首先尝试使用原生方法获取下载路径(公共 Download 目录)
|
||
final nativePath = await methodChannel.invokeMethod<String>('getDownloadPath');
|
||
if (nativePath != null && nativePath.isNotEmpty) {
|
||
// 验证路径是否可写(对于 Android 9 及以下)
|
||
if (Platform.isAndroid) {
|
||
final sdkVersion = await _getAndroidSdkVersion();
|
||
if (sdkVersion <= 28) {
|
||
try {
|
||
if (await _canWriteToDirectory(nativePath)) {
|
||
debugPrint('使用原生下载路径: $nativePath');
|
||
return nativePath;
|
||
} else {
|
||
debugPrint('无法写入公共下载目录,使用应用私有目录');
|
||
return await _getAppPrivateDownloadPath();
|
||
}
|
||
} catch (e) {
|
||
debugPrint('检查下载路径权限失败: $e,使用应用私有目录');
|
||
return await _getAppPrivateDownloadPath();
|
||
}
|
||
} else {
|
||
debugPrint('使用原生下载路径: $nativePath');
|
||
return nativePath;
|
||
}
|
||
} else {
|
||
debugPrint('使用原生下载路径: $nativePath');
|
||
return nativePath;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
debugPrint('获取原生下载路径失败: $e');
|
||
}
|
||
|
||
// 备用方案:使用 path_provider
|
||
try {
|
||
Directory? directory;
|
||
|
||
if (Platform.isAndroid) {
|
||
// Android: 优先使用外部存储下载目录
|
||
directory = await getExternalStorageDirectory();
|
||
if (directory != null) {
|
||
// 创建 Download 子目录
|
||
final downloadDir = Directory('${directory.path}/Download');
|
||
if (!await downloadDir.exists()) {
|
||
try {
|
||
await downloadDir.create(recursive: true);
|
||
} catch (e) {
|
||
debugPrint('无法创建外部存储下载目录: $e,使用应用私有目录');
|
||
return await _getAppPrivateDownloadPath();
|
||
}
|
||
}
|
||
debugPrint('使用外部存储下载路径: ${downloadDir.path}');
|
||
return downloadDir.path;
|
||
}
|
||
}
|
||
|
||
// 如果外部存储不可用,使用应用文档目录
|
||
return await _getAppPrivateDownloadPath();
|
||
} catch (e) {
|
||
debugPrint('获取备用下载路径失败: $e');
|
||
return await _getAppPrivateDownloadPath();
|
||
}
|
||
}
|
||
|
||
/// 获取应用私有下载路径(不需要权限)
|
||
/// 使用外部存储的应用私有目录(Android/data/包名/files/),因为它在 file_paths.xml 中已配置
|
||
/// 这个目录在 Android 10+ 不需要 WRITE_EXTERNAL_STORAGE 权限
|
||
Future<String> _getAppPrivateDownloadPath() async {
|
||
try {
|
||
// 使用外部存储的应用私有目录(/storage/emulated/0/Android/data/包名/files/)
|
||
// 这个目录在 Android 10+ 不需要 WRITE_EXTERNAL_STORAGE 权限
|
||
// 在 Android 9 及以下,如果无法写入会自动抛出异常,会被下面的 catch 捕获
|
||
// 并且在 file_paths.xml 中通过 external-files-path 配置,FileProvider 可以访问
|
||
final externalDir = await getExternalStorageDirectory();
|
||
if (externalDir != null) {
|
||
final downloadDir = Directory('${externalDir.path}/downloads');
|
||
if (!await downloadDir.exists()) {
|
||
await downloadDir.create(recursive: true);
|
||
}
|
||
debugPrint('使用外部存储应用私有下载路径: ${downloadDir.path}');
|
||
return downloadDir.path;
|
||
}
|
||
} catch (e) {
|
||
debugPrint('无法使用外部存储应用私有目录: $e');
|
||
}
|
||
|
||
// 备用方案:使用内部 files 目录(/data/data/包名/files/)
|
||
// 这个目录在 file_paths.xml 中通过 files-path 配置,FileProvider 可以访问
|
||
try {
|
||
// 获取应用数据目录的父目录,然后访问 files 子目录
|
||
final appDataDir = await getApplicationSupportDirectory();
|
||
final parentDir = Directory(appDataDir.path).parent;
|
||
final filesPath = '${parentDir.path}/files';
|
||
final downloadDir = Directory('$filesPath/downloads');
|
||
if (!await downloadDir.exists()) {
|
||
await downloadDir.create(recursive: true);
|
||
}
|
||
debugPrint('使用内部 files 下载路径: ${downloadDir.path}');
|
||
return downloadDir.path;
|
||
} catch (e) {
|
||
debugPrint('无法使用内部 files 目录: $e');
|
||
// 最后的备用方案:使用 cache 目录(/data/data/包名/cache/)
|
||
// 这个目录在 file_paths.xml 中通过 cache-path 配置,FileProvider 可以访问
|
||
try {
|
||
final cacheDir = await getTemporaryDirectory();
|
||
final downloadDir = Directory('${cacheDir.path}/downloads');
|
||
if (!await downloadDir.exists()) {
|
||
await downloadDir.create(recursive: true);
|
||
}
|
||
debugPrint('使用 cache 下载路径: ${downloadDir.path}');
|
||
return downloadDir.path;
|
||
} catch (e) {
|
||
debugPrint('无法使用 cache 目录: $e');
|
||
// 如果所有方案都失败,抛出异常
|
||
throw Exception('无法获取可用的下载路径');
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 检查是否可以写入目录
|
||
Future<bool> _canWriteToDirectory(String path) async {
|
||
try {
|
||
final dir = Directory(path);
|
||
if (!await dir.exists()) {
|
||
await dir.create(recursive: true);
|
||
}
|
||
// 尝试创建一个测试文件
|
||
final testFile = File('$path/.test_write_${DateTime.now().millisecondsSinceEpoch}');
|
||
await testFile.writeAsString('test');
|
||
await testFile.delete();
|
||
return true;
|
||
} catch (e) {
|
||
debugPrint('无法写入目录 $path: $e');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// 获取 Android SDK 版本
|
||
Future<int> _getAndroidSdkVersion() async {
|
||
if (!Platform.isAndroid) return 0;
|
||
try {
|
||
final sdkVersion = await methodChannel.invokeMethod<int>('getAndroidSdkVersion');
|
||
return sdkVersion ?? 0;
|
||
} catch (e) {
|
||
debugPrint('获取 Android SDK 版本失败: $e');
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<bool> checkApkExists(String version, String? md5) async {
|
||
if (!Platform.isAndroid) return false;
|
||
final result = await methodChannel.invokeMethod<bool>('checkApkExists', {
|
||
'version': version,
|
||
'md5': md5,
|
||
});
|
||
return result ?? false;
|
||
}
|
||
|
||
@override
|
||
Future<bool> checkMarketAvailable({
|
||
String? packageName,
|
||
String? marketPackage,
|
||
String? url,
|
||
}) async {
|
||
if (!Platform.isAndroid) return false;
|
||
try {
|
||
final result = await methodChannel.invokeMethod<bool>('checkMarketAvailable', {
|
||
'packageName': packageName,
|
||
'marketPackage': marketPackage,
|
||
'url': url,
|
||
});
|
||
return result ?? false;
|
||
} catch (e) {
|
||
debugPrint('检查应用市场可用性失败: $e');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// 比较版本号
|
||
/// 返回值:1表示v1大于v2,0表示相等,-1表示v1小于v2
|
||
int _compareVersion(int version1, int version2) {
|
||
try {
|
||
if (version1 > version2) {
|
||
return 1;
|
||
} else if (version1 < version2) {
|
||
return -1;
|
||
} else {
|
||
return 0;
|
||
}
|
||
} catch (e) {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
/// 测试下载URL的连接性
|
||
Future<bool> testDownloadUrl(String url) async {
|
||
try {
|
||
debugPrint('测试下载URL连接: $url');
|
||
|
||
final response = await _dio.head(
|
||
url,
|
||
options: Options(
|
||
receiveTimeout: const Duration(seconds: 10),
|
||
sendTimeout: const Duration(seconds: 10),
|
||
validateStatus: (status) => true,
|
||
headers: {
|
||
'User-Agent': 'AppUpgradePlugin/1.0',
|
||
},
|
||
),
|
||
);
|
||
|
||
debugPrint('URL测试响应状态码: ${response.statusCode}');
|
||
debugPrint('响应头: ${response.headers.map}');
|
||
|
||
if (response.statusCode == 200 || response.statusCode == 206) {
|
||
final contentLength = response.headers.value('content-length');
|
||
if (contentLength != null) {
|
||
final size = int.tryParse(contentLength) ?? 0;
|
||
debugPrint('文件大小: ${(size / 1024 / 1024).toStringAsFixed(2)} MB');
|
||
}
|
||
return true;
|
||
} else if (response.statusCode == 404) {
|
||
debugPrint('错误: 文件不存在 (404)');
|
||
} else if (response.statusCode == 403) {
|
||
debugPrint('错误: 访问被拒绝 (403)');
|
||
} else if (response.statusCode == 401) {
|
||
debugPrint('错误: 需要认证 (401)');
|
||
} else {
|
||
debugPrint('错误: 未知状态 (${response.statusCode})');
|
||
}
|
||
|
||
return false;
|
||
} catch (e) {
|
||
debugPrint('测试URL连接失败: $e');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// 测试下载URL的连接性并返回错误信息
|
||
Future<String?> testDownloadUrlWithError(String url) async {
|
||
try {
|
||
debugPrint('测试下载URL连接: $url');
|
||
|
||
final response = await _dio.head(
|
||
url,
|
||
options: Options(
|
||
receiveTimeout: const Duration(seconds: 10),
|
||
sendTimeout: const Duration(seconds: 10),
|
||
validateStatus: (status) => true,
|
||
headers: {
|
||
'User-Agent': 'AppUpgradePlugin/1.0',
|
||
},
|
||
),
|
||
);
|
||
|
||
debugPrint('URL测试响应状态码: ${response.statusCode}');
|
||
debugPrint('响应头: ${response.headers.map}');
|
||
|
||
if (response.statusCode == 200 || response.statusCode == 206) {
|
||
final contentLength = response.headers.value('content-length');
|
||
if (contentLength != null) {
|
||
final size = int.tryParse(contentLength) ?? 0;
|
||
debugPrint('文件大小: ${(size / 1024 / 1024).toStringAsFixed(2)} MB');
|
||
}
|
||
return null; // 无错误
|
||
} else if (response.statusCode == 404) {
|
||
debugPrint('错误: 文件不存在 (404)');
|
||
return '文件不存在 (404),请检查下载地址';
|
||
} else if (response.statusCode == 403) {
|
||
debugPrint('错误: 访问被拒绝 (403)');
|
||
return '访问被拒绝 (403),请检查下载权限';
|
||
} else if (response.statusCode == 401) {
|
||
debugPrint('错误: 需要认证 (401)');
|
||
return '需要认证 (401),请检查下载权限';
|
||
} else {
|
||
debugPrint('错误: 未知状态 (${response.statusCode})');
|
||
return '无法连接到下载服务器 (${response.statusCode})';
|
||
}
|
||
} on DioException catch (e) {
|
||
debugPrint('测试URL连接失败: $e');
|
||
if (e.type == DioExceptionType.connectionTimeout) {
|
||
return '连接超时,请检查网络连接';
|
||
} else if (e.type == DioExceptionType.receiveTimeout) {
|
||
return '接收超时,请检查网络连接';
|
||
} else if (e.type == DioExceptionType.connectionError) {
|
||
return '无法连接到下载服务器,请检查网络';
|
||
} else {
|
||
return '无法连接到下载URL: ${e.message}';
|
||
}
|
||
} catch (e) {
|
||
debugPrint('测试URL连接失败: $e');
|
||
return '无法连接到下载URL: $e';
|
||
}
|
||
}
|
||
|
||
/// 记录Dio错误详情
|
||
void _logDioError(DioException e) {
|
||
debugPrint('========== Dio错误详情 ==========');
|
||
debugPrint('错误类型: ${e.type}');
|
||
debugPrint('错误消息: ${e.message}');
|
||
|
||
if (e.error != null) {
|
||
debugPrint('原始错误: ${e.error}');
|
||
if (e.error is SocketException) {
|
||
final socketError = e.error as SocketException;
|
||
debugPrint('Socket错误代码: ${socketError.osError?.errorCode}');
|
||
debugPrint('Socket错误消息: ${socketError.osError?.message}');
|
||
}
|
||
}
|
||
|
||
if (e.response != null) {
|
||
debugPrint('响应状态码: ${e.response?.statusCode}');
|
||
debugPrint('响应消息: ${e.response?.statusMessage}');
|
||
}
|
||
|
||
debugPrint('请求URL: ${e.requestOptions.uri}');
|
||
debugPrint('请求方法: ${e.requestOptions.method}');
|
||
debugPrint('================================');
|
||
}
|
||
|
||
/// 处理网络错误,提供友好的错误消息
|
||
void _handleNetworkError(DioException e) {
|
||
String errorMessage = '网络请求失败';
|
||
|
||
switch (e.type) {
|
||
case DioExceptionType.connectionTimeout:
|
||
errorMessage = '连接超时,请检查网络';
|
||
break;
|
||
case DioExceptionType.sendTimeout:
|
||
errorMessage = '发送超时,请检查网络';
|
||
break;
|
||
case DioExceptionType.receiveTimeout:
|
||
errorMessage = '接收超时,请检查网络';
|
||
break;
|
||
case DioExceptionType.badResponse:
|
||
errorMessage = '服务器响应错误 (${e.response?.statusCode})';
|
||
break;
|
||
case DioExceptionType.cancel:
|
||
errorMessage = '请求已取消';
|
||
break;
|
||
case DioExceptionType.connectionError:
|
||
if (e.error is SocketException) {
|
||
final socketError = e.error as SocketException;
|
||
if (socketError.message.contains('Failed host lookup')) {
|
||
errorMessage = 'DNS解析失败,请检查域名或网络设置';
|
||
} else if (socketError.message.contains('Connection refused')) {
|
||
errorMessage = '连接被拒绝,请检查服务器状态';
|
||
} else if (socketError.message.contains('Network is unreachable')) {
|
||
errorMessage = '网络不可达,请检查网络连接';
|
||
} else {
|
||
errorMessage = '网络连接错误: ${socketError.message}';
|
||
}
|
||
} else {
|
||
errorMessage = '连接错误: ${e.message}';
|
||
}
|
||
break;
|
||
case DioExceptionType.badCertificate:
|
||
errorMessage = '证书验证失败';
|
||
break;
|
||
case DioExceptionType.unknown:
|
||
errorMessage = '未知错误: ${e.message}';
|
||
break;
|
||
}
|
||
|
||
debugPrint('网络错误: $errorMessage');
|
||
|
||
// 如果需要,可以在这里抛出自定义异常或显示Toast
|
||
// throw NetworkException(errorMessage);
|
||
}
|
||
}
|