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
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)
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
}
}
Further investigation showed that my mistake was not doing the bonding in the context of discovery. If the createBond
process takes place in the context of a discovery session, then the intermediary system notification is not issued and you just get the dialog directly. This is true in android 10-13 (haven't tested beyond).
For Android 11 and below, the experience is Link(associate) dialog -> Pair Dialog -> completed. This happens regardless of whether the bond requires a passkey, pin, or otherwise
For Android 12 up, if no passkey or other use input is necessary, the flow is Link(Associate) dialog -> completed.