Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -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(
Expand All @@ -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()) {
Expand All @@ -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(
Expand All @@ -76,7 +104,7 @@ class PermissionHandler(

permissionRequestCallback = callback
ActivityCompat.requestPermissions(
activity,
currentActivity,
permissionsToRequest.toTypedArray(),
requestCode
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -86,6 +87,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
context.unregisterReceiver(broadcastReceiver)
callbackChannel = null
mainThreadHandler = null
permissionHandler = null
}

override fun getBluetoothAvailabilityState(callback: (Result<Long>) -> Unit) {
Expand Down Expand Up @@ -114,6 +116,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
)
)
)
return
}
permissionHandler?.requestPermissions(withAndroidFineLocation, callback)
}
Expand Down Expand Up @@ -1304,20 +1307,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(
Expand Down
11 changes: 11 additions & 0 deletions example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30" />

<!-- Foreground service permissions for background BLE monitoring -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application
android:label="Universal BLE"
android:name="${applicationName}"
Expand Down Expand Up @@ -35,5 +40,11 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />

<!-- Foreground service for background BLE monitoring -->
<service
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
android:foregroundServiceType="connectedDevice"
android:exported="false" />
</application>
</manifest>
Loading