@capacitor-community/bluetooth-le 7.2.0 → 8.0.0-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CapacitorCommunityBluetoothLe.podspec +17 -17
  2. package/LICENSE +21 -21
  3. package/Package.swift +28 -0
  4. package/README.md +68 -161
  5. package/android/build.gradle +71 -68
  6. package/android/src/main/AndroidManifest.xml +22 -22
  7. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/BluetoothLe.kt +1094 -1070
  8. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Conversion.kt +51 -51
  9. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt +771 -767
  10. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/DeviceList.kt +28 -28
  11. package/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/DeviceScanner.kt +189 -189
  12. package/dist/docs.json +906 -849
  13. package/dist/esm/bleClient.d.ts +278 -278
  14. package/dist/esm/bleClient.js +361 -347
  15. package/dist/esm/bleClient.js.map +1 -1
  16. package/dist/esm/config.d.ts +53 -53
  17. package/dist/esm/config.js +2 -2
  18. package/dist/esm/conversion.d.ts +56 -34
  19. package/dist/esm/conversion.js +134 -84
  20. package/dist/esm/conversion.js.map +1 -1
  21. package/dist/esm/definitions.d.ts +352 -313
  22. package/dist/esm/definitions.js +42 -42
  23. package/dist/esm/definitions.js.map +1 -1
  24. package/dist/esm/index.d.ts +5 -5
  25. package/dist/esm/index.js +5 -5
  26. package/dist/esm/plugin.d.ts +2 -2
  27. package/dist/esm/plugin.js +4 -4
  28. package/dist/esm/queue.d.ts +3 -3
  29. package/dist/esm/queue.js +17 -17
  30. package/dist/esm/queue.js.map +1 -1
  31. package/dist/esm/timeout.d.ts +1 -1
  32. package/dist/esm/timeout.js +9 -9
  33. package/dist/esm/validators.d.ts +1 -1
  34. package/dist/esm/validators.js +11 -11
  35. package/dist/esm/validators.js.map +1 -1
  36. package/dist/esm/web.d.ts +57 -56
  37. package/dist/esm/web.js +403 -340
  38. package/dist/esm/web.js.map +1 -1
  39. package/dist/plugin.cjs.js +967 -837
  40. package/dist/plugin.cjs.js.map +1 -1
  41. package/dist/plugin.js +967 -837
  42. package/dist/plugin.js.map +1 -1
  43. package/ios/{Plugin → Sources/BluetoothLe}/Conversion.swift +83 -83
  44. package/ios/{Plugin → Sources/BluetoothLe}/Device.swift +423 -423
  45. package/ios/Sources/BluetoothLe/DeviceListView.swift +121 -0
  46. package/ios/{Plugin → Sources/BluetoothLe}/DeviceManager.swift +503 -401
  47. package/ios/{Plugin → Sources/BluetoothLe}/Logging.swift +8 -8
  48. package/ios/{Plugin → Sources/BluetoothLe}/Plugin.swift +775 -682
  49. package/ios/{Plugin → Sources/BluetoothLe}/ThreadSafeDictionary.swift +15 -13
  50. package/ios/Tests/BluetoothLeTests/ConversionTests.swift +55 -0
  51. package/ios/Tests/BluetoothLeTests/PluginTests.swift +27 -0
  52. package/package.json +115 -101
  53. package/ios/Plugin/Info.plist +0 -24
  54. package/ios/Plugin/Plugin.h +0 -10
  55. package/ios/Plugin/Plugin.m +0 -41
