mirror of
https://github.com/m5stack/StackChan.git
synced 2026-04-28 03:22:39 +00:00
6314188835
- align Android/iOS native bridge implementations and audio handling paths - improve Bluetooth provisioning/verification flow and related error handling - refactor WebSocket, music, and device utility logic for more stable behavior - clean up noisy debug logs and normalize comments across Flutter and native code - update AR view, dance/agent/device pages, and platform integration details
328 lines
10 KiB
Dart
328 lines
10 KiB
Dart
/*
|
|
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
|
SPDX-License-Identifier: MIT
|
|
*/
|
|
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:network_info_plus/network_info_plus.dart';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
import 'package:stack_chan/util/native_bridge.dart';
|
|
import 'package:stack_chan/view/app.dart';
|
|
|
|
import '../../app_state.dart';
|
|
import '../../model/blue_model.dart';
|
|
import '../../util/blue_util.dart';
|
|
|
|
class DeviceWifiConfig extends StatefulWidget {
|
|
const DeviceWifiConfig({super.key, this.isWelCome});
|
|
|
|
final bool? isWelCome;
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => _DeviceWifiConfigState();
|
|
}
|
|
|
|
class WifiCacheKeys {
|
|
static const String wifiName = 'cached_wifi_name';
|
|
static const String wifiPassword = 'cached_wifi_password';
|
|
}
|
|
|
|
class _DeviceWifiConfigState extends State<DeviceWifiConfig> {
|
|
String _wifiName = "";
|
|
String _wifiPassword = "";
|
|
|
|
late TextEditingController _nameTextEditingController;
|
|
late TextEditingController _passwordTextEditingController;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_nameTextEditingController = TextEditingController(text: "");
|
|
_passwordTextEditingController = TextEditingController(text: "");
|
|
_onAppear();
|
|
_initializeWifiInfo();
|
|
_registerNativeHandler();
|
|
}
|
|
|
|
/// Register native handler for iOS WiFi name callback
|
|
void _registerNativeHandler() {
|
|
NativeBridge.shared.registerHandler(Method.wifiName, (message) async {
|
|
if (message.arguments is String) {
|
|
final wifiName = message.arguments as String;
|
|
if (wifiName.isNotEmpty) {
|
|
setState(() {
|
|
_wifiName = wifiName;
|
|
_nameTextEditingController.text = wifiName;
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Save WiFi information to local cache
|
|
Future<void> _saveWifiInfoToCache(String name, String password) async {
|
|
try {
|
|
await AppState.asyncPrefs.setString(WifiCacheKeys.wifiName, name);
|
|
await AppState.asyncPrefs.setString(WifiCacheKeys.wifiPassword, password);
|
|
} catch (e) {
|
|
AppState.shared.showToast("Failed to save WiFi information");
|
|
}
|
|
}
|
|
|
|
/// Load WiFi information from local cache
|
|
/// Returns true if cache exists and is valid
|
|
Future<bool> _loadCachedWifiInfo() async {
|
|
try {
|
|
final cachedName =
|
|
await AppState.asyncPrefs.getString(WifiCacheKeys.wifiName) ?? "";
|
|
final cachedPassword =
|
|
await AppState.asyncPrefs.getString(WifiCacheKeys.wifiPassword) ?? "";
|
|
|
|
if (cachedName.isNotEmpty) {
|
|
setState(() {
|
|
_wifiName = cachedName;
|
|
_wifiPassword = cachedPassword;
|
|
_nameTextEditingController.text = cachedName;
|
|
_passwordTextEditingController.text = cachedPassword;
|
|
});
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Initialize WiFi info - cache first, then system if no cache
|
|
Future<void> _initializeWifiInfo() async {
|
|
final hasCache = await _loadCachedWifiInfo();
|
|
if (!hasCache) {
|
|
await _fetchSystemWifiName();
|
|
}
|
|
}
|
|
|
|
/// Fetch WiFi name from system with platform-specific handling
|
|
Future<void> _fetchSystemWifiName() async {
|
|
if (Platform.isAndroid) {
|
|
// Android: Request permission first, then fetch if granted
|
|
final status = await Permission.location.request();
|
|
if (status.isGranted) {
|
|
await _fetchAndroidWifiName();
|
|
} else {
|
|
}
|
|
} else if (Platform.isIOS) {
|
|
// iOS: Request permission and immediately fetch via native bridge
|
|
// (no need to wait for permission result on iOS)
|
|
await Permission.locationWhenInUse.request();
|
|
NativeBridge.shared.sendMessage(Method.wifiName);
|
|
}
|
|
}
|
|
|
|
/// Fetch WiFi name on Android using network_info_plus library
|
|
Future<void> _fetchAndroidWifiName() async {
|
|
try {
|
|
final networkInfo = NetworkInfo();
|
|
String? wifiName = await networkInfo.getWifiName();
|
|
|
|
if (wifiName != null && wifiName.isNotEmpty) {
|
|
final cleanWifiName = wifiName.replaceAll('"', '');
|
|
if (cleanWifiName.isNotEmpty && cleanWifiName != "unknown ssid") {
|
|
setState(() {
|
|
_wifiName = cleanWifiName;
|
|
_nameTextEditingController.text = cleanWifiName;
|
|
});
|
|
}
|
|
}
|
|
} on PlatformException catch (e) {
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
NativeBridge.shared.unregisterHandler(Method.wifiName);
|
|
_nameTextEditingController.dispose();
|
|
_passwordTextEditingController.dispose();
|
|
BlueUtil.shared.characteristicCallback = null;
|
|
BlueUtil.shared.wifiSetCharacteristicCall = null;
|
|
BlueUtil.shared.onReconnectSuccess = null;
|
|
if (mounted) {
|
|
FocusScope.of(context).unfocus();
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
bool isSuccess = false;
|
|
|
|
void dismiss() {
|
|
AppState.shared.manualShutdownTime = DateTime.now();
|
|
BlueUtil.shared.characteristicCallback = null;
|
|
BlueUtil.shared.wifiSetCharacteristicCall = null;
|
|
BlueUtil.shared.onReconnectSuccess = null;
|
|
BlueUtil.shared.disconnectCurrentPeripheral();
|
|
|
|
if (widget.isWelCome == true) {
|
|
CupertinoSheetRoute.popSheet(context);
|
|
} else {
|
|
Navigator.of(context).pop();
|
|
}
|
|
}
|
|
|
|
void _onAppear() {
|
|
BlueUtil.shared.wifiSetCharacteristicCall = (data) {
|
|
String json = utf8.decode(data);
|
|
final model = BlueNotifyStateModel.fromJson(json);
|
|
if (model?.data?.state != null) {
|
|
String state = model!.data!.state!;
|
|
if (state == "wifiConnecting") {
|
|
setState(() {});
|
|
} else if (state == "wifiConnected") {
|
|
if (isSuccess) {
|
|
return;
|
|
}
|
|
isSuccess = true;
|
|
setState(() {});
|
|
dismiss();
|
|
} else if (state == "wifiConnectFailed") {
|
|
setState(() {});
|
|
Future.delayed(const Duration(seconds: 1), () {
|
|
if (mounted) {
|
|
showCupertinoDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
return CupertinoAlertDialog(
|
|
title: const Text("Configuration failed"),
|
|
content: const Text(
|
|
"Configuration failed, please re-enter wifi name and password",
|
|
),
|
|
actions: [
|
|
CupertinoDialogAction(
|
|
child: const Text("OK"),
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
_passwordTextEditingController.clear();
|
|
FocusScope.of(context).requestFocus(FocusNode());
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return CupertinoPageScaffold(
|
|
backgroundColor: CupertinoColors.systemGroupedBackground.resolveFrom(
|
|
context,
|
|
),
|
|
child: CustomScrollView(
|
|
slivers: [
|
|
CupertinoSliverNavigationBar(
|
|
largeTitle: Text("Wi-Fi Info"),
|
|
leading: CupertinoNavigationBarBackButton(
|
|
color: CupertinoColors.systemOrange,
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
trailing: CupertinoButton(
|
|
sizeStyle: .large,
|
|
onPressed: () {
|
|
TextInput.finishAutofillContext();
|
|
confirmWifi();
|
|
},
|
|
child: Icon(CupertinoIcons.check_mark),
|
|
),
|
|
),
|
|
SliverList.list(
|
|
children: [
|
|
Padding(
|
|
padding: .only(left: 20, right: 20),
|
|
child: Text(
|
|
"Input the Wi-Fi in your environment for StackChan to connect to. Compatible with 2.4GHz, not 5GHz.",
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
color: CupertinoColors.placeholderText.resolveFrom(context),
|
|
),
|
|
),
|
|
),
|
|
CupertinoListSection.insetGrouped(
|
|
header: Text("Wi-Fi Name"),
|
|
children: [
|
|
CupertinoListTile(
|
|
padding: .only(left: 10, right: 10),
|
|
title: CupertinoTextField(
|
|
controller: _nameTextEditingController,
|
|
decoration: BoxDecoration(),
|
|
autofocus: true,
|
|
textAlign: .start,
|
|
textInputAction: .next,
|
|
onChanged: (value) {
|
|
_wifiName = value;
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
CupertinoListSection.insetGrouped(
|
|
header: Text("Wi-Fi Password"),
|
|
children: [
|
|
CupertinoListTile(
|
|
padding: .only(left: 10, right: 10),
|
|
title: CupertinoTextField(
|
|
controller: _passwordTextEditingController,
|
|
decoration: BoxDecoration(),
|
|
textAlign: .start,
|
|
textInputAction: .done,
|
|
onSubmitted: (value) {
|
|
confirmWifi();
|
|
},
|
|
onChanged: (value) {
|
|
_wifiPassword = value;
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void confirmWifi() async {
|
|
if (_wifiName.isEmpty || _wifiPassword.isEmpty) {
|
|
AppState.shared.showToast("Please enter the full name and password");
|
|
return;
|
|
}
|
|
|
|
_saveWifiInfoToCache(_wifiName, _wifiPassword);
|
|
|
|
//savewifiinfo
|
|
final BlueWifiModel model = BlueWifiModel(
|
|
cmd: "setWifi",
|
|
data: BlueWifi(ssid: _wifiName, password: _wifiPassword),
|
|
);
|
|
final jsonString = model.toJson();
|
|
if (jsonString != null) {
|
|
final result = await BlueUtil.shared.sendWifiSetData(jsonString);
|
|
if (!result) {
|
|
dismiss();
|
|
App.showDialog(
|
|
"Bluetooth disconnected. Please re-pair WiFi on your StackChan.",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|