Browse Source

add

newdev_shunjiawei
wolfplus 1 year ago
parent
commit
eab56368db
  1. 4
      ios/Podfile.lock
  2. 6
      ios/Runner.xcodeproj/project.pbxproj
  3. 67
      ios/Runner/Info.plist
  4. 9
      ios/Runner/Runner.entitlements
  5. 452
      local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift
  6. 101
      local_plugins/ble_service/ios/Classes/BleService.swift
  7. 157
      local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift
  8. 232
      local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift

4
ios/Podfile.lock

@ -381,9 +381,9 @@ SPEC CHECKSUMS:
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
azure_speech: d65fa5c1f04be9cee7303abf4caea30680e814fd azure_speech: d65fa5c1f04be9cee7303abf4caea30680e814fd
ble_service: 3414a81261f88a6e673cbc71178d2353d96b2a1c ble_service: 7987f876505b5c2429303597d7604092d2545566
chat_storage: 933a96144157a07a2fef7f6acb28c78ff18465d8 chat_storage: 933a96144157a07a2fef7f6acb28c78ff18465d8
classic_bluetooth: c5e5c0c86acab8dcd6b8e9cef7fca654c3518034 classic_bluetooth: c146e1618e867a47fa7ab79a3bf262e7d69d557d
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60

6
ios/Runner.xcodeproj/project.pbxproj

@ -481,7 +481,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@ -617,7 +617,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -669,7 +669,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;

67
ios/Runner/Info.plist

@ -41,12 +41,14 @@
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>UIBackgroundModes</key> <key>FacebookAdvertiserIDCollectionEnabled</key>
<array> <true/>
<string>bluetooth-central</string> <key>FacebookAppID</key>
<string>bluetooth-peripheral</string> <string>3829022037361855</string>
<string>external-accessory</string> <key>FacebookAutoLogAppEventsEnabled</key>
</array> <true/>
<key>FacebookClientToken</key>
<string>193d0a2b1757c41cfa557ea8e8f21c33</string>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>
<array> <array>
<string>weixin</string> <string>weixin</string>
@ -70,6 +72,16 @@
</array> </array>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSAccessorySetupBluetoothCompanyIdentifiers</key>
<array>
<string>EFAB</string>
<string>ABEF</string>
</array>
<key>NSAccessorySetupKitSupports</key>
<array>
<string>Bluetooth</string>
</array>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
@ -77,8 +89,24 @@
<key>NSAllowsArbitraryLoadsInWebContent</key> <key>NSAllowsArbitraryLoadsInWebContent</key>
<true/> <true/>
</dict> </dict>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限用于连接和管理音频设备</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要蓝牙权限用于连接和管理音频设备</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>需要位置权限用于搜索附近的蓝牙设备</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>需要位置权限用于搜索附近的蓝牙设备</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要麦克风权限用于语音识别和录音功能</string>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
<string>external-accessory</string>
</array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
@ -96,32 +124,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限用于连接和管理音频设备</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要蓝牙权限用于连接和管理音频设备</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>需要位置权限用于搜索附近的蓝牙设备</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>需要位置权限用于搜索附近的蓝牙设备</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要麦克风权限用于语音识别和录音功能</string>
<key>FacebookClientToken</key>
<string>193d0a2b1757c41cfa557ea8e8f21c33</string>
<key>FacebookAppID</key>
<string>3829022037361855</string>
<key>FacebookAdvertiserIDCollectionEnabled</key>
<true/>
<key>FacebookAutoLogAppEventsEnabled</key>
<true/>
<key>NSAccessorySetupKitSupports</key>
<array>
<string>Bluetooth</string>
<string>WiFi</string>
</array>
<key>NSAccessorySetupBluetoothCompanyIdentifiers</key>
<array>
<string>ABEF</string>
</array>
</dict> </dict>
</plist> </plist>

9
ios/Runner/Runner.entitlements

@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict/> <dict>
<key>com.apple.developer.networking.wifi-info</key>
<true/>
<key>com.apple.external-accessory.wireless-configuration</key>
<true/>
<key>com.apple.developer.homekit</key>
<true/>
</dict>
</plist> </plist>

452
local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift

@ -1,6 +1,8 @@
import Foundation import Foundation
import CoreBluetooth import CoreBluetooth
import os.log import os.log
import ObjectiveC // 添加ObjC运行时支持
import AVFoundation
// 条件编译只导入在iOS 18+上可用的框架 // 条件编译只导入在iOS 18+上可用的框架
#if canImport(AccessorySetupKit) && swift(>=5.9) #if canImport(AccessorySetupKit) && swift(>=5.9)
@ -21,23 +23,51 @@ class BleAccessorySetup: NSObject {
@available(iOS 18.0, *) @available(iOS 18.0, *)
class AccessorySessionWrapper { class AccessorySessionWrapper {
var session: ASAccessorySession var session: ASAccessorySession
var isActivated = false
init() throws { init() throws {
do { do {
os_log("尝试初始化ASAccessorySession...", log: BleAccessorySetup.shared.logger, type: .debug)
// 根据WWDC24示例,使用无参初始化
self.session = try ASAccessorySession() self.session = try ASAccessorySession()
os_log("ASAccessorySession初始化成功", log: BleAccessorySetup.shared.logger, type: .info)
} catch { } 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 throw error
} }
} }
} }
var accessorySessionWrapper: Any? var accessorySessionWrapper: Any?
// 添加初始化完成回调
private var onSessionActivated: (() -> Void)?
typealias DeviceDiscoveredCallback = (UUID) -> Void typealias DeviceDiscoveredCallback = (UUID) -> Void
private var onDeviceDiscovered: DeviceDiscoveredCallback? private var onDeviceDiscovered: DeviceDiscoveredCallback?
#endif #endif
private var isInitializing = false
private override init() { private override init() {
super.init() super.init()
} }
@ -45,6 +75,7 @@ class BleAccessorySetup: NSObject {
// 设置BLE服务实例 // 设置BLE服务实例
func setBleService(_ service: Any) { func setBleService(_ service: Any) {
self.bleService = service self.bleService = service
os_log("BLE服务实例已设置", log: logger, type: .info)
} }
// 设置设备发现回调 // 设置设备发现回调
@ -52,6 +83,7 @@ class BleAccessorySetup: NSObject {
#if canImport(AccessorySetupKit) && swift(>=5.9) #if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) { if #available(iOS 18.0, *) {
self.onDeviceDiscovered = callback self.onDeviceDiscovered = callback
os_log("设备发现回调已设置", log: logger, type: .debug)
} }
#endif #endif
} }
@ -60,29 +92,77 @@ class BleAccessorySetup: NSObject {
func initializeDiscoverySession() { func initializeDiscoverySession() {
#if canImport(AccessorySetupKit) && swift(>=5.9) #if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) { 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配置 isInitializing = true
let plistCheck = Bundle.main.object(forInfoDictionaryKey: "NSAccessorySetupKitSupports") os_log("初始化iOS 18 AccessorySetupKit会话...", log: logger, type: .debug)
os_log("Info.plist NSAccessorySetupKitSupports: %{public}@", log: logger, type: .info, String(describing: plistCheck))
// 检查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) // 创建会话 (使用try-catch)
do { do {
os_log("尝试创建AccessorySessionWrapper...", log: logger, type: .debug)
accessorySessionWrapper = try AccessorySessionWrapper() accessorySessionWrapper = try AccessorySessionWrapper()
os_log("AccessorySessionWrapper创建成功", log: logger, type: .info)
// 激活会话并设置事件处理 // 激活会话并设置事件处理
if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper { if let wrapper = accessorySessionWrapper as? AccessorySessionWrapper {
os_log("尝试激活ASAccessorySession...", log: logger, type: .debug)
wrapper.session.activate(on: .main) { [weak self] event in 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 { switch event.eventType {
case .activated: case .activated:
os_log("AccessorySetupKit会话已激活", log: self.logger, type: .info) 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: case .accessoryAdded:
if let accessory = event.accessory, let uuid = accessory.bluetoothIdentifier { if let accessory = event.accessory, let uuid = accessory.bluetoothIdentifier {
os_log("发现新设备: %{public}@", log: self.logger, type: .info, accessory.displayName) os_log("发现新设备: %{public}@", log: self.logger, type: .info, accessory.displayName)
self.onDeviceDiscovered?(uuid) self.onDeviceDiscovered?(uuid)
} else {
os_log("设备添加事件但缺少设备信息或UUID", log: self.logger, type: .debug)
} }
case .accessoryRemoved: case .accessoryRemoved:
@ -100,6 +180,8 @@ class BleAccessorySetup: NSObject {
case .pickerSetupFailed: case .pickerSetupFailed:
if let error = event.error { if let error = event.error {
os_log("会话出错: %{public}@", log: self.logger, type: .error, error.localizedDescription) os_log("会话出错: %{public}@", log: self.logger, type: .error, error.localizedDescription)
} else {
os_log("会话出错但无错误详情", log: self.logger, type: .debug)
} }
default: default:
@ -108,76 +190,239 @@ class BleAccessorySetup: NSObject {
} }
// 检查现有授权设备 // 检查现有授权设备
if !wrapper.session.accessories.isEmpty { let accessoriesCount = wrapper.session.accessories.count
os_log("发现%{public}d个已授权设备", log: logger, type: .info, wrapper.session.accessories.count) os_log("发现%{public}d个已授权设备", log: logger, type: .info, accessoriesCount)
if accessoriesCount > 0 {
// 如果有现有设备,触发回调 // 如果有现有设备,触发回调
for accessory in wrapper.session.accessories { for accessory in wrapper.session.accessories {
if let uuid = accessory.bluetoothIdentifier { if let uuid = accessory.bluetoothIdentifier {
os_log("处理已授权设备: %{public}@", log: logger, type: .info, accessory.displayName) os_log("处理已授权设备: %{public}@", log: logger, type: .info, accessory.displayName)
self.onDeviceDiscovered?(uuid) self.onDeviceDiscovered?(uuid)
} else {
os_log("已授权设备缺少UUID: %{public}@", log: logger, type: .debug, accessory.displayName)
} }
} }
} }
} else {
os_log("accessorySessionWrapper类型转换失败", log: logger, type: .error)
} }
} catch { } 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 { } 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 #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 #endif
} }
// 安全显示设备选择器
func safeShowDevicePicker() {
// 重置会话状态,确保干净的重新初始化
resetSession()
initializeSessionAndRun { [weak self] in
guard let self = self else { return }
self.showDevicePicker()
}
}
// 显示设备选择器 // 显示设备选择器
func showDevicePicker() { func showDevicePicker() {
#if canImport(AccessorySetupKit) && swift(>=5.9) #if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) { 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 { 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 // 设置制造商ID
@available(iOS 18.0, *) os_log("设置公司ID: 0xABEF", log: logger, type: .debug)
func createAndShowPicker() { // descriptor.setValue(UInt16(0xABEF), forKey: "bluetoothCompanyIdentifier")
// 根据WWDC24视频,正确的类名是ASDiscoveryDescriptor descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(0xEFAB)
let descriptor = ASDiscoveryDescriptor() // 创建默认图像
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 }
// 设置过滤条件 os_log("调用showPicker方法...", log: self.logger, type: .debug)
// 使用UInt16格式的公司标识符
descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(UInt16(0xABEF)) // BleConst.MANUFACTURER_ID
descriptor.bluetoothServiceUUID = CBUUID(string: "0000abc0-0000-1111-2222-123456789abc") // BleConst.PRIMARY_SERVICE_UUID
// 创建默认图像以防系统图标不可用 // 再次检查Info.plist配置
let headphonesImage = UIImage(systemName: "headphones") ?? UIImage() 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 os_log("iOS 18环境下不检查蓝牙状态,避免创建CBCentralManager", log: self.logger, type: .debug)
let displayItem = ASPickerDisplayItem(
name: "语音助理耳机",
productImage: headphonesImage,
descriptor: descriptor
)
// 显示选择器 // 使用标准方法显示选择器
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 { if let error = error {
// 修复OSLog的错误用法
os_log("设备选择器显示错误: %{public}@", log: self.logger, type: .error, error.localizedDescription) 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 { } else {
os_log("会话未初始化", log: logger, type: .error) os_log("会话未初始化,无法显示设备选择器", log: logger, type: .error)
} }
} else { } else {
os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info) os_log("当前iOS版本不支持AccessorySetupKit", log: logger, type: .info)
} }
#else #else
os_log("iOS 18 AccessorySetupKit不可用", log: logger, type: .info) os_log("构建环境不支持AccessorySetupKit", log: logger, type: .info)
#endif #endif
} }
@ -185,24 +430,133 @@ class BleAccessorySetup: NSObject {
func handleDiscoveredAccessory(uuid: UUID) { func handleDiscoveredAccessory(uuid: UUID) {
#if canImport(AccessorySetupKit) && swift(>=5.9) #if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) { if #available(iOS 18.0, *) {
os_log("处理UUID为%{public}@的发现设备", log: logger, type: .debug, uuid.uuidString)
guard let service = bleService as? BleService else { guard let service = bleService as? BleService else {
os_log("BLE服务未设置", log: logger, type: .error) os_log("BLE服务未设置或类型转换失败", log: logger, type: .error)
return return
} }
// 获取中央管理器 os_log("按照iOS 18规范,直接使用UUID调用特定连接方法", log: logger, type: .debug)
let centralManager = service.getCentralManager() 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检索外设 safeShowDevicePickerWithUI(presentingVC: viewController)
let peripherals = centralManager.retrievePeripherals(withIdentifiers: [uuid]) } 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 { initializeSessionAndRun { [weak self, weak presentingVC] in
os_log("连接到UUID对应的设备: %{public}@", log: logger, type: .info, peripheral.name ?? uuid.uuidString) guard let self = self, let presentingVC = presentingVC else { return }
service.connect(to: peripheral)
} else { if let wrapper = self.accessorySessionWrapper as? AccessorySessionWrapper {
os_log("未找到UUID对应的设备: %{public}@", log: logger, type: .error, uuid.uuidString) 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 #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
}
} }

101
local_plugins/ble_service/ios/Classes/BleService.swift

@ -23,7 +23,7 @@ class BleService: NSObject {
} }
// MARK: - 属性 // MARK: - 属性
private var centralManager: CBCentralManager! private var centralManager: CBCentralManager?
private var peripheralDevice: CBPeripheral? private var peripheralDevice: CBPeripheral?
private var writeCharacteristic: CBCharacteristic? private var writeCharacteristic: CBCharacteristic?
private var notifyCharacteristic: CBCharacteristic? private var notifyCharacteristic: CBCharacteristic?
@ -39,6 +39,27 @@ class BleService: NSObject {
private override init() { private override init() {
super.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] = [ let options: [String: Any] = [
CBCentralManagerOptionRestoreIdentifierKey: "BLEAssistantCentral", CBCentralManagerOptionRestoreIdentifierKey: "BLEAssistantCentral",
@ -55,14 +76,30 @@ class BleService: NSObject {
self.delegate = delegate self.delegate = delegate
} }
/// 获取中央管理器 /// 获取中央管理器,如果未初始化则先初始化
func getCentralManager() -> CBCentralManager { func getCentralManager() -> CBCentralManager {
return centralManager if centralManager == nil {
initializeCentralManager()
}
return centralManager!
} }
/// 开始扫描BLE设备 /// 开始扫描BLE设备
func startScan() { 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) os_log("蓝牙未打开", log: logger, type: .error)
return return
} }
@ -75,7 +112,7 @@ class BleService: NSObject {
// 扫描特定服务的设备 // 扫描特定服务的设备
let serviceUUIDs = [BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID] let serviceUUIDs = [BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID]
centralManager.scanForPeripherals(withServices: serviceUUIDs, options: [ manager.scanForPeripherals(withServices: serviceUUIDs, options: [
CBCentralManagerScanOptionAllowDuplicatesKey: false CBCentralManagerScanOptionAllowDuplicatesKey: false
]) ])
@ -84,8 +121,8 @@ class BleService: NSObject {
/// 停止扫描 /// 停止扫描
func stopScan() { func stopScan() {
if isScanning { if isScanning && centralManager != nil {
centralManager.stopScan() centralManager?.stopScan()
isScanning = false isScanning = false
os_log("停止扫描BLE设备", log: logger, type: .info) os_log("停止扫描BLE设备", log: logger, type: .info)
} }
@ -98,11 +135,21 @@ class BleService: NSObject {
return return
} }
// 确保中央管理器已初始化
if centralManager == nil {
initializeCentralManager()
}
guard let manager = centralManager else {
os_log("蓝牙管理器未初始化", log: logger, type: .error)
return
}
updateConnectionState(BleConst.STATE_CONNECTING) updateConnectionState(BleConst.STATE_CONNECTING)
peripheralDevice = peripheral peripheralDevice = peripheral
peripheralDevice?.delegate = self peripheralDevice?.delegate = self
centralManager.connect(peripheral, options: [ manager.connect(peripheral, options: [
CBConnectPeripheralOptionNotifyOnConnectionKey: true, CBConnectPeripheralOptionNotifyOnConnectionKey: true,
CBConnectPeripheralOptionNotifyOnDisconnectionKey: true, CBConnectPeripheralOptionNotifyOnDisconnectionKey: true,
CBConnectPeripheralOptionNotifyOnNotificationKey: true CBConnectPeripheralOptionNotifyOnNotificationKey: true
@ -117,8 +164,10 @@ class BleService: NSObject {
return return
} }
updateConnectionState(BleConst.STATE_DISCONNECTING) if centralManager != nil {
centralManager.cancelPeripheralConnection(peripheral) updateConnectionState(BleConst.STATE_DISCONNECTING)
centralManager?.cancelPeripheralConnection(peripheral)
}
} }
/// 检查是否已连接 /// 检查是否已连接
@ -276,6 +325,38 @@ class BleService: NSObject {
os_log("未知的通知命令: %{public}d", log: logger, type: .debug, cmd) 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 // MARK: - CBCentralManagerDelegate

157
local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift

@ -9,21 +9,72 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
private var methodChannel: FlutterMethodChannel private var methodChannel: FlutterMethodChannel
private var dataEventSink: FlutterEventSink? private var dataEventSink: FlutterEventSink?
private var statusEventSink: FlutterEventSink? private var statusEventSink: FlutterEventSink?
private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleServicePlugin")
// 标记是否已初始化所有服务
private var servicesInitialized = false
// MARK: - 初始化方法 // MARK: - 初始化方法
init(_ channel: FlutterMethodChannel) { init(_ channel: FlutterMethodChannel) {
self.methodChannel = channel self.methodChannel = channel
super.init() 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服务回调 // 设置BLE服务回调
BleService.shared.setDelegate(self) BleService.shared.setDelegate(self)
// 初始化AccessorySetup - iOS 18相关代码 // 初始化AccessorySetup
BleAccessorySetup.shared.setBleService(BleService.shared) BleAccessorySetup.shared.setBleService(BleService.shared)
BleAccessorySetup.shared.setDeviceDiscoveredCallback { [weak self] uuid in BleAccessorySetup.shared.setDeviceDiscoveredCallback { [weak self] uuid in
self?.handleDiscoveredDevice(uuid: uuid) self?.handleDiscoveredDevice(uuid: uuid)
} }
BleAccessorySetup.shared.initializeDiscoverySession() BleAccessorySetup.shared.initializeDiscoverySession()
servicesInitialized = true
} }
// MARK: - FlutterPlugin Protocol // MARK: - FlutterPlugin Protocol
@ -43,9 +94,22 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method { switch call.method {
case "isConnected": case "isConnected":
// 确保服务已初始化
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
result(BleService.shared.isConnected()) result(BleService.shared.isConnected())
case "getBatteryLevel": case "getBatteryLevel":
// 确保服务已初始化
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
// 获取电池信息,通过回调返回结果 // 获取电池信息,通过回调返回结果
if BleService.shared.getBatteryInfo() { if BleService.shared.getBatteryInfo() {
result(0) // 通过回调返回实际值 result(0) // 通过回调返回实际值
@ -84,46 +148,102 @@ public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
result(true) result(true)
case "getDeviceVersion": case "getDeviceVersion":
// 确保服务已初始化
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
result(BleService.shared.getDeviceVersion()) result(BleService.shared.getDeviceVersion())
case "getProductId": case "getProductId":
// 确保服务已初始化
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
result(BleService.shared.getProductId()) result(BleService.shared.getProductId())
case "getBatteryInfo": case "getBatteryInfo":
// 确保服务已初始化
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
result(BleService.shared.getBatteryInfo()) result(BleService.shared.getBatteryInfo())
case "openEncoder": case "openEncoder":
// 确保服务已初始化
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
result(BleService.shared.openEncoder()) result(BleService.shared.openEncoder())
case "openDecoder": case "openDecoder":
// 确保服务已初始化
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
result(BleService.shared.openDecoder()) result(BleService.shared.openDecoder())
case "openA2DPDecoder": case "openA2DPDecoder":
// 确保服务已初始化
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
result(BleService.shared.openA2DPDecoder()) result(BleService.shared.openA2DPDecoder())
case "closeCodec": case "closeCodec":
// 确保服务已初始化
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
result(BleService.shared.closeCodec()) result(BleService.shared.closeCodec())
case "playA2DP": case "playA2DP":
// 确保服务已初始化
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
result(BleService.shared.playA2DP()) result(BleService.shared.playA2DP())
// iOS 18+ AccessorySetupKit相关方法 // iOS 18+ AccessorySetupKit相关方法
case "startDeviceAssociation": case "startDeviceAssociation":
#if canImport(AccessorySetupKit) && swift(>=5.9) #if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) { 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) result(true)
} else { } else {
os_log("iOS版本低于18.0,不支持AccessorySetupKit", os_log("iOS版本低于18.0,不支持AccessorySetupKit", log: self.logger, type: .info)
log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"),
type: .info)
result(true) result(true)
} }
#else #else
os_log("环境不支持AccessorySetupKit,模拟成功以继续流程", os_log("环境不支持AccessorySetupKit,模拟成功以继续流程", log: self.logger, type: .info)
log: OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleService"),
type: .info)
result(true) result(true)
#endif #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 // MARK: - FlutterStreamHandler

232
local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothHelper.swift

@ -1,5 +1,4 @@
import Foundation import Foundation
import CoreBluetooth
import AVFoundation import AVFoundation
/// 蓝牙事件类型 /// 蓝牙事件类型
@ -16,7 +15,6 @@ enum BluetoothState: String {
case on = "on" case on = "on"
case unauthorized = "unauthorized" case unauthorized = "unauthorized"
case unsupported = "unsupported" case unsupported = "unsupported"
case resetting = "resetting"
} }
/// 简化的蓝牙助手类,专注于iOS上的蓝牙耳机检测 /// 简化的蓝牙助手类,专注于iOS上的蓝牙耳机检测
@ -24,91 +22,35 @@ class ClassicBluetoothHelper: NSObject {
// 状态监控相关 // 状态监控相关
private var isMonitoring = false private var isMonitoring = false
private var stateCallback: ((BluetoothEventType, [String: Any]) -> Void)? private var stateCallback: ((BluetoothEventType, [String: Any]) -> Void)?
// 用于监控蓝牙系统状态,延迟初始化
private var centralManager: CBCentralManager?
// 音频路由变化观察者 // 音频路由变化观察者
private var routeChangeObserver: NSObjectProtocol? private var routeChangeObserver: NSObjectProtocol?
override init() {
super.init()
initializeCentralManagerIfNeeded()
}
deinit { deinit {
stopBluetoothStateMonitoring() stopBluetoothStateMonitoring()
} }
/// 延迟初始化CBCentralManager,只在实际需要时创建 /// 检查蓝牙是否启用(通过检查是否有连接的蓝牙音频设备)
private func initializeCentralManagerIfNeeded() {
if centralManager == nil {
// 移除 ShowPowerAlertKey 选项,避免不必要的系统弹窗
centralManager = CBCentralManager(
delegate: self,
queue: DispatchQueue.main
)
}
}
/// 检查蓝牙是否启用,此方法会触发权限请求
func isBluetoothEnabled() -> Bool { func isBluetoothEnabled() -> Bool {
// 此方法调用会触发权限申请 // 检查是否有连接的蓝牙音频设备
initializeCentralManagerIfNeeded() let hasBtDevices = !getConnectedAudioDevices().isEmpty
guard let central = centralManager else { // 如果有连接的蓝牙设备,则认为蓝牙是开启状态
print("检查蓝牙状态失败:centralManager未初始化") if hasBtDevices {
return false print("蓝牙状态检查: 已检测到蓝牙设备,蓝牙已开启")
}
// 如果状态是 .unknown,说明蓝牙管理器刚初始化,尚未收到系统回调
// 此时应该返回一个中性值,避免错误地显示"蓝牙已关闭"
if central.state == .unknown || central.state == .resetting {
print("蓝牙状态检查结果: 初始化中,等待系统回调")
// 返回 true 避免误报蓝牙关闭
return true return true
} }
// 检查蓝牙状态并记录 // 如果没有连接设备,返回false (无法100%确定蓝牙是否打开,只能知道没有连接设备)
let stateDescription: String print("蓝牙状态检查: 未检测到蓝牙设备")
let isEnabled: Bool return false
switch central.state {
case .poweredOn:
stateDescription = "已开启"
isEnabled = true
case .poweredOff:
stateDescription = "已关闭"
isEnabled = false
case .unauthorized:
stateDescription = "未授权"
isEnabled = false
case .unsupported:
stateDescription = "不支持"
isEnabled = false
case .resetting:
stateDescription = "重置中"
isEnabled = false
case .unknown:
stateDescription = "未知"
isEnabled = false
@unknown default:
stateDescription = "未定义状态"
isEnabled = false
}
print("蓝牙状态检查结果: \(stateDescription), isEnabled: \(isEnabled)")
return isEnabled
} }
/// 获取已连接的蓝牙耳机,此方法会触发权限请求 /// 获取已连接的蓝牙耳机
func getConnectedDevices(completion: @escaping ([[String: String]], Error?) -> Void) { func getConnectedDevices(completion: @escaping ([[String: String]], Error?) -> Void) {
// 确保蓝牙已初始化
initializeCentralManagerIfNeeded()
// 获取音频设备(主要是蓝牙耳机) // 获取音频设备(主要是蓝牙耳机)
let audioDevices = getConnectedAudioDevices() let audioDevices = getConnectedAudioDevices()
// 日志输出 // 日志输出
if !audioDevices.isEmpty { if !audioDevices.isEmpty {
print("已找到\(audioDevices.count)个蓝牙音频设备:") print("已找到\(audioDevices.count)个蓝牙音频设备:")
@ -118,17 +60,17 @@ class ClassicBluetoothHelper: NSObject {
} else { } else {
print("未找到蓝牙音频设备") print("未找到蓝牙音频设备")
} }
completion(audioDevices, nil) completion(audioDevices, nil)
} }
// 获取连接的蓝牙音频设备 // 获取连接的蓝牙音频设备
private func getConnectedAudioDevices() -> [[String: String]] { private func getConnectedAudioDevices() -> [[String: String]] {
var result: [[String: String]] = [] var result: [[String: String]] = []
// 获取当前音频路由 // 获取当前音频路由
let outputs = AVAudioSession.sharedInstance().currentRoute.outputs let outputs = AVAudioSession.sharedInstance().currentRoute.outputs
// 过滤蓝牙相关设备 // 过滤蓝牙相关设备
for output in outputs where isBluetoothOutput(output.portType) { for output in outputs where isBluetoothOutput(output.portType) {
let device: [String: String] = [ let device: [String: String] = [
@ -137,15 +79,15 @@ class ClassicBluetoothHelper: NSObject {
] ]
result.append(device) result.append(device)
} }
return result return result
} }
// 检查是否为蓝牙输出 // 检查是否为蓝牙输出
private func isBluetoothOutput(_ portType: AVAudioSession.Port) -> Bool { private func isBluetoothOutput(_ portType: AVAudioSession.Port) -> Bool {
return portType == .bluetoothA2DP || portType == .bluetoothHFP || portType == .bluetoothLE return portType == .bluetoothA2DP || portType == .bluetoothHFP || portType == .bluetoothLE
} }
// 清理设备地址 // 清理设备地址
private func cleanDeviceAddress(_ address: String) -> String { private func cleanDeviceAddress(_ address: String) -> String {
// 移除类似 "-tacl" 的后缀 // 移除类似 "-tacl" 的后缀
@ -154,17 +96,14 @@ class ClassicBluetoothHelper: NSObject {
} }
return address return address
} }
/// 开始监听蓝牙状态变化 /// 开始监听蓝牙状态变化
func startBluetoothStateMonitoring(callback: @escaping (BluetoothEventType, [String: Any]) -> Void) { func startBluetoothStateMonitoring(callback: @escaping (BluetoothEventType, [String: Any]) -> Void) {
self.stateCallback = callback self.stateCallback = callback
if !isMonitoring { if !isMonitoring {
isMonitoring = true isMonitoring = true
// 初始化蓝牙管理器
initializeCentralManagerIfNeeded()
// 观察音频路由变化 // 观察音频路由变化
let notificationCenter = NotificationCenter.default let notificationCenter = NotificationCenter.default
routeChangeObserver = notificationCenter.addObserver( routeChangeObserver = notificationCenter.addObserver(
@ -174,12 +113,15 @@ class ClassicBluetoothHelper: NSObject {
) { [weak self] notification in ) { [weak self] notification in
self?.handleAudioRouteChange(notification) self?.handleAudioRouteChange(notification)
} }
// 激活音频会话 // 激活音频会话
activateAudioSession() activateAudioSession()
// 报告当前状态
reportCurrentBluetoothState()
} }
} }
// 处理音频路由变化 // 处理音频路由变化
private func handleAudioRouteChange(_ notification: Notification) { private func handleAudioRouteChange(_ notification: Notification) {
guard let userInfo = notification.userInfo, guard let userInfo = notification.userInfo,
@ -187,7 +129,7 @@ class ClassicBluetoothHelper: NSObject {
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
return return
} }
switch reason { switch reason {
case .newDeviceAvailable: case .newDeviceAvailable:
// 新音频设备已连接 // 新音频设备已连接
@ -195,7 +137,12 @@ class ClassicBluetoothHelper: NSObject {
for device in audioDevices { for device in audioDevices {
stateCallback?(.deviceConnected, ["device": device]) stateCallback?(.deviceConnected, ["device": device])
} }
// 如果有蓝牙设备连接,报告蓝牙状态为开启
if !audioDevices.isEmpty {
reportBluetoothState(.on)
}
case .oldDeviceUnavailable: case .oldDeviceUnavailable:
// 获取断开之前的路由 // 获取断开之前的路由
if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription { if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
@ -206,13 +153,20 @@ class ClassicBluetoothHelper: NSObject {
] ]
stateCallback?(.deviceDisconnected, ["device": device]) stateCallback?(.deviceDisconnected, ["device": device])
} }
// 检查当前是否还有蓝牙设备
let currentDevices = getConnectedAudioDevices()
if currentDevices.isEmpty {
// 如果没有任何蓝牙设备连接,可以认为蓝牙状态变为未知
reportBluetoothState(.unknown)
}
} }
default: default:
break break
} }
} }
// 激活音频会话 // 激活音频会话
private func activateAudioSession() { private func activateAudioSession() {
do { do {
@ -223,26 +177,26 @@ class ClassicBluetoothHelper: NSObject {
print("设置音频会话失败: \(error.localizedDescription)") print("设置音频会话失败: \(error.localizedDescription)")
} }
} }
/// 停止监听蓝牙状态变化 /// 停止监听蓝牙状态变化
func stopBluetoothStateMonitoring() { func stopBluetoothStateMonitoring() {
if isMonitoring { if isMonitoring {
isMonitoring = false isMonitoring = false
// 移除音频路由观察者 // 移除音频路由观察者
if let observer = routeChangeObserver { if let observer = routeChangeObserver {
NotificationCenter.default.removeObserver(observer) NotificationCenter.default.removeObserver(observer)
routeChangeObserver = nil routeChangeObserver = nil
} }
// 关闭音频会话 // 关闭音频会话
deactivateAudioSession() deactivateAudioSession()
} }
// 清除回调 // 清除回调
stateCallback = nil stateCallback = nil
} }
// 关闭音频会话 // 关闭音频会话
private func deactivateAudioSession() { private func deactivateAudioSession() {
do { do {
@ -251,76 +205,24 @@ class ClassicBluetoothHelper: NSObject {
print("关闭音频会话失败: \(error.localizedDescription)") print("关闭音频会话失败: \(error.localizedDescription)")
} }
} }
/// 报告当前蓝牙状态 /// 报告当前蓝牙状态
func reportCurrentBluetoothState() { func reportCurrentBluetoothState() {
// 如果centralManager未初始化,不触发状态报告 // 检查当前是否有蓝牙设备连接
guard let central = centralManager else { return } let devices = getConnectedAudioDevices()
// 如果状态是 .unknown 或 .resetting,暂不向 Flutter 端报告 if !devices.isEmpty {
if central.state == .unknown || central.state == .resetting { print("蓝牙状态: 已检测到蓝牙设备,报告状态为开启")
print("蓝牙状态: 临时状态(.unknown/.resetting),暂不报告") reportBluetoothState(.on)
return } else {
print("蓝牙状态: 未检测到蓝牙设备,报告状态为未知")
reportBluetoothState(.unknown)
} }
// 直接调用delegate方法保持状态处理一致性
centralManagerDidUpdateState(central)
} }
}
// 报告蓝牙状态变化
// MARK: - CBCentralManagerDelegate private func reportBluetoothState(_ state: BluetoothState) {
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
}
print("向Flutter端报告的蓝牙状态: \(state.rawValue)") print("向Flutter端报告的蓝牙状态: \(state.rawValue)")
stateCallback?(.stateChanged, ["state": 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])
} }
} }
Loading…
Cancel
Save