@@ -1,1070 +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
- 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
- val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
761
- device.setNotifications(characteristic.first, characteristic.second, true, { response ->
762
- run {
763
- val key =
764
- "notification|${device.getId()}|${(characteristic.first)}|${(characteristic.second)}"
765
- val ret = JSObject()
766
- ret.put("value", response.value)
767
- try {
768
- notifyListeners(key, ret)
769
- } catch (e: ConcurrentModificationException) {
770
- Logger.error(TAG, "Error in notifyListeners: ${e.localizedMessage}", e)
771
- }
772
- }
773
- }, timeout, { response ->
774
- run {
775
- if (response.success) {
776
- call.resolve()
777
- } else {
778
- call.reject(response.value)
779
- }
780
- }
781
- })
782
- }
783
-
784
- @PluginMethod
785
- fun stopNotifications(call: PluginCall) {
786
- val device = getDevice(call) ?: return
787
- val characteristic = getCharacteristic(call) ?: return
788
- val timeout = call.getFloat("timeout", DEFAULT_TIMEOUT)!!.toLong()
789
- device.setNotifications(
790
- characteristic.first, characteristic.second, false, null, timeout
791
- ) { response ->
792
- run {
793
- if (response.success) {
794
- call.resolve()
795
- } else {
796
- call.reject(response.value)
797
- }
798
- }
799
- }
800
- }
801
-
802
- private fun assertBluetoothAdapter(call: PluginCall): Boolean? {
803
- if (bluetoothAdapter == null) {
804
- call.reject("Bluetooth LE not initialized.")
805
- return null
806
- }
807
- return true
808
- }
809
-
810
- private fun getScanFilters(call: PluginCall): List<ScanFilter>? {
811
- val filters: ArrayList<ScanFilter> = ArrayList()
812
-
813
- val services = (call.getArray("services", JSArray()) as JSArray).toList<String>()
814
- val manufacturerDataArray = call.getArray("manufacturerData", JSArray())
815
- val name = call.getString("name", null)
816
-
817
- try {
818
- // Create filters based on services
819
- for (service in services) {
820
- val filter = ScanFilter.Builder()
821
- filter.setServiceUuid(ParcelUuid.fromString(service))
822
- if (name != null) {
823
- filter.setDeviceName(name)
824
- }
825
- filters.add(filter.build())
826
- }
827
-
828
- // Manufacturer Data Handling (with optional parameters)
829
- manufacturerDataArray?.let {
830
- for (i in 0 until it.length()) {
831
- val manufacturerDataObject = it.getJSONObject(i)
832
-
833
- val companyIdentifier = manufacturerDataObject.getInt("companyIdentifier")
834
-
835
- val dataPrefix = if (manufacturerDataObject.has("dataPrefix")) {
836
- val dataPrefixObject = manufacturerDataObject.getJSONObject("dataPrefix")
837
- val byteLength = dataPrefixObject.length()
838
-
839
- ByteArray(byteLength).apply {
840
- for (idx in 0 until byteLength) {
841
- val key = idx.toString()
842
- this[idx] = (dataPrefixObject.getInt(key) and 0xFF).toByte()
843
- }
844
- }
845
- } else null
846
-
847
-
848
- val mask = if (manufacturerDataObject.has("mask")) {
849
- val maskObject = manufacturerDataObject.getJSONObject("mask")
850
- val byteLength = maskObject.length()
851
-
852
- ByteArray(byteLength).apply {
853
- for (idx in 0 until byteLength) {
854
- val key = idx.toString()
855
- this[idx] = (maskObject.getInt(key) and 0xFF).toByte()
856
- }
857
- }
858
- } else null
859
-
860
- val filterBuilder = ScanFilter.Builder()
861
-
862
- if (dataPrefix != null && mask != null) {
863
- filterBuilder.setManufacturerData(companyIdentifier, dataPrefix, mask)
864
- } else if (dataPrefix != null) {
865
- filterBuilder.setManufacturerData(companyIdentifier, dataPrefix)
866
- } else {
867
- // Android requires at least dataPrefix for manufacturer filters.
868
- call.reject("dataPrefix is required when specifying manufacturerData.")
869
- return null
870
- }
871
-
872
- if (name != null) {
873
- filterBuilder.setDeviceName(name)
874
- }
875
-
876
- filters.add(filterBuilder.build())
877
- }
878
- }
879
- // Create filters when providing only name
880
- if (name != null && filters.isEmpty()) {
881
- val filterBuilder = ScanFilter.Builder()
882
- filterBuilder.setDeviceName(name)
883
- filters.add(filterBuilder.build())
884
- }
885
-
886
- return filters;
887
- } catch (e: IllegalArgumentException) {
888
- call.reject("Invalid UUID or Manufacturer data provided.")
889
- return null
890
- } catch (e: Exception) {
891
- call.reject("Invalid or malformed filter data provided.")
892
- return null
893
- }
894
- }
895
-
896
- private fun getScanSettings(call: PluginCall): ScanSettings? {
897
- val scanSettings = ScanSettings.Builder()
898
- val scanMode = call.getInt("scanMode", ScanSettings.SCAN_MODE_BALANCED) as Int
899
- try {
900
- scanSettings.setScanMode(scanMode)
901
- } catch (e: IllegalArgumentException) {
902
- call.reject("Invalid scan mode.")
903
- return null
904
- }
905
- return scanSettings.build()
906
- }
907
-
908
- private fun getBleDevice(device: BluetoothDevice): JSObject {
909
- val bleDevice = JSObject()
910
- bleDevice.put("deviceId", device.address)
911
- if (device.name != null) {
912
- bleDevice.put("name", device.name)
913
- }
914
-
915
- val uuids = JSArray()
916
- device.uuids?.forEach { uuid -> uuids.put(uuid.toString()) }
917
- if (uuids.length() > 0) {
918
- bleDevice.put("uuids", uuids)
919
- }
920
-
921
- return bleDevice
922
- }
923
-
924
- private fun getScanResult(result: ScanResult): JSObject {
925
- val scanResult = JSObject()
926
-
927
- val bleDevice = getBleDevice(result.device)
928
- scanResult.put("device", bleDevice)
929
-
930
- if (result.device.name != null) {
931
- scanResult.put("localName", result.device.name)
932
- }
933
-
934
- scanResult.put("rssi", result.rssi)
935
-
936
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
937
- scanResult.put("txPower", result.txPower)
938
- } else {
939
- scanResult.put("txPower", 127)
940
- }
941
-
942
- val manufacturerData = JSObject()
943
- val manufacturerSpecificData = result.scanRecord?.manufacturerSpecificData
944
- if (manufacturerSpecificData != null) {
945
- for (i in 0 until manufacturerSpecificData.size()) {
946
- val key = manufacturerSpecificData.keyAt(i)
947
- val bytes = manufacturerSpecificData.get(key)
948
- manufacturerData.put(key.toString(), bytesToString(bytes))
949
- }
950
- }
951
- scanResult.put("manufacturerData", manufacturerData)
952
-
953
- val serviceDataObject = JSObject()
954
- val serviceData = result.scanRecord?.serviceData
955
- serviceData?.forEach {
956
- serviceDataObject.put(it.key.toString(), bytesToString(it.value))
957
- }
958
- scanResult.put("serviceData", serviceDataObject)
959
-
960
- val uuids = JSArray()
961
- result.scanRecord?.serviceUuids?.forEach { uuid -> uuids.put(uuid.toString()) }
962
- scanResult.put("uuids", uuids)
963
-
964
- scanResult.put("rawAdvertisement", result.scanRecord?.bytes?.let { bytesToString(it) })
965
- return scanResult
966
- }
967
-
968
- private fun getDisplayStrings(): DisplayStrings {
969
- return DisplayStrings(
970
- config.getString(
971
- "displayStrings.scanning", "Scanning..."
972
- ),
973
- config.getString(
974
- "displayStrings.cancel", "Cancel"
975
- ),
976
- config.getString(
977
- "displayStrings.availableDevices", "Available devices"
978
- ),
979
- config.getString(
980
- "displayStrings.noDeviceFound", "No device found"
981
- ),
982
- )
983
- }
984
-
985
- private fun getDeviceId(call: PluginCall): String? {
986
- val deviceId = call.getString("deviceId", null)
987
- if (deviceId == null) {
988
- call.reject("deviceId required.")
989
- return null
990
- }
991
- return deviceId
992
- }
993
-
994
- private fun getOrCreateDevice(call: PluginCall): Device? {
995
- assertBluetoothAdapter(call) ?: return null
996
- val deviceId = getDeviceId(call) ?: return null
997
- val device = deviceMap[deviceId]
998
- if (device != null) {
999
- return device
1000
- }
1001
- return try {
1002
- val newDevice = Device(
1003
- activity.applicationContext, bluetoothAdapter!!, deviceId
1004
- ) {
1005
- onDisconnect(deviceId)
1006
- }
1007
- deviceMap[deviceId] = newDevice
1008
- newDevice
1009
- } catch (e: IllegalArgumentException) {
1010
- call.reject("Invalid deviceId")
1011
- null
1012
- }
1013
- }
1014
-
1015
- private fun getDevice(call: PluginCall): Device? {
1016
- assertBluetoothAdapter(call) ?: return null
1017
- val deviceId = getDeviceId(call) ?: return null
1018
- val device = deviceMap[deviceId]
1019
- if (device == null || !device.isConnected()) {
1020
- call.reject("Not connected to device.")
1021
- return null
1022
- }
1023
- return device
1024
- }
1025
-
1026
- private fun getCharacteristic(call: PluginCall): Pair<UUID, UUID>? {
1027
- val serviceString = call.getString("service", null)
1028
- val serviceUUID: UUID?
1029
- try {
1030
- serviceUUID = UUID.fromString(serviceString)
1031
- } catch (e: IllegalArgumentException) {
1032
- call.reject("Invalid service UUID.")
1033
- return null
1034
- }
1035
- if (serviceUUID == null) {
1036
- call.reject("Service UUID required.")
1037
- return null
1038
- }
1039
- val characteristicString = call.getString("characteristic", null)
1040
- val characteristicUUID: UUID?
1041
- try {
1042
- characteristicUUID = UUID.fromString(characteristicString)
1043
- } catch (e: IllegalArgumentException) {
1044
- call.reject("Invalid characteristic UUID.")
1045
- return null
1046
- }
1047
- if (characteristicUUID == null) {
1048
- call.reject("Characteristic UUID required.")
1049
- return null
1050
- }
1051
- return Pair(serviceUUID, characteristicUUID)
1052
- }
1053
-
1054
- private fun getDescriptor(call: PluginCall): Triple<UUID, UUID, UUID>? {
1055
- val characteristic = getCharacteristic(call) ?: return null
1056
- val descriptorString = call.getString("descriptor", null)
1057
- val descriptorUUID: UUID?
1058
- try {
1059
- descriptorUUID = UUID.fromString(descriptorString)
1060
- } catch (e: IllegalAccessException) {
1061
- call.reject("Invalid descriptor UUID.")
1062
- return null
1063
- }
1064
- if (descriptorUUID == null) {
1065
- call.reject("Descriptor UUID required.")
1066
- return null
1067
- }
1068
- return Triple(characteristic.first, characteristic.second, descriptorUUID)
1069
- }
1070
- }
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
+ }