react-native-ble-nitro 1.2.0 → 1.3.1

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 (54) hide show
  1. package/README.md +89 -28
  2. package/android/CMakeLists.txt +32 -0
  3. package/android/build.gradle +140 -0
  4. package/android/fix-prefab.gradle +51 -0
  5. package/android/gradle.properties +5 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  8. package/android/src/main/java/com/margelo/nitro/co/zyke/ble/BleNitroBleManager.kt +899 -0
  9. package/android/src/main/java/com/margelo/nitro/co/zyke/ble/BleNitroPackage.kt +38 -0
  10. package/ios/BleNitroBleManager.swift +7 -3
  11. package/lib/commonjs/index.d.ts +15 -3
  12. package/lib/commonjs/index.d.ts.map +1 -1
  13. package/lib/commonjs/index.js +46 -28
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/commonjs/specs/NativeBleNitro.nitro.d.ts +8 -1
  16. package/lib/commonjs/specs/NativeBleNitro.nitro.d.ts.map +1 -1
  17. package/lib/commonjs/specs/NativeBleNitro.nitro.js +8 -1
  18. package/lib/commonjs/specs/NativeBleNitro.nitro.js.map +1 -1
  19. package/lib/index.d.ts +15 -3
  20. package/lib/index.js +44 -26
  21. package/lib/specs/NativeBleNitro.nitro.d.ts +8 -1
  22. package/lib/specs/NativeBleNitro.nitro.js +7 -0
  23. package/nitrogen/generated/android/BleNitroOnLoad.cpp +2 -2
  24. package/nitrogen/generated/android/c++/JAndroidScanMode.hpp +65 -0
  25. package/nitrogen/generated/android/c++/JFunc_void_std__optional_BLEDevice__std__optional_std__string_.hpp +86 -0
  26. package/nitrogen/generated/android/c++/JHybridNativeBleNitroSpec.cpp +8 -4
  27. package/nitrogen/generated/android/c++/JHybridNativeBleNitroSpec.hpp +1 -1
  28. package/nitrogen/generated/android/c++/JScanFilter.hpp +8 -2
  29. package/nitrogen/generated/android/kotlin/com/margelo/nitro/co/zyke/ble/AndroidScanMode.kt +23 -0
  30. package/nitrogen/generated/android/kotlin/com/margelo/nitro/co/zyke/ble/{Func_void_BLEDevice.kt → Func_void_std__optional_BLEDevice__std__optional_std__string_.kt} +14 -14
  31. package/nitrogen/generated/android/kotlin/com/margelo/nitro/co/zyke/ble/HybridNativeBleNitroSpec.kt +2 -2
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/co/zyke/ble/ScanFilter.kt +4 -1
  33. package/nitrogen/generated/ios/BleNitro-Swift-Cxx-Bridge.cpp +5 -5
  34. package/nitrogen/generated/ios/BleNitro-Swift-Cxx-Bridge.hpp +30 -21
  35. package/nitrogen/generated/ios/BleNitro-Swift-Cxx-Umbrella.hpp +3 -0
  36. package/nitrogen/generated/ios/c++/HybridNativeBleNitroSpecSwift.hpp +5 -2
  37. package/nitrogen/generated/ios/swift/AndroidScanMode.swift +48 -0
  38. package/nitrogen/generated/ios/swift/Func_void_std__optional_BLEDevice__std__optional_std__string_.swift +59 -0
  39. package/nitrogen/generated/ios/swift/HybridNativeBleNitroSpec.swift +1 -1
  40. package/nitrogen/generated/ios/swift/HybridNativeBleNitroSpec_cxx.swift +17 -5
  41. package/nitrogen/generated/ios/swift/ScanFilter.swift +13 -2
  42. package/nitrogen/generated/shared/c++/AndroidScanMode.hpp +64 -0
  43. package/nitrogen/generated/shared/c++/HybridNativeBleNitroSpec.hpp +3 -3
  44. package/nitrogen/generated/shared/c++/ScanFilter.hpp +9 -3
  45. package/package.json +1 -1
  46. package/plugin/build/index.d.ts +2 -0
  47. package/plugin/build/index.js +2 -0
  48. package/plugin/build/withBleNitro.d.ts +5 -1
  49. package/plugin/build/withBleNitro.js +18 -7
  50. package/src/__tests__/index.test.ts +45 -11
  51. package/src/index.ts +55 -27
  52. package/src/specs/NativeBleNitro.nitro.ts +9 -1
  53. package/nitrogen/generated/android/c++/JFunc_void_BLEDevice.hpp +0 -85
  54. package/nitrogen/generated/ios/swift/Func_void_BLEDevice.swift +0 -47
