@capacitor-community/bluetooth-le 7.1.1 → 7.3.0

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(
@@ -70,6 +72,7 @@ class Device(
70
72
  private var callbackMap = HashMap<String, ((CallbackResponse) -> Unit)>()
71
73
  private val timeoutQueue = ConcurrentLinkedQueue<TimeoutHandler>()
72
74
  private var bondStateReceiver: BroadcastReceiver? = null
75
+ private val pendingBondKeys = Collections.newSetFromMap(ConcurrentHashMap<String, Boolean>())
73
76
  private var currentMtu = -1
74
77
 
75
78
  private lateinit var callbacksHandlerThread: HandlerThread
@@ -91,6 +94,22 @@ class Device(
91
94
  }
92
95
  }
93
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
+
94
113
  private val gattCallback: BluetoothGattCallback = object : BluetoothGattCallback() {
95
114
  override fun onConnectionStateChange(
96
115
  gatt: BluetoothGatt, status: Int, newState: Int
@@ -109,7 +128,7 @@ class Device(
109
128
  bluetoothGatt?.close()
110
129
  bluetoothGatt = null
111
130
  Logger.debug(TAG, "Disconnected from GATT server.")
112
- cleanupCallbacksHandlerThread()
131
+ cleanup()
113
132
  resolve("disconnect", "Disconnected.")
114
133
  }
115
134
  }
@@ -358,9 +377,16 @@ class Device(
358
377
  fun createBond(timeout: Long, callback: (CallbackResponse) -> Unit) {
359
378
  val key = "createBond"
360
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
+
361
387
  try {
362
- createBondStateReceiver()
363
- } catch (e: Error) {
388
+ ensureBondStateReceiverRegistered()
389
+ } catch (e: Exception) {
364
390
  Logger.error(TAG, "Error while registering bondStateReceiver: ${e.localizedMessage}", e)
365
391
  reject(key, "Creating bond failed.")
366
392
  return
@@ -370,22 +396,17 @@ class Device(
370
396
  reject(key, "Creating bond failed.")
371
397
  return
372
398
  }
373
- // if already bonded, resolve immediately
374
- if (isBonded()) {
375
- resolve(key, "Creating bond succeeded.")
376
- return
377
- }
378
- // otherwise, wait for bond state change
399
+ // Wait for bond state change
379
400
  setTimeout(key, "Bonding timeout.", timeout)
380
401
  }
381
402
 
382
- private fun createBondStateReceiver() {
383
- if (bondStateReceiver == null) {
403
+ private fun ensureBondStateReceiverRegistered() {
404
+ synchronized(this) {
405
+ if (bondStateReceiver != null) return
406
+
384
407
  bondStateReceiver = object : BroadcastReceiver() {
385
- override fun onReceive(context: Context, intent: Intent) {
386
- val action = intent.action
387
- if (action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) {
388
- val key = "createBond"
408
+ override fun onReceive(ctx: Context, intent: Intent) {
409
+ if (intent.action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) {
389
410
  val updatedDevice =
390
411
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
391
412
  intent.getParcelableExtra(
@@ -395,27 +416,44 @@ class Device(
395
416
  } else {
396
417
  intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
397
418
  }
419
+
398
420
  // BroadcastReceiver receives bond state updates from all devices, need to filter by device
399
421
  if (device.address == updatedDevice?.address) {
400
- val bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1)
401
- val previousBondState =
402
- intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1)
403
- Logger.debug(
404
- TAG, "Bond state transition $previousBondState -> $bondState"
405
- )
406
- if (bondState == BluetoothDevice.BOND_BONDED) {
407
- resolve(key, "Creating bond succeeded.")
408
- } else if (previousBondState == BluetoothDevice.BOND_BONDING && bondState == BluetoothDevice.BOND_NONE) {
409
- reject(key, "Creating bond failed.")
410
- } else if (bondState == -1) {
411
- 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
+ }
412
444
  }
413
445
  }
414
446
  }
415
447
  }
416
448
  }
417
- val intentFilter = IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
418
- 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
+ }
419
457
  }
420
458
  }
421
459
 
@@ -546,6 +584,7 @@ class Device(
546
584
  characteristicUUID: UUID,
547
585
  enable: Boolean,
548
586
  notifyCallback: ((CallbackResponse) -> Unit)?,
587
+ timeout: Long,
549
588
  callback: (CallbackResponse) -> Unit,
550
589
  ) {
551
590
  val key = "writeDescriptor|$serviceUUID|$characteristicUUID|$CLIENT_CHARACTERISTIC_CONFIG"
@@ -585,6 +624,19 @@ class Device(
585
624
  BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
586
625
  }
587
626
 
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}")
637
+ }
638
+ }
639
+
588
640
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
589
641
  val statusCode = bluetoothGatt?.writeDescriptor(descriptor, value)
590
642
  if (statusCode != BluetoothStatusCodes.SUCCESS) {
@@ -600,6 +652,7 @@ class Device(
600
652
  }
601
653
 
602
654
  }
655
+ setTimeout(key, "Setting notification timeout.", timeout)
603
656
  // wait for onDescriptorWrite
604
657
  }
605
658
 
@@ -672,21 +725,19 @@ class Device(
672
725
  }
673
726
 
674
727
  private fun resolve(key: String, value: String) {
675
- if (callbackMap.containsKey(key)) {
728
+ pendingBondKeys.remove(key)
729
+ callbackMap.remove(key)?.let { callback ->
676
730
  Logger.debug(TAG, "resolve: $key $value")
677
731
  timeoutQueue.popFirstMatch { it.key == key }?.handler?.removeCallbacksAndMessages(null)
678
- val callback = callbackMap[key]
679
- callbackMap.remove(key)
680
732
  callback?.invoke(CallbackResponse(true, value))
681
733
  }
682
734
  }
683
735
 
684
736
  private fun reject(key: String, value: String) {
685
- if (callbackMap.containsKey(key)) {
737
+ pendingBondKeys.remove(key)
738
+ callbackMap.remove(key)?.let { callback ->
686
739
  Logger.debug(TAG, "reject: $key $value")
687
740
  timeoutQueue.popFirstMatch { it.key == key }?.handler?.removeCallbacksAndMessages(null)
688
- val callback = callbackMap[key]
689
- callbackMap.remove(key)
690
741
  callback?.invoke(CallbackResponse(false, value))
691
742
  }
692
743
  }
@@ -713,7 +764,7 @@ class Device(
713
764
  connectionState = STATE_DISCONNECTED
714
765
  gatt?.disconnect()
715
766
  gatt?.close()
716
- cleanupCallbacksHandlerThread()
767
+ cleanup()
717
768
  reject(key, message)
718
769
  }, timeout)
719
770
  }