Implement hasPermissions API (#201)

* Implement hasPermissions api

* Bump minimum MacOS version to 10.15

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Rohit Sangwan
2025-12-13 02:40:21 +05:30
committed by GitHub
parent 89abb3bf03
commit 97936046e0
23 changed files with 175 additions and 36 deletions
+3
View File
@@ -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
@@ -24,6 +24,18 @@ class PermissionHandler(
) {
private var permissionRequestCallback: ((Result<Unit>) -> 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.
*
@@ -510,6 +510,7 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() {
*/
interface UniversalBlePlatformChannel {
fun getBluetoothAvailabilityState(callback: (Result<Long>) -> Unit)
fun hasPermissions(withAndroidFineLocation: Boolean): Boolean
fun requestPermissions(withAndroidFineLocation: Boolean, callback: (Result<Unit>) -> Unit)
fun enableBluetooth(callback: (Result<Boolean>) -> Unit)
fun disableBluetooth(callback: (Result<Boolean>) -> Unit)
@@ -557,6 +558,23 @@ interface UniversalBlePlatformChannel {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.hasPermissions$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val withAndroidFineLocationArg = args[0] as Boolean
val wrapped: List<Any?> = try {
listOf(api.hasPermissions(withAndroidFineLocationArg))
} catch (exception: Throwable) {
UniversalBlePigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestPermissions$separatedMessageChannelSuffix", codec)
if (api != null) {
@@ -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>) -> Unit,
+16
View File
@@ -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<Int64, Error>) -> Void)
func hasPermissions(withAndroidFineLocation: Bool) throws -> Bool
func requestPermissions(withAndroidFineLocation: Bool, completion: @escaping (Result<Void, Error>) -> Void)
func enableBluetooth(completion: @escaping (Result<Bool, Error>) -> Void)
func disableBluetooth(completion: @escaping (Result<Bool, Error>) -> 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
+5 -13
View File
@@ -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, any Error>) -> 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
}
+2 -2
View File
@@ -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' }
+1 -1
View File
@@ -22,7 +22,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
universal_ble: 65e1257dffc557cc7991a93d253beeddc7c1dc92
PODFILE CHECKSUM: 4f1c12611da7338d21589c0b2ecd6bd20b109694
+15 -1
View File
@@ -216,7 +216,20 @@ class _MyAppState extends State<MyApp> {
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<MyApp> {
}
},
),
],
if (!isTrackingAvailabilityState)
PlatformButton(
text: 'Track Availability State',
+1 -1
View File
@@ -16,7 +16,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
universal_ble: 65e1257dffc557cc7991a93d253beeddc7c1dc92
PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3
@@ -195,7 +195,6 @@
BF9EC7C1208428926C8C045E /* Pods-RunnerTests.release.xcconfig */,
C5F6162E162B944885A4C57E /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
@@ -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;
};
+4 -4
View File
@@ -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:
+12
View File
@@ -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<bool> 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.
@@ -431,12 +431,6 @@ class UniversalBleLinux extends UniversalBlePlatform {
.toList();
}
@override
Future<void> requestPermissions(
{bool withAndroidFineLocation = false}) async {
// No permissions to request on linux
}
AvailabilityState get _availabilityState {
return _activeAdapter?.powered == true
? AvailabilityState.poweredOn
@@ -592,6 +592,35 @@ class UniversalBlePlatformChannel {
}
}
Future<bool> hasPermissions(bool withAndroidFineLocation) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.hasPermissions$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture =
pigeonVar_channel.send(<Object?>[withAndroidFineLocation]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
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<void> requestPermissions(bool withAndroidFineLocation) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestPermissions$pigeonVar_messageChannelSuffix';
@@ -145,6 +145,13 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform {
Future<void> unpair(String deviceId) =>
_executeWithErrorHandling(() => _channel.unPair(deviceId));
@override
Future<bool> hasPermissions({bool withAndroidFineLocation = false}) async {
return await _executeWithErrorHandling(
() => _channel.hasPermissions(withAndroidFineLocation),
);
}
@override
Future<void> requestPermissions(
{bool withAndroidFineLocation = false}) async {
@@ -37,7 +37,12 @@ abstract class UniversalBlePlatform {
Future<bool> disableBluetooth();
Future<void> requestPermissions({bool withAndroidFineLocation = false});
Future<bool> hasPermissions({bool withAndroidFineLocation = false}) async {
return true;
}
Future<void> requestPermissions(
{bool withAndroidFineLocation = false}) async {}
Future<void> startScan({
ScanFilter? scanFilter,
@@ -275,12 +275,6 @@ class UniversalBleWeb extends UniversalBlePlatform {
}
/// `Unimplemented`
@override
Future<void> requestPermissions(
{bool withAndroidFineLocation = false}) async {
// No permissions to request on Web
}
@override
Future<int> requestMtu(String deviceId, int expectedMtu) {
throw UniversalBleException(
+2
View File
@@ -24,6 +24,8 @@ abstract class UniversalBlePlatformChannel {
@async
int getBluetoothAvailabilityState();
bool hasPermissions(bool withAndroidFineLocation);
@async
void requestPermissions(bool withAndroidFineLocation);
+28
View File
@@ -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<EncodableValue>& reply) {
try {
const auto& args = std::get<EncodableList>(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<bool>(encodable_with_android_fine_location_arg);
ErrorOr<bool> 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) {
+1
View File
@@ -387,6 +387,7 @@ class UniversalBlePlatformChannel {
UniversalBlePlatformChannel& operator=(const UniversalBlePlatformChannel&) = delete;
virtual ~UniversalBlePlatformChannel() {}
virtual void GetBluetoothAvailabilityState(std::function<void(ErrorOr<int64_t> reply)> result) = 0;
virtual ErrorOr<bool> HasPermissions(bool with_android_fine_location) = 0;
virtual void RequestPermissions(
bool with_android_fine_location,
std::function<void(std::optional<FlutterError> reply)> result) = 0;
+5
View File
@@ -119,6 +119,11 @@ void UniversalBlePlugin::DisableBluetooth(
});
}
ErrorOr<bool> 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<void(std::optional<FlutterError> reply)> result) {
+1
View File
@@ -177,6 +177,7 @@ private:
ErrorOr<bool> IsScanning() override;
std::optional<FlutterError> Connect(const std::string &device_id) override;
std::optional<FlutterError> Disconnect(const std::string &device_id) override;
ErrorOr<bool> HasPermissions(bool with_android_fine_location) override;
void RequestPermissions(
bool with_android_fine_location,
std::function<void(std::optional<FlutterError> reply)> result) override;