import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:dio/io.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.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('yx_app_upgrade_flutter'); 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 getPlatformVersion() async { final version = await methodChannel.invokeMethod('getPlatformVersion'); return version; } @override Future checkUpdate(String url, {Map? 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 getAndroidSdkVersion() async { if (!Platform.isAndroid) return null; return await methodChannel.invokeMethod('getAndroidSdkVersion'); } @override Future openInstallPermissionSettings() async { if (!Platform.isAndroid) return false; try { final result = await methodChannel.invokeMethod('openInstallPermissionSettings'); return result ?? false; } catch (e) { debugPrint('Failed to open install permission settings: $e'); return false; } } @override Future?> getDeviceInfo() async { if (!Platform.isAndroid) return null; try { final result = await methodChannel.invokeMethod>('getDeviceInfo'); return result?.cast(); } catch (e) { debugPrint('Failed to get device info: $e'); return null; } } @override Future> getInstalledMarkets() async { if (!Platform.isAndroid) return []; try { final result = await methodChannel.invokeMethod>('getInstalledMarkets'); if (result == null) return []; return result.map((item) => item.toString()).toList(); } catch (e) { debugPrint('Failed to get installed markets: $e'); return []; } } @override Future> getAppInfo() async { final packageInfo = await PackageInfo.fromPlatform(); return { 'appName': packageInfo.appName, 'packageName': packageInfo.packageName, 'version': packageInfo.version, 'buildNumber': packageInfo.buildNumber, }; } @override Future 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'); // 下载前清理历史APK文件 await _clearHistoryApkFiles(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; } } /// 清理下载目录中的历史APK文件 /// [downloadPath] 下载目录路径 Future _clearHistoryApkFiles(String downloadPath) async { try { final dir = Directory(downloadPath); if (!await dir.exists()) { debugPrint('下载目录不存在,跳过清理: $downloadPath'); return; } final files = await dir.list().toList(); int deletedCount = 0; for (final file in files) { if (file is File && file.path.endsWith('.apk')) { try { await file.delete(); deletedCount++; debugPrint('已删除历史APK文件: ${file.path}'); } catch (e) { debugPrint('删除历史APK文件失败: ${file.path}, 错误: $e'); } } } if (deletedCount > 0) { debugPrint('清理完成,共删除 $deletedCount 个历史APK文件'); } } catch (e) { debugPrint('清理历史APK文件失败: $e'); // 清理失败不影响下载流程,继续执行 } } @override Future 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('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 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('installApkWithSystemFlow', {'filePath': filePath}); debugPrint('系统流程安装APK结果: $result'); return result ?? false; } catch (e) { debugPrint('系统流程安装APK异常: $e'); return false; } } @override Future 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 _installWithSystemFlow(String filePath) async { final result = await methodChannel.invokeMethod('installApkWithSystemFlow', {'filePath': filePath}); return result ?? false; } Future _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('installApk', {'filePath': filePath}); return result ?? false; } Future _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 goToAppStore(String url, {required BuildContext context}) async { try { final uri = Uri.parse(url); // 对于其他协议(如 https://),先检查是否可以打开 final bool flag = await canLaunchUrl(uri); if (flag) { await launchUrl(uri, mode: LaunchMode.externalApplication); return true; } else { // 对于 market:// 协议,canLaunchUrl 可能不准确(即使应用已上架也可能返回 false) // 所以直接尝试打开,如果失败再返回 false if (uri.scheme == 'market') { try { await launchUrl(uri, mode: LaunchMode.externalApplication); return true; } catch (e) { debugPrint('无法打开应用市场: $url, 错误: $e'); } } } debugPrint('无法打开URL: $url'); return false; } catch (e) { debugPrint('跳转应用商店失败: $e'); return false; } } @override Future getDownloadPath({bool checkPermission = true}) async { if (Platform.isAndroid && checkPermission) { // 检查存储权限状态 final sdkVersion = await _getAndroidSdkVersion(); if (sdkVersion >= 30) { final manageStatus = await Permission.manageExternalStorage.status; if (!manageStatus.isGranted) { debugPrint('MANAGE_EXTERNAL_STORAGE 未授予,使用应用私有目录'); return await _getAppPrivateDownloadPath(); } } else if (sdkVersion >= 29) { final storageStatus = await Permission.storage.status; if (!storageStatus.isGranted) { debugPrint('Android 10 存储权限未授予,使用应用私有目录'); return await _getAppPrivateDownloadPath(); } } else { // Android 9 及以下需要 WRITE_EXTERNAL_STORAGE 权限来写入公共目录 final permission = await Permission.storage.status; if (!permission.isGranted) { debugPrint('存储权限未授予,使用应用私有目录'); // 权限未授予,直接使用应用私有目录 return await _getAppPrivateDownloadPath(); } } } try { // 首先尝试使用原生方法获取下载路径(公共 Download 目录) final nativePath = await methodChannel.invokeMethod('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 _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 _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 _getAndroidSdkVersion() async { if (!Platform.isAndroid) return 0; try { final sdkVersion = await methodChannel.invokeMethod('getAndroidSdkVersion'); return sdkVersion ?? 0; } catch (e) { debugPrint('获取 Android SDK 版本失败: $e'); return 0; } } @override Future checkApkExists(String version, String? md5) async { if (!Platform.isAndroid) return false; final result = await methodChannel.invokeMethod('checkApkExists', { 'version': version, 'md5': md5, }); return result ?? false; } /// 测试下载URL的连接性并返回错误信息 Future 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); } }