@@ -6,6 +6,13 @@ import android.bluetooth.BluetoothAdapter.STATE_ON
66import android.bluetooth.BluetoothAdapter.STATE_TURNING_OFF
77import android.bluetooth.BluetoothAdapter.STATE_TURNING_ON
88import android.bluetooth.BluetoothDevice
9+ import android.bluetooth.BluetoothDevice.ACTION_BOND_STATE_CHANGED
10+ import android.bluetooth.BluetoothDevice.BOND_BONDED
11+ import android.bluetooth.BluetoothDevice.BOND_BONDING
12+ import android.bluetooth.BluetoothDevice.BOND_NONE
13+ import android.bluetooth.BluetoothDevice.ERROR
14+ import android.bluetooth.BluetoothDevice.EXTRA_BOND_STATE
15+ import android.bluetooth.BluetoothDevice.EXTRA_DEVICE
916import android.bluetooth.BluetoothGatt
1017import android.bluetooth.BluetoothGattCharacteristic.PROPERTY_INDICATE
1118import android.bluetooth.BluetoothGattCharacteristic.PROPERTY_NOTIFY
@@ -14,7 +21,13 @@ import android.bluetooth.BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
1421import android.bluetooth.BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
1522import android.bluetooth.BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
1623import android.bluetooth.BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
24+ import android.content.IntentFilter
25+ import androidx.core.content.IntentCompat
1726import com.benasher44.uuid.uuidFrom
27+ import com.juul.kable.AndroidPeripheral.Bond
28+ import com.juul.kable.AndroidPeripheral.Bond.Bonded
29+ import com.juul.kable.AndroidPeripheral.Bond.Bonding
30+ import com.juul.kable.AndroidPeripheral.Bond.None
1831import com.juul.kable.WriteType.WithResponse
1932import com.juul.kable.WriteType.WithoutResponse
2033import com.juul.kable.external.CLIENT_CHARACTERISTIC_CONFIG_UUID
@@ -40,10 +53,11 @@ import kotlinx.coroutines.flow.MutableStateFlow
4053import kotlinx.coroutines.flow.StateFlow
4154import kotlinx.coroutines.flow.asStateFlow
4255import kotlinx.coroutines.flow.filter
56+ import kotlinx.coroutines.flow.first
4357import kotlinx.coroutines.flow.launchIn
58+ import kotlinx.coroutines.flow.map
4459import kotlinx.coroutines.flow.onEach
4560import kotlinx.coroutines.flow.update
46- import kotlinx.coroutines.job
4761import kotlinx.coroutines.withContext
4862import kotlin.coroutines.CoroutineContext
4963
@@ -143,6 +157,20 @@ internal class BluetoothDeviceAndroidPeripheral(
143157
144158 override val name: String? get() = bluetoothDevice.name
145159
160+ override val bondState: Flow <Bond > =
161+ broadcastReceiverFlow(IntentFilter (ACTION_BOND_STATE_CHANGED ))
162+ .filter { intent ->
163+ bluetoothDevice == IntentCompat .getParcelableExtra(intent, EXTRA_DEVICE , BluetoothDevice ::class .java)
164+ }
165+ .map { intent ->
166+ when (val state = intent.getIntExtra(EXTRA_BOND_STATE , ERROR )) {
167+ BOND_NONE -> None
168+ BOND_BONDING -> Bonding
169+ BOND_BONDED -> Bonded
170+ else -> error(" Unsupported bond state: $state " )
171+ }
172+ }
173+
146174 private fun establishConnection (): Connection {
147175 logger.info { message = " Connecting" }
148176 return bluetoothDevice.connect(
@@ -162,7 +190,11 @@ internal class BluetoothDeviceAndroidPeripheral(
162190 /* * Creates a connect [Job] that completes when connection is established, or failure occurs. */
163191 private fun connectAsync () = scope.async(start = LAZY ) {
164192 try {
165- _connection = establishConnection()
193+ _connection = establishConnection().also {
194+ logger.debug { message = " Awaiting bond state" }
195+ val bond = bondState.first { it != Bonding }
196+ logger.debug { message = " Bond state: $bond " }
197+ }
166198 suspendUntilOrThrow<State .Connecting .Services >()
167199 discoverServices()
168200 onServicesDiscovered(ServicesDiscoveredPeripheral (this @BluetoothDeviceAndroidPeripheral))
@@ -261,10 +293,25 @@ internal class BluetoothDeviceAndroidPeripheral(
261293 }
262294
263295 val platformCharacteristic = discoveredServices.obtain(characteristic, writeType.properties)
264- connection.execute<OnCharacteristicWrite > {
265- platformCharacteristic.value = data
266- platformCharacteristic.writeType = writeType.intValue
267- writeCharacteristic(platformCharacteristic)
296+ try {
297+ connection.execute<OnCharacteristicWrite > {
298+ platformCharacteristic.value = data
299+ platformCharacteristic.writeType = writeType.intValue
300+ writeCharacteristic(platformCharacteristic)
301+ }
302+ } catch (_: BondRequiredException ) {
303+ awaitBond()
304+ logger.debug {
305+ message = " Retrying write"
306+ detail(characteristic)
307+ detail(writeType)
308+ detail(data)
309+ }
310+ connection.execute<OnCharacteristicWrite > {
311+ platformCharacteristic.value = data
312+ platformCharacteristic.writeType = writeType.intValue
313+ writeCharacteristic(platformCharacteristic)
314+ }
268315 }
269316 }
270317
@@ -277,8 +324,19 @@ internal class BluetoothDeviceAndroidPeripheral(
277324 }
278325
279326 val platformCharacteristic = discoveredServices.obtain(characteristic, Read )
280- return connection.execute<OnCharacteristicRead > {
281- readCharacteristic(platformCharacteristic)
327+ return try {
328+ connection.execute<OnCharacteristicRead > {
329+ readCharacteristic(platformCharacteristic)
330+ }
331+ } catch (_: BondRequiredException ) {
332+ awaitBond()
333+ logger.debug {
334+ message = " Retrying read"
335+ detail(characteristic)
336+ }
337+ connection.execute {
338+ readCharacteristic(platformCharacteristic)
339+ }
282340 }.value!!
283341 }
284342
@@ -299,9 +357,22 @@ internal class BluetoothDeviceAndroidPeripheral(
299357 detail(data)
300358 }
301359
302- connection.execute<OnDescriptorWrite > {
303- platformDescriptor.value = data
304- writeDescriptor(platformDescriptor)
360+ try {
361+ connection.execute<OnDescriptorWrite > {
362+ platformDescriptor.value = data
363+ writeDescriptor(platformDescriptor)
364+ }
365+ } catch (_: BondRequiredException ) {
366+ awaitBond()
367+ logger.debug {
368+ message = " Retrying write"
369+ detail(platformDescriptor)
370+ detail(data)
371+ }
372+ connection.execute<OnDescriptorWrite > {
373+ platformDescriptor.value = data
374+ writeDescriptor(platformDescriptor)
375+ }
305376 }
306377 }
307378
@@ -314,8 +385,20 @@ internal class BluetoothDeviceAndroidPeripheral(
314385 }
315386
316387 val platformDescriptor = discoveredServices.obtain(descriptor)
317- return connection.execute<OnDescriptorRead > {
318- readDescriptor(platformDescriptor)
388+
389+ return try {
390+ connection.execute<OnDescriptorRead > {
391+ readDescriptor(platformDescriptor)
392+ }
393+ } catch (_: BondRequiredException ) {
394+ awaitBond()
395+ logger.debug {
396+ message = " Retrying read"
397+ detail(descriptor)
398+ }
399+ connection.execute {
400+ readDescriptor(platformDescriptor)
401+ }
319402 }.value!!
320403 }
321404
@@ -396,6 +479,11 @@ internal class BluetoothDeviceAndroidPeripheral(
396479 }
397480 }
398481
482+ private suspend fun awaitBond () {
483+ logger.warn { message = " Insufficient authentication, awaiting bond" }
484+ bondState.first { it == Bonded }
485+ }
486+
399487 override fun toString (): String = " Peripheral(bluetoothDevice=$bluetoothDevice )"
400488}
401489
0 commit comments