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: