diff --git a/CHANGELOG.md b/CHANGELOG.md index 47950a17..f7a57b30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 2.1.0 * Add optional `queueId` parameter to all APIs +* iOS/macOS: add opt-in `AppleConnectionOptions` to `connect` (via `ConnectionPlatformConfig`) — map to `CBConnectPeripheralOptionNotifyOnConnectionKey`, `NotifyOnDisconnectionKey` and `NotifyOnNotificationKey`, so a suspended app is relaunched into the background to handle connection events (e.g. auto-reconnect while the phone is locked) * Android: add `legacy` to `AndroidOptions` — set `legacy: true` to scan legacy BLE 4.x advertisements (e.g. ESP32) on API 26+; default (`null`/`false`) keeps extended-advertisement scanning unchanged from prior releases * Android: add `addServicesInScanResponse` to `PeripheralAndroidOptions` - set `addServicesInScanResponse: true` to Put advertised service UUIDs in the scan response instead of the primary advertisement * Android: merge manufacturer data with duplicate company id in scan results diff --git a/README.md b/README.md index 8d59a148..9c10f354 100644 --- a/README.md +++ b/README.md @@ -268,6 +268,25 @@ You can enable automatic reconnection by setting the `autoConnect` parameter to await bleDevice.connect(autoConnect: true); ``` +#### Background connection events (iOS/macOS) + +By default, a suspended app does not get woken for connection events. Set `AppleConnectionOptions` to have the system alert the user and relaunch your app into the background when a connection event occurs while the app is suspended — e.g. to keep reacting to a previously paired peripheral (auto-reconnect) during a workout while the phone is locked. Requires the `bluetooth-central` background mode on iOS. + +Note: the system may show an alert to the user for these events, and `notifyOnNotification` fires per characteristic notification, so enable only what you need. + +```dart +await bleDevice.connect( + autoConnect: true, + platformConfig: ConnectionPlatformConfig( + apple: AppleConnectionOptions( + notifyOnConnection: true, + notifyOnDisconnection: true, + notifyOnNotification: true, + ), + ), +); +``` + ### Discovering Services After establishing a connection, services need to be discovered. This method will discover all services and their characteristics. diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt index 5b8ace6b..19fdad4c 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt @@ -968,6 +968,110 @@ data class UniversalManufacturerData ( } } +/** + * Apple options for `connect`. + * + * Each flag maps to a `CBConnectPeripheralOptionNotifyOn*Key` connect option. + * When `true`, the system displays an alert for the corresponding event if + * the app is suspended while it occurs, relaunching the app into the + * background to handle it. This keeps a backgrounded central responsive — + * e.g. while reconnecting to a previously paired peripheral — without the + * user having to foreground the app. Requires the `bluetooth-central` + * background mode on iOS. + * + * All flags default to `false` (no alerts, background events are not + * delivered while suspended). + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class AppleConnectionOptions ( + /** + * `CBConnectPeripheralOptionNotifyOnConnectionKey`: + * notify when a connection to the device succeeds while the app is suspended. + */ + val notifyOnConnection: Boolean? = null, + /** + * `CBConnectPeripheralOptionNotifyOnDisconnectionKey`: + * notify when the peripheral disconnects while the app is suspended. + */ + val notifyOnDisconnection: Boolean? = null, + /** + * `CBConnectPeripheralOptionNotifyOnNotificationKey`: + * notify when a characteristic notification arrives while the app is + * suspended. Fires per notification. + */ + val notifyOnNotification: Boolean? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): AppleConnectionOptions { + val notifyOnConnection = pigeonVar_list[0] as Boolean? + val notifyOnDisconnection = pigeonVar_list[1] as Boolean? + val notifyOnNotification = pigeonVar_list[2] as Boolean? + return AppleConnectionOptions(notifyOnConnection, notifyOnDisconnection, notifyOnNotification) + } + } + fun toList(): List { + return listOf( + notifyOnConnection, + notifyOnDisconnection, + notifyOnNotification, + ) + } + override fun equals(other: Any?): Boolean { + if (other == null || other.javaClass != javaClass) { + return false + } + if (this === other) { + return true + } + val other = other as AppleConnectionOptions + return UniversalBlePigeonUtils.deepEquals(this.notifyOnConnection, other.notifyOnConnection) && UniversalBlePigeonUtils.deepEquals(this.notifyOnDisconnection, other.notifyOnDisconnection) && UniversalBlePigeonUtils.deepEquals(this.notifyOnNotification, other.notifyOnNotification) + } + + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + UniversalBlePigeonUtils.deepHash(this.notifyOnConnection) + result = 31 * result + UniversalBlePigeonUtils.deepHash(this.notifyOnDisconnection) + result = 31 * result + UniversalBlePigeonUtils.deepHash(this.notifyOnNotification) + return result + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class ConnectionPlatformConfig ( + val apple: AppleConnectionOptions? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): ConnectionPlatformConfig { + val apple = pigeonVar_list[0] as AppleConnectionOptions? + return ConnectionPlatformConfig(apple) + } + } + fun toList(): List { + return listOf( + apple, + ) + } + override fun equals(other: Any?): Boolean { + if (other == null || other.javaClass != javaClass) { + return false + } + if (this === other) { + return true + } + val other = other as ConnectionPlatformConfig + return UniversalBlePigeonUtils.deepEquals(this.apple, other.apple) + } + + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + UniversalBlePigeonUtils.deepHash(this.apple) + return result + } +} + /** Generated class from Pigeon that represents data sent in messages. */ data class PeripheralAndroidOptions ( val addManufacturerDataInScanResponse: Boolean? = null, @@ -1399,35 +1503,45 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() { } 154.toByte() -> { return (readValue(buffer) as? List)?.let { - PeripheralAndroidOptions.fromList(it) + AppleConnectionOptions.fromList(it) } } 155.toByte() -> { return (readValue(buffer) as? List)?.let { - PeripheralPlatformConfig.fromList(it) + ConnectionPlatformConfig.fromList(it) } } 156.toByte() -> { return (readValue(buffer) as? List)?.let { - PeripheralService.fromList(it) + PeripheralAndroidOptions.fromList(it) } } 157.toByte() -> { return (readValue(buffer) as? List)?.let { - PeripheralCharacteristic.fromList(it) + PeripheralPlatformConfig.fromList(it) } } 158.toByte() -> { return (readValue(buffer) as? List)?.let { - PeripheralDescriptor.fromList(it) + PeripheralService.fromList(it) } } 159.toByte() -> { return (readValue(buffer) as? List)?.let { - PeripheralReadRequestResult.fromList(it) + PeripheralCharacteristic.fromList(it) } } 160.toByte() -> { + return (readValue(buffer) as? List)?.let { + PeripheralDescriptor.fromList(it) + } + } + 161.toByte() -> { + return (readValue(buffer) as? List)?.let { + PeripheralReadRequestResult.fromList(it) + } + } + 162.toByte() -> { return (readValue(buffer) as? List)?.let { PeripheralWriteRequestResult.fromList(it) } @@ -1537,34 +1651,42 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() { stream.write(153) writeValue(stream, value.toList()) } - is PeripheralAndroidOptions -> { + is AppleConnectionOptions -> { stream.write(154) writeValue(stream, value.toList()) } - is PeripheralPlatformConfig -> { + is ConnectionPlatformConfig -> { stream.write(155) writeValue(stream, value.toList()) } - is PeripheralService -> { + is PeripheralAndroidOptions -> { stream.write(156) writeValue(stream, value.toList()) } - is PeripheralCharacteristic -> { + is PeripheralPlatformConfig -> { stream.write(157) writeValue(stream, value.toList()) } - is PeripheralDescriptor -> { + is PeripheralService -> { stream.write(158) writeValue(stream, value.toList()) } - is PeripheralReadRequestResult -> { + is PeripheralCharacteristic -> { stream.write(159) writeValue(stream, value.toList()) } - is PeripheralWriteRequestResult -> { + is PeripheralDescriptor -> { stream.write(160) writeValue(stream, value.toList()) } + is PeripheralReadRequestResult -> { + stream.write(161) + writeValue(stream, value.toList()) + } + is PeripheralWriteRequestResult -> { + stream.write(162) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -1587,7 +1709,7 @@ interface UniversalBlePlatformChannel { fun startScan(filter: UniversalScanFilter?, config: UniversalScanConfig?) fun stopScan() fun isScanning(): Boolean - fun connect(deviceId: String, autoConnect: Boolean?) + fun connect(deviceId: String, autoConnect: Boolean?, platformConfig: ConnectionPlatformConfig?) fun disconnect(deviceId: String) fun setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: BleInputProperty, callback: (Result) -> Unit) fun discoverServices(deviceId: String, withDescriptors: Boolean, callback: (Result>) -> Unit) @@ -1759,8 +1881,9 @@ interface UniversalBlePlatformChannel { val args = message as List val deviceIdArg = args[0] as String val autoConnectArg = args[1] as Boolean? + val platformConfigArg = args[2] as ConnectionPlatformConfig? val wrapped: List = try { - api.connect(deviceIdArg, autoConnectArg) + api.connect(deviceIdArg, autoConnectArg, platformConfigArg) listOf(null) } catch (exception: Throwable) { UniversalBlePigeonUtils.wrapError(exception) diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt index cb548ef6..d86b75b6 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt @@ -250,7 +250,12 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), return safeScanner.isScanning() } - override fun connect(deviceId: String, autoConnect: Boolean?) { + override fun connect( + deviceId: String, + autoConnect: Boolean?, + platformConfig: ConnectionPlatformConfig?, + ) { + // Note: platformConfig only carries Apple-specific options // If already connected, send connected message, // if connecting, do nothing deviceId.findGatt()?.let { diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift index 91764fa3..85a6d671 100644 --- a/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift +++ b/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift @@ -842,6 +842,98 @@ struct UniversalManufacturerData: Hashable { } } +/// Apple options for `connect`. +/// +/// Each flag maps to a `CBConnectPeripheralOptionNotifyOn*Key` connect option. +/// When `true`, the system displays an alert for the corresponding event if +/// the app is suspended while it occurs, relaunching the app into the +/// background to handle it. This keeps a backgrounded central responsive — +/// e.g. while reconnecting to a previously paired peripheral — without the +/// user having to foreground the app. Requires the `bluetooth-central` +/// background mode on iOS. +/// +/// All flags default to `false` (no alerts, background events are not +/// delivered while suspended). +/// +/// Generated class from Pigeon that represents data sent in messages. +struct AppleConnectionOptions: Hashable { + /// `CBConnectPeripheralOptionNotifyOnConnectionKey`: + /// notify when a connection to the device succeeds while the app is suspended. + var notifyOnConnection: Bool? = nil + /// `CBConnectPeripheralOptionNotifyOnDisconnectionKey`: + /// notify when the peripheral disconnects while the app is suspended. + var notifyOnDisconnection: Bool? = nil + /// `CBConnectPeripheralOptionNotifyOnNotificationKey`: + /// notify when a characteristic notification arrives while the app is + /// suspended. Fires per notification. + var notifyOnNotification: Bool? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> AppleConnectionOptions? { + let notifyOnConnection: Bool? = nilOrValue(pigeonVar_list[0]) + let notifyOnDisconnection: Bool? = nilOrValue(pigeonVar_list[1]) + let notifyOnNotification: Bool? = nilOrValue(pigeonVar_list[2]) + + return AppleConnectionOptions( + notifyOnConnection: notifyOnConnection, + notifyOnDisconnection: notifyOnDisconnection, + notifyOnNotification: notifyOnNotification + ) + } + func toList() -> [Any?] { + return [ + notifyOnConnection, + notifyOnDisconnection, + notifyOnNotification, + ] + } + static func == (lhs: AppleConnectionOptions, rhs: AppleConnectionOptions) -> Bool { + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsUniversalBle(lhs.notifyOnConnection, rhs.notifyOnConnection) && deepEqualsUniversalBle(lhs.notifyOnDisconnection, rhs.notifyOnDisconnection) && deepEqualsUniversalBle(lhs.notifyOnNotification, rhs.notifyOnNotification) + } + + func hash(into hasher: inout Hasher) { + hasher.combine("AppleConnectionOptions") + deepHashUniversalBle(value: notifyOnConnection, hasher: &hasher) + deepHashUniversalBle(value: notifyOnDisconnection, hasher: &hasher) + deepHashUniversalBle(value: notifyOnNotification, hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct ConnectionPlatformConfig: Hashable { + var apple: AppleConnectionOptions? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> ConnectionPlatformConfig? { + let apple: AppleConnectionOptions? = nilOrValue(pigeonVar_list[0]) + + return ConnectionPlatformConfig( + apple: apple + ) + } + func toList() -> [Any?] { + return [ + apple + ] + } + static func == (lhs: ConnectionPlatformConfig, rhs: ConnectionPlatformConfig) -> Bool { + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsUniversalBle(lhs.apple, rhs.apple) + } + + func hash(into hasher: inout Hasher) { + hasher.combine("ConnectionPlatformConfig") + deepHashUniversalBle(value: apple, hasher: &hasher) + } +} + /// Generated class from Pigeon that represents data sent in messages. struct PeripheralAndroidOptions: Hashable { var addManufacturerDataInScanResponse: Bool? = nil @@ -1246,18 +1338,22 @@ private class UniversalBlePigeonCodecReader: FlutterStandardReader { case 153: return UniversalManufacturerData.fromList(self.readValue() as! [Any?]) case 154: - return PeripheralAndroidOptions.fromList(self.readValue() as! [Any?]) + return AppleConnectionOptions.fromList(self.readValue() as! [Any?]) case 155: - return PeripheralPlatformConfig.fromList(self.readValue() as! [Any?]) + return ConnectionPlatformConfig.fromList(self.readValue() as! [Any?]) case 156: - return PeripheralService.fromList(self.readValue() as! [Any?]) + return PeripheralAndroidOptions.fromList(self.readValue() as! [Any?]) case 157: - return PeripheralCharacteristic.fromList(self.readValue() as! [Any?]) + return PeripheralPlatformConfig.fromList(self.readValue() as! [Any?]) case 158: - return PeripheralDescriptor.fromList(self.readValue() as! [Any?]) + return PeripheralService.fromList(self.readValue() as! [Any?]) case 159: - return PeripheralReadRequestResult.fromList(self.readValue() as! [Any?]) + return PeripheralCharacteristic.fromList(self.readValue() as! [Any?]) case 160: + return PeripheralDescriptor.fromList(self.readValue() as! [Any?]) + case 161: + return PeripheralReadRequestResult.fromList(self.readValue() as! [Any?]) + case 162: return PeripheralWriteRequestResult.fromList(self.readValue() as! [Any?]) default: return super.readValue(ofType: type) @@ -1342,27 +1438,33 @@ private class UniversalBlePigeonCodecWriter: FlutterStandardWriter { } else if let value = value as? UniversalManufacturerData { super.writeByte(153) super.writeValue(value.toList()) - } else if let value = value as? PeripheralAndroidOptions { + } else if let value = value as? AppleConnectionOptions { super.writeByte(154) super.writeValue(value.toList()) - } else if let value = value as? PeripheralPlatformConfig { + } else if let value = value as? ConnectionPlatformConfig { super.writeByte(155) super.writeValue(value.toList()) - } else if let value = value as? PeripheralService { + } else if let value = value as? PeripheralAndroidOptions { super.writeByte(156) super.writeValue(value.toList()) - } else if let value = value as? PeripheralCharacteristic { + } else if let value = value as? PeripheralPlatformConfig { super.writeByte(157) super.writeValue(value.toList()) - } else if let value = value as? PeripheralDescriptor { + } else if let value = value as? PeripheralService { super.writeByte(158) super.writeValue(value.toList()) - } else if let value = value as? PeripheralReadRequestResult { + } else if let value = value as? PeripheralCharacteristic { super.writeByte(159) super.writeValue(value.toList()) - } else if let value = value as? PeripheralWriteRequestResult { + } else if let value = value as? PeripheralDescriptor { super.writeByte(160) super.writeValue(value.toList()) + } else if let value = value as? PeripheralReadRequestResult { + super.writeByte(161) + super.writeValue(value.toList()) + } else if let value = value as? PeripheralWriteRequestResult { + super.writeByte(162) + super.writeValue(value.toList()) } else { super.writeValue(value) } @@ -1398,7 +1500,7 @@ protocol UniversalBlePlatformChannel { func startScan(filter: UniversalScanFilter?, config: UniversalScanConfig?) throws func stopScan() throws func isScanning() throws -> Bool - func connect(deviceId: String, autoConnect: Bool?) throws + func connect(deviceId: String, autoConnect: Bool?, platformConfig: ConnectionPlatformConfig?) throws func disconnect(deviceId: String) throws func setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: BleInputProperty, completion: @escaping (Result) -> Void) func discoverServices(deviceId: String, withDescriptors: Bool, completion: @escaping (Result<[UniversalBleService], Error>) -> Void) @@ -1546,8 +1648,9 @@ class UniversalBlePlatformChannelSetup { let args = message as! [Any?] let deviceIdArg = args[0] as! String let autoConnectArg: Bool? = nilOrValue(args[1]) + let platformConfigArg: ConnectionPlatformConfig? = nilOrValue(args[2]) do { - try api.connect(deviceId: deviceIdArg, autoConnect: autoConnectArg) + try api.connect(deviceId: deviceIdArg, autoConnect: autoConnectArg, platformConfig: platformConfigArg) reply(wrapResult(nil)) } catch { reply(wrapError(error)) diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift index 10f8c871..f73f417c 100644 --- a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift +++ b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift @@ -206,16 +206,35 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral UniversalBleLogger.shared.setLogLevel(logLevel) } - func connect(deviceId: String, autoConnect: Bool?) throws { + func connect(deviceId: String, autoConnect: Bool?, platformConfig: ConnectionPlatformConfig?) throws { let peripheral = try deviceId.getPeripheral(manager: manager) peripheral.delegate = self let shouldAutoConnect = autoConnect ?? false + var options: [String: Any] = [:] + + // Opt-in: ask the system to surface connection-related events while the + // app is suspended (relaunching it into the background when one occurs), + // so a backgrounded central stays responsive — e.g. while reconnecting to + // a previously paired peripheral — without the user having to foreground + // the app. The system may show the user an alert for these events, which + // is why they are off unless requested via `AppleConnectionOptions`. + if let appleOptions = platformConfig?.apple { + if appleOptions.notifyOnConnection == true { + options[CBConnectPeripheralOptionNotifyOnConnectionKey] = true + } + if appleOptions.notifyOnDisconnection == true { + options[CBConnectPeripheralOptionNotifyOnDisconnectionKey] = true + } + if appleOptions.notifyOnNotification == true { + options[CBConnectPeripheralOptionNotifyOnNotificationKey] = true + } + } + if shouldAutoConnect { autoConnectDevices.insert(deviceId) if #available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) { - let options: [String: Any] = [CBConnectPeripheralOptionEnableAutoReconnect: true] - manager.connect(peripheral, options: options) + options[CBConnectPeripheralOptionEnableAutoReconnect] = true } else { // Auto-reconnect via CBConnectPeripheralOptionEnableAutoReconnect is only // available on iOS 17.0 / macOS 14.0 / watchOS 10.0 / tvOS 17.0 and later. @@ -228,12 +247,12 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral "is only available on iOS 17+/macOS 14+/watchOS 10+/tvOS 17+. " + "On this OS version, reconnections must be handled manually." ) - manager.connect(peripheral) } } else { autoConnectDevices.remove(deviceId) - manager.connect(peripheral) } + + manager.connect(peripheral, options: options.isEmpty ? nil : options) } func disconnect(deviceId: String) throws { diff --git a/example/lib/data/mock_universal_ble.dart b/example/lib/data/mock_universal_ble.dart index c5aa67f4..79dbb60d 100644 --- a/example/lib/data/mock_universal_ble.dart +++ b/example/lib/data/mock_universal_ble.dart @@ -44,7 +44,9 @@ class MockUniversalBle extends UniversalBlePlatform { @override Future connect(String deviceId, - {Duration? connectionTimeout, bool autoConnect = false}) async { + {Duration? connectionTimeout, + bool autoConnect = false, + ConnectionPlatformConfig? platformConfig}) async { updateConnection(deviceId, true); } diff --git a/lib/src/extensions/ble_device_extension.dart b/lib/src/extensions/ble_device_extension.dart index 11ade447..3436bd19 100644 --- a/lib/src/extensions/ble_device_extension.dart +++ b/lib/src/extensions/ble_device_extension.dart @@ -17,12 +17,17 @@ extension BleDeviceExtension on BleDevice { /// Connects to the device. /// [autoConnect] enables automatic reconnection when the device becomes available. - Future connect({bool autoConnect = false, Duration? timeout}) => - UniversalBle.connect( - deviceId, - autoConnect: autoConnect, - timeout: timeout, - ); + /// [platformConfig] sets platform specific connection options. + Future connect({ + bool autoConnect = false, + Duration? timeout, + ConnectionPlatformConfig? platformConfig, + }) => UniversalBle.connect( + deviceId, + autoConnect: autoConnect, + timeout: timeout, + platformConfig: platformConfig, + ); /// Disconnects from the device. Future disconnect({Duration? timeout, String? queueId}) => diff --git a/lib/src/interfaces/universal_ble_platform_interface.dart b/lib/src/interfaces/universal_ble_platform_interface.dart index 93fbdb30..b08cf4e3 100644 --- a/lib/src/interfaces/universal_ble_platform_interface.dart +++ b/lib/src/interfaces/universal_ble_platform_interface.dart @@ -65,6 +65,7 @@ abstract class UniversalBlePlatform { String deviceId, { Duration? connectionTimeout, bool autoConnect = false, + ConnectionPlatformConfig? platformConfig, }); Future disconnect(String deviceId); diff --git a/lib/src/universal_ble.dart b/lib/src/universal_ble.dart index 6a22c224..11ddfa1e 100644 --- a/lib/src/universal_ble.dart +++ b/lib/src/universal_ble.dart @@ -149,11 +149,16 @@ class UniversalBle { /// /// Call [disconnect] to prevent auto-reconnect even while a device is disconnected. /// + /// [platformConfig] sets platform specific connection options, + /// e.g. [AppleConnectionOptions] to get notified of connection events + /// while the app is suspended. Ignored on other platforms. + /// /// Can throw `ConnectionException` or `PlatformException`. static Future connect( String deviceId, { Duration? timeout, bool autoConnect = false, + ConnectionPlatformConfig? platformConfig, }) async { timeout ??= const Duration(seconds: 60); Completer completer = _connectionEventCompleter( @@ -162,7 +167,12 @@ class UniversalBle { ); _platform - .connect(deviceId, connectionTimeout: timeout, autoConnect: autoConnect) + .connect( + deviceId, + connectionTimeout: timeout, + autoConnect: autoConnect, + platformConfig: platformConfig, + ) .catchError((error) { if (completer.isCompleted) return; completer.completeError(ConnectionException(error)); diff --git a/lib/src/universal_ble.g.dart b/lib/src/universal_ble.g.dart index c53ef0fb..5ef82159 100644 --- a/lib/src/universal_ble.g.dart +++ b/lib/src/universal_ble.g.dart @@ -833,6 +833,116 @@ class UniversalManufacturerData { int get hashCode => _deepHash([runtimeType, ..._toList()]); } +/// Apple options for `connect`. +/// +/// Each flag maps to a `CBConnectPeripheralOptionNotifyOn*Key` connect option. +/// When `true`, the system displays an alert for the corresponding event if +/// the app is suspended while it occurs, relaunching the app into the +/// background to handle it. This keeps a backgrounded central responsive — +/// e.g. while reconnecting to a previously paired peripheral — without the +/// user having to foreground the app. Requires the `bluetooth-central` +/// background mode on iOS. +/// +/// All flags default to `false` (no alerts, background events are not +/// delivered while suspended). +class AppleConnectionOptions { + AppleConnectionOptions({ + this.notifyOnConnection, + this.notifyOnDisconnection, + this.notifyOnNotification, + }); + + /// `CBConnectPeripheralOptionNotifyOnConnectionKey`: + /// notify when a connection to the device succeeds while the app is suspended. + bool? notifyOnConnection; + + /// `CBConnectPeripheralOptionNotifyOnDisconnectionKey`: + /// notify when the peripheral disconnects while the app is suspended. + bool? notifyOnDisconnection; + + /// `CBConnectPeripheralOptionNotifyOnNotificationKey`: + /// notify when a characteristic notification arrives while the app is + /// suspended. Fires per notification. + bool? notifyOnNotification; + + List _toList() { + return [ + notifyOnConnection, + notifyOnDisconnection, + notifyOnNotification, + ]; + } + + Object encode() { + return _toList(); + } + + static AppleConnectionOptions decode(Object result) { + result as List; + return AppleConnectionOptions( + notifyOnConnection: result[0] as bool?, + notifyOnDisconnection: result[1] as bool?, + notifyOnNotification: result[2] as bool?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! AppleConnectionOptions || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(notifyOnConnection, other.notifyOnConnection) && + _deepEquals(notifyOnDisconnection, other.notifyOnDisconnection) && + _deepEquals(notifyOnNotification, other.notifyOnNotification); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); +} + +class ConnectionPlatformConfig { + ConnectionPlatformConfig({this.apple}); + + AppleConnectionOptions? apple; + + List _toList() { + return [apple]; + } + + Object encode() { + return _toList(); + } + + static ConnectionPlatformConfig decode(Object result) { + result as List; + return ConnectionPlatformConfig( + apple: result[0] as AppleConnectionOptions?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! ConnectionPlatformConfig || + other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(apple, other.apple); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); +} + class PeripheralAndroidOptions { PeripheralAndroidOptions({ this.addManufacturerDataInScanResponse, @@ -1260,27 +1370,33 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is UniversalManufacturerData) { buffer.putUint8(153); writeValue(buffer, value.encode()); - } else if (value is PeripheralAndroidOptions) { + } else if (value is AppleConnectionOptions) { buffer.putUint8(154); writeValue(buffer, value.encode()); - } else if (value is PeripheralPlatformConfig) { + } else if (value is ConnectionPlatformConfig) { buffer.putUint8(155); writeValue(buffer, value.encode()); - } else if (value is PeripheralService) { + } else if (value is PeripheralAndroidOptions) { buffer.putUint8(156); writeValue(buffer, value.encode()); - } else if (value is PeripheralCharacteristic) { + } else if (value is PeripheralPlatformConfig) { buffer.putUint8(157); writeValue(buffer, value.encode()); - } else if (value is PeripheralDescriptor) { + } else if (value is PeripheralService) { buffer.putUint8(158); writeValue(buffer, value.encode()); - } else if (value is PeripheralReadRequestResult) { + } else if (value is PeripheralCharacteristic) { buffer.putUint8(159); writeValue(buffer, value.encode()); - } else if (value is PeripheralWriteRequestResult) { + } else if (value is PeripheralDescriptor) { buffer.putUint8(160); writeValue(buffer, value.encode()); + } else if (value is PeripheralReadRequestResult) { + buffer.putUint8(161); + writeValue(buffer, value.encode()); + } else if (value is PeripheralWriteRequestResult) { + buffer.putUint8(162); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -1357,18 +1473,22 @@ class _PigeonCodec extends StandardMessageCodec { case 153: return UniversalManufacturerData.decode(readValue(buffer)!); case 154: - return PeripheralAndroidOptions.decode(readValue(buffer)!); + return AppleConnectionOptions.decode(readValue(buffer)!); case 155: - return PeripheralPlatformConfig.decode(readValue(buffer)!); + return ConnectionPlatformConfig.decode(readValue(buffer)!); case 156: - return PeripheralService.decode(readValue(buffer)!); + return PeripheralAndroidOptions.decode(readValue(buffer)!); case 157: - return PeripheralCharacteristic.decode(readValue(buffer)!); + return PeripheralPlatformConfig.decode(readValue(buffer)!); case 158: - return PeripheralDescriptor.decode(readValue(buffer)!); + return PeripheralService.decode(readValue(buffer)!); case 159: - return PeripheralReadRequestResult.decode(readValue(buffer)!); + return PeripheralCharacteristic.decode(readValue(buffer)!); case 160: + return PeripheralDescriptor.decode(readValue(buffer)!); + case 161: + return PeripheralReadRequestResult.decode(readValue(buffer)!); + case 162: return PeripheralWriteRequestResult.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -1554,7 +1674,11 @@ class UniversalBlePlatformChannel { return pigeonVar_replyValue! as bool; } - Future connect(String deviceId, {bool? autoConnect}) async { + Future connect( + String deviceId, { + bool? autoConnect, + ConnectionPlatformConfig? platformConfig, + }) async { final pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( @@ -1563,7 +1687,7 @@ class UniversalBlePlatformChannel { binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [deviceId, autoConnect], + [deviceId, autoConnect, platformConfig], ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; diff --git a/lib/src/universal_ble_linux/universal_ble_linux.dart b/lib/src/universal_ble_linux/universal_ble_linux.dart index 5f2f0d1f..7a0ba233 100644 --- a/lib/src/universal_ble_linux/universal_ble_linux.dart +++ b/lib/src/universal_ble_linux/universal_ble_linux.dart @@ -145,8 +145,10 @@ class UniversalBleLinux extends UniversalBlePlatform { String deviceId, { Duration? connectionTimeout, bool autoConnect = false, + ConnectionPlatformConfig? platformConfig, }) async { // Note: autoConnect is not directly supported on Linux platform + // Note: platformConfig only carries Apple-specific options final device = _findDeviceById(deviceId); if (device.connected) { updateConnection(deviceId, true); diff --git a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart index fdc9779e..133031c1 100644 --- a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart +++ b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart @@ -68,8 +68,13 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform String deviceId, { Duration? connectionTimeout, bool autoConnect = false, + ConnectionPlatformConfig? platformConfig, }) => _executeWithErrorHandling( - () => _channel.connect(deviceId, autoConnect: autoConnect), + () => _channel.connect( + deviceId, + autoConnect: autoConnect, + platformConfig: platformConfig, + ), ); @override diff --git a/lib/src/universal_ble_web/universal_ble_web.dart b/lib/src/universal_ble_web/universal_ble_web.dart index 9ddbb516..a118adc1 100644 --- a/lib/src/universal_ble_web/universal_ble_web.dart +++ b/lib/src/universal_ble_web/universal_ble_web.dart @@ -35,8 +35,10 @@ class UniversalBleWeb extends UniversalBlePlatform { String deviceId, { Duration? connectionTimeout = const Duration(seconds: 10), bool autoConnect = false, + ConnectionPlatformConfig? platformConfig, }) async { // Note: autoConnect is not directly supported on Web platform + // Note: platformConfig only carries Apple-specific options var device = _getDeviceById(deviceId); if (device == null) { throw UniversalBleException( diff --git a/lib/universal_ble.dart b/lib/universal_ble.dart index 6764fdc0..2c109143 100644 --- a/lib/universal_ble.dart +++ b/lib/universal_ble.dart @@ -11,6 +11,8 @@ export 'package:universal_ble/src/interfaces/universal_ble_peripheral_platform_i export 'package:universal_ble/src/universal_ble.g.dart' show AndroidOptions, + AppleConnectionOptions, + ConnectionPlatformConfig, AndroidScanMode, AndroidScanCallbackType, AndroidScanMatchMode, diff --git a/pigeon/universal_ble.dart b/pigeon/universal_ble.dart index c3056a39..38cc909e 100644 --- a/pigeon/universal_ble.dart +++ b/pigeon/universal_ble.dart @@ -259,6 +259,44 @@ class UniversalManufacturerData { }); } +/// Apple options for `connect`. +/// +/// Each flag maps to a `CBConnectPeripheralOptionNotifyOn*Key` connect option. +/// When `true`, the system displays an alert for the corresponding event if +/// the app is suspended while it occurs, relaunching the app into the +/// background to handle it. This keeps a backgrounded central responsive — +/// e.g. while reconnecting to a previously paired peripheral — without the +/// user having to foreground the app. Requires the `bluetooth-central` +/// background mode on iOS. +/// +/// All flags default to `false` (no alerts, background events are not +/// delivered while suspended). +class AppleConnectionOptions { + /// `CBConnectPeripheralOptionNotifyOnConnectionKey`: + /// notify when a connection to the device succeeds while the app is suspended. + bool? notifyOnConnection; + + /// `CBConnectPeripheralOptionNotifyOnDisconnectionKey`: + /// notify when the peripheral disconnects while the app is suspended. + bool? notifyOnDisconnection; + + /// `CBConnectPeripheralOptionNotifyOnNotificationKey`: + /// notify when a characteristic notification arrives while the app is + /// suspended. Fires per notification. + bool? notifyOnNotification; + + AppleConnectionOptions({ + this.notifyOnConnection, + this.notifyOnDisconnection, + this.notifyOnNotification, + }); +} + +class ConnectionPlatformConfig { + AppleConnectionOptions? apple; + ConnectionPlatformConfig({this.apple}); +} + class PeripheralAndroidOptions { bool? addManufacturerDataInScanResponse; @@ -351,7 +389,11 @@ abstract class UniversalBlePlatformChannel { bool isScanning(); - void connect(String deviceId, {bool? autoConnect}); + void connect( + String deviceId, { + bool? autoConnect, + ConnectionPlatformConfig? platformConfig, + }); void disconnect(String deviceId); diff --git a/test/universal_ble_test_mock.dart b/test/universal_ble_test_mock.dart index bc169ea0..c1f6fdb4 100644 --- a/test/universal_ble_test_mock.dart +++ b/test/universal_ble_test_mock.dart @@ -7,6 +7,7 @@ abstract class UniversalBlePlatformMock extends UniversalBlePlatform { String deviceId, { bool autoConnect = false, Duration? connectionTimeout, + ConnectionPlatformConfig? platformConfig, }) { throw UnimplementedError(); } diff --git a/windows/src/generated/universal_ble.g.cpp b/windows/src/generated/universal_ble.g.cpp index 981a6c83..149346c4 100644 --- a/windows/src/generated/universal_ble.g.cpp +++ b/windows/src/generated/universal_ble.g.cpp @@ -1180,6 +1180,165 @@ size_t PigeonInternalDeepHash(const UniversalManufacturerData& v) { return v.Hash(); } +// AppleConnectionOptions + +AppleConnectionOptions::AppleConnectionOptions() {} + +AppleConnectionOptions::AppleConnectionOptions( + const bool* notify_on_connection, + const bool* notify_on_disconnection, + const bool* notify_on_notification) + : notify_on_connection_(notify_on_connection ? std::optional(*notify_on_connection) : std::nullopt), + notify_on_disconnection_(notify_on_disconnection ? std::optional(*notify_on_disconnection) : std::nullopt), + notify_on_notification_(notify_on_notification ? std::optional(*notify_on_notification) : std::nullopt) {} + +const bool* AppleConnectionOptions::notify_on_connection() const { + return notify_on_connection_ ? &(*notify_on_connection_) : nullptr; +} + +void AppleConnectionOptions::set_notify_on_connection(const bool* value_arg) { + notify_on_connection_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void AppleConnectionOptions::set_notify_on_connection(bool value_arg) { + notify_on_connection_ = value_arg; +} + + +const bool* AppleConnectionOptions::notify_on_disconnection() const { + return notify_on_disconnection_ ? &(*notify_on_disconnection_) : nullptr; +} + +void AppleConnectionOptions::set_notify_on_disconnection(const bool* value_arg) { + notify_on_disconnection_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void AppleConnectionOptions::set_notify_on_disconnection(bool value_arg) { + notify_on_disconnection_ = value_arg; +} + + +const bool* AppleConnectionOptions::notify_on_notification() const { + return notify_on_notification_ ? &(*notify_on_notification_) : nullptr; +} + +void AppleConnectionOptions::set_notify_on_notification(const bool* value_arg) { + notify_on_notification_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void AppleConnectionOptions::set_notify_on_notification(bool value_arg) { + notify_on_notification_ = value_arg; +} + + +EncodableList AppleConnectionOptions::ToEncodableList() const { + EncodableList list; + list.reserve(3); + list.push_back(notify_on_connection_ ? EncodableValue(*notify_on_connection_) : EncodableValue()); + list.push_back(notify_on_disconnection_ ? EncodableValue(*notify_on_disconnection_) : EncodableValue()); + list.push_back(notify_on_notification_ ? EncodableValue(*notify_on_notification_) : EncodableValue()); + return list; +} + +AppleConnectionOptions AppleConnectionOptions::FromEncodableList(const EncodableList& list) { + AppleConnectionOptions decoded; + auto& encodable_notify_on_connection = list[0]; + if (!encodable_notify_on_connection.IsNull()) { + decoded.set_notify_on_connection(std::get(encodable_notify_on_connection)); + } + auto& encodable_notify_on_disconnection = list[1]; + if (!encodable_notify_on_disconnection.IsNull()) { + decoded.set_notify_on_disconnection(std::get(encodable_notify_on_disconnection)); + } + auto& encodable_notify_on_notification = list[2]; + if (!encodable_notify_on_notification.IsNull()) { + decoded.set_notify_on_notification(std::get(encodable_notify_on_notification)); + } + return decoded; +} + +bool AppleConnectionOptions::operator==(const AppleConnectionOptions& other) const { + return PigeonInternalDeepEquals(notify_on_connection_, other.notify_on_connection_) && PigeonInternalDeepEquals(notify_on_disconnection_, other.notify_on_disconnection_) && PigeonInternalDeepEquals(notify_on_notification_, other.notify_on_notification_); +} + +bool AppleConnectionOptions::operator!=(const AppleConnectionOptions& other) const { + return !(*this == other); +} + +size_t AppleConnectionOptions::Hash() const { + size_t result = 1; + result = result * 31 + PigeonInternalDeepHash(notify_on_connection_); + result = result * 31 + PigeonInternalDeepHash(notify_on_disconnection_); + result = result * 31 + PigeonInternalDeepHash(notify_on_notification_); + return result; +} + +size_t PigeonInternalDeepHash(const AppleConnectionOptions& v) { + return v.Hash(); +} + +// ConnectionPlatformConfig + +ConnectionPlatformConfig::ConnectionPlatformConfig() {} + +ConnectionPlatformConfig::ConnectionPlatformConfig(const AppleConnectionOptions* apple) + : apple_(apple ? std::make_unique(*apple) : nullptr) {} + +ConnectionPlatformConfig::ConnectionPlatformConfig(const ConnectionPlatformConfig& other) + : apple_(other.apple_ ? std::make_unique(*other.apple_) : nullptr) {} + +ConnectionPlatformConfig& ConnectionPlatformConfig::operator=(const ConnectionPlatformConfig& other) { + apple_ = other.apple_ ? std::make_unique(*other.apple_) : nullptr; + return *this; +} + +const AppleConnectionOptions* ConnectionPlatformConfig::apple() const { + return apple_.get(); +} + +void ConnectionPlatformConfig::set_apple(const AppleConnectionOptions* value_arg) { + apple_ = value_arg ? std::make_unique(*value_arg) : nullptr; +} + +void ConnectionPlatformConfig::set_apple(const AppleConnectionOptions& value_arg) { + apple_ = std::make_unique(value_arg); +} + + +EncodableList ConnectionPlatformConfig::ToEncodableList() const { + EncodableList list; + list.reserve(1); + list.push_back(apple_ ? CustomEncodableValue(*apple_) : EncodableValue()); + return list; +} + +ConnectionPlatformConfig ConnectionPlatformConfig::FromEncodableList(const EncodableList& list) { + ConnectionPlatformConfig decoded; + auto& encodable_apple = list[0]; + if (!encodable_apple.IsNull()) { + decoded.set_apple(std::any_cast(std::get(encodable_apple))); + } + return decoded; +} + +bool ConnectionPlatformConfig::operator==(const ConnectionPlatformConfig& other) const { + return PigeonInternalDeepEquals(apple_, other.apple_); +} + +bool ConnectionPlatformConfig::operator!=(const ConnectionPlatformConfig& other) const { + return !(*this == other); +} + +size_t ConnectionPlatformConfig::Hash() const { + size_t result = 1; + result = result * 31 + PigeonInternalDeepHash(apple_); + return result; +} + +size_t PigeonInternalDeepHash(const ConnectionPlatformConfig& v) { + return v.Hash(); +} + // PeripheralAndroidOptions PeripheralAndroidOptions::PeripheralAndroidOptions() {} @@ -1908,24 +2067,30 @@ EncodableValue PigeonInternalCodecSerializer::ReadValueOfType( return CustomEncodableValue(UniversalManufacturerData::FromEncodableList(std::get(ReadValue(stream)))); } case 154: { - return CustomEncodableValue(PeripheralAndroidOptions::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(AppleConnectionOptions::FromEncodableList(std::get(ReadValue(stream)))); } case 155: { - return CustomEncodableValue(PeripheralPlatformConfig::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(ConnectionPlatformConfig::FromEncodableList(std::get(ReadValue(stream)))); } case 156: { - return CustomEncodableValue(PeripheralService::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(PeripheralAndroidOptions::FromEncodableList(std::get(ReadValue(stream)))); } case 157: { - return CustomEncodableValue(PeripheralCharacteristic::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(PeripheralPlatformConfig::FromEncodableList(std::get(ReadValue(stream)))); } case 158: { - return CustomEncodableValue(PeripheralDescriptor::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(PeripheralService::FromEncodableList(std::get(ReadValue(stream)))); } case 159: { - return CustomEncodableValue(PeripheralReadRequestResult::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(PeripheralCharacteristic::FromEncodableList(std::get(ReadValue(stream)))); } case 160: { + return CustomEncodableValue(PeripheralDescriptor::FromEncodableList(std::get(ReadValue(stream)))); + } + case 161: { + return CustomEncodableValue(PeripheralReadRequestResult::FromEncodableList(std::get(ReadValue(stream)))); + } + case 162: { return CustomEncodableValue(PeripheralWriteRequestResult::FromEncodableList(std::get(ReadValue(stream)))); } default: @@ -2062,38 +2227,48 @@ void PigeonInternalCodecSerializer::WriteValue( WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } - if (custom_value->type() == typeid(PeripheralAndroidOptions)) { + if (custom_value->type() == typeid(AppleConnectionOptions)) { stream->WriteByte(154); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + if (custom_value->type() == typeid(ConnectionPlatformConfig)) { + stream->WriteByte(155); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + if (custom_value->type() == typeid(PeripheralAndroidOptions)) { + stream->WriteByte(156); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(PeripheralPlatformConfig)) { - stream->WriteByte(155); + stream->WriteByte(157); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(PeripheralService)) { - stream->WriteByte(156); + stream->WriteByte(158); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(PeripheralCharacteristic)) { - stream->WriteByte(157); + stream->WriteByte(159); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(PeripheralDescriptor)) { - stream->WriteByte(158); + stream->WriteByte(160); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(PeripheralReadRequestResult)) { - stream->WriteByte(159); + stream->WriteByte(161); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(PeripheralWriteRequestResult)) { - stream->WriteByte(160); + stream->WriteByte(162); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } @@ -2323,7 +2498,9 @@ void UniversalBlePlatformChannel::SetUp( const auto& device_id_arg = std::get(encodable_device_id_arg); const auto& encodable_auto_connect_arg = args.at(1); const auto* auto_connect_arg = std::get_if(&encodable_auto_connect_arg); - std::optional output = api->Connect(device_id_arg, auto_connect_arg); + const auto& encodable_platform_config_arg = args.at(2); + const auto* platform_config_arg = encodable_platform_config_arg.IsNull() ? nullptr : &(std::any_cast(std::get(encodable_platform_config_arg))); + std::optional output = api->Connect(device_id_arg, auto_connect_arg, platform_config_arg); if (output.has_value()) { reply(WrapError(output.value())); return; diff --git a/windows/src/generated/universal_ble.g.h b/windows/src/generated/universal_ble.g.h index ac2f84b7..c8f8ec43 100644 --- a/windows/src/generated/universal_ble.g.h +++ b/windows/src/generated/universal_ble.g.h @@ -722,6 +722,105 @@ class UniversalManufacturerData { }; +// Apple options for `connect`. +// +// Each flag maps to a `CBConnectPeripheralOptionNotifyOn*Key` connect option. +// When `true`, the system displays an alert for the corresponding event if +// the app is suspended while it occurs, relaunching the app into the +// background to handle it. This keeps a backgrounded central responsive — +// e.g. while reconnecting to a previously paired peripheral — without the +// user having to foreground the app. Requires the `bluetooth-central` +// background mode on iOS. +// +// All flags default to `false` (no alerts, background events are not +// delivered while suspended). +// +// Generated class from Pigeon that represents data sent in messages. +class AppleConnectionOptions { + public: + // Constructs an object setting all non-nullable fields. + AppleConnectionOptions(); + + // Constructs an object setting all fields. + explicit AppleConnectionOptions( + const bool* notify_on_connection, + const bool* notify_on_disconnection, + const bool* notify_on_notification); + + // `CBConnectPeripheralOptionNotifyOnConnectionKey`: + // notify when a connection to the device succeeds while the app is suspended. + const bool* notify_on_connection() const; + void set_notify_on_connection(const bool* value_arg); + void set_notify_on_connection(bool value_arg); + + // `CBConnectPeripheralOptionNotifyOnDisconnectionKey`: + // notify when the peripheral disconnects while the app is suspended. + const bool* notify_on_disconnection() const; + void set_notify_on_disconnection(const bool* value_arg); + void set_notify_on_disconnection(bool value_arg); + + // `CBConnectPeripheralOptionNotifyOnNotificationKey`: + // notify when a characteristic notification arrives while the app is + // suspended. Fires per notification. + const bool* notify_on_notification() const; + void set_notify_on_notification(const bool* value_arg); + void set_notify_on_notification(bool value_arg); + + bool operator==(const AppleConnectionOptions& other) const; + bool operator!=(const AppleConnectionOptions& other) const; + /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. + size_t Hash() const; + private: + static AppleConnectionOptions FromEncodableList(const ::flutter::EncodableList& list); + ::flutter::EncodableList ToEncodableList() const; + friend class ConnectionPlatformConfig; + friend class UniversalBlePlatformChannel; + friend class UniversalBleCallbackChannel; + friend class UniversalBlePeripheralChannel; + friend class UniversalBleAndroidChannel; + friend class UniversalBlePeripheralCallback; + friend class PigeonInternalCodecSerializer; + std::optional notify_on_connection_; + std::optional notify_on_disconnection_; + std::optional notify_on_notification_; +}; + + +// Generated class from Pigeon that represents data sent in messages. +class ConnectionPlatformConfig { + public: + // Constructs an object setting all non-nullable fields. + ConnectionPlatformConfig(); + + // Constructs an object setting all fields. + explicit ConnectionPlatformConfig(const AppleConnectionOptions* apple); + + ~ConnectionPlatformConfig() = default; + ConnectionPlatformConfig(const ConnectionPlatformConfig& other); + ConnectionPlatformConfig& operator=(const ConnectionPlatformConfig& other); + ConnectionPlatformConfig(ConnectionPlatformConfig&& other) = default; + ConnectionPlatformConfig& operator=(ConnectionPlatformConfig&& other) noexcept = default; + const AppleConnectionOptions* apple() const; + void set_apple(const AppleConnectionOptions* value_arg); + void set_apple(const AppleConnectionOptions& value_arg); + + bool operator==(const ConnectionPlatformConfig& other) const; + bool operator!=(const ConnectionPlatformConfig& other) const; + /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. + size_t Hash() const; + private: + static ConnectionPlatformConfig FromEncodableList(const ::flutter::EncodableList& list); + ::flutter::EncodableList ToEncodableList() const; + friend class UniversalBlePlatformChannel; + friend class UniversalBleCallbackChannel; + friend class UniversalBlePeripheralChannel; + friend class UniversalBleAndroidChannel; + friend class UniversalBlePeripheralCallback; + friend class PigeonInternalCodecSerializer; + std::unique_ptr apple_; +}; + + // Generated class from Pigeon that represents data sent in messages. class PeripheralAndroidOptions { public: @@ -1063,7 +1162,8 @@ class UniversalBlePlatformChannel { virtual ErrorOr IsScanning() = 0; virtual std::optional Connect( const std::string& device_id, - const bool* auto_connect) = 0; + const bool* auto_connect, + const ConnectionPlatformConfig* platform_config) = 0; virtual std::optional Disconnect(const std::string& device_id) = 0; virtual void SetNotifiable( const std::string& device_id, diff --git a/windows/src/universal_ble_plugin.cpp b/windows/src/universal_ble_plugin.cpp index ee5601e9..d2326ba5 100644 --- a/windows/src/universal_ble_plugin.cpp +++ b/windows/src/universal_ble_plugin.cpp @@ -282,8 +282,10 @@ UniversalBlePlugin::SetLogLevel(const BleLogLevel &log_level) { std::optional UniversalBlePlugin::Connect(const std::string &device_id, - const bool *auto_connect) { + const bool *auto_connect, + const ConnectionPlatformConfig *platform_config) { // Note: autoConnect is not directly supported on Windows platform + // Note: platformConfig only carries Apple-specific options ConnectAsync(str_to_mac_address(device_id)); return std::nullopt; }; diff --git a/windows/src/universal_ble_plugin.h b/windows/src/universal_ble_plugin.h index 39e398b2..48e7d218 100644 --- a/windows/src/universal_ble_plugin.h +++ b/windows/src/universal_ble_plugin.h @@ -242,7 +242,8 @@ class UniversalBlePlugin : public flutter::Plugin, StartScan(const UniversalScanFilter *filter, const UniversalScanConfig *config) override; std::optional StopScan() override; ErrorOr IsScanning() override; - std::optional Connect(const std::string &device_id, const bool *auto_connect) override; + std::optional Connect(const std::string &device_id, const bool *auto_connect, + const ConnectionPlatformConfig *platform_config) override; std::optional Disconnect(const std::string &device_id) override; ErrorOr HasPermissions(bool with_android_fine_location) override; void RequestPermissions(