From a0ad3a06fffea3e489555a0e296492b4ed34fe32 Mon Sep 17 00:00:00 2001
From: dzc <2388224167@qq.com>
Date: Fri, 24 Jul 2026 09:53:51 +0800
Subject: [PATCH] feat(kiosk): add android kiosk terminal mode support
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
完整实现收银终端的Android专用Kiosk模式支持,包括:
- 添加原生设备管理与Kiosk模式核心逻辑
- 新增Flutter设置页的Kiosk相关配置与控制UI
- 更新README添加部署命令
- 补充Android权限与组件声明
---
README.md | 13 +-
android/app/src/main/AndroidManifest.xml | 24 +++
.../pj_supermarket_cloud/KioskBootReceiver.kt | 19 ++
.../KioskDeviceAdminReceiver.kt | 10 +
.../pj_supermarket_cloud/KioskModeManager.kt | 172 ++++++++++++++++++
.../pj_supermarket_cloud/MainActivity.kt | 27 +++
android/app/src/main/res/values/strings.xml | 5 +
.../src/main/res/xml/kiosk_device_admin.xml | 4 +
lib/common/utils/app/app_runtime_tool.dart | 110 +++++++++++
lib/pages/setting/controller.dart | 52 +++++-
lib/pages/setting/view.dart | 79 ++++----
lib/pages/setting/widgets/index.dart | 1 +
.../setting/widgets/setting_baseInfo.dart | 14 ++
.../widgets/setting_kiosk_exit_button.dart | 64 +++++++
14 files changed, 557 insertions(+), 37 deletions(-)
create mode 100644 android/app/src/main/kotlin/com/example/pj_supermarket_cloud/KioskBootReceiver.kt
create mode 100644 android/app/src/main/kotlin/com/example/pj_supermarket_cloud/KioskDeviceAdminReceiver.kt
create mode 100644 android/app/src/main/kotlin/com/example/pj_supermarket_cloud/KioskModeManager.kt
create mode 100644 android/app/src/main/res/values/strings.xml
create mode 100644 android/app/src/main/res/xml/kiosk_device_admin.xml
create mode 100644 lib/pages/setting/widgets/setting_kiosk_exit_button.dart
diff --git a/README.md b/README.md
index 9573240..784044a 100644
--- a/README.md
+++ b/README.md
@@ -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
-```
\ No newline at end of file
+```
+
+### 开启Kiosk模式命令
+
+```
+adb shell dpm set-device-owner \
+ com.example.pj_supermarket_cloud/.KioskDeviceAdminReceiver
+```
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index c7f930f..26a1735 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -12,6 +12,7 @@
+
@@ -47,6 +48,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
{
+ 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 {
+ 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)
+}
diff --git a/android/app/src/main/kotlin/com/example/pj_supermarket_cloud/MainActivity.kt b/android/app/src/main/kotlin/com/example/pj_supermarket_cloud/MainActivity.kt
index f90689a..1e25d1f 100644
--- a/android/app/src/main/kotlin/com/example/pj_supermarket_cloud/MainActivity.kt
+++ b/android/app/src/main/kotlin/com/example/pj_supermarket_cloud/MainActivity.kt
@@ -36,6 +36,16 @@ class MainActivity : FlutterActivity() {
)
}
"getNetworkSnapshot" -> result.success(getNetworkSnapshot())
+ "getKioskStatus" -> result.success(KioskModeManager.status(this))
+ "setKioskAutoEnterEnabled" -> {
+ val enabled = call.argument("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 特征识别常见虚拟机,避免在虚拟设备上初始化串口、人脸和副屏硬件。
*/
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..ee19a0c
--- /dev/null
+++ b/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+
+ 资安信收银终端设备管理
+ 用于启用开机自启和专用设备锁定模式
+
diff --git a/android/app/src/main/res/xml/kiosk_device_admin.xml b/android/app/src/main/res/xml/kiosk_device_admin.xml
new file mode 100644
index 0000000..8b4c99b
--- /dev/null
+++ b/android/app/src/main/res/xml/kiosk_device_admin.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/lib/common/utils/app/app_runtime_tool.dart b/lib/common/utils/app/app_runtime_tool.dart
index 4917696..1e4d9a8 100644
--- a/lib/common/utils/app/app_runtime_tool.dart
+++ b/lib/common/utils/app/app_runtime_tool.dart
@@ -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? 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 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 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(
diff --git a/lib/pages/setting/controller.dart b/lib/pages/setting/controller.dart
index 39031d9..6f6c99a 100644
--- a/lib/pages/setting/controller.dart
+++ b/lib/pages/setting/controller.dart
@@ -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();
final sysSetting = Rxn();
final userSetting = Rxn();
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 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 refreshKioskStatus() async {
+ kioskStatus.value = await AppRuntimeTool.refreshKioskStatus();
+ }
+
Future 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 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 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();
+ }
+ }
}
diff --git a/lib/pages/setting/view.dart b/lib/pages/setting/view.dart
index d44a3cf..abd72bf 100644
--- a/lib/pages/setting/view.dart
+++ b/lib/pages/setting/view.dart
@@ -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(),
],
- ),
+ ],
);
}),
],
diff --git a/lib/pages/setting/widgets/index.dart b/lib/pages/setting/widgets/index.dart
index 44ef472..e99d5b7 100644
--- a/lib/pages/setting/widgets/index.dart
+++ b/lib/pages/setting/widgets/index.dart
@@ -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';
diff --git a/lib/pages/setting/widgets/setting_baseInfo.dart b/lib/pages/setting/widgets/setting_baseInfo.dart
index 35f00d4..12fbe4c 100644
--- a/lib/pages/setting/widgets/setting_baseInfo.dart
+++ b/lib/pages/setting/widgets/setting_baseInfo.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: '设置为主控',
diff --git a/lib/pages/setting/widgets/setting_kiosk_exit_button.dart b/lib/pages/setting/widgets/setting_kiosk_exit_button.dart
new file mode 100644
index 0000000..67d6fac
--- /dev/null
+++ b/lib/pages/setting/widgets/setting_kiosk_exit_button.dart
@@ -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 createState() => _SettingKioskExitButtonState();
+}
+
+class _SettingKioskExitButtonState extends State {
+ bool _busy = false;
+
+ @override
+ Widget build(BuildContext context) {
+ final controller = Get.find();
+ 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 _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);
+ }
+ }
+}