From b7c009f91c30afea583efd967308997ccf06eb7a Mon Sep 17 00:00:00 2001 From: TachibanaLolo Date: Sun, 25 Jan 2026 04:54:25 +0800 Subject: [PATCH 1/5] feat(kiosk): hotspot and mobile data toggles --- .../android/app/src/main/AndroidManifest.xml | 4 + .../kotlin/com/secgo/kiosk/MainActivity.kt | 95 ++++++++++++ Kiosk/lib/l10n/app_en.arb | 6 + Kiosk/lib/l10n/app_localizations.dart | 36 +++++ Kiosk/lib/l10n/app_localizations_en.dart | 18 +++ Kiosk/lib/l10n/app_localizations_zh.dart | 18 +++ Kiosk/lib/l10n/app_zh.arb | 6 + Kiosk/lib/screens/settings_screen.dart | 146 ++++++++++++++++-- .../lib/services/android_network_service.dart | 40 +++++ 9 files changed, 359 insertions(+), 10 deletions(-) create mode 100644 Kiosk/lib/services/android_network_service.dart diff --git a/Kiosk/android/app/src/main/AndroidManifest.xml b/Kiosk/android/app/src/main/AndroidManifest.xml index 8748df4..bb1f914 100644 --- a/Kiosk/android/app/src/main/AndroidManifest.xml +++ b/Kiosk/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,10 @@ + + + + result.notImplemented() } } + + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, NETWORK_CHANNEL).setMethodCallHandler { call, result -> + when (call.method) { + "getHotspotEnabled" -> result.success(localHotspotReservation != null) + "setHotspotEnabled" -> { + val enabled = call.argument("enabled") == true + if (!enabled) { + localHotspotReservation?.close() + localHotspotReservation = null + result.success(true) + return@setMethodCallHandler + } + + if (localHotspotReservation != null) { + result.success(true) + return@setMethodCallHandler + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + result.success(false) + return@setMethodCallHandler + } + + try { + val wifiManager = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + wifiManager.startLocalOnlyHotspot( + object : WifiManager.LocalOnlyHotspotCallback() { + override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation) { + localHotspotReservation = reservation + result.success(true) + } + + override fun onStopped() { + localHotspotReservation?.close() + localHotspotReservation = null + } + + override fun onFailed(reason: Int) { + localHotspotReservation?.close() + localHotspotReservation = null + result.success(false) + } + }, + Handler(Looper.getMainLooper()), + ) + } catch (_: Exception) { + result.success(false) + } + } + "openHotspotSettings" -> { + try { + startActivity(Intent("android.settings.TETHER_SETTINGS").addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + } catch (_: Exception) { + startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + } + result.success(true) + } + "getMobileDataEnabled" -> { + val enabled = try { + Settings.Global.getInt(contentResolver, "mobile_data", 0) == 1 + } catch (_: Exception) { + false + } + result.success(enabled) + } + "setMobileDataEnabled" -> { + val enabled = call.argument("enabled") == true + val ok = try { + Settings.Global.putInt(contentResolver, "mobile_data", if (enabled) 1 else 0) + } catch (_: Exception) { + false + } + result.success(ok) + } + "openInternetSettings" -> { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startActivity(Intent(Settings.Panel.ACTION_INTERNET_CONNECTIVITY).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + } else { + startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + } + } catch (_: Exception) { + } + result.success(true) + } + else -> result.notImplemented() + } + } } override fun onCreate(savedInstanceState: android.os.Bundle?) { @@ -169,6 +261,8 @@ class MainActivity : FlutterActivity() { } } receiver = null + localHotspotReservation?.close() + localHotspotReservation = null super.onDestroy() } @@ -271,6 +365,7 @@ class MainActivity : FlutterActivity() { companion object { private const val METHOD_CHANNEL = "com.secgo.kiosk/notification_listener" private const val EVENTS_CHANNEL = "com.secgo.kiosk/notifications" + private const val NETWORK_CHANNEL = "com.secgo.kiosk/network" @Volatile private var eventSink: EventChannel.EventSink? = null diff --git a/Kiosk/lib/l10n/app_en.arb b/Kiosk/lib/l10n/app_en.arb index 2d07b12..85ab409 100644 --- a/Kiosk/lib/l10n/app_en.arb +++ b/Kiosk/lib/l10n/app_en.arb @@ -70,6 +70,12 @@ "categoryAll": "All", "categoryUncategorized": "Uncategorized", "searchProducts": "Search products", + "networkSettings": "Network", + "hotspot": "Hotspot", + "hotspotHint": "Enable hotspot for pairing", + "mobileData": "Mobile data", + "mobileDataHint": "Toggle mobile data (may be restricted)", + "networkToggleFailed": "Action failed. Opening system settings…", "clear": "Clear", "save": "Save" } diff --git a/Kiosk/lib/l10n/app_localizations.dart b/Kiosk/lib/l10n/app_localizations.dart index 68031a9..8cc60b2 100644 --- a/Kiosk/lib/l10n/app_localizations.dart +++ b/Kiosk/lib/l10n/app_localizations.dart @@ -524,6 +524,42 @@ abstract class AppLocalizations { /// **'Search products'** String get searchProducts; + /// No description provided for @networkSettings. + /// + /// In en, this message translates to: + /// **'Network'** + String get networkSettings; + + /// No description provided for @hotspot. + /// + /// In en, this message translates to: + /// **'Hotspot'** + String get hotspot; + + /// No description provided for @hotspotHint. + /// + /// In en, this message translates to: + /// **'Enable hotspot for pairing'** + String get hotspotHint; + + /// No description provided for @mobileData. + /// + /// In en, this message translates to: + /// **'Mobile data'** + String get mobileData; + + /// No description provided for @mobileDataHint. + /// + /// In en, this message translates to: + /// **'Toggle mobile data (may be restricted)'** + String get mobileDataHint; + + /// No description provided for @networkToggleFailed. + /// + /// In en, this message translates to: + /// **'Action failed. Opening system settings…'** + String get networkToggleFailed; + /// No description provided for @clear. /// /// 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 77993b6..b84d766 100644 --- a/Kiosk/lib/l10n/app_localizations_en.dart +++ b/Kiosk/lib/l10n/app_localizations_en.dart @@ -247,6 +247,24 @@ class AppLocalizationsEn extends AppLocalizations { @override String get searchProducts => 'Search products'; + @override + String get networkSettings => 'Network'; + + @override + String get hotspot => 'Hotspot'; + + @override + String get hotspotHint => 'Enable hotspot for pairing'; + + @override + String get mobileData => 'Mobile data'; + + @override + String get mobileDataHint => 'Toggle mobile data (may be restricted)'; + + @override + String get networkToggleFailed => 'Action failed. Opening system settings…'; + @override String get clear => 'Clear'; diff --git a/Kiosk/lib/l10n/app_localizations_zh.dart b/Kiosk/lib/l10n/app_localizations_zh.dart index 6bce3fb..aff9d7f 100644 --- a/Kiosk/lib/l10n/app_localizations_zh.dart +++ b/Kiosk/lib/l10n/app_localizations_zh.dart @@ -243,6 +243,24 @@ class AppLocalizationsZh extends AppLocalizations { @override String get searchProducts => '搜索商品'; + @override + String get networkSettings => '网络设置'; + + @override + String get hotspot => '热点'; + + @override + String get hotspotHint => '开启热点用于配对'; + + @override + String get mobileData => '移动数据'; + + @override + String get mobileDataHint => '切换移动数据(可能受系统限制)'; + + @override + String get networkToggleFailed => '操作失败,正在打开系统设置…'; + @override String get clear => '清除'; diff --git a/Kiosk/lib/l10n/app_zh.arb b/Kiosk/lib/l10n/app_zh.arb index 53167a8..b8dd099 100644 --- a/Kiosk/lib/l10n/app_zh.arb +++ b/Kiosk/lib/l10n/app_zh.arb @@ -70,6 +70,12 @@ "categoryAll": "全部", "categoryUncategorized": "未分类", "searchProducts": "搜索商品", + "networkSettings": "网络设置", + "hotspot": "热点", + "hotspotHint": "开启热点用于配对", + "mobileData": "移动数据", + "mobileDataHint": "切换移动数据(可能受系统限制)", + "networkToggleFailed": "操作失败,正在打开系统设置…", "clear": "清除", "save": "保存" } diff --git a/Kiosk/lib/screens/settings_screen.dart b/Kiosk/lib/screens/settings_screen.dart index a051813..6b1e3d1 100644 --- a/Kiosk/lib/screens/settings_screen.dart +++ b/Kiosk/lib/screens/settings_screen.dart @@ -5,6 +5,7 @@ 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/android_network_service.dart'; import 'package:kiosk/services/settings_service.dart'; import 'package:kiosk/services/restore_notifier.dart'; @@ -20,12 +21,16 @@ class _SettingsScreenState extends State { final TextEditingController _pinController = TextEditingController(); final SettingsService _settingsService = SettingsService(); // Add SettingsService final AndroidLauncherService _launcherService = AndroidLauncherService(); + final AndroidNetworkService _networkService = AndroidNetworkService(); bool _isServerRunning = false; bool _isLoading = false; bool _showRestoreComplete = false; String? _qrData; String? _homeAppPackage; String? _homeAppLabel; + bool _hotspotEnabled = false; + bool _mobileDataEnabled = false; + bool _networkBusy = false; @override void initState() { @@ -42,6 +47,9 @@ class _SettingsScreenState extends State { _startServer(); }); } + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadNetworkState(); + }); } @override @@ -262,6 +270,106 @@ class _SettingsScreenState extends State { setState(() => _showRestoreComplete = false); } + Future _loadNetworkState() async { + try { + final hotspot = await _networkService.getHotspotEnabled(); + final mobile = await _networkService.getMobileDataEnabled(); + if (!mounted) return; + setState(() { + _hotspotEnabled = hotspot; + _mobileDataEnabled = mobile; + }); + } catch (_) { + } + } + + Future _setHotspot(bool enabled) async { + final l10n = AppLocalizations.of(context)!; + setState(() => _networkBusy = true); + try { + final ok = await _networkService.setHotspotEnabled(enabled); + if (!ok) { + await _networkService.openHotspotSettings(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.networkToggleFailed)), + ); + } + } + } catch (_) { + try { + await _networkService.openHotspotSettings(); + } catch (_) { + } + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.networkToggleFailed)), + ); + } + } finally { + await _loadNetworkState(); + if (mounted) setState(() => _networkBusy = false); + } + } + + Future _setMobileData(bool enabled) async { + final l10n = AppLocalizations.of(context)!; + setState(() => _networkBusy = true); + try { + final ok = await _networkService.setMobileDataEnabled(enabled); + if (!ok) { + await _networkService.openInternetSettings(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.networkToggleFailed)), + ); + } + } + } catch (_) { + try { + await _networkService.openInternetSettings(); + } catch (_) { + } + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.networkToggleFailed)), + ); + } + } finally { + await _loadNetworkState(); + if (mounted) setState(() => _networkBusy = false); + } + } + + Widget _buildNetworkSection() { + final l10n = AppLocalizations.of(context)!; + return Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.wifi_tethering_outlined), + title: Text(l10n.networkSettings), + ), + const Divider(height: 1), + SwitchListTile( + value: _hotspotEnabled, + onChanged: _networkBusy ? null : _setHotspot, + title: Text(l10n.hotspot), + subtitle: Text(l10n.hotspotHint), + ), + const Divider(height: 1), + SwitchListTile( + value: _mobileDataEnabled, + onChanged: _networkBusy ? null : _setMobileData, + title: Text(l10n.mobileData), + subtitle: Text(l10n.mobileDataHint), + ), + ], + ), + ); + } + Future _startServer() async { final l10n = AppLocalizations.of(context)!; if (_pinController.text.length < 4) { @@ -313,9 +421,10 @@ class _SettingsScreenState extends State { child: _isLoading ? const CircularProgressIndicator() : _isServerRunning - ? Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ + ? SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ Text( l10n.kioskReadyToSync, style: TextStyle(fontSize: 24, color: Colors.green), @@ -340,7 +449,14 @@ class _SettingsScreenState extends State { const SizedBox(height: 24), ConstrainedBox( constraints: const BoxConstraints(maxWidth: 520), - child: _buildLauncherSection(), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildLauncherSection(), + const SizedBox(height: 16), + _buildNetworkSection(), + ], + ), ), const SizedBox(height: 16), ElevatedButton( @@ -350,11 +466,13 @@ class _SettingsScreenState extends State { }, child: Text(l10n.close), ), - ], + ], + ), ) - : Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ + : SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ const Icon(Icons.error_outline, size: 60, color: Colors.red), const SizedBox(height: 20), Text( @@ -369,14 +487,22 @@ class _SettingsScreenState extends State { const SizedBox(height: 24), ConstrainedBox( constraints: const BoxConstraints(maxWidth: 520), - child: _buildLauncherSection(), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildLauncherSection(), + const SizedBox(height: 16), + _buildNetworkSection(), + ], + ), ), const SizedBox(height: 16), ElevatedButton( onPressed: _startServer, child: Text(l10n.retry), ), - ], + ], + ), ), ), ), diff --git a/Kiosk/lib/services/android_network_service.dart b/Kiosk/lib/services/android_network_service.dart new file mode 100644 index 0000000..8ffc483 --- /dev/null +++ b/Kiosk/lib/services/android_network_service.dart @@ -0,0 +1,40 @@ +import 'package:flutter/services.dart'; + +class AndroidNetworkService { + static const MethodChannel _channel = MethodChannel('com.secgo.kiosk/network'); + + Future getHotspotEnabled() async { + final enabled = await _channel.invokeMethod('getHotspotEnabled'); + return enabled ?? false; + } + + Future setHotspotEnabled(bool enabled) async { + final ok = await _channel.invokeMethod( + 'setHotspotEnabled', + {'enabled': enabled}, + ); + return ok ?? false; + } + + Future openHotspotSettings() async { + await _channel.invokeMethod('openHotspotSettings'); + } + + Future getMobileDataEnabled() async { + final enabled = await _channel.invokeMethod('getMobileDataEnabled'); + return enabled ?? false; + } + + Future setMobileDataEnabled(bool enabled) async { + final ok = await _channel.invokeMethod( + 'setMobileDataEnabled', + {'enabled': enabled}, + ); + return ok ?? false; + } + + Future openInternetSettings() async { + await _channel.invokeMethod('openInternetSettings'); + } +} + From 444d28886213871c97a0bc965710703ecdd4bf30 Mon Sep 17 00:00:00 2001 From: TachibanaLolo Date: Sun, 25 Jan 2026 05:06:04 +0800 Subject: [PATCH 2/5] feat(kiosk): request location and show hotspot credentials --- .../kotlin/com/secgo/kiosk/MainActivity.kt | 22 +++++++++++++++ Kiosk/lib/l10n/app_en.arb | 3 ++ Kiosk/lib/l10n/app_localizations.dart | 18 ++++++++++++ Kiosk/lib/l10n/app_localizations_en.dart | 10 +++++++ Kiosk/lib/l10n/app_localizations_zh.dart | 9 ++++++ Kiosk/lib/l10n/app_zh.arb | 3 ++ Kiosk/lib/screens/settings_screen.dart | 28 +++++++++++++++++-- .../lib/services/android_network_service.dart | 27 +++++++++++++++++- 8 files changed, 116 insertions(+), 4 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 fb46b80..684e73f 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 @@ -21,6 +21,8 @@ import org.json.JSONObject class MainActivity : FlutterActivity() { private var receiver: BroadcastReceiver? = null private var localHotspotReservation: WifiManager.LocalOnlyHotspotReservation? = null + private var localHotspotSsid: String? = null + private var localHotspotPassword: String? = null override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) @@ -108,11 +110,22 @@ class MainActivity : FlutterActivity() { MethodChannel(flutterEngine.dartExecutor.binaryMessenger, NETWORK_CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "getHotspotEnabled" -> result.success(localHotspotReservation != null) + "getHotspotInfo" -> { + result.success( + mapOf( + "enabled" to (localHotspotReservation != null), + "ssid" to localHotspotSsid, + "password" to localHotspotPassword, + ), + ) + } "setHotspotEnabled" -> { val enabled = call.argument("enabled") == true if (!enabled) { localHotspotReservation?.close() localHotspotReservation = null + localHotspotSsid = null + localHotspotPassword = null result.success(true) return@setMethodCallHandler } @@ -133,17 +146,24 @@ class MainActivity : FlutterActivity() { object : WifiManager.LocalOnlyHotspotCallback() { override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation) { localHotspotReservation = reservation + val config = reservation.wifiConfiguration + localHotspotSsid = config?.SSID + localHotspotPassword = config?.preSharedKey result.success(true) } override fun onStopped() { localHotspotReservation?.close() localHotspotReservation = null + localHotspotSsid = null + localHotspotPassword = null } override fun onFailed(reason: Int) { localHotspotReservation?.close() localHotspotReservation = null + localHotspotSsid = null + localHotspotPassword = null result.success(false) } }, @@ -263,6 +283,8 @@ class MainActivity : FlutterActivity() { receiver = null localHotspotReservation?.close() localHotspotReservation = null + localHotspotSsid = null + localHotspotPassword = null super.onDestroy() } diff --git a/Kiosk/lib/l10n/app_en.arb b/Kiosk/lib/l10n/app_en.arb index 85ab409..03848ee 100644 --- a/Kiosk/lib/l10n/app_en.arb +++ b/Kiosk/lib/l10n/app_en.arb @@ -73,6 +73,9 @@ "networkSettings": "Network", "hotspot": "Hotspot", "hotspotHint": "Enable hotspot for pairing", + "ssidLabel": "SSID", + "passwordLabel": "Password", + "locationPermissionRequired": "Location permission is required to enable hotspot.", "mobileData": "Mobile data", "mobileDataHint": "Toggle mobile data (may be restricted)", "networkToggleFailed": "Action failed. Opening system settings…", diff --git a/Kiosk/lib/l10n/app_localizations.dart b/Kiosk/lib/l10n/app_localizations.dart index 8cc60b2..7df02a7 100644 --- a/Kiosk/lib/l10n/app_localizations.dart +++ b/Kiosk/lib/l10n/app_localizations.dart @@ -542,6 +542,24 @@ abstract class AppLocalizations { /// **'Enable hotspot for pairing'** String get hotspotHint; + /// No description provided for @ssidLabel. + /// + /// In en, this message translates to: + /// **'SSID'** + String get ssidLabel; + + /// No description provided for @passwordLabel. + /// + /// In en, this message translates to: + /// **'Password'** + String get passwordLabel; + + /// No description provided for @locationPermissionRequired. + /// + /// In en, this message translates to: + /// **'Location permission is required to enable hotspot.'** + String get locationPermissionRequired; + /// No description provided for @mobileData. /// /// 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 b84d766..6df0def 100644 --- a/Kiosk/lib/l10n/app_localizations_en.dart +++ b/Kiosk/lib/l10n/app_localizations_en.dart @@ -256,6 +256,16 @@ class AppLocalizationsEn extends AppLocalizations { @override String get hotspotHint => 'Enable hotspot for pairing'; + @override + String get ssidLabel => 'SSID'; + + @override + String get passwordLabel => 'Password'; + + @override + String get locationPermissionRequired => + 'Location permission is required to enable hotspot.'; + @override String get mobileData => 'Mobile data'; diff --git a/Kiosk/lib/l10n/app_localizations_zh.dart b/Kiosk/lib/l10n/app_localizations_zh.dart index aff9d7f..9a5cae9 100644 --- a/Kiosk/lib/l10n/app_localizations_zh.dart +++ b/Kiosk/lib/l10n/app_localizations_zh.dart @@ -252,6 +252,15 @@ class AppLocalizationsZh extends AppLocalizations { @override String get hotspotHint => '开启热点用于配对'; + @override + String get ssidLabel => '名称'; + + @override + String get passwordLabel => '密码'; + + @override + String get locationPermissionRequired => '开启热点需要定位权限。'; + @override String get mobileData => '移动数据'; diff --git a/Kiosk/lib/l10n/app_zh.arb b/Kiosk/lib/l10n/app_zh.arb index b8dd099..d8a0075 100644 --- a/Kiosk/lib/l10n/app_zh.arb +++ b/Kiosk/lib/l10n/app_zh.arb @@ -73,6 +73,9 @@ "networkSettings": "网络设置", "hotspot": "热点", "hotspotHint": "开启热点用于配对", + "ssidLabel": "名称", + "passwordLabel": "密码", + "locationPermissionRequired": "开启热点需要定位权限。", "mobileData": "移动数据", "mobileDataHint": "切换移动数据(可能受系统限制)", "networkToggleFailed": "操作失败,正在打开系统设置…", diff --git a/Kiosk/lib/screens/settings_screen.dart b/Kiosk/lib/screens/settings_screen.dart index 6b1e3d1..f71d159 100644 --- a/Kiosk/lib/screens/settings_screen.dart +++ b/Kiosk/lib/screens/settings_screen.dart @@ -8,6 +8,7 @@ import 'package:kiosk/services/android_launcher_service.dart'; import 'package:kiosk/services/android_network_service.dart'; import 'package:kiosk/services/settings_service.dart'; import 'package:kiosk/services/restore_notifier.dart'; +import 'package:permission_handler/permission_handler.dart'; class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @@ -30,6 +31,8 @@ class _SettingsScreenState extends State { String? _homeAppLabel; bool _hotspotEnabled = false; bool _mobileDataEnabled = false; + String? _hotspotSsid; + String? _hotspotPassword; bool _networkBusy = false; @override @@ -272,11 +275,13 @@ class _SettingsScreenState extends State { Future _loadNetworkState() async { try { - final hotspot = await _networkService.getHotspotEnabled(); + final hotspotInfo = await _networkService.getHotspotInfo(); final mobile = await _networkService.getMobileDataEnabled(); if (!mounted) return; setState(() { - _hotspotEnabled = hotspot; + _hotspotEnabled = hotspotInfo.enabled; + _hotspotSsid = hotspotInfo.ssid; + _hotspotPassword = hotspotInfo.password; _mobileDataEnabled = mobile; }); } catch (_) { @@ -285,6 +290,20 @@ class _SettingsScreenState extends State { Future _setHotspot(bool enabled) async { final l10n = AppLocalizations.of(context)!; + if (enabled) { + final status = await Permission.location.status; + if (!status.isGranted) { + final next = await Permission.location.request(); + if (!next.isGranted) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.locationPermissionRequired)), + ); + } + return; + } + } + } setState(() => _networkBusy = true); try { final ok = await _networkService.setHotspotEnabled(enabled); @@ -343,6 +362,9 @@ class _SettingsScreenState extends State { Widget _buildNetworkSection() { final l10n = AppLocalizations.of(context)!; + final hotspotSubtitle = _hotspotEnabled && _hotspotSsid != null + ? '${l10n.hotspotHint}\n${l10n.ssidLabel}: ${_hotspotSsid ?? '-'}\n${l10n.passwordLabel}: ${_hotspotPassword ?? '-'}' + : l10n.hotspotHint; return Card( child: Column( mainAxisSize: MainAxisSize.min, @@ -356,7 +378,7 @@ class _SettingsScreenState extends State { value: _hotspotEnabled, onChanged: _networkBusy ? null : _setHotspot, title: Text(l10n.hotspot), - subtitle: Text(l10n.hotspotHint), + subtitle: Text(hotspotSubtitle), ), const Divider(height: 1), SwitchListTile( diff --git a/Kiosk/lib/services/android_network_service.dart b/Kiosk/lib/services/android_network_service.dart index 8ffc483..d94c668 100644 --- a/Kiosk/lib/services/android_network_service.dart +++ b/Kiosk/lib/services/android_network_service.dart @@ -1,5 +1,25 @@ import 'package:flutter/services.dart'; +class HotspotInfo { + final bool enabled; + final String? ssid; + final String? password; + + const HotspotInfo({ + required this.enabled, + required this.ssid, + required this.password, + }); + + factory HotspotInfo.fromMap(Map map) { + return HotspotInfo( + enabled: map['enabled'] == true, + ssid: map['ssid']?.toString(), + password: map['password']?.toString(), + ); + } +} + class AndroidNetworkService { static const MethodChannel _channel = MethodChannel('com.secgo.kiosk/network'); @@ -8,6 +28,12 @@ class AndroidNetworkService { return enabled ?? false; } + Future getHotspotInfo() async { + final info = await _channel.invokeMethod('getHotspotInfo'); + if (info is Map) return HotspotInfo.fromMap(info); + return const HotspotInfo(enabled: false, ssid: null, password: null); + } + Future setHotspotEnabled(bool enabled) async { final ok = await _channel.invokeMethod( 'setHotspotEnabled', @@ -37,4 +63,3 @@ class AndroidNetworkService { await _channel.invokeMethod('openInternetSettings'); } } - From 352ac3efbba1099c6e501bc8af9ac5d048af4baf Mon Sep 17 00:00:00 2001 From: TachibanaLolo Date: Sun, 25 Jan 2026 05:11:17 +0800 Subject: [PATCH 3/5] fix(kiosk): report hotspot failure reasons --- .../kotlin/com/secgo/kiosk/MainActivity.kt | 54 +++++++++++++++++++ Kiosk/lib/l10n/app_en.arb | 2 + Kiosk/lib/l10n/app_localizations.dart | 12 +++++ Kiosk/lib/l10n/app_localizations_en.dart | 9 ++++ Kiosk/lib/l10n/app_localizations_zh.dart | 8 +++ Kiosk/lib/l10n/app_zh.arb | 2 + Kiosk/lib/screens/settings_screen.dart | 17 +++++- .../lib/services/android_network_service.dart | 12 +++++ 8 files changed, 115 insertions(+), 1 deletion(-) 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 684e73f..925c426 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 @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager +import android.location.LocationManager import android.os.Build import android.os.Handler import android.os.Looper @@ -23,6 +24,8 @@ class MainActivity : FlutterActivity() { private var localHotspotReservation: WifiManager.LocalOnlyHotspotReservation? = null private var localHotspotSsid: String? = null private var localHotspotPassword: String? = null + private var lastHotspotErrorCode: Int? = null + private var lastHotspotErrorMessage: String? = null override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) @@ -119,8 +122,18 @@ class MainActivity : FlutterActivity() { ), ) } + "getHotspotLastError" -> { + result.success( + mapOf( + "code" to lastHotspotErrorCode, + "message" to lastHotspotErrorMessage, + ), + ) + } "setHotspotEnabled" -> { val enabled = call.argument("enabled") == true + lastHotspotErrorCode = null + lastHotspotErrorMessage = null if (!enabled) { localHotspotReservation?.close() localHotspotReservation = null @@ -136,12 +149,35 @@ class MainActivity : FlutterActivity() { } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + lastHotspotErrorMessage = "unsupported_sdk" result.success(false) return@setMethodCallHandler } try { val wifiManager = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + if (!wifiManager.isWifiEnabled) { + try { + @Suppress("DEPRECATION") + wifiManager.isWifiEnabled = true + } catch (_: Exception) { + } + } + + val locationManager = applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager + val locationEnabled = + try { + locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || + locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + } catch (_: Exception) { + false + } + if (!locationEnabled) { + lastHotspotErrorMessage = "location_disabled" + result.success(false) + return@setMethodCallHandler + } + wifiManager.startLocalOnlyHotspot( object : WifiManager.LocalOnlyHotspotCallback() { override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation) { @@ -164,12 +200,21 @@ class MainActivity : FlutterActivity() { localHotspotReservation = null localHotspotSsid = null localHotspotPassword = null + lastHotspotErrorCode = reason + lastHotspotErrorMessage = + when (reason) { + WifiManager.LocalOnlyHotspotCallback.ERROR_NO_CHANNEL -> "no_channel" + WifiManager.LocalOnlyHotspotCallback.ERROR_TETHERING_DISALLOWED -> "tethering_disallowed" + WifiManager.LocalOnlyHotspotCallback.ERROR_INCOMPATIBLE_MODE -> "incompatible_mode" + else -> "failed_$reason" + } result.success(false) } }, Handler(Looper.getMainLooper()), ) } catch (_: Exception) { + lastHotspotErrorMessage = "exception" result.success(false) } } @@ -181,6 +226,13 @@ class MainActivity : FlutterActivity() { } result.success(true) } + "openLocationSettings" -> { + try { + startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + } catch (_: Exception) { + } + result.success(true) + } "getMobileDataEnabled" -> { val enabled = try { Settings.Global.getInt(contentResolver, "mobile_data", 0) == 1 @@ -285,6 +337,8 @@ class MainActivity : FlutterActivity() { localHotspotReservation = null localHotspotSsid = null localHotspotPassword = null + lastHotspotErrorCode = null + lastHotspotErrorMessage = null super.onDestroy() } diff --git a/Kiosk/lib/l10n/app_en.arb b/Kiosk/lib/l10n/app_en.arb index 03848ee..7401c14 100644 --- a/Kiosk/lib/l10n/app_en.arb +++ b/Kiosk/lib/l10n/app_en.arb @@ -76,6 +76,8 @@ "ssidLabel": "SSID", "passwordLabel": "Password", "locationPermissionRequired": "Location permission is required to enable hotspot.", + "locationServiceRequired": "Please turn on Location service to enable hotspot.", + "hotspotFailedWithReason": "Hotspot failed: {reason}", "mobileData": "Mobile data", "mobileDataHint": "Toggle mobile data (may be restricted)", "networkToggleFailed": "Action failed. Opening system settings…", diff --git a/Kiosk/lib/l10n/app_localizations.dart b/Kiosk/lib/l10n/app_localizations.dart index 7df02a7..340ec35 100644 --- a/Kiosk/lib/l10n/app_localizations.dart +++ b/Kiosk/lib/l10n/app_localizations.dart @@ -560,6 +560,18 @@ abstract class AppLocalizations { /// **'Location permission is required to enable hotspot.'** String get locationPermissionRequired; + /// No description provided for @locationServiceRequired. + /// + /// In en, this message translates to: + /// **'Please turn on Location service to enable hotspot.'** + String get locationServiceRequired; + + /// No description provided for @hotspotFailedWithReason. + /// + /// In en, this message translates to: + /// **'Hotspot failed: {reason}'** + String hotspotFailedWithReason(Object reason); + /// No description provided for @mobileData. /// /// 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 6df0def..99b9669 100644 --- a/Kiosk/lib/l10n/app_localizations_en.dart +++ b/Kiosk/lib/l10n/app_localizations_en.dart @@ -266,6 +266,15 @@ class AppLocalizationsEn extends AppLocalizations { String get locationPermissionRequired => 'Location permission is required to enable hotspot.'; + @override + String get locationServiceRequired => + 'Please turn on Location service to enable hotspot.'; + + @override + String hotspotFailedWithReason(Object reason) { + return 'Hotspot failed: $reason'; + } + @override String get mobileData => 'Mobile data'; diff --git a/Kiosk/lib/l10n/app_localizations_zh.dart b/Kiosk/lib/l10n/app_localizations_zh.dart index 9a5cae9..5c752f0 100644 --- a/Kiosk/lib/l10n/app_localizations_zh.dart +++ b/Kiosk/lib/l10n/app_localizations_zh.dart @@ -261,6 +261,14 @@ class AppLocalizationsZh extends AppLocalizations { @override String get locationPermissionRequired => '开启热点需要定位权限。'; + @override + String get locationServiceRequired => '请开启定位服务后再打开热点。'; + + @override + String hotspotFailedWithReason(Object reason) { + return '热点开启失败:$reason'; + } + @override String get mobileData => '移动数据'; diff --git a/Kiosk/lib/l10n/app_zh.arb b/Kiosk/lib/l10n/app_zh.arb index d8a0075..ceadbad 100644 --- a/Kiosk/lib/l10n/app_zh.arb +++ b/Kiosk/lib/l10n/app_zh.arb @@ -76,6 +76,8 @@ "ssidLabel": "名称", "passwordLabel": "密码", "locationPermissionRequired": "开启热点需要定位权限。", + "locationServiceRequired": "请开启定位服务后再打开热点。", + "hotspotFailedWithReason": "热点开启失败:{reason}", "mobileData": "移动数据", "mobileDataHint": "切换移动数据(可能受系统限制)", "networkToggleFailed": "操作失败,正在打开系统设置…", diff --git a/Kiosk/lib/screens/settings_screen.dart b/Kiosk/lib/screens/settings_screen.dart index f71d159..d49a608 100644 --- a/Kiosk/lib/screens/settings_screen.dart +++ b/Kiosk/lib/screens/settings_screen.dart @@ -308,10 +308,25 @@ class _SettingsScreenState extends State { try { final ok = await _networkService.setHotspotEnabled(enabled); if (!ok) { + final err = await _networkService.getHotspotLastError(); + final message = (err['message'] ?? '').toString(); + if (message == 'location_disabled') { + await _networkService.openLocationSettings(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.locationServiceRequired)), + ); + } + return; + } await _networkService.openHotspotSettings(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.networkToggleFailed)), + SnackBar( + content: Text( + message.isEmpty ? l10n.networkToggleFailed : l10n.hotspotFailedWithReason(message), + ), + ), ); } } diff --git a/Kiosk/lib/services/android_network_service.dart b/Kiosk/lib/services/android_network_service.dart index d94c668..4a9696b 100644 --- a/Kiosk/lib/services/android_network_service.dart +++ b/Kiosk/lib/services/android_network_service.dart @@ -46,6 +46,18 @@ class AndroidNetworkService { await _channel.invokeMethod('openHotspotSettings'); } + Future> getHotspotLastError() async { + final raw = await _channel.invokeMethod('getHotspotLastError'); + if (raw is Map) { + return raw.map((k, v) => MapEntry(k.toString(), v)); + } + return const {}; + } + + Future openLocationSettings() async { + await _channel.invokeMethod('openLocationSettings'); + } + Future getMobileDataEnabled() async { final enabled = await _channel.invokeMethod('getMobileDataEnabled'); return enabled ?? false; From d3ee31477e9fa59ceb9713c93951569d616f71d0 Mon Sep 17 00:00:00 2001 From: TachibanaLolo Date: Sun, 25 Jan 2026 05:16:52 +0800 Subject: [PATCH 4/5] fix(kiosk): detect system hotspot already enabled --- .../kotlin/com/secgo/kiosk/MainActivity.kt | 33 ++++++++++++++++--- Kiosk/lib/l10n/app_en.arb | 2 ++ Kiosk/lib/l10n/app_localizations.dart | 12 +++++++ Kiosk/lib/l10n/app_localizations_en.dart | 8 +++++ Kiosk/lib/l10n/app_localizations_zh.dart | 6 ++++ Kiosk/lib/l10n/app_zh.arb | 2 ++ Kiosk/lib/screens/settings_screen.dart | 19 +++++++++-- .../lib/services/android_network_service.dart | 5 ++- 8 files changed, 79 insertions(+), 8 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 925c426..25a9a36 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 @@ -112,13 +112,15 @@ class MainActivity : FlutterActivity() { MethodChannel(flutterEngine.dartExecutor.binaryMessenger, NETWORK_CHANNEL).setMethodCallHandler { call, result -> when (call.method) { - "getHotspotEnabled" -> result.success(localHotspotReservation != null) + "getHotspotEnabled" -> result.success(localHotspotReservation != null || isSystemHotspotEnabled()) "getHotspotInfo" -> { + val systemEnabled = isSystemHotspotEnabled() result.success( mapOf( - "enabled" to (localHotspotReservation != null), - "ssid" to localHotspotSsid, - "password" to localHotspotPassword, + "enabled" to (localHotspotReservation != null || systemEnabled), + "mode" to (if (localHotspotReservation != null) "local" else if (systemEnabled) "system" else null), + "ssid" to (if (localHotspotReservation != null) localHotspotSsid else null), + "password" to (if (localHotspotReservation != null) localHotspotPassword else null), ), ) } @@ -143,6 +145,11 @@ class MainActivity : FlutterActivity() { return@setMethodCallHandler } + if (isSystemHotspotEnabled()) { + result.success(true) + return@setMethodCallHandler + } + if (localHotspotReservation != null) { result.success(true) return@setMethodCallHandler @@ -347,6 +354,24 @@ class MainActivity : FlutterActivity() { return enabled.contains(packageName) } + private fun isSystemHotspotEnabled(): Boolean { + return try { + val wifiManager = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + try { + val m = wifiManager.javaClass.getDeclaredMethod("isWifiApEnabled") + m.isAccessible = true + (m.invoke(wifiManager) as? Boolean) == true + } catch (_: Exception) { + val m = wifiManager.javaClass.getDeclaredMethod("getWifiApState") + m.isAccessible = true + val state = (m.invoke(wifiManager) as? Int) ?: return false + state == 13 || state == 12 + } + } catch (_: Exception) { + false + } + } + private fun getAlipayNotificationState(): Map { val prefs = getSharedPreferences(SecgoNotificationListenerService.PREFS_NAME, Context.MODE_PRIVATE) val hasAlipay = prefs.getBoolean(SecgoNotificationListenerService.KEY_HAS_ALIPAY, false) diff --git a/Kiosk/lib/l10n/app_en.arb b/Kiosk/lib/l10n/app_en.arb index 7401c14..9718f51 100644 --- a/Kiosk/lib/l10n/app_en.arb +++ b/Kiosk/lib/l10n/app_en.arb @@ -78,6 +78,8 @@ "locationPermissionRequired": "Location permission is required to enable hotspot.", "locationServiceRequired": "Please turn on Location service to enable hotspot.", "hotspotFailedWithReason": "Hotspot failed: {reason}", + "hotspotEnabledInSystemSettings": "Hotspot is already enabled in system settings.", + "hotspotChangeInSystemSettings": "Change hotspot in system settings.", "mobileData": "Mobile data", "mobileDataHint": "Toggle mobile data (may be restricted)", "networkToggleFailed": "Action failed. Opening system settings…", diff --git a/Kiosk/lib/l10n/app_localizations.dart b/Kiosk/lib/l10n/app_localizations.dart index 340ec35..785b572 100644 --- a/Kiosk/lib/l10n/app_localizations.dart +++ b/Kiosk/lib/l10n/app_localizations.dart @@ -572,6 +572,18 @@ abstract class AppLocalizations { /// **'Hotspot failed: {reason}'** String hotspotFailedWithReason(Object reason); + /// No description provided for @hotspotEnabledInSystemSettings. + /// + /// In en, this message translates to: + /// **'Hotspot is already enabled in system settings.'** + String get hotspotEnabledInSystemSettings; + + /// No description provided for @hotspotChangeInSystemSettings. + /// + /// In en, this message translates to: + /// **'Change hotspot in system settings.'** + String get hotspotChangeInSystemSettings; + /// No description provided for @mobileData. /// /// 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 99b9669..860652e 100644 --- a/Kiosk/lib/l10n/app_localizations_en.dart +++ b/Kiosk/lib/l10n/app_localizations_en.dart @@ -275,6 +275,14 @@ class AppLocalizationsEn extends AppLocalizations { return 'Hotspot failed: $reason'; } + @override + String get hotspotEnabledInSystemSettings => + 'Hotspot is already enabled in system settings.'; + + @override + String get hotspotChangeInSystemSettings => + 'Change hotspot in system settings.'; + @override String get mobileData => 'Mobile data'; diff --git a/Kiosk/lib/l10n/app_localizations_zh.dart b/Kiosk/lib/l10n/app_localizations_zh.dart index 5c752f0..67d617f 100644 --- a/Kiosk/lib/l10n/app_localizations_zh.dart +++ b/Kiosk/lib/l10n/app_localizations_zh.dart @@ -269,6 +269,12 @@ class AppLocalizationsZh extends AppLocalizations { return '热点开启失败:$reason'; } + @override + String get hotspotEnabledInSystemSettings => '系统热点已开启。'; + + @override + String get hotspotChangeInSystemSettings => '请在系统设置中修改热点。'; + @override String get mobileData => '移动数据'; diff --git a/Kiosk/lib/l10n/app_zh.arb b/Kiosk/lib/l10n/app_zh.arb index ceadbad..54b7220 100644 --- a/Kiosk/lib/l10n/app_zh.arb +++ b/Kiosk/lib/l10n/app_zh.arb @@ -78,6 +78,8 @@ "locationPermissionRequired": "开启热点需要定位权限。", "locationServiceRequired": "请开启定位服务后再打开热点。", "hotspotFailedWithReason": "热点开启失败:{reason}", + "hotspotEnabledInSystemSettings": "系统热点已开启。", + "hotspotChangeInSystemSettings": "请在系统设置中修改热点。", "mobileData": "移动数据", "mobileDataHint": "切换移动数据(可能受系统限制)", "networkToggleFailed": "操作失败,正在打开系统设置…", diff --git a/Kiosk/lib/screens/settings_screen.dart b/Kiosk/lib/screens/settings_screen.dart index d49a608..1c8945e 100644 --- a/Kiosk/lib/screens/settings_screen.dart +++ b/Kiosk/lib/screens/settings_screen.dart @@ -33,6 +33,7 @@ class _SettingsScreenState extends State { bool _mobileDataEnabled = false; String? _hotspotSsid; String? _hotspotPassword; + String? _hotspotMode; bool _networkBusy = false; @override @@ -280,6 +281,7 @@ class _SettingsScreenState extends State { if (!mounted) return; setState(() { _hotspotEnabled = hotspotInfo.enabled; + _hotspotMode = hotspotInfo.mode; _hotspotSsid = hotspotInfo.ssid; _hotspotPassword = hotspotInfo.password; _mobileDataEnabled = mobile; @@ -290,6 +292,15 @@ class _SettingsScreenState extends State { Future _setHotspot(bool enabled) async { final l10n = AppLocalizations.of(context)!; + if (_hotspotMode == 'system') { + await _networkService.openHotspotSettings(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.hotspotChangeInSystemSettings)), + ); + } + return; + } if (enabled) { final status = await Permission.location.status; if (!status.isGranted) { @@ -377,9 +388,11 @@ class _SettingsScreenState extends State { Widget _buildNetworkSection() { final l10n = AppLocalizations.of(context)!; - final hotspotSubtitle = _hotspotEnabled && _hotspotSsid != null - ? '${l10n.hotspotHint}\n${l10n.ssidLabel}: ${_hotspotSsid ?? '-'}\n${l10n.passwordLabel}: ${_hotspotPassword ?? '-'}' - : l10n.hotspotHint; + final hotspotSubtitle = _hotspotMode == 'system' + ? '${l10n.hotspotEnabledInSystemSettings}\n${l10n.hotspotChangeInSystemSettings}' + : (_hotspotEnabled && _hotspotSsid != null + ? '${l10n.hotspotHint}\n${l10n.ssidLabel}: ${_hotspotSsid ?? '-'}\n${l10n.passwordLabel}: ${_hotspotPassword ?? '-'}' + : l10n.hotspotHint); return Card( child: Column( mainAxisSize: MainAxisSize.min, diff --git a/Kiosk/lib/services/android_network_service.dart b/Kiosk/lib/services/android_network_service.dart index 4a9696b..1e728c5 100644 --- a/Kiosk/lib/services/android_network_service.dart +++ b/Kiosk/lib/services/android_network_service.dart @@ -2,11 +2,13 @@ import 'package:flutter/services.dart'; class HotspotInfo { final bool enabled; + final String? mode; final String? ssid; final String? password; const HotspotInfo({ required this.enabled, + required this.mode, required this.ssid, required this.password, }); @@ -14,6 +16,7 @@ class HotspotInfo { factory HotspotInfo.fromMap(Map map) { return HotspotInfo( enabled: map['enabled'] == true, + mode: map['mode']?.toString(), ssid: map['ssid']?.toString(), password: map['password']?.toString(), ); @@ -31,7 +34,7 @@ class AndroidNetworkService { Future getHotspotInfo() async { final info = await _channel.invokeMethod('getHotspotInfo'); if (info is Map) return HotspotInfo.fromMap(info); - return const HotspotInfo(enabled: false, ssid: null, password: null); + return const HotspotInfo(enabled: false, mode: null, ssid: null, password: null); } Future setHotspotEnabled(bool enabled) async { From a4890245171907408c35296b1617d4b4d1e51c46 Mon Sep 17 00:00:00 2001 From: TachibanaLolo Date: Sun, 25 Jan 2026 05:29:52 +0800 Subject: [PATCH 5/5] feat(kiosk): refresh network status and show system hotspot info --- .../kotlin/com/secgo/kiosk/MainActivity.kt | 58 ++++++- Kiosk/lib/screens/settings_screen.dart | 149 ++++++++++++++---- Kiosk/lib/services/server/kiosk_server.dart | 115 ++++++++------ .../Flutter/GeneratedPluginRegistrant.swift | 2 + Kiosk/pubspec.lock | 16 ++ Kiosk/pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + Kiosk/windows/flutter/generated_plugins.cmake | 1 + 8 files changed, 258 insertions(+), 87 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 25a9a36..bea91c2 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 @@ -6,6 +6,7 @@ import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager import android.location.LocationManager +import android.net.wifi.WifiConfiguration import android.os.Build import android.os.Handler import android.os.Looper @@ -115,12 +116,23 @@ class MainActivity : FlutterActivity() { "getHotspotEnabled" -> result.success(localHotspotReservation != null || isSystemHotspotEnabled()) "getHotspotInfo" -> { val systemEnabled = isSystemHotspotEnabled() + val systemConfig = if (systemEnabled && localHotspotReservation == null) getSystemHotspotConfig() else null result.success( mapOf( "enabled" to (localHotspotReservation != null || systemEnabled), "mode" to (if (localHotspotReservation != null) "local" else if (systemEnabled) "system" else null), - "ssid" to (if (localHotspotReservation != null) localHotspotSsid else null), - "password" to (if (localHotspotReservation != null) localHotspotPassword else null), + "ssid" to + (if (localHotspotReservation != null) { + localHotspotSsid + } else { + systemConfig?.first + }), + "password" to + (if (localHotspotReservation != null) { + localHotspotPassword + } else { + systemConfig?.second + }), ), ) } @@ -141,8 +153,14 @@ class MainActivity : FlutterActivity() { localHotspotReservation = null localHotspotSsid = null localHotspotPassword = null - result.success(true) - return@setMethodCallHandler + if (isSystemHotspotEnabled()) { + val ok = disableSystemHotspot() + result.success(ok) + return@setMethodCallHandler + } else { + result.success(true) + return@setMethodCallHandler + } } if (isSystemHotspotEnabled()) { @@ -372,6 +390,38 @@ class MainActivity : FlutterActivity() { } } + private fun getSystemHotspotConfig(): kotlin.Pair? { + return try { + val wifiManager = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + val m = wifiManager.javaClass.getDeclaredMethod("getWifiApConfiguration") + m.isAccessible = true + val config = m.invoke(wifiManager) as? WifiConfiguration ?: return null + kotlin.Pair(config.SSID, config.preSharedKey) + } catch (_: Exception) { + null + } + } + + private fun disableSystemHotspot(): Boolean { + return try { + val wifiManager = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + try { + val m = wifiManager.javaClass.getDeclaredMethod("setWifiApEnabled", WifiConfiguration::class.java, Boolean::class.java) + m.isAccessible = true + m.invoke(wifiManager, null, false) + true + } catch (_: Exception) { + val m = wifiManager.javaClass.getDeclaredMethod("stopSoftAp") + m.isAccessible = true + m.invoke(wifiManager) + true + } + } catch (_: Exception) { + lastHotspotErrorMessage = "system_disable_blocked" + false + } + } + private fun getAlipayNotificationState(): Map { val prefs = getSharedPreferences(SecgoNotificationListenerService.PREFS_NAME, Context.MODE_PRIVATE) val hasAlipay = prefs.getBoolean(SecgoNotificationListenerService.KEY_HAS_ALIPAY, false) diff --git a/Kiosk/lib/screens/settings_screen.dart b/Kiosk/lib/screens/settings_screen.dart index 1c8945e..ffbc109 100644 --- a/Kiosk/lib/screens/settings_screen.dart +++ b/Kiosk/lib/screens/settings_screen.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:kiosk/services/server/kiosk_server.dart'; import 'package:qr_flutter/qr_flutter.dart'; @@ -9,6 +10,7 @@ import 'package:kiosk/services/android_network_service.dart'; import 'package:kiosk/services/settings_service.dart'; import 'package:kiosk/services/restore_notifier.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @@ -17,7 +19,7 @@ class SettingsScreen extends StatefulWidget { State createState() => _SettingsScreenState(); } -class _SettingsScreenState extends State { +class _SettingsScreenState extends State with WidgetsBindingObserver { late final KioskServerService _serverService; final TextEditingController _pinController = TextEditingController(); final SettingsService _settingsService = SettingsService(); // Add SettingsService @@ -27,6 +29,7 @@ class _SettingsScreenState extends State { bool _isLoading = false; bool _showRestoreComplete = false; String? _qrData; + String? _deviceId; String? _homeAppPackage; String? _homeAppLabel; bool _hotspotEnabled = false; @@ -35,10 +38,13 @@ class _SettingsScreenState extends State { String? _hotspotPassword; String? _hotspotMode; bool _networkBusy = false; + StreamSubscription>? _connectivitySub; + Timer? _networkDebounce; @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); _serverService = KioskServerService(onRestoreComplete: _onRestoreComplete); _homeAppPackage = _settingsService.getHomeAppPackage(); _homeAppLabel = _settingsService.getHomeAppLabel(); @@ -54,15 +60,29 @@ class _SettingsScreenState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { _loadNetworkState(); }); + + _connectivitySub = Connectivity().onConnectivityChanged.listen((_) { + _onNetworkChanged(); + }); } @override void dispose() { + _networkDebounce?.cancel(); + _connectivitySub?.cancel(); + WidgetsBinding.instance.removeObserver(this); _serverService.stopServer(); _pinController.dispose(); super.dispose(); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + _onNetworkChanged(); + } + } + Future _confirmPin() async { final l10n = AppLocalizations.of(context)!; final expected = _settingsService.getPin() ?? _pinController.text.trim(); @@ -290,14 +310,80 @@ class _SettingsScreenState extends State { } } + void _onNetworkChanged() { + _networkDebounce?.cancel(); + _networkDebounce = Timer(const Duration(milliseconds: 600), () async { + if (!mounted) return; + await _loadNetworkState(); + await _refreshServerStatus(); + }); + } + + void _updateQrData() { + final ip = _serverService.ipAddress; + final pin = _pinController.text; + if (ip == null || pin.length < 4) { + _qrData = null; + return; + } + _qrData = jsonEncode({ + 'ip': ip, + 'port': _serverService.port, + 'pin': pin, + 'deviceId': _deviceId, + }); + } + + Future _refreshServerStatus() async { + if (_isLoading) return; + if (_serverService.isRunning) { + await _serverService.refreshIpAddress(); + if (!mounted) return; + setState(() { + _isServerRunning = _serverService.ipAddress != null; + _updateQrData(); + }); + return; + } + + await _serverService.refreshIpAddress(); + if (!mounted) return; + if (_serverService.ipAddress == null) { + setState(() { + _isServerRunning = false; + _qrData = null; + }); + return; + } + + if (_pinController.text.length >= 4) { + await _startServer(silent: true); + } + } + Future _setHotspot(bool enabled) async { final l10n = AppLocalizations.of(context)!; if (_hotspotMode == 'system') { - await _networkService.openHotspotSettings(); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.hotspotChangeInSystemSettings)), - ); + setState(() => _networkBusy = true); + try { + final ok = await _networkService.setHotspotEnabled(enabled); + if (!ok) { + await _networkService.openHotspotSettings(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.hotspotChangeInSystemSettings)), + ); + } + } + } catch (_) { + try { + await _networkService.openHotspotSettings(); + } catch (_) { + } + } finally { + await _loadNetworkState(); + await _refreshServerStatus(); + if (mounted) setState(() => _networkBusy = false); } return; } @@ -353,6 +439,7 @@ class _SettingsScreenState extends State { } } finally { await _loadNetworkState(); + await _refreshServerStatus(); if (mounted) setState(() => _networkBusy = false); } } @@ -382,6 +469,7 @@ class _SettingsScreenState extends State { } } finally { await _loadNetworkState(); + await _refreshServerStatus(); if (mounted) setState(() => _networkBusy = false); } } @@ -389,7 +477,9 @@ class _SettingsScreenState extends State { Widget _buildNetworkSection() { final l10n = AppLocalizations.of(context)!; final hotspotSubtitle = _hotspotMode == 'system' - ? '${l10n.hotspotEnabledInSystemSettings}\n${l10n.hotspotChangeInSystemSettings}' + ? (_hotspotSsid != null + ? '${l10n.hotspotEnabledInSystemSettings}\n${l10n.ssidLabel}: ${_hotspotSsid ?? '-'}\n${l10n.passwordLabel}: ${_hotspotPassword ?? '-'}' + : '${l10n.hotspotEnabledInSystemSettings}\n${l10n.hotspotChangeInSystemSettings}') : (_hotspotEnabled && _hotspotSsid != null ? '${l10n.hotspotHint}\n${l10n.ssidLabel}: ${_hotspotSsid ?? '-'}\n${l10n.passwordLabel}: ${_hotspotPassword ?? '-'}' : l10n.hotspotHint); @@ -420,41 +510,38 @@ class _SettingsScreenState extends State { ); } - Future _startServer() async { + Future _startServer({bool silent = false}) async { final l10n = AppLocalizations.of(context)!; + if (_isLoading) return; if (_pinController.text.length < 4) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.pinLength)), - ); + if (!silent) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.pinLength)), + ); + } return; } - setState(() { - _isLoading = true; - }); + if (!silent) { + setState(() { + _isLoading = true; + }); + } - final deviceId = await _settingsService.getOrCreateDeviceId(); - await _serverService.startServer(_pinController.text, deviceId: deviceId); + _deviceId ??= await _settingsService.getOrCreateDeviceId(); + await _serverService.startServer(_pinController.text, deviceId: _deviceId); if (mounted) { setState(() { _isLoading = false; - if (_serverService.ipAddress != null) { - _isServerRunning = true; - // QR Data format: {"ip": "192.168.x.x", "port": 8081, "pin": "1234"} - _qrData = jsonEncode({ - 'ip': _serverService.ipAddress, - 'port': _serverService.port, - 'pin': _pinController.text, - 'deviceId': deviceId, - }); - } else { - // Optional: Show error if IP is null (e.g. no wifi) - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.serverNoIp)), - ); - } + _isServerRunning = _serverService.ipAddress != null; + _updateQrData(); }); + if (!silent && _serverService.ipAddress == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.serverNoIp)), + ); + } } } diff --git a/Kiosk/lib/services/server/kiosk_server.dart b/Kiosk/lib/services/server/kiosk_server.dart index d14abf6..7563794 100644 --- a/Kiosk/lib/services/server/kiosk_server.dart +++ b/Kiosk/lib/services/server/kiosk_server.dart @@ -29,65 +29,18 @@ class KioskServerService { String? get ipAddress => _ipAddress; int get port => _port; + bool get isRunning => _server != null; Future startServer(String pin, {String? deviceId}) async { _pin = pin; _deviceId = deviceId; - - // Iterate interfaces to find a valid non-loopback IPv4 address - // Priority: Hotspot (ap0, tethering) -> Wi-Fi (wlan0) -> Exclude LTE (rmnet) - try { - final interfaces = await NetworkInterface.list( - includeLoopback: false, - type: InternetAddressType.IPv4, - ); - - NetworkInterface? selectedInterface; - - // 1. Prioritize Hotspot/Tethering interface (ap, tether, wlan1 usually) - try { - selectedInterface = interfaces.firstWhere( - (i) => i.name.toLowerCase().contains('ap') || - i.name.toLowerCase().contains('tether') - ); - } catch (_) {} - - // 2. If no Hotspot, try to find Wi-Fi interface (wlan) - if (selectedInterface == null) { - try { - selectedInterface = interfaces.firstWhere((i) => i.name.toLowerCase().contains('wlan')); - } catch (_) {} - } - // 3. If still nothing, look for any interface that IS NOT mobile data (rmnet, ccmni, pdp) - if (selectedInterface == null) { - try { - selectedInterface = interfaces.firstWhere( - (i) => !i.name.toLowerCase().contains('rmnet') && - !i.name.toLowerCase().contains('ccmni') && - !i.name.toLowerCase().contains('pdp') - ); - } catch (_) {} - } - - if (selectedInterface != null) { - debugPrint('Selected interface: ${selectedInterface.name}'); - for (var addr in selectedInterface.addresses) { - if (!addr.isLoopback) { - _ipAddress = addr.address; - break; - } - } - } - } catch (e) { - debugPrint('Error listing network interfaces: $e'); + if (_server != null) { + await refreshIpAddress(); + return; } - // Fallback to NetworkInfo if manual lookup fails (though manual is more robust for Hotspot/LTE) - if (_ipAddress == null) { - final info = NetworkInfo(); - _ipAddress = await info.getWifiIP(); - } + _ipAddress = await _resolveIpAddress(); if (_ipAddress == null) { debugPrint('Could not get IP address. Server not started.'); @@ -373,4 +326,62 @@ class KioskServerService { await _server?.close(); _server = null; } + + Future refreshIpAddress() async { + _ipAddress = await _resolveIpAddress(); + } + + Future _resolveIpAddress() async { + String? ip; + try { + final interfaces = await NetworkInterface.list( + includeLoopback: false, + type: InternetAddressType.IPv4, + ); + + NetworkInterface? selectedInterface; + + try { + selectedInterface = interfaces.firstWhere( + (i) => i.name.toLowerCase().contains('ap') || i.name.toLowerCase().contains('tether'), + ); + } catch (_) {} + + if (selectedInterface == null) { + try { + selectedInterface = interfaces.firstWhere((i) => i.name.toLowerCase().contains('wlan')); + } catch (_) {} + } + + if (selectedInterface == null) { + try { + selectedInterface = interfaces.firstWhere( + (i) => + !i.name.toLowerCase().contains('rmnet') && + !i.name.toLowerCase().contains('ccmni') && + !i.name.toLowerCase().contains('pdp'), + ); + } catch (_) {} + } + + if (selectedInterface != null) { + debugPrint('Selected interface: ${selectedInterface.name}'); + for (var addr in selectedInterface.addresses) { + if (!addr.isLoopback) { + ip = addr.address; + break; + } + } + } + } catch (e) { + debugPrint('Error listing network interfaces: $e'); + } + + if (ip == null) { + final info = NetworkInfo(); + ip = await info.getWifiIP(); + } + + return ip; + } } diff --git a/Kiosk/macos/Flutter/GeneratedPluginRegistrant.swift b/Kiosk/macos/Flutter/GeneratedPluginRegistrant.swift index 535949b..6d0fe86 100644 --- a/Kiosk/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/Kiosk/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import connectivity_plus import mobile_scanner import network_info_plus import package_info_plus @@ -14,6 +15,7 @@ import sqflite_darwin import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) diff --git a/Kiosk/pubspec.lock b/Kiosk/pubspec.lock index f3c4f0d..7c92d1e 100644 --- a/Kiosk/pubspec.lock +++ b/Kiosk/pubspec.lock @@ -145,6 +145,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec + url: "https://pub.dev" + source: hosted + version: "6.1.5" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + url: "https://pub.dev" + source: hosted + version: "2.0.1" convert: dependency: transitive description: diff --git a/Kiosk/pubspec.yaml b/Kiosk/pubspec.yaml index fbf2d5b..381b7f9 100644 --- a/Kiosk/pubspec.yaml +++ b/Kiosk/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: http: ^1.6.0 intl: ^0.20.2 mobile_scanner: ^7.1.4 + connectivity_plus: ^6.1.4 network_info_plus: ^7.0.0 path: ^1.9.1 permission_handler: ^12.0.1 diff --git a/Kiosk/windows/flutter/generated_plugin_registrant.cc b/Kiosk/windows/flutter/generated_plugin_registrant.cc index 48de52b..1523d31 100644 --- a/Kiosk/windows/flutter/generated_plugin_registrant.cc +++ b/Kiosk/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,12 @@ #include "generated_plugin_registrant.h" +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); } diff --git a/Kiosk/windows/flutter/generated_plugins.cmake b/Kiosk/windows/flutter/generated_plugins.cmake index 0e69e40..a103c74 100644 --- a/Kiosk/windows/flutter/generated_plugins.cmake +++ b/Kiosk/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + connectivity_plus permission_handler_windows )