We recently rewrote a significant part of our app, but are now running in to some behaviour around device pairing bonding in particular that we would like to improve. We connect to a number of different devices, some that require a bond and others that do not. In all cases, we now have them associate via the Companion Device Manager first, and then bond the device second.
While our app targets Android 12, our minimum supported android version is 10. We're seeing some very different behaviour between the version. Our frontend is written using Jetpack Compose
- On Android 12, when we request a bond, unless there's a need for a passkey, pin or other user interaction, the bond is accomplished silently.
- On Android 11-, for every device that we request a bond the user is required to consent to the bonding regardless of if any additional input is necessary.
In addition, There now seems to be a two-step process when the user has to approve a bond: A system notification is received first, and the user must respond to the system notification first before the consent/input dialog is seen. previously when we created the bond without first associating the device, the consent dialog just appeared directly.
So, here's the question(s)
- Why is the behaviour different between Android 12 and earlier versions? What has changed about how bonding is accomplished that we no longer need express consent every time?
- Why is there now a two step process? Is this because the request to bond is somehow tied to the companion device manager, or is something else going on?
- Can I shortcut/remove the system notification step from the process? Not only does it add additional steps to the overall flow, it also is making it complicated when an EMM/MDM is applied to the phones (a significant use case for us is within a kiosk-mode implementation, where the only visible app is our application and system notifications are suppressed)
Here's our code for associating the device:
fun CompanionDeviceManager.associateSingleDevice(
associationRequest:AssociationRequest,
activityResultLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
) {
this.associate(
associationRequest,
object : CompanionDeviceManager.Callback() {
@Deprecated("Required to implement for API versions 32 and below")
override fun onDeviceFound(intentSender: IntentSender) {
handleAssociationResponse(intentSender, activityResultLauncher)
}
override fun onAssociationPending(intentSender: IntentSender) {
handleAssociationResponse(intentSender, activityResultLauncher)
}
override fun onFailure(error: CharSequence?) {
//TODO: handle association failure
}
},
null
)
}
private fun handleAssociationResponse(
intentSender: IntentSender,
activityResultLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
) {
val senderRequest = IntentSenderRequest.Builder(intentSender).build()
activityResultLauncher.launch(senderRequest)
}
The association dialog is shown, here's the relevant activityResultLauncher used when the device requires a bond be established. There is a callback provided that allows the UI to be updated on the pairing state.
@SuppressLint("MissingPermission")
private fun bleRequiresBondActivityResultCallback(activityResult: ActivityResult) =
when (activityResult.resultCode) {
Activity.RESULT_OK -> activityResult.data
?.getParcelableExtra<ScanResult>(CompanionDeviceManager.EXTRA_DEVICE)
?.device!!.run {
callback.updatePairingState(PairingState.BONDING)
if(this.bondState!= BluetoothDevice.BOND_BONDED) {
val createBondResult = createBond()
logger.debug("Device bonding initiated: createBond=$createBondResult")
if(!createBondResult){
callback.updatePairingState(PairingState.PAIRING_FAILED)
}
} else {
logger.debug("Device already bonded, no need to create bond. Move straight to disconnecting")
callback.updatePairingState(PairingState.PAIRING_SUCCEEDED)
}
}
else -> callback.updatePairingState(PairingState.PAIRING_FAILED)
}
In Jetpack compose, we compose a component that provides some UI/UX, registers the launcher and then starts the pairing process (i.e.calls the companion device manager as above) from within a disposableEffect
val associationLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartIntentSenderForResult(),
onResult = pairingManager.getActivityResultHandler() //returns the handler above
)
DisposableEffect("") {
pairingManager.initializePairing() //Does some prework
pairingManager.startPairing(associationLauncher) //launches the association
onDispose {
Log.d("PairingOngoingContent", "PairingOngoingContent: dispose was called")
pairingManager.finalizePairing() //closes out the operations
}
}