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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -22,7 +22,7 @@ EXTERNAL SOURCES:
|
||||
SPEC CHECKSUMS:
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
|
||||
universal_ble: 65e1257dffc557cc7991a93d253beeddc7c1dc92
|
||||
|
||||
PODFILE CHECKSUM: 4f1c12611da7338d21589c0b2ecd6bd20b109694
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -24,6 +24,8 @@ abstract class UniversalBlePlatformChannel {
|
||||
@async
|
||||
int getBluetoothAvailabilityState();
|
||||
|
||||
bool hasPermissions(bool withAndroidFineLocation);
|
||||
|
||||
@async
|
||||
void requestPermissions(bool withAndroidFineLocation);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user