From a9e7102fde0bb80a8ff0fcf6df9efedb96ee2f85 Mon Sep 17 00:00:00 2001 From: "DESKTOP-I3JPKHK\\wy" <1111> Date: Sat, 15 Nov 2025 21:19:13 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 35 +++- .../app_upgrade_plugin/AppUpgradePlugin.kt | 56 ++++++ .../android/app/src/main/AndroidManifest.xml | 10 ++ .../app/src/main/res/xml/file_paths.xml | 3 + example/pubspec.lock | 4 +- lib/app_upgrade_plugin_method_channel.dart | 167 ++++++++++++++++-- ...app_upgrade_plugin_platform_interface.dart | 20 ++- lib/app_upgrade_simple.dart | 78 +++++++- pubspec.yaml | 2 +- 9 files changed, 354 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 7295358..74693f2 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,8 @@ AppUpgradeSimple.instance.configure(const UpgradeConfig( - + + + + + + + + + + + + + + + + + + + ``` @@ -124,11 +142,20 @@ AppUpgradeSimple.instance.configure(const UpgradeConfig( ```xml + + + + + + + + + ``` @@ -229,10 +256,13 @@ await AppUpgradePlugin().installApkWithConfig( ## 🐛 常见问题 -- 安装失败显示“解析包时出现问题”:检查 APK 完整性、签名与架构匹配 +- 安装失败显示"解析包时出现问题":检查 APK 完整性、签名与架构匹配 - 权限申请失败:确认 Manifest 权限、FileProvider 配置、在 MaterialApp 环境调用 - 下载失败/进度不更新:检查网络、下载 URL 可用性、服务端是否支持断点 - iOS 不跳转:确认 `appStoreUrl` 为有效的 App Store 链接 +- "前往浏览器下载"无反应:确认已在 AndroidManifest.xml 中添加 `` 声明(Android 11+ 必需) +- FileProvider 配置错误:确认 `file_paths.xml` 中已添加 `` 配置,用于权限被拒绝时的备用存储路径 +- Android 9 下载权限错误:插件会自动检测权限,无权限时使用应用私有目录,无需额外配置 ## 📚 主要 API 清单 @@ -253,6 +283,7 @@ await AppUpgradePlugin().installApkWithConfig( - `openInstallPermissionSettings()`:跳转安装权限设置(Android) - `getDeviceInfo()`、`getAndroidSdkVersion()`(Android) - `goToAppStore(url)`:跳转到应用商店 +- `checkMarketAvailable({packageName, marketPackage, url})`:检查应用市场是否可用(Android),用于判断设备是否有可用的应用市场 ### PermissionHelper(Android) diff --git a/android/src/main/kotlin/com/example/app_upgrade_plugin/AppUpgradePlugin.kt b/android/src/main/kotlin/com/example/app_upgrade_plugin/AppUpgradePlugin.kt index 7ed63ef..e64b579 100644 --- a/android/src/main/kotlin/com/example/app_upgrade_plugin/AppUpgradePlugin.kt +++ b/android/src/main/kotlin/com/example/app_upgrade_plugin/AppUpgradePlugin.kt @@ -20,6 +20,7 @@ import java.math.BigInteger import java.security.MessageDigest import android.os.Environment import android.content.ComponentName +import android.content.pm.PackageManager /** AppUpgradePlugin */ class AppUpgradePlugin: FlutterPlugin, MethodCallHandler, ActivityAware { @@ -101,6 +102,12 @@ class AppUpgradePlugin: FlutterPlugin, MethodCallHandler, ActivityAware { result.error("INVALID_ARGUMENT", "URL is null", null) } } + "checkMarketAvailable" -> { + val packageName = call.argument("packageName") + val marketPackage = call.argument("marketPackage") + val url = call.argument("url") + checkMarketAvailable(packageName, marketPackage, url, result) + } "getAndroidSdkVersion" -> { result.success(Build.VERSION.SDK_INT) } @@ -201,6 +208,55 @@ class AppUpgradePlugin: FlutterPlugin, MethodCallHandler, ActivityAware { activity?.startActivity(intent) } + /// 检查应用市场是否可用 + /// 返回 true 表示有应用可以处理 market:// 链接或指定的应用市场已安装 + private fun checkMarketAvailable(packageName: String?, marketPackage: String?, url: String?, result: Result) { + if (context == null) { + result.success(false) + return + } + + try { + val finalPackageName = packageName ?: context!!.packageName + + // 如果指定了特定的应用市场包名,检查该应用是否已安装 + if (marketPackage != null && marketPackage.isNotEmpty()) { + val pm = context!!.packageManager + try { + pm.getPackageInfo(marketPackage, PackageManager.GET_ACTIVITIES) + // 应用市场已安装 + result.success(true) + return + } catch (e: PackageManager.NameNotFoundException) { + // 指定的应用市场未安装 + result.success(false) + return + } + } + + // 检查是否有应用可以处理 market://details?id=包名 的 Intent + val marketIntent = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$finalPackageName")) + val resolveInfo = context!!.packageManager.queryIntentActivities(marketIntent, PackageManager.MATCH_DEFAULT_ONLY) + + if (resolveInfo.isNotEmpty()) { + // 有应用可以处理 market:// 链接 + result.success(true) + return + } + + // 如果没有应用可以处理 market:// 链接,但有 URL,检查是否可以打开 URL + if (url != null && url.isNotEmpty()) { + val urlIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + val urlResolveInfo = context!!.packageManager.queryIntentActivities(urlIntent, PackageManager.MATCH_DEFAULT_ONLY) + result.success(urlResolveInfo.isNotEmpty()) + } else { + result.success(false) + } + } catch (e: Exception) { + result.error("CHECK_MARKET_ERROR", "Failed to check market availability", e.message) + } + } + private fun openInstallPermissionSettings(result: Result) { if (activity == null) { result.error("NO_ACTIVITY", "Activity is not available", null) diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index f2d1d7d..4ad5219 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -44,6 +44,7 @@ + + + + + + + + + diff --git a/example/android/app/src/main/res/xml/file_paths.xml b/example/android/app/src/main/res/xml/file_paths.xml index 34b8a79..a25e199 100644 --- a/example/android/app/src/main/res/xml/file_paths.xml +++ b/example/android/app/src/main/res/xml/file_paths.xml @@ -10,4 +10,7 @@ + + + \ No newline at end of file diff --git a/example/pubspec.lock b/example/pubspec.lock index fabc538..49eaa28 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -213,10 +213,10 @@ packages: dependency: transitive description: name: fluttertoast - sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1" + sha256: "144ddd74d49c865eba47abe31cbc746c7b311c82d6c32e571fd73c4264b740e2" url: "https://pub.flutter-io.cn" source: hosted - version: "8.2.12" + version: "9.0.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter diff --git a/lib/app_upgrade_plugin_method_channel.dart b/lib/app_upgrade_plugin_method_channel.dart index 3ec9836..91c1f13 100644 --- a/lib/app_upgrade_plugin_method_channel.dart +++ b/lib/app_upgrade_plugin_method_channel.dart @@ -5,8 +5,10 @@ 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'; @@ -513,10 +515,12 @@ class MethodChannelAppUpgradePlugin extends AppUpgradePluginPlatform { Future goToAppStore(String url) async { try { final uri = Uri.parse(url); - if (await canLaunchUrl(uri)) { + 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'); @@ -525,13 +529,50 @@ class MethodChannelAppUpgradePlugin extends AppUpgradePluginPlatform { } @override - Future getDownloadPath() async { + Future 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('getDownloadPath'); if (nativePath != null && nativePath.isNotEmpty) { - debugPrint('使用原生下载路径: $nativePath'); - return nativePath; + // 验证路径是否可写(对于 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'); @@ -548,7 +589,12 @@ class MethodChannelAppUpgradePlugin extends AppUpgradePluginPlatform { // 创建 Download 子目录 final downloadDir = Directory('${directory.path}/Download'); if (!await downloadDir.exists()) { - await downloadDir.create(recursive: true); + try { + await downloadDir.create(recursive: true); + } catch (e) { + debugPrint('无法创建外部存储下载目录: $e,使用应用私有目录'); + return await _getAppPrivateDownloadPath(); + } } debugPrint('使用外部存储下载路径: ${downloadDir.path}'); return downloadDir.path; @@ -556,16 +602,95 @@ class MethodChannelAppUpgradePlugin extends AppUpgradePluginPlatform { } // 如果外部存储不可用,使用应用文档目录 - directory = await getApplicationDocumentsDirectory(); - final downloadDir = Directory('${directory.path}/downloads'); + 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('使用应用文档下载路径: ${downloadDir.path}'); + debugPrint('使用内部 files 下载路径: ${downloadDir.path}'); return downloadDir.path; } catch (e) { - debugPrint('获取备用下载路径失败: $e'); - return null; + 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; } } @@ -579,6 +704,26 @@ class MethodChannelAppUpgradePlugin extends AppUpgradePluginPlatform { return result ?? false; } + @override + Future checkMarketAvailable({ + String? packageName, + String? marketPackage, + String? url, + }) async { + if (!Platform.isAndroid) return false; + try { + final result = await methodChannel.invokeMethod('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) { diff --git a/lib/app_upgrade_plugin_platform_interface.dart b/lib/app_upgrade_plugin_platform_interface.dart index 0df3af6..f194646 100644 --- a/lib/app_upgrade_plugin_platform_interface.dart +++ b/lib/app_upgrade_plugin_platform_interface.dart @@ -87,7 +87,10 @@ abstract class AppUpgradePluginPlatform extends PlatformInterface { } /// 获取下载目录路径 - Future getDownloadPath() { + /// + /// [checkPermission] 是否检查存储权限(默认 true) + /// 如果为 true 且权限未授予,将使用应用私有目录 + Future getDownloadPath({bool checkPermission = true}) { throw UnimplementedError('getDownloadPath() has not been implemented.'); } @@ -95,4 +98,19 @@ abstract class AppUpgradePluginPlatform extends PlatformInterface { Future checkApkExists(String version, String? md5) { throw UnimplementedError('checkApkExists() has not been implemented.'); } + + /// 检查应用市场是否可用(仅Android) + /// + /// [packageName] 应用包名,如果为 null 则使用当前应用包名 + /// [marketPackage] 指定的应用市场包名(可选) + /// [url] 备用 URL(可选) + /// + /// 返回 true 表示有应用可以处理应用市场链接 + Future checkMarketAvailable({ + String? packageName, + String? marketPackage, + String? url, + }) { + throw UnimplementedError('checkMarketAvailable() has not been implemented.'); + } } diff --git a/lib/app_upgrade_simple.dart b/lib/app_upgrade_simple.dart index 150f9da..5f7ef48 100644 --- a/lib/app_upgrade_simple.dart +++ b/lib/app_upgrade_simple.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'app_upgrade_plugin_platform_interface.dart'; import 'core/permission_helper.dart'; @@ -612,6 +613,63 @@ mixin _UpgradeDialogLogic on State { } } + /// 前往浏览器 + void _goToBrowser() async { + final downloadApkUrl = info.downloadUrl; + if (!Platform.isAndroid || downloadApkUrl == null) { + showToast('下载地址为空'); + return; + } + if (!mounted) return; + + try { + final uri = Uri.parse(downloadApkUrl); + + // 对于 APK 下载链接,直接尝试打开,不先检查 canLaunchUrl + // 因为 canLaunchUrl 可能无法正确识别 APK 下载链接 + try { + final launched = await launchUrl( + uri, + mode: LaunchMode.externalApplication, + ); + if (!launched) { + // 如果 launchUrl 返回 false,尝试使用 platformDefault 模式 + await launchUrl( + uri, + mode: LaunchMode.platformDefault, + ); + } + // 关闭对话框 + if (mounted && Navigator.canPop(context)) { + Navigator.of(context).pop(); + } + // 不需要关闭弹窗 + Future.delayed(const Duration(seconds: 1), () { + onComplete?.call(); + }); + } catch (launchError) { + debugPrint('launchUrl 失败: $launchError'); + // 如果 launchUrl 失败,尝试检查是否可以启动 + final canLaunch = await canLaunchUrl(uri); + if (canLaunch) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + if (mounted && Navigator.canPop(context)) { + Navigator.of(context).pop(); + } + // 不需要关闭弹窗 + Future.delayed(const Duration(seconds: 1), () { + onComplete?.call(); + }); + } else { + showToast('无法打开下载链接,请检查是否安装了浏览器'); + } + } + } catch (e) { + debugPrint('打开浏览器失败: $e'); + showToast('打开浏览器失败: ${e.toString()}'); + } + } + Future _startDownloadAndInstall() async { if (!Platform.isAndroid || info.downloadUrl == null) return; if (!mounted) return; @@ -1709,10 +1767,16 @@ mixin _UpgradeDialogLogic on State { // Option 2: Direct Download ListTile( - leading: const Icon(Icons.download_for_offline_outlined), - title: const Text('直接下载安装包'), + leading: const Icon(Icons.system_update), + title: const Text('APP内更新'), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - onTap: () => Navigator.of(ctx).pop('download'), + onTap: () => Navigator.of(ctx).pop('update_within_APP'), + ), + ListTile( + leading: const Icon(Icons.download_for_offline_outlined), + title: const Text('前往浏览器下载安装包'), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + onTap: () => Navigator.of(ctx).pop('go_to_browser'), ), const Divider(height: 24), @@ -1755,8 +1819,14 @@ mixin _UpgradeDialogLogic on State { return; } - if (choice == 'download' && !_isDownloading) { + if (choice == 'update_within_APP' && !_isDownloading) { await _startDownloadAndInstall(); + return; + } + + /// 前往浏览器更新 + if (choice == 'go_to_browser' && !_isDownloading) { + _goToBrowser(); } } } diff --git a/pubspec.yaml b/pubspec.yaml index 67be71f..ec6a271 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,7 +24,7 @@ dependencies: shared_preferences: ^2.3.3 flutter_local_notifications: ^18.0.1 device_info_plus: ^11.2.0 - fluttertoast: ^8.2.11 + fluttertoast: ^9.0.0 dev_dependencies: flutter_test: