初步修改

This commit is contained in:
DESKTOP-I3JPKHK\wy 2025-11-15 21:19:13 +08:00
parent 644b02cc12
commit a9e7102fde
9 changed files with 354 additions and 21 deletions

View File

@ -101,7 +101,8 @@ AppUpgradeSimple.instance.configure(const UpgradeConfig(
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!-- Android 13+ 通知权限(可选) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- Android 9 写储存权限 sdk是28 否无无法写入内存 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application>
<!-- FileProviderAndroid 7.0+ APK 安装必需) -->
<provider
@ -114,6 +115,23 @@ AppUpgradeSimple.instance.configure(const UpgradeConfig(
android:resource="@xml/file_paths" />
</provider>
</application>
<!-- Android 11+ 查询声明:允许打开浏览器处理 HTTP/HTTPS URL -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<!-- 允许打开浏览器处理 HTTP/HTTPS 链接(用于"前往浏览器下载"功能) -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
</queries>
</manifest>
```
@ -124,11 +142,20 @@ AppUpgradeSimple.instance.configure(const UpgradeConfig(
```xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- External storage -->
<external-path name="external_files" path="." />
<!-- Internal app storage -->
<files-path name="files" path="." />
<!-- Cache directory -->
<cache-path name="cache" path="." />
<!-- External cache -->
<external-cache-path name="external_cache" path="." />
<!-- Downloads directory -->
<external-path name="downloads" path="Download/" />
<!-- External app-specific files directory (Android/data/包名/files/) -->
<!-- This is accessible without WRITE_EXTERNAL_STORAGE permission on Android 10+ -->
<!-- 用于权限被拒绝时,使用应用私有目录存储下载的 APK -->
<external-files-path name="external_app_files" path="." />
</paths>
```
@ -229,10 +256,13 @@ await AppUpgradePlugin().installApkWithConfig(
## 🐛 常见问题
- 安装失败显示“解析包时出现问题”:检查 APK 完整性、签名与架构匹配
- 安装失败显示"解析包时出现问题":检查 APK 完整性、签名与架构匹配
- 权限申请失败:确认 Manifest 权限、FileProvider 配置、在 MaterialApp 环境调用
- 下载失败/进度不更新:检查网络、下载 URL 可用性、服务端是否支持断点
- iOS 不跳转:确认 `appStoreUrl` 为有效的 App Store 链接
- "前往浏览器下载"无反应:确认已在 AndroidManifest.xml 中添加 `<queries>` 声明Android 11+ 必需)
- FileProvider 配置错误:确认 `file_paths.xml` 中已添加 `<external-files-path>` 配置,用于权限被拒绝时的备用存储路径
- Android 9 下载权限错误:插件会自动检测权限,无权限时使用应用私有目录,无需额外配置
## 📚 主要 API 清单
@ -253,6 +283,7 @@ await AppUpgradePlugin().installApkWithConfig(
- `openInstallPermissionSettings()`跳转安装权限设置Android
- `getDeviceInfo()`、`getAndroidSdkVersion()`Android
- `goToAppStore(url)`:跳转到应用商店
- `checkMarketAvailable({packageName, marketPackage, url})`检查应用市场是否可用Android用于判断设备是否有可用的应用市场
### PermissionHelperAndroid

View File

@ -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<String>("packageName")
val marketPackage = call.argument<String>("marketPackage")
val url = call.argument<String>("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)

View File

@ -44,6 +44,7 @@
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility?hl=en and
@ -57,5 +58,14 @@
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<!-- Allow opening browser for HTTP/HTTPS URLs -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
</queries>
</manifest>

View File

@ -10,4 +10,7 @@
<external-cache-path name="external_cache" path="." />
<!-- Downloads directory -->
<external-path name="downloads" path="Download/" />
<!-- External app-specific files directory (Android/data/包名/files/) -->
<!-- This is accessible without WRITE_EXTERNAL_STORAGE permission on Android 10+ -->
<external-files-path name="external_app_files" path="." />
</paths>

View File

@ -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

View File

@ -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<bool> 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<String?> getDownloadPath() async {
Future<String?> 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<String>('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<String> _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<bool> _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<int> _getAndroidSdkVersion() async {
if (!Platform.isAndroid) return 0;
try {
final sdkVersion = await methodChannel.invokeMethod<int>('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<bool> checkMarketAvailable({
String? packageName,
String? marketPackage,
String? url,
}) async {
if (!Platform.isAndroid) return false;
try {
final result = await methodChannel.invokeMethod<bool>('checkMarketAvailable', {
'packageName': packageName,
'marketPackage': marketPackage,
'url': url,
});
return result ?? false;
} catch (e) {
debugPrint('检查应用市场可用性失败: $e');
return false;
}
}
///
/// 1v1大于v20-1v1小于v2
int _compareVersion(int version1, int version2) {

View File

@ -87,7 +87,10 @@ abstract class AppUpgradePluginPlatform extends PlatformInterface {
}
///
Future<String?> getDownloadPath() {
///
/// [checkPermission] true
/// true 使
Future<String?> getDownloadPath({bool checkPermission = true}) {
throw UnimplementedError('getDownloadPath() has not been implemented.');
}
@ -95,4 +98,19 @@ abstract class AppUpgradePluginPlatform extends PlatformInterface {
Future<bool> checkApkExists(String version, String? md5) {
throw UnimplementedError('checkApkExists() has not been implemented.');
}
/// Android
///
/// [packageName] null 使
/// [marketPackage]
/// [url] URL
///
/// true
Future<bool> checkMarketAvailable({
String? packageName,
String? marketPackage,
String? url,
}) {
throw UnimplementedError('checkMarketAvailable() has not been implemented.');
}
}

View File

@ -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<T extends StatefulWidget> on State<T> {
}
}
///
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<void> _startDownloadAndInstall() async {
if (!Platform.isAndroid || info.downloadUrl == null) return;
if (!mounted) return;
@ -1709,10 +1767,16 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
// 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<T extends StatefulWidget> on State<T> {
return;
}
if (choice == 'download' && !_isDownloading) {
if (choice == 'update_within_APP' && !_isDownloading) {
await _startDownloadAndInstall();
return;
}
///
if (choice == 'go_to_browser' && !_isDownloading) {
_goToBrowser();
}
}
}

View File

@ -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: