diff --git a/CHANGELOG.md b/CHANGELOG.md index 9568284..e30880b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,11 @@ ## 1.0.0 * BREAKING CHANGE: `OnValueChange` callback also includes timestamp +* BREAKING CHANGE: Bump minimum IOS version to 13.1 +* BREAKING CHANGE: Bump minimum MacOS version to 10.15 * Fix Windows 11 crash on cancelling pairing * Unified error codes for all platforms * Add `isScanning` api +* Add `hasPermissions` api * Add `requestPermissions` api and auto ask permission on `startScan` * `disconnect` now waits for disconnection confirmation before returning * Improve Windows disconnection event handling and cleanup diff --git a/android/src/main/kotlin/com/navideck/universal_ble/PermissionHandler.kt b/android/src/main/kotlin/com/navideck/universal_ble/PermissionHandler.kt index f5d92ef..9bac59a 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/PermissionHandler.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/PermissionHandler.kt @@ -24,6 +24,18 @@ class PermissionHandler( ) { private var permissionRequestCallback: ((Result) -> Unit)? = null + /** + * Check if we have required permissions + */ + fun hasPermissions(withFineLocation: Boolean): Boolean { + val validationError = validateRequiredPermissions(withFineLocation) + if (validationError != null) { + throw validationError + } + val permissionsToRequest = getRequiredPermissions(withFineLocation) + return permissionsToRequest.isEmpty() + } + /** * Requests the required Bluetooth permissions based on the manifest and Android version. * 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 ffc3f1f..025e656 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 @@ -510,6 +510,7 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() { */ interface UniversalBlePlatformChannel { fun getBluetoothAvailabilityState(callback: (Result) -> Unit) + fun hasPermissions(withAndroidFineLocation: Boolean): Boolean fun requestPermissions(withAndroidFineLocation: Boolean, callback: (Result) -> Unit) fun enableBluetooth(callback: (Result) -> Unit) fun disableBluetooth(callback: (Result) -> Unit) @@ -557,6 +558,23 @@ interface UniversalBlePlatformChannel { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.hasPermissions$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val withAndroidFineLocationArg = args[0] as Boolean + val wrapped: List = try { + listOf(api.hasPermissions(withAndroidFineLocationArg)) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestPermissions$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 edd15c5..0faaae2 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt @@ -95,6 +95,10 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), ) } + override fun hasPermissions(withAndroidFineLocation: Boolean): Boolean { + return permissionHandler?.hasPermissions(withAndroidFineLocation) ?: false + } + override fun requestPermissions( withAndroidFineLocation: Boolean, callback: (Result) -> Unit, diff --git a/darwin/Classes/UniversalBle.g.swift b/darwin/Classes/UniversalBle.g.swift index bfbf983..af31208 100644 --- a/darwin/Classes/UniversalBle.g.swift +++ b/darwin/Classes/UniversalBle.g.swift @@ -529,6 +529,7 @@ class UniversalBlePigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol UniversalBlePlatformChannel { func getBluetoothAvailabilityState(completion: @escaping (Result) -> Void) + func hasPermissions(withAndroidFineLocation: Bool) throws -> Bool func requestPermissions(withAndroidFineLocation: Bool, completion: @escaping (Result) -> Void) func enableBluetooth(completion: @escaping (Result) -> Void) func disableBluetooth(completion: @escaping (Result) -> Void) @@ -571,6 +572,21 @@ class UniversalBlePlatformChannelSetup { } else { getBluetoothAvailabilityStateChannel.setMessageHandler(nil) } + let hasPermissionsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.hasPermissions\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + hasPermissionsChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let withAndroidFineLocationArg = args[0] as! Bool + do { + let result = try api.hasPermissions(withAndroidFineLocation: withAndroidFineLocationArg) + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + hasPermissionsChannel.setMessageHandler(nil) + } let requestPermissionsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestPermissions\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { requestPermissionsChannel.setMessageHandler { message, reply in diff --git a/darwin/Classes/UniversalBlePlugin.swift b/darwin/Classes/UniversalBlePlugin.swift index 2609cf3..d03bd81 100644 --- a/darwin/Classes/UniversalBlePlugin.swift +++ b/darwin/Classes/UniversalBlePlugin.swift @@ -56,6 +56,10 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } } + func hasPermissions(withAndroidFineLocation _: Bool) throws -> Bool { + return CBCentralManager.authorization == .allowedAlways + } + func requestPermissions(withAndroidFineLocation _: Bool, completion: @escaping (Result) -> Void) { if manager.state != .unknown { completePermissionRequest(completion: completion) @@ -114,21 +118,9 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } 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 { + if CBCentralManager.authorization == .allowedAlways { return manager.isScanning } - return isManageScanning } diff --git a/darwin/universal_ble.podspec b/darwin/universal_ble.podspec index bbae355..22c21cf 100644 --- a/darwin/universal_ble.podspec +++ b/darwin/universal_ble.podspec @@ -16,8 +16,8 @@ A new Flutter plugin project. s.source_files = 'Classes/**/*' s.ios.dependency 'Flutter' s.osx.dependency 'FlutterMacOS' - s.ios.deployment_target = '9.0' - s.osx.deployment_target = '10.12' + s.ios.deployment_target = '13.1' + s.osx.deployment_target = '10.15' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 200504d..ebaf8eb 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -22,7 +22,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 - universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6 + universal_ble: 65e1257dffc557cc7991a93d253beeddc7c1dc92 PODFILE CHECKSUM: 4f1c12611da7338d21589c0b2ecd6bd20b109694 diff --git a/example/lib/home/home.dart b/example/lib/home/home.dart index c374160..0ab9c57 100644 --- a/example/lib/home/home.dart +++ b/example/lib/home/home.dart @@ -216,7 +216,20 @@ class _MyAppState extends State { showSnackbar("BluetoothDisabled: $isDisabled"); }, ), - if (BleCapabilities.requiresRuntimePermission) + if (BleCapabilities.requiresRuntimePermission) ...[ + PlatformButton( + text: 'Has Permissions', + onPressed: () async { + try { + bool hasPermissions = await UniversalBle.hasPermissions( + withAndroidFineLocation: false, + ); + showSnackbar("Has Permissions: $hasPermissions"); + } catch (e) { + showSnackbar(e.toString()); + } + }, + ), PlatformButton( text: 'Request Permissions', onPressed: () async { @@ -230,6 +243,7 @@ class _MyAppState extends State { } }, ), + ], if (!isTrackingAvailabilityState) PlatformButton( text: 'Track Availability State', diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index 6a79838..0c1b92b 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -16,7 +16,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 - universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6 + universal_ble: 65e1257dffc557cc7991a93d253beeddc7c1dc92 PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index 0c2e8a5..6fa729e 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -195,7 +195,6 @@ BF9EC7C1208428926C8C045E /* Pods-RunnerTests.release.xcconfig */, C5F6162E162B944885A4C57E /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -575,6 +574,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -701,6 +701,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -721,6 +722,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.15; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; diff --git a/example/pubspec.lock b/example/pubspec.lock index 82cd526..97d521a 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -214,10 +214,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" path: dependency: transitive description: @@ -315,10 +315,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" typed_data: dependency: transitive description: diff --git a/lib/src/universal_ble.dart b/lib/src/universal_ble.dart index 8c40039..05c614d 100644 --- a/lib/src/universal_ble.dart +++ b/lib/src/universal_ble.dart @@ -71,6 +71,18 @@ class UniversalBle { ); } + /// Check if has permissions. + /// [withAndroidFineLocation] is used to check fine location permission on Android 12+ (API 31+). + /// On Android lower than 12, this method will check location permission regardless of the [withAndroidFineLocation] value. + /// `Windows`, `Linux` and `Web` will always return true. + static Future hasPermissions({ + bool withAndroidFineLocation = false, + }) async { + return _platform.hasPermissions( + withAndroidFineLocation: withAndroidFineLocation, + ); + } + /// Request permissions. /// if all permissions are already granted or granted by user, this method will succeed. /// it will throw exception if permissions are denied by user. diff --git a/lib/src/universal_ble_linux/universal_ble_linux.dart b/lib/src/universal_ble_linux/universal_ble_linux.dart index c026b7c..05206a9 100644 --- a/lib/src/universal_ble_linux/universal_ble_linux.dart +++ b/lib/src/universal_ble_linux/universal_ble_linux.dart @@ -431,12 +431,6 @@ class UniversalBleLinux extends UniversalBlePlatform { .toList(); } - @override - Future requestPermissions( - {bool withAndroidFineLocation = false}) async { - // No permissions to request on linux - } - AvailabilityState get _availabilityState { return _activeAdapter?.powered == true ? AvailabilityState.poweredOn diff --git a/lib/src/universal_ble_pigeon/universal_ble.g.dart b/lib/src/universal_ble_pigeon/universal_ble.g.dart index 4b650ad..edc655d 100644 --- a/lib/src/universal_ble_pigeon/universal_ble.g.dart +++ b/lib/src/universal_ble_pigeon/universal_ble.g.dart @@ -592,6 +592,35 @@ class UniversalBlePlatformChannel { } } + Future hasPermissions(bool withAndroidFineLocation) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.hasPermissions$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([withAndroidFineLocation]); + final 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 requestPermissions(bool withAndroidFineLocation) async { final pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestPermissions$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 c249789..4c80478 100644 --- a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart +++ b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart @@ -145,6 +145,13 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { Future unpair(String deviceId) => _executeWithErrorHandling(() => _channel.unPair(deviceId)); + @override + Future hasPermissions({bool withAndroidFineLocation = false}) async { + return await _executeWithErrorHandling( + () => _channel.hasPermissions(withAndroidFineLocation), + ); + } + @override Future requestPermissions( {bool withAndroidFineLocation = false}) async { diff --git a/lib/src/universal_ble_platform_interface.dart b/lib/src/universal_ble_platform_interface.dart index 9f6c576..1bd5f02 100644 --- a/lib/src/universal_ble_platform_interface.dart +++ b/lib/src/universal_ble_platform_interface.dart @@ -37,7 +37,12 @@ abstract class UniversalBlePlatform { Future disableBluetooth(); - Future requestPermissions({bool withAndroidFineLocation = false}); + Future hasPermissions({bool withAndroidFineLocation = false}) async { + return true; + } + + Future requestPermissions( + {bool withAndroidFineLocation = false}) async {} Future startScan({ ScanFilter? scanFilter, diff --git a/lib/src/universal_ble_web/universal_ble_web.dart b/lib/src/universal_ble_web/universal_ble_web.dart index 734e5cf..2cf2b28 100644 --- a/lib/src/universal_ble_web/universal_ble_web.dart +++ b/lib/src/universal_ble_web/universal_ble_web.dart @@ -275,12 +275,6 @@ class UniversalBleWeb extends UniversalBlePlatform { } /// `Unimplemented` - @override - Future requestPermissions( - {bool withAndroidFineLocation = false}) async { - // No permissions to request on Web - } - @override Future requestMtu(String deviceId, int expectedMtu) { throw UniversalBleException( diff --git a/pigeon/universal_ble.dart b/pigeon/universal_ble.dart index fb3ef67..ab491c3 100644 --- a/pigeon/universal_ble.dart +++ b/pigeon/universal_ble.dart @@ -24,6 +24,8 @@ abstract class UniversalBlePlatformChannel { @async int getBluetoothAvailabilityState(); + bool hasPermissions(bool withAndroidFineLocation); + @async void requestPermissions(bool withAndroidFineLocation); diff --git a/windows/src/generated/universal_ble.g.cpp b/windows/src/generated/universal_ble.g.cpp index 35944cf..c53a60d 100644 --- a/windows/src/generated/universal_ble.g.cpp +++ b/windows/src/generated/universal_ble.g.cpp @@ -612,6 +612,34 @@ void UniversalBlePlatformChannel::SetUp( channel.SetMessageHandler(nullptr); } } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.hasPermissions" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_with_android_fine_location_arg = args.at(0); + if (encodable_with_android_fine_location_arg.IsNull()) { + reply(WrapError("with_android_fine_location_arg unexpectedly null.")); + return; + } + const auto& with_android_fine_location_arg = std::get(encodable_with_android_fine_location_arg); + ErrorOr output = api->HasPermissions(with_android_fine_location_arg); + 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.requestPermissions" + 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 ce2f342..ac593d4 100644 --- a/windows/src/generated/universal_ble.g.h +++ b/windows/src/generated/universal_ble.g.h @@ -387,6 +387,7 @@ class UniversalBlePlatformChannel { UniversalBlePlatformChannel& operator=(const UniversalBlePlatformChannel&) = delete; virtual ~UniversalBlePlatformChannel() {} virtual void GetBluetoothAvailabilityState(std::function reply)> result) = 0; + virtual ErrorOr HasPermissions(bool with_android_fine_location) = 0; virtual void RequestPermissions( bool with_android_fine_location, std::function reply)> result) = 0; diff --git a/windows/src/universal_ble_plugin.cpp b/windows/src/universal_ble_plugin.cpp index acb8528..5466944 100644 --- a/windows/src/universal_ble_plugin.cpp +++ b/windows/src/universal_ble_plugin.cpp @@ -119,6 +119,11 @@ void UniversalBlePlugin::DisableBluetooth( }); } +ErrorOr UniversalBlePlugin::HasPermissions(bool with_android_fine_location) { + // Windows does not require runtime permissions for Bluetooth + return true; +} + void UniversalBlePlugin::RequestPermissions( bool with_android_fine_location, std::function reply)> result) { diff --git a/windows/src/universal_ble_plugin.h b/windows/src/universal_ble_plugin.h index 6cc2ab4..6045582 100644 --- a/windows/src/universal_ble_plugin.h +++ b/windows/src/universal_ble_plugin.h @@ -177,6 +177,7 @@ private: ErrorOr IsScanning() override; std::optional Connect(const std::string &device_id) override; std::optional Disconnect(const std::string &device_id) override; + ErrorOr HasPermissions(bool with_android_fine_location) override; void RequestPermissions( bool with_android_fine_location, std::function reply)> result) override;