@capacitor-community/bluetooth-le 7.2.0 → 7.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,7 +21,9 @@ import android.os.HandlerThread
21
21
  import android.os.Looper
22
22
  import androidx.annotation.RequiresApi
23
23
  import com.getcapacitor.Logger
24
+ import java.util.Collections
24
25
  import java.util.UUID
26
+ import java.util.concurrent.ConcurrentHashMap
25
27
  import java.util.concurrent.ConcurrentLinkedQueue
26
28
 
27
29
  class CallbackResponse(
@@ -68,9 +70,9 @@ class Device(
68
70
  private var device: BluetoothDevice = bluetoothAdapter.getRemoteDevice(address)
69
71
  private var bluetoothGatt: BluetoothGatt? = null
70
72
  private var callbackMap = HashMap<String, ((CallbackResponse) -> Unit)>()
71
- private val bondReceiverMap = HashMap<String, BroadcastReceiver>()
72
73
  private val timeoutQueue = ConcurrentLinkedQueue<TimeoutHandler>()
73
74
  private var bondStateReceiver: BroadcastReceiver? = null
75
+ private val pendingBondKeys = Collections.newSetFromMap(ConcurrentHashMap<String, Boolean>())
74
76
  private var currentMtu = -1
75
77
 
76
78
  private lateinit var callbacksHandlerThread: HandlerThread
@@ -92,6 +94,22 @@ class Device(
92
94
  }
93
95
  }
94
96
 
97
+ fun cleanup() {
98
+ synchronized(this) {
99
+ bondStateReceiver?.let { receiver ->
100
+ try {
101
+ context.unregisterReceiver(receiver)
102
+ } catch (e: IllegalArgumentException) {
103
+ Logger.debug(TAG, "Bond state receiver already unregistered")
104
+ }
105
+ bondStateReceiver = null
106
+ }
107
+
108
+ pendingBondKeys.clear()
109
+ cleanupCallbacksHandlerThread()
110
+ }
111
+ }
112
+
95
113
  private val gattCallback: BluetoothGattCallback = object : BluetoothGattCallback() {
96
114
  override fun onConnectionStateChange(
97
115
  gatt: BluetoothGatt, status: Int, newState: Int
@@ -110,7 +128,7 @@ class Device(
110
128
  bluetoothGatt?.close()
111
129
  bluetoothGatt = null
112
130
  Logger.debug(TAG, "Disconnected from GATT server.")
113
- cleanupCallbacksHandlerThread()
131
+ cleanup()
114
132
  resolve("disconnect", "Disconnected.")
115
133
  }
116
134
  }
@@ -281,9 +299,6 @@ class Device(
281
299
  super.onDescriptorWrite(gatt, descriptor, status)
282
300
  val key =
283
301
  "writeDescriptor|${descriptor.characteristic.service.uuid}|${descriptor.characteristic.uuid}|${descriptor.uuid}"
284
- bondReceiverMap.remove(key)?.let {
285
- context.unregisterReceiver(it)
286
- }
287
302
  if (status == BluetoothGatt.GATT_SUCCESS) {
288
303
  resolve(key, "Descriptor successfully written.")
289
304
  } else {
@@ -362,9 +377,16 @@ class Device(
362
377
  fun createBond(timeout: Long, callback: (CallbackResponse) -> Unit) {
363
378
  val key = "createBond"
364
379
  callbackMap[key] = callback
380
+
381
+ // Check if already bonded first to avoid race condition
382
+ if (isBonded()) {
383
+ resolve(key, "Creating bond succeeded.")
384
+ return
385
+ }
386
+
365
387
  try {
366
- createBondStateReceiver()
367
- } catch (e: Error) {
388
+ ensureBondStateReceiverRegistered()
389
+ } catch (e: Exception) {
368
390
  Logger.error(TAG, "Error while registering bondStateReceiver: ${e.localizedMessage}", e)
369
391
  reject(key, "Creating bond failed.")
370
392
  return
@@ -374,22 +396,17 @@ class Device(
374
396
  reject(key, "Creating bond failed.")
375
397
  return
376
398
  }
377
- // if already bonded, resolve immediately
378
- if (isBonded()) {
379
- resolve(key, "Creating bond succeeded.")
380
- return
381
- }
382
- // otherwise, wait for bond state change
399
+ // Wait for bond state change
383
400
  setTimeout(key, "Bonding timeout.", timeout)
384
401
  }
385
402
 
386
- private fun createBondStateReceiver() {
387
- if (bondStateReceiver == null) {
403
+ private fun ensureBondStateReceiverRegistered() {
404
+ synchronized(this) {
405
+ if (bondStateReceiver != null) return
406
+
388
407
  bondStateReceiver = object : BroadcastReceiver() {
389
- override fun onReceive(context: Context, intent: Intent) {
390
- val action = intent.action
391
- if (action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) {
392
- val key = "createBond"
408
+ override fun onReceive(ctx: Context, intent: Intent) {
409
+ if (intent.action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) {
393
410
  val updatedDevice =
394
411
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
395
412
  intent.getParcelableExtra(
@@ -399,27 +416,44 @@ class Device(
399
416
  } else {
400
417
  intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
401
418
  }
419
+
402
420
  // BroadcastReceiver receives bond state updates from all devices, need to filter by device
403
421
  if (device.address == updatedDevice?.address) {
404
- val bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1)
405
- val previousBondState =
406
- intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1)
407
- Logger.debug(
408
- TAG, "Bond state transition $previousBondState -> $bondState"
409
- )
410
- if (bondState == BluetoothDevice.BOND_BONDED) {
411
- resolve(key, "Creating bond succeeded.")
412
- } else if (previousBondState == BluetoothDevice.BOND_BONDING && bondState == BluetoothDevice.BOND_NONE) {
413
- reject(key, "Creating bond failed.")
414
- } else if (bondState == -1) {
415
- reject(key, "Creating bond failed.")
422
+ val prev = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1)
423
+ val state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1)
424
+ Logger.debug(TAG, "Bond state transition $prev -> $state")
425
+
426
+ // Handle createBond callback
427
+ if (callbackMap.containsKey("createBond")) {
428
+ if (state == BluetoothDevice.BOND_BONDED) {
429
+ resolve("createBond", "Creating bond succeeded.")
430
+ } else if (prev == BluetoothDevice.BOND_BONDING && state == BluetoothDevice.BOND_NONE) {
431
+ reject("createBond", "Creating bond failed.")
432
+ } else if (state == -1) {
433
+ reject("createBond", "Creating bond failed.")
434
+ }
435
+ }
436
+
437
+ // Handle setNotifications callbacks (only for operations waiting on bonding)
438
+ if (prev == BluetoothDevice.BOND_BONDING && state == BluetoothDevice.BOND_NONE) {
439
+ val keysToReject = pendingBondKeys.toList()
440
+ pendingBondKeys.clear()
441
+ keysToReject.forEach { key ->
442
+ reject(key, "Pairing request was cancelled by the user.")
443
+ }
416
444
  }
417
445
  }
418
446
  }
419
447
  }
420
448
  }
421
- val intentFilter = IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
422
- context.registerReceiver(bondStateReceiver, intentFilter)
449
+ try {
450
+ val intentFilter = IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
451
+ context.registerReceiver(bondStateReceiver, intentFilter)
452
+ } catch (e: Exception) {
453
+ Logger.error(TAG, "Error registering bond state receiver: ${e.localizedMessage}", e)
454
+ bondStateReceiver = null
455
+ throw e
456
+ }
423
457
  }
424
458
  }
425
459
 
@@ -590,47 +624,22 @@ class Device(
590
624
  BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
591
625
  }
592
626
 
593
- val bondReceiver = object : BroadcastReceiver() {
594
- override fun onReceive(ctx: Context, intent: Intent) {
595
- if (intent.action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) {
596
- val updatedDevice =
597
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
598
- intent.getParcelableExtra(
599
- BluetoothDevice.EXTRA_DEVICE,
600
- BluetoothDevice::class.java
601
- )
602
- } else {
603
- intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
604
- }
605
-
606
- // BroadcastReceiver receives bond state updates from all devices, need to filter by device
607
- if (device.address == updatedDevice?.address) {
608
- val prev = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1)
609
- val state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1)
610
- if (state == BluetoothDevice.BOND_BONDED) {
611
- ctx.unregisterReceiver(this)
612
- } else if (prev == BluetoothDevice.BOND_BONDING && state == BluetoothDevice.BOND_NONE) {
613
- ctx.unregisterReceiver(this)
614
- reject(key, "Pairing request was cancelled by the user.")
615
- } else if (state == -1) {
616
- ctx.unregisterReceiver(this)
617
- }
618
- }
619
- }
627
+ // Track this operation as potentially needing bonding
628
+ if (!isBonded()) {
629
+ try {
630
+ ensureBondStateReceiverRegistered()
631
+ pendingBondKeys.add(key)
632
+ } catch (e: Exception) {
633
+ // Don't fail the notification attempt just because bonding
634
+ // can't be tracked. The call will still timeout if bonding is
635
+ // required for some reason
636
+ Logger.warn(TAG, "Error while registering bondStateReceiver: ${e.localizedMessage}")
620
637
  }
621
638
  }
622
- bondReceiverMap[key] = bondReceiver
623
- context.registerReceiver(
624
- bondReceiver,
625
- IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
626
- )
627
639
 
628
640
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
629
641
  val statusCode = bluetoothGatt?.writeDescriptor(descriptor, value)
630
642
  if (statusCode != BluetoothStatusCodes.SUCCESS) {
631
- bondReceiverMap.remove(key)?.let {
632
- context.unregisterReceiver(it)
633
- }
634
643
  reject(key, "Setting notification failed with status code $statusCode.")
635
644
  return
636
645
  }
@@ -638,9 +647,6 @@ class Device(
638
647
  descriptor.value = value
639
648
  val resultDesc = bluetoothGatt?.writeDescriptor(descriptor)
640
649
  if (resultDesc != true) {
641
- bondReceiverMap.remove(key)?.let {
642
- context.unregisterReceiver(it)
643
- }
644
650
  reject(key, "Setting notification failed.")
645
651
  return
646
652
  }
@@ -719,21 +725,19 @@ class Device(
719
725
  }
720
726
 
721
727
  private fun resolve(key: String, value: String) {
722
- if (callbackMap.containsKey(key)) {
728
+ pendingBondKeys.remove(key)
729
+ callbackMap.remove(key)?.let { callback ->
723
730
  Logger.debug(TAG, "resolve: $key $value")
724
731
  timeoutQueue.popFirstMatch { it.key == key }?.handler?.removeCallbacksAndMessages(null)
725
- val callback = callbackMap[key]
726
- callbackMap.remove(key)
727
732
  callback?.invoke(CallbackResponse(true, value))
728
733
  }
729
734
  }
730
735
 
731
736
  private fun reject(key: String, value: String) {
732
- if (callbackMap.containsKey(key)) {
737
+ pendingBondKeys.remove(key)
738
+ callbackMap.remove(key)?.let { callback ->
733
739
  Logger.debug(TAG, "reject: $key $value")
734
740
  timeoutQueue.popFirstMatch { it.key == key }?.handler?.removeCallbacksAndMessages(null)
735
- val callback = callbackMap[key]
736
- callbackMap.remove(key)
737
741
  callback?.invoke(CallbackResponse(false, value))
738
742
  }
739
743
  }
@@ -760,7 +764,7 @@ class Device(
760
764
  connectionState = STATE_DISCONNECTED
761
765
  gatt?.disconnect()
762
766
  gatt?.close()
763
- cleanupCallbacksHandlerThread()
767
+ cleanup()
764
768
  reject(key, message)
765
769
  }, timeout)
766
770
  }