From 7e1ee18350fdaf4b58dc1b38dc0256ca32133df8 Mon Sep 17 00:00:00 2001 From: TachibanaLolo Date: Sun, 25 Jan 2026 02:00:27 +0800 Subject: [PATCH 1/3] feat(kiosk): open launcher from settings --- .../kotlin/com/secgo/kiosk/MainActivity.kt | 24 +++ Kiosk/lib/l10n/app_en.arb | 11 +- Kiosk/lib/l10n/app_localizations.dart | 54 +++++++ Kiosk/lib/l10n/app_localizations_en.dart | 31 ++++ Kiosk/lib/l10n/app_localizations_zh.dart | 31 ++++ Kiosk/lib/l10n/app_zh.arb | 11 +- Kiosk/lib/screens/main_screen.dart | 9 +- Kiosk/lib/screens/settings_screen.dart | 146 +++++++++++++++++- .../services/android_launcher_service.dart | 24 +++ Kiosk/lib/services/settings_service.dart | 19 +++ 10 files changed, 354 insertions(+), 6 deletions(-) create mode 100644 Kiosk/lib/services/android_launcher_service.dart diff --git a/Kiosk/android/app/src/main/kotlin/com/secgo/kiosk/MainActivity.kt b/Kiosk/android/app/src/main/kotlin/com/secgo/kiosk/MainActivity.kt index 3ec25ee..236c4a8 100644 --- a/Kiosk/android/app/src/main/kotlin/com/secgo/kiosk/MainActivity.kt +++ b/Kiosk/android/app/src/main/kotlin/com/secgo/kiosk/MainActivity.kt @@ -43,6 +43,30 @@ class MainActivity : FlutterActivity() { "getLatestWechatNotification" -> result.success(getLatestWechatNotification()) "getLatestWechatPaymentNotification" -> result.success(getLatestWechatPaymentNotification()) "getActiveWechatNotificationsSnapshot" -> result.success(getActiveWechatNotificationsSnapshot()) + "openLauncherHome" -> { + val intent = + Intent(Intent.ACTION_MAIN).apply { + addCategory(Intent.CATEGORY_HOME) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + startActivity(intent) + result.success(true) + } + "openApp" -> { + val pkg = call.argument("packageName")?.trim() + if (pkg.isNullOrBlank()) { + result.success(false) + return@setMethodCallHandler + } + val launchIntent = packageManager.getLaunchIntentForPackage(pkg) + if (launchIntent == null) { + result.success(false) + return@setMethodCallHandler + } + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(launchIntent) + result.success(true) + } "openNotificationListenerSettings" -> { startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) result.success(true) diff --git a/Kiosk/lib/l10n/app_en.arb b/Kiosk/lib/l10n/app_en.arb index 7b2dbe0..2adba17 100644 --- a/Kiosk/lib/l10n/app_en.arb +++ b/Kiosk/lib/l10n/app_en.arb @@ -53,5 +53,14 @@ "retry": "Retry", "serverNoIp": "Failed to start server: No IP address found", "restoreComplete": "Restore Complete", - "returningHome": "Refreshing data..." + "returningHome": "Refreshing data...", + "openLauncher": "Open Launcher", + "launcherTarget": "Target: {target}", + "launcherDefault": "Launcher", + "homeAppPackageTitle": "Home app package (optional)", + "homeAppPackageHint": "e.g. com.secgo.home", + "homeAppNotSet": "Not set (use Launcher)", + "homeAppOpenFailed": "Can't open {package}. Opening Launcher instead.", + "clear": "Clear", + "save": "Save" } diff --git a/Kiosk/lib/l10n/app_localizations.dart b/Kiosk/lib/l10n/app_localizations.dart index 59d356c..e1e7334 100644 --- a/Kiosk/lib/l10n/app_localizations.dart +++ b/Kiosk/lib/l10n/app_localizations.dart @@ -427,6 +427,60 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Refreshing data...'** String get returningHome; + + /// No description provided for @openLauncher. + /// + /// In en, this message translates to: + /// **'Open Launcher'** + String get openLauncher; + + /// No description provided for @launcherTarget. + /// + /// In en, this message translates to: + /// **'Target: {target}'** + String launcherTarget(Object target); + + /// No description provided for @launcherDefault. + /// + /// In en, this message translates to: + /// **'Launcher'** + String get launcherDefault; + + /// No description provided for @homeAppPackageTitle. + /// + /// In en, this message translates to: + /// **'Home app package (optional)'** + String get homeAppPackageTitle; + + /// No description provided for @homeAppPackageHint. + /// + /// In en, this message translates to: + /// **'e.g. com.secgo.home'** + String get homeAppPackageHint; + + /// No description provided for @homeAppNotSet. + /// + /// In en, this message translates to: + /// **'Not set (use Launcher)'** + String get homeAppNotSet; + + /// No description provided for @homeAppOpenFailed. + /// + /// In en, this message translates to: + /// **'Can\'t open {package}. Opening Launcher instead.'** + String homeAppOpenFailed(Object package); + + /// No description provided for @clear. + /// + /// In en, this message translates to: + /// **'Clear'** + String get clear; + + /// No description provided for @save. + /// + /// In en, this message translates to: + /// **'Save'** + String get save; } class _AppLocalizationsDelegate diff --git a/Kiosk/lib/l10n/app_localizations_en.dart b/Kiosk/lib/l10n/app_localizations_en.dart index 59d6c0e..2741b1f 100644 --- a/Kiosk/lib/l10n/app_localizations_en.dart +++ b/Kiosk/lib/l10n/app_localizations_en.dart @@ -194,4 +194,35 @@ class AppLocalizationsEn extends AppLocalizations { @override String get returningHome => 'Refreshing data...'; + + @override + String get openLauncher => 'Open Launcher'; + + @override + String launcherTarget(Object target) { + return 'Target: $target'; + } + + @override + String get launcherDefault => 'Launcher'; + + @override + String get homeAppPackageTitle => 'Home app package (optional)'; + + @override + String get homeAppPackageHint => 'e.g. com.secgo.home'; + + @override + String get homeAppNotSet => 'Not set (use Launcher)'; + + @override + String homeAppOpenFailed(Object package) { + return 'Can\'t open $package. Opening Launcher instead.'; + } + + @override + String get clear => 'Clear'; + + @override + String get save => 'Save'; } diff --git a/Kiosk/lib/l10n/app_localizations_zh.dart b/Kiosk/lib/l10n/app_localizations_zh.dart index 80206fc..4b08d66 100644 --- a/Kiosk/lib/l10n/app_localizations_zh.dart +++ b/Kiosk/lib/l10n/app_localizations_zh.dart @@ -190,4 +190,35 @@ class AppLocalizationsZh extends AppLocalizations { @override String get returningHome => '正在刷新数据...'; + + @override + String get openLauncher => '打开桌面启动器'; + + @override + String launcherTarget(Object target) { + return '目标:$target'; + } + + @override + String get launcherDefault => '桌面启动器'; + + @override + String get homeAppPackageTitle => '首页应用包名(可选)'; + + @override + String get homeAppPackageHint => '例如:com.secgo.home'; + + @override + String get homeAppNotSet => '未设置(使用桌面启动器)'; + + @override + String homeAppOpenFailed(Object package) { + return '无法打开 $package,将打开桌面启动器。'; + } + + @override + String get clear => '清除'; + + @override + String get save => '保存'; } diff --git a/Kiosk/lib/l10n/app_zh.arb b/Kiosk/lib/l10n/app_zh.arb index e06d87f..bb26e0d 100644 --- a/Kiosk/lib/l10n/app_zh.arb +++ b/Kiosk/lib/l10n/app_zh.arb @@ -53,5 +53,14 @@ "pinLength": "PIN码至少需要4位数字", "confirmPin": "确认PIN码", "pinMismatch": "两次输入的PIN码不一致", - "saveAndContinue": "保存并继续" + "saveAndContinue": "保存并继续", + "openLauncher": "打开桌面启动器", + "launcherTarget": "目标:{target}", + "launcherDefault": "桌面启动器", + "homeAppPackageTitle": "首页应用包名(可选)", + "homeAppPackageHint": "例如:com.secgo.home", + "homeAppNotSet": "未设置(使用桌面启动器)", + "homeAppOpenFailed": "无法打开 {package},将打开桌面启动器。", + "clear": "清除", + "save": "保存" } diff --git a/Kiosk/lib/screens/main_screen.dart b/Kiosk/lib/screens/main_screen.dart index 898ddf2..4060be4 100644 --- a/Kiosk/lib/screens/main_screen.dart +++ b/Kiosk/lib/screens/main_screen.dart @@ -632,11 +632,16 @@ class _MainScreenState extends State { // Settings Button (Hidden access) IconButton( icon: const Icon(Icons.settings, color: Colors.grey), - onPressed: () { - Navigator.push( + onPressed: () async { + final result = await Navigator.push( context, MaterialPageRoute(builder: (_) => const SettingsScreen()), ); + if (!mounted) return; + if (result == 'reset') { + _clearCart(); + setState(() => _isProcessing = false); + } }, ), ], diff --git a/Kiosk/lib/screens/settings_screen.dart b/Kiosk/lib/screens/settings_screen.dart index c0e37be..677a71c 100644 --- a/Kiosk/lib/screens/settings_screen.dart +++ b/Kiosk/lib/screens/settings_screen.dart @@ -4,6 +4,7 @@ import 'package:kiosk/services/server/kiosk_server.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:kiosk/l10n/app_localizations.dart'; +import 'package:kiosk/services/android_launcher_service.dart'; import 'package:kiosk/services/settings_service.dart'; import 'package:kiosk/services/restore_notifier.dart'; @@ -18,15 +19,18 @@ class _SettingsScreenState extends State { late final KioskServerService _serverService; final TextEditingController _pinController = TextEditingController(); final SettingsService _settingsService = SettingsService(); // Add SettingsService + final AndroidLauncherService _launcherService = AndroidLauncherService(); bool _isServerRunning = false; bool _isLoading = false; bool _showRestoreComplete = false; String? _qrData; + String? _homeAppPackage; @override void initState() { super.initState(); _serverService = KioskServerService(onRestoreComplete: _onRestoreComplete); + _homeAppPackage = _settingsService.getHomeAppPackage(); // Pre-fill PIN if available final savedPin = _settingsService.getPin(); if (savedPin != null) { @@ -41,9 +45,137 @@ class _SettingsScreenState extends State { @override void dispose() { _serverService.stopServer(); + _pinController.dispose(); super.dispose(); } + Future _confirmPin() async { + final l10n = AppLocalizations.of(context)!; + final expected = _settingsService.getPin() ?? _pinController.text.trim(); + final controller = TextEditingController(); + final ok = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text(l10n.adminConfirm), + content: TextField( + controller: controller, + obscureText: true, + keyboardType: TextInputType.number, + decoration: InputDecoration(labelText: l10n.enterPin), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: Text(l10n.cancel), + ), + TextButton( + onPressed: () { + if (controller.text.trim() == expected) { + Navigator.pop(context, true); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.invalidPin)), + ); + } + }, + child: Text(l10n.confirm), + ), + ], + ); + }, + ); + return ok ?? false; + } + + Future _editHomeAppPackage() async { + final l10n = AppLocalizations.of(context)!; + final controller = TextEditingController(text: _homeAppPackage ?? ''); + final result = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text(l10n.homeAppPackageTitle), + content: TextField( + controller: controller, + decoration: InputDecoration(hintText: l10n.homeAppPackageHint), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, null), + child: Text(l10n.cancel), + ), + TextButton( + onPressed: () => Navigator.pop(context, ''), + child: Text(l10n.clear), + ), + TextButton( + onPressed: () => Navigator.pop(context, controller.text), + child: Text(l10n.save), + ), + ], + ); + }, + ); + + if (!mounted) return; + if (result == null) return; + final value = result.trim(); + await _settingsService.setHomeAppPackage(value.isEmpty ? null : value); + if (!mounted) return; + setState(() => _homeAppPackage = value.isEmpty ? null : value); + } + + Future _openLauncher() async { + final l10n = AppLocalizations.of(context)!; + final ok = await _confirmPin(); + if (!ok) return; + + await _settingsService.setPendingPaymentOrderId(null); + + final pkg = _settingsService.getHomeAppPackage(); + bool launched = false; + if (pkg != null && pkg.isNotEmpty) { + launched = await _launcherService.openApp(pkg); + if (!launched && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.homeAppOpenFailed(pkg))), + ); + } + } + if (!launched) { + await _launcherService.openLauncherHome(); + } + + if (!mounted) return; + Navigator.pop(context, 'reset'); + } + + Widget _buildLauncherSection() { + final l10n = AppLocalizations.of(context)!; + final target = _homeAppPackage ?? l10n.launcherDefault; + return Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.home_outlined), + title: Text(l10n.openLauncher), + subtitle: Text(l10n.launcherTarget(target)), + onTap: _openLauncher, + ), + const Divider(height: 1), + ListTile( + leading: const Icon(Icons.apps_outlined), + title: Text(l10n.homeAppPackageTitle), + subtitle: Text(_homeAppPackage ?? l10n.homeAppNotSet), + onTap: _editHomeAppPackage, + ), + ], + ), + ); + } + void _onRestoreComplete() { if (_showRestoreComplete) return; _runRestoreCompleteFlow(); @@ -133,7 +265,12 @@ class _SettingsScreenState extends State { _serverService.port, ), ), - const SizedBox(height: 40), + const SizedBox(height: 24), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 520), + child: _buildLauncherSection(), + ), + const SizedBox(height: 16), ElevatedButton( onPressed: () { // Restart server logic or close page @@ -157,7 +294,12 @@ class _SettingsScreenState extends State { l10n.serverStartFailedMessage, textAlign: TextAlign.center, ), - const SizedBox(height: 30), + const SizedBox(height: 24), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 520), + child: _buildLauncherSection(), + ), + const SizedBox(height: 16), ElevatedButton( onPressed: _startServer, child: Text(l10n.retry), diff --git a/Kiosk/lib/services/android_launcher_service.dart b/Kiosk/lib/services/android_launcher_service.dart new file mode 100644 index 0000000..52135b3 --- /dev/null +++ b/Kiosk/lib/services/android_launcher_service.dart @@ -0,0 +1,24 @@ +import 'dart:io'; + +import 'package:flutter/services.dart'; + +class AndroidLauncherService { + static const MethodChannel _methodChannel = + MethodChannel('com.secgo.kiosk/notification_listener'); + + Future openLauncherHome() async { + if (!Platform.isAndroid) return false; + final ok = await _methodChannel.invokeMethod('openLauncherHome'); + return ok ?? false; + } + + Future openApp(String packageName) async { + if (!Platform.isAndroid) return false; + final ok = await _methodChannel.invokeMethod( + 'openApp', + {'packageName': packageName}, + ); + return ok ?? false; + } +} + diff --git a/Kiosk/lib/services/settings_service.dart b/Kiosk/lib/services/settings_service.dart index aa0cb0f..87eb682 100644 --- a/Kiosk/lib/services/settings_service.dart +++ b/Kiosk/lib/services/settings_service.dart @@ -8,6 +8,7 @@ class SettingsService { static const String _paymentQrKey = 'payment_qr'; static const String _paymentQrsKey = 'payment_qrs'; static const String _pendingPaymentOrderIdKey = 'pending_payment_order_id'; + static const String _homeAppPackageKey = 'home_app_package'; Future init() async { await Hive.openBox(_boxName); @@ -87,6 +88,24 @@ class SettingsService { await _box.put(_pendingPaymentOrderIdKey, id); } + String? getHomeAppPackage() { + final v = _box.get(_homeAppPackageKey); + if (v is String) { + final s = v.trim(); + return s.isEmpty ? null : s; + } + return null; + } + + Future setHomeAppPackage(String? packageName) async { + final v = packageName?.trim(); + if (v == null || v.isEmpty) { + await _box.delete(_homeAppPackageKey); + return; + } + await _box.put(_homeAppPackageKey, v); + } + Future getOrCreateDeviceId() async { final existing = getDeviceId(); From bfecc77f96f65bdd858373975af208d39d36f011 Mon Sep 17 00:00:00 2001 From: TachibanaLolo Date: Sun, 25 Jan 2026 02:10:39 +0800 Subject: [PATCH 2/3] feat(kiosk): pick home app from installed apps --- .../kotlin/com/secgo/kiosk/MainActivity.kt | 26 ++++ Kiosk/lib/l10n/app_en.arb | 3 + Kiosk/lib/l10n/app_localizations.dart | 18 +++ Kiosk/lib/l10n/app_localizations_en.dart | 9 ++ Kiosk/lib/l10n/app_localizations_zh.dart | 9 ++ Kiosk/lib/l10n/app_zh.arb | 3 + Kiosk/lib/screens/settings_screen.dart | 132 ++++++++++++++---- .../services/android_launcher_service.dart | 16 ++- Kiosk/lib/services/settings_service.dart | 24 ++++ 9 files changed, 209 insertions(+), 31 deletions(-) diff --git a/Kiosk/android/app/src/main/kotlin/com/secgo/kiosk/MainActivity.kt b/Kiosk/android/app/src/main/kotlin/com/secgo/kiosk/MainActivity.kt index 236c4a8..561388e 100644 --- a/Kiosk/android/app/src/main/kotlin/com/secgo/kiosk/MainActivity.kt +++ b/Kiosk/android/app/src/main/kotlin/com/secgo/kiosk/MainActivity.kt @@ -4,6 +4,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.pm.PackageManager import android.os.Build import android.provider.Settings import android.util.Log @@ -67,6 +68,31 @@ class MainActivity : FlutterActivity() { startActivity(launchIntent) result.success(true) } + "listLaunchableApps" -> { + val intent = + Intent(Intent.ACTION_MAIN).apply { + addCategory(Intent.CATEGORY_LAUNCHER) + } + + val activities = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(0)) + } else { + @Suppress("DEPRECATION") + packageManager.queryIntentActivities(intent, 0) + } + + val resultList = + activities.map { ri -> + val label = ri.loadLabel(packageManager)?.toString() ?: "" + val pkg = ri.activityInfo?.packageName ?: "" + mapOf( + "packageName" to pkg, + "label" to label, + ) + }.filter { it["packageName"]?.isNotBlank() == true } + result.success(resultList) + } "openNotificationListenerSettings" -> { startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) result.success(true) diff --git a/Kiosk/lib/l10n/app_en.arb b/Kiosk/lib/l10n/app_en.arb index 2adba17..bcfe823 100644 --- a/Kiosk/lib/l10n/app_en.arb +++ b/Kiosk/lib/l10n/app_en.arb @@ -60,6 +60,9 @@ "homeAppPackageTitle": "Home app package (optional)", "homeAppPackageHint": "e.g. com.secgo.home", "homeAppNotSet": "Not set (use Launcher)", + "homeAppTitle": "Home App (optional)", + "searchApps": "Search apps", + "noAppsFound": "No apps found", "homeAppOpenFailed": "Can't open {package}. Opening Launcher instead.", "clear": "Clear", "save": "Save" diff --git a/Kiosk/lib/l10n/app_localizations.dart b/Kiosk/lib/l10n/app_localizations.dart index e1e7334..495dfed 100644 --- a/Kiosk/lib/l10n/app_localizations.dart +++ b/Kiosk/lib/l10n/app_localizations.dart @@ -464,6 +464,24 @@ abstract class AppLocalizations { /// **'Not set (use Launcher)'** String get homeAppNotSet; + /// No description provided for @homeAppTitle. + /// + /// In en, this message translates to: + /// **'Home App (optional)'** + String get homeAppTitle; + + /// No description provided for @searchApps. + /// + /// In en, this message translates to: + /// **'Search apps'** + String get searchApps; + + /// No description provided for @noAppsFound. + /// + /// In en, this message translates to: + /// **'No apps found'** + String get noAppsFound; + /// No description provided for @homeAppOpenFailed. /// /// In en, this message translates to: diff --git a/Kiosk/lib/l10n/app_localizations_en.dart b/Kiosk/lib/l10n/app_localizations_en.dart index 2741b1f..58823a7 100644 --- a/Kiosk/lib/l10n/app_localizations_en.dart +++ b/Kiosk/lib/l10n/app_localizations_en.dart @@ -215,6 +215,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get homeAppNotSet => 'Not set (use Launcher)'; + @override + String get homeAppTitle => 'Home App (optional)'; + + @override + String get searchApps => 'Search apps'; + + @override + String get noAppsFound => 'No apps found'; + @override String homeAppOpenFailed(Object package) { return 'Can\'t open $package. Opening Launcher instead.'; diff --git a/Kiosk/lib/l10n/app_localizations_zh.dart b/Kiosk/lib/l10n/app_localizations_zh.dart index 4b08d66..fc6e174 100644 --- a/Kiosk/lib/l10n/app_localizations_zh.dart +++ b/Kiosk/lib/l10n/app_localizations_zh.dart @@ -211,6 +211,15 @@ class AppLocalizationsZh extends AppLocalizations { @override String get homeAppNotSet => '未设置(使用桌面启动器)'; + @override + String get homeAppTitle => '首页应用(可选)'; + + @override + String get searchApps => '搜索应用'; + + @override + String get noAppsFound => '未找到应用'; + @override String homeAppOpenFailed(Object package) { return '无法打开 $package,将打开桌面启动器。'; diff --git a/Kiosk/lib/l10n/app_zh.arb b/Kiosk/lib/l10n/app_zh.arb index bb26e0d..8222a95 100644 --- a/Kiosk/lib/l10n/app_zh.arb +++ b/Kiosk/lib/l10n/app_zh.arb @@ -60,6 +60,9 @@ "homeAppPackageTitle": "首页应用包名(可选)", "homeAppPackageHint": "例如:com.secgo.home", "homeAppNotSet": "未设置(使用桌面启动器)", + "homeAppTitle": "首页应用(可选)", + "searchApps": "搜索应用", + "noAppsFound": "未找到应用", "homeAppOpenFailed": "无法打开 {package},将打开桌面启动器。", "clear": "清除", "save": "保存" diff --git a/Kiosk/lib/screens/settings_screen.dart b/Kiosk/lib/screens/settings_screen.dart index 677a71c..a051813 100644 --- a/Kiosk/lib/screens/settings_screen.dart +++ b/Kiosk/lib/screens/settings_screen.dart @@ -25,12 +25,14 @@ class _SettingsScreenState extends State { bool _showRestoreComplete = false; String? _qrData; String? _homeAppPackage; + String? _homeAppLabel; @override void initState() { super.initState(); _serverService = KioskServerService(onRestoreComplete: _onRestoreComplete); _homeAppPackage = _settingsService.getHomeAppPackage(); + _homeAppLabel = _settingsService.getHomeAppLabel(); // Pre-fill PIN if available final savedPin = _settingsService.getPin(); if (savedPin != null) { @@ -88,42 +90,112 @@ class _SettingsScreenState extends State { return ok ?? false; } - Future _editHomeAppPackage() async { + Future _pickHomeApp() async { final l10n = AppLocalizations.of(context)!; - final controller = TextEditingController(text: _homeAppPackage ?? ''); - final result = await showDialog( + final apps = await _launcherService.listLaunchableApps(); + apps.sort((a, b) { + final al = (a['label'] ?? a['packageName'] ?? '').toLowerCase(); + final bl = (b['label'] ?? b['packageName'] ?? '').toLowerCase(); + return al.compareTo(bl); + }); + + if (!mounted) return; + final result = await showDialog?>( context: context, builder: (context) { - return AlertDialog( - title: Text(l10n.homeAppPackageTitle), - content: TextField( - controller: controller, - decoration: InputDecoration(hintText: l10n.homeAppPackageHint), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, null), - child: Text(l10n.cancel), - ), - TextButton( - onPressed: () => Navigator.pop(context, ''), - child: Text(l10n.clear), - ), - TextButton( - onPressed: () => Navigator.pop(context, controller.text), - child: Text(l10n.save), - ), - ], + var query = ''; + final searchController = TextEditingController(); + return StatefulBuilder( + builder: (context, setState) { + final filtered = query.isEmpty + ? apps + : apps.where((e) { + final label = (e['label'] ?? '').toLowerCase(); + final pkg = (e['packageName'] ?? '').toLowerCase(); + final q = query.toLowerCase(); + return label.contains(q) || pkg.contains(q); + }).toList(); + + return AlertDialog( + title: Text(l10n.homeAppTitle), + content: SizedBox( + width: 520, + height: 520, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: searchController, + decoration: InputDecoration( + hintText: l10n.searchApps, + prefixIcon: const Icon(Icons.search), + ), + onChanged: (v) => setState(() => query = v.trim()), + ), + const SizedBox(height: 12), + Expanded( + child: filtered.isEmpty + ? Center(child: Text(l10n.noAppsFound)) + : ListView.builder( + itemCount: filtered.length + 1, + itemBuilder: (context, index) { + if (index == 0) { + final selected = _homeAppPackage == null; + return ListTile( + leading: const Icon(Icons.home_outlined), + title: Text(l10n.launcherDefault), + trailing: selected ? const Icon(Icons.check) : null, + onTap: () => Navigator.pop(context, {'packageName': '', 'label': ''}), + ); + } + final app = filtered[index - 1]; + final pkg = app['packageName'] ?? ''; + final label = app['label'] ?? pkg; + final selected = _homeAppPackage == pkg; + return ListTile( + title: Text(label), + subtitle: Text(pkg), + trailing: selected ? const Icon(Icons.check) : null, + onTap: () => Navigator.pop(context, {'packageName': pkg, 'label': label}), + ); + }, + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, null), + child: Text(l10n.cancel), + ), + ], + ); + }, ); }, ); if (!mounted) return; if (result == null) return; - final value = result.trim(); - await _settingsService.setHomeAppPackage(value.isEmpty ? null : value); + final pkg = (result['packageName'] ?? '').trim(); + final label = (result['label'] ?? '').trim(); + if (pkg.isEmpty) { + await _settingsService.clearHomeAppSelection(); + if (!mounted) return; + setState(() { + _homeAppPackage = null; + _homeAppLabel = null; + }); + return; + } + await _settingsService.setHomeAppPackage(pkg); + await _settingsService.setHomeAppLabel(label.isEmpty ? pkg : label); if (!mounted) return; - setState(() => _homeAppPackage = value.isEmpty ? null : value); + setState(() { + _homeAppPackage = pkg; + _homeAppLabel = label.isEmpty ? pkg : label; + }); } Future _openLauncher() async { @@ -153,7 +225,7 @@ class _SettingsScreenState extends State { Widget _buildLauncherSection() { final l10n = AppLocalizations.of(context)!; - final target = _homeAppPackage ?? l10n.launcherDefault; + final target = _homeAppLabel ?? _homeAppPackage ?? l10n.launcherDefault; return Card( child: Column( mainAxisSize: MainAxisSize.min, @@ -167,9 +239,9 @@ class _SettingsScreenState extends State { const Divider(height: 1), ListTile( leading: const Icon(Icons.apps_outlined), - title: Text(l10n.homeAppPackageTitle), - subtitle: Text(_homeAppPackage ?? l10n.homeAppNotSet), - onTap: _editHomeAppPackage, + title: Text(l10n.homeAppTitle), + subtitle: Text(_homeAppLabel ?? _homeAppPackage ?? l10n.homeAppNotSet), + onTap: _pickHomeApp, ), ], ), diff --git a/Kiosk/lib/services/android_launcher_service.dart b/Kiosk/lib/services/android_launcher_service.dart index 52135b3..d29f24b 100644 --- a/Kiosk/lib/services/android_launcher_service.dart +++ b/Kiosk/lib/services/android_launcher_service.dart @@ -6,6 +6,21 @@ class AndroidLauncherService { static const MethodChannel _methodChannel = MethodChannel('com.secgo.kiosk/notification_listener'); + Future>> listLaunchableApps() async { + if (!Platform.isAndroid) return const []; + final data = await _methodChannel + .invokeMethod('listLaunchableApps') + .then((v) => (v ?? const []).map((e) => Map.from(e as Map)).toList()); + final result = >[]; + for (final e in data) { + final pkg = e['packageName']?.toString().trim(); + final label = e['label']?.toString().trim(); + if (pkg == null || pkg.isEmpty) continue; + result.add({'packageName': pkg, 'label': (label == null || label.isEmpty) ? pkg : label}); + } + return result; + } + Future openLauncherHome() async { if (!Platform.isAndroid) return false; final ok = await _methodChannel.invokeMethod('openLauncherHome'); @@ -21,4 +36,3 @@ class AndroidLauncherService { return ok ?? false; } } - diff --git a/Kiosk/lib/services/settings_service.dart b/Kiosk/lib/services/settings_service.dart index 87eb682..a31e22e 100644 --- a/Kiosk/lib/services/settings_service.dart +++ b/Kiosk/lib/services/settings_service.dart @@ -9,6 +9,7 @@ class SettingsService { static const String _paymentQrsKey = 'payment_qrs'; static const String _pendingPaymentOrderIdKey = 'pending_payment_order_id'; static const String _homeAppPackageKey = 'home_app_package'; + static const String _homeAppLabelKey = 'home_app_label'; Future init() async { await Hive.openBox(_boxName); @@ -106,6 +107,29 @@ class SettingsService { await _box.put(_homeAppPackageKey, v); } + String? getHomeAppLabel() { + final v = _box.get(_homeAppLabelKey); + if (v is String) { + final s = v.trim(); + return s.isEmpty ? null : s; + } + return null; + } + + Future setHomeAppLabel(String? label) async { + final v = label?.trim(); + if (v == null || v.isEmpty) { + await _box.delete(_homeAppLabelKey); + return; + } + await _box.put(_homeAppLabelKey, v); + } + + Future clearHomeAppSelection() async { + await _box.delete(_homeAppPackageKey); + await _box.delete(_homeAppLabelKey); + } + Future getOrCreateDeviceId() async { final existing = getDeviceId(); From 226c4b75cdd133c1d765ef536ab134704f6226e7 Mon Sep 17 00:00:00 2001 From: TachibanaLolo Date: Sun, 25 Jan 2026 02:16:28 +0800 Subject: [PATCH 3/3] feat(kiosk): require PIN to open settings --- Kiosk/lib/screens/main_screen.dart | 48 ++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/Kiosk/lib/screens/main_screen.dart b/Kiosk/lib/screens/main_screen.dart index 4060be4..e1ae02c 100644 --- a/Kiosk/lib/screens/main_screen.dart +++ b/Kiosk/lib/screens/main_screen.dart @@ -175,6 +175,48 @@ class _MainScreenState extends State { ); } + Future _confirmAdminPin() async { + final expected = _settingsService.getPin(); + if (expected == null || expected.isEmpty) return true; + + final l10n = AppLocalizations.of(context)!; + final controller = TextEditingController(); + final ok = await showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) { + return AlertDialog( + title: Text(l10n.adminConfirm), + content: TextField( + controller: controller, + obscureText: true, + keyboardType: TextInputType.number, + decoration: InputDecoration(labelText: l10n.enterPin), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext, false), + child: Text(l10n.cancel), + ), + TextButton( + onPressed: () { + if (controller.text.trim() == expected) { + Navigator.pop(dialogContext, true); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.invalidPin)), + ); + } + }, + child: Text(l10n.confirm), + ), + ], + ); + }, + ); + return ok ?? false; + } + Future _resumePendingPaymentIfAny() async { if (!mounted) return; final pendingId = _settingsService.getPendingPaymentOrderId(); @@ -633,8 +675,10 @@ class _MainScreenState extends State { IconButton( icon: const Icon(Icons.settings, color: Colors.grey), onPressed: () async { - final result = await Navigator.push( - context, + final navigator = Navigator.of(context); + final ok = await _confirmAdminPin(); + if (!ok || !mounted) return; + final result = await navigator.push( MaterialPageRoute(builder: (_) => const SettingsScreen()), ); if (!mounted) return;