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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
|
||||
@@ -131,4 +131,9 @@ class MockUniversalBle extends UniversalBlePlatform {
|
||||
Future<bool> disableBluetooth() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> requestPermissions({bool withAndroidFineLocation = false}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -24,6 +24,9 @@ abstract class UniversalBlePlatformChannel {
|
||||
@async
|
||||
int getBluetoothAvailabilityState();
|
||||
|
||||
@async
|
||||
void requestPermissions(bool withAndroidFineLocation);
|
||||
|
||||
@async
|
||||
bool enableBluetooth();
|
||||
|
||||
|
||||
@@ -124,4 +124,9 @@ class _UniversalBleMock extends UniversalBlePlatformMock {
|
||||
{Duration? timeout}) async {
|
||||
return charValue ?? Uint8List(0);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> requestPermissions({bool withAndroidFineLocation = false}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user