import Foundation import CoreBluetooth import AVFoundation import UIKit @objc class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate, CBPeripheralManagerDelegate { private let TAG = "ClassicBluetoothHelper" // AVAudioSession 用于获取已连接的蓝牙设备 private let audioSession = AVAudioSession.sharedInstance() // 蓝牙管理器 private var centralManager: CBCentralManager? private var peripheralManager: CBPeripheralManager? // 设备列表缓存 private var bluetoothDevices: [[String: String]] = [] // 设备连接和断开回调 private var deviceEventCallback: (([String: Any]) -> Void)? // 初始化 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 } // 尝试激活音频会话以获取设备信息 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 已初始化 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) } } // 获取权限状态文本 private func getPermissionStatusText(enabled: Bool, authorized: Bool) -> String { if !enabled { return "disabled" // 蓝牙已禁用 } if !authorized { return "denied" // 权限被拒绝 } 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 } // 监听蓝牙设备连接/断开 @objc func startBluetoothDeviceMonitoring() { // 注册音频会话通知 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 ) } // 处理音频路由变化 @objc private func handleRouteChange(_ notification: Notification) { guard let userInfo = notification.userInfo, let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt, let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { return } // 获取当前设备 let currentDevices = getConnectedAudioDevices() // 检查原因 switch reason { case .newDeviceAvailable: // 新设备已连接 - 查找新增的设备 for device in currentDevices { if !deviceExistsInCache(device: device) { // 找到新设备 NSLog("\(TAG): 新的音频设备已连接: \(device["name"] ?? "未知设备")") // 添加到缓存 bluetoothDevices.append(device) // 发送设备连接事件 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) // 发送设备断开事件 sendDeviceDisconnectedEvent(device: cachedDevice) } } // 从缓存中移除断开的设备 for device in devicesToRemove { if let index = bluetoothDevices.firstIndex(where: { $0["address"] == device["address"] }) { bluetoothDevices.remove(at: index) } } default: break } } // 处理蓝牙状态变化 @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 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) } } // 发送设备断开事件 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) } } // 检查设备是否存在于缓存中 private func deviceExistsInCache(device: [String: String]) -> Bool { return deviceExistsInList(device: device, list: bluetoothDevices) } // 检查设备是否存在于列表中 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 stopBluetoothDeviceMonitoring() { NotificationCenter.default.removeObserver(self, name: AVAudioSession.routeChangeNotification, object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: "CBCentralManagerDidUpdateStateNotification"), object: nil) } // 释放资源 @objc func dispose() { stopBluetoothDeviceMonitoring() deviceEventCallback = 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 } // 打开蓝牙设置(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 } } } // 如果无法直接跳转到蓝牙设置,则打开一般设置 return openSettings() } }