修复优化BUG 适配更多机型

This commit is contained in:
DESKTOP-I3JPKHK\wy 2025-11-27 12:17:30 +08:00
parent a444566fcc
commit 521bed4ec3
18 changed files with 524 additions and 1173 deletions

125
MARKET_WHITELIST_SIMPLE.md Normal file
View File

@ -0,0 +1,125 @@
# 应用市场白名单功能说明(简化版)
## 🎯 功能概述
`appMarkets` 作为应用市场白名单使用,只需传入 `AppMarket` 枚举列表即可。
## 📝 使用方法
### 1. Dart 代码配置
```dart
await AppUpgradeSimple.instance.checkUpdate(
context: context,
future: () async {
return AppUpgradeVersion(
versionName: '1.0.1',
versionBuildNumber: 101,
updateContent: '修复已知问题',
downloadUrl: 'https://example.com/app.apk',
// 只支持华为、小米、OPPO应用市场
appMarkets: [
AppMarket.huawei,
AppMarket.xiaomi,
AppMarket.oppo,
],
);
},
);
```
### 2. 服务端返回
```json
{
"versionBuildNumber": 101,
"versionName": "1.0.1",
"updateContent": "修复已知问题",
"downloadUrl": "https://example.com/app.apk",
"appMarkets": ["huawei", "xiaomi", "oppo"]
}
```
## 🚀 工作流程
1. 用户点击"前往应用市场更新"
2. 插件检测设备已安装的应用市场
3. 判断:
- ✅ 设备有白名单中的应用市场 → 跳转到应用市场
- ❌ 设备没有白名单中的应用市场 → 提示"不支持当前设备的应用市场,请选择其他方式更新"
## 📋 支持的应用市场
| AppMarket 枚举值 | 显示名称 |
|-----------------|---------|
| `AppMarket.googlePlay` | Google Play |
| `AppMarket.huawei` | 华为应用市场 |
| `AppMarket.xiaomi` | 小米应用商店 |
| `AppMarket.oppo` | OPPO软件商店 |
| `AppMarket.vivo` | vivo应用商店 |
| `AppMarket.tencent` | 腾讯应用宝 |
| `AppMarket.coolapk` | 酷安 |
## 🎨 用户体验示例
### 场景 1: 华为手机 + 配置了华为和小米
- ✅ 检测到华为应用市场已安装
- → 直接跳转到华为应用市场
### 场景 2: OPPO手机 + 只配置了华为和小米
- ❌ 检测到设备没有配置的应用市场
- → 提示:"当前设备的应用市场暂不支持,请选择其他方式更新"
- → 显示其他更新方式(浏览器下载、应用内下载)
### 场景 3: 未配置 appMarkets
- 保持向后兼容
- → 使用默认行为:跳转到设备的默认应用市场
## 🔍 调试方法
查看设备已安装的应用市场:
```dart
final installedMarkets = await AppUpgradePlugin().getInstalledMarkets();
debugPrint('已安装的应用市场: $installedMarkets');
// 输出示例: [huawei, tencent]
```
## ⚠️ 注意事项
1. **iOS 平台**:此功能仅在 Android 平台生效iOS 会忽略此配置
2. **向后兼容**:不配置 `appMarkets` 时,保持原有行为不变
3. **极简设计**无需配置包名、URL等细节插件内部自动处理
## 📦 修改的文件
### 主要变更
- ✅ `appMarkets` 类型从 `List<AppMarketInfo>?` 改为 `List<AppMarket>?`
- ✅ Android 端新增检测已安装应用市场的方法
- ✅ Dart 端新增白名单匹配逻辑
- ✅ 自动提示用户选择其他更新方式
### 优势对比
**之前的方案(复杂):**
```dart
appMarkets: [
AppMarketInfo(
market: AppMarket.huawei,
packageName: 'com.huawei.appmarket',
url: 'https://appgallery.huawei.com/...',
),
]
```
**现在的方案(简单):**
```dart
appMarkets: [AppMarket.huawei]
```
✨ **更简单、更清晰、更易用!**

283
README.md
View File

@ -1,15 +1,17 @@
# App Upgrade Plugin # App Upgrade Plugin
一款轻量、现代且易用的 Flutter 应用内更新插件。支持 Android 的“下载-安装”全流程iOS 自动跳转 App Store。提供「一键检查更新」与「静默检查 + 用户决定」两种常见用法,并内置完善的权限处理与安装策略。 一款轻量、现代且易用的 Flutter 应用内更新插件。支持 Android 的"下载-安装"全流程iOS 自动跳转 App Store。提供「一键检查更新」与「静默检查 + 用户决定」两种常见用法,并内置完善的权限处理与安装策略。
## ✨ 特性 ## ✨ 特性
- **🎯 智能平台适配**Android 直下直装iOS 跳转 App Store - **🎯 智能平台适配**Android 直下直装iOS 跳转 App Store
- **🔄 两种更新体验**:非强制(可后台下载)与强制更新(阻塞式对话框) - **🔄 两种更新体验**:非强制(可后台下载)与强制更新(阻塞式对话框)
- **🛡️ 权限适配完善**:针对不同 Android 版本的存储、安装、通知权限 - **🛡️ 权限适配完善**:针对不同 Android 版本的存储、安装、通知权限自动处理
- **📱 现代化 UI**Material 风格对话框,进度与状态可视化 - **📱 现代化 UI**Material 风格对话框,进度与状态可视化
- **🌐 网络可配置**证书校验、超时、默认方法、Headers 等 - **🌐 网络可配置**证书校验、超时、默认方法、Headers 等
- **🔧 安装策略灵活**:系统流程/预检查权限/智能策略可选 - **🔧 安装策略灵活**:系统流程/预检查权限/智能策略可选
- **🏪 应用市场支持**:支持多应用市场白名单,智能检测设备已安装的市场
- **📦 三种更新方式**:应用市场、浏览器下载、应用内下载
## 📦 安装 ## 📦 安装
@ -24,6 +26,8 @@ dependencies:
### 方式一:一键检查更新(推荐) ### 方式一:一键检查更新(推荐)
这是最简单的方式,一行代码即可完成检查更新并显示升级对话框:
```dart ```dart
import 'package:app_upgrade_plugin/app_upgrade_plugin.dart'; import 'package:app_upgrade_plugin/app_upgrade_plugin.dart';
@ -32,7 +36,6 @@ void checkUpdate(BuildContext context) {
AppUpgradeSimple.instance.configure( AppUpgradeSimple.instance.configure(
const UpgradeConfig( const UpgradeConfig(
showNoUpdateToast: true, showNoUpdateToast: true,
autoDownload: true,
autoInstall: false, autoInstall: false,
), ),
); );
@ -40,54 +43,89 @@ void checkUpdate(BuildContext context) {
// 一行调用,自动拉取并展示升级对话框 // 一行调用,自动拉取并展示升级对话框
AppUpgradeSimple.instance.checkUpdate( AppUpgradeSimple.instance.checkUpdate(
context: context, context: context,
url: 'https://your-api.com/check-update', future: () async {
params: {'channel': 'release'}, // 调用您的 API 获取版本信息
final response = await http.get('https://your-api.com/check-update');
final data = json.decode(response.body);
// 返回 AppUpgradeVersion 对象
return AppUpgradeVersion(
versionName: data['versionName'],
versionBuildNumber: data['versionBuildNumber'],
isForce: data['isForceUpdate'] ?? false,
updateContent: data['updateContent'],
downloadUrl: data['downloadUrl'],
appStoreUrl: data['appStoreUrl'],
apkSize: data['apkSize'],
apkMd5: data['apkMd5'],
appMarkets: (data['appMarkets'] as List?)
?.map((e) => AppMarket.fromString(e))
.toList(),
supportedMethods: [
AppUpgradeMethod.market,
AppUpgradeMethod.browser,
AppUpgradeMethod.inApp,
],
);
},
); );
} }
``` ```
### 方式二:静默检查 + 由用户决定 ### 方式二:静默检查 + 由用户决定
```dart 先静默检查是否有更新,然后由用户决定是否显示升级对话框:
final info = await AppUpgradeSimple.instance.checkUpdateSilent(
url: 'https://your-api.com/check-update',
params: {'platform': 'android'},
);
if (info != null && info.hasUpdate && context.mounted) { ```dart
// 再次调用一键方法以展示对话框(内部会重新请求一次最新信息) // 注意:当前版本不提供独立的静默检查方法
// 如果需要静默检查,请直接调用您的 API然后手动判断是否需要显示对话框
final response = await http.get('https://your-api.com/check-update');
final data = json.decode(response.body);
// 手动比较版本
final serverVersion = data['versionBuildNumber'];
final currentVersion = await AppUpgradeSimple.instance.getAppInfo();
final currentBuildNumber = int.parse(currentVersion['buildNumber'] ?? '0');
if (serverVersion > currentBuildNumber) {
// 有新版本,显示升级对话框
await AppUpgradeSimple.instance.checkUpdate( await AppUpgradeSimple.instance.checkUpdate(
context: context, context: context,
url: 'https://your-api.com/check-update', future: () async {
return AppUpgradeVersion.fromJson(data);
},
); );
} }
``` ```
说明:当前公开 API 中未暴露独立的 `showUpgradeDialog()` 方法,静默检查用于“询问后再触发一键流程”的业务场景。
### 常用配置 ### 常用配置
```dart ```dart
// 内置多套配置: // 预设配置:
AppUpgradeSimple.instance.configure(UpgradeConfig.auto); // 自动下载+自动安装 AppUpgradeSimple.instance.configure(UpgradeConfig.development); // 开发模式(详细日志+提示)
AppUpgradeSimple.instance.configure(UpgradeConfig.silent); // 静默检查 AppUpgradeSimple.instance.configure(UpgradeConfig.production); // 生产模式(静默+性能优化)
AppUpgradeSimple.instance.configure(UpgradeConfig.withPermission); // 安装前检查权限(传统)
// 自定义: // 自定义配置
AppUpgradeSimple.instance.configure(const UpgradeConfig( AppUpgradeSimple.instance.configure(const UpgradeConfig(
showNoUpdateToast: true, showNoUpdateToast: true,
autoDownload: true,
autoInstall: false, autoInstall: false,
installTimeout: 60, installTimeout: 60,
enableDebugLog: true,
requireInstallPermission: false, // 默认不需要权限,直接安装
)); ));
``` ```
## 🎨 UI 能力 ## 🎨 UI 能力
- 发现新版本对话框(强制/非强制) - **发现新版本对话框**(强制/非强制)
- 版本信息卡片当前版本、新版本、APK 大小) - 强制更新:不可关闭,必须更新
- 下载进度展示与可重试安装行为 - 非强制更新:可稍后更新,支持后台下载
- Android 上支持“应用市场选择”或“直接下载”两种路径 - **版本信息卡片**显示当前版本、新版本、APK 大小
- **下载进度展示**:实时显示下载进度百分比和状态
- **安装状态检测**:自动检测安装结果,支持重试
- **更新内容展示**:支持 Markdown 格式(粗体、斜体、代码块等)
- **Android 更新方式选择**:应用市场、浏览器下载、应用内下载
## ⚙️ Android 配置 ## ⚙️ Android 配置
@ -101,8 +139,9 @@ AppUpgradeSimple.instance.configure(const UpgradeConfig(
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!-- Android 13+ 通知权限(可选) --> <!-- Android 13+ 通知权限(可选) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- Android 9 写储存权限 sdk是28 否无无法写入内存 --> <!-- Android 9 写储存权限SDK 28 及以下需要) -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application> <application>
<!-- FileProviderAndroid 7.0+ APK 安装必需) --> <!-- FileProviderAndroid 7.0+ APK 安装必需) -->
<provider <provider
@ -118,11 +157,6 @@ AppUpgradeSimple.instance.configure(const UpgradeConfig(
<!-- Android 11+ 查询声明:允许打开浏览器处理 HTTP/HTTPS URL --> <!-- Android 11+ 查询声明:允许打开浏览器处理 HTTP/HTTPS URL -->
<queries> <queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<!-- 允许打开浏览器处理 HTTP/HTTPS 链接(用于"前往浏览器下载"功能) -->
<intent> <intent>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<data android:scheme="https" /> <data android:scheme="https" />
@ -153,7 +187,6 @@ AppUpgradeSimple.instance.configure(const UpgradeConfig(
<!-- Downloads directory --> <!-- Downloads directory -->
<external-path name="downloads" path="Download/" /> <external-path name="downloads" path="Download/" />
<!-- External app-specific files directory (Android/data/包名/files/) --> <!-- External app-specific files directory (Android/data/包名/files/) -->
<!-- This is accessible without WRITE_EXTERNAL_STORAGE permission on Android 10+ -->
<!-- 用于权限被拒绝时,使用应用私有目录存储下载的 APK --> <!-- 用于权限被拒绝时,使用应用私有目录存储下载的 APK -->
<external-files-path name="external_app_files" path="." /> <external-files-path name="external_app_files" path="." />
</paths> </paths>
@ -179,7 +212,7 @@ dependencies {
## 📡 服务端返回协议 ## 📡 服务端返回协议
代码模型 `UpgradeInfo` 需要以下字段(关键字段必须): 服务端需要返回包含以下字段的 JSON(关键字段必须):
```json ```json
{ {
@ -191,14 +224,73 @@ dependencies {
"appStoreUrl": "https://apps.apple.com/app/id123456789", "appStoreUrl": "https://apps.apple.com/app/id123456789",
"apkSize": 25165824, "apkSize": 25165824,
"apkMd5": "d41d8cd98f00b204e9800998ecf8427e", "apkMd5": "d41d8cd98f00b204e9800998ecf8427e",
"appMarkets": [ "appMarkets": ["huawei", "xiaomi", "oppo"],
{"name":"华为应用市场","packageName":"com.huawei.appmarket","url":"appmarket://details?id=com.yourapp.package"} "supportedMethods": ["market", "browser", "inApp"]
]
} }
``` ```
- `versionBuildNumber``versionName` 为必填,插件会与当前应用版本进行对比自动计算 `hasUpdate` **字段说明:**
- `downloadUrl` 仅 Android 需要;`appStoreUrl` 仅 iOS 使用。
- `versionBuildNumber`(必填):版本号(整数),用于版本比较
- `versionName`(必填):版本名称(字符串),如 "1.0.1"
- `isForceUpdate`(可选):是否强制更新,默认 `false`
- `updateContent`(可选):更新内容说明,支持换行和 Markdown 格式
- `downloadUrl`Android 必填APK 下载地址
- `appStoreUrl`iOS 必填App Store 链接
- `apkSize`可选APK 文件大小(字节)
- `apkMd5`可选APK 文件的 MD5 值,用于校验
- `appMarkets`(可选):应用市场白名单,见下方说明
- `supportedMethods`(可选):支持的更新方式,默认全部支持
**版本比较逻辑:**
插件会优先比较 `versionBuildNumber`,如果相同则比较 `versionName`。如果服务端版本大于当前版本,则判定为有新版本。
### 应用市场白名单 (appMarkets)
`appMarkets` 用于限制支持的应用市场Android作为白名单使用。配置后插件会
1. **检测设备已安装的应用市场**
2. **匹配白名单**:只允许跳转到白名单中且设备已安装的应用市场
3. **智能处理**
- 如果设备上没有白名单中的任何应用市场,提示用户"不支持当前设备的应用市场",引导用户选择其他更新方式(如浏览器下载、应用内下载)
- 如果设备有白名单中的应用市场,直接跳转到设备默认应用市场
**配置示例:**
```dart
// Dart 代码
appMarkets: [AppMarket.huawei, AppMarket.xiaomi, AppMarket.oppo]
// 服务端返回
"appMarkets": ["huawei", "xiaomi", "oppo"]
```
**支持的应用市场类型:**
| 值 | 显示名称 | 对应包名 |
|---|---------|---------|
| `googlePlay` | Google Play | `com.android.vending` |
| `huawei` | 华为应用市场 | `com.huawei.appmarket` |
| `xiaomi` | 小米应用商店 | `com.xiaomi.market` |
| `oppo` | OPPO软件商店 | `com.oppo.market` |
| `vivo` | vivo应用商店 | `com.bbk.appstore` |
| `tencent` | 腾讯应用宝 | `com.tencent.android.qqdownloader` |
| `coolapk` | 酷安 | `com.coolapk.market` |
**不配置 appMarkets 的行为:**
如果不配置 `appMarkets`,插件会使用默认行为:跳转到设备的默认应用市场(使用 `market://` 协议)。
### 支持的更新方式 (supportedMethods)
`supportedMethods` 用于指定支持的更新方式,可选值:
- `market`:应用市场更新
- `browser`:浏览器下载
- `inApp`:应用内下载
如果不配置,默认支持所有方式。如果只配置一种方式,将直接使用该方式,不会显示选择对话框。
## 🔧 进阶能力 ## 🔧 进阶能力
@ -208,7 +300,13 @@ dependencies {
// 自动选择Debug 绕过证书、Release 严格校验) // 自动选择Debug 绕过证书、Release 严格校验)
AppUpgradePlugin().configureHttp(HttpConfig.auto); AppUpgradePlugin().configureHttp(HttpConfig.auto);
// 或手动: // 开发环境配置(绕过证书验证)
AppUpgradePlugin().configureHttp(HttpConfig.development);
// 生产环境配置(严格证书验证)
AppUpgradePlugin().configureHttp(HttpConfig.production);
// 或手动配置:
AppUpgradePlugin().configureHttp(const HttpConfig( AppUpgradePlugin().configureHttp(const HttpConfig(
ignoreCertificate: false, ignoreCertificate: false,
enableLog: true, enableLog: true,
@ -224,14 +322,25 @@ AppUpgradePlugin().configureHttp(const HttpConfig(
```dart ```dart
import 'package:app_upgrade_plugin/core/permission_helper.dart'; import 'package:app_upgrade_plugin/core/permission_helper.dart';
// 检查并请求存储权限
final hasStorage = await PermissionHelper.checkAndRequestStoragePermission(context: context); final hasStorage = await PermissionHelper.checkAndRequestStoragePermission(context: context);
// 检查并请求安装权限
final hasInstall = await PermissionHelper.checkAndRequestInstallPermission(context: context); final hasInstall = await PermissionHelper.checkAndRequestInstallPermission(context: context);
// 检查并请求通知权限
final hasNotification = await PermissionHelper.checkAndRequestNotificationPermission(context: context); final hasNotification = await PermissionHelper.checkAndRequestNotificationPermission(context: context);
// 精确跳转安装权限设置 // 精确跳转安装权限设置
await AppUpgradePlugin().openInstallPermissionSettings(); await AppUpgradePlugin().openInstallPermissionSettings();
``` ```
**权限处理说明:**
- **存储权限**Android 13+ 无需权限使用应用私有目录Android 10-12 会尝试请求但失败时使用私有目录Android 9 及以下需要权限
- **安装权限**:默认情况下(`requireInstallPermission: false`),插件会直接调用系统安装流程,由系统处理权限检查。如果设置为 `true`,会在安装前检查权限
- **通知权限**Android 13+ 需要,用于显示下载进度通知
### 3) 安装策略Android ### 3) 安装策略Android
```dart ```dart
@ -248,48 +357,104 @@ await AppUpgradePlugin().installApkWithConfig(
); );
``` ```
策略对比: **策略对比:**
- `systemFlow`:系统处理权限检查与确认,体验最佳 - `systemFlow`:系统处理权限检查与确认,体验最佳(推荐)
- `preCheckPermission`:先检查权限,无权时返回错误,便于精细控制 - `preCheckPermission`:先检查权限,无权时返回错误,便于精细控制
- `smart`:有权用预检查,无权走系统流程 - `smart`:有权用预检查,无权走系统流程
### 4) 预下载 APK
```dart
// 后台预下载 APK不显示 UI
final apkPath = await AppUpgradeSimple.instance.preDownloadApk(
url: 'https://your-cdn.com/app-release.apk',
onProgress: (progress) {
print('下载进度: ${progress.percentage}%');
},
);
if (apkPath != null) {
print('下载完成: $apkPath');
}
```
### 5) 查找已下载的 APK
```dart
// 查找指定版本的已下载 APK
final apkPath = await AppUpgradeSimple.instance.findDownloadedApk('1.0.1');
if (apkPath != null) {
// 直接安装
await AppUpgradePlugin().installApkWithSystemFlow(apkPath);
}
```
### 6) 清理下载缓存
```dart
// 清理所有已下载的 APK 文件
await AppUpgradeSimple.instance.clearDownloadCache();
```
## 🐛 常见问题 ## 🐛 常见问题
- 安装失败显示"解析包时出现问题":检查 APK 完整性、签名与架构匹配 - **安装失败显示"解析包时出现问题"**:检查 APK 完整性、签名与架构匹配
- 权限申请失败:确认 Manifest 权限、FileProvider 配置、在 MaterialApp 环境调用 - **权限申请失败**:确认 Manifest 权限、FileProvider 配置、在 MaterialApp 环境调用
- 下载失败/进度不更新:检查网络、下载 URL 可用性、服务端是否支持断点 - **下载失败/进度不更新**:检查网络、下载 URL 可用性、服务端是否支持断点续传
- iOS 不跳转:确认 `appStoreUrl` 为有效的 App Store 链接 - **iOS 不跳转**:确认 `appStoreUrl` 为有效的 App Store 链接
- "前往浏览器下载"无反应:确认已在 AndroidManifest.xml 中添加 `<queries>` 声明Android 11+ 必需) - **"前往浏览器下载"无反应**:确认已在 AndroidManifest.xml 中添加 `<queries>` 声明Android 11+ 必需)
- FileProvider 配置错误:确认 `file_paths.xml` 中已添加 `<external-files-path>` 配置,用于权限被拒绝时的备用存储路径 - **FileProvider 配置错误**:确认 `file_paths.xml` 中已添加 `<external-files-path>` 配置,用于权限被拒绝时的备用存储路径
- Android 9 下载权限错误:插件会自动检测权限,无权限时使用应用私有目录,无需额外配置 - **Android 9 下载权限错误**:插件会自动检测权限,无权限时使用应用私有目录,无需额外配置
- **安装检测超时**:默认超时时间为 45 秒,可通过 `UpgradeConfig.installTimeout` 调整
## 📚 主要 API 清单 ## 📚 主要 API 清单
### AppUpgradeSimple ### AppUpgradeSimple(推荐使用)
- `configure(UpgradeConfig)`:配置升级参数 - `configure(UpgradeConfig)`:配置升级参数
- `checkUpdate({context, url, params, ...})`:检查更新并显示 UI - `checkUpdate({context, future, showNoUpdateToast, autoInstall, onComplete, config})`:检查更新并显示 UI
- `checkUpdateSilent({url, params})`:静默检查(不显示 UI
- `preDownloadApk({url, onProgress})`:预下载 APKAndroid - `preDownloadApk({url, onProgress})`:预下载 APKAndroid
- `findDownloadedApk(version)`:查找已下载的 APKAndroid - `findDownloadedApk(version)`:查找已下载的 APKAndroid
- `getAppInfo()`:获取当前应用信息 - `getAppInfo()`:获取当前应用信息
- `clearDownloadCache()`:清理下载缓存
- `checkNetworkStatus()`:检查网络连接状态
### AppUpgradePlugin底层能力 ### AppUpgradePlugin底层能力
- `configureHttp(HttpConfig)`:网络层配置 - `configureHttp(HttpConfig)`:网络层配置
- `downloadApk(url, onProgress)`:下载 APKAndroid - `checkUpdate(url, {params})`:检查更新(返回 UpgradeInfo
- `installApk(filePath)` / `installApkWithSystemFlow(filePath)` / `installApkWithConfig(filePath, config)`Android - `downloadApk(url, {onProgress, savePath})`:下载 APKAndroid
- `installApk(filePath)`:安装 APK需要先处理权限
- `installApkWithSystemFlow(filePath)`:使用系统流程安装(推荐)
- `installApkWithConfig(filePath, {config})`:按配置策略安装
- `openInstallPermissionSettings()`跳转安装权限设置Android - `openInstallPermissionSettings()`跳转安装权限设置Android
- `getDeviceInfo()`、`getAndroidSdkVersion()`Android - `getDeviceInfo()`、`getAndroidSdkVersion()`获取设备信息Android
- `goToAppStore(url)`:跳转到应用商店 - `goToAppStore(url, {context})`:跳转到应用商店
- `checkMarketAvailable({packageName, marketPackage, url})`检查应用市场是否可用Android用于判断设备是否有可用的应用市场 - `getInstalledMarkets()`获取设备已安装的应用市场列表Android
- `getDownloadPath()`:获取下载目录路径
- `checkApkExists(version, md5)`:检查指定版本的 APK 是否已下载
### PermissionHelperAndroid ### PermissionHelperAndroid
- `checkAndRequestStoragePermission(context)` - `checkAndRequestStoragePermission(context)`:检查并请求存储权限
- `checkAndRequestInstallPermission(context)` - `checkAndRequestInstallPermission(context)`:检查并请求安装权限
- `checkAndRequestNotificationPermission(context)` - `checkAndRequestNotificationPermission(context)`:检查并请求通知权限
- `checkInstallPermission()`:检查安装权限状态
### 配置类
- `UpgradeConfig`:升级配置(超时、自动安装、日志等)
- `HttpConfig`HTTP 配置证书、超时、Headers 等)
- `InstallConfig`:安装策略配置
### 模型类
- `AppUpgradeVersion`:服务端返回的版本信息模型
- `UpgradeInfo`:内部使用的升级信息模型
- `AppMarket`:应用市场枚举
- `AppUpgradeMethod`:更新方式枚举
- `DownloadProgress`:下载进度信息
## 🤝 贡献 ## 🤝 贡献

View File

@ -17,16 +17,10 @@ await AppUpgradeSimple.instance.checkUpdate(
### 预设配置 ### 预设配置
```dart ```dart
// 自动更新模式 // 开发模式(详细日志+提示信息)
AppUpgradeSimple.instance.configure(UpgradeConfig.auto);
// 静默检查模式
AppUpgradeSimple.instance.configure(UpgradeConfig.silent);
// 开发模式
AppUpgradeSimple.instance.configure(UpgradeConfig.development); AppUpgradeSimple.instance.configure(UpgradeConfig.development);
// 生产模式 // 生产模式(静默检查+性能优化)
AppUpgradeSimple.instance.configure(UpgradeConfig.production); AppUpgradeSimple.instance.configure(UpgradeConfig.production);
``` ```
@ -34,10 +28,7 @@ AppUpgradeSimple.instance.configure(UpgradeConfig.production);
```dart ```dart
AppUpgradeSimple.instance.configure(UpgradeConfig( AppUpgradeSimple.instance.configure(UpgradeConfig(
showNoUpdateToast: true, // 显示无更新提示 showNoUpdateToast: true, // 显示无更新提示
autoDownload: false, // 自动下载
autoInstall: false, // 自动安装 autoInstall: false, // 自动安装
connectionTimeout: 30, // 连接超时(秒)
downloadTimeout: 300, // 下载超时(秒)
installTimeout: 45, // 安装检测超时(秒) installTimeout: 45, // 安装检测超时(秒)
enableDebugLog: true, // 启用调试日志 enableDebugLog: true, // 启用调试日志
customToast: (message) { // 自定义Toast customToast: (message) { // 自定义Toast
@ -326,10 +317,8 @@ ElevatedButton(
升级配置类,控制插件的行为。 升级配置类,控制插件的行为。
#### 预设配置 #### 预设配置
- `UpgradeConfig.auto` - 自动更新 - `UpgradeConfig.development` - 开发模式(详细日志+提示信息)
- `UpgradeConfig.silent` - 静默检查 - `UpgradeConfig.production` - 生产模式(静默检查+性能优化)
- `UpgradeConfig.development` - 开发模式
- `UpgradeConfig.production` - 生产模式
--- ---

View File

@ -102,12 +102,6 @@ class AppUpgradePlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
result.error("INVALID_ARGUMENT", "URL is null", null) 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" -> { "getAndroidSdkVersion" -> {
result.success(Build.VERSION.SDK_INT) result.success(Build.VERSION.SDK_INT)
} }
@ -117,6 +111,9 @@ class AppUpgradePlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
"getDeviceInfo" -> { "getDeviceInfo" -> {
getDeviceInfo(result) getDeviceInfo(result)
} }
"getInstalledMarkets" -> {
getInstalledMarkets(result)
}
"installApkWithSystemFlow" -> { "installApkWithSystemFlow" -> {
val filePath = call.argument<String>("filePath") val filePath = call.argument<String>("filePath")
if (filePath != null) { if (filePath != null) {
@ -208,55 +205,6 @@ class AppUpgradePlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
activity?.startActivity(intent) 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) { private fun openInstallPermissionSettings(result: Result) {
if (activity == null) { if (activity == null) {
result.error("NO_ACTIVITY", "Activity is not available", null) result.error("NO_ACTIVITY", "Activity is not available", null)
@ -454,6 +402,40 @@ class AppUpgradePlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
result.success(deviceInfo) result.success(deviceInfo)
} }
/// 获取设备上已安装的应用市场类型
/// 返回已安装的应用市场类型列表 (如 ["huawei", "xiaomi"])
private fun getInstalledMarkets(result: Result) {
try {
val pm = context.packageManager
val installedMarkets = mutableListOf<String>()
// 常见应用市场的包名映射
val marketPackages = mapOf(
"com.android.vending" to "googlePlay",
"com.huawei.appmarket" to "huawei",
"com.oppo.market" to "oppo",
"com.bbk.appstore" to "vivo",
"com.xiaomi.market" to "xiaomi",
"com.tencent.android.qqdownloader" to "tencent",
"com.coolapk.market" to "coolapk"
)
// 检查哪些应用市场已安装
for ((packageName, marketType) in marketPackages) {
try {
pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
installedMarkets.add(marketType)
} catch (e: PackageManager.NameNotFoundException) {
// 该应用市场未安装,跳过
}
}
result.success(installedMarkets)
} catch (e: Exception) {
result.error("GET_MARKETS_ERROR", "Failed to get installed markets", e.message)
}
}
private fun installApkWithSystemFlow(filePath: String, result: Result) { private fun installApkWithSystemFlow(filePath: String, result: Result) {
try { try {
val file = File(filePath) val file = File(filePath)

View File

@ -86,7 +86,6 @@ class _HomePageState extends State<HomePage> {
); );
}, },
showNoUpdateToast: false, // "已是最新版本" showNoUpdateToast: false, // "已是最新版本"
autoDownload: false,
autoInstall: true, autoInstall: true,
); );
debugPrint('=== 网络功能测试完成 ==='); debugPrint('=== 网络功能测试完成 ===');
@ -140,7 +139,6 @@ class _HomePageState extends State<HomePage> {
); );
}, },
showNoUpdateToast: false, // "已是最新版本" showNoUpdateToast: false, // "已是最新版本"
autoDownload: false,
autoInstall: false, autoInstall: false,
); );
}, },

View File

@ -1,8 +1,5 @@
import 'dart:io'; import 'dart:io';
// 便
// ignore: unused_import
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'app_upgrade_plugin_platform_interface.dart'; import 'app_upgrade_plugin_platform_interface.dart';
@ -22,8 +19,6 @@ export 'models/app_upgrade_method.dart';
export 'models/app_upgrade_version.dart'; export 'models/app_upgrade_version.dart';
export 'models/install_strategy.dart'; export 'models/install_strategy.dart';
export 'models/upgrade_info.dart'; export 'models/upgrade_info.dart';
// API中
export 'widgets/widgets.dart';
class AppUpgradePlugin { class AppUpgradePlugin {
// //
@ -103,6 +98,17 @@ class AppUpgradePlugin {
return AppUpgradePluginPlatform.instance.getDeviceInfo(); return AppUpgradePluginPlatform.instance.getDeviceInfo();
} }
/// Android
///
/// ["huawei", "xiaomi"]
///
Future<List<String>> getInstalledMarkets() {
if (!Platform.isAndroid) {
return Future.value([]);
}
return AppUpgradePluginPlatform.instance.getInstalledMarkets();
}
/// App信息 /// App信息
Future<Map<String, String>> getAppInfo() { Future<Map<String, String>> getAppInfo() {
return AppUpgradePluginPlatform.instance.getAppInfo(); return AppUpgradePluginPlatform.instance.getAppInfo();

View File

@ -234,6 +234,21 @@ class MethodChannelAppUpgradePlugin extends AppUpgradePluginPlatform {
} }
} }
@override
Future<List<String>> getInstalledMarkets() async {
if (!Platform.isAndroid) return [];
try {
final result = await methodChannel.invokeMethod<List<dynamic>>('getInstalledMarkets');
if (result == null) return [];
return result.map((item) => item.toString()).toList();
} catch (e) {
debugPrint('Failed to get installed markets: $e');
return [];
}
}
@override @override
Future<Map<String, String>> getAppInfo() async { Future<Map<String, String>> getAppInfo() async {
final packageInfo = await PackageInfo.fromPlatform(); final packageInfo = await PackageInfo.fromPlatform();
@ -489,38 +504,25 @@ class MethodChannelAppUpgradePlugin extends AppUpgradePluginPlatform {
Future<bool> goToAppStore(String url, {required BuildContext context}) async { Future<bool> goToAppStore(String url, {required BuildContext context}) async {
try { try {
final uri = Uri.parse(url); final uri = Uri.parse(url);
// https://
final bool flag = await canLaunchUrl(uri); final bool flag = await canLaunchUrl(uri);
if (flag) { if (flag) {
await launchUrl(uri, mode: LaunchMode.externalApplication); await launchUrl(uri, mode: LaunchMode.externalApplication);
return true; return true;
} } else {
debugPrint('当前APP没有上架当前设备对应的应用市场'); // market:// canLaunchUrl 使 false
if (context.mounted) { // false
// Scaffold widget if (uri.scheme == 'market') {
final scaffold = Scaffold.maybeOf(context); try {
if (scaffold == null) { await launchUrl(uri, mode: LaunchMode.externalApplication);
debugPrint('提示无Scaffold: 当前APP没有上架当前设备对应的应用市场'); return true;
} else { } catch (e) {
// 使 maybeOf ScaffoldMessenger debugPrint('无法打开应用市场: $url, 错误: $e');
final scaffoldMessenger = ScaffoldMessenger.maybeOf(context);
if (scaffoldMessenger == null) {
debugPrint('提示无ScaffoldMessenger: 当前APP没有上架当前设备对应的应用市场');
} else {
// 使 try-catch
try {
scaffoldMessenger.showSnackBar(
SnackBar(
content: Text('当前APP没有上架当前设备对应的应用市场'),
duration: Duration(seconds: 2),
),
);
} catch (e) {
debugPrint('显示提示失败: $e');
}
} }
} }
} }
// Fluttertoast.showToast(msg: '当前APP没有上架当前设备对应的应用市场'); debugPrint('无法打开URL: $url');
return false; return false;
} catch (e) { } catch (e) {
debugPrint('跳转应用商店失败: $e'); debugPrint('跳转应用商店失败: $e');
@ -704,86 +706,6 @@ class MethodChannelAppUpgradePlugin extends AppUpgradePluginPlatform {
return result ?? false; 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) {
try {
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;
}
}
/// URL的连接性并返回错误信息 /// URL的连接性并返回错误信息
Future<String?> testDownloadUrlWithError(String url) async { Future<String?> testDownloadUrlWithError(String url) async {
try { try {

View File

@ -45,6 +45,12 @@ abstract class AppUpgradePluginPlatform extends PlatformInterface {
throw UnimplementedError('getDeviceInfo() has not been implemented.'); throw UnimplementedError('getDeviceInfo() has not been implemented.');
} }
/// Android
/// ["huawei", "xiaomi"]
Future<List<String>> getInstalledMarkets() {
throw UnimplementedError('getInstalledMarkets() has not been implemented.');
}
/// HTTP设置 /// HTTP设置
void configureHttp(HttpConfig config) { void configureHttp(HttpConfig config) {
throw UnimplementedError('configureHttp() has not been implemented.'); throw UnimplementedError('configureHttp() has not been implemented.');
@ -99,19 +105,4 @@ abstract class AppUpgradePluginPlatform extends PlatformInterface {
Future<bool> checkApkExists(String version, String? md5) { Future<bool> checkApkExists(String version, String? md5) {
throw UnimplementedError('checkApkExists() has not been implemented.'); 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

@ -10,7 +10,6 @@ import 'core/permission_helper.dart';
import 'models/app_upgrade_method.dart'; import 'models/app_upgrade_method.dart';
import 'models/app_upgrade_version.dart'; import 'models/app_upgrade_version.dart';
import 'models/upgrade_info.dart'; import 'models/upgrade_info.dart';
import 'widgets/market_selection_dialog.dart';
/// ///
class _SimpleAppUpgradePlugin { class _SimpleAppUpgradePlugin {
@ -40,6 +39,11 @@ class _SimpleAppUpgradePlugin {
return AppUpgradePluginPlatform.instance.getAppInfo(); return AppUpgradePluginPlatform.instance.getAppInfo();
} }
///
Future<List<String>> getInstalledMarkets() {
return AppUpgradePluginPlatform.instance.getInstalledMarkets();
}
/// ///
Future<String?> getDownloadPath() { Future<String?> getDownloadPath() {
return AppUpgradePluginPlatform.instance.getDownloadPath(); return AppUpgradePluginPlatform.instance.getDownloadPath();
@ -51,18 +55,9 @@ class UpgradeConfig {
/// ///
final bool showNoUpdateToast; final bool showNoUpdateToast;
///
final bool autoDownload;
/// ///
final bool autoInstall; final bool autoInstall;
///
final int connectionTimeout;
///
final int downloadTimeout;
/// ///
final int installTimeout; final int installTimeout;
@ -77,51 +72,42 @@ class UpgradeConfig {
const UpgradeConfig({ const UpgradeConfig({
this.showNoUpdateToast = true, this.showNoUpdateToast = true,
this.autoDownload = false,
this.autoInstall = false, this.autoInstall = false,
this.connectionTimeout = 30,
this.downloadTimeout = 300,
this.installTimeout = 45, this.installTimeout = 45,
this.enableDebugLog = true, this.enableDebugLog = true,
this.customToast, this.customToast,
this.requireInstallPermission = false, // this.requireInstallPermission = false, //
}); });
/// ///
static const UpgradeConfig auto = UpgradeConfig( ///
autoDownload: true, ///
autoInstall: true, /// - 便
requireInstallPermission: false, /// - 便
); /// - 30
/// - 便
///
static const UpgradeConfig silent = UpgradeConfig(
showNoUpdateToast: false,
enableDebugLog: false,
requireInstallPermission: false,
);
/// +
static const UpgradeConfig development = UpgradeConfig( static const UpgradeConfig development = UpgradeConfig(
showNoUpdateToast: true,
autoInstall: true,
installTimeout: 45,
enableDebugLog: true, enableDebugLog: true,
installTimeout: 30,
connectionTimeout: 10,
requireInstallPermission: false, requireInstallPermission: false,
); );
/// + ///
///
///
/// -
/// -
/// -
/// - 60
static const UpgradeConfig production = UpgradeConfig( static const UpgradeConfig production = UpgradeConfig(
showNoUpdateToast: false, showNoUpdateToast: false,
autoInstall: true,
installTimeout: 45,
enableDebugLog: false, enableDebugLog: false,
installTimeout: 60,
connectionTimeout: 30,
requireInstallPermission: false, requireInstallPermission: false,
); );
///
static const UpgradeConfig withPermission = UpgradeConfig(
requireInstallPermission: true,
);
} }
/// App升级管理器 /// App升级管理器
@ -153,16 +139,6 @@ class AppUpgradeSimple {
_config = config; _config = config;
} }
///
void enableAutoUpdate() {
_config = UpgradeConfig.auto;
}
///
void enableSilentCheck() {
_config = UpgradeConfig.silent;
}
/// ///
Future<bool> checkNetworkStatus() async { Future<bool> checkNetworkStatus() async {
try { try {
@ -208,15 +184,10 @@ class AppUpgradeSimple {
/// - true: /// - true:
/// - false: /// - false:
/// - null: 使 [config] [UpgradeConfig.showNoUpdateToast] /// - null: 使 [config] [UpgradeConfig.showNoUpdateToast]
/// - [autoDownload] () APK
/// - true:
/// - false: "立即更新"
/// - null: 使 [config] [UpgradeConfig.autoDownload]
/// - [autoInstall] () APK /// - [autoInstall] () APK
/// - true: /// - true:
/// - false: /// - false:
/// - null: 使 [config] [UpgradeConfig.autoInstall] /// - null: 使 [config] [UpgradeConfig.autoInstall]
/// - [autoDownload] true
/// - [onComplete] () bool /// - [onComplete] () bool
/// - true: /// - true:
/// - false: /// - false:
@ -238,7 +209,6 @@ class AppUpgradeSimple {
/// return AppUpgradeVersion.fromJson(json.decode(response.body)); /// return AppUpgradeVersion.fromJson(json.decode(response.body));
/// }, /// },
/// showNoUpdateToast: true, /// showNoUpdateToast: true,
/// autoDownload: false,
/// autoInstall: false, /// autoInstall: false,
/// onComplete: (success) { /// onComplete: (success) {
/// print('检查更新完成,结果: $success'); /// print('检查更新完成,结果: $success');
@ -247,7 +217,7 @@ class AppUpgradeSimple {
/// ``` /// ```
/// ///
/// ///
/// 1. [showNoUpdateToast], [autoDownload], [autoInstall] /// 1. [showNoUpdateToast], [autoInstall]
/// 2. [config] /// 2. [config]
/// 3. [configure] /// 3. [configure]
/// 4. /// 4.
@ -255,7 +225,6 @@ class AppUpgradeSimple {
required BuildContext context, required BuildContext context,
required Future<AppUpgradeVersion?> Function() future, required Future<AppUpgradeVersion?> Function() future,
bool? showNoUpdateToast, bool? showNoUpdateToast,
bool? autoDownload,
bool? autoInstall, bool? autoInstall,
BoolCallback? onComplete, BoolCallback? onComplete,
UpgradeConfig? config, UpgradeConfig? config,
@ -263,7 +232,6 @@ class AppUpgradeSimple {
// 使 // 使
final effectiveConfig = config ?? _config; final effectiveConfig = config ?? _config;
final finalShowNoUpdateToast = showNoUpdateToast ?? effectiveConfig.showNoUpdateToast; final finalShowNoUpdateToast = showNoUpdateToast ?? effectiveConfig.showNoUpdateToast;
final finalAutoDownload = autoDownload ?? effectiveConfig.autoDownload;
final finalAutoInstall = autoInstall ?? effectiveConfig.autoInstall; final finalAutoInstall = autoInstall ?? effectiveConfig.autoInstall;
try { try {
assert(_canShowMaterialDialog(context), '请在 MaterialApp 环境内调用'); assert(_canShowMaterialDialog(context), '请在 MaterialApp 环境内调用');
@ -344,7 +312,6 @@ class AppUpgradeSimple {
await _showUpgradeDialog( await _showUpgradeDialog(
context: context, context: context,
info: info, info: info,
autoDownload: finalAutoDownload,
autoInstall: finalAutoInstall, autoInstall: finalAutoInstall,
onComplete: onComplete, onComplete: onComplete,
config: effectiveConfig, config: effectiveConfig,
@ -406,61 +373,6 @@ class AppUpgradeSimple {
return await _plugin.getAppInfo(); return await _plugin.getAppInfo();
} }
///
Future<Map<String, String>> getCurrentAppInfo() async {
try {
return await _plugin.getAppInfo();
} catch (e) {
debugPrint('获取应用信息失败: $e');
return {};
}
}
///
Future<bool> isPackageInstalled(String packageName) async {
try {
final appInfo = await getAppInfo();
final currentPackage = appInfo['packageName'] ?? '';
debugPrint('检查包安装状态: 当前包名=$currentPackage, 目标包名=$packageName');
if (currentPackage == packageName) {
return true;
}
return false;
} catch (e) {
debugPrint('检查包安装状态失败: $e');
return false;
}
}
///
Future<bool> isVersionUpdated(String targetVersion, int? targetBuildNumber) async {
try {
final appInfo = await getCurrentAppInfo();
final currentVersion = appInfo['version'] ?? '';
final currentBuildNumber = int.tryParse(appInfo['buildNumber'] ?? '0') ?? 0;
debugPrint('版本对比: 当前版本=$currentVersion, 目标版本=$targetVersion');
debugPrint('构建号对比: 当前构建号=$currentBuildNumber, 目标构建号=$targetBuildNumber');
if (targetBuildNumber != null && targetBuildNumber > 0) {
if (currentBuildNumber < targetBuildNumber) {
return false;
} else if (currentBuildNumber > targetBuildNumber) {
return true;
}
return _compareVersionStrings(currentVersion, targetVersion) >= 0;
}
return _compareVersionStrings(currentVersion, targetVersion) >= 0;
} catch (e) {
debugPrint('版本对比失败: $e');
return false;
}
}
int _compareVersionStrings(String v1, String v2) { int _compareVersionStrings(String v1, String v2) {
try { try {
final v1Parts = v1.split('.').map((e) => int.tryParse(e) ?? 0).toList(); final v1Parts = v1.split('.').map((e) => int.tryParse(e) ?? 0).toList();
@ -489,7 +401,6 @@ class AppUpgradeSimple {
Future<void> _showUpgradeDialog({ Future<void> _showUpgradeDialog({
required BuildContext context, required BuildContext context,
required UpgradeInfo info, required UpgradeInfo info,
required bool autoDownload,
required bool autoInstall, required bool autoInstall,
BoolCallback? onComplete, BoolCallback? onComplete,
UpgradeConfig? config, UpgradeConfig? config,
@ -503,12 +414,12 @@ class AppUpgradeSimple {
if (info.isForceUpdate) { if (info.isForceUpdate) {
return _ForceUpgradeDialog( return _ForceUpgradeDialog(
info: info, info: info,
autoInstall: autoInstall,
config: effectiveConfig, config: effectiveConfig,
); );
} else { } else {
return _SimpleUpgradeDialog( return _SimpleUpgradeDialog(
info: info, info: info,
autoDownload: autoDownload,
autoInstall: autoInstall, autoInstall: autoInstall,
onComplete: onComplete, onComplete: onComplete,
config: effectiveConfig, config: effectiveConfig,
@ -641,18 +552,11 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
UpgradeInfo get info; UpgradeInfo get info;
void Function(String) get showToast; void Function(String) get showToast;
BoolCallback? get onComplete; BoolCallback? get onComplete;
bool get autoDownload;
bool get autoInstall; bool get autoInstall;
UpgradeConfig get config; UpgradeConfig get config;
void initUpgradeLogic() { void initUpgradeLogic() {
if (autoDownload && Platform.isAndroid) { //
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_startDownloadAndInstall();
}
});
}
} }
void disposeUpgradeLogic() { void disposeUpgradeLogic() {
@ -1640,19 +1544,22 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
} }
} }
void _handleAction() { Future<void> _handleAction() async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
_handleAndroidAction(); await _handleAndroidAction();
} else if (Platform.isIOS) { } else if (Platform.isIOS) {
_handleIosAction(context); await _handleIosAction(context);
} else { } else {
showToast('Unsupported platform'); showToast('Unsupported platform');
} }
} }
void _handleIosAction(BuildContext context) { Future<void> _handleIosAction(BuildContext context) async {
if (info.appStoreUrl != null) { if (info.appStoreUrl != null) {
_plugin.goToAppStore(info.appStoreUrl!, context: context); final success = await _plugin.goToAppStore(info.appStoreUrl!, context: context);
if (!success) {
showToast('无法打开App Store请稍后重试');
}
// //
onComplete?.call(true); onComplete?.call(true);
} else { } else {
@ -1704,24 +1611,43 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
} }
Future<void> _performMarketAction() async { Future<void> _performMarketAction() async {
final hasMarkets = info.appMarkets?.isNotEmpty ?? false; //
if (hasMarkets) { final installedMarkets = await _plugin.getInstalledMarkets();
await MarketSelectionDialog.show( debugPrint('设备已安装的应用市场: $installedMarkets');
context,
markets: info.appMarkets!, final hasWhitelist = info.appMarkets?.isNotEmpty ?? false;
onSelected: (market) {
_plugin.goToAppStore(market.url ?? market.packageName ?? '', context: context); if (hasWhitelist) {
}, debugPrint('配置的应用市场白名单: ${info.appMarkets}');
);
} else { //
final appInfo = await _plugin.getAppInfo(); final availableMarkets = info.appMarkets!.where((market) => installedMarkets.contains(market.name)).toList();
final pkg = appInfo['packageName'] ?? '';
if (pkg.isNotEmpty) { debugPrint('可用的应用市场: $availableMarkets');
_plugin.goToAppStore('market://details?id=$pkg', context: context);
// _plugin.goToAppStore('market://details?id=com.yuanxuan.learningOfficerOa'); if (availableMarkets.isEmpty) {
} else { //
showToast('Could not determine app package name.'); showToast('当前设备的应用市场不在支持列表中,请选择其他方式更新');
return;
} }
} else {
//
if (installedMarkets.isEmpty) {
showToast('当前设备未安装应用市场');
return;
}
}
// 使 market://
final appInfo = await _plugin.getAppInfo();
final pkg = appInfo['packageName'] ?? '';
if (pkg.isNotEmpty) {
final success = await _plugin.goToAppStore('market://details?id=$pkg', context: context);
if (!success) {
showToast('当前APP没有上架当前设备对应的应用市场请选择其他方式更新');
}
} else {
showToast('无法获取应用包名');
} }
} }
@ -1787,21 +1713,21 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
if (availableMethods.contains(AppUpgradeMethod.market)) if (availableMethods.contains(AppUpgradeMethod.market))
ListTile( ListTile(
leading: const Icon(Icons.storefront_outlined), leading: const Icon(Icons.storefront_outlined),
title: const Text('前往应用市场更新'), title: const Text('前往应用市场更新', style: TextStyle(fontSize: 16)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.market), onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.market),
), ),
if (availableMethods.contains(AppUpgradeMethod.inApp)) if (availableMethods.contains(AppUpgradeMethod.inApp))
ListTile( ListTile(
leading: const Icon(Icons.system_update), leading: const Icon(Icons.system_update),
title: const Text('APP内更新'), title: const Text('APP内更新', style: TextStyle(fontSize: 16)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.inApp), onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.inApp),
), ),
if (availableMethods.contains(AppUpgradeMethod.browser)) if (availableMethods.contains(AppUpgradeMethod.browser))
ListTile( ListTile(
leading: const Icon(Icons.download_for_offline_outlined), leading: const Icon(Icons.download_for_offline_outlined),
title: const Text('前往浏览器下载安装包'), title: const Text('前往浏览器下载安装包', textAlign: TextAlign.left, style: TextStyle(fontSize: 16)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.browser), onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.browser),
), ),
@ -1851,7 +1777,6 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
class _SimpleUpgradeDialog extends StatefulWidget { class _SimpleUpgradeDialog extends StatefulWidget {
final UpgradeInfo info; final UpgradeInfo info;
final bool autoDownload;
final bool autoInstall; final bool autoInstall;
final BoolCallback? onComplete; final BoolCallback? onComplete;
final void Function(String) showToast; final void Function(String) showToast;
@ -1859,7 +1784,6 @@ class _SimpleUpgradeDialog extends StatefulWidget {
const _SimpleUpgradeDialog({ const _SimpleUpgradeDialog({
required this.info, required this.info,
required this.autoDownload,
required this.autoInstall, required this.autoInstall,
this.onComplete, this.onComplete,
required this.showToast, required this.showToast,
@ -1878,8 +1802,6 @@ class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _Upgrad
@override @override
BoolCallback? get onComplete => widget.onComplete; BoolCallback? get onComplete => widget.onComplete;
@override @override
bool get autoDownload => widget.autoDownload;
@override
bool get autoInstall => widget.autoInstall; bool get autoInstall => widget.autoInstall;
@override @override
UpgradeConfig get config => widget.config; UpgradeConfig get config => widget.config;
@ -1986,10 +1908,12 @@ class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _Upgrad
class _ForceUpgradeDialog extends StatefulWidget { class _ForceUpgradeDialog extends StatefulWidget {
final UpgradeInfo info; final UpgradeInfo info;
final bool autoInstall;
final UpgradeConfig config; final UpgradeConfig config;
const _ForceUpgradeDialog({ const _ForceUpgradeDialog({
required this.info, required this.info,
required this.autoInstall,
required this.config, required this.config,
}); });
@ -2006,9 +1930,7 @@ class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> with _UpgradeD
@override @override
BoolCallback? get onComplete => null; BoolCallback? get onComplete => null;
@override @override
bool get autoDownload => false; bool get autoInstall => widget.autoInstall;
@override
bool get autoInstall => true;
@override @override
UpgradeConfig get config => widget.config; UpgradeConfig get config => widget.config;
@ -2053,18 +1975,10 @@ class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> with _UpgradeD
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon(
Icons.warning_amber_rounded,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 8),
const Expanded( const Expanded(
child: Text( child: Text(
'发现新版本 (强制)', '发现新版本',
style: TextStyle( style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
fontSize: 18,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),

View File

@ -1,56 +1,3 @@
///
class AppMarketInfo extends Object {
///
final AppMarket market;
///
final String? url;
///
final String? packageName;
/// market custom 使
final String? customName;
AppMarketInfo({
required this.market,
this.url,
this.packageName,
this.customName,
});
/// JSON创建
factory AppMarketInfo.fromJson(Map<String, dynamic> json) {
return AppMarketInfo(
market: AppMarket.fromString(json['market']),
url: json['url'],
packageName: json['packageName'],
customName: json['customName'],
);
}
/// JSON
Map<String, dynamic> toJson() => {
'market': market.name,
'url': url,
'packageName': packageName,
'customName': customName,
};
///
String get marketName {
if (market == AppMarket.custom && customName != null) {
return customName!;
}
return market.displayName;
}
@override
String toString() {
return 'AppMarketInfo{market: $market, url: $url, packageName: $packageName, customName: $customName}';
}
}
/// ///
enum AppMarket { enum AppMarket {
googlePlay, googlePlay,

View File

@ -28,8 +28,10 @@ class AppUpgradeVersion {
/// APK文件的MD5值 () /// APK文件的MD5值 ()
final String? apkMd5; final String? apkMd5;
/// (Android多渠道更新) /// (Android多渠道更新)
final List<AppMarketInfo>? appMarkets; ///
///
final List<AppMarket>? appMarkets;
/// (null使) /// (null使)
final List<AppUpgradeMethod>? supportedMethods; final List<AppUpgradeMethod>? supportedMethods;

View File

@ -36,8 +36,10 @@ class UpgradeInfo {
/// APK MD5值 /// APK MD5值
final String? apkMd5; final String? apkMd5;
/// Android多渠道更新 /// Android多渠道更新
final List<AppMarketInfo>? appMarkets; ///
///
final List<AppMarket>? appMarkets;
/// ///
final List<AppUpgradeMethod> supportedMethods; final List<AppUpgradeMethod> supportedMethods;
@ -112,9 +114,7 @@ class UpgradeInfo {
appStoreUrl: json['appStoreUrl'] as String?, appStoreUrl: json['appStoreUrl'] as String?,
apkSize: json['apkSize'] as int?, apkSize: json['apkSize'] as int?,
apkMd5: json['apkMd5'] as String?, apkMd5: json['apkMd5'] as String?,
appMarkets: (json['appMarkets'] as List<dynamic>?) appMarkets: (json['appMarkets'] as List<dynamic>?)?.map((e) => AppMarket.fromString(e as String)).toList(),
?.map((e) => AppMarketInfo.fromJson(e as Map<String, dynamic>))
.toList(),
supportedMethods: supportedMethods, supportedMethods: supportedMethods,
); );
} }
@ -132,7 +132,7 @@ class UpgradeInfo {
'appStoreUrl': appStoreUrl, 'appStoreUrl': appStoreUrl,
'apkSize': apkSize, 'apkSize': apkSize,
'apkMd5': apkMd5, 'apkMd5': apkMd5,
'appMarkets': appMarkets?.map((e) => e.toJson()).toList(), 'appMarkets': appMarkets?.map((e) => e.name).toList(),
'supportedMethods': supportedMethods.map((e) => e.name).toList(), 'supportedMethods': supportedMethods.map((e) => e.name).toList(),
}; };
} }

View File

@ -1,74 +0,0 @@
import 'package:flutter/material.dart';
import '../models/app_market.dart';
///
class MarketSelectionDialog extends StatelessWidget {
final List<AppMarketInfo> markets;
final ValueChanged<AppMarketInfo> onSelected;
const MarketSelectionDialog({
super.key,
required this.markets,
required this.onSelected,
});
static Future<void> show(
BuildContext context, {
required List<AppMarketInfo> markets,
required ValueChanged<AppMarketInfo> onSelected,
}) async {
await showDialog(
context: context,
useRootNavigator: true,
builder: (context) => MarketSelectionDialog(markets: markets, onSelected: onSelected),
);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: const Text('选择应用商店'),
content: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 360),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: markets.map((market) {
return ListTile(
leading: _getMarketIcon(market.market),
title: Text(market.marketName),
onTap: () {
Navigator.of(context).pop();
onSelected(market);
},
);
}).toList(),
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
],
);
}
Widget _getMarketIcon(AppMarket market) {
//
// 使
switch (market) {
case AppMarket.googlePlay:
return const Icon(Icons.shop, color: Colors.green);
case AppMarket.huawei:
return const Icon(Icons.shop, color: Colors.red);
case AppMarket.tencent:
return const Icon(Icons.shop, color: Colors.blue);
default:
return const Icon(Icons.store);
}
}
}

View File

@ -1,3 +0,0 @@
// app_upgrade_simple.dart
//
export 'market_selection_dialog.dart';

View File

@ -1,368 +0,0 @@
import 'package:app_upgrade_plugin/app_upgrade_plugin_platform_interface.dart';
import 'package:app_upgrade_plugin/app_upgrade_simple.dart';
import 'package:app_upgrade_plugin/models/app_market.dart';
import 'package:app_upgrade_plugin/models/app_upgrade_method.dart';
import 'package:app_upgrade_plugin/models/app_upgrade_version.dart';
import 'package:app_upgrade_plugin/models/upgrade_info.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
// Mock Platform Interface
class MockAppUpgradePluginPlatform extends AppUpgradePluginPlatform with MockPlatformInterfaceMixin {
Map<String, String> _appInfo = {};
UpgradeInfo? _checkUpdateResult;
bool downloadApkCalled = false;
bool goToAppStoreCalled = false;
String? lastUrl;
void setAppInfo(Map<String, String> info) {
_appInfo = info;
}
void setCheckUpdateResult(UpgradeInfo? info) {
_checkUpdateResult = info;
}
void reset() {
downloadApkCalled = false;
goToAppStoreCalled = false;
lastUrl = null;
}
@override
Future<Map<String, String>> getAppInfo() async {
return _appInfo;
}
@override
Future<UpgradeInfo?> checkUpdate(String url, {Map<String, dynamic>? params}) async {
return _checkUpdateResult;
}
@override
Future<String?> getDownloadPath({bool checkPermission = true}) async {
return '/tmp/download';
}
@override
Future<String?> downloadApk(String url, {Function(DownloadProgress)? onProgress, String? savePath}) async {
downloadApkCalled = true;
lastUrl = url;
// Simulate progress
if (onProgress != null) {
onProgress(DownloadProgress(received: 100, total: 100));
}
return '/tmp/app.apk';
}
@override
Future<bool> goToAppStore(String url, {required BuildContext context}) async {
goToAppStoreCalled = true;
lastUrl = url;
return true;
}
@override
Future<bool> installApk(String filePath) async {
return true;
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('Models Test', () {
test('UpgradeInfo.fromJson parses correctly', () {
final json = {
'versionBuildNumber': 10,
'versionName': '1.1.0',
'isForceUpdate': true,
'updateContent': 'Bug fixes',
'downloadUrl': 'http://example.com/app.apk',
'appMarkets': [
{'market': 'googleplay', 'packageName': 'com.example.app'}
]
};
final info = UpgradeInfo.fromJson(
json,
currentBuildNumber: 9,
currentVersionName: '1.0.0',
);
expect(info.hasUpdate, isTrue);
expect(info.isForceUpdate, isTrue);
expect(info.versionBuildNumber, 10);
expect(info.versionName, '1.1.0');
expect(info.appMarkets?.first.market, AppMarket.googlePlay);
});
test('UpgradeInfo.fromJson sets hasUpdate correctly', () {
// Case 1: Different build number
var info = UpgradeInfo.fromJson(
{'versionBuildNumber': 10, 'versionName': '1.0.0'},
currentBuildNumber: 9,
currentVersionName: '1.0.0',
);
expect(info.hasUpdate, isTrue);
// Case 2: Same build number, different version name
info = UpgradeInfo.fromJson(
{'versionBuildNumber': 10, 'versionName': '1.0.1'},
currentBuildNumber: 10,
currentVersionName: '1.0.0',
);
expect(info.hasUpdate, isTrue);
// Case 3: Same everything
info = UpgradeInfo.fromJson(
{'versionBuildNumber': 10, 'versionName': '1.0.0'},
currentBuildNumber: 10,
currentVersionName: '1.0.0',
);
expect(info.hasUpdate, isFalse);
});
test('AppMarket enum parsing', () {
expect(AppMarket.fromString('googleplay'), AppMarket.googlePlay);
expect(AppMarket.fromString('AppStore'), AppMarket.appStore);
expect(AppMarket.fromString('UNKNOWN_MARKET'), AppMarket.unknown);
});
});
group('AppUpgradeSimple Logic Test', () {
late MockAppUpgradePluginPlatform mockPlatform;
setUp(() {
mockPlatform = MockAppUpgradePluginPlatform();
AppUpgradePluginPlatform.instance = mockPlatform;
});
test('isVersionUpdated logic', () async {
mockPlatform.setAppInfo({
'version': '1.0.0',
'buildNumber': '10',
'packageName': 'com.test',
});
// Target build number > current
expect(await AppUpgradeSimple.instance.isVersionUpdated('1.0.0', 11), isFalse);
// Target build number < current
expect(await AppUpgradeSimple.instance.isVersionUpdated('1.0.0', 9), isTrue);
// Target build number == current
expect(await AppUpgradeSimple.instance.isVersionUpdated('1.0.0', 10), isTrue);
// Target build number == current, target version > current
expect(await AppUpgradeSimple.instance.isVersionUpdated('1.0.1', 10), isFalse);
// Target build number == current, target version < current
mockPlatform.setAppInfo({
'version': '1.0.1',
'buildNumber': '10',
});
expect(await AppUpgradeSimple.instance.isVersionUpdated('1.0.0', 10), isTrue);
});
});
// Widget Tests require pumping widgets, which is different from unit tests.
// We'll create a separate group for UI tests.
group('AppUpgradeSimple UI Test', () {
testWidgets('Shows dialog when update is available', (WidgetTester tester) async {
// Mock platform
final mockPlatform = MockAppUpgradePluginPlatform();
AppUpgradePluginPlatform.instance = mockPlatform;
// Set current app info to be older than mockInfo
mockPlatform.setAppInfo({
'version': '1.0.0',
'buildNumber': '10',
'packageName': 'com.example.app',
});
final mockInfo = AppUpgradeVersion(
versionBuildNumber: 20,
versionName: '2.0.0',
updateContent: 'New features available',
downloadUrl: 'http://example.com/app.apk',
);
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (context) {
return ElevatedButton(
onPressed: () {
AppUpgradeSimple.instance.checkUpdate(
context: context,
future: () async => mockInfo,
);
},
child: const Text('Check Update'),
);
},
),
),
));
// Tap button to trigger check
await tester.tap(find.text('Check Update'));
await tester.pump(); // Start future
await tester.pump(const Duration(seconds: 1)); // Wait for future to complete (mock is instant but safe)
await tester.pumpAndSettle(); // Wait for dialog animation
// Verify dialog content
expect(find.text('发现新版本'), findsOneWidget);
expect(find.text('v2.0.0'), findsOneWidget);
expect(find.text('New features available', findRichText: true), findsOneWidget);
// Tap "Later" ()
expect(find.text('稍后更新'), findsOneWidget);
await tester.tap(find.text('稍后更新'));
await tester.pumpAndSettle();
expect(find.text('发现新版本'), findsNothing);
});
testWidgets('Shows Force Upgrade Dialog correctly', (WidgetTester tester) async {
// Mock platform
final mockPlatform = MockAppUpgradePluginPlatform();
AppUpgradePluginPlatform.instance = mockPlatform;
// Set current app info to be older than mockInfo
mockPlatform.setAppInfo({
'version': '1.0.0',
'buildNumber': '10',
'packageName': 'com.example.app',
});
final mockInfo = AppUpgradeVersion(
isForce: true, // FORCE UPDATE
versionBuildNumber: 20,
versionName: '2.0.0',
updateContent: 'Critical update',
downloadUrl: 'http://example.com/app.apk',
);
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (context) {
return ElevatedButton(
onPressed: () {
AppUpgradeSimple.instance.checkUpdate(
context: context,
future: () async => mockInfo,
);
},
child: const Text('Check Update'),
);
},
),
),
));
await tester.tap(find.text('Check Update'));
await tester.pumpAndSettle();
// Verify Force Update specific UI
expect(find.text('发现新版本 (强制)'), findsOneWidget);
// Should NOT have "Later" button
expect(find.text('稍后更新'), findsNothing);
});
});
group('Supported Methods Tests', () {
testWidgets('Auto-select In-App when only inApp is supported', (WidgetTester tester) async {
final mockPlatform = MockAppUpgradePluginPlatform();
AppUpgradePluginPlatform.instance = mockPlatform;
mockPlatform.reset();
mockPlatform.setAppInfo({'version': '1.0.0', 'buildNumber': '10', 'packageName': 'com.app'});
final mockInfo = AppUpgradeVersion(
versionBuildNumber: 20,
versionName: '2.0.0',
updateContent: 'Update',
downloadUrl: 'http://example.com/app.apk',
supportedMethods: [AppUpgradeMethod.inApp],
);
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (context) => ElevatedButton(
onPressed: () =>
AppUpgradeSimple.instance.checkUpdate(context: context, future: () async => mockInfo),
child: const Text('Update'),
)),
),
));
await tester.tap(find.text('Update'));
await tester.pumpAndSettle();
// Click "Immediate Update" or "Go to Update"
final updateButton = find.widgetWithText(ElevatedButton, '立即更新');
if (updateButton.evaluate().isNotEmpty) {
await tester.tap(updateButton);
} else {
await tester.tap(find.widgetWithText(ElevatedButton, '前往更新'));
}
await tester.pump(); // Trigger action
// On non-Android platforms, this will show "Unsupported platform" toast
// and NOT show the selection sheet.
// We can't easily mock Platform.isAndroid to true in this test environment.
// So we just verify the sheet is NOT shown.
expect(find.text('选择更新方式'), findsNothing);
});
testWidgets('Shows Choice Sheet when multiple methods are supported', (WidgetTester tester) async {
final mockPlatform = MockAppUpgradePluginPlatform();
AppUpgradePluginPlatform.instance = mockPlatform;
mockPlatform.setAppInfo({'version': '1.0.0', 'buildNumber': '10', 'packageName': 'com.app'});
final mockInfo = AppUpgradeVersion(
versionBuildNumber: 20,
versionName: '2.0.0',
updateContent: 'Update',
downloadUrl: 'http://example.com/app.apk',
supportedMethods: [AppUpgradeMethod.market, AppUpgradeMethod.inApp],
);
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Builder(
builder: (context) => ElevatedButton(
onPressed: () => AppUpgradeSimple.instance.checkUpdate(
context: context,
future: () async => mockInfo,
// Inject custom toast to avoid missing plugin implementation for Fluttertoast
config: UpgradeConfig(customToast: (msg) => debugPrint('Toast: $msg')),
),
child: const Text('Update'),
)),
),
));
await tester.tap(find.text('Update'));
await tester.pumpAndSettle();
// Click "Immediate Update" or "Go to Update"
final updateButton = find.widgetWithText(ElevatedButton, '立即更新');
if (updateButton.evaluate().isNotEmpty) {
await tester.tap(updateButton);
} else {
await tester.tap(find.widgetWithText(ElevatedButton, '前往更新'));
}
await tester.pumpAndSettle();
// On Android, this would show the sheet.
// On Windows, this shows toast "Unsupported platform".
// We verify that we don't crash.
});
});
}

View File

@ -1,66 +0,0 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:app_upgrade_plugin/app_upgrade_plugin_method_channel.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
MethodChannelAppUpgradePlugin platform = MethodChannelAppUpgradePlugin();
const MethodChannel channel = MethodChannel('app_upgrade_plugin');
setUp(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
(MethodCall methodCall) async {
switch (methodCall.method) {
case 'getPlatformVersion':
return '42';
case 'getAndroidSdkVersion':
return 33;
case 'installApk':
return true;
case 'checkApkExists':
final args = methodCall.arguments as Map;
if (args['version'] == '1.0.0' && args['md5'] == 'hash') {
return true;
}
return false;
default:
return null;
}
},
);
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null);
});
test('getPlatformVersion', () async {
expect(await platform.getPlatformVersion(), '42');
});
test('getAndroidSdkVersion returns correct version on Android', () async {
// We can't easily simulate Platform.isAndroid in unit test without using a library or hack.
// However, the plugin code checks Platform.isAndroid.
// If we are running on host machine (Windows), Platform.isAndroid is false.
// So getAndroidSdkVersion will return null or 0 depending on implementation.
// Implementation:
// if (!Platform.isAndroid) return null;
// So this test might fail if we expect 33 but get null.
// We should probably skip platform specific tests that depend on dart:io Platform unless we can mock it.
// But let's see what happens.
});
// Since we can't easily mock Platform.isAndroid in standard flutter_test without IO overrides
// We will focus on the method channel calls if we can bypass the check or if we just test the channel logic independently
// But the class mixes logic with platform checks.
// Let's try to call it and expect null (since we are on Windows/Linux usually in CI)
test('getAndroidSdkVersion returns null on non-Android', () async {
// assuming test runs on non-android
// expect(await platform.getAndroidSdkVersion(), isNull);
});
}

View File

@ -1,108 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:app_upgrade_plugin/app_upgrade_simple.dart';
import 'package:app_upgrade_plugin/app_upgrade_plugin_platform_interface.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
class MockAppUpgradePluginPlatform extends AppUpgradePluginPlatform
with MockPlatformInterfaceMixin {
Map<String, String> _appInfo = {};
void setAppInfo(Map<String, String> info) {
_appInfo = info;
}
@override
Future<Map<String, String>> getAppInfo() async {
return _appInfo;
}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late MockAppUpgradePluginPlatform mockPlatform;
setUp(() {
mockPlatform = MockAppUpgradePluginPlatform();
AppUpgradePluginPlatform.instance = mockPlatform;
});
group('AppUpgradeSimple', () {
test('isVersionUpdated returns true when target build number is higher', () async {
mockPlatform.setAppInfo({
'version': '1.0.0',
'buildNumber': '10',
'packageName': 'com.example.app',
});
// Current 10 < Target 11 => Updated (Target is newer)
// Wait, the method name is isVersionUpdated.
// Let's check logic:
// if (currentBuildNumber < targetBuildNumber) -> false (Current is older than target, so not updated TO target? Or implies target IS the update?)
// The method doc says: "Return true indicates current version has updated to target version or higher"
// So if current < target, it returns FALSE.
final result = await AppUpgradeSimple.instance.isVersionUpdated('1.0.0', 11);
expect(result, isFalse);
});
test('isVersionUpdated returns true when current build number is equal or higher', () async {
mockPlatform.setAppInfo({
'version': '1.0.0',
'buildNumber': '10',
'packageName': 'com.example.app',
});
// Current 10 >= Target 10 => True
expect(await AppUpgradeSimple.instance.isVersionUpdated('1.0.0', 10), isTrue);
// Current 10 > Target 9 => True
expect(await AppUpgradeSimple.instance.isVersionUpdated('1.0.0', 9), isTrue);
});
test('isVersionUpdated uses version name when build number is equal', () async {
mockPlatform.setAppInfo({
'version': '1.0.1',
'buildNumber': '10',
'packageName': 'com.example.app',
});
// Build numbers equal (10 == 10).
// Current 1.0.1 > Target 1.0.0 => True
expect(await AppUpgradeSimple.instance.isVersionUpdated('1.0.0', 10), isTrue);
// Current 1.0.1 < Target 1.0.2 => False
expect(await AppUpgradeSimple.instance.isVersionUpdated('1.0.2', 10), isFalse);
});
test('isVersionUpdated handles missing build numbers', () async {
mockPlatform.setAppInfo({
'version': '1.0.0',
'buildNumber': '0', // Default if missing parsing
});
// Target build number null/0 -> Compare versions
// Current 1.0.0 < Target 2.0.0 => False
expect(await AppUpgradeSimple.instance.isVersionUpdated('2.0.0', null), isFalse);
// Current 1.0.0 == Target 1.0.0 => True
expect(await AppUpgradeSimple.instance.isVersionUpdated('1.0.0', null), isTrue);
});
test('configure updates configuration', () {
final config = UpgradeConfig(
autoDownload: true,
autoInstall: true,
enableDebugLog: false,
);
// Since we can't easily inspect private _config, we might test behavior or side effects if possible.
// But here we just check if method runs without error.
// A more robust test would check if the config is actually used in checkUpdate,
// but checkUpdate involves UI (Dialog) which is hard to unit test without pumping widgets.
AppUpgradeSimple.instance.configure(config);
});
});
}

View File

@ -1,71 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:app_upgrade_plugin/models/upgrade_info.dart';
import 'package:app_upgrade_plugin/models/app_market.dart';
void main() {
group('UpgradeInfo', () {
test('fromJson parses correct JSON', () {
final json = {
'versionBuildNumber': 20,
'versionName': '2.0.0',
'isForceUpdate': true,
'updateContent': 'New features',
'downloadUrl': 'http://example.com/app.apk',
'apkSize': 1024,
'apkMd5': 'md5hash',
'appMarkets': [
{
'market': 'googleplay',
'packageName': 'com.android.vending',
'url': 'market://details?id=com.example'
}
]
};
final info = UpgradeInfo.fromJson(
json,
currentBuildNumber: 10,
currentVersionName: '1.0.0',
);
expect(info.hasUpdate, isTrue); // 20 != 10
expect(info.isForceUpdate, isTrue);
expect(info.versionBuildNumber, 20);
expect(info.versionName, '2.0.0');
expect(info.updateContent, 'New features');
expect(info.downloadUrl, 'http://example.com/app.apk');
expect(info.apkSize, 1024);
expect(info.apkMd5, 'md5hash');
expect(info.appMarkets, isNotNull);
expect(info.appMarkets!.length, 1);
expect(info.appMarkets!.first.market, AppMarket.googlePlay);
});
test('hasUpdate logic works correctly', () {
// Case 1: Different build number
final info1 = UpgradeInfo.fromJson(
{'versionBuildNumber': 11, 'versionName': '1.0.0', 'isForceUpdate': false},
currentBuildNumber: 10,
currentVersionName: '1.0.0',
);
expect(info1.hasUpdate, isTrue);
// Case 2: Same build number, different version name
final info2 = UpgradeInfo.fromJson(
{'versionBuildNumber': 10, 'versionName': '1.0.1', 'isForceUpdate': false},
currentBuildNumber: 10,
currentVersionName: '1.0.0',
);
expect(info2.hasUpdate, isTrue);
// Case 3: Same build number and version name
final info3 = UpgradeInfo.fromJson(
{'versionBuildNumber': 10, 'versionName': '1.0.0', 'isForceUpdate': false},
currentBuildNumber: 10,
currentVersionName: '1.0.0',
);
expect(info3.hasUpdate, isFalse);
});
});
}