From 55b1d5082f25802a66b2e97c25e9e74131d354c4 Mon Sep 17 00:00:00 2001 From: wolfplus Date: Sat, 15 Mar 2025 16:56:32 +0000 Subject: [PATCH] 009 --- .../deep_voice/ClassicBluetoothHelper.kt | 242 ++++++++ .../com/example/deep_voice/MainActivity.kt | 222 ++++++- lib/core/bindings/initial_binding.dart | 4 + lib/data/services/bluetooth_service.dart | 436 ------------- .../services/classic_bluetooth_service.dart | 452 ++++++++++++++ lib/main.dart | 3 +- .../home/controllers/home_controller.dart | 571 +++++------------- .../controllers/pairing_controller.dart | 366 ++++------- lib/modules/pairing/views/pairing_view.dart | 489 +++++++-------- .../translation/views/translation_view.dart | 66 +- pubspec.yaml | 1 + 11 files changed, 1444 insertions(+), 1408 deletions(-) create mode 100644 android/app/src/main/kotlin/com/example/deep_voice/ClassicBluetoothHelper.kt delete mode 100644 lib/data/services/bluetooth_service.dart create mode 100644 lib/data/services/classic_bluetooth_service.dart diff --git a/android/app/src/main/kotlin/com/example/deep_voice/ClassicBluetoothHelper.kt b/android/app/src/main/kotlin/com/example/deep_voice/ClassicBluetoothHelper.kt new file mode 100644 index 000000000..9e2cdb28e --- /dev/null +++ b/android/app/src/main/kotlin/com/example/deep_voice/ClassicBluetoothHelper.kt @@ -0,0 +1,242 @@ +package com.example.deep_voice + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothProfile +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.util.Log +import android.Manifest +import androidx.core.content.ContextCompat +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +/** + * 经典蓝牙辅助类,用于获取已连接的蓝牙设备信息 + */ +class ClassicBluetoothHelper(private val context: Context) { + + private val TAG = "ClassicBluetoothHelper" + + // 缓存蓝牙代理对象 + private var a2dpProxy: BluetoothProfile? = null + private var headsetProxy: BluetoothProfile? = null + + /** + * 获取已连接的A2DP设备(音频输出设备) + * @param callback 回调接口,返回设备列表或错误信息 + */ + fun getConnectedA2dpDevices(callback: BluetoothDevicesCallback) { + getConnectedBluetoothDevices(BluetoothProfile.A2DP, callback) + } + + /** + * 获取已连接的蓝牙耳机设备 + * @param callback 回调接口,返回设备列表或错误信息 + */ + fun getConnectedHeadsetDevices(callback: BluetoothDevicesCallback) { + getConnectedBluetoothDevices(BluetoothProfile.HEADSET, callback) + } + + /** + * 检查是否有蓝牙连接权限 + * @return 是否有权限 + */ + private fun hasBluetoothConnectPermission(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + ContextCompat.checkSelfPermission( + context, + Manifest.permission.BLUETOOTH_CONNECT + ) == PackageManager.PERMISSION_GRANTED + } else { + true + } + } + + /** + * 检查蓝牙是否启用 + * @return 蓝牙是否启用 + */ + fun isBluetoothEnabled(): Boolean { + try { + val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() + if (bluetoothAdapter == null) { + Log.w(TAG, "设备不支持蓝牙") + return false + } + + return bluetoothAdapter.isEnabled + } catch (e: Exception) { + Log.e(TAG, "检查蓝牙状态失败: ${e.message}") + return false + } + } + + /** + * 获取指定配置文件的已连接蓝牙设备 + * @param profile 蓝牙配置文件类型,如 BluetoothProfile.A2DP 或 BluetoothProfile.HEADSET + * @param callback 回调接口,返回设备列表或错误信息 + */ + private fun getConnectedBluetoothDevices(profile: Int, callback: BluetoothDevicesCallback) { + val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() + if (bluetoothAdapter == null) { + Log.e(TAG, "没有找到蓝牙适配器") + callback.onError("NO_ADAPTER", "没有找到蓝牙适配器") + return + } + + // 检查蓝牙权限 + if (!hasBluetoothConnectPermission()) { + Log.e(TAG, "缺少BLUETOOTH_CONNECT权限") + callback.onError("PERMISSION_DENIED", "缺少蓝牙连接权限,请在应用设置中授予该权限") + return + } + + try { + // 检查是否已经有缓存的代理对象 + val cachedProxy = when (profile) { + BluetoothProfile.A2DP -> a2dpProxy + BluetoothProfile.HEADSET -> headsetProxy + else -> null + } + + if (cachedProxy != null) { + // 使用已缓存的代理对象 + processDeviceList(cachedProxy, profile, callback) + return + } + + // 使用原子布尔值确保回调只被调用一次 + val callbackCalled = AtomicBoolean(false) + + // 使用CountDownLatch等待服务连接 + val latch = CountDownLatch(1) + + // 通过 getProfileProxy 获取指定 Profile (A2DP/HEADSET) 的连接状态 + bluetoothAdapter.getProfileProxy(context, + object : BluetoothProfile.ServiceListener { + override fun onServiceConnected(profileId: Int, proxy: BluetoothProfile) { + Log.d(TAG, "蓝牙配置文件服务已连接: $profileId") + if (profileId == profile) { + // 缓存代理对象 + when (profileId) { + BluetoothProfile.A2DP -> a2dpProxy = proxy + BluetoothProfile.HEADSET -> headsetProxy = proxy + } + + try { + processDeviceList(proxy, profileId, callback) + latch.countDown() + } catch (e: Exception) { + Log.e(TAG, "处理设备信息时出错: ${e.message}") + + // 确保回调只被调用一次 + if (callbackCalled.compareAndSet(false, true)) { + callback.onError("DEVICE_ERROR", "处理设备信息时出错: ${e.message}") + } + latch.countDown() + } + } + } + + override fun onServiceDisconnected(profileId: Int) { + // Profile 断开连接 + Log.e(TAG, "蓝牙配置文件服务断开连接: $profileId") + + // 清除对应的缓存代理 + when (profileId) { + BluetoothProfile.A2DP -> a2dpProxy = null + BluetoothProfile.HEADSET -> headsetProxy = null + } + + // 确保回调只被调用一次,但不在这里调用错误回调 + // 避免服务断开时也返回错误导致重复回调 + if (callbackCalled.compareAndSet(false, true) && !latch.await(0, TimeUnit.MILLISECONDS)) { + callback.onError("SERVICE_DISCONNECTED", "蓝牙配置文件服务断开连接") + latch.countDown() + } + } + }, profile + ) + + // 设置超时处理 + Thread { + try { + // 最多等待3秒 + if (!latch.await(3, TimeUnit.SECONDS)) { + Log.e(TAG, "获取蓝牙设备超时") + if (callbackCalled.compareAndSet(false, true)) { + callback.onError("TIMEOUT", "获取蓝牙设备超时") + } + } + } catch (e: InterruptedException) { + Log.e(TAG, "等待蓝牙服务被中断: ${e.message}") + if (callbackCalled.compareAndSet(false, true)) { + callback.onError("INTERRUPTED", "等待蓝牙服务被中断: ${e.message}") + } + } + }.start() + + } catch (e: Exception) { + Log.e(TAG, "获取蓝牙设备失败: ${e.message}") + callback.onError("BLUETOOTH_ERROR", "获取蓝牙设备失败: ${e.message}") + } + } + + /** + * 处理设备列表 + */ + private fun processDeviceList(proxy: BluetoothProfile, profileId: Int, callback: BluetoothDevicesCallback) { + // 获取已连接设备 + val connectedDevices = proxy.connectedDevices + + // 转换为设备信息列表 + val devicesList = connectedDevices.map { device -> + mapOf( + "name" to (device.name ?: "未知设备"), + "address" to device.address + ) + } + + Log.d(TAG, "获取到的设备列表:${devicesList.size}个设备") + + // 返回设备列表(不关闭代理) + callback.onSuccess(devicesList) + } + + /** + * 释放资源 + */ + fun dispose() { + val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() ?: return + + // 释放A2DP代理 + a2dpProxy?.let { + try { + bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, it) + } catch (e: Exception) { + Log.e(TAG, "关闭A2DP代理失败: ${e.message}") + } + a2dpProxy = null + } + + // 释放HEADSET代理 + headsetProxy?.let { + try { + bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, it) + } catch (e: Exception) { + Log.e(TAG, "关闭HEADSET代理失败: ${e.message}") + } + headsetProxy = null + } + } + + /** + * 蓝牙设备回调接口 + */ + interface BluetoothDevicesCallback { + fun onSuccess(devices: List>) + fun onError(code: String, message: String) + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt index 96b4dc652..968652ae1 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt @@ -15,6 +15,8 @@ import android.content.BroadcastReceiver import android.content.IntentFilter import android.os.Handler import android.os.Looper +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice class MainActivity: FlutterActivity() { @@ -23,11 +25,15 @@ class MainActivity: FlutterActivity() { private val AZURE_TTS_CHANNEL = "com.deep_voice.azure_tts" private val VOICE_INTERACTION_CHANNEL = "com.deep_voice.voice_interaction" private val VOICE_INTERACTION_EVENT_CHANNEL = "com.deep_voice.voice_interaction_events" + private val CLASSIC_BLUETOOTH_CHANNEL = "com.deep_voice.classic_bluetooth" + private val CLASSIC_BLUETOOTH_EVENT_CHANNEL = "com.deep_voice.classic_bluetooth_events" private val TAG = "MainActivity" private lateinit var azureAsrHelper: AzureAsrHelper private lateinit var azureTtsHelper: AzureTtsHelper + private lateinit var classicBluetoothHelper: ClassicBluetoothHelper private var azureAsrEventSink: EventChannel.EventSink? = null private var voiceInteractionEventSink: EventChannel.EventSink? = null + private var bluetoothEventSink: EventChannel.EventSink? = null // 广播接收器 private val voiceInteractionReceiver = object : BroadcastReceiver() { @@ -59,6 +65,75 @@ class MainActivity: FlutterActivity() { } } + // 蓝牙状态变化广播接收器 + private val bluetoothStateReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + when (intent?.action) { + BluetoothDevice.ACTION_ACL_CONNECTED -> { + val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) + } + + if (device != null) { + Log.d(TAG, "蓝牙设备已连接: ${device.name ?: "未知设备"} (${device.address})") + sendBluetoothEvent(mapOf( + "type" to "deviceConnected", + "device" to mapOf( + "name" to (device.name ?: "未知设备"), + "address" to device.address + ), + "timestamp" to System.currentTimeMillis() + )) + } + } + BluetoothDevice.ACTION_ACL_DISCONNECTED -> { + val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) + } + + if (device != null) { + Log.d(TAG, "蓝牙设备已断开: ${device.name ?: "未知设备"} (${device.address})") + sendBluetoothEvent(mapOf( + "type" to "deviceDisconnected", + "device" to mapOf( + "name" to (device.name ?: "未知设备"), + "address" to device.address + ), + "timestamp" to System.currentTimeMillis() + )) + } + } + BluetoothAdapter.ACTION_STATE_CHANGED -> { + val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR) + when (state) { + BluetoothAdapter.STATE_ON -> { + Log.d(TAG, "蓝牙已开启") + sendBluetoothEvent(mapOf( + "type" to "bluetoothStateChanged", + "state" to "on", + "timestamp" to System.currentTimeMillis() + )) + } + BluetoothAdapter.STATE_OFF -> { + Log.d(TAG, "蓝牙已关闭") + sendBluetoothEvent(mapOf( + "type" to "bluetoothStateChanged", + "state" to "off", + "timestamp" to System.currentTimeMillis() + )) + } + } + } + } + } + } + // 使用伴生对象存储配置信息,使其在整个应用中可访问 companion object { // Azure 语音服务配置 @@ -74,9 +149,10 @@ class MainActivity: FlutterActivity() { Log.d(TAG, "初始化本地的 Azure 语音服务实例") azureAsrHelper = AzureAsrHelper() azureTtsHelper = AzureTtsHelper() + classicBluetoothHelper = ClassicBluetoothHelper(applicationContext) // 注册广播接收器 - registerVoiceInteractionReceiver() + registerReceivers() // 确保 Flutter 引擎准备好后立即设置事件通道 flutterEngine?.let { engine -> @@ -85,15 +161,21 @@ class MainActivity: FlutterActivity() { } } - /** - * 注册语音交互服务的广播接收器 - */ - private fun registerVoiceInteractionReceiver() { - val intentFilter = IntentFilter().apply { + private fun registerReceivers() { + // 注册语音交互广播接收器 + val voiceInteractionFilter = IntentFilter().apply { addAction(VoiceInteractionService.ACTION_RECOGNITION_STARTED) addAction(VoiceInteractionService.ACTION_CHAT_HISTORY_UPDATED) } - registerReceiver(voiceInteractionReceiver, intentFilter) + registerReceiver(voiceInteractionReceiver, voiceInteractionFilter) + + // 注册蓝牙状态变化广播接收器 + val bluetoothFilter = IntentFilter().apply { + addAction(BluetoothDevice.ACTION_ACL_CONNECTED) + addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED) + addAction(BluetoothAdapter.ACTION_STATE_CHANGED) + } + registerReceiver(bluetoothStateReceiver, bluetoothFilter) } override fun configureFlutterEngine(flutterEngine: FlutterEngine) { @@ -149,6 +231,26 @@ class MainActivity: FlutterActivity() { } ) + // 设置蓝牙事件通道 + EventChannel(flutterEngine.dartExecutor.binaryMessenger, CLASSIC_BLUETOOTH_EVENT_CHANNEL).setStreamHandler( + object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + Log.d(TAG, "蓝牙事件通道开始监听") + bluetoothEventSink = events + // 发送就绪事件 + events?.success(mapOf( + "type" to "channelReady", + "timestamp" to System.currentTimeMillis() + )) + } + + override fun onCancel(arguments: Any?) { + Log.d(TAG, "蓝牙事件通道停止监听") + bluetoothEventSink = null + } + } + ) + } catch (e: Exception) { Log.e(TAG, "设置事件通道时发生错误: ${e.message}", e) } @@ -158,7 +260,9 @@ class MainActivity: FlutterActivity() { * 设置方法通道 */ private fun setupMethodChannels(flutterEngine: FlutterEngine) { - // Azure ASR 方法通道 + Log.d(TAG, "开始设置方法通道") + + // 设置 Azure ASR 方法通道 MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "initialize" -> { @@ -308,7 +412,7 @@ class MainActivity: FlutterActivity() { } } - // Azure TTS 方法通道 + // 设置 Azure TTS 方法通道 MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_TTS_CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "initialize" -> { @@ -387,7 +491,7 @@ class MainActivity: FlutterActivity() { } } - // 语音交互服务通道 + // 设置语音交互方法通道 MethodChannel(flutterEngine.dartExecutor.binaryMessenger, VOICE_INTERACTION_CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "startVoiceInteractionService" -> { @@ -409,6 +513,79 @@ class MainActivity: FlutterActivity() { } } } + + // 设置经典蓝牙方法通道 + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CLASSIC_BLUETOOTH_CHANNEL).setMethodCallHandler { call, result -> + when (call.method) { + "getConnectedA2dpDevices" -> { + try { + classicBluetoothHelper.getConnectedA2dpDevices(object : ClassicBluetoothHelper.BluetoothDevicesCallback { + override fun onSuccess(devices: List>) { + try { + result.success(devices) + } catch (e: Exception) { + Log.e(TAG, "返回A2DP设备时发生异常: ${e.message}") + } + } + + override fun onError(code: String, message: String) { + try { + result.error(code, message, null) + } catch (e: Exception) { + Log.e(TAG, "返回A2DP设备错误时发生异常: ${e.message}") + } + } + }) + } catch (e: Exception) { + try { + Log.e(TAG, "获取A2DP设备时发生异常: ${e.message}") + result.error("UNEXPECTED_ERROR", "获取A2DP设备时发生异常: ${e.message}", e.toString()) + } catch (e2: Exception) { + Log.e(TAG, "处理A2DP设备获取异常时发生二次异常: ${e2.message}") + } + } + } + "getConnectedHeadsetDevices" -> { + try { + classicBluetoothHelper.getConnectedHeadsetDevices(object : ClassicBluetoothHelper.BluetoothDevicesCallback { + override fun onSuccess(devices: List>) { + try { + result.success(devices) + } catch (e: Exception) { + Log.e(TAG, "返回耳机设备时发生异常: ${e.message}") + } + } + + override fun onError(code: String, message: String) { + try { + result.error(code, message, null) + } catch (e: Exception) { + Log.e(TAG, "返回耳机设备错误时发生异常: ${e.message}") + } + } + }) + } catch (e: Exception) { + try { + Log.e(TAG, "获取耳机设备时发生异常: ${e.message}") + result.error("UNEXPECTED_ERROR", "获取耳机设备时发生异常: ${e.message}", e.toString()) + } catch (e2: Exception) { + Log.e(TAG, "处理耳机设备获取异常时发生二次异常: ${e2.message}") + } + } + } + "isBluetoothEnabled" -> { + try { + val isEnabled = classicBluetoothHelper.isBluetoothEnabled() + result.success(isEnabled) + Log.d(TAG, "检查蓝牙状态: ${if (isEnabled) "已启用" else "未启用"}") + } catch (e: Exception) { + Log.e(TAG, "检查蓝牙状态时发生异常: ${e.message}") + result.error("BLUETOOTH_ERROR", "检查蓝牙状态失败: ${e.message}", e.toString()) + } + } + else -> result.notImplemented() + } + } } // ASR 事件发送方法 @@ -517,10 +694,32 @@ class MainActivity: FlutterActivity() { } } + // 发送蓝牙事件到Flutter + private fun sendBluetoothEvent(event: Map) { + if (bluetoothEventSink == null) { + Log.w(TAG, "无法发送蓝牙事件:事件通道未准备好") + return + } + + try { + runOnUiThread { + try { + bluetoothEventSink?.success(event) + Log.d(TAG, "已发送蓝牙事件: ${event["type"]}") + } catch (e: Exception) { + Log.e(TAG, "发送蓝牙事件失败: ${e.message}") + } + } + } catch (e: Exception) { + Log.e(TAG, "转到UI线程发送蓝牙事件失败: ${e.message}") + } + } + override fun onDestroy() { - // 注销广播接收器 + // 注销所有广播接收器 try { unregisterReceiver(voiceInteractionReceiver) + unregisterReceiver(bluetoothStateReceiver) } catch (e: Exception) { Log.e(TAG, "注销广播接收器失败: ${e.message}") } @@ -528,6 +727,7 @@ class MainActivity: FlutterActivity() { // 释放资源 azureAsrHelper.dispose() azureTtsHelper.dispose() + classicBluetoothHelper.dispose() super.onDestroy() } diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index d6df1de2e..9327bdb58 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -6,6 +6,7 @@ import '../../data/services/volcano_translation_service.dart'; import '../../data/services/voice_interaction_service.dart'; import '../../data/services/language_manager.dart'; import '../../data/services/volcano_ai_service.dart'; +import '../../data/services/classic_bluetooth_service.dart'; import '../../core/utils/logger.dart'; /// 初始绑定,用于管理全局依赖 @@ -34,5 +35,8 @@ class InitialBinding extends Bindings { // 注意:VoiceInteractionService 在 HomeController 中初始化 Get.lazyPut(() => VoiceInteractionService(), fenix: true); + + // 经典蓝牙服务 + Get.put(ClassicBluetoothService()..initialize(), permanent: true); } } \ No newline at end of file diff --git a/lib/data/services/bluetooth_service.dart b/lib/data/services/bluetooth_service.dart deleted file mode 100644 index 22e18ddab..000000000 --- a/lib/data/services/bluetooth_service.dart +++ /dev/null @@ -1,436 +0,0 @@ -import 'dart:async'; -import 'package:flutter_blue_plus/flutter_blue_plus.dart'; -import 'package:get/get.dart'; -import '../../core/utils/logger.dart'; -import 'dart:io' show Platform; -import 'package:permission_handler/permission_handler.dart'; - -/// 设备类型枚举 -enum DeviceType { - unknown, // 未知设备类型 - classicAudio, // 经典蓝牙音频设备(耳机等) - ble, // BLE设备 - dual // 双模设备(同时支持经典蓝牙和BLE) -} - -/// 简化的蓝牙设备服务,仅用于获取已连接或配对的蓝牙耳机信息 -class BluetoothService extends GetxService { - static BluetoothService get to => Get.find(); - - // 内部实例 - BluetoothService._internal(); - static final BluetoothService _instance = BluetoothService._internal(); - factory BluetoothService() => _instance; - - // ====== 状态管理 ====== - // 蓝牙适配器状态 - final _isAvailable = false.obs; - final _isOn = false.obs; - final _isInitialized = false.obs; - - // 权限状态 - final _permissionsGranted = false.obs; - - // 设备管理 - final _connectedDevices = [].obs; - final _systemPairedDevices = [].obs; - final _selectedDevice = Rx(null); - final _deviceType = Rx(DeviceType.unknown); - final _connectionState = Rx(BluetoothConnectionState.disconnected); - final _lastConnectedDeviceInfo = Rx>({}); - - // 流订阅 - StreamSubscription? _stateSubscription; - - // 防止重复刷新的标志 - bool _isRefreshingDevices = false; - - // ====== Getters ====== - // 基本getter - bool get isAvailable => _isAvailable.value; - bool get isOn => _isOn.value; - bool get isInitialized => _isInitialized.value; - bool get permissionsGranted => _permissionsGranted.value; - - // 设备相关getter - List get connectedDevices => _connectedDevices; - List get systemPairedDevices => _systemPairedDevices; - BluetoothDevice? get selectedDevice => _selectedDevice.value; - DeviceType get deviceType => _deviceType.value; - BluetoothConnectionState get connectionState => _connectionState.value; - Map get lastConnectedDeviceInfo => _lastConnectedDeviceInfo.value; - - // Observable getters - RxBool get isAvailableObs => _isAvailable; - RxBool get isOnObs => _isOn; - RxBool get isInitializedObs => _isInitialized; - RxBool get permissionsGrantedObs => _permissionsGranted; - RxList get connectedDevicesObs => _connectedDevices; - Rx get selectedDeviceObs => _selectedDevice; - Rx get deviceTypeObs => _deviceType; - Rx get connectionStateObs => _connectionState; - Rx> get lastConnectedDeviceInfoObs => _lastConnectedDeviceInfo; - - /// 初始化蓝牙服务 - Future init() async { - if (_isInitialized.value) { - Logger.info('蓝牙服务已初始化,无需重复初始化'); - return this; - } - - Logger.info('开始初始化蓝牙服务'); - - try { - // 1. 设置蓝牙状态监听 - _setupBluetoothListeners(); - - // 2. 初始检查蓝牙状态 - _checkBluetoothState(); - - // 3. 请求权限 - await requestPermissions(); - - // 4. 刷新系统设备 - if (_isOn.value && _permissionsGranted.value) { - await _refreshSystemDevices(); - } - - // 5. 标记初始化完成 - _isInitialized.value = true; - Logger.info('蓝牙服务初始化完成'); - return this; - } catch (e) { - Logger.error('初始化蓝牙服务失败: $e'); - return this; - } - } - - /// 设置蓝牙状态监听 - void _setupBluetoothListeners() { - // 取消旧订阅(如果有) - _stateSubscription?.cancel(); - - // 监听蓝牙适配器状态变化 - _stateSubscription = FlutterBluePlus.adapterState.listen((state) { - _isOn.value = state == BluetoothAdapterState.on; - _isAvailable.value = state != BluetoothAdapterState.unavailable; - - if (state == BluetoothAdapterState.on) { - Logger.info('蓝牙已开启'); - if (_permissionsGranted.value) { - _refreshSystemDevices(); - } - } else if (state == BluetoothAdapterState.off) { - Logger.info('蓝牙已关闭'); - _resetDeviceState(); - } - }); - } - - /// 初始检查蓝牙状态 - Future _checkBluetoothState() async { - try { - bool isOn = await FlutterBluePlus.isOn - .timeout(const Duration(seconds: 5), onTimeout: () => false); - - _isOn.value = isOn; - _isAvailable.value = isOn; - Logger.info('当前蓝牙是否已开启: $isOn'); - } catch (e) { - Logger.error('检查蓝牙状态失败: $e'); - _isOn.value = false; - _isAvailable.value = false; - } - } - - /// 重置设备状态 - void _resetDeviceState() { - _connectedDevices.clear(); - _systemPairedDevices.clear(); - _selectedDevice.value = null; - _lastConnectedDeviceInfo.value = {}; - _deviceType.value = DeviceType.unknown; - } - - /// 检查并请求蓝牙权限 - Future requestPermissions() async { - try { - // iOS无需额外权限 - if (!Platform.isAndroid) { - _permissionsGranted.value = true; - return true; - } - - // 请求所有必要权限 - final permissions = [ - // Permission.bluetoothScan, - // Permission.bluetoothConnect, - // Permission.location, // Android需要定位权限来扫描BLE设备 - Permission.microphone, - ]; - - // 一次性请求所有权限 - Map statuses = await permissions.request(); - - // 检查所有权限是否已授予 - bool allGranted = statuses.values.every((status) => status.isGranted); - _permissionsGranted.value = allGranted; - - if (!allGranted) { - // 检查哪些权限被拒绝 - List deniedPermissions = []; - statuses.forEach((permission, status) { - if (!status.isGranted) { - deniedPermissions.add(permission.toString().split('.').last); - } - }); - - Logger.warning('权限被拒绝: ${deniedPermissions.join(", ")}'); - } - - return allGranted; - } catch (e) { - Logger.error('请求权限失败: $e'); - _permissionsGranted.value = false; - return false; - } - } - - /// 刷新系统蓝牙设备(已连接和已配对) - Future _refreshSystemDevices() async { - if (!_permissionsGranted.value || !_isOn.value) { - Logger.warning('无法刷新系统设备:蓝牙未开启或缺少权限'); - return; - } - - // 防止重复刷新 - if (_isRefreshingDevices) { - return; - } - - _isRefreshingDevices = true; - - try { - // 获取已连接的系统设备 - List devices = await FlutterBluePlus.connectedSystemDevices - .timeout(const Duration(seconds: 10), onTimeout: () => []); - - _connectedDevices.value = devices; - Logger.info('获取到${devices.length}个系统已连接设备'); - - // 更新选中设备的连接状态(如果有) - if (_selectedDevice.value != null) { - bool stillConnected = _connectedDevices.any( - (d) => d.remoteId.str == _selectedDevice.value!.remoteId.str - ); - - _connectionState.value = stillConnected - ? BluetoothConnectionState.connected - : BluetoothConnectionState.disconnected; - - if (stillConnected) { - _updateLastConnectedDeviceInfo(); - } - } - - // 获取系统已配对设备 - List pairedDevices = await FlutterBluePlus.bondedDevices - .timeout(const Duration(seconds: 10), onTimeout: () => []); - - _systemPairedDevices.value = pairedDevices; - Logger.info('获取到${pairedDevices.length}个系统已配对设备'); - } catch (e) { - Logger.error('刷新系统设备失败: $e'); - } finally { - _isRefreshingDevices = false; - } - } - - /// 查找音频设备 - BluetoothDevice? _findAudioDevice(List devices) { - if (devices.isEmpty) return null; - - // 音频设备的可能名称关键词 - final List audioKeywords = [ - 'headphone', 'earphone', 'earbuds', 'headset', - 'speaker', 'audio', '耳机', '音箱', '音响' - ]; - - // 尝试匹配音频设备名称 - for (BluetoothDevice device in devices) { - String name = device.platformName.toLowerCase(); - if (audioKeywords.any((keyword) => name.contains(keyword))) { - return device; - } - } - - return null; - } - - /// 确定设备类型 - void _determineDeviceType(BluetoothDevice device) { - final String deviceName = device.platformName.toLowerCase(); - - // 音频设备关键词 - final List audioKeywords = [ - 'headphone', 'earphone', 'earbuds', 'headset', - 'speaker', 'audio', '耳机', '音箱', '音响' - ]; - - bool hasAudioKeyword = audioKeywords.any((keyword) => deviceName.contains(keyword)); - bool inConnectedList = _connectedDevices.any((d) => d.remoteId.str == device.remoteId.str); - - if (hasAudioKeyword || inConnectedList) { - _deviceType.value = DeviceType.classicAudio; - } else { - _deviceType.value = DeviceType.ble; - } - } - - /// 获取已连接设备的信息 - Map getConnectedDeviceInfo() { - if (_selectedDevice.value == null) { - return { - 'connected': false, - 'device': null, - 'deviceType': _deviceType.value.toString().split('.').last - }; - } - - // 判断设备是否在已连接列表中 - bool isConnected = _connectedDevices.any( - (d) => d.remoteId.str == _selectedDevice.value!.remoteId.str - ); - - // 判断设备是否在已配对列表中 - bool isPaired = _systemPairedDevices.any( - (d) => d.remoteId.str == _selectedDevice.value!.remoteId.str - ); - - final deviceInfo = { - 'connected': isConnected, - 'paired': isPaired, - 'device': { - 'name': _selectedDevice.value!.platformName.isEmpty ? - '未命名设备' : _selectedDevice.value!.platformName, - 'id': _selectedDevice.value!.remoteId.str, - 'isConnected': isConnected, - 'isPaired': isPaired, - }, - 'deviceType': _deviceType.value.toString().split('.').last - }; - - return deviceInfo; - } - - /// 获取已配对设备的信息 - Map getPairedDeviceInfo(BluetoothDevice device) { - if (device == null) { - return { - 'connected': false, - 'paired': false, - 'device': null, - 'deviceType': DeviceType.unknown.toString().split('.').last - }; - } - - // 判断设备是否在已连接列表中 - bool isConnected = _connectedDevices.any( - (d) => d.remoteId.str == device.remoteId.str - ); - - // 确定设备类型 - DeviceType type = DeviceType.unknown; - final String deviceName = device.platformName.toLowerCase(); - - // 音频设备关键词 - final List audioKeywords = [ - 'headphone', 'earphone', 'earbuds', 'headset', - 'speaker', 'audio', '耳机', '音箱', '音响' - ]; - - bool hasAudioKeyword = audioKeywords.any((keyword) => deviceName.contains(keyword)); - if (hasAudioKeyword || isConnected) { - type = DeviceType.classicAudio; - } else { - type = DeviceType.ble; - } - - final deviceInfo = { - 'connected': isConnected, - 'paired': true, - 'device': { - 'name': device.platformName.isEmpty ? - '未命名设备' : device.platformName, - 'id': device.remoteId.str, - 'isConnected': isConnected, - 'isPaired': true, - }, - 'deviceType': type.toString().split('.').last - }; - - return deviceInfo; - } - - /// 设置选中的设备(不进行连接操作) - void setSelectedDevice(BluetoothDevice device) { - if (device != null) { - _selectedDevice.value = device; - _determineDeviceType(device); - _updateLastConnectedDeviceInfo(); - Logger.info('已设置选中设备: ${device.platformName}'); - } - } - - /// 更新最后连接的设备信息 - void _updateLastConnectedDeviceInfo() { - if (_selectedDevice.value != null) { - _lastConnectedDeviceInfo.value = getConnectedDeviceInfo(); - } - } - - /// 查找系统已连接设备 - Future findConnectedSystemDevice() async { - if (!_permissionsGranted.value || !_isOn.value) { - Logger.warning('蓝牙未开启或缺少权限,无法查找系统连接设备'); - return null; - } - - try { - // 刷新系统设备列表(避免重复刷新) - if (!_isRefreshingDevices) { - await _refreshSystemDevices(); - } - - // 如果没有已连接设备,直接返回null - if (_connectedDevices.isEmpty) { - Logger.info('未找到已连接的蓝牙设备'); - return null; - } - - // 优先找出可能的音频设备 - BluetoothDevice? audioDevice = _findAudioDevice(_connectedDevices); - BluetoothDevice device = audioDevice ?? _connectedDevices.first; - - // 自动选择设备 - _selectedDevice.value = device; - _determineDeviceType(device); - _connectionState.value = BluetoothConnectionState.connected; - _updateLastConnectedDeviceInfo(); - - Logger.info('找到已连接的蓝牙设备: ${device.platformName}'); - return device; - } catch (e) { - Logger.error('查找系统已连接设备失败: $e'); - return null; - } - } - - /// 清理资源 - @override - void onClose() { - // 取消所有订阅 - _stateSubscription?.cancel(); - super.onClose(); - } -} \ No newline at end of file diff --git a/lib/data/services/classic_bluetooth_service.dart b/lib/data/services/classic_bluetooth_service.dart new file mode 100644 index 000000000..39e77f4bf --- /dev/null +++ b/lib/data/services/classic_bluetooth_service.dart @@ -0,0 +1,452 @@ +import 'dart:async'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:app_settings/app_settings.dart'; +import '../../core/utils/logger.dart'; + +/// 蓝牙设备信息 +class BluetoothDevice { + final String name; + final String address; + final String type; + + BluetoothDevice({ + required this.name, + required this.address, + required this.type + }); + + @override + String toString() => 'BluetoothDevice(name: $name, address: $address, type: $type)'; +} + +/// 蓝牙事件类型 +enum BluetoothEventType { + /// 设备已连接 + deviceConnected, + + /// 设备已断开 + deviceDisconnected, + + /// 蓝牙状态改变(开启/关闭) + bluetoothStateChanged +} + +/// 蓝牙连接状态事件 +class BluetoothConnectionEvent { + /// 事件类型 + final BluetoothEventType type; + + /// 相关设备(可能为空,比如蓝牙状态改变事件) + final BluetoothDevice? device; + + /// 蓝牙状态(仅适用于bluetoothStateChanged事件) + final String? state; + + /// 事件发生时间戳 + final int timestamp; + + BluetoothConnectionEvent({ + required this.type, + this.device, + this.state, + required this.timestamp, + }); + + factory BluetoothConnectionEvent.fromMap(Map map) { + BluetoothEventType eventType; + + switch (map['type']) { + case 'deviceConnected': + eventType = BluetoothEventType.deviceConnected; + break; + case 'deviceDisconnected': + eventType = BluetoothEventType.deviceDisconnected; + break; + case 'bluetoothStateChanged': + eventType = BluetoothEventType.bluetoothStateChanged; + break; + default: + throw ArgumentError('未知事件类型: ${map['type']}'); + } + + final deviceMap = map['device'] as Map?; + final BluetoothDevice? device = deviceMap != null + ? BluetoothDevice( + name: deviceMap['name'] ?? '未知设备', + address: deviceMap['address'] ?? '', + type: deviceMap['type'] ?? 'unknown' + ) + : null; + + return BluetoothConnectionEvent( + type: eventType, + device: device, + state: map['state'] as String?, + timestamp: map['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch, + ); + } + + @override + String toString() { + switch (type) { + case BluetoothEventType.deviceConnected: + return '蓝牙设备已连接: ${device?.name}'; + case BluetoothEventType.deviceDisconnected: + return '蓝牙设备已断开: ${device?.name}'; + case BluetoothEventType.bluetoothStateChanged: + return '蓝牙状态改变: $state'; + } + } +} + +/// 经典蓝牙服务 +class ClassicBluetoothService extends GetxService { + static ClassicBluetoothService get to => Get.find(); + + // 注意: MainActivity.kt中定义的通道名称为 com.deep_voice.classic_bluetooth + final MethodChannel _channel = const MethodChannel('com.deep_voice.classic_bluetooth'); + final EventChannel _eventChannel = const EventChannel('com.deep_voice.classic_bluetooth_events'); + + /// 蓝牙事件流控制器 + final _connectionEventController = StreamController.broadcast(); + + /// 蓝牙事件流 + Stream get connectionEvents => _connectionEventController.stream; + + /// 当前连接的设备 + final currentDevice = Rx(null); + + /// 已连接设备列表 + final connectedDevices = [].obs; + + /// 蓝牙是否可用 + final _isBluetoothEnabled = true.obs; + bool get isBluetoothEnabled => _isBluetoothEnabled.value; + + /// 是否正在加载 + final isLoading = false.obs; + + /// 错误信息 + final errorMessage = ''.obs; + + /// 蓝牙状态变化事件流 + StreamSubscription? _bluetoothStateListener; + + /// 设备连接回调 + final List _deviceConnectedCallbacks = []; + + /// 设备断开回调 + final List _deviceDisconnectedCallbacks = []; + + /// 蓝牙状态变化回调 + final List _bluetoothStateChangedCallbacks = []; + + /// 服务是否已初始化 + bool _isInitialized = false; + + ClassicBluetoothService() { + // 创建服务时不立即初始化,避免还没有原生支持时就调用方法 + Logger.info('创建 ClassicBluetoothService 实例'); + } + + /// 初始化蓝牙服务 + /// 必须在使用任何方法前调用此方法确保通道已设置 + Future initialize() async { + if (_isInitialized) { + Logger.info('ClassicBluetoothService 已经初始化'); + return true; + } + + try { + Logger.info('正在初始化 ClassicBluetoothService...'); + + // 检查通道是否可用 + try { + // 调用一个简单方法检查通道是否已设置 + await _channel.invokeMethod('isBluetoothEnabled'); + Logger.info('蓝牙方法通道可用'); + } catch (e) { + Logger.error('蓝牙方法通道不可用: $e'); + // 这里不要立即返回false,尝试继续设置其他内容 + } + + // 设置事件监听器 + _setupBluetoothStateListener(); + + _isInitialized = true; + Logger.info('ClassicBluetoothService 初始化成功'); + return true; + } catch (e) { + Logger.error('初始化 ClassicBluetoothService 失败: $e'); + errorMessage.value = '初始化蓝牙服务失败'; + return false; + } + } + + /// 检查并请求蓝牙权限 + Future checkAndRequestPermissions() async { + try { + // 首先确保服务已初始化 + if (!_isInitialized && !(await initialize())) { + Logger.error('无法初始化蓝牙服务'); + errorMessage.value = '无法初始化蓝牙服务'; + return false; + } + + // 检查蓝牙相关权限 + Map statuses = await [ + Permission.bluetooth, + Permission.bluetoothConnect, + // Permission.bluetoothScan, + // Permission.location, + Permission.microphone, + ].request(); + + // 检查蓝牙连接权限 + if (statuses[Permission.bluetoothConnect] != PermissionStatus.granted) { + Logger.error('蓝牙连接权限被拒绝'); + errorMessage.value = '蓝牙连接权限被拒绝'; + return false; + } + + return true; + } catch (e) { + Logger.error('检查蓝牙权限失败: $e'); + errorMessage.value = '检查蓝牙权限失败'; + return false; + } + } + + /// 刷新已连接的蓝牙设备 + Future refreshConnectedDevices() async { + try { + // 首先确保服务已初始化 + if (!_isInitialized && !(await initialize())) { + Logger.error('无法初始化蓝牙服务'); + errorMessage.value = '无法初始化蓝牙服务'; + return false; + } + + errorMessage.value = ''; + + // 获取A2DP设备 + final a2dpDevices = await getConnectedA2dpDevices(); + Logger.info('已连接的A2DP设备: ${a2dpDevices.length}'); + + // 获取耳机设备 + final headsetDevices = await getConnectedHeadsetDevices(); + Logger.info('已连接的耳机设备: ${headsetDevices.length}'); + + // 更新设备列表 + connectedDevices.value = [...a2dpDevices, ...headsetDevices]; + + // 更新当前设备 + if (connectedDevices.isNotEmpty && currentDevice.value == null) { + currentDevice.value = connectedDevices.first; + } + + return connectedDevices.isNotEmpty; + } catch (e) { + Logger.error('刷新已连接设备失败: $e'); + errorMessage.value = '获取设备信息失败'; + return false; + } + } + + /// 获取已连接的A2DP设备(音频设备) + Future> getConnectedA2dpDevices() async { + try { + // 确保服务已初始化 + if (!_isInitialized && !(await initialize())) { + Logger.error('无法初始化蓝牙服务'); + return []; + } + + final result = await _channel.invokeMethod('getConnectedA2dpDevices'); + Logger.info('获取已连接的A2DP设备结果: $result'); + + if (result is List) { + final devices = result.map((device) => BluetoothDevice( + name: device['name'] ?? '未知设备', + address: device['address'] ?? '', + type: 'a2dp' + )).toList(); + + return devices; + } + + return []; + } catch (e) { + Logger.error('获取已连接的A2DP设备失败: $e'); + return []; + } + } + + /// 获取已连接的耳机设备 + Future> getConnectedHeadsetDevices() async { + try { + // 确保服务已初始化 + if (!_isInitialized && !(await initialize())) { + Logger.error('无法初始化蓝牙服务'); + return []; + } + + final result = await _channel.invokeMethod('getConnectedHeadsetDevices'); + Logger.info('获取已连接的耳机设备结果: $result'); + + if (result is List) { + final devices = result.map((device) => BluetoothDevice( + name: device['name'] ?? '未知设备', + address: device['address'] ?? '', + type: 'headset' + )).toList(); + + return devices; + } + + return []; + } catch (e) { + Logger.error('获取已连接的耳机设备失败: $e'); + return []; + } + } + + /// 设置蓝牙状态监听 + void _setupBluetoothStateListener() { + try { + _bluetoothStateListener = _eventChannel + .receiveBroadcastStream() + .listen((event) { + Logger.info('收到蓝牙事件: $event'); + + if (event is Map) { + final type = event['type'] as String?; + + if (type == 'stateChanged') { + final state = event['state'] as String?; + if (state != null) { + _handleBluetoothStateChange(state); + } + } else if (type == 'deviceConnected') { + final deviceMap = event['device'] as Map?; + if (deviceMap != null) { + final device = BluetoothDevice( + name: deviceMap['name'] ?? '未知设备', + address: deviceMap['address'] ?? '', + type: deviceMap['type'] ?? 'unknown' + ); + _handleDeviceConnected(device); + } + } else if (type == 'deviceDisconnected') { + final deviceMap = event['device'] as Map?; + if (deviceMap != null) { + final device = BluetoothDevice( + name: deviceMap['name'] ?? '未知设备', + address: deviceMap['address'] ?? '', + type: deviceMap['type'] ?? 'unknown' + ); + _handleDeviceDisconnected(device); + } + } + } + }, onError: (error) { + Logger.error('蓝牙状态监听错误: $error'); + }); + } catch (e) { + Logger.error('设置蓝牙状态监听失败: $e'); + } + } + + /// 处理蓝牙状态变化 + void _handleBluetoothStateChange(String state) { + Logger.info('蓝牙状态变化: $state'); + + // 如果蓝牙关闭,清除设备信息 + if (state == 'off') { + connectedDevices.clear(); + currentDevice.value = null; + } + + // 通知所有回调 + for (var callback in _bluetoothStateChangedCallbacks) { + callback(state); + } + } + + /// 处理设备连接 + void _handleDeviceConnected(BluetoothDevice device) { + Logger.info('设备已连接: ${device.name}'); + + // 更新设备列表 + if (!connectedDevices.any((d) => d.address == device.address)) { + connectedDevices.add(device); + } + + // 更新当前设备 + currentDevice.value = device; + + // 通知所有回调 + for (var callback in _deviceConnectedCallbacks) { + callback(device); + } + } + + /// 处理设备断开 + void _handleDeviceDisconnected(BluetoothDevice device) { + Logger.info('设备已断开: ${device.name}'); + + // 更新设备列表 + connectedDevices.removeWhere((d) => d.address == device.address); + + // 如果是当前设备,清除当前设备 + if (currentDevice.value?.address == device.address) { + currentDevice.value = connectedDevices.isEmpty ? null : connectedDevices.first; + } + + // 通知所有回调 + for (var callback in _deviceDisconnectedCallbacks) { + callback(device); + } + } + + /// 注册设备连接回调 + void onDeviceConnected(Function(BluetoothDevice) callback) { + _deviceConnectedCallbacks.add(callback); + } + + /// 注册设备断开回调 + void onDeviceDisconnected(Function(BluetoothDevice) callback) { + _deviceDisconnectedCallbacks.add(callback); + } + + /// 注册蓝牙状态变化回调 + void onBluetoothStateChanged(Function(String) callback) { + _bluetoothStateChangedCallbacks.add(callback); + } + + /// 打开蓝牙设置 + Future openBluetoothSettings() async { + try { + await AppSettings.openAppSettings(); + return true; + } catch (e) { + Logger.error('打开蓝牙设置失败: $e'); + return false; + } + } + + @override + void onClose() { + // 取消蓝牙状态监听 + _bluetoothStateListener?.cancel(); + + // 清除回调 + _deviceConnectedCallbacks.clear(); + _deviceDisconnectedCallbacks.clear(); + _bluetoothStateChangedCallbacks.clear(); + + super.onClose(); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index d256d0c33..e374b1554 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -73,8 +73,7 @@ class MainApp extends StatelessWidget { translations: AppTranslations(), locale: const Locale('zh', 'CN'), fallbackLocale: const Locale('en', 'US'), - // 注意:VoiceInteractionService 在 HomeController 的 onInit 方法中初始化 - // 不要在这里重复初始化,以避免多次注册事件监听器 + ); }, ); diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart index 5d15736b2..6b82d1892 100644 --- a/lib/modules/home/controllers/home_controller.dart +++ b/lib/modules/home/controllers/home_controller.dart @@ -1,10 +1,11 @@ import 'package:get/get.dart'; import '../../../routes/app_routes.dart'; -import '../../../data/services/bluetooth_service.dart'; +import '../../../data/services/classic_bluetooth_service.dart' as classic; import '../../../core/utils/logger.dart'; -import 'package:flutter_blue_plus/flutter_blue_plus.dart' hide BluetoothService; +import 'package:flutter_blue_plus/flutter_blue_plus.dart' hide BluetoothDevice; import 'package:flutter/material.dart'; import '../../../data/services/voice_interaction_service.dart'; +import 'package:permission_handler/permission_handler.dart'; // 连接状态枚举 enum ConnectionStatus { disconnected, // 未连接 @@ -16,6 +17,9 @@ enum ConnectionStatus { class HomeController extends GetxController { static HomeController get to => Get.find(); + // 经典蓝牙服务 + late classic.ClassicBluetoothService _bluetoothService; + // 设备连接状态 final _isConnected = false.obs; bool get isConnected => _isConnected.value; @@ -24,495 +28,228 @@ class HomeController extends GetxController { final _connectionStatus = ConnectionStatus.disconnected.obs; ConnectionStatus get connectionStatus => _connectionStatus.value; - // 电池状态 + // 电池状态(模拟数据) final _deviceBattery = Rx(null); - int? get deviceBattery => _deviceBattery.value; - // 固件版本 + // 固件版本(模拟数据) final String firmwareVersion = 'v2.4.1'; // 蓝牙设备信息 final _deviceInfo = {}.obs; Map get deviceInfo => _deviceInfo.value; - // 蓝牙服务 - BluetoothService? _bluetoothService; - // 连接错误信息 final _connectionErrorMsg = ''.obs; String get connectionErrorMsg => _connectionErrorMsg.value; + // 当前连接的经典蓝牙设备 + final _connectedClassicDevices = [].obs; + List get connectedClassicDevices => _connectedClassicDevices; + @override void onInit() { super.onInit(); - // 立即加载缓存的设备信息并显示 - _loadCachedDeviceInfo(); - - // 先进行简单初始化,避免立即进行蓝牙操作 - _initBasicServices(); + // 初始化蓝牙服务和状态监控 + _initBluetoothService(); - // 延迟初始化蓝牙相关功能,让UI先渲染完成 - Future.delayed(const Duration(milliseconds: 1200), () { - _initBluetoothService(); - }); + // 电池模拟更新(实际应用中可以通过蓝牙获取) + _simulateBatteryUpdate(); } - // 立即加载缓存的设备信息 - void _loadCachedDeviceInfo() { + // 初始化蓝牙服务 + void _initBluetoothService() { try { - // 1. 首先检查是否有传入的设备信息(从PairingController传入) - if (Get.arguments != null && Get.arguments is Map) { - Logger.info('使用传入的设备信息'); - _deviceInfo.assignAll(Get.arguments); - _updateConnectionStatus(); - update(); // 立即更新UI - return; - } + // 获取经典蓝牙服务实例 + _bluetoothService = Get.find(); - // 2. 如果没有传入信息,尝试从BluetoothService获取上次连接的设备信息 - if (Get.isRegistered()) { - BluetoothService bluetoothService = Get.find(); - if (bluetoothService.lastConnectedDeviceInfo.isNotEmpty) { - Logger.info('使用BluetoothService中存储的上次连接设备信息'); - _deviceInfo.assignAll(bluetoothService.lastConnectedDeviceInfo); - _updateConnectionStatus(); - update(); // 立即更新UI - return; - } - } + // 更新当前连接状态 + _updateConnectionStatus(); - // 3. 如果上述都没有,显示未连接状态 - Logger.info('没有找到缓存的设备信息,显示未连接状态'); - _connectionStatus.value = ConnectionStatus.disconnected; - _isConnected.value = false; - update(); // 立即更新UI - } catch (e) { - Logger.error('加载缓存设备信息失败: $e'); - } - } - - // 基础服务初始化,不涉及蓝牙等耗时操作 - void _initBasicServices() { - try { - // 初始化语音交互服务并启动服务 - Future.microtask(() async { - // 先初始化服务 - await VoiceInteractionService.to.initialize(); - - // 启动服务 - bool serviceStarted = await VoiceInteractionService.to.startService(); - if (serviceStarted) { - Logger.info('语音交互服务已成功启动'); - } else { - Logger.error('语音交互服务启动失败'); - } - }); - - // 检查是否有传入的蓝牙设备信息 - if (Get.arguments != null && Get.arguments is Map) { - _deviceInfo.assignAll(Get.arguments); - _updateConnectionStatus(); - } + // 监听蓝牙连接状态变化 + _setupBluetoothListeners(); - // 模拟电池状态更新 - _simulateBatteryUpdates(); - } catch (e) { - Logger.error('初始化基础服务失败: $e'); - } - } - - // 初始化蓝牙服务 - void _initBluetoothService() { - try { - if (Get.isRegistered()) { - _bluetoothService = Get.find(); - - // 直接获取当前设备信息 - _updateDeviceInfoFromService(); - - // 监听连接状态变化 - ever(_bluetoothService!.connectionStateObs, (state) { - Logger.info('蓝牙连接状态变化: $state'); - _processConnectionStateChange(state); - }); - - // 监听选中设备变化 - ever(_bluetoothService!.selectedDeviceObs, (device) { - Logger.info('蓝牙选中设备变化: ${device?.platformName ?? "无设备"}'); - _updateDeviceInfoFromService(); - }); - - // 监听最后连接的设备信息变化 - ever(_bluetoothService!.lastConnectedDeviceInfoObs, (info) { - Logger.info('最后连接的设备信息变化'); - if (info.isNotEmpty) { - _deviceInfo.assignAll(info); - _updateConnectionStatus(); - update(); // 通知UI刷新 - } - }); - } } catch (e) { - Logger.error('获取蓝牙服务失败: $e'); - _connectionErrorMsg.value = '获取蓝牙服务失败'; - update(); // 通知UI刷新 + Logger.error('初始化蓝牙服务失败: $e'); + _connectionStatus.value = ConnectionStatus.failed; } } - // 从蓝牙服务更新设备信息 - void _updateDeviceInfoFromService() { - if (_bluetoothService != null) { - // 获取当前设备信息 - Map info = _bluetoothService!.lastConnectedDeviceInfo; - if (info.isNotEmpty) { - _deviceInfo.assignAll(info); - _updateConnectionStatus(); - update(); // 通知UI刷新 - } else if (_bluetoothService!.selectedDevice != null) { - // 如果最后连接的设备信息为空,但有选中的设备,获取当前连接的设备信息 - _deviceInfo.assignAll(_bluetoothService!.getConnectedDeviceInfo()); - _updateConnectionStatus(); - update(); // 通知UI刷新 + // 电池电量模拟更新 + void _simulateBatteryUpdate() { + // 每5分钟更新一次电池电量(模拟) + Future.delayed(const Duration(seconds: 2), () { + if (isConnected) { + final timestamp = DateTime.now().millisecondsSinceEpoch; + _deviceBattery.value = 50 + (timestamp % 50); // 50-99的随机值 + } else { + _deviceBattery.value = null; } - } - } - - // 检查并更新当前连接状态 - void _checkAndUpdateCurrentConnectionState() { - if (_bluetoothService != null) { - _updateDeviceInfoFromService(); - } - } - - // 处理连接状态变化 - void _processConnectionStateChange(BluetoothConnectionState state) { - // 从蓝牙服务获取最新设备信息 - _updateDeviceInfoFromService(); - - switch (state) { - case BluetoothConnectionState.connected: - _connectionStatus.value = ConnectionStatus.connected; - _connectionErrorMsg.value = ''; - _isConnected.value = true; - break; - case BluetoothConnectionState.disconnected: - _connectionStatus.value = ConnectionStatus.disconnected; - _isConnected.value = false; - break; - case BluetoothConnectionState.connecting: - _connectionStatus.value = ConnectionStatus.connecting; - break; - default: - // 其他状态暂不处理 - break; - } - - update(); // 通知UI刷新 - } - - // 显示连接成功消息 - void _showConnectionSuccessMessage() { - if (_deviceInfo.isNotEmpty && _deviceInfo['device'] != null) { - String deviceName = _deviceInfo['device']['name'] ?? '未命名设备'; - Get.snackbar( - '连接成功', - '已成功连接到设备: $deviceName', - snackPosition: SnackPosition.TOP, - backgroundColor: Colors.green[100], - colorText: Colors.green[800], - duration: const Duration(seconds: 2), - ); - } + + // 递归调用实现定时更新 + _simulateBatteryUpdate(); + }); } // 更新连接状态 void _updateConnectionStatus() { - if (_deviceInfo.containsKey('connected') || _deviceInfo.containsKey('paired')) { - // 检查是否已连接 - bool isConnected = _deviceInfo['connected'] ?? false; - // 检查是否已配对 - bool isPaired = _deviceInfo['paired'] ?? false; - - // 如果已连接,设置为已连接状态 - if (isConnected) { - _isConnected.value = true; - _connectionStatus.value = ConnectionStatus.connected; - } - // 如果已配对但未连接,也视为已连接状态(简化处理) - else if (isPaired) { - _isConnected.value = true; - _connectionStatus.value = ConnectionStatus.connected; - } - // 如果既未连接也未配对,设置为未连接状态 - else { - _isConnected.value = false; - _connectionStatus.value = ConnectionStatus.disconnected; - } + // 检查当前设备 + final currentDevice = _bluetoothService.currentDevice.value; + + if (currentDevice != null) { + _isConnected.value = true; + _connectionStatus.value = ConnectionStatus.connected; } else { _isConnected.value = false; _connectionStatus.value = ConnectionStatus.disconnected; } - } - - // 模拟电池状态更新 - void _simulateBatteryUpdates() { - // 不再获取设备电量 - _deviceBattery.value = 0; - Logger.info('不再获取设备电量'); - } - - // 获取设备电量 - Future _fetchBatteryLevel() async { - // 不再获取设备电量 - _deviceBattery.value = 0; - Logger.info('不再获取设备电量'); - } - - // 更新设备电量信息(提供给UI手动刷新使用) - Future refreshBatteryLevel() async { - await _fetchBatteryLevel(); - Get.snackbar( - '提示', - '电量信息已更新', - snackPosition: SnackPosition.TOP, - duration: const Duration(seconds: 1), - ); - } - - // 获取格式化后的电量显示文本 - String getBatteryText() { - if (_deviceBattery.value == null) { - return '电量未知'; - } else { - return '${_deviceBattery.value}%'; - } - } - - // 探索AI功能 - void exploreAIFeatures() { - Get.toNamed(Routes.chat, arguments: {'agentId': 'personal_assistant'}); - } - - // 更新固件 - void updateFirmware() { - Get.snackbar( - '提示', - '开始下载固件更新...', - snackPosition: SnackPosition.TOP, - ); - // 模拟更新过程 - // 在真实应用中,这里应该有固件更新的实际逻辑 - } - - // 打开降噪功能 - void toggleNoiseReduction() { - Get.snackbar( - '提示', - '降噪功能已开启', - snackPosition: SnackPosition.TOP, - ); - } - - // 音效设置 - void openSoundEffects() { - Get.snackbar( - '提示', - '音效设置功能尚未实现', - snackPosition: SnackPosition.TOP, - ); - } - - // 环境音功能 - void toggleAmbientSound() { - Get.snackbar( - '提示', - '环境音模式已开启', - snackPosition: SnackPosition.TOP, - ); - } - - // 收藏的设置 - void openFavorites() { - Get.snackbar( - '提示', - '收藏功能尚未实现', - snackPosition: SnackPosition.TOP, - ); - } - - // 打开设置页面 - void openSettings() { - Get.toNamed(Routes.settings); - } - - // 重新扫描设备 - void rescanDevices() { - if (_bluetoothService != null) { - Get.toNamed(Routes.pairing); - } else { - Get.snackbar( - '提示', - '蓝牙服务不可用', - snackPosition: SnackPosition.TOP, - ); - } - } - - // 获取设备名称 - String getDeviceName() { - if (_deviceInfo.isEmpty || _deviceInfo['device'] == null) { - return '未连接设备'; - } - - final device = _deviceInfo['device'] as Map; - return device['name'] ?? '未命名设备'; - } - - // 获取设备ID - String getDeviceId() { - if (_deviceInfo.isEmpty || _deviceInfo['device'] == null) { - return ''; - } - - final device = _deviceInfo['device'] as Map; - return device['id'] ?? ''; + update(); // 通知UI刷新 } - // 获取设备信号强度 - String getSignalStrength() { - if (_deviceInfo.isEmpty || _deviceInfo['device'] == null) { - return '无信号'; - } - - final device = _deviceInfo['device'] as Map; - final rssi = device['rssi']; + // 设置蓝牙监听器 + void _setupBluetoothListeners() { + // 监听设备连接 + _bluetoothService.onDeviceConnected((device) { + Logger.info('监听到蓝牙设备连接: ${device.name}'); + _isConnected.value = true; + _connectionStatus.value = ConnectionStatus.connected; + update(); + }); - if (rssi == null) { - return '未知'; - } + // 监听设备断开 + _bluetoothService.onDeviceDisconnected((device) { + Logger.info('监听到蓝牙设备断开: ${device.name}'); + _isConnected.value = false; + _connectionStatus.value = ConnectionStatus.disconnected; + _deviceBattery.value = null; + update(); + }); - // RSSI通常在-100到0之间,值越大信号越强 - if (rssi > -60) { - return '强'; - } else if (rssi > -80) { - return '中'; - } else { - return '弱'; - } + // 监听蓝牙状态变化 + _bluetoothService.onBluetoothStateChanged((state) { + Logger.info('监听到蓝牙状态变化: $state'); + + if (state == 'off') { + _isConnected.value = false; + _connectionStatus.value = ConnectionStatus.disconnected; + _deviceBattery.value = null; + update(); + } + }); } // 刷新设备信息 Future refreshDeviceInfo() async { try { - // 检查蓝牙是否可用 - if (_bluetoothService == null) { - Get.snackbar( - '提示', - '蓝牙服务不可用', - snackPosition: SnackPosition.TOP, - backgroundColor: Colors.red[100], - colorText: Colors.red[800], - ); - return; - } - - // 更新连接状态为正在连接,但保持当前设备信息显示 + // 设置状态为连接中 _connectionStatus.value = ConnectionStatus.connecting; - update(); // 立即更新UI状态 + update(); - // 从BluetoothService获取最新设备信息 - _updateDeviceInfoFromService(); + // 刷新设备列表 + final hasDevices = await _bluetoothService.refreshConnectedDevices(); - // 刷新电量信息 - await _fetchBatteryLevel(); + // 更新状态 + _updateConnectionStatus(); - // 根据当前状态给出提示 - if (_isConnected.value) { - Get.snackbar( - '提示', - '设备信息已更新', - snackPosition: SnackPosition.TOP, - backgroundColor: Colors.green[100], - colorText: Colors.green[800], - ); + // 根据结果更新状态 + if (!hasDevices) { + Logger.info('刷新设备:未找到设备'); + // 这里可以设置状态变量而不是使用snackbar + _connectionErrorMsg.value = _bluetoothService.errorMessage.value.isEmpty + ? '未找到已连接的设备' + : _bluetoothService.errorMessage.value; } else { - Get.snackbar( - '提示', - '无法获取设备信息,请确保设备已连接', - snackPosition: SnackPosition.TOP, - backgroundColor: Colors.amber[100], - colorText: Colors.amber[800], - ); + _connectionErrorMsg.value = ''; + Logger.info('刷新设备:成功'); } } catch (e) { Logger.error('刷新设备信息失败: $e'); - _connectionErrorMsg.value = '刷新设备信息失败: ${e.toString()}'; - update(); // 确保状态更新 - - Get.snackbar( - '提示', - '刷新设备信息失败', - snackPosition: SnackPosition.TOP, - backgroundColor: Colors.red[100], - colorText: Colors.red[800], - ); + _connectionStatus.value = ConnectionStatus.failed; + _connectionErrorMsg.value = '无法获取设备信息'; + update(); } } - // 获取设备状态文本 - String getConnectionStatusText() { - // 检查是否已配对但未连接 - if (_deviceInfo.containsKey('paired') && _deviceInfo['paired'] == true && - (_deviceInfo['connected'] == false || _deviceInfo['connected'] == null)) { - return "已配对"; - } - + // 重新扫描设备 + void rescanDevices() { + Get.toNamed(Routes.pairing); + } + + // 打开翻译功能 + void openTranslation() { + Get.toNamed(Routes.translation); + } + + // 打开AI伴侣功能 + void openAIGirlfriend() { + Get.toNamed(Routes.chat); + } + + // 展示AI功能 + void exploreAIFeatures() { + // 简单实现,直接跳转到AI聊天页面 + openAIGirlfriend(); + } + + // 获取连接状态对应的颜色 + Color getConnectionStatusColor() { switch (_connectionStatus.value) { case ConnectionStatus.connected: - return "已连接"; + return Colors.green; case ConnectionStatus.connecting: - return "连接中..."; - case ConnectionStatus.disconnected: - return "未连接"; + return Colors.amber; case ConnectionStatus.failed: - return "连接失败"; + return Colors.red; + case ConnectionStatus.disconnected: + default: + return Colors.grey; } } - // 获取连接状态颜色 - Color getConnectionStatusColor() { - // 检查是否已配对但未连接 - if (_deviceInfo.containsKey('paired') && _deviceInfo['paired'] == true && - (_deviceInfo['connected'] == false || _deviceInfo['connected'] == null)) { - return Colors.orange; // 已配对但未连接使用橙色 - } - + // 获取连接状态对应的文本 + String getConnectionStatusText() { switch (_connectionStatus.value) { case ConnectionStatus.connected: - return Colors.green; + return '已连接'; case ConnectionStatus.connecting: - return Colors.blue; - case ConnectionStatus.disconnected: - return Colors.red; + return '连接中...'; case ConnectionStatus.failed: - return Colors.red; + return '连接失败'; + case ConnectionStatus.disconnected: + default: + return '未连接'; } } - // 快捷入口方法 - - // 打开同声翻译页面 - void openTranslation() { - Get.toNamed(Routes.translation); + // 获取设备名称 + String getDeviceName() { + final device = _bluetoothService.currentDevice.value; + return device?.name ?? '未知设备'; } - // 打开AI女友页面 - void openAIGirlfriend() { - Get.toNamed(Routes.chat, arguments: {'agentId': 'cyber_girlfriend'}); + // 获取信号强度(模拟数据) + String getSignalStrength() { + if (!_isConnected.value) { + return '无信号'; + } + + // 简单模拟信号强度 + final random = DateTime.now().millisecondsSinceEpoch % 3; + switch (random) { + case 0: + return '良好'; + case 1: + return '很好'; + default: + return '极佳'; + } } - // 打开AI助理(已有方法 exploreAIFeatures) + // 获取设备ID + String getDeviceId() { + final device = _bluetoothService.currentDevice.value; + return device?.address ?? '未知'; + } } \ No newline at end of file diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index e98b1e7c3..14318b870 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -1,303 +1,185 @@ import 'package:get/get.dart'; +import 'dart:async'; import '../../../routes/app_routes.dart'; -import '../../../data/services/bluetooth_service.dart'; -import 'package:flutter_blue_plus/flutter_blue_plus.dart' hide BluetoothService; +import '../../../data/services/classic_bluetooth_service.dart' as classic; import '../../../core/utils/logger.dart'; -import 'package:permission_handler/permission_handler.dart'; import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; -/// 简化的配对控制器,仅用于显示已连接或配对的蓝牙耳机信息 +/// 简化的配对控制器,仅用于显示已连接经典蓝牙设备和处理基本配对流程 class PairingController extends GetxController { - // 蓝牙服务 - late BluetoothService _bluetoothService; + static PairingController get to => Get.find(); - // 设备信息 - final _deviceInfo = Rx>({}); - Map get deviceInfo => _deviceInfo.value; + // 经典蓝牙服务 + late classic.ClassicBluetoothService _bluetoothService; - // 蓝牙状态 - final _isBluetoothAvailable = false.obs; - final _isBluetoothOn = false.obs; - bool get isBluetoothAvailable => _isBluetoothAvailable.value; - bool get isBluetoothOn => _isBluetoothOn.value; + // 是否开始搜索 + final _isSearching = false.obs; + bool get isSearching => _isSearching.value; - // 错误消息 - final _errorMessage = ''.obs; - String get errorMessage => _errorMessage.value; + // 设备连接状态 + final _isConnecting = false.obs; + bool get isConnecting => _isConnecting.value; - // 已连接的设备列表 - final _connectedDevices = [].obs; - List get connectedDevices => _connectedDevices; + // 蓝牙服务是否可用 + final _isBluetoothServiceAvailable = true.obs; + bool get isBluetoothServiceAvailable => _isBluetoothServiceAvailable.value; - // 已配对的设备列表 - final _pairedDevices = [].obs; - List get pairedDevices => _pairedDevices; + // 当前连接设备 + Rx get currentDevice => _bluetoothService.currentDevice; - // 状态描述(用于UI显示) - final _statusDescription = ''.obs; - String get statusDescription => _statusDescription.value; + // 连接设备列表 + RxList get connectedDevices => _bluetoothService.connectedDevices; + + // 错误信息 + RxString get errorMessage => _bluetoothService.errorMessage; - // 防止重复刷新的标志 - bool _isRefreshing = false; + // 连接设备列表(兼容旧代码) + List get connectedClassicDevices => + _bluetoothService.currentDevice.value != null + ? [_bluetoothService.currentDevice.value!] + : []; - // 上次刷新时间 - DateTime? _lastRefreshTime; + // 状态描述(用于UI显示) + final statusDescription = '正在初始化...'.obs; + + // 加载状态 + final isLoading = false.obs; @override void onInit() { super.onInit(); - _initializeProcess(); + _initialize(); } - // 初始化流程 - Future _initializeProcess() async { - _updateStatus('正在初始化...'); - - try { - // 获取或初始化蓝牙服务 - await _getBluetoothService(); - - // 检查权限并获取设备信息 - await _checkAndRequestPermissions(); - } catch (e) { - Logger.error('初始化过程失败: $e'); - _updateStatus('初始化失败'); - _setError('初始化蓝牙服务失败,请重试'); - } + @override + void onReady() { + super.onReady(); + // 设置连接监听器,确保在页面完全加载后设置,避免过早监听可能导致的问题 + _setupDeviceListeners(); } - // 获取蓝牙服务 - Future _getBluetoothService() async { + /// 设置设备监听器 + void _setupDeviceListeners() { try { - // 检查是否已注册 - if (Get.isRegistered()) { - _bluetoothService = Get.find(); - } else { - // 初始化并注册服务 - _bluetoothService = await Get.put(BluetoothService()).init(); - } - - // 设置蓝牙状态监听 - _setupBluetoothListeners(); + if (!_isBluetoothServiceAvailable.value) return; - // 获取设备信息 - _refreshDeviceInfo(); + // 监听设备连接事件 + _bluetoothService.onDeviceConnected((device) { + Logger.info('检测到蓝牙设备连接: ${device.name}'); + + // 更新状态并跳转到主页 + statusDescription.value = '已连接到设备: ${device.name}'; + + // 立即跳转到主页 + _navigateToHome(); + }); } catch (e) { - Logger.error('获取蓝牙服务失败: $e'); - throw Exception('获取蓝牙服务失败: $e'); + Logger.error('设置设备监听器失败: $e'); } } - // 设置蓝牙状态监听 - void _setupBluetoothListeners() { - // 监听蓝牙可用性 - _isBluetoothAvailable.value = _bluetoothService.isAvailable; - ever(_bluetoothService.isAvailableObs, (bool available) { - _isBluetoothAvailable.value = available; - - if (!available) { - _updateStatus('蓝牙不可用'); - _setError('蓝牙不可用,请检查设备是否支持蓝牙功能'); - } - }); - - // 监听蓝牙开关状态 - _isBluetoothOn.value = _bluetoothService.isOn; - ever(_bluetoothService.isOnObs, (bool on) { - _isBluetoothOn.value = on; - - if (on) { - // 蓝牙开启时,延迟一下再刷新设备信息,避免频繁刷新 - Future.delayed(const Duration(milliseconds: 500), () { - _refreshDeviceInfo(); - }); - } else { - _updateStatus('蓝牙已关闭'); - _setError('蓝牙已关闭,请开启蓝牙后再试'); - } - }); - - // 监听已连接设备变化 - _connectedDevices.value = _bluetoothService.connectedDevices; - ever(_bluetoothService.connectedDevicesObs, (List devices) { - _connectedDevices.value = devices; - - // 只有当设备列表不为空时才刷新设备信息 - if (devices.isNotEmpty) { - // 延迟一下再刷新,避免频繁刷新 - Future.delayed(const Duration(milliseconds: 500), () { - _refreshDeviceInfo(); - }); - } - }); - - // 监听已配对设备变化 - _pairedDevices.value = _bluetoothService.systemPairedDevices; + /// 立即跳转到主页 + void _navigateToHome() { + Get.offAllNamed(Routes.home); } - // 检查并请求权限 - Future _checkAndRequestPermissions() async { - _updateStatus('正在请求权限...'); - + /// 完整的初始化流程 + Future _initialize() async { try { - bool hasPermissions = await _bluetoothService.requestPermissions(); + isLoading.value = true; + statusDescription.value = '正在初始化...'; + + // 1. 获取蓝牙服务 + _bluetoothService = Get.find(); - if (!hasPermissions) { - // 权限被拒绝 - _handlePermissionDenied(); + // 确保蓝牙服务已初始化 + if (!await _bluetoothService.initialize()) { + Logger.error('蓝牙服务初始化失败'); + statusDescription.value = '蓝牙服务初始化失败'; + _isBluetoothServiceAvailable.value = false; + isLoading.value = false; return; } - // 权限已授予,检查蓝牙状态 - if (!_bluetoothService.isOn) { - _updateStatus('蓝牙已关闭'); - _setError('蓝牙已关闭,请开启蓝牙后再试'); + // 2. 检查权限 + statusDescription.value = '正在检查权限...'; + final hasPermission = await _bluetoothService.checkAndRequestPermissions(); + if (!hasPermission) { + statusDescription.value = '缺少蓝牙权限'; + isLoading.value = false; return; } - // 刷新设备信息 - _refreshDeviceInfo(); - } catch (e) { - Logger.error('权限请求失败: $e'); - _updateStatus('权限请求失败'); - _setError('权限请求失败,请手动授予蓝牙和位置权限'); - } - } - - // 处理权限被拒绝的情况 - void _handlePermissionDenied() async { - // 检查是否永久拒绝 - bool isPermanentlyDenied = false; - - if (GetPlatform.isAndroid) { - PermissionStatus bluetoothScan = await Permission.bluetoothScan.status; - PermissionStatus bluetoothConnect = await Permission.bluetoothConnect.status; - PermissionStatus location = await Permission.location.status; + // 3. 刷新设备列表 + statusDescription.value = '正在搜索设备...'; + await refreshConnectedDevices(); - // 如果任一权限被永久拒绝 - isPermanentlyDenied = - bluetoothScan.isPermanentlyDenied || - bluetoothConnect.isPermanentlyDenied || - location.isPermanentlyDenied; - } - - _updateStatus('缺少必要权限'); - - if (isPermanentlyDenied) { - _setError('缺少必要权限,您已永久拒绝权限,请前往系统设置中手动开启'); - // 显示前往设置的提示 - _showGoToSettingsDialog(); - } else { - _setError('缺少必要权限,请授予蓝牙和位置权限以继续'); + // 4. 处理结果 + if (_bluetoothService.connectedDevices.isNotEmpty) { + statusDescription.value = '找到已连接的设备'; + // 延迟跳转到主页 + _autoNavigateToHome(); + } else { + // 没有找到设备 + statusDescription.value = '未找到已连接的蓝牙设备'; + } + + } catch (e) { + Logger.error('初始化配对控制器失败: $e'); + statusDescription.value = '初始化失败'; + _isBluetoothServiceAvailable.value = false; + } finally { + isLoading.value = false; } } - // 显示前往设置的对话框 - void _showGoToSettingsDialog() { - Get.dialog( - AlertDialog( - title: const Text('需要权限'), - content: const Text('蓝牙搜索需要特定权限,请前往系统设置开启'), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('稍后'), - ), - TextButton( - onPressed: () { - Get.back(); - openAppSettings(); - }, - child: const Text('前往设置'), - ), - ], - ), - barrierDismissible: false, - ); - } - - // 刷新设备信息 - Future _refreshDeviceInfo() async { - // 防止重复刷新 - if (_isRefreshing) return; - - _isRefreshing = true; - - if (!_isBluetoothOn.value) { - _updateStatus('蓝牙已关闭'); - _deviceInfo.value = {}; - _isRefreshing = false; - return; - } - + /// 刷新设备 + Future refreshConnectedDevices() async { try { - // 更新已配对设备列表 - _pairedDevices.value = _bluetoothService.systemPairedDevices; + isLoading.value = true; + statusDescription.value = '正在搜索设备...'; - // 查找系统已连接设备 - BluetoothDevice? device = await _bluetoothService.findConnectedSystemDevice(); + final hasDevices = await _bluetoothService.refreshConnectedDevices(); - if (device != null) { - // 已连接设备 - _deviceInfo.value = _bluetoothService.getConnectedDeviceInfo(); - _updateStatus('已连接到蓝牙设备'); - _goToHomePage(); - } else if (_bluetoothService.lastConnectedDeviceInfo.isNotEmpty) { - // 使用上次连接的设备信息 - _deviceInfo.value = _bluetoothService.lastConnectedDeviceInfo; - _updateStatus('使用上次连接的设备信息'); - _goToHomePage(); - } else if (_pairedDevices.isNotEmpty) { - // 使用已配对设备 - BluetoothDevice pairedDevice = _pairedDevices.first; - _deviceInfo.value = _bluetoothService.getPairedDeviceInfo(pairedDevice); - _bluetoothService.setSelectedDevice(pairedDevice); - _updateStatus('已找到配对设备'); - _goToHomePage(); + if (hasDevices) { + statusDescription.value = '找到已连接的设备'; + // 延迟跳转到主页 + _autoNavigateToHome(); } else { - // 没有找到任何设备 - _deviceInfo.value = {}; - _updateStatus('未找到已连接或配对的蓝牙设备'); + // 没有找到设备 + statusDescription.value = '未找到已连接的蓝牙设备'; } } catch (e) { - Logger.error('刷新设备信息失败: $e'); - _updateStatus('获取设备信息失败'); + Logger.error('刷新设备失败: $e'); + statusDescription.value = '刷新设备失败'; } finally { - _isRefreshing = false; + isLoading.value = false; } } - // 更新状态描述 - void _updateStatus(String description) { - _statusDescription.value = description; - Logger.info('状态更新: $description'); - } - - // 设置错误信息 - void _setError(String message) { - _errorMessage.value = message; - Logger.warning('错误: $message'); + /// 自动跳转到主页 + void _autoNavigateToHome() { + if (_bluetoothService.connectedDevices.isEmpty) return; + + Logger.info('已找到连接的设备,自动跳转到主页'); + + Future.delayed(const Duration(milliseconds: 500), () { + Get.offAllNamed(Routes.home); + }); } - // 跳转到主页 - void _goToHomePage() { - Get.offAllNamed(Routes.home, arguments: _deviceInfo.value); + /// 打开系统蓝牙设置 + void openBluetoothSettings() { + _bluetoothService.openBluetoothSettings(); } - // 刷新搜索 - void refreshSearch() { - // 防止频繁刷新,限制最小刷新间隔为2秒 - final now = DateTime.now(); - if (_lastRefreshTime != null && - now.difference(_lastRefreshTime!).inSeconds < 2) { - return; - } - - _lastRefreshTime = now; - _refreshDeviceInfo(); + /// 刷新设备(兼容旧代码) + Future refreshDevices() async { + await refreshConnectedDevices(); } - // 跳过连接 + /// 跳过连接 void skipConnecting() { Get.offAllNamed(Routes.home); } diff --git a/lib/modules/pairing/views/pairing_view.dart b/lib/modules/pairing/views/pairing_view.dart index e85143d0e..40de49a0c 100644 --- a/lib/modules/pairing/views/pairing_view.dart +++ b/lib/modules/pairing/views/pairing_view.dart @@ -4,7 +4,6 @@ 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'; -import 'package:permission_handler/permission_handler.dart'; class PairingView extends GetView { const PairingView({Key? key}) : super(key: key); @@ -16,15 +15,10 @@ class PairingView extends GetView { return Scaffold( backgroundColor: isDarkMode ? Colors.black : Colors.white, body: SafeArea( - child: Stack( - children: [ - // 主要内容 - _buildMainContent(isDarkMode), - ], - ), + child: _buildMainContent(isDarkMode), ), bottomNavigationBar: Opacity( - opacity: 0.5, // 保持半透明效果 + opacity: 0.5, child: const CommonBottomNav(currentIndex: 0), ), ); @@ -32,40 +26,57 @@ class PairingView extends GetView { // 主要内容 Widget _buildMainContent(bool isDarkMode) { - return Padding( - padding: EdgeInsets.fromLTRB(24.w, 40.h, 24.w, 24.h), + return Container( + width: double.infinity, + height: MediaQuery.of(Get.context!).size.height, + padding: EdgeInsets.symmetric(horizontal: 24.w), + // 添加SingleChildScrollView以防止溢出 child: SingleChildScrollView( - child: Column( - children: [ - // 标题部分 - _buildHeader(isDarkMode), - - // 设备动画 - SizedBox(height: 40.h), - _buildDeviceAnimation(), - - // 搜索状态 - SizedBox(height: 40.h), - _buildSearchStatus(isDarkMode), - - // 底部按钮 - SizedBox(height: 60.h), - _buildBottomButtons(isDarkMode), - SizedBox(height: 80.h), // 留出底部导航的空间 - ], + physics: BouncingScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: MediaQuery.of(Get.context!).size.height - 100.h, // 减去底部导航和状态栏高度 + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: 20.h), // 顶部间距 + + // 标题部分 + _buildHeader(isDarkMode), + + SizedBox(height: 20.h), + + // 设备动画 - 更大尺寸 + _buildDeviceAnimation(), + + SizedBox(height: 20.h), + + // 状态信息 + _buildStatusInfo(isDarkMode), + + SizedBox(height: 40.h), + + // 底部按钮 + _buildActionButtons(isDarkMode), + + SizedBox(height: 20.h), + ], + ), ), ), ); } - // 标题部分 + // 头部标题 Widget _buildHeader(bool isDarkMode) { return Column( children: [ Text( 'DeepVoice', style: TextStyle( - fontSize: 24.sp, + fontSize: 30.sp, fontWeight: FontWeight.bold, color: isDarkMode ? Colors.white : Colors.black, ), @@ -74,8 +85,8 @@ class PairingView extends GetView { Text( '连接世界,超越声音', style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, + fontSize: 16.sp, + color: isDarkMode ? Colors.white60 : Colors.black54, ), ), ], @@ -84,298 +95,236 @@ class PairingView extends GetView { // 设备动画 Widget _buildDeviceAnimation() { - return SizedBox( + return Container( width: 240.w, height: 240.w, child: Stack( alignment: Alignment.center, children: [ - // 脉动动画背景 - _buildPulseBackground(), + // 脉搏背景 - 更显眼的渐变效果 + TweenAnimationBuilder( + tween: Tween(begin: 0.8, end: 1.1), + duration: const Duration(seconds: 2), + curve: Curves.easeInOut, + builder: (context, value, child) { + return Container( + width: 200.w * value, + height: 200.w * value, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF0066CC).withOpacity(0.3), + Color(0xFF5E5CE6).withOpacity(0.3), + ], + ), + shape: BoxShape.circle, + ), + ); + }, + onEnd: () { + // 重新开始动画 + }, + ), - // 耳机图片 - _buildHeadphoneImage(), + // 旋转边框 + SizedBox( + width: 100.w, + height: 100.w, + child: CircularProgressIndicator( + strokeWidth: 4.w, + valueColor: AlwaysStoppedAnimation( + Color(0xFF0066CC), + ), + backgroundColor: Colors.grey.withOpacity(0.2), + ), + ), - // 旋转加载指示器 - _buildLoadingIndicator(), - ], - ), - ); - } - - // 脉动背景 - Widget _buildPulseBackground() { - return TweenAnimationBuilder( - tween: Tween(begin: 0.8, end: 1.1), - duration: const Duration(seconds: 2), - curve: Curves.easeInOut, - builder: (context, value, child) { - return Transform.scale( - scale: value, - child: Container( - width: 200.w, - height: 200.w, + // 设备图标 + Container( + width: 120.w, + height: 120.w, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - const Color(0xFF0066CC).withOpacity(0.2), - const Color(0xFF5E5CE6).withOpacity(0.2), + Color(0xFF0066CC), + Color(0xFF5E5CE6), ], ), shape: BoxShape.circle, ), + child: Icon( + Icons.headphones, + color: Colors.white, + size: 60.w, + ), ), - ); - }, - ); - } - - // 耳机图片 - Widget _buildHeadphoneImage() { - return Container( - width: 140.w, - height: 140.w, - decoration: BoxDecoration( - gradient: const LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Color(0xFF0066CC), - Color(0xFF5E5CE6), - ], - ), - shape: BoxShape.circle, - ), - child: Icon( - Icons.headset, - size: 80.sp, - color: Colors.white, - ), - ); - } - - // 加载指示器 - Widget _buildLoadingIndicator() { - return SizedBox( - width: 180.w, - height: 180.w, - child: CircularProgressIndicator( - strokeWidth: 4.w, - valueColor: const AlwaysStoppedAnimation(Color(0xFF0066CC)), - backgroundColor: const Color(0xFF0066CC).withOpacity(0.1), + ], ), ); } - - // 搜索状态 - Widget _buildSearchStatus(bool isDarkMode) { + + // 状态信息 + Widget _buildStatusInfo(bool isDarkMode) { return Obx(() { - final String status = controller.statusDescription; - final bool hasDevice = controller.deviceInfo.isNotEmpty && controller.deviceInfo['device'] != null; - final bool hasError = controller.errorMessage.isNotEmpty; - - // 已找到设备 - if (hasDevice) { - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.check_circle, color: Colors.green, size: 20.sp), - SizedBox(width: 8.w), - Text( - '设备信息获取成功', - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.w600, - color: Colors.green, - ), - ), - ], - ), - SizedBox(height: 8.h), - Text( - '正在跳转到主页...', - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - ], - ); - } + final status = controller.statusDescription.value; + final isLoading = controller.isLoading.value; - // 有错误信息 - if (hasError) { - // 蓝牙关闭错误 - if (status.contains('蓝牙已关闭')) { - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.bluetooth_disabled, color: Colors.orange, size: 20.sp), - SizedBox(width: 8.w), - Text( - '蓝牙已关闭', - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.w600, - color: Colors.orange, - ), - ), - ], - ), - SizedBox(height: 8.h), - Text( - controller.errorMessage, - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - textAlign: TextAlign.center, - ), - ], - ); - } - - // 权限错误 - if (status.contains('权限')) { - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.no_encryption, color: Colors.orange, size: 20.sp), - SizedBox(width: 8.w), - Text( - '缺少必要权限', - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.w600, - color: Colors.orange, - ), - ), - ], - ), - SizedBox(height: 8.h), - Text( - controller.errorMessage, - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - textAlign: TextAlign.center, - ), - SizedBox(height: 16.h), - ElevatedButton( - onPressed: () => openAppSettings(), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.orange, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.r), - ), - ), - child: const Text('打开设置'), - ), - ], - ); - } - - // 其他错误 - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.error, color: Colors.red, size: 20.sp), - SizedBox(width: 8.w), - Text( - status.isEmpty ? '未找到设备' : status, - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.w600, - color: Colors.red, - ), - ), - ], - ), - SizedBox(height: 8.h), - Text( - controller.errorMessage.isEmpty ? - '请确保耳机已开机并在附近' : controller.errorMessage, - style: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - textAlign: TextAlign.center, - ), - ], - ); - } - - // 默认状态(正在搜索/初始化) return Column( children: [ + // 状态文本 - 更大更醒目 Text( status, style: TextStyle( - fontSize: 18.sp, + fontSize: 20.sp, fontWeight: FontWeight.w600, color: isDarkMode ? Colors.white : Colors.black, ), + textAlign: TextAlign.center, ), - SizedBox(height: 8.h), + + SizedBox(height: 10.h), + + // 子状态提示文本 Text( '请打开耳机充电盒,将耳机放入您的耳朵', style: TextStyle( fontSize: 14.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, + color: isDarkMode ? Colors.white60 : Colors.grey[600], ), + textAlign: TextAlign.center, ), + + // 设备列表(有设备时才显示) + if (controller.connectedClassicDevices.isNotEmpty) ...[ + SizedBox(height: 24.h), + _buildConnectedDevicesList(isDarkMode), + ], ], ); }); } - - // 底部按钮 - Widget _buildBottomButtons(bool isDarkMode) { - return Column( - children: [ - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: controller.refreshSearch, + + // 已连接设备列表 + Widget _buildConnectedDevicesList(bool isDarkMode) { + return Obx(() { + final devices = controller.connectedClassicDevices; + + if (devices.isEmpty) { + return const SizedBox.shrink(); + } + + return Container( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '当前已连接:', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white70 : Colors.black87, + ), + ), + SizedBox(height: 8.h), + ...devices.map((device) => _buildDeviceItem(device, isDarkMode)), + ], + ), + ); + }); + } + + // 设备项 + Widget _buildDeviceItem(device, bool isDarkMode) { + return Container( + margin: EdgeInsets.only(bottom: 8.h), + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.grey[100], + borderRadius: BorderRadius.circular(8.r), + ), + child: Row( + children: [ + Icon( + Icons.headphones, + color: isDarkMode ? Colors.white70 : Colors.black54, + size: 24.w, + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + device.name, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + SizedBox(height: 4.h), + Text( + device.address, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white54 : Colors.black45, + ), + ), + ], + ), + ), + Icon( + Icons.bluetooth_connected, + color: Colors.green, + size: 20.w, + ), + ], + ), + ); + } + + // 操作按钮 + Widget _buildActionButtons(bool isDarkMode) { + return Container( + width: double.infinity, + child: Column( + children: [ + // 主按钮 - 改为更宽的圆角按钮 + ElevatedButton( + onPressed: () => controller.openBluetoothSettings(), style: ElevatedButton.styleFrom( - backgroundColor: isDarkMode ? Color(0xFF1A1A1A) : Colors.grey.withOpacity(0.1), - foregroundColor: isDarkMode ? Colors.white : Colors.black, + backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[200], + foregroundColor: isDarkMode ? Colors.white : Colors.black87, padding: EdgeInsets.symmetric(vertical: 16.h), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16.r), ), elevation: 0, + minimumSize: Size(double.infinity, 56.h), ), child: Text( - '刷新搜索', + '去系统设置', style: TextStyle( fontSize: 16.sp, - fontWeight: FontWeight.w500, + fontWeight: FontWeight.w600, ), ), ), - ), - SizedBox(height: 12.h), - SizedBox( - width: double.infinity, - child: TextButton( - onPressed: controller.skipConnecting, + + SizedBox(height: 12.h), + + // 次要按钮 + TextButton( + onPressed: () => controller.skipConnecting(), style: TextButton.styleFrom( - foregroundColor: const Color(0xFF0066CC), + foregroundColor: Color(0xFF0066CC), padding: EdgeInsets.symmetric(vertical: 12.h), + minimumSize: Size(double.infinity, 48.h), ), child: Text( '稍后再说', @@ -385,8 +334,8 @@ class PairingView extends GetView { ), ), ), - ), - ], + ], + ), ); } } \ No newline at end of file diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index 6b57f13c3..727eb72b2 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -41,22 +41,25 @@ class TranslationView extends GetView { mainAxisSize: MainAxisSize.min, children: [ // 源语言选择 - GestureDetector( - onTap: () => _showLanguageSelectionDialog(true, isDarkMode), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), - decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], - borderRadius: BorderRadius.circular(16.r), - ), - child: Obx(() => Text( - controller.sourceLanguage.value.split(' ')[0], // Just show the Chinese part - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, + Flexible( + child: GestureDetector( + onTap: () => _showLanguageSelectionDialog(true, isDarkMode), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( + color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], + borderRadius: BorderRadius.circular(16.r), ), - )), + child: Obx(() => Text( + controller.sourceLanguage.value.split(' ')[0], // Just show the Chinese part + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + overflow: TextOverflow.ellipsis, + )), + ), ), ), @@ -79,22 +82,25 @@ class TranslationView extends GetView { ), // 目标语言选择 - GestureDetector( - onTap: () => _showLanguageSelectionDialog(false, isDarkMode), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), - decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], - borderRadius: BorderRadius.circular(16.r), - ), - child: Obx(() => Text( - controller.targetLanguage.value.split(' ')[0], // Just show the English part - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, + Flexible( + child: GestureDetector( + onTap: () => _showLanguageSelectionDialog(false, isDarkMode), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( + color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], + borderRadius: BorderRadius.circular(16.r), ), - )), + child: Obx(() => Text( + controller.targetLanguage.value.split(' ')[0], // Just show the English part + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black, + ), + overflow: TextOverflow.ellipsis, + )), + ), ), ), ], diff --git a/pubspec.yaml b/pubspec.yaml index 89598e4a7..f4e29d2e3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,6 +46,7 @@ dependencies: audio_waveforms: ^1.3.0 font_awesome_flutter: ^10.8.0 lottie: ^3.3.1 + app_settings: 5.2.0 dev_dependencies: flutter_test: