You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

275 lines
9.2 KiB

import Foundation
import AVFoundation
import CoreBluetooth
/// 蓝牙事件类型
enum BluetoothEventType {
case stateChanged
case deviceConnected
case deviceDisconnected
}
/// 蓝牙状态枚举
enum BluetoothState: String {
case unknown = "unknown"
case off = "off"
case on = "on"
case unauthorized = "unauthorized"
case unsupported = "unsupported"
}
/// 简化的蓝牙助手类,专注于iOS上的蓝牙耳机检测
class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate {
private var centralManager: CBCentralManager?
private var bluetoothState: CBManagerState = .unknown
// 添加缺少的属性声明
private var stateCallback: ((BluetoothEventType, [String: Any]) -> Void)?
private var isMonitoring: Bool = false
// 音频路由变化观察者
private var routeChangeObserver: NSObjectProtocol?
override init() {
super.init()
centralManager = CBCentralManager(delegate: self, queue: nil)
}
deinit {
stopBluetoothStateMonitoring()
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
bluetoothState = central.state
print("lxm---蓝牙状态更新: \(bluetoothState.rawValue)")
// 向Flutter端报告蓝牙状态变化
if isMonitoring {
let currentState = getBluetoothAvailability()
reportBluetoothState(currentState)
}
}
func isBluetoothEnabled() -> Bool {
switch bluetoothState {
case .poweredOn:
return true
case .poweredOff, .unsupported, .unauthorized, .resetting:
return false
case .unknown:
// 如果状态未知,回退到原来的音频设备检测方法
return !getConnectedAudioDevices().isEmpty
@unknown default:
return false
}
}
/// 获取已连接的蓝牙耳机
func getConnectedDevices(completion: @escaping ([[String: String]], Error?) -> Void) {
// 获取音频设备(主要是蓝牙耳机)
let audioDevices = getConnectedAudioDevices()
// 日志输出
if !audioDevices.isEmpty {
print("已找到\(audioDevices.count)个蓝牙音频设备:")
for device in audioDevices {
print("- \(device["name"] ?? "未知") (\(device["address"] ?? "未知地址"))")
}
} else {
print("未找到蓝牙音频设备")
}
completion(audioDevices, nil)
}
// 获取连接的蓝牙音频设备
private func getConnectedAudioDevices() -> [[String: String]] {
var result: [[String: String]] = []
// 获取当前音频路由
let outputs = AVAudioSession.sharedInstance().currentRoute.outputs
// 过滤蓝牙相关设备
for output in outputs where isBluetoothOutput(output.portType) {
let device: [String: String] = [
"name": output.portName,
"address": cleanDeviceAddress(output.uid),
"isActive": "true" // 在 currentRoute.outputs 中的蓝牙设备都是活跃的
]
result.append(device)
}
return result
}
// 检查是否为蓝牙输出
private func isBluetoothOutput(_ portType: AVAudioSession.Port) -> Bool {
return portType == .bluetoothA2DP || portType == .bluetoothHFP || portType == .bluetoothLE
}
// 清理设备地址
private func cleanDeviceAddress(_ address: String) -> String {
// 移除类似 "-tacl" 的后缀
if let range = address.range(of: "-", options: .backwards) {
return String(address[..<range.lowerBound])
}
return address
}
/// 开始监听蓝牙状态变化
func startBluetoothStateMonitoring(callback: @escaping (BluetoothEventType, [String: Any]) -> Void) {
self.stateCallback = callback
if !isMonitoring {
isMonitoring = true
// 观察音频路由变化
let notificationCenter = NotificationCenter.default
routeChangeObserver = notificationCenter.addObserver(
forName: AVAudioSession.routeChangeNotification,
object: nil,
queue: nil
) { [weak self] notification in
self?.handleAudioRouteChange(notification)
}
// 激活音频会话
activateAudioSession()
// 报告当前状态
reportCurrentBluetoothState()
}
}
// 处理音频路由变化
private func handleAudioRouteChange(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
return
}
switch reason {
case .newDeviceAvailable:
// 新音频设备已连接
let audioDevices = getConnectedAudioDevices()
for device in audioDevices {
stateCallback?(.deviceConnected, ["device": device])
}
// 如果有蓝牙设备连接,报告蓝牙状态为开启
if !audioDevices.isEmpty {
reportBluetoothState(.on)
}
case .oldDeviceUnavailable:
// 获取断开之前的路由
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)
]
stateCallback?(.deviceDisconnected, ["device": device])
}
// 检查当前是否还有蓝牙设备
let currentDevices = getConnectedAudioDevices()
if currentDevices.isEmpty {
// 如果没有任何蓝牙设备连接,可以认为蓝牙状态变为未知
reportBluetoothState(.unknown)
}
}
default:
break
}
}
// 激活音频会话
private func activateAudioSession() {
do {
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .default)
try audioSession.setActive(true)
} catch {
print("设置音频会话失败: \(error.localizedDescription)")
}
}
/// 停止监听蓝牙状态变化
func stopBluetoothStateMonitoring() {
if isMonitoring {
isMonitoring = false
// 移除音频路由观察者
if let observer = routeChangeObserver {
NotificationCenter.default.removeObserver(observer)
routeChangeObserver = nil
}
// 关闭音频会话
deactivateAudioSession()
}
// 清除回调
stateCallback = nil
}
// 关闭音频会话
private func deactivateAudioSession() {
do {
try AVAudioSession.sharedInstance().setActive(false)
} catch {
print("关闭音频会话失败: \(error.localizedDescription)")
}
}
/// 获取蓝牙可用性状态(优化后的统一状态判断方法)
func getBluetoothAvailability() -> BluetoothState {
switch bluetoothState {
case .poweredOn:
return .on
case .poweredOff:
return .off
case .unauthorized:
return .unauthorized
case .unsupported:
return .unsupported
case .unknown, .resetting:
// 当CoreBluetooth状态未知时,回退到音频设备检测
return getConnectedAudioDevices().isEmpty ? .unknown : .on
@unknown default:
return .unknown
}
}
/// 报告当前蓝牙状态(优化后使用统一的状态判断方法)
func reportCurrentBluetoothState() {
// 使用优化后的统一状态判断方法
let currentState = getBluetoothAvailability()
// 根据状态输出相应的日志信息
switch currentState {
case .on:
print("蓝牙状态: 蓝牙已开启")
case .off:
print("蓝牙状态: 蓝牙已关闭")
case .unauthorized:
print("蓝牙状态: 应用未授权使用蓝牙")
case .unsupported:
print("蓝牙状态: 设备不支持蓝牙")
case .unknown:
print("蓝牙状态: 蓝牙状态未知")
}
// 向Flutter端报告状态
reportBluetoothState(currentState)
}
// 报告蓝牙状态变化
private func reportBluetoothState(_ state: BluetoothState) {
print("向Flutter端报告的蓝牙状态: \(state.rawValue)")
stateCallback?(.stateChanged, ["state": state.rawValue])
}
}