feat(kiosk): add android kiosk terminal mode support

完整实现收银终端的Android专用Kiosk模式支持,包括:
- 添加原生设备管理与Kiosk模式核心逻辑
- 新增Flutter设置页的Kiosk相关配置与控制UI
- 更新README添加部署命令
- 补充Android权限与组件声明
This commit is contained in:
dzc committed 2026-07-24 09:53:51 +08:00
1 parent 94f8c5e67b
commit a0ad3a06ff
14 files changed
+557 -37

No files matched your search

+12 -1
View File
@@ -1,8 +1,11 @@
## 生成代码命令
```
flutter pub run build_runner build --delete-conflicting-outputs~
```
## flutter打包
```
flutter build apk --build-name 1.3.0 --build-number 130
--build-name为版本号(展示)
@@ -10,6 +13,14 @@ flutter build apk --build-name 1.3.0 --build-number 130
```
## 生成log命令
```
dart run icons_launcher:create
```
```
### 开启Kiosk模式命令
```
adb shell dpm set-device-owner \
com.example.pj_supermarket_cloud/.KioskDeviceAdminReceiver
```
+24
View File
@@ -12,6 +12,7 @@
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<queries>
<intent>
@@ -47,6 +48,29 @@
</intent-filter>
</activity>
<receiver
android:name=".KioskDeviceAdminReceiver"
android:description="@string/kiosk_device_admin_description"
android:exported="true"
android:label="@string/kiosk_device_admin_label"
android:permission="android.permission.BIND_DEVICE_ADMIN">
<meta-data
android:name="android.app.device_admin"
android:resource="@xml/kiosk_device_admin" />
<intent-filter>
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>
<receiver
android:name=".KioskBootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<activity
android:name="io.flutter.plugins.facediscern.ui.FaceDiscern"
android:exported="true"
@@ -0,0 +1,19 @@
package com.example.pj_supermarket_cloud
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
/**
* 仅在应用已经是 Device Owner 时响应开机广播,避免普通安装包无条件打断系统启动流程。
*/
class KioskBootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_BOOT_COMPLETED) return
if (!KioskModeManager.prepareForBoot(context)) return
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) ?: return
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
context.startActivity(launchIntent)
}
}
@@ -0,0 +1,10 @@
package com.example.pj_supermarket_cloud
import android.app.admin.DeviceAdminReceiver
/**
* 专用收银终端的设备管理接收器。
*
* 该接收器只作为 Device Owner 的系统入口,应用不会在普通安装状态下自行提升权限。
*/
class KioskDeviceAdminReceiver : DeviceAdminReceiver()
@@ -0,0 +1,172 @@
package com.example.pj_supermarket_cloud
import android.app.Activity
import android.app.ActivityManager
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
/**
* 管理 Android 专用设备模式。
*
* 只有 Device Owner 才能可靠限制主页、最近任务和状态栏。普通设备管理员或沉浸式全屏
* 都不视为 Kiosk 可用,避免界面把“看起来全屏”误报为“无法退出”。
*/
object KioskModeManager {
private const val PREFERENCES_NAME = "kiosk_runtime"
private const val AUTO_ENTER_ENABLED_KEY = "auto_enter_enabled"
fun status(context: Context): Map<String, Any> {
val policyManager = policyManager(context)
val deviceOwner = policyManager.isDeviceOwnerApp(context.packageName)
val autoEnterEnabled = isAutoEnterEnabled(context)
return mapOf(
"deviceAdminActive" to policyManager.isAdminActive(adminComponent(context)),
"deviceOwner" to deviceOwner,
"available" to deviceOwner,
"active" to isKioskActive(context),
"lockTaskPermitted" to policyManager.isLockTaskPermitted(context.packageName),
"autoEnterEnabled" to autoEnterEnabled,
"bootAutoStartSupported" to deviceOwner
)
}
/**
* Activity 每次回到前台时按配置补齐白名单并进入锁定任务。管理员退出到桌面的过程
* 不会触发新的 onPostResume;用户再次主动打开 App 时才会重新进入 Kiosk。
*/
fun enterIfAvailable(activity: Activity) {
if (!isAutoEnterEnabled(activity) || !ensureLockTaskPolicy(activity)) return
if (!isKioskActive(activity)) {
try {
activity.startLockTask()
} catch (_: IllegalArgumentException) {
return
} catch (_: IllegalStateException) {
return
} catch (_: SecurityException) {
return
}
}
if (isKioskActive(activity)) hideSystemBars(activity)
}
/**
* Device Owner 设备始终支持开机拉起 App;只有开启自动进入时才预先恢复 Kiosk 策略。
*/
fun prepareForBoot(context: Context): Boolean {
if (!policyManager(context).isDeviceOwnerApp(context.packageName)) return false
if (isAutoEnterEnabled(context)) {
return ensureLockTaskPolicy(context)
}
return try {
policyManager(context).setStatusBarDisabled(adminComponent(context), false)
true
} catch (_: Exception) {
true
}
}
/**
* 解除当前锁定并回到系统 HOME。保留 Lock Task 白名单,自动进入配置开启时,用户
* 下次主动打开 App 会通过 onPostResume 重新锁定。
*/
fun exitToHome(activity: Activity): Boolean {
val policyManager = policyManager(activity)
if (!policyManager.isDeviceOwnerApp(activity.packageName)) return false
return try {
if (isKioskActive(activity)) activity.stopLockTask()
policyManager.setStatusBarDisabled(adminComponent(activity), false)
showSystemBars(activity)
val homeIntent = Intent(Intent.ACTION_MAIN).apply {
addCategory(Intent.CATEGORY_HOME)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
}
if (homeIntent.resolveActivity(activity.packageManager) != null) {
activity.startActivity(homeIntent)
} else {
activity.moveTaskToBack(true)
}
true
} catch (_: Exception) {
// 退出失败时恢复终端策略,不能把设备留在既未锁定也未回到桌面的中间状态。
if (isAutoEnterEnabled(activity) && ensureLockTaskPolicy(activity)) {
enterIfAvailable(activity)
}
false
}
}
/**
* 保存“进入 App 自动开启”配置。关闭配置只影响后续进入,当前 Kiosk 会继续运行,
* 仍需通过管理密码出口返回桌面,避免一个普通开关绕过终端退出保护。
*/
fun setAutoEnterEnabled(activity: Activity, enabled: Boolean): Map<String, Any> {
if (!policyManager(activity).isDeviceOwnerApp(activity.packageName)) return status(activity)
preferences(activity).edit().putBoolean(AUTO_ENTER_ENABLED_KEY, enabled).apply()
if (enabled) {
enterIfAvailable(activity)
}
return status(activity)
}
fun reapplySystemBars(activity: Activity) {
if (isKioskActive(activity)) hideSystemBars(activity)
}
fun isKioskActive(context: Context): Boolean {
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
return activityManager.lockTaskModeState == ActivityManager.LOCK_TASK_MODE_LOCKED
}
private fun ensureLockTaskPolicy(context: Context): Boolean {
val policyManager = policyManager(context)
if (!policyManager.isDeviceOwnerApp(context.packageName)) return false
return try {
policyManager.setLockTaskPackages(adminComponent(context), arrayOf(context.packageName))
policyManager.setStatusBarDisabled(adminComponent(context), true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
policyManager.setLockTaskFeatures(
adminComponent(context),
DevicePolicyManager.LOCK_TASK_FEATURE_NONE
)
}
policyManager.isLockTaskPermitted(context.packageName)
} catch (_: Exception) {
false
}
}
private fun hideSystemBars(activity: Activity) {
WindowCompat.setDecorFitsSystemWindows(activity.window, false)
WindowCompat.getInsetsController(activity.window, activity.window.decorView).apply {
hide(WindowInsetsCompat.Type.systemBars())
systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
private fun showSystemBars(activity: Activity) {
WindowCompat.setDecorFitsSystemWindows(activity.window, true)
WindowCompat.getInsetsController(activity.window, activity.window.decorView)
.show(WindowInsetsCompat.Type.systemBars())
}
private fun adminComponent(context: Context) = ComponentName(context, KioskDeviceAdminReceiver::class.java)
private fun policyManager(context: Context) =
context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
private fun isAutoEnterEnabled(context: Context) =
preferences(context).getBoolean(AUTO_ENTER_ENABLED_KEY, true)
private fun preferences(context: Context) =
context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
}
@@ -36,6 +36,16 @@ class MainActivity : FlutterActivity() {
)
}
"getNetworkSnapshot" -> result.success(getNetworkSnapshot())
"getKioskStatus" -> result.success(KioskModeManager.status(this))
"setKioskAutoEnterEnabled" -> {
val enabled = call.argument<Boolean>("enabled")
if (enabled == null) {
result.error("INVALID_ARGUMENT", "缺少 Kiosk 自动进入配置", null)
} else {
result.success(KioskModeManager.setAutoEnterEnabled(this, enabled))
}
}
"exitKioskToHome" -> result.success(KioskModeManager.exitToHome(this))
else -> result.notImplemented()
}
}
@@ -51,6 +61,23 @@ class MainActivity : FlutterActivity() {
// )
}
override fun onPostResume() {
super.onPostResume()
KioskModeManager.enterIfAvailable(this)
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) KioskModeManager.reapplySystemBars(this)
}
@Suppress("DEPRECATION")
override fun onBackPressed() {
// 当前 FlutterActivity 基线没有 OnBackPressedDispatcher,Kiosk 运行时直接拦截系统返回。
if (KioskModeManager.isKioskActive(this)) return
super.onBackPressed()
}
/**
* 使用 Android Build 特征识别常见虚拟机,避免在虚拟设备上初始化串口、人脸和副屏硬件。
*/
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="kiosk_device_admin_label">资安信收银终端设备管理</string>
<string name="kiosk_device_admin_description">用于启用开机自启和专用设备锁定模式</string>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
<uses-policies />
</device-admin>
+110
View File
@@ -18,6 +18,75 @@ class AppRuntimeEnvironment {
}
}
/// 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;
@@ -53,6 +122,7 @@ abstract class AppRuntimeTool {
);
static bool _initialized = false;
static bool _manualHardwareInput = false;
static AppKioskStatus _kioskStatus = const AppKioskStatus.unavailable();
static bool get isEmulator => _environment.isEmulator;
@@ -65,6 +135,8 @@ abstract class AppRuntimeTool {
static bool get hasMultipleDisplays => displayCount >= 2;
static AppKioskStatus get kioskStatus => _kioskStatus;
static Future<void> initializePlatform() async {
if (_initialized) return;
try {
@@ -72,16 +144,54 @@ abstract class AppRuntimeTool {
'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>(
+51 -1
View File
@@ -1,18 +1,20 @@
import 'dart:async';
import 'package:common_utils/common_utils.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart';
import 'package:pj_supermarket_cloud/common/index.dart';
class SettingController extends GetxController {
class SettingController extends GetxController with WidgetsBindingObserver {
static const String _tag = '设置 Controller';
final appC = Get.find<AppControl>();
final sysSetting = Rxn<SysSetting>();
final userSetting = Rxn<UserSetting>();
final manualHardwareInput = false.obs;
final kioskStatus = AppRuntimeTool.kioskStatus.obs;
final cuurentMenuType = SettingMenuType.baseInfo.obs;
final isLoading = true.obs;
final isBound = false.obs;
@@ -27,6 +29,7 @@ class SettingController extends GetxController {
@override
void onInit() {
super.onInit();
WidgetsBinding.instance.addObserver(this);
_initialize();
}
@@ -49,15 +52,22 @@ class SettingController extends GetxController {
@override
void onClose() {
WidgetsBinding.instance.removeObserver(this);
_mqUpdateListen?.cancel();
super.onClose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) refreshKioskStatus();
}
void doMenu(SettingMenuType type) {
cuurentMenuType.value = type;
}
Future<void> getSetting() async {
await refreshKioskStatus();
final setting = await AppSettingTool.getSysSetting();
sysSetting.value = setting;
userSetting.value = await AppSettingTool.getUserSetting();
@@ -67,6 +77,10 @@ class SettingController extends GetxController {
AppRuntimeTool.applyManualHardwareInput(setting.manualHardwareInput);
}
Future<void> refreshKioskStatus() async {
kioskStatus.value = await AppRuntimeTool.refreshKioskStatus();
}
Future<void> setGlobalFontSize(SettingGlobalFontSize fontSize) async {
final setting = sysSetting.value;
if (setting == null || setting.globalFontSize == fontSize) return;
@@ -255,4 +269,40 @@ class SettingController extends GetxController {
sysSetting.refresh();
Toast.success(msg: enabled ? '已设置为主控手动输入模式' : '已恢复真实硬件模式');
}
/// 该配置只控制进入 App 时是否自动锁定;关闭后仍需通过管理密码退出当前 Kiosk。
Future<void> setKioskAutoEnterEnabled(bool enabled) async {
if (!kioskStatus.value.available) return;
final status = await AppRuntimeTool.setKioskAutoEnterEnabled(enabled);
kioskStatus.value = status;
final applied = status.autoEnterEnabled == enabled &&
(!enabled || status.active || status.lockTaskPermitted);
if (!applied) {
Toast.error(msg: 'Kiosk 自动开启配置失败,请检查设备策略');
return;
}
Toast.success(
msg: enabled ? '每次进入 App 将自动开启 Kiosk' : '已关闭进入 App 自动开启 Kiosk');
}
/// 管理密码验证由按钮组件负责,此处只处理原生退出结果与失败反馈。
Future<void> exitKioskToAndroidHome() async {
if (!kioskStatus.value.canExitToAndroidHome) return;
EasyLoading.show(
status: '正在退出专用设备模式...',
maskType: EasyLoadingMaskType.black,
);
try {
final success = await AppRuntimeTool.exitKioskToAndroidHome();
kioskStatus.value = await AppRuntimeTool.refreshKioskStatus();
if (!success) {
Toast.error(msg: '退出 Android 桌面失败,请检查设备策略');
}
} catch (e, stack) {
logger.e('|$_tag|退出Kiosk失败error:|$e|stack:|$stack|');
Toast.error(msg: '退出 Android 桌面失败,请稍后重试');
} finally {
EasyLoading.dismiss();
}
}
}
+44 -35
View File
@@ -67,51 +67,60 @@ class _SettingMenu extends StatelessWidget {
const Spacer(),
Obx(() {
final bound = controller.isBound.value;
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: const Color(0xFFF7F8FA),
border: Border.all(color: XfjDesignTokens.line),
borderRadius: BorderRadius.circular(
XfjDesignTokens.controlRadius,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: const Color(0xFFF7F8FA),
border: Border.all(color: XfjDesignTokens.line),
borderRadius: BorderRadius.circular(
XfjDesignTokens.controlRadius,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: bound
? XfjDesignTokens.success
: XfjDesignTokens.warning,
shape: BoxShape.circle,
),
Row(
children: [
Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: bound
? XfjDesignTokens.success
: XfjDesignTokens.warning,
shape: BoxShape.circle,
),
),
SizedBox(width: 8.w),
Text(
bound ? '设备已绑定' : '设备未绑定',
style: theme.typography.sm.copyWith(
color: bound
? XfjDesignTokens.success
: XfjDesignTokens.warning,
fontWeight: FontWeight.w700,
),
),
],
),
SizedBox(width: 8.w),
SizedBox(height: 8.h),
Text(
bound ? '设备已绑定' : '设备未绑定',
bound ? '部分设置修改后会自动同步到本机服务。' : '请先在基本信息中绑定设备后使用业务设置。',
style: theme.typography.sm.copyWith(
color: bound
? XfjDesignTokens.success
: XfjDesignTokens.warning,
fontWeight: FontWeight.w700,
color: XfjDesignTokens.muted,
),
),
],
),
),
if (controller.kioskStatus.value.canExitToAndroidHome) ...[
SizedBox(height: 8.h),
Text(
bound ? '部分设置修改后会自动同步到本机服务。' : '请先在基本信息中绑定设备后使用业务设置。',
style: theme.typography.sm.copyWith(
color: XfjDesignTokens.muted,
),
),
const SettingKioskExitButton(),
],
),
],
);
}),
],
+1
View File
@@ -3,6 +3,7 @@ library;
export './setting_backup.dart';
export './setting_baseInfo.dart';
export './setting_face.dart';
export './setting_kiosk_exit_button.dart';
export './setting_pay.dart';
export './setting_product.dart';
export './setting_sync.dart';
@@ -136,6 +136,20 @@ Widget settingBaseInfo(BuildContext context) {
? 'Android 虚拟机'
: 'Android 真实设备',
),
SettingInfoRow(
icon: controller.kioskStatus.value.available
? FAssets.icons.shieldCheck
: FAssets.icons.circleAlert,
label: '专用设备 / Kiosk',
subtitle: controller.kioskStatus.value.description,
value: controller.kioskStatus.value.statusLabel,
trailing: FSwitch(
semanticLabel: '进入 App 自动开启 Kiosk',
value: controller.kioskStatus.value.autoEnterEnabled,
enabled: controller.kioskStatus.value.available,
onChange: controller.setKioskAutoEnterEnabled,
),
),
SettingInfoRow(
icon: FAssets.icons.keyboard,
label: '设置为主控',
@@ -0,0 +1,64 @@
import 'package:ducafe_ui_core/ducafe_ui_core.dart';
import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
import 'package:get/get.dart';
import 'package:pj_supermarket_cloud/common/index.dart';
import '../controller.dart';
/// Kiosk 能力成立时才展示系统出口,普通全屏设备不能出现具有误导性的“退出桌面”按钮。
class SettingKioskExitButton extends StatefulWidget {
const SettingKioskExitButton({super.key});
@override
State<SettingKioskExitButton> createState() => _SettingKioskExitButtonState();
}
class _SettingKioskExitButtonState extends State<SettingKioskExitButton> {
bool _busy = false;
@override
Widget build(BuildContext context) {
final controller = Get.find<SettingController>();
final theme = FTheme.of(context);
return Obx(() {
if (!controller.kioskStatus.value.canExitToAndroidHome) {
return const SizedBox.shrink();
}
return SizedBox(
width: double.infinity,
child: FButton(
style: FButtonStyle.destructive,
prefix: FIcon(FAssets.icons.house, color: Colors.white, size: 20.sp),
label: Text(
'退出 Android 桌面',
style: theme.typography.sm.copyWith(
color: Colors.white,
fontWeight: FontWeight.w700,
),
),
onPress: _busy ? null : () => _exitToAndroidHome(context, controller),
),
);
});
}
Future<void> _exitToAndroidHome(
BuildContext context,
SettingController controller,
) async {
if (_busy) return;
setState(() => _busy = true);
try {
final verified = await terminalAdminPasswordDialog(
context,
actionLabel: '退出到 Android 桌面',
);
if (!verified) return;
await controller.exitKioskToAndroidHome();
} finally {
if (mounted) setState(() => _busy = false);
}
}
}