@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,1009 +1,1061 @@
1
- package com.capacitorjs.community.plugins.bluetoothle
2
-
3
- import android.Manifest
4
- import android.annotation.SuppressLint
5
- import android.app.Activity
6
- import android.bluetooth.BluetoothAdapter
7
- import android.bluetooth.BluetoothAdapter.ACTION_REQUEST_ENABLE
8
- import android.bluetooth.BluetoothDevice
9
- import android.bluetooth.BluetoothGatt
10
- import android.bluetooth.BluetoothGattCharacteristic
11
- import android.bluetooth.BluetoothManager
12
- import android.bluetooth.BluetoothProfile
13
- import android.bluetooth.le.ScanFilter
14
- import android.bluetooth.le.ScanResult
15
- import android.bluetooth.le.ScanSettings
16
- import android.content.BroadcastReceiver
17
- import android.content.Context
18
- import android.content.Intent
19
- import android.content.IntentFilter
20
- import android.content.pm.PackageManager
21
- import android.location.LocationManager
22
- import android.net.Uri
23
- import android.os.Build
24
- import android.os.ParcelUuid
25
- import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
26
- import android.provider.Settings.ACTION_BLUETOOTH_SETTINGS
27
- import android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS
28
- import androidx.activity.result.ActivityResult
29
- import androidx.core.location.LocationManagerCompat
30
- import com.getcapacitor.JSArray
31
- import com.getcapacitor.JSObject
32
- import com.getcapacitor.Logger
33
- import com.getcapacitor.PermissionState
34
- import com.getcapacitor.Plugin
35
- import com.getcapacitor.PluginCall
36
- import com.getcapacitor.PluginMethod
37
- import com.getcapacitor.annotation.ActivityCallback
38
- import com.getcapacitor.annotation.CapacitorPlugin
39
- import com.getcapacitor.annotation.Permission
40
- import com.getcapacitor.annotation.PermissionCallback
41
- import java.util.UUID
42
-
43
-
44
- @SuppressLint("MissingPermission")
45
- @CapacitorPlugin(
46
- name = "BluetoothLe",
47
- permissions = [
48
- Permission(
49
- strings = [
50
- Manifest.permission.ACCESS_COARSE_LOCATION,
51
- ], alias = "ACCESS_COARSE_LOCATION"
52
- ),
53
- Permission(
54
- strings = [
55
- Manifest.permission.ACCESS_FINE_LOCATION,
56
- ], alias = "ACCESS_FINE_LOCATION"
57
- ),
58
- Permission(
59
- strings = [
60
- Manifest.permission.BLUETOOTH,
61
- ], alias = "BLUETOOTH"
62
- ),
63
- Permission(
64
- strings = [
65
- Manifest.permission.BLUETOOTH_ADMIN,
66
- ], alias = "BLUETOOTH_ADMIN"
67
- ),
68
- Permission(
69
- strings = [
70
- // Manifest.permission.BLUETOOTH_SCAN
71
- "android.permission.BLUETOOTH_SCAN",
72
- ], alias = "BLUETOOTH_SCAN"
73
- ),
74
- Permission(
75
- strings = [
76
- // Manifest.permission.BLUETOOTH_ADMIN
77
- "android.permission.BLUETOOTH_CONNECT",
78
- ], alias = "BLUETOOTH_CONNECT"
79
- ),
80
- ]
81
- )
82
- class BluetoothLe : Plugin() {
83
- companion object {
84
- private val TAG = BluetoothLe::class.java.simpleName
85
-
86
- // maximal scan duration for requestDevice
87
- private const val MAX_SCAN_DURATION: Long = 30000
88
- private const val CONNECTION_TIMEOUT: Float = 10000.0F
89
- private const val DEFAULT_TIMEOUT: Float = 5000.0F
90
- }
91
-
92
- private var bluetoothAdapter: BluetoothAdapter? = null
93
- private var stateReceiver: BroadcastReceiver? = null
94
- private var deviceMap = HashMap<String, Device>()
95
- private var deviceScanner: DeviceScanner? = null
96
- private var displayStrings: DisplayStrings? = null
97
- private var aliases: Array<String> = arrayOf()
98
-
99
- override fun load() {
100
- displayStrings = getDisplayStrings()
101
- }
102
-
103
- @PluginMethod
104
- fun initialize(call: PluginCall) {
105
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
106
- val neverForLocation = call.getBoolean("androidNeverForLocation", false) as Boolean
107
- aliases = if (neverForLocation) {
108
- arrayOf(
109
- "BLUETOOTH_SCAN",
110
- "BLUETOOTH_CONNECT",
111
- )
112
- } else {
113
- arrayOf(
114
- "BLUETOOTH_SCAN",
115
- "BLUETOOTH_CONNECT",
116
- "ACCESS_FINE_LOCATION",
117
- )
118
- }
119
- } else {
120
- aliases = arrayOf(
121
- "ACCESS_COARSE_LOCATION",
122
- "ACCESS_FINE_LOCATION",
123
- "BLUETOOTH",
124
- "BLUETOOTH_ADMIN",
125
- )
126
- }
127
- requestPermissionForAliases(aliases, call, "checkPermission")
128
- }
129
-
130
- @PermissionCallback
131
- private fun checkPermission(call: PluginCall) {
132
- val granted: List<Boolean> = aliases.map { alias ->
133
- getPermissionState(alias) == PermissionState.GRANTED
134
- }
135
- // all have to be true
136
- if (granted.all { it }) {
137
- runInitialization(call)
138
- } else {
139
- call.reject("Permission denied.")
140
- }
141
- }
142
-
143
- private fun runInitialization(call: PluginCall) {
144
- if (!activity.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
145
- call.reject("BLE is not supported.")
146
- return
147
- }
148
-
149
- bluetoothAdapter =
150
- (activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
151
-
152
- if (bluetoothAdapter == null) {
153
- call.reject("BLE is not available.")
154
- return
155
- }
156
- call.resolve()
157
- }
158
-
159
- @PluginMethod
160
- fun isEnabled(call: PluginCall) {
161
- assertBluetoothAdapter(call) ?: return
162
- val enabled = bluetoothAdapter?.isEnabled == true
163
- val result = JSObject()
164
- result.put("value", enabled)
165
- call.resolve(result)
166
- }
167
-
168
- @PluginMethod
169
- fun requestEnable(call: PluginCall) {
170
- assertBluetoothAdapter(call) ?: return
171
- val intent = Intent(ACTION_REQUEST_ENABLE)
172
- startActivityForResult(call, intent, "handleRequestEnableResult")
173
- }
174
-
175
- @ActivityCallback
176
- private fun handleRequestEnableResult(call: PluginCall, result: ActivityResult) {
177
- if (result.resultCode == Activity.RESULT_OK) {
178
- call.resolve()
179
- } else {
180
- call.reject("requestEnable failed.")
181
- }
182
- }
183
-
184
- @PluginMethod
185
- fun enable(call: PluginCall) {
186
- assertBluetoothAdapter(call) ?: return
187
- val result = bluetoothAdapter?.enable()
188
- if (result != true) {
189
- call.reject("Enable failed.")
190
- return
191
- }
192
- call.resolve()
193
- }
194
-
195
- @PluginMethod
196
- fun disable(call: PluginCall) {
197
- assertBluetoothAdapter(call) ?: return
198
- val result = bluetoothAdapter?.disable()
199
- if (result != true) {
200
- call.reject("Disable failed.")
201
- return
202
- }
203
- call.resolve()
204
- }
205
-
206
- @PluginMethod
207
- fun startEnabledNotifications(call: PluginCall) {
208
- assertBluetoothAdapter(call) ?: return
209
-
210
- try {
211
- createStateReceiver()
212
- } catch (e: Error) {
213
- Logger.error(
214
- TAG, "Error while registering enabled state receiver: ${e.localizedMessage}", e
215
- )
216
- call.reject("startEnabledNotifications failed.")
217
- return
218
- }
219
- call.resolve()
220
- }
221
-
222
- private fun createStateReceiver() {
223
- if (stateReceiver == null) {
224
- stateReceiver = object : BroadcastReceiver() {
225
- override fun onReceive(context: Context, intent: Intent) {
226
- val action = intent.action
227
- if (action == BluetoothAdapter.ACTION_STATE_CHANGED) {
228
- val state = intent.getIntExtra(
229
- BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR
230
- )
231
- val enabled = state == BluetoothAdapter.STATE_ON
232
- val result = JSObject()
233
- result.put("value", enabled)
234
- try {
235
- notifyListeners("onEnabledChanged", result)
236
- } catch (e: ConcurrentModificationException) {
237
- Logger.error(TAG, "Error in notifyListeners: ${e.localizedMessage}", e)
238
- }
239
- }
240
- }
241
- }
242
- val intentFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
243
- context.registerReceiver(stateReceiver, intentFilter)
244
- }
245
- }
246
-
247
- @PluginMethod
248
- fun stopEnabledNotifications(call: PluginCall) {
249
- if (stateReceiver != null) {
250
- context.unregisterReceiver(stateReceiver)
251
- }
252
- stateReceiver = null
253
- call.resolve()
254
- }
255
-
256
- @PluginMethod
257
- fun isLocationEnabled(call: PluginCall) {
258
- val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
259
- val enabled = LocationManagerCompat.isLocationEnabled(lm)
260
- Logger.debug(TAG, "location $enabled")
261
- val result = JSObject()
262
- result.put("value", enabled)
263
- call.resolve(result)
264
- }
265
-
266
- @PluginMethod
267
- fun openLocationSettings(call: PluginCall) {
268
- val intent = Intent(ACTION_LOCATION_SOURCE_SETTINGS)
269
- activity.startActivity(intent)
270
- call.resolve()
271
- }
272
-
273
- @PluginMethod
274
- fun openBluetoothSettings(call: PluginCall) {
275
- val intent = Intent(ACTION_BLUETOOTH_SETTINGS)
276
- activity.startActivity(intent)
277
- call.resolve()
278
- }
279
-
280
- @PluginMethod
281
- fun openAppSettings(call: PluginCall) {
282
- val intent = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
283
- intent.data = Uri.parse("package:" + activity.packageName)
284
- activity.startActivity(intent)
285
- call.resolve()
286
- }
287
-
288
- @PluginMethod
289
- fun setDisplayStrings(call: PluginCall) {
290
- displayStrings = DisplayStrings(
291
- call.getString(
292
- "scanning", displayStrings!!.scanning
293
- ) as String,
294
- call.getString(
295
- "cancel", displayStrings!!.cancel
296
- ) as String,
297
- call.getString(
298
- "availableDevices", displayStrings!!.availableDevices
299
- ) as String,
300
- call.getString(
301
- "noDeviceFound", displayStrings!!.noDeviceFound
302
- ) as String,
303
- )
304
- call.resolve()
305
- }
306
-
307
- @PluginMethod
308
- fun requestDevice(call: PluginCall) {
309
- assertBluetoothAdapter(call) ?: return
310
- val scanFilters = getScanFilters(call) ?: return
311
- val scanSettings = getScanSettings(call) ?: return
312
- val namePrefix = call.getString("namePrefix", "") as String
313
-
314
- try {
315
- deviceScanner?.stopScanning()
316
- } catch (e: IllegalStateException) {
317
- Logger.error(TAG, "Error in requestDevice: ${e.localizedMessage}", e)
318
- call.reject(e.localizedMessage)
319
- return
320
- }
321
-
322
- deviceScanner = DeviceScanner(
323
- context,
324
- bluetoothAdapter!!,
325
- scanDuration = MAX_SCAN_DURATION,
326
- displayStrings = displayStrings!!,
327
- showDialog = true,
328
- )
329
- deviceScanner?.startScanning(
330
- scanFilters, scanSettings, false, namePrefix, { scanResponse ->
331
- run {
332
- if (scanResponse.success) {
333
- if (scanResponse.device == null) {
334
- call.reject("No device found.")
335
- } else {
336
- val bleDevice = getBleDevice(scanResponse.device)
337
- call.resolve(bleDevice)
338
- }
339
- } else {
340
- call.reject(scanResponse.message)
341
-
342
- }
343
- }
344
- }, null
345
- )
346
- }
347
-
348
- @PluginMethod
349
- fun requestLEScan(call: PluginCall) {
350
- assertBluetoothAdapter(call) ?: return
351
- val scanFilters = getScanFilters(call) ?: return
352
- val scanSettings = getScanSettings(call) ?: return
353
- val namePrefix = call.getString("namePrefix", "") as String
354
- val allowDuplicates = call.getBoolean("allowDuplicates", false) as Boolean
355
-
356
- try {
357
- deviceScanner?.stopScanning()
358
- } catch (e: IllegalStateException) {
359
- Logger.error(TAG, "Error in requestLEScan: ${e.localizedMessage}", e)
360
- call.reject(e.localizedMessage)
361
- return
362
- }
363
-
364
- deviceScanner = DeviceScanner(
365
- context,
366
- bluetoothAdapter!!,
367
- scanDuration = null,
368
- displayStrings = displayStrings!!,
369
- showDialog = false,
370
- )
371
- deviceScanner?.startScanning(scanFilters,
372
- scanSettings,
373
- allowDuplicates,
374
- namePrefix,
375
- { scanResponse ->
376
- run {
377
- if (scanResponse.success) {
378
- call.resolve()
379
- } else {
380
- call.reject(scanResponse.message)
381
- }
382
- }
383
- },
384
- { result ->
385
- run {
386
- val scanResult = getScanResult(result)
387
- try {
388
- notifyListeners("onScanResult", scanResult)
389
- } catch (e: ConcurrentModificationException) {
390
- Logger.error(TAG, "Error in notifyListeners: ${e.localizedMessage}", e)
391
- }
392
- }
393
- })
394
- }
395
-
396
- @PluginMethod
397
- fun stopLEScan(call: PluginCall) {
398
- assertBluetoothAdapter(call) ?: return
399
- try {
400
- deviceScanner?.stopScanning()
401
- } catch (e: IllegalStateException) {
402
- Logger.error(TAG, "Error in stopLEScan: ${e.localizedMessage}", e)
403
- }
404
- call.resolve()
405
- }
406
-
407
- @PluginMethod
408
- fun getDevices(call: PluginCall) {
409
- assertBluetoothAdapter(call) ?: return
410
- val deviceIds = (call.getArray("deviceIds", JSArray()) as JSArray).toList<String>()
411
- val bleDevices = JSArray()
412
- deviceIds.forEach { deviceId ->
413
- val bleDevice = JSObject()
414
- bleDevice.put("deviceId", deviceId)
415
- bleDevices.put(bleDevice)
416
- }
417
- val result = JSObject()
418
- result.put("devices", bleDevices)
419
- call.resolve(result)
420
- }
421
-
422
- @PluginMethod
423
- fun getConnectedDevices(call: PluginCall) {
424
- assertBluetoothAdapter(call) ?: return
425
- val bluetoothManager =
426
- (activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager)
427
- val devices = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT)
428
- val bleDevices = JSArray()
429
- devices.forEach { device ->
430
- bleDevices.put(getBleDevice(device))
431
- }
432
- val result = JSObject()
433
- result.put("devices", bleDevices)
434
- call.resolve(result)
435
- }
436
-
437
- @PluginMethod
438
- fun getBondedDevices(call: PluginCall) {
439
- assertBluetoothAdapter(call) ?: return
440
-
441
- val bluetoothManager = activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
442
- val bluetoothAdapter = bluetoothManager.adapter
443
-
444
- if (bluetoothAdapter == null) {
445
- call.reject("Bluetooth is not supported on this device")
446
- return
447
- }
448
-
449
- val bondedDevices = bluetoothAdapter.bondedDevices
450
- val bleDevices = JSArray()
451
-
452
- bondedDevices.forEach { device ->
453
- bleDevices.put(getBleDevice(device))
454
- }
455
-
456
- val result = JSObject()
457
- result.put("devices", bleDevices)
458
- call.resolve(result)
459
- }
460
-
461
- @PluginMethod
462
- fun connect(call: PluginCall) {
463
- val device = getOrCreateDevice(call) ?: return
464
- val timeout = call.getFloat("timeout", CONNECTION_TIMEOUT)!!.toLong()
465
- device.connect(timeout) { response ->
466
- run {
467
- if (response.success) {
468
- call.resolve()
469
- } else {
470
- call.reject(response.value)
471
- }
472
- }
473
- }
474
- }
475
-
476
- private fun onDisconnect(deviceId: String) {
477
- try {
478
- notifyListeners("disconnected|${deviceId}", null)
479
- } catch (e: ConcurrentModificationException) {
480
- Logger.error(TAG, "Error in notifyListeners: ${e.localizedMessage}", e)
481
- }
482
- }
483
-
484
- @PluginMethod
485
- fun createBond(call: PluginCall) {
486
- val device = getOrCreateDevice(call) ?: return
487
- val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
488
- device.createBond(timeout) { response ->
489
- run {
490
- if (response.success) {
491
- call.resolve()
492
- } else {
493
- call.reject(response.value)
494
- }
495
- }
496
- }
497
- }
498
-
499
- @PluginMethod
500
- fun isBonded(call: PluginCall) {
501
- val device = getOrCreateDevice(call) ?: return
502
- val isBonded = device.isBonded()
503
- val result = JSObject()
504
- result.put("value", isBonded)
505
- call.resolve(result)
506
- }
507
-
508
- @PluginMethod
509
- fun disconnect(call: PluginCall) {
510
- val device = getOrCreateDevice(call) ?: return
511
- val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
512
- device.disconnect(timeout) { response ->
513
- run {
514
- if (response.success) {
515
- deviceMap.remove(device.getId())
516
- call.resolve()
517
- } else {
518
- call.reject(response.value)
519
- }
520
- }
521
- }
522
- }
523
-
524
- @PluginMethod
525
- fun getServices(call: PluginCall) {
526
- val device = getDevice(call) ?: return
527
- val services = device.getServices()
528
- val bleServices = JSArray()
529
- services.forEach { service ->
530
- val bleCharacteristics = JSArray()
531
- service.characteristics.forEach { characteristic ->
532
- val bleCharacteristic = JSObject()
533
- bleCharacteristic.put("uuid", characteristic.uuid)
534
- bleCharacteristic.put("properties", getProperties(characteristic))
535
- val bleDescriptors = JSArray()
536
- characteristic.descriptors.forEach { descriptor ->
537
- val bleDescriptor = JSObject()
538
- bleDescriptor.put("uuid", descriptor.uuid)
539
- bleDescriptors.put(bleDescriptor)
540
- }
541
- bleCharacteristic.put("descriptors", bleDescriptors)
542
- bleCharacteristics.put(bleCharacteristic)
543
- }
544
- val bleService = JSObject()
545
- bleService.put("uuid", service.uuid)
546
- bleService.put("characteristics", bleCharacteristics)
547
- bleServices.put(bleService)
548
- }
549
- val ret = JSObject()
550
- ret.put("services", bleServices)
551
- call.resolve(ret)
552
- }
553
-
554
- private fun getProperties(characteristic: BluetoothGattCharacteristic): JSObject {
555
- val properties = JSObject()
556
- properties.put(
557
- "broadcast",
558
- characteristic.properties and BluetoothGattCharacteristic.PROPERTY_BROADCAST > 0
559
- )
560
- properties.put(
561
- "read", characteristic.properties and BluetoothGattCharacteristic.PROPERTY_READ > 0
562
- )
563
- properties.put(
564
- "writeWithoutResponse",
565
- characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE > 0
566
- )
567
- properties.put(
568
- "write", characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE > 0
569
- )
570
- properties.put(
571
- "notify", characteristic.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0
572
- )
573
- properties.put(
574
- "indicate",
575
- characteristic.properties and BluetoothGattCharacteristic.PROPERTY_INDICATE > 0
576
- )
577
- properties.put(
578
- "authenticatedSignedWrites",
579
- characteristic.properties and BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE > 0
580
- )
581
- properties.put(
582
- "extendedProperties",
583
- characteristic.properties and BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS > 0
584
- )
585
- return properties
586
- }
587
-
588
- @PluginMethod
589
- fun discoverServices(call: PluginCall) {
590
- val device = getDevice(call) ?: return
591
- val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
592
- device.discoverServices(timeout) { response ->
593
- run {
594
- if (response.success) {
595
- call.resolve()
596
- } else {
597
- call.reject(response.value)
598
- }
599
- }
600
- }
601
- }
602
-
603
- @PluginMethod
604
- fun getMtu(call: PluginCall) {
605
- val device = getDevice(call) ?: return
606
- val mtu = device.getMtu()
607
- val ret = JSObject()
608
- ret.put("value", mtu)
609
- call.resolve(ret)
610
- }
611
-
612
- @PluginMethod
613
- fun requestConnectionPriority(call: PluginCall) {
614
- val device = getDevice(call) ?: return
615
- val connectionPriority = call.getInt("connectionPriority", -1) as Int
616
- if (connectionPriority < BluetoothGatt.CONNECTION_PRIORITY_BALANCED || connectionPriority > BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER) {
617
- call.reject("Invalid connectionPriority.")
618
- return
619
- }
620
-
621
- val result = device.requestConnectionPriority(connectionPriority)
622
- if (result) {
623
- call.resolve()
624
- } else {
625
- call.reject("requestConnectionPriority failed.")
626
- }
627
- }
628
-
629
- @PluginMethod
630
- fun readRssi(call: PluginCall) {
631
- val device = getDevice(call) ?: return
632
- val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
633
- device.readRssi(timeout) { response ->
634
- run {
635
- if (response.success) {
636
- val ret = JSObject()
637
- ret.put("value", response.value)
638
- call.resolve(ret)
639
- } else {
640
- call.reject(response.value)
641
- }
642
- }
643
- }
644
- }
645
-
646
- @PluginMethod
647
- fun read(call: PluginCall) {
648
- val device = getDevice(call) ?: return
649
- val characteristic = getCharacteristic(call) ?: return
650
- val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
651
- device.read(characteristic.first, characteristic.second, timeout) { response ->
652
- run {
653
- if (response.success) {
654
- val ret = JSObject()
655
- ret.put("value", response.value)
656
- call.resolve(ret)
657
- } else {
658
- call.reject(response.value)
659
- }
660
- }
661
- }
662
- }
663
-
664
- @PluginMethod
665
- fun write(call: PluginCall) {
666
- val device = getDevice(call) ?: return
667
- val characteristic = getCharacteristic(call) ?: return
668
- val value = call.getString("value", null)
669
- if (value == null) {
670
- call.reject("Value required.")
671
- return
672
- }
673
- val writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
674
- val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
675
- device.write(
676
- characteristic.first, characteristic.second, value, writeType, timeout
677
- ) { response ->
678
- run {
679
- if (response.success) {
680
- call.resolve()
681
- } else {
682
- call.reject(response.value)
683
- }
684
- }
685
- }
686
- }
687
-
688
- @PluginMethod
689
- fun writeWithoutResponse(call: PluginCall) {
690
- val device = getDevice(call) ?: return
691
- val characteristic = getCharacteristic(call) ?: return
692
- val value = call.getString("value", null)
693
- if (value == null) {
694
- call.reject("Value required.")
695
- return
696
- }
697
- val writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
698
- val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
699
- device.write(
700
- characteristic.first, characteristic.second, value, writeType, timeout
701
- ) { response ->
702
- run {
703
- if (response.success) {
704
- call.resolve()
705
- } else {
706
- call.reject(response.value)
707
- }
708
- }
709
- }
710
- }
711
-
712
- @PluginMethod
713
- fun readDescriptor(call: PluginCall) {
714
- val device = getDevice(call) ?: return
715
- val descriptor = getDescriptor(call) ?: return
716
- val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
717
- device.readDescriptor(
718
- descriptor.first, descriptor.second, descriptor.third, timeout
719
- ) { response ->
720
- run {
721
- if (response.success) {
722
- val ret = JSObject()
723
- ret.put("value", response.value)
724
- call.resolve(ret)
725
- } else {
726
- call.reject(response.value)
727
- }
728
- }
729
- }
730
- }
731
-
732
- @PluginMethod
733
- fun writeDescriptor(call: PluginCall) {
734
- val device = getDevice(call) ?: return
735
- val descriptor = getDescriptor(call) ?: return
736
- val value = call.getString("value", null)
737
- if (value == null) {
738
- call.reject("Value required.")
739
- return
740
- }
741
- val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
742
- device.writeDescriptor(
743
- descriptor.first, descriptor.second, descriptor.third, value, timeout
744
- ) { response ->
745
- run {
746
- if (response.success) {
747
- call.resolve()
748
- } else {
749
- call.reject(response.value)
750
- }
751
- }
752
- }
753
- }
754
-
755
- @PluginMethod
756
- fun startNotifications(call: PluginCall) {
757
- val device = getDevice(call) ?: return
758
- val characteristic = getCharacteristic(call) ?: return
759
- device.setNotifications(characteristic.first, characteristic.second, true, { response ->
760
- run {
761
- val key =
762
- "notification|${device.getId()}|${(characteristic.first)}|${(characteristic.second)}"
763
- val ret = JSObject()
764
- ret.put("value", response.value)
765
- try {
766
- notifyListeners(key, ret)
767
- } catch (e: ConcurrentModificationException) {
768
- Logger.error(TAG, "Error in notifyListeners: ${e.localizedMessage}", e)
769
- }
770
- }
771
- }, { response ->
772
- run {
773
- if (response.success) {
774
- call.resolve()
775
- } else {
776
- call.reject(response.value)
777
- }
778
- }
779
- })
780
- }
781
-
782
- @PluginMethod
783
- fun stopNotifications(call: PluginCall) {
784
- val device = getDevice(call) ?: return
785
- val characteristic = getCharacteristic(call) ?: return
786
- device.setNotifications(
787
- characteristic.first, characteristic.second, false, null
788
- ) { response ->
789
- run {
790
- if (response.success) {
791
- call.resolve()
792
- } else {
793
- call.reject(response.value)
794
- }
795
- }
796
- }
797
- }
798
-
799
- private fun assertBluetoothAdapter(call: PluginCall): Boolean? {
800
- if (bluetoothAdapter == null) {
801
- call.reject("Bluetooth LE not initialized.")
802
- return null
803
- }
804
- return true
805
- }
806
-
807
- private fun getScanFilters(call: PluginCall): List<ScanFilter>? {
808
- val filters: ArrayList<ScanFilter> = ArrayList()
809
-
810
- val services = (call.getArray("services", JSArray()) as JSArray).toList<String>()
811
- val name = call.getString("name", null)
812
- try {
813
- for (service in services) {
814
- val filter = ScanFilter.Builder()
815
- filter.setServiceUuid(ParcelUuid.fromString(service))
816
- if (name != null) {
817
- filter.setDeviceName(name)
818
- }
819
- filters.add(filter.build())
820
- }
821
- } catch (e: IllegalArgumentException) {
822
- call.reject("Invalid service UUID.")
823
- return null
824
- }
825
-
826
- if (name != null && filters.isEmpty()) {
827
- val filter = ScanFilter.Builder()
828
- filter.setDeviceName(name)
829
- filters.add(filter.build())
830
- }
831
-
832
- return filters
833
- }
834
-
835
- private fun getScanSettings(call: PluginCall): ScanSettings? {
836
- val scanSettings = ScanSettings.Builder()
837
- val scanMode = call.getInt("scanMode", ScanSettings.SCAN_MODE_BALANCED) as Int
838
- try {
839
- scanSettings.setScanMode(scanMode)
840
- } catch (e: IllegalArgumentException) {
841
- call.reject("Invalid scan mode.")
842
- return null
843
- }
844
- return scanSettings.build()
845
- }
846
-
847
- private fun getBleDevice(device: BluetoothDevice): JSObject {
848
- val bleDevice = JSObject()
849
- bleDevice.put("deviceId", device.address)
850
- if (device.name != null) {
851
- bleDevice.put("name", device.name)
852
- }
853
-
854
- val uuids = JSArray()
855
- device.uuids?.forEach { uuid -> uuids.put(uuid.toString()) }
856
- if (uuids.length() > 0) {
857
- bleDevice.put("uuids", uuids)
858
- }
859
-
860
- return bleDevice
861
- }
862
-
863
- private fun getScanResult(result: ScanResult): JSObject {
864
- val scanResult = JSObject()
865
-
866
- val bleDevice = getBleDevice(result.device)
867
- scanResult.put("device", bleDevice)
868
-
869
- if (result.device.name != null) {
870
- scanResult.put("localName", result.device.name)
871
- }
872
-
873
- scanResult.put("rssi", result.rssi)
874
-
875
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
876
- scanResult.put("txPower", result.txPower)
877
- } else {
878
- scanResult.put("txPower", 127)
879
- }
880
-
881
- val manufacturerData = JSObject()
882
- val manufacturerSpecificData = result.scanRecord?.manufacturerSpecificData
883
- if (manufacturerSpecificData != null) {
884
- for (i in 0 until manufacturerSpecificData.size()) {
885
- val key = manufacturerSpecificData.keyAt(i)
886
- val bytes = manufacturerSpecificData.get(key)
887
- manufacturerData.put(key.toString(), bytesToString(bytes))
888
- }
889
- }
890
- scanResult.put("manufacturerData", manufacturerData)
891
-
892
- val serviceDataObject = JSObject()
893
- val serviceData = result.scanRecord?.serviceData
894
- serviceData?.forEach {
895
- serviceDataObject.put(it.key.toString(), bytesToString(it.value))
896
- }
897
- scanResult.put("serviceData", serviceDataObject)
898
-
899
- val uuids = JSArray()
900
- result.scanRecord?.serviceUuids?.forEach { uuid -> uuids.put(uuid.toString()) }
901
- scanResult.put("uuids", uuids)
902
-
903
- scanResult.put("rawAdvertisement", result.scanRecord?.bytes?.let { bytesToString(it) })
904
- return scanResult
905
- }
906
-
907
- private fun getDisplayStrings(): DisplayStrings {
908
- return DisplayStrings(
909
- config.getString(
910
- "displayStrings.scanning", "Scanning..."
911
- ),
912
- config.getString(
913
- "displayStrings.cancel", "Cancel"
914
- ),
915
- config.getString(
916
- "displayStrings.availableDevices", "Available devices"
917
- ),
918
- config.getString(
919
- "displayStrings.noDeviceFound", "No device found"
920
- ),
921
- )
922
- }
923
-
924
- private fun getDeviceId(call: PluginCall): String? {
925
- val deviceId = call.getString("deviceId", null)
926
- if (deviceId == null) {
927
- call.reject("deviceId required.")
928
- return null
929
- }
930
- return deviceId
931
- }
932
-
933
- private fun getOrCreateDevice(call: PluginCall): Device? {
934
- assertBluetoothAdapter(call) ?: return null
935
- val deviceId = getDeviceId(call) ?: return null
936
- val device = deviceMap[deviceId]
937
- if (device != null) {
938
- return device
939
- }
940
- return try {
941
- val newDevice = Device(
942
- activity.applicationContext, bluetoothAdapter!!, deviceId
943
- ) {
944
- onDisconnect(deviceId)
945
- }
946
- deviceMap[deviceId] = newDevice
947
- newDevice
948
- } catch (e: IllegalArgumentException) {
949
- call.reject("Invalid deviceId")
950
- null
951
- }
952
- }
953
-
954
- private fun getDevice(call: PluginCall): Device? {
955
- assertBluetoothAdapter(call) ?: return null
956
- val deviceId = getDeviceId(call) ?: return null
957
- val device = deviceMap[deviceId]
958
- if (device == null || !device.isConnected()) {
959
- call.reject("Not connected to device.")
960
- return null
961
- }
962
- return device
963
- }
964
-
965
- private fun getCharacteristic(call: PluginCall): Pair<UUID, UUID>? {
966
- val serviceString = call.getString("service", null)
967
- val serviceUUID: UUID?
968
- try {
969
- serviceUUID = UUID.fromString(serviceString)
970
- } catch (e: IllegalArgumentException) {
971
- call.reject("Invalid service UUID.")
972
- return null
973
- }
974
- if (serviceUUID == null) {
975
- call.reject("Service UUID required.")
976
- return null
977
- }
978
- val characteristicString = call.getString("characteristic", null)
979
- val characteristicUUID: UUID?
980
- try {
981
- characteristicUUID = UUID.fromString(characteristicString)
982
- } catch (e: IllegalArgumentException) {
983
- call.reject("Invalid characteristic UUID.")
984
- return null
985
- }
986
- if (characteristicUUID == null) {
987
- call.reject("Characteristic UUID required.")
988
- return null
989
- }
990
- return Pair(serviceUUID, characteristicUUID)
991
- }
992
-
993
- private fun getDescriptor(call: PluginCall): Triple<UUID, UUID, UUID>? {
994
- val characteristic = getCharacteristic(call) ?: return null
995
- val descriptorString = call.getString("descriptor", null)
996
- val descriptorUUID: UUID?
997
- try {
998
- descriptorUUID = UUID.fromString(descriptorString)
999
- } catch (e: IllegalAccessException) {
1000
- call.reject("Invalid descriptor UUID.")
1001
- return null
1002
- }
1003
- if (descriptorUUID == null) {
1004
- call.reject("Descriptor UUID required.")
1005
- return null
1006
- }
1007
- return Triple(characteristic.first, characteristic.second, descriptorUUID)
1008
- }
1009
- }
1
+ package com.capacitorjs.community.plugins.bluetoothle
2
+
3
+ import android.Manifest
4
+ import android.annotation.SuppressLint
5
+ import android.app.Activity
6
+ import android.bluetooth.BluetoothAdapter
7
+ import android.bluetooth.BluetoothAdapter.ACTION_REQUEST_ENABLE
8
+ import android.bluetooth.BluetoothDevice
9
+ import android.bluetooth.BluetoothGatt
10
+ import android.bluetooth.BluetoothGattCharacteristic
11
+ import android.bluetooth.BluetoothManager
12
+ import android.bluetooth.BluetoothProfile
13
+ import android.bluetooth.le.ScanFilter
14
+ import android.bluetooth.le.ScanResult
15
+ import android.bluetooth.le.ScanSettings
16
+ import android.content.BroadcastReceiver
17
+ import android.content.Context
18
+ import android.content.Intent
19
+ import android.content.IntentFilter
20
+ import android.content.pm.PackageManager
21
+ import android.location.LocationManager
22
+ import android.net.Uri
23
+ import android.os.Build
24
+ import android.os.ParcelUuid
25
+ import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
26
+ import android.provider.Settings.ACTION_BLUETOOTH_SETTINGS
27
+ import android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS
28
+ import androidx.activity.result.ActivityResult
29
+ import androidx.core.location.LocationManagerCompat
30
+ import com.getcapacitor.JSArray
31
+ import com.getcapacitor.JSObject
32
+ import com.getcapacitor.Logger
33
+ import com.getcapacitor.PermissionState
34
+ import com.getcapacitor.Plugin
35
+ import com.getcapacitor.PluginCall
36
+ import com.getcapacitor.PluginMethod
37
+ import com.getcapacitor.annotation.ActivityCallback
38
+ import com.getcapacitor.annotation.CapacitorPlugin
39
+ import com.getcapacitor.annotation.Permission
40
+ import com.getcapacitor.annotation.PermissionCallback
41
+ import java.util.UUID
42
+
43
+
44
+ @SuppressLint("MissingPermission")
45
+ @CapacitorPlugin(
46
+ name = "BluetoothLe",
47
+ permissions = [
48
+ Permission(
49
+ strings = [
50
+ Manifest.permission.ACCESS_COARSE_LOCATION,
51
+ ], alias = "ACCESS_COARSE_LOCATION"
52
+ ),
53
+ Permission(
54
+ strings = [
55
+ Manifest.permission.ACCESS_FINE_LOCATION,
56
+ ], alias = "ACCESS_FINE_LOCATION"
57
+ ),
58
+ Permission(
59
+ strings = [
60
+ Manifest.permission.BLUETOOTH,
61
+ ], alias = "BLUETOOTH"
62
+ ),
63
+ Permission(
64
+ strings = [
65
+ Manifest.permission.BLUETOOTH_ADMIN,
66
+ ], alias = "BLUETOOTH_ADMIN"
67
+ ),
68
+ Permission(
69
+ strings = [
70
+ // Manifest.permission.BLUETOOTH_SCAN
71
+ "android.permission.BLUETOOTH_SCAN",
72
+ ], alias = "BLUETOOTH_SCAN"
73
+ ),
74
+ Permission(
75
+ strings = [
76
+ // Manifest.permission.BLUETOOTH_ADMIN
77
+ "android.permission.BLUETOOTH_CONNECT",
78
+ ], alias = "BLUETOOTH_CONNECT"
79
+ ),
80
+ ]
81
+ )
82
+ class BluetoothLe : Plugin() {
83
+ companion object {
84
+ private val TAG = BluetoothLe::class.java.simpleName
85
+
86
+ // maximal scan duration for requestDevice
87
+ private const val MAX_SCAN_DURATION: Long = 30000
88
+ private const val CONNECTION_TIMEOUT: Float = 10000.0F
89
+ private const val DEFAULT_TIMEOUT: Float = 5000.0F
90
+ }
91
+
92
+ private var bluetoothAdapter: BluetoothAdapter? = null
93
+ private var stateReceiver: BroadcastReceiver? = null
94
+ private var deviceMap = HashMap<String, Device>()
95
+ private var deviceScanner: DeviceScanner? = null
96
+ private var displayStrings: DisplayStrings? = null
97
+ private var aliases: Array<String> = arrayOf()
98
+
99
+ override fun load() {
100
+ displayStrings = getDisplayStrings()
101
+ }
102
+
103
+ @PluginMethod
104
+ fun initialize(call: PluginCall) {
105
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
106
+ val neverForLocation = call.getBoolean("androidNeverForLocation", false) as Boolean
107
+ aliases = if (neverForLocation) {
108
+ arrayOf(
109
+ "BLUETOOTH_SCAN",
110
+ "BLUETOOTH_CONNECT",
111
+ )
112
+ } else {
113
+ arrayOf(
114
+ "BLUETOOTH_SCAN",
115
+ "BLUETOOTH_CONNECT",
116
+ "ACCESS_FINE_LOCATION",
117
+ )
118
+ }
119
+ } else {
120
+ aliases = arrayOf(
121
+ "ACCESS_COARSE_LOCATION",
122
+ "ACCESS_FINE_LOCATION",
123
+ "BLUETOOTH",
124
+ "BLUETOOTH_ADMIN",
125
+ )
126
+ }
127
+ requestPermissionForAliases(aliases, call, "checkPermission")
128
+ }
129
+
130
+ @PermissionCallback
131
+ private fun checkPermission(call: PluginCall) {
132
+ val granted: List<Boolean> = aliases.map { alias ->
133
+ getPermissionState(alias) == PermissionState.GRANTED
134
+ }
135
+ // all have to be true
136
+ if (granted.all { it }) {
137
+ runInitialization(call)
138
+ } else {
139
+ call.reject("Permission denied.")
140
+ }
141
+ }
142
+
143
+ private fun runInitialization(call: PluginCall) {
144
+ if (!activity.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
145
+ call.reject("BLE is not supported.")
146
+ return
147
+ }
148
+
149
+ bluetoothAdapter =
150
+ (activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
151
+
152
+ if (bluetoothAdapter == null) {
153
+ call.reject("BLE is not available.")
154
+ return
155
+ }
156
+ call.resolve()
157
+ }
158
+
159
+ @PluginMethod
160
+ fun isEnabled(call: PluginCall) {
161
+ assertBluetoothAdapter(call) ?: return
162
+ val enabled = bluetoothAdapter?.isEnabled == true
163
+ val result = JSObject()
164
+ result.put("value", enabled)
165
+ call.resolve(result)
166
+ }
167
+
168
+ @PluginMethod
169
+ fun requestEnable(call: PluginCall) {
170
+ assertBluetoothAdapter(call) ?: return
171
+ val intent = Intent(ACTION_REQUEST_ENABLE)
172
+ startActivityForResult(call, intent, "handleRequestEnableResult")
173
+ }
174
+
175
+ @ActivityCallback
176
+ private fun handleRequestEnableResult(call: PluginCall, result: ActivityResult) {
177
+ if (result.resultCode == Activity.RESULT_OK) {
178
+ call.resolve()
179
+ } else {
180
+ call.reject("requestEnable failed.")
181
+ }
182
+ }
183
+
184
+ @PluginMethod
185
+ fun enable(call: PluginCall) {
186
+ assertBluetoothAdapter(call) ?: return
187
+ val result = bluetoothAdapter?.enable()
188
+ if (result != true) {
189
+ call.reject("Enable failed.")
190
+ return
191
+ }
192
+ call.resolve()
193
+ }
194
+
195
+ @PluginMethod
196
+ fun disable(call: PluginCall) {
197
+ assertBluetoothAdapter(call) ?: return
198
+ val result = bluetoothAdapter?.disable()
199
+ if (result != true) {
200
+ call.reject("Disable failed.")
201
+ return
202
+ }
203
+ call.resolve()
204
+ }
205
+
206
+ @PluginMethod
207
+ fun startEnabledNotifications(call: PluginCall) {
208
+ assertBluetoothAdapter(call) ?: return
209
+
210
+ try {
211
+ createStateReceiver()
212
+ } catch (e: Error) {
213
+ Logger.error(
214
+ TAG, "Error while registering enabled state receiver: ${e.localizedMessage}", e
215
+ )
216
+ call.reject("startEnabledNotifications failed.")
217
+ return
218
+ }
219
+ call.resolve()
220
+ }
221
+
222
+ private fun createStateReceiver() {
223
+ if (stateReceiver == null) {
224
+ stateReceiver = object : BroadcastReceiver() {
225
+ override fun onReceive(context: Context, intent: Intent) {
226
+ val action = intent.action
227
+ if (action == BluetoothAdapter.ACTION_STATE_CHANGED) {
228
+ val state = intent.getIntExtra(
229
+ BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR
230
+ )
231
+ val enabled = state == BluetoothAdapter.STATE_ON
232
+ val result = JSObject()
233
+ result.put("value", enabled)
234
+ try {
235
+ notifyListeners("onEnabledChanged", result)
236
+ } catch (e: ConcurrentModificationException) {
237
+ Logger.error(TAG, "Error in notifyListeners: ${e.localizedMessage}", e)
238
+ }
239
+ }
240
+ }
241
+ }
242
+ val intentFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
243
+ context.registerReceiver(stateReceiver, intentFilter)
244
+ }
245
+ }
246
+
247
+ @PluginMethod
248
+ fun stopEnabledNotifications(call: PluginCall) {
249
+ if (stateReceiver != null) {
250
+ context.unregisterReceiver(stateReceiver)
251
+ }
252
+ stateReceiver = null
253
+ call.resolve()
254
+ }
255
+
256
+ @PluginMethod
257
+ fun isLocationEnabled(call: PluginCall) {
258
+ val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
259
+ val enabled = LocationManagerCompat.isLocationEnabled(lm)
260
+ Logger.debug(TAG, "location $enabled")
261
+ val result = JSObject()
262
+ result.put("value", enabled)
263
+ call.resolve(result)
264
+ }
265
+
266
+ @PluginMethod
267
+ fun openLocationSettings(call: PluginCall) {
268
+ val intent = Intent(ACTION_LOCATION_SOURCE_SETTINGS)
269
+ activity.startActivity(intent)
270
+ call.resolve()
271
+ }
272
+
273
+ @PluginMethod
274
+ fun openBluetoothSettings(call: PluginCall) {
275
+ val intent = Intent(ACTION_BLUETOOTH_SETTINGS)
276
+ activity.startActivity(intent)
277
+ call.resolve()
278
+ }
279
+
280
+ @PluginMethod
281
+ fun openAppSettings(call: PluginCall) {
282
+ val intent = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
283
+ intent.data = Uri.parse("package:" + activity.packageName)
284
+ activity.startActivity(intent)
285
+ call.resolve()
286
+ }
287
+
288
+ @PluginMethod
289
+ fun setDisplayStrings(call: PluginCall) {
290
+ displayStrings = DisplayStrings(
291
+ call.getString(
292
+ "scanning", displayStrings!!.scanning
293
+ ) as String,
294
+ call.getString(
295
+ "cancel", displayStrings!!.cancel
296
+ ) as String,
297
+ call.getString(
298
+ "availableDevices", displayStrings!!.availableDevices
299
+ ) as String,
300
+ call.getString(
301
+ "noDeviceFound", displayStrings!!.noDeviceFound
302
+ ) as String,
303
+ )
304
+ call.resolve()
305
+ }
306
+
307
+ @PluginMethod
308
+ fun requestDevice(call: PluginCall) {
309
+ assertBluetoothAdapter(call) ?: return
310
+ val scanFilters = getScanFilters(call) ?: return
311
+ val scanSettings = getScanSettings(call) ?: return
312
+ val namePrefix = call.getString("namePrefix", "") as String
313
+
314
+ try {
315
+ deviceScanner?.stopScanning()
316
+ } catch (e: IllegalStateException) {
317
+ Logger.error(TAG, "Error in requestDevice: ${e.localizedMessage}", e)
318
+ call.reject(e.localizedMessage)
319
+ return
320
+ }
321
+
322
+ deviceScanner = DeviceScanner(
323
+ context,
324
+ bluetoothAdapter!!,
325
+ scanDuration = MAX_SCAN_DURATION,
326
+ displayStrings = displayStrings!!,
327
+ showDialog = true,
328
+ )
329
+ deviceScanner?.startScanning(
330
+ scanFilters, scanSettings, false, namePrefix, { scanResponse ->
331
+ run {
332
+ if (scanResponse.success) {
333
+ if (scanResponse.device == null) {
334
+ call.reject("No device found.")
335
+ } else {
336
+ val bleDevice = getBleDevice(scanResponse.device)
337
+ call.resolve(bleDevice)
338
+ }
339
+ } else {
340
+ call.reject(scanResponse.message)
341
+
342
+ }
343
+ }
344
+ }, null
345
+ )
346
+ }
347
+
348
+ @PluginMethod
349
+ fun requestLEScan(call: PluginCall) {
350
+ assertBluetoothAdapter(call) ?: return
351
+ val scanFilters = getScanFilters(call) ?: return
352
+ val scanSettings = getScanSettings(call) ?: return
353
+ val namePrefix = call.getString("namePrefix", "") as String
354
+ val allowDuplicates = call.getBoolean("allowDuplicates", false) as Boolean
355
+
356
+ try {
357
+ deviceScanner?.stopScanning()
358
+ } catch (e: IllegalStateException) {
359
+ Logger.error(TAG, "Error in requestLEScan: ${e.localizedMessage}", e)
360
+ call.reject(e.localizedMessage)
361
+ return
362
+ }
363
+
364
+ deviceScanner = DeviceScanner(
365
+ context,
366
+ bluetoothAdapter!!,
367
+ scanDuration = null,
368
+ displayStrings = displayStrings!!,
369
+ showDialog = false,
370
+ )
371
+ deviceScanner?.startScanning(
372
+ scanFilters,
373
+ scanSettings,
374
+ allowDuplicates,
375
+ namePrefix,
376
+ { scanResponse ->
377
+ run {
378
+ if (scanResponse.success) {
379
+ call.resolve()
380
+ } else {
381
+ call.reject(scanResponse.message)
382
+ }
383
+ }
384
+ },
385
+ { result ->
386
+ run {
387
+ val scanResult = getScanResult(result)
388
+ try {
389
+ notifyListeners("onScanResult", scanResult)
390
+ } catch (e: ConcurrentModificationException) {
391
+ Logger.error(TAG, "Error in notifyListeners: ${e.localizedMessage}", e)
392
+ }
393
+ }
394
+ })
395
+ }
396
+
397
+ @PluginMethod
398
+ fun stopLEScan(call: PluginCall) {
399
+ assertBluetoothAdapter(call) ?: return
400
+ try {
401
+ deviceScanner?.stopScanning()
402
+ } catch (e: IllegalStateException) {
403
+ Logger.error(TAG, "Error in stopLEScan: ${e.localizedMessage}", e)
404
+ }
405
+ call.resolve()
406
+ }
407
+
408
+ @PluginMethod
409
+ fun getDevices(call: PluginCall) {
410
+ assertBluetoothAdapter(call) ?: return
411
+ val deviceIds = (call.getArray("deviceIds", JSArray()) as JSArray).toList<String>()
412
+ val bleDevices = JSArray()
413
+ deviceIds.forEach { deviceId ->
414
+ val bleDevice = JSObject()
415
+ bleDevice.put("deviceId", deviceId)
416
+ bleDevices.put(bleDevice)
417
+ }
418
+ val result = JSObject()
419
+ result.put("devices", bleDevices)
420
+ call.resolve(result)
421
+ }
422
+
423
+ @PluginMethod
424
+ fun getConnectedDevices(call: PluginCall) {
425
+ assertBluetoothAdapter(call) ?: return
426
+ val bluetoothManager =
427
+ (activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager)
428
+ val devices = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT)
429
+ val bleDevices = JSArray()
430
+ devices.forEach { device ->
431
+ bleDevices.put(getBleDevice(device))
432
+ }
433
+ val result = JSObject()
434
+ result.put("devices", bleDevices)
435
+ call.resolve(result)
436
+ }
437
+
438
+ @PluginMethod
439
+ fun getBondedDevices(call: PluginCall) {
440
+ assertBluetoothAdapter(call) ?: return
441
+
442
+ val bluetoothManager = activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
443
+ val bluetoothAdapter = bluetoothManager.adapter
444
+
445
+ if (bluetoothAdapter == null) {
446
+ call.reject("Bluetooth is not supported on this device")
447
+ return
448
+ }
449
+
450
+ val bondedDevices = bluetoothAdapter.bondedDevices
451
+ val bleDevices = JSArray()
452
+
453
+ bondedDevices.forEach { device ->
454
+ bleDevices.put(getBleDevice(device))
455
+ }
456
+
457
+ val result = JSObject()
458
+ result.put("devices", bleDevices)
459
+ call.resolve(result)
460
+ }
461
+
462
+ @PluginMethod
463
+ fun connect(call: PluginCall) {
464
+ val device = getOrCreateDevice(call) ?: return
465
+ val timeout = call.getFloat("timeout", CONNECTION_TIMEOUT)!!.toLong()
466
+ device.connect(timeout) { response ->
467
+ run {
468
+ if (response.success) {
469
+ call.resolve()
470
+ } else {
471
+ call.reject(response.value)
472
+ }
473
+ }
474
+ }
475
+ }
476
+
477
+ private fun onDisconnect(deviceId: String) {
478
+ try {
479
+ notifyListeners("disconnected|${deviceId}", null)
480
+ } catch (e: ConcurrentModificationException) {
481
+ Logger.error(TAG, "Error in notifyListeners: ${e.localizedMessage}", e)
482
+ }
483
+ }
484
+
485
+ @PluginMethod
486
+ fun createBond(call: PluginCall) {
487
+ val device = getOrCreateDevice(call) ?: return
488
+ val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
489
+ device.createBond(timeout) { response ->
490
+ run {
491
+ if (response.success) {
492
+ call.resolve()
493
+ } else {
494
+ call.reject(response.value)
495
+ }
496
+ }
497
+ }
498
+ }
499
+
500
+ @PluginMethod
501
+ fun isBonded(call: PluginCall) {
502
+ val device = getOrCreateDevice(call) ?: return
503
+ val isBonded = device.isBonded()
504
+ val result = JSObject()
505
+ result.put("value", isBonded)
506
+ call.resolve(result)
507
+ }
508
+
509
+ @PluginMethod
510
+ fun disconnect(call: PluginCall) {
511
+ val device = getOrCreateDevice(call) ?: return
512
+ val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
513
+ device.disconnect(timeout) { response ->
514
+ run {
515
+ if (response.success) {
516
+ deviceMap.remove(device.getId())
517
+ call.resolve()
518
+ } else {
519
+ call.reject(response.value)
520
+ }
521
+ }
522
+ }
523
+ }
524
+
525
+ @PluginMethod
526
+ fun getServices(call: PluginCall) {
527
+ val device = getDevice(call) ?: return
528
+ val services = device.getServices()
529
+ val bleServices = JSArray()
530
+ services.forEach { service ->
531
+ val bleCharacteristics = JSArray()
532
+ service.characteristics.forEach { characteristic ->
533
+ val bleCharacteristic = JSObject()
534
+ bleCharacteristic.put("uuid", characteristic.uuid)
535
+ bleCharacteristic.put("properties", getProperties(characteristic))
536
+ val bleDescriptors = JSArray()
537
+ characteristic.descriptors.forEach { descriptor ->
538
+ val bleDescriptor = JSObject()
539
+ bleDescriptor.put("uuid", descriptor.uuid)
540
+ bleDescriptors.put(bleDescriptor)
541
+ }
542
+ bleCharacteristic.put("descriptors", bleDescriptors)
543
+ bleCharacteristics.put(bleCharacteristic)
544
+ }
545
+ val bleService = JSObject()
546
+ bleService.put("uuid", service.uuid)
547
+ bleService.put("characteristics", bleCharacteristics)
548
+ bleServices.put(bleService)
549
+ }
550
+ val ret = JSObject()
551
+ ret.put("services", bleServices)
552
+ call.resolve(ret)
553
+ }
554
+
555
+ private fun getProperties(characteristic: BluetoothGattCharacteristic): JSObject {
556
+ val properties = JSObject()
557
+ properties.put(
558
+ "broadcast",
559
+ characteristic.properties and BluetoothGattCharacteristic.PROPERTY_BROADCAST > 0
560
+ )
561
+ properties.put(
562
+ "read", characteristic.properties and BluetoothGattCharacteristic.PROPERTY_READ > 0
563
+ )
564
+ properties.put(
565
+ "writeWithoutResponse",
566
+ characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE > 0
567
+ )
568
+ properties.put(
569
+ "write", characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE > 0
570
+ )
571
+ properties.put(
572
+ "notify", characteristic.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0
573
+ )
574
+ properties.put(
575
+ "indicate",
576
+ characteristic.properties and BluetoothGattCharacteristic.PROPERTY_INDICATE > 0
577
+ )
578
+ properties.put(
579
+ "authenticatedSignedWrites",
580
+ characteristic.properties and BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE > 0
581
+ )
582
+ properties.put(
583
+ "extendedProperties",
584
+ characteristic.properties and BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS > 0
585
+ )
586
+ return properties
587
+ }
588
+
589
+ @PluginMethod
590
+ fun discoverServices(call: PluginCall) {
591
+ val device = getDevice(call) ?: return
592
+ val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
593
+ device.discoverServices(timeout) { response ->
594
+ run {
595
+ if (response.success) {
596
+ call.resolve()
597
+ } else {
598
+ call.reject(response.value)
599
+ }
600
+ }
601
+ }
602
+ }
603
+
604
+ @PluginMethod
605
+ fun getMtu(call: PluginCall) {
606
+ val device = getDevice(call) ?: return
607
+ val mtu = device.getMtu()
608
+ val ret = JSObject()
609
+ ret.put("value", mtu)
610
+ call.resolve(ret)
611
+ }
612
+
613
+ @PluginMethod
614
+ fun requestConnectionPriority(call: PluginCall) {
615
+ val device = getDevice(call) ?: return
616
+ val connectionPriority = call.getInt("connectionPriority", -1) as Int
617
+ if (connectionPriority < BluetoothGatt.CONNECTION_PRIORITY_BALANCED || connectionPriority > BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER) {
618
+ call.reject("Invalid connectionPriority.")
619
+ return
620
+ }
621
+
622
+ val result = device.requestConnectionPriority(connectionPriority)
623
+ if (result) {
624
+ call.resolve()
625
+ } else {
626
+ call.reject("requestConnectionPriority failed.")
627
+ }
628
+ }
629
+
630
+ @PluginMethod
631
+ fun readRssi(call: PluginCall) {
632
+ val device = getDevice(call) ?: return
633
+ val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
634
+ device.readRssi(timeout) { response ->
635
+ run {
636
+ if (response.success) {
637
+ val ret = JSObject()
638
+ ret.put("value", response.value)
639
+ call.resolve(ret)
640
+ } else {
641
+ call.reject(response.value)
642
+ }
643
+ }
644
+ }
645
+ }
646
+
647
+ @PluginMethod
648
+ fun read(call: PluginCall) {
649
+ val device = getDevice(call) ?: return
650
+ val characteristic = getCharacteristic(call) ?: return
651
+ val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
652
+ device.read(characteristic.first, characteristic.second, timeout) { response ->
653
+ run {
654
+ if (response.success) {
655
+ val ret = JSObject()
656
+ ret.put("value", response.value)
657
+ call.resolve(ret)
658
+ } else {
659
+ call.reject(response.value)
660
+ }
661
+ }
662
+ }
663
+ }
664
+
665
+ @PluginMethod
666
+ fun write(call: PluginCall) {
667
+ val device = getDevice(call) ?: return
668
+ val characteristic = getCharacteristic(call) ?: return
669
+ val value = call.getString("value", null)
670
+ if (value == null) {
671
+ call.reject("Value required.")
672
+ return
673
+ }
674
+ val writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
675
+ val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
676
+ device.write(
677
+ characteristic.first, characteristic.second, value, writeType, timeout
678
+ ) { response ->
679
+ run {
680
+ if (response.success) {
681
+ call.resolve()
682
+ } else {
683
+ call.reject(response.value)
684
+ }
685
+ }
686
+ }
687
+ }
688
+
689
+ @PluginMethod
690
+ fun writeWithoutResponse(call: PluginCall) {
691
+ val device = getDevice(call) ?: return
692
+ val characteristic = getCharacteristic(call) ?: return
693
+ val value = call.getString("value", null)
694
+ if (value == null) {
695
+ call.reject("Value required.")
696
+ return
697
+ }
698
+ val writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
699
+ val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
700
+ device.write(
701
+ characteristic.first, characteristic.second, value, writeType, timeout
702
+ ) { response ->
703
+ run {
704
+ if (response.success) {
705
+ call.resolve()
706
+ } else {
707
+ call.reject(response.value)
708
+ }
709
+ }
710
+ }
711
+ }
712
+
713
+ @PluginMethod
714
+ fun readDescriptor(call: PluginCall) {
715
+ val device = getDevice(call) ?: return
716
+ val descriptor = getDescriptor(call) ?: return
717
+ val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
718
+ device.readDescriptor(
719
+ descriptor.first, descriptor.second, descriptor.third, timeout
720
+ ) { response ->
721
+ run {
722
+ if (response.success) {
723
+ val ret = JSObject()
724
+ ret.put("value", response.value)
725
+ call.resolve(ret)
726
+ } else {
727
+ call.reject(response.value)
728
+ }
729
+ }
730
+ }
731
+ }
732
+
733
+ @PluginMethod
734
+ fun writeDescriptor(call: PluginCall) {
735
+ val device = getDevice(call) ?: return
736
+ val descriptor = getDescriptor(call) ?: return
737
+ val value = call.getString("value", null)
738
+ if (value == null) {
739
+ call.reject("Value required.")
740
+ return
741
+ }
742
+ val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
743
+ device.writeDescriptor(
744
+ descriptor.first, descriptor.second, descriptor.third, value, timeout
745
+ ) { response ->
746
+ run {
747
+ if (response.success) {
748
+ call.resolve()
749
+ } else {
750
+ call.reject(response.value)
751
+ }
752
+ }
753
+ }
754
+ }
755
+
756
+ @PluginMethod
757
+ fun startNotifications(call: PluginCall) {
758
+ val device = getDevice(call) ?: return
759
+ val characteristic = getCharacteristic(call) ?: return
760
+ device.setNotifications(characteristic.first, characteristic.second, true, { response ->
761
+ run {
762
+ val key =
763
+ "notification|${device.getId()}|${(characteristic.first)}|${(characteristic.second)}"
764
+ val ret = JSObject()
765
+ ret.put("value", response.value)
766
+ try {
767
+ notifyListeners(key, ret)
768
+ } catch (e: ConcurrentModificationException) {
769
+ Logger.error(TAG, "Error in notifyListeners: ${e.localizedMessage}", e)
770
+ }
771
+ }
772
+ }, { response ->
773
+ run {
774
+ if (response.success) {
775
+ call.resolve()
776
+ } else {
777
+ call.reject(response.value)
778
+ }
779
+ }
780
+ })
781
+ }
782
+
783
+ @PluginMethod
784
+ fun stopNotifications(call: PluginCall) {
785
+ val device = getDevice(call) ?: return
786
+ val characteristic = getCharacteristic(call) ?: return
787
+ device.setNotifications(
788
+ characteristic.first, characteristic.second, false, null
789
+ ) { response ->
790
+ run {
791
+ if (response.success) {
792
+ call.resolve()
793
+ } else {
794
+ call.reject(response.value)
795
+ }
796
+ }
797
+ }
798
+ }
799
+
800
+ private fun assertBluetoothAdapter(call: PluginCall): Boolean? {
801
+ if (bluetoothAdapter == null) {
802
+ call.reject("Bluetooth LE not initialized.")
803
+ return null
804
+ }
805
+ return true
806
+ }
807
+
808
+ private fun getScanFilters(call: PluginCall): List<ScanFilter>? {
809
+ val filters: ArrayList<ScanFilter> = ArrayList()
810
+
811
+ val services = (call.getArray("services", JSArray()) as JSArray).toList<String>()
812
+ val manufacturerDataArray = call.getArray("manufacturerData", JSArray())
813
+ val name = call.getString("name", null)
814
+
815
+ try {
816
+ // Create filters based on services
817
+ for (service in services) {
818
+ val filter = ScanFilter.Builder()
819
+ filter.setServiceUuid(ParcelUuid.fromString(service))
820
+ if (name != null) {
821
+ filter.setDeviceName(name)
822
+ }
823
+ filters.add(filter.build())
824
+ }
825
+
826
+ // Manufacturer Data Handling (with optional parameters)
827
+ manufacturerDataArray?.let {
828
+ for (i in 0 until it.length()) {
829
+ val manufacturerDataObject = it.getJSONObject(i)
830
+
831
+ val companyIdentifier = manufacturerDataObject.getInt("companyIdentifier")
832
+
833
+ val dataPrefix = if (manufacturerDataObject.has("dataPrefix")) {
834
+ val dataPrefixObject = manufacturerDataObject.getJSONObject("dataPrefix")
835
+ val byteLength = dataPrefixObject.length()
836
+
837
+ ByteArray(byteLength).apply {
838
+ for (idx in 0 until byteLength) {
839
+ val key = idx.toString()
840
+ this[idx] = (dataPrefixObject.getInt(key) and 0xFF).toByte()
841
+ }
842
+ }
843
+ } else null
844
+
845
+
846
+ val mask = if (manufacturerDataObject.has("mask")) {
847
+ val maskObject = manufacturerDataObject.getJSONObject("mask")
848
+ val byteLength = maskObject.length()
849
+
850
+ ByteArray(byteLength).apply {
851
+ for (idx in 0 until byteLength) {
852
+ val key = idx.toString()
853
+ this[idx] = (maskObject.getInt(key) and 0xFF).toByte()
854
+ }
855
+ }
856
+ } else null
857
+
858
+ val filterBuilder = ScanFilter.Builder()
859
+
860
+ if (dataPrefix != null && mask != null) {
861
+ filterBuilder.setManufacturerData(companyIdentifier, dataPrefix, mask)
862
+ } else if (dataPrefix != null) {
863
+ filterBuilder.setManufacturerData(companyIdentifier, dataPrefix)
864
+ } else {
865
+ // Android requires at least dataPrefix for manufacturer filters.
866
+ call.reject("dataPrefix is required when specifying manufacturerData.")
867
+ return null
868
+ }
869
+
870
+ if (name != null) {
871
+ filterBuilder.setDeviceName(name)
872
+ }
873
+
874
+ filters.add(filterBuilder.build())
875
+ }
876
+ }
877
+ return filters;
878
+ } catch (e: IllegalArgumentException) {
879
+ call.reject("Invalid UUID or Manufacturer data provided.")
880
+ return null
881
+ } catch (e: Exception) {
882
+ call.reject("Invalid or malformed filter data provided.")
883
+ return null
884
+ }
885
+ }
886
+
887
+ private fun getScanSettings(call: PluginCall): ScanSettings? {
888
+ val scanSettings = ScanSettings.Builder()
889
+ val scanMode = call.getInt("scanMode", ScanSettings.SCAN_MODE_BALANCED) as Int
890
+ try {
891
+ scanSettings.setScanMode(scanMode)
892
+ } catch (e: IllegalArgumentException) {
893
+ call.reject("Invalid scan mode.")
894
+ return null
895
+ }
896
+ return scanSettings.build()
897
+ }
898
+
899
+ private fun getBleDevice(device: BluetoothDevice): JSObject {
900
+ val bleDevice = JSObject()
901
+ bleDevice.put("deviceId", device.address)
902
+ if (device.name != null) {
903
+ bleDevice.put("name", device.name)
904
+ }
905
+
906
+ val uuids = JSArray()
907
+ device.uuids?.forEach { uuid -> uuids.put(uuid.toString()) }
908
+ if (uuids.length() > 0) {
909
+ bleDevice.put("uuids", uuids)
910
+ }
911
+
912
+ return bleDevice
913
+ }
914
+
915
+ private fun getScanResult(result: ScanResult): JSObject {
916
+ val scanResult = JSObject()
917
+
918
+ val bleDevice = getBleDevice(result.device)
919
+ scanResult.put("device", bleDevice)
920
+
921
+ if (result.device.name != null) {
922
+ scanResult.put("localName", result.device.name)
923
+ }
924
+
925
+ scanResult.put("rssi", result.rssi)
926
+
927
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
928
+ scanResult.put("txPower", result.txPower)
929
+ } else {
930
+ scanResult.put("txPower", 127)
931
+ }
932
+
933
+ val manufacturerData = JSObject()
934
+ val manufacturerSpecificData = result.scanRecord?.manufacturerSpecificData
935
+ if (manufacturerSpecificData != null) {
936
+ for (i in 0 until manufacturerSpecificData.size()) {
937
+ val key = manufacturerSpecificData.keyAt(i)
938
+ val bytes = manufacturerSpecificData.get(key)
939
+ manufacturerData.put(key.toString(), bytesToString(bytes))
940
+ }
941
+ }
942
+ scanResult.put("manufacturerData", manufacturerData)
943
+
944
+ val serviceDataObject = JSObject()
945
+ val serviceData = result.scanRecord?.serviceData
946
+ serviceData?.forEach {
947
+ serviceDataObject.put(it.key.toString(), bytesToString(it.value))
948
+ }
949
+ scanResult.put("serviceData", serviceDataObject)
950
+
951
+ val uuids = JSArray()
952
+ result.scanRecord?.serviceUuids?.forEach { uuid -> uuids.put(uuid.toString()) }
953
+ scanResult.put("uuids", uuids)
954
+
955
+ scanResult.put("rawAdvertisement", result.scanRecord?.bytes?.let { bytesToString(it) })
956
+ return scanResult
957
+ }
958
+
959
+ private fun getDisplayStrings(): DisplayStrings {
960
+ return DisplayStrings(
961
+ config.getString(
962
+ "displayStrings.scanning", "Scanning..."
963
+ ),
964
+ config.getString(
965
+ "displayStrings.cancel", "Cancel"
966
+ ),
967
+ config.getString(
968
+ "displayStrings.availableDevices", "Available devices"
969
+ ),
970
+ config.getString(
971
+ "displayStrings.noDeviceFound", "No device found"
972
+ ),
973
+ )
974
+ }
975
+
976
+ private fun getDeviceId(call: PluginCall): String? {
977
+ val deviceId = call.getString("deviceId", null)
978
+ if (deviceId == null) {
979
+ call.reject("deviceId required.")
980
+ return null
981
+ }
982
+ return deviceId
983
+ }
984
+
985
+ private fun getOrCreateDevice(call: PluginCall): Device? {
986
+ assertBluetoothAdapter(call) ?: return null
987
+ val deviceId = getDeviceId(call) ?: return null
988
+ val device = deviceMap[deviceId]
989
+ if (device != null) {
990
+ return device
991
+ }
992
+ return try {
993
+ val newDevice = Device(
994
+ activity.applicationContext, bluetoothAdapter!!, deviceId
995
+ ) {
996
+ onDisconnect(deviceId)
997
+ }
998
+ deviceMap[deviceId] = newDevice
999
+ newDevice
1000
+ } catch (e: IllegalArgumentException) {
1001
+ call.reject("Invalid deviceId")
1002
+ null
1003
+ }
1004
+ }
1005
+
1006
+ private fun getDevice(call: PluginCall): Device? {
1007
+ assertBluetoothAdapter(call) ?: return null
1008
+ val deviceId = getDeviceId(call) ?: return null
1009
+ val device = deviceMap[deviceId]
1010
+ if (device == null || !device.isConnected()) {
1011
+ call.reject("Not connected to device.")
1012
+ return null
1013
+ }
1014
+ return device
1015
+ }
1016
+
1017
+ private fun getCharacteristic(call: PluginCall): Pair<UUID, UUID>? {
1018
+ val serviceString = call.getString("service", null)
1019
+ val serviceUUID: UUID?
1020
+ try {
1021
+ serviceUUID = UUID.fromString(serviceString)
1022
+ } catch (e: IllegalArgumentException) {
1023
+ call.reject("Invalid service UUID.")
1024
+ return null
1025
+ }
1026
+ if (serviceUUID == null) {
1027
+ call.reject("Service UUID required.")
1028
+ return null
1029
+ }
1030
+ val characteristicString = call.getString("characteristic", null)
1031
+ val characteristicUUID: UUID?
1032
+ try {
1033
+ characteristicUUID = UUID.fromString(characteristicString)
1034
+ } catch (e: IllegalArgumentException) {
1035
+ call.reject("Invalid characteristic UUID.")
1036
+ return null
1037
+ }
1038
+ if (characteristicUUID == null) {
1039
+ call.reject("Characteristic UUID required.")
1040
+ return null
1041
+ }
1042
+ return Pair(serviceUUID, characteristicUUID)
1043
+ }
1044
+
1045
+ private fun getDescriptor(call: PluginCall): Triple<UUID, UUID, UUID>? {
1046
+ val characteristic = getCharacteristic(call) ?: return null
1047
+ val descriptorString = call.getString("descriptor", null)
1048
+ val descriptorUUID: UUID?
1049
+ try {
1050
+ descriptorUUID = UUID.fromString(descriptorString)
1051
+ } catch (e: IllegalAccessException) {
1052
+ call.reject("Invalid descriptor UUID.")
1053
+ return null
1054
+ }
1055
+ if (descriptorUUID == null) {
1056
+ call.reject("Descriptor UUID required.")
1057
+ return null
1058
+ }
1059
+ return Triple(characteristic.first, characteristic.second, descriptorUUID)
1060
+ }
1061
+ }