@@ -0,0 +1,899 @@
1
+ package com.margelo.nitro.co.zyke.ble
2
+
3
+ import android.Manifest
4
+ import android.bluetooth.BluetoothAdapter
5
+ import android.bluetooth.BluetoothDevice
6
+ import android.bluetooth.BluetoothGatt
7
+ import android.bluetooth.BluetoothGattCallback
8
+ import android.bluetooth.BluetoothGattCharacteristic
9
+ import android.bluetooth.BluetoothGattDescriptor
10
+ import android.bluetooth.BluetoothGattService
11
+ import android.bluetooth.BluetoothManager
12
+ import android.bluetooth.BluetoothProfile
13
+ import android.bluetooth.le.BluetoothLeScanner
14
+ import android.bluetooth.le.ScanCallback
15
+ import android.bluetooth.le.ScanFilter
16
+ import android.bluetooth.le.ScanResult
17
+ import android.bluetooth.le.ScanSettings
18
+ import android.content.BroadcastReceiver
19
+ import android.content.Context
20
+ import android.content.Intent
21
+ import android.content.IntentFilter
22
+ import android.content.pm.PackageManager
23
+ import android.os.Build
24
+ import android.os.ParcelUuid
25
+ import android.provider.Settings
26
+ import androidx.core.content.ContextCompat
27
+ import com.margelo.nitro.core.*
28
+ import java.util.UUID
29
+ import java.util.concurrent.ConcurrentHashMap
30
+
31
+ /**
32
+ * Android implementation of the BLE Nitro Module
33
+ * This class provides the actual BLE functionality for Android devices
34
+ */
35
+ class BleNitroBleManager : HybridNativeBleNitroSpec() {
36
+
37
+ private var bluetoothAdapter: BluetoothAdapter? = null
38
+ private var stateCallback: ((state: BLEState) -> Unit)? = null
39
+ private var bluetoothStateReceiver: BroadcastReceiver? = null
40
+
41
+ // BLE Scanning
42
+ private var bleScanner: BluetoothLeScanner? = null
43
+ private var isCurrentlyScanning = false
44
+ private var scanCallback: ScanCallback? = null
45
+ private var deviceFoundCallback: ((device: BLEDevice?, error: String?) -> Unit)? = null
46
+ private val discoveredDevicesInCurrentScan = mutableSetOf<String>()
47
+
48
+ // Device connections
49
+ private val connectedDevices = ConcurrentHashMap<String, BluetoothGatt>()
50
+ private val deviceCallbacks = ConcurrentHashMap<String, DeviceCallbacks>()
51
+
52
+ // Helper class to store device callbacks
53
+ private data class DeviceCallbacks(
54
+ var connectCallback: ((success: Boolean, deviceId: String, error: String) -> Unit)? = null,
55
+ var disconnectCallback: ((deviceId: String, interrupted: Boolean, error: String) -> Unit)? = null,
56
+ var serviceDiscoveryCallback: ((success: Boolean, error: String) -> Unit)? = null,
57
+ var characteristicSubscriptions: MutableMap<String, (characteristicId: String, data: ArrayBuffer) -> Unit> = mutableMapOf()
58
+ )
59
+
60
+ init {
61
+ // Try to get context from React Native application context
62
+ tryToGetContextFromReactNative()
63
+ }
64
+
65
+ companion object {
66
+ private var appContext: Context? = null
67
+
68
+ fun setContext(context: Context) {
69
+ appContext = context.applicationContext
70
+ }
71
+
72
+ fun getContext(): Context? = appContext
73
+ }
74
+
75
+ private fun tryToGetContextFromReactNative() {
76
+ if (appContext == null) {
77
+ try {
78
+ // Try to get Application context using reflection
79
+ val activityThread = Class.forName("android.app.ActivityThread")
80
+ val currentApplicationMethod = activityThread.getMethod("currentApplication")
81
+ val application = currentApplicationMethod.invoke(null) as? android.app.Application
82
+
83
+ if (application != null) {
84
+ setContext(application)
85
+ }
86
+ } catch (e: Exception) {
87
+ // Context will be set by package initialization if reflection fails
88
+ }
89
+ }
90
+ }
91
+
92
+ private fun initializeBluetoothIfNeeded() {
93
+ if (bluetoothAdapter == null) {
94
+ try {
95
+ val context = appContext ?: return
96
+ val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
97
+ bluetoothAdapter = bluetoothManager?.adapter
98
+ } catch (e: Exception) {
99
+ // Handle initialization error silently
100
+ }
101
+ }
102
+ }
103
+
104
+ private fun hasBluetoothPermissions(): Boolean {
105
+ val context = appContext ?: return false
106
+
107
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
108
+ // Android 12+ (API 31+) - check new Bluetooth permissions
109
+ ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED &&
110
+ ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED
111
+ } else {
112
+ // Android < 12 - check legacy permissions
113
+ ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_GRANTED &&
114
+ ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_GRANTED
115
+ }
116
+ }
117
+
118
+ private fun getMissingPermissions(): List<String> {
119
+ val context = appContext ?: return emptyList()
120
+ val missing = mutableListOf<String>()
121
+
122
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
123
+ // Android 12+ permissions
124
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
125
+ missing.add(Manifest.permission.BLUETOOTH_CONNECT)
126
+ }
127
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
128
+ missing.add(Manifest.permission.BLUETOOTH_SCAN)
129
+ }
130
+ } else {
131
+ // Legacy permissions
132
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH) != PackageManager.PERMISSION_GRANTED) {
133
+ missing.add(Manifest.permission.BLUETOOTH)
134
+ }
135
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_ADMIN) != PackageManager.PERMISSION_GRANTED) {
136
+ missing.add(Manifest.permission.BLUETOOTH_ADMIN)
137
+ }
138
+ }
139
+
140
+ // Location permissions for BLE scanning
141
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
142
+ missing.add(Manifest.permission.ACCESS_FINE_LOCATION)
143
+ }
144
+
145
+ return missing
146
+ }
147
+
148
+ private fun bluetoothStateToBlEState(bluetoothState: Int): BLEState {
149
+ return when (bluetoothState) {
150
+ BluetoothAdapter.STATE_OFF -> BLEState.POWEREDOFF
151
+ BluetoothAdapter.STATE_ON -> BLEState.POWEREDON
152
+ BluetoothAdapter.STATE_TURNING_ON -> BLEState.RESETTING
153
+ BluetoothAdapter.STATE_TURNING_OFF -> BLEState.RESETTING
154
+ else -> BLEState.UNKNOWN
155
+ }
156
+ }
157
+
158
+ private fun createBluetoothStateReceiver(): BroadcastReceiver {
159
+ return object : BroadcastReceiver() {
160
+ override fun onReceive(context: Context?, intent: Intent?) {
161
+ if (intent?.action == BluetoothAdapter.ACTION_STATE_CHANGED) {
162
+ val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
163
+ val bleState = bluetoothStateToBlEState(state)
164
+ stateCallback?.invoke(bleState)
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ private fun createBLEDeviceFromScanResult(scanResult: ScanResult): BLEDevice {
171
+ val device = scanResult.device
172
+ val scanRecord = scanResult.scanRecord
173
+
174
+ // Extract manufacturer data
175
+ val manufacturerData = scanRecord?.manufacturerSpecificData?.let { sparseArray ->
176
+ val entries = mutableListOf<ManufacturerDataEntry>()
177
+ for (i in 0 until sparseArray.size()) {
178
+ val key = sparseArray.keyAt(i)
179
+ val value = sparseArray.get(key)
180
+
181
+ // Create direct ByteBuffer as required by ArrayBuffer.wrap()
182
+ val directBuffer = java.nio.ByteBuffer.allocateDirect(value.size)
183
+ directBuffer.put(value)
184
+ directBuffer.flip()
185
+
186
+ entries.add(ManufacturerDataEntry(
187
+ id = key.toString(),
188
+ data = ArrayBuffer.wrap(directBuffer)
189
+ ))
190
+ }
191
+ ManufacturerData(companyIdentifiers = entries.toTypedArray())
192
+ } ?: ManufacturerData(companyIdentifiers = emptyArray())
193
+
194
+ // Extract service UUIDs
195
+ val serviceUUIDs = scanRecord?.serviceUuids?.map { it.toString() }?.toTypedArray() ?: emptyArray()
196
+
197
+ return BLEDevice(
198
+ id = device.address,
199
+ name = device.name ?: "",
200
+ rssi = scanResult.rssi.toDouble(),
201
+ manufacturerData = manufacturerData,
202
+ serviceUUIDs = serviceUUIDs,
203
+ isConnectable = true // Assume scannable devices are connectable
204
+ )
205
+ }
206
+
207
+ private fun createAndroidScanFilters(filter: com.margelo.nitro.co.zyke.ble.ScanFilter): List<android.bluetooth.le.ScanFilter> {
208
+ val filters = mutableListOf<android.bluetooth.le.ScanFilter>()
209
+
210
+ // Add service UUID filters
211
+ filter.serviceUUIDs.forEach { serviceId ->
212
+ try {
213
+ val builder = android.bluetooth.le.ScanFilter.Builder()
214
+ val uuid = UUID.fromString(serviceId)
215
+ builder.setServiceUuid(ParcelUuid(uuid))
216
+ filters.add(builder.build())
217
+ } catch (e: Exception) {
218
+ // Invalid UUID, skip
219
+ }
220
+ }
221
+
222
+ // If no specific filters, add empty filter to scan all devices
223
+ if (filters.isEmpty()) {
224
+ val builder = android.bluetooth.le.ScanFilter.Builder()
225
+ filters.add(builder.build())
226
+ }
227
+
228
+ return filters
229
+ }
230
+
231
+ private fun createGattCallback(deviceId: String): BluetoothGattCallback {
232
+ return object : BluetoothGattCallback() {
233
+ override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
234
+ val callbacks = deviceCallbacks[deviceId]
235
+
236
+ when (newState) {
237
+ BluetoothProfile.STATE_CONNECTED -> {
238
+ callbacks?.connectCallback?.invoke(true, deviceId, "")
239
+ }
240
+ BluetoothProfile.STATE_DISCONNECTED -> {
241
+ // Clean up
242
+ connectedDevices.remove(deviceId)
243
+ val interrupted = status != BluetoothGatt.GATT_SUCCESS
244
+ callbacks?.disconnectCallback?.invoke(deviceId, interrupted, if (interrupted) "Connection lost" else "")
245
+ deviceCallbacks.remove(deviceId)
246
+ gatt.close()
247
+ }
248
+ }
249
+ }
250
+
251
+ override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
252
+ val callbacks = deviceCallbacks[deviceId]
253
+ val serviceDiscoveryCallback = callbacks?.serviceDiscoveryCallback
254
+
255
+ if (status == BluetoothGatt.GATT_SUCCESS) {
256
+ serviceDiscoveryCallback?.invoke(true, "")
257
+ } else {
258
+ serviceDiscoveryCallback?.invoke(false, "Service discovery failed with status: $status")
259
+ }
260
+
261
+ // Clear the service discovery callback as it's one-time use
262
+ callbacks?.serviceDiscoveryCallback = null
263
+ }
264
+
265
+ override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
266
+ // Handle characteristic read result
267
+ val data = if (status == BluetoothGatt.GATT_SUCCESS) {
268
+ val value = characteristic.value ?: byteArrayOf()
269
+ // Create direct ByteBuffer as required by ArrayBuffer.wrap()
270
+ val directBuffer = java.nio.ByteBuffer.allocateDirect(value.size)
271
+ directBuffer.put(value)
272
+ directBuffer.flip()
273
+ ArrayBuffer.wrap(directBuffer)
274
+ } else {
275
+ ArrayBuffer.allocate(0)
276
+ }
277
+ // This will be handled by pending operations
278
+ }
279
+
280
+ override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
281
+ // Handle characteristic write result
282
+ // This will be handled by pending operations
283
+ }
284
+
285
+ override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
286
+ // Handle characteristic notifications
287
+ val characteristicId = characteristic.uuid.toString()
288
+ val value = characteristic.value ?: byteArrayOf()
289
+
290
+ // Create direct ByteBuffer as required by ArrayBuffer.wrap()
291
+ val directBuffer = java.nio.ByteBuffer.allocateDirect(value.size)
292
+ directBuffer.put(value)
293
+ directBuffer.flip()
294
+
295
+ val data = ArrayBuffer.wrap(directBuffer)
296
+
297
+ val callbacks = deviceCallbacks[deviceId]
298
+ callbacks?.characteristicSubscriptions?.get(characteristicId)?.invoke(characteristicId, data)
299
+ }
300
+
301
+ override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
302
+ // Handle descriptor write (for enabling/disabling notifications)
303
+ }
304
+ }
305
+ }
306
+
307
+ // Scanning operations
308
+ override fun startScan(filter: com.margelo.nitro.co.zyke.ble.ScanFilter, callback: (device: BLEDevice?, error: String?) -> Unit) {
309
+ try {
310
+ initializeBluetoothIfNeeded()
311
+ val adapter = bluetoothAdapter ?: return
312
+
313
+ if (!adapter.isEnabled) {
314
+ return
315
+ }
316
+
317
+ if (isCurrentlyScanning) {
318
+ return
319
+ }
320
+
321
+ // Clear discovered devices for fresh scan session
322
+ discoveredDevicesInCurrentScan.clear()
323
+
324
+ // Initialize scanner
325
+ bleScanner = adapter.bluetoothLeScanner ?: return
326
+ deviceFoundCallback = callback
327
+
328
+ // Create scan callback
329
+ scanCallback = object : ScanCallback() {
330
+ override fun onScanResult(callbackType: Int, result: ScanResult) {
331
+ val device = createBLEDeviceFromScanResult(result)
332
+
333
+ // Apply RSSI threshold filtering
334
+ if (device.rssi < filter.rssiThreshold) {
335
+ return
336
+ }
337
+
338
+ // Apply application-level duplicate filtering if needed
339
+ if (!filter.allowDuplicates) {
340
+ if (discoveredDevicesInCurrentScan.contains(device.id)) {
341
+ return // Skip duplicate
342
+ }
343
+ discoveredDevicesInCurrentScan.add(device.id)
344
+ }
345
+
346
+ callback(device, null)
347
+ }
348
+
349
+ override fun onBatchScanResults(results: MutableList<ScanResult>) {
350
+ results.forEach { result ->
351
+ val device = createBLEDeviceFromScanResult(result)
352
+
353
+ // Apply RSSI threshold filtering
354
+ if (device.rssi < filter.rssiThreshold) {
355
+ return@forEach
356
+ }
357
+
358
+ // Apply application-level duplicate filtering if needed
359
+ if (!filter.allowDuplicates) {
360
+ if (discoveredDevicesInCurrentScan.contains(device.id)) {
361
+ return@forEach // Skip duplicate
362
+ }
363
+ discoveredDevicesInCurrentScan.add(device.id)
364
+ }
365
+
366
+ callback(device, null)
367
+ }
368
+ }
369
+
370
+ override fun onScanFailed(errorCode: Int) {
371
+ val errorMessage = when (errorCode) {
372
+ ScanCallback.SCAN_FAILED_ALREADY_STARTED -> "Scan already started"
373
+ ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED -> "App registration failed"
374
+ ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED -> "Feature unsupported"
375
+ ScanCallback.SCAN_FAILED_INTERNAL_ERROR -> "Internal error"
376
+ ScanCallback.SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES -> "Out of hardware resources"
377
+ ScanCallback.SCAN_FAILED_SCANNING_TOO_FREQUENTLY -> "Scanning too frequently"
378
+ else -> "Scan failed with error code: $errorCode"
379
+ }
380
+ callback(null, errorMessage)
381
+ stopScan()
382
+ }
383
+ }
384
+
385
+ // Create scan filters and settings
386
+ val scanFilters = createAndroidScanFilters(filter)
387
+ val scanMode = when (filter.androidScanMode) {
388
+ AndroidScanMode.LOWLATENCY -> ScanSettings.SCAN_MODE_LOW_LATENCY
389
+ AndroidScanMode.LOWPOWER -> ScanSettings.SCAN_MODE_LOW_POWER
390
+ AndroidScanMode.BALANCED -> ScanSettings.SCAN_MODE_BALANCED
391
+ AndroidScanMode.OPPORTUNISTIC -> ScanSettings.SCAN_MODE_OPPORTUNISTIC
392
+ }
393
+
394
+ val scanSettingsBuilder = ScanSettings.Builder()
395
+ .setScanMode(scanMode)
396
+ .setReportDelay(0) // Report each advertisement individually
397
+
398
+ // Always use CALLBACK_TYPE_ALL_MATCHES for application-level duplicate filtering
399
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
400
+ scanSettingsBuilder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
401
+ }
402
+
403
+ val scanSettings = scanSettingsBuilder.build()
404
+
405
+ // Start scanning
406
+ bleScanner?.startScan(scanFilters, scanSettings, scanCallback)
407
+ isCurrentlyScanning = true
408
+
409
+ } catch (e: SecurityException) {
410
+ isCurrentlyScanning = false
411
+ } catch (e: Exception) {
412
+ isCurrentlyScanning = false
413
+ }
414
+ }
415
+
416
+ override fun stopScan(): Boolean {
417
+ return try {
418
+ if (scanCallback != null && isCurrentlyScanning) {
419
+ bleScanner?.stopScan(scanCallback)
420
+ }
421
+ isCurrentlyScanning = false
422
+ scanCallback = null
423
+ deviceFoundCallback = null
424
+ bleScanner = null
425
+ discoveredDevicesInCurrentScan.clear() // Clear discovered devices for next scan session
426
+ true
427
+ } catch (e: Exception) {
428
+ isCurrentlyScanning = false
429
+ scanCallback = null
430
+ deviceFoundCallback = null
431
+ bleScanner = null
432
+ discoveredDevicesInCurrentScan.clear()
433
+ false
434
+ }
435
+ }
436
+
437
+ override fun isScanning(): Boolean {
438
+ return isCurrentlyScanning
439
+ }
440
+
441
+ // Device discovery
442
+ override fun getConnectedDevices(services: Array<String>): Array<BLEDevice> {
443
+ return try {
444
+ val bluetoothManager = appContext?.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
445
+ val connectedDevices = bluetoothManager?.getConnectedDevices(BluetoothProfile.GATT) ?: emptyList()
446
+
447
+ connectedDevices.map { device ->
448
+ BLEDevice(
449
+ id = device.address,
450
+ name = device.name ?: "",
451
+ rssi = 0.0, // RSSI not available for already connected devices
452
+ manufacturerData = ManufacturerData(companyIdentifiers = emptyArray()),
453
+ serviceUUIDs = emptyArray(), // Service UUIDs not available without service discovery
454
+ isConnectable = true
455
+ )
456
+ }.toTypedArray()
457
+ } catch (e: Exception) {
458
+ emptyArray()
459
+ }
460
+ }
461
+
462
+ // Connection management
463
+ override fun connect(
464
+ deviceId: String,
465
+ callback: (success: Boolean, deviceId: String, error: String) -> Unit,
466
+ disconnectCallback: ((deviceId: String, interrupted: Boolean, error: String) -> Unit)?
467
+ ) {
468
+ try {
469
+ initializeBluetoothIfNeeded()
470
+ val adapter = bluetoothAdapter
471
+ if (adapter == null) {
472
+ callback(false, deviceId, "Bluetooth not available")
473
+ return
474
+ }
475
+
476
+ val device = adapter.getRemoteDevice(deviceId)
477
+ if (device == null) {
478
+ callback(false, deviceId, "Device not found")
479
+ return
480
+ }
481
+
482
+ // Store callbacks for this device
483
+ deviceCallbacks[deviceId] = DeviceCallbacks(
484
+ connectCallback = callback,
485
+ disconnectCallback = disconnectCallback
486
+ )
487
+
488
+ // Create GATT callback
489
+ val gattCallback = createGattCallback(deviceId)
490
+
491
+ // Connect to device
492
+ val context = appContext
493
+ if (context != null) {
494
+ val gatt = device.connectGatt(context, false, gattCallback)
495
+ connectedDevices[deviceId] = gatt
496
+ } else {
497
+ callback(false, deviceId, "Context not available")
498
+ }
499
+
500
+ } catch (e: SecurityException) {
501
+ callback(false, deviceId, "Permission denied")
502
+ } catch (e: Exception) {
503
+ callback(false, deviceId, "Connection error: ${e.message}")
504
+ }
505
+ }
506
+
507
+ override fun disconnect(deviceId: String, callback: (success: Boolean, error: String) -> Unit) {
508
+ try {
509
+ val gatt = connectedDevices[deviceId]
510
+ if (gatt != null) {
511
+ gatt.disconnect()
512
+ callback(true, "")
513
+ } else {
514
+ callback(false, "Device not connected")
515
+ }
516
+ } catch (e: Exception) {
517
+ callback(false, "Disconnect error: ${e.message}")
518
+ }
519
+ }
520
+
521
+ override fun isConnected(deviceId: String): Boolean {
522
+ return connectedDevices.containsKey(deviceId)
523
+ }
524
+
525
+ override fun requestMTU(deviceId: String, mtu: Double): Double {
526
+ return try {
527
+ val gatt = connectedDevices[deviceId]
528
+ if (gatt != null) {
529
+ val success = gatt.requestMtu(mtu.toInt())
530
+ if (success) mtu else 0.0
531
+ } else {
532
+ 0.0
533
+ }
534
+ } catch (e: Exception) {
535
+ 0.0
536
+ }
537
+ }
538
+
539
+ // Service discovery
540
+ override fun discoverServices(deviceId: String, callback: (success: Boolean, error: String) -> Unit) {
541
+ try {
542
+ val gatt = connectedDevices[deviceId]
543
+ if (gatt != null) {
544
+ val callbacks = deviceCallbacks[deviceId]
545
+ if (callbacks != null) {
546
+ // Store the callback for when service discovery completes
547
+ callbacks.serviceDiscoveryCallback = callback
548
+
549
+ // Start service discovery
550
+ val success = gatt.discoverServices()
551
+ if (!success) {
552
+ // Clear callback and report failure immediately
553
+ callbacks.serviceDiscoveryCallback = null
554
+ callback(false, "Failed to start service discovery")
555
+ }
556
+ // If success, the callback will be invoked in onServicesDiscovered
557
+ } else {
558
+ callback(false, "Device callback not found")
559
+ }
560
+ } else {
561
+ callback(false, "Device not connected")
562
+ }
563
+ } catch (e: Exception) {
564
+ callback(false, "Service discovery error: ${e.message}")
565
+ }
566
+ }
567
+
568
+ override fun getServices(deviceId: String): Array<String> {
569
+ return try {
570
+ val gatt = connectedDevices[deviceId]
571
+ gatt?.services?.map { service ->
572
+ service.uuid.toString()
573
+ }?.toTypedArray() ?: emptyArray()
574
+ } catch (e: Exception) {
575
+ emptyArray()
576
+ }
577
+ }
578
+
579
+ override fun getCharacteristics(deviceId: String, serviceId: String): Array<String> {
580
+ return try {
581
+ val gatt = connectedDevices[deviceId]
582
+ val service = gatt?.getService(UUID.fromString(serviceId))
583
+ service?.characteristics?.map { characteristic ->
584
+ characteristic.uuid.toString()
585
+ }?.toTypedArray() ?: emptyArray()
586
+ } catch (e: Exception) {
587
+ emptyArray()
588
+ }
589
+ }
590
+
591
+ // Characteristic operations
592
+ override fun readCharacteristic(
593
+ deviceId: String,
594
+ serviceId: String,
595
+ characteristicId: String,
596
+ callback: (success: Boolean, data: ArrayBuffer, error: String) -> Unit
597
+ ) {
598
+ try {
599
+ val gatt = connectedDevices[deviceId]
600
+ if (gatt == null) {
601
+ callback(false, ArrayBuffer.allocate(0), "Device not connected")
602
+ return
603
+ }
604
+
605
+ val service = gatt.getService(UUID.fromString(serviceId))
606
+ if (service == null) {
607
+ callback(false, ArrayBuffer.allocate(0), "Service not found")
608
+ return
609
+ }
610
+
611
+ val characteristic = service.getCharacteristic(UUID.fromString(characteristicId))
612
+ if (characteristic == null) {
613
+ callback(false, ArrayBuffer.allocate(0), "Characteristic not found")
614
+ return
615
+ }
616
+
617
+ val success = gatt.readCharacteristic(characteristic)
618
+ if (!success) {
619
+ callback(false, ArrayBuffer.allocate(0), "Failed to start read operation")
620
+ }
621
+ // The actual result will come in onCharacteristicRead callback
622
+ // For now, we'll return the cached value
623
+ val data = characteristic.value?.let { value ->
624
+ // Create direct ByteBuffer as required by ArrayBuffer.wrap()
625
+ val directBuffer = java.nio.ByteBuffer.allocateDirect(value.size)
626
+ directBuffer.put(value)
627
+ directBuffer.flip()
628
+ ArrayBuffer.wrap(directBuffer)
629
+ } ?: ArrayBuffer.allocate(0)
630
+ callback(success, data, if (success) "" else "Read operation failed")
631
+
632
+ } catch (e: Exception) {
633
+ callback(false, ArrayBuffer.allocate(0), "Read error: ${e.message}")
634
+ }
635
+ }
636
+
637
+ override fun writeCharacteristic(
638
+ deviceId: String,
639
+ serviceId: String,
640
+ characteristicId: String,
641
+ data: ArrayBuffer,
642
+ withResponse: Boolean,
643
+ callback: (success: Boolean, error: String) -> Unit
644
+ ) {
645
+ try {
646
+ val gatt = connectedDevices[deviceId]
647
+ if (gatt == null) {
648
+ callback(false, "Device not connected")
649
+ return
650
+ }
651
+
652
+ val service = gatt.getService(UUID.fromString(serviceId))
653
+ if (service == null) {
654
+ callback(false, "Service not found")
655
+ return
656
+ }
657
+
658
+ val characteristic = service.getCharacteristic(UUID.fromString(characteristicId))
659
+ if (characteristic == null) {
660
+ callback(false, "Characteristic not found")
661
+ return
662
+ }
663
+
664
+ // Convert ArrayBuffer to byte array using proper Nitro API
665
+ val byteBuffer = data.getBuffer(copyIfNeeded = true)
666
+ val bytes = ByteArray(byteBuffer.remaining())
667
+ byteBuffer.get(bytes)
668
+
669
+ characteristic.value = bytes
670
+ characteristic.writeType = if (withResponse) {
671
+ BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
672
+ } else {
673
+ BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
674
+ }
675
+
676
+ val success = gatt.writeCharacteristic(characteristic)
677
+ callback(success, if (success) "" else "Write operation failed")
678
+
679
+ } catch (e: Exception) {
680
+ callback(false, "Write error: ${e.message}")
681
+ }
682
+ }
683
+
684
+ override fun subscribeToCharacteristic(
685
+ deviceId: String,
686
+ serviceId: String,
687
+ characteristicId: String,
688
+ updateCallback: (characteristicId: String, data: ArrayBuffer) -> Unit,
689
+ resultCallback: (success: Boolean, error: String) -> Unit
690
+ ) {
691
+ try {
692
+ val gatt = connectedDevices[deviceId]
693
+ if (gatt == null) {
694
+ resultCallback(false, "Device not connected")
695
+ return
696
+ }
697
+
698
+ val service = gatt.getService(UUID.fromString(serviceId))
699
+ if (service == null) {
700
+ resultCallback(false, "Service not found")
701
+ return
702
+ }
703
+
704
+ val characteristic = service.getCharacteristic(UUID.fromString(characteristicId))
705
+ if (characteristic == null) {
706
+ resultCallback(false, "Characteristic not found")
707
+ return
708
+ }
709
+
710
+ // Enable notifications
711
+ val success = gatt.setCharacteristicNotification(characteristic, true)
712
+ if (!success) {
713
+ resultCallback(false, "Failed to enable notifications")
714
+ return
715
+ }
716
+
717
+ // Store the callback
718
+ val callbacks = deviceCallbacks[deviceId]
719
+ if (callbacks != null) {
720
+ callbacks.characteristicSubscriptions[characteristicId] = updateCallback
721
+ }
722
+
723
+ // Write to the descriptor to enable notifications on the device
724
+ val descriptor = characteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
725
+ if (descriptor != null) {
726
+ descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
727
+ gatt.writeDescriptor(descriptor)
728
+ }
729
+
730
+ resultCallback(true, "")
731
+
732
+ } catch (e: Exception) {
733
+ resultCallback(false, "Subscription error: ${e.message}")
734
+ }
735
+ }
736
+
737
+ override fun unsubscribeFromCharacteristic(
738
+ deviceId: String,
739
+ serviceId: String,
740
+ characteristicId: String,
741
+ callback: (success: Boolean, error: String) -> Unit
742
+ ) {
743
+ try {
744
+ val gatt = connectedDevices[deviceId]
745
+ if (gatt == null) {
746
+ callback(false, "Device not connected")
747
+ return
748
+ }
749
+
750
+ val service = gatt.getService(UUID.fromString(serviceId))
751
+ if (service == null) {
752
+ callback(false, "Service not found")
753
+ return
754
+ }
755
+
756
+ val characteristic = service.getCharacteristic(UUID.fromString(characteristicId))
757
+ if (characteristic == null) {
758
+ callback(false, "Characteristic not found")
759
+ return
760
+ }
761
+
762
+ // Disable notifications
763
+ val success = gatt.setCharacteristicNotification(characteristic, false)
764
+ if (!success) {
765
+ callback(false, "Failed to disable notifications")
766
+ return
767
+ }
768
+
769
+ // Remove the callback
770
+ val callbacks = deviceCallbacks[deviceId]
771
+ callbacks?.characteristicSubscriptions?.remove(characteristicId)
772
+
773
+ // Write to the descriptor to disable notifications on the device
774
+ val descriptor = characteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
775
+ if (descriptor != null) {
776
+ descriptor.value = BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
777
+ gatt.writeDescriptor(descriptor)
778
+ }
779
+
780
+ callback(true, "")
781
+
782
+ } catch (e: Exception) {
783
+ callback(false, "Unsubscription error: ${e.message}")
784
+ }
785
+ }
786
+
787
+ // Bluetooth state management
788
+ override fun requestBluetoothEnable(callback: (success: Boolean, error: String) -> Unit) {
789
+ try {
790
+ initializeBluetoothIfNeeded()
791
+ val adapter = bluetoothAdapter
792
+ if (adapter == null) {
793
+ callback(false, "Bluetooth not supported on this device")
794
+ return
795
+ }
796
+
797
+ if (adapter.isEnabled) {
798
+ callback(true, "")
799
+ return
800
+ }
801
+
802
+ // Request user to enable Bluetooth
803
+ try {
804
+ val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
805
+ appContext?.let { ctx ->
806
+ enableBtIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
807
+ ctx.startActivity(enableBtIntent)
808
+ callback(true, "Bluetooth enable request sent")
809
+ } ?: callback(false, "Context not available")
810
+ } catch (securityException: SecurityException) {
811
+ callback(false, "Permission denied: Cannot request Bluetooth enable. Please check app permissions.")
812
+ }
813
+
814
+ } catch (e: Exception) {
815
+ callback(false, "Error requesting Bluetooth enable: ${e.message}")
816
+ }
817
+ }
818
+
819
+ override fun state(): BLEState {
820
+ // Check permissions first
821
+ if (!hasBluetoothPermissions()) {
822
+ return BLEState.UNAUTHORIZED
823
+ }
824
+
825
+ initializeBluetoothIfNeeded()
826
+ val adapter = bluetoothAdapter ?: return BLEState.UNSUPPORTED
827
+
828
+ return try {
829
+ bluetoothStateToBlEState(adapter.state)
830
+ } catch (securityException: SecurityException) {
831
+ BLEState.UNAUTHORIZED
832
+ }
833
+ }
834
+
835
+ override fun subscribeToStateChange(stateCallback: (state: BLEState) -> Unit): OperationResult {
836
+ try {
837
+ val context = appContext ?: return OperationResult(success = false, error = "Context not available")
838
+
839
+ // Unsubscribe from any existing subscription
840
+ unsubscribeFromStateChange()
841
+
842
+ // Store the callback
843
+ this.stateCallback = stateCallback
844
+
845
+ // Create and register broadcast receiver
846
+ bluetoothStateReceiver = createBluetoothStateReceiver()
847
+ val intentFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
848
+
849
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
850
+ context.registerReceiver(bluetoothStateReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED)
851
+ } else {
852
+ context.registerReceiver(bluetoothStateReceiver, intentFilter)
853
+ }
854
+
855
+ return OperationResult(success = true, error = null)
856
+ } catch (e: Exception) {
857
+ return OperationResult(success = false, error = "Error subscribing to state changes: ${e.message}")
858
+ }
859
+ }
860
+
861
+ override fun unsubscribeFromStateChange(): OperationResult {
862
+ try {
863
+ // Clear the callback
864
+ this.stateCallback = null
865
+
866
+ // Unregister broadcast receiver if it exists
867
+ bluetoothStateReceiver?.let { receiver ->
868
+ val context = appContext
869
+ if (context != null) {
870
+ try {
871
+ context.unregisterReceiver(receiver)
872
+ } catch (e: IllegalArgumentException) {
873
+ // Receiver was not registered, ignore
874
+ }
875
+ }
876
+ bluetoothStateReceiver = null
877
+ }
878
+
879
+ return OperationResult(success = true, error = null)
880
+ } catch (e: Exception) {
881
+ return OperationResult(success = false, error = "Error unsubscribing from state changes: ${e.message}")
882
+ }
883
+ }
884
+
885
+ override fun openSettings(): Promise<Unit> {
886
+ val promise = Promise<Unit>()
887
+ try {
888
+ val intent = Intent(Settings.ACTION_BLUETOOTH_SETTINGS)
889
+ appContext?.let { ctx ->
890
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
891
+ ctx.startActivity(intent)
892
+ promise.resolve(Unit)
893
+ } ?: promise.reject(Exception("Context not available"))
894
+ } catch (e: Exception) {
895
+ promise.reject(Exception("Error opening Bluetooth settings: ${e.message}"))
896
+ }
897
+ return promise
898
+ }
899
+ }