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
@@ -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(