Background service support on Android (#215)
* - Added support for BLE scanning from background services on Android. - Enhanced `PermissionHandler` to be activity-aware, allowing silent success for already granted permissions when no activity is available. - Updated README with best practices for requesting permissions in foreground before background operations. * Simplify implementation
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
* Add `autoConnect` parameter to `connect()` method for automatic reconnection support on Android and iOS/macOS
|
||||
* Add `serviceData` in `BleDevice`
|
||||
* Add `AndroidScanMode` and `reportDelayMillis` to `AndroidOptions` for scanning
|
||||
* Add support for BLE scanning from background services (ForegroundTask) on Android. `PermissionHandler` is now activity-aware and succeeds silently if permissions are already granted when no activity is available
|
||||
|
||||
## 1.1.0
|
||||
* Add readRssi method
|
||||
|
||||
@@ -657,6 +657,23 @@ The `withAndroidFineLocation` parameter in `requestPermissions()` controls locat
|
||||
- Location permission is always requested if declared in your manifest (required for BLE scanning)
|
||||
- The `withAndroidFineLocation` parameter is ignored
|
||||
|
||||
#### Background Scanning (ForegroundTask)
|
||||
|
||||
Universal BLE supports BLE scanning from background services (e.g., using `flutter_foreground_task` or similar packages) on Android. When running in a background context without an Activity:
|
||||
|
||||
- **If permissions are already granted**: Scanning works normally
|
||||
- **If permissions are not granted**: An error is thrown with the message "Permissions not granted and activity is not available to request them"
|
||||
|
||||
**Best Practice**: Request permissions while your app is in the foreground before starting any background BLE operations:
|
||||
|
||||
```dart
|
||||
// Request permissions in foreground (e.g., during app setup)
|
||||
await UniversalBle.requestPermissions();
|
||||
|
||||
// Later, in your ForegroundTask, scanning will work if permissions were granted
|
||||
await UniversalBle.startScan();
|
||||
```
|
||||
|
||||
### iOS / macOS
|
||||
|
||||
Add `NSBluetoothPeripheralUsageDescription` and `NSBluetoothAlwaysUsageDescription` to Info.plist of your iOS and macOS app.
|
||||
|
||||
@@ -19,9 +19,17 @@ private const val TAG = "PermissionHandler"
|
||||
*/
|
||||
class PermissionHandler(
|
||||
private val context: Context,
|
||||
private val activity: Activity,
|
||||
private val requestCode: Int,
|
||||
) {
|
||||
private var activity: Activity? = null
|
||||
|
||||
/**
|
||||
* Attaches or detaches an activity to the permission handler.
|
||||
* Call with an Activity when attached, or null when detached.
|
||||
*/
|
||||
fun attachActivity(activity: Activity?) {
|
||||
this.activity = activity
|
||||
}
|
||||
private var permissionRequestCallback: ((Result<Unit>) -> Unit)? = null
|
||||
|
||||
/**
|
||||
@@ -39,6 +47,10 @@ class PermissionHandler(
|
||||
/**
|
||||
* Requests the required Bluetooth permissions based on the manifest and Android version.
|
||||
*
|
||||
* If activity is not available (e.g., running in a ForegroundTask), this method will
|
||||
* check if all required permissions are already granted and succeed silently if so.
|
||||
* It will only fail if permissions are needed but cannot be requested due to missing activity.
|
||||
*
|
||||
* @param callback Called with the result of the permission request
|
||||
*/
|
||||
fun requestPermissions(
|
||||
@@ -52,7 +64,7 @@ class PermissionHandler(
|
||||
return
|
||||
}
|
||||
|
||||
// Check which permissions are declared in manifest
|
||||
// Check which permissions need to be requested
|
||||
val permissionsToRequest = getRequiredPermissions(withFineLocation)
|
||||
|
||||
if (permissionsToRequest.isEmpty()) {
|
||||
@@ -61,6 +73,22 @@ class PermissionHandler(
|
||||
return
|
||||
}
|
||||
|
||||
// Permissions need to be requested - check if activity is available
|
||||
val currentActivity = activity
|
||||
if (currentActivity == null) {
|
||||
// No activity available and permissions not granted
|
||||
callback(
|
||||
Result.failure(
|
||||
createFlutterError(
|
||||
UniversalBleErrorCode.FAILED,
|
||||
"Permissions not granted and activity is not available to request them. " +
|
||||
"Please request permissions while the app is in foreground."
|
||||
)
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we already have a pending permission request
|
||||
if (permissionRequestCallback != null) {
|
||||
callback(
|
||||
@@ -76,7 +104,7 @@ class PermissionHandler(
|
||||
|
||||
permissionRequestCallback = callback
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
currentActivity,
|
||||
permissionsToRequest.toTypedArray(),
|
||||
requestCode
|
||||
)
|
||||
|
||||
@@ -68,6 +68,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
|
||||
callbackChannel = UniversalBleCallbackChannel(flutterPluginBinding.binaryMessenger)
|
||||
context = flutterPluginBinding.applicationContext
|
||||
mainThreadHandler = Handler(Looper.getMainLooper())
|
||||
permissionHandler = PermissionHandler(context, permissionRequestCode)
|
||||
bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||
safeScanner = SafeScanner(bluetoothManager)
|
||||
|
||||
@@ -86,6 +87,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
|
||||
context.unregisterReceiver(broadcastReceiver)
|
||||
callbackChannel = null
|
||||
mainThreadHandler = null
|
||||
permissionHandler = null
|
||||
}
|
||||
|
||||
override fun getBluetoothAvailabilityState(callback: (Result<Long>) -> Unit) {
|
||||
@@ -1304,20 +1306,23 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
permissionHandler = PermissionHandler(context, binding.activity, permissionRequestCode)
|
||||
binding.addActivityResultListener(this)
|
||||
binding.addRequestPermissionsResultListener(this)
|
||||
permissionHandler?.attachActivity(binding.activity)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
activity = null
|
||||
permissionHandler = null
|
||||
permissionHandler?.attachActivity(null)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
// Activity will be reattached, keep the reference
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {}
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
permissionHandler = PermissionHandler(context, binding.activity, permissionRequestCode)
|
||||
permissionHandler?.attachActivity(binding.activity)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
|
||||
@@ -209,7 +209,10 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform {
|
||||
}
|
||||
|
||||
Future<void> _ensureInitialized(PlatformConfig? platformConfig) async {
|
||||
// Check bluetooth availability on Apple and Android
|
||||
// Request permissions on Apple and Android
|
||||
// On Android: If activity is available, requests permissions if needed.
|
||||
// If activity is not available (e.g., ForegroundTask), succeeds
|
||||
// if permissions are already granted, fails otherwise.
|
||||
if (defaultTargetPlatform == TargetPlatform.android ||
|
||||
defaultTargetPlatform == TargetPlatform.iOS ||
|
||||
defaultTargetPlatform == TargetPlatform.macOS) {
|
||||
|
||||
Reference in New Issue
Block a user