diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ae3b5c..a2dfa16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 1.0.0 * Unified error codes for all platforms +* Add `isScanning` api ## 0.21.1 * Fix device name resolution on Windows diff --git a/README.md b/README.md index 5bec1c8..465dc41 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,9 @@ UniversalBle.startScan( // Stop scanning UniversalBle.stopScan(); + +// Check if scanning +UniversalBle.isScanning(); ``` Before initiating a scan, ensure that Bluetooth is available: diff --git a/android/src/main/kotlin/com/navideck/universal_ble/SafeScanner.kt b/android/src/main/kotlin/com/navideck/universal_ble/SafeScanner.kt index e0458a8..0f16674 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/SafeScanner.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/SafeScanner.kt @@ -36,6 +36,7 @@ class SafeScanner(private val bluetoothManager: BluetoothManager) { private val handler = Handler(Looper.myLooper()!!) private val startTimes = LinkedList() private var awaitingScan = false + private var isScanning = false fun startScan(filters: List, settings: ScanSettings, callback: ScanCallback) { val now = System.currentTimeMillis() @@ -52,7 +53,7 @@ class SafeScanner(private val bluetoothManager: BluetoothManager) { } awaitingScan = true - val delay = startTimes.first + EXCESSIVE_SCANNING_PERIOD_MS - now + 2_000 + val delay = startTimes.first() + EXCESSIVE_SCANNING_PERIOD_MS - now + 2_000 Log.e(TAG, "startScan: too frequent, schedule auto-start after $delay ms $startTimes") handler.postDelayed({ @@ -65,8 +66,10 @@ class SafeScanner(private val bluetoothManager: BluetoothManager) { startTimes.addLast(now) try { bluetoothManager.adapter.bluetoothLeScanner?.startScan(filters, settings, callback) + isScanning = true } catch (e: Exception) { Log.e(TAG, "Failed to start Scan : $e") + isScanning = false } } } @@ -75,5 +78,10 @@ class SafeScanner(private val bluetoothManager: BluetoothManager) { awaitingScan = false handler.removeCallbacksAndMessages(null) bluetoothManager.adapter.bluetoothLeScanner?.stopScan(callback) + isScanning = false + } + + fun isScanning(): Boolean { + return isScanning || awaitingScan } } \ No newline at end of file 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 ac103cc..0b5da0b 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 @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.0.5), do not edit directly. +// Autogenerated from Pigeon (v26.1.0), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -447,6 +447,7 @@ interface UniversalBlePlatformChannel { fun disableBluetooth(callback: (Result) -> Unit) fun startScan(filter: UniversalScanFilter?) fun stopScan() + fun isScanning(): Boolean fun connect(deviceId: String) fun disconnect(deviceId: String) fun setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: Long, callback: (Result) -> Unit) @@ -557,6 +558,21 @@ interface UniversalBlePlatformChannel { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isScanning$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.isScanning()) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect$separatedMessageChannelSuffix", codec) if (api != null) { 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 7dd39ae..2666bba 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt @@ -187,6 +187,10 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), safeScanner.stopScan(scanCallback) } + override fun isScanning(): Boolean { + return safeScanner.isScanning() + } + override fun connect(deviceId: String) { // If already connected, send connected message, // if connecting, do nothing diff --git a/darwin/Classes/UniversalBle.g.swift b/darwin/Classes/UniversalBle.g.swift index 4ead34e..eca68be 100644 --- a/darwin/Classes/UniversalBle.g.swift +++ b/darwin/Classes/UniversalBle.g.swift @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.0.5), do not edit directly. +// Autogenerated from Pigeon (v26.1.0), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation @@ -477,6 +477,7 @@ protocol UniversalBlePlatformChannel { func disableBluetooth(completion: @escaping (Result) -> Void) func startScan(filter: UniversalScanFilter?) throws func stopScan() throws + func isScanning() throws -> Bool func connect(deviceId: String) throws func disconnect(deviceId: String) throws func setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: Int64, completion: @escaping (Result) -> Void) @@ -570,6 +571,19 @@ class UniversalBlePlatformChannelSetup { } else { stopScanChannel.setMessageHandler(nil) } + let isScanningChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isScanning\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + isScanningChannel.setMessageHandler { _, reply in + do { + let result = try api.isScanning() + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + isScanningChannel.setMessageHandler(nil) + } let connectChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { connectChannel.setMessageHandler { message, reply in diff --git a/darwin/Classes/UniversalBlePlugin.swift b/darwin/Classes/UniversalBlePlugin.swift index 7ffced3..1b09de4 100644 --- a/darwin/Classes/UniversalBlePlugin.swift +++ b/darwin/Classes/UniversalBlePlugin.swift @@ -39,6 +39,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral private var characteristicWriteWithoutResponseFutures = [CharacteristicWriteFuture]() private var characteristicNotifyFutures = [CharacteristicNotifyFuture]() private var discoverServicesFutures = [DiscoverServicesFuture]() + private var isManageScanning = false init(callbackChannel: UniversalBleCallbackChannel) { self.callbackChannel = callbackChannel @@ -82,10 +83,31 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral let options = [CBCentralManagerScanOptionAllowDuplicatesKey: true] manager.scanForPeripherals(withServices: withServices, options: options) + isManageScanning = true } func stopScan() throws { manager.stopScan() + isManageScanning = false + } + + func isScanning() throws -> Bool { + var hasAuthorization = true + #if os(iOS) + if #available(iOS 13.1, *) { + hasAuthorization = CBCentralManager.authorization == .allowedAlways + } else { + return isManageScanning + } + #elseif os(macOS) + hasAuthorization = CBCentralManager.authorization == .allowedAlways + #endif + + if hasAuthorization { + return manager.isScanning + } + + return isManageScanning } func connect(deviceId: String) throws { diff --git a/example/lib/data/mock_universal_ble.dart b/example/lib/data/mock_universal_ble.dart index 233e8c6..dd00613 100644 --- a/example/lib/data/mock_universal_ble.dart +++ b/example/lib/data/mock_universal_ble.dart @@ -13,6 +13,7 @@ class MockUniversalBle extends UniversalBlePlatform { ); Uint8List _serviceValue = utf8.encode('Result'); + bool _isScanning = false; final BleService _mockService = BleService('180', [ BleCharacteristic('180A', [ @@ -26,11 +27,20 @@ class MockUniversalBle extends UniversalBlePlatform { Future startScan({ ScanFilter? scanFilter, PlatformConfig? platformConfig, - }) async => - updateScanResult(_mockBleDevice); + }) async { + _isScanning = true; + updateScanResult(_mockBleDevice); + } @override - Future stopScan() async {} + Future stopScan() async { + _isScanning = false; + } + + @override + Future isScanning() async { + return _isScanning; + } @override Future connect(String deviceId, {Duration? connectionTimeout}) async { diff --git a/example/lib/home/home.dart b/example/lib/home/home.dart index d3f7c7b..8fc97bc 100644 --- a/example/lib/home/home.dart +++ b/example/lib/home/home.dart @@ -63,6 +63,13 @@ class _MyAppState extends State { // UniversalBle.onQueueUpdate = (String id, int remainingItems) { // debugPrint("Queue: $id RemainingItems: $remainingItems"); // }; + + UniversalBle.isScanning().then((value) { + debugPrint("Is Scanning: $value"); + setState(() { + _isScanning = value; + }); + }); } void trackAvailabilityState() { diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index fcf7fb4..bf1d37e 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -20,9 +20,9 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin SPEC CHECKSUMS: - device_info_plus: b0fafc687fb901e2af612763340f1b0d4352f8e5 + device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 - universal_ble: ff19787898040d721109c6324472e5dd4bc86adc + universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6 PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 diff --git a/lib/src/universal_ble.dart b/lib/src/universal_ble.dart index 1a0b093..7ea145d 100644 --- a/lib/src/universal_ble.dart +++ b/lib/src/universal_ble.dart @@ -88,6 +88,14 @@ class UniversalBle { ); } + /// Check if currently scanning for devices. + /// Returns `true` if scanning is active, `false` otherwise. + static Future isScanning() async { + return await _bleCommandQueue.queueCommand( + () => _platform.isScanning(), + ); + } + /// Connect to a device. /// It is advised to stop scanning before connecting. /// It throws error if device connection fails. diff --git a/lib/src/universal_ble_linux/universal_ble_linux.dart b/lib/src/universal_ble_linux/universal_ble_linux.dart index ebca5e2..ace1ad5 100644 --- a/lib/src/universal_ble_linux/universal_ble_linux.dart +++ b/lib/src/universal_ble_linux/universal_ble_linux.dart @@ -125,6 +125,12 @@ class UniversalBleLinux extends UniversalBlePlatform { } } + @override + Future isScanning() async { + await _ensureInitialized(); + return _activeAdapter?.discovering == true; + } + @override Future getConnectionState(String deviceId) async { BlueZDevice? device = _devices[deviceId] ?? diff --git a/lib/src/universal_ble_pigeon/universal_ble.g.dart b/lib/src/universal_ble_pigeon/universal_ble.g.dart index 5e451d7..63847b8 100644 --- a/lib/src/universal_ble_pigeon/universal_ble.g.dart +++ b/lib/src/universal_ble_pigeon/universal_ble.g.dart @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.0.5), do not edit directly. +// Autogenerated from Pigeon (v26.1.0), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -633,6 +633,36 @@ class UniversalBlePlatformChannel { } } + Future isScanning() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isScanning$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } + Future connect(String deviceId) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect$pigeonVar_messageChannelSuffix'; 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 0892e20..6c03e25 100644 --- a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart +++ b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart @@ -57,6 +57,10 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { Future stopScan() => _executeWithErrorHandling(() => _channel.stopScan()); + @override + Future isScanning() => + _executeWithErrorHandling(() => _channel.isScanning()); + @override Future getConnectionState(String deviceId) async { int state = await _executeWithErrorHandling( diff --git a/lib/src/universal_ble_platform_interface.dart b/lib/src/universal_ble_platform_interface.dart index 16a7d64..25d46cc 100644 --- a/lib/src/universal_ble_platform_interface.dart +++ b/lib/src/universal_ble_platform_interface.dart @@ -43,6 +43,8 @@ abstract class UniversalBlePlatform { Future stopScan(); + Future isScanning(); + Future connect(String deviceId, {Duration? connectionTimeout}); Future disconnect(String deviceId); diff --git a/lib/src/universal_ble_web/universal_ble_web.dart b/lib/src/universal_ble_web/universal_ble_web.dart index 0fa55f2..ed3791b 100644 --- a/lib/src/universal_ble_web/universal_ble_web.dart +++ b/lib/src/universal_ble_web/universal_ble_web.dart @@ -20,6 +20,7 @@ class UniversalBleWeb extends UniversalBlePlatform { final Map _connectedDeviceStreamList = {}; final Map _characteristicStreamList = {}; final Map> _serviceCache = {}; + bool _isScanning = false; @override Future getConnectionState(String deviceId) async { @@ -83,6 +84,7 @@ class UniversalBleWeb extends UniversalBlePlatform { PlatformConfig? platformConfig, }) async { try { + _isScanning = true; FlutterWebBluetooth.instance.isAvailable; BluetoothDevice device = await FlutterWebBluetooth.instance.requestDevice( _getRequestOptionBuilder(scanFilter, platformConfig?.web), @@ -101,6 +103,8 @@ class UniversalBleWeb extends UniversalBlePlatform { throw WebBluetoothGloballyDisabled(message: error); } rethrow; + } finally { + _isScanning = false; } } @@ -146,6 +150,9 @@ class UniversalBleWeb extends UniversalBlePlatform { _disposeAdvertisementWatcher(); } + @override + Future isScanning() async => _isScanning; + @override Future setNotifiable( String deviceId, diff --git a/pigeon/universal_ble.dart b/pigeon/universal_ble.dart index b52b8ee..334ba42 100644 --- a/pigeon/universal_ble.dart +++ b/pigeon/universal_ble.dart @@ -34,6 +34,8 @@ abstract class UniversalBlePlatformChannel { void stopScan(); + bool isScanning(); + void connect(String deviceId); void disconnect(String deviceId); diff --git a/pubspec.yaml b/pubspec.yaml index 5b25216..730219a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,7 +38,7 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^6.0.0 - pigeon: ^26.0.5 + pigeon: ^26.1.0 flutter: plugin: diff --git a/test/universal_ble_test_mock.dart b/test/universal_ble_test_mock.dart index c940346..e1c0824 100644 --- a/test/universal_ble_test_mock.dart +++ b/test/universal_ble_test_mock.dart @@ -95,4 +95,9 @@ abstract class UniversalBlePlatformMock extends UniversalBlePlatform { BleOutputProperty bleOutputProperty) { throw UnimplementedError(); } + + @override + Future isScanning() { + throw UnimplementedError(); + } } diff --git a/windows/src/generated/universal_ble.g.cpp b/windows/src/generated/universal_ble.g.cpp index 00e3cbf..14b8e21 100644 --- a/windows/src/generated/universal_ble.g.cpp +++ b/windows/src/generated/universal_ble.g.cpp @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.0.5), do not edit directly. +// Autogenerated from Pigeon (v26.1.0), do not edit directly. // See also: https://pub.dev/packages/pigeon #undef _HAS_EXCEPTIONS @@ -623,6 +623,27 @@ void UniversalBlePlatformChannel::SetUp( channel.SetMessageHandler(nullptr); } } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isScanning" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + ErrorOr output = api->IsScanning(); + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } { BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect" + prepended_suffix, &GetCodec()); if (api != nullptr) { diff --git a/windows/src/generated/universal_ble.g.h b/windows/src/generated/universal_ble.g.h index bc77688..a0cf7fd 100644 --- a/windows/src/generated/universal_ble.g.h +++ b/windows/src/generated/universal_ble.g.h @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.0.5), do not edit directly. +// Autogenerated from Pigeon (v26.1.0), do not edit directly. // See also: https://pub.dev/packages/pigeon #ifndef PIGEON_UNIVERSAL_BLE_G_H_ @@ -352,6 +352,7 @@ class UniversalBlePlatformChannel { virtual void DisableBluetooth(std::function reply)> result) = 0; virtual std::optional StartScan(const UniversalScanFilter* filter) = 0; virtual std::optional StopScan() = 0; + virtual ErrorOr IsScanning() = 0; virtual std::optional Connect(const std::string& device_id) = 0; virtual std::optional Disconnect(const std::string& device_id) = 0; virtual void SetNotifiable( diff --git a/windows/src/universal_ble_plugin.cpp b/windows/src/universal_ble_plugin.cpp index 46b9661..e22bca0 100644 --- a/windows/src/universal_ble_plugin.cpp +++ b/windows/src/universal_ble_plugin.cpp @@ -212,6 +212,15 @@ std::optional UniversalBlePlugin::StopScan() { } }; +ErrorOr UniversalBlePlugin::IsScanning() { + if (bluetooth_le_watcher_ != nullptr) { + try { + return bluetooth_le_watcher_.Status() == BluetoothLEAdvertisementWatcherStatus::Started; + } catch (...) {} + } + return false; +} + ErrorOr UniversalBlePlugin::GetConnectionState(const std::string &device_id) { const auto it = connected_devices_.find(str_to_mac_address(device_id)); diff --git a/windows/src/universal_ble_plugin.h b/windows/src/universal_ble_plugin.h index b3b188b..c16003c 100644 --- a/windows/src/universal_ble_plugin.h +++ b/windows/src/universal_ble_plugin.h @@ -158,6 +158,7 @@ namespace universal_ble ErrorOr GetConnectionState(const std::string &device_id) override; std::optional StartScan(const UniversalScanFilter *filter) override; std::optional StopScan() override; + ErrorOr IsScanning() override; std::optional Connect(const std::string &device_id) override; std::optional Disconnect(const std::string &device_id) override; void DiscoverServices(