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:
Foti Dim
2026-02-04 20:12:12 +02:00
committed by GitHub
parent bcf65e4573
commit ceda9a31f7
5 changed files with 62 additions and 8 deletions
+1
View File
@@ -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
+17
View File
@@ -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) {