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.
- package/README.md +89 -28
- package/android/CMakeLists.txt +32 -0
- package/android/build.gradle +140 -0
- package/android/fix-prefab.gradle +51 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/co/zyke/ble/BleNitroBleManager.kt +899 -0
- package/android/src/main/java/com/margelo/nitro/co/zyke/ble/BleNitroPackage.kt +38 -0
- package/ios/BleNitroBleManager.swift +7 -3
- package/lib/commonjs/index.d.ts +15 -3
- package/lib/commonjs/index.d.ts.map +1 -1
- package/lib/commonjs/index.js +46 -28
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/specs/NativeBleNitro.nitro.d.ts +8 -1
- package/lib/commonjs/specs/NativeBleNitro.nitro.d.ts.map +1 -1
- package/lib/commonjs/specs/NativeBleNitro.nitro.js +8 -1
- package/lib/commonjs/specs/NativeBleNitro.nitro.js.map +1 -1
- package/lib/index.d.ts +15 -3
- package/lib/index.js +44 -26
- package/lib/specs/NativeBleNitro.nitro.d.ts +8 -1
- package/lib/specs/NativeBleNitro.nitro.js +7 -0
- package/nitrogen/generated/android/BleNitroOnLoad.cpp +2 -2
- package/nitrogen/generated/android/c++/JAndroidScanMode.hpp +65 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_BLEDevice__std__optional_std__string_.hpp +86 -0
- package/nitrogen/generated/android/c++/JHybridNativeBleNitroSpec.cpp +8 -4
- package/nitrogen/generated/android/c++/JHybridNativeBleNitroSpec.hpp +1 -1
- package/nitrogen/generated/android/c++/JScanFilter.hpp +8 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/co/zyke/ble/AndroidScanMode.kt +23 -0
- 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
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/co/zyke/ble/HybridNativeBleNitroSpec.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/co/zyke/ble/ScanFilter.kt +4 -1
- package/nitrogen/generated/ios/BleNitro-Swift-Cxx-Bridge.cpp +5 -5
- package/nitrogen/generated/ios/BleNitro-Swift-Cxx-Bridge.hpp +30 -21
- package/nitrogen/generated/ios/BleNitro-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridNativeBleNitroSpecSwift.hpp +5 -2
- package/nitrogen/generated/ios/swift/AndroidScanMode.swift +48 -0
- package/nitrogen/generated/ios/swift/Func_void_std__optional_BLEDevice__std__optional_std__string_.swift +59 -0
- package/nitrogen/generated/ios/swift/HybridNativeBleNitroSpec.swift +1 -1
- package/nitrogen/generated/ios/swift/HybridNativeBleNitroSpec_cxx.swift +17 -5
- package/nitrogen/generated/ios/swift/ScanFilter.swift +13 -2
- package/nitrogen/generated/shared/c++/AndroidScanMode.hpp +64 -0
- package/nitrogen/generated/shared/c++/HybridNativeBleNitroSpec.hpp +3 -3
- package/nitrogen/generated/shared/c++/ScanFilter.hpp +9 -3
- package/package.json +1 -1
- package/plugin/build/index.d.ts +2 -0
- package/plugin/build/index.js +2 -0
- package/plugin/build/withBleNitro.d.ts +5 -1
- package/plugin/build/withBleNitro.js +18 -7
- package/src/__tests__/index.test.ts +45 -11
- package/src/index.ts +55 -27
- package/src/specs/NativeBleNitro.nitro.ts +9 -1
- package/nitrogen/generated/android/c++/JFunc_void_BLEDevice.hpp +0 -85
- 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
|
+
}
|