Commit bbb642ff by tanghuan

iot更新

1 parent a2c3bf0e
......@@ -18,6 +18,9 @@
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<!-- APP 自动更新:安装未知来源应用 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:label="班小二"
......
......@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:appframe/config/constant.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/config/routes.dart';
import 'package:appframe/services/app_upgrade_service.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluwx/fluwx.dart';
......@@ -80,6 +81,9 @@ class SettingCubit extends Cubit<SettingState> {
// IM 登出
// await getIt.get<ImService>().logout();
// 退出登录后,停止APP自动更新轮询
getIt.get<AppUpgradeService>().stop();
router.go('/loginMain');
}
......
......@@ -8,6 +8,7 @@ import 'package:appframe/config/locator.dart';
import 'package:appframe/config/routes.dart';
import 'package:appframe/data/models/message/h5_message.dart';
import 'package:appframe/services/dispatcher.dart';
import 'package:appframe/services/app_upgrade_service.dart';
import 'package:appframe/services/im_service.dart';
import 'package:appframe/services/local_server_service.dart';
import 'package:appframe/services/player_service.dart';
......@@ -264,6 +265,9 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
// 登录IM
_loginIM();
// 登录成功后启动 APP 自动更新轮询(仅 Android 内部生效)
getIt.get<AppUpgradeService>().start();
// 注册 WebCubit 实例,供其它页面使用
WebCubitHolder.register(this);
}
......
......@@ -49,8 +49,12 @@ class Constant {
/// 版本相关
/// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///
/// app 版本号规则
static const String appVersion = EnvConfig.version;
/// app 版本号
///
/// 实际取值来源于 pubspec.yaml 的 version 字段,
/// 在 main.dart 中通过 package_info_plus 读取后赋值;
/// EnvConfig.version 仅作为读取失败时的兜底默认值。
static String appVersion = EnvConfig.version;
/// H5的起始终最低版本号规则
static String h5Version = '0.2.6';
......@@ -68,6 +72,13 @@ class Constant {
// ? 'http://192.168.2.177/xeapp_conf_dev.json'
: 'https://bxe-obs.banxiaoer.com/conf/xeapp_conf_pro.json';
/// Android APP 最新版本配置地址(用于 APP 自动更新)
/// APP 自动更新轮询间隔
static const Duration appUpgradeCheckInterval = Duration(minutes: 5);
/// APP 自动更新时,下载到本地的 APK 缓存子目录
static const String appUpgradeApkDir = 'app_upgrade';
/// 内部 H5 dist 目录
static const String h5DistDir = 'http_dist_assets';
......
......@@ -42,8 +42,10 @@ import 'package:appframe/data/repositories/message/window_info_handler.dart';
import 'package:appframe/data/repositories/phone_auth_repository.dart';
import 'package:appframe/data/repositories/user_auth_repository.dart';
import 'package:appframe/data/repositories/subs_repository.dart';
import 'package:appframe/data/repositories/version_repository.dart';
import 'package:appframe/data/repositories/wechat_auth_repository.dart';
import 'package:appframe/services/api_service.dart';
import 'package:appframe/services/app_upgrade_service.dart';
import 'package:appframe/services/dispatcher.dart';
import 'package:appframe/services/im_service.dart';
import 'package:appframe/services/local_server_service.dart';
......@@ -232,6 +234,9 @@ Future<void> setupLocator() async {
/// imService
getIt.registerSingleton<ImService>(ImService());
/// APP 自动更新服务(仅 Android 生效)
getIt.registerLazySingleton<AppUpgradeService>(() => AppUpgradeService());
/// 播放
getIt.registerSingleton<PlayerService>(PlayerService());
......@@ -243,5 +248,6 @@ Future<void> setupLocator() async {
getIt.registerLazySingleton<PhoneAuthRepository>(() => PhoneAuthRepository());
getIt.registerLazySingleton<UserAuthRepository>(() => UserAuthRepository());
getIt.registerLazySingleton<SubsRepository>(() => SubsRepository());
getIt.registerLazySingleton<VersionRepository>(() => VersionRepository());
}
......@@ -4,6 +4,7 @@ import 'package:app_settings/app_settings.dart';
import 'package:appframe/config/constant.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/config/routes.dart';
import 'package:appframe/services/app_upgrade_service.dart';
import 'package:appframe/services/dispatcher.dart';
import 'package:fluwx/fluwx.dart';
import 'package:path_provider/path_provider.dart';
......@@ -174,6 +175,9 @@ class OpenSettingHandler extends MessageHandler {
// IM 登出
// await getIt.get<ImService>().logout();
// 退出登录后,停止APP自动更新轮询
getIt.get<AppUpgradeService>().stop();
router.go('/loginMain');
}
}
import 'package:appframe/config/locator.dart';
import 'package:appframe/services/api_service.dart';
import 'package:dio/dio.dart';
class VersionRepository {
late final ApiService _appService;
VersionRepository() {
_appService = getIt<ApiService>(instanceName: 'appApiService');
}
///
/// 参数
/// {
/// "userid":"xxxxxxx",
/// "ver":"1.0.9",
/// "sence":"xxj"
/// }
/// 返回
/// {"code":0,"data":{"force":0,"lastVersion":"","url":""},"message":"查询成功"}
///
///
Future<dynamic> globalVersion(String userid, String ver, String sence) async {
Response resp = await _appService.get(
'/api/v1/comm/golbal/version',
queryParameters: {
"userid": userid,
"ver": ver,
"sence": sence,
},
);
return resp.data;
}
}
......@@ -8,6 +8,7 @@ import 'package:appframe/ui/widgets/ios_edge_swipe_detector.dart';
import 'package:archive/archive.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'app.dart';
......@@ -16,12 +17,28 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized();
await setupLocator();
await _initAppVersion();
await _initH5Version();
await _initIOSGesture();
runApp(const App());
}
/// 从 pubspec.yaml 中读取并初始化 APP 版本号
///
/// pubspec.yaml 中的 version 字段作为全局唯一数据源,
/// 避免与构建参数、硬编码出现不一致。
Future<void> _initAppVersion() async {
try {
final info = await PackageInfo.fromPlatform();
if (info.version.isNotEmpty) {
Constant.appVersion = info.version;
}
} catch (e) {
debugPrint('读取 APP 版本号失败,使用默认值: $e');
}
}
Future<void> _initH5Version() async {
// 1 读取保存的H5版本
var h5Version = getIt.get<SharedPreferences>().getString(Constant.h5VersionKey);
......
import 'dart:async';
import 'dart:io';
import 'package:appframe/config/constant.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/data/repositories/version_repository.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:open_file/open_file.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// APP 自动更新服务(仅 Android)
///
/// 功能:
/// 1. 登录成功后每 [Constant.appUpgradeCheckInterval] 轮询服务端最新版本配置;
/// 2. 与当前 APP 版本([Constant.appVersion],来源于 pubspec.yaml)比较,判断是否需要更新;
/// 3. 需要更新时下载 APK 并调用系统安装界面进行覆盖安装。
class AppUpgradeService {
late final VersionRepository _versionRepository;
AppUpgradeService() {
_versionRepository = getIt.get<VersionRepository>();
}
Timer? _timer;
/// 是否正在执行一次完整的检测/下载/安装流程,避免并发触发
bool _busy = false;
/// 已成功触发安装的版本,避免重复下载与安装
String? _installedVersion;
/// 启动定时轮询。仅 Android 平台生效,仅在登录成功后调用。
void start() {
if (!Platform.isAndroid) {
return;
}
if (_timer != null) {
return;
}
// 立即执行一次,再按周期循环
_check();
_timer = Timer.periodic(Constant.appUpgradeCheckInterval, (_) => _check());
}
/// 停止轮询
void stop() {
_timer?.cancel();
_timer = null;
}
Future<void> _check() async {
if (_busy) {
return;
}
_busy = true;
try {
final remote = await _fetchLatestVersion();
if (remote == null) {
return;
}
final remoteVersion = remote['version'];
final remoteUrl = remote['url'];
if (remoteVersion == null || remoteVersion.isEmpty || remoteUrl == null || remoteUrl.isEmpty) {
return;
}
// 已安装过同一版本,跳过(避免重复弹安装框)
if (_installedVersion == remoteVersion) {
return;
}
// 版本一致或本地版本更高则不升级
if (!_isNewerVersion(remoteVersion, Constant.appVersion)) {
return;
}
final apkPath = await _downloadApk(remoteVersion, remoteUrl);
if (apkPath == null) {
return;
}
final ok = await _ensureInstallPermission();
if (!ok) {
debugPrint('[AppUpgrade] 用户未授予未知应用安装权限');
return;
}
final r = await OpenFile.open(apkPath, type: 'application/vnd.android.package-archive');
debugPrint('[AppUpgrade] 触发安装: type=${r.type} message=${r.message}');
if (r.type == ResultType.done) {
_installedVersion = remoteVersion;
}
} catch (e, s) {
debugPrint('[AppUpgrade] 检测/升级失败: $e\n$s');
} finally {
_busy = false;
}
}
/// 从服务端拉取最新版本信息
Future<Map<String, String>?> _fetchLatestVersion() async {
try {
var sharedPreferences = getIt.get<SharedPreferences>();
var userCode = sharedPreferences.getString('auth_userCode') ?? '';
if (userCode.isEmpty) {
return null;
}
final result = await _versionRepository.globalVersion(
userCode,
Constant.appVersion,
'xxj',
);
if (result == null || result is! Map) {
return null;
}
final code = result['code'];
if (code != 0) {
return null;
}
final data = result['data'];
if (data == null || data is! Map) {
return null;
}
return {
'version': (data['lastVersion'] ?? '').toString(),
'url': (data['url'] ?? '').toString(),
};
// return {
// 'version': '1.0.9',
// 'url': 'https://bxe-files.obs.cn-north-4.myhuaweicloud.com/pubs/apks/test/bxeapp-dev-1.0.2606011.apk',
// };
} catch (e) {
debugPrint('[AppUpgrade] 拉取版本配置失败: $e');
return null;
}
}
/// 下载 APK 到本地缓存目录,返回本地路径。已存在则直接复用。
Future<String?> _downloadApk(String version, String url) async {
final baseDir = await getExternalStorageDirectory() ?? await getApplicationSupportDirectory();
final dirPath = '${baseDir.path}/${Constant.appUpgradeApkDir}';
final dir = Directory(dirPath);
if (!await dir.exists()) {
await dir.create(recursive: true);
}
final apkPath = '$dirPath/app_$version.apk';
final apkFile = File(apkPath);
if (await apkFile.exists() && await apkFile.length() > 0) {
return apkPath;
}
// 下载到 .tmp,下载完成后再重命名,避免半成品文件被识别为有效 APK
final tmpPath = '$apkPath.tmp';
final tmpFile = File(tmpPath);
if (await tmpFile.exists()) {
await tmpFile.delete();
}
final dio = Dio();
try {
final resp = await dio.download(
url,
tmpPath,
options: Options(receiveTimeout: const Duration(minutes: 10)),
onReceiveProgress: (received, total) {
if (total > 0) {
final percent = (received / total * 100).toStringAsFixed(0);
debugPrint('[AppUpgrade] 下载 $version: $percent%');
}
},
);
if (resp.statusCode != 200) {
debugPrint('[AppUpgrade] 下载失败: statusCode=${resp.statusCode} url=$url');
if (await tmpFile.exists()) {
await tmpFile.delete();
}
return null;
}
await tmpFile.rename(apkPath);
return apkPath;
} catch (e) {
debugPrint('[AppUpgrade] 下载 APK 失败: $e');
if (await tmpFile.exists()) {
try {
await tmpFile.delete();
} catch (_) {}
}
return null;
} finally {
dio.close(force: true);
}
}
/// 确保 Android 8.0+ 下具有"未知应用安装"权限
Future<bool> _ensureInstallPermission() async {
if (!Platform.isAndroid) {
return true;
}
final status = await Permission.requestInstallPackages.status;
if (status.isGranted) {
return true;
}
final result = await Permission.requestInstallPackages.request();
return result.isGranted;
}
/// 判断 [remote] 是否比 [current] 更新
/// 版本格式:x.y.z(不足位补 0)
bool _isNewerVersion(String remote, String current) {
final r = _parseVersion(remote);
final c = _parseVersion(current);
final len = r.length > c.length ? r.length : c.length;
for (var i = 0; i < len; i++) {
final a = i < r.length ? r[i] : 0;
final b = i < c.length ? c[i] : 0;
if (a > b) {
return true;
}
if (a < b) {
return false;
}
}
return false;
}
List<int> _parseVersion(String v) {
return v
.split('.')
.map((e) => int.tryParse(e.replaceAll(RegExp(r'[^0-9]'), '')) ?? 0)
.toList();
}
}
......@@ -18,6 +18,7 @@ dependencies:
archive: ^4.0.7
connectivity_plus: ^7.0.0
device_info_plus: ^11.5.0
package_info_plus: ^8.3.1
dio: ^5.9.0
equatable: ^2.0.7
get_it: ^8.2.0
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!