@capacitor-community/bluetooth-le 8.0.1 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CapacitorCommunityBluetoothLe.podspec +17 -17
  2. package/LICENSE +21 -21
  3. package/Package.swift +27 -27
  4. package/README.md +2 -1
  5. package/android/build.gradle +73 -73
  6. package/android/src/main/AndroidManifest.xml +22 -22
  7. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/BluetoothLe.kt +1094 -1094
  8. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Conversion.kt +51 -51
  9. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt +771 -771
  10. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/DeviceList.kt +28 -28
  11. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/DeviceScanner.kt +189 -189
  12. package/dist/docs.json +43 -43
  13. package/dist/esm/bleClient.d.ts +278 -278
  14. package/dist/esm/bleClient.js +361 -361
  15. package/dist/esm/bleClient.js.map +1 -1
  16. package/dist/esm/config.d.ts +53 -53
  17. package/dist/esm/config.js +2 -2
  18. package/dist/esm/conversion.d.ts +56 -56
  19. package/dist/esm/conversion.js +134 -134
  20. package/dist/esm/conversion.js.map +1 -1
  21. package/dist/esm/definitions.d.ts +352 -352
  22. package/dist/esm/definitions.js +42 -42
  23. package/dist/esm/definitions.js.map +1 -1
  24. package/dist/esm/index.d.ts +5 -5
  25. package/dist/esm/index.js +5 -5
  26. package/dist/esm/plugin.d.ts +2 -2
  27. package/dist/esm/plugin.js +4 -4
  28. package/dist/esm/queue.d.ts +3 -3
  29. package/dist/esm/queue.js +17 -17
  30. package/dist/esm/timeout.d.ts +1 -1
  31. package/dist/esm/timeout.js +9 -9
  32. package/dist/esm/validators.d.ts +1 -1
  33. package/dist/esm/validators.js +11 -11
  34. package/dist/esm/web.d.ts +57 -57
  35. package/dist/esm/web.js +403 -403
  36. package/dist/esm/web.js.map +1 -1
  37. package/dist/plugin.cjs.js +964 -964
  38. package/dist/plugin.cjs.js.map +1 -1
  39. package/dist/plugin.js +964 -964
  40. package/dist/plugin.js.map +1 -1
  41. package/ios/Sources/BluetoothLe/Conversion.swift +83 -83
  42. package/ios/Sources/BluetoothLe/Device.swift +422 -423
  43. package/ios/Sources/BluetoothLe/DeviceListView.swift +121 -121
  44. package/ios/Sources/BluetoothLe/DeviceManager.swift +409 -415
  45. package/ios/Sources/BluetoothLe/Logging.swift +8 -8
  46. package/ios/Sources/BluetoothLe/Plugin.swift +768 -763
  47. package/ios/Sources/BluetoothLe/ScanFilters.swift +114 -114
  48. package/ios/Sources/BluetoothLe/ThreadSafeDictionary.swift +61 -15
  49. package/ios/Tests/BluetoothLeTests/ConversionTests.swift +55 -55
  50. package/ios/Tests/BluetoothLeTests/PluginTests.swift +27 -27
  51. package/ios/Tests/BluetoothLeTests/ScanFiltersTests.swift +153 -153
  52. package/package.json +114 -114
@@ -1,1094 +1,1094 @@
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
- device.cleanup()
517
- deviceMap.remove(device.getId())
518
- call.resolve()
519
- } else {
520
- call.reject(response.value)
521
- }
522
- }
523
- }
524
- }
525
-
526
- @PluginMethod
527
- fun getServices(call: PluginCall) {
528
- val device = getDevice(call) ?: return
529
- val services = device.getServices()
530
- val bleServices = JSArray()
531
- services.forEach { service ->
532
- val bleCharacteristics = JSArray()
533
- service.characteristics.forEach { characteristic ->
534
- val bleCharacteristic = JSObject()
535
- bleCharacteristic.put("uuid", characteristic.uuid)
536
- bleCharacteristic.put("properties", getProperties(characteristic))
537
- val bleDescriptors = JSArray()
538
- characteristic.descriptors.forEach { descriptor ->
539
- val bleDescriptor = JSObject()
540
- bleDescriptor.put("uuid", descriptor.uuid)
541
- bleDescriptors.put(bleDescriptor)
542
- }
543
- bleCharacteristic.put("descriptors", bleDescriptors)
544
- bleCharacteristics.put(bleCharacteristic)
545
- }
546
- val bleService = JSObject()
547
- bleService.put("uuid", service.uuid)
548
- bleService.put("characteristics", bleCharacteristics)
549
- bleServices.put(bleService)
550
- }
551
- val ret = JSObject()
552
- ret.put("services", bleServices)
553
- call.resolve(ret)
554
- }
555
-
556
- private fun getProperties(characteristic: BluetoothGattCharacteristic): JSObject {
557
- val properties = JSObject()
558
- properties.put(
559
- "broadcast",
560
- characteristic.properties and BluetoothGattCharacteristic.PROPERTY_BROADCAST > 0
561
- )
562
- properties.put(
563
- "read", characteristic.properties and BluetoothGattCharacteristic.PROPERTY_READ > 0
564
- )
565
- properties.put(
566
- "writeWithoutResponse",
567
- characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE > 0
568
- )
569
- properties.put(
570
- "write", characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE > 0
571
- )
572
- properties.put(
573
- "notify", characteristic.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0
574
- )
575
- properties.put(
576
- "indicate",
577
- characteristic.properties and BluetoothGattCharacteristic.PROPERTY_INDICATE > 0
578
- )
579
- properties.put(
580
- "authenticatedSignedWrites",
581
- characteristic.properties and BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE > 0
582
- )
583
- properties.put(
584
- "extendedProperties",
585
- characteristic.properties and BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS > 0
586
- )
587
- return properties
588
- }
589
-
590
- @PluginMethod
591
- fun discoverServices(call: PluginCall) {
592
- val device = getDevice(call) ?: return
593
- val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
594
- device.discoverServices(timeout) { response ->
595
- run {
596
- if (response.success) {
597
- call.resolve()
598
- } else {
599
- call.reject(response.value)
600
- }
601
- }
602
- }
603
- }
604
-
605
- @PluginMethod
606
- fun getMtu(call: PluginCall) {
607
- val device = getDevice(call) ?: return
608
- val mtu = device.getMtu()
609
- val ret = JSObject()
610
- ret.put("value", mtu)
611
- call.resolve(ret)
612
- }
613
-
614
- @PluginMethod
615
- fun requestConnectionPriority(call: PluginCall) {
616
- val device = getDevice(call) ?: return
617
- val connectionPriority = call.getInt("connectionPriority", -1) as Int
618
- if (connectionPriority < BluetoothGatt.CONNECTION_PRIORITY_BALANCED || connectionPriority > BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER) {
619
- call.reject("Invalid connectionPriority.")
620
- return
621
- }
622
-
623
- val result = device.requestConnectionPriority(connectionPriority)
624
- if (result) {
625
- call.resolve()
626
- } else {
627
- call.reject("requestConnectionPriority failed.")
628
- }
629
- }
630
-
631
- @PluginMethod
632
- fun readRssi(call: PluginCall) {
633
- val device = getDevice(call) ?: return
634
- val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
635
- device.readRssi(timeout) { response ->
636
- run {
637
- if (response.success) {
638
- val ret = JSObject()
639
- ret.put("value", response.value)
640
- call.resolve(ret)
641
- } else {
642
- call.reject(response.value)
643
- }
644
- }
645
- }
646
- }
647
-
648
- @PluginMethod
649
- fun read(call: PluginCall) {
650
- val device = getDevice(call) ?: return
651
- val characteristic = getCharacteristic(call) ?: return
652
- val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
653
- device.read(characteristic.first, characteristic.second, timeout) { response ->
654
- run {
655
- if (response.success) {
656
- val ret = JSObject()
657
- ret.put("value", response.value)
658
- call.resolve(ret)
659
- } else {
660
- call.reject(response.value)
661
- }
662
- }
663
- }
664
- }
665
-
666
- @PluginMethod
667
- fun write(call: PluginCall) {
668
- val device = getDevice(call) ?: return
669
- val characteristic = getCharacteristic(call) ?: return
670
- val value = call.getString("value", null)
671
- if (value == null) {
672
- call.reject("Value required.")
673
- return
674
- }
675
- val writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
676
- val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
677
- device.write(
678
- characteristic.first, characteristic.second, value, writeType, timeout
679
- ) { response ->
680
- run {
681
- if (response.success) {
682
- call.resolve()
683
- } else {
684
- call.reject(response.value)
685
- }
686
- }
687
- }
688
- }
689
-
690
- @PluginMethod
691
- fun writeWithoutResponse(call: PluginCall) {
692
- val device = getDevice(call) ?: return
693
- val characteristic = getCharacteristic(call) ?: return
694
- val value = call.getString("value", null)
695
- if (value == null) {
696
- call.reject("Value required.")
697
- return
698
- }
699
- val writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
700
- val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
701
- device.write(
702
- characteristic.first, characteristic.second, value, writeType, timeout
703
- ) { response ->
704
- run {
705
- if (response.success) {
706
- call.resolve()
707
- } else {
708
- call.reject(response.value)
709
- }
710
- }
711
- }
712
- }
713
-
714
- @PluginMethod
715
- fun readDescriptor(call: PluginCall) {
716
- val device = getDevice(call) ?: return
717
- val descriptor = getDescriptor(call) ?: return
718
- val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
719
- device.readDescriptor(
720
- descriptor.first, descriptor.second, descriptor.third, timeout
721
- ) { response ->
722
- run {
723
- if (response.success) {
724
- val ret = JSObject()
725
- ret.put("value", response.value)
726
- call.resolve(ret)
727
- } else {
728
- call.reject(response.value)
729
- }
730
- }
731
- }
732
- }
733
-
734
- @PluginMethod
735
- fun writeDescriptor(call: PluginCall) {
736
- val device = getDevice(call) ?: return
737
- val descriptor = getDescriptor(call) ?: return
738
- val value = call.getString("value", null)
739
- if (value == null) {
740
- call.reject("Value required.")
741
- return
742
- }
743
- val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
744
- device.writeDescriptor(
745
- descriptor.first, descriptor.second, descriptor.third, value, timeout
746
- ) { response ->
747
- run {
748
- if (response.success) {
749
- call.resolve()
750
- } else {
751
- call.reject(response.value)
752
- }
753
- }
754
- }
755
- }
756
-
757
- @PluginMethod
758
- fun startNotifications(call: PluginCall) {
759
- val device = getDevice(call) ?: return
760
- val characteristic = getCharacteristic(call) ?: return
761
- val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
762
- device.setNotifications(characteristic.first, characteristic.second, true, { response ->
763
- run {
764
- val key =
765
- "notification|${device.getId()}|${(characteristic.first)}|${(characteristic.second)}"
766
- val ret = JSObject()
767
- ret.put("value", response.value)
768
- try {
769
- notifyListeners(key, ret)
770
- } catch (e: ConcurrentModificationException) {
771
- Logger.error(TAG, "Error in notifyListeners: ${e.localizedMessage}", e)
772
- }
773
- }
774
- }, timeout, { response ->
775
- run {
776
- if (response.success) {
777
- call.resolve()
778
- } else {
779
- call.reject(response.value)
780
- }
781
- }
782
- })
783
- }
784
-
785
- @PluginMethod
786
- fun stopNotifications(call: PluginCall) {
787
- val device = getDevice(call) ?: return
788
- val characteristic = getCharacteristic(call) ?: return
789
- val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
790
- device.setNotifications(
791
- characteristic.first, characteristic.second, false, null, timeout
792
- ) { response ->
793
- run {
794
- if (response.success) {
795
- call.resolve()
796
- } else {
797
- call.reject(response.value)
798
- }
799
- }
800
- }
801
- }
802
-
803
- private fun assertBluetoothAdapter(call: PluginCall): Boolean? {
804
- if (bluetoothAdapter == null) {
805
- call.reject("Bluetooth LE not initialized.")
806
- return null
807
- }
808
- return true
809
- }
810
-
811
- private fun getScanFilters(call: PluginCall): List<ScanFilter>? {
812
- val filters: ArrayList<ScanFilter> = ArrayList()
813
-
814
- val services = (call.getArray("services", JSArray()) as JSArray).toList<String>()
815
- val manufacturerDataArray = call.getArray("manufacturerData", JSArray())
816
- val serviceDataArray = call.getArray("serviceData", JSArray())
817
- val name = call.getString("name", null)
818
-
819
- try {
820
- // Create filters based on services
821
- for (service in services) {
822
- val filter = ScanFilter.Builder()
823
- filter.setServiceUuid(ParcelUuid.fromString(service))
824
- if (name != null) {
825
- filter.setDeviceName(name)
826
- }
827
- filters.add(filter.build())
828
- }
829
-
830
- // Service Data Handling (for filtering by service data like OpenDroneID)
831
- serviceDataArray?.let {
832
- for (i in 0 until it.length()) {
833
- val serviceDataObject = it.getJSONObject(i)
834
-
835
- val serviceUuid = serviceDataObject.getString("serviceUuid")
836
- val servicePuuid = ParcelUuid.fromString(serviceUuid)
837
-
838
- val dataPrefix = if (serviceDataObject.has("dataPrefix")) {
839
- val dataPrefixString = serviceDataObject.getString("dataPrefix")
840
- stringToBytes(dataPrefixString)
841
- } else null
842
-
843
- val mask = if (serviceDataObject.has("mask")) {
844
- val maskString = serviceDataObject.getString("mask")
845
- stringToBytes(maskString)
846
- } else null
847
-
848
- val filterBuilder = ScanFilter.Builder()
849
-
850
- if (dataPrefix != null && mask != null) {
851
- filterBuilder.setServiceData(servicePuuid, dataPrefix, mask)
852
- } else if (dataPrefix != null) {
853
- filterBuilder.setServiceData(servicePuuid, dataPrefix)
854
- } else {
855
- // Set service data filter without data (just match the service UUID)
856
- filterBuilder.setServiceData(servicePuuid, byteArrayOf())
857
- }
858
-
859
- if (name != null) {
860
- filterBuilder.setDeviceName(name)
861
- }
862
-
863
- filters.add(filterBuilder.build())
864
- }
865
- }
866
-
867
- // Manufacturer Data Handling (with optional parameters)
868
- manufacturerDataArray?.let {
869
- for (i in 0 until it.length()) {
870
- val manufacturerDataObject = it.getJSONObject(i)
871
-
872
- val companyIdentifier = manufacturerDataObject.getInt("companyIdentifier")
873
-
874
- val dataPrefix = if (manufacturerDataObject.has("dataPrefix")) {
875
- val dataPrefixString = manufacturerDataObject.getString("dataPrefix")
876
- stringToBytes(dataPrefixString)
877
- } else null
878
-
879
- val mask = if (manufacturerDataObject.has("mask")) {
880
- val maskString = manufacturerDataObject.getString("mask")
881
- stringToBytes(maskString)
882
- } else null
883
-
884
- val filterBuilder = ScanFilter.Builder()
885
-
886
- if (dataPrefix != null && mask != null) {
887
- filterBuilder.setManufacturerData(companyIdentifier, dataPrefix, mask)
888
- } else if (dataPrefix != null) {
889
- filterBuilder.setManufacturerData(companyIdentifier, dataPrefix)
890
- } else {
891
- // Android requires at least dataPrefix for manufacturer filters.
892
- call.reject("dataPrefix is required when specifying manufacturerData.")
893
- return null
894
- }
895
-
896
- if (name != null) {
897
- filterBuilder.setDeviceName(name)
898
- }
899
-
900
- filters.add(filterBuilder.build())
901
- }
902
- }
903
- // Create filters when providing only name
904
- if (name != null && filters.isEmpty()) {
905
- val filterBuilder = ScanFilter.Builder()
906
- filterBuilder.setDeviceName(name)
907
- filters.add(filterBuilder.build())
908
- }
909
-
910
- return filters;
911
- } catch (e: IllegalArgumentException) {
912
- call.reject("Invalid UUID or Manufacturer data provided.")
913
- return null
914
- } catch (e: Exception) {
915
- call.reject("Invalid or malformed filter data provided.")
916
- return null
917
- }
918
- }
919
-
920
- private fun getScanSettings(call: PluginCall): ScanSettings? {
921
- val scanSettings = ScanSettings.Builder()
922
- val scanMode = call.getInt("scanMode", ScanSettings.SCAN_MODE_BALANCED) as Int
923
- try {
924
- scanSettings.setScanMode(scanMode)
925
- } catch (e: IllegalArgumentException) {
926
- call.reject("Invalid scan mode.")
927
- return null
928
- }
929
- return scanSettings.build()
930
- }
931
-
932
- private fun getBleDevice(device: BluetoothDevice): JSObject {
933
- val bleDevice = JSObject()
934
- bleDevice.put("deviceId", device.address)
935
- if (device.name != null) {
936
- bleDevice.put("name", device.name)
937
- }
938
-
939
- val uuids = JSArray()
940
- device.uuids?.forEach { uuid -> uuids.put(uuid.toString()) }
941
- if (uuids.length() > 0) {
942
- bleDevice.put("uuids", uuids)
943
- }
944
-
945
- return bleDevice
946
- }
947
-
948
- private fun getScanResult(result: ScanResult): JSObject {
949
- val scanResult = JSObject()
950
-
951
- val bleDevice = getBleDevice(result.device)
952
- scanResult.put("device", bleDevice)
953
-
954
- if (result.device.name != null) {
955
- scanResult.put("localName", result.device.name)
956
- }
957
-
958
- scanResult.put("rssi", result.rssi)
959
-
960
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
961
- scanResult.put("txPower", result.txPower)
962
- } else {
963
- scanResult.put("txPower", 127)
964
- }
965
-
966
- val manufacturerData = JSObject()
967
- val manufacturerSpecificData = result.scanRecord?.manufacturerSpecificData
968
- if (manufacturerSpecificData != null) {
969
- for (i in 0 until manufacturerSpecificData.size()) {
970
- val key = manufacturerSpecificData.keyAt(i)
971
- val bytes = manufacturerSpecificData.get(key)
972
- manufacturerData.put(key.toString(), bytesToString(bytes))
973
- }
974
- }
975
- scanResult.put("manufacturerData", manufacturerData)
976
-
977
- val serviceDataObject = JSObject()
978
- val serviceData = result.scanRecord?.serviceData
979
- serviceData?.forEach {
980
- serviceDataObject.put(it.key.toString(), bytesToString(it.value))
981
- }
982
- scanResult.put("serviceData", serviceDataObject)
983
-
984
- val uuids = JSArray()
985
- result.scanRecord?.serviceUuids?.forEach { uuid -> uuids.put(uuid.toString()) }
986
- scanResult.put("uuids", uuids)
987
-
988
- scanResult.put("rawAdvertisement", result.scanRecord?.bytes?.let { bytesToString(it) })
989
- return scanResult
990
- }
991
-
992
- private fun getDisplayStrings(): DisplayStrings {
993
- return DisplayStrings(
994
- config.getString(
995
- "displayStrings.scanning", "Scanning..."
996
- ),
997
- config.getString(
998
- "displayStrings.cancel", "Cancel"
999
- ),
1000
- config.getString(
1001
- "displayStrings.availableDevices", "Available devices"
1002
- ),
1003
- config.getString(
1004
- "displayStrings.noDeviceFound", "No device found"
1005
- ),
1006
- )
1007
- }
1008
-
1009
- private fun getDeviceId(call: PluginCall): String? {
1010
- val deviceId = call.getString("deviceId", null)
1011
- if (deviceId == null) {
1012
- call.reject("deviceId required.")
1013
- return null
1014
- }
1015
- return deviceId
1016
- }
1017
-
1018
- private fun getOrCreateDevice(call: PluginCall): Device? {
1019
- assertBluetoothAdapter(call) ?: return null
1020
- val deviceId = getDeviceId(call) ?: return null
1021
- val device = deviceMap[deviceId]
1022
- if (device != null) {
1023
- return device
1024
- }
1025
- return try {
1026
- val newDevice = Device(
1027
- activity.applicationContext, bluetoothAdapter!!, deviceId
1028
- ) {
1029
- onDisconnect(deviceId)
1030
- }
1031
- deviceMap[deviceId] = newDevice
1032
- newDevice
1033
- } catch (e: IllegalArgumentException) {
1034
- call.reject("Invalid deviceId")
1035
- null
1036
- }
1037
- }
1038
-
1039
- private fun getDevice(call: PluginCall): Device? {
1040
- assertBluetoothAdapter(call) ?: return null
1041
- val deviceId = getDeviceId(call) ?: return null
1042
- val device = deviceMap[deviceId]
1043
- if (device == null || !device.isConnected()) {
1044
- call.reject("Not connected to device.")
1045
- return null
1046
- }
1047
- return device
1048
- }
1049
-
1050
- private fun getCharacteristic(call: PluginCall): Pair<UUID, UUID>? {
1051
- val serviceString = call.getString("service", null)
1052
- val serviceUUID: UUID?
1053
- try {
1054
- serviceUUID = UUID.fromString(serviceString)
1055
- } catch (e: IllegalArgumentException) {
1056
- call.reject("Invalid service UUID.")
1057
- return null
1058
- }
1059
- if (serviceUUID == null) {
1060
- call.reject("Service UUID required.")
1061
- return null
1062
- }
1063
- val characteristicString = call.getString("characteristic", null)
1064
- val characteristicUUID: UUID?
1065
- try {
1066
- characteristicUUID = UUID.fromString(characteristicString)
1067
- } catch (e: IllegalArgumentException) {
1068
- call.reject("Invalid characteristic UUID.")
1069
- return null
1070
- }
1071
- if (characteristicUUID == null) {
1072
- call.reject("Characteristic UUID required.")
1073
- return null
1074
- }
1075
- return Pair(serviceUUID, characteristicUUID)
1076
- }
1077
-
1078
- private fun getDescriptor(call: PluginCall): Triple<UUID, UUID, UUID>? {
1079
- val characteristic = getCharacteristic(call) ?: return null
1080
- val descriptorString = call.getString("descriptor", null)
1081
- val descriptorUUID: UUID?
1082
- try {
1083
- descriptorUUID = UUID.fromString(descriptorString)
1084
- } catch (e: IllegalAccessException) {
1085
- call.reject("Invalid descriptor UUID.")
1086
- return null
1087
- }
1088
- if (descriptorUUID == null) {
1089
- call.reject("Descriptor UUID required.")
1090
- return null
1091
- }
1092
- return Triple(characteristic.first, characteristic.second, descriptorUUID)
1093
- }
1094
- }
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
+ device.cleanup()
517
+ deviceMap.remove(device.getId())
518
+ call.resolve()
519
+ } else {
520
+ call.reject(response.value)
521
+ }
522
+ }
523
+ }
524
+ }
525
+
526
+ @PluginMethod
527
+ fun getServices(call: PluginCall) {
528
+ val device = getDevice(call) ?: return
529
+ val services = device.getServices()
530
+ val bleServices = JSArray()
531
+ services.forEach { service ->
532
+ val bleCharacteristics = JSArray()
533
+ service.characteristics.forEach { characteristic ->
534
+ val bleCharacteristic = JSObject()
535
+ bleCharacteristic.put("uuid", characteristic.uuid)
536
+ bleCharacteristic.put("properties", getProperties(characteristic))
537
+ val bleDescriptors = JSArray()
538
+ characteristic.descriptors.forEach { descriptor ->
539
+ val bleDescriptor = JSObject()
540
+ bleDescriptor.put("uuid", descriptor.uuid)
541
+ bleDescriptors.put(bleDescriptor)
542
+ }
543
+ bleCharacteristic.put("descriptors", bleDescriptors)
544
+ bleCharacteristics.put(bleCharacteristic)
545
+ }
546
+ val bleService = JSObject()
547
+ bleService.put("uuid", service.uuid)
548
+ bleService.put("characteristics", bleCharacteristics)
549
+ bleServices.put(bleService)
550
+ }
551
+ val ret = JSObject()
552
+ ret.put("services", bleServices)
553
+ call.resolve(ret)
554
+ }
555
+
556
+ private fun getProperties(characteristic: BluetoothGattCharacteristic): JSObject {
557
+ val properties = JSObject()
558
+ properties.put(
559
+ "broadcast",
560
+ characteristic.properties and BluetoothGattCharacteristic.PROPERTY_BROADCAST > 0
561
+ )
562
+ properties.put(
563
+ "read", characteristic.properties and BluetoothGattCharacteristic.PROPERTY_READ > 0
564
+ )
565
+ properties.put(
566
+ "writeWithoutResponse",
567
+ characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE > 0
568
+ )
569
+ properties.put(
570
+ "write", characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE > 0
571
+ )
572
+ properties.put(
573
+ "notify", characteristic.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0
574
+ )
575
+ properties.put(
576
+ "indicate",
577
+ characteristic.properties and BluetoothGattCharacteristic.PROPERTY_INDICATE > 0
578
+ )
579
+ properties.put(
580
+ "authenticatedSignedWrites",
581
+ characteristic.properties and BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE > 0
582
+ )
583
+ properties.put(
584
+ "extendedProperties",
585
+ characteristic.properties and BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS > 0
586
+ )
587
+ return properties
588
+ }
589
+
590
+ @PluginMethod
591
+ fun discoverServices(call: PluginCall) {
592
+ val device = getDevice(call) ?: return
593
+ val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
594
+ device.discoverServices(timeout) { response ->
595
+ run {
596
+ if (response.success) {
597
+ call.resolve()
598
+ } else {
599
+ call.reject(response.value)
600
+ }
601
+ }
602
+ }
603
+ }
604
+
605
+ @PluginMethod
606
+ fun getMtu(call: PluginCall) {
607
+ val device = getDevice(call) ?: return
608
+ val mtu = device.getMtu()
609
+ val ret = JSObject()
610
+ ret.put("value", mtu)
611
+ call.resolve(ret)
612
+ }
613
+
614
+ @PluginMethod
615
+ fun requestConnectionPriority(call: PluginCall) {
616
+ val device = getDevice(call) ?: return
617
+ val connectionPriority = call.getInt("connectionPriority", -1) as Int
618
+ if (connectionPriority < BluetoothGatt.CONNECTION_PRIORITY_BALANCED || connectionPriority > BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER) {
619
+ call.reject("Invalid connectionPriority.")
620
+ return
621
+ }
622
+
623
+ val result = device.requestConnectionPriority(connectionPriority)
624
+ if (result) {
625
+ call.resolve()
626
+ } else {
627
+ call.reject("requestConnectionPriority failed.")
628
+ }
629
+ }
630
+
631
+ @PluginMethod
632
+ fun readRssi(call: PluginCall) {
633
+ val device = getDevice(call) ?: return
634
+ val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
635
+ device.readRssi(timeout) { response ->
636
+ run {
637
+ if (response.success) {
638
+ val ret = JSObject()
639
+ ret.put("value", response.value)
640
+ call.resolve(ret)
641
+ } else {
642
+ call.reject(response.value)
643
+ }
644
+ }
645
+ }
646
+ }
647
+
648
+ @PluginMethod
649
+ fun read(call: PluginCall) {
650
+ val device = getDevice(call) ?: return
651
+ val characteristic = getCharacteristic(call) ?: return
652
+ val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
653
+ device.read(characteristic.first, characteristic.second, timeout) { response ->
654
+ run {
655
+ if (response.success) {
656
+ val ret = JSObject()
657
+ ret.put("value", response.value)
658
+ call.resolve(ret)
659
+ } else {
660
+ call.reject(response.value)
661
+ }
662
+ }
663
+ }
664
+ }
665
+
666
+ @PluginMethod
667
+ fun write(call: PluginCall) {
668
+ val device = getDevice(call) ?: return
669
+ val characteristic = getCharacteristic(call) ?: return
670
+ val value = call.getString("value", null)
671
+ if (value == null) {
672
+ call.reject("Value required.")
673
+ return
674
+ }
675
+ val writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
676
+ val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
677
+ device.write(
678
+ characteristic.first, characteristic.second, value, writeType, timeout
679
+ ) { response ->
680
+ run {
681
+ if (response.success) {
682
+ call.resolve()
683
+ } else {
684
+ call.reject(response.value)
685
+ }
686
+ }
687
+ }
688
+ }
689
+
690
+ @PluginMethod
691
+ fun writeWithoutResponse(call: PluginCall) {
692
+ val device = getDevice(call) ?: return
693
+ val characteristic = getCharacteristic(call) ?: return
694
+ val value = call.getString("value", null)
695
+ if (value == null) {
696
+ call.reject("Value required.")
697
+ return
698
+ }
699
+ val writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
700
+ val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
701
+ device.write(
702
+ characteristic.first, characteristic.second, value, writeType, timeout
703
+ ) { response ->
704
+ run {
705
+ if (response.success) {
706
+ call.resolve()
707
+ } else {
708
+ call.reject(response.value)
709
+ }
710
+ }
711
+ }
712
+ }
713
+
714
+ @PluginMethod
715
+ fun readDescriptor(call: PluginCall) {
716
+ val device = getDevice(call) ?: return
717
+ val descriptor = getDescriptor(call) ?: return
718
+ val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
719
+ device.readDescriptor(
720
+ descriptor.first, descriptor.second, descriptor.third, timeout
721
+ ) { response ->
722
+ run {
723
+ if (response.success) {
724
+ val ret = JSObject()
725
+ ret.put("value", response.value)
726
+ call.resolve(ret)
727
+ } else {
728
+ call.reject(response.value)
729
+ }
730
+ }
731
+ }
732
+ }
733
+
734
+ @PluginMethod
735
+ fun writeDescriptor(call: PluginCall) {
736
+ val device = getDevice(call) ?: return
737
+ val descriptor = getDescriptor(call) ?: return
738
+ val value = call.getString("value", null)
739
+ if (value == null) {
740
+ call.reject("Value required.")
741
+ return
742
+ }
743
+ val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
744
+ device.writeDescriptor(
745
+ descriptor.first, descriptor.second, descriptor.third, value, timeout
746
+ ) { response ->
747
+ run {
748
+ if (response.success) {
749
+ call.resolve()
750
+ } else {
751
+ call.reject(response.value)
752
+ }
753
+ }
754
+ }
755
+ }
756
+
757
+ @PluginMethod
758
+ fun startNotifications(call: PluginCall) {
759
+ val device = getDevice(call) ?: return
760
+ val characteristic = getCharacteristic(call) ?: return
761
+ val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
762
+ device.setNotifications(characteristic.first, characteristic.second, true, { response ->
763
+ run {
764
+ val key =
765
+ "notification|${device.getId()}|${(characteristic.first)}|${(characteristic.second)}"
766
+ val ret = JSObject()
767
+ ret.put("value", response.value)
768
+ try {
769
+ notifyListeners(key, ret)
770
+ } catch (e: ConcurrentModificationException) {
771
+ Logger.error(TAG, "Error in notifyListeners: ${e.localizedMessage}", e)
772
+ }
773
+ }
774
+ }, timeout, { response ->
775
+ run {
776
+ if (response.success) {
777
+ call.resolve()
778
+ } else {
779
+ call.reject(response.value)
780
+ }
781
+ }
782
+ })
783
+ }
784
+
785
+ @PluginMethod
786
+ fun stopNotifications(call: PluginCall) {
787
+ val device = getDevice(call) ?: return
788
+ val characteristic = getCharacteristic(call) ?: return
789
+ val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
790
+ device.setNotifications(
791
+ characteristic.first, characteristic.second, false, null, timeout
792
+ ) { response ->
793
+ run {
794
+ if (response.success) {
795
+ call.resolve()
796
+ } else {
797
+ call.reject(response.value)
798
+ }
799
+ }
800
+ }
801
+ }
802
+
803
+ private fun assertBluetoothAdapter(call: PluginCall): Boolean? {
804
+ if (bluetoothAdapter == null) {
805
+ call.reject("Bluetooth LE not initialized.")
806
+ return null
807
+ }
808
+ return true
809
+ }
810
+
811
+ private fun getScanFilters(call: PluginCall): List<ScanFilter>? {
812
+ val filters: ArrayList<ScanFilter> = ArrayList()
813
+
814
+ val services = (call.getArray("services", JSArray()) as JSArray).toList<String>()
815
+ val manufacturerDataArray = call.getArray("manufacturerData", JSArray())
816
+ val serviceDataArray = call.getArray("serviceData", JSArray())
817
+ val name = call.getString("name", null)
818
+
819
+ try {
820
+ // Create filters based on services
821
+ for (service in services) {
822
+ val filter = ScanFilter.Builder()
823
+ filter.setServiceUuid(ParcelUuid.fromString(service))
824
+ if (name != null) {
825
+ filter.setDeviceName(name)
826
+ }
827
+ filters.add(filter.build())
828
+ }
829
+
830
+ // Service Data Handling (for filtering by service data like OpenDroneID)
831
+ serviceDataArray?.let {
832
+ for (i in 0 until it.length()) {
833
+ val serviceDataObject = it.getJSONObject(i)
834
+
835
+ val serviceUuid = serviceDataObject.getString("serviceUuid")
836
+ val servicePuuid = ParcelUuid.fromString(serviceUuid)
837
+
838
+ val dataPrefix = if (serviceDataObject.has("dataPrefix")) {
839
+ val dataPrefixString = serviceDataObject.getString("dataPrefix")
840
+ stringToBytes(dataPrefixString)
841
+ } else null
842
+
843
+ val mask = if (serviceDataObject.has("mask")) {
844
+ val maskString = serviceDataObject.getString("mask")
845
+ stringToBytes(maskString)
846
+ } else null
847
+
848
+ val filterBuilder = ScanFilter.Builder()
849
+
850
+ if (dataPrefix != null && mask != null) {
851
+ filterBuilder.setServiceData(servicePuuid, dataPrefix, mask)
852
+ } else if (dataPrefix != null) {
853
+ filterBuilder.setServiceData(servicePuuid, dataPrefix)
854
+ } else {
855
+ // Set service data filter without data (just match the service UUID)
856
+ filterBuilder.setServiceData(servicePuuid, byteArrayOf())
857
+ }
858
+
859
+ if (name != null) {
860
+ filterBuilder.setDeviceName(name)
861
+ }
862
+
863
+ filters.add(filterBuilder.build())
864
+ }
865
+ }
866
+
867
+ // Manufacturer Data Handling (with optional parameters)
868
+ manufacturerDataArray?.let {
869
+ for (i in 0 until it.length()) {
870
+ val manufacturerDataObject = it.getJSONObject(i)
871
+
872
+ val companyIdentifier = manufacturerDataObject.getInt("companyIdentifier")
873
+
874
+ val dataPrefix = if (manufacturerDataObject.has("dataPrefix")) {
875
+ val dataPrefixString = manufacturerDataObject.getString("dataPrefix")
876
+ stringToBytes(dataPrefixString)
877
+ } else null
878
+
879
+ val mask = if (manufacturerDataObject.has("mask")) {
880
+ val maskString = manufacturerDataObject.getString("mask")
881
+ stringToBytes(maskString)
882
+ } else null
883
+
884
+ val filterBuilder = ScanFilter.Builder()
885
+
886
+ if (dataPrefix != null && mask != null) {
887
+ filterBuilder.setManufacturerData(companyIdentifier, dataPrefix, mask)
888
+ } else if (dataPrefix != null) {
889
+ filterBuilder.setManufacturerData(companyIdentifier, dataPrefix)
890
+ } else {
891
+ // Android requires at least dataPrefix for manufacturer filters.
892
+ call.reject("dataPrefix is required when specifying manufacturerData.")
893
+ return null
894
+ }
895
+
896
+ if (name != null) {
897
+ filterBuilder.setDeviceName(name)
898
+ }
899
+
900
+ filters.add(filterBuilder.build())
901
+ }
902
+ }
903
+ // Create filters when providing only name
904
+ if (name != null && filters.isEmpty()) {
905
+ val filterBuilder = ScanFilter.Builder()
906
+ filterBuilder.setDeviceName(name)
907
+ filters.add(filterBuilder.build())
908
+ }
909
+
910
+ return filters;
911
+ } catch (e: IllegalArgumentException) {
912
+ call.reject("Invalid UUID or Manufacturer data provided.")
913
+ return null
914
+ } catch (e: Exception) {
915
+ call.reject("Invalid or malformed filter data provided.")
916
+ return null
917
+ }
918
+ }
919
+
920
+ private fun getScanSettings(call: PluginCall): ScanSettings? {
921
+ val scanSettings = ScanSettings.Builder()
922
+ val scanMode = call.getInt("scanMode", ScanSettings.SCAN_MODE_BALANCED) as Int
923
+ try {
924
+ scanSettings.setScanMode(scanMode)
925
+ } catch (e: IllegalArgumentException) {
926
+ call.reject("Invalid scan mode.")
927
+ return null
928
+ }
929
+ return scanSettings.build()
930
+ }
931
+
932
+ private fun getBleDevice(device: BluetoothDevice): JSObject {
933
+ val bleDevice = JSObject()
934
+ bleDevice.put("deviceId", device.address)
935
+ if (device.name != null) {
936
+ bleDevice.put("name", device.name)
937
+ }
938
+
939
+ val uuids = JSArray()
940
+ device.uuids?.forEach { uuid -> uuids.put(uuid.toString()) }
941
+ if (uuids.length() > 0) {
942
+ bleDevice.put("uuids", uuids)
943
+ }
944
+
945
+ return bleDevice
946
+ }
947
+
948
+ private fun getScanResult(result: ScanResult): JSObject {
949
+ val scanResult = JSObject()
950
+
951
+ val bleDevice = getBleDevice(result.device)
952
+ scanResult.put("device", bleDevice)
953
+
954
+ if (result.device.name != null) {
955
+ scanResult.put("localName", result.device.name)
956
+ }
957
+
958
+ scanResult.put("rssi", result.rssi)
959
+
960
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
961
+ scanResult.put("txPower", result.txPower)
962
+ } else {
963
+ scanResult.put("txPower", 127)
964
+ }
965
+
966
+ val manufacturerData = JSObject()
967
+ val manufacturerSpecificData = result.scanRecord?.manufacturerSpecificData
968
+ if (manufacturerSpecificData != null) {
969
+ for (i in 0 until manufacturerSpecificData.size()) {
970
+ val key = manufacturerSpecificData.keyAt(i)
971
+ val bytes = manufacturerSpecificData.get(key)
972
+ manufacturerData.put(key.toString(), bytesToString(bytes))
973
+ }
974
+ }
975
+ scanResult.put("manufacturerData", manufacturerData)
976
+
977
+ val serviceDataObject = JSObject()
978
+ val serviceData = result.scanRecord?.serviceData
979
+ serviceData?.forEach {
980
+ serviceDataObject.put(it.key.toString(), bytesToString(it.value))
981
+ }
982
+ scanResult.put("serviceData", serviceDataObject)
983
+
984
+ val uuids = JSArray()
985
+ result.scanRecord?.serviceUuids?.forEach { uuid -> uuids.put(uuid.toString()) }
986
+ scanResult.put("uuids", uuids)
987
+
988
+ scanResult.put("rawAdvertisement", result.scanRecord?.bytes?.let { bytesToString(it) })
989
+ return scanResult
990
+ }
991
+
992
+ private fun getDisplayStrings(): DisplayStrings {
993
+ return DisplayStrings(
994
+ config.getString(
995
+ "displayStrings.scanning", "Scanning..."
996
+ ),
997
+ config.getString(
998
+ "displayStrings.cancel", "Cancel"
999
+ ),
1000
+ config.getString(
1001
+ "displayStrings.availableDevices", "Available devices"
1002
+ ),
1003
+ config.getString(
1004
+ "displayStrings.noDeviceFound", "No device found"
1005
+ ),
1006
+ )
1007
+ }
1008
+
1009
+ private fun getDeviceId(call: PluginCall): String? {
1010
+ val deviceId = call.getString("deviceId", null)
1011
+ if (deviceId == null) {
1012
+ call.reject("deviceId required.")
1013
+ return null
1014
+ }
1015
+ return deviceId
1016
+ }
1017
+
1018
+ private fun getOrCreateDevice(call: PluginCall): Device? {
1019
+ assertBluetoothAdapter(call) ?: return null
1020
+ val deviceId = getDeviceId(call) ?: return null
1021
+ val device = deviceMap[deviceId]
1022
+ if (device != null) {
1023
+ return device
1024
+ }
1025
+ return try {
1026
+ val newDevice = Device(
1027
+ activity.applicationContext, bluetoothAdapter!!, deviceId
1028
+ ) {
1029
+ onDisconnect(deviceId)
1030
+ }
1031
+ deviceMap[deviceId] = newDevice
1032
+ newDevice
1033
+ } catch (e: IllegalArgumentException) {
1034
+ call.reject("Invalid deviceId")
1035
+ null
1036
+ }
1037
+ }
1038
+
1039
+ private fun getDevice(call: PluginCall): Device? {
1040
+ assertBluetoothAdapter(call) ?: return null
1041
+ val deviceId = getDeviceId(call) ?: return null
1042
+ val device = deviceMap[deviceId]
1043
+ if (device == null || !device.isConnected()) {
1044
+ call.reject("Not connected to device.")
1045
+ return null
1046
+ }
1047
+ return device
1048
+ }
1049
+
1050
+ private fun getCharacteristic(call: PluginCall): Pair<UUID, UUID>? {
1051
+ val serviceString = call.getString("service", null)
1052
+ val serviceUUID: UUID?
1053
+ try {
1054
+ serviceUUID = UUID.fromString(serviceString)
1055
+ } catch (e: IllegalArgumentException) {
1056
+ call.reject("Invalid service UUID.")
1057
+ return null
1058
+ }
1059
+ if (serviceUUID == null) {
1060
+ call.reject("Service UUID required.")
1061
+ return null
1062
+ }
1063
+ val characteristicString = call.getString("characteristic", null)
1064
+ val characteristicUUID: UUID?
1065
+ try {
1066
+ characteristicUUID = UUID.fromString(characteristicString)
1067
+ } catch (e: IllegalArgumentException) {
1068
+ call.reject("Invalid characteristic UUID.")
1069
+ return null
1070
+ }
1071
+ if (characteristicUUID == null) {
1072
+ call.reject("Characteristic UUID required.")
1073
+ return null
1074
+ }
1075
+ return Pair(serviceUUID, characteristicUUID)
1076
+ }
1077
+
1078
+ private fun getDescriptor(call: PluginCall): Triple<UUID, UUID, UUID>? {
1079
+ val characteristic = getCharacteristic(call) ?: return null
1080
+ val descriptorString = call.getString("descriptor", null)
1081
+ val descriptorUUID: UUID?
1082
+ try {
1083
+ descriptorUUID = UUID.fromString(descriptorString)
1084
+ } catch (e: IllegalAccessException) {
1085
+ call.reject("Invalid descriptor UUID.")
1086
+ return null
1087
+ }
1088
+ if (descriptorUUID == null) {
1089
+ call.reject("Descriptor UUID required.")
1090
+ return null
1091
+ }
1092
+ return Triple(characteristic.first, characteristic.second, descriptorUUID)
1093
+ }
1094
+ }