Files
ShowenV2/clients/flutter/lib/providers/device_provider.dart
showen d30c111c71 feat: M1.1 完成 + M1.2 启动 — 全量更新
M1.1 收尾:
- 24项 P0/P1/P2 bug 修复 (Rust 107 tests + Flutter 15 tests)
- Flutter App v0.3: cupertino_icons 修复, 单元测试, 调试面板, APK 52.6MB
- 示例插件完善: manifest.json + 请求/响应示范 + 7个测试
- API 文档重写 (以 routes.rs 为唯一权威)
- MILESTONES.md 更新至 100%

M1.2 启动:
- P0: 插件管理 API 闭环 (handle_manager_message Custom 分支 + broadcast_plugin_states)
- ServiceManager 集成测试 8/8 (tests/m1_2_service_manager.rs)
- M1.2 测试计划 (docs/M1.2_TEST_PLAN.md, 18个E2E场景)
- 动态插件系统: auto_rollback + version_manager GC + 路径穿越防护

总计: Rust 115/115 测试, Flutter 15/15 测试, 零 warning

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 18:12:42 +08:00

431 lines
14 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../models/ble_status.dart';
import '../models/device_status.dart';
import '../models/player_status.dart';
import '../models/wifi_status.dart';
import '../services/device_storage_service.dart';
import '../services/http_api_service.dart';
import '../services/web_socket_service.dart';
import 'debug_provider.dart';
class DeviceProvider extends ChangeNotifier {
DeviceProvider({
required HttpApiService httpApiService,
required WebSocketService webSocketService,
required DeviceStorageService deviceStorageService,
required DebugProvider debugProvider,
String initialDeviceIp = '127.0.0.1',
int initialDevicePort = 5000,
String? initialDeviceName,
}) : _httpApiService = httpApiService,
_webSocketService = webSocketService,
_debugProvider = debugProvider,
_deviceStorageService = deviceStorageService,
_deviceIp = _normalizeDeviceIp(initialDeviceIp),
_devicePort = _normalizePort(initialDevicePort),
_deviceName = _normalizeDeviceName(initialDeviceName) {
_httpApiService.baseUrl = _buildBaseUrl(_deviceIp, _devicePort);
_connectionSubscription = _webSocketService.onConnectionChanged.listen(
_handleConnectionChanged,
);
_statusSubscription = _webSocketService.onStatusUpdate.listen(
_handleStatusUpdate,
);
_wifiSubscription =
_webSocketService.onWifiUpdate.listen(_handleWifiUpdate);
_bleSubscription = _webSocketService.onBleUpdate.listen(_handleBleUpdate);
}
final HttpApiService _httpApiService;
final WebSocketService _webSocketService;
final DebugProvider _debugProvider;
final DeviceStorageService _deviceStorageService;
late final StreamSubscription<SocketConnectionStatus> _connectionSubscription;
late final StreamSubscription<Map<String, dynamic>> _statusSubscription;
late final StreamSubscription<Map<String, dynamic>> _wifiSubscription;
late final StreamSubscription<Map<String, dynamic>> _bleSubscription;
DeviceStatus _status = DeviceStatus.initial();
bool _isLoading = false;
String? _errorMessage;
bool _webSocketConnected = false;
String _deviceIp;
int _devicePort;
String _deviceName;
List<SavedDevice> _deviceList = const <SavedDevice>[];
DeviceStatus get status => _status;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
bool get webSocketConnected => _webSocketConnected;
String get deviceIp => _deviceIp;
int get devicePort => _devicePort;
List<SavedDevice> get deviceList =>
List<SavedDevice>.unmodifiable(_deviceList);
HttpApiService get httpApiService => _httpApiService;
Future<void> initialize() async {
_debugProvider.addHttpLog(
'Initialize device provider',
details: <String, Object>{'device': '$_deviceIp:$_devicePort'},
);
final lastDevice = await _deviceStorageService.getLastDevice();
if (lastDevice != null) {
_applyDevice(
ip: lastDevice.ip, port: lastDevice.port, name: lastDevice.name);
}
await _refreshDeviceList();
await refresh();
await connect();
}
Future<void> refresh() async {
_setLoading(true);
_debugProvider.addHttpLog(
'Refresh device overview',
details: <String, Object>{'device': '$_deviceIp:$_devicePort'},
);
try {
final results = await Future.wait<dynamic>([
_httpApiService.getPlaybackStatus(),
_httpApiService.getWifiStatus(),
_httpApiService.getBleStatus(),
]);
_status = _buildStatus(
playerStatus: results[0] as PlayerStatus,
wifiStatus: results[1] as WifiStatus,
bleStatus: results[2] as BleServiceStatus,
);
_errorMessage = null;
_debugProvider.addHttpLog('Device overview refreshed');
} catch (error) {
_errorMessage = error.toString();
_debugProvider.addHttpLog(
'Refresh device overview failed',
details: error,
);
_status = _status.copyWith(
connected: false,
connectionType: 'offline',
deviceName: _deviceName,
ipAddress: '$_deviceIp:$_devicePort',
);
} finally {
_setLoading(false);
}
}
Future<void> loadDeviceOverview() => refresh();
Future<void> connect() async {
_debugProvider.addWsLog(
'Connect WebSocket',
details: <String, Object>{'device': '$_deviceIp:$_devicePort'},
);
try {
await _webSocketService.connect(_deviceIp, port: _devicePort);
_webSocketConnected = _webSocketService.isConnected;
_errorMessage = null;
_debugProvider.addWsLog('WebSocket connect request completed');
notifyListeners();
} catch (error) {
_webSocketConnected = false;
_errorMessage = error.toString();
_debugProvider.addWsLog('WebSocket connect failed', details: error);
notifyListeners();
}
}
Future<void> switchDevice(String input, {String? name}) async {
final nextDevice = _parseDeviceInput(input, fallbackPort: _devicePort);
final nextName =
_normalizeDeviceName(name ?? _status.deviceName ?? _deviceName);
_setLoading(true);
_debugProvider.addHttpLog(
'Switch device to ${nextDevice.ip}:${nextDevice.port}',
details: <String, Object>{'name': nextName},
);
try {
await _validateDeviceReachable(nextDevice.ip, nextDevice.port);
await _webSocketService.disconnect();
_applyDevice(ip: nextDevice.ip, port: nextDevice.port, name: nextName);
notifyListeners();
await _deviceStorageService.saveDevice(
nextDevice.ip, nextDevice.port, nextName);
await _refreshDeviceList();
await refresh();
await connect();
final resolvedName = _normalizeDeviceName(_status.deviceName ?? nextName);
if (resolvedName != nextName) {
_deviceName = resolvedName;
await _deviceStorageService.saveDevice(
nextDevice.ip,
nextDevice.port,
resolvedName,
);
await _refreshDeviceList();
notifyListeners();
}
_debugProvider.addHttpLog(
'Device switched successfully',
details: <String, Object>{'device': '${nextDevice.ip}:${nextDevice.port}'},
);
} catch (error) {
_errorMessage = error.toString();
_debugProvider.addHttpLog('Switch device failed', details: error);
notifyListeners();
rethrow;
} finally {
_setLoading(false);
}
}
Future<void> updateDeviceIp(String ip) async {
await switchDevice(ip);
}
Future<void> removeStoredDevice(SavedDevice device) async {
await _deviceStorageService.removeDevice(device.ip, device.port);
await _refreshDeviceList();
_debugProvider.addHttpLog(
'Removed saved device ${device.address}',
details: <String, Object>{'name': device.name},
);
notifyListeners();
}
Future<void> startBle({String? deviceName}) async {
_setLoading(true);
_debugProvider.addHttpLog(
'Start BLE service',
details: <String, Object?>{'deviceName': deviceName},
);
try {
await _httpApiService.startBle(deviceName);
await refresh();
_debugProvider.addHttpLog('BLE service started');
} catch (error) {
_errorMessage = error.toString();
_debugProvider.addHttpLog('Start BLE service failed', details: error);
notifyListeners();
} finally {
_setLoading(false);
}
}
Future<void> stopBle() async {
_setLoading(true);
_debugProvider.addHttpLog('Stop BLE service');
try {
await _httpApiService.stopBle();
await refresh();
_debugProvider.addHttpLog('BLE service stopped');
} catch (error) {
_errorMessage = error.toString();
_debugProvider.addHttpLog('Stop BLE service failed', details: error);
notifyListeners();
} finally {
_setLoading(false);
}
}
void _handleConnectionChanged(SocketConnectionStatus connectionStatus) {
_webSocketConnected = connectionStatus == SocketConnectionStatus.connected;
_debugProvider.addWsLog(
'Device provider connection state: ${connectionStatus.name}',
);
if (!_webSocketConnected) {
_status = _status.copyWith(connectionType: _status.connectionType);
}
notifyListeners();
}
void _handleStatusUpdate(Map<String, dynamic> payload) {
_debugProvider.addWsLog('Received status update', details: payload);
final playerStatus = PlayerStatus.fromJson(payload);
_status = _buildStatus(
playerStatus: playerStatus,
wifiStatus: _status.wifiStatus ?? WifiStatus.disconnected(),
bleStatus: _status.bleStatus ?? BleServiceStatus.initial(),
);
notifyListeners();
}
void _handleWifiUpdate(Map<String, dynamic> payload) {
_debugProvider.addWsLog('Received wifi update', details: payload);
final wifiStatus = WifiStatus.fromJson(payload);
_status = _buildStatus(
playerStatus: _status.playerStatus ?? PlayerStatus.initial(),
wifiStatus: wifiStatus,
bleStatus: _status.bleStatus ?? BleServiceStatus.initial(),
);
notifyListeners();
}
void _handleBleUpdate(Map<String, dynamic> payload) {
_debugProvider.addBleLog('Received BLE update', details: payload);
final normalized = <String, dynamic>{
'running': payload['running'] ?? payload['ready'] ?? false,
'embedded': payload['embedded'] ?? false,
'device_name': payload['device_name'] ?? payload['name'],
};
final bleStatus = BleServiceStatus.fromJson(normalized);
_status = _buildStatus(
playerStatus: _status.playerStatus ?? PlayerStatus.initial(),
wifiStatus: _status.wifiStatus ?? WifiStatus.disconnected(),
bleStatus: bleStatus,
);
notifyListeners();
}
DeviceStatus _buildStatus({
required PlayerStatus playerStatus,
required WifiStatus wifiStatus,
required BleServiceStatus bleStatus,
}) {
final connectionType = wifiStatus.connected
? 'wifi'
: bleStatus.running
? 'ble'
: 'offline';
return DeviceStatus(
connected:
wifiStatus.connected || bleStatus.running || _webSocketConnected,
connectionType: connectionType,
deviceName: bleStatus.deviceName ?? _deviceName,
ipAddress: wifiStatus.ip ?? '$_deviceIp:$_devicePort',
playerStatus: playerStatus,
wifiStatus: wifiStatus,
bleStatus: bleStatus,
updatedAt: DateTime.now(),
);
}
Future<void> _refreshDeviceList() async {
_deviceList = await _deviceStorageService.getDevices();
}
Future<void> _validateDeviceReachable(String ip, int port) async {
final probeService = HttpApiService(baseUrl: _buildBaseUrl(ip, port));
try {
await probeService.getStatus().timeout(const Duration(seconds: 3));
} on TimeoutException {
throw Exception('设备不可达连接超时3 秒)');
} on Exception catch (error) {
throw Exception('设备不可达:$error');
} finally {
probeService.dispose();
}
}
void _applyDevice({
required String ip,
required int port,
String? name,
}) {
_deviceIp = _normalizeDeviceIp(ip);
_devicePort = _normalizePort(port);
_deviceName = _normalizeDeviceName(name);
_httpApiService.baseUrl = _buildBaseUrl(_deviceIp, _devicePort);
_status = _status.copyWith(
deviceName: _deviceName,
ipAddress: '$_deviceIp:$_devicePort',
updatedAt: DateTime.now(),
);
}
void _setLoading(bool value) {
_isLoading = value;
notifyListeners();
}
static String _buildBaseUrl(String ip, int port) => 'http://$ip:$port';
static String _normalizeDeviceIp(String input) {
var value = input.trim();
if (value.startsWith('http://')) {
value = value.substring(7);
} else if (value.startsWith('https://')) {
value = value.substring(8);
} else if (value.startsWith('ws://')) {
value = value.substring(5);
} else if (value.startsWith('wss://')) {
value = value.substring(6);
}
final slashIndex = value.indexOf('/');
if (slashIndex >= 0) {
value = value.substring(0, slashIndex);
}
final colonIndex = value.indexOf(':');
if (colonIndex >= 0) {
value = value.substring(0, colonIndex);
}
return value.isEmpty ? '127.0.0.1' : value;
}
static int _normalizePort(int input) {
if (input <= 0 || input > 65535) {
return 5000;
}
return input;
}
static String _normalizeDeviceName(String? input) {
final value = input?.trim() ?? '';
return value.isEmpty ? 'Showen' : value;
}
static SavedDevice _parseDeviceInput(String input,
{int fallbackPort = 5000}) {
var value = input.trim();
if (value.startsWith('http://')) {
value = value.substring(7);
} else if (value.startsWith('https://')) {
value = value.substring(8);
} else if (value.startsWith('ws://')) {
value = value.substring(5);
} else if (value.startsWith('wss://')) {
value = value.substring(6);
}
final slashIndex = value.indexOf('/');
if (slashIndex >= 0) {
value = value.substring(0, slashIndex);
}
final colonIndex = value.lastIndexOf(':');
final hasPort = colonIndex > 0 && colonIndex < value.length - 1;
final ip =
_normalizeDeviceIp(hasPort ? value.substring(0, colonIndex) : value);
final port = hasPort
? _normalizePort(
int.tryParse(value.substring(colonIndex + 1)) ?? fallbackPort)
: _normalizePort(fallbackPort);
return SavedDevice(
ip: ip,
port: port,
name: 'Showen',
lastUsedAt: DateTime.now(),
);
}
@override
void dispose() {
unawaited(_connectionSubscription.cancel());
unawaited(_statusSubscription.cancel());
unawaited(_wifiSubscription.cancel());
unawaited(_bleSubscription.cancel());
super.dispose();
}
}