diff --git a/android/app/src/main/res/raw/sample.mp3 b/android/app/src/main/res/raw/sample.mp3 new file mode 100644 index 000000000..31ebc28ce Binary files /dev/null and b/android/app/src/main/res/raw/sample.mp3 differ diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 3fa1bbd4e..88a2148e0 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -8,6 +8,9 @@ /* Begin PBXBuildFile section */ 0A5EDBFC2D89C4FD00215E26 /* ClassicBluetoothHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */; }; + 0A5EDC042D8B544100215E26 /* BluetoothMediaButtonHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */; }; + 0ADE29692D8C25660032BFCB /* silence.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0ADE29682D8C25660032BFCB /* silence.mp3 */; }; + 0ADE296A2D8C25660032BFCB /* sample.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0ADE29672D8C25660032BFCB /* sample.mp3 */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3406320CAE3314A73EF78043 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */; }; @@ -46,6 +49,9 @@ 02CF74744224D1C933681603 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 0906EF7A73CBA5EAFC64E05D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicBluetoothHelper.swift; sourceTree = ""; }; + 0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMediaButtonHelper.swift; sourceTree = ""; }; + 0ADE29672D8C25660032BFCB /* sample.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = sample.mp3; sourceTree = ""; }; + 0ADE29682D8C25660032BFCB /* silence.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = silence.mp3; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; @@ -145,6 +151,9 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 0ADE29672D8C25660032BFCB /* sample.mp3 */, + 0ADE29682D8C25660032BFCB /* silence.mp3 */, + 0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */, 0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, @@ -266,6 +275,8 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 0ADE29692D8C25660032BFCB /* silence.mp3 in Resources */, + 0ADE296A2D8C25660032BFCB /* sample.mp3 in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -399,6 +410,7 @@ files = ( 0A5EDBFC2D89C4FD00215E26 /* ClassicBluetoothHelper.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 0A5EDC042D8B544100215E26 /* BluetoothMediaButtonHelper.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index d44758105..70e9d28f1 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -4,119 +4,189 @@ import UIKit @main @objc class AppDelegate: FlutterAppDelegate { private var classicBluetoothHelper: ClassicBluetoothHelper? + private var mediaButtonHelper: Any? override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - // 初始化ClassicBluetoothHelper - setupClassicBluetoothHelper() + // 初始化蓝牙辅助类 + classicBluetoothHelper = ClassicBluetoothHelper() + + // 立即初始化蓝牙,不要等待Flutter调用 + classicBluetoothHelper?.initBluetooth() + + // 仅在iOS 13.0及以上版本初始化媒体按钮辅助类 + if #available(iOS 13.0, *) { + mediaButtonHelper = BluetoothMediaButtonHelper() + } + + // 建立Flutter方法通道 + registerMethodChannel() GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } - private func setupClassicBluetoothHelper() { + private func registerMethodChannel() { let controller = window?.rootViewController as? FlutterViewController - if let controller = controller { - // 初始化ClassicBluetoothHelper - classicBluetoothHelper = ClassicBluetoothHelper() - - // 启动蓝牙设备监控 - classicBluetoothHelper?.startBluetoothDeviceMonitoring() + guard let controller = controller else { + NSLog("AppDelegate: FlutterViewController 不可用") + return + } + + // 注册经典蓝牙通道 + let classicChannel = FlutterMethodChannel( + name: "com.deep_voice.classic_bluetooth", + binaryMessenger: controller.binaryMessenger + ) + + classicChannel.setMethodCallHandler { [weak self] (call, result) in + guard let self = self else { + result(FlutterError(code: "UNAVAILABLE", message: "AppDelegate实例不可用", details: nil)) + return + } - // 注册蓝牙通道 - let channel = FlutterMethodChannel(name: "com.deep_voice.classic_bluetooth", binaryMessenger: controller.binaryMessenger) + switch call.method { + case "isBluetoothEnabled": + result(self.classicBluetoothHelper?.isBluetoothEnabled() ?? false) + + case "getConnectedHeadsetDevices": + // 获取已连接的耳机设备 + self.classicBluetoothHelper?.getConnectedHeadsetDevices { devices, error in + if let error = error { + result(FlutterError(code: "GET_DEVICES_FAILED", message: error, details: nil)) + } else { + result(devices) + } + } + + case "startBluetoothDeviceMonitoring": + // 启动蓝牙设备监控 + self.classicBluetoothHelper?.initBluetooth() + result(true) + + default: + result(FlutterMethodNotImplemented) + } + } + + // 注册蓝牙事件通道 + let classicEventChannel = FlutterEventChannel( + name: "com.deep_voice.classic_bluetooth_events", + binaryMessenger: controller.binaryMessenger + ) + + // 创建事件处理器 + let bluetoothEventHandler = BluetoothStreamHandler(bluetoothHelper: self.classicBluetoothHelper!) + classicEventChannel.setStreamHandler(bluetoothEventHandler) + + // 仅在iOS 13.0及以上版本注册媒体按钮通道 + if #available(iOS 13.0, *), let mediaButtonHelper = mediaButtonHelper as? BluetoothMediaButtonHelper { + // 注册媒体按钮方法通道 + let mediaButtonChannel = FlutterMethodChannel( + name: "com.deep_voice.bluetooth_media_button", + binaryMessenger: controller.binaryMessenger + ) - channel.setMethodCallHandler { [weak self] (call, result) in - guard let self = self else { - result(FlutterError(code: "UNAVAILABLE", message: "ClassicBluetoothHelper实例不可用", details: nil)) + mediaButtonChannel.setMethodCallHandler { [weak self, weak mediaButtonHelper] (call, result) in + guard let _ = self, let mediaButtonHelper = mediaButtonHelper else { + result(FlutterError(code: "UNAVAILABLE", message: "媒体按钮辅助实例不可用", details: nil)) return } switch call.method { - case "isBluetoothEnabled": - result(self.classicBluetoothHelper?.isBluetoothEnabled()) - - case "getConnectedA2dpDevices": - self.classicBluetoothHelper?.getConnectedA2dpDevices { devices, error in - if let error = error { - result(FlutterError(code: "GET_A2DP_DEVICES_FAILED", message: error, details: nil)) - } else { - result(devices) - } - } - - case "getConnectedHeadsetDevices": - self.classicBluetoothHelper?.getConnectedHeadsetDevices { devices, error in - if let error = error { - result(FlutterError(code: "GET_HEADSET_DEVICES_FAILED", message: error, details: nil)) - } else { - result(devices) - } - } - - case "checkAndRequestNativePermission": - self.classicBluetoothHelper?.checkAndRequestNativePermission { permissionStatus in - result(permissionStatus) - } - - case "startBluetoothDeviceMonitoring": - self.classicBluetoothHelper?.startBluetoothDeviceMonitoring() - result(true) - - case "stopBluetoothDeviceMonitoring": - self.classicBluetoothHelper?.stopBluetoothDeviceMonitoring() - result(true) + case "startButtonListening": + let success = mediaButtonHelper.startButtonListening() + result(success) - case "openSettings": - result(self.classicBluetoothHelper?.openSettings()) - - case "openBluetoothSettings": - result(self.classicBluetoothHelper?.openBluetoothSettings()) + case "stopButtonListening": + let success = mediaButtonHelper.stopButtonListening() + result(success) default: result(FlutterMethodNotImplemented) } } - // 注册蓝牙事件通道 - let eventChannel = FlutterEventChannel(name: "com.deep_voice.classic_bluetooth_events", binaryMessenger: controller.binaryMessenger) + // 注册媒体按钮事件通道 + let mediaButtonEventChannel = FlutterEventChannel( + name: "com.deep_voice.bluetooth_media_button_events", + binaryMessenger: controller.binaryMessenger + ) - // 创建事件处理器 - let eventHandler = BluetoothStreamHandler(bluetoothHelper: self.classicBluetoothHelper!) - eventChannel.setStreamHandler(eventHandler) + // 创建媒体按钮事件处理器 + let mediaButtonEventHandler = MediaButtonStreamHandler(mediaButtonHelper: mediaButtonHelper) + mediaButtonEventChannel.setStreamHandler(mediaButtonEventHandler) } } } // 蓝牙事件流处理器 -class BluetoothStreamHandler: NSObject, FlutterStreamHandler { +class BluetoothStreamHandler: NSObject, FlutterStreamHandler, BluetoothEventHandler { private let bluetoothHelper: ClassicBluetoothHelper private var eventSink: FlutterEventSink? init(bluetoothHelper: ClassicBluetoothHelper) { self.bluetoothHelper = bluetoothHelper super.init() + + // 设置事件处理器 + bluetoothHelper.setEventHandler(self) } func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { NSLog("BluetoothStreamHandler: 开始监听蓝牙事件") self.eventSink = events - - // 注册设备事件回调 - bluetoothHelper.registerDeviceEventCallback { [weak self] event in - NSLog("BluetoothStreamHandler: 收到蓝牙事件 \(event)") + return nil + } + + func onCancel(withArguments arguments: Any?) -> FlutterError? { + NSLog("BluetoothStreamHandler: 取消监听蓝牙事件") + eventSink = nil + return nil + } + + // 实现BluetoothEventHandler协议 + func sendEvent(_ event: [String: Any]) { + NSLog("BluetoothStreamHandler: 接收到蓝牙事件: \(event)") + DispatchQueue.main.async { [weak self] in self?.eventSink?(event) } + } +} + +// 媒体按钮事件流处理器 +@available(iOS 13.0, *) +class MediaButtonStreamHandler: NSObject, FlutterStreamHandler, MediaButtonEventHandler { + private let mediaButtonHelper: BluetoothMediaButtonHelper + private var eventSink: FlutterEventSink? + + init(mediaButtonHelper: BluetoothMediaButtonHelper) { + self.mediaButtonHelper = mediaButtonHelper + super.init() + // 设置事件处理器 + mediaButtonHelper.setEventHandler(self) + } + + func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + NSLog("MediaButtonStreamHandler: 开始监听媒体按钮事件") + self.eventSink = events return nil } func onCancel(withArguments arguments: Any?) -> FlutterError? { - NSLog("BluetoothStreamHandler: 取消监听蓝牙事件") - // 不要在这里调用dispose,只需清除回调 + NSLog("MediaButtonStreamHandler: 取消监听媒体按钮事件") eventSink = nil return nil } + + // 实现MediaButtonEventHandler协议 + func sendEvent(_ event: [String: Any]) { + NSLog("MediaButtonStreamHandler: 收到按钮事件 \(event)") + DispatchQueue.main.async { [weak self] in + self?.eventSink?(event) + } + } } diff --git a/ios/Runner/BluetoothMediaButtonHelper.swift b/ios/Runner/BluetoothMediaButtonHelper.swift new file mode 100644 index 000000000..a7495693e --- /dev/null +++ b/ios/Runner/BluetoothMediaButtonHelper.swift @@ -0,0 +1,499 @@ +import Foundation +import MediaPlayer +import AVFoundation +import UIKit + +/// 媒体按钮事件处理协议 +@available(iOS 13.0, *) +@objc protocol MediaButtonEventHandler: AnyObject { + func sendEvent(_ event: [String: Any]) +} + +/// 蓝牙媒体按键事件处理类 +/// 该类仅在iOS 13.0及以上版本可用 +@available(iOS 13.0, *) +class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate { + // MARK: - 属性 + + /// 日志标签 + private let TAG = "BluetoothMediaButtonHelper" + + /// 远程控制中心 + private var remoteCommandCenter: MPRemoteCommandCenter + + /// 事件处理器 - 用于发送事件到Flutter + private weak var eventHandler: MediaButtonEventHandler? + + /// 是否已启用监听 + private var isListening = false + + /// 静音音频播放器 - 用于保持音频会话活跃 + private var silentAudioPlayer: AVAudioPlayer? + + /// 播放计时器 - 用于定期更新播放信息 + private var playbackTimer: Timer? + + /// 当前播放时间 - 模拟播放进度 + private var currentPlaybackTime: TimeInterval = 0 + + /// 总时长 - 模拟 + private let totalDuration: TimeInterval = 300.0 + + // MARK: - 初始化 + + override init() { + // 获取远程控制中心单例 + self.remoteCommandCenter = MPRemoteCommandCenter.shared() + + super.init() + + NSLog("\(TAG): 初始化媒体按钮处理器") + + // 初始化静音音频播放器 + initSilentAudioPlayer() + } + + deinit { + stopButtonListening() + NSLog("\(TAG): 媒体按钮处理器已释放") + } + + // MARK: - 公共方法 + + /// 设置事件处理器 + @objc func setEventHandler(_ handler: MediaButtonEventHandler) { + eventHandler = handler + NSLog("\(TAG): 已设置媒体按钮事件处理器") + } + + /// 开始监听蓝牙媒体按钮事件 + @objc func startButtonListening() -> Bool { + if isListening { + NSLog("\(TAG): 已经在监听蓝牙媒体按钮事件") + return true + } + + NSLog("\(TAG): 开始监听蓝牙媒体按钮事件") + + // 立即注册远程命令 - 这不需要音频会话 + registerRemoteCommands() + + // 标记为已在监听 + isListening = true + + // 确保NowPlaying信息可用 - 这也不依赖于活跃的音频会话 + setupNowPlaying(isPlaying: false) + + // 尝试启用静音音频播放(但即使失败,远程命令仍然可用) + do { + try passivelySetupAudioSession() + // startSilentAudio() + // startUpdatingPlaybackInfo() + } catch { + NSLog("\(TAG): 音频设置或播放失败: \(error.localizedDescription)") + // 继续执行,因为远程命令在某些设备上不需要播放音频也能工作 + } + + NSLog("\(TAG): 蓝牙媒体按钮监听已启动") + return true + } + + /// 停止监听蓝牙媒体按钮事件 + @objc func stopButtonListening() -> Bool { + if !isListening { + return true + } + + NSLog("\(TAG): 停止监听蓝牙媒体按钮事件") + + // 停止播放静音音频 + stopSilentAudio() + + // 停止更新播放信息 + stopUpdatingPlaybackInfo() + + // 取消注册远程命令 + unregisterRemoteCommands() + + // 清除NowPlaying信息 + DispatchQueue.main.async { + MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + } + + isListening = false + NSLog("\(TAG): 蓝牙媒体按钮监听已停止") + return true + } + + // MARK: - 私有方法 + + /// 初始化静音音频播放器 + private func initSilentAudioPlayer() { + do { + // 尝试加载sample.mp3文件 + if let audioPath = Bundle.main.path(forResource: "sample", ofType: "mp3") { + let audioUrl = URL(fileURLWithPath: audioPath) + + silentAudioPlayer = try AVAudioPlayer(contentsOf: audioUrl) + silentAudioPlayer?.numberOfLoops = -1 // 无限循环播放 + silentAudioPlayer?.volume = 1.0 // 设置为全音量 + silentAudioPlayer?.prepareToPlay() // 预加载 + silentAudioPlayer?.delegate = self + + NSLog("\(TAG): 音频播放器使用sample.mp3初始化成功") + return + } + + NSLog("\(TAG): 未找到sample.mp3文件,使用生成的静音数据") + + } catch { + NSLog("\(TAG): 初始化静音音频播放器失败: \(error.localizedDescription)") + } + } + + /// 开始播放静音音频 + private func startSilentAudio() { + guard let player = silentAudioPlayer else { + NSLog("\(TAG): 无法开始播放,播放器未初始化") + return + } + + if !player.isPlaying { + let success = player.play() + if success { + NSLog("\(TAG): 开始播放静音音频") + } else { + NSLog("\(TAG): 播放启动失败") + } + } else { + NSLog("\(TAG): 静音音频已在播放中") + } + } + + /// 停止播放静音音频 + private func stopSilentAudio() { + silentAudioPlayer?.stop() + NSLog("\(TAG): 停止播放静音音频") + } + + /// 开始更新播放信息 + private func startUpdatingPlaybackInfo() { + // 初始设置 + setupNowPlaying(isPlaying: true) + + // 定期更新播放信息 - 恢复定时器,确保持续更新播放状态 + playbackTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in + guard let self = self else { return } + + // 更新播放时间(模拟) + self.currentPlaybackTime += 1.0 + if self.currentPlaybackTime >= self.totalDuration { + self.currentPlaybackTime = 0.0 + } + + // 更新NowPlaying信息 + self.setupNowPlaying(isPlaying: true) + } + + NSLog("\(TAG): 开始更新播放信息") + } + + /// 停止更新播放信息 + private func stopUpdatingPlaybackInfo() { + playbackTimer?.invalidate() + playbackTimer = nil + + // 清空NowPlaying信息 + DispatchQueue.main.async { + MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + } + + NSLog("\(TAG): 停止更新播放信息") + } + + /// 设置NowPlaying信息 + private func setupNowPlaying(isPlaying: Bool) { + NSLog("\(TAG): 设置NowPlaying信息, isPlaying=\(isPlaying)") + + // 创建音频专辑图片 + let albumArt = UIImage(named: "AppIcon") // 使用应用图标作为专辑图片 + var nowPlayingInfo: [String: Any] = [ + MPMediaItemPropertyTitle: "DeepVoice 语音助手", + MPMediaItemPropertyArtist: "DeepVoice", + MPNowPlayingInfoPropertyPlaybackRate: isPlaying ? 1.0 : 0.0, // 1.0表示正在播放 + MPNowPlayingInfoPropertyElapsedPlaybackTime: currentPlaybackTime, + MPMediaItemPropertyPlaybackDuration: totalDuration + ] + + // 如果有专辑图片,添加到NowPlaying信息中 + if let albumArt = albumArt { + let artwork = MPMediaItemArtwork(boundsSize: albumArt.size) { size in + return albumArt + } + nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork + } + + // 设置NowPlaying信息 (在主线程执行) + DispatchQueue.main.async { + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + NSLog("\(self.TAG): NowPlaying信息设置完成") + } + } + + /// 注册远程命令 + private func registerRemoteCommands() { + let commandCenter = MPRemoteCommandCenter.shared() + + // 播放/暂停命令 + commandCenter.togglePlayPauseCommand.isEnabled = true + commandCenter.togglePlayPauseCommand.addTarget(self, action: #selector(handlePlayPauseCommand(_:))) + commandCenter.playCommand.isEnabled = true + commandCenter.playCommand.addTarget(self, action: #selector(handlePlayPauseCommand(_:))) + commandCenter.pauseCommand.isEnabled = true + commandCenter.pauseCommand.addTarget(self, action: #selector(handlePlayPauseCommand(_:))) + + // 下一曲/上一曲命令 + commandCenter.nextTrackCommand.isEnabled = true + commandCenter.nextTrackCommand.addTarget(self, action: #selector(handleNextTrackCommand(_:))) + commandCenter.previousTrackCommand.isEnabled = true + commandCenter.previousTrackCommand.addTarget(self, action: #selector(handlePreviousTrackCommand(_:))) + + // 快进/快退命令 + commandCenter.seekForwardCommand.isEnabled = true + commandCenter.seekForwardCommand.addTarget(self, action: #selector(handleSeekForwardCommand(_:))) + commandCenter.seekBackwardCommand.isEnabled = true + commandCenter.seekBackwardCommand.addTarget(self, action: #selector(handleSeekBackwardCommand(_:))) + + NSLog("\(TAG): 远程命令已注册") + } + + /// 取消注册远程命令 + private func unregisterRemoteCommands() { + NSLog("\(TAG): 取消注册远程命令") + + // 取消所有命令 + remoteCommandCenter.playCommand.isEnabled = false + remoteCommandCenter.playCommand.removeTarget(nil) + + remoteCommandCenter.pauseCommand.isEnabled = false + remoteCommandCenter.pauseCommand.removeTarget(nil) + + remoteCommandCenter.togglePlayPauseCommand.isEnabled = false + remoteCommandCenter.togglePlayPauseCommand.removeTarget(nil) + + remoteCommandCenter.nextTrackCommand.isEnabled = false + remoteCommandCenter.nextTrackCommand.removeTarget(nil) + + remoteCommandCenter.previousTrackCommand.isEnabled = false + remoteCommandCenter.previousTrackCommand.removeTarget(nil) + + remoteCommandCenter.seekForwardCommand.isEnabled = false + remoteCommandCenter.seekForwardCommand.removeTarget(nil) + + remoteCommandCenter.seekBackwardCommand.isEnabled = false + remoteCommandCenter.seekBackwardCommand.removeTarget(nil) + } + + /// 被动设置音频会话 - 只读取现有配置,不做侵入性修改 + private func passivelySetupAudioSession() throws { + let audioSession = AVAudioSession.sharedInstance() + + // 如果会话已经活跃,我们完全不修改它 + if audioSession.isOtherAudioPlaying || audioSession.isInputAvailable { + NSLog("\(TAG): 检测到活跃的音频会话,不进行任何修改") + return + } + + // 检查应用是否已经拥有音频会话 + if audioSession.category != .ambient && audioSession.category != .soloAmbient { + // 应用已经有一个有意义的类别,不要修改它 + NSLog("\(TAG): 检测到应用已设置音频会话类别: \(audioSession.category)") + return + } + + NSLog("\(TAG): 当前没有活跃的音频,设置最小化会话配置") + + // 使用最不侵入的配置 + try audioSession.setCategory(.ambient, mode: .default) + + // 注意:我们不设置活跃状态,让系统自己管理 + + NSLog("\(TAG): 被动音频会话配置完成") + } + + // MARK: - 命令处理方法 + + /// 处理播放/暂停命令 + @objc func handlePlayPauseCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { + NSLog("\(TAG): 收到播放/暂停命令") + + // 创建事件数据 + let eventData: [String: Any] = [ + "type": "mediaButtonEvent", + "buttonType": "playPause", + "timestamp": Int(Date().timeIntervalSince1970 * 1000) + ] + + // 发送事件到处理器 + eventHandler?.sendEvent(eventData) + + // 检查是否有其他音频播放 + // let audioSession = AVAudioSession.sharedInstance() + // let isOtherAudioPlaying = audioSession.isOtherAudioPlaying + + // if isOtherAudioPlaying { + // NSLog("\(TAG): 检测到其他音频在播放") + // // 注意:不能直接控制其他应用的媒体,iOS不允许这种跨应用控制 + // } + + // // 更新播放状态(切换) + // let isCurrentlyPlaying = (silentAudioPlayer?.isPlaying ?? false) + // if isCurrentlyPlaying { + // stopSilentAudio() + // setupNowPlaying(isPlaying: false) + // } else { + // startSilentAudio() + // setupNowPlaying(isPlaying: true) + // } + + return .success + } + + /// 处理下一曲命令 + @objc func handleNextTrackCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { + NSLog("\(TAG): 收到下一曲命令") + + // 创建事件数据 + let eventData: [String: Any] = [ + "type": "mediaButtonEvent", + "buttonType": "next", + "timestamp": Int(Date().timeIntervalSince1970 * 1000) + ] + + // 发送事件到处理器 + eventHandler?.sendEvent(eventData) + + // 检查是否有其他音频播放 + let audioSession = AVAudioSession.sharedInstance() + if audioSession.isOtherAudioPlaying { + NSLog("\(TAG): 检测到其他音频在播放") + // 注意:不能直接控制其他应用的媒体,iOS不允许这种跨应用控制 + } + + return .success + } + + /// 处理上一曲命令 + @objc func handlePreviousTrackCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { + NSLog("\(TAG): 收到上一曲命令") + + // 创建事件数据 + let eventData: [String: Any] = [ + "type": "mediaButtonEvent", + "buttonType": "previous", + "timestamp": Int(Date().timeIntervalSince1970 * 1000) + ] + + // 发送事件到处理器 + eventHandler?.sendEvent(eventData) + + // 检查是否有其他音频播放 + let audioSession = AVAudioSession.sharedInstance() + if audioSession.isOtherAudioPlaying { + NSLog("\(TAG): 检测到其他音频在播放") + // 注意:不能直接控制其他应用的媒体,iOS不允许这种跨应用控制 + } + + return .success + } + + /// 处理快进命令 + @objc func handleSeekForwardCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { + NSLog("\(TAG): 收到快进命令") + + // 创建事件数据 + let eventData: [String: Any] = [ + "type": "mediaButtonEvent", + "buttonType": "seekForward", + "timestamp": Int(Date().timeIntervalSince1970 * 1000) + ] + + // 发送事件到处理器 + eventHandler?.sendEvent(eventData) + + // 模拟快进 + currentPlaybackTime += 10.0 + if currentPlaybackTime > totalDuration { + currentPlaybackTime = totalDuration + } + + setupNowPlaying(isPlaying: true) + + return .success + } + + /// 处理快退命令 + @objc func handleSeekBackwardCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { + NSLog("\(TAG): 收到快退命令") + + // 创建事件数据 + let eventData: [String: Any] = [ + "type": "mediaButtonEvent", + "buttonType": "seekBackward", + "timestamp": Int(Date().timeIntervalSince1970 * 1000) + ] + + // 发送事件到处理器 + eventHandler?.sendEvent(eventData) + + // 模拟快退 + currentPlaybackTime -= 10.0 + if currentPlaybackTime < 0 { + currentPlaybackTime = 0 + } + setupNowPlaying(isPlaying: true) + + return .success + } + + // MARK: - AVAudioPlayerDelegate方法 + + /// 音频播放结束回调 + func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + NSLog("\(TAG): 音频播放结束, 成功: \(flag)") + + // 如果仍在监听状态,重新开始播放 + if isListening && player == silentAudioPlayer { + player.play() + } + } + + /// 音频播放错误回调 + func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) { + if let error = error { + NSLog("\(TAG): 音频解码错误: \(error.localizedDescription)") + } else { + NSLog("\(TAG): 音频解码发生未知错误") + } + + // 重新初始化播放器 + if player == silentAudioPlayer && isListening { + initSilentAudioPlayer() + startSilentAudio() + } + } +} + +extension UInt32 { + var data: Data { + var int = self + return Data(bytes: &int, count: MemoryLayout.size) + } +} + +extension UInt16 { + var data: Data { + var int = self + return Data(bytes: &int, count: MemoryLayout.size) + } +} \ No newline at end of file diff --git a/ios/Runner/ClassicBluetoothHelper.swift b/ios/Runner/ClassicBluetoothHelper.swift index 871bf8475..ea28b553b 100644 --- a/ios/Runner/ClassicBluetoothHelper.swift +++ b/ios/Runner/ClassicBluetoothHelper.swift @@ -3,326 +3,94 @@ import CoreBluetooth import AVFoundation import UIKit -@objc class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate, CBPeripheralManagerDelegate { - private let TAG = "ClassicBluetoothHelper" +/// 蓝牙事件处理协议 +@objc protocol BluetoothEventHandler: AnyObject { + func sendEvent(_ event: [String: Any]) +} + +@objc class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate { + // MARK: - 属性 - // AVAudioSession 用于获取已连接的蓝牙设备 + // 音频会话 private let audioSession = AVAudioSession.sharedInstance() // 蓝牙管理器 private var centralManager: CBCentralManager? - private var peripheralManager: CBPeripheralManager? - // 设备列表缓存 - private var bluetoothDevices: [[String: String]] = [] + // 事件处理器 + private weak var eventHandler: BluetoothEventHandler? - // 设备连接和断开回调 - private var deviceEventCallback: (([String: Any]) -> Void)? + // MARK: - 初始化与设置 - // 初始化 override init() { super.init() - - // 初始化中央管理器来检查蓝牙状态,并将自己设置为代理 - centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: true]) - - // 初始化外设管理器 - peripheralManager = CBPeripheralManager(delegate: self, queue: nil) - - // 请求权限 - requestBluetoothPermissions() } - // 请求蓝牙权限 - private func requestBluetoothPermissions() { - // 在 iOS 13 及更高版本中,需要主动请求蓝牙权限 - if #available(iOS 13.0, *) { - // 激活音频会话将触发系统蓝牙权限请求 - do { - try audioSession.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .allowBluetoothA2DP]) - try audioSession.setActive(true) - NSLog("\(TAG): 已请求音频会话蓝牙权限") - } catch { - NSLog("\(TAG): 激活音频会话失败: \(error.localizedDescription)") - } - } + // 设置事件处理器 + @objc func setEventHandler(_ handler: BluetoothEventHandler) { + eventHandler = handler } - // CBCentralManagerDelegate 方法 - func centralManagerDidUpdateState(_ central: CBCentralManager) { - var stateString = "unknown" - - switch central.state { - case .poweredOn: - stateString = "on" - NSLog("\(TAG): centralManagerDidUpdateState: 蓝牙已启用") - // 蓝牙已打开,可以开始扫描或其他操作 - // 初始化设备列表 - 主要是为了触发 iOS 的权限请求 - _ = getConnectedAudioDevices() - case .poweredOff: - stateString = "off" - NSLog("\(TAG): centralManagerDidUpdateState: 蓝牙已关闭") - // 清空设备列表 - bluetoothDevices.removeAll() - case .resetting: - stateString = "resetting" - NSLog("\(TAG): centralManagerDidUpdateState: 蓝牙正在重置") - case .unauthorized: - stateString = "unauthorized" - NSLog("\(TAG): centralManagerDidUpdateState: 蓝牙使用未授权") - case .unsupported: - stateString = "unsupported" - NSLog("\(TAG): centralManagerDidUpdateState: 设备不支持蓝牙") - case .unknown: - stateString = "unknown" - NSLog("\(TAG): centralManagerDidUpdateState: 蓝牙状态未知") - @unknown default: - stateString = "unknown" - NSLog("\(TAG): centralManagerDidUpdateState: 蓝牙状态未知(default)") + // 初始化蓝牙(主入口方法) + @objc func initBluetooth() { + // 初始化蓝牙中央管理器 + if centralManager == nil { + centralManager = CBCentralManager(delegate: self, queue: nil) } - NSLog("\(TAG): centralManagerDidUpdateState: 发送蓝牙状态变化事件: \(stateString)") + // 配置音频会话 + configureAudioSession() - // 发送蓝牙状态变化事件 - if let callback = deviceEventCallback { - let event: [String: Any] = [ - "type": "bluetoothStateChanged", - "state": stateString, - "timestamp": Int(Date().timeIntervalSince1970 * 1000) - ] - callback(event) - } else { - NSLog("\(TAG): centralManagerDidUpdateState: 没有回调注册,无法发送事件") - } - } - - // CBPeripheralManagerDelegate 方法 - func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { - NSLog("\(TAG): 外设管理器状态变化: \(peripheral.state.rawValue)") - } - - // 注册设备事件回调 - @objc func registerDeviceEventCallback(_ callback: @escaping ([String: Any]) -> Void) { - deviceEventCallback = callback - } - - // 检查蓝牙是否启用 - @objc func isBluetoothEnabled() -> Bool { - guard let manager = centralManager else { return false } - - return manager.state == .poweredOn - } - - // 获取已连接的音频设备 (A2DP 设备) - @objc func getConnectedA2dpDevices(_ completion: @escaping ([[String: String]]?, String?) -> Void) { - // 检查蓝牙状态和权限 - let (enabled, authorized) = checkBluetoothPermission() - if !enabled { - NSLog("\(TAG): 蓝牙未启用") - completion(nil, "蓝牙未启用,请在设置中开启蓝牙") - return - } - - if !authorized { - NSLog("\(TAG): 蓝牙权限被拒绝") - completion(nil, "蓝牙权限被拒绝,请在设置中允许蓝牙访问") - return - } + // 启动设备监控 + startBluetoothDeviceMonitoring() - // 尝试激活音频会话以获取设备信息 - do { - try audioSession.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .allowBluetoothA2DP]) - try audioSession.setActive(true) - - // 获取输出设备 - let devices = self.getConnectedAudioDevices() - - // 记录找到的设备数量 - NSLog("\(TAG): 找到 \(devices.count) 个A2DP设备") - - completion(devices, nil) - } catch { - NSLog("\(TAG): 获取A2DP设备失败: \(error.localizedDescription)") - completion(nil, "获取A2DP设备失败: \(error.localizedDescription)") + // 发送当前蓝牙状态 + if let manager = centralManager { + let stateString = getBluetoothStateString(state: manager.state) + sendBluetoothStateEvent(state: stateString) } } - // 获取已连接的耳机设备 - @objc func getConnectedHeadsetDevices(_ completion: @escaping ([[String: String]]?, String?) -> Void) { - // 检查蓝牙状态和权限 - let (enabled, authorized) = checkBluetoothPermission() - if !enabled { - NSLog("\(TAG): 蓝牙未启用") - completion(nil, "蓝牙未启用,请在设置中开启蓝牙") - return - } - - if !authorized { - NSLog("\(TAG): 蓝牙权限被拒绝") - completion(nil, "蓝牙权限被拒绝,请在设置中允许蓝牙访问") - return - } - - // 尝试激活音频会话以获取设备信息 - do { - try audioSession.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth]) - try audioSession.setActive(true) - - // 获取输出设备 - let devices = self.getConnectedAudioDevices() - - // 记录找到的设备数量 - NSLog("\(TAG): 找到 \(devices.count) 个耳机设备") - - completion(devices, nil) - } catch { - NSLog("\(TAG): 获取耳机设备失败: \(error.localizedDescription)") - completion(nil, "获取耳机设备失败: \(error.localizedDescription)") - } - } + // MARK: - CBCentralManagerDelegate - // 使用原生方式检查蓝牙权限 - private func checkBluetoothPermission() -> (enabled: Bool, authorized: Bool) { - // 检查蓝牙是否启用 - let isEnabled = centralManager?.state == .poweredOn - - // 检查蓝牙权限 - var isAuthorized = true - var authDescription = "unknown" - - if #available(iOS 13.0, *) { - let authStatus = centralManager?.authorization - - switch authStatus { - case .allowedAlways: - authDescription = "allowedAlways" - isAuthorized = true - case .denied: - authDescription = "denied" - isAuthorized = false - case .restricted: - authDescription = "restricted" - isAuthorized = false - case .notDetermined: - authDescription = "notDetermined" - // 未决定状态仍然当作授权, 因为系统会在实际使用时弹出请求 - isAuthorized = true - default: - authDescription = "unknown" - isAuthorized = true - } - - NSLog("\(TAG): 原生蓝牙权限状态: \(authDescription)") - } else { - // iOS 13 以下版本没有细粒度的权限控制 - authDescription = "legacy_version" - NSLog("\(TAG): iOS版本低于13,使用旧版权限模型") - } - - NSLog("\(TAG): 蓝牙状态: 启用=\(isEnabled), 已授权=\(isAuthorized), 权限描述=\(authDescription)") - return (isEnabled, isAuthorized) + func centralManagerDidUpdateState(_ central: CBCentralManager) { + let stateString = getBluetoothStateString(state: central.state) + sendBluetoothStateEvent(state: stateString) } - // 原生方式检查蓝牙权限并返回给 Flutter - @objc func checkAndRequestNativePermission(_ completion: @escaping ([String: Any]) -> Void) { - // 确保 centralManager 已初始化 - if centralManager == nil { - centralManager = CBCentralManager(delegate: self, queue: nil) - NSLog("\(self.TAG): 创建新的蓝牙管理器用于权限检查") - } - - // 延迟执行检查,确保 centralManager 状态已更新 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - // 再次检查蓝牙权限,以获取最新状态 - let (enabled, authorized) = self.checkBluetoothPermission() - - // 尝试主动请求蓝牙权限(如果尚未决定) - if #available(iOS 13.0, *) { - if self.centralManager?.authorization == .notDetermined { - NSLog("\(self.TAG): 蓝牙权限状态为未决定,尝试主动请求权限") - // 启动扫描会触发系统权限请求 - self.centralManager?.scanForPeripherals(withServices: nil, options: nil) - // 立即停止扫描 - self.centralManager?.stopScan() - } - } - - let result: [String: Any] = [ - "enabled": enabled, - "authorized": authorized, - "status": self.getPermissionStatusText(enabled: enabled, authorized: authorized) - ] - - NSLog("\(self.TAG): 返回权限检查结果: \(result)") - completion(result) - } - } + // MARK: - 设备监控 - // 获取权限状态文本 - private func getPermissionStatusText(enabled: Bool, authorized: Bool) -> String { - if !enabled { - return "disabled" // 蓝牙已禁用 - } - - if !authorized { - return "denied" // 权限被拒绝 + // 获取蓝牙状态字符串 + private func getBluetoothStateString(state: CBManagerState) -> String { + switch state { + case .poweredOn: + return "on" + case .poweredOff: + return "off" + default: + return "unknown" } - - return "granted" // 已授权 } - // 获取当前连接的音频设备 - private func getConnectedAudioDevices() -> [[String: String]] { - var result: [[String: String]] = [] - - // 尝试初始化设备列表 - if bluetoothDevices.isEmpty { - // 只在第一次使用时初始化 - NSLog("\(TAG): 第一次获取蓝牙设备,初始化设备列表") - } - - // 获取可用的音频输出设备 - guard let outputs = audioSession.currentRoute.outputs as? [AVAudioSessionPortDescription] else { - NSLog("\(TAG): 无法获取当前音频输出设备") - return result - } - - NSLog("\(TAG): 当前音频路由包含 \(outputs.count) 个输出设备") - - // 过滤蓝牙相关设备 - for output in outputs { - NSLog("\(TAG): 检查音频输出设备: \(output.portName), 类型: \(output.portType.rawValue)") - - if output.portType == .bluetoothA2DP || output.portType == .bluetoothHFP || output.portType == .bluetoothLE { - let device: [String: String] = [ - "name": output.portName, - "address": output.uid // iOS 使用 UID 作为设备标识符 - ] - result.append(device) - NSLog("\(TAG): 添加蓝牙设备: \(output.portName)") - } - } - - return result + // 发送蓝牙状态事件 + private func sendBluetoothStateEvent(state: String) { + let event: [String: Any] = [ + "type": "bluetoothStateChanged", + "state": state, + "timestamp": Int(Date().timeIntervalSince1970 * 1000) + ] + eventHandler?.sendEvent(event) } - // 监听蓝牙设备连接/断开 + // 启动蓝牙设备监控 @objc func startBluetoothDeviceMonitoring() { - // 注册音频会话通知 + NotificationCenter.default.removeObserver(self) NotificationCenter.default.addObserver( self, selector: #selector(handleRouteChange(_:)), name: AVAudioSession.routeChangeNotification, object: nil ) - - // 注册蓝牙状态变化通知 - NotificationCenter.default.addObserver( - self, - selector: #selector(handleBluetoothStateChange(_:)), - name: NSNotification.Name(rawValue: "CBCentralManagerDidUpdateStateNotification"), - object: nil - ) } // 处理音频路由变化 @@ -339,39 +107,24 @@ import UIKit // 检查原因 switch reason { case .newDeviceAvailable: - // 新设备已连接 - 查找新增的设备 + // 新设备已连接 for device in currentDevices { - if !deviceExistsInCache(device: device) { - // 找到新设备 - NSLog("\(TAG): 新的音频设备已连接: \(device["name"] ?? "未知设备")") - - // 添加到缓存 - bluetoothDevices.append(device) - - // 发送设备连接事件 - sendDeviceConnectedEvent(device: device) - } + // 发送设备连接事件 + NSLog("发送设备连接事件 - 设备: \(device["name"] ?? "未知设备") [\(device["address"] ?? "未知地址")]") + sendDeviceConnectedEvent(device: device) } case .oldDeviceUnavailable: - // 设备已断开 - 查找从缓存中移除的设备 - var devicesToRemove: [[String: String]] = [] - - for cachedDevice in bluetoothDevices { - if !deviceExistsInList(device: cachedDevice, list: currentDevices) { - // 设备已断开 - NSLog("\(TAG): 音频设备已断开: \(cachedDevice["name"] ?? "未知设备")") - devicesToRemove.append(cachedDevice) - + // 获取断开之前的路由 + if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription { + // 过滤蓝牙相关设备 + for output in previousRoute.outputs where isBluetoothOutput(output.portType) { + let device: [String: String] = [ + "name": output.portName, + "address": cleanDeviceAddress(output.uid) + ] // 发送设备断开事件 - sendDeviceDisconnectedEvent(device: cachedDevice) - } - } - - // 从缓存中移除断开的设备 - for device in devicesToRemove { - if let index = bluetoothDevices.firstIndex(where: { $0["address"] == device["address"] }) { - bluetoothDevices.remove(at: index) + sendDeviceDisconnectedEvent(device: device) } } @@ -380,144 +133,127 @@ import UIKit } } - // 处理蓝牙状态变化 - @objc private func handleBluetoothStateChange(_ notification: Notification) { - guard let manager = centralManager else { return } - - var stateString = "unknown" - - switch manager.state { - case .poweredOn: - stateString = "on" - NSLog("\(TAG): 蓝牙状态变化: 已启用") - case .poweredOff: - stateString = "off" - NSLog("\(TAG): 蓝牙状态变化: 已关闭") - // 清空设备列表 - bluetoothDevices.removeAll() - case .resetting: - stateString = "resetting" - NSLog("\(TAG): 蓝牙状态变化: 正在重置") - case .unauthorized: - stateString = "unauthorized" - NSLog("\(TAG): 蓝牙状态变化: 未授权") - case .unsupported: - stateString = "unsupported" - NSLog("\(TAG): 蓝牙状态变化: 不支持") - case .unknown: - stateString = "unknown" - NSLog("\(TAG): 蓝牙状态变化: 未知") - @unknown default: - stateString = "unknown" - NSLog("\(TAG): 蓝牙状态变化: 未知(default)") - } - - NSLog("\(TAG): 发送蓝牙状态变化事件: \(stateString)") - - // 发送蓝牙状态变化事件 - if let callback = deviceEventCallback { - let event: [String: Any] = [ - "type": "bluetoothStateChanged", - "state": stateString, - "timestamp": Int(Date().timeIntervalSince1970 * 1000) - ] - callback(event) - } + // 检查是否为蓝牙输出 + private func isBluetoothOutput(_ portType: AVAudioSession.Port) -> Bool { + return portType == .bluetoothA2DP || portType == .bluetoothHFP || portType == .bluetoothLE } + // MARK: - 设备事件 + // 发送设备连接事件 private func sendDeviceConnectedEvent(device: [String: String]) { - if let callback = deviceEventCallback, - let name = device["name"], - let address = device["address"] { - - let deviceMap: [String: Any] = [ - "name": name, - "address": address, - "type": "a2dp" // iOS 默认认为是 A2DP 设备 - ] - - let event: [String: Any] = [ - "type": "deviceConnected", - "device": deviceMap, - "timestamp": Int(Date().timeIntervalSince1970 * 1000) - ] - - callback(event) - } + guard let name = device["name"], let address = device["address"] else { return } + + let deviceMap: [String: Any] = [ + "name": name, + "address": address, + "type": "headset" + ] + + let event: [String: Any] = [ + "type": "deviceConnected", + "device": deviceMap, + "timestamp": Int(Date().timeIntervalSince1970 * 1000) + ] + + eventHandler?.sendEvent(event) } // 发送设备断开事件 private func sendDeviceDisconnectedEvent(device: [String: String]) { - if let callback = deviceEventCallback, - let name = device["name"], - let address = device["address"] { - - let deviceMap: [String: Any] = [ - "name": name, - "address": address, - "type": "a2dp" // iOS 默认认为是 A2DP 设备 - ] - - let event: [String: Any] = [ - "type": "deviceDisconnected", - "device": deviceMap, - "timestamp": Int(Date().timeIntervalSince1970 * 1000) - ] - - callback(event) - } + guard let name = device["name"], let address = device["address"] else { return } + + let deviceMap: [String: Any] = [ + "name": name, + "address": address, + "type": "headset" + ] + + let event: [String: Any] = [ + "type": "deviceDisconnected", + "device": deviceMap, + "timestamp": Int(Date().timeIntervalSince1970 * 1000) + ] + + eventHandler?.sendEvent(event) } - // 检查设备是否存在于缓存中 - private func deviceExistsInCache(device: [String: String]) -> Bool { - return deviceExistsInList(device: device, list: bluetoothDevices) - } + // MARK: - 公共API - // 检查设备是否存在于列表中 - private func deviceExistsInList(device: [String: String], list: [[String: String]]) -> Bool { - guard let address = device["address"] else { return false } - return list.contains { $0["address"] == address } + // 获取当前连接的耳机设备 + @objc func getConnectedHeadsetDevices(_ completion: @escaping ([[String: String]]?, String?) -> Void) { + // 检查蓝牙状态 + if centralManager?.state != .poweredOn { + completion(nil, "蓝牙未启用") + return + } + + do { + // 获取设备 + let devices = getConnectedAudioDevices() + completion(devices, nil) + } catch { + completion(nil, "获取设备失败: \(error.localizedDescription)") + } } - // 停止监听 - @objc func stopBluetoothDeviceMonitoring() { - NotificationCenter.default.removeObserver(self, name: AVAudioSession.routeChangeNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: "CBCentralManagerDidUpdateStateNotification"), object: nil) + // 获取当前连接的音频设备 + private func getConnectedAudioDevices() -> [[String: String]] { + var result: [[String: String]] = [] + + // 获取当前音频路由 + let outputs = audioSession.currentRoute.outputs + + // 过滤蓝牙相关设备 + for output in outputs where isBluetoothOutput(output.portType) { + let device: [String: String] = [ + "name": output.portName, + "address": cleanDeviceAddress(output.uid) // 处理设备地址,移除后缀 + ] + result.append(device) + } + + return result } // 释放资源 @objc func dispose() { - stopBluetoothDeviceMonitoring() - deviceEventCallback = nil + NotificationCenter.default.removeObserver(self) + eventHandler = nil } - // 打开系统设置 - @objc func openSettings() -> Bool { - NSLog("\(TAG): 尝试打开应用设置") - if let url = URL(string: UIApplication.openSettingsURLString) { - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - return true - } - } - return false + // 配置音频会话 + private func configureAudioSession() { + do { + try audioSession.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .allowBluetoothA2DP]) + try audioSession.setActive(true) + } catch {} } - // 打开蓝牙设置(iOS 只能打开系统设置,无法直接跳转到蓝牙设置) - @objc func openBluetoothSettings() -> Bool { - // 在 iOS 10+ 上直接跳转到蓝牙设置页面 - if #available(iOS 10.0, *) { - NSLog("\(TAG): 尝试使用 URL Scheme 打开蓝牙设置") - if let url = URL(string: "App-prefs:root=Bluetooth") { - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - return true - } + // 检查蓝牙是否启用 + @objc func isBluetoothEnabled() -> Bool { + return centralManager?.state == .poweredOn + } + + // 清理设备地址,移除后缀 + private func cleanDeviceAddress(_ address: String) -> String { + // 检查是否包含常见分隔符 + if let range = address.range(of: "-") ?? address.range(of: ":") ?? address.range(of: "_") { + // 只保留分隔符之前的部分 + return String(address[..$(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + NSBluetoothAlwaysUsageDescription + 需要蓝牙权限来连接和管理音频设备 + NSBluetoothPeripheralUsageDescription + 需要蓝牙权限来连接和管理音频设备 + NSMicrophoneUsageDescription + 需要麦克风权限来进行语音识别 + NSSpeechRecognitionUsageDescription + 需要语音识别权限来提供语音交互服务 UIApplicationSupportsIndirectInputEvents + UIBackgroundModes + + audio + bluetooth-central + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -45,17 +58,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - NSBluetoothAlwaysUsageDescription - 需要蓝牙权限来连接和管理音频设备 - NSBluetoothPeripheralUsageDescription - 需要蓝牙权限来连接和管理音频设备 - NSMicrophoneUsageDescription - 需要麦克风权限来进行语音识别 - NSSpeechRecognitionUsageDescription - 需要语音识别权限来提供语音交互服务 - UIBackgroundModes - - bluetooth-central - diff --git a/ios/Runner/Resources/.gitkeep b/ios/Runner/Resources/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/ios/Runner/Resources/sample.mp3 b/ios/Runner/Resources/sample.mp3 new file mode 100644 index 000000000..31ebc28ce Binary files /dev/null and b/ios/Runner/Resources/sample.mp3 differ diff --git a/ios/Runner/Resources/silence.mp3 b/ios/Runner/Resources/silence.mp3 new file mode 100644 index 000000000..f5bd92f88 Binary files /dev/null and b/ios/Runner/Resources/silence.mp3 differ diff --git a/ios/Runner/sample.mp3 b/ios/Runner/sample.mp3 new file mode 100644 index 000000000..31ebc28ce Binary files /dev/null and b/ios/Runner/sample.mp3 differ diff --git a/ios/Runner/silence.mp3 b/ios/Runner/silence.mp3 new file mode 100644 index 000000000..f5bd92f88 Binary files /dev/null and b/ios/Runner/silence.mp3 differ diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 1dc794dfd..96978a573 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -6,6 +6,7 @@ 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 '../../data/services/bluetooth_media_button_service.dart'; import '../../core/utils/logger.dart'; /// 初始绑定,用于管理全局依赖 @@ -36,6 +37,7 @@ class InitialBinding extends Bindings { // 经典蓝牙服务 Get.lazyPut(() => ClassicBluetoothService(), fenix: true); - + // 蓝牙媒体按钮服务 + Get.lazyPut(() => BluetoothMediaButtonService(), fenix: true); } } \ No newline at end of file diff --git a/lib/data/services/bluetooth_media_button_service.dart b/lib/data/services/bluetooth_media_button_service.dart new file mode 100644 index 000000000..8a76089fd --- /dev/null +++ b/lib/data/services/bluetooth_media_button_service.dart @@ -0,0 +1,330 @@ +import 'dart:async'; +import 'dart:io' show Platform; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import '../../core/utils/logger.dart'; + +/// 蓝牙媒体按钮事件类型 +enum MediaButtonType { + /// 播放按钮 + play, + + /// 暂停按钮 + pause, + + /// 下一曲按钮 + next, + + /// 上一曲按钮 + previous, + + /// 快进按钮 + seekForward, + + /// 快退按钮 + seekBackward, + + /// 播放/暂停切换按钮 + togglePlayPause, + + /// 未知按钮 + unknown +} + +/// 蓝牙媒体按钮事件 +class MediaButtonEvent { + /// 按钮类型 + final MediaButtonType buttonType; + + /// 事件发生时间戳 + final int timestamp; + + MediaButtonEvent({ + required this.buttonType, + required this.timestamp, + }); + + factory MediaButtonEvent.fromMap(Map map) { + MediaButtonType buttonType; + + switch (map['buttonType']) { + case 'play': + buttonType = MediaButtonType.play; + break; + case 'pause': + buttonType = MediaButtonType.pause; + break; + case 'next': + buttonType = MediaButtonType.next; + break; + case 'previous': + buttonType = MediaButtonType.previous; + break; + case 'seekForward': + buttonType = MediaButtonType.seekForward; + break; + case 'seekBackward': + buttonType = MediaButtonType.seekBackward; + break; + case 'togglePlayPause': + buttonType = MediaButtonType.togglePlayPause; + break; + default: + buttonType = MediaButtonType.unknown; + } + + return MediaButtonEvent( + buttonType: buttonType, + timestamp: map['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch, + ); + } + + @override + String toString() { + switch (buttonType) { + case MediaButtonType.play: + return '蓝牙媒体按钮: 播放'; + case MediaButtonType.pause: + return '蓝牙媒体按钮: 暂停'; + case MediaButtonType.next: + return '蓝牙媒体按钮: 下一曲'; + case MediaButtonType.previous: + return '蓝牙媒体按钮: 上一曲'; + case MediaButtonType.seekForward: + return '蓝牙媒体按钮: 快进'; + case MediaButtonType.seekBackward: + return '蓝牙媒体按钮: 快退'; + case MediaButtonType.togglePlayPause: + return '蓝牙媒体按钮: 播放/暂停'; + case MediaButtonType.unknown: + return '蓝牙媒体按钮: 未知'; + } + } +} + +/// 蓝牙媒体按钮服务 +class BluetoothMediaButtonService extends GetxService { + static BluetoothMediaButtonService get to => Get.find(); + + // 方法通道名称 + final MethodChannel _channel = const MethodChannel('com.deep_voice.bluetooth_media_button'); + + // 事件通道名称 + final EventChannel _eventChannel = const EventChannel('com.deep_voice.bluetooth_media_button_events'); + + /// 按钮事件流控制器 + final _buttonEventController = StreamController.broadcast(); + + /// 按钮事件流 + Stream get buttonEvents => _buttonEventController.stream; + + /// 是否已启用监听 + final _isListening = false.obs; + bool get isListening => _isListening.value; + + /// 是否正在加载 + final isLoading = false.obs; + + /// 错误信息 + final errorMessage = ''.obs; + + /// 按钮事件流订阅 + StreamSubscription? _buttonEventSubscription; + + /// 服务是否已初始化 + bool _isInitialized = false; + + /// 是否支持媒体按钮服务(iOS 13.0及以上) + bool get isSupported { + if (!Platform.isIOS) return true; // 非iOS平台默认支持 + + // 获取平台版本信息 + final String version = Platform.operatingSystemVersion; + + // 尝试提取iOS版本号 (格式如: "Version 14.5 (Build 18E182)") + try { + // 提取版本号 + final RegExp versionRegex = RegExp(r'Version\s+(\d+)\.(\d+)'); + final match = versionRegex.firstMatch(version); + + if (match != null) { + final int majorVersion = int.parse(match.group(1) ?? '0'); + return majorVersion >= 13; // iOS 13及以上支持 + } + } catch (e) { + Logger.error('解析iOS版本失败: $e'); + } + + // 无法确定版本时,假设不支持 + return false; + } + + /// 初始化服务 + BluetoothMediaButtonService() { + Logger.info('创建 BluetoothMediaButtonService 实例'); + } + + /// 初始化媒体按钮服务 + Future initialize() async { + if (_isInitialized) { + Logger.info('BluetoothMediaButtonService 已经初始化'); + return true; + } + + // 检查系统版本是否支持 + if (!isSupported) { + Logger.info('当前iOS版本不支持蓝牙媒体按钮功能(需要iOS 13.0+)'); + return false; + } + + try { + Logger.info('正在初始化 BluetoothMediaButtonService...'); + + // 设置事件监听器 + _setupButtonEventListener(); + + _isInitialized = true; + Logger.info('BluetoothMediaButtonService 初始化成功'); + return true; + } catch (e) { + Logger.error('初始化 BluetoothMediaButtonService 失败: $e'); + errorMessage.value = '初始化蓝牙媒体按钮服务失败'; + return false; + } + } + + /// 设置按钮事件监听器 + void _setupButtonEventListener() { + _buttonEventSubscription?.cancel(); + + // 设置事件通道监听器 + _buttonEventSubscription = _eventChannel + .receiveBroadcastStream() + .listen(_handleButtonEvent, onError: _handleEventStreamError); + + Logger.info('已设置蓝牙媒体按钮事件监听器'); + } + + /// 处理按钮事件 + void _handleButtonEvent(dynamic event) { + Logger.info('BluetoothMediaButtonService 收到原始事件: $event'); + + if (event is! Map) { + Logger.error('收到无效的蓝牙媒体按钮事件 (非Map类型): $event'); + return; + } + + final Map eventMap = event; + Logger.info('蓝牙媒体按钮事件Map: $eventMap'); + + try { + if (eventMap['type'] == 'mediaButtonEvent') { + final buttonEvent = MediaButtonEvent.fromMap(eventMap); + + // 广播事件 + _buttonEventController.add(buttonEvent); + + Logger.info('成功处理蓝牙媒体按钮事件: $buttonEvent'); + } else { + Logger.warning('收到未知类型的事件: ${eventMap['type']}'); + } + } catch (e) { + Logger.error('处理蓝牙媒体按钮事件时出错: $e'); + } + } + + /// 处理事件流错误 + void _handleEventStreamError(dynamic error) { + Logger.error('蓝牙媒体按钮事件流错误: $error'); + errorMessage.value = '蓝牙媒体按钮监听出错'; + } + + /// 开始监听蓝牙媒体按钮事件 + Future startButtonListening() async { + // 检查系统版本是否支持 + if (!isSupported) { + Logger.info('当前iOS版本不支持蓝牙媒体按钮功能(需要iOS 13.0+)'); + return false; + } + + if (!_isInitialized && !(await initialize())) { + return false; + } + + if (_isListening.value) { + Logger.info('已经在监听蓝牙媒体按钮事件'); + return true; + } + + isLoading.value = true; + + try { + final result = await _channel.invokeMethod('startButtonListening') ?? false; + + if (result) { + _isListening.value = true; + Logger.info('开始监听蓝牙媒体按钮事件成功'); + } else { + errorMessage.value = '开始监听蓝牙媒体按钮事件失败'; + Logger.error('开始监听蓝牙媒体按钮事件失败'); + } + + isLoading.value = false; + return result; + } catch (e) { + isLoading.value = false; + errorMessage.value = '开始监听蓝牙媒体按钮事件出错: $e'; + Logger.error('开始监听蓝牙媒体按钮事件出错: $e'); + return false; + } + } + + /// 停止监听蓝牙媒体按钮事件 + Future stopButtonListening() async { + if (!_isInitialized) { + return false; + } + + if (!_isListening.value) { + return true; + } + + isLoading.value = true; + + try { + final result = await _channel.invokeMethod('stopButtonListening') ?? false; + + if (result) { + _isListening.value = false; + Logger.info('停止监听蓝牙媒体按钮事件成功'); + } else { + errorMessage.value = '停止监听蓝牙媒体按钮事件失败'; + Logger.error('停止监听蓝牙媒体按钮事件失败'); + } + + isLoading.value = false; + return result; + } catch (e) { + isLoading.value = false; + errorMessage.value = '停止监听蓝牙媒体按钮事件出错: $e'; + Logger.error('停止监听蓝牙媒体按钮事件出错: $e'); + return false; + } + } + + /// 释放资源 + void dispose() { + _buttonEventSubscription?.cancel(); + _buttonEventSubscription = null; + _buttonEventController.close(); + _isInitialized = false; + _isListening.value = false; + Logger.info('BluetoothMediaButtonService 已释放'); + } + + @override + void onClose() { + dispose(); + 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 index 20f93e436..dc3324134 100644 --- a/lib/data/services/classic_bluetooth_service.dart +++ b/lib/data/services/classic_bluetooth_service.dart @@ -2,7 +2,6 @@ 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'; /// 蓝牙设备信息 @@ -21,263 +20,88 @@ class BluetoothDevice { 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 bluetoothState = 'on'.obs; - /// 设备断开回调 - final List _deviceDisconnectedCallbacks = []; - - /// 蓝牙状态变化回调 - final List _bluetoothStateChangedCallbacks = []; + /// 蓝牙是否可用 + bool get isBluetoothOn => bluetoothState.value == 'on'; /// 服务是否已初始化 bool _isInitialized = false; - ClassicBluetoothService() { - // 创建服务时不立即初始化,避免还没有原生支持时就调用方法 - Logger.info('创建 ClassicBluetoothService 实例'); - } - - /// 初始化蓝牙服务 - /// 必须在使用任何方法前调用此方法确保通道已设置 + /// 初始化蓝牙服务 - 单一入口点 Future initialize() async { if (_isInitialized) { - Logger.info('ClassicBluetoothService 已经初始化'); + Logger.info('蓝牙服务已经初始化,跳过'); return true; } try { - Logger.info('正在初始化 ClassicBluetoothService...'); + Logger.info('正在初始化蓝牙服务...'); - // 开始监控蓝牙设备连接状态 + // 启动蓝牙设备监控 - 这将调用原生代码中的initBluetooth await _channel.invokeMethod('startBluetoothDeviceMonitoring'); - Logger.info('启动蓝牙设备监控'); - // 设置事件监听器 + // 设置事件监听 _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; - } - - // 根据平台选择不同的权限检查方式 - if (GetPlatform.isIOS) { - // iOS: 使用原生权限检查 - final nativeResult = await checkNativePermission(); - if (nativeResult != null) { - Logger.info('iOS平台蓝牙权限检查结果: $nativeResult'); - - if (nativeResult['status'] == 'granted') { - return true; - } else if (nativeResult['status'] == 'denied') { - Logger.error('iOS蓝牙权限被拒绝'); - errorMessage.value = '蓝牙权限被拒绝,请在设置中允许蓝牙访问'; - return false; - } else if (nativeResult['status'] == 'disabled') { - Logger.error('iOS蓝牙未启用'); - errorMessage.value = '蓝牙未启用,请开启蓝牙'; - return false; - } - } - // 如果原生检查失败,继续使用Flutter权限包(作为备选) - Logger.info('iOS平台回退到Flutter权限检查'); - } - - // Android: 使用Flutter权限包进行检查(原有实现) - Logger.info('${GetPlatform.isAndroid ? "Android" : "通用"}平台使用Flutter权限检查'); - - // 检查蓝牙相关权限 - 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; - } - + Logger.info('蓝牙服务初始化成功'); return true; } catch (e) { - Logger.error('检查蓝牙权限失败: $e'); - errorMessage.value = '检查蓝牙权限失败'; + Logger.error('初始化蓝牙服务失败: $e'); return false; } } - /// 使用原生方法检查蓝牙权限 - Future?> checkNativePermission() async { - try { - final result = await _channel.invokeMethod('checkAndRequestNativePermission'); - Logger.info('原生蓝牙权限检查原始结果: $result'); - - if (result is Map) { - return result; - } - - return null; - } catch (e) { - Logger.error('原生蓝牙权限检查失败: $e'); - return null; - } + /// 检查蓝牙权限 + Future checkPermissions() async { + // 检查蓝牙相关权限 + Map statuses = await [ + Permission.bluetooth, + Permission.bluetoothConnect, + Permission.bluetoothScan, + ].request(); + + return statuses[Permission.bluetoothConnect] == PermissionStatus.granted; } - /// 刷新已连接的蓝牙设备 + /// 刷新已连接的蓝牙耳机设备 Future refreshConnectedDevices() async { + // 确保服务已初始化 + if (!_isInitialized && !(await initialize())) { + return false; + } + + // 如果蓝牙未开启,无法获取设备 + if (!isBluetoothOn) { + Logger.info('蓝牙未开启,无法获取设备'); + return false; + } + 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}'); + // 只获取耳机设备 + final devices = await getConnectedHeadsetDevices(); // 更新设备列表 - connectedDevices.value = [...a2dpDevices, ...headsetDevices]; + connectedDevices.value = devices; // 更新当前设备 if (connectedDevices.isNotEmpty && currentDevice.value == null) { @@ -287,60 +111,26 @@ class ClassicBluetoothService extends GetxService { 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( + return result.map((device) => BluetoothDevice( name: device['name'] ?? '未知设备', address: device['address'] ?? '', type: 'headset' )).toList(); - - return devices; } return []; @@ -352,125 +142,87 @@ class ClassicBluetoothService extends GetxService { /// 设置蓝牙状态监听 void _setupBluetoothStateListener() { - try { - _bluetoothStateListener = _eventChannel - .receiveBroadcastStream() - .listen((event) { - Logger.info('收到蓝牙事件: $event'); + _bluetoothStateListener?.cancel(); + + _bluetoothStateListener = _eventChannel + .receiveBroadcastStream() + .listen((event) { + if (event is Map) { + final type = event['type'] as String?; - if (event is Map) { - final type = event['type'] as String?; - - if (type == 'bluetoothStateChanged') { - 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); - } + if (type == 'bluetoothStateChanged') { + final state = event['state'] as String?; + if (state != null) { + _handleBluetoothStateChange(state); } + } else if (type == 'deviceConnected') { + _handleDeviceConnected(event); + } else if (type == 'deviceDisconnected') { + _handleDeviceDisconnected(event); } - }, onError: (error) { - Logger.error('蓝牙状态监听错误: $error'); - }); - } catch (e) { - Logger.error('设置蓝牙状态监听失败: $e'); - } + } + }, onError: (error) { + Logger.error('蓝牙状态监听错误: $error'); + }); } - /// 处理蓝牙状态变化 - void _handleBluetoothStateChange(String state) { - Logger.info('蓝牙状态变化: $state'); - - // 如果蓝牙关闭,清除设备信息 - if (state == 'off') { - connectedDevices.clear(); - currentDevice.value = null; - } - - // 通知所有回调 - for (var callback in _bluetoothStateChangedCallbacks) { - callback(state); + /// 处理设备连接事件 + void _handleDeviceConnected(Map event) { + final deviceMap = event['device'] as Map?; + if (deviceMap != null && (deviceMap['type'] == 'headset' || deviceMap['type'] == 'unknown')) { + final device = BluetoothDevice( + name: deviceMap['name'] ?? '未知设备', + address: deviceMap['address'] ?? '', + type: deviceMap['type'] ?? 'unknown' + ); + + // 更新设备列表 + if (!connectedDevices.any((d) => d.address == device.address)) { + connectedDevices.add(device); + } + + // 更新当前设备 + currentDevice.value = device; + + Logger.info('设备已连接: ${device.name}'); } } - /// 处理设备连接 - 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(Map event) { + final deviceMap = event['device'] as Map?; + if (deviceMap != null) { + final address = deviceMap['address'] as String?; + + if (address != null) { + // 更新设备列表 + connectedDevices.removeWhere((d) => d.address == address); + + // 如果是当前设备,清除当前设备 + if (currentDevice.value?.address == address) { + currentDevice.value = connectedDevices.isEmpty ? null : connectedDevices.first; + Logger.info('当前设备已断开连接'); + } + } } } - /// 处理设备断开 - void _handleDeviceDisconnected(BluetoothDevice device) { - Logger.info('设备已断开: ${device.name}'); - - // 更新设备列表 - connectedDevices.removeWhere((d) => d.address == device.address); + /// 处理蓝牙状态变化 + void _handleBluetoothStateChange(String state) { + Logger.info('蓝牙状态变化: $state'); - // 如果是当前设备,清除当前设备 - if (currentDevice.value?.address == device.address) { - currentDevice.value = connectedDevices.isEmpty ? null : connectedDevices.first; - } + // 更新蓝牙状态 + bluetoothState.value = state; - // 通知所有回调 - 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 { - final result = await _channel.invokeMethod('openBluetoothSettings'); - return result == true; - } catch (e) { - Logger.error('打开蓝牙设置失败: $e'); - return false; + if (state == 'off') { + // 蓝牙关闭,清空设备信息 + connectedDevices.clear(); + currentDevice.value = null; + } else if (state == 'on') { + // 蓝牙重新打开,延迟一小段时间后刷新设备 + Future.delayed(const Duration(milliseconds: 1000), () { + refreshConnectedDevices(); + }); } } @@ -478,12 +230,6 @@ class ClassicBluetoothService extends GetxService { void onClose() { // 取消蓝牙状态监听 _bluetoothStateListener?.cancel(); - - // 清除回调 - _deviceConnectedCallbacks.clear(); - _deviceDisconnectedCallbacks.clear(); - _bluetoothStateChangedCallbacks.clear(); - super.onClose(); } } \ No newline at end of file diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart index 6b82d1892..13151d58a 100644 --- a/lib/modules/home/controllers/home_controller.dart +++ b/lib/modules/home/controllers/home_controller.dart @@ -1,6 +1,8 @@ import 'package:get/get.dart'; +import 'dart:async'; import '../../../routes/app_routes.dart'; -import '../../../data/services/classic_bluetooth_service.dart' as classic; +import '../../../data/services/classic_bluetooth_service.dart'; +import '../../../data/services/bluetooth_media_button_service.dart'; import '../../../core/utils/logger.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart' hide BluetoothDevice; import 'package:flutter/material.dart'; @@ -18,7 +20,10 @@ class HomeController extends GetxController { static HomeController get to => Get.find(); // 经典蓝牙服务 - late classic.ClassicBluetoothService _bluetoothService; + late ClassicBluetoothService _bluetoothService; + + // 蓝牙媒体按钮服务 + late BluetoothMediaButtonService _mediaButtonService; // 设备连接状态 final _isConnected = false.obs; @@ -44,8 +49,16 @@ class HomeController extends GetxController { String get connectionErrorMsg => _connectionErrorMsg.value; // 当前连接的经典蓝牙设备 - final _connectedClassicDevices = [].obs; - List get connectedClassicDevices => _connectedClassicDevices; + List get connectedClassicDevices => _bluetoothService.connectedDevices; + + // 蓝牙状态 + RxString get bluetoothState => _bluetoothService.bluetoothState; + + // 按钮事件流订阅 + StreamSubscription? _mediaButtonEventSubscription; + + // 状态更新标志,防止多次重复更新 + bool _isUpdatingStatus = false; @override void onInit() { @@ -54,6 +67,9 @@ class HomeController extends GetxController { // 初始化蓝牙服务和状态监控 _initBluetoothService(); + // 初始化蓝牙媒体按钮服务 + _initMediaButtonService(); + // 电池模拟更新(实际应用中可以通过蓝牙获取) _simulateBatteryUpdate(); } @@ -62,7 +78,7 @@ class HomeController extends GetxController { void _initBluetoothService() { try { // 获取经典蓝牙服务实例 - _bluetoothService = Get.find(); + _bluetoothService = Get.find(); // 更新当前连接状态 _updateConnectionStatus(); @@ -76,6 +92,45 @@ class HomeController extends GetxController { } } + // 初始化蓝牙媒体按钮服务 + void _initMediaButtonService() { + try { + // 获取媒体按钮服务实例 + _mediaButtonService = Get.find(); + + // 检查是否支持该功能 + if (!_mediaButtonService.isSupported) { + Logger.info('当前设备不支持蓝牙媒体按钮功能'); + return; + } + + // 初始化服务 + _mediaButtonService.initialize().then((success) { + if (success) { + // 开始监听蓝牙媒体按钮事件 + _mediaButtonService.startButtonListening(); + + // 订阅按钮事件流 + _mediaButtonEventSubscription = _mediaButtonService.buttonEvents.listen( + (event) { + Logger.info('HomeController: 收到蓝牙媒体按钮事件 $event'); + _handleMediaButtonEvent(event); + }, + onError: (error) { + Logger.error('HomeController: 蓝牙媒体按钮事件流错误: $error'); + } + ); + + Logger.info('蓝牙媒体按钮服务初始化成功'); + } else { + Logger.error('蓝牙媒体按钮服务初始化失败'); + } + }); + } catch (e) { + Logger.error('初始化蓝牙媒体按钮服务失败: $e'); + } + } + // 电池电量模拟更新 void _simulateBatteryUpdate() { // 每5分钟更新一次电池电量(模拟) @@ -94,43 +149,49 @@ class HomeController extends GetxController { // 更新连接状态 void _updateConnectionStatus() { - // 检查当前设备 - final currentDevice = _bluetoothService.currentDevice.value; + // 如果正在更新,则跳过 + if (_isUpdatingStatus) return; - if (currentDevice != null) { - _isConnected.value = true; - _connectionStatus.value = ConnectionStatus.connected; - } else { - _isConnected.value = false; - _connectionStatus.value = ConnectionStatus.disconnected; - } + _isUpdatingStatus = true; - update(); // 通知UI刷新 + try { + // 检查当前设备 + final currentDevice = _bluetoothService.currentDevice.value; + + if (currentDevice != null) { + _isConnected.value = true; + _connectionStatus.value = ConnectionStatus.connected; + } else { + _isConnected.value = false; + _connectionStatus.value = ConnectionStatus.disconnected; + } + + update(); // 通知UI刷新 + } finally { + _isUpdatingStatus = false; + } } // 设置蓝牙监听器 void _setupBluetoothListeners() { - // 监听设备连接 - _bluetoothService.onDeviceConnected((device) { - Logger.info('监听到蓝牙设备连接: ${device.name}'); - _isConnected.value = true; - _connectionStatus.value = ConnectionStatus.connected; - update(); - }); + // 只监听当前设备和蓝牙状态 - // 监听设备断开 - _bluetoothService.onDeviceDisconnected((device) { - Logger.info('监听到蓝牙设备断开: ${device.name}'); - _isConnected.value = false; - _connectionStatus.value = ConnectionStatus.disconnected; - _deviceBattery.value = null; + // 监听当前设备状态 + ever(_bluetoothService.currentDevice, (device) { + if (device != null) { + _isConnected.value = true; + _connectionStatus.value = ConnectionStatus.connected; + } else if (_isConnected.value) { + // 如果之前连接着设备但现在变为null,那么设备已断开 + _isConnected.value = false; + _connectionStatus.value = ConnectionStatus.disconnected; + _deviceBattery.value = null; + } update(); }); - // 监听蓝牙状态变化 - _bluetoothService.onBluetoothStateChanged((state) { - Logger.info('监听到蓝牙状态变化: $state'); - + // 蓝牙状态变化监听 + ever(_bluetoothService.bluetoothState, (state) { if (state == 'off') { _isConnected.value = false; _connectionStatus.value = ConnectionStatus.disconnected; @@ -142,6 +203,11 @@ class HomeController extends GetxController { // 刷新设备信息 Future refreshDeviceInfo() async { + // 如果正在更新,则跳过 + if (_isUpdatingStatus) return; + + _isUpdatingStatus = true; + try { // 设置状态为连接中 _connectionStatus.value = ConnectionStatus.connecting; @@ -156,10 +222,7 @@ class HomeController extends GetxController { // 根据结果更新状态 if (!hasDevices) { Logger.info('刷新设备:未找到设备'); - // 这里可以设置状态变量而不是使用snackbar - _connectionErrorMsg.value = _bluetoothService.errorMessage.value.isEmpty - ? '未找到已连接的设备' - : _bluetoothService.errorMessage.value; + _connectionErrorMsg.value = '未找到已连接的设备'; } else { _connectionErrorMsg.value = ''; Logger.info('刷新设备:成功'); @@ -169,6 +232,8 @@ class HomeController extends GetxController { _connectionStatus.value = ConnectionStatus.failed; _connectionErrorMsg.value = '无法获取设备信息'; update(); + } finally { + _isUpdatingStatus = false; } } @@ -210,6 +275,11 @@ class HomeController extends GetxController { // 获取连接状态对应的文本 String getConnectionStatusText() { + // 如果蓝牙关闭,优先显示蓝牙状态 + if (_bluetoothService.bluetoothState.value == 'off') { + return '蓝牙已关闭'; + } + switch (_connectionStatus.value) { case ConnectionStatus.connected: return '已连接'; @@ -252,4 +322,83 @@ class HomeController extends GetxController { final device = _bluetoothService.currentDevice.value; return device?.address ?? '未知'; } + + // 处理蓝牙媒体按钮事件 + void _handleMediaButtonEvent(MediaButtonEvent event) { + Logger.info('收到蓝牙媒体按钮事件: $event'); + + // 根据按钮类型执行相应操作 + switch (event.buttonType) { + case MediaButtonType.play: + // 处理播放按钮事件 + _handlePlayButtonPressed(); + break; + + case MediaButtonType.pause: + // 处理暂停按钮事件 + _handlePauseButtonPressed(); + break; + + case MediaButtonType.togglePlayPause: + // 处理播放/暂停切换按钮事件 + _handleTogglePlayPause(); + break; + + case MediaButtonType.next: + // 处理下一曲按钮事件 + _handleNextButtonPressed(); + break; + + case MediaButtonType.previous: + // 处理上一曲按钮事件 + _handlePreviousButtonPressed(); + break; + + default: + // 处理其他按钮事件 + break; + } + } + + // 处理播放按钮事件 + void _handlePlayButtonPressed() { + Logger.info('处理播放按钮事件'); + // 在这里添加播放功能的实现 + // 例如:启动语音助手 + } + + // 处理暂停按钮事件 + void _handlePauseButtonPressed() { + Logger.info('处理暂停按钮事件'); + // 在这里添加暂停功能的实现 + // 例如:暂停语音交互 + } + + // 处理播放/暂停切换按钮事件 + void _handleTogglePlayPause() { + Logger.info('处理播放/暂停切换按钮事件'); + // 在这里添加播放/暂停切换功能的实现 + } + + // 处理下一曲按钮事件 + void _handleNextButtonPressed() { + Logger.info('处理下一曲按钮事件'); + // 在这里添加下一曲功能的实现 + // 例如:跳到下一个问题或调整音量 + } + + // 处理上一曲按钮事件 + void _handlePreviousButtonPressed() { + Logger.info('处理上一曲按钮事件'); + // 在这里添加上一曲功能的实现 + // 例如:回到上一个问题或调整音量 + } + + @override + void onClose() { + // 取消按钮事件流订阅 + _mediaButtonEventSubscription?.cancel(); + + super.onClose(); + } } \ 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 0d71aa111..e0b970ed7 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -2,10 +2,12 @@ import 'package:deep_voice/data/services/voice_interaction_service.dart'; import 'package:get/get.dart'; import 'dart:async'; import '../../../routes/app_routes.dart'; -import '../../../data/services/classic_bluetooth_service.dart' as classic; +import '../../../data/services/classic_bluetooth_service.dart'; import '../../../core/utils/logger.dart'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:app_settings/app_settings.dart'; +import 'package:flutter/services.dart'; /// 简化的配对控制器,仅用于显示已连接经典蓝牙设备和处理基本配对流程 @@ -13,7 +15,7 @@ class PairingController extends GetxController { static PairingController get to => Get.find(); // 经典蓝牙服务 - late classic.ClassicBluetoothService _bluetoothService; + late ClassicBluetoothService _bluetoothService; // 是否开始搜索 final _isSearching = false.obs; @@ -28,16 +30,19 @@ class PairingController extends GetxController { bool get isBluetoothServiceAvailable => _isBluetoothServiceAvailable.value; // 当前连接设备 - Rx get currentDevice => _bluetoothService.currentDevice; + Rx get currentDevice => _bluetoothService.currentDevice; // 连接设备列表 - RxList get connectedDevices => _bluetoothService.connectedDevices; + RxList get connectedDevices => _bluetoothService.connectedDevices; + + // 蓝牙状态 + RxString get bluetoothState => _bluetoothService.bluetoothState; // 错误信息 - RxString get errorMessage => _bluetoothService.errorMessage; + final errorMessage = ''.obs; // 连接设备列表(兼容旧代码) - List get connectedClassicDevices => + List get connectedClassicDevices => _bluetoothService.currentDevice.value != null ? [_bluetoothService.currentDevice.value!] : []; @@ -48,28 +53,71 @@ class PairingController extends GetxController { // 加载状态 final isLoading = false.obs; + // 是否正在跳转到主页 + bool _isNavigatingToHome = false; + @override void onInit() { super.onInit(); _initialize(); } + /// 设置状态变化监听 + void _setupStatusListeners() { + if (!_isBluetoothServiceAvailable.value) return; + + // 监听蓝牙开关状态 + ever(_bluetoothService.bluetoothState, (state) { + _updateStatusDescription(); + }); + + // 监听当前设备状态 + ever(_bluetoothService.currentDevice, (device) { + _updateStatusDescription(); + + // 如果有设备连接,自动跳转到主页 + if (device != null) { + _autoNavigateToHome(); + } + }); + } + /// 更新状态描述 + void _updateStatusDescription() { + if (_bluetoothService.bluetoothState.value == 'off') { + statusDescription.value = '蓝牙已关闭,请打开蓝牙'; + return; + } + + if (_bluetoothService.currentDevice.value != null) { + statusDescription.value = '已连接到设备: ${_bluetoothService.currentDevice.value!.name}'; + return; + } + + if (_bluetoothService.connectedDevices.isNotEmpty) { + statusDescription.value = '找到已连接的设备: ${_bluetoothService.connectedDevices.first.name}'; + return; + } + + if (isLoading.value) { + statusDescription.value = '正在搜索设备...'; + return; + } + + statusDescription.value = '未找到已连接的蓝牙设备'; + } - /// 完整的初始化流程 + /// 初始化流程 Future _initialize() async { try { - + // 初始化语音交互服务 VoiceInteractionService.to.initialize(); VoiceInteractionService.to.startService(); - - - isLoading.value = true; statusDescription.value = '正在初始化...'; - // 1. 获取蓝牙服务 - _bluetoothService = Get.find(); + // 获取蓝牙服务 + _bluetoothService = Get.find(); // 确保蓝牙服务已初始化 if (!await _bluetoothService.initialize()) { @@ -79,32 +127,22 @@ class PairingController extends GetxController { isLoading.value = false; return; } + _setupStatusListeners(); + // 检查权限 + // statusDescription.value = '正在检查权限...'; + // final hasPermission = await _bluetoothService.checkPermissions(); + // if (!hasPermission) { + // statusDescription.value = '缺少蓝牙权限'; + // isLoading.value = false; + // return; + // } - // 设置连接监听器,确保在页面完全加载后设置,避免过早监听可能导致的问题 - _setupDeviceListeners(); - - // 2. 检查权限 - statusDescription.value = '正在检查权限...'; - final hasPermission = await _bluetoothService.checkAndRequestPermissions(); - if (!hasPermission) { - statusDescription.value = '缺少蓝牙权限'; - isLoading.value = false; - return; - } - - // 3. 刷新设备列表 + // 刷新设备列表 statusDescription.value = '正在搜索设备...'; await refreshConnectedDevices(); - // 4. 处理结果 - if (_bluetoothService.connectedDevices.isNotEmpty) { - statusDescription.value = '找到已连接的设备'; - // 延迟跳转到主页 - _autoNavigateToHome(); - } else { - // 没有找到设备 - statusDescription.value = '未找到已连接的蓝牙设备'; - } + // 更新状态描述 + _updateStatusDescription(); } catch (e) { Logger.error('初始化配对控制器失败: $e'); @@ -114,33 +152,6 @@ class PairingController extends GetxController { isLoading.value = false; } } - - - /// 设置设备监听器 - void _setupDeviceListeners() { - try { - if (!_isBluetoothServiceAvailable.value) return; - - // 监听设备连接事件 - _bluetoothService.onDeviceConnected((device) { - Logger.info('检测到蓝牙设备连接: ${device.name}'); - - // 更新状态并跳转到主页 - statusDescription.value = '已连接到设备: ${device.name}'; - - // 立即跳转到主页 - _navigateToHome(); - }); - } catch (e) { - Logger.error('设置设备监听器失败: $e'); - } - } - - /// 立即跳转到主页 - void _navigateToHome() { - Get.offAllNamed(Routes.home); - } - /// 刷新设备 Future refreshConnectedDevices() async { @@ -150,14 +161,9 @@ class PairingController extends GetxController { final hasDevices = await _bluetoothService.refreshConnectedDevices(); - if (hasDevices) { - statusDescription.value = '找到已连接的设备'; - // 延迟跳转到主页 - _autoNavigateToHome(); - } else { - // 没有找到设备 - statusDescription.value = '未找到已连接的蓝牙设备'; - } + // 更新状态描述 + _updateStatusDescription(); + } catch (e) { Logger.error('刷新设备失败: $e'); statusDescription.value = '刷新设备失败'; @@ -168,7 +174,11 @@ class PairingController extends GetxController { /// 自动跳转到主页 void _autoNavigateToHome() { - if (_bluetoothService.connectedDevices.isEmpty) return; + // 如果已经在跳转或设备列表为空,则不执行 + if (_isNavigatingToHome || _bluetoothService.connectedDevices.isEmpty) return; + + // 设置跳转标志 + _isNavigatingToHome = true; Logger.info('已找到连接的设备,自动跳转到主页'); @@ -179,7 +189,12 @@ class PairingController extends GetxController { /// 打开系统蓝牙设置 void openBluetoothSettings() { - _bluetoothService.openBluetoothSettings(); + try { + // 使用app_settings插件打开蓝牙设置 + AppSettings.openAppSettings(type: AppSettingsType.bluetooth); + } catch (e) { + Logger.error('打开蓝牙设置失败: $e'); + } } /// 刷新设备(兼容旧代码) @@ -189,6 +204,8 @@ class PairingController extends GetxController { /// 跳过连接 void skipConnecting() { + // 设置跳转标志防止自动跳转再次触发 + _isNavigatingToHome = true; Get.offAllNamed(Routes.home); } } \ No newline at end of file