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