import 'package:flutter/services.dart'; /// 当前 Android 设备的运行环境。 class AppRuntimeEnvironment { final bool isEmulator; final int displayCount; const AppRuntimeEnvironment({ required this.isEmulator, required this.displayCount, }); factory AppRuntimeEnvironment.fromMap(Map? data) { return AppRuntimeEnvironment( isEmulator: data?['isEmulator'] == true, displayCount: (data?['displayCount'] as num?)?.toInt() ?? 1, ); } } /// 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? 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,普通全屏不能保证无法退出;需在恢复出厂或未初始化的设备上部署。'; } } 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? 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 进程从启动至采样时累计消耗的 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.processCpuTimeMillis, required this.logicalProcessorCount, required this.systemMemoryTotalBytes, required this.systemMemoryAvailableBytes, required this.appMemoryBytes, required this.storageTotalBytes, required this.storageAvailableBytes, }); /// 创建平台通道不可用时的空快照,百分比 Getter 将稳定返回 0。 const AppPerformanceSnapshot.empty() : processCpuTimeMillis = 0, logicalProcessorCount = 1, systemMemoryTotalBytes = 0, systemMemoryAvailableBytes = 0, appMemoryBytes = 0, storageTotalBytes = 0, storageAvailableBytes = 0; /// 从 Android MethodChannel 返回值解析资源快照,缺失或类型异常字段按 0 处理。 factory AppPerformanceSnapshot.fromMap(Map? data) { int value(String key) => (data?[key] as num?)?.toInt() ?? 0; return AppPerformanceSnapshot( 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, ); static bool _initialized = false; static bool _manualHardwareInput = false; static AppKioskStatus _kioskStatus = const AppKioskStatus.unavailable(); static bool get isEmulator => _environment.isEmulator; static int get displayCount => _environment.displayCount; /// 虚拟机始终使用手动输入;真机可在设置中主动开启手动输入模式。 static bool get useManualHardwareInput => isEmulator || _manualHardwareInput; static bool get canUsePhysicalHardware => !useManualHardwareInput; static bool get hasMultipleDisplays => displayCount >= 2; static AppKioskStatus get kioskStatus => _kioskStatus; static Future initializePlatform() async { if (_initialized) return; try { final data = await _channel.invokeMapMethod( 'getRuntimeEnvironment', ); _environment = AppRuntimeEnvironment.fromMap(data); _kioskStatus = await refreshKioskStatus(); } on PlatformException { // 原生通道异常时按单屏处理,避免错误地启动副屏。 _environment = const AppRuntimeEnvironment( isEmulator: false, displayCount: 1, ); _kioskStatus = const AppKioskStatus.unavailable(); } _initialized = true; } static Future refreshKioskStatus() async { try { final data = await _channel.invokeMapMethod( 'getKioskStatus', ); _kioskStatus = AppKioskStatus.fromMap(data); } on PlatformException { _kioskStatus = const AppKioskStatus.unavailable(); } return _kioskStatus; } /// 配置每次进入 App 时是否自动恢复 Kiosk,配置由 Android 原生层持久化。 static Future setKioskAutoEnterEnabled(bool enabled) async { try { final data = await _channel.invokeMapMethod( 'setKioskAutoEnterEnabled', {'enabled': enabled}, ); _kioskStatus = AppKioskStatus.fromMap(data); } on PlatformException { // 保留原状态,设置页会据此判断本次配置没有生效。 } return _kioskStatus; } /// 只有原生层确认 Device Owner/Kiosk 可用时才允许退出到系统桌面。 static Future exitKioskToAndroidHome() async { if (!_kioskStatus.canExitToAndroidHome) return false; try { return await _channel.invokeMethod('exitKioskToHome') ?? false; } on PlatformException { return false; } } static Future getNetworkSnapshot() async { try { final data = await _channel.invokeMapMethod( 'getNetworkSnapshot', ); return AppNetworkSnapshot.fromMap(data); } on PlatformException { return const AppNetworkSnapshot( connected: false, type: 'offline', receivedBytes: 0, transmittedBytes: 0, ); } } /// 读取一次 Android 进程、内存和存储快照;通道失败时返回可安全展示的空快照。 static Future getPerformanceSnapshot() async { try { final data = await _channel.invokeMapMethod( 'getPerformanceSnapshot', ); return AppPerformanceSnapshot.fromMap(data); } on PlatformException { return const AppPerformanceSnapshot.empty(); } } /// 查询插件当前 Presentation 是否仍在展示,用于避免 showSubDisplay() 先关后开造成闪屏。 static Future isSubScreenShowing() async { try { return await _channel.invokeMethod('isSubScreenShowing') ?? false; } on PlatformException { return false; } } /// 单向转发业务状态到副屏 Engine,原生层转发后立即回包。 /// /// 锁定的双屏插件不会结束普通业务消息的主屏 MethodChannel 请求,因此人脸等 /// 硬件主链路必须使用此入口,避免等待副屏回包导致 Camera 初始化被永久阻塞。 static Future sendViceMessage( String method, { Map? params, }) async { return await _channel.invokeMethod('sendViceMessage', { 'method': method, 'params': params ?? {}, }) ?? false; } static void applyManualHardwareInput(bool enabled) { _manualHardwareInput = enabled; } }