Implement requestPermission API (#190)

* Implement requestPermission api

* Update example ios podfile

* Update example windows dependencies

* Update Readme and Changelog

* Resolve Ai Comments

* Improve bluetooth permission handling

* Update Readme

* Improve readme

---------

Co-authored-by: Foti Dim <foti@navideck.com>
This commit is contained in:
Rohit Sangwan
2025-11-13 21:52:17 +05:30
committed by GitHub
parent 6e82ceed77
commit 970db165dd
36 changed files with 606 additions and 305 deletions
+1
View File
@@ -1,6 +1,7 @@
## 1.0.0
* Unified error codes for all platforms
* Add `isScanning` api
* Add `requestPermissions` api and auto ask permission on `startScan`
## 0.21.1
* Fix device name resolution on Windows
+96 -29
View File
@@ -19,26 +19,28 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE
- [Timeout](#timeout)
- [Error Handling](#error-handling)
- [UUID Format Agnostic](#uuid-format-agnostic)
- [Permissions](#permissions)
## API Support
| | Android | iOS | macOS | Windows | Linux | Web |
| :------------------- | :-----: | :-: | :---: | :-----: | :----------: | :-: |
| startScan/stopScan | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| connect/disconnect | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| getSystemDevices | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
| discoverServices | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| read | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| write | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| subscriptions | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| pair | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ⏺ |
| unpair | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ |
| isPaired | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| onPairingStateChange | ✔️ | ⏺ | ⏺ | ✔️ | ✔️ | ⏺ |
| getBluetoothAvailabilityState | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
| enable/disable Bluetooth | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ |
| onAvailabilityChange | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| requestMtu | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
| | Android | iOS | macOS | Windows | Linux | Web |
| :---------------------------- | :-----: | :-: | :---: | :-----: | :---: | :-: |
| startScan/stopScan | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| connect/disconnect | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| getSystemDevices | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
| discoverServices | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| read | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| write | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| subscriptions | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| pair | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ⏺ |
| unpair | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ |
| isPaired | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| onPairingStateChange | ✔️ | ⏺ | ⏺ | ✔️ | ✔️ | ⏺ |
| getBluetoothAvailabilityState | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
| enable/disable Bluetooth | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ |
| onAvailabilityChange | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| requestMtu | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
| requestPermissions | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
## Getting Started
@@ -55,8 +57,12 @@ and import it wherever you want to use it:
import 'package:universal_ble/universal_ble.dart';
```
> **Important**: Before using BLE features, make sure to check the [Permissions](#permissions) section to see what setup is needed for your target platform (Android, iOS, macOS, Windows, Linux, or Web).
### Scanning
The very first thing you need to do before being able to connect to a device is to discover it by calling `startScan();`
```dart
// Get scan updates from stream
UniversalBle.scanStream.listen((BleDevice bleDevice) {
@@ -128,7 +134,7 @@ You can optionally set a filter when scanning. A filter can have multiple condit
##### With Services
When setting this parameter, the scan results will only include devices that advertise any of the specified services.
When setting this parameter, the scan results will only include devices that advertise any of the specified services.
```dart
List<String> withServices;
@@ -213,7 +219,7 @@ BleConnectionState connectionState = await bleDevice.connectionState;
### Discovering Services
After establishing a connection, services need to be discovered. This method will discover all services and their characteristics.
After establishing a connection, services need to be discovered. This method will discover all services and their characteristics.
If you don't call this method then it will be automatically called when you try to get any service or characteristic.
@@ -320,15 +326,17 @@ await bleDevice.pair();
```
##### Pair on Apple and web
For Apple and Web, pairing support depends on the device. Pairing is triggered automatically by the OS when you try to read/write from/to an encrypted characteristic.
Calling `bleDevice.pair()` will only trigger pairing if the device has an *encrypted read characteristic*.
Calling `bleDevice.pair()` will only trigger pairing if the device has an _encrypted read characteristic_.
If your device only has encrypted write characteristics or you happen to know which encrypted read characteristic you want to use, you can pass it with a `pairingCommand`.
```dart
await bleDevice.pair(pairingCommand: BleCommand(service:"SERVICE", characteristic:"ENCRYPTED_CHARACTERISTIC"));
```
After pairing you can check the pairing status.
#### Pairing status
@@ -349,6 +357,7 @@ bool? isPaired = await bleDevice.isPaired(pairingCommand: BleCommand(service:"SE
```
##### Discovering encrypted characteristic
To discover encrypted characteristics, make sure your device is not paired and use the example app to read/write to all discovered characteristics one by one. If one of them triggers pairing, that means it is encrypted and you can use it to construct `BleCommand(service:"SERVICE", characteristic:"ENCRYPTED_CHARACTERISTIC")`.
#### Pairing state changes
@@ -361,6 +370,7 @@ bleDevice.pairingStateStream.listen((bool paired) {
```
#### Unpair
```dart
bleDevice.unpair();
```
@@ -411,7 +421,7 @@ When developing cross-platform BLE applications and devices:
#### Resetting State on Hot Restart
During Flutter hot restart in debug mode, the app state is reset but native Bluetooth connections and scan operations may persist. This can lead to connection issues or stale state.
During Flutter hot restart in debug mode, the app state is reset but native Bluetooth connections and scan operations may persist. This can lead to connection issues or stale state.
<details>
<summary>Use the following helper function to properly clean up BLE state before your app restarts.</summary>
@@ -468,7 +478,7 @@ Future<void> resetBleState() async {
}
```
</details>
</details>
## Command Queue
@@ -500,6 +510,7 @@ UniversalBle.onQueueUpdate = (String id, int remainingItems) {
```
To clear the queue:
```dart
/// Use [BleCommandQueue.globalQueueId] to clear the global queue.
/// To clear the queue of a specific device, use `deviceId` as [id].
@@ -535,6 +546,7 @@ Universal BLE provides a unified and type-safe error handling system across all
### Error Codes
All errors are categorized using the `UniversalBleErrorCode` enum, which includes codes for:
- Connection errors (timeout, failed, rejected, etc.)
- Pairing errors (failed, cancelled, not allowed, etc.)
- Operation errors (not supported, timeout, cancelled, etc.)
@@ -601,10 +613,14 @@ BleUuidParser.number(0x180A); // "0000180a-0000-1000-8000-00805f9b34fb"
BleUuidParser.compare("180a","0000180A-0000-1000-8000-00805F9B34FB"); // true
```
## Platform-specific Setup
## Permissions
You need to perform the following setups:
### Android
#### Manifest Permissions
Add the following permissions to your AndroidManifest.xml file:
```xml
@@ -623,9 +639,16 @@ If your app uses iBeacons or BLUETOOTH_SCAN to determine location, change the la
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
```
You need to programmatically request permissions on runtime. You could use a package such as [permission_handler](https://pub.dev/packages/permission_handler).
For Android 12+, request `Permission.bluetoothScan` and `Permission.bluetoothConnect`.
For Android 11 and below, request `Permission.location`.
#### Android Location Permission
The `withAndroidFineLocation` parameter in `requestPermissions()` controls location permission requests on Android:
- **Android 12+ (API 31+)**:
- `withAndroidFineLocation: true` → Requests `ACCESS_FINE_LOCATION` permission
- `withAndroidFineLocation: false` → Only requests Bluetooth permissions (no location permission)
- **Android 11 and below**:
- Location permission is always requested if declared in your manifest (required for BLE scanning)
- The `withAndroidFineLocation` parameter is ignored
### iOS / macOS
@@ -633,13 +656,20 @@ Add `NSBluetoothPeripheralUsageDescription` and `NSBluetoothAlwaysUsageDescripti
Add the `Bluetooth` capability to the macOS app from Xcode.
### Windows / Linux
**Permissions are automatically requested when calling `startScan()`.** You can also manually call `requestPermissions()` if needed.
### Windows
Your Bluetooth adapter needs to support at least Bluetooth 4.0. If you have more than 1 adapters, the first one returned from the system will be picked.
When publishing on Windows, you need to declare the following [capabilities](https://learn.microsoft.com/en-us/windows/uwp/packaging/app-capability-declarations): `bluetooth, radios`.
### Linux
Your Bluetooth adapter needs to support at least Bluetooth 4.0. If you have more than 1 adapters, the first one returned from the system will be picked.
When publishing on Linux as a snap, you need to declare the `bluez` plug in `snapcraft.yaml`.
```
...
plugs:
@@ -668,6 +698,42 @@ UniversalBle.startScan(
)
```
**No runtime permissions are required.** The `requestPermissions()` method always succeeds on Web.
### Manually Requesting Permissions
**Calling `requestPermissions()` is optional.** Permissions are automatically requested when calling `startScan()`. However, you can manually call `requestPermissions()` if you want to:
- Request permissions before scanning (e.g., to handle permission errors separately)
- Ensure permissions are granted before other operations like `connect()`, `read()`, `write()`, etc., which don't automatically request permissions
The `requestPermissions()` method:
- Returns successfully if all permissions are already granted or accepted by the user
- Throws a `UniversalBleException` if permissions are denied by the user
- Always succeeds on `Windows`, `Linux`, and `Web` (no runtime permissions required)
```dart
// Optional: Manually request permissions
UniversalBle.requestPermissions(
withAndroidFineLocation: false,
);
```
> **Note**: When calling `startScan()`, permissions are automatically requested. To configure location permission requests during scanning, use the `platformConfig` parameter:
```dart
UniversalBle.startScan(
platformConfig: PlatformConfig(
android: AndroidOptions(
requestLocationPermission: false,
),
),
);
```
**No runtime permissions are required.** The `requestPermissions()` method always succeeds on Windows and Linux platforms.
## Customizing Platform Implementation of Universal Ble
```dart
@@ -688,7 +754,8 @@ For more granular control, you can use the [Low-Level API](README.low_level.md).
Here are some of the apps leveraging the power of `universal_ble` in production:
| <img src="assets/bt_cam_icon.svg" alt="BT Cam Icon" width="224" height="224"> | [**BT Cam**](https://btcam.app)<br>A Bluetooth remote app for DSLR and mirrorless cameras. Compatible with Canon, Nikon, Sony, Fujifilm, GoPro, Olympus, Panasonic, Pentax, and Blackmagic. Built using Universal BLE to connect and control cameras across iOS, Android, macOS, Windows, Linux & Web. |
|:--:|:--|
| :---------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
> 💡 **Built something cool with Universal BLE?**
> We'd love to showcase your app here!
> Open a pull request and add it to this section. Please include your app icon in svg!
> Open a pull request and add it to this section. Please include your app icon in svg!
@@ -0,0 +1,249 @@
package com.navideck.universal_ble
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.core.app.ActivityCompat
import android.Manifest
import android.annotation.SuppressLint
private const val TAG = "PermissionHandler"
/**
* Handles Bluetooth-related permission requests for Android.
* Automatically determines which permissions to request based on:
* 1. Android version (SDK level)
* 2. Permissions declared in AndroidManifest.xml
*/
class PermissionHandler(
private val context: Context,
private val activity: Activity,
private val requestCode: Int,
) {
private var permissionRequestCallback: ((Result<Unit>) -> Unit)? = null
/**
* Requests the required Bluetooth permissions based on the manifest and Android version.
*
* @param callback Called with the result of the permission request
*/
fun requestPermissions(
withFineLocation: Boolean,
callback: (Result<Unit>) -> Unit,
) {
// Validate required permissions are declared in manifest
val validationError = validateRequiredPermissions(withFineLocation)
if (validationError != null) {
callback(Result.failure(validationError))
return
}
// Check which permissions are declared in manifest
val permissionsToRequest = getRequiredPermissions(withFineLocation)
if (permissionsToRequest.isEmpty()) {
// All required permissions are already granted
callback(Result.success(Unit))
return
}
// Check if we already have a pending permission request
if (permissionRequestCallback != null) {
callback(
Result.failure(
createFlutterError(
UniversalBleErrorCode.OPERATION_IN_PROGRESS,
"Permission request already in progress"
)
)
)
return
}
permissionRequestCallback = callback
ActivityCompat.requestPermissions(
activity,
permissionsToRequest.toTypedArray(),
requestCode
)
}
/**
* Handles permission request results.
* Should be called from onRequestPermissionsResult in the Activity.
*
* @param requestCode The request code from the permission request
* @param permissions The permissions that were requested
* @param grantResults The grant results for each permission
* @return true if the request code matches and the result was handled, false otherwise
*/
fun handlePermissionResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray,
): Boolean {
if (requestCode != this.requestCode) {
return false
}
val callback = permissionRequestCallback ?: return false
permissionRequestCallback = null
val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
if (allGranted) {
callback(Result.success(Unit))
} else {
val deniedPermissions = permissions.filterIndexed { index, _ ->
grantResults[index] != PackageManager.PERMISSION_GRANTED
}
callback(
Result.failure(
createFlutterError(
UniversalBleErrorCode.FAILED,
"Permissions denied: ${deniedPermissions.joinToString(", ")}"
)
)
)
}
return true
}
/**
* Determines which permissions need to be requested based on:
* 1. Android version
* 2. Permissions declared in AndroidManifest.xml
* 3. Whether user wants to request location permission (withFineLocation parameter)
*
* @param withFineLocation If true, request location permission when needed.
* On Android 11 and below, location is always requested if declared
* (it's mandatory for BLE scanning).
*
* Returns a list of permissions that need to be requested (excluding already granted ones)
*/
private fun getRequiredPermissions(withFineLocation: Boolean): List<String> {
val permissionsToRequest = mutableListOf<String>()
// Android 12+ (API 31+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// BLUETOOTH_SCAN is mandatory
if (!hasPermissionGranted(Manifest.permission.BLUETOOTH_SCAN)) {
permissionsToRequest.add(Manifest.permission.BLUETOOTH_SCAN)
}
// BLUETOOTH_CONNECT is mandatory
if (!hasPermissionGranted(Manifest.permission.BLUETOOTH_CONNECT)) {
permissionsToRequest.add(Manifest.permission.BLUETOOTH_CONNECT)
}
// Location permission is optional - only request if user wants it
if (withFineLocation) {
// Prefer ACCESS_FINE_LOCATION over ACCESS_COARSE_LOCATION
permissionsToRequest.addAll(getLocationPermissionsToAsk())
}
} else {
// Android 11 and below
if (!hasPermissionGranted(Manifest.permission.BLUETOOTH)) {
permissionsToRequest.add(Manifest.permission.BLUETOOTH)
}
// Location permission is MANDATORY
// Prefer ACCESS_FINE_LOCATION over ACCESS_COARSE_LOCATION
permissionsToRequest.addAll(getLocationPermissionsToAsk())
}
return permissionsToRequest
}
private fun getLocationPermissionsToAsk(): List<String> {
val permissionsToRequest = mutableListOf<String>()
if (hasPermissionInManifest(Manifest.permission.ACCESS_FINE_LOCATION)) {
if (!hasPermissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)) {
permissionsToRequest.add(Manifest.permission.ACCESS_FINE_LOCATION)
}
} else if (hasPermissionInManifest(Manifest.permission.ACCESS_COARSE_LOCATION)) {
if (!hasPermissionGranted(Manifest.permission.ACCESS_COARSE_LOCATION)) {
permissionsToRequest.add(Manifest.permission.ACCESS_COARSE_LOCATION)
}
}
return permissionsToRequest
}
/**
* Checks if a permission is declared in AndroidManifest.xml
*/
private fun hasPermissionInManifest(permission: String): Boolean {
return try {
val packageInfo = context.packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_PERMISSIONS
)
packageInfo.requestedPermissions?.contains(permission) == true
} catch (e: Exception) {
Log.e(TAG, "Error checking permission in manifest: ${e.message}")
false
}
}
private fun hasPermissionGranted(permission: String): Boolean {
return ActivityCompat.checkSelfPermission(
context,
permission
) == PackageManager.PERMISSION_GRANTED
}
/**
* Validates that all required permissions are declared in AndroidManifest.xml.
* Returns an error if any required permission is missing.
*
* @param withFineLocation Whether location permission should be requested
* @return FlutterError if validation fails, null if all required permissions are declared
*/
private fun validateRequiredPermissions(withFineLocation: Boolean): FlutterError? {
val sdkInt = Build.VERSION.SDK_INT
val missingPermissions = mutableListOf<String>()
val hasDeclaredFineLocation =
hasPermissionInManifest(Manifest.permission.ACCESS_FINE_LOCATION)
val hasDeclaredCoarseLocation =
hasPermissionInManifest(Manifest.permission.ACCESS_COARSE_LOCATION)
val hasDeclaredLocationPermission = hasDeclaredFineLocation || hasDeclaredCoarseLocation
// Android 12+ (API 31+)
@SuppressLint("InlinedApi")
if (sdkInt >= Build.VERSION_CODES.S) {
// BLUETOOTH_SCAN is mandatory on Android 12+
if (!hasPermissionInManifest(Manifest.permission.BLUETOOTH_SCAN)) {
missingPermissions.add(Manifest.permission.BLUETOOTH_SCAN)
}
// BLUETOOTH_CONNECT is mandatory on Android 12+
if (!hasPermissionInManifest(Manifest.permission.BLUETOOTH_CONNECT)) {
missingPermissions.add(Manifest.permission.BLUETOOTH_CONNECT)
}
// Location permission is optional on Android 12+ (depends on neverForLocation and withFineLocation)
// Only validate if it's actually needed
if (withFineLocation && !hasDeclaredLocationPermission) {
missingPermissions.add("${Manifest.permission.ACCESS_FINE_LOCATION} or ${Manifest.permission.ACCESS_COARSE_LOCATION}")
}
} else {
// Android 11 and below
if (!hasPermissionInManifest(Manifest.permission.BLUETOOTH)) {
missingPermissions.add(Manifest.permission.BLUETOOTH)
}
// Android 11 and below, Location permission is MANDATORY for BLE scanning
if (!hasDeclaredLocationPermission) {
missingPermissions.add("${Manifest.permission.ACCESS_FINE_LOCATION} or ${Manifest.permission.ACCESS_COARSE_LOCATION}")
}
}
if (missingPermissions.isNotEmpty()) {
return createFlutterError(
UniversalBleErrorCode.FAILED,
"Required permissions are not declared in AndroidManifest.xml",
"Missing permissions: ${missingPermissions.joinToString(", ")}. " +
"Please add these permissions to your AndroidManifest.xml file. " +
"See README.md for more information."
)
}
return null
}
}
@@ -443,6 +443,7 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() {
*/
interface UniversalBlePlatformChannel {
fun getBluetoothAvailabilityState(callback: (Result<Long>) -> Unit)
fun requestPermissions(withAndroidFineLocation: Boolean, callback: (Result<Unit>) -> Unit)
fun enableBluetooth(callback: (Result<Boolean>) -> Unit)
fun disableBluetooth(callback: (Result<Boolean>) -> Unit)
fun startScan(filter: UniversalScanFilter?)
@@ -488,6 +489,25 @@ interface UniversalBlePlatformChannel {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestPermissions$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val withAndroidFineLocationArg = args[0] as Boolean
api.requestPermissions(withAndroidFineLocationArg) { result: Result<Unit> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(UniversalBlePigeonUtils.wrapError(error))
} else {
reply.reply(UniversalBlePigeonUtils.wrapResult(null))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.enableBluetooth$separatedMessageChannelSuffix", codec)
if (api != null) {
@@ -39,9 +39,12 @@ private const val TAG = "UniversalBlePlugin"
@SuppressLint("MissingPermission")
class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), FlutterPlugin,
ActivityAware, PluginRegistry.ActivityResultListener {
ActivityAware, PluginRegistry.ActivityResultListener,
PluginRegistry.RequestPermissionsResultListener {
private val bluetoothEnableRequestCode = 2342313
private val bluetoothDisableRequestCode = 2342414
private val permissionRequestCode = 2342515
private var permissionHandler: PermissionHandler? = null
private var callbackChannel: UniversalBleCallbackChannel? = null
private var mainThreadHandler: Handler? = null
private lateinit var context: Context
@@ -95,6 +98,20 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
)
}
override fun requestPermissions(withAndroidFineLocation: Boolean, callback: (Result<Unit>) -> Unit) {
if (permissionHandler == null) {
callback(
Result.failure(
createFlutterError(
UniversalBleErrorCode.FAILED,
"PermissionHandler is not initialized"
)
)
)
}
permissionHandler?.requestPermissions(withAndroidFineLocation, callback)
}
override fun enableBluetooth(callback: (Result<Boolean>) -> Unit) {
if (bluetoothManager.adapter.isEnabled) {
callback(Result.success(true))
@@ -188,7 +205,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
}
override fun isScanning(): Boolean {
return safeScanner.isScanning()
return safeScanner.isScanning()
}
override fun connect(deviceId: String) {
@@ -1130,13 +1147,28 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
permissionHandler = PermissionHandler(context, binding.activity, permissionRequestCode)
binding.addActivityResultListener(this)
binding.addRequestPermissionsResultListener(this)
}
override fun onDetachedFromActivity() {
activity = null
permissionHandler = null
}
override fun onDetachedFromActivityForConfigChanges() {}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
permissionHandler = PermissionHandler(context, binding.activity, permissionRequestCode)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray,
): Boolean {
return permissionHandler?.handlePermissionResult(requestCode, permissions, grantResults)
?: false
}
}
+18
View File
@@ -473,6 +473,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 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)
func startScan(filter: UniversalScanFilter?) throws
@@ -513,6 +514,23 @@ class UniversalBlePlatformChannelSetup {
} else {
getBluetoothAvailabilityStateChannel.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
let args = message as! [Any?]
let withAndroidFineLocationArg = args[0] as! Bool
api.requestPermissions(withAndroidFineLocation: withAndroidFineLocationArg) { result in
switch result {
case .success:
reply(wrapResult(nil))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
requestPermissionsChannel.setMessageHandler(nil)
}
let enableBluetoothChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.enableBluetooth\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
enableBluetoothChannel.setMessageHandler { _, reply in
+27
View File
@@ -33,6 +33,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
private var universalBleFilterUtil = UniversalBleFilterUtil()
private lazy var manager: CBCentralManager = .init(delegate: self, queue: nil)
private var availabilityStateUpdateHandlers: [(Result<Int64, Error>) -> Void] = []
private var requestPermissionStateUpdateHandlers: [(Result<Void, Error>) -> Void] = []
private var discoveredServicesProgressMap: [String: [UniversalBleService]] = [:]
private var characteristicReadFutures = [CharacteristicReadFuture]()
private var characteristicWriteFutures = [CharacteristicWriteFuture]()
@@ -55,6 +56,27 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
}
}
func requestPermissions(withAndroidFineLocation _: Bool, completion: @escaping (Result<Void, any Error>) -> Void) {
if manager.state != .unknown {
completePermissionRequest(completion: completion)
} else {
requestPermissionStateUpdateHandlers.append(completion)
_ = manager
}
}
func completePermissionRequest(completion: @escaping (Result<Void, any Error>) -> Void) {
let state = manager.state
switch state {
case .unauthorized:
completion(.failure(createFlutterError(code: .bluetoothUnauthorized, message: "Not authorized to access Bluetooth")))
case .unsupported:
completion(.failure(createFlutterError(code: .notSupported, message: "Bluetooth is not supported")))
default:
completion(.success(()))
}
}
func enableBluetooth(completion: @escaping (Result<Bool, Error>) -> Void) {
completion(Result.failure(createFlutterError(code: .notSupported)))
}
@@ -347,6 +369,11 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
handler(.success(state))
return true
}
// Complete Pending permission request handler
requestPermissionStateUpdateHandlers.removeAll { handler in
completePermissionRequest(completion: handler)
return true
}
}
public func centralManager(_: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
+2 -2
View File
@@ -24,7 +24,7 @@ if (flutterVersionName == null) {
android {
namespace "com.navideck.universal_ble_example"
compileSdkVersion 35
compileSdkVersion 36
ndkVersion flutter.ndkVersion
compileOptions {
@@ -45,7 +45,7 @@ android {
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion flutter.minSdkVersion
targetSdkVersion 35
targetSdkVersion 36
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
@@ -1,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30" />
+1 -1
View File
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https://services.gradle.org/distributions/gradle-8.10.2-all.zip
distributionUrl=https://services.gradle.org/distributions/gradle-8.14.3-all.zip
+2 -2
View File
@@ -18,8 +18,8 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.6.1" apply false
id "org.jetbrains.kotlin.android" version "1.9.0" apply false
id "com.android.application" version "8.13.1" apply false
id "org.jetbrains.kotlin.android" version "2.2.21" apply false
}
include ":app"
-12
View File
@@ -1,39 +1,27 @@
PODS:
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- integration_test (0.0.1):
- Flutter
- permission_handler_apple (9.3.0):
- Flutter
- universal_ble (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- universal_ble (from `.symlinks/plugins/universal_ble/darwin`)
EXTERNAL SOURCES:
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
universal_ble:
:path: ".symlinks/plugins/universal_ble/darwin"
SPEC CHECKSUMS:
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
PODFILE CHECKSUM: 4f1c12611da7338d21589c0b2ecd6bd20b109694
@@ -198,7 +198,6 @@
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
2F5EDC3CC74E7431E4CB95DF /* [CP] Embed Pods Frameworks */,
6B74A273405B2F59259FD41D /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -325,23 +324,6 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
6B74A273405B2F59259FD41D /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
7E79F47357B64513A488AC3E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
+5
View File
@@ -131,4 +131,9 @@ class MockUniversalBle extends UniversalBlePlatform {
Future<bool> disableBluetooth() {
throw UnimplementedError();
}
@override
Future<void> requestPermissions({bool withAndroidFineLocation = false}) {
throw UnimplementedError();
}
}
-101
View File
@@ -1,101 +0,0 @@
// ignore_for_file: avoid_print
import 'dart:async';
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:permission_handler/permission_handler.dart';
/*
Required Permissions :
<----------->
IOS :
- Bluetooth
<----------->
Android :
if AndroidVersions < 12
- Location
- Bluetooth
else
- Bluetooth Scan
- Bluetooth Connect
<----------->
Macos :
<----------->
Windows : None
<----------->
Linux : None
<----------->
Web :
Check if Browser Supports Bluetooth
*/
class PermissionHandler {
static Future<bool> arePermissionsGranted() async {
if (!isMobilePlatform) return true;
var status = await _permissionStatus;
bool blePermissionGranted = status[0];
bool locationPermissionGranted = status[1];
if (locationPermissionGranted && blePermissionGranted) return true;
if (!blePermissionGranted) {
PermissionStatus blePermissionCheck =
await Permission.bluetooth.request();
if (blePermissionCheck.isPermanentlyDenied) {
print("Bluetooth Permission Permanently Denied");
openAppSettings();
}
return false;
}
if (!locationPermissionGranted) {
PermissionStatus locationPermissionCheck =
await Permission.location.request();
if (locationPermissionCheck.isPermanentlyDenied) {
print("Location Permission Permanently Denied");
openAppSettings();
}
return false;
}
return false;
}
static Future<List<bool>> get _permissionStatus async {
bool blePermissionGranted = false;
bool locationPermissionGranted = false;
if (await requiresExplicitAndroidBluetoothPermissions) {
bool bleConnectPermission =
(await Permission.bluetoothConnect.request()).isGranted;
bool bleScanPermission =
(await Permission.bluetoothScan.request()).isGranted;
blePermissionGranted = bleConnectPermission && bleScanPermission;
locationPermissionGranted = true;
} else {
PermissionStatus permissionStatus = await Permission.bluetooth.request();
blePermissionGranted = permissionStatus.isGranted;
locationPermissionGranted = await requiresLocationPermission
? (await Permission.locationWhenInUse.request()).isGranted
: true;
}
return [blePermissionGranted, locationPermissionGranted];
}
static bool get isMobilePlatform =>
!kIsWeb && (Platform.isAndroid || Platform.isIOS);
static Future<bool> get requiresLocationPermission async =>
!kIsWeb &&
Platform.isAndroid &&
(!await requiresExplicitAndroidBluetoothPermissions);
static Future<bool> get requiresExplicitAndroidBluetoothPermissions async {
if (kIsWeb || !Platform.isAndroid) return false;
AndroidDeviceInfo androidInfo = await DeviceInfoPlugin().androidInfo;
return androidInfo.version.sdkInt >= 31;
}
}
+8 -8
View File
@@ -7,7 +7,6 @@ import 'package:universal_ble_example/data/mock_universal_ble.dart';
import 'package:universal_ble_example/home/widgets/scan_filter_widget.dart';
import 'package:universal_ble_example/home/widgets/scanned_devices_placeholder_widget.dart';
import 'package:universal_ble_example/home/widgets/scanned_item_widget.dart';
import 'package:universal_ble_example/data/permission_handler.dart';
import 'package:universal_ble_example/peripheral_details/peripheral_detail_page.dart';
import 'package:universal_ble_example/widgets/platform_button.dart';
import 'package:universal_ble_example/widgets/responsive_buttons_grid.dart';
@@ -90,9 +89,7 @@ class _MyAppState extends State<MyApp> {
}
Future<void> startScan() async {
await UniversalBle.startScan(
scanFilter: scanFilter,
);
await UniversalBle.startScan(scanFilter: scanFilter);
}
Future<void> _getSystemDevices() async {
@@ -221,12 +218,15 @@ class _MyAppState extends State<MyApp> {
),
if (BleCapabilities.requiresRuntimePermission)
PlatformButton(
text: 'Check Permissions',
text: 'Request Permissions',
onPressed: () async {
bool hasPermissions =
await PermissionHandler.arePermissionsGranted();
if (hasPermissions) {
try {
await UniversalBle.requestPermissions(
withAndroidFineLocation: false,
);
showSnackbar("Permissions granted");
} catch (e) {
showSnackbar(e.toString());
}
},
),
@@ -5,10 +5,8 @@
import FlutterMacOS
import Foundation
import device_info_plus
import universal_ble
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
UniversalBlePlugin.register(with: registry.registrar(forPlugin: "UniversalBlePlugin"))
}
-6
View File
@@ -1,26 +1,20 @@
PODS:
- device_info_plus (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- universal_ble (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- universal_ble (from `Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin`)
EXTERNAL SOURCES:
device_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
FlutterMacOS:
:path: Flutter/ephemeral
universal_ble:
:path: Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin
SPEC CHECKSUMS:
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
+8 -93
View File
@@ -81,22 +81,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.11"
device_info_plus:
dependency: "direct main"
description:
name: device_info_plus
sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110"
url: "https://pub.dev"
source: hosted
version: "9.1.2"
device_info_plus_platform_interface:
dependency: transitive
description:
name: device_info_plus_platform_interface
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
url: "https://pub.dev"
source: hosted
version: "7.0.3"
expandable:
dependency: "direct main"
description:
@@ -160,11 +144,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
@@ -179,10 +158,10 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0"
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.1"
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
@@ -235,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:
@@ -247,54 +226,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
url: "https://pub.dev"
source: hosted
version: "11.4.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
url: "https://pub.dev"
source: hosted
version: "12.1.0"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
url: "https://pub.dev"
source: hosted
version: "9.4.7"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.dev"
source: hosted
version: "0.1.3+5"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
url: "https://pub.dev"
source: hosted
version: "4.3.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
petitparser:
dependency: transitive
description:
@@ -384,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:
@@ -402,7 +333,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.21.1"
version: "1.0.0"
vector_math:
dependency: transitive
description:
@@ -435,22 +366,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.0"
win32:
dependency: transitive
description:
name: win32
sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
url: "https://pub.dev"
source: hosted
version: "5.14.0"
win32_registry:
dependency: transitive
description:
name: win32_registry
sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852"
url: "https://pub.dev"
source: hosted
version: "1.1.5"
xml:
dependency: transitive
description:
@@ -461,4 +376,4 @@ packages:
version: "6.6.1"
sdks:
dart: ">=3.8.0 <4.0.0"
flutter: ">=3.29.0"
flutter: ">=3.18.0-18.0.pre.54"
-2
View File
@@ -12,8 +12,6 @@ dependencies:
convert: ^3.1.1
expandable: ^5.0.1
cupertino_icons: ^1.0.2
permission_handler: ^11.3.1
device_info_plus: ^9.0.2
universal_ble:
path: ../
@@ -6,12 +6,9 @@
#include "generated_plugin_registrant.h"
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <universal_ble/universal_ble_plugin_c_api.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
UniversalBlePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UniversalBlePluginCApi"));
}
@@ -3,7 +3,6 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
permission_handler_windows
universal_ble
)
+9 -1
View File
@@ -1,8 +1,9 @@
/// Platform specific config to scan devices
class PlatformConfig {
WebOptions? web;
AndroidOptions? android;
PlatformConfig({this.web});
PlatformConfig({this.web, this.android});
}
/// Web options to scan devices
@@ -21,3 +22,10 @@ class WebOptions {
this.optionalManufacturerData = const [],
});
}
/// Android options to scan devices
class AndroidOptions {
bool requestLocationPermission;
AndroidOptions({this.requestLocationPermission = true});
}
+14
View File
@@ -63,6 +63,20 @@ class UniversalBle {
);
}
/// 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.
/// [withAndroidFineLocation] is used to request fine location permission on Android 12+ (API 31+).
/// on Android lower than 12, this method will request location permission regardless of the [withAndroidFineLocation] value.
/// `Windows`, `Linux` and `Web` will always succeed.
static Future<void> requestPermissions({
bool withAndroidFineLocation = false,
}) async {
return _platform.requestPermissions(
withAndroidFineLocation: withAndroidFineLocation,
);
}
/// Start scan.
/// Scan results will arrive in [onScanResult] listener.
/// It might throw errors if Bluetooth is not available.
+2 -1
View File
@@ -15,7 +15,8 @@ class UniversalBleException implements Exception {
});
@override
String toString() => message;
String toString() =>
"UniversalBleException: Code: $code, Message: $message, Details: $details";
factory UniversalBleException.fromError(dynamic error) {
String message = error.toString();
@@ -402,6 +402,12 @@ 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
@@ -522,6 +522,32 @@ class UniversalBlePlatformChannel {
}
}
Future<void> requestPermissions(bool withAndroidFineLocation) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestPermissions$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture =
pigeonVar_channel.send(<Object?>[withAndroidFineLocation]);
final List<Object?>? 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 {
return;
}
}
Future<bool> enableBluetooth() async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.enableBluetooth$pigeonVar_messageChannelSuffix';
@@ -44,7 +44,7 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform {
ScanFilter? scanFilter,
PlatformConfig? platformConfig,
}) async {
await _ensureInitialized();
await _ensureInitialized(platformConfig);
_bleFilter.scanFilter = scanFilter;
await _executeWithErrorHandling(
() => _channel.startScan(
@@ -142,6 +142,14 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform {
Future<void> unpair(String deviceId) =>
_executeWithErrorHandling(() => _channel.unPair(deviceId));
@override
Future<void> requestPermissions(
{bool withAndroidFineLocation = false}) async {
await _executeWithErrorHandling(
() => _channel.requestPermissions(withAndroidFineLocation),
);
}
@override
Future<List<BleDevice>> getSystemDevices(
List<String>? withServices,
@@ -179,26 +187,15 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform {
}
}
Future<void> _ensureInitialized() async {
// Check bluetooth availability on Apple
// so that it will ask permission only when required, and throw error on failed
if (defaultTargetPlatform == TargetPlatform.iOS ||
Future<void> _ensureInitialized(PlatformConfig? platformConfig) async {
// Check bluetooth availability on Apple and Android
if (defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.macOS) {
AvailabilityState state = await getBluetoothAvailabilityState();
switch (state) {
case AvailabilityState.unauthorized:
throw UniversalBleException(
code: UniversalBleErrorCode.bluetoothUnauthorized,
message: "Not authorized to access Bluetooth",
);
case AvailabilityState.unsupported:
throw UniversalBleException(
code: UniversalBleErrorCode.notSupported,
message: "Bluetooth is not supported",
);
default:
// Ignore rest..
}
await requestPermissions(
withAndroidFineLocation:
platformConfig?.android?.requestLocationPermission ?? false,
);
}
}
}
@@ -36,6 +36,8 @@ abstract class UniversalBlePlatform {
Future<bool> disableBluetooth();
Future<void> requestPermissions({bool withAndroidFineLocation = false});
Future<void> startScan({
ScanFilter? scanFilter,
PlatformConfig? platformConfig,
@@ -251,6 +251,12 @@ 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(
+3
View File
@@ -24,6 +24,9 @@ abstract class UniversalBlePlatformChannel {
@async
int getBluetoothAvailabilityState();
@async
void requestPermissions(bool withAndroidFineLocation);
@async
bool enableBluetooth();
+5
View File
@@ -124,4 +124,9 @@ class _UniversalBleMock extends UniversalBlePlatformMock {
{Duration? timeout}) async {
return charValue ?? Uint8List(0);
}
@override
Future<void> requestPermissions({bool withAndroidFineLocation = false}) {
throw UnimplementedError();
}
}
+29
View File
@@ -534,6 +534,35 @@ void UniversalBlePlatformChannel::SetUp(
channel.SetMessageHandler(nullptr);
}
}
{
BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestPermissions" + 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);
api->RequestPermissions(with_android_fine_location_arg, [reply](std::optional<FlutterError>&& output) {
if (output.has_value()) {
reply(WrapError(output.value()));
return;
}
EncodableList wrapped;
wrapped.push_back(EncodableValue());
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.enableBluetooth" + prepended_suffix, &GetCodec());
if (api != nullptr) {
+3
View File
@@ -348,6 +348,9 @@ class UniversalBlePlatformChannel {
UniversalBlePlatformChannel& operator=(const UniversalBlePlatformChannel&) = delete;
virtual ~UniversalBlePlatformChannel() {}
virtual void GetBluetoothAvailabilityState(std::function<void(ErrorOr<int64_t> reply)> result) = 0;
virtual void RequestPermissions(
bool with_android_fine_location,
std::function<void(std::optional<FlutterError> reply)> result) = 0;
virtual void EnableBluetooth(std::function<void(ErrorOr<bool> reply)> result) = 0;
virtual void DisableBluetooth(std::function<void(ErrorOr<bool> reply)> result) = 0;
virtual std::optional<FlutterError> StartScan(const UniversalScanFilter* filter) = 0;
+8
View File
@@ -118,6 +118,14 @@ void UniversalBlePlugin::DisableBluetooth(
});
}
void UniversalBlePlugin::RequestPermissions(
bool with_android_fine_location,
std::function<void(std::optional<FlutterError> reply)> result) {
// Windows does not require runtime permissions for Bluetooth
result(std::nullopt);
return;
}
std::optional<FlutterError>
UniversalBlePlugin::StartScan(const UniversalScanFilter *filter) {
+3
View File
@@ -161,6 +161,9 @@ namespace universal_ble
ErrorOr<bool> IsScanning() override;
std::optional<FlutterError> Connect(const std::string &device_id) override;
std::optional<FlutterError> Disconnect(const std::string &device_id) override;
void RequestPermissions(
bool with_android_fine_location,
std::function<void(std::optional<FlutterError> reply)> result) override;
void DiscoverServices(
const std::string &device_id,
std::function<void(ErrorOr<flutter::EncodableList> reply)> result) override;