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