diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index aaffa8ffc..acf36ac6b 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -6,6 +6,7 @@ import 'package:deep_voice/core/utils/logger.dart'; import 'package:get_storage/get_storage.dart'; import 'dart:convert'; import 'package:agent_service/agent_service.dart'; +import 'bluetooth_manager.dart'; // 重新导出BLE连接状态常量,让外部可以通过BleManager访问 export 'package:ble_service/ble_service.dart' show BleConnectionState; @@ -107,6 +108,10 @@ class BleManager extends GetxService { final BleService _bleService = BleService(); // 经典蓝牙实例 + // 当前连接的设备信息 + String? _currentDeviceAddress; + String? _currentDeviceName; + // BLE连接状态 final _connectionState = BleConnectionState.disconnected.obs; int get connectionState => _connectionState.value; @@ -179,19 +184,8 @@ class BleManager extends GetxService { state == BleConnectionState.error) { Logger.info('BLE设备已断开或发生错误'); _isConnected.value = false; - // 更新设备信息 - _updateDeviceInfoIfNeeded( - batteryLevel: 0, - leftEarBattery: 0, - rightEarBattery: 0, - caseBattery: 0, - leftEarCharging: false, - rightEarCharging: false, - caseCharging: false, - firmwareVersion: '', - firmwareProductId: '', - firmwareProductColor: '', - ); + // 断开连接时不清除设备信息,保留最后的电量信息 + // 只有在需要完全清除设备时才重置信息 } } @@ -408,11 +402,14 @@ class BleManager extends GetxService { else if (deviceInfo.value == null && _isConnected.value && (firmwareVersion != null || batteryLevel != null)) { - // 提示尝试创建设备信息但缺少必要信息 - Logger.warning('尝试更新设备信息,但设备基本信息不存在'); + // 使用保存的设备信息创建新的DeviceInfo对象 + String deviceAddress = _currentDeviceAddress ?? ""; + String deviceName = _currentDeviceName ?? ""; + + Logger.info('创建新的设备信息对象: address=$deviceAddress, name=$deviceName'); deviceInfo.value = DeviceInfo( - address: "", // 应确保从连接信息中获取真实值 - name: "", + address: deviceAddress, + name: deviceName, batteryLevel: batteryLevel ?? 0, leftEarBattery: leftEarBattery ?? 0, rightEarBattery: rightEarBattery ?? 0, @@ -427,35 +424,59 @@ class BleManager extends GetxService { } } + /// 清除设备信息(用于完全断开连接或切换设备时) + void clearDeviceInfo() { + Logger.info('清除设备信息'); + deviceInfo.value = null; + _currentDeviceAddress = null; + _currentDeviceName = null; + } + /// 连接成功后获取设备信息 Future fetchDeviceInfo() async { Logger.info('开始获取BLE设备详细信息'); try { - // 获取电池信息 - final batteryResult = await _bleService.getBatteryInfo(); - if (batteryResult) { - Logger.info('电池信息获取指令已发送'); - } else { + // 确保设备信息对象存在 + if (deviceInfo.value == null && _isConnected.value) { + // 创建初始设备信息对象 + String deviceAddress = _currentDeviceAddress ?? ""; + String deviceName = _currentDeviceName ?? ""; + + Logger.info('初始化设备信息对象: address=$deviceAddress, name=$deviceName'); + deviceInfo.value = DeviceInfo( + address: deviceAddress, + name: deviceName, + batteryLevel: 0, + leftEarBattery: 0, + rightEarBattery: 0, + caseBattery: 0, + leftEarCharging: false, + rightEarCharging: false, + caseCharging: false, + firmwareVersion: '', + firmwareProductId: '', + firmwareProductColor: '', + ); + } + + // 并行获取所有设备信息,不使用延迟 + final results = await Future.wait([ + _bleService.getBatteryInfo(), + _bleService.getDeviceVersion(), + _bleService.getProductId(), + ]); + + Logger.info('设备信息获取结果: 电池=${results[0]}, 版本=${results[1]}, 产品ID=${results[2]}'); + + if (!results[0]) { Logger.warning('电池信息获取指令发送失败'); } - // 添加延迟 - await Future.delayed(const Duration(seconds: 1)); - // 获取设备版本信息 - final versionResult = await _bleService.getDeviceVersion(); - if (versionResult) { - Logger.info('版本信息获取指令已发送'); - } else { + if (!results[1]) { Logger.warning('版本信息获取指令发送失败'); } - // 添加延迟 - await Future.delayed(const Duration(seconds: 1)); - // 获取产品id - final productIdResult = await _bleService.getProductId(); - if (productIdResult) { - Logger.info('产品id获取指令已发送'); - } else { - Logger.warning('产品id获取指令发送失败'); + if (!results[2]) { + Logger.warning('产品ID获取指令发送失败'); } } catch (e) { Logger.error('获取BLE设备信息失败: ${e.toString()}'); @@ -579,6 +600,8 @@ class BleManager extends GetxService { Logger.info('BLE服务停止成功'); _isConnected.value = false; _connectionState.value = BleConnectionState.disconnected; + // 停止服务时清除设备信息 + clearDeviceInfo(); } else { Logger.error('BLE服务停止失败'); } @@ -759,6 +782,15 @@ class BleManager extends GetxService { /// 注册后台扫描 Future registerBackgroundScan({String? macAddress}) async { try { + // 保存设备地址信息 + if (macAddress != null) { + _currentDeviceAddress = macAddress; + // 从蓝牙管理器获取设备名称 + final bluetoothManager = Get.find(); + _currentDeviceName = bluetoothManager.currentDevice?.name ?? ""; + Logger.info('保存设备信息: address=$_currentDeviceAddress, name=$_currentDeviceName'); + } + return await _bleService.registerBackgroundScan(macAddress: macAddress); } catch (e) { Logger.error('注册后台扫描失败: ${e.toString()}'); diff --git a/lib/data/services/bluetooth_manager.dart b/lib/data/services/bluetooth_manager.dart index eaa6844e5..a5de719ff 100644 --- a/lib/data/services/bluetooth_manager.dart +++ b/lib/data/services/bluetooth_manager.dart @@ -13,6 +13,7 @@ class BluetoothManager extends GetxService { // 蓝牙状态 final _bluetoothState = 'unknown'.obs; String get bluetoothState => _bluetoothState.value; + RxString get bluetoothStateRx => _bluetoothState; // 当前经典蓝牙设备 final Rx currentDeviceRx = Rx(null); @@ -28,9 +29,19 @@ class BluetoothManager extends GetxService { super.onInit(); Logger.info('蓝牙服务初始化'); - // 设置蓝牙事件监听 _setupBluetoothListener(); + + // 主动检查当前已连接的设备和蓝牙状态 + Future.delayed(Duration.zero, () async { + Logger.info('检查初始蓝牙连接状态'); + + // 先检查蓝牙是否开启 + await isBluetoothEnabled(); + + // 然后刷新设备连接 + await refreshDeviceConnection(); + }); } /// 释放资源 diff --git a/lib/data/services/music_manager.dart b/lib/data/services/music_manager.dart index d74fa0b83..90324c290 100644 --- a/lib/data/services/music_manager.dart +++ b/lib/data/services/music_manager.dart @@ -70,4 +70,14 @@ class MusicManager extends GetxService { Future> getStatus() async { return await _musicService.getStatus(); } + + //获取播放进度 + Future> getProgress() async { + return await _musicService.getProgress(); + } + + //跳转到指定位置 + Future seekTo(int positionMs) async { + await _musicService.seekTo(positionMs); + } } diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 4a0fd14ef..0a8d204a8 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -37,6 +37,11 @@ class AgentController extends GetxController { var isPlaying = false.obs; var currentUrl = "".obs; + // 播放进度相关状态 + var currentPosition = 0.obs; // 当前播放位置(毫秒) + var duration = 0.obs; // 总时长(毫秒) + var progress = 0.0.obs; // 播放进度(0.0-1.0) + // 各种状态标志 final isInitialized = false.obs; final isListening = false.obs; //是否正在语音识别 @@ -64,6 +69,9 @@ class AgentController extends GetxController { // 导航流事件订阅 StreamSubscription? _amapeventSubscription; + // 播放进度更新定时器 + Timer? _progressTimer; + // 用于跟踪AI回复状态 bool _isNewAssistantResponse = true; String _currentResponseId = ''; @@ -91,6 +99,8 @@ class AgentController extends GetxController { textController.dispose(); scrollController.dispose(); _eventSubscription?.cancel(); + _amapeventSubscription?.cancel(); + _progressTimer?.cancel(); super.onClose(); } @@ -845,17 +855,61 @@ class AgentController extends GetxController { _musiceManager.play(song); isPlaying.value = true; currentUrl.value = song['url'] ?? ''; + _startProgressTimer(); } // 播放音乐 void musicPause() { _musiceManager.pause(); isPlaying.value = false; + _stopProgressTimer(); } void musicResume() { _musiceManager.resume(); isPlaying.value = true; + _startProgressTimer(); + } + + // 启动进度更新定时器 + void _startProgressTimer() { + _stopProgressTimer(); // 先停止现有定时器 + _progressTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (isPlaying.value) { + updateMusicProgress(); + } else { + timer.cancel(); + } + }); + } + + // 停止进度更新定时器 + void _stopProgressTimer() { + _progressTimer?.cancel(); + _progressTimer = null; + } + + // 获取播放进度 + Future updateMusicProgress() async { + try { + var progressInfo = await _musiceManager.getProgress(); + currentPosition.value = progressInfo['currentPosition'] ?? 0; + duration.value = progressInfo['duration'] ?? 0; + progress.value = progressInfo['progress'] ?? 0.0; + } catch (e) { + Logger.e('AgentController', '获取播放进度失败: $e'); + } + } + + // 跳转到指定位置 + Future seekToPosition(int positionMs) async { + try { + await _musiceManager.seekTo(positionMs); + // 立即更新进度状态 + await updateMusicProgress(); + } catch (e) { + Logger.e('AgentController', '跳转播放位置失败: $e'); + } } // 修改自动停止处理,遵循TTS设置 diff --git a/lib/modules/agent/views/circular_progress_button.dart b/lib/modules/agent/views/circular_progress_button.dart new file mode 100644 index 000000000..510bf95a4 --- /dev/null +++ b/lib/modules/agent/views/circular_progress_button.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'dart:math' as math; + +/// 环形进度条播放按钮组件 +class CircularProgressButton extends StatelessWidget { + final bool isPlaying; + final double progress; // 0.0 - 1.0 + final VoidCallback onPlayPause; + final Function(double)? onSeek; // 点击进度条跳转回调 + final double size; + final Color primaryColor; + final Color backgroundColor; + final double strokeWidth; + + const CircularProgressButton({ + Key? key, + required this.isPlaying, + required this.progress, + required this.onPlayPause, + this.onSeek, + this.size = 36.0, + required this.primaryColor, + this.backgroundColor = Colors.grey, + this.strokeWidth = 2.0, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapDown: (details) { + if (onSeek != null) { + // 计算点击位置对应的进度 + final center = Offset(size / 2, size / 2); + final tapPosition = details.localPosition; + final dx = tapPosition.dx - center.dx; + final dy = tapPosition.dy - center.dy; + + // 计算角度 (从顶部开始,顺时针) + double angle = math.atan2(dy, dx) + math.pi / 2; + if (angle < 0) angle += 2 * math.pi; + + // 转换为进度值 (0.0 - 1.0) + final seekProgress = angle / (2 * math.pi); + + // 检查点击是否在圆环区域内 + final distance = math.sqrt(dx * dx + dy * dy); + final innerRadius = (size / 2) - strokeWidth - 4; + final outerRadius = (size / 2) + 4; + + if (distance >= innerRadius && distance <= outerRadius) { + onSeek!(seekProgress); + } else if (distance < innerRadius) { + // 点击中心区域,播放/暂停 + onPlayPause(); + } + } else { + onPlayPause(); + } + }, + child: SizedBox( + width: size, + height: size, + child: Stack( + alignment: Alignment.center, + children: [ + // 背景圆环 + CustomPaint( + size: Size(size, size), + painter: _CircularProgressPainter( + progress: 1.0, + color: backgroundColor.withValues(alpha: 0.3), + strokeWidth: strokeWidth, + ), + ), + // 进度圆环 + CustomPaint( + size: Size(size, size), + painter: _CircularProgressPainter( + progress: progress, + color: primaryColor, + strokeWidth: strokeWidth, + ), + ), + // 播放/暂停按钮 + Icon( + isPlaying ? Icons.pause : Icons.play_arrow, + color: primaryColor, + size: size * 0.5, + ), + ], + ), + ), + ); + } +} + +/// 环形进度条绘制器 +class _CircularProgressPainter extends CustomPainter { + final double progress; + final Color color; + final double strokeWidth; + + _CircularProgressPainter({ + required this.progress, + required this.color, + required this.strokeWidth, + }); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + final radius = (size.width / 2) - strokeWidth / 2; + + final paint = Paint() + ..color = color + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round; + + // 绘制圆弧,从顶部开始 (-π/2),顺时针绘制 + const startAngle = -math.pi / 2; + final sweepAngle = 2 * math.pi * progress; + + canvas.drawArc( + Rect.fromCircle(center: center, radius: radius), + startAngle, + sweepAngle, + false, + paint, + ); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return oldDelegate is! _CircularProgressPainter || + oldDelegate.progress != progress || + oldDelegate.color != color || + oldDelegate.strokeWidth != strokeWidth; + } +} diff --git a/lib/modules/agent/views/music_card.dart b/lib/modules/agent/views/music_card.dart index 0202e1bb8..7162472b6 100644 --- a/lib/modules/agent/views/music_card.dart +++ b/lib/modules/agent/views/music_card.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../controllers/agent_controller.dart'; +import 'circular_progress_button.dart'; /// 构建音乐项目 Widget buildMusicItem(BuildContext context, int id, String name, String url, @@ -23,7 +24,7 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(isDarkMode ? 0.2 : 0.03), + color: Colors.black.withValues(alpha: isDarkMode ? 0.2 : 0.03), blurRadius: 3, spreadRadius: 1, offset: const Offset(0, 1), @@ -83,8 +84,18 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url, final isThisPlaying = controller.isPlaying.value && controller.currentUrl.value == url; - return IconButton( - onPressed: () { + // 只有当前播放的歌曲才显示进度 + final currentProgress = + isThisPlaying ? controller.progress.value : 0.0; + + return CircularProgressButton( + isPlaying: isThisPlaying, + progress: currentProgress, + primaryColor: Theme.of(context).primaryColor, + backgroundColor: Colors.grey, + size: 36.0, + strokeWidth: 2.0, + onPlayPause: () { if (controller.isPlaying.value) { if (controller.currentUrl.value == url) { controller.musicPause(); @@ -99,13 +110,14 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url, } } }, - icon: Icon( - isThisPlaying ? Icons.pause_circle : Icons.play_circle, - color: Theme.of(context).primaryColor, - ), - padding: EdgeInsets.zero, - constraints: BoxConstraints(), - iconSize: 36, + onSeek: isThisPlaying + ? (seekProgress) { + // 只有当前播放的歌曲才允许跳转 + final targetPosition = + (controller.duration.value * seekProgress).round(); + controller.seekToPosition(targetPosition); + } + : null, ); }), ], @@ -124,7 +136,8 @@ Widget buildMusicContent( Text( '以下是一些推荐的音乐:', style: TextStyle( - color: isDarkMode ? Colors.white.withOpacity(0.9) : Colors.black87, + color: + isDarkMode ? Colors.white.withValues(alpha: 0.9) : Colors.black87, fontSize: 14, height: 1.4, fontWeight: FontWeight.w400, @@ -198,10 +211,14 @@ Widget buildMusicContent( const SizedBox(height: 8), Center( child: TextButton( - onPressed: () {}, + onPressed: () { + // 跳转到完整歌单页面,传递完整的歌单数据 + Get.toNamed('/music/playlist', arguments: playlist); + }, style: TextButton.styleFrom( minimumSize: Size.zero, - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: Text( diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart index 4616d5c49..701291cba 100644 --- a/lib/modules/home/controllers/home_controller.dart +++ b/lib/modules/home/controllers/home_controller.dart @@ -6,32 +6,26 @@ import 'package:flutter/material.dart'; import '../../../data/services/bluetooth_manager.dart'; import '../../../data/services/ble_manager.dart'; -/// 连接状态枚举 enum ConnectionStatus { - disconnected, // 未连接 - connecting, // 连接中 - connected, // 已连接 - failed // 连接失败 + disconnected, + connecting, + connected, + failed } /// 主页控制器,管理设备连接状态和信息展示 class HomeController extends GetxController { + static const String _tag = 'HomeCtrl'; static HomeController get to => Get.find(); - // 连接状态相关 - final _connectionStatus = ConnectionStatus.disconnected.obs; - final _connectionErrorMsg = ''.obs; - final _bluetoothState = 'unknown'.obs; - - // Getter方法 - ConnectionStatus get connectionStatus => _connectionStatus.value; - String get connectionErrorMsg => _connectionErrorMsg.value; - String get bluetoothState => _bluetoothState.value; + final _isFullyConnected = false.obs; + String? _lastConnectedDeviceName; + + bool get isFullyConnected => _isFullyConnected.value; + RxBool get isFullyConnectedRx => _isFullyConnected; BluetoothDevice? get currentDevice => _bluetoothManager.currentDevice; Rx get deviceInfo => _bleManager.deviceInfo; - bool get isConnected => currentDevice != null; - // 设备详细信息的getter,从deviceInfo获取数据 final leftEarBattery = 0.obs; final rightEarBattery = 0.obs; final caseBattery = 0.obs; @@ -39,15 +33,6 @@ class HomeController extends GetxController { final rightEarCharging = false.obs; final caseCharging = false.obs; final firmwareVersion = ''.obs; - - // int get leftEarBattery => deviceInfo?.leftEarBattery ?? 0; - // int get rightEarBattery => deviceInfo?.rightEarBattery ?? 0; - // int get caseBattery => deviceInfo?.caseBattery ?? 0; - // bool get leftEarCharging => deviceInfo?.leftEarCharging ?? false; - // bool get rightEarCharging => deviceInfo?.rightEarCharging ?? false; - // bool get caseCharging => deviceInfo?.caseCharging ?? false; - // String get firmwareVersion => deviceInfo?.firmwareVersion ?? ''; - /// 当有新的deviceInfo时更新所有Observable变量 void updateDeviceInfo(DeviceInfo? newDeviceInfo) { leftEarBattery.value = newDeviceInfo?.leftEarBattery ?? 0; rightEarBattery.value = newDeviceInfo?.rightEarBattery ?? 0; @@ -58,12 +43,10 @@ class HomeController extends GetxController { firmwareVersion.value = newDeviceInfo?.firmwareVersion ?? ''; } - // 监听和订阅 Worker? _deviceInfoWorker; - Worker? _bluetoothStateWorker; - Worker? _deviceWorker; + Worker? _classicBluetoothWorker; + Worker? _bleConnectionWorker; - // 蓝牙服务 final _bluetoothManager = Get.find(); final _bleManager = Get.find(); @override @@ -75,175 +58,120 @@ class HomeController extends GetxController { @override void onClose() { _deviceInfoWorker?.dispose(); - _bluetoothStateWorker?.dispose(); - _deviceWorker?.dispose(); + _classicBluetoothWorker?.dispose(); + _bleConnectionWorker?.dispose(); super.onClose(); } - /// 使用缓存的蓝牙设备信息初始化 void _initWithCachedInfo() { - // 使用蓝牙服务中的当前状态 - _bluetoothState.value = _bluetoothManager.bluetoothState; - - // 根据当前设备状态设置连接状态 - if (currentDevice != null) { - _connectionStatus.value = ConnectionStatus.connected; - _connectionErrorMsg.value = ''; - updateDeviceInfo(_bleManager.deviceInfo.value); - Logger.info('主页加载缓存设备信息: ${currentDevice!.name}'); - } else { - _connectionStatus.value = ConnectionStatus.disconnected; - if (_bluetoothState.value == 'off') { - _connectionErrorMsg.value = '蓝牙未开启'; - } else { - _connectionErrorMsg.value = '未连接到设备'; - } - } - - // 设置监听器监听蓝牙服务状态变化 _setupStateListeners(); - - // 在初始化时自动触发一次刷新设备连接状态 - refreshDeviceConnection(); + _checkConnectionStatus(); } - /// 设置状态监听器 void _setupStateListeners() { - // 设备信息监听 - 设备信息更新时自动触发UI更新 _deviceInfoWorker = ever(_bleManager.deviceInfo, (DeviceInfo? info) { - Logger.info('设备信息监听'); if (info != null) { - Logger.info('主页更新设备信息: ${info.toString()}'); updateDeviceInfo(info); - } else { - Logger.info('主页清空设备信息'); } - update(); }); - // 蓝牙状态监听 - _bluetoothStateWorker = - ever(_bluetoothManager.bluetoothState.obs, (String state) { - _bluetoothState.value = state; - - // 如果蓝牙关闭,更新错误信息 - if (state == 'off') { - _connectionErrorMsg.value = '蓝牙未开启'; - _connectionStatus.value = ConnectionStatus.disconnected; - } - - update(); - }); - - // 设备连接状态监听 - 直接监听设备变化 - _deviceWorker = - ever(_bluetoothManager.currentDeviceRx, (BluetoothDevice? device) { + _classicBluetoothWorker = ever(_bluetoothManager.currentDeviceRx, (BluetoothDevice? device) { if (device != null) { - // 设备已连接 - if (_connectionStatus.value != ConnectionStatus.connected) { - _connectionStatus.value = ConnectionStatus.connected; - _connectionErrorMsg.value = ''; - Logger.info('主页检测到设备已连接: ${device.name}'); - } - } else if (_connectionStatus.value == ConnectionStatus.connected) { - // 设备已断开 - _connectionStatus.value = ConnectionStatus.disconnected; - _connectionErrorMsg.value = '设备已断开连接'; - - Logger.info('主页检测到设备已断开连接'); + _lastConnectedDeviceName = device.name; } - - update(); + _checkConnectionStatus(); + }); + + _bleConnectionWorker = ever(_bleManager.connectionStateRx, (int state) { + _checkConnectionStatus(); }); } - /// 刷新设备连接状态 - /// 注意:此方法不会主动刷新设备,只更新UI状态 Future refreshDeviceConnection() async { - // 更新当前状态以反映UI变化 - bool hasDevice = currentDevice != null; - - if (hasDevice) { - _connectionStatus.value = ConnectionStatus.connected; - _connectionErrorMsg.value = ''; - await _bleManager.fetchDeviceInfo(); - - Logger.info('使用缓存设备信息: ${currentDevice!.name}'); - } else { - if (_bluetoothState.value == 'off') { - _connectionErrorMsg.value = '蓝牙未开启'; - } else { - _connectionErrorMsg.value = '未连接到设备'; - } - _connectionStatus.value = ConnectionStatus.disconnected; - } - - // 刷新UI - update(); + await _checkConnectionStatus(); } - // 导航和功能方法 - - /// 重新扫描设备 void rescanDevices() { Get.toNamed(Routes.pairing); } - /// 打开翻译功能 void openTranslation() { Get.toNamed(Routes.translation); } - /// 展示AI功能 void exploreAIFeatures() { Get.toNamed(Routes.agent); } + + Future _checkConnectionStatus() async { + try { + final currentBleState = await _bleManager.getConnectionState(); + + bool hasClassicBluetooth = currentDevice != null; + bool hasBleConnection = currentBleState == 2; + bool wasConnected = _isFullyConnected.value; + bool isNowConnected = hasClassicBluetooth && hasBleConnection; + + if (isNowConnected && currentDevice == null) { + isNowConnected = false; + } + + if (_isFullyConnected.value != isNowConnected) { + _isFullyConnected.value = isNowConnected; + _isFullyConnected.refresh(); + update(); + + if (!wasConnected && isNowConnected) { + await _bleManager.fetchDeviceInfo(); + } + } + + update(); + } catch (e) { + Logger.e(_tag, '检查连接状态失败: $e'); + if (_isFullyConnected.value) { + _isFullyConnected.value = false; + _isFullyConnected.refresh(); + update(); + } + } + } - // UI辅助方法 - - /// 获取连接状态对应的颜色 Color getConnectionStatusColor() { - switch (_connectionStatus.value) { - case ConnectionStatus.connected: - return Colors.green; - case ConnectionStatus.connecting: - return Colors.amber; - case ConnectionStatus.failed: - return Colors.red; - case ConnectionStatus.disconnected: - default: - return Colors.grey; - } + return _isFullyConnected.value ? Colors.green : Colors.grey; } - /// 获取连接状态对应的文本 String getConnectionStatusText() { - if (bluetoothState == 'off') { - return 'bluetoothOff'.tr; - } - - switch (_connectionStatus.value) { - case ConnectionStatus.connected: - return 'connected'.tr; - case ConnectionStatus.connecting: - return 'connecting'.tr; - case ConnectionStatus.failed: - return 'connectionFailure'.tr; - case ConnectionStatus.disconnected: - default: - return 'unconnected'.tr; - } + return _isFullyConnected.value ? 'connected'.tr : 'unconnected'.tr; } - /// 获取设备名称 + bool get shouldShowDeviceInfo => currentDevice != null; + String getDeviceName() { - return currentDevice?.name ?? 'unknownEquipment'.tr; + if (_isFullyConnected.value && currentDevice == null) { + _checkConnectionStatus(); + } + + final deviceName = currentDevice?.name; + if (deviceName != null && deviceName.isNotEmpty) { + return deviceName; + } + + final bleDeviceName = deviceInfo.value?.name; + if (bleDeviceName != null && bleDeviceName.isNotEmpty) { + return bleDeviceName; + } + + if (!_isFullyConnected.value && _lastConnectedDeviceName != null && _lastConnectedDeviceName!.isNotEmpty) { + return _lastConnectedDeviceName!; + } + + return 'unknownEquipment'.tr; } - /// 获取信号强度(模拟数据) String getSignalStrength() { - if (!isConnected) { + if (!_isFullyConnected.value) { return 'zeroSignal'.tr; } @@ -258,12 +186,10 @@ class HomeController extends GetxController { } } - /// 获取设备ID String getDeviceId() { return deviceInfo.value?.address ?? currentDevice?.address ?? 'unknown'.tr; } - /// 获取设备电量文本 String getBatteryText() { return deviceInfo.value?.batteryLevel != null && deviceInfo.value!.batteryLevel > 0 @@ -271,9 +197,8 @@ class HomeController extends GetxController { : 'unknown'.tr; } - /// 获取耳机左右电量文本 String getEarbudsText() { - if (deviceInfo == null || + if (deviceInfo.value == null || (deviceInfo.value!.leftEarBattery <= 0 && deviceInfo.value!.rightEarBattery <= 0 || deviceInfo.value!.caseBattery > 100)) { @@ -295,7 +220,6 @@ class HomeController extends GetxController { return '左耳: $leftStatus / 右耳: $rightStatus'; } - /// 获取充电仓电量文本 String getCaseText() { if (deviceInfo.value == null || deviceInfo.value!.caseBattery <= 0 || @@ -311,9 +235,8 @@ class HomeController extends GetxController { return '充电仓: $caseStatus'; } - /// 获取充电状态文本 String getChargingText() { - if (deviceInfo == null) { + if (deviceInfo.value == null) { return 'unknown'.tr; } @@ -324,7 +247,6 @@ class HomeController extends GetxController { return isCharging ? 'charging'.tr : 'notCharging'.tr; } - /// 获取设备版本文本 String getVersionText() { return deviceInfo.value?.firmwareVersion.isNotEmpty == true ? deviceInfo.value!.firmwareVersion diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index be4cef42d..d100d1a6c 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -5,8 +5,6 @@ import '../controllers/home_controller.dart'; import '../../../routes/app_pages.dart'; import '../../../routes/app_routes.dart'; import '../../../core/widgets/common_bottom_nav.dart'; -// 导入枚举 -import '../controllers/home_controller.dart' show ConnectionStatus; // 呼吸灯动画组件 class AnimatedPulsing extends StatefulWidget { @@ -205,8 +203,12 @@ class HomeView extends GetView { // 设备状态卡片 Widget _buildDeviceStatusCard(bool isDarkMode) { - return Container( - decoration: BoxDecoration( + return Obx(() { + // 使用连接状态作为key,强制在状态改变时重建widget + final isConnected = controller.isFullyConnectedRx.value; + return Container( + key: ValueKey('device_status_$isConnected'), + decoration: BoxDecoration( color: isDarkMode ? const Color(0xFF1A1A1A) : Colors.white, borderRadius: BorderRadius.circular(16.r), boxShadow: [ @@ -226,18 +228,9 @@ class HomeView extends GetView { // 设备信息 SizedBox(height: 12.h), Obx(() { - // 如果连接失败且有错误信息,显示错误提示 - if (controller.connectionStatus == ConnectionStatus.failed && - controller.connectionErrorMsg.isNotEmpty) { - return _buildConnectionErrorSection(isDarkMode); //错误提示 - } - - // 如果正在连接中,显示连接中提示 - if (controller.connectionStatus == ConnectionStatus.connecting) { - return _buildConnectingIndicator(); //连接中提示 - } - - return controller.isConnected + // 移除连接失败和连接中的状态判断,直接根据是否完全连接来判断 + // 使用响应式访问确保UI更新 + return controller.isFullyConnectedRx.value ? _buildConnectedDeviceInfo(isDarkMode) //已连接设备信息 : _buildDisconnectedInfo(isDarkMode); //未连接提示 }), @@ -245,7 +238,8 @@ class HomeView extends GetView { // 连接按钮(仅在未连接时显示) SizedBox(height: 12.h), Obx(() { - if (controller.connectionStatus == ConnectionStatus.disconnected) { + // 使用响应式访问确保UI更新 + if (!controller.isFullyConnectedRx.value) { return _buildConnectButton(); } else { return const SizedBox(); @@ -253,7 +247,8 @@ class HomeView extends GetView { }), ], ), - ); + ); + }); } // 设备状态头部 @@ -332,95 +327,7 @@ class HomeView extends GetView { } // 连接错误部分 - Widget _buildConnectionErrorSection(bool isDarkMode) { - return Column( - children: [ - Container( - padding: EdgeInsets.all(12.w), - decoration: BoxDecoration( - color: - isDarkMode ? Colors.red[900]!.withOpacity(0.2) : Colors.red[50], - borderRadius: BorderRadius.circular(12.r), - border: Border.all( - color: isDarkMode ? Colors.red[800]! : Colors.red[200]!, - ), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - Icons.error_outline, - color: Colors.red, - size: 18.sp, - ), - SizedBox(width: 8.w), - Expanded( - child: Text( - controller.connectionErrorMsg, - style: TextStyle( - fontSize: 12.sp, - height: 1.4, - color: isDarkMode ? Colors.red[300] : Colors.red[800], - ), - ), - ), - ], - ), - ), - SizedBox(height: 12.h), - // 连接按钮 - ElevatedButton( - onPressed: controller.rescanDevices, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - minimumSize: Size(double.infinity, 40.h), - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.r), - ), - ), - child: Text( - 'reconnect'.tr, // 重新连接 - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ); - } - // 连接中指示器 - Widget _buildConnectingIndicator() { - return Container( - padding: EdgeInsets.symmetric(vertical: 20.h), - child: Center( - child: Column( - children: [ - SizedBox( - width: 30.w, - height: 30.w, - child: CircularProgressIndicator( - strokeWidth: 2.5.w, - valueColor: AlwaysStoppedAnimation(Colors.blue), - ), - ), - SizedBox(height: 12.h), - Text( - 'connectingDevice'.tr, - style: TextStyle( - fontSize: 13.sp, - color: Colors.blue, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ); - } // 已连接设备信息(新增设备名称 + 信号强度 + 电池信息) Widget _buildConnectedDeviceInfo(bool isDarkMode) { @@ -608,110 +515,6 @@ class HomeView extends GetView { } // 设备详情卡片 - Widget _buildDeviceDetailsCard(bool isDarkMode) { - return Container( - padding: EdgeInsets.all(12.w), - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900] : Colors.grey[50], - borderRadius: BorderRadius.circular(12.r), - border: Border.all( - color: isDarkMode ? Colors.grey[800]! : Colors.grey[200]!, - width: 1, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 设备ID - _buildInfoRow( - icon: Icons.perm_device_info, - label: 'deviceId'.tr, - value: controller.getDeviceId(), - isDarkMode: isDarkMode, - ), - SizedBox(height: 8.h), - - // 固件版本 - _buildInfoRow( - icon: Icons.info_outline, - label: 'firmwareVersion'.tr, - value: controller.firmwareVersion.value, - isDarkMode: isDarkMode, - ), - - // 连接状态标签 - // SizedBox(height: 10.h), - // Container( - // padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), - // decoration: BoxDecoration( - // color: controller.getConnectionStatusColor().withOpacity(0.1), - // borderRadius: BorderRadius.circular(6.r), - // border: Border.all( - // color: controller.getConnectionStatusColor().withOpacity(0.3), - // width: 1, - // ), - // ), - // child: Row( - // mainAxisSize: MainAxisSize.min, - // children: [ - // Icon( - // Icons.check_circle_outline, - // size: 12.sp, - // color: controller.getConnectionStatusColor(), - // ), - // SizedBox(width: 4.w), - // Text( - // controller.getConnectionStatusText(), - // style: TextStyle( - // fontSize: 11.sp, - // color: controller.getConnectionStatusColor(), - // fontWeight: FontWeight.w500, - // ), - // ), - // ], - // ), - // ), - ], - ), - ); - } - - // 信息行 - Widget _buildInfoRow({ - required IconData icon, - required String label, - required String value, - required bool isDarkMode, - }) { - return Row( - children: [ - Icon( - icon, - size: 14.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - SizedBox(width: 6.w), - Text( - '$label: ', - style: TextStyle( - fontSize: 11.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - ), - Expanded( - child: Text( - value.isEmpty ? 'unknown'.tr : value, - style: TextStyle( - fontSize: 11.sp, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.grey[300] : Colors.grey[700], - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ); - } // 未连接信息 Widget _buildDisconnectedInfo(bool isDarkMode) { diff --git a/lib/modules/music/bindings/music_playlist_binding.dart b/lib/modules/music/bindings/music_playlist_binding.dart new file mode 100644 index 000000000..85f385d42 --- /dev/null +++ b/lib/modules/music/bindings/music_playlist_binding.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; +import '../controllers/music_playlist_controller.dart'; + +class MusicPlaylistBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut( + () => MusicPlaylistController(), + ); + } +} diff --git a/lib/modules/music/controllers/music_playlist_controller.dart b/lib/modules/music/controllers/music_playlist_controller.dart new file mode 100644 index 000000000..ac0c8d2ee --- /dev/null +++ b/lib/modules/music/controllers/music_playlist_controller.dart @@ -0,0 +1,19 @@ +import 'package:get/get.dart'; + +class MusicPlaylistController extends GetxController { + // 歌单数据 + final playlist = >[].obs; + + @override + void onInit() { + super.onInit(); + // 从路由参数获取歌单数据 + final List> playlistData = Get.arguments ?? []; + playlist.assignAll(playlistData); + } + + @override + void onClose() { + super.onClose(); + } +} diff --git a/lib/modules/music/views/music_playlist_view.dart b/lib/modules/music/views/music_playlist_view.dart new file mode 100644 index 000000000..c5ca02817 --- /dev/null +++ b/lib/modules/music/views/music_playlist_view.dart @@ -0,0 +1,203 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../agent/controllers/agent_controller.dart'; +import '../../agent/views/circular_progress_button.dart'; + +class MusicPlaylistView extends StatelessWidget { + const MusicPlaylistView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = Get.find(); + final List> playlist = Get.arguments ?? []; + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + appBar: AppBar( + title: const Text('音乐播放列表'), + backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, + foregroundColor: isDarkMode ? Colors.white : Colors.black, + elevation: 0, + ), + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + body: playlist.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.music_off, + size: 64, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + '暂无音乐', + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], + ), + ), + ], + ), + ) + : ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: playlist.length, + itemBuilder: (context, index) { + final item = playlist[index]; + final id = item["id"]?.toString() ?? ""; + final name = item["name"] ?? "未知歌曲"; + final url = item["url"] ?? ""; + final singer = item["singer"] ?? item["sgener"] ?? "未知歌手"; + final image = item["image"] ?? ""; + + return _buildMusicItem( + context, + controller, + id, + name, + url, + singer, + image, + isDarkMode, + ); + }, + ), + ); + } + + Widget _buildMusicItem( + BuildContext context, + AgentController controller, + String id, + String name, + String url, + String singer, + String image, + bool isDarkMode, + ) { + final song = { + 'id': id, + 'title': name, + 'url': url, + 'artist': singer, + 'coverUrl': image, + }; + + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: isDarkMode ? 0.2 : 0.03), + blurRadius: 3, + spreadRadius: 1, + offset: const Offset(0, 1), + ), + ], + ), + child: Row( + children: [ + // 封面图片 + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Colors.grey[300], + ), + child: image.isNotEmpty + ? ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + image, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.music_note, + color: Colors.grey[600], + ); + }, + ), + ) + : Icon( + Icons.music_note, + color: Colors.grey[600], + ), + ), + const SizedBox(width: 12), + // 歌曲信息 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black87, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + singer, + style: TextStyle( + fontSize: 14, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(width: 12), + // 播放按钮 + Obx(() { + final isThisPlaying = + controller.isPlaying.value && controller.currentUrl.value == url; + + // 只有当前播放的歌曲才显示进度 + final currentProgress = isThisPlaying ? controller.progress.value : 0.0; + + return CircularProgressButton( + isPlaying: isThisPlaying, + progress: currentProgress, + primaryColor: Theme.of(context).primaryColor, + backgroundColor: Colors.grey, + size: 40.0, + strokeWidth: 2.5, + onPlayPause: () { + if (controller.isPlaying.value) { + if (controller.currentUrl.value == url) { + controller.musicPause(); + } else { + controller.musicPlay(song); + } + } else { + if (controller.currentUrl.value == url) { + controller.musicResume(); + } else { + controller.musicPlay(song); + } + } + }, + onSeek: isThisPlaying ? (seekProgress) { + // 只有当前播放的歌曲才允许跳转 + final targetPosition = (controller.duration.value * seekProgress).round(); + controller.seekToPosition(targetPosition); + } : null, + ); + }), + ], + ), + ); + } +} diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 69fe1dfd3..34574e0f0 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -9,815 +9,577 @@ import '../../../data/services/bluetooth_manager.dart'; import '../../../data/services/ble_manager.dart'; import '../../../core/utils/permission_util.dart'; -/// 配对状态枚举 +/// 配对状态枚举 - 简化版本 enum PairingState { - init, // 初始状态 - btReady, // 蓝牙已就绪 - classicOk, // 经典蓝牙已连接 - serverVerify, // 服务器验证中 - pairing, // 配对中 - paired, // 已配对 - // 错误/退出状态 - exitNoPermission, // 无权限 - exitBtOff, // 蓝牙关闭 - exitNoClassic, // 无经典蓝牙连接 - exitNotMatch, // 设备不匹配 - exitIllegal, // 非法设备 - exitBound, // 已被他人绑定 - exitPairFailed, // 配对失败 - exitTimeout, // 超时 + idle, // 空闲/初始状态 + checking, // 检查中(蓝牙、权限等) + waitingClassic, // 等待经典蓝牙连接 + verifying, // 验证设备 + associating, // 正在进行BLE设备关联 + waitingBle, // 等待BLE连接 + completed, // 配对完成 + error, // 错误状态 } -/// 配对控制器 - 基于有限状态机的实现 +/// 错误类型枚举 +enum ErrorType { + none, + bluetoothOff, // 蓝牙关闭 + noPermission, // 无权限 + noClassicDevice, // 无经典蓝牙设备 + deviceNotMatch, // 设备不匹配 + deviceBound, // 设备已被绑定 + associationFailed, // 关联失败 + bleTimeout, // BLE连接超时 + networkError, // 网络错误 +} + +/// 配对控制器 - 基于简化状态机的实现 class PairingController extends GetxController { + static const String _tag = 'PairingCtrl'; static PairingController get to => Get.find(); - // ==================== 状态管理 ==================== - final _state = PairingState.init.obs; - PairingState get state => _state.value; - - // ==================== 手动模式控制 ==================== - final isManualMode = true.obs; - final isReadyToStart = false.obs; - - // ==================== 状态数据 ==================== - final statusDescription = ''.obs; - final actionButtonText = ''.obs; + // ========== 状态管理 ========== + final _currentState = PairingState.idle.obs; + final _errorType = ErrorType.none.obs; + final _errorMessage = ''.obs; + + PairingState get currentState => _currentState.value; + ErrorType get errorType => _errorType.value; + String get errorMessage => _errorMessage.value; + + // ========== UI状态 ========== + final statusText = ''.obs; final isLoading = false.obs; + final showConnectButton = false.obs; + final showRetryButton = false.obs; + final showSkipButton = false.obs; + final connectButtonText = '连接设备'.obs; + final retryButtonText = '重试'.obs; - // 当前设备信息 - BluetoothDevice? _currentClassicDevice; + // ========== 监听器管理 ========== + final List _workers = []; + Timer? _bleTimeoutTimer; - // 计时器和异步操作管理 - Timer? _stateTimer; - StreamSubscription? _bluetoothStateSubscription; - StreamSubscription? _deviceSubscription; - StreamSubscription? _bleStatusSubscription; + // ========== 依赖服务 ========== + late final BluetoothManager _bluetoothManager; + late final BleManager _bleManager; - // 常量定义 - static const _tag = 'PairingController'; - static const _validDeviceNamePattern = 'DeapSound'; + // ========== 当前设备信息 ========== + BluetoothDevice? _currentClassicDevice; + String? _verifiedDeviceAddress; + + // Public getter for view access + BluetoothDevice? get currentDevice => _currentClassicDevice; - // ==================== 生命周期 ==================== @override void onInit() { super.onInit(); - _setupStateObserver(); - start(); + Logger.i(_tag, '配对控制器初始化'); + + // 确保服务已初始化 + _bluetoothManager = Get.find(); + _bleManager = Get.find(); + + // 集中设置所有监听器 + _setupAllListeners(); + + // 开始初始状态检查 + // 延迟稍长时间,确保BluetoothManager有足够时间获取已连接设备 + Future.delayed(const Duration(milliseconds: 300), () { + _startInitialCheck(); + }); } @override void onClose() { - _cancelAllOperations(); - _bluetoothStateSubscription?.cancel(); - _deviceSubscription?.cancel(); - _bleStatusSubscription?.cancel(); + Logger.i(_tag, '配对控制器销毁'); + // 清理所有Worker + for (var worker in _workers) { + worker.dispose(); + } + _workers.clear(); + + // 清理定时器 + _bleTimeoutTimer?.cancel(); + super.onClose(); } - // ==================== 公共 API ==================== + // ==================== 监听器设置 ==================== - /// 启动配对流程 - Future start() async { - _transition(PairingState.init); + /// 集中设置所有监听器 + void _setupAllListeners() { + // 1. 监听蓝牙开关状态 + _setupBluetoothStateListener(); + + // 2. 监听经典蓝牙连接状态 + _setupClassicConnectionListener(); + + // 3. 监听BLE连接状态 + _setupBleConnectionListener(); } - /// 手动开始配对 - Future startManualPairing() async { - if (!isReadyToStart.value) return; - - Logger.i(_tag, '用户手动启动配对流程'); - isManualMode.value = false; - isReadyToStart.value = false; + /// 监听蓝牙开关状态 + void _setupBluetoothStateListener() { + _workers.add( + ever(_bluetoothManager.bluetoothStateRx, (String state) { + Logger.d(_tag, '蓝牙状态变化: $state'); + _handleBluetoothStateChange(state); + }) + ); + } - // 开始执行配对流程 - await _executeInitFlow(); + /// 监听经典蓝牙连接状态 + void _setupClassicConnectionListener() { + _workers.add( + ever(_bluetoothManager.currentDeviceRx, (BluetoothDevice? device) { + Logger.d(_tag, '经典蓝牙设备变化: ${device?.name}'); + _handleClassicDeviceChange(device); + }) + ); } - /// 重新开始配对流程 - Future restart() async { - _cancelAllOperations(); - _transition(PairingState.init); + /// 监听BLE连接状态 + void _setupBleConnectionListener() { + _workers.add( + ever(_bleManager.connectionStateRx, (int state) { + Logger.d(_tag, 'BLE连接状态变化: $state'); + _handleBleStateChange(state); + }) + ); } - /// 重置到手动模式 - void resetToManualMode() { - _cancelAllOperations(); - isManualMode.value = true; - isReadyToStart.value = true; - _transition(PairingState.init); + // ==================== 状态处理 ==================== + + /// 处理蓝牙开关状态变化 + void _handleBluetoothStateChange(String state) { + if (state == 'off') { + // 蓝牙关闭,进入错误状态 + _setError(ErrorType.bluetoothOff, '请开启蓝牙'); + } else if (state == 'on' && _errorType.value == ErrorType.bluetoothOff) { + // 蓝牙重新开启,恢复流程 + Logger.i(_tag, '检测到蓝牙已开启,自动恢复配对流程'); + _clearError(); + // 延迟一下确保蓝牙服务完全就绪 + Future.delayed(const Duration(milliseconds: 500), () { + _checkCurrentStatus(); + }); + } } - /// 处理按钮点击 - void handleActionButtonTap() { - switch (state) { - case PairingState.exitNoPermission: - _openAppSettings(); - break; - case PairingState.exitBtOff: - _openBluetoothSettings(); - break; - case PairingState.exitNoClassic: - case PairingState.exitNotMatch: - _openBluetoothSettings(); + /// 处理经典蓝牙设备变化 + void _handleClassicDeviceChange(BluetoothDevice? device) { + _currentClassicDevice = device; + + switch (_currentState.value) { + case PairingState.waitingClassic: + if (device != null) { + // 有设备连接,进入验证阶段 + Logger.i(_tag, '检测到经典蓝牙设备连接,开始验证'); + _transition(PairingState.verifying); + _verifyDevice(device); + } break; - case PairingState.exitTimeout: - restart(); + + case PairingState.error: + // 如果在错误状态下检测到设备连接,且错误是无设备连接,则自动恢复 + if (device != null && _errorType.value == ErrorType.noClassicDevice) { + Logger.i(_tag, '检测到设备重新连接,自动恢复配对流程'); + _clearError(); + _transition(PairingState.verifying); + _verifyDevice(device); + } break; - case PairingState.exitPairFailed: - case PairingState.exitBound: - _retryPairing(); + + case PairingState.verifying: + case PairingState.waitingBle: + case PairingState.completed: + if (device == null) { + // 设备断开,返回等待连接状态 + _setError(ErrorType.noClassicDevice, '设备已断开'); + } break; + default: break; } } - /// 跳过配对 - void skipPairing() async { - Logger.i(_tag, '用户选择跳过配对'); - // 权限申请 - await _checkAndRequestAgentPermissions(); - // 启动BLE后台服务 - await BleManager.to.startAgentService(); - Logger.i(_tag, 'BLE后台服务已启动'); - _navigateToHome(); + /// 处理BLE状态变化 + void _handleBleStateChange(int state) { + if (_currentState.value == PairingState.waitingBle) { + if (state == 2) { // BLE已连接 + _bleTimeoutTimer?.cancel(); + _transition(PairingState.completed); + } + } + + // 如果在完成状态下BLE断开,显示警告但不改变状态 + if (_currentState.value == PairingState.completed && state != 2) { + Logger.w(_tag, 'BLE连接已断开,但配对流程已完成'); + } } - // ==================== FSM 核心实现 ==================== + // ==================== 状态转换 ==================== /// 状态转换 - void _transition(PairingState next) { - final current = state; - - // 允许init状态重复执行,其他状态避免重复转换 - if (current == next && next != PairingState.init) return; - - Logger.d(_tag, 'FSM: ${current.name} → ${next.name}'); - _cancelStateTimer(); - _state.value = next; - - // 根据新状态执行对应的进入方法 - switch (next) { - case PairingState.init: - _enterInit(); + void _transition(PairingState newState) { + final oldState = _currentState.value; + _currentState.value = newState; + Logger.i(_tag, '状态转换: $oldState -> $newState'); + + // 根据新状态执行相应操作 + _onStateChanged(newState); + } + + /// 状态变化处理 + void _onStateChanged(PairingState state) { + // 重置UI状态 + isLoading.value = false; + showConnectButton.value = false; + showRetryButton.value = false; + showSkipButton.value = false; + + switch (state) { + case PairingState.idle: + statusText.value = '准备开始配对'; + break; + + case PairingState.checking: + statusText.value = '正在检查环境...'; + isLoading.value = true; break; - case PairingState.btReady: - _enterBtReady(); + + case PairingState.waitingClassic: + statusText.value = '请确保耳机已开启并进入配对模式'; + showConnectButton.value = true; + showSkipButton.value = true; + connectButtonText.value = '前往蓝牙设置'; break; - case PairingState.classicOk: - _enterClassicOk(); + + case PairingState.verifying: + statusText.value = '正在验证设备...'; + isLoading.value = true; break; - case PairingState.serverVerify: - _enterServerVerify(); + + case PairingState.associating: + statusText.value = '正在关联设备...'; + isLoading.value = true; + // 延迟执行关联,确保状态已更新 + Future.delayed(const Duration(milliseconds: 200), () { + _startDeviceAssociation(); + }); break; - case PairingState.pairing: - _enterPairing(); + + case PairingState.waitingBle: + statusText.value = '正在建立安全连接...'; + isLoading.value = true; + // 延迟一下再调用,给BLE管理器时间更新状态 + Future.delayed(const Duration(milliseconds: 200), () { + _startBleConnection(); + }); break; - case PairingState.paired: - _enterPaired(); + + case PairingState.completed: + statusText.value = '配对成功!'; + isLoading.value = false; + _navigateToHome(); break; - // 错误状态 - case PairingState.exitNoPermission: - case PairingState.exitBtOff: - case PairingState.exitNoClassic: - case PairingState.exitNotMatch: - case PairingState.exitIllegal: - case PairingState.exitBound: - case PairingState.exitPairFailed: - case PairingState.exitTimeout: - _handleErrorState(next); + + case PairingState.error: + statusText.value = _errorMessage.value; + isLoading.value = false; + showSkipButton.value = true; + + // 根据错误类型决定显示哪个按钮和按钮文本 + switch (_errorType.value) { + case ErrorType.bluetoothOff: + showConnectButton.value = true; + connectButtonText.value = '开启蓝牙'; + break; + case ErrorType.noClassicDevice: + showConnectButton.value = true; + connectButtonText.value = '前往蓝牙设置'; + break; + case ErrorType.noPermission: + showRetryButton.value = true; + retryButtonText.value = '授予权限'; + break; + case ErrorType.associationFailed: + showRetryButton.value = true; + retryButtonText.value = '重新关联'; + break; + default: + showRetryButton.value = true; + retryButtonText.value = '重试'; + break; + } break; } } - /// 设置状态观察器 - void _setupStateObserver() { - ever(_state, (PairingState state) { - _updateUI(); - }); - } - - // ==================== 状态处理方法 ==================== - - /// INIT: 初始化状态 - Future _enterInit() async { - Logger.i(_tag, '进入初始化状态'); - - if (isManualMode.value) { - // 手动模式:仅显示待机状态,不执行任何操作 - isLoading.value = false; - statusDescription.value = '准备就绪'; - isReadyToStart.value = true; - return; - } + // ==================== 错误处理 ==================== - // 自动模式:执行原有的初始化逻辑 - await _executeInitFlow(); + /// 设置错误状态 + void _setError(ErrorType type, String message) { + _errorType.value = type; + _errorMessage.value = message; + _transition(PairingState.error); } - /// 执行初始化流程 - Future _executeInitFlow() async { - isLoading.value = true; - statusDescription.value = '正在初始化...'; - - try { - // 初始视觉反馈延迟 - await Future.delayed(const Duration(milliseconds: 800)); - - // 更新状态让用户知道正在进行权限检查 - statusDescription.value = '正在检查所需权限...'; - - // iOS平台跳过权限检查 - if (!Platform.isIOS) { - // 添加额外视觉反馈延迟 - await Future.delayed(const Duration(milliseconds: 600)); - - final hasPermission = await _checkAndRequestPermissions(); - if (!hasPermission) { - statusDescription.value = '缺少必要的权限'; - await Future.delayed(const Duration(milliseconds: 500)); - _transition(PairingState.exitNoPermission); - return; - } - } - - // 设置蓝牙状态监听 - _setupBluetoothListener(); - - // 更新状态让用户知道正在进行蓝牙检查 - statusDescription.value = '检查蓝牙状态...'; - await Future.delayed(const Duration(milliseconds: 400)); + /// 清除错误状态 + void _clearError() { + _errorType.value = ErrorType.none; + _errorMessage.value = ''; + } - // 检查蓝牙状态 - final btState = BluetoothManager.to.bluetoothState; - Logger.i(_tag, '当前蓝牙状态: $btState'); + // ==================== 核心流程 ==================== - if (btState == 'on' || btState == 'unknown') { - _transition(PairingState.btReady); - } else { - statusDescription.value = '蓝牙未开启'; - await Future.delayed(const Duration(milliseconds: 500)); - _transition(PairingState.exitBtOff); - } - } catch (e) { - Logger.e(_tag, 'Init failed: $e'); - statusDescription.value = '初始化出错'; - await Future.delayed(const Duration(milliseconds: 500)); - _transition(PairingState.exitNoPermission); - } finally { - isLoading.value = false; - } + /// 开始初始检查 + void _startInitialCheck() { + _transition(PairingState.checking); + _checkCurrentStatus(); } - /// BT_READY: 蓝牙已就绪,检查经典蓝牙连接 - Future _enterBtReady() async { - isLoading.value = true; - statusDescription.value = '正在检查设备连接...'; - + /// 检查当前状态并决定下一步 + void _checkCurrentStatus() async { try { - // 刷新设备连接 - await BluetoothManager.to.refreshDeviceConnection(); - - // 获取当前连接的经典蓝牙设备 - final device = BluetoothManager.to.currentDevice; - if (device == null) { - _transition(PairingState.exitNoClassic); + // 1. 检查蓝牙状态 + if (_bluetoothManager.bluetoothState == 'off') { + _setError(ErrorType.bluetoothOff, '请开启蓝牙'); return; } - - _currentClassicDevice = device; - _transition(PairingState.classicOk); - } catch (e) { - Logger.e(_tag, 'BT ready check failed: $e'); - _transition(PairingState.exitNoClassic); - } finally { - isLoading.value = false; - } - } - - /// CLASSIC_OK: 经典蓝牙已连接,检查是否需要BLE配对 - Future _enterClassicOk() async { - if (_currentClassicDevice == null) { - _transition(PairingState.exitNoClassic); - return; - } - - statusDescription.value = '已连接: ${_currentClassicDevice!.name}'; - - // 检查设备名称是否合规 - if (!_isValidDeviceName(_currentClassicDevice!.name)) { - _transition(PairingState.exitNotMatch); - return; - } - - // 检查是否已有BLE配对记录 - try { - // 1. 检查平台是否支持Companion功能 - final isSupported = await BleManager.to.isCompanionSupported(); - if (!isSupported) { - Logger.i(_tag, '设备不支持Companion功能,跳过BLE配对'); - _transition(PairingState.paired); + + // 2. 检查权限 + if (!await _checkPermissions()) { + _setError(ErrorType.noPermission, '请授予必要权限'); return; } - - // 2. 获取已配对的MAC地址 - final pairedMac = await BleManager.to.getPairedMacAddress(); - - // 3. 检查是否已经配对且是同一设备 - if (pairedMac != null && pairedMac == _currentClassicDevice!.address) { - Logger.i(_tag, '设备已完成BLE配对: $pairedMac'); - - // 4. 检查BLE连接状态 - final isBleConnected = await BleManager.to.isBleConnected(); - if (isBleConnected) { - Logger.i(_tag, 'BLE已连接,跳转到配对成功'); - _transition(PairingState.paired); - } else { - Logger.i(_tag, 'BLE未连接,尝试重新配对'); - _transition(PairingState.pairing); - } + + // 3. 先刷新设备连接状态,确保获取最新的连接信息 + await _bluetoothManager.refreshDeviceConnection(); + + // 4. 检查经典蓝牙连接 + final classicDevice = _bluetoothManager.currentDevice; + if (classicDevice == null) { + _transition(PairingState.waitingClassic); return; } - - // 5. 如果没有配对或配对的是其他设备,进入验证流程 - _transition(PairingState.serverVerify); + + // 5. 如果有设备,进行验证 + _currentClassicDevice = classicDevice; + _transition(PairingState.verifying); + await _verifyDevice(classicDevice); + } catch (e) { - Logger.e(_tag, 'Classic OK check failed: $e'); - // 出错时直接尝试配对 - _transition(PairingState.pairing); + Logger.e(_tag, '状态检查失败: $e'); + _setError(ErrorType.networkError, '检查失败,请重试'); } } - /// SERVER_VERIFY: 服务器验证 - Future _enterServerVerify() async { - isLoading.value = true; - statusDescription.value = '正在验证设备...'; + /// 检查权限 + Future _checkPermissions() async { + // Android权限检查 + if (Platform.isAndroid) { + // 检查蓝牙相关权限 + final bluetoothScan = await Permission.bluetoothScan.request(); + final bluetoothConnect = await Permission.bluetoothConnect.request(); + final location = await Permission.locationWhenInUse.request(); + + return bluetoothScan.isGranted && + bluetoothConnect.isGranted && + location.isGranted; + } + return true; + } + /// 验证设备 + Future _verifyDevice(BluetoothDevice device) async { try { - if (_currentClassicDevice == null) { - _transition(PairingState.exitNotMatch); - return; - } - - // 验证设备是否可以配对 - final canPair = await BleManager.to - .verifyDeviceCanPair(_currentClassicDevice!.address); - - if (canPair) { - Logger.i(_tag, '设备验证通过,可以配对'); - _transition(PairingState.pairing); + Logger.i(_tag, '开始验证设备: ${device.name} (${device.address})'); + + // TODO: 实现API验证逻辑 + // 暂时模拟验证通过 + await Future.delayed(const Duration(seconds: 2)); + + // 检查设备名称是否包含 DeepSound 或 DeapSound + final deviceName = device.name ?? ''; + if (deviceName.contains('DeepSound') || deviceName.contains('DeapSound')) { + Logger.i(_tag, '设备验证通过'); + _verifiedDeviceAddress = device.address; + + // 检查是否已有BLE关联 + final pairedMacAddress = await _bleManager.getPairedMacAddress(); + Logger.i(_tag, '已关联的MAC地址: $pairedMacAddress'); + + if (pairedMacAddress != null && pairedMacAddress.toUpperCase() == device.address.toUpperCase()) { + // 已有关联,检查BLE连接状态 + Logger.i(_tag, '设备已关联,检查BLE连接状态'); + final currentBleState = await _bleManager.getConnectionState(); + Logger.i(_tag, '验证时BLE状态: $currentBleState'); + + if (currentBleState == 2) { // BleConnectionState.connected + Logger.i(_tag, 'BLE已连接,直接完成配对'); + _transition(PairingState.completed); + } else { + Logger.i(_tag, 'BLE未连接,等待BLE连接'); + _transition(PairingState.waitingBle); + } + } else { + // 没有关联或关联的是其他设备,需要进行关联 + Logger.i(_tag, '需要进行BLE设备关联'); + _transition(PairingState.associating); + } } else { - Logger.e(_tag, '设备验证失败,不能配对'); - _transition(PairingState.exitIllegal); + _setError(ErrorType.deviceNotMatch, '请连接 DeepSound 设备'); } } catch (e) { - Logger.e(_tag, 'Server verify failed: $e'); - _transition(PairingState.exitIllegal); - } finally { - isLoading.value = false; + Logger.e(_tag, '设备验证异常: $e'); + _setError(ErrorType.networkError, '验证失败,请重试'); } } - /// PAIRING: 执行配对 - Future _enterPairing() async { - isLoading.value = true; - statusDescription.value = '正在配对设备...'; - + /// 开始设备关联 + void _startDeviceAssociation() async { try { - if (_currentClassicDevice == null) { - _transition(PairingState.exitPairFailed); + if (_verifiedDeviceAddress == null) { + _setError(ErrorType.associationFailed, '设备地址无效'); return; } - - // 启动BLE状态监听 - _setupBleStatusListener(); - - // 检查当前BLE连接状态,防止错过已经连接的事件 - final isBleConnected = await BleManager.to.isBleConnected(); - if (isBleConnected) { - Logger.i(_tag, 'BLE已经连接,直接完成配对'); - _onBleConnected(); - return; - } - - // 设置配对超时保护 - _stateTimer = Timer(const Duration(seconds: 30), () { - if (state == PairingState.pairing) { - Logger.e(_tag, 'BLE配对超时'); - _transition(PairingState.exitTimeout); - } - }); - - // 使用系统配对框架执行BLE配对 - // iOS: AccessorySetupKit - // Android: Companion Device Manager - final success = await BleManager.to.startDeviceAssociation( - deviceNamePattern: _currentClassicDevice!.name, - macAddress: _currentClassicDevice!.address, + + Logger.i(_tag, '开始BLE设备关联: $_verifiedDeviceAddress'); + + // 调用BLE管理器的关联方法 + final success = await _bleManager.startDeviceAssociation( + deviceNamePattern: 'DeapSound', // 或者使用 _currentClassicDevice?.name + macAddress: _verifiedDeviceAddress!, ); - + if (success) { - Logger.i(_tag, 'BLE配对启动成功,立即注册后台扫描'); - - // 配对启动成功后立即注册后台扫描 - try { - // Android平台注册后台扫描 - if (Platform.isAndroid) { - await BleManager.to.registerBackgroundScan( - macAddress: _currentClassicDevice!.address); - Logger.i(_tag, '已注册后台扫描'); - } - } catch (e) { - Logger.e(_tag, '注册后台扫描失败: $e'); - // 注册失败不影响配对流程继续 - } - - Logger.i(_tag, '等待BLE连接完成...'); - // 配对结果将通过BLE状态监听回调处理 + Logger.i(_tag, 'BLE设备关联成功'); + // 关联成功后,转到等待BLE连接状态 + _transition(PairingState.waitingBle); } else { - Logger.e(_tag, 'BLE配对启动失败'); - _transition(PairingState.exitPairFailed); + _setError(ErrorType.associationFailed, 'BLE设备关联失败'); } } catch (e) { - Logger.e(_tag, 'Pairing failed: $e'); - _transition(PairingState.exitPairFailed); - } - } - - /// 设置BLE状态监听 - void _setupBleStatusListener() { - Logger.d(_tag, '🎯 开始设置BLE状态监听器...'); - - _bleStatusSubscription?.cancel(); - - Logger.d(_tag, '📱 当前BLE连接状态: ${BleManager.to.connectionState}'); - Logger.d(_tag, '🔗 开始监听BleManager.connectionStateRx...'); - - // 监听BleManager的连接状态变化,而不是直接监听BleService - _bleStatusSubscription = BleManager.to.connectionStateRx.listen( - (bleState) { - // 只在配对状态下处理BLE连接事件 - if (state == PairingState.pairing) { - if (bleState == BleConnectionState.connected) { - Logger.i(_tag, 'BLE连接成功,处理配对完成'); - _onBleConnected(); - } else if (bleState == BleConnectionState.error) { - Logger.e(_tag, 'BLE连接错误'); - _transition(PairingState.exitPairFailed); - } else { - Logger.d(_tag, 'BLE状态变化,但非关键状态: $bleState'); - } - } else { - Logger.d(_tag, '当前状态为${state.name},忽略BLE连接事件'); - } - }, - onError: (error) { - Logger.e(_tag, 'BLE状态监听错误: $error'); - }, - onDone: () { - Logger.w(_tag, 'BLE状态监听流已关闭'); - }, - ); - - Logger.d(_tag, 'BLE状态监听器已设置完成'); - } - - /// 处理BLE连接成功 - Future _onBleConnected() async { - Logger.i(_tag, 'BLE连接成功'); - - // 取消超时计时器 - _cancelStateTimer(); - - // BLE连接成功即表示配对完成 - // iOS使用UUID,Android使用MAC地址,但连接成功都表示配对验证通过 - Logger.i(_tag, 'BLE配对验证成功,经典蓝牙和BLE均已连接'); - _transition(PairingState.paired); - } - - /// PAIRED: 配对成功 - Future _enterPaired() async { - isLoading.value = false; - statusDescription.value = '配对成功!'; - - try { - //检测agent权限是否已授予 - _checkAndRequestAgentPermissions(); - // 启动BLE后台服务 - await BleManager.to.startAgentService(); - Logger.i(_tag, 'BLE后台服务已启动'); - - // 延迟后跳转到主页 - await Future.delayed(const Duration(milliseconds: 500)); - _navigateToHome(); - } catch (e) { - Logger.e(_tag, 'Post pairing setup failed: $e'); - // 即使失败也跳转到主页 - // _navigateToHome(); - } - } - - /// 处理错误状态 - void _handleErrorState(PairingState errorState) { - isLoading.value = false; - - switch (errorState) { - case PairingState.exitNoPermission: - statusDescription.value = '需要蓝牙权限'; - actionButtonText.value = '去设置'; - break; - case PairingState.exitBtOff: - statusDescription.value = '请开启蓝牙'; - actionButtonText.value = '去设置'; - break; - case PairingState.exitNoClassic: - statusDescription.value = '请先连接DeapSound设备'; - actionButtonText.value = '去设置'; - break; - case PairingState.exitNotMatch: - statusDescription.value = '请连接DeapSound设备'; - actionButtonText.value = '去设置'; - break; - case PairingState.exitTimeout: - statusDescription.value = '扫描超时'; - actionButtonText.value = '重试'; - break; - case PairingState.exitPairFailed: - statusDescription.value = '配对失败'; - actionButtonText.value = '重新配对'; - break; - case PairingState.exitBound: - statusDescription.value = '设备已被其他用户绑定'; - actionButtonText.value = '联系客服'; - break; - case PairingState.exitIllegal: - statusDescription.value = '设备验证失败'; - actionButtonText.value = '重试'; - break; - default: - break; + Logger.e(_tag, 'BLE设备关联异常: $e'); + _setError(ErrorType.associationFailed, '设备关联失败,请重试'); } } - // ==================== 辅助方法 ==================== - - /// 检查并请求权限 - Future _checkAndRequestPermissions() async { + /// 开始BLE连接 + void _startBleConnection() async { try { - if (Platform.isIOS) { - // iOS权限由系统自动处理 - return true; - } - - // Android权限检查 - 使用 PermissionUtil - // 根据 Android 版本确定需要的权限 - List requiredPermissions = [ - Permission.locationWhenInUse, - ]; - - if (Platform.isAndroid && await _getAndroidSdkVersion() >= 31) { - requiredPermissions.add(Permission.bluetoothScan); - requiredPermissions.add(Permission.bluetoothConnect); - } - - // 逐个请求权限 - for (var permission in requiredPermissions) { - final permissionName = _getPermissionName(permission); - Logger.d(_tag, '正在请求 $permissionName 权限'); - - final granted = await PermissionUtil.instance.requestPermission( - permissionType: permission, - permissionName: permissionName, - explanationText: _getExplanationText(permission), - permanentDenialText: '您已永久拒绝$permissionName权限,请前往应用设置开启', - settingsButtonText: '去设置', - cancelButtonText: '取消', - ); - - if (!granted) { - Logger.w(_tag, '$permissionName 权限未授予'); - return false; + // 设置超时定时器 + _bleTimeoutTimer?.cancel(); + _bleTimeoutTimer = Timer(const Duration(seconds: 30), () { + if (_currentState.value == PairingState.waitingBle) { + _setError(ErrorType.bleTimeout, 'BLE连接超时,请重试'); } + }); + + // 检查是否已连接 - 直接从原生层获取最新状态 + final currentBleState = await _bleManager.getConnectionState(); + Logger.i(_tag, 'BLE当前连接状态: $currentBleState'); + + if (currentBleState == 2) { // BleConnectionState.connected + Logger.i(_tag, 'BLE已经连接,直接完成配对'); + _bleTimeoutTimer?.cancel(); + _transition(PairingState.completed); + return; } - - return true; - } catch (e) { - Logger.e(_tag, '权限检查失败: $e'); - return false; - } - } - - Future _checkAndRequestAgentPermissions() async { - try { - if (Platform.isIOS) { - // iOS权限由系统自动处理 - return true; - } - - // Android权限检查 - 使用 PermissionUtil - // 根据 Android 版本确定需要的权限 - List requiredPermissions = [ - Permission.locationWhenInUse, - ]; - - if (Platform.isAndroid && await _getAndroidSdkVersion() >= 31) { - requiredPermissions.add(Permission.locationAlways); - requiredPermissions.add(Permission.notification); - } - - // 逐个请求权限 - for (var permission in requiredPermissions) { - final permissionName = _getPermissionName(permission); - Logger.d(_tag, '正在请求 $permissionName 权限'); - - final granted = await PermissionUtil.instance.requestPermission( - permissionType: permission, - permissionName: permissionName, - explanationText: _getExplanationText(permission), - permanentDenialText: '您已永久拒绝$permissionName权限,请前往应用设置开启', - settingsButtonText: '去设置', - cancelButtonText: '取消', - ); - - if (!granted) { - Logger.w(_tag, '$permissionName 权限未授予'); - return false; + + // 注册后台扫描以开始BLE配对 + if (_verifiedDeviceAddress != null) { + final success = await _bleManager.registerBackgroundScan(macAddress: _verifiedDeviceAddress); + if (!success && _currentState.value == PairingState.waitingBle) { + _setError(ErrorType.bleTimeout, 'BLE配对启动失败'); } } - - return true; } catch (e) { - Logger.e(_tag, '权限检查失败: $e'); - return false; + Logger.e(_tag, 'BLE连接失败: $e'); + _setError(ErrorType.bleTimeout, 'BLE连接失败'); } } - // 获取权限的用户友好名称 - String _getPermissionName(Permission permission) { - if (permission == Permission.location) return '定位'; - if (permission == Permission.locationWhenInUse) return '定位'; - if (permission == Permission.locationAlways) return '定位'; - if (permission == Permission.notification) return '通知'; - if (permission == Permission.accessNotificationPolicy) return '通知'; - if (permission == Permission.bluetoothScan) return '蓝牙扫描'; - if (permission == Permission.bluetoothConnect) return '蓝牙连接'; - return '未知权限'; - } - - // 获取权限说明文本 - String _getExplanationText(Permission permission) { - if (permission == Permission.location || - permission == Permission.locationWhenInUse) { - return '用于发现和连接蓝牙设备。我们不会在应用后台使用定位功能。'; - } - - if (permission == Permission.bluetoothScan || - permission == Permission.bluetoothConnect) { - return '用于扫描和连接蓝牙设备,这是与耳机通信所必需的权限。'; - } - - return '此权限是应用正常运行所必需的。'; - } - - /// 设置蓝牙状态监听 - void _setupBluetoothListener() { - _bluetoothStateSubscription?.cancel(); - _bluetoothStateSubscription = - BluetoothManager.to.bluetoothState.obs.listen((btState) { - Logger.d(_tag, '蓝牙状态变化: $btState'); - - // 根据当前状态决定如何响应蓝牙状态变化 - if (btState == 'off' && state != PairingState.exitBtOff) { - _transition(PairingState.exitBtOff); - } else if (btState == 'on' && state == PairingState.exitBtOff) { - _transition(PairingState.btReady); - } - }); - - // 监听设备连接状态 - _deviceSubscription?.cancel(); - _deviceSubscription = BluetoothManager.to.currentDeviceRx.listen((device) { - Logger.d(_tag, '设备连接状态变化: ${device?.name}'); - - // 根据当前状态决定如何响应设备变化 - if (device != null && state == PairingState.exitNoClassic) { - _currentClassicDevice = device; - _transition(PairingState.classicOk); - } else if (device == null && - (state == PairingState.classicOk || - state == PairingState.pairing || - state == PairingState.serverVerify)) { - _transition(PairingState.exitNoClassic); - } + /// 导航到主页 + void _navigateToHome() { + Future.delayed(const Duration(seconds: 1), () { + Get.offAllNamed(Routes.home); }); } - /// 检查设备名称是否合规 - bool _isValidDeviceName(String deviceName) { - return deviceName.contains(_validDeviceNamePattern); - } - - /// 获取Android SDK版本 - Future _getAndroidSdkVersion() async { - // 简化处理,实际应该通过platform channel获取 - return 31; - } - - /// 更新UI - void _updateUI() { - // 根据状态更新UI文本 - switch (state) { - case PairingState.init: - if (isManualMode.value && isReadyToStart.value) { - statusDescription.value = '准备就绪'; - } else { - statusDescription.value = '初始化中...'; - } - break; - case PairingState.btReady: - statusDescription.value = '蓝牙已就绪'; - break; - case PairingState.classicOk: - if (_currentClassicDevice != null) { - statusDescription.value = '已连接: ${_currentClassicDevice!.name}'; - } - break; - case PairingState.serverVerify: - statusDescription.value = '正在验证设备...'; - break; - case PairingState.pairing: - statusDescription.value = '正在配对...'; - break; - case PairingState.paired: - statusDescription.value = '配对成功!'; - break; - default: - // 错误状态在_handleErrorState中处理 - break; + // ==================== 用户操作 ==================== + + /// 用户点击连接按钮 + void onConnectTap() { + Logger.i(_tag, '用户点击连接按钮'); + + // 根据当前状态或错误类型决定行为 + if (_currentState.value == PairingState.error) { + switch (_errorType.value) { + case ErrorType.bluetoothOff: + // 引导用户去开启蓝牙 + Logger.i(_tag, '引导用户开启蓝牙'); + AppSettings.openAppSettings(type: AppSettingsType.bluetooth); + break; + case ErrorType.noClassicDevice: + // 引导用户去蓝牙设置连接设备 + Logger.i(_tag, '引导用户去蓝牙设置'); + AppSettings.openAppSettings(type: AppSettingsType.bluetooth); + break; + default: + // 其他错误情况,刷新并重试 + _bluetoothManager.refreshDeviceConnection(); + _checkCurrentStatus(); + break; + } + } else if (_currentState.value == PairingState.waitingClassic) { + // 等待经典蓝牙连接状态,引导去设置 + Logger.i(_tag, '等待经典蓝牙连接,引导用户去蓝牙设置'); + AppSettings.openAppSettings(type: AppSettingsType.bluetooth); + } else { + // 其他情况,刷新设备连接 + _bluetoothManager.refreshDeviceConnection(); + _checkCurrentStatus(); } } - /// 取消所有异步操作 - void _cancelAllOperations() { - _stateTimer?.cancel(); - } - - /// 取消状态计时器 - void _cancelStateTimer() { - _stateTimer?.cancel(); - } - - /// 重试配对 - void _retryPairing() { - if (_currentClassicDevice != null) { - _transition(PairingState.pairing); + /// 用户点击重试按钮 + void onRetryTap() { + Logger.i(_tag, '用户点击重试按钮'); + + // 如果是权限问题,引导去设置 + if (_errorType.value == ErrorType.noPermission) { + Logger.i(_tag, '引导用户去应用设置授予权限'); + AppSettings.openAppSettings(); } else { - restart(); + // 其他情况,清除错误并重试 + _clearError(); + _startInitialCheck(); } } - /// 打开应用设置 - void _openAppSettings() { - AppSettings.openAppSettings(); + /// 用户点击跳过按钮 + void onSkipTap() { + Logger.i(_tag, '用户选择跳过配对'); + Get.offAllNamed(Routes.home); } /// 打开蓝牙设置 - void _openBluetoothSettings() { - if (Platform.isAndroid) { - AppSettings.openAppSettings(type: AppSettingsType.bluetooth); - } else { - AppSettings.openAppSettings(); - } + void openBluetoothSettings() { + AppSettings.openAppSettings(type: AppSettingsType.bluetooth); } - - /// 导航到主页 - void _navigateToHome() { - Logger.i(_tag, '跳转到主页'); - Get.offAllNamed(Routes.home); + + /// 清除设备关联(在需要重新配对时调用) + Future clearDeviceAssociation() async { + try { + Logger.i(_tag, '清除设备关联'); + await _bleManager.clearPairedDevices(); + } catch (e) { + Logger.e(_tag, '清除设备关联失败: $e'); + } } - - // ==================== 公共属性 ==================== - - /// 当前连接的经典蓝牙设备 - BluetoothDevice? get currentDevice => _currentClassicDevice; - - /// 是否可以跳过配对 - bool get canSkip => - !isLoading.value && - ((isManualMode.value && isReadyToStart.value) || // 手动模式待机状态可以跳过 - state == PairingState.exitNoClassic || - state == PairingState.exitNotMatch || - state == PairingState.exitTimeout); - - /// 是否显示操作按钮 - bool get showActionButton => - state == PairingState.exitNoPermission || - state == PairingState.exitBtOff || - state == PairingState.exitNoClassic || - state == PairingState.exitNotMatch || - state == PairingState.exitTimeout || - state == PairingState.exitPairFailed || - state == PairingState.exitBound || - state == PairingState.exitIllegal; -} +} \ No newline at end of file diff --git a/lib/modules/pairing/views/pairing_view.dart b/lib/modules/pairing/views/pairing_view.dart index 9d8da4845..b0cf500c3 100644 --- a/lib/modules/pairing/views/pairing_view.dart +++ b/lib/modules/pairing/views/pairing_view.dart @@ -2,8 +2,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../controllers/pairing_controller.dart'; -import '../../../core/widgets/common_bottom_nav.dart'; -import '../../../core/themes/app_theme.dart'; class PairingView extends GetView { const PairingView({Key? key}) : super(key: key); @@ -97,28 +95,25 @@ class PairingView extends GetView { // 设备动画 - 根据状态显示不同的动画效果 Widget _buildDeviceAnimation(bool isDarkMode) { return Obx(() { - final state = controller.state; + final state = controller.currentState; final isLoading = controller.isLoading.value; - final isReadyToStart = controller.isReadyToStart.value; - final isManualMode = controller.isManualMode.value; - // 根据状态决定动画颜色 Color primaryColor = Color(0xFF0066CC); Color secondaryColor = Color(0xFF5E5CE6); // 错误状态使用红色 - if (state.name.startsWith('exit')) { + if (state == PairingState.error) { primaryColor = Colors.red; secondaryColor = Colors.redAccent; } // 成功状态使用绿色 - else if (state == PairingState.paired) { + else if (state == PairingState.completed) { primaryColor = Colors.green; secondaryColor = Colors.greenAccent; } - // 手动模式待机状态的特殊处理 - if (isManualMode && isReadyToStart) { + // 待机状态的特殊处理 + if (state == PairingState.idle || state == PairingState.waitingClassic) { return Container( width: 240.w, height: 240.w, @@ -237,19 +232,24 @@ class PairingView extends GetView { // 根据状态获取图标 IconData _getIconForState(PairingState state) { switch (state) { - case PairingState.paired: + case PairingState.completed: return Icons.check_circle; - case PairingState.exitNoPermission: - case PairingState.exitBtOff: - return Icons.bluetooth_disabled; - case PairingState.exitNoClassic: - case PairingState.exitNotMatch: - return Icons.headphones_outlined; - case PairingState.exitPairFailed: - case PairingState.exitTimeout: - case PairingState.exitIllegal: - case PairingState.exitBound: - return Icons.error_outline; + case PairingState.associating: + return Icons.link; + case PairingState.error: + final errorType = controller.errorType; + switch (errorType) { + case ErrorType.bluetoothOff: + case ErrorType.noPermission: + return Icons.bluetooth_disabled; + case ErrorType.noClassicDevice: + case ErrorType.deviceNotMatch: + return Icons.headphones_outlined; + case ErrorType.associationFailed: + return Icons.link_off; + default: + return Icons.error_outline; + } default: return Icons.headphones; } @@ -258,8 +258,8 @@ class PairingView extends GetView { // 状态信息 Widget _buildStatusInfo(bool isDarkMode) { return Obx(() { - final status = controller.statusDescription.value; - final state = controller.state; + final status = controller.statusText.value; + final state = controller.currentState; final device = controller.currentDevice; return Column( @@ -289,10 +289,9 @@ class PairingView extends GetView { // 当前连接设备(有设备时才显示) if (device != null && - (state == PairingState.classicOk || - state == PairingState.serverVerify || - state == PairingState.pairing || - state == PairingState.paired)) ...[ + (state == PairingState.verifying || + state == PairingState.waitingBle || + state == PairingState.completed)) ...[ SizedBox(height: 24.h), _buildConnectedDevice(device, isDarkMode), ], @@ -303,37 +302,43 @@ class PairingView extends GetView { // 根据状态获取副标题 String _getSubtitleForState(PairingState state) { - if (controller.isManualMode.value && controller.isReadyToStart.value) { - return '点击下方按钮开始配对您的DeapSound设备'; - } - switch (state) { - case PairingState.init: - case PairingState.btReady: - return controller.isManualMode.value ? '准备开始配对' : '正在准备配对环境...'; - case PairingState.classicOk: + case PairingState.idle: + return '准备开始配对'; + case PairingState.checking: + return '正在准备配对环境...'; + case PairingState.waitingClassic: return '请先通过系统蓝牙连接DeapSound设备'; - case PairingState.serverVerify: - case PairingState.pairing: + case PairingState.verifying: + return '正在验证设备...'; + case PairingState.associating: + return '正在建立设备关联...'; + case PairingState.waitingBle: return '请稍候,正在完成配对...'; - case PairingState.paired: + case PairingState.completed: return '配对成功!即将跳转到主页...'; - case PairingState.exitNoPermission: - return '需要蓝牙权限才能继续'; - case PairingState.exitBtOff: - return '请开启蓝牙后重试'; - case PairingState.exitNoClassic: - return '请先通过系统蓝牙连接DeapSound设备'; - case PairingState.exitNotMatch: - return '请确保连接的是DeapSound设备'; - case PairingState.exitTimeout: - return '扫描超时,请确保设备在附近'; - case PairingState.exitPairFailed: - return '配对失败,请重试'; - case PairingState.exitBound: - return '该设备已被其他账号绑定'; - case PairingState.exitIllegal: - return '设备验证失败,请检查设备是否正常'; + case PairingState.error: + final errorType = controller.errorType; + switch (errorType) { + case ErrorType.bluetoothOff: + return '请开启蓝牙后重试'; + case ErrorType.noPermission: + return '需要蓝牙权限才能继续'; + case ErrorType.noClassicDevice: + return '请先通过系统蓝牙连接DeapSound设备'; + case ErrorType.deviceNotMatch: + return '请确保连接的是DeapSound设备'; + case ErrorType.deviceBound: + return '该设备已被其他账号绑定'; + case ErrorType.associationFailed: + return '设备关联失败,请重试'; + case ErrorType.bleTimeout: + return '扫描超时,请确保设备在附近'; + case ErrorType.networkError: + return '网络错误,请检查网络连接'; + default: + return '配对失败,请重试'; + } default: return ''; } @@ -411,23 +416,32 @@ class PairingView extends GetView { ); } + // 获取连接按钮图标 + IconData _getConnectButtonIcon() { + final buttonText = controller.connectButtonText.value; + if (buttonText.contains('蓝牙')) { + return Icons.bluetooth; + } else if (buttonText.contains('设置')) { + return Icons.settings; + } + return Icons.bluetooth_searching; + } + // 操作按钮 Widget _buildActionButtons(bool isDarkMode) { return Obx(() { - final showActionButton = controller.showActionButton; - final canSkip = controller.canSkip; - final actionButtonText = controller.actionButtonText.value; - final isReadyToStart = controller.isReadyToStart.value; - final isManualMode = controller.isManualMode.value; + final showConnectButton = controller.showConnectButton.value; + final showRetryButton = controller.showRetryButton.value; + final showSkipButton = controller.showSkipButton.value; return Container( width: double.infinity, child: Column( children: [ - // 手动模式:显示开始配对按钮 - if (isManualMode && isReadyToStart) + // 显示连接按钮 + if (showConnectButton) ElevatedButton( - onPressed: () => controller.startManualPairing(), + onPressed: () => controller.onConnectTap(), style: ElevatedButton.styleFrom( backgroundColor: Color(0xFF0066CC), foregroundColor: Colors.white, @@ -441,10 +455,10 @@ class PairingView extends GetView { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.bluetooth_searching, size: 20.w), + Icon(_getConnectButtonIcon(), size: 20.w), SizedBox(width: 8.w), Text( - '开始配对设备', + controller.connectButtonText.value, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -454,13 +468,12 @@ class PairingView extends GetView { ), ), - // 原有的状态按钮(错误处理等) - if (showActionButton && !isManualMode) + // 显示重试按钮 + if (showRetryButton) ElevatedButton( - onPressed: () => controller.handleActionButtonTap(), + onPressed: () => controller.onRetryTap(), style: ElevatedButton.styleFrom( - backgroundColor: - _getButtonColor(controller.state, isDarkMode), + backgroundColor: Colors.red, foregroundColor: Colors.white, padding: EdgeInsets.symmetric(vertical: 16.h), shape: RoundedRectangleBorder( @@ -470,7 +483,7 @@ class PairingView extends GetView { minimumSize: Size(double.infinity, 56.h), ), child: Text( - actionButtonText, + controller.retryButtonText.value, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, @@ -478,11 +491,11 @@ class PairingView extends GetView { ), ), - // 跳过按钮和重置按钮 - if (canSkip) ...[ + // 跳过按钮 + if (showSkipButton) ...[ SizedBox(height: 12.h), TextButton( - onPressed: () => controller.skipPairing(), + onPressed: () => controller.onSkipTap(), style: TextButton.styleFrom( foregroundColor: Color(0xFF0066CC), padding: EdgeInsets.symmetric(vertical: 12.h), @@ -498,41 +511,10 @@ class PairingView extends GetView { ), ], - // 添加重置按钮(在错误状态下) - if (!isManualMode && - !controller.isLoading.value && - controller.state.name.startsWith('exit')) ...[ - SizedBox(height: 8.h), - TextButton( - onPressed: () => controller.resetToManualMode(), - style: TextButton.styleFrom( - foregroundColor: Colors.grey, - padding: EdgeInsets.symmetric(vertical: 8.h), - ), - child: Text( - '重新配对', - style: TextStyle(fontSize: 14.sp), - ), - ), - ], ], ), ); }); } - // 根据状态获取按钮颜色 - Color _getButtonColor(PairingState state, bool isDarkMode) { - switch (state) { - case PairingState.exitNoPermission: - case PairingState.exitBtOff: - return isDarkMode ? Colors.grey[800]! : Colors.grey[600]!; - case PairingState.exitPairFailed: - case PairingState.exitBound: - case PairingState.exitIllegal: - return Colors.red; - default: - return Color(0xFF0066CC); - } - } } diff --git a/lib/modules/pairing/views/pairing_view_new.dart b/lib/modules/pairing/views/pairing_view_new.dart new file mode 100644 index 000000000..eae654a1b --- /dev/null +++ b/lib/modules/pairing/views/pairing_view_new.dart @@ -0,0 +1,258 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../controllers/pairing_controller.dart'; +import '../../../core/themes/app_theme.dart'; + +/// 配对视图 - 简化版本 +class PairingView extends GetView { + const PairingView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final bool isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + body: SafeArea( + child: Padding( + padding: EdgeInsets.all(24.w), + child: Column( + children: [ + // 顶部标题 + _buildHeader(isDarkMode), + + // 中间内容 + Expanded( + child: Center( + child: Obx(() => _buildContent(isDarkMode)), + ), + ), + + // 底部按钮 + Obx(() => _buildButtons(isDarkMode)), + + SizedBox(height: 20.h), + ], + ), + ), + ), + ); + } + + /// 构建标题 + Widget _buildHeader(bool isDarkMode) { + return Column( + children: [ + Text( + '设备配对', + style: TextStyle( + fontSize: 24.sp, + fontWeight: FontWeight.bold, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + SizedBox(height: 8.h), + Text( + '连接您的 DeepSound 耳机', + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ), + SizedBox(height: 40.h), + ], + ); + } + + /// 构建主要内容 + Widget _buildContent(bool isDarkMode) { + final state = controller.currentState; + final isLoading = controller.isLoading.value; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 状态图标 + _buildStateIcon(state, isLoading, isDarkMode), + + SizedBox(height: 40.h), + + // 状态文本 + Text( + controller.statusText.value, + style: TextStyle( + fontSize: 16.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + textAlign: TextAlign.center, + ), + + // 错误详情 + if (state == PairingState.error) ...[ + SizedBox(height: 16.h), + Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + borderRadius: BorderRadius.circular(12.r), + border: Border.all(color: Colors.red.withOpacity(0.3)), + ), + child: Row( + children: [ + Icon( + Icons.error_outline, + color: Colors.red, + size: 20.sp, + ), + SizedBox(width: 12.w), + Expanded( + child: Text( + controller.errorMessage, + style: TextStyle( + fontSize: 14.sp, + color: Colors.red, + ), + ), + ), + ], + ), + ), + ], + ], + ); + } + + /// 构建状态图标 + Widget _buildStateIcon(PairingState state, bool isLoading, bool isDarkMode) { + if (isLoading) { + return CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Theme.of(Get.context!).primaryColor, + ), + ); + } + + IconData iconData; + Color iconColor; + + switch (state) { + case PairingState.idle: + case PairingState.checking: + iconData = Icons.bluetooth_searching; + iconColor = Colors.blue; + break; + + case PairingState.waitingClassic: + iconData = Icons.bluetooth; + iconColor = Colors.grey; + break; + + case PairingState.verifying: + iconData = Icons.verified_user; + iconColor = Colors.orange; + break; + + case PairingState.associating: + iconData = Icons.link; + iconColor = Colors.blue; + break; + + case PairingState.waitingBle: + iconData = Icons.sync; + iconColor = Colors.blue; + break; + + case PairingState.completed: + iconData = Icons.check_circle; + iconColor = Colors.green; + break; + + case PairingState.error: + iconData = Icons.error; + iconColor = Colors.red; + break; + } + + return Container( + width: 100.w, + height: 100.w, + decoration: BoxDecoration( + color: iconColor.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon( + iconData, + size: 60.sp, + color: iconColor, + ), + ); + } + + /// 构建按钮 + Widget _buildButtons(bool isDarkMode) { + final showConnectButton = controller.showConnectButton.value; + final showRetryButton = controller.showRetryButton.value; + final showSkipButton = controller.showSkipButton.value; + + return Column( + children: [ + // 主要操作按钮 + if (showConnectButton) + _buildPrimaryButton( + '连接设备', + controller.onConnectTap, + isDarkMode, + ), + + if (showRetryButton) + _buildPrimaryButton( + '重试', + controller.onRetryTap, + isDarkMode, + ), + + SizedBox(height: 12.h), + + // 跳过按钮 + if (showSkipButton) + TextButton( + onPressed: controller.onSkipTap, + child: Text( + '跳过', + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ), + ), + ], + ); + } + + /// 构建主要按钮 + Widget _buildPrimaryButton(String text, VoidCallback onPressed, bool isDarkMode) { + return SizedBox( + width: double.infinity, + height: 48.h, + child: ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(Get.context!).primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + elevation: 0, + ), + child: Text( + text, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index fd468e296..4631245f0 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -42,6 +42,8 @@ import '../modules/ota_upgrade/views/ota_upgrade_view.dart'; import '../modules/ota_upgrade/bindings/ota_upgrade_binding.dart'; import '../modules/realtime/bindings/realtime_binding.dart'; import '../modules/realtime/views/realtime_view.dart'; +import '../modules/music/views/music_playlist_view.dart'; +import '../modules/music/bindings/music_playlist_binding.dart'; abstract class AppPages { static final pages = [ @@ -206,5 +208,12 @@ abstract class AppPages { page: () => const RealtimeView(), binding: RealtimeBinding(), ), + GetPage( + name: Routes.musicPlaylist, + page: () => const MusicPlaylistView(), + binding: MusicPlaylistBinding(), + transition: Transition.rightToLeft, + transitionDuration: Duration(milliseconds: 250), + ), ]; } diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 066c8af22..3e2e1be89 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -46,6 +46,9 @@ abstract class Routes { static const agent = '/agent'; // 在线客服 static const navigation = '/navigation'; // 实时导航 + // 音乐相关 + static const musicPlaylist = '/music/playlist'; // 音乐播放列表 + // 实时语音对话 static const realtime = '/realtime'; } diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index a99c84f5f..8bfd57230 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -631,7 +631,7 @@ object AgentService : CoroutineScope { ) val _systemPrompt = Utils.fillTemplate(systemPrompt,location) messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt)) - Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}") + // Log.d(TAG, "liwei--------- 系统提示词: ${_systemPrompt}") } // 再添加历史消息 diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt index ecf154a17..9ffd523bc 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt @@ -391,7 +391,7 @@ class BleCompanionManager(private val context: Context) { Log.e(TAG, "注册后台扫描失败: 无法获取蓝牙扫描器") return } - + unregisterBackgroundScan() // 构建过滤器 val filter = ScanFilter.Builder() .setDeviceAddress(mac) diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt index b4dc2a923..2e04578f3 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt @@ -43,6 +43,39 @@ class MusicService : MediaBrowserServiceCompat() { fun isServiceRunning() = isRunning.get() fun isPlaying() = currentStatus fun getCurrentUrl(): String? = currentUrlInternal + + // 获取播放进度信息 + fun getProgress(): Map { + return if (instance != null && instance!!.mediaPlayer != null) { + val player = instance!!.mediaPlayer!! + try { + val currentPosition = if (player.isPlaying || currentStatus) player.currentPosition else 0 + val duration = player.duration + val progress = if (duration > 0) currentPosition.toDouble() / duration.toDouble() else 0.0 + + mapOf( + "currentPosition" to currentPosition, + "duration" to duration, + "progress" to progress + ) + } catch (e: Exception) { + Log.e(TAG, "获取播放进度失败: ${e.message}") + mapOf( + "currentPosition" to 0, + "duration" to 0, + "progress" to 0.0 + ) + } + } else { + mapOf( + "currentPosition" to 0, + "duration" to 0, + "progress" to 0.0 + ) + } + } + + private var instance: MusicService? = null } private var isReceiverRegistered = false private lateinit var mediaSession: MediaSessionCompat @@ -51,6 +84,7 @@ class MusicService : MediaBrowserServiceCompat() { override fun onCreate() { super.onCreate() + instance = this isRunning.set(true) setupMediaSession() startForeground(NOTIFICATION_ID, createNotification("等待播放","DeepSound","")) @@ -116,6 +150,12 @@ class MusicService : MediaBrowserServiceCompat() { "stop" -> stopSelf() "next" -> if (currentIndex + 1 < playlist.size) play(playlist[++currentIndex]) "previous" -> if (currentIndex - 1 >= 0) play(playlist[--currentIndex]) + "seekTo" -> { + val position = intent.getIntExtra("position", -1) + if (position >= 0) { + seekTo(position) + } + } } return START_STICKY } @@ -401,6 +441,19 @@ class MusicService : MediaBrowserServiceCompat() { else stopSelf() } + private fun seekTo(positionMs: Int) { + mediaPlayer?.let { player -> + try { + if (player.isPlaying || currentStatus) { + player.seekTo(positionMs) + Log.i(TAG, "跳转到位置: ${positionMs}ms") + } + } catch (e: Exception) { + Log.e(TAG, "跳转失败: ${e.message}") + } + } + } + private fun updateNotification(title: String,artist: String,coverUrl: String?) { val notification = createNotification(title, artist, coverUrl) val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt index b5cfd25cb..ba83f5209 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt @@ -70,6 +70,19 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler { ) result.success(status) } + "getProgress" -> { + val progressInfo = MusicService.getProgress() + result.success(progressInfo) + } + "seekTo" -> { + val position = call.argument("position") + if (position != null) { + MusicServiceStarter.startServiceWithCommand(context, "seekTo", position = position) + result.success(null) + } else { + result.error("INVALID_ARGUMENT", "Missing 'position' parameter", null) + } + } else -> result.notImplemented() } } diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt index 91ee32b28..08821753a 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt @@ -18,7 +18,7 @@ import java.util.jar.Manifest object MusicServiceStarter { private val TAG = "MusicServiceStarter" //播放单曲 - fun startServiceWithCommand(context: Context, command: String, song: Map? = null, volume: Float? = null) { + fun startServiceWithCommand(context: Context, command: String, song: Map? = null, volume: Float? = null, position: Int? = null) { Log.i(TAG, "liwei------------------- startServiceWithCommand: $command $song") val intent = Intent(context, MusicService::class.java).apply { putExtra("command", command) @@ -26,6 +26,7 @@ object MusicServiceStarter { putExtra("song", HashMap(it)) // 只有非 null 时才 putExtra } volume?.let { putExtra("volume", it) } + position?.let { putExtra("position", it) } } val success = try { ContextCompat.startForegroundService(context, intent) diff --git a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift index 92c4e9a21..81aa3d2c3 100644 --- a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift +++ b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift @@ -75,6 +75,36 @@ public class MusicService: NSObject { func setVolume(volume: Float) { player?.volume = volume } + + func getProgress() -> [String: Any] { + guard let player = player, let currentItem = player.currentItem else { + return [ + "currentPosition": 0, + "duration": 0, + "progress": 0.0 + ] + } + + let currentTime = CMTimeGetSeconds(player.currentTime()) + let duration = CMTimeGetSeconds(currentItem.duration) + + // 处理无效值 + let currentPosition = currentTime.isNaN || currentTime.isInfinite ? 0 : Int(currentTime * 1000) + let totalDuration = duration.isNaN || duration.isInfinite ? 0 : Int(duration * 1000) + let progress = totalDuration > 0 ? currentTime / duration : 0.0 + + return [ + "currentPosition": currentPosition, + "duration": totalDuration, + "progress": progress.isNaN || progress.isInfinite ? 0.0 : progress + ] + } + + func seekTo(positionMs: Int) { + guard let player = player else { return } + let time = CMTime(seconds: Double(positionMs) / 1000.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + player.seek(to: time) + } private func setupRemoteCommandCenter() { let commandCenter = MPRemoteCommandCenter.shared() diff --git a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift index 5a9a1180e..61e1a844e 100644 --- a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift +++ b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift @@ -65,6 +65,20 @@ public class MusicServicePlugin: NSObject, FlutterPlugin { "url": currentUrl != nil ? currentUrl! : NSNull() ] result(status) + + case "getProgress": + let progressInfo = MusicService.shared.getProgress() + result(progressInfo) + + case "seekTo": + if let args = call.arguments as? [String: Any], + let position = args["position"] as? Int { + MusicService.shared.seekTo(positionMs: position) + result(nil) + } else { + result(FlutterError(code: "INVALID_ARGUMENT", message: "Missing or invalid 'position'", details: nil)) + } + default: result(FlutterMethodNotImplemented) } diff --git a/local_plugins/music_service/lib/music_service.dart b/local_plugins/music_service/lib/music_service.dart index 082e2e2c8..27c80b8e9 100644 --- a/local_plugins/music_service/lib/music_service.dart +++ b/local_plugins/music_service/lib/music_service.dart @@ -59,4 +59,19 @@ class MusicService { 'url': result?['url'] ?? "", }; } + + /// 获取播放进度信息 + Future> getProgress() async { + final result = await _methodChannel.invokeMethod('getProgress'); + return { + 'currentPosition': result?['currentPosition'] ?? 0, // 当前播放位置(毫秒) + 'duration': result?['duration'] ?? 0, // 总时长(毫秒) + 'progress': result?['progress'] ?? 0.0, // 播放进度(0.0-1.0) + }; + } + + /// 跳转到指定位置 + Future seekTo(int positionMs) async { + await _methodChannel.invokeMethod('seekTo', {'position': positionMs}); + } } diff --git a/test.json b/test.json index 178eae9f1..500557378 100644 --- a/test.json +++ b/test.json @@ -15,7 +15,7 @@ adb shell am broadcast \ adb shell dumpsys package com.yunqiinnovation.deepsound | grep receiver - adb logcat -v time | grep -i BleForegroundService + adb logcat -v time | grep -Ei "BleWakeReceiver|BleMediaService"