Browse Source

blue button

weicu
wolfplus 2 years ago
parent
commit
7f11773489
  1. BIN
      android/app/src/main/res/raw/sample.mp3
  2. 12
      ios/Runner.xcodeproj/project.pbxproj
  3. 166
      ios/Runner/AppDelegate.swift
  4. 499
      ios/Runner/BluetoothMediaButtonHelper.swift
  5. 522
      ios/Runner/ClassicBluetoothHelper.swift
  6. 25
      ios/Runner/Info.plist
  7. 0
      ios/Runner/Resources/.gitkeep
  8. BIN
      ios/Runner/Resources/sample.mp3
  9. BIN
      ios/Runner/Resources/silence.mp3
  10. BIN
      ios/Runner/sample.mp3
  11. BIN
      ios/Runner/silence.mp3
  12. 4
      lib/core/bindings/initial_binding.dart
  13. 330
      lib/data/services/bluetooth_media_button_service.dart
  14. 386
      lib/data/services/classic_bluetooth_service.dart
  15. 193
      lib/modules/home/controllers/home_controller.dart
  16. 163
      lib/modules/pairing/controllers/pairing_controller.dart

BIN
android/app/src/main/res/raw/sample.mp3

Binary file not shown.

12
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 = "<group>"; };
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 = "<group>"; };
0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicBluetoothHelper.swift; sourceTree = "<group>"; };
0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMediaButtonHelper.swift; sourceTree = "<group>"; };
0ADE29672D8C25660032BFCB /* sample.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = sample.mp3; sourceTree = "<group>"; };
0ADE29682D8C25660032BFCB /* silence.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = silence.mp3; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
@ -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;

166
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 channel = FlutterMethodChannel(name: "com.deep_voice.classic_bluetooth", binaryMessenger: controller.binaryMessenger)
// 注册经典蓝牙通道
let classicChannel = FlutterMethodChannel(
name: "com.deep_voice.classic_bluetooth",
binaryMessenger: controller.binaryMessenger
)
channel.setMethodCallHandler { [weak self] (call, result) in
classicChannel.setMethodCallHandler { [weak self] (call, result) in
guard let self = self else {
result(FlutterError(code: "UNAVAILABLE", message: "ClassicBluetoothHelper实例不可用", details: nil))
result(FlutterError(code: "UNAVAILABLE", message: "AppDelegate实例不可用", 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)
}
}
result(self.classicBluetoothHelper?.isBluetoothEnabled() ?? false)
case "getConnectedHeadsetDevices":
// 获取已连接的耳机设备
self.classicBluetoothHelper?.getConnectedHeadsetDevices { devices, error in
if let error = error {
result(FlutterError(code: "GET_HEADSET_DEVICES_FAILED", message: error, details: nil))
result(FlutterError(code: "GET_DEVICES_FAILED", message: error, details: nil))
} else {
result(devices)
}
}
case "checkAndRequestNativePermission":
self.classicBluetoothHelper?.checkAndRequestNativePermission { permissionStatus in
result(permissionStatus)
}
case "startBluetoothDeviceMonitoring":
self.classicBluetoothHelper?.startBluetoothDeviceMonitoring()
// 启动蓝牙设备监控
self.classicBluetoothHelper?.initBluetooth()
result(true)
case "stopBluetoothDeviceMonitoring":
self.classicBluetoothHelper?.stopBluetoothDeviceMonitoring()
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
case "openSettings":
result(self.classicBluetoothHelper?.openSettings())
// 注册蓝牙事件通道
let classicEventChannel = FlutterEventChannel(
name: "com.deep_voice.classic_bluetooth_events",
binaryMessenger: controller.binaryMessenger
)
case "openBluetoothSettings":
result(self.classicBluetoothHelper?.openBluetoothSettings())
// 创建事件处理器
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
)
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 "startButtonListening":
let success = mediaButtonHelper.startButtonListening()
result(success)
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
return nil
}
// 注册设备事件回调
bluetoothHelper.registerDeviceEventCallback { [weak self] event in
NSLog("BluetoothStreamHandler: 收到蓝牙事件 \(event)")
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)
}
}
}

499
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<UInt32>.size)
}
}
extension UInt16 {
var data: Data {
var int = self
return Data(bytes: &int, count: MemoryLayout<UInt16>.size)
}
}

522
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)")
}
}
}
// 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)")
}
NSLog("\(TAG): centralManagerDidUpdateState: 发送蓝牙状态变化事件: \(stateString)")
// 发送蓝牙状态变化事件
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
// 设置事件处理器
@objc func setEventHandler(_ handler: BluetoothEventHandler) {
eventHandler = handler
}
// 尝试激活音频会话以获取设备信息
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)")
}
}
// 获取已连接的耳机设备
@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)")
}
}
// 使用原生方式检查蓝牙权限
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)
}
// 原生方式检查蓝牙权限并返回给 Flutter
@objc func checkAndRequestNativePermission(_ completion: @escaping ([String: Any]) -> Void) {
// 确保 centralManager 已初始化
// 初始化蓝牙(主入口方法)
@objc func initBluetooth() {
// 初始化蓝牙中央管理器
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()
// 配置音频会话
configureAudioSession()
// 尝试主动请求蓝牙权限(如果尚未决定)
if #available(iOS 13.0, *) {
if self.centralManager?.authorization == .notDetermined {
NSLog("\(self.TAG): 蓝牙权限状态为未决定,尝试主动请求权限")
// 启动扫描会触发系统权限请求
self.centralManager?.scanForPeripherals(withServices: nil, options: nil)
// 立即停止扫描
self.centralManager?.stopScan()
}
}
// 启动设备监控
startBluetoothDeviceMonitoring()
let result: [String: Any] = [
"enabled": enabled,
"authorized": authorized,
"status": self.getPermissionStatusText(enabled: enabled, authorized: authorized)
]
NSLog("\(self.TAG): 返回权限检查结果: \(result)")
completion(result)
// 发送当前蓝牙状态
if let manager = centralManager {
let stateString = getBluetoothStateString(state: manager.state)
sendBluetoothStateEvent(state: stateString)
}
}
// 获取权限状态文本
private func getPermissionStatusText(enabled: Bool, authorized: Bool) -> String {
if !enabled {
return "disabled" // 蓝牙已禁用
}
// MARK: - CBCentralManagerDelegate
if !authorized {
return "denied" // 权限被拒绝
}
return "granted" // 已授权
func centralManagerDidUpdateState(_ central: CBCentralManager) {
let stateString = getBluetoothStateString(state: central.state)
sendBluetoothStateEvent(state: stateString)
}
// 获取当前连接的音频设备
private func getConnectedAudioDevices() -> [[String: String]] {
var result: [[String: String]] = []
// MARK: - 设备监控
// 尝试初始化设备列表
if bluetoothDevices.isEmpty {
// 只在第一次使用时初始化
NSLog("\(TAG): 第一次获取蓝牙设备,初始化设备列表")
// 获取蓝牙状态字符串
private func getBluetoothStateString(state: CBManagerState) -> String {
switch state {
case .poweredOn:
return "on"
case .poweredOff:
return "off"
default:
return "unknown"
}
// 获取可用的音频输出设备
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 作为设备标识符
// 发送蓝牙状态事件
private func sendBluetoothStateEvent(state: String) {
let event: [String: Any] = [
"type": "bluetoothStateChanged",
"state": state,
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
result.append(device)
NSLog("\(TAG): 添加蓝牙设备: \(output.portName)")
}
eventHandler?.sendEvent(event)
}
return result
}
// 监听蓝牙设备连接/断开
// 启动蓝牙设备监控
@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)
// 发送设备连接事件
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,61 +133,21 @@ 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"] {
guard let name = device["name"], let address = device["address"] else { return }
let deviceMap: [String: Any] = [
"name": name,
"address": address,
"type": "a2dp" // iOS 默认认为是 A2DP 设备
"type": "headset"
]
let event: [String: Any] = [
@ -443,20 +156,17 @@ import UIKit
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
callback(event)
}
eventHandler?.sendEvent(event)
}
// 发送设备断开事件
private func sendDeviceDisconnectedEvent(device: [String: String]) {
if let callback = deviceEventCallback,
let name = device["name"],
let address = device["address"] {
guard let name = device["name"], let address = device["address"] else { return }
let deviceMap: [String: Any] = [
"name": name,
"address": address,
"type": "a2dp" // iOS 默认认为是 A2DP 设备
"type": "headset"
]
let event: [String: Any] = [
@ -465,59 +175,85 @@ import UIKit
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
callback(event)
eventHandler?.sendEvent(event)
}
// MARK: - 公共API
// 获取当前连接的耳机设备
@objc func getConnectedHeadsetDevices(_ completion: @escaping ([[String: String]]?, String?) -> Void) {
// 检查蓝牙状态
if centralManager?.state != .poweredOn {
completion(nil, "蓝牙未启用")
return
}
// 检查设备是否存在于缓存中
private func deviceExistsInCache(device: [String: String]) -> Bool {
return deviceExistsInList(device: device, list: bluetoothDevices)
do {
// 获取设备
let devices = getConnectedAudioDevices()
completion(devices, nil)
} catch {
completion(nil, "获取设备失败: \(error.localizedDescription)")
}
}
// 检查设备是否存在于列表中
private func deviceExistsInList(device: [String: String], list: [[String: String]]) -> Bool {
guard let address = device["address"] else { return false }
return list.contains { $0["address"] == address }
// 获取当前连接的音频设备
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)
}
// 停止监听
@objc func stopBluetoothDeviceMonitoring() {
NotificationCenter.default.removeObserver(self, name: AVAudioSession.routeChangeNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: "CBCentralManagerDidUpdateStateNotification"), object: nil)
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
}
// 配置音频会话
private func configureAudioSession() {
do {
try audioSession.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .allowBluetoothA2DP])
try audioSession.setActive(true)
} catch {}
}
return false
// 检查蓝牙是否启用
@objc func isBluetoothEnabled() -> Bool {
return centralManager?.state == .poweredOn
}
// 打开蓝牙设置(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
// 清理设备地址,移除后缀
private func cleanDeviceAddress(_ address: String) -> String {
// 检查是否包含常见分隔符
if let range = address.range(of: "-") ?? address.range(of: ":") ?? address.range(of: "_") {
// 只保留分隔符之前的部分
return String(address[..<range.lowerBound])
}
// iOS设备UID有时会包含额外信息,尝试只保留标准MAC地址格式部分
// MAC地址通常是12个十六进制字符,可能带有分隔符
let hexPattern = "[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){5}|[0-9A-Fa-f]{12}"
if let regex = try? NSRegularExpression(pattern: hexPattern, options: []),
let match = regex.firstMatch(in: address, options: [], range: NSRange(location: 0, length: address.utf16.count)) {
let matchRange = match.range
if let range = Range(matchRange, in: address) {
return String(address[range])
}
}
// 如果无法直接跳转到蓝牙设置,则打开一般设置
return openSettings()
return address
}
}

25
ios/Runner/Info.plist

@ -26,8 +26,21 @@
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限来连接和管理音频设备</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要蓝牙权限来连接和管理音频设备</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要麦克风权限来进行语音识别</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>需要语音识别权限来提供语音交互服务</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>bluetooth-central</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@ -45,17 +58,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限来连接和管理音频设备</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要蓝牙权限来连接和管理音频设备</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要麦克风权限来进行语音识别</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>需要语音识别权限来提供语音交互服务</string>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
</array>
</dict>
</plist>

0
ios/Runner/Resources/.gitkeep

BIN
ios/Runner/Resources/sample.mp3

Binary file not shown.

BIN
ios/Runner/Resources/silence.mp3

Binary file not shown.

BIN
ios/Runner/sample.mp3

Binary file not shown.

BIN
ios/Runner/silence.mp3

Binary file not shown.

4
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>(() => ClassicBluetoothService(), fenix: true);
// 蓝牙媒体按钮服务
Get.lazyPut<BluetoothMediaButtonService>(() => BluetoothMediaButtonService(), fenix: true);
}
}

330
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<dynamic, dynamic> 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<BluetoothMediaButtonService>();
// 方法通道名称
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<MediaButtonEvent>.broadcast();
/// 按钮事件流
Stream<MediaButtonEvent> 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<bool> 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<dynamic, dynamic> 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<bool> 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<bool>('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<bool> stopButtonListening() async {
if (!_isInitialized) {
return false;
}
if (!_isListening.value) {
return true;
}
isLoading.value = true;
try {
final result = await _channel.invokeMethod<bool>('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();
}
}

386
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<dynamic, dynamic> 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<dynamic, dynamic>?;
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<ClassicBluetoothService>();
// 注意: 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<BluetoothConnectionEvent>.broadcast();
/// 蓝牙事件流
Stream<BluetoothConnectionEvent> get connectionEvents => _connectionEventController.stream;
/// 当前连接的设备
final currentDevice = Rx<BluetoothDevice?>(null);
/// 已连接设备列表
/// 已连接耳机设备列表
final connectedDevices = <BluetoothDevice>[].obs;
/// 蓝牙是否可用
final _isBluetoothEnabled = true.obs;
bool get isBluetoothEnabled => _isBluetoothEnabled.value;
/// 是否正在加载
final isLoading = false.obs;
/// 错误信息
final errorMessage = ''.obs;
/// 蓝牙状态变化事件流
/// 蓝牙设备状态监听
StreamSubscription? _bluetoothStateListener;
/// 设备连接回调
final List<Function(BluetoothDevice)> _deviceConnectedCallbacks = [];
/// 蓝牙开关状态
final bluetoothState = 'on'.obs;
/// 设备断开回调
final List<Function(BluetoothDevice)> _deviceDisconnectedCallbacks = [];
/// 蓝牙状态变化回调
final List<Function(String)> _bluetoothStateChangedCallbacks = [];
/// 蓝牙是否可用
bool get isBluetoothOn => bluetoothState.value == 'on';
/// 服务是否已初始化
bool _isInitialized = false;
ClassicBluetoothService() {
// 创建服务时不立即初始化,避免还没有原生支持时就调用方法
Logger.info('创建 ClassicBluetoothService 实例');
}
/// 初始化蓝牙服务
/// 必须在使用任何方法前调用此方法确保通道已设置
/// 初始化蓝牙服务 - 单一入口点
Future<bool> 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 初始化成功');
Logger.info('蓝牙服务初始化成功');
return true;
} catch (e) {
Logger.error('初始化 ClassicBluetoothService 失败: $e');
errorMessage.value = '初始化蓝牙服务失败';
Logger.error('初始化蓝牙服务失败: $e');
return false;
}
}
/// 检查并请求蓝牙权限
Future<bool> 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权限检查');
/// 检查蓝牙权限
Future<bool> checkPermissions() async {
// 检查蓝牙相关权限
Map<Permission, PermissionStatus> 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 statuses[Permission.bluetoothConnect] == PermissionStatus.granted;
}
return true;
} catch (e) {
Logger.error('检查蓝牙权限失败: $e');
errorMessage.value = '检查蓝牙权限失败';
return false;
}
}
/// 使用原生方法检查蓝牙权限
Future<Map<dynamic, dynamic>?> 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<bool> 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}');
// 如果蓝牙未开启,无法获取设备
if (!isBluetoothOn) {
Logger.info('蓝牙未开启,无法获取设备');
return false;
}
// 获取耳机设备
final headsetDevices = await getConnectedHeadsetDevices();
Logger.info('已连接的耳机设备: ${headsetDevices.length}');
try {
// 只获取耳机设备
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<List<BluetoothDevice>> 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<List<BluetoothDevice>> 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,12 +142,11 @@ class ClassicBluetoothService extends GetxService {
/// 设置蓝牙状态监听
void _setupBluetoothStateListener() {
try {
_bluetoothStateListener?.cancel();
_bluetoothStateListener = _eventChannel
.receiveBroadcastStream()
.listen((event) {
Logger.info('收到蓝牙事件: $event');
if (event is Map) {
final type = event['type'] as String?;
@ -367,54 +156,25 @@ class ClassicBluetoothService extends GetxService {
_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);
}
_handleDeviceConnected(event);
} 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);
}
_handleDeviceDisconnected(event);
}
}
}, 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}');
/// 处理设备连接事件
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)) {
@ -424,53 +184,45 @@ class ClassicBluetoothService extends GetxService {
// 更新当前设备
currentDevice.value = device;
// 通知所有回调
for (var callback in _deviceConnectedCallbacks) {
callback(device);
Logger.info('设备已连接: ${device.name}');
}
}
/// 处理设备断开
void _handleDeviceDisconnected(BluetoothDevice device) {
Logger.info('设备已断开: ${device.name}');
/// 处理设备断开事件
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 == device.address);
connectedDevices.removeWhere((d) => d.address == address);
// 如果是当前设备,清除当前设备
if (currentDevice.value?.address == device.address) {
if (currentDevice.value?.address == address) {
currentDevice.value = connectedDevices.isEmpty ? null : connectedDevices.first;
Logger.info('当前设备已断开连接');
}
// 通知所有回调
for (var callback in _deviceDisconnectedCallbacks) {
callback(device);
}
}
/// 注册设备连接回调
void onDeviceConnected(Function(BluetoothDevice) callback) {
_deviceConnectedCallbacks.add(callback);
}
/// 注册设备断开回调
void onDeviceDisconnected(Function(BluetoothDevice) callback) {
_deviceDisconnectedCallbacks.add(callback);
}
/// 处理蓝牙状态变化
void _handleBluetoothStateChange(String state) {
Logger.info('蓝牙状态变化: $state');
/// 注册蓝牙状态变化回调
void onBluetoothStateChanged(Function(String) callback) {
_bluetoothStateChangedCallbacks.add(callback);
}
// 更新蓝牙状态
bluetoothState.value = state;
/// 打开蓝牙设置
Future<bool> 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();
}
}

193
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 = <classic.BluetoothDevice>[].obs;
List<classic.BluetoothDevice> get connectedClassicDevices => _connectedClassicDevices;
List<BluetoothDevice> get connectedClassicDevices => _bluetoothService.connectedDevices;
// 蓝牙状态
RxString get bluetoothState => _bluetoothService.bluetoothState;
// 按钮事件流订阅
StreamSubscription<MediaButtonEvent>? _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<classic.ClassicBluetoothService>();
_bluetoothService = Get.find<ClassicBluetoothService>();
// 更新当前连接状态
_updateConnectionStatus();
@ -76,6 +92,45 @@ class HomeController extends GetxController {
}
}
// 初始化蓝牙媒体按钮服务
void _initMediaButtonService() {
try {
// 获取媒体按钮服务实例
_mediaButtonService = Get.find<BluetoothMediaButtonService>();
// 检查是否支持该功能
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,6 +149,12 @@ class HomeController extends GetxController {
// 更新连接状态
void _updateConnectionStatus() {
// 如果正在更新,则跳过
if (_isUpdatingStatus) return;
_isUpdatingStatus = true;
try {
// 检查当前设备
final currentDevice = _bluetoothService.currentDevice.value;
@ -106,31 +167,31 @@ class HomeController extends GetxController {
}
update(); // 通知UI刷新
} finally {
_isUpdatingStatus = false;
}
}
// 设置蓝牙监听器
void _setupBluetoothListeners() {
// 监听设备连接
_bluetoothService.onDeviceConnected((device) {
Logger.info('监听到蓝牙设备连接: ${device.name}');
// 只监听当前设备和蓝牙状态
// 监听当前设备状态
ever(_bluetoothService.currentDevice, (device) {
if (device != null) {
_isConnected.value = true;
_connectionStatus.value = ConnectionStatus.connected;
update();
});
// 监听设备断开
_bluetoothService.onDeviceDisconnected((device) {
Logger.info('监听到蓝牙设备断开: ${device.name}');
} 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<void> 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();
}
}

163
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<classic.BluetoothDevice?> get currentDevice => _bluetoothService.currentDevice;
Rx<BluetoothDevice?> get currentDevice => _bluetoothService.currentDevice;
// 连接设备列表
RxList<classic.BluetoothDevice> get connectedDevices => _bluetoothService.connectedDevices;
RxList<BluetoothDevice> get connectedDevices => _bluetoothService.connectedDevices;
// 蓝牙状态
RxString get bluetoothState => _bluetoothService.bluetoothState;
// 错误信息
RxString get errorMessage => _bluetoothService.errorMessage;
final errorMessage = ''.obs;
// 连接设备列表(兼容旧代码)
List<classic.BluetoothDevice> get connectedClassicDevices =>
List<BluetoothDevice> 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();
});
/// 完整的初始化流程
Future<void> _initialize() async {
try {
// 监听当前设备状态
ever(_bluetoothService.currentDevice, (device) {
_updateStatusDescription();
VoiceInteractionService.to.initialize();
VoiceInteractionService.to.startService();
// 如果有设备连接,自动跳转到主页
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<void> _initialize() async {
try {
// 初始化语音交互服务
VoiceInteractionService.to.initialize();
VoiceInteractionService.to.startService();
isLoading.value = true;
statusDescription.value = '正在初始化...';
// 1. 获取蓝牙服务
_bluetoothService = Get.find<classic.ClassicBluetoothService>();
// 获取蓝牙服务
_bluetoothService = Get.find<ClassicBluetoothService>();
// 确保蓝牙服务已初始化
if (!await _bluetoothService.initialize()) {
@ -79,32 +127,22 @@ class PairingController extends GetxController {
isLoading.value = false;
return;
}
// 设置连接监听器,确保在页面完全加载后设置,避免过早监听可能导致的问题
_setupDeviceListeners();
// 2. 检查权限
statusDescription.value = '正在检查权限...';
final hasPermission = await _bluetoothService.checkAndRequestPermissions();
if (!hasPermission) {
statusDescription.value = '缺少蓝牙权限';
isLoading.value = false;
return;
}
// 3. 刷新设备列表
_setupStatusListeners();
// 检查权限
// statusDescription.value = '正在检查权限...';
// final hasPermission = await _bluetoothService.checkPermissions();
// if (!hasPermission) {
// statusDescription.value = '缺少蓝牙权限';
// isLoading.value = false;
// return;
// }
// 刷新设备列表
statusDescription.value = '正在搜索设备...';
await refreshConnectedDevices();
// 4. 处理结果
if (_bluetoothService.connectedDevices.isNotEmpty) {
statusDescription.value = '找到已连接的设备';
// 延迟跳转到主页
_autoNavigateToHome();
} else {
// 没有找到设备
statusDescription.value = '未找到已连接的蓝牙设备';
}
// 更新状态描述
_updateStatusDescription();
} catch (e) {
Logger.error('初始化配对控制器失败: $e');
@ -115,33 +153,6 @@ class PairingController extends GetxController {
}
}
/// 设置设备监听器
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<void> refreshConnectedDevices() async {
try {
@ -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);
}
}
Loading…
Cancel
Save