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.
305 lines
10 KiB
305 lines
10 KiB
import Foundation
|
|
import CoreBluetooth
|
|
import AVFoundation
|
|
|
|
/// 蓝牙事件类型
|
|
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"
|
|
case resetting = "resetting"
|
|
}
|
|
|
|
/// 简化的蓝牙助手类,专注于iOS上的蓝牙耳机检测
|
|
class ClassicBluetoothHelper: NSObject {
|
|
// 状态监控相关
|
|
private var isMonitoring = false
|
|
private var stateCallback: ((BluetoothEventType, [String: Any]) -> Void)?
|
|
|
|
// 用于监控蓝牙系统状态,延迟初始化
|
|
private var centralManager: CBCentralManager?
|
|
|
|
// 音频路由变化观察者
|
|
private var routeChangeObserver: NSObjectProtocol?
|
|
|
|
override init() {
|
|
super.init()
|
|
// 初始化时不立即创建centralManager,延迟到实际需要时
|
|
}
|
|
|
|
deinit {
|
|
stopBluetoothStateMonitoring()
|
|
}
|
|
|
|
/// 延迟初始化CBCentralManager,只在实际需要时创建
|
|
private func initializeCentralManagerIfNeeded() {
|
|
if centralManager == nil {
|
|
centralManager = CBCentralManager(
|
|
delegate: self,
|
|
queue: DispatchQueue.main,
|
|
options: [CBCentralManagerOptionShowPowerAlertKey: true]
|
|
)
|
|
}
|
|
}
|
|
|
|
/// 检查蓝牙是否启用,此方法会触发权限请求
|
|
func isBluetoothEnabled() -> Bool {
|
|
// 此方法调用会触发权限申请
|
|
initializeCentralManagerIfNeeded()
|
|
|
|
guard let central = centralManager else {
|
|
print("检查蓝牙状态失败:centralManager未初始化")
|
|
return false
|
|
}
|
|
|
|
// 检查蓝牙状态并记录
|
|
let stateDescription: String
|
|
let isEnabled: Bool
|
|
|
|
switch central.state {
|
|
case .poweredOn:
|
|
stateDescription = "已开启"
|
|
isEnabled = true
|
|
case .poweredOff:
|
|
stateDescription = "已关闭"
|
|
isEnabled = false
|
|
case .unauthorized:
|
|
stateDescription = "未授权"
|
|
isEnabled = false
|
|
case .unsupported:
|
|
stateDescription = "不支持"
|
|
isEnabled = false
|
|
case .resetting:
|
|
stateDescription = "重置中"
|
|
isEnabled = false
|
|
case .unknown:
|
|
stateDescription = "未知"
|
|
isEnabled = false
|
|
@unknown default:
|
|
stateDescription = "未定义状态"
|
|
isEnabled = false
|
|
}
|
|
|
|
print("蓝牙状态检查结果: \(stateDescription), isEnabled: \(isEnabled)")
|
|
return isEnabled
|
|
}
|
|
|
|
/// 获取已连接的蓝牙耳机,此方法会触发权限请求
|
|
func getConnectedDevices(completion: @escaping ([[String: String]], Error?) -> Void) {
|
|
// 确保蓝牙已初始化
|
|
initializeCentralManagerIfNeeded()
|
|
|
|
// 获取音频设备(主要是蓝牙耳机)
|
|
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)
|
|
]
|
|
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
|
|
|
|
// 初始化蓝牙管理器
|
|
initializeCentralManagerIfNeeded()
|
|
|
|
// 观察音频路由变化
|
|
let notificationCenter = NotificationCenter.default
|
|
routeChangeObserver = notificationCenter.addObserver(
|
|
forName: AVAudioSession.routeChangeNotification,
|
|
object: nil,
|
|
queue: nil
|
|
) { [weak self] notification in
|
|
self?.handleAudioRouteChange(notification)
|
|
}
|
|
|
|
// 激活音频会话
|
|
activateAudioSession()
|
|
}
|
|
}
|
|
|
|
// 处理音频路由变化
|
|
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])
|
|
}
|
|
|
|
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])
|
|
}
|
|
}
|
|
|
|
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 reportCurrentBluetoothState() {
|
|
// 如果centralManager未初始化,不触发状态报告
|
|
guard let centralManager = centralManager else { return }
|
|
|
|
// 直接调用delegate方法保持状态处理一致性
|
|
centralManagerDidUpdateState(centralManager)
|
|
}
|
|
}
|
|
|
|
// MARK: - CBCentralManagerDelegate
|
|
extension ClassicBluetoothHelper: CBCentralManagerDelegate {
|
|
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
let state: BluetoothState
|
|
|
|
// 添加详细日志输出
|
|
print("iOS蓝牙状态原始值: \(central.state.rawValue)")
|
|
|
|
switch central.state {
|
|
case .poweredOn:
|
|
print("蓝牙状态: 已开启(.poweredOn)")
|
|
state = .on
|
|
case .poweredOff:
|
|
print("蓝牙状态: 已关闭(.poweredOff)")
|
|
state = .off
|
|
case .unauthorized:
|
|
print("蓝牙状态: 未授权(.unauthorized)")
|
|
state = .unauthorized
|
|
case .unsupported:
|
|
print("蓝牙状态: 不支持(.unsupported)")
|
|
state = .unsupported
|
|
case .resetting:
|
|
print("蓝牙状态: 重置中(.resetting)")
|
|
state = .resetting
|
|
case .unknown:
|
|
print("蓝牙状态: 未知(.unknown)")
|
|
state = .unknown
|
|
@unknown default:
|
|
print("蓝牙状态: 未处理的新状态(\(central.state.rawValue))")
|
|
state = .unknown
|
|
}
|
|
|
|
print("向Flutter端报告的蓝牙状态: \(state.rawValue)")
|
|
|
|
// 仅发送兼容的状态(为保持向后兼容,只发送on/off/unknown)
|
|
let reportState: String
|
|
switch state {
|
|
case .on:
|
|
reportState = BluetoothState.on.rawValue
|
|
case .off, .unauthorized, .unsupported:
|
|
// 这些状态对用户来说都意味着蓝牙不可用
|
|
reportState = BluetoothState.off.rawValue
|
|
default:
|
|
reportState = BluetoothState.unknown.rawValue
|
|
}
|
|
|
|
stateCallback?(.stateChanged, ["state": reportState])
|
|
}
|
|
}
|