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:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'app_upgrade_plugin_platform_interface.dart'; import 'core/http_config.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 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 currentVersion = packageInfo.version; final currentBuildNumber = packageInfo.buildNumber; // 准备请求参数 final requestParams = { 'version': currentVersion, 'buildNumber': currentBuildNumber, 'platform': Platform.isAndroid ? 'android' : 'ios', ...?params, }; debugPrint('==== 检查更新 ===='); debugPrint('当前版本: $currentVersion (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'); // TODO 解析更新信息 // final upgradeInfo = UpgradeInfo.fromJson(responseData); final upgradeInfo = UpgradeInfo.fromJson({ "hasUpdate": true, "isForceUpdate": true, "versionCode": "101", // Android buildNumber, 必须是数字字符串 "versionName": "1.0.1", // 显示的版本名 "updateContent": "1. 修复了xxx Bug。\n2. 优化了用户体验。", "downloadUrl": "https://dpc-job-oss.23544.com/infra-app/making_school_asignment_app/1.0.5/1/app-release.apk", // APK 下载地址 "appStoreUrl": "https://itunes.apple.com/app/id123456", // iOS App Store 地址 "apkSize": 20971520, // APK 文件大小 (单位: byte) "apkMd5": "b10a8db164e0754105b7a99be72e3fe5" // APK 的 MD5 (可选,用于校验) }); // 比较版本 if (_compareVersion(upgradeInfo.versionCode, currentBuildNumber) > 0) { 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> 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 canConnect = await testDownloadUrl(url); if (!canConnect) { debugPrint('错误: 无法连接到下载URL'); return null; } 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 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 goToAppStore(String url) async { try { final uri = Uri.parse(url); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); return true; } return false; } catch (e) { debugPrint('跳转应用商店失败: $e'); return false; } } @override Future getDownloadPath() async { try { // 首先尝试使用原生方法获取下载路径 final nativePath = await methodChannel.invokeMethod('getDownloadPath'); if (nativePath != null && nativePath.isNotEmpty) { 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()) { await downloadDir.create(recursive: true); } debugPrint('使用外部存储下载路径: ${downloadDir.path}'); return downloadDir.path; } } // 如果外部存储不可用,使用应用文档目录 directory = await getApplicationDocumentsDirectory(); final downloadDir = Directory('${directory.path}/downloads'); if (!await downloadDir.exists()) { await downloadDir.create(recursive: true); } debugPrint('使用应用文档下载路径: ${downloadDir.path}'); return downloadDir.path; } catch (e) { debugPrint('获取备用下载路径失败: $e'); return null; } } @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; } /// 比较版本号 /// 返回值:1表示v1大于v2,0表示相等,-1表示v1小于v2 int _compareVersion(String v1, String v2) { try { final version1 = int.tryParse(v1) ?? 0; final version2 = int.tryParse(v2) ?? 0; if (version1 > version2) { return 1; } else if (version1 < version2) { return -1; } else { return 0; } } catch (e) { return 0; } } /// 测试下载URL的连接性 Future 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; } } /// 记录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); } }