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