munim-bluetooth 0.3.27 → 0.4.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.
- package/CHANGELOG.md +16 -0
- package/README.md +476 -74
- package/android/gradle.properties +2 -2
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/cpp/cpp-adapter.cpp +4 -1
- package/android/src/main/java/com/munimbluetooth/HybridMunimBluetooth.kt +2006 -209
- package/android/src/main/java/com/munimbluetooth/MunimBluetoothBackgroundService.kt +561 -53
- package/app.plugin.js +155 -0
- package/ios/HybridMunimBluetooth.swift +2123 -298
- package/ios/MunimBluetoothEventEmitter.swift +68 -8
- package/lib/commonjs/index.js +272 -11
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +243 -11
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +310 -7
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/munim-bluetooth.nitro.d.ts +219 -5
- package/lib/typescript/src/specs/munim-bluetooth.nitro.d.ts.map +1 -1
- package/nitro.json +9 -3
- package/nitrogen/generated/android/c++/JAdvertisingDataTypes.hpp +96 -96
- package/nitrogen/generated/android/c++/JAdvertisingOptions.hpp +8 -8
- package/nitrogen/generated/android/c++/JBackgroundSessionOptions.hpp +8 -8
- package/nitrogen/generated/android/c++/JBluetoothCapabilities.hpp +105 -0
- package/nitrogen/generated/android/c++/JBluetoothPhy.hpp +61 -0
- package/nitrogen/generated/android/c++/JBluetoothPhyOption.hpp +61 -0
- package/nitrogen/generated/android/c++/JBondState.hpp +64 -0
- package/nitrogen/generated/android/c++/JDescriptorValue.hpp +69 -0
- package/nitrogen/generated/android/c++/JExtendedAdvertisingOptions.hpp +131 -0
- package/nitrogen/generated/android/c++/JGATTCharacteristic.hpp +35 -11
- package/nitrogen/generated/android/c++/JGATTDescriptor.hpp +85 -0
- package/nitrogen/generated/android/c++/JGATTService.hpp +33 -9
- package/nitrogen/generated/android/c++/JHybridMunimBluetoothSpec.cpp +422 -12
- package/nitrogen/generated/android/c++/JHybridMunimBluetoothSpec.hpp +29 -0
- package/nitrogen/generated/android/c++/JL2CAPChannel.hpp +66 -0
- package/nitrogen/generated/android/c++/JMultipeerDiscoveryInfoEntry.hpp +61 -0
- package/nitrogen/generated/android/c++/JMultipeerEncryptionPreference.hpp +61 -0
- package/nitrogen/generated/android/c++/JMultipeerPeer.hpp +93 -0
- package/nitrogen/generated/android/c++/JMultipeerPeerState.hpp +61 -0
- package/nitrogen/generated/android/c++/JMultipeerSessionOptions.hpp +105 -0
- package/nitrogen/generated/android/c++/JPhyStatus.hpp +62 -0
- package/nitrogen/generated/android/c++/JScanOptions.hpp +8 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/AdvertisingDataTypes.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/AdvertisingOptions.kt +19 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BackgroundSessionOptions.kt +27 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BluetoothCapabilities.kt +111 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BluetoothPhy.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BluetoothPhyOption.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BondState.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/CharacteristicValue.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/DescriptorValue.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/ExtendedAdvertisingOptions.kt +111 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/GATTCharacteristic.kt +25 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/GATTDescriptor.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/GATTService.kt +23 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/HybridMunimBluetoothSpec.kt +138 -22
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/L2CAPChannel.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/MultipeerDiscoveryInfoEntry.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/MultipeerEncryptionPreference.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/MultipeerPeer.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/MultipeerPeerState.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/MultipeerSessionOptions.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/PhyStatus.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/ScanOptions.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/ServiceDataEntry.kt +15 -0
- package/nitrogen/generated/ios/MunimBluetooth+autolinking.rb +2 -0
- package/nitrogen/generated/ios/MunimBluetooth-Swift-Cxx-Bridge.cpp +61 -5
- package/nitrogen/generated/ios/MunimBluetooth-Swift-Cxx-Bridge.hpp +494 -49
- package/nitrogen/generated/ios/MunimBluetooth-Swift-Cxx-Umbrella.hpp +42 -0
- package/nitrogen/generated/ios/c++/HybridMunimBluetoothSpecSwift.hpp +254 -0
- package/nitrogen/generated/ios/swift/BluetoothCapabilities.swift +89 -0
- package/nitrogen/generated/ios/swift/BluetoothPhy.swift +44 -0
- package/nitrogen/generated/ios/swift/BluetoothPhyOption.swift +44 -0
- package/nitrogen/generated/ios/swift/BondState.swift +48 -0
- package/nitrogen/generated/ios/swift/DescriptorValue.swift +44 -0
- package/nitrogen/generated/ios/swift/ExtendedAdvertisingOptions.swift +243 -0
- package/nitrogen/generated/ios/swift/Func_void_BluetoothCapabilities.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_BondState.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_DescriptorValue.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_L2CAPChannel.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_PhyStatus.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_MultipeerPeer_.swift +46 -0
- package/nitrogen/generated/ios/swift/GATTCharacteristic.swift +25 -1
- package/nitrogen/generated/ios/swift/GATTDescriptor.swift +71 -0
- package/nitrogen/generated/ios/swift/GATTService.swift +25 -1
- package/nitrogen/generated/ios/swift/HybridMunimBluetoothSpec.swift +29 -0
- package/nitrogen/generated/ios/swift/HybridMunimBluetoothSpec_cxx.swift +556 -23
- package/nitrogen/generated/ios/swift/L2CAPChannel.swift +52 -0
- package/nitrogen/generated/ios/swift/MultipeerDiscoveryInfoEntry.swift +34 -0
- package/nitrogen/generated/ios/swift/MultipeerEncryptionPreference.swift +44 -0
- package/nitrogen/generated/ios/swift/MultipeerPeer.swift +63 -0
- package/nitrogen/generated/ios/swift/MultipeerPeerState.swift +44 -0
- package/nitrogen/generated/ios/swift/MultipeerSessionOptions.swift +136 -0
- package/nitrogen/generated/ios/swift/PhyStatus.swift +34 -0
- package/nitrogen/generated/shared/c++/BluetoothCapabilities.hpp +131 -0
- package/nitrogen/generated/shared/c++/BluetoothPhy.hpp +80 -0
- package/nitrogen/generated/shared/c++/BluetoothPhyOption.hpp +80 -0
- package/nitrogen/generated/shared/c++/BondState.hpp +84 -0
- package/nitrogen/generated/shared/c++/DescriptorValue.hpp +95 -0
- package/nitrogen/generated/shared/c++/ExtendedAdvertisingOptions.hpp +138 -0
- package/nitrogen/generated/shared/c++/GATTCharacteristic.hpp +9 -3
- package/nitrogen/generated/shared/c++/GATTDescriptor.hpp +93 -0
- package/nitrogen/generated/shared/c++/GATTService.hpp +7 -2
- package/nitrogen/generated/shared/c++/HybridMunimBluetoothSpec.cpp +29 -0
- package/nitrogen/generated/shared/c++/HybridMunimBluetoothSpec.hpp +61 -2
- package/nitrogen/generated/shared/c++/L2CAPChannel.hpp +92 -0
- package/nitrogen/generated/shared/c++/MultipeerDiscoveryInfoEntry.hpp +87 -0
- package/nitrogen/generated/shared/c++/MultipeerEncryptionPreference.hpp +80 -0
- package/nitrogen/generated/shared/c++/MultipeerPeer.hpp +102 -0
- package/nitrogen/generated/shared/c++/MultipeerPeerState.hpp +80 -0
- package/nitrogen/generated/shared/c++/MultipeerSessionOptions.hpp +114 -0
- package/nitrogen/generated/shared/c++/PhyStatus.hpp +88 -0
- package/package.json +22 -11
- package/src/index.ts +416 -31
- package/src/specs/munim-bluetooth.nitro.ts +298 -14
|
@@ -11,9 +11,15 @@ import android.bluetooth.BluetoothGattServerCallback
|
|
|
11
11
|
import android.bluetooth.BluetoothGattService
|
|
12
12
|
import android.bluetooth.BluetoothManager
|
|
13
13
|
import android.bluetooth.BluetoothProfile
|
|
14
|
+
import android.bluetooth.BluetoothServerSocket
|
|
15
|
+
import android.bluetooth.BluetoothSocket
|
|
16
|
+
import android.bluetooth.BluetoothStatusCodes
|
|
14
17
|
import android.bluetooth.le.AdvertiseCallback
|
|
15
18
|
import android.bluetooth.le.AdvertiseData
|
|
16
19
|
import android.bluetooth.le.AdvertiseSettings
|
|
20
|
+
import android.bluetooth.le.AdvertisingSet
|
|
21
|
+
import android.bluetooth.le.AdvertisingSetCallback
|
|
22
|
+
import android.bluetooth.le.AdvertisingSetParameters
|
|
17
23
|
import android.bluetooth.le.BluetoothLeAdvertiser
|
|
18
24
|
import android.bluetooth.le.BluetoothLeScanner
|
|
19
25
|
import android.bluetooth.le.ScanCallback
|
|
@@ -21,13 +27,16 @@ import android.bluetooth.le.ScanFilter
|
|
|
21
27
|
import android.bluetooth.le.ScanRecord
|
|
22
28
|
import android.bluetooth.le.ScanResult
|
|
23
29
|
import android.bluetooth.le.ScanSettings
|
|
30
|
+
import android.content.BroadcastReceiver
|
|
24
31
|
import android.content.Context
|
|
25
32
|
import android.content.Intent
|
|
33
|
+
import android.content.IntentFilter
|
|
26
34
|
import android.content.pm.PackageManager
|
|
27
35
|
import android.os.Build
|
|
28
36
|
import android.os.ParcelUuid
|
|
29
37
|
import android.util.Log
|
|
30
38
|
import com.facebook.react.bridge.Arguments
|
|
39
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
31
40
|
import com.facebook.react.bridge.WritableArray
|
|
32
41
|
import com.facebook.react.bridge.WritableMap
|
|
33
42
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
@@ -38,10 +47,21 @@ import com.margelo.nitro.core.Promise
|
|
|
38
47
|
import com.margelo.nitro.munimbluetooth.AdvertisingDataTypes
|
|
39
48
|
import com.margelo.nitro.munimbluetooth.AdvertisingOptions
|
|
40
49
|
import com.margelo.nitro.munimbluetooth.BackgroundSessionOptions
|
|
50
|
+
import com.margelo.nitro.munimbluetooth.BluetoothCapabilities
|
|
51
|
+
import com.margelo.nitro.munimbluetooth.BluetoothPhy
|
|
52
|
+
import com.margelo.nitro.munimbluetooth.BluetoothPhyOption
|
|
53
|
+
import com.margelo.nitro.munimbluetooth.BondState
|
|
41
54
|
import com.margelo.nitro.munimbluetooth.CharacteristicValue
|
|
55
|
+
import com.margelo.nitro.munimbluetooth.DescriptorValue
|
|
56
|
+
import com.margelo.nitro.munimbluetooth.ExtendedAdvertisingOptions
|
|
42
57
|
import com.margelo.nitro.munimbluetooth.GATTCharacteristic
|
|
58
|
+
import com.margelo.nitro.munimbluetooth.GATTDescriptor
|
|
43
59
|
import com.margelo.nitro.munimbluetooth.GATTService
|
|
44
60
|
import com.margelo.nitro.munimbluetooth.HybridMunimBluetoothSpec
|
|
61
|
+
import com.margelo.nitro.munimbluetooth.L2CAPChannel
|
|
62
|
+
import com.margelo.nitro.munimbluetooth.MultipeerPeer
|
|
63
|
+
import com.margelo.nitro.munimbluetooth.MultipeerSessionOptions
|
|
64
|
+
import com.margelo.nitro.munimbluetooth.PhyStatus
|
|
45
65
|
import com.margelo.nitro.munimbluetooth.ScanMode
|
|
46
66
|
import com.margelo.nitro.munimbluetooth.ScanOptions
|
|
47
67
|
import com.margelo.nitro.munimbluetooth.ServiceDataEntry
|
|
@@ -52,6 +72,9 @@ import kotlinx.coroutines.Job
|
|
|
52
72
|
import kotlinx.coroutines.SupervisorJob
|
|
53
73
|
import kotlinx.coroutines.delay
|
|
54
74
|
import kotlinx.coroutines.launch
|
|
75
|
+
import org.json.JSONArray
|
|
76
|
+
import org.json.JSONObject
|
|
77
|
+
import java.io.IOException
|
|
55
78
|
import java.util.UUID
|
|
56
79
|
|
|
57
80
|
class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
@@ -59,6 +82,8 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
59
82
|
|
|
60
83
|
private var advertiser: BluetoothLeAdvertiser? = null
|
|
61
84
|
private var advertiseCallback: AdvertiseCallback? = null
|
|
85
|
+
private val extendedAdvertisingSets = mutableMapOf<String, AdvertisingSet>()
|
|
86
|
+
private val extendedAdvertisingCallbacks = mutableMapOf<String, AdvertisingSetCallback>()
|
|
62
87
|
private var gattServer: BluetoothGattServer? = null
|
|
63
88
|
private var gattServerReady = false
|
|
64
89
|
private var advertiseJob: Job? = null
|
|
@@ -67,6 +92,7 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
67
92
|
private var currentLocalName: String? = null
|
|
68
93
|
private var currentManufacturerData: String? = null
|
|
69
94
|
private var previousAdapterName: String? = null
|
|
95
|
+
private var configuredServices: Array<GATTService> = emptyArray()
|
|
70
96
|
private var bluetoothManager: BluetoothManager? = null
|
|
71
97
|
private var bluetoothAdapter: BluetoothAdapter? = null
|
|
72
98
|
|
|
@@ -79,9 +105,28 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
79
105
|
private val pendingServiceDiscoveries = mutableMapOf<String, Promise<Array<GATTService>>>()
|
|
80
106
|
private val pendingReads = mutableMapOf<String, Promise<CharacteristicValue>>()
|
|
81
107
|
private val pendingWrites = mutableMapOf<String, Promise<Unit>>()
|
|
108
|
+
private val pendingDescriptorReads = mutableMapOf<String, Promise<DescriptorValue>>()
|
|
109
|
+
private val pendingDescriptorWrites = mutableMapOf<String, Promise<Unit>>()
|
|
110
|
+
private val pendingMtuRequests = mutableMapOf<String, Promise<Double>>()
|
|
111
|
+
private val pendingPhyReads = mutableMapOf<String, Promise<PhyStatus>>()
|
|
82
112
|
private val pendingRssiReads = mutableMapOf<String, Promise<Double>>()
|
|
113
|
+
private val pendingConnectionTimeouts = mutableMapOf<String, Job>()
|
|
114
|
+
private val pendingConnectionAttempts = mutableMapOf<String, Int>()
|
|
115
|
+
private val pendingOperationTimeouts = mutableMapOf<String, Job>()
|
|
116
|
+
private val pendingConnectionGatts = mutableMapOf<String, BluetoothGatt>()
|
|
83
117
|
private val lastCharacteristicValues = mutableMapOf<String, CharacteristicValue>()
|
|
84
118
|
private val lastRssiValues = mutableMapOf<String, Double>()
|
|
119
|
+
private val subscribedDevices = mutableMapOf<UUID, MutableSet<BluetoothDevice>>()
|
|
120
|
+
private var classicScanReceiver: BroadcastReceiver? = null
|
|
121
|
+
private val classicDevices = mutableMapOf<String, BluetoothDevice>()
|
|
122
|
+
private val classicSockets = mutableMapOf<String, BluetoothSocket>()
|
|
123
|
+
private val classicReadJobs = mutableMapOf<String, Job>()
|
|
124
|
+
private val classicServerSockets = mutableMapOf<String, BluetoothServerSocket>()
|
|
125
|
+
private val classicServerJobs = mutableMapOf<String, Job>()
|
|
126
|
+
private val l2capServerSockets = mutableMapOf<Int, BluetoothServerSocket>()
|
|
127
|
+
private val l2capAcceptJobs = mutableMapOf<Int, Job>()
|
|
128
|
+
private val l2capSockets = mutableMapOf<String, BluetoothSocket>()
|
|
129
|
+
private val l2capReadJobs = mutableMapOf<String, Job>()
|
|
85
130
|
private val eventEmitter = NitroEventEmitter(TAG)
|
|
86
131
|
private var nextPermissionRequestCode = BLUETOOTH_PERMISSION_REQUEST_CODE
|
|
87
132
|
|
|
@@ -163,7 +208,11 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
163
208
|
}
|
|
164
209
|
|
|
165
210
|
if (!gattServerReady) {
|
|
166
|
-
|
|
211
|
+
if (configuredServices.isNotEmpty()) {
|
|
212
|
+
setServices(configuredServices)
|
|
213
|
+
} else {
|
|
214
|
+
setServicesFromOptions(options.serviceUUIDs)
|
|
215
|
+
}
|
|
167
216
|
}
|
|
168
217
|
restartAdvertising(delayMs = 300L)
|
|
169
218
|
}
|
|
@@ -188,8 +237,21 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
188
237
|
advertiseCallback?.let { callback ->
|
|
189
238
|
advertiser?.stopAdvertising(callback)
|
|
190
239
|
}
|
|
240
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
241
|
+
val activeAdvertiser = advertiser ?: bluetoothAdapter?.bluetoothLeAdvertiser
|
|
242
|
+
extendedAdvertisingCallbacks.values.forEach { callback ->
|
|
243
|
+
activeAdvertiser?.stopAdvertisingSet(callback)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
extendedAdvertisingCallbacks.clear()
|
|
247
|
+
extendedAdvertisingSets.clear()
|
|
191
248
|
advertiseCallback = null
|
|
192
249
|
advertiser = null
|
|
250
|
+
gattServer?.clearServices()
|
|
251
|
+
gattServer?.close()
|
|
252
|
+
gattServer = null
|
|
253
|
+
gattServerReady = false
|
|
254
|
+
subscribedDevices.clear()
|
|
193
255
|
currentAdvertisingData = null
|
|
194
256
|
currentServiceUUIDs = emptyArray()
|
|
195
257
|
currentLocalName = null
|
|
@@ -202,6 +264,7 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
202
264
|
return
|
|
203
265
|
}
|
|
204
266
|
|
|
267
|
+
configuredServices = services
|
|
205
268
|
ensureBluetoothManager()
|
|
206
269
|
gattServerReady = false
|
|
207
270
|
|
|
@@ -212,6 +275,8 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
212
275
|
gattServer = manager.openGattServer(context, buildGattServerCallback())
|
|
213
276
|
gattServer?.clearServices()
|
|
214
277
|
|
|
278
|
+
val nativeServices = linkedMapOf<String, BluetoothGattService>()
|
|
279
|
+
|
|
215
280
|
for (serviceData in services) {
|
|
216
281
|
val service = BluetoothGattService(
|
|
217
282
|
UUID.fromString(serviceData.uuid),
|
|
@@ -226,17 +291,71 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
226
291
|
BluetoothGattCharacteristic.PERMISSION_WRITE
|
|
227
292
|
)
|
|
228
293
|
characteristicData.value?.let { value ->
|
|
229
|
-
characteristic
|
|
294
|
+
setCharacteristicValue(characteristic, hexStringToByteArray(value) ?: value.toByteArray())
|
|
295
|
+
}
|
|
296
|
+
characteristicData.descriptors?.forEach { descriptorData ->
|
|
297
|
+
val descriptor = BluetoothGattDescriptor(
|
|
298
|
+
UUID.fromString(descriptorData.uuid),
|
|
299
|
+
descriptorPermissionsFromArray(descriptorData.permissions)
|
|
300
|
+
)
|
|
301
|
+
descriptorData.value?.let { value ->
|
|
302
|
+
setDescriptorValue(descriptor, hexStringToByteArray(value) ?: value.toByteArray())
|
|
303
|
+
}
|
|
304
|
+
characteristic.addDescriptor(descriptor)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
val hasClientConfigDescriptor = characteristic.descriptors.any {
|
|
308
|
+
it.uuid == CLIENT_CHARACTERISTIC_CONFIG_UUID
|
|
309
|
+
}
|
|
310
|
+
if (supportsNotifyOrIndicate(characteristic) && !hasClientConfigDescriptor) {
|
|
311
|
+
characteristic.addDescriptor(
|
|
312
|
+
BluetoothGattDescriptor(
|
|
313
|
+
CLIENT_CHARACTERISTIC_CONFIG_UUID,
|
|
314
|
+
BluetoothGattDescriptor.PERMISSION_READ or
|
|
315
|
+
BluetoothGattDescriptor.PERMISSION_WRITE
|
|
316
|
+
)
|
|
317
|
+
)
|
|
230
318
|
}
|
|
231
319
|
service.addCharacteristic(characteristic)
|
|
232
320
|
}
|
|
233
321
|
|
|
322
|
+
nativeServices[serviceData.uuid.lowercase()] = service
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
for (serviceData in services) {
|
|
326
|
+
val service = nativeServices[serviceData.uuid.lowercase()] ?: continue
|
|
327
|
+
serviceData.includedServices?.forEach { includedServiceUuid ->
|
|
328
|
+
nativeServices[includedServiceUuid.lowercase()]?.let { includedService ->
|
|
329
|
+
service.addService(includedService)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
nativeServices.values.forEach { service ->
|
|
234
335
|
gattServer?.addService(service)
|
|
235
336
|
}
|
|
236
337
|
|
|
237
338
|
gattServerReady = true
|
|
238
339
|
}
|
|
239
340
|
|
|
341
|
+
override fun updateCharacteristicValue(
|
|
342
|
+
serviceUUID: String,
|
|
343
|
+
characteristicUUID: String,
|
|
344
|
+
value: String,
|
|
345
|
+
notify: Boolean?
|
|
346
|
+
): Promise<Unit> {
|
|
347
|
+
val payload = hexStringToByteArray(value)
|
|
348
|
+
?: return Promise.rejected(IllegalArgumentException("Value must be a hex string"))
|
|
349
|
+
val characteristic = findLocalCharacteristic(serviceUUID, characteristicUUID)
|
|
350
|
+
?: return Promise.rejected(IllegalArgumentException("Local characteristic $characteristicUUID was not found"))
|
|
351
|
+
|
|
352
|
+
setCharacteristicValue(characteristic, payload)
|
|
353
|
+
if (notify == true) {
|
|
354
|
+
notifySubscribedDevices(characteristic)
|
|
355
|
+
}
|
|
356
|
+
return Promise.resolved(Unit)
|
|
357
|
+
}
|
|
358
|
+
|
|
240
359
|
override fun isBluetoothEnabled(): Promise<Boolean> {
|
|
241
360
|
if (!hasRequiredBluetoothPermissions()) {
|
|
242
361
|
return Promise.resolved(false)
|
|
@@ -290,6 +409,29 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
290
409
|
return promise
|
|
291
410
|
}
|
|
292
411
|
|
|
412
|
+
override fun getCapabilities(): Promise<BluetoothCapabilities> {
|
|
413
|
+
ensureBluetoothManager()
|
|
414
|
+
val adapter = bluetoothAdapter
|
|
415
|
+
return Promise.resolved(
|
|
416
|
+
BluetoothCapabilities(
|
|
417
|
+
platform = "android",
|
|
418
|
+
supportsBleCentral = true,
|
|
419
|
+
supportsBlePeripheral = adapter?.bluetoothLeAdvertiser != null,
|
|
420
|
+
supportsDescriptors = true,
|
|
421
|
+
supportsIncludedServices = true,
|
|
422
|
+
supportsMtu = true,
|
|
423
|
+
supportsPhy = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O,
|
|
424
|
+
supportsBonding = true,
|
|
425
|
+
supportsExtendedAdvertising = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
|
426
|
+
adapter?.isLeExtendedAdvertisingSupported == true,
|
|
427
|
+
supportsL2cap = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q,
|
|
428
|
+
supportsClassicBluetooth = adapter != null,
|
|
429
|
+
supportsBackgroundBle = true,
|
|
430
|
+
supportsMultipeerConnectivity = false
|
|
431
|
+
)
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
|
|
293
435
|
override fun startScan(options: ScanOptions?) {
|
|
294
436
|
if (!ensureBluetoothPermissions("start scanning")) {
|
|
295
437
|
return
|
|
@@ -336,20 +478,27 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
336
478
|
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
|
337
479
|
val device = result.device
|
|
338
480
|
discoveredDevices[device.address] = device
|
|
339
|
-
|
|
481
|
+
emitDeviceFound(buildScanPayload(result))
|
|
340
482
|
}
|
|
341
483
|
|
|
342
484
|
override fun onBatchScanResults(results: MutableList<ScanResult>) {
|
|
343
485
|
results.forEach { result ->
|
|
344
486
|
val device = result.device
|
|
345
487
|
discoveredDevices[device.address] = device
|
|
346
|
-
|
|
488
|
+
emitDeviceFound(buildScanPayload(result))
|
|
347
489
|
}
|
|
348
490
|
}
|
|
349
491
|
|
|
350
492
|
override fun onScanFailed(errorCode: Int) {
|
|
351
493
|
Log.e(TAG, "Scan failed: $errorCode")
|
|
352
494
|
isScanning = false
|
|
495
|
+
eventEmitter.emit(
|
|
496
|
+
"scanFailed",
|
|
497
|
+
mapOf(
|
|
498
|
+
"errorCode" to errorCode,
|
|
499
|
+
"message" to scanFailureMessage(errorCode)
|
|
500
|
+
)
|
|
501
|
+
)
|
|
353
502
|
}
|
|
354
503
|
}
|
|
355
504
|
|
|
@@ -378,10 +527,10 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
378
527
|
}
|
|
379
528
|
}
|
|
380
529
|
|
|
381
|
-
val context = NitroModules.applicationContext
|
|
382
|
-
?: return Promise.rejected(IllegalStateException("React context unavailable"))
|
|
383
530
|
val adapter = bluetoothAdapter
|
|
384
531
|
?: return Promise.rejected(IllegalStateException("Bluetooth adapter unavailable"))
|
|
532
|
+
NitroModules.applicationContext
|
|
533
|
+
?: return Promise.rejected(IllegalStateException("React context unavailable"))
|
|
385
534
|
|
|
386
535
|
val device = discoveredDevices[deviceId] ?: run {
|
|
387
536
|
try {
|
|
@@ -393,20 +542,22 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
393
542
|
|
|
394
543
|
val promise = Promise<Unit>()
|
|
395
544
|
pendingConnections[deviceId] = promise
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
} else {
|
|
400
|
-
device.connectGatt(context, false, createGattCallback(deviceId))
|
|
401
|
-
}
|
|
402
|
-
connectedDevices[deviceId] = gatt
|
|
545
|
+
pendingConnectionAttempts[deviceId] = 0
|
|
546
|
+
scheduleConnectionTimeout(deviceId)
|
|
547
|
+
startGattConnection(deviceId, device)
|
|
403
548
|
return promise
|
|
404
549
|
}
|
|
405
550
|
|
|
406
551
|
override fun disconnect(deviceId: String) {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
552
|
+
pendingConnectionTimeouts.remove(deviceId)?.cancel()
|
|
553
|
+
pendingConnectionAttempts.remove(deviceId)
|
|
554
|
+
pendingConnectionGatts.remove(deviceId)?.let { gatt ->
|
|
555
|
+
gatt.disconnect()
|
|
556
|
+
gatt.close()
|
|
557
|
+
}
|
|
558
|
+
pendingConnections.remove(deviceId)?.reject(
|
|
559
|
+
IllegalStateException("Disconnected from $deviceId")
|
|
560
|
+
)
|
|
410
561
|
|
|
411
562
|
val gatt = connectedDevices.remove(deviceId)
|
|
412
563
|
gatt?.disconnect()
|
|
@@ -426,7 +577,11 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
426
577
|
|
|
427
578
|
val promise = Promise<Array<GATTService>>()
|
|
428
579
|
pendingServiceDiscoveries[deviceId] = promise
|
|
580
|
+
schedulePendingOperationTimeout("services|$deviceId", "Service discovery for $deviceId") {
|
|
581
|
+
pendingServiceDiscoveries.remove(deviceId)
|
|
582
|
+
}
|
|
429
583
|
if (!gatt.discoverServices()) {
|
|
584
|
+
cancelPendingOperationTimeout("services|$deviceId")
|
|
430
585
|
pendingServiceDiscoveries.remove(deviceId)
|
|
431
586
|
return Promise.rejected(IllegalStateException("Failed to start service discovery for $deviceId"))
|
|
432
587
|
}
|
|
@@ -448,14 +603,43 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
448
603
|
val promise = Promise<CharacteristicValue>()
|
|
449
604
|
val key = characteristicKey(deviceId, serviceUUID, characteristicUUID)
|
|
450
605
|
pendingReads[key] = promise
|
|
606
|
+
schedulePendingOperationTimeout("read|$key", "Characteristic read $key") {
|
|
607
|
+
pendingReads.remove(key)
|
|
608
|
+
}
|
|
451
609
|
|
|
452
610
|
if (!gatt.readCharacteristic(characteristic)) {
|
|
611
|
+
cancelPendingOperationTimeout("read|$key")
|
|
453
612
|
pendingReads.remove(key)
|
|
454
613
|
return Promise.rejected(IllegalStateException("Failed to start characteristic read"))
|
|
455
614
|
}
|
|
456
615
|
return promise
|
|
457
616
|
}
|
|
458
617
|
|
|
618
|
+
override fun readDescriptor(
|
|
619
|
+
deviceId: String,
|
|
620
|
+
serviceUUID: String,
|
|
621
|
+
characteristicUUID: String,
|
|
622
|
+
descriptorUUID: String
|
|
623
|
+
): Promise<DescriptorValue> {
|
|
624
|
+
val gatt = connectedDevices[deviceId]
|
|
625
|
+
?: return Promise.rejected(IllegalStateException("Device not connected: $deviceId"))
|
|
626
|
+
val descriptor = findDescriptor(gatt, serviceUUID, characteristicUUID, descriptorUUID)
|
|
627
|
+
?: return Promise.rejected(IllegalStateException("Descriptor not found: $descriptorUUID"))
|
|
628
|
+
|
|
629
|
+
val promise = Promise<DescriptorValue>()
|
|
630
|
+
val key = descriptorKey(deviceId, serviceUUID, characteristicUUID, descriptorUUID)
|
|
631
|
+
pendingDescriptorReads[key] = promise
|
|
632
|
+
schedulePendingOperationTimeout("descriptorRead|$key", "Descriptor read $key") {
|
|
633
|
+
pendingDescriptorReads.remove(key)
|
|
634
|
+
}
|
|
635
|
+
if (!gatt.readDescriptor(descriptor)) {
|
|
636
|
+
cancelPendingOperationTimeout("descriptorRead|$key")
|
|
637
|
+
pendingDescriptorReads.remove(key)
|
|
638
|
+
return Promise.rejected(IllegalStateException("Failed to start descriptor read"))
|
|
639
|
+
}
|
|
640
|
+
return promise
|
|
641
|
+
}
|
|
642
|
+
|
|
459
643
|
override fun writeCharacteristic(
|
|
460
644
|
deviceId: String,
|
|
461
645
|
serviceUUID: String,
|
|
@@ -472,20 +656,56 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
472
656
|
val data = hexStringToByteArray(value)
|
|
473
657
|
?: return Promise.rejected(IllegalArgumentException("Invalid hex string for characteristic write"))
|
|
474
658
|
|
|
475
|
-
|
|
476
|
-
characteristic.writeType = when (writeType) {
|
|
659
|
+
val resolvedWriteType = when (writeType) {
|
|
477
660
|
WriteType.WRITEWITHOUTRESPONSE -> BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
478
661
|
else -> BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
|
479
662
|
}
|
|
480
663
|
|
|
481
664
|
val promise = Promise<Unit>()
|
|
482
665
|
val key = characteristicKey(deviceId, serviceUUID, characteristicUUID)
|
|
483
|
-
|
|
666
|
+
if (resolvedWriteType != BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE) {
|
|
667
|
+
pendingWrites[key] = promise
|
|
668
|
+
schedulePendingOperationTimeout("write|$key", "Characteristic write $key") {
|
|
669
|
+
pendingWrites.remove(key)
|
|
670
|
+
}
|
|
671
|
+
}
|
|
484
672
|
|
|
485
|
-
if (!gatt
|
|
673
|
+
if (!writeGattCharacteristic(gatt, characteristic, data, resolvedWriteType)) {
|
|
674
|
+
cancelPendingOperationTimeout("write|$key")
|
|
486
675
|
pendingWrites.remove(key)
|
|
487
676
|
return Promise.rejected(IllegalStateException("Failed to start characteristic write"))
|
|
488
677
|
}
|
|
678
|
+
if (resolvedWriteType == BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE) {
|
|
679
|
+
promise.resolve(Unit)
|
|
680
|
+
}
|
|
681
|
+
return promise
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
override fun writeDescriptor(
|
|
685
|
+
deviceId: String,
|
|
686
|
+
serviceUUID: String,
|
|
687
|
+
characteristicUUID: String,
|
|
688
|
+
descriptorUUID: String,
|
|
689
|
+
value: String
|
|
690
|
+
): Promise<Unit> {
|
|
691
|
+
val gatt = connectedDevices[deviceId]
|
|
692
|
+
?: return Promise.rejected(IllegalStateException("Device not connected: $deviceId"))
|
|
693
|
+
val descriptor = findDescriptor(gatt, serviceUUID, characteristicUUID, descriptorUUID)
|
|
694
|
+
?: return Promise.rejected(IllegalStateException("Descriptor not found: $descriptorUUID"))
|
|
695
|
+
val data = hexStringToByteArray(value)
|
|
696
|
+
?: return Promise.rejected(IllegalArgumentException("Invalid hex string for descriptor write"))
|
|
697
|
+
|
|
698
|
+
val promise = Promise<Unit>()
|
|
699
|
+
val key = descriptorKey(deviceId, serviceUUID, characteristicUUID, descriptorUUID)
|
|
700
|
+
pendingDescriptorWrites[key] = promise
|
|
701
|
+
schedulePendingOperationTimeout("descriptorWrite|$key", "Descriptor write $key") {
|
|
702
|
+
pendingDescriptorWrites.remove(key)
|
|
703
|
+
}
|
|
704
|
+
if (!writeGattDescriptor(gatt, descriptor, data)) {
|
|
705
|
+
cancelPendingOperationTimeout("descriptorWrite|$key")
|
|
706
|
+
pendingDescriptorWrites.remove(key)
|
|
707
|
+
return Promise.rejected(IllegalStateException("Failed to start descriptor write"))
|
|
708
|
+
}
|
|
489
709
|
return promise
|
|
490
710
|
}
|
|
491
711
|
|
|
@@ -499,8 +719,14 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
499
719
|
gatt.setCharacteristicNotification(characteristic, true)
|
|
500
720
|
|
|
501
721
|
characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID)?.let { descriptor ->
|
|
502
|
-
|
|
503
|
-
|
|
722
|
+
val subscriptionValue = when {
|
|
723
|
+
characteristic.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0 ->
|
|
724
|
+
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
|
725
|
+
characteristic.properties and BluetoothGattCharacteristic.PROPERTY_INDICATE != 0 ->
|
|
726
|
+
BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
|
|
727
|
+
else -> return
|
|
728
|
+
}
|
|
729
|
+
writeGattDescriptor(gatt, descriptor, subscriptionValue)
|
|
504
730
|
}
|
|
505
731
|
}
|
|
506
732
|
|
|
@@ -514,8 +740,7 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
514
740
|
gatt.setCharacteristicNotification(characteristic, false)
|
|
515
741
|
|
|
516
742
|
characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID)?.let { descriptor ->
|
|
517
|
-
descriptor
|
|
518
|
-
gatt.writeDescriptor(descriptor)
|
|
743
|
+
writeGattDescriptor(gatt, descriptor, BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)
|
|
519
744
|
}
|
|
520
745
|
}
|
|
521
746
|
|
|
@@ -533,156 +758,809 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
533
758
|
|
|
534
759
|
val promise = Promise<Double>()
|
|
535
760
|
pendingRssiReads[deviceId] = promise
|
|
761
|
+
schedulePendingOperationTimeout("rssi|$deviceId", "RSSI read for $deviceId") {
|
|
762
|
+
pendingRssiReads.remove(deviceId)
|
|
763
|
+
}
|
|
536
764
|
if (!gatt.readRemoteRssi()) {
|
|
765
|
+
cancelPendingOperationTimeout("rssi|$deviceId")
|
|
537
766
|
pendingRssiReads.remove(deviceId)
|
|
538
767
|
return Promise.rejected(IllegalStateException("Failed to start RSSI read"))
|
|
539
768
|
}
|
|
540
769
|
return promise
|
|
541
770
|
}
|
|
542
771
|
|
|
543
|
-
override fun
|
|
544
|
-
val
|
|
545
|
-
|
|
546
|
-
|
|
772
|
+
override fun requestMTU(deviceId: String, mtu: Double): Promise<Double> {
|
|
773
|
+
val gatt = connectedDevices[deviceId]
|
|
774
|
+
?: return Promise.rejected(IllegalStateException("Device not connected: $deviceId"))
|
|
775
|
+
|
|
776
|
+
val requestedMtu = mtu.toInt().coerceIn(23, 517)
|
|
777
|
+
val promise = Promise<Double>()
|
|
778
|
+
pendingMtuRequests[deviceId] = promise
|
|
779
|
+
schedulePendingOperationTimeout("mtu|$deviceId", "MTU request for $deviceId") {
|
|
780
|
+
pendingMtuRequests.remove(deviceId)
|
|
547
781
|
}
|
|
782
|
+
if (!gatt.requestMtu(requestedMtu)) {
|
|
783
|
+
cancelPendingOperationTimeout("mtu|$deviceId")
|
|
784
|
+
pendingMtuRequests.remove(deviceId)
|
|
785
|
+
return Promise.rejected(IllegalStateException("Failed to start MTU request"))
|
|
786
|
+
}
|
|
787
|
+
return promise
|
|
788
|
+
}
|
|
548
789
|
|
|
549
|
-
|
|
550
|
-
|
|
790
|
+
override fun setPreferredPhy(
|
|
791
|
+
deviceId: String,
|
|
792
|
+
txPhy: BluetoothPhy,
|
|
793
|
+
rxPhy: BluetoothPhy,
|
|
794
|
+
phyOption: BluetoothPhyOption?
|
|
795
|
+
): Promise<Unit> {
|
|
796
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
797
|
+
return unsupportedPromise("BLE PHY selection requires Android 8.0 or newer")
|
|
551
798
|
}
|
|
552
799
|
|
|
553
|
-
val
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
)
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
)
|
|
567
|
-
putExtra(
|
|
568
|
-
MunimBluetoothBackgroundService.EXTRA_SCAN_MODE,
|
|
569
|
-
options.scanMode?.name ?: ScanMode.LOWPOWER.name
|
|
570
|
-
)
|
|
571
|
-
putExtra(
|
|
572
|
-
MunimBluetoothBackgroundService.EXTRA_NOTIFICATION_CHANNEL_ID,
|
|
573
|
-
options.androidNotificationChannelId
|
|
574
|
-
?: MunimBluetoothBackgroundService.DEFAULT_CHANNEL_ID
|
|
575
|
-
)
|
|
576
|
-
putExtra(
|
|
577
|
-
MunimBluetoothBackgroundService.EXTRA_NOTIFICATION_CHANNEL_NAME,
|
|
578
|
-
options.androidNotificationChannelName
|
|
579
|
-
?: MunimBluetoothBackgroundService.DEFAULT_CHANNEL_NAME
|
|
580
|
-
)
|
|
581
|
-
putExtra(
|
|
582
|
-
MunimBluetoothBackgroundService.EXTRA_NOTIFICATION_TITLE,
|
|
583
|
-
options.androidNotificationTitle
|
|
584
|
-
?: MunimBluetoothBackgroundService.DEFAULT_NOTIFICATION_TITLE
|
|
585
|
-
)
|
|
586
|
-
putExtra(
|
|
587
|
-
MunimBluetoothBackgroundService.EXTRA_NOTIFICATION_TEXT,
|
|
588
|
-
options.androidNotificationText
|
|
589
|
-
?: MunimBluetoothBackgroundService.DEFAULT_NOTIFICATION_TEXT
|
|
590
|
-
)
|
|
800
|
+
val gatt = connectedDevices[deviceId]
|
|
801
|
+
?: return Promise.rejected(IllegalStateException("Device not connected: $deviceId"))
|
|
802
|
+
|
|
803
|
+
gatt.setPreferredPhy(
|
|
804
|
+
phyToMask(txPhy),
|
|
805
|
+
phyToMask(rxPhy),
|
|
806
|
+
phyOptionToConstant(phyOption ?: BluetoothPhyOption.NONE)
|
|
807
|
+
)
|
|
808
|
+
return Promise.resolved(Unit)
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
override fun readPhy(deviceId: String): Promise<PhyStatus> {
|
|
812
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
813
|
+
return unsupportedPromise("BLE PHY reads require Android 8.0 or newer")
|
|
591
814
|
}
|
|
592
815
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
816
|
+
val gatt = connectedDevices[deviceId]
|
|
817
|
+
?: return Promise.rejected(IllegalStateException("Device not connected: $deviceId"))
|
|
818
|
+
|
|
819
|
+
val promise = Promise<PhyStatus>()
|
|
820
|
+
pendingPhyReads[deviceId] = promise
|
|
821
|
+
schedulePendingOperationTimeout("phy|$deviceId", "PHY read for $deviceId") {
|
|
822
|
+
pendingPhyReads.remove(deviceId)
|
|
597
823
|
}
|
|
824
|
+
gatt.readPhy()
|
|
825
|
+
return promise
|
|
598
826
|
}
|
|
599
827
|
|
|
600
|
-
override fun
|
|
601
|
-
val
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
}
|
|
605
|
-
context.startService(intent)
|
|
828
|
+
override fun getBondState(deviceId: String): Promise<BondState> {
|
|
829
|
+
val device = resolveBluetoothDevice(deviceId)
|
|
830
|
+
?: return Promise.rejected(IllegalArgumentException("Device not found: $deviceId"))
|
|
831
|
+
return Promise.resolved(bondStateFor(device))
|
|
606
832
|
}
|
|
607
833
|
|
|
608
|
-
override fun
|
|
609
|
-
|
|
834
|
+
override fun createBond(deviceId: String): Promise<BondState> {
|
|
835
|
+
val device = resolveBluetoothDevice(deviceId)
|
|
836
|
+
?: return Promise.rejected(IllegalArgumentException("Device not found: $deviceId"))
|
|
837
|
+
|
|
838
|
+
if (device.bondState == BluetoothDevice.BOND_BONDED) {
|
|
839
|
+
return Promise.resolved(BondState.BONDED)
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return if (device.createBond()) {
|
|
843
|
+
Promise.resolved(bondStateFor(device))
|
|
844
|
+
} else {
|
|
845
|
+
Promise.rejected(IllegalStateException("Failed to start bond creation for $deviceId"))
|
|
846
|
+
}
|
|
610
847
|
}
|
|
611
848
|
|
|
612
|
-
override fun
|
|
613
|
-
|
|
849
|
+
override fun removeBond(deviceId: String): Promise<BondState> {
|
|
850
|
+
val device = resolveBluetoothDevice(deviceId)
|
|
851
|
+
?: return Promise.rejected(IllegalArgumentException("Device not found: $deviceId"))
|
|
852
|
+
|
|
853
|
+
return try {
|
|
854
|
+
val method = device.javaClass.getMethod("removeBond")
|
|
855
|
+
val removed = method.invoke(device) as? Boolean ?: false
|
|
856
|
+
if (removed) {
|
|
857
|
+
Promise.resolved(bondStateFor(device))
|
|
858
|
+
} else {
|
|
859
|
+
Promise.rejected(IllegalStateException("Failed to remove bond for $deviceId"))
|
|
860
|
+
}
|
|
861
|
+
} catch (error: ReflectiveOperationException) {
|
|
862
|
+
Promise.rejected(UnsupportedOperationException("Removing bonds is unavailable on this Android build", error))
|
|
863
|
+
}
|
|
614
864
|
}
|
|
615
865
|
|
|
616
|
-
|
|
617
|
-
if (
|
|
618
|
-
return
|
|
866
|
+
override fun startExtendedAdvertising(options: ExtendedAdvertisingOptions): Promise<String> {
|
|
867
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
868
|
+
return unsupportedPromise("BLE extended advertising requires Android 8.0 or newer")
|
|
869
|
+
}
|
|
870
|
+
if (!ensureBluetoothPermissions("start extended advertising")) {
|
|
871
|
+
return Promise.rejected(IllegalStateException("Bluetooth permissions not granted"))
|
|
619
872
|
}
|
|
620
873
|
|
|
621
874
|
ensureBluetoothManager()
|
|
622
875
|
val adapter = bluetoothAdapter
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
return
|
|
876
|
+
?: return Promise.rejected(IllegalStateException("Bluetooth adapter unavailable"))
|
|
877
|
+
if (!adapter.isLeExtendedAdvertisingSupported) {
|
|
878
|
+
return unsupportedPromise("BLE extended advertising is not supported by this device")
|
|
626
879
|
}
|
|
880
|
+
val advertiser = adapter.bluetoothLeAdvertiser
|
|
881
|
+
?: return Promise.rejected(IllegalStateException("Bluetooth LE advertiser is unavailable"))
|
|
627
882
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
883
|
+
val id = UUID.randomUUID().toString()
|
|
884
|
+
val promise = Promise<String>()
|
|
885
|
+
val dataBuilder = AdvertiseData.Builder()
|
|
886
|
+
options.serviceUUIDs?.forEach { uuid ->
|
|
887
|
+
dataBuilder.addServiceUuid(ParcelUuid.fromString(uuid))
|
|
631
888
|
}
|
|
889
|
+
normalizeAdvertisingData(
|
|
890
|
+
options.advertisingData,
|
|
891
|
+
options.localName,
|
|
892
|
+
options.manufacturerData
|
|
893
|
+
).let { data ->
|
|
894
|
+
processAdvertisingData(data, dataBuilder, includeServiceUuids = true)
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
val scanResponseBuilder = AdvertiseData.Builder()
|
|
898
|
+
options.localName?.let { scanResponseBuilder.setIncludeDeviceName(true) }
|
|
899
|
+
|
|
900
|
+
val parameters = AdvertisingSetParameters.Builder()
|
|
901
|
+
.setLegacyMode(options.legacyMode ?: false)
|
|
902
|
+
.setConnectable(options.connectable ?: true)
|
|
903
|
+
.setScannable(options.scannable ?: false)
|
|
904
|
+
.setAnonymous(options.anonymous ?: false)
|
|
905
|
+
.setIncludeTxPower(options.includeTxPower ?: false)
|
|
906
|
+
.setPrimaryPhy(phyToAdvertisingPhy(options.primaryPhy ?: BluetoothPhy.LE1M))
|
|
907
|
+
.setSecondaryPhy(phyToAdvertisingPhy(options.secondaryPhy ?: BluetoothPhy.LE1M))
|
|
908
|
+
.setInterval(options.interval?.toInt() ?: AdvertisingSetParameters.INTERVAL_MEDIUM)
|
|
909
|
+
.setTxPowerLevel(options.txPowerLevel?.toInt() ?: AdvertisingSetParameters.TX_POWER_HIGH)
|
|
910
|
+
.build()
|
|
632
911
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
912
|
+
val callback = object : AdvertisingSetCallback() {
|
|
913
|
+
override fun onAdvertisingSetStarted(
|
|
914
|
+
advertisingSet: AdvertisingSet?,
|
|
915
|
+
txPower: Int,
|
|
916
|
+
status: Int
|
|
917
|
+
) {
|
|
918
|
+
if (status == AdvertisingSetCallback.ADVERTISE_SUCCESS && advertisingSet != null) {
|
|
919
|
+
extendedAdvertisingSets[id] = advertisingSet
|
|
920
|
+
promise.resolve(id)
|
|
921
|
+
eventEmitter.emit("advertisingStarted", mapOf("advertisingId" to id))
|
|
922
|
+
} else {
|
|
923
|
+
extendedAdvertisingCallbacks.remove(id)
|
|
924
|
+
promise.reject(IllegalStateException("Extended advertising failed (status=$status)"))
|
|
925
|
+
eventEmitter.emit(
|
|
926
|
+
"advertisingStartFailed",
|
|
927
|
+
mapOf(
|
|
928
|
+
"advertisingId" to id,
|
|
929
|
+
"errorCode" to status,
|
|
930
|
+
"message" to advertiseFailureMessage(status)
|
|
931
|
+
)
|
|
932
|
+
)
|
|
933
|
+
}
|
|
636
934
|
}
|
|
935
|
+
}
|
|
637
936
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
937
|
+
extendedAdvertisingCallbacks[id] = callback
|
|
938
|
+
advertiser.startAdvertisingSet(
|
|
939
|
+
parameters,
|
|
940
|
+
dataBuilder.build(),
|
|
941
|
+
scanResponseBuilder.build(),
|
|
942
|
+
null,
|
|
943
|
+
null,
|
|
944
|
+
callback
|
|
945
|
+
)
|
|
946
|
+
return promise
|
|
947
|
+
}
|
|
644
948
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
currentServiceUUIDs.forEach { uuid ->
|
|
648
|
-
dataBuilder.addServiceUuid(ParcelUuid.fromString(uuid))
|
|
649
|
-
}
|
|
949
|
+
override fun stopExtendedAdvertising(advertisingId: String) {
|
|
950
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
|
650
951
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
.build()
|
|
952
|
+
ensureBluetoothManager()
|
|
953
|
+
extendedAdvertisingSets.remove(advertisingId)
|
|
954
|
+
val callback = extendedAdvertisingCallbacks.remove(advertisingId) ?: return
|
|
955
|
+
bluetoothAdapter?.bluetoothLeAdvertiser?.stopAdvertisingSet(callback)
|
|
956
|
+
}
|
|
657
957
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
958
|
+
override fun publishL2CAPChannel(encryptionRequired: Boolean?): Promise<L2CAPChannel> {
|
|
959
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
|
960
|
+
return unsupportedPromise("BLE L2CAP channel streams require Android 10 or newer")
|
|
961
|
+
}
|
|
962
|
+
if (!hasRequiredBluetoothPermissions()) {
|
|
963
|
+
return Promise.rejected(SecurityException("Missing Bluetooth permissions"))
|
|
964
|
+
}
|
|
662
965
|
|
|
663
|
-
|
|
664
|
-
|
|
966
|
+
ensureBluetoothManager()
|
|
967
|
+
val adapter = bluetoothAdapter
|
|
968
|
+
?: return Promise.rejected(IllegalStateException("Bluetooth adapter is unavailable"))
|
|
969
|
+
val promise = Promise<L2CAPChannel>()
|
|
970
|
+
|
|
971
|
+
bluetoothScope.launch(Dispatchers.IO) {
|
|
972
|
+
try {
|
|
973
|
+
val serverSocket = if (encryptionRequired == true) {
|
|
974
|
+
adapter.listenUsingL2capChannel()
|
|
975
|
+
} else {
|
|
976
|
+
adapter.listenUsingInsecureL2capChannel()
|
|
665
977
|
}
|
|
978
|
+
val psm = serverSocket.psm
|
|
979
|
+
l2capServerSockets[psm] = serverSocket
|
|
980
|
+
l2capAcceptJobs[psm]?.cancel()
|
|
981
|
+
l2capAcceptJobs[psm] = bluetoothScope.launch(Dispatchers.IO) {
|
|
982
|
+
acceptL2CAPConnections(psm, serverSocket)
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
val channel = L2CAPChannel("server:$psm", psm.toDouble(), null)
|
|
986
|
+
eventEmitter.emit(
|
|
987
|
+
"l2capChannelPublished",
|
|
988
|
+
mapOf("channelId" to channel.id, "psm" to channel.psm)
|
|
989
|
+
)
|
|
990
|
+
promise.resolve(channel)
|
|
991
|
+
} catch (error: SecurityException) {
|
|
992
|
+
promise.reject(error)
|
|
993
|
+
} catch (error: IOException) {
|
|
994
|
+
promise.reject(error)
|
|
666
995
|
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return promise
|
|
999
|
+
}
|
|
667
1000
|
|
|
668
|
-
|
|
1001
|
+
override fun unpublishL2CAPChannel(psm: Double) {
|
|
1002
|
+
val psmValue = psm.toInt()
|
|
1003
|
+
l2capAcceptJobs.remove(psmValue)?.cancel()
|
|
1004
|
+
try {
|
|
1005
|
+
l2capServerSockets.remove(psmValue)?.close()
|
|
1006
|
+
} catch (error: IOException) {
|
|
1007
|
+
Log.w(TAG, "Unable to close L2CAP server socket for PSM $psmValue", error)
|
|
669
1008
|
}
|
|
670
1009
|
}
|
|
671
1010
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
1011
|
+
override fun openL2CAPChannel(deviceId: String, psm: Double): Promise<L2CAPChannel> {
|
|
1012
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
|
1013
|
+
return unsupportedPromise("BLE L2CAP channel streams require Android 10 or newer")
|
|
1014
|
+
}
|
|
1015
|
+
if (!hasRequiredBluetoothPermissions()) {
|
|
1016
|
+
return Promise.rejected(SecurityException("Missing Bluetooth permissions"))
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
val device = resolveBluetoothDevice(deviceId)
|
|
1020
|
+
?: return Promise.rejected(IllegalArgumentException("Device $deviceId was not found"))
|
|
1021
|
+
val promise = Promise<L2CAPChannel>()
|
|
1022
|
+
|
|
1023
|
+
bluetoothScope.launch(Dispatchers.IO) {
|
|
1024
|
+
try {
|
|
1025
|
+
val socket = device.createL2capChannel(psm.toInt())
|
|
1026
|
+
socket.connect()
|
|
1027
|
+
val channel = registerL2CAPSocket(socket, psm.toInt(), device.address)
|
|
1028
|
+
promise.resolve(channel)
|
|
1029
|
+
} catch (error: SecurityException) {
|
|
1030
|
+
promise.reject(error)
|
|
1031
|
+
} catch (error: IOException) {
|
|
1032
|
+
promise.reject(error)
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
return promise
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
override fun closeL2CAPChannel(channelId: String) {
|
|
1040
|
+
closeL2CAPChannelInternal(channelId, true)
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
override fun sendL2CAPData(channelId: String, value: String): Promise<Unit> {
|
|
1044
|
+
val socket = l2capSockets[channelId]
|
|
1045
|
+
?: return Promise.rejected(IllegalArgumentException("L2CAP channel $channelId is not open"))
|
|
1046
|
+
val payload = hexStringToByteArray(value)
|
|
1047
|
+
?: return Promise.rejected(IllegalArgumentException("Value must be a hex string"))
|
|
1048
|
+
val promise = Promise<Unit>()
|
|
1049
|
+
|
|
1050
|
+
bluetoothScope.launch(Dispatchers.IO) {
|
|
1051
|
+
try {
|
|
1052
|
+
socket.outputStream.write(payload)
|
|
1053
|
+
socket.outputStream.flush()
|
|
1054
|
+
promise.resolve(Unit)
|
|
1055
|
+
} catch (error: IOException) {
|
|
1056
|
+
promise.reject(error)
|
|
1057
|
+
closeL2CAPChannelInternal(channelId, true)
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
return promise
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
override fun startClassicScan() {
|
|
1065
|
+
if (!ensureBluetoothPermissions("start Classic Bluetooth discovery")) {
|
|
1066
|
+
return
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
ensureBluetoothManager()
|
|
1070
|
+
val adapter = bluetoothAdapter
|
|
1071
|
+
val context = NitroModules.applicationContext ?: return
|
|
1072
|
+
if (adapter == null || !adapter.isEnabled) {
|
|
1073
|
+
eventEmitter.emit(
|
|
1074
|
+
"classicScanFailed",
|
|
1075
|
+
mapOf("message" to "Bluetooth is not enabled or unavailable")
|
|
1076
|
+
)
|
|
1077
|
+
return
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
if (classicScanReceiver == null) {
|
|
1081
|
+
classicScanReceiver = object : BroadcastReceiver() {
|
|
1082
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
1083
|
+
when (intent.action) {
|
|
1084
|
+
BluetoothDevice.ACTION_FOUND -> {
|
|
1085
|
+
val device = getBluetoothDeviceExtra(intent) ?: return
|
|
1086
|
+
classicDevices[device.address] = device
|
|
1087
|
+
eventEmitter.emit(
|
|
1088
|
+
"classicDeviceFound",
|
|
1089
|
+
mapOf(
|
|
1090
|
+
"id" to device.address,
|
|
1091
|
+
"name" to device.name,
|
|
1092
|
+
"bondState" to bondStateFor(device).name.lowercase()
|
|
1093
|
+
)
|
|
1094
|
+
)
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {
|
|
1098
|
+
eventEmitter.emit("classicScanFinished", emptyMap())
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
val filter = IntentFilter().apply {
|
|
1104
|
+
addAction(BluetoothDevice.ACTION_FOUND)
|
|
1105
|
+
addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
|
|
1106
|
+
}
|
|
1107
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
1108
|
+
context.registerReceiver(classicScanReceiver, filter, Context.RECEIVER_EXPORTED)
|
|
1109
|
+
} else {
|
|
1110
|
+
@Suppress("DEPRECATION")
|
|
1111
|
+
context.registerReceiver(classicScanReceiver, filter)
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
try {
|
|
1116
|
+
if (adapter.isDiscovering) {
|
|
1117
|
+
adapter.cancelDiscovery()
|
|
1118
|
+
}
|
|
1119
|
+
if (!adapter.startDiscovery()) {
|
|
1120
|
+
eventEmitter.emit(
|
|
1121
|
+
"classicScanFailed",
|
|
1122
|
+
mapOf("message" to "Classic Bluetooth discovery failed to start")
|
|
1123
|
+
)
|
|
1124
|
+
}
|
|
1125
|
+
} catch (error: SecurityException) {
|
|
1126
|
+
eventEmitter.emit("classicScanFailed", mapOf("message" to (error.message ?: "Missing Bluetooth permissions")))
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
override fun stopClassicScan() {
|
|
1131
|
+
ensureBluetoothManager()
|
|
1132
|
+
try {
|
|
1133
|
+
bluetoothAdapter?.cancelDiscovery()
|
|
1134
|
+
} catch (error: SecurityException) {
|
|
1135
|
+
Log.w(TAG, "Unable to cancel Classic Bluetooth discovery", error)
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
val context = NitroModules.applicationContext ?: return
|
|
1139
|
+
classicScanReceiver?.let { receiver ->
|
|
1140
|
+
try {
|
|
1141
|
+
context.unregisterReceiver(receiver)
|
|
1142
|
+
} catch (error: IllegalArgumentException) {
|
|
1143
|
+
Log.w(TAG, "Classic Bluetooth discovery receiver was not registered", error)
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
classicScanReceiver = null
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
override fun connectClassic(deviceId: String, serviceUUID: String?): Promise<Unit> {
|
|
1150
|
+
if (!hasRequiredBluetoothPermissions()) {
|
|
1151
|
+
return Promise.rejected(SecurityException("Missing Bluetooth permissions"))
|
|
1152
|
+
}
|
|
1153
|
+
val device = resolveClassicDevice(deviceId)
|
|
1154
|
+
?: return Promise.rejected(IllegalArgumentException("Classic Bluetooth device $deviceId was not found"))
|
|
1155
|
+
val uuid = try {
|
|
1156
|
+
UUID.fromString(serviceUUID ?: SERIAL_PORT_PROFILE_UUID.toString())
|
|
1157
|
+
} catch (error: IllegalArgumentException) {
|
|
1158
|
+
return Promise.rejected(error)
|
|
1159
|
+
}
|
|
1160
|
+
val promise = Promise<Unit>()
|
|
1161
|
+
|
|
1162
|
+
bluetoothScope.launch(Dispatchers.IO) {
|
|
1163
|
+
try {
|
|
1164
|
+
bluetoothAdapter?.cancelDiscovery()
|
|
1165
|
+
closeClassicSocket(deviceId, false)
|
|
1166
|
+
val socket = device.createRfcommSocketToServiceRecord(uuid)
|
|
1167
|
+
socket.connect()
|
|
1168
|
+
classicSockets[deviceId] = socket
|
|
1169
|
+
startClassicReadLoop(deviceId, socket)
|
|
1170
|
+
eventEmitter.emit("classicConnected", mapOf("deviceId" to deviceId))
|
|
1171
|
+
promise.resolve(Unit)
|
|
1172
|
+
} catch (error: SecurityException) {
|
|
1173
|
+
promise.reject(error)
|
|
1174
|
+
} catch (error: IOException) {
|
|
1175
|
+
closeClassicSocket(deviceId, false)
|
|
1176
|
+
promise.reject(error)
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
return promise
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
override fun startClassicServer(serviceUUID: String?, serviceName: String?): Promise<Unit> {
|
|
1184
|
+
if (!hasRequiredBluetoothPermissions()) {
|
|
1185
|
+
return Promise.rejected(SecurityException("Missing Bluetooth permissions"))
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
ensureBluetoothManager()
|
|
1189
|
+
val adapter = bluetoothAdapter
|
|
1190
|
+
?: return Promise.rejected(IllegalStateException("Bluetooth adapter unavailable"))
|
|
1191
|
+
val uuid = try {
|
|
1192
|
+
UUID.fromString(serviceUUID ?: SERIAL_PORT_PROFILE_UUID.toString())
|
|
1193
|
+
} catch (error: IllegalArgumentException) {
|
|
1194
|
+
return Promise.rejected(error)
|
|
1195
|
+
}
|
|
1196
|
+
val key = uuid.toString().lowercase()
|
|
1197
|
+
val promise = Promise<Unit>()
|
|
1198
|
+
|
|
1199
|
+
bluetoothScope.launch(Dispatchers.IO) {
|
|
1200
|
+
try {
|
|
1201
|
+
closeClassicServerInternal(key, false)
|
|
1202
|
+
val serverSocket = adapter.listenUsingRfcommWithServiceRecord(
|
|
1203
|
+
serviceName ?: DEFAULT_CLASSIC_SERVICE_NAME,
|
|
1204
|
+
uuid
|
|
1205
|
+
)
|
|
1206
|
+
classicServerSockets[key] = serverSocket
|
|
1207
|
+
classicServerJobs[key] = bluetoothScope.launch(Dispatchers.IO) {
|
|
1208
|
+
acceptClassicConnections(key, serverSocket)
|
|
1209
|
+
}
|
|
1210
|
+
eventEmitter.emit(
|
|
1211
|
+
"classicServerStarted",
|
|
1212
|
+
mapOf("serviceUUID" to uuid.toString(), "serviceName" to (serviceName ?: DEFAULT_CLASSIC_SERVICE_NAME))
|
|
1213
|
+
)
|
|
1214
|
+
promise.resolve(Unit)
|
|
1215
|
+
} catch (error: SecurityException) {
|
|
1216
|
+
promise.reject(error)
|
|
1217
|
+
} catch (error: IOException) {
|
|
1218
|
+
promise.reject(error)
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
return promise
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
override fun stopClassicServer(serviceUUID: String?) {
|
|
1226
|
+
val uuid = try {
|
|
1227
|
+
UUID.fromString(serviceUUID ?: SERIAL_PORT_PROFILE_UUID.toString())
|
|
1228
|
+
} catch (_: IllegalArgumentException) {
|
|
1229
|
+
return
|
|
1230
|
+
}
|
|
1231
|
+
closeClassicServerInternal(uuid.toString().lowercase(), true)
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
override fun disconnectClassic(deviceId: String) {
|
|
1235
|
+
closeClassicSocket(deviceId, true)
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
override fun writeClassic(deviceId: String, value: String): Promise<Unit> {
|
|
1239
|
+
val socket = classicSockets[deviceId]
|
|
1240
|
+
?: return Promise.rejected(IllegalArgumentException("Classic Bluetooth device $deviceId is not connected"))
|
|
1241
|
+
val payload = hexStringToByteArray(value)
|
|
1242
|
+
?: return Promise.rejected(IllegalArgumentException("Value must be a hex string"))
|
|
1243
|
+
val promise = Promise<Unit>()
|
|
1244
|
+
|
|
1245
|
+
bluetoothScope.launch(Dispatchers.IO) {
|
|
1246
|
+
try {
|
|
1247
|
+
socket.outputStream.write(payload)
|
|
1248
|
+
socket.outputStream.flush()
|
|
1249
|
+
promise.resolve(Unit)
|
|
1250
|
+
} catch (error: IOException) {
|
|
1251
|
+
promise.reject(error)
|
|
1252
|
+
closeClassicSocket(deviceId, true)
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
return promise
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
override fun startBackgroundSession(options: BackgroundSessionOptions) {
|
|
1260
|
+
val context = NitroModules.applicationContext ?: run {
|
|
1261
|
+
Log.w(TAG, "Unable to start background BLE session: application context unavailable")
|
|
1262
|
+
return
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
if (!ensureBluetoothPermissions("start background BLE session")) {
|
|
1266
|
+
return
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
val intent = Intent(context, MunimBluetoothBackgroundService::class.java).apply {
|
|
1270
|
+
action = MunimBluetoothBackgroundService.ACTION_START
|
|
1271
|
+
putExtra(
|
|
1272
|
+
MunimBluetoothBackgroundService.EXTRA_SERVICE_UUIDS,
|
|
1273
|
+
options.serviceUUIDs
|
|
1274
|
+
)
|
|
1275
|
+
putExtra(
|
|
1276
|
+
MunimBluetoothBackgroundService.EXTRA_LOCAL_NAME,
|
|
1277
|
+
options.localName
|
|
1278
|
+
)
|
|
1279
|
+
putExtra(
|
|
1280
|
+
MunimBluetoothBackgroundService.EXTRA_ALLOW_DUPLICATES,
|
|
1281
|
+
options.allowDuplicates ?: false
|
|
1282
|
+
)
|
|
1283
|
+
putExtra(
|
|
1284
|
+
MunimBluetoothBackgroundService.EXTRA_SCAN_MODE,
|
|
1285
|
+
options.scanMode?.name ?: ScanMode.LOWPOWER.name
|
|
1286
|
+
)
|
|
1287
|
+
serializeConfiguredServices()?.let { servicesJson ->
|
|
1288
|
+
putExtra(
|
|
1289
|
+
MunimBluetoothBackgroundService.EXTRA_GATT_SERVICES_JSON,
|
|
1290
|
+
servicesJson
|
|
1291
|
+
)
|
|
1292
|
+
}
|
|
1293
|
+
putExtra(
|
|
1294
|
+
MunimBluetoothBackgroundService.EXTRA_NOTIFICATION_CHANNEL_ID,
|
|
1295
|
+
options.androidNotificationChannelId
|
|
1296
|
+
?: MunimBluetoothBackgroundService.DEFAULT_CHANNEL_ID
|
|
1297
|
+
)
|
|
1298
|
+
putExtra(
|
|
1299
|
+
MunimBluetoothBackgroundService.EXTRA_NOTIFICATION_CHANNEL_NAME,
|
|
1300
|
+
options.androidNotificationChannelName
|
|
1301
|
+
?: MunimBluetoothBackgroundService.DEFAULT_CHANNEL_NAME
|
|
1302
|
+
)
|
|
1303
|
+
putExtra(
|
|
1304
|
+
MunimBluetoothBackgroundService.EXTRA_NOTIFICATION_TITLE,
|
|
1305
|
+
options.androidNotificationTitle
|
|
1306
|
+
?: MunimBluetoothBackgroundService.DEFAULT_NOTIFICATION_TITLE
|
|
1307
|
+
)
|
|
1308
|
+
putExtra(
|
|
1309
|
+
MunimBluetoothBackgroundService.EXTRA_NOTIFICATION_TEXT,
|
|
1310
|
+
options.androidNotificationText
|
|
1311
|
+
?: MunimBluetoothBackgroundService.DEFAULT_NOTIFICATION_TEXT
|
|
1312
|
+
)
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
try {
|
|
1316
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1317
|
+
context.startForegroundService(intent)
|
|
1318
|
+
} else {
|
|
1319
|
+
context.startService(intent)
|
|
1320
|
+
}
|
|
1321
|
+
eventEmitter.emit(
|
|
1322
|
+
"backgroundSessionStarted",
|
|
1323
|
+
mapOf(
|
|
1324
|
+
"platform" to "android",
|
|
1325
|
+
"serviceUUIDs" to options.serviceUUIDs.toList(),
|
|
1326
|
+
"localName" to options.localName
|
|
1327
|
+
)
|
|
1328
|
+
)
|
|
1329
|
+
} catch (error: RuntimeException) {
|
|
1330
|
+
Log.w(TAG, "Unable to start background BLE session", error)
|
|
1331
|
+
eventEmitter.emit(
|
|
1332
|
+
"backgroundSessionStartFailed",
|
|
1333
|
+
mapOf(
|
|
1334
|
+
"platform" to "android",
|
|
1335
|
+
"error" to (error.message ?: "Unable to start background BLE session")
|
|
1336
|
+
)
|
|
1337
|
+
)
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
override fun stopBackgroundSession() {
|
|
1342
|
+
val context = NitroModules.applicationContext ?: return
|
|
1343
|
+
val intent = Intent(context, MunimBluetoothBackgroundService::class.java).apply {
|
|
1344
|
+
action = MunimBluetoothBackgroundService.ACTION_STOP
|
|
1345
|
+
}
|
|
1346
|
+
context.startService(intent)
|
|
1347
|
+
eventEmitter.emit("backgroundSessionStopped", mapOf("platform" to "android"))
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
override fun startMultipeerSession(options: MultipeerSessionOptions) {
|
|
1351
|
+
eventEmitter.emit(
|
|
1352
|
+
"multipeerStartFailed",
|
|
1353
|
+
mapOf(
|
|
1354
|
+
"platform" to "android",
|
|
1355
|
+
"error" to MULTIPEER_UNSUPPORTED_MESSAGE
|
|
1356
|
+
)
|
|
1357
|
+
)
|
|
1358
|
+
throw UnsupportedOperationException(MULTIPEER_UNSUPPORTED_MESSAGE)
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
override fun stopMultipeerSession() {
|
|
1362
|
+
eventEmitter.emit("multipeerStopped", mapOf("platform" to "android"))
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
override fun inviteMultipeerPeer(peerId: String) {
|
|
1366
|
+
throw UnsupportedOperationException(MULTIPEER_UNSUPPORTED_MESSAGE)
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
override fun getMultipeerPeers(): Promise<Array<MultipeerPeer>> {
|
|
1370
|
+
return Promise.resolved(emptyArray<MultipeerPeer>())
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
override fun sendMultipeerMessage(
|
|
1374
|
+
value: String,
|
|
1375
|
+
peerIds: Array<String>?,
|
|
1376
|
+
reliable: Boolean?
|
|
1377
|
+
): Promise<Unit> {
|
|
1378
|
+
return unsupportedPromise(MULTIPEER_UNSUPPORTED_MESSAGE)
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
override fun addListener(eventName: String) {
|
|
1382
|
+
// Nitro uses JS-side listener registration. No native bookkeeping required here.
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
override fun removeListeners(count: Double) {
|
|
1386
|
+
// Nitro uses JS-side listener registration. No native bookkeeping required here.
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
private fun restartAdvertising(delayMs: Long) {
|
|
1390
|
+
if (!ensureBluetoothPermissions("restart advertising")) {
|
|
1391
|
+
return
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
ensureBluetoothManager()
|
|
1395
|
+
val adapter = bluetoothAdapter
|
|
1396
|
+
if (adapter == null || !adapter.isEnabled) {
|
|
1397
|
+
Log.e(TAG, "Bluetooth is not enabled or not available")
|
|
1398
|
+
return
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
advertiseJob?.cancel()
|
|
1402
|
+
advertiseCallback?.let { callback ->
|
|
1403
|
+
advertiser?.stopAdvertising(callback)
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
advertiseJob = bluetoothScope.launch {
|
|
1407
|
+
if (delayMs > 0) {
|
|
1408
|
+
delay(delayMs)
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
advertiser = adapter.bluetoothLeAdvertiser
|
|
1412
|
+
val activeAdvertiser = advertiser
|
|
1413
|
+
if (activeAdvertiser == null) {
|
|
1414
|
+
Log.e(TAG, "Bluetooth LE advertiser is not available")
|
|
1415
|
+
return@launch
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
val dataBuilder = AdvertiseData.Builder()
|
|
1419
|
+
currentServiceUUIDs.forEach { uuid ->
|
|
1420
|
+
dataBuilder.addServiceUuid(ParcelUuid.fromString(uuid))
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
val scanResponseBuilder = AdvertiseData.Builder()
|
|
1424
|
+
currentAdvertisingData?.let {
|
|
1425
|
+
processAdvertisingData(
|
|
1426
|
+
data = it,
|
|
1427
|
+
dataBuilder = scanResponseBuilder,
|
|
1428
|
+
includeServiceUuids = false
|
|
1429
|
+
)
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
val settings = AdvertiseSettings.Builder()
|
|
1433
|
+
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
|
|
1434
|
+
.setConnectable(true)
|
|
1435
|
+
.setTimeout(0)
|
|
1436
|
+
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
|
|
1437
|
+
.build()
|
|
1438
|
+
|
|
1439
|
+
advertiseCallback = object : AdvertiseCallback() {
|
|
1440
|
+
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
|
|
1441
|
+
Log.i(TAG, "Advertising started successfully")
|
|
1442
|
+
eventEmitter.emit("advertisingStarted", emptyMap())
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
override fun onStartFailure(errorCode: Int) {
|
|
1446
|
+
Log.e(TAG, "Advertising failed: $errorCode")
|
|
1447
|
+
eventEmitter.emit(
|
|
1448
|
+
"advertisingStartFailed",
|
|
1449
|
+
mapOf(
|
|
1450
|
+
"errorCode" to errorCode,
|
|
1451
|
+
"message" to advertiseFailureMessage(errorCode)
|
|
1452
|
+
)
|
|
1453
|
+
)
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
activeAdvertiser.startAdvertising(
|
|
1458
|
+
settings,
|
|
1459
|
+
dataBuilder.build(),
|
|
1460
|
+
scanResponseBuilder.build(),
|
|
1461
|
+
advertiseCallback
|
|
1462
|
+
)
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
private fun serializeConfiguredServices(): String? {
|
|
1467
|
+
if (configuredServices.isEmpty()) {
|
|
1468
|
+
return null
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
val services = JSONArray()
|
|
1472
|
+
configuredServices.forEach { service ->
|
|
1473
|
+
val serviceJson = JSONObject()
|
|
1474
|
+
.put("uuid", service.uuid)
|
|
1475
|
+
.put("characteristics", JSONArray().also { characteristics ->
|
|
1476
|
+
service.characteristics.forEach { characteristic ->
|
|
1477
|
+
val characteristicJson = JSONObject()
|
|
1478
|
+
.put("uuid", characteristic.uuid)
|
|
1479
|
+
.put("properties", stringArrayJson(characteristic.properties))
|
|
1480
|
+
characteristic.value?.let { characteristicJson.put("value", it) }
|
|
1481
|
+
characteristic.descriptors?.let { descriptors ->
|
|
1482
|
+
characteristicJson.put(
|
|
1483
|
+
"descriptors",
|
|
1484
|
+
JSONArray().also { descriptorArray ->
|
|
1485
|
+
descriptors.forEach { descriptor ->
|
|
1486
|
+
val descriptorJson = JSONObject()
|
|
1487
|
+
.put("uuid", descriptor.uuid)
|
|
1488
|
+
descriptor.value?.let { descriptorJson.put("value", it) }
|
|
1489
|
+
descriptor.permissions?.let { permissions ->
|
|
1490
|
+
descriptorJson.put(
|
|
1491
|
+
"permissions",
|
|
1492
|
+
stringArrayJson(permissions)
|
|
1493
|
+
)
|
|
1494
|
+
}
|
|
1495
|
+
descriptorArray.put(descriptorJson)
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
)
|
|
1499
|
+
}
|
|
1500
|
+
characteristics.put(characteristicJson)
|
|
1501
|
+
}
|
|
1502
|
+
})
|
|
1503
|
+
service.includedServices?.let {
|
|
1504
|
+
serviceJson.put("includedServices", stringArrayJson(it))
|
|
1505
|
+
}
|
|
1506
|
+
services.put(serviceJson)
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
return services.toString()
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
private fun stringArrayJson(values: Array<String>): JSONArray {
|
|
1513
|
+
return JSONArray().also { array ->
|
|
1514
|
+
values.forEach(array::put)
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
private fun buildGattServerCallback(): BluetoothGattServerCallback {
|
|
1519
|
+
return object : BluetoothGattServerCallback() {
|
|
1520
|
+
override fun onCharacteristicReadRequest(
|
|
1521
|
+
device: BluetoothDevice,
|
|
1522
|
+
requestId: Int,
|
|
1523
|
+
offset: Int,
|
|
1524
|
+
characteristic: BluetoothGattCharacteristic
|
|
1525
|
+
) {
|
|
1526
|
+
if ((characteristic.properties and BluetoothGattCharacteristic.PROPERTY_READ) == 0) {
|
|
1527
|
+
gattServer?.sendResponse(
|
|
1528
|
+
device,
|
|
1529
|
+
requestId,
|
|
1530
|
+
BluetoothGatt.GATT_READ_NOT_PERMITTED,
|
|
1531
|
+
offset,
|
|
1532
|
+
null
|
|
1533
|
+
)
|
|
1534
|
+
return
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
val value = getCharacteristicValue(characteristic) ?: byteArrayOf()
|
|
1538
|
+
if (offset > value.size) {
|
|
1539
|
+
gattServer?.sendResponse(
|
|
1540
|
+
device,
|
|
1541
|
+
requestId,
|
|
1542
|
+
BluetoothGatt.GATT_INVALID_OFFSET,
|
|
1543
|
+
offset,
|
|
1544
|
+
null
|
|
1545
|
+
)
|
|
1546
|
+
return
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
gattServer?.sendResponse(
|
|
1550
|
+
device,
|
|
682
1551
|
requestId,
|
|
683
1552
|
BluetoothGatt.GATT_SUCCESS,
|
|
684
1553
|
offset,
|
|
685
|
-
|
|
1554
|
+
value.copyOfRange(offset, value.size)
|
|
1555
|
+
)
|
|
1556
|
+
eventEmitter.emit(
|
|
1557
|
+
"peripheralReadRequest",
|
|
1558
|
+
mapOf(
|
|
1559
|
+
"centralId" to device.address,
|
|
1560
|
+
"serviceUUID" to characteristic.service.uuid.toString(),
|
|
1561
|
+
"characteristicUUID" to characteristic.uuid.toString(),
|
|
1562
|
+
"value" to value.toHexString()
|
|
1563
|
+
)
|
|
686
1564
|
)
|
|
687
1565
|
}
|
|
688
1566
|
|
|
@@ -695,7 +1573,173 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
695
1573
|
offset: Int,
|
|
696
1574
|
value: ByteArray?
|
|
697
1575
|
) {
|
|
698
|
-
characteristic.
|
|
1576
|
+
val canWrite = (characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE) != 0 ||
|
|
1577
|
+
(characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0
|
|
1578
|
+
if (!canWrite) {
|
|
1579
|
+
if (responseNeeded) {
|
|
1580
|
+
gattServer?.sendResponse(
|
|
1581
|
+
device,
|
|
1582
|
+
requestId,
|
|
1583
|
+
BluetoothGatt.GATT_WRITE_NOT_PERMITTED,
|
|
1584
|
+
offset,
|
|
1585
|
+
null
|
|
1586
|
+
)
|
|
1587
|
+
}
|
|
1588
|
+
return
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
val incomingValue = value ?: byteArrayOf()
|
|
1592
|
+
val currentValue = getCharacteristicValue(characteristic) ?: byteArrayOf()
|
|
1593
|
+
if (offset > currentValue.size) {
|
|
1594
|
+
if (responseNeeded) {
|
|
1595
|
+
gattServer?.sendResponse(
|
|
1596
|
+
device,
|
|
1597
|
+
requestId,
|
|
1598
|
+
BluetoothGatt.GATT_INVALID_OFFSET,
|
|
1599
|
+
offset,
|
|
1600
|
+
null
|
|
1601
|
+
)
|
|
1602
|
+
}
|
|
1603
|
+
return
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
val nextValue = if (offset == 0) {
|
|
1607
|
+
incomingValue
|
|
1608
|
+
} else {
|
|
1609
|
+
val replaceEnd = minOf(offset + incomingValue.size, currentValue.size)
|
|
1610
|
+
currentValue.copyOfRange(0, offset) +
|
|
1611
|
+
incomingValue +
|
|
1612
|
+
currentValue.copyOfRange(replaceEnd, currentValue.size)
|
|
1613
|
+
}
|
|
1614
|
+
setCharacteristicValue(characteristic, nextValue)
|
|
1615
|
+
if (responseNeeded) {
|
|
1616
|
+
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null)
|
|
1617
|
+
}
|
|
1618
|
+
notifySubscribedDevices(characteristic)
|
|
1619
|
+
eventEmitter.emit(
|
|
1620
|
+
"peripheralWriteRequest",
|
|
1621
|
+
mapOf(
|
|
1622
|
+
"centralId" to device.address,
|
|
1623
|
+
"serviceUUID" to characteristic.service.uuid.toString(),
|
|
1624
|
+
"characteristicUUID" to characteristic.uuid.toString(),
|
|
1625
|
+
"value" to nextValue.toHexString()
|
|
1626
|
+
)
|
|
1627
|
+
)
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
override fun onDescriptorReadRequest(
|
|
1631
|
+
device: BluetoothDevice,
|
|
1632
|
+
requestId: Int,
|
|
1633
|
+
offset: Int,
|
|
1634
|
+
descriptor: BluetoothGattDescriptor
|
|
1635
|
+
) {
|
|
1636
|
+
if (descriptor.uuid != CLIENT_CHARACTERISTIC_CONFIG_UUID) {
|
|
1637
|
+
if ((descriptor.permissions and BluetoothGattDescriptor.PERMISSION_READ) == 0) {
|
|
1638
|
+
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_READ_NOT_PERMITTED, offset, null)
|
|
1639
|
+
return
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
val value = getDescriptorValue(descriptor) ?: byteArrayOf()
|
|
1643
|
+
if (offset > value.size) {
|
|
1644
|
+
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_INVALID_OFFSET, offset, null)
|
|
1645
|
+
return
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
gattServer?.sendResponse(
|
|
1649
|
+
device,
|
|
1650
|
+
requestId,
|
|
1651
|
+
BluetoothGatt.GATT_SUCCESS,
|
|
1652
|
+
offset,
|
|
1653
|
+
value.copyOfRange(offset, value.size)
|
|
1654
|
+
)
|
|
1655
|
+
return
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
val characteristic = descriptor.characteristic
|
|
1659
|
+
val subscribers = subscribedDevices[characteristic.uuid]
|
|
1660
|
+
val value = if (subscribers?.contains(device) == true) {
|
|
1661
|
+
if ((characteristic.properties and BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) {
|
|
1662
|
+
BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
|
|
1663
|
+
} else {
|
|
1664
|
+
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
|
1665
|
+
}
|
|
1666
|
+
} else {
|
|
1667
|
+
BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value)
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
override fun onDescriptorWriteRequest(
|
|
1674
|
+
device: BluetoothDevice,
|
|
1675
|
+
requestId: Int,
|
|
1676
|
+
descriptor: BluetoothGattDescriptor,
|
|
1677
|
+
preparedWrite: Boolean,
|
|
1678
|
+
responseNeeded: Boolean,
|
|
1679
|
+
offset: Int,
|
|
1680
|
+
value: ByteArray?
|
|
1681
|
+
) {
|
|
1682
|
+
if (descriptor.uuid != CLIENT_CHARACTERISTIC_CONFIG_UUID) {
|
|
1683
|
+
if ((descriptor.permissions and BluetoothGattDescriptor.PERMISSION_WRITE) == 0) {
|
|
1684
|
+
if (responseNeeded) {
|
|
1685
|
+
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_WRITE_NOT_PERMITTED, offset, null)
|
|
1686
|
+
}
|
|
1687
|
+
return
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
val incomingValue = value ?: byteArrayOf()
|
|
1691
|
+
val currentValue = getDescriptorValue(descriptor) ?: byteArrayOf()
|
|
1692
|
+
if (offset > currentValue.size) {
|
|
1693
|
+
if (responseNeeded) {
|
|
1694
|
+
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_INVALID_OFFSET, offset, null)
|
|
1695
|
+
}
|
|
1696
|
+
return
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
val nextValue = if (offset == 0) {
|
|
1700
|
+
incomingValue
|
|
1701
|
+
} else {
|
|
1702
|
+
val replaceEnd = minOf(offset + incomingValue.size, currentValue.size)
|
|
1703
|
+
currentValue.copyOfRange(0, offset) +
|
|
1704
|
+
incomingValue +
|
|
1705
|
+
currentValue.copyOfRange(replaceEnd, currentValue.size)
|
|
1706
|
+
}
|
|
1707
|
+
setDescriptorValue(descriptor, nextValue)
|
|
1708
|
+
if (responseNeeded) {
|
|
1709
|
+
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null)
|
|
1710
|
+
}
|
|
1711
|
+
return
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
val characteristic = descriptor.characteristic
|
|
1715
|
+
val requestedValue = value ?: BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
|
|
1716
|
+
val enabled = requestedValue.contentEquals(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) ||
|
|
1717
|
+
requestedValue.contentEquals(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE)
|
|
1718
|
+
|
|
1719
|
+
if (enabled) {
|
|
1720
|
+
subscribedDevices.getOrPut(characteristic.uuid) { mutableSetOf() }.add(device)
|
|
1721
|
+
setDescriptorValue(descriptor, requestedValue)
|
|
1722
|
+
eventEmitter.emit(
|
|
1723
|
+
"peripheralSubscribed",
|
|
1724
|
+
mapOf(
|
|
1725
|
+
"centralId" to device.address,
|
|
1726
|
+
"serviceUUID" to characteristic.service.uuid.toString(),
|
|
1727
|
+
"characteristicUUID" to characteristic.uuid.toString()
|
|
1728
|
+
)
|
|
1729
|
+
)
|
|
1730
|
+
} else {
|
|
1731
|
+
subscribedDevices[characteristic.uuid]?.remove(device)
|
|
1732
|
+
setDescriptorValue(descriptor, BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)
|
|
1733
|
+
eventEmitter.emit(
|
|
1734
|
+
"peripheralUnsubscribed",
|
|
1735
|
+
mapOf(
|
|
1736
|
+
"centralId" to device.address,
|
|
1737
|
+
"serviceUUID" to characteristic.service.uuid.toString(),
|
|
1738
|
+
"characteristicUUID" to characteristic.uuid.toString()
|
|
1739
|
+
)
|
|
1740
|
+
)
|
|
1741
|
+
}
|
|
1742
|
+
|
|
699
1743
|
if (responseNeeded) {
|
|
700
1744
|
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null)
|
|
701
1745
|
}
|
|
@@ -707,25 +1751,35 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
707
1751
|
return object : BluetoothGattCallback() {
|
|
708
1752
|
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
|
709
1753
|
if (status != BluetoothGatt.GATT_SUCCESS && newState != BluetoothProfile.STATE_CONNECTED) {
|
|
1754
|
+
if (retryPendingConnection(deviceId, gatt, status)) {
|
|
1755
|
+
return
|
|
1756
|
+
}
|
|
1757
|
+
pendingConnectionTimeouts.remove(deviceId)?.cancel()
|
|
1758
|
+
pendingConnectionAttempts.remove(deviceId)
|
|
710
1759
|
pendingConnections.remove(deviceId)?.reject(
|
|
711
1760
|
IllegalStateException("Failed to connect to $deviceId (status=$status)")
|
|
712
1761
|
)
|
|
713
|
-
connectedDevices.remove(deviceId)?.close()
|
|
1762
|
+
(pendingConnectionGatts.remove(deviceId) ?: connectedDevices.remove(deviceId))?.close()
|
|
714
1763
|
return
|
|
715
1764
|
}
|
|
716
1765
|
|
|
717
1766
|
when (newState) {
|
|
718
1767
|
BluetoothProfile.STATE_CONNECTED -> {
|
|
1768
|
+
pendingConnectionTimeouts.remove(deviceId)?.cancel()
|
|
1769
|
+
pendingConnectionAttempts.remove(deviceId)
|
|
1770
|
+
pendingConnectionGatts.remove(deviceId)
|
|
719
1771
|
connectedDevices[deviceId] = gatt
|
|
720
1772
|
pendingConnections.remove(deviceId)?.resolve(Unit)
|
|
721
1773
|
eventEmitter.emit("deviceConnected", mapOf("deviceId" to deviceId))
|
|
722
1774
|
}
|
|
723
1775
|
|
|
724
1776
|
BluetoothProfile.STATE_DISCONNECTED -> {
|
|
1777
|
+
pendingConnectionTimeouts.remove(deviceId)?.cancel()
|
|
1778
|
+
pendingConnectionAttempts.remove(deviceId)
|
|
725
1779
|
pendingConnections.remove(deviceId)?.reject(
|
|
726
1780
|
IllegalStateException("Disconnected from $deviceId")
|
|
727
1781
|
)
|
|
728
|
-
connectedDevices.remove(deviceId)?.close()
|
|
1782
|
+
(pendingConnectionGatts.remove(deviceId) ?: connectedDevices.remove(deviceId))?.close()
|
|
729
1783
|
rejectPendingOperationsForDevice(
|
|
730
1784
|
deviceId,
|
|
731
1785
|
IllegalStateException("Disconnected from $deviceId")
|
|
@@ -736,23 +1790,13 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
736
1790
|
}
|
|
737
1791
|
|
|
738
1792
|
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
|
1793
|
+
cancelPendingOperationTimeout("services|$deviceId")
|
|
739
1794
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
740
1795
|
val services = buildGattServices(gatt)
|
|
741
1796
|
pendingServiceDiscoveries.remove(deviceId)?.resolve(services)
|
|
742
1797
|
eventEmitter.emit(
|
|
743
1798
|
"servicesDiscovered",
|
|
744
|
-
mapOf("deviceId" to deviceId, "services" to services.map {
|
|
745
|
-
mapOf(
|
|
746
|
-
"uuid" to service.uuid,
|
|
747
|
-
"characteristics" to service.characteristics.map { characteristic ->
|
|
748
|
-
mapOf(
|
|
749
|
-
"uuid" to characteristic.uuid,
|
|
750
|
-
"properties" to characteristic.properties.toList(),
|
|
751
|
-
"value" to characteristic.value
|
|
752
|
-
)
|
|
753
|
-
}
|
|
754
|
-
)
|
|
755
|
-
})
|
|
1799
|
+
mapOf("deviceId" to deviceId, "services" to services.map { servicePayload(it) })
|
|
756
1800
|
)
|
|
757
1801
|
} else {
|
|
758
1802
|
pendingServiceDiscoveries.remove(deviceId)?.reject(
|
|
@@ -766,29 +1810,16 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
766
1810
|
characteristic: BluetoothGattCharacteristic,
|
|
767
1811
|
status: Int
|
|
768
1812
|
) {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
"characteristicValueChanged",
|
|
780
|
-
mapOf(
|
|
781
|
-
"deviceId" to deviceId,
|
|
782
|
-
"serviceUUID" to value.serviceUUID,
|
|
783
|
-
"characteristicUUID" to value.characteristicUUID,
|
|
784
|
-
"value" to value.value
|
|
785
|
-
)
|
|
786
|
-
)
|
|
787
|
-
} else {
|
|
788
|
-
pendingReads.remove(key)?.reject(
|
|
789
|
-
IllegalStateException("Failed to read characteristic $key (status=$status)")
|
|
790
|
-
)
|
|
791
|
-
}
|
|
1813
|
+
handleCharacteristicRead(deviceId, characteristic, getCharacteristicValue(characteristic), status)
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
override fun onCharacteristicRead(
|
|
1817
|
+
gatt: BluetoothGatt,
|
|
1818
|
+
characteristic: BluetoothGattCharacteristic,
|
|
1819
|
+
value: ByteArray,
|
|
1820
|
+
status: Int
|
|
1821
|
+
) {
|
|
1822
|
+
handleCharacteristicRead(deviceId, characteristic, value, status)
|
|
792
1823
|
}
|
|
793
1824
|
|
|
794
1825
|
override fun onCharacteristicWrite(
|
|
@@ -801,6 +1832,7 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
801
1832
|
characteristic.service.uuid.toString(),
|
|
802
1833
|
characteristic.uuid.toString()
|
|
803
1834
|
)
|
|
1835
|
+
cancelPendingOperationTimeout("write|$key")
|
|
804
1836
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
805
1837
|
pendingWrites.remove(key)?.resolve(Unit)
|
|
806
1838
|
} else {
|
|
@@ -814,21 +1846,87 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
814
1846
|
gatt: BluetoothGatt,
|
|
815
1847
|
characteristic: BluetoothGattCharacteristic
|
|
816
1848
|
) {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1849
|
+
handleCharacteristicChanged(deviceId, characteristic, getCharacteristicValue(characteristic))
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
override fun onCharacteristicChanged(
|
|
1853
|
+
gatt: BluetoothGatt,
|
|
1854
|
+
characteristic: BluetoothGattCharacteristic,
|
|
1855
|
+
value: ByteArray
|
|
1856
|
+
) {
|
|
1857
|
+
handleCharacteristicChanged(deviceId, characteristic, value)
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
override fun onDescriptorRead(
|
|
1861
|
+
gatt: BluetoothGatt,
|
|
1862
|
+
descriptor: BluetoothGattDescriptor,
|
|
1863
|
+
status: Int
|
|
1864
|
+
) {
|
|
1865
|
+
handleDescriptorRead(deviceId, descriptor, getDescriptorValue(descriptor), status)
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
override fun onDescriptorRead(
|
|
1869
|
+
gatt: BluetoothGatt,
|
|
1870
|
+
descriptor: BluetoothGattDescriptor,
|
|
1871
|
+
status: Int,
|
|
1872
|
+
value: ByteArray
|
|
1873
|
+
) {
|
|
1874
|
+
handleDescriptorRead(deviceId, descriptor, value, status)
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
override fun onDescriptorWrite(
|
|
1878
|
+
gatt: BluetoothGatt,
|
|
1879
|
+
descriptor: BluetoothGattDescriptor,
|
|
1880
|
+
status: Int
|
|
1881
|
+
) {
|
|
1882
|
+
val characteristic = descriptor.characteristic
|
|
1883
|
+
val key = descriptorKey(
|
|
1884
|
+
deviceId,
|
|
1885
|
+
characteristic.service.uuid.toString(),
|
|
1886
|
+
characteristic.uuid.toString(),
|
|
1887
|
+
descriptor.uuid.toString()
|
|
828
1888
|
)
|
|
1889
|
+
cancelPendingOperationTimeout("descriptorWrite|$key")
|
|
1890
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
1891
|
+
pendingDescriptorWrites.remove(key)?.resolve(Unit)
|
|
1892
|
+
} else {
|
|
1893
|
+
pendingDescriptorWrites.remove(key)?.reject(
|
|
1894
|
+
IllegalStateException("Failed to write descriptor $key (status=$status)")
|
|
1895
|
+
)
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
|
|
1900
|
+
cancelPendingOperationTimeout("mtu|$deviceId")
|
|
1901
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
1902
|
+
pendingMtuRequests.remove(deviceId)?.resolve(mtu.toDouble())
|
|
1903
|
+
} else {
|
|
1904
|
+
pendingMtuRequests.remove(deviceId)?.reject(
|
|
1905
|
+
IllegalStateException("Failed to request MTU for $deviceId (status=$status)")
|
|
1906
|
+
)
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
override fun onPhyRead(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) {
|
|
1911
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
|
1912
|
+
|
|
1913
|
+
cancelPendingOperationTimeout("phy|$deviceId")
|
|
1914
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
1915
|
+
pendingPhyReads.remove(deviceId)?.resolve(
|
|
1916
|
+
PhyStatus(
|
|
1917
|
+
txPhy = constantToPhy(txPhy),
|
|
1918
|
+
rxPhy = constantToPhy(rxPhy)
|
|
1919
|
+
)
|
|
1920
|
+
)
|
|
1921
|
+
} else {
|
|
1922
|
+
pendingPhyReads.remove(deviceId)?.reject(
|
|
1923
|
+
IllegalStateException("Failed to read PHY for $deviceId (status=$status)")
|
|
1924
|
+
)
|
|
1925
|
+
}
|
|
829
1926
|
}
|
|
830
1927
|
|
|
831
1928
|
override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) {
|
|
1929
|
+
cancelPendingOperationTimeout("rssi|$deviceId")
|
|
832
1930
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
833
1931
|
val rssiValue = rssi.toDouble()
|
|
834
1932
|
lastRssiValues[deviceId] = rssiValue
|
|
@@ -846,15 +1944,213 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
846
1944
|
}
|
|
847
1945
|
}
|
|
848
1946
|
|
|
1947
|
+
private fun handleCharacteristicRead(
|
|
1948
|
+
deviceId: String,
|
|
1949
|
+
characteristic: BluetoothGattCharacteristic,
|
|
1950
|
+
valueBytes: ByteArray?,
|
|
1951
|
+
status: Int
|
|
1952
|
+
) {
|
|
1953
|
+
val key = characteristicKey(
|
|
1954
|
+
deviceId,
|
|
1955
|
+
characteristic.service.uuid.toString(),
|
|
1956
|
+
characteristic.uuid.toString()
|
|
1957
|
+
)
|
|
1958
|
+
cancelPendingOperationTimeout("read|$key")
|
|
1959
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
1960
|
+
val value = buildCharacteristicValue(characteristic, valueBytes)
|
|
1961
|
+
lastCharacteristicValues[key] = value
|
|
1962
|
+
pendingReads.remove(key)?.resolve(value)
|
|
1963
|
+
emitCharacteristicValueChanged(deviceId, value)
|
|
1964
|
+
} else {
|
|
1965
|
+
pendingReads.remove(key)?.reject(
|
|
1966
|
+
IllegalStateException("Failed to read characteristic $key (status=$status)")
|
|
1967
|
+
)
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
private fun handleCharacteristicChanged(
|
|
1972
|
+
deviceId: String,
|
|
1973
|
+
characteristic: BluetoothGattCharacteristic,
|
|
1974
|
+
valueBytes: ByteArray?
|
|
1975
|
+
) {
|
|
1976
|
+
val value = buildCharacteristicValue(characteristic, valueBytes)
|
|
1977
|
+
val key = characteristicKey(deviceId, value.serviceUUID, value.characteristicUUID)
|
|
1978
|
+
lastCharacteristicValues[key] = value
|
|
1979
|
+
emitCharacteristicValueChanged(deviceId, value)
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
private fun handleDescriptorRead(
|
|
1983
|
+
deviceId: String,
|
|
1984
|
+
descriptor: BluetoothGattDescriptor,
|
|
1985
|
+
valueBytes: ByteArray?,
|
|
1986
|
+
status: Int
|
|
1987
|
+
) {
|
|
1988
|
+
val characteristic = descriptor.characteristic
|
|
1989
|
+
val key = descriptorKey(
|
|
1990
|
+
deviceId,
|
|
1991
|
+
characteristic.service.uuid.toString(),
|
|
1992
|
+
characteristic.uuid.toString(),
|
|
1993
|
+
descriptor.uuid.toString()
|
|
1994
|
+
)
|
|
1995
|
+
cancelPendingOperationTimeout("descriptorRead|$key")
|
|
1996
|
+
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
1997
|
+
pendingDescriptorReads.remove(key)?.resolve(buildDescriptorValue(descriptor, valueBytes))
|
|
1998
|
+
} else {
|
|
1999
|
+
pendingDescriptorReads.remove(key)?.reject(
|
|
2000
|
+
IllegalStateException("Failed to read descriptor $key (status=$status)")
|
|
2001
|
+
)
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
private fun emitCharacteristicValueChanged(deviceId: String, value: CharacteristicValue) {
|
|
2006
|
+
eventEmitter.emit(
|
|
2007
|
+
"characteristicValueChanged",
|
|
2008
|
+
mapOf(
|
|
2009
|
+
"deviceId" to deviceId,
|
|
2010
|
+
"serviceUUID" to value.serviceUUID,
|
|
2011
|
+
"characteristicUUID" to value.characteristicUUID,
|
|
2012
|
+
"value" to value.value
|
|
2013
|
+
)
|
|
2014
|
+
)
|
|
2015
|
+
}
|
|
2016
|
+
|
|
849
2017
|
private fun rejectPendingOperationsForDevice(deviceId: String, error: Throwable) {
|
|
2018
|
+
pendingConnectionTimeouts.remove(deviceId)?.cancel()
|
|
2019
|
+
pendingConnectionAttempts.remove(deviceId)
|
|
2020
|
+
cancelPendingOperationTimeoutsForDevice(deviceId)
|
|
850
2021
|
pendingReads.keys
|
|
851
2022
|
.filter { it.startsWith("$deviceId|") }
|
|
852
2023
|
.forEach { key -> pendingReads.remove(key)?.reject(error) }
|
|
853
2024
|
pendingWrites.keys
|
|
854
2025
|
.filter { it.startsWith("$deviceId|") }
|
|
855
2026
|
.forEach { key -> pendingWrites.remove(key)?.reject(error) }
|
|
2027
|
+
pendingDescriptorReads.keys
|
|
2028
|
+
.filter { it.startsWith("$deviceId|") }
|
|
2029
|
+
.forEach { key -> pendingDescriptorReads.remove(key)?.reject(error) }
|
|
2030
|
+
pendingDescriptorWrites.keys
|
|
2031
|
+
.filter { it.startsWith("$deviceId|") }
|
|
2032
|
+
.forEach { key -> pendingDescriptorWrites.remove(key)?.reject(error) }
|
|
856
2033
|
pendingServiceDiscoveries.remove(deviceId)?.reject(error)
|
|
857
2034
|
pendingRssiReads.remove(deviceId)?.reject(error)
|
|
2035
|
+
pendingMtuRequests.remove(deviceId)?.reject(error)
|
|
2036
|
+
pendingPhyReads.remove(deviceId)?.reject(error)
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
private fun scheduleConnectionTimeout(deviceId: String) {
|
|
2040
|
+
pendingConnectionTimeouts.remove(deviceId)?.cancel()
|
|
2041
|
+
pendingConnectionTimeouts[deviceId] = bluetoothScope.launch {
|
|
2042
|
+
delay(CONNECTION_TIMEOUT_MS)
|
|
2043
|
+
pendingConnectionTimeouts.remove(deviceId)
|
|
2044
|
+
pendingConnectionAttempts.remove(deviceId)
|
|
2045
|
+
val promise = pendingConnections.remove(deviceId) ?: return@launch
|
|
2046
|
+
(pendingConnectionGatts.remove(deviceId) ?: connectedDevices.remove(deviceId))?.let { gatt ->
|
|
2047
|
+
gatt.disconnect()
|
|
2048
|
+
gatt.close()
|
|
2049
|
+
}
|
|
2050
|
+
promise.reject(IllegalStateException("Connection timed out for $deviceId"))
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
private fun startGattConnection(deviceId: String, device: BluetoothDevice) {
|
|
2055
|
+
val context = NitroModules.applicationContext
|
|
2056
|
+
if (context == null) {
|
|
2057
|
+
pendingConnectionTimeouts.remove(deviceId)?.cancel()
|
|
2058
|
+
pendingConnectionAttempts.remove(deviceId)
|
|
2059
|
+
pendingConnections.remove(deviceId)?.reject(IllegalStateException("React context unavailable"))
|
|
2060
|
+
return
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
val gatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
2064
|
+
device.connectGatt(context, false, createGattCallback(deviceId), BluetoothDevice.TRANSPORT_LE)
|
|
2065
|
+
} else {
|
|
2066
|
+
device.connectGatt(context, false, createGattCallback(deviceId))
|
|
2067
|
+
}
|
|
2068
|
+
pendingConnectionGatts[deviceId] = gatt
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
private fun retryPendingConnection(deviceId: String, failedGatt: BluetoothGatt, status: Int): Boolean {
|
|
2072
|
+
val promise = pendingConnections[deviceId] ?: return false
|
|
2073
|
+
val attempt = pendingConnectionAttempts[deviceId] ?: 0
|
|
2074
|
+
if (attempt >= MAX_CONNECTION_RETRIES) {
|
|
2075
|
+
return false
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
pendingConnectionAttempts[deviceId] = attempt + 1
|
|
2079
|
+
pendingConnectionGatts.remove(deviceId)
|
|
2080
|
+
failedGatt.close()
|
|
2081
|
+
|
|
2082
|
+
Log.w(
|
|
2083
|
+
TAG,
|
|
2084
|
+
"BLE connection attempt ${attempt + 1} for $deviceId failed with status=$status; retrying"
|
|
2085
|
+
)
|
|
2086
|
+
|
|
2087
|
+
bluetoothScope.launch {
|
|
2088
|
+
delay(CONNECTION_RETRY_DELAY_MS * (attempt + 1))
|
|
2089
|
+
if (pendingConnections[deviceId] === promise) {
|
|
2090
|
+
startGattConnection(deviceId, failedGatt.device)
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
return true
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
private fun <T> schedulePendingOperationTimeout(
|
|
2097
|
+
timeoutKey: String,
|
|
2098
|
+
description: String,
|
|
2099
|
+
removePending: () -> Promise<T>?
|
|
2100
|
+
) {
|
|
2101
|
+
pendingOperationTimeouts.remove(timeoutKey)?.cancel()
|
|
2102
|
+
pendingOperationTimeouts[timeoutKey] = bluetoothScope.launch {
|
|
2103
|
+
delay(OPERATION_TIMEOUT_MS)
|
|
2104
|
+
pendingOperationTimeouts.remove(timeoutKey)
|
|
2105
|
+
removePending()?.reject(IllegalStateException("$description timed out"))
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
private fun cancelPendingOperationTimeout(timeoutKey: String) {
|
|
2110
|
+
pendingOperationTimeouts.remove(timeoutKey)?.cancel()
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
private fun cancelPendingOperationTimeoutsForDevice(deviceId: String) {
|
|
2114
|
+
pendingOperationTimeouts.keys
|
|
2115
|
+
.filter { key ->
|
|
2116
|
+
key == "services|$deviceId" ||
|
|
2117
|
+
key == "rssi|$deviceId" ||
|
|
2118
|
+
key == "mtu|$deviceId" ||
|
|
2119
|
+
key == "phy|$deviceId" ||
|
|
2120
|
+
key.contains("|$deviceId|")
|
|
2121
|
+
}
|
|
2122
|
+
.forEach { key -> pendingOperationTimeouts.remove(key)?.cancel() }
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
private fun notifySubscribedDevices(characteristic: BluetoothGattCharacteristic) {
|
|
2126
|
+
if (!supportsNotifyOrIndicate(characteristic)) return
|
|
2127
|
+
|
|
2128
|
+
val subscribers = subscribedDevices[characteristic.uuid]?.toList().orEmpty()
|
|
2129
|
+
if (subscribers.isEmpty()) return
|
|
2130
|
+
|
|
2131
|
+
subscribers.forEach { device ->
|
|
2132
|
+
val confirm = (characteristic.properties and BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0
|
|
2133
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
2134
|
+
gattServer?.notifyCharacteristicChanged(
|
|
2135
|
+
device,
|
|
2136
|
+
characteristic,
|
|
2137
|
+
confirm,
|
|
2138
|
+
getCharacteristicValue(characteristic) ?: byteArrayOf()
|
|
2139
|
+
)
|
|
2140
|
+
} else {
|
|
2141
|
+
@Suppress("DEPRECATION")
|
|
2142
|
+
gattServer?.notifyCharacteristicChanged(
|
|
2143
|
+
device,
|
|
2144
|
+
characteristic,
|
|
2145
|
+
confirm
|
|
2146
|
+
)
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
private fun supportsNotifyOrIndicate(characteristic: BluetoothGattCharacteristic): Boolean {
|
|
2152
|
+
return (characteristic.properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0 ||
|
|
2153
|
+
(characteristic.properties and BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0
|
|
858
2154
|
}
|
|
859
2155
|
|
|
860
2156
|
private fun buildGattServices(gatt: BluetoothGatt): Array<GATTService> {
|
|
@@ -865,21 +2161,66 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
865
2161
|
GATTCharacteristic(
|
|
866
2162
|
uuid = characteristic.uuid.toString(),
|
|
867
2163
|
properties = propertiesToArray(characteristic.properties),
|
|
868
|
-
value = characteristic
|
|
2164
|
+
value = getCharacteristicValue(characteristic)?.toHexString(),
|
|
2165
|
+
descriptors = characteristic.descriptors.map { descriptor ->
|
|
2166
|
+
GATTDescriptor(
|
|
2167
|
+
uuid = descriptor.uuid.toString(),
|
|
2168
|
+
value = getDescriptorValue(descriptor)?.toHexString(),
|
|
2169
|
+
permissions = permissionsToArray(descriptor.permissions)
|
|
2170
|
+
)
|
|
2171
|
+
}.toTypedArray()
|
|
869
2172
|
)
|
|
870
|
-
}.toTypedArray()
|
|
2173
|
+
}.toTypedArray(),
|
|
2174
|
+
includedServices = service.includedServices.map { it.uuid.toString() }.toTypedArray()
|
|
871
2175
|
)
|
|
872
2176
|
}.toTypedArray()
|
|
873
2177
|
}
|
|
874
2178
|
|
|
875
|
-
private fun
|
|
2179
|
+
private fun servicePayload(service: GATTService): Map<String, Any?> {
|
|
2180
|
+
return mapOf(
|
|
2181
|
+
"uuid" to service.uuid,
|
|
2182
|
+
"characteristics" to service.characteristics.map { characteristic ->
|
|
2183
|
+
mapOf(
|
|
2184
|
+
"uuid" to characteristic.uuid,
|
|
2185
|
+
"properties" to characteristic.properties.toList(),
|
|
2186
|
+
"value" to characteristic.value,
|
|
2187
|
+
"descriptors" to characteristic.descriptors?.map { descriptor ->
|
|
2188
|
+
mapOf(
|
|
2189
|
+
"uuid" to descriptor.uuid,
|
|
2190
|
+
"value" to descriptor.value,
|
|
2191
|
+
"permissions" to descriptor.permissions?.toList()
|
|
2192
|
+
)
|
|
2193
|
+
}
|
|
2194
|
+
)
|
|
2195
|
+
},
|
|
2196
|
+
"includedServices" to service.includedServices?.toList()
|
|
2197
|
+
)
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
private fun buildCharacteristicValue(
|
|
2201
|
+
characteristic: BluetoothGattCharacteristic,
|
|
2202
|
+
valueBytes: ByteArray? = getCharacteristicValue(characteristic)
|
|
2203
|
+
): CharacteristicValue {
|
|
876
2204
|
return CharacteristicValue(
|
|
877
|
-
value =
|
|
2205
|
+
value = valueBytes?.toHexString() ?: "",
|
|
878
2206
|
serviceUUID = characteristic.service.uuid.toString(),
|
|
879
2207
|
characteristicUUID = characteristic.uuid.toString()
|
|
880
2208
|
)
|
|
881
2209
|
}
|
|
882
2210
|
|
|
2211
|
+
private fun buildDescriptorValue(
|
|
2212
|
+
descriptor: BluetoothGattDescriptor,
|
|
2213
|
+
valueBytes: ByteArray? = getDescriptorValue(descriptor)
|
|
2214
|
+
): DescriptorValue {
|
|
2215
|
+
val characteristic = descriptor.characteristic
|
|
2216
|
+
return DescriptorValue(
|
|
2217
|
+
value = valueBytes?.toHexString() ?: "",
|
|
2218
|
+
serviceUUID = characteristic.service.uuid.toString(),
|
|
2219
|
+
characteristicUUID = characteristic.uuid.toString(),
|
|
2220
|
+
descriptorUUID = descriptor.uuid.toString()
|
|
2221
|
+
)
|
|
2222
|
+
}
|
|
2223
|
+
|
|
883
2224
|
private fun findCharacteristic(
|
|
884
2225
|
gatt: BluetoothGatt,
|
|
885
2226
|
serviceUUID: String,
|
|
@@ -892,6 +2233,68 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
892
2233
|
}
|
|
893
2234
|
}
|
|
894
2235
|
|
|
2236
|
+
private fun findDescriptor(
|
|
2237
|
+
gatt: BluetoothGatt,
|
|
2238
|
+
serviceUUID: String,
|
|
2239
|
+
characteristicUUID: String,
|
|
2240
|
+
descriptorUUID: String
|
|
2241
|
+
): BluetoothGattDescriptor? {
|
|
2242
|
+
val characteristic = findCharacteristic(gatt, serviceUUID, characteristicUUID) ?: return null
|
|
2243
|
+
return characteristic.descriptors.firstOrNull {
|
|
2244
|
+
it.uuid.toString().equals(descriptorUUID, ignoreCase = true)
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
@Suppress("DEPRECATION")
|
|
2249
|
+
private fun getCharacteristicValue(characteristic: BluetoothGattCharacteristic): ByteArray? {
|
|
2250
|
+
return characteristic.value
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
@Suppress("DEPRECATION")
|
|
2254
|
+
private fun setCharacteristicValue(characteristic: BluetoothGattCharacteristic, value: ByteArray) {
|
|
2255
|
+
characteristic.value = value
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
@Suppress("DEPRECATION")
|
|
2259
|
+
private fun getDescriptorValue(descriptor: BluetoothGattDescriptor): ByteArray? {
|
|
2260
|
+
return descriptor.value
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
@Suppress("DEPRECATION")
|
|
2264
|
+
private fun setDescriptorValue(descriptor: BluetoothGattDescriptor, value: ByteArray) {
|
|
2265
|
+
descriptor.value = value
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
@Suppress("DEPRECATION")
|
|
2269
|
+
private fun writeGattCharacteristic(
|
|
2270
|
+
gatt: BluetoothGatt,
|
|
2271
|
+
characteristic: BluetoothGattCharacteristic,
|
|
2272
|
+
value: ByteArray,
|
|
2273
|
+
writeType: Int
|
|
2274
|
+
): Boolean {
|
|
2275
|
+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
2276
|
+
gatt.writeCharacteristic(characteristic, value, writeType) == BluetoothStatusCodes.SUCCESS
|
|
2277
|
+
} else {
|
|
2278
|
+
characteristic.value = value
|
|
2279
|
+
characteristic.writeType = writeType
|
|
2280
|
+
gatt.writeCharacteristic(characteristic)
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
@Suppress("DEPRECATION")
|
|
2285
|
+
private fun writeGattDescriptor(
|
|
2286
|
+
gatt: BluetoothGatt,
|
|
2287
|
+
descriptor: BluetoothGattDescriptor,
|
|
2288
|
+
value: ByteArray
|
|
2289
|
+
): Boolean {
|
|
2290
|
+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
2291
|
+
gatt.writeDescriptor(descriptor, value) == BluetoothStatusCodes.SUCCESS
|
|
2292
|
+
} else {
|
|
2293
|
+
descriptor.value = value
|
|
2294
|
+
gatt.writeDescriptor(descriptor)
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
|
|
895
2298
|
private fun characteristicKey(
|
|
896
2299
|
deviceId: String,
|
|
897
2300
|
serviceUUID: String,
|
|
@@ -900,6 +2303,38 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
900
2303
|
return "$deviceId|${serviceUUID.lowercase()}|${characteristicUUID.lowercase()}"
|
|
901
2304
|
}
|
|
902
2305
|
|
|
2306
|
+
private fun descriptorKey(
|
|
2307
|
+
deviceId: String,
|
|
2308
|
+
serviceUUID: String,
|
|
2309
|
+
characteristicUUID: String,
|
|
2310
|
+
descriptorUUID: String
|
|
2311
|
+
): String {
|
|
2312
|
+
return "${characteristicKey(deviceId, serviceUUID, characteristicUUID)}|${descriptorUUID.lowercase()}"
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
private fun resolveBluetoothDevice(deviceId: String): BluetoothDevice? {
|
|
2316
|
+
ensureBluetoothManager()
|
|
2317
|
+
return discoveredDevices[deviceId]
|
|
2318
|
+
?: connectedDevices[deviceId]?.device
|
|
2319
|
+
?: try {
|
|
2320
|
+
bluetoothAdapter?.getRemoteDevice(deviceId)
|
|
2321
|
+
} catch (_: IllegalArgumentException) {
|
|
2322
|
+
null
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
private fun findLocalCharacteristic(
|
|
2327
|
+
serviceUUID: String,
|
|
2328
|
+
characteristicUUID: String
|
|
2329
|
+
): BluetoothGattCharacteristic? {
|
|
2330
|
+
return try {
|
|
2331
|
+
val service = gattServer?.getService(UUID.fromString(serviceUUID))
|
|
2332
|
+
service?.getCharacteristic(UUID.fromString(characteristicUUID))
|
|
2333
|
+
} catch (_: IllegalArgumentException) {
|
|
2334
|
+
null
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
|
|
903
2338
|
private fun buildScanPayload(result: ScanResult): Map<String, Any?> {
|
|
904
2339
|
val record = result.scanRecord
|
|
905
2340
|
val manufacturerData = extractManufacturerData(record)
|
|
@@ -911,14 +2346,10 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
911
2346
|
record?.deviceName?.let { advertisingData["completeLocalName"] = it }
|
|
912
2347
|
txPower?.let { advertisingData["txPowerLevel"] = it }
|
|
913
2348
|
manufacturerData?.let { advertisingData["manufacturerData"] = it }
|
|
914
|
-
serviceUUIDs?.let { advertisingData
|
|
2349
|
+
serviceUUIDs?.let { addServiceUuidBuckets(it, advertisingData) }
|
|
915
2350
|
serviceData?.takeIf { it.isNotEmpty() }?.let { entries ->
|
|
916
|
-
|
|
917
|
-
}
|
|
918
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
919
|
-
advertisingData["isConnectable"] = result.isConnectable
|
|
2351
|
+
addServiceDataBuckets(entries, advertisingData)
|
|
920
2352
|
}
|
|
921
|
-
advertisingData["rssi"] = result.rssi
|
|
922
2353
|
|
|
923
2354
|
return mapOf(
|
|
924
2355
|
"id" to result.device.address,
|
|
@@ -934,6 +2365,65 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
934
2365
|
)
|
|
935
2366
|
}
|
|
936
2367
|
|
|
2368
|
+
private fun addServiceUuidBuckets(
|
|
2369
|
+
uuidStrings: List<String>,
|
|
2370
|
+
target: MutableMap<String, Any?>
|
|
2371
|
+
) {
|
|
2372
|
+
addUuidBuckets(
|
|
2373
|
+
uuidStrings,
|
|
2374
|
+
key16 = "completeServiceUUIDs16",
|
|
2375
|
+
key32 = "completeServiceUUIDs32",
|
|
2376
|
+
key128 = "completeServiceUUIDs128",
|
|
2377
|
+
target = target
|
|
2378
|
+
)
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
private fun addUuidBuckets(
|
|
2382
|
+
uuidStrings: List<String>,
|
|
2383
|
+
key16: String,
|
|
2384
|
+
key32: String,
|
|
2385
|
+
key128: String,
|
|
2386
|
+
target: MutableMap<String, Any?>
|
|
2387
|
+
) {
|
|
2388
|
+
val uuid16 = uuidStrings.filter { uuidBitWidth(it) == 16 }
|
|
2389
|
+
val uuid32 = uuidStrings.filter { uuidBitWidth(it) == 32 }
|
|
2390
|
+
val uuid128 = uuidStrings.filter { uuidBitWidth(it) == 128 }
|
|
2391
|
+
|
|
2392
|
+
if (uuid16.isNotEmpty()) target[key16] = uuid16
|
|
2393
|
+
if (uuid32.isNotEmpty()) target[key32] = uuid32
|
|
2394
|
+
if (uuid128.isNotEmpty()) target[key128] = uuid128
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
private fun addServiceDataBuckets(
|
|
2398
|
+
entries: List<ServiceDataEntry>,
|
|
2399
|
+
target: MutableMap<String, Any?>
|
|
2400
|
+
) {
|
|
2401
|
+
val serviceData16 = entries.filter { uuidBitWidth(it.uuid) == 16 }
|
|
2402
|
+
.map { mapOf("uuid" to it.uuid, "data" to it.data) }
|
|
2403
|
+
val serviceData32 = entries.filter { uuidBitWidth(it.uuid) == 32 }
|
|
2404
|
+
.map { mapOf("uuid" to it.uuid, "data" to it.data) }
|
|
2405
|
+
val serviceData128 = entries.filter { uuidBitWidth(it.uuid) == 128 }
|
|
2406
|
+
.map { mapOf("uuid" to it.uuid, "data" to it.data) }
|
|
2407
|
+
|
|
2408
|
+
if (serviceData16.isNotEmpty()) target["serviceData16"] = serviceData16
|
|
2409
|
+
if (serviceData32.isNotEmpty()) target["serviceData32"] = serviceData32
|
|
2410
|
+
if (serviceData128.isNotEmpty()) target["serviceData128"] = serviceData128
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
private fun uuidBitWidth(uuidString: String): Int {
|
|
2414
|
+
return when (uuidString.replace("-", "").length) {
|
|
2415
|
+
4 -> 16
|
|
2416
|
+
8 -> 32
|
|
2417
|
+
else -> 128
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
private fun emitDeviceFound(payload: Map<String, Any?>) {
|
|
2422
|
+
eventEmitter.emit("deviceFound", payload)
|
|
2423
|
+
eventEmitter.emit("onDeviceFound", payload)
|
|
2424
|
+
eventEmitter.emit("scanResult", payload)
|
|
2425
|
+
}
|
|
2426
|
+
|
|
937
2427
|
private fun extractManufacturerData(record: ScanRecord?): String? {
|
|
938
2428
|
val data = record?.manufacturerSpecificData ?: return null
|
|
939
2429
|
if (data.size() == 0) return null
|
|
@@ -950,14 +2440,17 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
950
2440
|
|
|
951
2441
|
private fun processAdvertisingData(
|
|
952
2442
|
data: AdvertisingDataTypes,
|
|
953
|
-
dataBuilder: AdvertiseData.Builder
|
|
2443
|
+
dataBuilder: AdvertiseData.Builder,
|
|
2444
|
+
includeServiceUuids: Boolean = true
|
|
954
2445
|
) {
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
2446
|
+
if (includeServiceUuids) {
|
|
2447
|
+
addServiceUUIDs(data.incompleteServiceUUIDs16, dataBuilder)
|
|
2448
|
+
addServiceUUIDs(data.completeServiceUUIDs16, dataBuilder)
|
|
2449
|
+
addServiceUUIDs(data.incompleteServiceUUIDs32, dataBuilder)
|
|
2450
|
+
addServiceUUIDs(data.completeServiceUUIDs32, dataBuilder)
|
|
2451
|
+
addServiceUUIDs(data.incompleteServiceUUIDs128, dataBuilder)
|
|
2452
|
+
addServiceUUIDs(data.completeServiceUUIDs128, dataBuilder)
|
|
2453
|
+
}
|
|
961
2454
|
|
|
962
2455
|
if (data.shortenedLocalName != null || data.completeLocalName != null) {
|
|
963
2456
|
dataBuilder.setIncludeDeviceName(true)
|
|
@@ -966,9 +2459,11 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
966
2459
|
dataBuilder.setIncludeTxPowerLevel(true)
|
|
967
2460
|
}
|
|
968
2461
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
2462
|
+
if (includeServiceUuids) {
|
|
2463
|
+
addServiceUUIDs(data.serviceSolicitationUUIDs16, dataBuilder)
|
|
2464
|
+
addServiceUUIDs(data.serviceSolicitationUUIDs32, dataBuilder)
|
|
2465
|
+
addServiceUUIDs(data.serviceSolicitationUUIDs128, dataBuilder)
|
|
2466
|
+
}
|
|
972
2467
|
addServiceData(data.serviceData16, dataBuilder)
|
|
973
2468
|
addServiceData(data.serviceData32, dataBuilder)
|
|
974
2469
|
addServiceData(data.serviceData128, dataBuilder)
|
|
@@ -1054,6 +2549,273 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
1054
2549
|
return result.toTypedArray()
|
|
1055
2550
|
}
|
|
1056
2551
|
|
|
2552
|
+
private fun descriptorPermissionsFromArray(permissions: Array<String>?): Int {
|
|
2553
|
+
if (permissions.isNullOrEmpty()) {
|
|
2554
|
+
return BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
var result = 0
|
|
2558
|
+
permissions.forEach { permission ->
|
|
2559
|
+
when (permission) {
|
|
2560
|
+
"read" -> result = result or BluetoothGattDescriptor.PERMISSION_READ
|
|
2561
|
+
"write" -> result = result or BluetoothGattDescriptor.PERMISSION_WRITE
|
|
2562
|
+
"readEncrypted" -> result = result or BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED
|
|
2563
|
+
"writeEncrypted" -> result = result or BluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED
|
|
2564
|
+
"readEncryptedMitm" -> {
|
|
2565
|
+
result = result or BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED_MITM
|
|
2566
|
+
}
|
|
2567
|
+
"writeEncryptedMitm" -> {
|
|
2568
|
+
result = result or BluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED_MITM
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
return result
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
private fun permissionsToArray(permissions: Int): Array<String> {
|
|
2576
|
+
val result = mutableListOf<String>()
|
|
2577
|
+
if (permissions and BluetoothGattDescriptor.PERMISSION_READ != 0) result += "read"
|
|
2578
|
+
if (permissions and BluetoothGattDescriptor.PERMISSION_WRITE != 0) result += "write"
|
|
2579
|
+
if (permissions and BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED != 0) result += "readEncrypted"
|
|
2580
|
+
if (permissions and BluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED != 0) result += "writeEncrypted"
|
|
2581
|
+
if (permissions and BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED_MITM != 0) {
|
|
2582
|
+
result += "readEncryptedMitm"
|
|
2583
|
+
}
|
|
2584
|
+
if (permissions and BluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED_MITM != 0) {
|
|
2585
|
+
result += "writeEncryptedMitm"
|
|
2586
|
+
}
|
|
2587
|
+
return result.toTypedArray()
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
private fun bondStateFor(device: BluetoothDevice): BondState {
|
|
2591
|
+
return when (device.bondState) {
|
|
2592
|
+
BluetoothDevice.BOND_BONDING -> BondState.BONDING
|
|
2593
|
+
BluetoothDevice.BOND_BONDED -> BondState.BONDED
|
|
2594
|
+
else -> BondState.NONE
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
private fun phyToMask(phy: BluetoothPhy): Int {
|
|
2599
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return 0
|
|
2600
|
+
return when (phy) {
|
|
2601
|
+
BluetoothPhy.LE2M -> BluetoothDevice.PHY_LE_2M_MASK
|
|
2602
|
+
BluetoothPhy.LECODED -> BluetoothDevice.PHY_LE_CODED_MASK
|
|
2603
|
+
BluetoothPhy.LE1M -> BluetoothDevice.PHY_LE_1M_MASK
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
private fun phyToAdvertisingPhy(phy: BluetoothPhy): Int {
|
|
2608
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return 1
|
|
2609
|
+
return when (phy) {
|
|
2610
|
+
BluetoothPhy.LE2M -> BluetoothDevice.PHY_LE_2M
|
|
2611
|
+
BluetoothPhy.LECODED -> BluetoothDevice.PHY_LE_CODED
|
|
2612
|
+
BluetoothPhy.LE1M -> BluetoothDevice.PHY_LE_1M
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
private fun constantToPhy(phy: Int): BluetoothPhy {
|
|
2617
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
2618
|
+
return when (phy) {
|
|
2619
|
+
BluetoothDevice.PHY_LE_2M -> BluetoothPhy.LE2M
|
|
2620
|
+
BluetoothDevice.PHY_LE_CODED -> BluetoothPhy.LECODED
|
|
2621
|
+
else -> BluetoothPhy.LE1M
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
return BluetoothPhy.LE1M
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
private fun phyOptionToConstant(option: BluetoothPhyOption): Int {
|
|
2628
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return 0
|
|
2629
|
+
return when (option) {
|
|
2630
|
+
BluetoothPhyOption.S2 -> BluetoothDevice.PHY_OPTION_S2
|
|
2631
|
+
BluetoothPhyOption.S8 -> BluetoothDevice.PHY_OPTION_S8
|
|
2632
|
+
BluetoothPhyOption.NONE -> BluetoothDevice.PHY_OPTION_NO_PREFERRED
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
private fun acceptL2CAPConnections(psm: Int, serverSocket: BluetoothServerSocket) {
|
|
2637
|
+
while (l2capServerSockets[psm] === serverSocket) {
|
|
2638
|
+
try {
|
|
2639
|
+
val socket = serverSocket.accept()
|
|
2640
|
+
val deviceId = try {
|
|
2641
|
+
socket.remoteDevice?.address
|
|
2642
|
+
} catch (_: SecurityException) {
|
|
2643
|
+
null
|
|
2644
|
+
}
|
|
2645
|
+
registerL2CAPSocket(socket, psm, deviceId)
|
|
2646
|
+
} catch (error: IOException) {
|
|
2647
|
+
if (l2capServerSockets[psm] === serverSocket) {
|
|
2648
|
+
Log.w(TAG, "L2CAP accept failed for PSM $psm", error)
|
|
2649
|
+
}
|
|
2650
|
+
break
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
private fun registerL2CAPSocket(socket: BluetoothSocket, psm: Int, deviceId: String?): L2CAPChannel {
|
|
2656
|
+
val channelId = UUID.randomUUID().toString()
|
|
2657
|
+
l2capSockets[channelId] = socket
|
|
2658
|
+
val channel = L2CAPChannel(channelId, psm.toDouble(), deviceId)
|
|
2659
|
+
eventEmitter.emit(
|
|
2660
|
+
"l2capChannelOpened",
|
|
2661
|
+
mapOf("channelId" to channelId, "psm" to channel.psm, "deviceId" to deviceId)
|
|
2662
|
+
)
|
|
2663
|
+
startL2CAPReadLoop(channelId, socket, channel)
|
|
2664
|
+
return channel
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
private fun startL2CAPReadLoop(channelId: String, socket: BluetoothSocket, channel: L2CAPChannel) {
|
|
2668
|
+
l2capReadJobs.remove(channelId)?.cancel()
|
|
2669
|
+
l2capReadJobs[channelId] = bluetoothScope.launch(Dispatchers.IO) {
|
|
2670
|
+
val buffer = ByteArray(DEFAULT_STREAM_BUFFER_SIZE)
|
|
2671
|
+
try {
|
|
2672
|
+
while (true) {
|
|
2673
|
+
val count = socket.inputStream.read(buffer)
|
|
2674
|
+
if (count < 0) break
|
|
2675
|
+
if (count > 0) {
|
|
2676
|
+
eventEmitter.emit(
|
|
2677
|
+
"l2capDataReceived",
|
|
2678
|
+
mapOf(
|
|
2679
|
+
"channelId" to channelId,
|
|
2680
|
+
"psm" to channel.psm,
|
|
2681
|
+
"deviceId" to channel.deviceId,
|
|
2682
|
+
"value" to buffer.copyOf(count).toHexString()
|
|
2683
|
+
)
|
|
2684
|
+
)
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
} catch (error: IOException) {
|
|
2688
|
+
if (l2capSockets[channelId] === socket) {
|
|
2689
|
+
Log.w(TAG, "L2CAP channel $channelId closed", error)
|
|
2690
|
+
}
|
|
2691
|
+
} finally {
|
|
2692
|
+
if (l2capSockets[channelId] === socket) {
|
|
2693
|
+
closeL2CAPChannelInternal(channelId, true)
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
private fun closeL2CAPChannelInternal(channelId: String, emitEvent: Boolean) {
|
|
2700
|
+
l2capReadJobs.remove(channelId)?.cancel()
|
|
2701
|
+
val socket = l2capSockets.remove(channelId)
|
|
2702
|
+
try {
|
|
2703
|
+
socket?.close()
|
|
2704
|
+
} catch (error: IOException) {
|
|
2705
|
+
Log.w(TAG, "Unable to close L2CAP channel $channelId", error)
|
|
2706
|
+
}
|
|
2707
|
+
if (emitEvent) {
|
|
2708
|
+
eventEmitter.emit("l2capChannelClosed", mapOf("channelId" to channelId))
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
private fun startClassicReadLoop(deviceId: String, socket: BluetoothSocket) {
|
|
2713
|
+
classicReadJobs.remove(deviceId)?.cancel()
|
|
2714
|
+
classicReadJobs[deviceId] = bluetoothScope.launch(Dispatchers.IO) {
|
|
2715
|
+
val buffer = ByteArray(DEFAULT_STREAM_BUFFER_SIZE)
|
|
2716
|
+
try {
|
|
2717
|
+
while (true) {
|
|
2718
|
+
val count = socket.inputStream.read(buffer)
|
|
2719
|
+
if (count < 0) break
|
|
2720
|
+
if (count > 0) {
|
|
2721
|
+
eventEmitter.emit(
|
|
2722
|
+
"classicDataReceived",
|
|
2723
|
+
mapOf("deviceId" to deviceId, "value" to buffer.copyOf(count).toHexString())
|
|
2724
|
+
)
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
} catch (error: IOException) {
|
|
2728
|
+
if (classicSockets[deviceId] === socket) {
|
|
2729
|
+
Log.w(TAG, "Classic Bluetooth socket for $deviceId closed", error)
|
|
2730
|
+
}
|
|
2731
|
+
} finally {
|
|
2732
|
+
if (classicSockets[deviceId] === socket) {
|
|
2733
|
+
closeClassicSocket(deviceId, true)
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
private fun acceptClassicConnections(key: String, serverSocket: BluetoothServerSocket) {
|
|
2740
|
+
while (classicServerSockets[key] === serverSocket) {
|
|
2741
|
+
try {
|
|
2742
|
+
val socket = serverSocket.accept()
|
|
2743
|
+
val device = socket.remoteDevice
|
|
2744
|
+
val deviceId = device.address
|
|
2745
|
+
classicDevices[deviceId] = device
|
|
2746
|
+
closeClassicSocket(deviceId, false)
|
|
2747
|
+
classicSockets[deviceId] = socket
|
|
2748
|
+
startClassicReadLoop(deviceId, socket)
|
|
2749
|
+
eventEmitter.emit("classicConnectionReceived", mapOf("deviceId" to deviceId))
|
|
2750
|
+
eventEmitter.emit("classicConnected", mapOf("deviceId" to deviceId))
|
|
2751
|
+
} catch (error: IOException) {
|
|
2752
|
+
if (classicServerSockets[key] === serverSocket) {
|
|
2753
|
+
Log.w(TAG, "Classic Bluetooth RFCOMM accept failed", error)
|
|
2754
|
+
}
|
|
2755
|
+
break
|
|
2756
|
+
} catch (error: SecurityException) {
|
|
2757
|
+
Log.w(TAG, "Classic Bluetooth RFCOMM accept failed due to permissions", error)
|
|
2758
|
+
break
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
private fun closeClassicSocket(deviceId: String, emitEvent: Boolean) {
|
|
2764
|
+
classicReadJobs.remove(deviceId)?.cancel()
|
|
2765
|
+
try {
|
|
2766
|
+
classicSockets.remove(deviceId)?.close()
|
|
2767
|
+
} catch (error: IOException) {
|
|
2768
|
+
Log.w(TAG, "Unable to close Classic Bluetooth socket for $deviceId", error)
|
|
2769
|
+
}
|
|
2770
|
+
if (emitEvent) {
|
|
2771
|
+
eventEmitter.emit("classicDisconnected", mapOf("deviceId" to deviceId))
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
private fun closeClassicServerInternal(key: String, emitEvent: Boolean) {
|
|
2776
|
+
classicServerJobs.remove(key)?.cancel()
|
|
2777
|
+
try {
|
|
2778
|
+
classicServerSockets.remove(key)?.close()
|
|
2779
|
+
} catch (error: IOException) {
|
|
2780
|
+
Log.w(TAG, "Unable to close Classic Bluetooth server socket for $key", error)
|
|
2781
|
+
}
|
|
2782
|
+
if (emitEvent) {
|
|
2783
|
+
eventEmitter.emit("classicServerStopped", mapOf("serviceUUID" to key))
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
private fun resolveClassicDevice(deviceId: String): BluetoothDevice? {
|
|
2788
|
+
ensureBluetoothManager()
|
|
2789
|
+
classicDevices[deviceId]?.let { return it }
|
|
2790
|
+
discoveredDevices[deviceId]?.let { return it }
|
|
2791
|
+
|
|
2792
|
+
val adapter = bluetoothAdapter ?: return null
|
|
2793
|
+
try {
|
|
2794
|
+
adapter.bondedDevices?.firstOrNull { it.address == deviceId }?.let { return it }
|
|
2795
|
+
} catch (error: SecurityException) {
|
|
2796
|
+
Log.w(TAG, "Unable to inspect bonded Classic Bluetooth devices", error)
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
return try {
|
|
2800
|
+
adapter.getRemoteDevice(deviceId)
|
|
2801
|
+
} catch (_: IllegalArgumentException) {
|
|
2802
|
+
null
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
private fun getBluetoothDeviceExtra(intent: Intent): BluetoothDevice? {
|
|
2807
|
+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
2808
|
+
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
|
|
2809
|
+
} else {
|
|
2810
|
+
@Suppress("DEPRECATION")
|
|
2811
|
+
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
private fun <T> unsupportedPromise(message: String): Promise<T> {
|
|
2816
|
+
return Promise.rejected(UnsupportedOperationException(message))
|
|
2817
|
+
}
|
|
2818
|
+
|
|
1057
2819
|
private fun addServiceUUIDs(uuids: Array<String>?, dataBuilder: AdvertiseData.Builder) {
|
|
1058
2820
|
uuids?.forEach { uuid ->
|
|
1059
2821
|
dataBuilder.addServiceUuid(ParcelUuid.fromString(uuid))
|
|
@@ -1071,6 +2833,29 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
1071
2833
|
}
|
|
1072
2834
|
}
|
|
1073
2835
|
|
|
2836
|
+
private fun scanFailureMessage(errorCode: Int): String {
|
|
2837
|
+
return when (errorCode) {
|
|
2838
|
+
ScanCallback.SCAN_FAILED_ALREADY_STARTED -> "Scan already started"
|
|
2839
|
+
ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED -> "Application registration failed"
|
|
2840
|
+
ScanCallback.SCAN_FAILED_INTERNAL_ERROR -> "Internal scan error"
|
|
2841
|
+
ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED -> "BLE scan feature unsupported"
|
|
2842
|
+
ScanCallback.SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES -> "Out of hardware scan resources"
|
|
2843
|
+
ScanCallback.SCAN_FAILED_SCANNING_TOO_FREQUENTLY -> "Scanning too frequently"
|
|
2844
|
+
else -> "Scan failed"
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
private fun advertiseFailureMessage(errorCode: Int): String {
|
|
2849
|
+
return when (errorCode) {
|
|
2850
|
+
AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE -> "Advertising data too large"
|
|
2851
|
+
AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS -> "Too many advertisers"
|
|
2852
|
+
AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED -> "Advertising already started"
|
|
2853
|
+
AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR -> "Internal advertising error"
|
|
2854
|
+
AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED -> "BLE advertising feature unsupported"
|
|
2855
|
+
else -> "Advertising failed"
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
|
|
1074
2859
|
private fun hexStringToByteArray(hexString: String?): ByteArray? {
|
|
1075
2860
|
if (hexString == null) return null
|
|
1076
2861
|
|
|
@@ -1129,6 +2914,16 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
1129
2914
|
companion object {
|
|
1130
2915
|
private const val TAG = "HybridMunimBluetooth"
|
|
1131
2916
|
private const val BLUETOOTH_PERMISSION_REQUEST_CODE = 9137
|
|
2917
|
+
private const val CONNECTION_TIMEOUT_MS = 15_000L
|
|
2918
|
+
private const val CONNECTION_RETRY_DELAY_MS = 350L
|
|
2919
|
+
private const val MAX_CONNECTION_RETRIES = 2
|
|
2920
|
+
private const val OPERATION_TIMEOUT_MS = 15_000L
|
|
2921
|
+
private const val DEFAULT_STREAM_BUFFER_SIZE = 4096
|
|
2922
|
+
private const val DEFAULT_CLASSIC_SERVICE_NAME = "MunimBluetooth"
|
|
2923
|
+
private const val MULTIPEER_UNSUPPORTED_MESSAGE =
|
|
2924
|
+
"Apple Multipeer Connectivity is only available on Apple platforms"
|
|
2925
|
+
private val SERIAL_PORT_PROFILE_UUID =
|
|
2926
|
+
UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
|
|
1132
2927
|
private val CLIENT_CHARACTERISTIC_CONFIG_UUID =
|
|
1133
2928
|
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
|
|
1134
2929
|
}
|
|
@@ -1141,17 +2936,19 @@ private class NitroEventEmitter(private val tag: String) {
|
|
|
1141
2936
|
Log.w(tag, "Unable to emit $eventName: React context unavailable")
|
|
1142
2937
|
return
|
|
1143
2938
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
2939
|
+
|
|
2940
|
+
UiThreadUtil.runOnUiThread {
|
|
2941
|
+
val writable = Arguments.createMap()
|
|
2942
|
+
payload.forEach { (key, value) ->
|
|
2943
|
+
writeValue(writable, key, value)
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
context
|
|
2947
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
2948
|
+
.emit(eventName, writable)
|
|
1148
2949
|
}
|
|
1149
|
-
|
|
1150
|
-
context
|
|
1151
|
-
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
1152
|
-
.emit(eventName, writable)
|
|
1153
2950
|
}
|
|
1154
|
-
|
|
2951
|
+
|
|
1155
2952
|
private fun writeValue(map: WritableMap, key: String, value: Any?) {
|
|
1156
2953
|
when (value) {
|
|
1157
2954
|
null -> map.putNull(key)
|
|
@@ -1166,7 +2963,7 @@ private class NitroEventEmitter(private val tag: String) {
|
|
|
1166
2963
|
else -> map.putString(key, value.toString())
|
|
1167
2964
|
}
|
|
1168
2965
|
}
|
|
1169
|
-
|
|
2966
|
+
|
|
1170
2967
|
private fun convertMap(map: Map<*, *>): WritableMap {
|
|
1171
2968
|
val writable = Arguments.createMap()
|
|
1172
2969
|
map.forEach { (key, value) ->
|
|
@@ -1176,7 +2973,7 @@ private class NitroEventEmitter(private val tag: String) {
|
|
|
1176
2973
|
}
|
|
1177
2974
|
return writable
|
|
1178
2975
|
}
|
|
1179
|
-
|
|
2976
|
+
|
|
1180
2977
|
private fun convertArray(list: List<*>): WritableArray {
|
|
1181
2978
|
val writable = Arguments.createArray()
|
|
1182
2979
|
list.forEach { value ->
|