Permissions and pairing¶
The permission matrix¶
BlueLib asks for exactly the permissions the running Android release requires, and nothing else.
| Operation | Android 5–11 (API 21–30) | Android 12+ (API 31+) |
|---|---|---|
BluetoothOperation.SCAN |
BLUETOOTH, BLUETOOTH_ADMIN, ACCESS_FINE_LOCATION |
BLUETOOTH_SCAN |
BluetoothOperation.BACKGROUND_SCAN |
the above plus ACCESS_BACKGROUND_LOCATION (API 29+) |
BLUETOOTH_SCAN (+ background location unless the app is exempt) |
BluetoothOperation.CONNECT |
BLUETOOTH, BLUETOOTH_ADMIN |
BLUETOOTH_CONNECT |
BluetoothOperation.GATT_SERVER |
BLUETOOTH, BLUETOOTH_ADMIN |
BLUETOOTH_CONNECT |
BluetoothOperation.CLASSIC_DISCOVERY |
BLUETOOTH, BLUETOOTH_ADMIN, ACCESS_FINE_LOCATION |
BLUETOOTH_CONNECT, BLUETOOTH_SCAN |
BluetoothOperation.ADVERTISE |
BLUETOOTH, BLUETOOTH_ADMIN |
BLUETOOTH_ADVERTISE |
BluetoothOperation.RANGING |
not available | RANGING (Android 16+) |
val report = blueLib.permissionsFor(BluetoothOperation.SCAN)
when {
report.isSatisfied -> scan()
report.notDeclared.isNotEmpty() -> crashInDebug("Add ${report.notDeclared} to the manifest")
else -> launcher.launch(report.missing.toTypedArray())
}
missing is "the user said no (or never answered)" and notDeclared is "the app never asked for it in
the manifest". They are never mixed, because the fixes are completely different: one is a UI flow, the
other is a code change. A permission that is not declared at all counts as missing — checkSelfPermission
reports it as denied on every release, so treating it as granted would only delay the failure to the
platform call.
neverForLocation¶
If your app asserts that scan results are never used to derive location, tell BlueLib so it stops
demanding ACCESS_FINE_LOCATION on Android 12+:
This mirrors android:usesPermissionFlags="neverForLocation" on the BLUETOOTH_SCAN declaration. Both
places matter: the manifest declaration is what Google Play evaluates, the meta-data is what BlueLib
reads at runtime.
Bluetooth-off handling¶
BlueLib never calls BluetoothAdapter.enable() (deprecated and privileged). Instead:
and observe the state through blueLib.adapterState, which is driven by ACTION_STATE_CHANGED.
Bluetooth broadcasts need an exported receiver
android.bluetooth.* broadcasts are sent by the Bluetooth module (its own uid), not by the system
server. A receiver registered with RECEIVER_NOT_EXPORTED is left out of the recipient list without
any error, so bond results, discovery results and adapter changes never arrive. BlueLib registers its
receivers with RECEIVER_EXPORTED; these are protected broadcasts that only the stack can send.
Pairing¶
blueLib.bond(deviceId).onFailure { error ->
when (error) {
is BlueLibError.BondFailed -> show("Pairing failed: ${error.reason}")
is BlueLibError.Timeout -> show("Pairing dialog was not answered")
else -> show(error.message)
}
}
What BlueLib guarantees:
createBond()returningtrueonly means the request was accepted. BlueLib waits forACTION_BOND_STATE_CHANGEDto reportBOND_BONDED, and reportsBOND_NONE(user declined, or the device went out of range) asBondFailedinstead of hanging.- The bond timeout is
BlueLibConfig.bondTimeoutMillis(30 s by default) because pairing needs a user. bondState(deviceId)streams the state, so a UI can show "pairing…" from the firstBONDINGevent.Transport.LEis forwarded tocreateBond(transport)on Android 17+ only: although the transport constants exist since API 30, the publiccreateBond(int)overload is API 37. Below that the platform chooses the transport, and BlueLib says so rather than calling a method that does not exist.
Dynamic Auto Pairing¶
For workflows that require discovering and pairing with a target device without requiring the user to manually enter MAC addresses or pick from a list, BlueLib provides blueLib.autoPair:
val filter = AutoPairFilter(
deviceName = "SmartScale-Pro",
minRssi = -65, // Proximity threshold: device must be nearby
serviceUuids = listOf(BluetoothUuid.parse("0000181d-0000-1000-8000-00805f9b34fb")),
transport = Transport.LE,
)
blueLib.autoPair(filter).onSuccess { deviceId ->
println("Auto-paired with $deviceId")
}.onFailure { error ->
println("Auto-pairing failed: ${error.message}")
}
Dynamic matching criteria supported by AutoPairFilter:
* targetAddress (BluetoothAddress): Exact MAC address matching.
* deviceName (String): Exact device name.
* namePrefix (String): Case-insensitive device name prefix.
* serviceUuids (List<BluetoothUuid>): Advertised GATT service UUIDs.
* manufacturerId (Int): Manufacturer company SIG identifier.
* minRssi (Int): Minimum signal strength threshold in dBm (e.g. -70).
* transport (Transport): Transport selection (AUTO, LE, BREDR).
If the discovered target device is already bonded, autoPair returns immediately with BlueLibResult.Success(deviceId) without re-requesting a bond.
Bond loss on Android 16.1 and 17¶
Android 16.1 added EXTRA_BOND_LOSS_REASON to the bond broadcast, and Android 17 added autonomous
re-pairing: when a bond is lost the platform may run its own pairing flow and broadcast
ACTION_KEY_MISSING only when that attempt fails.
BlueLib therefore never forces a manual pairing flow on Android 17:
blueLib.diagnostics
.filterIsInstance<DiagnosticEvent.ErrorReported>()
.map { it.error }
.filterIsInstance<BlueLibError.BondLost>()
.collect { lost ->
if (lost.systemRepairInProgress) {
show("Reconnecting is being handled by Android…")
} else {
show("The bond to ${lost.device} was lost (${lost.reason}). Pair again to continue.")
}
}
BondLossReason distinguishes an authentication failure, an incoming pairing attempt on the other side,
and an LE encryption failure, which is usually enough to decide whether re-pairing can work at all.
Unpairing¶
BluetoothDevice.removeBond() is still not public API in Android 17. The only supported path is the
Companion Device Manager:
- Android 16+ (API 36): for a device associated through
CompanionDeviceManager.associate(...), BlueLib callsremoveBond(associationId). This is the first release in whichremoveBondis public API — before Android 16 it was a system API, so an app could only appear to work on those releases. - For a device the app is not associated with, the call fails with
OperationRejectedexplaining that the user has to forget the device from the system Bluetooth settings — which is the honest answer.
Companion device pairing¶
For devices that should be re-discoverable in the background, Android's recommended flow is
CompanionDeviceManager.associate(AssociationRequest, …) with a
BluetoothDeviceFilter/BluetoothLeDeviceFilter. BlueLib does not wrap this: the request objects are
about your UI and your device filters, and wrapping them would hide the per-device-type
configuration that matters. Associate first (which also gives you unbond above and the ability to
start observing presence), then use BlueLib for the Bluetooth work itself.