603 lines
21 KiB
Dart
603 lines
21 KiB
Dart
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;
|
||
}
|
||
}
|