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