@capacitor-community/bluetooth-le 7.3.0 → 8.0.0-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.
Files changed (52) hide show
  1. package/CapacitorCommunityBluetoothLe.podspec +17 -17
  2. package/LICENSE +21 -21
  3. package/Package.swift +28 -0
  4. package/android/build.gradle +71 -68
  5. package/android/src/main/AndroidManifest.xml +22 -22
  6. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/BluetoothLe.kt +1094 -1094
  7. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Conversion.kt +51 -51
  8. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt +771 -771
  9. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/DeviceList.kt +28 -28
  10. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/DeviceScanner.kt +189 -189
  11. package/dist/esm/bleClient.d.ts +278 -278
  12. package/dist/esm/bleClient.js +361 -361
  13. package/dist/esm/bleClient.js.map +1 -1
  14. package/dist/esm/config.d.ts +53 -53
  15. package/dist/esm/config.js +2 -2
  16. package/dist/esm/conversion.d.ts +56 -46
  17. package/dist/esm/conversion.js +134 -117
  18. package/dist/esm/conversion.js.map +1 -1
  19. package/dist/esm/definitions.d.ts +352 -352
  20. package/dist/esm/definitions.js +42 -42
  21. package/dist/esm/index.d.ts +5 -5
  22. package/dist/esm/index.js +5 -5
  23. package/dist/esm/plugin.d.ts +2 -2
  24. package/dist/esm/plugin.js +4 -4
  25. package/dist/esm/queue.d.ts +3 -3
  26. package/dist/esm/queue.js +17 -17
  27. package/dist/esm/queue.js.map +1 -1
  28. package/dist/esm/timeout.d.ts +1 -1
  29. package/dist/esm/timeout.js +9 -9
  30. package/dist/esm/validators.d.ts +1 -1
  31. package/dist/esm/validators.js +11 -11
  32. package/dist/esm/validators.js.map +1 -1
  33. package/dist/esm/web.d.ts +57 -57
  34. package/dist/esm/web.js +403 -403
  35. package/dist/esm/web.js.map +1 -1
  36. package/dist/plugin.cjs.js +965 -947
  37. package/dist/plugin.cjs.js.map +1 -1
  38. package/dist/plugin.js +965 -947
  39. package/dist/plugin.js.map +1 -1
  40. package/ios/{Plugin → Sources/BluetoothLe}/Conversion.swift +83 -83
  41. package/ios/{Plugin → Sources/BluetoothLe}/Device.swift +423 -423
  42. package/ios/{Plugin → Sources/BluetoothLe}/DeviceListView.swift +121 -121
  43. package/ios/{Plugin → Sources/BluetoothLe}/DeviceManager.swift +503 -502
  44. package/ios/{Plugin → Sources/BluetoothLe}/Logging.swift +8 -8
  45. package/ios/{Plugin → Sources/BluetoothLe}/Plugin.swift +775 -737
  46. package/ios/{Plugin → Sources/BluetoothLe}/ThreadSafeDictionary.swift +15 -13
  47. package/ios/Tests/BluetoothLeTests/ConversionTests.swift +55 -0
  48. package/ios/Tests/BluetoothLeTests/PluginTests.swift +27 -0
  49. package/package.json +115 -111
  50. package/ios/Plugin/Info.plist +0 -24
  51. package/ios/Plugin/Plugin.h +0 -10
  52. package/ios/Plugin/Plugin.m +0 -41
