@capacitor-community/bluetooth-le 7.2.0 → 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.
- package/README.md +68 -161
- package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/BluetoothLe.kt +43 -19
- package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt +82 -78
- package/dist/docs.json +906 -849
- package/dist/esm/bleClient.js +15 -1
- package/dist/esm/bleClient.js.map +1 -1
- package/dist/esm/conversion.d.ts +12 -0
- package/dist/esm/conversion.js +33 -0
- package/dist/esm/conversion.js.map +1 -1
- package/dist/esm/definitions.d.ts +41 -2
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +1 -0
- package/dist/esm/web.js +66 -3
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +115 -3
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +115 -3
- package/dist/plugin.js.map +1 -1
- package/ios/Plugin/DeviceListView.swift +121 -0
- package/ios/Plugin/DeviceManager.swift +114 -13
- package/ios/Plugin/Plugin.swift +61 -6
- package/package.json +13 -3
|
@@ -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
|
-
|
|
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
|
-
|
|
367
|
-
} catch (e:
|
|
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
|
-
//
|
|
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
|
|
387
|
-
|
|
403
|
+
private fun ensureBondStateReceiverRegistered() {
|
|
404
|
+
synchronized(this) {
|
|
405
|
+
if (bondStateReceiver != null) return
|
|
406
|
+
|
|
388
407
|
bondStateReceiver = object : BroadcastReceiver() {
|
|
389
|
-
override fun onReceive(
|
|
390
|
-
|
|
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
|
|
405
|
-
val
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
422
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
767
|
+
cleanup()
|
|
764
768
|
reject(key, message)
|
|
765
769
|
}, timeout)
|
|
766
770
|
}
|