diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 57a277978..d3216ce7f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -381,9 +381,9 @@ SPEC CHECKSUMS: audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf azure_speech: d65fa5c1f04be9cee7303abf4caea30680e814fd - ble_service: 3414a81261f88a6e673cbc71178d2353d96b2a1c + ble_service: 7987f876505b5c2429303597d7604092d2545566 chat_storage: 933a96144157a07a2fef7f6acb28c78ff18465d8 - classic_bluetooth: c5e5c0c86acab8dcd6b8e9cef7fca654c3518034 + classic_bluetooth: c146e1618e867a47fa7ab79a3bf262e7d69d557d device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index f8125569b..03e8fadd9 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -481,7 +481,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -617,7 +617,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -669,7 +669,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 1d2871f5d..2b92ac731 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -41,12 +41,14 @@ CFBundleVersion $(FLUTTER_BUILD_NUMBER) - UIBackgroundModes - - bluetooth-central - bluetooth-peripheral - external-accessory - + FacebookAdvertiserIDCollectionEnabled + + FacebookAppID + 3829022037361855 + FacebookAutoLogAppEventsEnabled + + FacebookClientToken + 193d0a2b1757c41cfa557ea8e8f21c33 LSApplicationQueriesSchemes weixin @@ -70,6 +72,16 @@ LSRequiresIPhoneOS + NSAccessorySetupBluetoothCompanyIdentifiers + + EFAB + ABEF + + NSAccessorySetupKitSupports + + Bluetooth + + NSAppTransportSecurity NSAllowsArbitraryLoads @@ -77,8 +89,24 @@ NSAllowsArbitraryLoadsInWebContent + NSBluetoothAlwaysUsageDescription + 需要蓝牙权限用于连接和管理音频设备 + NSBluetoothPeripheralUsageDescription + 需要蓝牙权限用于连接和管理音频设备 + NSLocationAlwaysAndWhenInUseUsageDescription + 需要位置权限用于搜索附近的蓝牙设备 + NSLocationWhenInUseUsageDescription + 需要位置权限用于搜索附近的蓝牙设备 + NSMicrophoneUsageDescription + 需要麦克风权限用于语音识别和录音功能 UIApplicationSupportsIndirectInputEvents + UIBackgroundModes + + bluetooth-central + bluetooth-peripheral + external-accessory + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -96,32 +124,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - NSBluetoothAlwaysUsageDescription - 需要蓝牙权限用于连接和管理音频设备 - NSBluetoothPeripheralUsageDescription - 需要蓝牙权限用于连接和管理音频设备 - NSLocationWhenInUseUsageDescription - 需要位置权限用于搜索附近的蓝牙设备 - NSLocationAlwaysAndWhenInUseUsageDescription - 需要位置权限用于搜索附近的蓝牙设备 - NSMicrophoneUsageDescription - 需要麦克风权限用于语音识别和录音功能 - FacebookClientToken - 193d0a2b1757c41cfa557ea8e8f21c33 - FacebookAppID - 3829022037361855 - FacebookAdvertiserIDCollectionEnabled - - FacebookAutoLogAppEventsEnabled - - NSAccessorySetupKitSupports - - Bluetooth - WiFi - - NSAccessorySetupBluetoothCompanyIdentifiers - - ABEF - diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 0c67376eb..032f72d37 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -1,5 +1,12 @@ - + + com.apple.developer.networking.wifi-info + + com.apple.external-accessory.wireless-configuration + + com.apple.developer.homekit + + diff --git a/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift b/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift index 67ccf3772..990f5661a 100644 --- a/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift +++ b/local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift @@ -1,6 +1,8 @@ import Foundation import CoreBluetooth import os.log +import ObjectiveC // 添加ObjC运行时支持 +import AVFoundation // 条件编译只导入在iOS 18+上可用的框架 #if canImport(AccessorySetupKit) && swift(>=5.9) @@ -21,23 +23,51 @@ class BleAccessorySetup: NSObject { @available(iOS 18.0, *) class AccessorySessionWrapper { var session: ASAccessorySession + var isActivated = false init() throws { do { + os_log("尝试初始化ASAccessorySession...", log: BleAccessorySetup.shared.logger, type: .debug) + // 根据WWDC24示例,使用无参初始化 self.session = try ASAccessorySession() + os_log("ASAccessorySession初始化成功", log: BleAccessorySetup.shared.logger, type: .info) } catch { - print("ASAccessorySession 初始化错误: \(error.localizedDescription)") + os_log("ASAccessorySession初始化错误: %{public}@", log: BleAccessorySetup.shared.logger, type: .error, error.localizedDescription) + + // 检查Info.plist配置 + if let plistSupports = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupKitSupports") { + os_log("NSAccessorySetupKitSupports: %{public}@", log: BleAccessorySetup.shared.logger, type: .debug, String(describing: plistSupports)) + } else { + os_log("NSAccessorySetupKitSupports未配置", log: BleAccessorySetup.shared.logger, type: .error) + } + + if let services = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothServices") { + os_log("NSAccessorySetupBluetoothServices: %{public}@", log: BleAccessorySetup.shared.logger, type: .debug, String(describing: services)) + } else { + os_log("NSAccessorySetupBluetoothServices未配置", log: BleAccessorySetup.shared.logger, type: .error) + } + + if let companyIds = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothCompanyIdentifiers") { + os_log("NSAccessorySetupBluetoothCompanyIdentifiers: %{public}@", log: BleAccessorySetup.shared.logger, type: .debug, String(describing: companyIds)) + } else { + os_log("NSAccessorySetupBluetoothCompanyIdentifiers未配置", log: BleAccessorySetup.shared.logger, type: .error) + } + throw error } } } var accessorySessionWrapper: Any? + // 添加初始化完成回调 + private var onSessionActivated: (() -> Void)? typealias DeviceDiscoveredCallback = (UUID) -> Void private var onDeviceDiscovered: DeviceDiscoveredCallback? #endif + private var isInitializing = false + private override init() { super.init() } @@ -45,6 +75,7 @@ class BleAccessorySetup: NSObject { // 设置BLE服务实例 func setBleService(_ service: Any) { self.bleService = service + os_log("BLE服务实例已设置", log: logger, type: .info) } // 设置设备发现回调 @@ -52,6 +83,7 @@ class BleAccessorySetup: NSObject { #if canImport(AccessorySetupKit) && swift(>=5.9) if #available(iOS 18.0, *) { self.onDeviceDiscovered = callback + os_log("设备发现回调已设置", log: logger, type: .debug) } #endif } @@ -60,29 +92,77 @@ class BleAccessorySetup: NSObject { func initializeDiscoverySession() { #if canImport(AccessorySetupKit) && swift(>=5.9) if #available(iOS 18.0, *) { - os_log("初始化iOS 18 AccessorySetupKit会话", log: logger, type: .info) + // 避免重复初始化 + if isInitializing { + os_log("初始化正在进行中,请等待...", log: logger, type: .debug) + return + } + + // 如果会话已经存在且已激活,无需重新初始化 + if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper, wrapper.isActivated { + os_log("会话已经初始化并激活", log: logger, type: .info) + return + } - // 检查Info.plist配置 - let plistCheck = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupKitSupports") - os_log("Info.plist NSAccessorySetupKitSupports: %{public}@", log: logger, type: .info, String(describing: plistCheck)) + isInitializing = true + os_log("初始化iOS 18 AccessorySetupKit会话...", log: logger, type: .debug) + + // 检查Info.plist配置并详细打印 + if let plistCheck = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupKitSupports") { + os_log("NSAccessorySetupKitSupports配置: %{public}@", log: logger, type: .debug, String(describing: plistCheck)) + } else { + os_log("NSAccessorySetupKitSupports未配置", log: logger, type: .error) + } + + if let services = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothServices") { + os_log("NSAccessorySetupBluetoothServices配置: %{public}@", log: logger, type: .debug, String(describing: services)) + } else { + os_log("NSAccessorySetupBluetoothServices未配置", log: logger, type: .error) + } + + if let companyIds = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothCompanyIdentifiers") { + os_log("NSAccessorySetupBluetoothCompanyIdentifiers配置: %{public}@", log: logger, type: .debug, String(describing: companyIds)) + } else { + os_log("NSAccessorySetupBluetoothCompanyIdentifiers未配置", log: logger, type: .error) + } // 创建会话 (使用try-catch) do { + os_log("尝试创建AccessorySessionWrapper...", log: logger, type: .debug) accessorySessionWrapper = try AccessorySessionWrapper() + os_log("AccessorySessionWrapper创建成功", log: logger, type: .info) // 激活会话并设置事件处理 if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper { + os_log("尝试激活ASAccessorySession...", log: logger, type: .debug) wrapper.session.activate(on: .main) { [weak self] event in - guard let self = self else { return } + guard let self = self else { + os_log("self已被释放,无法处理事件", log: BleAccessorySetup.shared.logger, type: .error) + return + } + + os_log("收到ASAccessorySession事件: %{public}d", log: self.logger, type: .debug, event.eventType.rawValue) switch event.eventType { case .activated: os_log("AccessorySetupKit会话已激活", log: self.logger, type: .info) + // 标记会话已激活 + if let wrapper = self.accessorySessionWrapper as? AccessorySessionWrapper { + wrapper.isActivated = true + // 触发激活回调 + DispatchQueue.main.async { + self.onSessionActivated?() + self.onSessionActivated = nil // 防止重复调用 + } + } + case .accessoryAdded: if let accessory = event.accessory, let uuid = accessory.bluetoothIdentifier { os_log("发现新设备: %{public}@", log: self.logger, type: .info, accessory.displayName) self.onDeviceDiscovered?(uuid) + } else { + os_log("设备添加事件但缺少设备信息或UUID", log: self.logger, type: .debug) } case .accessoryRemoved: @@ -100,6 +180,8 @@ class BleAccessorySetup: NSObject { case .pickerSetupFailed: if let error = event.error { os_log("会话出错: %{public}@", log: self.logger, type: .error, error.localizedDescription) + } else { + os_log("会话出错但无错误详情", log: self.logger, type: .debug) } default: @@ -108,76 +190,239 @@ class BleAccessorySetup: NSObject { } // 检查现有授权设备 - if !wrapper.session.accessories.isEmpty { - os_log("发现%{public}d个已授权设备", log: logger, type: .info, wrapper.session.accessories.count) - + let accessoriesCount = wrapper.session.accessories.count + os_log("发现%{public}d个已授权设备", log: logger, type: .info, accessoriesCount) + + if accessoriesCount > 0 { // 如果有现有设备,触发回调 for accessory in wrapper.session.accessories { if let uuid = accessory.bluetoothIdentifier { os_log("处理已授权设备: %{public}@", log: logger, type: .info, accessory.displayName) self.onDeviceDiscovered?(uuid) + } else { + os_log("已授权设备缺少UUID: %{public}@", log: logger, type: .debug, accessory.displayName) } } } + } else { + os_log("accessorySessionWrapper类型转换失败", log: logger, type: .error) } } catch { - os_log("AccessorySetupKit初始化错误: %{public}@", log: logger, type: .error, error.localizedDescription) + os_log("致命错误:AccessorySetupKit初始化失败: %{public}@", log: logger, type: .error, error.localizedDescription) + + // 重置初始化状态 + isInitializing = false + + // 释放失败的会话 + accessorySessionWrapper = nil + + // 尝试通知可能正在等待的回调 + DispatchQueue.main.async { + self.onSessionActivated?() + self.onSessionActivated = nil + } } } else { - os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info) + os_log("当前iOS版本不支持AccessorySetupKit", log: logger, type: .info) + + // 即使在不支持的系统上,也要调用回调 + DispatchQueue.main.async { + self.onSessionActivated?() + self.onSessionActivated = nil + } + } + #else + os_log("构建环境不支持AccessorySetupKit", log: logger, type: .info) + + // 即使在不支持的环境中,也要调用回调 + DispatchQueue.main.async { + self.onSessionActivated?() + self.onSessionActivated = nil + } + #endif + + // 完成初始化过程 + isInitializing = false + } + + // 初始化会话并在完成后执行回调 + func initializeSessionAndRun(completion: @escaping () -> Void) { + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + os_log("安全初始化会话...", log: logger, type: .debug) + + // 如果会话已存在且已激活 + if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper, wrapper.isActivated { + os_log("会话已初始化并激活,直接执行回调", log: logger, type: .info) + completion() + return + } + + // 如果会话存在但未激活,设置回调 + if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper, !wrapper.isActivated { + os_log("会话已创建但未激活,等待激活...", log: logger, type: .debug) + self.onSessionActivated = completion + return + } + + // 需要从头创建会话 + os_log("需要创建新会话...", log: logger, type: .debug) + self.onSessionActivated = completion + initializeDiscoverySession() + } else { + // 不支持iOS 18,直接执行回调 + completion() } #else - os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info) + // 不支持AccessorySetupKit,直接执行回调 + completion() + #endif + } + + // 重置会话状态 + func resetSession() { + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + os_log("重置AccessorySetupKit会话...", log: logger, type: .debug) + + // 取消可能挂起的回调 + onSessionActivated = nil + + // 清理现有会话 + if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper { + os_log("清理现有会话", log: logger, type: .debug) + wrapper.session.invalidate() + } + + // 重置状态 + accessorySessionWrapper = nil + isInitializing = false + + os_log("会话已重置", log: logger, type: .info) + } #endif } + // 安全显示设备选择器 + func safeShowDevicePicker() { + // 重置会话状态,确保干净的重新初始化 + resetSession() + + initializeSessionAndRun { [weak self] in + guard let self = self else { return } + self.showDevicePicker() + } + } + // 显示设备选择器 func showDevicePicker() { #if canImport(AccessorySetupKit) && swift(>=5.9) if #available(iOS 18.0, *) { + os_log("尝试显示设备选择器...", log: logger, type: .debug) + + // 检查会话是否已初始化 + if accessorySessionWrapper == nil { + os_log("会话未初始化,尝试自动初始化...", log: logger, type: .debug) + safeShowDevicePicker() + return + } + if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper { - os_log("显示iOS 18设备选择器", log: logger, type: .info) + // 检查会话是否已激活 + if !wrapper.isActivated { + os_log("会话尚未激活,等待激活后再显示...", log: logger, type: .debug) + onSessionActivated = { [weak self] in + self?.showDevicePicker() + } + return + } + + os_log("找到会话包装器,准备显示选择器", log: logger, type: .info) + + // 根据WWDC24示例代码创建设备描述符和显示项 + os_log("创建设备描述符...", log: logger, type: .debug) + let descriptor = ASDiscoveryDescriptor() + + // 设置蓝牙服务UUID + // let serviceUUID = "0000abc0-0000-1111-2222-123456789abc" // BleConst.PRIMARY_SERVICE_UUID + // os_log("📝 设置服务UUID: %{public}@", log: logger, type: .debug, serviceUUID) + // descriptor.bluetoothServiceUUID = CBUUID(string: serviceUUID) - // 创建描述符和选择器项,这些API也需要iOS 18 - @available(iOS 18.0, *) - func createAndShowPicker() { - // 根据WWDC24视频,正确的类名是ASDiscoveryDescriptor - let descriptor = ASDiscoveryDescriptor() + // 设置制造商ID + os_log("设置公司ID: 0xABEF", log: logger, type: .debug) + // descriptor.setValue(UInt16(0xABEF), forKey: "bluetoothCompanyIdentifier") + descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(0xEFAB) + // 创建默认图像 + os_log("获取设备图标...", log: logger, type: .debug) + let deviceImage = UIImage(systemName: "headphones") ?? UIImage() + + // 创建显示项 + os_log("创建选择器显示项...", log: logger, type: .debug) + let displayItem = ASPickerDisplayItem( + name: "语音助理耳机", + productImage: deviceImage, + descriptor: descriptor + ) + + // 确保在主线程上调用 + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } - // 设置过滤条件 - // 使用UInt16格式的公司标识符 - descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(UInt16(0xABEF)) // BleConst.MANUFACTURER_ID - descriptor.bluetoothServiceUUID = CBUUID(string: "0000abc0-0000-1111-2222-123456789abc") // BleConst.PRIMARY_SERVICE_UUID + os_log("调用showPicker方法...", log: self.logger, type: .debug) - // 创建默认图像以防系统图标不可用 - let headphonesImage = UIImage(systemName: "headphones") ?? UIImage() + // 再次检查Info.plist配置 + let bundle = Bundle.main + os_log("Info.plist检查:", log: self.logger, type: .debug) + if let supports = bundle.object(forInfoDictionaryKey: "NSAccessorySetupKitSupports") as? [String] { + os_log("NSAccessorySetupKitSupports: %{public}@", log: self.logger, type: .debug, supports.description) + } + if let services = bundle.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothServices") as? [String] { + os_log("NSAccessorySetupBluetoothServices: %{public}@", log: self.logger, type: .debug, services.description) + } + if let companyIds = bundle.object(forInfoDictionaryKey: "NSAccessorySetupBluetoothCompanyIdentifiers") as? [String] { + os_log("NSAccessorySetupBluetoothCompanyIdentifiers: %{public}@", log: self.logger, type: .debug, companyIds.description) + } - // 根据WWDC24视频,正确的类名是ASPickerDisplayItem - let displayItem = ASPickerDisplayItem( - name: "语音助理耳机", - productImage: headphonesImage, - descriptor: descriptor - ) + os_log("iOS 18环境下不检查蓝牙状态,避免创建CBCentralManager", log: self.logger, type: .debug) - // 显示选择器 - wrapper.session.showPicker(for: [displayItem]) { error in + // 使用标准方法显示选择器 + wrapper.session.showPicker(for: [displayItem]) { [weak self] error in + guard let self = self else { return } + if let error = error { - // 修复OSLog的错误用法 os_log("设备选择器显示错误: %{public}@", log: self.logger, type: .error, error.localizedDescription) + + // 错误码解析 + if let nsError = error as? NSError { + os_log("错误域: %{public}@, 错误码: %{public}d", log: self.logger, type: .error, nsError.domain, nsError.code) + + // 解析常见错误 + switch nsError.code { + case 550: + os_log("错误550: CBCentralManager冲突 - 进程中已存在使用全局权限的CBCentralManager", log: self.logger, type: .error) + case 500...599: + os_log("错误%{public}d: 可能是AccessorySetupKit内部错误", log: self.logger, type: .error, nsError.code) + default: + os_log("未知错误码", log: self.logger, type: .error) + } + + if let info = nsError.userInfo as? [String: Any] { + os_log("错误信息: %{public}@", log: self.logger, type: .error, info.description) + } + } + } else { + os_log("设备选择器显示已触发", log: self.logger, type: .info) } } } - - // 调用内部函数 - createAndShowPicker() } else { - os_log("会话未初始化", log: logger, type: .error) + os_log("会话未初始化,无法显示设备选择器", log: logger, type: .error) } } else { - os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info) + os_log("当前iOS版本不支持AccessorySetupKit", log: logger, type: .info) } #else - os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info) + os_log("构建环境不支持AccessorySetupKit", log: logger, type: .info) #endif } @@ -185,24 +430,133 @@ class BleAccessorySetup: NSObject { func handleDiscoveredAccessory(uuid: UUID) { #if canImport(AccessorySetupKit) && swift(>=5.9) if #available(iOS 18.0, *) { + os_log("处理UUID为%{public}@的发现设备", log: logger, type: .debug, uuid.uuidString) + guard let service = bleService as? BleService else { - os_log("BLE服务未设置", log: logger, type: .error) + os_log("BLE服务未设置或类型转换失败", log: logger, type: .error) return } - // 获取中央管理器 - let centralManager = service.getCentralManager() + os_log("按照iOS 18规范,直接使用UUID调用特定连接方法", log: logger, type: .debug) + service.connectToAccessory(uuid: uuid) + } + #endif + } + + // 从指定的视图控制器显示设备选择器 + func showDevicePickerFromViewController(_ viewController: UIViewController) { + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + os_log("从视图控制器显示设备选择器...", log: logger, type: .debug) - // 通过UUID检索外设 - let peripherals = centralManager.retrievePeripherals(withIdentifiers: [uuid]) + safeShowDevicePickerWithUI(presentingVC: viewController) + } else { + os_log("当前iOS版本不支持AccessorySetupKit", log: logger, type: .info) + } + #else + os_log("构建环境不支持AccessorySetupKit", log: logger, type: .info) + #endif + } + + // 带UI上下文的安全显示选择器 + private func safeShowDevicePickerWithUI(presentingVC: UIViewController) { + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + resetSession() - if let peripheral = peripherals.first { - os_log("连接到UUID对应的设备: %{public}@", log: logger, type: .info, peripheral.name ?? uuid.uuidString) - service.connect(to: peripheral) - } else { - os_log("未找到UUID对应的设备: %{public}@", log: logger, type: .error, uuid.uuidString) + initializeSessionAndRun { [weak self, weak presentingVC] in + guard let self = self, let presentingVC = presentingVC else { return } + + if let wrapper = self.accessorySessionWrapper as? AccessorySessionWrapper { + os_log("会话已激活,准备显示选择器", log: self.logger, type: .info) + + // 从主线程创建和显示 + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + os_log("创建设备描述符...", log: self.logger, type: .debug) + let descriptor = ASDiscoveryDescriptor() + + // 设置服务UUID和制造商ID + // let serviceUUID = "0000abc0-0000-1111-2222-123456789abc" + // os_log("设置服务UUID: %{public}@", log: self.logger, type: .debug, serviceUUID) + // descriptor.bluetoothServiceUUID = CBUUID(string: serviceUUID) + + os_log("设置公司ID: 0xABEF", log: self.logger, type: .debug) + // descriptor.setValue(UInt16(0xABEF), forKey: "bluetoothCompanyIdentifier") + descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(0xEFAB) + + // 创建默认图像 + let deviceImage = UIImage(systemName: "headphones") ?? UIImage() + + // 创建显示项 + let displayItem = ASPickerDisplayItem( + name: "语音助理耳机", + productImage: deviceImage, + descriptor: descriptor + ) + + // 检查权限,但不创建CBCentralManager + os_log("iOS 18环境下不检查蓝牙状态,避免创建CBCentralManager", log: self.logger, type: .debug) + + // 显示选择器 + wrapper.session.showPicker(for: [displayItem]) { [weak self] error in + guard let self = self else { return } + + if let error = error { + os_log("设备选择器显示错误: %{public}@", log: self.logger, type: .error, error.localizedDescription) + + // 错误码解析 + if let nsError = error as? NSError { + os_log("错误域: %{public}@, 错误码: %{public}d", log: self.logger, type: .error, nsError.domain, nsError.code) + + // 解析常见错误 + switch nsError.code { + case 550: + os_log("错误550: CBCentralManager冲突 - 进程中已存在使用全局权限的CBCentralManager", log: self.logger, type: .error) + case 500...599: + os_log("错误%{public}d: 可能是AccessorySetupKit内部错误", log: self.logger, type: .error, nsError.code) + default: + os_log("未知错误码", log: self.logger, type: .error) + } + + if let info = nsError.userInfo as? [String: Any] { + os_log("错误信息: %{public}@", log: self.logger, type: .error, info.description) + } + } + } else { + os_log("设备选择器显示已触发", log: self.logger, type: .info) + } + } + } + } } } #endif } + + // 检查蓝牙是否可用,不使用CBCentralManager + func isBluetoothAvailable() -> Bool { + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + // iOS 18+环境下不进行蓝牙状态检查 + os_log("iOS 18环境下,跳过蓝牙状态检查", log: logger, type: .debug) + return true + } + #endif + + // 使用AVAudioSession检测是否有蓝牙设备连接 + let audioSession = AVAudioSession.sharedInstance() + let outputs = audioSession.currentRoute.outputs + + // 检查是否有蓝牙输出 + for output in outputs { + if output.portType == .bluetoothA2DP || output.portType == .bluetoothHFP || output.portType == .bluetoothLE { + return true + } + } + + // 没有检测到蓝牙设备,但不确定蓝牙是否已打开 + return false + } } \ No newline at end of file diff --git a/local_plugins/ble_service/ios/Classes/BleService.swift b/local_plugins/ble_service/ios/Classes/BleService.swift index a86acc11c..8269289bb 100644 --- a/local_plugins/ble_service/ios/Classes/BleService.swift +++ b/local_plugins/ble_service/ios/Classes/BleService.swift @@ -23,7 +23,7 @@ class BleService: NSObject { } // MARK: - 属性 - private var centralManager: CBCentralManager! + private var centralManager: CBCentralManager? private var peripheralDevice: CBPeripheral? private var writeCharacteristic: CBCharacteristic? private var notifyCharacteristic: CBCharacteristic? @@ -39,6 +39,27 @@ class BleService: NSObject { private override init() { super.init() + // iOS 18+ 下不在初始化阶段创建 CBCentralManager + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + os_log("iOS 18+: 延迟初始化CBCentralManager,等待AccessorySetupKit完成", log: logger, type: .info) + } else { + initializeCentralManager() + } + #else + initializeCentralManager() + #endif + } + + // 延迟初始化中央管理器 + private func initializeCentralManager() { + // 防止重复初始化 + if centralManager != nil { + return + } + + os_log("正在初始化CBCentralManager", log: logger, type: .info) + // 初始化中央管理器,提供恢复标识以支持后台恢复 let options: [String: Any] = [ CBCentralManagerOptionRestoreIdentifierKey: "BLEAssistantCentral", @@ -55,14 +76,30 @@ class BleService: NSObject { self.delegate = delegate } - /// 获取中央管理器 + /// 获取中央管理器,如果未初始化则先初始化 func getCentralManager() -> CBCentralManager { - return centralManager + if centralManager == nil { + initializeCentralManager() + } + return centralManager! } /// 开始扫描BLE设备 func startScan() { - guard centralManager.state == .poweredOn else { + // iOS 18+ 需要处理AccessorySetupKit兼容性 + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + os_log("iOS 18+环境: 不进行传统扫描,请使用AccessorySetupKit配对", log: logger, type: .info) + return + } + #endif + + // iOS 17及以下版本继续使用传统扫描 + if centralManager == nil { + initializeCentralManager() + } + + guard let manager = centralManager, manager.state == .poweredOn else { os_log("蓝牙未打开", log: logger, type: .error) return } @@ -75,7 +112,7 @@ class BleService: NSObject { // 扫描特定服务的设备 let serviceUUIDs = [BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID] - centralManager.scanForPeripherals(withServices: serviceUUIDs, options: [ + manager.scanForPeripherals(withServices: serviceUUIDs, options: [ CBCentralManagerScanOptionAllowDuplicatesKey: false ]) @@ -84,8 +121,8 @@ class BleService: NSObject { /// 停止扫描 func stopScan() { - if isScanning { - centralManager.stopScan() + if isScanning && centralManager != nil { + centralManager?.stopScan() isScanning = false os_log("停止扫描BLE设备", log: logger, type: .info) } @@ -98,11 +135,21 @@ class BleService: NSObject { return } + // 确保中央管理器已初始化 + if centralManager == nil { + initializeCentralManager() + } + + guard let manager = centralManager else { + os_log("蓝牙管理器未初始化", log: logger, type: .error) + return + } + updateConnectionState(BleConst.STATE_CONNECTING) peripheralDevice = peripheral peripheralDevice?.delegate = self - centralManager.connect(peripheral, options: [ + manager.connect(peripheral, options: [ CBConnectPeripheralOptionNotifyOnConnectionKey: true, CBConnectPeripheralOptionNotifyOnDisconnectionKey: true, CBConnectPeripheralOptionNotifyOnNotificationKey: true @@ -117,8 +164,10 @@ class BleService: NSObject { return } - updateConnectionState(BleConst.STATE_DISCONNECTING) - centralManager.cancelPeripheralConnection(peripheral) + if centralManager != nil { + updateConnectionState(BleConst.STATE_DISCONNECTING) + centralManager?.cancelPeripheralConnection(peripheral) + } } /// 检查是否已连接 @@ -276,6 +325,38 @@ class BleService: NSObject { os_log("未知的通知命令: %{public}d", log: logger, type: .debug, cmd) } } + + // MARK: - AccessorySetupKit + + /// 在接收到AccessorySetupKit的accessoryAdded事件后调用 + func connectToAccessory(uuid: UUID) { + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + // 确保CBCentralManager已初始化 + if centralManager == nil { + initializeCentralManager() + } + + os_log("通过AccessorySetupKit连接设备: %{public}@", log: logger, type: .info, uuid.uuidString) + + // 检查centralManager状态 + guard let manager = centralManager, manager.state == .poweredOn else { + os_log("CBCentralManager尚未准备好或状态不是poweredOn: %d", log: logger, type: .error, centralManager?.state.rawValue ?? -1) + return + } + + // 使用retrievePeripherals而不是扫描 + let peripherals = manager.retrievePeripherals(withIdentifiers: [uuid]) + + if let peripheral = peripherals.first { + os_log("找到AccessorySetupKit配对的设备,尝试连接", log: logger, type: .info) + connect(to: peripheral) + } else { + os_log("未找到对应UUID的设备: %{public}@", log: logger, type: .error, uuid.uuidString) + } + } + #endif + } } // MARK: - CBCentralManagerDelegate diff --git a/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift b/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift index 457118fea..d4a3444b1 100644 --- a/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift +++ b/local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift @@ -9,21 +9,72 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { private var methodChannel: FlutterMethodChannel private var dataEventSink: FlutterEventSink? private var statusEventSink: FlutterEventSink? + private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleServicePlugin") + + // 标记是否已初始化所有服务 + private var servicesInitialized = false // MARK: - 初始化方法 init(_ channel: FlutterMethodChannel) { self.methodChannel = channel super.init() + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + // iOS 18环境下,先只初始化AccessorySetup + os_log("iOS 18环境: 延迟BleService初始化,先初始化AccessorySetup", log: logger, type: .info) + let bleAccessorySetup = BleAccessorySetup.shared + + // 在accessoryAdded回调中延迟设置BleService + bleAccessorySetup.setDeviceDiscoveredCallback { [weak self] uuid in + // 确保服务已初始化 + self?.ensureServicesInitialized() + // 然后处理发现的设备 + self?.handleDiscoveredDevice(uuid: uuid) + } + + // 初始化发现会话 + bleAccessorySetup.initializeDiscoverySession() + } else { + // 低于iOS 18,直接初始化所有服务 + initializeAllServices() + } + #else + // 非AccessorySetupKit环境,直接初始化所有服务 + initializeAllServices() + #endif + } + + // 确保所有服务都已初始化 + private func ensureServicesInitialized() { + if !servicesInitialized { + os_log("按需初始化BleService和相关服务", log: logger, type: .info) + + // 设置BLE服务回调 + BleService.shared.setDelegate(self) + + // 设置BleService到AccessorySetup + BleAccessorySetup.shared.setBleService(BleService.shared) + + servicesInitialized = true + } + } + + // 初始化所有服务(iOS 17及以下版本的标准行为) + private func initializeAllServices() { + os_log("标准初始化所有服务", log: logger, type: .info) + // 设置BLE服务回调 BleService.shared.setDelegate(self) - // 初始化AccessorySetup - iOS 18相关代码 + // 初始化AccessorySetup BleAccessorySetup.shared.setBleService(BleService.shared) BleAccessorySetup.shared.setDeviceDiscoveredCallback { [weak self] uuid in self?.handleDiscoveredDevice(uuid: uuid) } BleAccessorySetup.shared.initializeDiscoverySession() + + servicesInitialized = true } // MARK: - FlutterPlugin Protocol @@ -43,9 +94,22 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case "isConnected": + // 确保服务已初始化 + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + ensureServicesInitialized() + } + #endif result(BleService.shared.isConnected()) case "getBatteryLevel": + // 确保服务已初始化 + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + ensureServicesInitialized() + } + #endif + // 获取电池信息,通过回调返回结果 if BleService.shared.getBatteryInfo() { result(0) // 通过回调返回实际值 @@ -84,46 +148,102 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { result(true) case "getDeviceVersion": + // 确保服务已初始化 + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + ensureServicesInitialized() + } + #endif result(BleService.shared.getDeviceVersion()) case "getProductId": + // 确保服务已初始化 + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + ensureServicesInitialized() + } + #endif result(BleService.shared.getProductId()) case "getBatteryInfo": + // 确保服务已初始化 + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + ensureServicesInitialized() + } + #endif result(BleService.shared.getBatteryInfo()) case "openEncoder": + // 确保服务已初始化 + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + ensureServicesInitialized() + } + #endif result(BleService.shared.openEncoder()) case "openDecoder": + // 确保服务已初始化 + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + ensureServicesInitialized() + } + #endif result(BleService.shared.openDecoder()) case "openA2DPDecoder": + // 确保服务已初始化 + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + ensureServicesInitialized() + } + #endif result(BleService.shared.openA2DPDecoder()) case "closeCodec": + // 确保服务已初始化 + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + ensureServicesInitialized() + } + #endif result(BleService.shared.closeCodec()) case "playA2DP": + // 确保服务已初始化 + #if canImport(AccessorySetupKit) && swift(>=5.9) + if #available(iOS 18.0, *) { + ensureServicesInitialized() + } + #endif result(BleService.shared.playA2DP()) // iOS 18+ AccessorySetupKit相关方法 case "startDeviceAssociation": #if canImport(AccessorySetupKit) && swift(>=5.9) if #available(iOS 18.0, *) { - // 调用AccessorySetupKit的设备选择器 - BleAccessorySetup.shared.showDevicePicker() + // 从主线程调用 + DispatchQueue.main.async { + // 获取当前活跃的视图控制器 + if let rootVC = UIApplication.shared.windows.first?.rootViewController { + let topVC = self.getTopViewController(rootVC) + + os_log("尝试从顶层视图控制器显示设备选择器: %{public}@", log: self.logger, type: .debug, String(describing: topVC)) + // 调用带UI上下文的方法 + BleAccessorySetup.shared.showDevicePickerFromViewController(topVC) + } else { + os_log("无法获取顶层视图控制器,使用安全方法", log: self.logger, type: .error) + BleAccessorySetup.shared.safeShowDevicePicker() + } + } result(true) } else { - os_log("iOS版本低于18.0,不支持AccessorySetupKit", - log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), - type: .info) + os_log("iOS版本低于18.0,不支持AccessorySetupKit", log: self.logger, type: .info) result(true) } #else - os_log("环境不支持AccessorySetupKit,模拟成功以继续流程", - log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"), - type: .info) + os_log("环境不支持AccessorySetupKit,模拟成功以继续流程", log: self.logger, type: .info) result(true) #endif @@ -146,6 +266,25 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin { } } } + + // 获取顶层视图控制器的辅助方法 + private func getTopViewController(_ viewController: UIViewController) -> UIViewController { + if let presented = viewController.presentedViewController { + return getTopViewController(presented) + } + + if let tabBarController = viewController as? UITabBarController, + let selected = tabBarController.selectedViewController { + return getTopViewController(selected) + } + + if let navigationController = viewController as? UINavigationController, + let visibleViewController = navigationController.visibleViewController { + return getTopViewController(visibleViewController) + } + + return viewController + } } // MARK: - FlutterStreamHandler diff --git a/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift b/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift index c9f8f0cb9..1e1ecc147 100644 --- a/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift +++ b/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift @@ -1,5 +1,4 @@ import Foundation -import CoreBluetooth import AVFoundation /// 蓝牙事件类型 @@ -16,7 +15,6 @@ enum BluetoothState: String { case on = "on" case unauthorized = "unauthorized" case unsupported = "unsupported" - case resetting = "resetting" } /// 简化的蓝牙助手类,专注于iOS上的蓝牙耳机检测 @@ -24,91 +22,35 @@ 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() - initializeCentralManagerIfNeeded() - } - + deinit { stopBluetoothStateMonitoring() } - - /// 延迟初始化CBCentralManager,只在实际需要时创建 - private func initializeCentralManagerIfNeeded() { - if centralManager == nil { - // 移除 ShowPowerAlertKey 选项,避免不必要的系统弹窗 - centralManager = CBCentralManager( - delegate: self, - queue: DispatchQueue.main - ) - } - } - - /// 检查蓝牙是否启用,此方法会触发权限请求 + + /// 检查蓝牙是否启用(通过检查是否有连接的蓝牙音频设备) func isBluetoothEnabled() -> Bool { - // 此方法调用会触发权限申请 - initializeCentralManagerIfNeeded() - - guard let central = centralManager else { - print("检查蓝牙状态失败:centralManager未初始化") - return false - } - - // 如果状态是 .unknown,说明蓝牙管理器刚初始化,尚未收到系统回调 - // 此时应该返回一个中性值,避免错误地显示"蓝牙已关闭" - if central.state == .unknown || central.state == .resetting { - print("蓝牙状态检查结果: 初始化中,等待系统回调") - // 返回 true 避免误报蓝牙关闭 + // 检查是否有连接的蓝牙音频设备 + let hasBtDevices = !getConnectedAudioDevices().isEmpty + + // 如果有连接的蓝牙设备,则认为蓝牙是开启状态 + if hasBtDevices { + print("蓝牙状态检查: 已检测到蓝牙设备,蓝牙已开启") return true } - - // 检查蓝牙状态并记录 - 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 + + // 如果没有连接设备,返回false (无法100%确定蓝牙是否打开,只能知道没有连接设备) + print("蓝牙状态检查: 未检测到蓝牙设备") + return false } - - /// 获取已连接的蓝牙耳机,此方法会触发权限请求 + + /// 获取已连接的蓝牙耳机 func getConnectedDevices(completion: @escaping ([[String: String]], Error?) -> Void) { - // 确保蓝牙已初始化 - initializeCentralManagerIfNeeded() - // 获取音频设备(主要是蓝牙耳机) let audioDevices = getConnectedAudioDevices() - + // 日志输出 if !audioDevices.isEmpty { print("已找到\(audioDevices.count)个蓝牙音频设备:") @@ -118,17 +60,17 @@ class ClassicBluetoothHelper: NSObject { } 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] = [ @@ -137,15 +79,15 @@ class ClassicBluetoothHelper: NSObject { ] 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" 的后缀 @@ -154,17 +96,14 @@ class ClassicBluetoothHelper: NSObject { } 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( @@ -174,12 +113,15 @@ class ClassicBluetoothHelper: NSObject { ) { [weak self] notification in self?.handleAudioRouteChange(notification) } - + // 激活音频会话 activateAudioSession() + + // 报告当前状态 + reportCurrentBluetoothState() } } - + // 处理音频路由变化 private func handleAudioRouteChange(_ notification: Notification) { guard let userInfo = notification.userInfo, @@ -187,7 +129,7 @@ class ClassicBluetoothHelper: NSObject { let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { return } - + switch reason { case .newDeviceAvailable: // 新音频设备已连接 @@ -195,7 +137,12 @@ class ClassicBluetoothHelper: NSObject { for device in audioDevices { stateCallback?(.deviceConnected, ["device": device]) } - + + // 如果有蓝牙设备连接,报告蓝牙状态为开启 + if !audioDevices.isEmpty { + reportBluetoothState(.on) + } + case .oldDeviceUnavailable: // 获取断开之前的路由 if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription { @@ -206,13 +153,20 @@ class ClassicBluetoothHelper: NSObject { ] stateCallback?(.deviceDisconnected, ["device": device]) } + + // 检查当前是否还有蓝牙设备 + let currentDevices = getConnectedAudioDevices() + if currentDevices.isEmpty { + // 如果没有任何蓝牙设备连接,可以认为蓝牙状态变为未知 + reportBluetoothState(.unknown) + } } - + default: break } } - + // 激活音频会话 private func activateAudioSession() { do { @@ -223,26 +177,26 @@ class ClassicBluetoothHelper: NSObject { 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 { @@ -251,76 +205,24 @@ class ClassicBluetoothHelper: NSObject { print("关闭音频会话失败: \(error.localizedDescription)") } } - + /// 报告当前蓝牙状态 func reportCurrentBluetoothState() { - // 如果centralManager未初始化,不触发状态报告 - guard let central = centralManager else { return } - - // 如果状态是 .unknown 或 .resetting,暂不向 Flutter 端报告 - if central.state == .unknown || central.state == .resetting { - print("蓝牙状态: 临时状态(.unknown/.resetting),暂不报告") - return + // 检查当前是否有蓝牙设备连接 + let devices = getConnectedAudioDevices() + + if !devices.isEmpty { + print("蓝牙状态: 已检测到蓝牙设备,报告状态为开启") + reportBluetoothState(.on) + } else { + print("蓝牙状态: 未检测到蓝牙设备,报告状态为未知") + reportBluetoothState(.unknown) } - - // 直接调用delegate方法保持状态处理一致性 - centralManagerDidUpdateState(central) } -} - -// MARK: - CBCentralManagerDelegate -extension ClassicBluetoothHelper: CBCentralManagerDelegate { - func centralManagerDidUpdateState(_ central: CBCentralManager) { - // 添加详细日志输出 - print("iOS蓝牙状态原始值: \(central.state.rawValue)") - - // 如果状态是 .unknown 或 .resetting,暂不向 Flutter 端报告 - // 这些是临时状态,等待系统确定真正的状态后再报告 - guard central.state != .unknown && central.state != .resetting else { - print("蓝牙状态: 临时状态(.unknown/.resetting),暂不报告") - return - } - - let state: BluetoothState - - 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 - } - + + // 报告蓝牙状态变化 + private func reportBluetoothState(_ state: BluetoothState) { 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]) + stateCallback?(.stateChanged, ["state": state.rawValue]) } } \ No newline at end of file