@@ -1,771 +1,771 @@
1
- package com.capacitorjs.community.plugins.bluetoothle
2
-
3
- import android.annotation.SuppressLint
4
- import android.annotation.TargetApi
5
- import android.bluetooth.BluetoothAdapter
6
- import android.bluetooth.BluetoothDevice
7
- import android.bluetooth.BluetoothGatt
8
- import android.bluetooth.BluetoothGattCallback
9
- import android.bluetooth.BluetoothGattCharacteristic
10
- import android.bluetooth.BluetoothGattDescriptor
11
- import android.bluetooth.BluetoothGattService
12
- import android.bluetooth.BluetoothProfile
13
- import android.bluetooth.BluetoothStatusCodes
14
- import android.content.BroadcastReceiver
15
- import android.content.Context
16
- import android.content.Intent
17
- import android.content.IntentFilter
18
- import android.os.Build
19
- import android.os.Handler
20
- import android.os.HandlerThread
21
- import android.os.Looper
22
- import androidx.annotation.RequiresApi
23
- import com.getcapacitor.Logger
24
- import java.util.Collections
25
- import java.util.UUID
26
- import java.util.concurrent.ConcurrentHashMap
27
- import java.util.concurrent.ConcurrentLinkedQueue
28
-
29
- class CallbackResponse(
30
- val success: Boolean,
31
- val value: String,
32
- )
33
-
34
- class TimeoutHandler(
35
- val key: String,
36
- val handler: Handler
37
- )
38
-
39
- fun <T> ConcurrentLinkedQueue<T>.popFirstMatch(predicate: (T) -> Boolean): T? {
40
- synchronized(this) {
41
- val iterator = this.iterator()
42
- while (iterator.hasNext()) {
43
- val nextItem = iterator.next()
44
- if (predicate(nextItem)) {
45
- iterator.remove()
46
- return nextItem
47
- }
48
- }
49
- return null
50
- }
51
- }
52
-
53
- @SuppressLint("MissingPermission")
54
- class Device(
55
- private val context: Context,
56
- bluetoothAdapter: BluetoothAdapter,
57
- private val address: String,
58
- private val onDisconnect: () -> Unit
59
- ) {
60
- companion object {
61
- private val TAG = Device::class.java.simpleName
62
- private const val STATE_DISCONNECTED = 0
63
- private const val STATE_CONNECTING = 1
64
- private const val STATE_CONNECTED = 2
65
- private const val CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb"
66
- private const val REQUEST_MTU = 512
67
- }
68
-
69
- private var connectionState = STATE_DISCONNECTED
70
- private var device: BluetoothDevice = bluetoothAdapter.getRemoteDevice(address)
71
- private var bluetoothGatt: BluetoothGatt? = null
72
- private var callbackMap = HashMap<String, ((CallbackResponse) -> Unit)>()
73
- private val timeoutQueue = ConcurrentLinkedQueue<TimeoutHandler>()
74
- private var bondStateReceiver: BroadcastReceiver? = null
75
- private val pendingBondKeys = Collections.newSetFromMap(ConcurrentHashMap<String, Boolean>())
76
- private var currentMtu = -1
77
-
78
- private lateinit var callbacksHandlerThread: HandlerThread
79
- private lateinit var callbacksHandler: Handler
80
-
81
- private fun initializeCallbacksHandlerThread() {
82
- synchronized(this) {
83
- callbacksHandlerThread = HandlerThread("Callbacks thread")
84
- callbacksHandlerThread.start()
85
- callbacksHandler = Handler(callbacksHandlerThread.looper)
86
- }
87
- }
88
-
89
- private fun cleanupCallbacksHandlerThread() {
90
- synchronized(this) {
91
- if (::callbacksHandlerThread.isInitialized) {
92
- callbacksHandlerThread.quitSafely()
93
- }
94
- }
95
- }
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
-
113
- private val gattCallback: BluetoothGattCallback = object : BluetoothGattCallback() {
114
- override fun onConnectionStateChange(
115
- gatt: BluetoothGatt, status: Int, newState: Int
116
- ) {
117
- if (newState == BluetoothProfile.STATE_CONNECTED) {
118
- connectionState = STATE_CONNECTED
119
- // service discovery is required to use services
120
- Logger.debug(TAG, "Connected to GATT server. Starting service discovery.")
121
- val result = bluetoothGatt?.discoverServices()
122
- if (result != true) {
123
- reject("connect", "Starting service discovery failed.")
124
- }
125
- } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
126
- connectionState = STATE_DISCONNECTED
127
- onDisconnect()
128
- bluetoothGatt?.close()
129
- bluetoothGatt = null
130
- Logger.debug(TAG, "Disconnected from GATT server.")
131
- cleanup()
132
- resolve("disconnect", "Disconnected.")
133
- }
134
- }
135
-
136
- override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
137
- super.onServicesDiscovered(gatt, status)
138
- if (status == BluetoothGatt.GATT_SUCCESS) {
139
- resolve("discoverServices", "Services discovered.")
140
- if (connectCallOngoing()) {
141
- // Try requesting a larger MTU. Maximally supported MTU will be selected.
142
- requestMtu(REQUEST_MTU)
143
- }
144
- } else {
145
- reject("discoverServices", "Service discovery failed.")
146
- reject("connect", "Service discovery failed.")
147
- }
148
- }
149
-
150
- override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
151
- super.onMtuChanged(gatt, mtu, status)
152
- if (status == BluetoothGatt.GATT_SUCCESS) {
153
- currentMtu = mtu
154
- Logger.debug(TAG, "MTU changed: $mtu")
155
- } else {
156
- Logger.debug(TAG, "MTU change failed: $mtu")
157
- }
158
- resolve("connect", "Connected.")
159
- }
160
-
161
- override fun onReadRemoteRssi(gatt: BluetoothGatt?, rssi: Int, status: Int) {
162
- super.onReadRemoteRssi(gatt, rssi, status)
163
- val key = "readRssi"
164
- if (status == BluetoothGatt.GATT_SUCCESS) {
165
- resolve(key, rssi.toString())
166
- } else {
167
- reject(key, "Reading RSSI failed.")
168
- }
169
- }
170
-
171
- @TargetApi(Build.VERSION_CODES.S_V2)
172
- override fun onCharacteristicRead(
173
- gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int
174
- ) {
175
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
176
- // handled by new callback below
177
- return
178
- }
179
- Logger.verbose(TAG, "Using deprecated onCharacteristicRead.")
180
- super.onCharacteristicRead(gatt, characteristic, status)
181
- val key = "read|${characteristic.service.uuid}|${characteristic.uuid}"
182
- if (status == BluetoothGatt.GATT_SUCCESS) {
183
- val data = characteristic.value
184
- if (data != null) {
185
- val value = bytesToString(data)
186
- resolve(key, value)
187
- } else {
188
- reject(key, "No data received while reading characteristic.")
189
- }
190
- } else {
191
- reject(key, "Reading characteristic failed.")
192
- }
193
- }
194
-
195
- @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
196
- override fun onCharacteristicRead(
197
- gatt: BluetoothGatt,
198
- characteristic: BluetoothGattCharacteristic,
199
- data: ByteArray,
200
- status: Int
201
- ) {
202
- Logger.verbose(TAG, "Using onCharacteristicRead from API level 33.")
203
- super.onCharacteristicRead(gatt, characteristic, data, status)
204
- val key = "read|${characteristic.service.uuid}|${characteristic.uuid}"
205
- if (status == BluetoothGatt.GATT_SUCCESS) {
206
- val value = bytesToString(data)
207
- resolve(key, value)
208
- } else {
209
- reject(key, "Reading characteristic failed.")
210
- }
211
- }
212
-
213
- override fun onCharacteristicWrite(
214
- gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int
215
- ) {
216
- super.onCharacteristicWrite(gatt, characteristic, status)
217
- val key = "write|${characteristic.service.uuid}|${characteristic.uuid}"
218
- if (status == BluetoothGatt.GATT_SUCCESS) {
219
- resolve(key, "Characteristic successfully written.")
220
- } else {
221
- reject(key, "Writing characteristic failed.")
222
- }
223
-
224
- }
225
-
226
- @TargetApi(Build.VERSION_CODES.S_V2)
227
- override fun onCharacteristicChanged(
228
- gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic
229
- ) {
230
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
231
- // handled by new callback below
232
- return
233
- }
234
- Logger.verbose(TAG, "Using deprecated onCharacteristicChanged.")
235
- super.onCharacteristicChanged(gatt, characteristic)
236
- val notifyKey = "notification|${characteristic.service.uuid}|${characteristic.uuid}"
237
- val data = characteristic.value
238
- if (data != null) {
239
- val value = bytesToString(data)
240
- callbackMap[notifyKey]?.invoke(CallbackResponse(true, value))
241
- }
242
- }
243
-
244
- @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
245
- override fun onCharacteristicChanged(
246
- gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, data: ByteArray
247
- ) {
248
- Logger.verbose(TAG, "Using onCharacteristicChanged from API level 33.")
249
- super.onCharacteristicChanged(gatt, characteristic, data)
250
- val notifyKey = "notification|${characteristic.service.uuid}|${characteristic.uuid}"
251
- val value = bytesToString(data)
252
- callbackMap[notifyKey]?.invoke(CallbackResponse(true, value))
253
- }
254
-
255
- @TargetApi(Build.VERSION_CODES.S_V2)
256
- override fun onDescriptorRead(
257
- gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int
258
- ) {
259
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
260
- // handled by new callback below
261
- return
262
- }
263
- Logger.verbose(TAG, "Using deprecated onDescriptorRead.")
264
- super.onDescriptorRead(gatt, descriptor, status)
265
- val key =
266
- "readDescriptor|${descriptor.characteristic.service.uuid}|${descriptor.characteristic.uuid}|${descriptor.uuid}"
267
- if (status == BluetoothGatt.GATT_SUCCESS) {
268
- val data = descriptor.value
269
- if (data != null) {
270
- val value = bytesToString(data)
271
- resolve(key, value)
272
- } else {
273
- reject(key, "No data received while reading descriptor.")
274
- }
275
- } else {
276
- reject(key, "Reading descriptor failed.")
277
- }
278
- }
279
-
280
- @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
281
- override fun onDescriptorRead(
282
- gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int, data: ByteArray
283
- ) {
284
- Logger.verbose(TAG, "Using onDescriptorRead from API level 33.")
285
- super.onDescriptorRead(gatt, descriptor, status, data)
286
- val key =
287
- "readDescriptor|${descriptor.characteristic.service.uuid}|${descriptor.characteristic.uuid}|${descriptor.uuid}"
288
- if (status == BluetoothGatt.GATT_SUCCESS) {
289
- val value = bytesToString(data)
290
- resolve(key, value)
291
- } else {
292
- reject(key, "Reading descriptor failed.")
293
- }
294
- }
295
-
296
- override fun onDescriptorWrite(
297
- gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int
298
- ) {
299
- super.onDescriptorWrite(gatt, descriptor, status)
300
- val key =
301
- "writeDescriptor|${descriptor.characteristic.service.uuid}|${descriptor.characteristic.uuid}|${descriptor.uuid}"
302
- if (status == BluetoothGatt.GATT_SUCCESS) {
303
- resolve(key, "Descriptor successfully written.")
304
- } else {
305
- reject(key, "Writing descriptor failed.")
306
- }
307
- }
308
- }
309
-
310
- fun getId(): String {
311
- return address
312
- }
313
-
314
- /**
315
- * Actions that will be executed (see gattCallback)
316
- * - connect to gatt server
317
- * - discover services
318
- * - request MTU
319
- */
320
- fun connect(
321
- timeout: Long, callback: (CallbackResponse) -> Unit
322
- ) {
323
- val key = "connect"
324
- callbackMap[key] = callback
325
- if (isConnected()) {
326
- resolve(key, "Already connected.")
327
- return
328
- }
329
- bluetoothGatt?.close()
330
- connectionState = STATE_CONNECTING
331
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
332
- initializeCallbacksHandlerThread()
333
- bluetoothGatt = device.connectGatt(
334
- context,
335
- false,
336
- gattCallback,
337
- BluetoothDevice.TRANSPORT_LE,
338
- BluetoothDevice.PHY_OPTION_NO_PREFERRED,
339
- callbacksHandler
340
- )
341
- } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
342
- bluetoothGatt = device.connectGatt(
343
- context, false, gattCallback, BluetoothDevice.TRANSPORT_LE
344
- )
345
- } else {
346
- bluetoothGatt = device.connectGatt(
347
- context, false, gattCallback
348
- )
349
- }
350
- setConnectionTimeout(key, "Connection timeout.", bluetoothGatt, timeout)
351
- }
352
-
353
- private fun connectCallOngoing(): Boolean {
354
- return callbackMap.containsKey("connect")
355
- }
356
-
357
- fun isConnected(): Boolean {
358
- return bluetoothGatt != null && connectionState == STATE_CONNECTED
359
- }
360
-
361
- private fun requestMtu(mtu: Int) {
362
- Logger.debug(TAG, "requestMtu $mtu")
363
- val result = bluetoothGatt?.requestMtu(mtu)
364
- if (result != true) {
365
- reject("connect", "Starting requestMtu failed.")
366
- }
367
- }
368
-
369
- fun getMtu(): Int {
370
- return currentMtu
371
- }
372
-
373
- fun requestConnectionPriority(connectionPriority: Int): Boolean {
374
- return bluetoothGatt?.requestConnectionPriority(connectionPriority) ?: false
375
- }
376
-
377
- fun createBond(timeout: Long, callback: (CallbackResponse) -> Unit) {
378
- val key = "createBond"
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
-
387
- try {
388
- ensureBondStateReceiverRegistered()
389
- } catch (e: Exception) {
390
- Logger.error(TAG, "Error while registering bondStateReceiver: ${e.localizedMessage}", e)
391
- reject(key, "Creating bond failed.")
392
- return
393
- }
394
- val result = device.createBond()
395
- if (!result) {
396
- reject(key, "Creating bond failed.")
397
- return
398
- }
399
- // Wait for bond state change
400
- setTimeout(key, "Bonding timeout.", timeout)
401
- }
402
-
403
- private fun ensureBondStateReceiverRegistered() {
404
- synchronized(this) {
405
- if (bondStateReceiver != null) return
406
-
407
- bondStateReceiver = object : BroadcastReceiver() {
408
- override fun onReceive(ctx: Context, intent: Intent) {
409
- if (intent.action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) {
410
- val updatedDevice =
411
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
412
- intent.getParcelableExtra(
413
- BluetoothDevice.EXTRA_DEVICE,
414
- BluetoothDevice::class.java
415
- )
416
- } else {
417
- intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
418
- }
419
-
420
- // BroadcastReceiver receives bond state updates from all devices, need to filter by device
421
- if (device.address == updatedDevice?.address) {
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
- }
444
- }
445
- }
446
- }
447
- }
448
- }
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
- }
457
- }
458
- }
459
-
460
- fun isBonded(): Boolean {
461
- return device.bondState == BluetoothDevice.BOND_BONDED
462
- }
463
-
464
- fun disconnect(
465
- timeout: Long, callback: (CallbackResponse) -> Unit
466
- ) {
467
- val key = "disconnect"
468
- callbackMap[key] = callback
469
- if (bluetoothGatt == null) {
470
- resolve(key, "Disconnected.")
471
- return
472
- }
473
- bluetoothGatt?.disconnect()
474
- setTimeout(key, "Disconnection timeout.", timeout)
475
- }
476
-
477
- fun getServices(): MutableList<BluetoothGattService> {
478
- return bluetoothGatt?.services ?: mutableListOf()
479
- }
480
-
481
- fun discoverServices(
482
- timeout: Long, callback: (CallbackResponse) -> Unit
483
- ) {
484
- val key = "discoverServices"
485
- callbackMap[key] = callback
486
- refreshDeviceCache()
487
- val result = bluetoothGatt?.discoverServices()
488
- if (result != true) {
489
- reject(key, "Service discovery failed.")
490
- return
491
- }
492
- setTimeout(key, "Service discovery timeout.", timeout)
493
- }
494
-
495
- private fun refreshDeviceCache(): Boolean {
496
- var result = false
497
-
498
- try {
499
- if (bluetoothGatt != null) {
500
- val refresh = bluetoothGatt!!.javaClass.getMethod("refresh")
501
- result = (refresh.invoke(bluetoothGatt) as Boolean)
502
- }
503
- } catch (e: Exception) {
504
- Logger.error(TAG, "Error while refreshing device cache: ${e.localizedMessage}", e)
505
- }
506
-
507
- Logger.debug(TAG, "Device cache refresh $result")
508
- return result
509
- }
510
-
511
- fun readRssi(
512
- timeout: Long, callback: (CallbackResponse) -> Unit
513
- ) {
514
- val key = "readRssi"
515
- callbackMap[key] = callback
516
- val result = bluetoothGatt?.readRemoteRssi()
517
- if (result != true) {
518
- reject(key, "Reading RSSI failed.")
519
- return
520
- }
521
- setTimeout(key, "Reading RSSI timeout.", timeout)
522
- }
523
-
524
- fun read(
525
- serviceUUID: UUID,
526
- characteristicUUID: UUID,
527
- timeout: Long,
528
- callback: (CallbackResponse) -> Unit
529
- ) {
530
- val key = "read|$serviceUUID|$characteristicUUID"
531
- callbackMap[key] = callback
532
- val service = bluetoothGatt?.getService(serviceUUID)
533
- val characteristic = service?.getCharacteristic(characteristicUUID)
534
- if (characteristic == null) {
535
- reject(key, "Characteristic not found.")
536
- return
537
- }
538
- val result = bluetoothGatt?.readCharacteristic(characteristic)
539
- if (result != true) {
540
- reject(key, "Reading characteristic failed.")
541
- return
542
- }
543
- setTimeout(key, "Read timeout.", timeout)
544
- }
545
-
546
- fun write(
547
- serviceUUID: UUID,
548
- characteristicUUID: UUID,
549
- value: String,
550
- writeType: Int,
551
- timeout: Long,
552
- callback: (CallbackResponse) -> Unit
553
- ) {
554
- val key = "write|$serviceUUID|$characteristicUUID"
555
- callbackMap[key] = callback
556
- val service = bluetoothGatt?.getService(serviceUUID)
557
- val characteristic = service?.getCharacteristic(characteristicUUID)
558
- if (characteristic == null) {
559
- reject(key, "Characteristic not found.")
560
- return
561
- }
562
- val bytes = stringToBytes(value)
563
-
564
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
565
- val statusCode = bluetoothGatt?.writeCharacteristic(characteristic, bytes, writeType)
566
- if (statusCode != BluetoothStatusCodes.SUCCESS) {
567
- reject(key, "Writing characteristic failed with status code $statusCode.")
568
- return
569
- }
570
- } else {
571
- characteristic.value = bytes
572
- characteristic.writeType = writeType
573
- val result = bluetoothGatt?.writeCharacteristic(characteristic)
574
- if (result != true) {
575
- reject(key, "Writing characteristic failed.")
576
- return
577
- }
578
- }
579
- setTimeout(key, "Write timeout.", timeout)
580
- }
581
-
582
- fun setNotifications(
583
- serviceUUID: UUID,
584
- characteristicUUID: UUID,
585
- enable: Boolean,
586
- notifyCallback: ((CallbackResponse) -> Unit)?,
587
- timeout: Long,
588
- callback: (CallbackResponse) -> Unit,
589
- ) {
590
- val key = "writeDescriptor|$serviceUUID|$characteristicUUID|$CLIENT_CHARACTERISTIC_CONFIG"
591
- val notifyKey = "notification|$serviceUUID|$characteristicUUID"
592
- callbackMap[key] = callback
593
- if (notifyCallback != null) {
594
- callbackMap[notifyKey] = notifyCallback
595
- }
596
- val service = bluetoothGatt?.getService(serviceUUID)
597
- val characteristic = service?.getCharacteristic(characteristicUUID)
598
- if (characteristic == null) {
599
- reject(key, "Characteristic not found.")
600
- return
601
- }
602
-
603
- val result = bluetoothGatt?.setCharacteristicNotification(characteristic, enable)
604
- if (result != true) {
605
- reject(key, "Setting notification failed.")
606
- return
607
- }
608
-
609
- val descriptor = characteristic.getDescriptor(UUID.fromString(CLIENT_CHARACTERISTIC_CONFIG))
610
- if (descriptor == null) {
611
- reject(key, "Setting notification failed.")
612
- return
613
- }
614
-
615
- val value = if (enable) {
616
- if ((characteristic.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
617
- BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
618
- } else if ((characteristic.properties and BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) {
619
- BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
620
- } else {
621
- BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
622
- }
623
- } else {
624
- BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
625
- }
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
-
640
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
641
- val statusCode = bluetoothGatt?.writeDescriptor(descriptor, value)
642
- if (statusCode != BluetoothStatusCodes.SUCCESS) {
643
- reject(key, "Setting notification failed with status code $statusCode.")
644
- return
645
- }
646
- } else {
647
- descriptor.value = value
648
- val resultDesc = bluetoothGatt?.writeDescriptor(descriptor)
649
- if (resultDesc != true) {
650
- reject(key, "Setting notification failed.")
651
- return
652
- }
653
-
654
- }
655
- setTimeout(key, "Setting notification timeout.", timeout)
656
- // wait for onDescriptorWrite
657
- }
658
-
659
- fun readDescriptor(
660
- serviceUUID: UUID,
661
- characteristicUUID: UUID,
662
- descriptorUUID: UUID,
663
- timeout: Long,
664
- callback: (CallbackResponse) -> Unit
665
- ) {
666
- val key = "readDescriptor|$serviceUUID|$characteristicUUID|$descriptorUUID"
667
- callbackMap[key] = callback
668
- val service = bluetoothGatt?.getService(serviceUUID)
669
- val characteristic = service?.getCharacteristic(characteristicUUID)
670
- if (characteristic == null) {
671
- reject(key, "Characteristic not found.")
672
- return
673
- }
674
- val descriptor = characteristic.getDescriptor(descriptorUUID)
675
- if (descriptor == null) {
676
- reject(key, "Descriptor not found.")
677
- return
678
- }
679
- val result = bluetoothGatt?.readDescriptor(descriptor)
680
- if (result != true) {
681
- reject(key, "Reading descriptor failed.")
682
- return
683
- }
684
- setTimeout(key, "Read descriptor timeout.", timeout)
685
- }
686
-
687
- fun writeDescriptor(
688
- serviceUUID: UUID,
689
- characteristicUUID: UUID,
690
- descriptorUUID: UUID,
691
- value: String,
692
- timeout: Long,
693
- callback: (CallbackResponse) -> Unit
694
- ) {
695
- val key = "writeDescriptor|$serviceUUID|$characteristicUUID|$descriptorUUID"
696
- callbackMap[key] = callback
697
- val service = bluetoothGatt?.getService(serviceUUID)
698
- val characteristic = service?.getCharacteristic(characteristicUUID)
699
- if (characteristic == null) {
700
- reject(key, "Characteristic not found.")
701
- return
702
- }
703
- val descriptor = characteristic.getDescriptor(descriptorUUID)
704
- if (descriptor == null) {
705
- reject(key, "Descriptor not found.")
706
- return
707
- }
708
- val bytes = stringToBytes(value)
709
-
710
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
711
- val statusCode = bluetoothGatt?.writeDescriptor(descriptor, bytes)
712
- if (statusCode != BluetoothStatusCodes.SUCCESS) {
713
- reject(key, "Writing descriptor failed with status code $statusCode.")
714
- return
715
- }
716
- } else {
717
- descriptor.value = bytes
718
- val result = bluetoothGatt?.writeDescriptor(descriptor)
719
- if (result != true) {
720
- reject(key, "Writing descriptor failed.")
721
- return
722
- }
723
- }
724
- setTimeout(key, "Write timeout.", timeout)
725
- }
726
-
727
- private fun resolve(key: String, value: String) {
728
- pendingBondKeys.remove(key)
729
- callbackMap.remove(key)?.let { callback ->
730
- Logger.debug(TAG, "resolve: $key $value")
731
- timeoutQueue.popFirstMatch { it.key == key }?.handler?.removeCallbacksAndMessages(null)
732
- callback?.invoke(CallbackResponse(true, value))
733
- }
734
- }
735
-
736
- private fun reject(key: String, value: String) {
737
- pendingBondKeys.remove(key)
738
- callbackMap.remove(key)?.let { callback ->
739
- Logger.debug(TAG, "reject: $key $value")
740
- timeoutQueue.popFirstMatch { it.key == key }?.handler?.removeCallbacksAndMessages(null)
741
- callback?.invoke(CallbackResponse(false, value))
742
- }
743
- }
744
-
745
- private fun setTimeout(
746
- key: String, message: String, timeout: Long
747
- ) {
748
- val handler = Handler(Looper.getMainLooper())
749
- timeoutQueue.add(TimeoutHandler(key, handler))
750
- handler.postDelayed({
751
- reject(key, message)
752
- }, timeout)
753
- }
754
-
755
- private fun setConnectionTimeout(
756
- key: String,
757
- message: String,
758
- gatt: BluetoothGatt?,
759
- timeout: Long,
760
- ) {
761
- val handler = Handler(Looper.getMainLooper())
762
- timeoutQueue.add(TimeoutHandler(key, handler))
763
- handler.postDelayed({
764
- connectionState = STATE_DISCONNECTED
765
- gatt?.disconnect()
766
- gatt?.close()
767
- cleanup()
768
- reject(key, message)
769
- }, timeout)
770
- }
771
- }
1
+ package com.capacitorjs.community.plugins.bluetoothle
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.annotation.TargetApi
5
+ import android.bluetooth.BluetoothAdapter
6
+ import android.bluetooth.BluetoothDevice
7
+ import android.bluetooth.BluetoothGatt
8
+ import android.bluetooth.BluetoothGattCallback
9
+ import android.bluetooth.BluetoothGattCharacteristic
10
+ import android.bluetooth.BluetoothGattDescriptor
11
+ import android.bluetooth.BluetoothGattService
12
+ import android.bluetooth.BluetoothProfile
13
+ import android.bluetooth.BluetoothStatusCodes
14
+ import android.content.BroadcastReceiver
15
+ import android.content.Context
16
+ import android.content.Intent
17
+ import android.content.IntentFilter
18
+ import android.os.Build
19
+ import android.os.Handler
20
+ import android.os.HandlerThread
21
+ import android.os.Looper
22
+ import androidx.annotation.RequiresApi
23
+ import com.getcapacitor.Logger
24
+ import java.util.Collections
25
+ import java.util.UUID
26
+ import java.util.concurrent.ConcurrentHashMap
27
+ import java.util.concurrent.ConcurrentLinkedQueue
28
+
29
+ class CallbackResponse(
30
+ val success: Boolean,
31
+ val value: String,
32
+ )
33
+
34
+ class TimeoutHandler(
35
+ val key: String,
36
+ val handler: Handler
37
+ )
38
+
39
+ fun <T> ConcurrentLinkedQueue<T>.popFirstMatch(predicate: (T) -> Boolean): T? {
40
+ synchronized(this) {
41
+ val iterator = this.iterator()
42
+ while (iterator.hasNext()) {
43
+ val nextItem = iterator.next()
44
+ if (predicate(nextItem)) {
45
+ iterator.remove()
46
+ return nextItem
47
+ }
48
+ }
49
+ return null
50
+ }
51
+ }
52
+
53
+ @SuppressLint("MissingPermission")
54
+ class Device(
55
+ private val context: Context,
56
+ bluetoothAdapter: BluetoothAdapter,
57
+ private val address: String,
58
+ private val onDisconnect: () -> Unit
59
+ ) {
60
+ companion object {
61
+ private val TAG = Device::class.java.simpleName
62
+ private const val STATE_DISCONNECTED = 0
63
+ private const val STATE_CONNECTING = 1
64
+ private const val STATE_CONNECTED = 2
65
+ private const val CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb"
66
+ private const val REQUEST_MTU = 512
67
+ }
68
+
69
+ private var connectionState = STATE_DISCONNECTED
70
+ private var device: BluetoothDevice = bluetoothAdapter.getRemoteDevice(address)
71
+ private var bluetoothGatt: BluetoothGatt? = null
72
+ private var callbackMap = HashMap<String, ((CallbackResponse) -> Unit)>()
73
+ private val timeoutQueue = ConcurrentLinkedQueue<TimeoutHandler>()
74
+ private var bondStateReceiver: BroadcastReceiver? = null
75
+ private val pendingBondKeys = Collections.newSetFromMap(ConcurrentHashMap<String, Boolean>())
76
+ private var currentMtu = -1
77
+
78
+ private lateinit var callbacksHandlerThread: HandlerThread
79
+ private lateinit var callbacksHandler: Handler
80
+
81
+ private fun initializeCallbacksHandlerThread() {
82
+ synchronized(this) {
83
+ callbacksHandlerThread = HandlerThread("Callbacks thread")
84
+ callbacksHandlerThread.start()
85
+ callbacksHandler = Handler(callbacksHandlerThread.looper)
86
+ }
87
+ }
88
+
89
+ private fun cleanupCallbacksHandlerThread() {
90
+ synchronized(this) {
91
+ if (::callbacksHandlerThread.isInitialized) {
92
+ callbacksHandlerThread.quitSafely()
93
+ }
94
+ }
95
+ }
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
+
113
+ private val gattCallback: BluetoothGattCallback = object : BluetoothGattCallback() {
114
+ override fun onConnectionStateChange(
115
+ gatt: BluetoothGatt, status: Int, newState: Int
116
+ ) {
117
+ if (newState == BluetoothProfile.STATE_CONNECTED) {
118
+ connectionState = STATE_CONNECTED
119
+ // service discovery is required to use services
120
+ Logger.debug(TAG, "Connected to GATT server. Starting service discovery.")
121
+ val result = bluetoothGatt?.discoverServices()
122
+ if (result != true) {
123
+ reject("connect", "Starting service discovery failed.")
124
+ }
125
+ } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
126
+ connectionState = STATE_DISCONNECTED
127
+ onDisconnect()
128
+ bluetoothGatt?.close()
129
+ bluetoothGatt = null
130
+ Logger.debug(TAG, "Disconnected from GATT server.")
131
+ cleanup()
132
+ resolve("disconnect", "Disconnected.")
133
+ }
134
+ }
135
+
136
+ override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
137
+ super.onServicesDiscovered(gatt, status)
138
+ if (status == BluetoothGatt.GATT_SUCCESS) {
139
+ resolve("discoverServices", "Services discovered.")
140
+ if (connectCallOngoing()) {
141
+ // Try requesting a larger MTU. Maximally supported MTU will be selected.
142
+ requestMtu(REQUEST_MTU)
143
+ }
144
+ } else {
145
+ reject("discoverServices", "Service discovery failed.")
146
+ reject("connect", "Service discovery failed.")
147
+ }
148
+ }
149
+
150
+ override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
151
+ super.onMtuChanged(gatt, mtu, status)
152
+ if (status == BluetoothGatt.GATT_SUCCESS) {
153
+ currentMtu = mtu
154
+ Logger.debug(TAG, "MTU changed: $mtu")
155
+ } else {
156
+ Logger.debug(TAG, "MTU change failed: $mtu")
157
+ }
158
+ resolve("connect", "Connected.")
159
+ }
160
+
161
+ override fun onReadRemoteRssi(gatt: BluetoothGatt?, rssi: Int, status: Int) {
162
+ super.onReadRemoteRssi(gatt, rssi, status)
163
+ val key = "readRssi"
164
+ if (status == BluetoothGatt.GATT_SUCCESS) {
165
+ resolve(key, rssi.toString())
166
+ } else {
167
+ reject(key, "Reading RSSI failed.")
168
+ }
169
+ }
170
+
171
+ @TargetApi(Build.VERSION_CODES.S_V2)
172
+ override fun onCharacteristicRead(
173
+ gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int
174
+ ) {
175
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
176
+ // handled by new callback below
177
+ return
178
+ }
179
+ Logger.verbose(TAG, "Using deprecated onCharacteristicRead.")
180
+ super.onCharacteristicRead(gatt, characteristic, status)
181
+ val key = "read|${characteristic.service.uuid}|${characteristic.uuid}"
182
+ if (status == BluetoothGatt.GATT_SUCCESS) {
183
+ val data = characteristic.value
184
+ if (data != null) {
185
+ val value = bytesToString(data)
186
+ resolve(key, value)
187
+ } else {
188
+ reject(key, "No data received while reading characteristic.")
189
+ }
190
+ } else {
191
+ reject(key, "Reading characteristic failed.")
192
+ }
193
+ }
194
+
195
+ @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
196
+ override fun onCharacteristicRead(
197
+ gatt: BluetoothGatt,
198
+ characteristic: BluetoothGattCharacteristic,
199
+ data: ByteArray,
200
+ status: Int
201
+ ) {
202
+ Logger.verbose(TAG, "Using onCharacteristicRead from API level 33.")
203
+ super.onCharacteristicRead(gatt, characteristic, data, status)
204
+ val key = "read|${characteristic.service.uuid}|${characteristic.uuid}"
205
+ if (status == BluetoothGatt.GATT_SUCCESS) {
206
+ val value = bytesToString(data)
207
+ resolve(key, value)
208
+ } else {
209
+ reject(key, "Reading characteristic failed.")
210
+ }
211
+ }
212
+
213
+ override fun onCharacteristicWrite(
214
+ gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int
215
+ ) {
216
+ super.onCharacteristicWrite(gatt, characteristic, status)
217
+ val key = "write|${characteristic.service.uuid}|${characteristic.uuid}"
218
+ if (status == BluetoothGatt.GATT_SUCCESS) {
219
+ resolve(key, "Characteristic successfully written.")
220
+ } else {
221
+ reject(key, "Writing characteristic failed.")
222
+ }
223
+
224
+ }
225
+
226
+ @TargetApi(Build.VERSION_CODES.S_V2)
227
+ override fun onCharacteristicChanged(
228
+ gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic
229
+ ) {
230
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
231
+ // handled by new callback below
232
+ return
233
+ }
234
+ Logger.verbose(TAG, "Using deprecated onCharacteristicChanged.")
235
+ super.onCharacteristicChanged(gatt, characteristic)
236
+ val notifyKey = "notification|${characteristic.service.uuid}|${characteristic.uuid}"
237
+ val data = characteristic.value
238
+ if (data != null) {
239
+ val value = bytesToString(data)
240
+ callbackMap[notifyKey]?.invoke(CallbackResponse(true, value))
241
+ }
242
+ }
243
+
244
+ @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
245
+ override fun onCharacteristicChanged(
246
+ gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, data: ByteArray
247
+ ) {
248
+ Logger.verbose(TAG, "Using onCharacteristicChanged from API level 33.")
249
+ super.onCharacteristicChanged(gatt, characteristic, data)
250
+ val notifyKey = "notification|${characteristic.service.uuid}|${characteristic.uuid}"
251
+ val value = bytesToString(data)
252
+ callbackMap[notifyKey]?.invoke(CallbackResponse(true, value))
253
+ }
254
+
255
+ @TargetApi(Build.VERSION_CODES.S_V2)
256
+ override fun onDescriptorRead(
257
+ gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int
258
+ ) {
259
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
260
+ // handled by new callback below
261
+ return
262
+ }
263
+ Logger.verbose(TAG, "Using deprecated onDescriptorRead.")
264
+ super.onDescriptorRead(gatt, descriptor, status)
265
+ val key =
266
+ "readDescriptor|${descriptor.characteristic.service.uuid}|${descriptor.characteristic.uuid}|${descriptor.uuid}"
267
+ if (status == BluetoothGatt.GATT_SUCCESS) {
268
+ val data = descriptor.value
269
+ if (data != null) {
270
+ val value = bytesToString(data)
271
+ resolve(key, value)
272
+ } else {
273
+ reject(key, "No data received while reading descriptor.")
274
+ }
275
+ } else {
276
+ reject(key, "Reading descriptor failed.")
277
+ }
278
+ }
279
+
280
+ @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
281
+ override fun onDescriptorRead(
282
+ gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int, data: ByteArray
283
+ ) {
284
+ Logger.verbose(TAG, "Using onDescriptorRead from API level 33.")
285
+ super.onDescriptorRead(gatt, descriptor, status, data)
286
+ val key =
287
+ "readDescriptor|${descriptor.characteristic.service.uuid}|${descriptor.characteristic.uuid}|${descriptor.uuid}"
288
+ if (status == BluetoothGatt.GATT_SUCCESS) {
289
+ val value = bytesToString(data)
290
+ resolve(key, value)
291
+ } else {
292
+ reject(key, "Reading descriptor failed.")
293
+ }
294
+ }
295
+
296
+ override fun onDescriptorWrite(
297
+ gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int
298
+ ) {
299
+ super.onDescriptorWrite(gatt, descriptor, status)
300
+ val key =
301
+ "writeDescriptor|${descriptor.characteristic.service.uuid}|${descriptor.characteristic.uuid}|${descriptor.uuid}"
302
+ if (status == BluetoothGatt.GATT_SUCCESS) {
303
+ resolve(key, "Descriptor successfully written.")
304
+ } else {
305
+ reject(key, "Writing descriptor failed.")
306
+ }
307
+ }
308
+ }
309
+
310
+ fun getId(): String {
311
+ return address
312
+ }
313
+
314
+ /**
315
+ * Actions that will be executed (see gattCallback)
316
+ * - connect to gatt server
317
+ * - discover services
318
+ * - request MTU
319
+ */
320
+ fun connect(
321
+ timeout: Long, callback: (CallbackResponse) -> Unit
322
+ ) {
323
+ val key = "connect"
324
+ callbackMap[key] = callback
325
+ if (isConnected()) {
326
+ resolve(key, "Already connected.")
327
+ return
328
+ }
329
+ bluetoothGatt?.close()
330
+ connectionState = STATE_CONNECTING
331
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
332
+ initializeCallbacksHandlerThread()
333
+ bluetoothGatt = device.connectGatt(
334
+ context,
335
+ false,
336
+ gattCallback,
337
+ BluetoothDevice.TRANSPORT_LE,
338
+ BluetoothDevice.PHY_OPTION_NO_PREFERRED,
339
+ callbacksHandler
340
+ )
341
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
342
+ bluetoothGatt = device.connectGatt(
343
+ context, false, gattCallback, BluetoothDevice.TRANSPORT_LE
344
+ )
345
+ } else {
346
+ bluetoothGatt = device.connectGatt(
347
+ context, false, gattCallback
348
+ )
349
+ }
350
+ setConnectionTimeout(key, "Connection timeout.", bluetoothGatt, timeout)
351
+ }
352
+
353
+ private fun connectCallOngoing(): Boolean {
354
+ return callbackMap.containsKey("connect")
355
+ }
356
+
357
+ fun isConnected(): Boolean {
358
+ return bluetoothGatt != null && connectionState == STATE_CONNECTED
359
+ }
360
+
361
+ private fun requestMtu(mtu: Int) {
362
+ Logger.debug(TAG, "requestMtu $mtu")
363
+ val result = bluetoothGatt?.requestMtu(mtu)
364
+ if (result != true) {
365
+ reject("connect", "Starting requestMtu failed.")
366
+ }
367
+ }
368
+
369
+ fun getMtu(): Int {
370
+ return currentMtu
371
+ }
372
+
373
+ fun requestConnectionPriority(connectionPriority: Int): Boolean {
374
+ return bluetoothGatt?.requestConnectionPriority(connectionPriority) ?: false
375
+ }
376
+
377
+ fun createBond(timeout: Long, callback: (CallbackResponse) -> Unit) {
378
+ val key = "createBond"
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
+
387
+ try {
388
+ ensureBondStateReceiverRegistered()
389
+ } catch (e: Exception) {
390
+ Logger.error(TAG, "Error while registering bondStateReceiver: ${e.localizedMessage}", e)
391
+ reject(key, "Creating bond failed.")
392
+ return
393
+ }
394
+ val result = device.createBond()
395
+ if (!result) {
396
+ reject(key, "Creating bond failed.")
397
+ return
398
+ }
399
+ // Wait for bond state change
400
+ setTimeout(key, "Bonding timeout.", timeout)
401
+ }
402
+
403
+ private fun ensureBondStateReceiverRegistered() {
404
+ synchronized(this) {
405
+ if (bondStateReceiver != null) return
406
+
407
+ bondStateReceiver = object : BroadcastReceiver() {
408
+ override fun onReceive(ctx: Context, intent: Intent) {
409
+ if (intent.action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) {
410
+ val updatedDevice =
411
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
412
+ intent.getParcelableExtra(
413
+ BluetoothDevice.EXTRA_DEVICE,
414
+ BluetoothDevice::class.java
415
+ )
416
+ } else {
417
+ intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
418
+ }
419
+
420
+ // BroadcastReceiver receives bond state updates from all devices, need to filter by device
421
+ if (device.address == updatedDevice?.address) {
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
+ }
444
+ }
445
+ }
446
+ }
447
+ }
448
+ }
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
+ }
457
+ }
458
+ }
459
+
460
+ fun isBonded(): Boolean {
461
+ return device.bondState == BluetoothDevice.BOND_BONDED
462
+ }
463
+
464
+ fun disconnect(
465
+ timeout: Long, callback: (CallbackResponse) -> Unit
466
+ ) {
467
+ val key = "disconnect"
468
+ callbackMap[key] = callback
469
+ if (bluetoothGatt == null) {
470
+ resolve(key, "Disconnected.")
471
+ return
472
+ }
473
+ bluetoothGatt?.disconnect()
474
+ setTimeout(key, "Disconnection timeout.", timeout)
475
+ }
476
+
477
+ fun getServices(): MutableList<BluetoothGattService> {
478
+ return bluetoothGatt?.services ?: mutableListOf()
479
+ }
480
+
481
+ fun discoverServices(
482
+ timeout: Long, callback: (CallbackResponse) -> Unit
483
+ ) {
484
+ val key = "discoverServices"
485
+ callbackMap[key] = callback
486
+ refreshDeviceCache()
487
+ val result = bluetoothGatt?.discoverServices()
488
+ if (result != true) {
489
+ reject(key, "Service discovery failed.")
490
+ return
491
+ }
492
+ setTimeout(key, "Service discovery timeout.", timeout)
493
+ }
494
+
495
+ private fun refreshDeviceCache(): Boolean {
496
+ var result = false
497
+
498
+ try {
499
+ if (bluetoothGatt != null) {
500
+ val refresh = bluetoothGatt!!.javaClass.getMethod("refresh")
501
+ result = (refresh.invoke(bluetoothGatt) as Boolean)
502
+ }
503
+ } catch (e: Exception) {
504
+ Logger.error(TAG, "Error while refreshing device cache: ${e.localizedMessage}", e)
505
+ }
506
+
507
+ Logger.debug(TAG, "Device cache refresh $result")
508
+ return result
509
+ }
510
+
511
+ fun readRssi(
512
+ timeout: Long, callback: (CallbackResponse) -> Unit
513
+ ) {
514
+ val key = "readRssi"
515
+ callbackMap[key] = callback
516
+ val result = bluetoothGatt?.readRemoteRssi()
517
+ if (result != true) {
518
+ reject(key, "Reading RSSI failed.")
519
+ return
520
+ }
521
+ setTimeout(key, "Reading RSSI timeout.", timeout)
522
+ }
523
+
524
+ fun read(
525
+ serviceUUID: UUID,
526
+ characteristicUUID: UUID,
527
+ timeout: Long,
528
+ callback: (CallbackResponse) -> Unit
529
+ ) {
530
+ val key = "read|$serviceUUID|$characteristicUUID"
531
+ callbackMap[key] = callback
532
+ val service = bluetoothGatt?.getService(serviceUUID)
533
+ val characteristic = service?.getCharacteristic(characteristicUUID)
534
+ if (characteristic == null) {
535
+ reject(key, "Characteristic not found.")
536
+ return
537
+ }
538
+ val result = bluetoothGatt?.readCharacteristic(characteristic)
539
+ if (result != true) {
540
+ reject(key, "Reading characteristic failed.")
541
+ return
542
+ }
543
+ setTimeout(key, "Read timeout.", timeout)
544
+ }
545
+
546
+ fun write(
547
+ serviceUUID: UUID,
548
+ characteristicUUID: UUID,
549
+ value: String,
550
+ writeType: Int,
551
+ timeout: Long,
552
+ callback: (CallbackResponse) -> Unit
553
+ ) {
554
+ val key = "write|$serviceUUID|$characteristicUUID"
555
+ callbackMap[key] = callback
556
+ val service = bluetoothGatt?.getService(serviceUUID)
557
+ val characteristic = service?.getCharacteristic(characteristicUUID)
558
+ if (characteristic == null) {
559
+ reject(key, "Characteristic not found.")
560
+ return
561
+ }
562
+ val bytes = stringToBytes(value)
563
+
564
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
565
+ val statusCode = bluetoothGatt?.writeCharacteristic(characteristic, bytes, writeType)
566
+ if (statusCode != BluetoothStatusCodes.SUCCESS) {
567
+ reject(key, "Writing characteristic failed with status code $statusCode.")
568
+ return
569
+ }
570
+ } else {
571
+ characteristic.value = bytes
572
+ characteristic.writeType = writeType
573
+ val result = bluetoothGatt?.writeCharacteristic(characteristic)
574
+ if (result != true) {
575
+ reject(key, "Writing characteristic failed.")
576
+ return
577
+ }
578
+ }
579
+ setTimeout(key, "Write timeout.", timeout)
580
+ }
581
+
582
+ fun setNotifications(
583
+ serviceUUID: UUID,
584
+ characteristicUUID: UUID,
585
+ enable: Boolean,
586
+ notifyCallback: ((CallbackResponse) -> Unit)?,
587
+ timeout: Long,
588
+ callback: (CallbackResponse) -> Unit,
589
+ ) {
590
+ val key = "writeDescriptor|$serviceUUID|$characteristicUUID|$CLIENT_CHARACTERISTIC_CONFIG"
591
+ val notifyKey = "notification|$serviceUUID|$characteristicUUID"
592
+ callbackMap[key] = callback
593
+ if (notifyCallback != null) {
594
+ callbackMap[notifyKey] = notifyCallback
595
+ }
596
+ val service = bluetoothGatt?.getService(serviceUUID)
597
+ val characteristic = service?.getCharacteristic(characteristicUUID)
598
+ if (characteristic == null) {
599
+ reject(key, "Characteristic not found.")
600
+ return
601
+ }
602
+
603
+ val result = bluetoothGatt?.setCharacteristicNotification(characteristic, enable)
604
+ if (result != true) {
605
+ reject(key, "Setting notification failed.")
606
+ return
607
+ }
608
+
609
+ val descriptor = characteristic.getDescriptor(UUID.fromString(CLIENT_CHARACTERISTIC_CONFIG))
610
+ if (descriptor == null) {
611
+ reject(key, "Setting notification failed.")
612
+ return
613
+ }
614
+
615
+ val value = if (enable) {
616
+ if ((characteristic.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
617
+ BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
618
+ } else if ((characteristic.properties and BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) {
619
+ BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
620
+ } else {
621
+ BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
622
+ }
623
+ } else {
624
+ BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
625
+ }
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
+
640
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
641
+ val statusCode = bluetoothGatt?.writeDescriptor(descriptor, value)
642
+ if (statusCode != BluetoothStatusCodes.SUCCESS) {
643
+ reject(key, "Setting notification failed with status code $statusCode.")
644
+ return
645
+ }
646
+ } else {
647
+ descriptor.value = value
648
+ val resultDesc = bluetoothGatt?.writeDescriptor(descriptor)
649
+ if (resultDesc != true) {
650
+ reject(key, "Setting notification failed.")
651
+ return
652
+ }
653
+
654
+ }
655
+ setTimeout(key, "Setting notification timeout.", timeout)
656
+ // wait for onDescriptorWrite
657
+ }
658
+
659
+ fun readDescriptor(
660
+ serviceUUID: UUID,
661
+ characteristicUUID: UUID,
662
+ descriptorUUID: UUID,
663
+ timeout: Long,
664
+ callback: (CallbackResponse) -> Unit
665
+ ) {
666
+ val key = "readDescriptor|$serviceUUID|$characteristicUUID|$descriptorUUID"
667
+ callbackMap[key] = callback
668
+ val service = bluetoothGatt?.getService(serviceUUID)
669
+ val characteristic = service?.getCharacteristic(characteristicUUID)
670
+ if (characteristic == null) {
671
+ reject(key, "Characteristic not found.")
672
+ return
673
+ }
674
+ val descriptor = characteristic.getDescriptor(descriptorUUID)
675
+ if (descriptor == null) {
676
+ reject(key, "Descriptor not found.")
677
+ return
678
+ }
679
+ val result = bluetoothGatt?.readDescriptor(descriptor)
680
+ if (result != true) {
681
+ reject(key, "Reading descriptor failed.")
682
+ return
683
+ }
684
+ setTimeout(key, "Read descriptor timeout.", timeout)
685
+ }
686
+
687
+ fun writeDescriptor(
688
+ serviceUUID: UUID,
689
+ characteristicUUID: UUID,
690
+ descriptorUUID: UUID,
691
+ value: String,
692
+ timeout: Long,
693
+ callback: (CallbackResponse) -> Unit
694
+ ) {
695
+ val key = "writeDescriptor|$serviceUUID|$characteristicUUID|$descriptorUUID"
696
+ callbackMap[key] = callback
697
+ val service = bluetoothGatt?.getService(serviceUUID)
698
+ val characteristic = service?.getCharacteristic(characteristicUUID)
699
+ if (characteristic == null) {
700
+ reject(key, "Characteristic not found.")
701
+ return
702
+ }
703
+ val descriptor = characteristic.getDescriptor(descriptorUUID)
704
+ if (descriptor == null) {
705
+ reject(key, "Descriptor not found.")
706
+ return
707
+ }
708
+ val bytes = stringToBytes(value)
709
+
710
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
711
+ val statusCode = bluetoothGatt?.writeDescriptor(descriptor, bytes)
712
+ if (statusCode != BluetoothStatusCodes.SUCCESS) {
713
+ reject(key, "Writing descriptor failed with status code $statusCode.")
714
+ return
715
+ }
716
+ } else {
717
+ descriptor.value = bytes
718
+ val result = bluetoothGatt?.writeDescriptor(descriptor)
719
+ if (result != true) {
720
+ reject(key, "Writing descriptor failed.")
721
+ return
722
+ }
723
+ }
724
+ setTimeout(key, "Write timeout.", timeout)
725
+ }
726
+
727
+ private fun resolve(key: String, value: String) {
728
+ pendingBondKeys.remove(key)
729
+ callbackMap.remove(key)?.let { callback ->
730
+ Logger.debug(TAG, "resolve: $key $value")
731
+ timeoutQueue.popFirstMatch { it.key == key }?.handler?.removeCallbacksAndMessages(null)
732
+ callback?.invoke(CallbackResponse(true, value))
733
+ }
734
+ }
735
+
736
+ private fun reject(key: String, value: String) {
737
+ pendingBondKeys.remove(key)
738
+ callbackMap.remove(key)?.let { callback ->
739
+ Logger.debug(TAG, "reject: $key $value")
740
+ timeoutQueue.popFirstMatch { it.key == key }?.handler?.removeCallbacksAndMessages(null)
741
+ callback?.invoke(CallbackResponse(false, value))
742
+ }
743
+ }
744
+
745
+ private fun setTimeout(
746
+ key: String, message: String, timeout: Long
747
+ ) {
748
+ val handler = Handler(Looper.getMainLooper())
749
+ timeoutQueue.add(TimeoutHandler(key, handler))
750
+ handler.postDelayed({
751
+ reject(key, message)
752
+ }, timeout)
753
+ }
754
+
755
+ private fun setConnectionTimeout(
756
+ key: String,
757
+ message: String,
758
+ gatt: BluetoothGatt?,
759
+ timeout: Long,
760
+ ) {
761
+ val handler = Handler(Looper.getMainLooper())
762
+ timeoutQueue.add(TimeoutHandler(key, handler))
763
+ handler.postDelayed({
764
+ connectionState = STATE_DISCONNECTED
765
+ gatt?.disconnect()
766
+ gatt?.close()
767
+ cleanup()
768
+ reject(key, message)
769
+ }, timeout)
770
+ }
771
+ }