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.

523 lines
20 KiB

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()
}
}