diff --git a/CHANGELOG.md b/CHANGELOG.md index da17864f..fb27dcf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.4 +* iOS: make Bluetooth state restoration optional by enabling it only when the `bluetooth-central` background mode is declared +* iOS: when state restoration is enabled, init `CBCentralManager` at launch only if Bluetooth permission is granted + ## 2.0.3 * iOS/macOS: defer `CBPeripheralManager` creation until peripheral APIs are used, fixing Bluetooth permission prompt on app launch * iOS: add CoreBluetooth state preservation/restoration so the app can be relaunched in the background by a connected peripheral and resume the live connection. Requires the `bluetooth-central` background mode in `Info.plist` (see README → Permissions → iOS / macOS). diff --git a/README.md b/README.md index c898fb47..dff8b9de 100644 --- a/README.md +++ b/README.md @@ -996,9 +996,9 @@ Add the `Bluetooth` capability to the macOS app from Xcode. #### iOS background state restoration -On iOS, the central manager is created with a `CBCentralManagerOptionRestoreIdentifierKey`, so CoreBluetooth can [relaunch your app](https://developer.apple.com/documentation/technotes/tn3115-bluetooth-state-restoration-app-relaunch-rules) in the background when a connected peripheral has activity, and hand the live connection back to the plugin. This happens automatically — no API call is required. +On iOS, when your app declares the `bluetooth-central` background mode and Bluetooth permission is already granted, the central manager is created at launch with a `CBCentralManagerOptionRestoreIdentifierKey`, so CoreBluetooth can [relaunch your app](https://developer.apple.com/documentation/technotes/tn3115-bluetooth-state-restoration-app-relaunch-rules) in the background when a connected peripheral has activity, and hand the live connection back to the plugin. If permission has not been granted yet, creation is deferred until a central BLE API (such as `startScan()` or `connect()`) is called. -To benefit from it, your app must declare the `Uses Bluetooth LE accessories` background mode. After enabling it, in `Info.plist` you should have: +To opt in, declare the `Uses Bluetooth LE accessories` background mode. After enabling it, in `Info.plist` you should have: ```xml UIBackgroundModes @@ -1007,10 +1007,10 @@ To benefit from it, your app must declare the `Uses Bluetooth LE accessories` ba bluetooth-central ... - +``` Notes: -- Without the `bluetooth-central` background mode, iOS will not relaunch the app for Bluetooth events and state restoration is effectively disabled. +- Without the `bluetooth-central` background mode, `CBCentralManager` is created lazily on the first central BLE API call and state restoration is disabled. - macOS does not support CoreBluetooth state restoration; this behavior is iOS-only. - On relaunch, the plugin re-adopts the restored peripherals and emits `onConnectionChanged` for any that are still connected, so your Dart code can resume where it left off. diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift index 42c75797..10f8c871 100644 --- a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift +++ b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift @@ -22,8 +22,9 @@ public class UniversalBlePlugin: NSObject, FlutterPlugin { let peripheralApi = UniversalBlePeripheralPlugin(callbackChannel: peripheralCallbackChannel) UniversalBlePlatformChannelSetup.setUp(binaryMessenger: messenger, api: api) #if os(iOS) - // Build the manager during launch so CoreBluetooth can deliver - // `willRestoreState:` after a background relaunch (see activateStateRestoration). + // When the host app declares `bluetooth-central`, build the manager during + // launch so CoreBluetooth can deliver `willRestoreState:` after a background + // relaunch (see activateStateRestoration). api.activateStateRestoration() #endif UniversalBlePeripheralChannelSetup.setUp( @@ -43,17 +44,48 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral // Identifier CoreBluetooth uses to restore this central across relaunches. static let stateRestorationIdentifier = "com.universalble.central.restoration" + #if os(iOS) + /// True when the host app declares the `bluetooth-central` background mode. + /// State restoration and eager manager creation are only enabled in that case. + private static let hasBluetoothCentralBackgroundMode: Bool = { + guard let modes = Bundle.main.object(forInfoDictionaryKey: "UIBackgroundModes") as? [String] else { + return false + } + return modes.contains("bluetooth-central") + }() + + private static var hasBluetoothPermission: Bool { + CBCentralManager.authorization == .allowedAlways + } + + /// Availability derived from `CBCentralManager.authorization` without creating a manager. + private static var availabilityStateFromAuthorization: AvailabilityState { + switch CBCentralManager.authorization { + case .restricted, .denied: + return .unauthorized + case .notDetermined: + return .unknown + default: + return .unknown + } + } + #endif + var callbackChannel: UniversalBleCallbackChannel private var universalBleFilterUtil = UniversalBleFilterUtil() #if os(iOS) - // iOS: opt into state restoration so `willRestoreState:` fires on relaunch. - private lazy var manager: CBCentralManager = .init( - delegate: self, - queue: nil, - options: [ - CBCentralManagerOptionRestoreIdentifierKey: BleCentralDarwin.stateRestorationIdentifier, - ] - ) + private lazy var manager: CBCentralManager = { + if Self.hasBluetoothCentralBackgroundMode { + return CBCentralManager( + delegate: self, + queue: nil, + options: [ + CBCentralManagerOptionRestoreIdentifierKey: BleCentralDarwin.stateRestorationIdentifier, + ] + ) + } + return CBCentralManager(delegate: self, queue: nil) + }() #else // macOS does not support CoreBluetooth state restoration. private lazy var manager: CBCentralManager = .init(delegate: self, queue: nil) @@ -75,13 +107,25 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral super.init() } - /// Eagerly creates the central manager at launch so CoreBluetooth can deliver - /// `willRestoreState:` when a managed peripheral relaunches the app. - func activateStateRestoration() { - _ = manager - } + #if os(iOS) + /// Eagerly creates the central manager at launch when the app declares the + /// `bluetooth-central` background mode and Bluetooth permission is already + /// granted, so CoreBluetooth can deliver `willRestoreState:` when a managed + /// peripheral relaunches the app. Otherwise creation stays deferred until + /// a central BLE API (e.g. `startScan`, `connect`) is called. + func activateStateRestoration() { + guard Self.hasBluetoothCentralBackgroundMode, Self.hasBluetoothPermission else { return } + _ = manager + } + #endif func getBluetoothAvailabilityState(completion: @escaping (Result) -> Void) { + #if os(iOS) + if Self.hasBluetoothCentralBackgroundMode, !Self.hasBluetoothPermission { + completion(.success(Self.availabilityStateFromAuthorization)) + return + } + #endif if manager.state != .unknown { completion(.success(manager.state.toAvailabilityState())) } else { diff --git a/pubspec.yaml b/pubspec.yaml index 47aa8d4a..3fd55acf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: universal_ble description: A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE) plugin for Flutter -version: 2.0.3 +version: 2.0.4 homepage: https://navideck.com repository: https://github.com/Navideck/universal_ble issue_tracker: https://github.com/Navideck/universal_ble/issues