支持后端返回的各大应用市场判断 前往应用市场更新按钮是否可用
This commit is contained in:
parent
11a42dd186
commit
fd9e4ef30f
|
|
@ -1,12 +1,32 @@
|
||||||
# 应用市场白名单功能说明(简化版)
|
# 应用市场更新入口可点击状态说明
|
||||||
|
|
||||||
## 🎯 功能概述
|
## 功能概述
|
||||||
|
|
||||||
`appMarkets` 作为应用市场白名单使用,只需传入 `AppMarket` 枚举列表即可。
|
`appMarkets` 用于控制 Android 设备上的“应用市场更新”入口是否可点击。
|
||||||
|
|
||||||
## 📝 使用方法
|
它不是应用市场跳转白名单,也不再使用 `AppMarket` 枚举。后端直接返回字符串数组即可,例如 `["XIAOMI", "HUAWEI", "HONOR", "TENCENT"]`。
|
||||||
|
|
||||||
### 1. Dart 代码配置
|
## 使用方法
|
||||||
|
|
||||||
|
### 后端字段约定
|
||||||
|
|
||||||
|
`appMarkets` 是一个可空字符串数组:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"appMarkets": ["XIAOMI", "HUAWEI", "HONOR"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
字段含义只和“应用市场更新”入口是否可点击有关:
|
||||||
|
|
||||||
|
- 不返回或返回 `null`:不限制市场更新按钮,保持旧逻辑。
|
||||||
|
- 返回 `[]`:市场更新入口置灰不可点击,并提示暂未开放。
|
||||||
|
- 返回非空数组:当前 Android 设备匹配任意一个值时,市场更新入口可点击;不匹配时入口置灰并展示原因。
|
||||||
|
|
||||||
|
推荐后端统一返回大写标准值。SDK 会兼容小写、混合大小写、分隔符和常见别名。
|
||||||
|
|
||||||
|
### Dart 代码配置
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
await AppUpgradeSimple.instance.checkUpdate(
|
await AppUpgradeSimple.instance.checkUpdate(
|
||||||
|
|
@ -17,18 +37,31 @@ await AppUpgradeSimple.instance.checkUpdate(
|
||||||
versionBuildNumber: 101,
|
versionBuildNumber: 101,
|
||||||
updateContent: '修复已知问题',
|
updateContent: '修复已知问题',
|
||||||
downloadUrl: 'https://example.com/app.apk',
|
downloadUrl: 'https://example.com/app.apk',
|
||||||
// 只支持华为、小米、OPPO应用市场
|
// 控制 Android “应用市场更新”入口是否可点击
|
||||||
appMarkets: [
|
appMarkets: ['XIAOMI', 'HUAWEI', 'HONOR', 'TENCENT'],
|
||||||
AppMarket.huawei,
|
|
||||||
AppMarket.xiaomi,
|
|
||||||
AppMarket.oppo,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 服务端返回
|
如果业务层是手动把服务端 JSON 转成 `AppUpgradeVersion`,建议这样写:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
AppUpgradeVersion(
|
||||||
|
versionName: json['versionName'],
|
||||||
|
versionBuildNumber: json['versionBuildNumber'],
|
||||||
|
updateContent: json['updateContent'] ?? '',
|
||||||
|
downloadUrl: json['downloadUrl'],
|
||||||
|
appMarkets: AndroidMarketProvider.parseList(json['appMarkets']),
|
||||||
|
supportedMethods: const [
|
||||||
|
AppUpgradeMethod.market,
|
||||||
|
AppUpgradeMethod.browser,
|
||||||
|
AppUpgradeMethod.inApp,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 服务端返回
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|
@ -36,90 +69,113 @@ await AppUpgradeSimple.instance.checkUpdate(
|
||||||
"versionName": "1.0.1",
|
"versionName": "1.0.1",
|
||||||
"updateContent": "修复已知问题",
|
"updateContent": "修复已知问题",
|
||||||
"downloadUrl": "https://example.com/app.apk",
|
"downloadUrl": "https://example.com/app.apk",
|
||||||
"appMarkets": ["huawei", "xiaomi", "oppo"]
|
"appMarkets": ["XIAOMI", "HUAWEI", "HONOR", "TENCENT"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🚀 工作流程
|
## 可点击规则
|
||||||
|
|
||||||
1. 用户点击"前往应用市场更新"
|
| appMarkets 值 | 行为 |
|
||||||
2. 插件检测设备已安装的应用市场
|
|---------------|------|
|
||||||
3. 判断:
|
| `null` 或不返回 | 跳过校验,正常可点击“应用市场更新” |
|
||||||
- ✅ 设备有白名单中的应用市场 → 跳转到应用市场
|
| `[]` | “应用市场更新”置灰不可点击,提示暂未开放 |
|
||||||
- ❌ 设备没有白名单中的应用市场 → 提示"不支持当前设备的应用市场,请选择其他方式更新"
|
| `["XIAOMI"]` | 小米、Redmi、POCO 设备可点击 |
|
||||||
|
| `["HUAWEI"]` | 华为设备可点击,不包含荣耀 |
|
||||||
|
| `["HONOR"]` | 荣耀设备可点击 |
|
||||||
|
| `["VIVO"]` | vivo、iQOO 设备可点击 |
|
||||||
|
| `["OPPO"]` | OPPO、OnePlus、realme 设备可点击 |
|
||||||
|
| `["TENCENT"]` | 已安装应用宝时可点击 |
|
||||||
|
|
||||||
## 📋 支持的应用市场
|
SDK 会自动做大小写和别名归一化,例如 `"xiaomi"`、`"REDMI"`、`"poco"` 都会按 `"XIAOMI"` 处理。
|
||||||
|
|
||||||
| AppMarket 枚举值 | 显示名称 |
|
## 支持的字符串
|
||||||
|-----------------|---------|
|
|
||||||
| `AppMarket.googlePlay` | Google Play |
|
|
||||||
| `AppMarket.huawei` | 华为应用市场 |
|
|
||||||
| `AppMarket.xiaomi` | 小米应用商店 |
|
|
||||||
| `AppMarket.oppo` | OPPO软件商店 |
|
|
||||||
| `AppMarket.vivo` | vivo应用商店 |
|
|
||||||
| `AppMarket.tencent` | 腾讯应用宝 |
|
|
||||||
| `AppMarket.coolapk` | 酷安 |
|
|
||||||
|
|
||||||
## 🎨 用户体验示例
|
后端推荐统一返回大写值:
|
||||||
|
|
||||||
### 场景 1: 华为手机 + 配置了华为和小米
|
| 字符串值 | 说明 |
|
||||||
|
|---------|------|
|
||||||
|
| `XIAOMI` | 小米、小米应用商店;兼容 `REDMI`、`POCO`、`MIUI` |
|
||||||
|
| `HUAWEI` | 华为、华为应用市场 |
|
||||||
|
| `HONOR` | 荣耀、荣耀应用市场 |
|
||||||
|
| `OPPO` | OPPO 软件商店;兼容 `ONEPLUS`、`REALME` |
|
||||||
|
| `VIVO` | vivo 应用商店;兼容 `IQOO` |
|
||||||
|
| `TENCENT` | 腾讯应用宝;兼容 `YINGYONGBAO`、`YYB` |
|
||||||
|
| `SAMSUNG` | 三星 Galaxy Store |
|
||||||
|
| `MEIZU` | 魅族应用商店 |
|
||||||
|
| `LENOVO` | 联想/摩托罗拉生态 |
|
||||||
|
| `ZTE` | 中兴/努比亚生态 |
|
||||||
|
| `GOOGLE_PLAY` | Google Play;兼容 `GOOGLEPLAY`、`GOOGLE` |
|
||||||
|
| `QIHOO_360` | 360 手机助手;兼容 `360`、`QIHOO360` |
|
||||||
|
| `BAIDU` | 百度手机助手 |
|
||||||
|
| `WANDOUJIA` | 豌豆荚 |
|
||||||
|
| `COOLAPK` | 酷安 |
|
||||||
|
|
||||||
- ✅ 检测到华为应用市场已安装
|
## 典型场景
|
||||||
- → 直接跳转到华为应用市场
|
|
||||||
|
|
||||||
### 场景 2: OPPO手机 + 只配置了华为和小米
|
### 小米手机 + appMarkets 包含 XIAOMI
|
||||||
|
|
||||||
- ❌ 检测到设备没有配置的应用市场
|
- “应用市场更新”可点击
|
||||||
- → 提示:"当前设备的应用市场暂不支持,请选择其他方式更新"
|
- 点击后跳转设备默认应用市场
|
||||||
- → 显示其他更新方式(浏览器下载、应用内下载)
|
|
||||||
|
|
||||||
### 场景 3: 未配置 appMarkets
|
### Redmi / POCO 设备 + appMarkets 包含 XIAOMI
|
||||||
|
|
||||||
- 保持向后兼容
|
- 归一为小米生态
|
||||||
- → 使用默认行为:跳转到设备的默认应用市场
|
- “应用市场更新”可点击
|
||||||
|
|
||||||
## 🔍 调试方法
|
### 荣耀手机 + appMarkets 只包含 HUAWEI
|
||||||
|
|
||||||
|
- “应用市场更新”置灰不可点击
|
||||||
|
- 荣耀和华为按不同服务商处理,避免审核进度不一致时误展示
|
||||||
|
|
||||||
|
### 后端返回空数组
|
||||||
|
|
||||||
|
- “应用市场更新”置灰不可点击,提示“当前设备暂不支持应用市场直接更新,请使用其他方式更新APP”
|
||||||
|
- 浏览器更新、应用内下载更新仍按 `supportedMethods` 和 `downloadUrl` 正常展示
|
||||||
|
|
||||||
|
### 后端返回 TENCENT
|
||||||
|
|
||||||
|
- 如果设备已安装应用宝,“应用市场更新”可点击
|
||||||
|
- 如果未安装应用宝,“应用市场更新”置灰不可点击
|
||||||
|
|
||||||
|
### 应用宝、百度、360 等无对应手机品牌的市场
|
||||||
|
|
||||||
|
这类市场不按手机厂商判断,只按设备是否安装了对应应用市场 App 判断:
|
||||||
|
|
||||||
|
| appMarkets 值 | 需要安装的应用市场 |
|
||||||
|
|---------------|-------------------|
|
||||||
|
| `TENCENT` | 应用宝 |
|
||||||
|
| `BAIDU` | 百度手机助手 |
|
||||||
|
| `QIHOO_360` | 360 手机助手 |
|
||||||
|
| `WANDOUJIA` | 豌豆荚 |
|
||||||
|
| `COOLAPK` | 酷安 |
|
||||||
|
| `GOOGLE_PLAY` | Google Play |
|
||||||
|
|
||||||
|
例如服务端返回 `["BAIDU"]` 时,即使设备品牌字段里出现 `BAIDU`,也不会直接可点击;只有 Android 原生检测到已安装百度手机助手时才可点击。
|
||||||
|
|
||||||
|
### 未配置 appMarkets
|
||||||
|
|
||||||
|
- 保持兼容
|
||||||
|
- “应用市场更新”正常可点击
|
||||||
|
|
||||||
|
## 调试方法
|
||||||
|
|
||||||
查看设备已安装的应用市场:
|
查看设备已安装的应用市场:
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
final installedMarkets = await AppUpgradePlugin().getInstalledMarkets();
|
final installedMarkets = await AppUpgradePlugin().getInstalledMarkets();
|
||||||
debugPrint('已安装的应用市场: $installedMarkets');
|
debugPrint('已安装的应用市场: $installedMarkets');
|
||||||
|
|
||||||
// 输出示例: [huawei, tencent]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## ⚠️ 注意事项
|
查看设备信息:
|
||||||
|
|
||||||
1. **iOS 平台**:此功能仅在 Android 平台生效,iOS 会忽略此配置
|
|
||||||
2. **向后兼容**:不配置 `appMarkets` 时,保持原有行为不变
|
|
||||||
3. **极简设计**:无需配置包名、URL等细节,插件内部自动处理
|
|
||||||
|
|
||||||
## 📦 修改的文件
|
|
||||||
|
|
||||||
### 主要变更
|
|
||||||
- ✅ `appMarkets` 类型从 `List<AppMarketInfo>?` 改为 `List<AppMarket>?`
|
|
||||||
- ✅ Android 端新增检测已安装应用市场的方法
|
|
||||||
- ✅ Dart 端新增白名单匹配逻辑
|
|
||||||
- ✅ 自动提示用户选择其他更新方式
|
|
||||||
|
|
||||||
### 优势对比
|
|
||||||
|
|
||||||
**之前的方案(复杂):**
|
|
||||||
```dart
|
```dart
|
||||||
appMarkets: [
|
final deviceInfo = await AppUpgradePlugin().getDeviceInfo();
|
||||||
AppMarketInfo(
|
debugPrint('设备信息: $deviceInfo');
|
||||||
market: AppMarket.huawei,
|
|
||||||
packageName: 'com.huawei.appmarket',
|
|
||||||
url: 'https://appgallery.huawei.com/...',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**现在的方案(简单):**
|
## 注意事项
|
||||||
```dart
|
|
||||||
appMarkets: [AppMarket.huawei]
|
|
||||||
```
|
|
||||||
|
|
||||||
✨ **更简单、更清晰、更易用!**
|
|
||||||
|
|
||||||
|
1. 此功能只影响 Android,iOS 会忽略 `appMarkets`。
|
||||||
|
2. `appMarkets` 只控制市场更新入口是否可点击,不再作为点击后的应用市场白名单。
|
||||||
|
3. 后端可返回大写字符串;SDK 仍兼容小写、混合大小写和常见别名。
|
||||||
|
4. 如果同时配置 `supportedMethods`,必须包含 `"market"`,否则即使 `appMarkets` 匹配也不会出现“应用市场更新”入口。
|
||||||
|
|
|
||||||
39
README.md
39
README.md
|
|
@ -16,7 +16,7 @@
|
||||||
- **🛡️ 权限适配完善**:针对不同 Android 版本的存储、安装、通知权限自动处理
|
- **🛡️ 权限适配完善**:针对不同 Android 版本的存储、安装、通知权限自动处理
|
||||||
- **🌐 网络可配置**:证书校验、超时、默认方法、Headers 等
|
- **🌐 网络可配置**:证书校验、超时、默认方法、Headers 等
|
||||||
- **🔧 安装策略灵活**:系统流程/预检查权限/智能策略可选
|
- **🔧 安装策略灵活**:系统流程/预检查权限/智能策略可选
|
||||||
- **🏪 应用市场支持**:支持多应用市场白名单,智能检测设备已安装的市场
|
- **🏪 应用市场支持**:支持按 Android 厂商/服务商控制应用市场更新入口
|
||||||
- **📦 多种更新方式**:应用市场、浏览器下载、应用内下载
|
- **📦 多种更新方式**:应用市场、浏览器下载、应用内下载
|
||||||
|
|
||||||
## 📦 安装
|
## 📦 安装
|
||||||
|
|
@ -270,7 +270,7 @@ debugPrint('bodyItems: ${parsed.bodyItems.map((e) => e.text).toList()}');
|
||||||
</provider>
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
<!-- Android 11+ 查询声明:允许打开浏览器处理 HTTP/HTTPS URL -->
|
<!-- Android 11+ 查询声明:允许打开浏览器和应用市场 -->
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
@ -280,6 +280,18 @@ debugPrint('bodyItems: ${parsed.bodyItems.map((e) => e.text).toList()}');
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<data android:scheme="http" />
|
<data android:scheme="http" />
|
||||||
</intent>
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:scheme="market" />
|
||||||
|
</intent>
|
||||||
|
<package android:name="com.huawei.appmarket" />
|
||||||
|
<package android:name="com.hihonor.appmarket" />
|
||||||
|
<package android:name="com.xiaomi.market" />
|
||||||
|
<package android:name="com.oppo.market" />
|
||||||
|
<package android:name="com.heytap.market" />
|
||||||
|
<package android:name="com.bbk.appstore" />
|
||||||
|
<package android:name="com.vivo.appstore" />
|
||||||
|
<package android:name="com.tencent.android.qqdownloader" />
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
```
|
```
|
||||||
|
|
@ -314,11 +326,32 @@ debugPrint('bodyItems: ${parsed.bodyItems.map((e) => e.text).toList()}');
|
||||||
"appStoreUrl": "https://apps.apple.com/app/id123456789",
|
"appStoreUrl": "https://apps.apple.com/app/id123456789",
|
||||||
"apkSize": 25165824,
|
"apkSize": 25165824,
|
||||||
"apkMd5": "d41d8cd98f00b204e9800998ecf8427e",
|
"apkMd5": "d41d8cd98f00b204e9800998ecf8427e",
|
||||||
"appMarkets": ["huawei", "xiaomi", "oppo"],
|
"appMarkets": ["XIAOMI", "HUAWEI", "HONOR", "TENCENT"],
|
||||||
"supportedMethods": ["market", "browser", "inApp"]
|
"supportedMethods": ["market", "browser", "inApp"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`appMarkets` 用于控制 Android 设备上的“应用市场更新”入口是否可点击:不返回或为 `null` 时保持旧逻辑;空数组时入口置灰不可点击;非空数组时会按当前设备厂商/已安装独立市场匹配,未匹配时入口置灰并展示原因。SDK 会将 `REDMI`、`POCO` 归一为 `XIAOMI`,将 `IQOO` 归一为 `VIVO`,将 `YINGYONGBAO`/`YYB` 归一为 `TENCENT`。此字段只控制入口是否可点击,不作为点击后的应用市场白名单。
|
||||||
|
|
||||||
|
手动转换服务端 JSON 时可以直接复用 SDK 的解析工具:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
AppUpgradeVersion(
|
||||||
|
versionName: json['versionName'],
|
||||||
|
versionBuildNumber: json['versionBuildNumber'],
|
||||||
|
updateContent: json['updateContent'] ?? '',
|
||||||
|
downloadUrl: json['downloadUrl'],
|
||||||
|
appMarkets: AndroidMarketProvider.parseList(json['appMarkets']),
|
||||||
|
supportedMethods: const [
|
||||||
|
AppUpgradeMethod.market,
|
||||||
|
AppUpgradeMethod.browser,
|
||||||
|
AppUpgradeMethod.inApp,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
`appMarkets` 常用值:`XIAOMI`、`HUAWEI`、`HONOR`、`OPPO`、`VIVO`、`TENCENT`、`SAMSUNG`、`MEIZU`、`LENOVO`、`ZTE`、`GOOGLE_PLAY`、`QIHOO_360`、`BAIDU`、`WANDOUJIA`、`COOLAPK`。其中 `HUAWEI` 和 `HONOR` 会分开匹配,适合处理不同应用市场审核进度不一致的场景。
|
||||||
|
|
||||||
## 🔧 进阶能力
|
## 🔧 进阶能力
|
||||||
|
|
||||||
### 1) 网络配置
|
### 1) 网络配置
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,25 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.example.app_upgrade_plugin">
|
package="com.example.app_upgrade_plugin">
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:scheme="market" />
|
||||||
|
</intent>
|
||||||
|
<package android:name="com.android.vending" />
|
||||||
|
<package android:name="com.huawei.appmarket" />
|
||||||
|
<package android:name="com.hihonor.appmarket" />
|
||||||
|
<package android:name="com.oppo.market" />
|
||||||
|
<package android:name="com.heytap.market" />
|
||||||
|
<package android:name="com.bbk.appstore" />
|
||||||
|
<package android:name="com.vivo.appstore" />
|
||||||
|
<package android:name="com.xiaomi.market" />
|
||||||
|
<package android:name="com.tencent.android.qqdownloader" />
|
||||||
|
<package android:name="com.sec.android.app.samsungapps" />
|
||||||
|
<package android:name="com.meizu.mstore" />
|
||||||
|
<package android:name="com.lenovo.leos.appstore" />
|
||||||
|
<package android:name="com.qihoo.appstore" />
|
||||||
|
<package android:name="com.baidu.appsearch" />
|
||||||
|
<package android:name="com.wandoujia.phoenix2" />
|
||||||
|
<package android:name="com.coolapk.market" />
|
||||||
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -407,16 +407,25 @@ class AppUpgradePlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||||
private fun getInstalledMarkets(result: Result) {
|
private fun getInstalledMarkets(result: Result) {
|
||||||
try {
|
try {
|
||||||
val pm = context.packageManager
|
val pm = context.packageManager
|
||||||
val installedMarkets = mutableListOf<String>()
|
val installedMarkets = mutableSetOf<String>()
|
||||||
|
|
||||||
// 常见应用市场的包名映射
|
// 常见应用市场的包名映射
|
||||||
val marketPackages = mapOf(
|
val marketPackages = mapOf(
|
||||||
"com.android.vending" to "googlePlay",
|
"com.android.vending" to "googlePlay",
|
||||||
"com.huawei.appmarket" to "huawei",
|
"com.huawei.appmarket" to "huawei",
|
||||||
|
"com.hihonor.appmarket" to "honor",
|
||||||
"com.oppo.market" to "oppo",
|
"com.oppo.market" to "oppo",
|
||||||
|
"com.heytap.market" to "oppo",
|
||||||
"com.bbk.appstore" to "vivo",
|
"com.bbk.appstore" to "vivo",
|
||||||
|
"com.vivo.appstore" to "vivo",
|
||||||
"com.xiaomi.market" to "xiaomi",
|
"com.xiaomi.market" to "xiaomi",
|
||||||
"com.tencent.android.qqdownloader" to "tencent",
|
"com.tencent.android.qqdownloader" to "tencent",
|
||||||
|
"com.sec.android.app.samsungapps" to "samsung",
|
||||||
|
"com.meizu.mstore" to "meizu",
|
||||||
|
"com.lenovo.leos.appstore" to "lenovo",
|
||||||
|
"com.qihoo.appstore" to "qihoo360",
|
||||||
|
"com.baidu.appsearch" to "baidu",
|
||||||
|
"com.wandoujia.phoenix2" to "wandoujia",
|
||||||
"com.coolapk.market" to "coolapk"
|
"com.coolapk.market" to "coolapk"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -430,7 +439,7 @@ class AppUpgradePlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.success(installedMarkets)
|
result.success(installedMarkets.toList())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
result.error("GET_MARKETS_ERROR", "Failed to get installed markets", e.message)
|
result.error("GET_MARKETS_ERROR", "Failed to get installed markets", e.message)
|
||||||
}
|
}
|
||||||
|
|
@ -514,4 +523,4 @@ class AppUpgradePlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||||
override fun onDetachedFromActivity() {
|
override fun onDetachedFromActivity() {
|
||||||
activity = null
|
activity = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"flutter": "3.35.5"
|
||||||
|
}
|
||||||
|
|
@ -123,7 +123,8 @@ class _HomePageState extends State<HomePage> {
|
||||||
/// 将 UpdateappResult 转换为 AppUpgradeVersion
|
/// 将 UpdateappResult 转换为 AppUpgradeVersion
|
||||||
AppUpgradeVersion _convertToAppUpgradeVersion(Map<String, dynamic> model) {
|
AppUpgradeVersion _convertToAppUpgradeVersion(Map<String, dynamic> model) {
|
||||||
// 将文件大小从 KB 转换为字节
|
// 将文件大小从 KB 转换为字节
|
||||||
final int? apkSizeBytes = model['fileSize'] != null ? model['fileSize'] * 1024 : null;
|
final int? apkSizeBytes =
|
||||||
|
model['fileSize'] != null ? model['fileSize'] * 1024 : null;
|
||||||
final filePath = model['filePath'];
|
final filePath = model['filePath'];
|
||||||
final appUpgradeVersion = AppUpgradeVersion(
|
final appUpgradeVersion = AppUpgradeVersion(
|
||||||
versionName: model['versionName'],
|
versionName: model['versionName'],
|
||||||
|
|
@ -134,11 +135,16 @@ class _HomePageState extends State<HomePage> {
|
||||||
appStoreUrl: filePath,
|
appStoreUrl: filePath,
|
||||||
apkSize: apkSizeBytes,
|
apkSize: apkSizeBytes,
|
||||||
apkMd5: null, // UpdateappResult 中没有 MD5 字段
|
apkMd5: null, // UpdateappResult 中没有 MD5 字段
|
||||||
// appMarkets: null, // UpdateappResult 中没有应用商店列表字段
|
// 后端可返回 ["XIAOMI", "HUAWEI", "HONOR", "TENCENT"] 控制市场更新按钮显隐
|
||||||
supportedMethods: [AppUpgradeMethod.browser, AppUpgradeMethod.inApp, AppUpgradeMethod.market],
|
appMarkets: AndroidMarketProvider.parseList(model['appMarkets']),
|
||||||
|
supportedMethods: [
|
||||||
|
AppUpgradeMethod.browser,
|
||||||
|
AppUpgradeMethod.inApp,
|
||||||
|
AppUpgradeMethod.market
|
||||||
|
],
|
||||||
// appMarkets: [
|
// appMarkets: [
|
||||||
// AppMarket.huawei,
|
// 'HUAWEI',
|
||||||
// // AppMarket.xiaomi,
|
// 'XIAOMI',
|
||||||
// ],
|
// ],
|
||||||
);
|
);
|
||||||
return appUpgradeVersion;
|
return appUpgradeVersion;
|
||||||
|
|
@ -159,7 +165,8 @@ class _HomePageState extends State<HomePage> {
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
// 1. 静默检查更新(不显示任何 UI)
|
// 1. 静默检查更新(不显示任何 UI)
|
||||||
final upgradeInfo = await AppUpgradeSimple.instance.silentCheckUpdate(
|
final upgradeInfo =
|
||||||
|
await AppUpgradeSimple.instance.silentCheckUpdate(
|
||||||
future: () async {
|
future: () async {
|
||||||
final updateAppEvent = await _getUpdateAppEvent();
|
final updateAppEvent = await _getUpdateAppEvent();
|
||||||
debugPrint("获取最新版本: $updateAppEvent");
|
debugPrint("获取最新版本: $updateAppEvent");
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export 'core/http_config.dart';
|
||||||
export 'core/permission_helper.dart';
|
export 'core/permission_helper.dart';
|
||||||
// 导出升级方式枚举
|
// 导出升级方式枚举
|
||||||
export 'models/app_upgrade_method.dart';
|
export 'models/app_upgrade_method.dart';
|
||||||
|
export 'models/android_market_provider.dart';
|
||||||
// 导出新定义的模型
|
// 导出新定义的模型
|
||||||
export 'models/app_upgrade_version.dart';
|
export 'models/app_upgrade_version.dart';
|
||||||
export 'models/install_strategy.dart';
|
export 'models/install_strategy.dart';
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import 'package:yx_app_upgrade_flutter/core/upgrade_utils.dart';
|
||||||
|
|
||||||
import 'app_upgrade_plugin_platform_interface.dart';
|
import 'app_upgrade_plugin_platform_interface.dart';
|
||||||
import 'core/permission_helper.dart';
|
import 'core/permission_helper.dart';
|
||||||
|
import 'models/android_market_provider.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';
|
||||||
|
|
@ -23,10 +24,8 @@ class _SimpleAppUpgradePlugin {
|
||||||
return AppUpgradePluginPlatform.instance.checkUpdate(url, params: params);
|
return AppUpgradePluginPlatform.instance.checkUpdate(url, params: params);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> downloadApk(String url,
|
Future<String?> downloadApk(String url, {Function(DownloadProgress)? onProgress}) {
|
||||||
{Function(DownloadProgress)? onProgress}) {
|
return AppUpgradePluginPlatform.instance.downloadApk(url, onProgress: onProgress);
|
||||||
return AppUpgradePluginPlatform.instance
|
|
||||||
.downloadApk(url, onProgress: onProgress);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> installApk(String filePath) {
|
Future<bool> installApk(String filePath) {
|
||||||
|
|
@ -34,14 +33,17 @@ class _SimpleAppUpgradePlugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> goToAppStore(String url, {required BuildContext context}) {
|
Future<bool> goToAppStore(String url, {required BuildContext context}) {
|
||||||
return AppUpgradePluginPlatform.instance
|
return AppUpgradePluginPlatform.instance.goToAppStore(url, context: context);
|
||||||
.goToAppStore(url, context: context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, String>> getAppInfo() {
|
Future<Map<String, String>> getAppInfo() {
|
||||||
return AppUpgradePluginPlatform.instance.getAppInfo();
|
return AppUpgradePluginPlatform.instance.getAppInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> getDeviceInfo() {
|
||||||
|
return AppUpgradePluginPlatform.instance.getDeviceInfo();
|
||||||
|
}
|
||||||
|
|
||||||
/// 获取已安装的应用市场列表
|
/// 获取已安装的应用市场列表
|
||||||
Future<List<String>> getInstalledMarkets() {
|
Future<List<String>> getInstalledMarkets() {
|
||||||
return AppUpgradePluginPlatform.instance.getInstalledMarkets();
|
return AppUpgradePluginPlatform.instance.getInstalledMarkets();
|
||||||
|
|
@ -139,6 +141,25 @@ class ParsedUpgradeContentItem {
|
||||||
final bool hasLeadingMarker;
|
final bool hasLeadingMarker;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _MarketMethodState {
|
||||||
|
const _MarketMethodState({
|
||||||
|
required this.supported,
|
||||||
|
required this.enabled,
|
||||||
|
this.disabledReason,
|
||||||
|
});
|
||||||
|
|
||||||
|
const _MarketMethodState.unsupported()
|
||||||
|
: supported = false,
|
||||||
|
enabled = false,
|
||||||
|
disabledReason = null;
|
||||||
|
|
||||||
|
final bool supported;
|
||||||
|
final bool enabled;
|
||||||
|
final String? disabledReason;
|
||||||
|
|
||||||
|
bool get disabled => supported && !enabled;
|
||||||
|
}
|
||||||
|
|
||||||
/// 简化版App升级管理器
|
/// 简化版App升级管理器
|
||||||
/// 提供最简单的API,一行代码即可实现App升级功能
|
/// 提供最简单的API,一行代码即可实现App升级功能
|
||||||
class AppUpgradeSimple {
|
class AppUpgradeSimple {
|
||||||
|
|
@ -156,8 +177,7 @@ class AppUpgradeSimple {
|
||||||
}
|
}
|
||||||
|
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
AppUpgradeSimple.private({_SimpleAppUpgradePlugin? plugin})
|
AppUpgradeSimple.private({_SimpleAppUpgradePlugin? plugin}) : _plugin = plugin ?? _SimpleAppUpgradePlugin.instance;
|
||||||
: _plugin = plugin ?? _SimpleAppUpgradePlugin.instance;
|
|
||||||
|
|
||||||
AppUpgradeSimple._() : _plugin = _SimpleAppUpgradePlugin.instance;
|
AppUpgradeSimple._() : _plugin = _SimpleAppUpgradePlugin.instance;
|
||||||
|
|
||||||
|
|
@ -187,10 +207,7 @@ class AppUpgradeSimple {
|
||||||
if (downloadPath != null) {
|
if (downloadPath != null) {
|
||||||
final dir = Directory(downloadPath);
|
final dir = Directory(downloadPath);
|
||||||
if (await dir.exists()) {
|
if (await dir.exists()) {
|
||||||
final files = await dir
|
final files = await dir.list().where((file) => file.path.endsWith('.apk')).toList();
|
||||||
.list()
|
|
||||||
.where((file) => file.path.endsWith('.apk'))
|
|
||||||
.toList();
|
|
||||||
for (final file in files) {
|
for (final file in files) {
|
||||||
try {
|
try {
|
||||||
await file.delete();
|
await file.delete();
|
||||||
|
|
@ -265,14 +282,12 @@ class AppUpgradeSimple {
|
||||||
}) async {
|
}) async {
|
||||||
// 使用传入的配置或默认配置
|
// 使用传入的配置或默认配置
|
||||||
final effectiveConfig = config ?? _config;
|
final effectiveConfig = config ?? _config;
|
||||||
final finalShowNoUpdateToast =
|
final finalShowNoUpdateToast = showNoUpdateToast ?? effectiveConfig.showNoUpdateToast;
|
||||||
showNoUpdateToast ?? effectiveConfig.showNoUpdateToast;
|
|
||||||
final finalAutoInstall = autoInstall ?? effectiveConfig.autoInstall;
|
final finalAutoInstall = autoInstall ?? effectiveConfig.autoInstall;
|
||||||
try {
|
try {
|
||||||
assert(_canShowMaterialDialog(context), '请在 MaterialApp 环境内调用');
|
assert(_canShowMaterialDialog(context), '请在 MaterialApp 环境内调用');
|
||||||
|
|
||||||
final info =
|
final info = await _prepareUpgradeInfo(future: future, config: effectiveConfig);
|
||||||
await _prepareUpgradeInfo(future: future, config: effectiveConfig);
|
|
||||||
if (info == null) {
|
if (info == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -316,8 +331,7 @@ class AppUpgradeSimple {
|
||||||
}) async {
|
}) async {
|
||||||
final effectiveConfig = config ?? _config;
|
final effectiveConfig = config ?? _config;
|
||||||
try {
|
try {
|
||||||
final info =
|
final info = await _prepareUpgradeInfo(future: future, config: effectiveConfig);
|
||||||
await _prepareUpgradeInfo(future: future, config: effectiveConfig);
|
|
||||||
if (effectiveConfig.enableDebugLog) {
|
if (effectiveConfig.enableDebugLog) {
|
||||||
if (info == null) {
|
if (info == null) {
|
||||||
debugPrint('🔕 静默检查结果: 未返回版本信息');
|
debugPrint('🔕 静默检查结果: 未返回版本信息');
|
||||||
|
|
@ -413,15 +427,13 @@ class AppUpgradeSimple {
|
||||||
if (versionBuildNumber > currentBuildNumber) {
|
if (versionBuildNumber > currentBuildNumber) {
|
||||||
hasUpdate = true;
|
hasUpdate = true;
|
||||||
} else {
|
} else {
|
||||||
if (versionName != null &&
|
if (versionName != null && compareVersionStrings(versionName, currentVersionName) > 0) {
|
||||||
compareVersionStrings(versionName, currentVersionName) > 0) {
|
|
||||||
hasUpdate = true;
|
hasUpdate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 只比较版本名
|
// 只比较版本名
|
||||||
if (versionName != null &&
|
if (versionName != null && compareVersionStrings(versionName, currentVersionName) > 0) {
|
||||||
compareVersionStrings(versionName, currentVersionName) > 0) {
|
|
||||||
hasUpdate = true;
|
hasUpdate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -450,8 +462,7 @@ class AppUpgradeSimple {
|
||||||
// 构建 UpgradeInfo
|
// 构建 UpgradeInfo
|
||||||
// 兜底处理,避免 serverInfo 里的可空字段传入非空参数导致崩溃
|
// 兜底处理,避免 serverInfo 里的可空字段传入非空参数导致崩溃
|
||||||
final safeVersionName = serverInfo.versionName ?? currentVersionName;
|
final safeVersionName = serverInfo.versionName ?? currentVersionName;
|
||||||
final safeVersionBuildNumber =
|
final safeVersionBuildNumber = serverInfo.versionBuildNumber ?? currentBuildNumber;
|
||||||
serverInfo.versionBuildNumber ?? currentBuildNumber;
|
|
||||||
|
|
||||||
return UpgradeInfo(
|
return UpgradeInfo(
|
||||||
hasUpdate: hasUpdate,
|
hasUpdate: hasUpdate,
|
||||||
|
|
@ -465,13 +476,9 @@ class AppUpgradeSimple {
|
||||||
appStoreUrl: serverInfo.appStoreUrl,
|
appStoreUrl: serverInfo.appStoreUrl,
|
||||||
apkSize: serverInfo.apkSize,
|
apkSize: serverInfo.apkSize,
|
||||||
apkMd5: serverInfo.apkMd5,
|
apkMd5: serverInfo.apkMd5,
|
||||||
appMarkets: serverInfo.appMarkets,
|
appMarkets: AndroidMarketProvider.parseList(serverInfo.appMarkets),
|
||||||
supportedMethods: serverInfo.supportedMethods ??
|
supportedMethods: serverInfo.supportedMethods ??
|
||||||
const [
|
const [AppUpgradeMethod.market, AppUpgradeMethod.browser, AppUpgradeMethod.inApp],
|
||||||
AppUpgradeMethod.market,
|
|
||||||
AppUpgradeMethod.browser,
|
|
||||||
AppUpgradeMethod.inApp
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -523,8 +530,7 @@ class AppUpgradeSimple {
|
||||||
}
|
}
|
||||||
|
|
||||||
static ParsedUpgradeContent parseUpdateContent(String content) {
|
static ParsedUpgradeContent parseUpdateContent(String content) {
|
||||||
final normalizedContent =
|
final normalizedContent = content.replaceAll('\r\n', '\n').replaceAll('\r', '\n').trim();
|
||||||
content.replaceAll('\r\n', '\n').replaceAll('\r', '\n').trim();
|
|
||||||
|
|
||||||
if (normalizedContent.isEmpty) {
|
if (normalizedContent.isEmpty) {
|
||||||
return const ParsedUpgradeContent();
|
return const ParsedUpgradeContent();
|
||||||
|
|
@ -557,8 +563,7 @@ class AppUpgradeSimple {
|
||||||
final v1Parts = v1.split('.').map((e) => int.tryParse(e) ?? 0).toList();
|
final v1Parts = v1.split('.').map((e) => int.tryParse(e) ?? 0).toList();
|
||||||
final v2Parts = v2.split('.').map((e) => int.tryParse(e) ?? 0).toList();
|
final v2Parts = v2.split('.').map((e) => int.tryParse(e) ?? 0).toList();
|
||||||
|
|
||||||
final maxLength =
|
final maxLength = v1Parts.length > v2Parts.length ? v1Parts.length : v2Parts.length;
|
||||||
v1Parts.length > v2Parts.length ? v1Parts.length : v2Parts.length;
|
|
||||||
|
|
||||||
for (int i = 0; i < maxLength; i++) {
|
for (int i = 0; i < maxLength; i++) {
|
||||||
final part1 = i < v1Parts.length ? v1Parts[i] : 0;
|
final part1 = i < v1Parts.length ? v1Parts[i] : 0;
|
||||||
|
|
@ -603,8 +608,7 @@ class AppUpgradeSimple {
|
||||||
autoInstall: autoInstall,
|
autoInstall: autoInstall,
|
||||||
onUpdateLater: onUpdateLater,
|
onUpdateLater: onUpdateLater,
|
||||||
config: effectiveConfig,
|
config: effectiveConfig,
|
||||||
showToast: (message) =>
|
showToast: (message) => _showToast(message, context, effectiveConfig),
|
||||||
_showToast(message, context, effectiveConfig),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -612,8 +616,7 @@ class AppUpgradeSimple {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 使用 Overlay 显示 Toast(不依赖 Scaffold)
|
/// 使用 Overlay 显示 Toast(不依赖 Scaffold)
|
||||||
void _showOverlayToast(
|
void _showOverlayToast(BuildContext context, String message, UpgradeConfig config) {
|
||||||
BuildContext context, String message, UpgradeConfig config) {
|
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
debugPrint('Toast消息(context已卸载): $message');
|
debugPrint('Toast消息(context已卸载): $message');
|
||||||
return;
|
return;
|
||||||
|
|
@ -648,8 +651,7 @@ class AppUpgradeSimple {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 尝试使用 ScaffoldMessenger 显示 SnackBar(如果可用)
|
/// 尝试使用 ScaffoldMessenger 显示 SnackBar(如果可用)
|
||||||
void _tryShowSnackBar(
|
void _tryShowSnackBar(BuildContext context, String message, UpgradeConfig config) {
|
||||||
BuildContext context, String message, UpgradeConfig config) {
|
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
_showOverlayToast(context, message, config);
|
_showOverlayToast(context, message, config);
|
||||||
return;
|
return;
|
||||||
|
|
@ -675,9 +677,8 @@ class AppUpgradeSimple {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优先使用根 context 的 ScaffoldMessenger
|
// 优先使用根 context 的 ScaffoldMessenger
|
||||||
final messenger = rootContext != null && rootContext.mounted
|
final messenger =
|
||||||
? ScaffoldMessenger.maybeOf(rootContext)
|
rootContext != null && rootContext.mounted ? ScaffoldMessenger.maybeOf(rootContext) : scaffoldMessenger;
|
||||||
: scaffoldMessenger;
|
|
||||||
|
|
||||||
if (messenger == null) {
|
if (messenger == null) {
|
||||||
_showOverlayToast(context, message, config);
|
_showOverlayToast(context, message, config);
|
||||||
|
|
@ -702,8 +703,7 @@ class AppUpgradeSimple {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 显示Toast提示
|
/// 显示Toast提示
|
||||||
void _showToast(String message, BuildContext context,
|
void _showToast(String message, BuildContext context, [UpgradeConfig? config]) {
|
||||||
[UpgradeConfig? config]) {
|
|
||||||
final effectiveConfig = config ?? _config;
|
final effectiveConfig = config ?? _config;
|
||||||
if (effectiveConfig.customToast != null) {
|
if (effectiveConfig.customToast != null) {
|
||||||
effectiveConfig.customToast!(message);
|
effectiveConfig.customToast!(message);
|
||||||
|
|
@ -716,9 +716,7 @@ class AppUpgradeSimple {
|
||||||
bool _canShowMaterialDialog(BuildContext context) {
|
bool _canShowMaterialDialog(BuildContext context) {
|
||||||
if (!context.mounted) return false;
|
if (!context.mounted) return false;
|
||||||
try {
|
try {
|
||||||
return Localizations.of<MaterialLocalizations>(
|
return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations) != null;
|
||||||
context, MaterialLocalizations) !=
|
|
||||||
null;
|
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -750,8 +748,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void onAppLifecycleStateChanged(AppLifecycleState state) {
|
void onAppLifecycleStateChanged(AppLifecycleState state) {
|
||||||
debugPrint(
|
debugPrint('🔄 应用生命周期状态变化: $state, _isWaitingForInstallation=$_isWaitingForInstallation');
|
||||||
'🔄 应用生命周期状态变化: $state, _isWaitingForInstallation=$_isWaitingForInstallation');
|
|
||||||
|
|
||||||
if (_isWaitingForInstallation && state == AppLifecycleState.resumed) {
|
if (_isWaitingForInstallation && state == AppLifecycleState.resumed) {
|
||||||
debugPrint('⚡ 应用回到前台,检查安装状态');
|
debugPrint('⚡ 应用回到前台,检查安装状态');
|
||||||
|
|
@ -808,14 +805,12 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
if (!Platform.isAndroid || info.downloadUrl == null) return;
|
if (!Platform.isAndroid || info.downloadUrl == null) return;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
final hasStorage = await PermissionHelper.checkAndRequestStoragePermission(
|
final hasStorage = await PermissionHelper.checkAndRequestStoragePermission(context: context);
|
||||||
context: context);
|
|
||||||
if (!hasStorage) {
|
if (!hasStorage) {
|
||||||
showToast('缺少存储权限,无法下载');
|
showToast('缺少存储权限,无法下载');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await PermissionHelper.checkAndRequestNotificationPermission(
|
await PermissionHelper.checkAndRequestNotificationPermission(context: context);
|
||||||
context: context);
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isDownloading = true;
|
_isDownloading = true;
|
||||||
|
|
@ -878,9 +873,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
|
|
||||||
if (config.requireInstallPermission) {
|
if (config.requireInstallPermission) {
|
||||||
debugPrint('🔐 检查安装权限(配置要求)');
|
debugPrint('🔐 检查安装权限(配置要求)');
|
||||||
final hasPermission =
|
final hasPermission = await PermissionHelper.checkAndRequestInstallPermission(context: context);
|
||||||
await PermissionHelper.checkAndRequestInstallPermission(
|
|
||||||
context: context);
|
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -948,8 +941,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
|
|
||||||
Future<void> _checkInstallationResult() async {
|
Future<void> _checkInstallationResult() async {
|
||||||
if (!mounted || !_isWaitingForInstallation) {
|
if (!mounted || !_isWaitingForInstallation) {
|
||||||
debugPrint(
|
debugPrint('跳过安装结果检查: mounted=$mounted, _isWaitingForInstallation=$_isWaitingForInstallation');
|
||||||
'跳过安装结果检查: mounted=$mounted, _isWaitingForInstallation=$_isWaitingForInstallation');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -958,30 +950,23 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
try {
|
try {
|
||||||
final appInfo = await _plugin.getAppInfo();
|
final appInfo = await _plugin.getAppInfo();
|
||||||
final currentVersion = appInfo['version'] ?? '';
|
final currentVersion = appInfo['version'] ?? '';
|
||||||
final currentBuildNumber =
|
final currentBuildNumber = int.tryParse(appInfo['buildNumber'] ?? '0') ?? 0;
|
||||||
int.tryParse(appInfo['buildNumber'] ?? '0') ?? 0;
|
|
||||||
|
|
||||||
debugPrint('📱 当前版本: $currentVersion, 构建号: $currentBuildNumber');
|
debugPrint('📱 当前版本: $currentVersion, 构建号: $currentBuildNumber');
|
||||||
debugPrint(
|
debugPrint('🎯 目标版本: ${info.versionName}, 构建号: ${info.versionBuildNumber}');
|
||||||
'🎯 目标版本: ${info.versionName}, 构建号: ${info.versionBuildNumber}');
|
|
||||||
|
|
||||||
bool isUpdated = false;
|
bool isUpdated = false;
|
||||||
if (info.versionBuildNumber > 0) {
|
if (info.versionBuildNumber > 0) {
|
||||||
if (currentBuildNumber > info.versionBuildNumber) {
|
if (currentBuildNumber > info.versionBuildNumber) {
|
||||||
isUpdated = true;
|
isUpdated = true;
|
||||||
} else {
|
} else {
|
||||||
isUpdated = AppUpgradeSimple.compareVersionStrings(
|
isUpdated = AppUpgradeSimple.compareVersionStrings(currentVersion, info.versionName) > 0;
|
||||||
currentVersion, info.versionName) >
|
|
||||||
0;
|
|
||||||
}
|
}
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'📊 构建号比较: $currentBuildNumber vs ${info.versionBuildNumber}, 版本比较(如需): ${info.versionName} -> $isUpdated');
|
'📊 构建号比较: $currentBuildNumber vs ${info.versionBuildNumber}, 版本比较(如需): ${info.versionName} -> $isUpdated');
|
||||||
} else {
|
} else {
|
||||||
isUpdated = AppUpgradeSimple.compareVersionStrings(
|
isUpdated = AppUpgradeSimple.compareVersionStrings(currentVersion, info.versionName) > 0;
|
||||||
currentVersion, info.versionName) >
|
debugPrint('📊 版本号比较: $currentVersion vs ${info.versionName} = $isUpdated');
|
||||||
0;
|
|
||||||
debugPrint(
|
|
||||||
'📊 版本号比较: $currentVersion vs ${info.versionName} = $isUpdated');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUpdated) {
|
if (isUpdated) {
|
||||||
|
|
@ -1035,9 +1020,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_statusText == '权限被拒绝' && config.requireInstallPermission) {
|
if (_statusText == '权限被拒绝' && config.requireInstallPermission) {
|
||||||
final hasPermission =
|
final hasPermission = await PermissionHelper.checkAndRequestInstallPermission(context: context);
|
||||||
await PermissionHelper.checkAndRequestInstallPermission(
|
|
||||||
context: context);
|
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
showToast('仍未获得安装权限,请在设置中手动开启');
|
showToast('仍未获得安装权限,请在设置中手动开启');
|
||||||
return;
|
return;
|
||||||
|
|
@ -1106,8 +1089,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
if (info.isForceUpdate) ...[
|
if (info.isForceUpdate) ...[
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Container(
|
Container(
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colorScheme.error,
|
color: colorScheme.error,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
|
@ -1149,8 +1131,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
icon: Icons.update,
|
icon: Icons.update,
|
||||||
label: '已安装版本',
|
label: '已安装版本',
|
||||||
// 显示版本号
|
// 显示版本号
|
||||||
value:
|
value: '${info.currentVersionName} +${info.currentBuildNumber}',
|
||||||
'${info.currentVersionName} +${info.currentBuildNumber}',
|
|
||||||
colorScheme: colorScheme,
|
colorScheme: colorScheme,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -1214,8 +1195,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
|
|
||||||
Widget _buildUpdateContent(BuildContext context) {
|
Widget _buildUpdateContent(BuildContext context) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final parsedContent =
|
final parsedContent = AppUpgradeSimple.parseUpdateContent(info.updateContent);
|
||||||
AppUpgradeSimple.parseUpdateContent(info.updateContent);
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
@ -1266,8 +1246,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
parsedContent.header!,
|
parsedContent.header!,
|
||||||
colorScheme,
|
colorScheme,
|
||||||
),
|
),
|
||||||
if (parsedContent.hasBodyItems)
|
if (parsedContent.hasBodyItems) const SizedBox(height: 10),
|
||||||
const SizedBox(height: 10),
|
|
||||||
],
|
],
|
||||||
...parsedContent.bodyItems.asMap().entries.map((entry) {
|
...parsedContent.bodyItems.asMap().entries.map((entry) {
|
||||||
final index = entry.key;
|
final index = entry.key;
|
||||||
|
|
@ -1275,9 +1254,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
margin: EdgeInsets.only(
|
margin: EdgeInsets.only(
|
||||||
bottom: index < parsedContent.bodyItems.length - 1
|
bottom: index < parsedContent.bodyItems.length - 1 ? 8 : 0,
|
||||||
? 8
|
|
||||||
: 0,
|
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
@ -1385,12 +1362,10 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
|
|
||||||
// [高亮]
|
// [高亮]
|
||||||
if (currentChar == '[') {
|
if (currentChar == '[') {
|
||||||
final innerResult =
|
final innerResult = _parseRichTextInternal(text, styles, index + 1, ']');
|
||||||
_parseRichTextInternal(text, styles, index + 1, ']');
|
|
||||||
if (innerResult.closed) {
|
if (innerResult.closed) {
|
||||||
flushBuffer();
|
flushBuffer();
|
||||||
final innerText =
|
final innerText = text.substring(index + 1, innerResult.nextIndex - 1);
|
||||||
text.substring(index + 1, innerResult.nextIndex - 1);
|
|
||||||
spans.addAll(_applyStyleToSpans(
|
spans.addAll(_applyStyleToSpans(
|
||||||
innerResult.spans,
|
innerResult.spans,
|
||||||
styles.highlightStyle,
|
styles.highlightStyle,
|
||||||
|
|
@ -1426,12 +1401,10 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
}
|
}
|
||||||
// **粗体**
|
// **粗体**
|
||||||
else if (text.startsWith('**', index)) {
|
else if (text.startsWith('**', index)) {
|
||||||
final innerResult =
|
final innerResult = _parseRichTextInternal(text, styles, index + 2, '**');
|
||||||
_parseRichTextInternal(text, styles, index + 2, '**');
|
|
||||||
if (innerResult.closed) {
|
if (innerResult.closed) {
|
||||||
flushBuffer();
|
flushBuffer();
|
||||||
final innerText =
|
final innerText = text.substring(index + 2, innerResult.nextIndex - 2);
|
||||||
text.substring(index + 2, innerResult.nextIndex - 2);
|
|
||||||
spans.addAll(_applyStyleToSpans(
|
spans.addAll(_applyStyleToSpans(
|
||||||
innerResult.spans,
|
innerResult.spans,
|
||||||
styles.boldStyle,
|
styles.boldStyle,
|
||||||
|
|
@ -1448,12 +1421,10 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
}
|
}
|
||||||
// __斜体__
|
// __斜体__
|
||||||
else if (text.startsWith('__', index)) {
|
else if (text.startsWith('__', index)) {
|
||||||
final innerResult =
|
final innerResult = _parseRichTextInternal(text, styles, index + 2, '__');
|
||||||
_parseRichTextInternal(text, styles, index + 2, '__');
|
|
||||||
if (innerResult.closed) {
|
if (innerResult.closed) {
|
||||||
flushBuffer();
|
flushBuffer();
|
||||||
final innerText =
|
final innerText = text.substring(index + 2, innerResult.nextIndex - 2);
|
||||||
text.substring(index + 2, innerResult.nextIndex - 2);
|
|
||||||
spans.addAll(_applyStyleToSpans(
|
spans.addAll(_applyStyleToSpans(
|
||||||
innerResult.spans,
|
innerResult.spans,
|
||||||
styles.italicStyle,
|
styles.italicStyle,
|
||||||
|
|
@ -1478,8 +1449,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
return _RichTextParseResult(spans, index, false);
|
return _RichTextParseResult(spans, index, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TextSpan> _applyStyleToSpans(
|
List<TextSpan> _applyStyleToSpans(List<TextSpan> spans, TextStyle style, String fallbackText) {
|
||||||
List<TextSpan> spans, TextStyle style, String fallbackText) {
|
|
||||||
if (spans.isEmpty) {
|
if (spans.isEmpty) {
|
||||||
return [
|
return [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
|
|
@ -1493,10 +1463,8 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
TextSpan _mergeTextSpanStyle(TextSpan span, TextStyle style) {
|
TextSpan _mergeTextSpanStyle(TextSpan span, TextStyle style) {
|
||||||
final mergedChildren = span.children
|
final mergedChildren =
|
||||||
?.map((child) =>
|
span.children?.map((child) => child is TextSpan ? _mergeTextSpanStyle(child, style) : child).toList();
|
||||||
child is TextSpan ? _mergeTextSpanStyle(child, style) : child)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
final mergedStyle = span.style != null ? style.merge(span.style) : style;
|
final mergedStyle = span.style != null ? style.merge(span.style) : style;
|
||||||
|
|
||||||
|
|
@ -1507,8 +1475,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEnhancedDownloadProgress(
|
Widget _buildEnhancedDownloadProgress(BuildContext context, ColorScheme colorScheme) {
|
||||||
BuildContext context, ColorScheme colorScheme) {
|
|
||||||
final bool showRetryButton = _downloadedFilePath != null &&
|
final bool showRetryButton = _downloadedFilePath != null &&
|
||||||
!_isDownloading &&
|
!_isDownloading &&
|
||||||
!_isInstalling &&
|
!_isInstalling &&
|
||||||
|
|
@ -1612,8 +1579,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colorScheme.primaryContainer
|
color: colorScheme.primaryContainer.withOpacity(0.2),
|
||||||
.withOpacity(0.2),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: colorScheme.primary.withOpacity(0.3),
|
color: colorScheme.primary.withOpacity(0.3),
|
||||||
|
|
@ -1642,8 +1608,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
'系统将自动检测安装结果',
|
'系统将自动检测安装结果',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: colorScheme.onSurface
|
color: colorScheme.onSurface.withOpacity(0.7),
|
||||||
.withOpacity(0.7),
|
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
|
@ -1660,11 +1625,9 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: _retryInstall,
|
onPressed: _retryInstall,
|
||||||
icon: Icon(_getRetryButtonIcon(), size: 16),
|
icon: Icon(_getRetryButtonIcon(), size: 16),
|
||||||
label: Text(_getRetryButtonText(),
|
label: Text(_getRetryButtonText(), style: const TextStyle(fontSize: 12)),
|
||||||
style: const TextStyle(fontSize: 12)),
|
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
horizontal: 12, vertical: 8),
|
|
||||||
minimumSize: Size.zero,
|
minimumSize: Size.zero,
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
backgroundColor: _getRetryButtonColor(colorScheme),
|
backgroundColor: _getRetryButtonColor(colorScheme),
|
||||||
|
|
@ -1673,8 +1636,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (_isDownloading ||
|
if (_isDownloading || (_downloadProgress > 0 && _downloadProgress < 1.0)) ...[
|
||||||
(_downloadProgress > 0 && _downloadProgress < 1.0)) ...[
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
|
@ -1749,9 +1711,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
return Icons.cancel_outlined;
|
return Icons.cancel_outlined;
|
||||||
} else if (_statusText == '安装超时' || _statusText == '检测失败') {
|
} else if (_statusText == '安装超时' || _statusText == '检测失败') {
|
||||||
return Icons.schedule;
|
return Icons.schedule;
|
||||||
} else if (_statusText == '安装失败' ||
|
} else if (_statusText == '安装失败' || _statusText == '安装异常' || _statusText == '权限被拒绝') {
|
||||||
_statusText == '安装异常' ||
|
|
||||||
_statusText == '权限被拒绝') {
|
|
||||||
return Icons.error_outline;
|
return Icons.error_outline;
|
||||||
} else if (_downloadProgress >= 1.0) {
|
} else if (_downloadProgress >= 1.0) {
|
||||||
return Icons.check_circle_outline;
|
return Icons.check_circle_outline;
|
||||||
|
|
@ -1777,9 +1737,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
return colorScheme.secondary.withOpacity(0.8);
|
return colorScheme.secondary.withOpacity(0.8);
|
||||||
} else if (_statusText == '安装超时' || _statusText == '检测失败') {
|
} else if (_statusText == '安装超时' || _statusText == '检测失败') {
|
||||||
return colorScheme.secondary.withOpacity(0.7);
|
return colorScheme.secondary.withOpacity(0.7);
|
||||||
} else if (_statusText == '安装失败' ||
|
} else if (_statusText == '安装失败' || _statusText == '安装异常' || _statusText == '权限被拒绝') {
|
||||||
_statusText == '安装异常' ||
|
|
||||||
_statusText == '权限被拒绝') {
|
|
||||||
return colorScheme.error;
|
return colorScheme.error;
|
||||||
} else if (_downloadProgress >= 1.0) {
|
} else if (_downloadProgress >= 1.0) {
|
||||||
return colorScheme.tertiary;
|
return colorScheme.tertiary;
|
||||||
|
|
@ -1831,10 +1789,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
return colorScheme.secondary;
|
return colorScheme.secondary;
|
||||||
} else if (_statusText == '安装失败' || _statusText == '安装异常') {
|
} else if (_statusText == '安装失败' || _statusText == '安装异常') {
|
||||||
return colorScheme.error;
|
return colorScheme.error;
|
||||||
} else if (_statusText == '安装超时' ||
|
} else if (_statusText == '安装超时' || _statusText == '安装被取消' || _statusText == '检测失败' || _statusText == '等待安装中') {
|
||||||
_statusText == '安装被取消' ||
|
|
||||||
_statusText == '检测失败' ||
|
|
||||||
_statusText == '等待安装中') {
|
|
||||||
return colorScheme.secondary.withOpacity(0.8);
|
return colorScheme.secondary.withOpacity(0.8);
|
||||||
} else if (_statusText == '请完成安装') {
|
} else if (_statusText == '请完成安装') {
|
||||||
return colorScheme.secondary;
|
return colorScheme.secondary;
|
||||||
|
|
@ -1894,8 +1849,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
|
|
||||||
Future<void> _handleIosAction(BuildContext context) async {
|
Future<void> _handleIosAction(BuildContext context) async {
|
||||||
if (info.appStoreUrl != null) {
|
if (info.appStoreUrl != null) {
|
||||||
final success =
|
final success = await _plugin.goToAppStore(info.appStoreUrl!, context: context);
|
||||||
await _plugin.goToAppStore(info.appStoreUrl!, context: context);
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
showToast('无法打开App Store,请稍后重试');
|
showToast('无法打开App Store,请稍后重试');
|
||||||
}
|
}
|
||||||
|
|
@ -1908,8 +1862,11 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
Future<void> _handleAndroidAction() async {
|
Future<void> _handleAndroidAction() async {
|
||||||
final List<AppUpgradeMethod> availableMethods = [];
|
final List<AppUpgradeMethod> availableMethods = [];
|
||||||
final supported = info.supportedMethods;
|
final supported = info.supportedMethods;
|
||||||
|
final marketState = supported.contains(AppUpgradeMethod.market)
|
||||||
|
? await _resolveMarketMethodState()
|
||||||
|
: const _MarketMethodState.unsupported();
|
||||||
|
|
||||||
if (supported.contains(AppUpgradeMethod.market)) {
|
if (marketState.enabled) {
|
||||||
availableMethods.add(AppUpgradeMethod.market);
|
availableMethods.add(AppUpgradeMethod.market);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1925,11 +1882,18 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
debugPrint('可用更新方式: $availableMethods');
|
debugPrint('可用更新方式: $availableMethods');
|
||||||
|
|
||||||
if (availableMethods.isEmpty) {
|
if (availableMethods.isEmpty) {
|
||||||
|
if (marketState.disabled) {
|
||||||
|
await _showDownloadChoiceSheet(
|
||||||
|
availableMethods,
|
||||||
|
marketState: marketState,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
showToast('未找到可用的更新方式');
|
showToast('未找到可用的更新方式');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (availableMethods.length == 1) {
|
if (availableMethods.length == 1 && !marketState.disabled) {
|
||||||
final method = availableMethods.first;
|
final method = availableMethods.first;
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case AppUpgradeMethod.market:
|
case AppUpgradeMethod.market:
|
||||||
|
|
@ -1945,7 +1909,51 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _showDownloadChoiceSheet(availableMethods);
|
await _showDownloadChoiceSheet(
|
||||||
|
availableMethods,
|
||||||
|
marketState: marketState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<_MarketMethodState> _resolveMarketMethodState() async {
|
||||||
|
final providers = info.appMarkets;
|
||||||
|
if (providers == null) {
|
||||||
|
return const _MarketMethodState(supported: true, enabled: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic>? deviceInfo;
|
||||||
|
List<String> installedMarkets = const <String>[];
|
||||||
|
|
||||||
|
try {
|
||||||
|
deviceInfo = await _plugin.getDeviceInfo();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('获取设备厂商信息失败: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
installedMarkets = await _plugin.getInstalledMarkets();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('获取已安装应用市场失败: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = AndroidMarketProvider.checkMarketUpdate(
|
||||||
|
allowedProviders: providers,
|
||||||
|
deviceInfo: deviceInfo,
|
||||||
|
installedMarkets: installedMarkets,
|
||||||
|
);
|
||||||
|
|
||||||
|
debugPrint(
|
||||||
|
'应用市场更新按钮校验: allowedProviders=${AndroidMarketProvider.parseList(providers)}, '
|
||||||
|
'deviceProvider=${result.deviceProvider}, '
|
||||||
|
'installedMarkets=${result.installedProviders}, '
|
||||||
|
'enabled=${result.allowed}, disabledReason=${result.disabledReason}',
|
||||||
|
);
|
||||||
|
|
||||||
|
return _MarketMethodState(
|
||||||
|
supported: true,
|
||||||
|
enabled: result.allowed,
|
||||||
|
disabledReason: result.disabledReason,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _performMarketAction() async {
|
Future<void> _performMarketAction() async {
|
||||||
|
|
@ -1953,37 +1961,16 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
final installedMarkets = await _plugin.getInstalledMarkets();
|
final installedMarkets = await _plugin.getInstalledMarkets();
|
||||||
debugPrint('设备已安装的应用市场: $installedMarkets');
|
debugPrint('设备已安装的应用市场: $installedMarkets');
|
||||||
|
|
||||||
final hasWhitelist = info.appMarkets?.isNotEmpty ?? false;
|
if (installedMarkets.isEmpty) {
|
||||||
|
showToast('当前设备未安装应用市场');
|
||||||
if (hasWhitelist) {
|
return;
|
||||||
debugPrint('配置的应用市场白名单: ${info.appMarkets}');
|
|
||||||
|
|
||||||
// 筛选出设备上已安装且在白名单中的应用市场
|
|
||||||
final availableMarkets = info.appMarkets!
|
|
||||||
.where((market) => installedMarkets.contains(market.name))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
debugPrint('可用的应用市场: $availableMarkets');
|
|
||||||
|
|
||||||
if (availableMarkets.isEmpty) {
|
|
||||||
// 没有匹配的应用市场,仅提示用户
|
|
||||||
showToast('当前设备的应用市场不在支持列表中,请选择其他方式更新');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 未配置白名单,但也要检查设备是否有应用市场
|
|
||||||
if (installedMarkets.isEmpty) {
|
|
||||||
showToast('当前设备未安装应用市场');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳转到应用市场(使用设备默认的 market:// 协议)
|
// 跳转到应用市场(使用设备默认的 market:// 协议)
|
||||||
final appInfo = await _plugin.getAppInfo();
|
final appInfo = await _plugin.getAppInfo();
|
||||||
final pkg = appInfo['packageName'] ?? '';
|
final pkg = appInfo['packageName'] ?? '';
|
||||||
if (pkg.isNotEmpty) {
|
if (pkg.isNotEmpty) {
|
||||||
final success = await _plugin.goToAppStore('market://details?id=$pkg',
|
final success = await _plugin.goToAppStore('market://details?id=$pkg', context: context);
|
||||||
context: context);
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
showToast('当前APP没有上架当前设备对应的应用市场,请选择其他方式更新');
|
showToast('当前APP没有上架当前设备对应的应用市场,请选择其他方式更新');
|
||||||
}
|
}
|
||||||
|
|
@ -1999,7 +1986,9 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showDownloadChoiceSheet(
|
Future<void> _showDownloadChoiceSheet(
|
||||||
List<AppUpgradeMethod> availableMethods) async {
|
List<AppUpgradeMethod> availableMethods, {
|
||||||
|
_MarketMethodState marketState = const _MarketMethodState.unsupported(),
|
||||||
|
}) async {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
final choice = await showModalBottomSheet<AppUpgradeMethod>(
|
final choice = await showModalBottomSheet<AppUpgradeMethod>(
|
||||||
|
|
@ -2038,9 +2027,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text('选择更新方式',
|
child: Text('选择更新方式',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold))),
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold))),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
|
|
@ -2053,33 +2040,51 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (availableMethods.contains(AppUpgradeMethod.market))
|
if (marketState.supported)
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.storefront_outlined),
|
enabled: marketState.enabled,
|
||||||
title:
|
leading: Icon(
|
||||||
const Text('前往应用市场更新', style: TextStyle(fontSize: 16)),
|
Icons.storefront_outlined,
|
||||||
shape: RoundedRectangleBorder(
|
color: marketState.enabled ? null : Theme.of(ctx).disabledColor,
|
||||||
borderRadius: BorderRadius.circular(12)),
|
),
|
||||||
onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.market),
|
title: Text(
|
||||||
|
'前往应用市场更新',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: marketState.enabled ? null : Theme.of(ctx).disabledColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: marketState.disabled
|
||||||
|
? Text(
|
||||||
|
marketState.disabledReason ?? AndroidMarketProvider.marketUpdateDisabledReason,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(ctx).disabledColor,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
trailing: marketState.disabled
|
||||||
|
? Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
color: Theme.of(ctx).disabledColor,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
onTap: marketState.enabled ? () => Navigator.of(ctx).pop(AppUpgradeMethod.market) : null,
|
||||||
),
|
),
|
||||||
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内更新', style: TextStyle(fontSize: 16)),
|
title: const Text('APP内更新', style: TextStyle(fontSize: 16)),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
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)),
|
||||||
textAlign: TextAlign.left,
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
style: TextStyle(fontSize: 16)),
|
onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.browser),
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12)),
|
|
||||||
onTap: () =>
|
|
||||||
Navigator.of(ctx).pop(AppUpgradeMethod.browser),
|
|
||||||
),
|
),
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
|
@ -2088,8 +2093,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
foregroundColor:
|
foregroundColor: Theme.of(context).textTheme.bodyLarge?.color,
|
||||||
Theme.of(context).textTheme.bodyLarge?.color,
|
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
shadowColor: Colors.grey.withOpacity(0.5),
|
shadowColor: Colors.grey.withOpacity(0.5),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
|
|
@ -2098,9 +2102,7 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () => Navigator.of(ctx).pop(),
|
onPressed: () => Navigator.of(ctx).pop(),
|
||||||
child: const Text('取消',
|
child: const Text('取消', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16, fontWeight: FontWeight.w500)),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -2146,8 +2148,7 @@ class _SimpleUpgradeDialog extends StatefulWidget {
|
||||||
State<_SimpleUpgradeDialog> createState() => _SimpleUpgradeDialogState();
|
State<_SimpleUpgradeDialog> createState() => _SimpleUpgradeDialogState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog>
|
class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _UpgradeDialogLogic, WidgetsBindingObserver {
|
||||||
with _UpgradeDialogLogic, WidgetsBindingObserver {
|
|
||||||
@override
|
@override
|
||||||
UpgradeInfo get info => widget.info;
|
UpgradeInfo get info => widget.info;
|
||||||
@override
|
@override
|
||||||
|
|
@ -2222,13 +2223,11 @@ class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog>
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
_buildVersionInfoCard(
|
_buildVersionInfoCard(context, Theme.of(context).colorScheme),
|
||||||
context, Theme.of(context).colorScheme),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildUpdateContent(context),
|
_buildUpdateContent(context),
|
||||||
if (_isDownloading || _downloadedFilePath != null)
|
if (_isDownloading || _downloadedFilePath != null)
|
||||||
_buildEnhancedDownloadProgress(
|
_buildEnhancedDownloadProgress(context, Theme.of(context).colorScheme),
|
||||||
context, Theme.of(context).colorScheme),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -2276,13 +2275,12 @@ class _ForceUpgradeDialog extends StatefulWidget {
|
||||||
State<_ForceUpgradeDialog> createState() => _ForceUpgradeDialogState();
|
State<_ForceUpgradeDialog> createState() => _ForceUpgradeDialogState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog>
|
class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> with _UpgradeDialogLogic, WidgetsBindingObserver {
|
||||||
with _UpgradeDialogLogic, WidgetsBindingObserver {
|
|
||||||
@override
|
@override
|
||||||
UpgradeInfo get info => widget.info;
|
UpgradeInfo get info => widget.info;
|
||||||
@override
|
@override
|
||||||
void Function(String) get showToast => (message) =>
|
void Function(String) get showToast =>
|
||||||
AppUpgradeSimple.instance._showToast(message, context, widget.config);
|
(message) => AppUpgradeSimple.instance._showToast(message, context, widget.config);
|
||||||
@override
|
@override
|
||||||
bool get autoInstall => widget.autoInstall;
|
bool get autoInstall => widget.autoInstall;
|
||||||
@override
|
@override
|
||||||
|
|
@ -2332,8 +2330,7 @@ class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog>
|
||||||
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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -2348,13 +2345,11 @@ class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog>
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
_buildVersionInfoCard(
|
_buildVersionInfoCard(context, Theme.of(context).colorScheme),
|
||||||
context, Theme.of(context).colorScheme),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildUpdateContent(context),
|
_buildUpdateContent(context),
|
||||||
if (_isDownloading || _downloadedFilePath != null)
|
if (_isDownloading || _downloadedFilePath != null)
|
||||||
_buildEnhancedDownloadProgress(
|
_buildEnhancedDownloadProgress(context, Theme.of(context).colorScheme),
|
||||||
context, Theme.of(context).colorScheme),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -2407,8 +2402,7 @@ class _ToastWidget extends StatelessWidget {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
|
@ -2533,9 +2527,7 @@ _UpgradeContentHeaderExtraction _extractUpgradeContentHeader(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pendingBlankLine &&
|
if (pendingBlankLine && headerLines.isNotEmpty && headerLines.last.isNotEmpty) {
|
||||||
headerLines.isNotEmpty &&
|
|
||||||
headerLines.last.isNotEmpty) {
|
|
||||||
headerLines.add('');
|
headerLines.add('');
|
||||||
}
|
}
|
||||||
headerLines.add(normalizedLine);
|
headerLines.add(normalizedLine);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,407 @@
|
||||||
|
/// Android app market provider codes accepted from backend strings.
|
||||||
|
///
|
||||||
|
/// These are intentionally strings instead of enums because the SDK receives
|
||||||
|
/// provider values from different backend services. Values are normalized to
|
||||||
|
/// uppercase canonical codes before matching.
|
||||||
|
class AndroidMarketProvider {
|
||||||
|
AndroidMarketProvider._();
|
||||||
|
|
||||||
|
static const String xiaomi = 'XIAOMI';
|
||||||
|
static const String huawei = 'HUAWEI';
|
||||||
|
static const String honor = 'HONOR';
|
||||||
|
static const String oppo = 'OPPO';
|
||||||
|
static const String vivo = 'VIVO';
|
||||||
|
static const String samsung = 'SAMSUNG';
|
||||||
|
static const String meizu = 'MEIZU';
|
||||||
|
static const String lenovo = 'LENOVO';
|
||||||
|
static const String zte = 'ZTE';
|
||||||
|
static const String coolpad = 'COOLPAD';
|
||||||
|
static const String googlePlay = 'GOOGLE_PLAY';
|
||||||
|
static const String tencent = 'TENCENT';
|
||||||
|
static const String qihoo360 = 'QIHOO_360';
|
||||||
|
static const String baidu = 'BAIDU';
|
||||||
|
static const String wandoujia = 'WANDOUJIA';
|
||||||
|
static const String coolapk = 'COOLAPK';
|
||||||
|
static const String sony = 'SONY';
|
||||||
|
static const String asus = 'ASUS';
|
||||||
|
static const String htc = 'HTC';
|
||||||
|
static const String tcl = 'TCL';
|
||||||
|
static const String smartisan = 'SMARTISAN';
|
||||||
|
static const String gionee = 'GIONEE';
|
||||||
|
static const String hisense = 'HISENSE';
|
||||||
|
static const String marketUpdateDisabledReason =
|
||||||
|
'当前设备暂不支持应用市场直接更新,请使用其他方式更新APP';
|
||||||
|
|
||||||
|
static final RegExp _separatorRegExp = RegExp(r'[\s_\-\.]+');
|
||||||
|
static final RegExp _listSeparatorRegExp = RegExp(r'[,;|,;、]+');
|
||||||
|
|
||||||
|
static const Map<String, String> _exactAliases = {
|
||||||
|
'XIAOMI': xiaomi,
|
||||||
|
'MI': xiaomi,
|
||||||
|
'MIUI': xiaomi,
|
||||||
|
'REDMI': xiaomi,
|
||||||
|
'POCO': xiaomi,
|
||||||
|
'HUAWEI': huawei,
|
||||||
|
'HONOR': honor,
|
||||||
|
'HIHONOR': honor,
|
||||||
|
'OPPO': oppo,
|
||||||
|
'HEY TAP': oppo,
|
||||||
|
'HEY_TAP': oppo,
|
||||||
|
'HEYTAP': oppo,
|
||||||
|
'ONEPLUS': oppo,
|
||||||
|
'ONEPLUSONE': oppo,
|
||||||
|
'REALME': oppo,
|
||||||
|
'VIVO': vivo,
|
||||||
|
'IQOO': vivo,
|
||||||
|
'SAMSUNG': samsung,
|
||||||
|
'GALAXY': samsung,
|
||||||
|
'MEIZU': meizu,
|
||||||
|
'LENOVO': lenovo,
|
||||||
|
'MOTOROLA': lenovo,
|
||||||
|
'MOTO': lenovo,
|
||||||
|
'ZTE': zte,
|
||||||
|
'NUBIA': zte,
|
||||||
|
'COOLPAD': coolpad,
|
||||||
|
'GOOGLE': googlePlay,
|
||||||
|
'PIXEL': googlePlay,
|
||||||
|
'GOOGLEPLAY': googlePlay,
|
||||||
|
'GOOGLESTORE': googlePlay,
|
||||||
|
'ANDROIDMARKET': googlePlay,
|
||||||
|
'TENCENT': tencent,
|
||||||
|
'YINGYONGBAO': tencent,
|
||||||
|
'YYB': tencent,
|
||||||
|
'APPBAO': tencent,
|
||||||
|
'QQDOWNLOADER': tencent,
|
||||||
|
'TENCENTAPPSTORE': tencent,
|
||||||
|
'360': qihoo360,
|
||||||
|
'QIHOO': qihoo360,
|
||||||
|
'QIHOO360': qihoo360,
|
||||||
|
'360APPSTORE': qihoo360,
|
||||||
|
'BAIDU': baidu,
|
||||||
|
'BAIDUAPPSEARCH': baidu,
|
||||||
|
'WANDOUJIA': wandoujia,
|
||||||
|
'COOLAPK': coolapk,
|
||||||
|
'SONY': sony,
|
||||||
|
'ASUS': asus,
|
||||||
|
'HTC': htc,
|
||||||
|
'TCL': tcl,
|
||||||
|
'SMARTISAN': smartisan,
|
||||||
|
'GIONEE': gionee,
|
||||||
|
'HISENSE': hisense,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const Map<String, String> _deviceExactAliases = {
|
||||||
|
'XIAOMI': xiaomi,
|
||||||
|
'MI': xiaomi,
|
||||||
|
'MIUI': xiaomi,
|
||||||
|
'REDMI': xiaomi,
|
||||||
|
'POCO': xiaomi,
|
||||||
|
'HUAWEI': huawei,
|
||||||
|
'HONOR': honor,
|
||||||
|
'HIHONOR': honor,
|
||||||
|
'OPPO': oppo,
|
||||||
|
'HEY TAP': oppo,
|
||||||
|
'HEY_TAP': oppo,
|
||||||
|
'HEYTAP': oppo,
|
||||||
|
'ONEPLUS': oppo,
|
||||||
|
'ONEPLUSONE': oppo,
|
||||||
|
'REALME': oppo,
|
||||||
|
'VIVO': vivo,
|
||||||
|
'IQOO': vivo,
|
||||||
|
'SAMSUNG': samsung,
|
||||||
|
'GALAXY': samsung,
|
||||||
|
'MEIZU': meizu,
|
||||||
|
'LENOVO': lenovo,
|
||||||
|
'MOTOROLA': lenovo,
|
||||||
|
'MOTO': lenovo,
|
||||||
|
'ZTE': zte,
|
||||||
|
'NUBIA': zte,
|
||||||
|
'COOLPAD': coolpad,
|
||||||
|
'GOOGLE': googlePlay,
|
||||||
|
'PIXEL': googlePlay,
|
||||||
|
'SONY': sony,
|
||||||
|
'ASUS': asus,
|
||||||
|
'HTC': htc,
|
||||||
|
'TCL': tcl,
|
||||||
|
'SMARTISAN': smartisan,
|
||||||
|
'GIONEE': gionee,
|
||||||
|
'HISENSE': hisense,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const List<MapEntry<String, String>> _deviceTextAliases = [
|
||||||
|
MapEntry('XIAOMI', xiaomi),
|
||||||
|
MapEntry('MI', xiaomi),
|
||||||
|
MapEntry('REDMI', xiaomi),
|
||||||
|
MapEntry('POCO', xiaomi),
|
||||||
|
MapEntry('MIUI', xiaomi),
|
||||||
|
MapEntry('HUAWEI', huawei),
|
||||||
|
MapEntry('HIHONOR', honor),
|
||||||
|
MapEntry('HONOR', honor),
|
||||||
|
MapEntry('ONEPLUS', oppo),
|
||||||
|
MapEntry('REALME', oppo),
|
||||||
|
MapEntry('OPPO', oppo),
|
||||||
|
MapEntry('IQOO', vivo),
|
||||||
|
MapEntry('VIVO', vivo),
|
||||||
|
MapEntry('SAMSUNG', samsung),
|
||||||
|
MapEntry('GALAXY', samsung),
|
||||||
|
MapEntry('MEIZU', meizu),
|
||||||
|
MapEntry('MOTOROLA', lenovo),
|
||||||
|
MapEntry('LENOVO', lenovo),
|
||||||
|
MapEntry('NUBIA', zte),
|
||||||
|
MapEntry('ZTE', zte),
|
||||||
|
MapEntry('COOLPAD', coolpad),
|
||||||
|
MapEntry('PIXEL', googlePlay),
|
||||||
|
MapEntry('GOOGLE', googlePlay),
|
||||||
|
MapEntry('SONY', sony),
|
||||||
|
MapEntry('ASUS', asus),
|
||||||
|
MapEntry('HTC', htc),
|
||||||
|
MapEntry('TCL', tcl),
|
||||||
|
MapEntry('SMARTISAN', smartisan),
|
||||||
|
MapEntry('GIONEE', gionee),
|
||||||
|
MapEntry('HISENSE', hisense),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Normalize one backend or native provider string to a canonical code.
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
/// - "xiaomi", "REDMI", "poco" -> "XIAOMI"
|
||||||
|
/// - "iQOO" -> "VIVO"
|
||||||
|
/// - "yingyongbao", "yyb" -> "TENCENT"
|
||||||
|
static String? normalize(String? provider) {
|
||||||
|
final raw = provider?.trim();
|
||||||
|
if (raw == null || raw.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final upper = raw.toUpperCase();
|
||||||
|
final compact = upper.replaceAll(_separatorRegExp, '');
|
||||||
|
|
||||||
|
return _exactAliases[upper] ?? _exactAliases[compact] ?? compact;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse and normalize a backend value. Accepts a List or a comma-separated
|
||||||
|
/// String for compatibility with loosely typed JSON mapping code.
|
||||||
|
static List<String>? parseList(dynamic value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Iterable<dynamic> items;
|
||||||
|
if (value is String) {
|
||||||
|
final trimmed = value.trim();
|
||||||
|
if (trimmed.isEmpty) {
|
||||||
|
return const <String>[];
|
||||||
|
}
|
||||||
|
items = trimmed.split(_listSeparatorRegExp);
|
||||||
|
} else if (value is Iterable) {
|
||||||
|
items = value;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final normalized = <String>[];
|
||||||
|
final seen = <String>{};
|
||||||
|
|
||||||
|
for (final item in items) {
|
||||||
|
final provider = normalize(item?.toString());
|
||||||
|
if (provider != null && seen.add(provider)) {
|
||||||
|
normalized.add(provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the Android market update entry should be visible.
|
||||||
|
///
|
||||||
|
/// This method only answers the display question. Use [checkMarketUpdate]
|
||||||
|
/// when the UI needs to know whether the visible entry is clickable.
|
||||||
|
static bool shouldShowMarketUpdateButton({
|
||||||
|
required List<String>? allowedProviders,
|
||||||
|
Map<String, dynamic>? deviceInfo,
|
||||||
|
}) {
|
||||||
|
final allowed = parseList(allowedProviders);
|
||||||
|
if (allowed == null || allowed.isEmpty) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final deviceProvider = resolveDeviceProvider(deviceInfo);
|
||||||
|
return deviceProvider != null && allowed.contains(deviceProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Async helper for callers that need to read device info before deciding
|
||||||
|
/// whether the Android market update entry should be visible.
|
||||||
|
static Future<bool> shouldShowMarketUpdateButtonWithDeviceInfo({
|
||||||
|
required List<String>? allowedProviders,
|
||||||
|
required Future<Map<String, dynamic>?> Function() getDeviceInfo,
|
||||||
|
}) async {
|
||||||
|
final allowed = parseList(allowedProviders);
|
||||||
|
if (allowed == null || allowed.isEmpty) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return shouldShowMarketUpdateButton(
|
||||||
|
allowedProviders: allowed,
|
||||||
|
deviceInfo: await getDeviceInfo(),
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the current Android device to a market provider code.
|
||||||
|
///
|
||||||
|
/// Manufacturer-like aliases are intentionally mapped to the market ecosystem
|
||||||
|
/// they use. For example REDMI and POCO both resolve to XIAOMI, while iQOO
|
||||||
|
/// resolves to VIVO.
|
||||||
|
static String? resolveDeviceProvider(Map<String, dynamic>? deviceInfo) {
|
||||||
|
if (deviceInfo == null || deviceInfo.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final candidates = <String?>[
|
||||||
|
deviceInfo['manufacturer']?.toString(),
|
||||||
|
deviceInfo['brand']?.toString(),
|
||||||
|
deviceInfo['device']?.toString(),
|
||||||
|
deviceInfo['model']?.toString(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Older Honor devices may report manufacturer=HUAWEI while brand/model
|
||||||
|
// still contains HONOR. Prefer HONOR so Huawei and Honor reviews can be
|
||||||
|
// controlled independently.
|
||||||
|
for (final candidate in candidates) {
|
||||||
|
if (_resolveDeviceText(candidate) == honor) {
|
||||||
|
return honor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final candidate in candidates) {
|
||||||
|
final provider = _resolveDeviceText(candidate);
|
||||||
|
if (provider != null) {
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true when market update should be shown for the current device.
|
||||||
|
///
|
||||||
|
/// Matching rules:
|
||||||
|
/// - null allow-list: skip validation and keep old behavior
|
||||||
|
/// - empty allow-list: market update is disabled
|
||||||
|
/// - device provider match: market update is enabled
|
||||||
|
/// - installed standalone market match, such as TENCENT: market update is enabled
|
||||||
|
static bool allowsMarketUpdate({
|
||||||
|
required List<String>? allowedProviders,
|
||||||
|
Map<String, dynamic>? deviceInfo,
|
||||||
|
List<String> installedMarkets = const <String>[],
|
||||||
|
}) {
|
||||||
|
return checkMarketUpdate(
|
||||||
|
allowedProviders: allowedProviders,
|
||||||
|
deviceInfo: deviceInfo,
|
||||||
|
installedMarkets: installedMarkets,
|
||||||
|
).allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether market update is enabled and return a user-facing disabled
|
||||||
|
/// reason when it is not.
|
||||||
|
static AndroidMarketProviderCheckResult checkMarketUpdate({
|
||||||
|
required List<String>? allowedProviders,
|
||||||
|
Map<String, dynamic>? deviceInfo,
|
||||||
|
List<String> installedMarkets = const <String>[],
|
||||||
|
}) {
|
||||||
|
final allowed = parseList(allowedProviders);
|
||||||
|
final installedProviders = parseList(installedMarkets) ?? const <String>[];
|
||||||
|
|
||||||
|
if (allowed == null) {
|
||||||
|
return AndroidMarketProviderCheckResult(
|
||||||
|
allowed: true,
|
||||||
|
allowedProviders: null,
|
||||||
|
deviceProvider: resolveDeviceProvider(deviceInfo),
|
||||||
|
installedProviders: installedProviders,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowed.isEmpty) {
|
||||||
|
return AndroidMarketProviderCheckResult(
|
||||||
|
allowed: false,
|
||||||
|
allowedProviders: allowed,
|
||||||
|
deviceProvider: resolveDeviceProvider(deviceInfo),
|
||||||
|
installedProviders: installedProviders,
|
||||||
|
disabledReason: marketUpdateDisabledReason,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final allowedSet = allowed.toSet();
|
||||||
|
final deviceProvider = resolveDeviceProvider(deviceInfo);
|
||||||
|
if (deviceProvider != null && allowedSet.contains(deviceProvider)) {
|
||||||
|
return AndroidMarketProviderCheckResult(
|
||||||
|
allowed: true,
|
||||||
|
allowedProviders: allowed,
|
||||||
|
deviceProvider: deviceProvider,
|
||||||
|
installedProviders: installedProviders,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final installedProviderMatched =
|
||||||
|
installedProviders.any(allowedSet.contains);
|
||||||
|
if (installedProviderMatched) {
|
||||||
|
return AndroidMarketProviderCheckResult(
|
||||||
|
allowed: true,
|
||||||
|
allowedProviders: allowed,
|
||||||
|
deviceProvider: deviceProvider,
|
||||||
|
installedProviders: installedProviders,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AndroidMarketProviderCheckResult(
|
||||||
|
allowed: false,
|
||||||
|
allowedProviders: allowed,
|
||||||
|
deviceProvider: deviceProvider,
|
||||||
|
installedProviders: installedProviders,
|
||||||
|
disabledReason: marketUpdateDisabledReason,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? _resolveDeviceText(String? value) {
|
||||||
|
final raw = value?.trim();
|
||||||
|
if (raw == null || raw.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final upper = raw.toUpperCase();
|
||||||
|
final compact = upper.replaceAll(_separatorRegExp, '');
|
||||||
|
final exact = _deviceExactAliases[upper] ?? _deviceExactAliases[compact];
|
||||||
|
if (exact != null) {
|
||||||
|
return exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final alias in _deviceTextAliases) {
|
||||||
|
if (compact.contains(alias.key)) {
|
||||||
|
return alias.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AndroidMarketProviderCheckResult {
|
||||||
|
const AndroidMarketProviderCheckResult({
|
||||||
|
required this.allowed,
|
||||||
|
required this.allowedProviders,
|
||||||
|
required this.deviceProvider,
|
||||||
|
required this.installedProviders,
|
||||||
|
this.disabledReason,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool allowed;
|
||||||
|
final List<String>? allowedProviders;
|
||||||
|
final String? deviceProvider;
|
||||||
|
final List<String> installedProviders;
|
||||||
|
final String? disabledReason;
|
||||||
|
|
||||||
|
bool get isRestricted => allowedProviders != null;
|
||||||
|
}
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
/// 应用商店枚举
|
|
||||||
enum AppMarket {
|
|
||||||
googlePlay,
|
|
||||||
appStore,
|
|
||||||
huawei,
|
|
||||||
oppo,
|
|
||||||
vivo,
|
|
||||||
xiaomi,
|
|
||||||
tencent,
|
|
||||||
coolapk,
|
|
||||||
custom,
|
|
||||||
unknown;
|
|
||||||
|
|
||||||
/// 从字符串创建
|
|
||||||
static AppMarket fromString(String? market) {
|
|
||||||
switch (market?.toLowerCase()) {
|
|
||||||
case 'googleplay':
|
|
||||||
return AppMarket.googlePlay;
|
|
||||||
case 'appstore':
|
|
||||||
return AppMarket.appStore;
|
|
||||||
case 'huawei':
|
|
||||||
return AppMarket.huawei;
|
|
||||||
case 'oppo':
|
|
||||||
return AppMarket.oppo;
|
|
||||||
case 'vivo':
|
|
||||||
return AppMarket.vivo;
|
|
||||||
case 'xiaomi':
|
|
||||||
return AppMarket.xiaomi;
|
|
||||||
case 'tencent':
|
|
||||||
return AppMarket.tencent;
|
|
||||||
case 'coolapk':
|
|
||||||
return AppMarket.coolapk;
|
|
||||||
case 'custom':
|
|
||||||
return AppMarket.custom;
|
|
||||||
default:
|
|
||||||
return AppMarket.unknown;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取显示名称
|
|
||||||
String get displayName {
|
|
||||||
switch (this) {
|
|
||||||
case AppMarket.googlePlay:
|
|
||||||
return 'Google Play';
|
|
||||||
case AppMarket.appStore:
|
|
||||||
return 'App Store';
|
|
||||||
case AppMarket.huawei:
|
|
||||||
return '华为应用市场';
|
|
||||||
case AppMarket.oppo:
|
|
||||||
return 'OPPO软件商店';
|
|
||||||
case AppMarket.vivo:
|
|
||||||
return 'vivo应用商店';
|
|
||||||
case AppMarket.xiaomi:
|
|
||||||
return '小米应用商店';
|
|
||||||
case AppMarket.tencent:
|
|
||||||
return '腾讯应用宝';
|
|
||||||
case AppMarket.coolapk:
|
|
||||||
return '酷安';
|
|
||||||
case AppMarket.custom:
|
|
||||||
return '自定义';
|
|
||||||
default:
|
|
||||||
return '未知';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import 'app_market.dart';
|
|
||||||
import 'app_upgrade_method.dart';
|
import 'app_upgrade_method.dart';
|
||||||
|
|
||||||
/// 应用升级版本信息(由服务器返回的数据模型)
|
/// 应用升级版本信息(由服务器返回的数据模型)
|
||||||
|
|
@ -28,10 +27,12 @@ class AppUpgradeVersion {
|
||||||
/// APK文件的MD5值 (用于校验)
|
/// APK文件的MD5值 (用于校验)
|
||||||
final String? apkMd5;
|
final String? apkMd5;
|
||||||
|
|
||||||
/// 应用商店白名单 (用于Android多渠道更新)
|
/// Android 应用市场服务商列表 (用于控制 market 更新入口是否可点击)
|
||||||
/// 配置后,只允许跳转到白名单中且设备已安装的应用市场
|
///
|
||||||
/// 如果设备上没有白名单中的任何应用市场,将提示用户选择其他更新方式
|
/// 后端推荐返回大写字符串,如 ["XIAOMI", "HUAWEI", "HONOR", "TENCENT"]。
|
||||||
final List<AppMarket>? appMarkets;
|
/// SDK 会兼容大小写,并将 REDMI/POCO 归一为 XIAOMI,iQOO 归一为 VIVO。
|
||||||
|
/// null 表示跳过校验;空数组表示 market 更新方式置灰不可点击。
|
||||||
|
final List<String>? appMarkets;
|
||||||
|
|
||||||
/// 支持的更新方式 (如果为null,默认使用所有可用的方式)
|
/// 支持的更新方式 (如果为null,默认使用所有可用的方式)
|
||||||
final List<AppUpgradeMethod>? supportedMethods;
|
final List<AppUpgradeMethod>? supportedMethods;
|
||||||
|
|
@ -51,7 +52,7 @@ class AppUpgradeVersion {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'AppUpgradeVersion(versionName: $versionName, versionBuildNumber: $versionBuildNumber, isForce: $isForce, downloadUrl: $downloadUrl, supportedMethods: $supportedMethods)';
|
return 'AppUpgradeVersion(versionName: $versionName, versionBuildNumber: $versionBuildNumber, isForce: $isForce, downloadUrl: $downloadUrl, appMarkets: $appMarkets, supportedMethods: $supportedMethods)';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取App Store地址
|
/// 获取App Store地址
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
library;
|
library;
|
||||||
|
|
||||||
export 'app_market.dart';
|
|
||||||
export 'app_upgrade_method.dart';
|
export 'app_upgrade_method.dart';
|
||||||
export 'app_upgrade_version.dart';
|
export 'app_upgrade_version.dart';
|
||||||
|
export 'android_market_provider.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import 'app_market.dart';
|
|
||||||
import 'app_upgrade_method.dart';
|
import 'app_upgrade_method.dart';
|
||||||
|
import 'android_market_provider.dart';
|
||||||
|
|
||||||
/// App升级信息模型
|
/// App升级信息模型
|
||||||
class UpgradeInfo {
|
class UpgradeInfo {
|
||||||
|
|
@ -36,10 +36,13 @@ class UpgradeInfo {
|
||||||
/// APK MD5值(用于校验)
|
/// APK MD5值(用于校验)
|
||||||
final String? apkMd5;
|
final String? apkMd5;
|
||||||
|
|
||||||
/// 应用商店白名单(用于Android多渠道更新)
|
/// Android 应用市场服务商列表(用于控制 market 更新入口是否可点击)
|
||||||
/// 配置后,只允许跳转到白名单中且设备已安装的应用市场
|
///
|
||||||
/// 如果设备上没有白名单中的任何应用市场,将提示用户选择其他更新方式
|
/// 由后端返回字符串数组,例如 ["XIAOMI", "HUAWEI", "HONOR", "TENCENT"]。
|
||||||
final List<AppMarket>? appMarkets;
|
/// - null: 跳过校验,保持旧逻辑,market 更新方式可点击
|
||||||
|
/// - 空数组: market 更新方式置灰不可点击
|
||||||
|
/// - 非空数组: 当前设备厂商或已安装独立市场匹配时才可点击
|
||||||
|
final List<String>? appMarkets;
|
||||||
|
|
||||||
/// 支持的更新方式
|
/// 支持的更新方式
|
||||||
final List<AppUpgradeMethod> supportedMethods;
|
final List<AppUpgradeMethod> supportedMethods;
|
||||||
|
|
@ -57,7 +60,11 @@ class UpgradeInfo {
|
||||||
this.apkSize,
|
this.apkSize,
|
||||||
this.apkMd5,
|
this.apkMd5,
|
||||||
this.appMarkets,
|
this.appMarkets,
|
||||||
this.supportedMethods = const [AppUpgradeMethod.market, AppUpgradeMethod.browser, AppUpgradeMethod.inApp],
|
this.supportedMethods = const [
|
||||||
|
AppUpgradeMethod.market,
|
||||||
|
AppUpgradeMethod.browser,
|
||||||
|
AppUpgradeMethod.inApp
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 从JSON创建
|
/// 从JSON创建
|
||||||
|
|
@ -99,11 +106,16 @@ class UpgradeInfo {
|
||||||
return AppUpgradeMethod.inApp;
|
return AppUpgradeMethod.inApp;
|
||||||
}).toList();
|
}).toList();
|
||||||
} else {
|
} else {
|
||||||
supportedMethods = [AppUpgradeMethod.market, AppUpgradeMethod.browser, AppUpgradeMethod.inApp];
|
supportedMethods = [
|
||||||
|
AppUpgradeMethod.market,
|
||||||
|
AppUpgradeMethod.browser,
|
||||||
|
AppUpgradeMethod.inApp
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return UpgradeInfo(
|
return UpgradeInfo(
|
||||||
hasUpdate: versionBuildNumber != currentBuildNumber || versionName != currentVersionName,
|
hasUpdate: versionBuildNumber != currentBuildNumber ||
|
||||||
|
versionName != currentVersionName,
|
||||||
isForceUpdate: json['isForceUpdate'] ?? false,
|
isForceUpdate: json['isForceUpdate'] ?? false,
|
||||||
versionBuildNumber: versionBuildNumber,
|
versionBuildNumber: versionBuildNumber,
|
||||||
versionName: versionName,
|
versionName: versionName,
|
||||||
|
|
@ -114,7 +126,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>?)?.map((e) => AppMarket.fromString(e as String)).toList(),
|
appMarkets: AndroidMarketProvider.parseList(json['appMarkets']),
|
||||||
supportedMethods: supportedMethods,
|
supportedMethods: supportedMethods,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -132,7 +144,7 @@ class UpgradeInfo {
|
||||||
'appStoreUrl': appStoreUrl,
|
'appStoreUrl': appStoreUrl,
|
||||||
'apkSize': apkSize,
|
'apkSize': apkSize,
|
||||||
'apkMd5': apkMd5,
|
'apkMd5': apkMd5,
|
||||||
'appMarkets': appMarkets?.map((e) => e.name).toList(),
|
'appMarkets': appMarkets,
|
||||||
'supportedMethods': supportedMethods.map((e) => e.name).toList(),
|
'supportedMethods': supportedMethods.map((e) => e.name).toList(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^5.0.0
|
flutter_lints: ^5.0.0
|
||||||
|
mocktail: ^1.0.5
|
||||||
|
device_info_plus: ^12.4.0
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,589 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:yx_app_upgrade_flutter/models/android_market_provider.dart';
|
||||||
|
import 'package:yx_app_upgrade_flutter/models/upgrade_info.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('AndroidMarketProvider', () {
|
||||||
|
test('normalizes Xiaomi ecosystem aliases to XIAOMI', () {
|
||||||
|
expect(AndroidMarketProvider.normalize('xiaomi'), 'XIAOMI');
|
||||||
|
expect(AndroidMarketProvider.normalize('REDMI'), 'XIAOMI');
|
||||||
|
expect(AndroidMarketProvider.normalize('poco'), 'XIAOMI');
|
||||||
|
expect(AndroidMarketProvider.normalize('miui'), 'XIAOMI');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normalizes common backend aliases and installed-market values', () {
|
||||||
|
final cases = <String, String>{
|
||||||
|
'googlePlay': 'GOOGLE_PLAY',
|
||||||
|
'google-play': 'GOOGLE_PLAY',
|
||||||
|
'GOOGLE_PLAY': 'GOOGLE_PLAY',
|
||||||
|
'yingyongbao': 'TENCENT',
|
||||||
|
'YYB': 'TENCENT',
|
||||||
|
'qq_downloader': 'TENCENT',
|
||||||
|
'qihoo360': 'QIHOO_360',
|
||||||
|
'360': 'QIHOO_360',
|
||||||
|
'baidu_app_search': 'BAIDU',
|
||||||
|
'wandoujia': 'WANDOUJIA',
|
||||||
|
'coolapk': 'COOLAPK',
|
||||||
|
'motorola': 'LENOVO',
|
||||||
|
'nubia': 'ZTE',
|
||||||
|
'hey tap': 'OPPO',
|
||||||
|
};
|
||||||
|
|
||||||
|
for (final entry in cases.entries) {
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.normalize(entry.key),
|
||||||
|
entry.value,
|
||||||
|
reason: '${entry.key} should normalize to ${entry.value}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normalizes null, blank, unknown and separated values safely', () {
|
||||||
|
expect(AndroidMarketProvider.normalize(null), isNull);
|
||||||
|
expect(AndroidMarketProvider.normalize(''), isNull);
|
||||||
|
expect(AndroidMarketProvider.normalize(' '), isNull);
|
||||||
|
expect(AndroidMarketProvider.normalize('custom_vendor'), 'CUSTOMVENDOR');
|
||||||
|
expect(AndroidMarketProvider.normalize('foo-bar.baz'), 'FOOBARBAZ');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolves device aliases to their app market ecosystem', () {
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.resolveDeviceProvider({
|
||||||
|
'manufacturer': 'Redmi',
|
||||||
|
'brand': 'Xiaomi',
|
||||||
|
'model': 'Redmi Note 12',
|
||||||
|
}),
|
||||||
|
'XIAOMI',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.resolveDeviceProvider({
|
||||||
|
'manufacturer': 'iQOO',
|
||||||
|
'brand': 'vivo',
|
||||||
|
}),
|
||||||
|
'VIVO',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.resolveDeviceProvider({
|
||||||
|
'manufacturer': 'OnePlus',
|
||||||
|
'brand': 'OnePlus',
|
||||||
|
}),
|
||||||
|
'OPPO',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.resolveDeviceProvider({
|
||||||
|
'manufacturer': 'HONOR',
|
||||||
|
'brand': 'HONOR',
|
||||||
|
}),
|
||||||
|
'HONOR',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.resolveDeviceProvider({
|
||||||
|
'manufacturer': 'HUAWEI',
|
||||||
|
'brand': 'HONOR',
|
||||||
|
'model': 'HONOR 30',
|
||||||
|
}),
|
||||||
|
'HONOR',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'resolves provider from brand, device or model when manufacturer is unclear',
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.resolveDeviceProvider({
|
||||||
|
'manufacturer': 'unknown',
|
||||||
|
'brand': 'POCO',
|
||||||
|
'model': 'POCO F5',
|
||||||
|
}),
|
||||||
|
'XIAOMI',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.resolveDeviceProvider({
|
||||||
|
'manufacturer': 'unknown',
|
||||||
|
'brand': 'unknown',
|
||||||
|
'device': 'VIVO2019',
|
||||||
|
}),
|
||||||
|
'VIVO',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.resolveDeviceProvider({
|
||||||
|
'manufacturer': 'unknown',
|
||||||
|
'brand': 'unknown',
|
||||||
|
'model': 'Samsung SM-S9280',
|
||||||
|
}),
|
||||||
|
'SAMSUNG',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolves Google or Pixel emulator as GOOGLE_PLAY provider', () {
|
||||||
|
final googleEmulator = {
|
||||||
|
'manufacturer': 'Google',
|
||||||
|
'brand': 'google',
|
||||||
|
'device': 'emulator64_x86_64',
|
||||||
|
'model': 'sdk_gphone64_x86_64',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.resolveDeviceProvider(googleEmulator),
|
||||||
|
'GOOGLE_PLAY',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.allowsMarketUpdate(
|
||||||
|
allowedProviders: const ['GOOGLE_PLAY'],
|
||||||
|
deviceInfo: googleEmulator,
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.allowsMarketUpdate(
|
||||||
|
allowedProviders: const ['XIAOMI'],
|
||||||
|
deviceInfo: googleEmulator,
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('treats generic AOSP emulator without market ecosystem as unknown',
|
||||||
|
() {
|
||||||
|
final aospEmulator = {
|
||||||
|
'manufacturer': 'unknown',
|
||||||
|
'brand': 'Android',
|
||||||
|
'device': 'generic_x86',
|
||||||
|
'model': 'Android SDK built for x86',
|
||||||
|
};
|
||||||
|
|
||||||
|
final result = AndroidMarketProvider.checkMarketUpdate(
|
||||||
|
allowedProviders: const ['GOOGLE_PLAY'],
|
||||||
|
deviceInfo: aospEmulator,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(AndroidMarketProvider.resolveDeviceProvider(aospEmulator), isNull);
|
||||||
|
expect(result.allowed, isFalse);
|
||||||
|
expect(
|
||||||
|
result.disabledReason,
|
||||||
|
AndroidMarketProvider.marketUpdateDisabledReason,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for missing or unknown device provider', () {
|
||||||
|
expect(AndroidMarketProvider.resolveDeviceProvider(null), isNull);
|
||||||
|
expect(AndroidMarketProvider.resolveDeviceProvider({}), isNull);
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.resolveDeviceProvider({
|
||||||
|
'manufacturer': 'unknown',
|
||||||
|
'brand': 'unknown',
|
||||||
|
'model': 'unknown',
|
||||||
|
}),
|
||||||
|
isNull,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not resolve standalone app markets as device providers', () {
|
||||||
|
final standaloneValues = [
|
||||||
|
'TENCENT',
|
||||||
|
'YINGYONGBAO',
|
||||||
|
'YYB',
|
||||||
|
'BAIDU',
|
||||||
|
'BAIDUAPPSEARCH',
|
||||||
|
'QIHOO360',
|
||||||
|
'360',
|
||||||
|
'WANDOUJIA',
|
||||||
|
'COOLAPK',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final value in standaloneValues) {
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.resolveDeviceProvider({
|
||||||
|
'manufacturer': value,
|
||||||
|
'brand': value,
|
||||||
|
'model': value,
|
||||||
|
}),
|
||||||
|
isNull,
|
||||||
|
reason: '$value is an app market, not a device provider',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps null allow-list as old behavior and disables empty allow-list',
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.allowsMarketUpdate(
|
||||||
|
allowedProviders: null,
|
||||||
|
deviceInfo: {'manufacturer': 'Unknown'},
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.allowsMarketUpdate(
|
||||||
|
allowedProviders: const <String>[],
|
||||||
|
deviceInfo: {'manufacturer': 'Xiaomi'},
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns disabled reason for explicit empty appMarkets', () {
|
||||||
|
final result = AndroidMarketProvider.checkMarketUpdate(
|
||||||
|
allowedProviders: const <String>[],
|
||||||
|
deviceInfo: {'manufacturer': 'Xiaomi'},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.allowed, isFalse);
|
||||||
|
expect(result.isRestricted, isTrue);
|
||||||
|
expect(result.allowedProviders, const <String>[]);
|
||||||
|
expect(result.deviceProvider, 'XIAOMI');
|
||||||
|
expect(
|
||||||
|
result.disabledReason,
|
||||||
|
AndroidMarketProvider.marketUpdateDisabledReason,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns disabled reason when current device provider is not allowed',
|
||||||
|
() {
|
||||||
|
final result = AndroidMarketProvider.checkMarketUpdate(
|
||||||
|
allowedProviders: const ['HUAWEI', 'HONOR'],
|
||||||
|
deviceInfo: {'manufacturer': 'Redmi'},
|
||||||
|
installedMarkets: const ['xiaomi'],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.allowed, isFalse);
|
||||||
|
expect(result.allowedProviders, const ['HUAWEI', 'HONOR']);
|
||||||
|
expect(result.deviceProvider, 'XIAOMI');
|
||||||
|
expect(result.installedProviders, const ['XIAOMI']);
|
||||||
|
expect(
|
||||||
|
result.disabledReason,
|
||||||
|
AndroidMarketProvider.marketUpdateDisabledReason,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns disabled reason when device provider cannot be recognized',
|
||||||
|
() {
|
||||||
|
final result = AndroidMarketProvider.checkMarketUpdate(
|
||||||
|
allowedProviders: const ['HUAWEI'],
|
||||||
|
deviceInfo: {'manufacturer': 'UnknownVendor'},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.allowed, isFalse);
|
||||||
|
expect(result.deviceProvider, isNull);
|
||||||
|
expect(
|
||||||
|
result.disabledReason,
|
||||||
|
AndroidMarketProvider.marketUpdateDisabledReason,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns enabled result when appMarkets is null', () {
|
||||||
|
final result = AndroidMarketProvider.checkMarketUpdate(
|
||||||
|
allowedProviders: null,
|
||||||
|
deviceInfo: {'manufacturer': 'UnknownVendor'},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.allowed, isTrue);
|
||||||
|
expect(result.isRestricted, isFalse);
|
||||||
|
expect(result.disabledReason, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns enabled result with normalized fields on provider match', () {
|
||||||
|
final result = AndroidMarketProvider.checkMarketUpdate(
|
||||||
|
allowedProviders: const ['XIAOMI'],
|
||||||
|
deviceInfo: {'manufacturer': 'Redmi'},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.allowed, isTrue);
|
||||||
|
expect(result.allowedProviders, const ['XIAOMI']);
|
||||||
|
expect(result.deviceProvider, 'XIAOMI');
|
||||||
|
expect(result.disabledReason, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses list values, removes blanks and deduplicates aliases', () {
|
||||||
|
expect(AndroidMarketProvider.parseList(null), isNull);
|
||||||
|
expect(AndroidMarketProvider.parseList(''), const <String>[]);
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.parseList(' , ; | , ; 、 '), const <String>[]);
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.parseList([
|
||||||
|
'xiaomi',
|
||||||
|
'REDMI',
|
||||||
|
' poco ',
|
||||||
|
null,
|
||||||
|
'',
|
||||||
|
'TENCENT',
|
||||||
|
'yingyongbao',
|
||||||
|
360,
|
||||||
|
]),
|
||||||
|
const ['XIAOMI', 'TENCENT', 'QIHOO_360'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses mixed separators from backend strings', () {
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.parseList(
|
||||||
|
'xiaomi, huawei;honor|oppo,vivo;yyb、360'),
|
||||||
|
const [
|
||||||
|
'XIAOMI',
|
||||||
|
'HUAWEI',
|
||||||
|
'HONOR',
|
||||||
|
'OPPO',
|
||||||
|
'VIVO',
|
||||||
|
'TENCENT',
|
||||||
|
'QIHOO_360',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('matches device provider when allow-list is not empty', () {
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.allowsMarketUpdate(
|
||||||
|
allowedProviders: const ['xiaomi', 'HUAWEI'],
|
||||||
|
deviceInfo: {'manufacturer': 'Redmi'},
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.allowsMarketUpdate(
|
||||||
|
allowedProviders: const ['HUAWEI'],
|
||||||
|
deviceInfo: {'manufacturer': 'HONOR'},
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.allowsMarketUpdate(
|
||||||
|
allowedProviders: const ['HONOR'],
|
||||||
|
deviceInfo: {'manufacturer': 'HONOR'},
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('treats Huawei and Honor as separate app market providers', () {
|
||||||
|
final oldHonorDevice = {
|
||||||
|
'manufacturer': 'HUAWEI',
|
||||||
|
'brand': 'HONOR',
|
||||||
|
'model': 'HONOR 30',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.allowsMarketUpdate(
|
||||||
|
allowedProviders: const ['HUAWEI'],
|
||||||
|
deviceInfo: oldHonorDevice,
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.allowsMarketUpdate(
|
||||||
|
allowedProviders: const ['HONOR'],
|
||||||
|
deviceInfo: oldHonorDevice,
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.allowsMarketUpdate(
|
||||||
|
allowedProviders: const ['HUAWEI'],
|
||||||
|
deviceInfo: {'manufacturer': 'HUAWEI', 'brand': 'HUAWEI'},
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not allow market update when device and installed market miss',
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.allowsMarketUpdate(
|
||||||
|
allowedProviders: const ['HUAWEI', 'HONOR'],
|
||||||
|
deviceInfo: {'manufacturer': 'Redmi'},
|
||||||
|
installedMarkets: const ['xiaomi'],
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('matches standalone market providers by installed markets', () {
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.allowsMarketUpdate(
|
||||||
|
allowedProviders: const ['TENCENT'],
|
||||||
|
deviceInfo: {'manufacturer': 'Xiaomi'},
|
||||||
|
installedMarkets: const ['tencent'],
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.allowsMarketUpdate(
|
||||||
|
allowedProviders: const ['TENCENT'],
|
||||||
|
deviceInfo: {'manufacturer': 'Xiaomi'},
|
||||||
|
installedMarkets: const <String>[],
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('requires installation for app markets without matching phone brand',
|
||||||
|
() {
|
||||||
|
final cases = <String, String>{
|
||||||
|
'TENCENT': 'tencent',
|
||||||
|
'BAIDU': 'baidu',
|
||||||
|
'QIHOO_360': 'qihoo360',
|
||||||
|
'WANDOUJIA': 'wandoujia',
|
||||||
|
'COOLAPK': 'coolapk',
|
||||||
|
};
|
||||||
|
|
||||||
|
for (final entry in cases.entries) {
|
||||||
|
final notInstalled = AndroidMarketProvider.checkMarketUpdate(
|
||||||
|
allowedProviders: [entry.key],
|
||||||
|
deviceInfo: {
|
||||||
|
'manufacturer': entry.key,
|
||||||
|
'brand': entry.key,
|
||||||
|
'model': entry.key,
|
||||||
|
},
|
||||||
|
installedMarkets: const <String>[],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(notInstalled.allowed, isFalse,
|
||||||
|
reason: '${entry.key} must not match by device brand');
|
||||||
|
expect(notInstalled.deviceProvider, isNull);
|
||||||
|
expect(
|
||||||
|
notInstalled.disabledReason,
|
||||||
|
AndroidMarketProvider.marketUpdateDisabledReason,
|
||||||
|
);
|
||||||
|
|
||||||
|
final installed = AndroidMarketProvider.checkMarketUpdate(
|
||||||
|
allowedProviders: [entry.key],
|
||||||
|
deviceInfo: {
|
||||||
|
'manufacturer': 'UnknownVendor',
|
||||||
|
'brand': 'UnknownVendor',
|
||||||
|
},
|
||||||
|
installedMarkets: [entry.value],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(installed.allowed, isTrue,
|
||||||
|
reason: '${entry.key} should match when installed market exists');
|
||||||
|
expect(installed.deviceProvider, isNull);
|
||||||
|
expect(installed.installedProviders, [entry.key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('matches installed OEM markets with normalized package mapping values',
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.allowsMarketUpdate(
|
||||||
|
allowedProviders: const ['GOOGLE_PLAY'],
|
||||||
|
deviceInfo: {'manufacturer': 'Samsung'},
|
||||||
|
installedMarkets: const ['googlePlay'],
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.allowsMarketUpdate(
|
||||||
|
allowedProviders: const ['QIHOO_360'],
|
||||||
|
deviceInfo: {'manufacturer': 'Xiaomi'},
|
||||||
|
installedMarkets: const ['qihoo360'],
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.allowsMarketUpdate(
|
||||||
|
allowedProviders: const ['BAIDU'],
|
||||||
|
deviceInfo: {'manufacturer': 'Xiaomi'},
|
||||||
|
installedMarkets: const ['baidu'],
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'allows unknown custom providers only when installed market also matches',
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.allowsMarketUpdate(
|
||||||
|
allowedProviders: const ['CUSTOM_VENDOR'],
|
||||||
|
deviceInfo: {'manufacturer': 'Xiaomi'},
|
||||||
|
installedMarkets: const ['custom-vendor'],
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.allowsMarketUpdate(
|
||||||
|
allowedProviders: const ['CUSTOM_VENDOR'],
|
||||||
|
deviceInfo: {'manufacturer': 'Xiaomi'},
|
||||||
|
installedMarkets: const ['tencent'],
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses comma-separated provider strings for compatibility', () {
|
||||||
|
expect(
|
||||||
|
AndroidMarketProvider.parseList('xiaomi, redmi; yingyongbao|iqoo'),
|
||||||
|
const ['XIAOMI', 'TENCENT', 'VIVO'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('UpgradeInfo', () {
|
||||||
|
test('parses appMarkets from json and normalizes aliases', () {
|
||||||
|
final info = UpgradeInfo.fromJson(
|
||||||
|
{
|
||||||
|
'isForceUpdate': false,
|
||||||
|
'versionBuildNumber': 2,
|
||||||
|
'versionName': '2.0.0',
|
||||||
|
'updateContent': 'test',
|
||||||
|
'appMarkets': ['xiaomi', 'REDMI', 'yingyongbao'],
|
||||||
|
},
|
||||||
|
currentBuildNumber: 1,
|
||||||
|
currentVersionName: '1.0.0',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
info.appMarkets,
|
||||||
|
const ['XIAOMI', 'TENCENT'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps null appMarkets when server does not return the field', () {
|
||||||
|
final info = UpgradeInfo.fromJson(
|
||||||
|
{
|
||||||
|
'isForceUpdate': false,
|
||||||
|
'versionBuildNumber': 2,
|
||||||
|
'versionName': '2.0.0',
|
||||||
|
'updateContent': 'test',
|
||||||
|
},
|
||||||
|
currentBuildNumber: 1,
|
||||||
|
currentVersionName: '1.0.0',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(info.appMarkets, isNull);
|
||||||
|
expect(info.toJson()['appMarkets'], isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps empty appMarkets as explicit disabled-market instruction', () {
|
||||||
|
final info = UpgradeInfo.fromJson(
|
||||||
|
{
|
||||||
|
'isForceUpdate': false,
|
||||||
|
'versionBuildNumber': 2,
|
||||||
|
'versionName': '2.0.0',
|
||||||
|
'updateContent': 'test',
|
||||||
|
'appMarkets': <String>[],
|
||||||
|
},
|
||||||
|
currentBuildNumber: 1,
|
||||||
|
currentVersionName: '1.0.0',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(info.appMarkets, const <String>[]);
|
||||||
|
expect(info.toJson()['appMarkets'], const <String>[]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses appMarkets from comma-separated server string', () {
|
||||||
|
final info = UpgradeInfo.fromJson(
|
||||||
|
{
|
||||||
|
'isForceUpdate': false,
|
||||||
|
'versionBuildNumber': 2,
|
||||||
|
'versionName': '2.0.0',
|
||||||
|
'updateContent': 'test',
|
||||||
|
'appMarkets': 'huawei, honor, xiaomi, redmi',
|
||||||
|
},
|
||||||
|
currentBuildNumber: 1,
|
||||||
|
currentVersionName: '1.0.0',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(info.appMarkets, const ['HUAWEI', 'HONOR', 'XIAOMI']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:yx_app_upgrade_flutter/models/android_market_provider.dart';
|
||||||
|
|
||||||
|
class MockDeviceInfoPlugin extends Mock implements DeviceInfoPlugin {}
|
||||||
|
|
||||||
|
class MockAndroidDeviceInfo extends Mock implements AndroidDeviceInfo {}
|
||||||
|
|
||||||
|
Future<bool> shouldShowMarketUpdateButtonWithPlugin({
|
||||||
|
required List<String>? appMarkets,
|
||||||
|
required DeviceInfoPlugin deviceInfoPlugin,
|
||||||
|
}) {
|
||||||
|
return AndroidMarketProvider.shouldShowMarketUpdateButtonWithDeviceInfo(
|
||||||
|
allowedProviders: appMarkets,
|
||||||
|
getDeviceInfo: () async {
|
||||||
|
final androidInfo = await deviceInfoPlugin.androidInfo;
|
||||||
|
return {
|
||||||
|
'manufacturer': androidInfo.manufacturer,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('shouldShowMarketUpdateButton', () {
|
||||||
|
late MockDeviceInfoPlugin deviceInfoPlugin;
|
||||||
|
late MockAndroidDeviceInfo androidInfo;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
deviceInfoPlugin = MockDeviceInfoPlugin();
|
||||||
|
androidInfo = MockAndroidDeviceInfo();
|
||||||
|
});
|
||||||
|
|
||||||
|
void mockManufacturer(String manufacturer) {
|
||||||
|
when(() => androidInfo.manufacturer).thenReturn(manufacturer);
|
||||||
|
when(() => deviceInfoPlugin.androidInfo)
|
||||||
|
.thenAnswer((_) async => androidInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('returns true when backend whitelist is null', () async {
|
||||||
|
final result = await shouldShowMarketUpdateButtonWithPlugin(
|
||||||
|
appMarkets: null,
|
||||||
|
deviceInfoPlugin: deviceInfoPlugin,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result, isTrue);
|
||||||
|
verifyNever(() => deviceInfoPlugin.androidInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns true when backend whitelist is empty', () async {
|
||||||
|
final result = await shouldShowMarketUpdateButtonWithPlugin(
|
||||||
|
appMarkets: const <String>[],
|
||||||
|
deviceInfoPlugin: deviceInfoPlugin,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result, isTrue);
|
||||||
|
verifyNever(() => deviceInfoPlugin.androidInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns true when current manufacturer is in whitelist ignoring case',
|
||||||
|
() async {
|
||||||
|
mockManufacturer('Xiaomi');
|
||||||
|
|
||||||
|
final result = await shouldShowMarketUpdateButtonWithPlugin(
|
||||||
|
appMarkets: const ['XIAOMI'],
|
||||||
|
deviceInfoPlugin: deviceInfoPlugin,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result, isTrue);
|
||||||
|
verify(() => deviceInfoPlugin.androidInfo).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns true for Google emulator when whitelist contains GOOGLE_PLAY',
|
||||||
|
() async {
|
||||||
|
mockManufacturer('Google');
|
||||||
|
|
||||||
|
final result = await shouldShowMarketUpdateButtonWithPlugin(
|
||||||
|
appMarkets: const ['GOOGLE_PLAY'],
|
||||||
|
deviceInfoPlugin: deviceInfoPlugin,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result, isTrue);
|
||||||
|
verify(() => deviceInfoPlugin.androidInfo).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false when current manufacturer is not in whitelist',
|
||||||
|
() async {
|
||||||
|
mockManufacturer('Samsung');
|
||||||
|
|
||||||
|
final result = await shouldShowMarketUpdateButtonWithPlugin(
|
||||||
|
appMarkets: const ['XIAOMI', 'HUAWEI'],
|
||||||
|
deviceInfoPlugin: deviceInfoPlugin,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result, isFalse);
|
||||||
|
verify(() => deviceInfoPlugin.androidInfo).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false when reading manufacturer throws', () async {
|
||||||
|
when(() => deviceInfoPlugin.androidInfo).thenThrow(
|
||||||
|
Exception('failed to read device info'),
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await shouldShowMarketUpdateButtonWithPlugin(
|
||||||
|
appMarkets: const ['XIAOMI'],
|
||||||
|
deviceInfoPlugin: deviceInfoPlugin,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result, isFalse);
|
||||||
|
verify(() => deviceInfoPlugin.androidInfo).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false when manufacturer is blank', () async {
|
||||||
|
mockManufacturer('');
|
||||||
|
|
||||||
|
final result = await shouldShowMarketUpdateButtonWithPlugin(
|
||||||
|
appMarkets: const ['XIAOMI'],
|
||||||
|
deviceInfoPlugin: deviceInfoPlugin,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result, isFalse);
|
||||||
|
verify(() => deviceInfoPlugin.androidInfo).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue