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 || 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 {
+ systemConfig?.first
+ }),
+ "password" to
+ (if (localHotspotReservation != null) {
+ localHotspotPassword
+ } else {
+ systemConfig?.second
+ }),
+ ),
+ )
+ }
+ "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
+ localHotspotSsid = null
+ localHotspotPassword = null
+ if (isSystemHotspotEnabled()) {
+ val ok = disableSystemHotspot()
+ result.success(ok)
+ return@setMethodCallHandler
+ } else {
+ result.success(true)
+ return@setMethodCallHandler
+ }
+ }
+
+ if (isSystemHotspotEnabled()) {
+ result.success(true)
+ return@setMethodCallHandler
+ }
+
+ if (localHotspotReservation != null) {
+ result.success(true)
+ return@setMethodCallHandler
+ }
+
+ 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) {
+ 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
+ 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)
+ }
+ }
+ "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)
+ }
+ "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
+ } 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 +358,12 @@ class MainActivity : FlutterActivity() {
}
}
receiver = null
+ localHotspotReservation?.close()
+ localHotspotReservation = null
+ localHotspotSsid = null
+ localHotspotPassword = null
+ lastHotspotErrorCode = null
+ lastHotspotErrorMessage = null
super.onDestroy()
}
@@ -177,6 +372,56 @@ 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 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)
@@ -271,6 +516,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..9718f51 100644
--- a/Kiosk/lib/l10n/app_en.arb
+++ b/Kiosk/lib/l10n/app_en.arb
@@ -70,6 +70,19 @@
"categoryAll": "All",
"categoryUncategorized": "Uncategorized",
"searchProducts": "Search products",
+ "networkSettings": "Network",
+ "hotspot": "Hotspot",
+ "hotspotHint": "Enable hotspot for pairing",
+ "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}",
+ "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…",
"clear": "Clear",
"save": "Save"
}
diff --git a/Kiosk/lib/l10n/app_localizations.dart b/Kiosk/lib/l10n/app_localizations.dart
index 68031a9..785b572 100644
--- a/Kiosk/lib/l10n/app_localizations.dart
+++ b/Kiosk/lib/l10n/app_localizations.dart
@@ -524,6 +524,84 @@ 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 @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 @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 @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:
+ /// **'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..860652e 100644
--- a/Kiosk/lib/l10n/app_localizations_en.dart
+++ b/Kiosk/lib/l10n/app_localizations_en.dart
@@ -247,6 +247,51 @@ 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 ssidLabel => 'SSID';
+
+ @override
+ String get passwordLabel => 'Password';
+
+ @override
+ 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 hotspotEnabledInSystemSettings =>
+ 'Hotspot is already enabled in system settings.';
+
+ @override
+ String get hotspotChangeInSystemSettings =>
+ 'Change hotspot in system settings.';
+
+ @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..67d617f 100644
--- a/Kiosk/lib/l10n/app_localizations_zh.dart
+++ b/Kiosk/lib/l10n/app_localizations_zh.dart
@@ -243,6 +243,47 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get searchProducts => '搜索商品';
+ @override
+ String get networkSettings => '网络设置';
+
+ @override
+ String get hotspot => '热点';
+
+ @override
+ String get hotspotHint => '开启热点用于配对';
+
+ @override
+ String get ssidLabel => '名称';
+
+ @override
+ String get passwordLabel => '密码';
+
+ @override
+ String get locationPermissionRequired => '开启热点需要定位权限。';
+
+ @override
+ String get locationServiceRequired => '请开启定位服务后再打开热点。';
+
+ @override
+ String hotspotFailedWithReason(Object reason) {
+ return '热点开启失败:$reason';
+ }
+
+ @override
+ String get hotspotEnabledInSystemSettings => '系统热点已开启。';
+
+ @override
+ String get hotspotChangeInSystemSettings => '请在系统设置中修改热点。';
+
+ @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..54b7220 100644
--- a/Kiosk/lib/l10n/app_zh.arb
+++ b/Kiosk/lib/l10n/app_zh.arb
@@ -70,6 +70,19 @@
"categoryAll": "全部",
"categoryUncategorized": "未分类",
"searchProducts": "搜索商品",
+ "networkSettings": "网络设置",
+ "hotspot": "热点",
+ "hotspotHint": "开启热点用于配对",
+ "ssidLabel": "名称",
+ "passwordLabel": "密码",
+ "locationPermissionRequired": "开启热点需要定位权限。",
+ "locationServiceRequired": "请开启定位服务后再打开热点。",
+ "hotspotFailedWithReason": "热点开启失败:{reason}",
+ "hotspotEnabledInSystemSettings": "系统热点已开启。",
+ "hotspotChangeInSystemSettings": "请在系统设置中修改热点。",
+ "mobileData": "移动数据",
+ "mobileDataHint": "切换移动数据(可能受系统限制)",
+ "networkToggleFailed": "操作失败,正在打开系统设置…",
"clear": "清除",
"save": "保存"
}
diff --git a/Kiosk/lib/screens/settings_screen.dart b/Kiosk/lib/screens/settings_screen.dart
index a051813..ffbc109 100644
--- a/Kiosk/lib/screens/settings_screen.dart
+++ b/Kiosk/lib/screens/settings_screen.dart
@@ -1,12 +1,16 @@
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';
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';
+import 'package:permission_handler/permission_handler.dart';
+import 'package:connectivity_plus/connectivity_plus.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@@ -15,21 +19,32 @@ 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
final AndroidLauncherService _launcherService = AndroidLauncherService();
+ final AndroidNetworkService _networkService = AndroidNetworkService();
bool _isServerRunning = false;
bool _isLoading = false;
bool _showRestoreComplete = false;
String? _qrData;
+ String? _deviceId;
String? _homeAppPackage;
String? _homeAppLabel;
+ bool _hotspotEnabled = false;
+ bool _mobileDataEnabled = false;
+ String? _hotspotSsid;
+ 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();
@@ -42,15 +57,32 @@ class _SettingsScreenState extends State {
_startServer();
});
}
+ 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();
@@ -262,41 +294,254 @@ class _SettingsScreenState extends State {
setState(() => _showRestoreComplete = false);
}
- Future _startServer() async {
+ Future _loadNetworkState() async {
+ try {
+ final hotspotInfo = await _networkService.getHotspotInfo();
+ final mobile = await _networkService.getMobileDataEnabled();
+ if (!mounted) return;
+ setState(() {
+ _hotspotEnabled = hotspotInfo.enabled;
+ _hotspotMode = hotspotInfo.mode;
+ _hotspotSsid = hotspotInfo.ssid;
+ _hotspotPassword = hotspotInfo.password;
+ _mobileDataEnabled = mobile;
+ });
+ } catch (_) {
+ }
+ }
+
+ 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') {
+ 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;
+ }
+ 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);
+ 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(
+ message.isEmpty ? l10n.networkToggleFailed : l10n.hotspotFailedWithReason(message),
+ ),
+ ),
+ );
+ }
+ }
+ } catch (_) {
+ try {
+ await _networkService.openHotspotSettings();
+ } catch (_) {
+ }
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(l10n.networkToggleFailed)),
+ );
+ }
+ } finally {
+ await _loadNetworkState();
+ await _refreshServerStatus();
+ 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();
+ await _refreshServerStatus();
+ if (mounted) setState(() => _networkBusy = false);
+ }
+ }
+
+ Widget _buildNetworkSection() {
final l10n = AppLocalizations.of(context)!;
+ final hotspotSubtitle = _hotspotMode == 'system'
+ ? (_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);
+ 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(hotspotSubtitle),
+ ),
+ const Divider(height: 1),
+ SwitchListTile(
+ value: _mobileDataEnabled,
+ onChanged: _networkBusy ? null : _setMobileData,
+ title: Text(l10n.mobileData),
+ subtitle: Text(l10n.mobileDataHint),
+ ),
+ ],
+ ),
+ );
+ }
+
+ 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)),
+ );
+ }
}
}
@@ -313,9 +558,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 +586,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 +603,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 +624,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..1e728c5
--- /dev/null
+++ b/Kiosk/lib/services/android_network_service.dart
@@ -0,0 +1,80 @@
+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,
+ });
+
+ factory HotspotInfo.fromMap(Map map) {
+ return HotspotInfo(
+ enabled: map['enabled'] == true,
+ mode: map['mode']?.toString(),
+ ssid: map['ssid']?.toString(),
+ password: map['password']?.toString(),
+ );
+ }
+}
+
+class AndroidNetworkService {
+ static const MethodChannel _channel = MethodChannel('com.secgo.kiosk/network');
+
+ Future getHotspotEnabled() async {
+ final enabled = await _channel.invokeMethod('getHotspotEnabled');
+ 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, mode: null, ssid: null, password: null);
+ }
+
+ Future setHotspotEnabled(bool enabled) async {
+ final ok = await _channel.invokeMethod(
+ 'setHotspotEnabled',
+ {'enabled': enabled},
+ );
+ return ok ?? false;
+ }
+
+ Future openHotspotSettings() async {
+ await _channel.invokeMethod('openHotspotSettings');
+ }
+
+ Future