diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 37b30eb9..af29de6c 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,7 +1,18 @@ +## 2.0.0 +This version is introducing the following breaking changes: +* Add parameter requireLocationServicesEnabled to Ble scan that can toggle requirement of location services to be running +* Make filter on advertising services optional and add possibility to filter on more services +* Add manufacturer specific data to scanresults +* Remove global set error handler java + +Other improvements: +* Improvements for example app +* Add support for Flutter hot reload on iOS + ## 1.1.0 * Add RSSI value to discovered device results -* Improved parsing of UUIDs -* Migrated to latest Android plugin binding +* Improve parsing of UUIDs +* Migrate to latest Android plugin binding * Small improvements ## 1.0.2 diff --git a/README.md b/README.md index 8f07e91c..dd3ae4d4 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ The reactive BLE lib supports the following: Discovering BLE devices should be done like this: ```dart -reactivebleclient.scanForDevices(withService: uuid, scanMode: ScanMode.lowLatency).listen((device) { +reactivebleclient.scanForDevices(withServices: [serviceId], scanMode: ScanMode.lowLatency).listen((device) { //code for handling results }, onError: () { //code for handling error @@ -30,7 +30,7 @@ reactivebleclient.scanForDevices(withService: uuid, scanMode: ScanMode.lowLatenc ``` -The `withService` parameter is required. The parameter `scanMode` is only used on Android and follows the conventions described on [ScanSettings](https://developer.android.com/reference/android/bluetooth/le/ScanSettings#SCAN_MODE_BALANCED) Android reference page. If `scanMode` is omitted the balanced scan mode will be used. +The `withServices` parameter specifies the advertised service IDs to look for. If an empty list is passed, all the advertising devices will be reported. The parameter `scanMode` is only used on Android and follows the conventions described on [ScanSettings](https://developer.android.com/reference/android/bluetooth/le/ScanSettings#SCAN_MODE_BALANCED) Android reference page. If `scanMode` is omitted the balanced scan mode will be used. ### Establishing connection diff --git a/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/BleClient.kt b/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/BleClient.kt index dd605ab8..66dffef6 100644 --- a/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/BleClient.kt +++ b/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/BleClient.kt @@ -15,7 +15,7 @@ interface BleClient { val connectionUpdateSubject: BehaviorSubject fun initializeClient() - fun scanForDevices(service: ParcelUuid, scanMode: ScanMode): Observable + fun scanForDevices(services: List, scanMode: ScanMode, requireLocationServicesEnabled: Boolean): Observable fun connectToDevice(deviceId: String, timeout: Duration) fun disconnectDevice(deviceId: String) fun disconnectAllDevices() diff --git a/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/BleWrapper.kt b/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/BleWrapper.kt index f73801e0..e9c6fa64 100644 --- a/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/BleWrapper.kt +++ b/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/BleWrapper.kt @@ -54,7 +54,7 @@ enum class BleStatus(val code: Int) { UNSUPPORTED(code = 1), UNAUTHORIZED(code = 2), POWERED_OFF(code = 3), - DISCOVERY_DISABLED(code = 4), + LOCATION_SERVICES_DISABLED(code = 4), READY(code = 5) } diff --git a/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/ReactiveBleClient.kt b/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/ReactiveBleClient.kt index 0dc08a4b..37900182 100644 --- a/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/ReactiveBleClient.kt +++ b/android/src/main/kotlin/com/signify/hue/flutterreactiveble/ble/ReactiveBleClient.kt @@ -27,6 +27,16 @@ import io.reactivex.subjects.BehaviorSubject import timber.log.Timber import java.util.UUID import java.util.concurrent.TimeUnit +import kotlin.collections.List +import kotlin.collections.MutableMap +import kotlin.collections.asList +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.forEach +import kotlin.collections.getOrPut +import kotlin.collections.map +import kotlin.collections.mutableMapOf +import kotlin.collections.toTypedArray @Suppress("TooManyFunctions") open class ReactiveBleClient(private val context: Context) : BleClient { @@ -49,201 +59,211 @@ open class ReactiveBleClient(private val context: Context) : BleClient { Timber.d("Created bleclient") } - override fun scanForDevices(service: ParcelUuid, scanMode: ScanMode): Observable { - return rxBleClient.scanBleDevices( - ScanSettings.Builder() - .setScanMode(scanMode.toScanSettings()) - .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) - .build(), - ScanFilter.Builder() - .setServiceUuid(service) - .build() - ) - .map { result -> - ScanInfo(result.bleDevice.macAddress, result.bleDevice.name - ?: "", - result.rssi, - result.scanRecord.serviceData.mapKeys { it.key.uuid }, - extractManufacturerData(result.scanRecord.manufacturerSpecificData)) - } - } +/*yes spread operator is not performant but after kotlin v1.60 it is less bad and it is also the +recommended way to call varargs in java https://kotlinlang.org/docs/reference/java-interop.html#java-varargs +*/ +@Suppress("SpreadOperator") +override fun scanForDevices(services: List, scanMode: ScanMode, requireLocationServicesEnabled: Boolean): Observable { - override fun connectToDevice(deviceId: String, timeout: Duration) { - allConnections.add(getConnection(deviceId, timeout) - .subscribe({ result -> - when (result) { - is EstablishedConnection -> { - Timber.d("Connection established for ${result.deviceId}") - } - is EstablishConnectionFailure -> { - Timber.d("Connect ${result.deviceId} fails: ${result.errorMessage}") - connectionUpdateSubject.onNext(ConnectionUpdateError(deviceId, result.errorMessage)) - } + val filters = services.map { service -> + ScanFilter.Builder() + .setServiceUuid(service) + .build() + }.toTypedArray() + + return rxBleClient.scanBleDevices( + ScanSettings.Builder() + .setScanMode(scanMode.toScanSettings()) + .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) + .setShouldCheckLocationServicesState(requireLocationServicesEnabled) + .build(), + *filters + ) + .map { result -> + ScanInfo(result.bleDevice.macAddress, result.bleDevice.name + ?: "", + result.rssi, + result.scanRecord.serviceData.mapKeys { it.key.uuid }, + extractManufacturerData(result.scanRecord.manufacturerSpecificData)) + } +} + +override fun connectToDevice(deviceId: String, timeout: Duration) { + allConnections.add(getConnection(deviceId, timeout) + .subscribe({ result -> + when (result) { + is EstablishedConnection -> { + Timber.d("Connection established for ${result.deviceId}") } - }, { error -> - connectionUpdateSubject.onNext(ConnectionUpdateError(deviceId, error?.message - ?: "unknown error")) - })) - } + is EstablishConnectionFailure -> { + Timber.d("Connect ${result.deviceId} fails: ${result.errorMessage}") + connectionUpdateSubject.onNext(ConnectionUpdateError(deviceId, result.errorMessage)) + } + } + }, { error -> + connectionUpdateSubject.onNext(ConnectionUpdateError(deviceId, error?.message + ?: "unknown error")) + })) +} - override fun disconnectDevice(deviceId: String) { - activeConnections[deviceId]?.disconnectDevice() - activeConnections.remove(deviceId) - } +override fun disconnectDevice(deviceId: String) { + activeConnections[deviceId]?.disconnectDevice() + activeConnections.remove(deviceId) +} - override fun disconnectAllDevices() { - activeConnections.forEach { (_, connector) -> connector.disconnectDevice() } - allConnections.dispose() - } +override fun disconnectAllDevices() { + activeConnections.forEach { (_, connector) -> connector.disconnectDevice() } + allConnections.dispose() +} - override fun clearGattCache(deviceId: String): Completable = - activeConnections[deviceId]?.let(DeviceConnector::clearGattCache) - ?: Completable.error(IllegalStateException("Device is not connected")) +override fun clearGattCache(deviceId: String): Completable = + activeConnections[deviceId]?.let(DeviceConnector::clearGattCache) + ?: Completable.error(IllegalStateException("Device is not connected")) - override fun readCharacteristic(deviceId: String, characteristic: UUID): Single = - getConnection(deviceId).flatMapSingle { connectionResult -> - when (connectionResult) { - is EstablishedConnection -> - connectionResult.rxConnection.readCharacteristic(characteristic) - /* - On Android7 the ble stack frequently gives incorrectly - the error GAT_AUTH_FAIL(137) when reading char that will establish - the bonding with the peripheral. By retrying the operation once we - deviate between this flaky one time error and real auth failed cases - */ - .retry(1) { Build.VERSION.SDK_INT < Build.VERSION_CODES.O } - .map { value -> - CharOperationSuccessful(deviceId, value.asList()) - } - is EstablishConnectionFailure -> - Single.just( - CharOperationFailed(deviceId, - "failed to connect ${connectionResult.errorMessage}")) - } - }.first(CharOperationFailed(deviceId, "read char failed")) - - override fun writeCharacteristicWithResponse( - deviceId: String, - characteristic: UUID, - value: ByteArray - ): Single = - executeWriteOperation(deviceId, characteristic, value, RxBleConnection::writeCharWithResponse) - - override fun writeCharacteristicWithoutResponse( - deviceId: String, - characteristic: UUID, - value: ByteArray - ): Single = - - executeWriteOperation(deviceId, characteristic, value, RxBleConnection::writeCharWithoutResponse) - - override fun setupNotification(deviceId: String, characteristic: UUID): Observable { - return getConnection(deviceId) - .flatMap { deviceConnection -> setupNotificationOrIndication(deviceConnection, characteristic) } - // now we have setup the subscription and we want the actual value - .doOnNext { Timber.d("subscription established") } - .flatMap { notificationObservable -> - notificationObservable - } - } +override fun readCharacteristic(deviceId: String, characteristic: UUID): Single = + getConnection(deviceId).flatMapSingle { connectionResult -> + when (connectionResult) { + is EstablishedConnection -> + connectionResult.rxConnection.readCharacteristic(characteristic) + /* + On Android7 the ble stack frequently gives incorrectly + the error GAT_AUTH_FAIL(137) when reading char that will establish + the bonding with the peripheral. By retrying the operation once we + deviate between this flaky one time error and real auth failed cases + */ + .retry(1) { Build.VERSION.SDK_INT < Build.VERSION_CODES.O } + .map { value -> + CharOperationSuccessful(deviceId, value.asList()) + } + is EstablishConnectionFailure -> + Single.just( + CharOperationFailed(deviceId, + "failed to connect ${connectionResult.errorMessage}")) + } + }.first(CharOperationFailed(deviceId, "read char failed")) - override fun negotiateMtuSize(deviceId: String, size: Int): Single = - getConnection(deviceId).flatMapSingle { connectionResult -> - when (connectionResult) { - is EstablishedConnection -> connectionResult.rxConnection.requestMtu(size) - .map { value -> MtuNegotiateSuccesful(deviceId, value) } +override fun writeCharacteristicWithResponse( + deviceId: String, + characteristic: UUID, + value: ByteArray +): Single = + executeWriteOperation(deviceId, characteristic, value, RxBleConnection::writeCharWithResponse) - is EstablishConnectionFailure -> - Single.just(MtuNegotiateFailed(deviceId, - "failed to connect ${connectionResult.errorMessage}")) - } - }.first(MtuNegotiateFailed(deviceId, "negotiate mtu timed out")) +override fun writeCharacteristicWithoutResponse( + deviceId: String, + characteristic: UUID, + value: ByteArray +): Single = + + executeWriteOperation(deviceId, characteristic, value, RxBleConnection::writeCharWithoutResponse) - override fun observeBleStatus(): Observable = rxBleClient.observeStateChanges() - .startWith(rxBleClient.state) - .map { it.toBleState() } +override fun setupNotification(deviceId: String, characteristic: UUID): Observable { + return getConnection(deviceId) + .flatMap { deviceConnection -> setupNotificationOrIndication(deviceConnection, characteristic) } + // now we have setup the subscription and we want the actual value + .doOnNext { Timber.d("subscription established") } + .flatMap { notificationObservable -> + notificationObservable + } +} - @VisibleForTesting - internal open fun createDeviceConnector(device: RxBleDevice, timeout: Duration) = - DeviceConnector(device, timeout, connectionUpdateSubject::onNext, connectionQueue) +override fun negotiateMtuSize(deviceId: String, size: Int): Single = + getConnection(deviceId).flatMapSingle { connectionResult -> + when (connectionResult) { + is EstablishedConnection -> connectionResult.rxConnection.requestMtu(size) + .map { value -> MtuNegotiateSuccesful(deviceId, value) } - private fun getConnection( - deviceId: String, - timeout: Duration = Duration(0, TimeUnit.MILLISECONDS) - ): Observable { - val device = rxBleClient.getBleDevice(deviceId) - val connector = activeConnections.getOrPut(deviceId) { createDeviceConnector(device, timeout) } + is EstablishConnectionFailure -> + Single.just(MtuNegotiateFailed(deviceId, + "failed to connect ${connectionResult.errorMessage}")) + } + }.first(MtuNegotiateFailed(deviceId, "negotiate mtu timed out")) - return connector.connection - } +override fun observeBleStatus(): Observable = rxBleClient.observeStateChanges() + .startWith(rxBleClient.state) + .map { it.toBleState() } - private fun executeWriteOperation( - deviceId: String, - characteristic: UUID, - value: ByteArray, - bleOperation: RxBleConnection.(characteristic: UUID, value: ByteArray) -> Single - ): Single { - return getConnection(deviceId) - .flatMapSingle { connectionResult -> - when (connectionResult) { - is EstablishedConnection -> { - connectionResult.rxConnection.bleOperation(characteristic, value) - .map { value -> CharOperationSuccessful(deviceId, value.asList()) } - } - is EstablishConnectionFailure -> { - Timber.d("Failed conn for write ") - Single.just( - CharOperationFailed(deviceId, - "failed to connect ${connectionResult.errorMessage}")) - } +@VisibleForTesting +internal open fun createDeviceConnector(device: RxBleDevice, timeout: Duration) = + DeviceConnector(device, timeout, connectionUpdateSubject::onNext, connectionQueue) + +private fun getConnection( + deviceId: String, + timeout: Duration = Duration(0, TimeUnit.MILLISECONDS) +): Observable { + val device = rxBleClient.getBleDevice(deviceId) + val connector = activeConnections.getOrPut(deviceId) { createDeviceConnector(device, timeout) } + + return connector.connection +} + +private fun executeWriteOperation( + deviceId: String, + characteristic: UUID, + value: ByteArray, + bleOperation: RxBleConnection.(characteristic: UUID, value: ByteArray) -> Single +): Single { + return getConnection(deviceId) + .flatMapSingle { connectionResult -> + when (connectionResult) { + is EstablishedConnection -> { + connectionResult.rxConnection.bleOperation(characteristic, value) + .map { value -> CharOperationSuccessful(deviceId, value.asList()) } } - }.first(CharOperationFailed(deviceId, "Writechar timed-out")) - } + is EstablishConnectionFailure -> { + Timber.d("Failed conn for write ") + Single.just( + CharOperationFailed(deviceId, + "failed to connect ${connectionResult.errorMessage}")) + } + } + }.first(CharOperationFailed(deviceId, "Writechar timed-out")) +} - private fun setupNotificationOrIndication( - deviceConnection: EstablishConnectionResult, - characteristic: UUID - ): Observable> = - - when (deviceConnection) { - is EstablishedConnection -> { - deviceConnection.rxConnection.discoverServices() - .flatMap { deviceServices -> deviceServices.getCharacteristic(characteristic) } - .flatMapObservable { char -> - if ((char.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) { - deviceConnection.rxConnection.setupNotification(characteristic) - } else { - deviceConnection.rxConnection.setupIndication(characteristic) - } +private fun setupNotificationOrIndication( + deviceConnection: EstablishConnectionResult, + characteristic: UUID +): Observable> = + + when (deviceConnection) { + is EstablishedConnection -> { + deviceConnection.rxConnection.discoverServices() + .flatMap { deviceServices -> deviceServices.getCharacteristic(characteristic) } + .flatMapObservable { char -> + if ((char.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) { + deviceConnection.rxConnection.setupNotification(characteristic) + } else { + deviceConnection.rxConnection.setupIndication(characteristic) } - } - is EstablishConnectionFailure -> { - Observable.just(Observable.empty()) - } + } + } + is EstablishConnectionFailure -> { + Observable.just(Observable.empty()) } + } - override fun requestConnectionPriority( - deviceId: String, - priority: ConnectionPriority - ): Single = - getConnection(deviceId).switchMapSingle { connectionResult -> - when (connectionResult) { - is EstablishedConnection -> - connectionResult.rxConnection.requestConnectionPriority(priority.code, 2, TimeUnit.SECONDS) - .toSingle { - RequestConnectionPrioritySuccess(deviceId) - } - is EstablishConnectionFailure -> Single.fromCallable { - RequestConnectionPriorityFailed(deviceId, connectionResult.errorMessage) - } +override fun requestConnectionPriority( + deviceId: String, + priority: ConnectionPriority +): Single = + getConnection(deviceId).switchMapSingle { connectionResult -> + when (connectionResult) { + is EstablishedConnection -> + connectionResult.rxConnection.requestConnectionPriority(priority.code, 2, TimeUnit.SECONDS) + .toSingle { + RequestConnectionPrioritySuccess(deviceId) + } + is EstablishConnectionFailure -> Single.fromCallable { + RequestConnectionPriorityFailed(deviceId, connectionResult.errorMessage) } - }.first(RequestConnectionPriorityFailed(deviceId, "Unknown failure")) - - // enable this for extra debug output on the android stack - private fun enableDebugLogging() = RxBleClient - .updateLogOptions(LogOptions.Builder().setLogLevel(LogConstants.VERBOSE) - .setMacAddressLogSetting(LogConstants.MAC_ADDRESS_FULL) - .setUuidsLogSetting(LogConstants.UUIDS_FULL) - .setShouldLogAttributeValues(true) - .build()) + } + }.first(RequestConnectionPriorityFailed(deviceId, "Unknown failure")) + +// enable this for extra debug output on the android stack +private fun enableDebugLogging() = RxBleClient + .updateLogOptions(LogOptions.Builder().setLogLevel(LogConstants.VERBOSE) + .setMacAddressLogSetting(LogConstants.MAC_ADDRESS_FULL) + .setUuidsLogSetting(LogConstants.UUIDS_FULL) + .setShouldLogAttributeValues(true) + .build()) } diff --git a/android/src/main/kotlin/com/signify/hue/flutterreactiveble/channelhandlers/ScanDevicesHandler.kt b/android/src/main/kotlin/com/signify/hue/flutterreactiveble/channelhandlers/ScanDevicesHandler.kt index 4eeab622..65f4c77c 100644 --- a/android/src/main/kotlin/com/signify/hue/flutterreactiveble/channelhandlers/ScanDevicesHandler.kt +++ b/android/src/main/kotlin/com/signify/hue/flutterreactiveble/channelhandlers/ScanDevicesHandler.kt @@ -19,33 +19,33 @@ class ScanDevicesHandler(private val bleClient: com.signify.hue.flutterreactiveb private var scanParameters: ScanParameters? = null override fun onListen(objectSink: Any?, eventSink: EventChannel.EventSink?) { - eventSink?.let { scanDevicesSink = eventSink - startDeviceScan() + startDeviceScan() } } override fun onCancel(objectSink: Any?) { - Timber.d("Scanfordevices cancelled called") + Timber.d("Scanning canceled") stopDeviceScan() scanDevicesSink = null } private fun startDeviceScan() { - scanParameters?.let { params -> - scanForDevicesDisposable = bleClient.scanForDevices(params.filter, params.mode) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { scanResult -> - handleDeviceScanResult(converter.convertScanInfo(scanResult)) - }, - { throwable -> - Timber.d("Error while scanning for devices: ${throwable.message}") - handleDeviceScanResult(converter.convertScanErrorInfo(throwable.message)) - } - ) - } ?: handleDeviceScanResult(converter.convertScanErrorInfo("Scanparameters are not being set")) + scanParameters?.let { params -> + scanForDevicesDisposable = bleClient.scanForDevices(params.filter, params.mode, params.locationServiceIsMandatory) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { scanResult -> + handleDeviceScanResult(converter.convertScanInfo(scanResult)) + }, + { throwable -> + Timber.d("Error while scanning for devices: ${throwable.message}") + handleDeviceScanResult(converter.convertScanErrorInfo(throwable.message)) + } + ) + } + ?: handleDeviceScanResult(converter.convertScanErrorInfo("Scanning parameters are not set")) } fun stopDeviceScan() { @@ -54,11 +54,11 @@ class ScanDevicesHandler(private val bleClient: com.signify.hue.flutterreactiveb } fun prepareScan(scanMessage: pb.ScanForDevicesRequest) { - val scanUuid = scanMessage.serviceUuid.data.toByteArray() - val filter = ParcelUuid(UuidConverter().uuidFromByteArray(scanUuid)) + val filter = scanMessage.serviceUuidsList + .map { ParcelUuid(UuidConverter().uuidFromByteArray(it.data.toByteArray())) } val scanMode = createScanMode(scanMessage.scanMode) - scanParameters = ScanParameters(filter, scanMode) + scanParameters = ScanParameters(filter, scanMode, scanMessage.requireLocationServicesEnabled) } private fun handleDeviceScanResult(discoveryMessage: pb.DeviceScanInfo) { @@ -66,4 +66,4 @@ class ScanDevicesHandler(private val bleClient: com.signify.hue.flutterreactiveb } } -private data class ScanParameters(val filter: ParcelUuid, val mode: ScanMode) +private data class ScanParameters(val filter: List, val mode: ScanMode, val locationServiceIsMandatory: Boolean) diff --git a/android/src/main/kotlin/com/signify/hue/flutterreactiveble/utils/BleWrapperExtensions.kt b/android/src/main/kotlin/com/signify/hue/flutterreactiveble/utils/BleWrapperExtensions.kt index 9b220b89..c4a120dd 100644 --- a/android/src/main/kotlin/com/signify/hue/flutterreactiveble/utils/BleWrapperExtensions.kt +++ b/android/src/main/kotlin/com/signify/hue/flutterreactiveble/utils/BleWrapperExtensions.kt @@ -9,19 +9,19 @@ import com.polidea.rxandroidble2.RxBleClient.State.READY import com.signify.hue.flutterreactiveble.ble.BleStatus import com.signify.hue.flutterreactiveble.ble.ConnectionPriority -fun RxBleClient.State.toBleState(): com.signify.hue.flutterreactiveble.ble.BleStatus = +fun RxBleClient.State.toBleState(): BleStatus = when (this) { - BLUETOOTH_NOT_AVAILABLE -> com.signify.hue.flutterreactiveble.ble.BleStatus.UNSUPPORTED - LOCATION_PERMISSION_NOT_GRANTED -> com.signify.hue.flutterreactiveble.ble.BleStatus.UNAUTHORIZED - BLUETOOTH_NOT_ENABLED -> com.signify.hue.flutterreactiveble.ble.BleStatus.POWERED_OFF - LOCATION_SERVICES_NOT_ENABLED -> com.signify.hue.flutterreactiveble.ble.BleStatus.DISCOVERY_DISABLED - READY -> com.signify.hue.flutterreactiveble.ble.BleStatus.READY + BLUETOOTH_NOT_AVAILABLE -> BleStatus.UNSUPPORTED + LOCATION_PERMISSION_NOT_GRANTED -> BleStatus.UNAUTHORIZED + BLUETOOTH_NOT_ENABLED -> BleStatus.POWERED_OFF + LOCATION_SERVICES_NOT_ENABLED -> BleStatus.LOCATION_SERVICES_DISABLED + READY -> BleStatus.READY } fun Int.toConnectionPriority() = when (this) { - 0 -> com.signify.hue.flutterreactiveble.ble.ConnectionPriority.BALANCED - 1 -> com.signify.hue.flutterreactiveble.ble.ConnectionPriority.HIGH_PERFORMACE - 2 -> com.signify.hue.flutterreactiveble.ble.ConnectionPriority.LOW_POWER - else -> com.signify.hue.flutterreactiveble.ble.ConnectionPriority.BALANCED + 0 -> ConnectionPriority.BALANCED + 1 -> ConnectionPriority.HIGH_PERFORMACE + 2 -> ConnectionPriority.LOW_POWER + else -> ConnectionPriority.BALANCED } diff --git a/example/lib/src/ble/ble_scanner.dart b/example/lib/src/ble/ble_scanner.dart index be72d126..190353d6 100644 --- a/example/lib/src/ble/ble_scanner.dart +++ b/example/lib/src/ble/ble_scanner.dart @@ -16,10 +16,11 @@ class BleScanner implements ReactiveState { @override Stream get state => _stateStreamController.stream; - void startScan(Uuid uuid) { + void startScan(List serviceIds) { _devices.clear(); _subscription?.cancel(); - _subscription = _ble.scanForDevices(withService: uuid).listen((device) { + _subscription = + _ble.scanForDevices(withServices: serviceIds).listen((device) { final knownDeviceIndex = _devices.indexWhere((d) => d.id == device.id); if (knownDeviceIndex >= 0) { _devices[knownDeviceIndex] = device; diff --git a/example/lib/src/ui/ble_status_screen.dart b/example/lib/src/ui/ble_status_screen.dart index ea6188e3..c2682556 100644 --- a/example/lib/src/ui/ble_status_screen.dart +++ b/example/lib/src/ui/ble_status_screen.dart @@ -16,8 +16,8 @@ class BleStatusScreen extends StatelessWidget { return "Authorize the FlutterReactiveBle example app to use Bluetooth and location"; case BleStatus.poweredOff: return "Bluetooth is powered off on your device turn it on"; - case BleStatus.discoveryDisabled: - return "Bluetooth discovery is disabled turn on location services"; + case BleStatus.locationServicesDisabled: + return "Enable location services"; case BleStatus.ready: return "Bluetooth is up and running"; default: diff --git a/example/lib/src/ui/device_list.dart b/example/lib/src/ui/device_list.dart index dab6ac79..b31fd8db 100644 --- a/example/lib/src/ui/device_list.dart +++ b/example/lib/src/ui/device_list.dart @@ -27,7 +27,7 @@ class _DeviceList extends StatefulWidget { assert(stopScan != null); final BleScannerState scannerState; - final void Function(Uuid) startScan; + final void Function(List) startScan; final VoidCallback stopScan; @override @@ -53,17 +53,21 @@ class _DeviceListState extends State<_DeviceList> { bool _isValidUuidInput() { final uuidText = _uuidController.text; - try { - Uuid.parse(uuidText); + if (uuidText.isEmpty) { return true; - } on Exception { - return false; + } else { + try { + Uuid.parse(uuidText); + return true; + } on Exception { + return false; + } } } void _startScanning() { - final uuid = Uuid.parse(_uuidController.text); - widget.startScan(uuid); + final text = _uuidController.text; + widget.startScan(text.isEmpty ? [] : [Uuid.parse(_uuidController.text)]); } @override diff --git a/example/pubspec.lock b/example/pubspec.lock index 67cde225..78c29f4b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -82,7 +82,7 @@ packages: path: ".." relative: true source: path - version: "1.1.0" + version: "2.0.0" flutter_test: dependency: "direct dev" description: flutter diff --git a/example/pubspec.yaml b/example/pubspec.yaml index c18ef0f3..44e56af2 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_reactive_ble_example description: Demonstrates how to use the flutter_reactive_ble plugin. -version: 1.1.0 +version: 2.0.0 publish_to: 'none' environment: diff --git a/ios/Classes/BleData/bledata.pb.swift b/ios/Classes/BleData/bledata.pb.swift index 2dedee2d..e1a1d89a 100644 --- a/ios/Classes/BleData/bledata.pb.swift +++ b/ios/Classes/BleData/bledata.pb.swift @@ -24,25 +24,15 @@ struct ScanForDevicesRequest { // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. - var serviceUuid: Uuid { - get {return _storage._serviceUuid ?? Uuid()} - set {_uniqueStorage()._serviceUuid = newValue} - } - /// Returns true if `serviceUuid` has been explicitly set. - var hasServiceUuid: Bool {return _storage._serviceUuid != nil} - /// Clears the value of `serviceUuid`. Subsequent reads from it will return its default value. - mutating func clearServiceUuid() {_uniqueStorage()._serviceUuid = nil} + var serviceUuids: [Uuid] = [] - var scanMode: Int32 { - get {return _storage._scanMode} - set {_uniqueStorage()._scanMode = newValue} - } + var scanMode: Int32 = 0 + + var requireLocationServicesEnabled: Bool = false var unknownFields = SwiftProtobuf.UnknownStorage() init() {} - - fileprivate var _storage = _StorageClass.defaultInstance } struct DeviceScanInfo { @@ -579,67 +569,39 @@ struct GenericFailure { extension ScanForDevicesRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "ScanForDevicesRequest" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "serviceUuid"), + 1: .same(proto: "serviceUuids"), 2: .same(proto: "scanMode"), + 3: .same(proto: "requireLocationServicesEnabled"), ] - fileprivate class _StorageClass { - var _serviceUuid: Uuid? = nil - var _scanMode: Int32 = 0 - - static let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _serviceUuid = source._serviceUuid - _scanMode = source._scanMode - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularMessageField(value: &_storage._serviceUuid) - case 2: try decoder.decodeSingularInt32Field(value: &_storage._scanMode) - default: break - } + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeRepeatedMessageField(value: &self.serviceUuids) + case 2: try decoder.decodeSingularInt32Field(value: &self.scanMode) + case 3: try decoder.decodeSingularBoolField(value: &self.requireLocationServicesEnabled) + default: break } } } func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if let v = _storage._serviceUuid { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } - if _storage._scanMode != 0 { - try visitor.visitSingularInt32Field(value: _storage._scanMode, fieldNumber: 2) - } + if !self.serviceUuids.isEmpty { + try visitor.visitRepeatedMessageField(value: self.serviceUuids, fieldNumber: 1) + } + if self.scanMode != 0 { + try visitor.visitSingularInt32Field(value: self.scanMode, fieldNumber: 2) + } + if self.requireLocationServicesEnabled != false { + try visitor.visitSingularBoolField(value: self.requireLocationServicesEnabled, fieldNumber: 3) } try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: ScanForDevicesRequest, rhs: ScanForDevicesRequest) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._serviceUuid != rhs_storage._serviceUuid {return false} - if _storage._scanMode != rhs_storage._scanMode {return false} - return true - } - if !storagesAreEqual {return false} - } + if lhs.serviceUuids != rhs.serviceUuids {return false} + if lhs.scanMode != rhs.scanMode {return false} + if lhs.requireLocationServicesEnabled != rhs.requireLocationServicesEnabled {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/ios/Classes/Plugin/PluginController.swift b/ios/Classes/Plugin/PluginController.swift index d6d5a450..928d49e2 100644 --- a/ios/Classes/Plugin/PluginController.swift +++ b/ios/Classes/Plugin/PluginController.swift @@ -6,7 +6,7 @@ import var CoreBluetooth.CBAdvertisementDataManufacturerDataKey final class PluginController { struct Scan { - let service: CBUUID + let services: [CBUUID] } private var central: Central? @@ -145,13 +145,7 @@ final class PluginController { assert(!central.isScanning) - guard args.hasServiceUuid && !args.serviceUuid.data.isEmpty - else { - completion(.failure(PluginError.invalidMethodCall(method: name, details: "\"serviceUuid\" is required").asFlutterError)) - return - } - - scan = StreamingTask(parameters: .init(service: CBUUID(data: args.serviceUuid.data))) + scan = StreamingTask(parameters: .init(services: args.serviceUuids.map({ uuid in CBUUID(data: uuid.data) }))) completion(.success(nil)) } @@ -165,7 +159,7 @@ final class PluginController { self.scan = scan.with(sink: sink) - central.scanForDevices(with: [scan.parameters.service]) + central.scanForDevices(with: scan.parameters.services) return nil } diff --git a/lib/src/generated/bledata.pb.dart b/lib/src/generated/bledata.pb.dart index 57cad58b..9c6ba15d 100644 --- a/lib/src/generated/bledata.pb.dart +++ b/lib/src/generated/bledata.pb.dart @@ -12,9 +12,11 @@ import 'package:protobuf/protobuf.dart' as $pb; class ScanForDevicesRequest extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo('ScanForDevicesRequest', createEmptyInstance: create) - ..aOM(1, 'serviceUuid', - protoName: 'serviceUuid', subBuilder: Uuid.create) + ..pc(1, 'serviceUuids', $pb.PbFieldType.PM, + protoName: 'serviceUuids', subBuilder: Uuid.create) ..a<$core.int>(2, 'scanMode', $pb.PbFieldType.O3, protoName: 'scanMode') + ..aOB(3, 'requireLocationServicesEnabled', + protoName: 'requireLocationServicesEnabled') ..hasRequiredFields = false; ScanForDevicesRequest._() : super(); @@ -42,18 +44,7 @@ class ScanForDevicesRequest extends $pb.GeneratedMessage { static ScanForDevicesRequest _defaultInstance; @$pb.TagNumber(1) - Uuid get serviceUuid => $_getN(0); - @$pb.TagNumber(1) - set serviceUuid(Uuid v) { - setField(1, v); - } - - @$pb.TagNumber(1) - $core.bool hasServiceUuid() => $_has(0); - @$pb.TagNumber(1) - void clearServiceUuid() => clearField(1); - @$pb.TagNumber(1) - Uuid ensureServiceUuid() => $_ensure(0); + $core.List get serviceUuids => $_getList(0); @$pb.TagNumber(2) $core.int get scanMode => $_getIZ(1); @@ -66,6 +57,18 @@ class ScanForDevicesRequest extends $pb.GeneratedMessage { $core.bool hasScanMode() => $_has(1); @$pb.TagNumber(2) void clearScanMode() => clearField(2); + + @$pb.TagNumber(3) + $core.bool get requireLocationServicesEnabled => $_getBF(2); + @$pb.TagNumber(3) + set requireLocationServicesEnabled($core.bool v) { + $_setBool(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasRequireLocationServicesEnabled() => $_has(2); + @$pb.TagNumber(3) + void clearRequireLocationServicesEnabled() => clearField(3); } class DeviceScanInfo extends $pb.GeneratedMessage { diff --git a/lib/src/generated/bledata.pbjson.dart b/lib/src/generated/bledata.pbjson.dart index f42c1e94..a7ee1362 100644 --- a/lib/src/generated/bledata.pbjson.dart +++ b/lib/src/generated/bledata.pbjson.dart @@ -9,14 +9,21 @@ const ScanForDevicesRequest$json = const { '1': 'ScanForDevicesRequest', '2': const [ const { - '1': 'serviceUuid', + '1': 'serviceUuids', '3': 1, - '4': 1, + '4': 3, '5': 11, '6': '.Uuid', - '10': 'serviceUuid' + '10': 'serviceUuids' }, const {'1': 'scanMode', '3': 2, '4': 1, '5': 5, '10': 'scanMode'}, + const { + '1': 'requireLocationServicesEnabled', + '3': 3, + '4': 1, + '5': 8, + '10': 'requireLocationServicesEnabled' + }, ], }; diff --git a/lib/src/model/ble_status.dart b/lib/src/model/ble_status.dart index 58839c43..98616564 100644 --- a/lib/src/model/ble_status.dart +++ b/lib/src/model/ble_status.dart @@ -3,6 +3,6 @@ enum BleStatus { unsupported, unauthorized, poweredOff, - discoveryDisabled, + locationServicesDisabled, ready } diff --git a/lib/src/model/scan_session.dart b/lib/src/model/scan_session.dart index 99abf493..9499a2c2 100644 --- a/lib/src/model/scan_session.dart +++ b/lib/src/model/scan_session.dart @@ -2,8 +2,8 @@ import 'package:flutter_reactive_ble/src/model/uuid.dart'; import 'package:meta/meta.dart'; class ScanSession { - final Uuid withService; + final List withServices; final Future future; - const ScanSession({@required this.withService, @required this.future}); + const ScanSession({@required this.withServices, @required this.future}); } diff --git a/lib/src/prescan_connector.dart b/lib/src/prescan_connector.dart index 7a2fe566..de46e279 100644 --- a/lib/src/prescan_connector.dart +++ b/lib/src/prescan_connector.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_reactive_ble/src/discovered_devices_registry.dart'; import 'package:flutter_reactive_ble/src/model/connection_state_update.dart'; @@ -27,22 +28,24 @@ class PrescanConnector { Duration connectionTimeout, }) connectDevice; - final Stream Function({Uuid withService, ScanMode scanMode}) - scanDevices; + final Stream Function({ + List withServices, + ScanMode scanMode, + }) scanDevices; final ScanSession Function() getCurrentScan; final Duration delayAfterScanFailure; Stream connectToAdvertisingDevice({ @required String id, - @required Uuid withService, + @required List withServices, @required Duration prescanDuration, Map> servicesWithCharacteristicsToDiscover, Duration connectionTimeout, }) { if (getCurrentScan() != null) { return awaitCurrentScanAndConnect( - withService, + withServices, prescanDuration, id, servicesWithCharacteristicsToDiscover, @@ -53,7 +56,7 @@ class PrescanConnector { id, servicesWithCharacteristicsToDiscover, connectionTimeout, - withService, + withServices, prescanDuration, ); } @@ -64,7 +67,7 @@ class PrescanConnector { String id, Map> servicesWithCharacteristicsToDiscover, Duration connectionTimeout, - Uuid withService, + List withServices, Duration prescanDuration, ) { if (scanRegistry.deviceIsDiscoveredRecently( @@ -77,7 +80,7 @@ class PrescanConnector { ); } else { final scanSubscription = - scanDevices(withService: withService, scanMode: ScanMode.lowLatency) + scanDevices(withServices: withServices, scanMode: ScanMode.lowLatency) .listen((DiscoveredDevice scanData) {}, onError: (Object _) {}); Future.delayed(prescanDuration).then((_) { scanSubscription.cancel(); @@ -94,14 +97,17 @@ class PrescanConnector { return connectIfRecentlyDiscovered( id, servicesWithCharacteristicsToDiscover, connectionTimeout); } else { - /*When the scan fails 99% of the times it is due to violation of the scan threshold: - https://blog.classycode.com/undocumented-android-7-ble-behavior-changes-d1a9bd87d983 . Previously we did - autoconnect but that gives slow connection times (up to 2 min) on a lot of devices. - */ + // When the scan fails 99% of the times it is due to violation of the scan threshold: + // https://blog.classycode.com/undocumented-android-7-ble-behavior-changes-d1a9bd87d983 + // + // Previously we used "autoconnect" but that gives slow connection times (up to 2 min) on a lot of devices. return Future.delayed(delayAfterScanFailure) .asStream() - .asyncExpand((_) => connectIfRecentlyDiscovered(id, - servicesWithCharacteristicsToDiscover, connectionTimeout)); + .asyncExpand((_) => connectIfRecentlyDiscovered( + id, + servicesWithCharacteristicsToDiscover, + connectionTimeout, + )); } }, ); @@ -110,13 +116,14 @@ class PrescanConnector { @visibleForTesting Stream awaitCurrentScanAndConnect( - Uuid withService, + List withServices, Duration prescanDuration, String id, Map> servicesWithCharacteristicsToDiscover, Duration connectionTimeout, ) { - if (getCurrentScan().withService == withService) { + if (const DeepCollectionEquality() + .equals(getCurrentScan().withServices, withServices)) { return getCurrentScan() .future .timeout(prescanDuration + const Duration(seconds: 1)) diff --git a/lib/src/reactive_ble.dart b/lib/src/reactive_ble.dart index ace060ad..df0b4185 100644 --- a/lib/src/reactive_ble.dart +++ b/lib/src/reactive_ble.dart @@ -220,14 +220,23 @@ class FlutterReactiveBle { .then((message) => message.result.dematerialize()); } - /// Scan for devices that are advertising a specific service - /// [scanMode] is used only on Android to enforce a more battery or latency intensive scan strategy + /// Scan for BLE peripherals advertising the services specified in [withServices] + /// or for all BLE peripherals, if no services is specified. It is recommended to always specify some services. /// - Stream scanForDevices( - {@required Uuid withService, ScanMode scanMode = ScanMode.balanced}) { + /// There are two Android specific parameters that are ignored on iOS: + /// + /// - [scanMode] allows to choose between different levels of power efficient and/or low latency scan modes. + /// - [requireLocationServicesEnabled] specifies whether to check if location services are enabled before scanning. + /// When set to true and location services are disabled, an exception is thrown. Default is true. + /// Setting the value to false can result in not finding BLE peripherals on some Android devices. + Stream scanForDevices({ + @required List withServices, + ScanMode scanMode = ScanMode.balanced, + bool requireLocationServicesEnabled = true, + }) { final completer = Completer(); _currentScan = - ScanSession(withService: withService, future: completer.future); + ScanSession(withServices: withServices, future: completer.future); final scanRepeater = Repeater( onListenEmitFrom: () => @@ -253,14 +262,20 @@ class FlutterReactiveBle { _scanStreamDisposable.set(scanRepeater); - final args = pb.ScanForDevicesRequest() - ..serviceUuid = (pb.Uuid()..data = withService.data) - ..scanMode = convertScanModeToArgs(scanMode); + final scanRequest = pb.ScanForDevicesRequest() + ..scanMode = convertScanModeToArgs(scanMode) + ..requireLocationServicesEnabled = requireLocationServicesEnabled; + + if (withServices != null) { + for (final withService in withServices) { + scanRequest.serviceUuids.add((pb.Uuid()..data = withService.data)); + } + } return initialize() .then((_) { _methodChannel.invokeMethod( - "scanForDevices", args.writeToBuffer()); + "scanForDevices", scanRequest.writeToBuffer()); }) .asStream() .asyncExpand((Object _) => scanRepeater.stream) @@ -325,14 +340,14 @@ class FlutterReactiveBle { Stream connectToAdvertisingDevice({ @required String id, - @required Uuid withService, + @required List withServices, @required Duration prescanDuration, Map> servicesWithCharacteristicsToDiscover, Duration connectionTimeout, }) => _prescanConnector.connectToAdvertisingDevice( id: id, - withService: withService, + withServices: withServices, prescanDuration: prescanDuration, servicesWithCharacteristicsToDiscover: servicesWithCharacteristicsToDiscover, diff --git a/protos/Readme.md b/protos/Readme.md index 30436f8d..5e87c554 100644 --- a/protos/Readme.md +++ b/protos/Readme.md @@ -5,12 +5,9 @@ ``` brew install swift-protobuf``` 3. If you don't have dart sdk on your computer please install it. ```brew isntall dart``` -4. Get the latest dart-protoc-plugin from https://github.com/dart-lang/dart-protoc-plugin -5. Go to dart-protoc-plugin-<> folder and call -```pub install``` -6. Add plugin path to PATH -7. Make sure protoc + protoc-gen-dart + dart bins are all in the same path -8. Run the following command from the protos folder +4. Run ```pub global activate protoc_plugin``` +5. OPTIONAL Add plugin path to PATH +6. Run the following command from the protos folder ``` protoc --dart_out=../lib/src/generated ./bledata.proto protoc --swift_out=../ios/Classes/BleData ./bledata.proto diff --git a/protos/bledata.proto b/protos/bledata.proto index 225db7a0..3ab431ba 100644 --- a/protos/bledata.proto +++ b/protos/bledata.proto @@ -3,8 +3,9 @@ option java_package = "com.signify.hue.flutterreactiveble"; option java_outer_classname = "ProtobufModel"; message ScanForDevicesRequest { - Uuid serviceUuid = 1; + repeated Uuid serviceUuids = 1; int32 scanMode = 2; + bool requireLocationServicesEnabled=3; } message DeviceScanInfo { diff --git a/pubspec.yaml b/pubspec.yaml index 5fd09b56..0494a16a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_reactive_ble description: Reactive Bluetooth low energy (BLE) plugin that can communicate with multiple devices -version: 1.1.0 +version: 2.0.0 homepage: https://github.com/PhilipsHue/flutter_reactive_ble environment: diff --git a/test/prescan_connector_test.dart b/test/prescan_connector_test.dart index 00bdec47..959ec589 100644 --- a/test/prescan_connector_test.dart +++ b/test/prescan_connector_test.dart @@ -19,7 +19,7 @@ abstract class _PrescanConnectorStub { Map> servicesWithCharacteristicsToDiscover, Duration connectionTimeout}); - Stream scan({Uuid withService, ScanMode scanMode}); + Stream scan({List withServices, ScanMode scanMode}); ScanSession currentScan(); } @@ -63,7 +63,7 @@ void main() { _sut.connectToAdvertisingDevice( id: _device, - withService: _uuid, + withServices: [_uuid], prescanDuration: _duration, servicesWithCharacteristicsToDiscover: {}, connectionTimeout: _duration); @@ -80,7 +80,7 @@ void main() { test("And does not scan for the device", () { verifyNever(_prescanMock.scan( - withService: anyNamed("withService"), + withServices: anyNamed("withServices"), scanMode: anyNamed("scanMode"))); }); }); @@ -100,14 +100,15 @@ void main() { setUp(() { completer = Completer(); - session = ScanSession(withService: _uuid, future: completer.future); + session = + ScanSession(withServices: [_uuid], future: completer.future); when(_prescanMock.currentScan()) .thenAnswer((_) => currentScanResponses.removeAt(0)); completer.complete(); when(_prescanMock.scan( - withService: anyNamed("withService"), + withServices: anyNamed("withServices"), scanMode: anyNamed("scanMode"))) .thenAnswer((_) => Stream.fromIterable([ DiscoveredDevice( @@ -125,13 +126,13 @@ void main() { _sut.connectToAdvertisingDevice( id: _device, - withService: _uuid, + withServices: [_uuid], prescanDuration: _duration, servicesWithCharacteristicsToDiscover: {}, connectionTimeout: _duration); verify(_prescanMock.scan( - withService: anyNamed("withService"), + withServices: anyNamed("withServices"), scanMode: anyNamed("scanMode"))) .called(1); }); @@ -158,7 +159,7 @@ void main() { ])); await _sut - .prescanAndConnect(_device, {}, _duration, _uuid, _duration) + .prescanAndConnect(_device, {}, _duration, [_uuid], _duration) .first; verify(_prescanMock.connect( @@ -189,7 +190,7 @@ void main() { ])); await _sut - .prescanAndConnect(_device, {}, _duration, _uuid, _duration) + .prescanAndConnect(_device, {}, _duration, [_uuid], _duration) .first; verifyNever(_prescanMock.connect( @@ -206,13 +207,13 @@ void main() { setUp(() { completer = Completer(); final session = - ScanSession(withService: _uuid, future: completer.future); + ScanSession(withServices: [_uuid], future: completer.future); final response = [session, session]; when(_prescanMock.currentScan()) .thenAnswer((_) => response.removeAt(0)); when(_prescanMock.scan( - withService: anyNamed("withService"), + withServices: anyNamed("withServices"), scanMode: anyNamed("scanMode"))) .thenAnswer((_) => Stream.fromIterable([ DiscoveredDevice( @@ -241,7 +242,7 @@ void main() { completer.completeError(null); await _sut - .prescanAndConnect(_device, {}, _duration, _uuid, _duration) + .prescanAndConnect(_device, {}, _duration, [_uuid], _duration) .first; verify(_registry.deviceIsDiscoveredRecently( @@ -258,11 +259,11 @@ void main() { "Fails to connect when there is already a scan running for another service", () async { when(_prescanMock.currentScan()).thenReturn(ScanSession( - withService: Uuid.parse("432A"), future: Future.value())); + withServices: [Uuid.parse("432A")], future: Future.value())); final update = await _sut .awaitCurrentScanAndConnect( - _uuid, _duration, _device, {}, _duration) + [_uuid], _duration, _device, {}, _duration) .first; expect(update.failure.code, ConnectionError.failedToConnect); @@ -272,7 +273,7 @@ void main() { final completer = Completer(); when(_prescanMock.currentScan()).thenReturn( - ScanSession(withService: _uuid, future: completer.future)); + ScanSession(withServices: [_uuid], future: completer.future)); when(_registry.deviceIsDiscoveredRecently( deviceId: anyNamed("deviceId"), @@ -282,7 +283,7 @@ void main() { completer.complete(); await _sut .awaitCurrentScanAndConnect( - _uuid, _duration, _device, {}, _duration) + [_uuid], _duration, _device, {}, _duration) .first; verify(_registry.deviceIsDiscoveredRecently( diff --git a/test/select_from_test.dart b/test/select_from_test.dart index c5ae6c15..0cef1fd4 100644 --- a/test/select_from_test.dart +++ b/test/select_from_test.dart @@ -24,4 +24,4 @@ void main() { }); } -enum _Enum { unknown, a, b } +enum _Enum { unknown, a }