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