Files
biz_xfj_cloud/lib/common/utils/app/app_runtime_tool.dart
T
dzc a0ad3a06ff feat(kiosk): add android kiosk terminal mode support
完整实现收银终端的Android专用Kiosk模式支持,包括:
- 添加原生设备管理与Kiosk模式核心逻辑
- 新增Flutter设置页的Kiosk相关配置与控制UI
- 更新README添加部署命令
- 补充Android权限与组件声明
2026-07-24 09:53:51 +08:00

215 lines
6.9 KiB
Dart

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<dynamic, dynamic>? 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<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,普通全屏不能保证无法退出;需在恢复出厂或未初始化的设备上部署。';
}
}
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,
);
}
}
/// 统一管理虚拟机识别和硬件手动输入模式,避免各业务页面自行猜测设备能力。
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<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,
);
_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;
}
/// 只有原生层确认 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,
);
}
}
static void applyManualHardwareInput(bool enabled) {
_manualHardwareInput = enabled;
}
}