Files
biz_xfj_cloud/lib/common/utils/app/app_runtime_tool.dart
T
2026-08-06 20:55:09 +08:00

603 lines
21 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/services.dart';
import 'package:pj_supermarket_cloud/common/models/manage/root_access_models.dart';
/// 当前 Android 设备的运行环境。
class AppRuntimeEnvironment {
/// 是否命中 Android 虚拟机特征;通道不可用时按真机降级,避免阻塞应用启动。
final bool isEmulator;
/// Android DisplayManager 当前识别的显示屏数量,最少按单屏处理。
final int displayCount;
/// 当前 Android 系统发行版本,例如 `11`;原生字段不可用时为空字符串。
final String androidVersion;
/// 当前 Android API Level,例如 Android 11 对应 30;原生字段不可用时为 0。
final int androidSdkInt;
/// 当前进程实际安装 APK 的展示版本,不使用后台待更新版本代替。
final String apkVersionName;
/// 当前进程实际安装 APK 的内部版本码;原生字段不可用时为 0。
final int apkVersionCode;
/// 创建一份由 Android 原生运行时采集的不可变环境快照。
const AppRuntimeEnvironment({
required this.isEmulator,
required this.displayCount,
required this.androidVersion,
required this.androidSdkInt,
required this.apkVersionName,
required this.apkVersionCode,
});
/// 从 MethodChannel 返回值解析环境快照,缺失版本字段使用可安全展示的空值。
factory AppRuntimeEnvironment.fromMap(Map<dynamic, dynamic>? data) {
return AppRuntimeEnvironment(
isEmulator: data?['isEmulator'] == true,
displayCount: (data?['displayCount'] as num?)?.toInt() ?? 1,
androidVersion: data?['androidVersion']?.toString() ?? '',
androidSdkInt: (data?['androidSdkInt'] as num?)?.toInt() ?? 0,
apkVersionName: data?['apkVersionName']?.toString() ?? '',
apkVersionCode: (data?['apkVersionCode'] as num?)?.toInt() ?? 0,
);
}
}
/// Android 专用设备模式的实时状态。
class AppKioskStatus {
final bool deviceAdminActive;
final bool deviceOwner;
final bool available;
final bool active;
final bool lockTaskPermitted;
final bool autoEnterEnabled;
final bool bootAutoStartSupported;
const AppKioskStatus({
required this.deviceAdminActive,
required this.deviceOwner,
required this.available,
required this.active,
required this.lockTaskPermitted,
required this.autoEnterEnabled,
required this.bootAutoStartSupported,
});
const AppKioskStatus.unavailable()
: deviceAdminActive = false,
deviceOwner = false,
available = false,
active = false,
lockTaskPermitted = false,
autoEnterEnabled = false,
bootAutoStartSupported = false;
factory AppKioskStatus.fromMap(Map<dynamic, dynamic>? data) {
return AppKioskStatus(
deviceAdminActive: data?['deviceAdminActive'] == true,
deviceOwner: data?['deviceOwner'] == true,
available: data?['available'] == true,
active: data?['active'] == true,
lockTaskPermitted: data?['lockTaskPermitted'] == true,
autoEnterEnabled: data?['autoEnterEnabled'] == true,
bootAutoStartSupported: data?['bootAutoStartSupported'] == true,
);
}
bool get canExitToAndroidHome => available && active;
String get statusLabel {
if (!available) return '暂不可实现';
if (active && !autoEnterEnabled) return 'Kiosk 运行中 · 自动开启已关闭';
if (active) return '可以实现 · Kiosk 运行中';
if (!autoEnterEnabled) return '可以实现 · 自动开启已关闭';
if (lockTaskPermitted) return '可以实现 · 正在进入';
return '可以实现 · 等待策略生效';
}
String get description {
if (available && active && !autoEnterEnabled) {
return '当前仍在 Kiosk;管理员退出后,再次打开或回到 App 将不再自动锁定。';
}
if (available && !autoEnterEnabled) {
return '设备具备 Device Owner 权限;开启右侧配置后,每次打开或回到 App 都会自动进入 Kiosk。';
}
if (available) {
return '已配置 Device Owner;每次打开或回到 App 都会自动进入 Kiosk,并隐藏主页、最近任务和系统栏。';
}
if (deviceAdminActive) {
return '当前仅激活了设备管理员,仍不能可靠禁用主页和最近任务;需要配置为 Device Owner。';
}
return '未配置 Device Owner,普通全屏不能保证无法退出;需在恢复出厂或未初始化的设备上部署。';
}
}
/// Root 执行 DPM 后返回的结构化 Kiosk 启用结果。
class AppKioskProvisionResult {
/// 创建一份包含稳定错误编码、可操作说明和最新 Kiosk 状态的结果。
const AppKioskProvisionResult({
required this.success,
required this.code,
required this.message,
required this.status,
});
/// DPM 是否成功配置当前应用为 Device Owner 并进入 Kiosk。
final bool success;
/// 原生层稳定错误编码,用于区分账户、Owner、用户和厂商限制。
final String code;
/// 面向终端管理员展示的中文处理建议。
final String message;
/// 命令执行完成后重新读取的 Kiosk 实时状态。
final AppKioskStatus status;
/// 从原生结构化返回值恢复结果,通道字段缺失时使用安全失败值。
factory AppKioskProvisionResult.fromMap(Map<dynamic, dynamic>? data) {
return AppKioskProvisionResult(
success: data?['success'] == true,
code: data?['code']?.toString() ?? 'UNKNOWN',
message: data?['message']?.toString() ?? 'Kiosk 启用失败,请检查设备策略',
status: AppKioskStatus.fromMap(data?['status'] as Map?),
);
}
}
/// Android APK 校验或 Root 安装命令的结构化结果。
class AppApkOperationResult {
/// 创建一份供自动更新流程判断的原生操作结果。
const AppApkOperationResult({
required this.success,
required this.code,
required this.message,
this.versionCode,
});
/// APK 校验通过或安装命令成功执行时为 true。
final bool success;
/// 原生稳定结果编码,例如 SIGNATURE_MISMATCH 或 INSTALL_STARTED。
final String code;
/// 面向任务日志或人工操作展示的中文摘要。
final String message;
/// 原生 APK 实际版本码;返回数据不含版本时为空。
final int? versionCode;
/// 从 MethodChannel 返回映射创建操作结果。
factory AppApkOperationResult.fromMap(Map<dynamic, dynamic>? data) {
return AppApkOperationResult(
success: data?['valid'] == true,
code: data?['code']?.toString() ?? 'UNKNOWN',
message: data?['message']?.toString() ?? '原生 APK 操作失败',
versionCode: (data?['versionCode'] as num?)?.toInt(),
);
}
}
/// 应用启动时消费的待更新恢复结果。
class AppUpdateRecoveryResult {
/// 创建一份待更新确认结果。
const AppUpdateRecoveryResult({
required this.pending,
required this.success,
required this.message,
});
/// 原生层是否存在上次安装前持久化的待更新记录。
final bool pending;
/// 当前安装版本是否达到待更新目标版本码。
final bool success;
/// 启动确认结果摘要,用于完成任务记录。
final String message;
/// 从 MethodChannel 返回映射创建恢复结果。
factory AppUpdateRecoveryResult.fromMap(Map<dynamic, dynamic>? data) {
return AppUpdateRecoveryResult(
pending: data?['pending'] == true,
success: data?['success'] == true,
message: data?['message']?.toString() ?? '没有待确认的自动更新',
);
}
}
class AppNetworkSnapshot {
final bool connected;
final String type;
final int receivedBytes;
final int transmittedBytes;
const AppNetworkSnapshot({
required this.connected,
required this.type,
required this.receivedBytes,
required this.transmittedBytes,
});
factory AppNetworkSnapshot.fromMap(Map<dynamic, dynamic>? data) {
return AppNetworkSnapshot(
connected: data?['connected'] == true,
type: data?['type']?.toString() ?? 'offline',
receivedBytes: (data?['rxBytes'] as num?)?.toInt() ?? 0,
transmittedBytes: (data?['txBytes'] as num?)?.toInt() ?? 0,
);
}
}
/// Android 进程与设备资源快照。
///
/// CPU 使用率需要比较两次 [processCpuTimeMillis],原生层只返回累计值,避免在平台通道内
/// 持有采样定时器;内存和存储同时保留总量,便于顶栏给出百分比并在 Tooltip 展示明细。
class AppPerformanceSnapshot {
/// Android 系统自本次开机后的运行时长,单位为秒。
final int systemUptimeSeconds;
/// 当前应用进程自启动后的运行时长,单位为秒。
final int appUptimeSeconds;
/// Android 进程从启动至采样时累计消耗的 CPU 时间,单位为毫秒。
final int processCpuTimeMillis;
/// Android Runtime 报告的逻辑处理器数量,最小按 1 处理。
final int logicalProcessorCount;
/// 系统物理内存总量,单位为字节;平台数据不可用时为 0。
final int systemMemoryTotalBytes;
/// 系统当前可用物理内存,单位为字节;平台数据不可用时为 0。
final int systemMemoryAvailableBytes;
/// 当前应用进程的 PSS 内存占用,单位为字节。
final int appMemoryBytes;
/// 应用文件目录所在存储卷的总容量,单位为字节;平台数据不可用时为 0。
final int storageTotalBytes;
/// 应用文件目录所在存储卷的可用容量,单位为字节;平台数据不可用时为 0。
final int storageAvailableBytes;
/// 创建一份原生平台已经完成单位换算的资源快照。
const AppPerformanceSnapshot({
required this.systemUptimeSeconds,
required this.appUptimeSeconds,
required this.processCpuTimeMillis,
required this.logicalProcessorCount,
required this.systemMemoryTotalBytes,
required this.systemMemoryAvailableBytes,
required this.appMemoryBytes,
required this.storageTotalBytes,
required this.storageAvailableBytes,
});
/// 创建平台通道不可用时的空快照,百分比 Getter 将稳定返回 0。
const AppPerformanceSnapshot.empty()
: systemUptimeSeconds = 0,
appUptimeSeconds = 0,
processCpuTimeMillis = 0,
logicalProcessorCount = 1,
systemMemoryTotalBytes = 0,
systemMemoryAvailableBytes = 0,
appMemoryBytes = 0,
storageTotalBytes = 0,
storageAvailableBytes = 0;
/// 从 Android MethodChannel 返回值解析资源快照,缺失或类型异常字段按 0 处理。
factory AppPerformanceSnapshot.fromMap(Map<dynamic, dynamic>? data) {
int value(String key) => (data?[key] as num?)?.toInt() ?? 0;
return AppPerformanceSnapshot(
systemUptimeSeconds: value('systemUptimeSeconds'),
appUptimeSeconds: value('appUptimeSeconds'),
processCpuTimeMillis: value('processCpuTimeMs'),
logicalProcessorCount:
value('logicalProcessorCount').clamp(1, 256).toInt(),
systemMemoryTotalBytes: value('memoryTotalBytes'),
systemMemoryAvailableBytes: value('memoryAvailableBytes'),
appMemoryBytes: value('appMemoryBytes'),
storageTotalBytes: value('storageTotalBytes'),
storageAvailableBytes: value('storageAvailableBytes'),
);
}
/// 系统内存已使用比例,范围为 0 到 1;总量未知时返回 0。
double get memoryUsageRatio => _usedRatio(
total: systemMemoryTotalBytes,
available: systemMemoryAvailableBytes,
);
/// 应用所在存储卷已使用比例,范围为 0 到 1;总量未知时返回 0。
double get storageUsageRatio => _usedRatio(
total: storageTotalBytes,
available: storageAvailableBytes,
);
/// 根据总量和可用量计算受限于 0 到 1 的占用比例,容忍原生瞬时采样误差。
static double _usedRatio({required int total, required int available}) {
if (total <= 0) return 0;
return ((total - available) / total).clamp(0, 1).toDouble();
}
}
/// 统一管理虚拟机识别和硬件手动输入模式,避免各业务页面自行猜测设备能力。
abstract class AppRuntimeTool {
static const MethodChannel _channel = MethodChannel(
'biz_xfj/runtime_environment',
);
static AppRuntimeEnvironment _environment = const AppRuntimeEnvironment(
isEmulator: false,
displayCount: 1,
androidVersion: '',
androidSdkInt: 0,
apkVersionName: '',
apkVersionCode: 0,
);
static bool _initialized = false;
static bool _manualHardwareInput = false;
static AppKioskStatus _kioskStatus = const AppKioskStatus.unavailable();
/// 请求 Android 终端执行短震动;通道不可用或平台不支持时回退到 Flutter 触觉反馈。
static Future<void> vibrate({int durationMs = 60}) async {
final safeDuration = durationMs.clamp(20, 200).toInt();
try {
final handled = await _channel.invokeMethod<bool>(
'vibrate',
{'durationMs': safeDuration},
);
if (handled == true) return;
} on MissingPluginException {
// 非 Android 平台没有运行时通道,继续使用 Flutter 提供的系统触觉能力。
} on PlatformException {
// 厂商设备禁用震动或原生调用异常时,回退触觉不能影响弹框关闭控制。
}
await HapticFeedback.mediumImpact();
}
static bool get isEmulator => _environment.isEmulator;
static int get displayCount => _environment.displayCount;
/// 返回设置页使用的 Android 发行版本与 API Level,通道不可用时明确显示未知。
static String get androidVersionLabel {
final version = _environment.androidVersion.trim();
final sdk = _environment.androidSdkInt;
if (version.isNotEmpty && sdk > 0) return 'Android $version(API $sdk)';
if (version.isNotEmpty) return 'Android $version';
if (sdk > 0) return 'Android API $sdk';
return '未知';
}
/// 返回当前实际安装 APK 的展示版本与 Build,字段缺失时仍保留已知部分。
static String get apkVersionLabel {
final versionName = _environment.apkVersionName.trim();
final versionCode = _environment.apkVersionCode;
if (versionName.isNotEmpty && versionCode > 0) {
return '$versionName(Build $versionCode)';
}
if (versionName.isNotEmpty) return versionName;
if (versionCode > 0) return 'Build $versionCode';
return '未知';
}
/// 虚拟机始终使用手动输入;真机可在设置中主动开启手动输入模式。
static bool get useManualHardwareInput => isEmulator || _manualHardwareInput;
static bool get canUsePhysicalHardware => !useManualHardwareInput;
static bool get hasMultipleDisplays => displayCount >= 2;
static AppKioskStatus get kioskStatus => _kioskStatus;
static Future<void> initializePlatform() async {
if (_initialized) return;
try {
final data = await _channel.invokeMapMethod<dynamic, dynamic>(
'getRuntimeEnvironment',
);
_environment = AppRuntimeEnvironment.fromMap(data);
_kioskStatus = await refreshKioskStatus();
} on PlatformException {
// 原生通道异常时按单屏处理,避免错误地启动副屏。
_environment = const AppRuntimeEnvironment(
isEmulator: false,
displayCount: 1,
androidVersion: '',
androidSdkInt: 0,
apkVersionName: '',
apkVersionCode: 0,
);
_kioskStatus = const AppKioskStatus.unavailable();
}
_initialized = true;
}
static Future<AppKioskStatus> refreshKioskStatus() async {
try {
final data = await _channel.invokeMapMethod<dynamic, dynamic>(
'getKioskStatus',
);
_kioskStatus = AppKioskStatus.fromMap(data);
} on PlatformException {
_kioskStatus = const AppKioskStatus.unavailable();
}
return _kioskStatus;
}
/// 配置每次进入 App 时是否自动恢复 Kiosk,配置由 Android 原生层持久化。
static Future<AppKioskStatus> setKioskAutoEnterEnabled(bool enabled) async {
try {
final data = await _channel.invokeMapMethod<dynamic, dynamic>(
'setKioskAutoEnterEnabled',
{'enabled': enabled},
);
_kioskStatus = AppKioskStatus.fromMap(data);
} on PlatformException {
// 保留原状态,设置页会据此判断本次配置没有生效。
}
return _kioskStatus;
}
/// 用户确认后通过 Root 请求 Android 配置当前应用为 Device Owner,并立即进入 Kiosk。
static Future<AppKioskProvisionResult> enableKioskWithRoot() async {
try {
final data = await _channel.invokeMapMethod<dynamic, dynamic>(
'enableKioskWithRoot',
);
final provision = AppKioskProvisionResult.fromMap(data);
_kioskStatus = provision.status;
return provision;
} on PlatformException catch (error) {
return AppKioskProvisionResult(
success: false,
code: error.code,
message: error.message ?? 'Kiosk 启用失败,请稍后重试',
status: _kioskStatus,
);
}
}
/// 执行带原生 5 秒超时的 `su -c id`,只有退出码为零且输出确认 uid=0 才返回可用。
static Future<RootAccessCheckResult> checkRootAccess() async {
try {
final data = await _channel.invokeMapMethod<dynamic, dynamic>(
'checkRootAccess',
);
return RootAccessCheckResult.fromMap(data);
} on PlatformException catch (error) {
return RootAccessCheckResult(
status: RootAccessStatus.unavailable,
checkedAt: DateTime.now(),
diagnostic: error.message ?? 'Root 检查通道不可用',
);
}
}
/// 由 Android PackageManager 校验 APK 包名、签名和目标版本码。
static Future<AppApkOperationResult> validateApk({
required String path,
required int expectedVersionCode,
}) async {
try {
final data = await _channel.invokeMapMethod<dynamic, dynamic>(
'validateApk',
{'path': path, 'expectedVersionCode': expectedVersionCode},
);
return AppApkOperationResult.fromMap(data);
} on PlatformException catch (error) {
return AppApkOperationResult(
success: false,
code: error.code,
message: error.message ?? 'APK 校验失败',
);
}
}
/// 安装前由原生层再次校验 Root 与 APK,并执行不降级、不卸载数据的 `pm install -r`。
static Future<AppApkOperationResult> installApkWithRoot({
required String path,
required int expectedVersionCode,
}) async {
try {
final data = await _channel.invokeMapMethod<dynamic, dynamic>(
'installApkWithRoot',
{'path': path, 'expectedVersionCode': expectedVersionCode},
);
return AppApkOperationResult.fromMap(data);
} on PlatformException catch (error) {
return AppApkOperationResult(
success: false,
code: error.code,
message: error.message ?? 'APK 安装失败',
);
}
}
/// 启动时消费原生待更新状态,确认版本后原生层同步清理私有 APK。
static Future<AppUpdateRecoveryResult> consumePendingUpdate() async {
try {
final data = await _channel.invokeMapMethod<dynamic, dynamic>(
'consumePendingUpdate',
);
return AppUpdateRecoveryResult.fromMap(data);
} on PlatformException catch (error) {
return AppUpdateRecoveryResult(
pending: false,
success: false,
message: error.message ?? '自动更新恢复检查失败',
);
}
}
/// 只有原生层确认 Device Owner/Kiosk 可用时才允许退出到系统桌面。
static Future<bool> exitKioskToAndroidHome() async {
if (!_kioskStatus.canExitToAndroidHome) return false;
try {
return await _channel.invokeMethod<bool>('exitKioskToHome') ?? false;
} on PlatformException {
return false;
}
}
static Future<AppNetworkSnapshot> getNetworkSnapshot() async {
try {
final data = await _channel.invokeMapMethod<dynamic, dynamic>(
'getNetworkSnapshot',
);
return AppNetworkSnapshot.fromMap(data);
} on PlatformException {
return const AppNetworkSnapshot(
connected: false,
type: 'offline',
receivedBytes: 0,
transmittedBytes: 0,
);
}
}
/// 读取一次 Android 进程、内存和存储快照;通道失败时返回可安全展示的空快照。
static Future<AppPerformanceSnapshot> getPerformanceSnapshot() async {
try {
final data = await _channel.invokeMapMethod<dynamic, dynamic>(
'getPerformanceSnapshot',
);
return AppPerformanceSnapshot.fromMap(data);
} on PlatformException {
return const AppPerformanceSnapshot.empty();
}
}
/// 查询插件当前 Presentation 是否仍在展示,用于避免 showSubDisplay() 先关后开造成闪屏。
static Future<bool> isSubScreenShowing() async {
try {
return await _channel.invokeMethod<bool>('isSubScreenShowing') ?? false;
} on PlatformException {
return false;
}
}
/// 单向转发业务状态到副屏 Engine,原生层转发后立即回包。
///
/// 锁定的双屏插件不会结束普通业务消息的主屏 MethodChannel 请求,因此人脸等
/// 硬件主链路必须使用此入口,避免等待副屏回包导致 Camera 初始化被永久阻塞。
static Future<bool> sendViceMessage(
String method, {
Map<String, dynamic>? params,
}) async {
return await _channel.invokeMethod<bool>('sendViceMessage', {
'method': method,
'params': params ?? <String, dynamic>{},
}) ??
false;
}
static void applyManualHardwareInput(bool enabled) {
_manualHardwareInput = enabled;
}
}