yx_app_upgrade_flutter/lib/app_upgrade_plugin_method_c...

562 lines
18 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<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 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<int?> getAndroidSdkVersion() async {
if (!Platform.isAndroid) return null;
return await methodChannel.invokeMethod<int>('getAndroidSdkVersion');
}
@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 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<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> 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<String?> getDownloadPath() async {
try {
// 首先尝试使用原生方法获取下载路径
final nativePath = await methodChannel.invokeMethod<String>('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<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;
}
/// 比较版本号
/// 返回值1表示v1大于v20表示相等-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<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;
}
}
/// 记录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);
}
}