munim-bluetooth 0.3.26 → 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/BluetoothPermissionUtils.kt +40 -0
- package/android/src/main/java/com/munimbluetooth/HybridMunimBluetooth.kt +2116 -217
- package/android/src/main/java/com/munimbluetooth/MunimBluetoothBackgroundService.kt +591 -56
- 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,24 +27,41 @@ 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
|
|
34
|
+
import android.content.pm.PackageManager
|
|
26
35
|
import android.os.Build
|
|
27
36
|
import android.os.ParcelUuid
|
|
28
37
|
import android.util.Log
|
|
29
38
|
import com.facebook.react.bridge.Arguments
|
|
39
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
30
40
|
import com.facebook.react.bridge.WritableArray
|
|
31
41
|
import com.facebook.react.bridge.WritableMap
|
|
32
42
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
43
|
+
import com.facebook.react.modules.core.PermissionAwareActivity
|
|
44
|
+
import com.facebook.react.modules.core.PermissionListener
|
|
33
45
|
import com.margelo.nitro.NitroModules
|
|
34
46
|
import com.margelo.nitro.core.Promise
|
|
35
47
|
import com.margelo.nitro.munimbluetooth.AdvertisingDataTypes
|
|
36
48
|
import com.margelo.nitro.munimbluetooth.AdvertisingOptions
|
|
37
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
|
|
38
54
|
import com.margelo.nitro.munimbluetooth.CharacteristicValue
|
|
55
|
+
import com.margelo.nitro.munimbluetooth.DescriptorValue
|
|
56
|
+
import com.margelo.nitro.munimbluetooth.ExtendedAdvertisingOptions
|
|
39
57
|
import com.margelo.nitro.munimbluetooth.GATTCharacteristic
|
|
58
|
+
import com.margelo.nitro.munimbluetooth.GATTDescriptor
|
|
40
59
|
import com.margelo.nitro.munimbluetooth.GATTService
|
|
41
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
|
|
42
65
|
import com.margelo.nitro.munimbluetooth.ScanMode
|
|
43
66
|
import com.margelo.nitro.munimbluetooth.ScanOptions
|
|
44
67
|
import com.margelo.nitro.munimbluetooth.ServiceDataEntry
|
|
@@ -49,6 +72,9 @@ import kotlinx.coroutines.Job
|
|
|
49
72
|
import kotlinx.coroutines.SupervisorJob
|
|
50
73
|
import kotlinx.coroutines.delay
|
|
51
74
|
import kotlinx.coroutines.launch
|
|
75
|
+
import org.json.JSONArray
|
|
76
|
+
import org.json.JSONObject
|
|
77
|
+
import java.io.IOException
|
|
52
78
|
import java.util.UUID
|
|
53
79
|
|
|
54
80
|
class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
@@ -56,6 +82,8 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
56
82
|
|
|
57
83
|
private var advertiser: BluetoothLeAdvertiser? = null
|
|
58
84
|
private var advertiseCallback: AdvertiseCallback? = null
|
|
85
|
+
private val extendedAdvertisingSets = mutableMapOf<String, AdvertisingSet>()
|
|
86
|
+
private val extendedAdvertisingCallbacks = mutableMapOf<String, AdvertisingSetCallback>()
|
|
59
87
|
private var gattServer: BluetoothGattServer? = null
|
|
60
88
|
private var gattServerReady = false
|
|
61
89
|
private var advertiseJob: Job? = null
|
|
@@ -64,6 +92,7 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
64
92
|
private var currentLocalName: String? = null
|
|
65
93
|
private var currentManufacturerData: String? = null
|
|
66
94
|
private var previousAdapterName: String? = null
|
|
95
|
+
private var configuredServices: Array<GATTService> = emptyArray()
|
|
67
96
|
private var bluetoothManager: BluetoothManager? = null
|
|
68
97
|
private var bluetoothAdapter: BluetoothAdapter? = null
|
|
69
98
|
|
|
@@ -76,10 +105,30 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
76
105
|
private val pendingServiceDiscoveries = mutableMapOf<String, Promise<Array<GATTService>>>()
|
|
77
106
|
private val pendingReads = mutableMapOf<String, Promise<CharacteristicValue>>()
|
|
78
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>>()
|
|
79
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>()
|
|
80
117
|
private val lastCharacteristicValues = mutableMapOf<String, CharacteristicValue>()
|
|
81
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>()
|
|
82
130
|
private val eventEmitter = NitroEventEmitter(TAG)
|
|
131
|
+
private var nextPermissionRequestCode = BLUETOOTH_PERMISSION_REQUEST_CODE
|
|
83
132
|
|
|
84
133
|
private fun getBluetoothManager(): BluetoothManager? {
|
|
85
134
|
val context = NitroModules.applicationContext ?: return null
|
|
@@ -93,7 +142,35 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
93
142
|
}
|
|
94
143
|
}
|
|
95
144
|
|
|
145
|
+
private fun hasRequiredBluetoothPermissions(): Boolean {
|
|
146
|
+
val context = NitroModules.applicationContext ?: return false
|
|
147
|
+
return BluetoothPermissionUtils.hasRequiredPermissions(context)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private fun ensureBluetoothPermissions(operationName: String): Boolean {
|
|
151
|
+
val context = NitroModules.applicationContext
|
|
152
|
+
if (context == null) {
|
|
153
|
+
Log.w(TAG, "Unable to $operationName: React context unavailable")
|
|
154
|
+
return false
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
val missingPermissions = BluetoothPermissionUtils.missingPermissions(context)
|
|
158
|
+
if (missingPermissions.isNotEmpty()) {
|
|
159
|
+
Log.w(
|
|
160
|
+
TAG,
|
|
161
|
+
"Unable to $operationName: missing Bluetooth permissions (${missingPermissions.joinToString()})"
|
|
162
|
+
)
|
|
163
|
+
return false
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return true
|
|
167
|
+
}
|
|
168
|
+
|
|
96
169
|
override fun startAdvertising(options: AdvertisingOptions) {
|
|
170
|
+
if (!ensureBluetoothPermissions("start advertising")) {
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
97
174
|
ensureBluetoothManager()
|
|
98
175
|
val adapter = bluetoothAdapter
|
|
99
176
|
if (adapter == null || !adapter.isEnabled) {
|
|
@@ -115,7 +192,12 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
115
192
|
)
|
|
116
193
|
|
|
117
194
|
if (!currentLocalName.isNullOrBlank() && previousAdapterName == null) {
|
|
118
|
-
previousAdapterName =
|
|
195
|
+
previousAdapterName = try {
|
|
196
|
+
adapter.name
|
|
197
|
+
} catch (error: SecurityException) {
|
|
198
|
+
Log.w(TAG, "Unable to read Bluetooth adapter name", error)
|
|
199
|
+
null
|
|
200
|
+
}
|
|
119
201
|
}
|
|
120
202
|
if (!currentLocalName.isNullOrBlank()) {
|
|
121
203
|
try {
|
|
@@ -126,7 +208,11 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
126
208
|
}
|
|
127
209
|
|
|
128
210
|
if (!gattServerReady) {
|
|
129
|
-
|
|
211
|
+
if (configuredServices.isNotEmpty()) {
|
|
212
|
+
setServices(configuredServices)
|
|
213
|
+
} else {
|
|
214
|
+
setServicesFromOptions(options.serviceUUIDs)
|
|
215
|
+
}
|
|
130
216
|
}
|
|
131
217
|
restartAdvertising(delayMs = 300L)
|
|
132
218
|
}
|
|
@@ -151,8 +237,21 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
151
237
|
advertiseCallback?.let { callback ->
|
|
152
238
|
advertiser?.stopAdvertising(callback)
|
|
153
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()
|
|
154
248
|
advertiseCallback = null
|
|
155
249
|
advertiser = null
|
|
250
|
+
gattServer?.clearServices()
|
|
251
|
+
gattServer?.close()
|
|
252
|
+
gattServer = null
|
|
253
|
+
gattServerReady = false
|
|
254
|
+
subscribedDevices.clear()
|
|
156
255
|
currentAdvertisingData = null
|
|
157
256
|
currentServiceUUIDs = emptyArray()
|
|
158
257
|
currentLocalName = null
|
|
@@ -161,6 +260,11 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
161
260
|
}
|
|
162
261
|
|
|
163
262
|
override fun setServices(services: Array<GATTService>) {
|
|
263
|
+
if (!ensureBluetoothPermissions("set GATT services")) {
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
configuredServices = services
|
|
164
268
|
ensureBluetoothManager()
|
|
165
269
|
gattServerReady = false
|
|
166
270
|
|
|
@@ -171,6 +275,8 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
171
275
|
gattServer = manager.openGattServer(context, buildGattServerCallback())
|
|
172
276
|
gattServer?.clearServices()
|
|
173
277
|
|
|
278
|
+
val nativeServices = linkedMapOf<String, BluetoothGattService>()
|
|
279
|
+
|
|
174
280
|
for (serviceData in services) {
|
|
175
281
|
val service = BluetoothGattService(
|
|
176
282
|
UUID.fromString(serviceData.uuid),
|
|
@@ -185,27 +291,152 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
185
291
|
BluetoothGattCharacteristic.PERMISSION_WRITE
|
|
186
292
|
)
|
|
187
293
|
characteristicData.value?.let { value ->
|
|
188
|
-
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
|
+
)
|
|
189
318
|
}
|
|
190
319
|
service.addCharacteristic(characteristic)
|
|
191
320
|
}
|
|
192
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 ->
|
|
193
335
|
gattServer?.addService(service)
|
|
194
336
|
}
|
|
195
337
|
|
|
196
338
|
gattServerReady = true
|
|
197
339
|
}
|
|
198
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
|
+
|
|
199
359
|
override fun isBluetoothEnabled(): Promise<Boolean> {
|
|
360
|
+
if (!hasRequiredBluetoothPermissions()) {
|
|
361
|
+
return Promise.resolved(false)
|
|
362
|
+
}
|
|
363
|
+
|
|
200
364
|
ensureBluetoothManager()
|
|
201
365
|
return Promise.resolved(bluetoothAdapter?.isEnabled == true)
|
|
202
366
|
}
|
|
203
367
|
|
|
204
368
|
override fun requestBluetoothPermission(): Promise<Boolean> {
|
|
205
|
-
|
|
369
|
+
val context = NitroModules.applicationContext ?: run {
|
|
370
|
+
Log.w(TAG, "Unable to request Bluetooth permissions: React context unavailable")
|
|
371
|
+
return Promise.resolved(false)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
val missingPermissions = BluetoothPermissionUtils.missingPermissions(context)
|
|
375
|
+
if (missingPermissions.isEmpty()) {
|
|
376
|
+
return Promise.resolved(true)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
val activity = context.currentActivity as? PermissionAwareActivity
|
|
380
|
+
if (activity == null) {
|
|
381
|
+
Log.w(TAG, "Unable to request Bluetooth permissions: current activity unavailable")
|
|
382
|
+
return Promise.resolved(false)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
val requestCode = nextPermissionRequestCode++
|
|
386
|
+
val promise = Promise<Boolean>()
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
activity.requestPermissions(
|
|
390
|
+
missingPermissions,
|
|
391
|
+
requestCode,
|
|
392
|
+
PermissionListener { callbackRequestCode, _, grantResults ->
|
|
393
|
+
if (callbackRequestCode != requestCode) {
|
|
394
|
+
return@PermissionListener false
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
val isGranted =
|
|
398
|
+
grantResults.isNotEmpty() &&
|
|
399
|
+
grantResults.all { it == PackageManager.PERMISSION_GRANTED }
|
|
400
|
+
promise.resolve(isGranted)
|
|
401
|
+
true
|
|
402
|
+
}
|
|
403
|
+
)
|
|
404
|
+
} catch (error: IllegalStateException) {
|
|
405
|
+
Log.w(TAG, "Unable to request Bluetooth permissions", error)
|
|
406
|
+
promise.resolve(false)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return promise
|
|
410
|
+
}
|
|
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
|
+
)
|
|
206
433
|
}
|
|
207
434
|
|
|
208
435
|
override fun startScan(options: ScanOptions?) {
|
|
436
|
+
if (!ensureBluetoothPermissions("start scanning")) {
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
|
|
209
440
|
ensureBluetoothManager()
|
|
210
441
|
val adapter = bluetoothAdapter
|
|
211
442
|
if (adapter == null || !adapter.isEnabled) {
|
|
@@ -247,20 +478,27 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
247
478
|
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
|
248
479
|
val device = result.device
|
|
249
480
|
discoveredDevices[device.address] = device
|
|
250
|
-
|
|
481
|
+
emitDeviceFound(buildScanPayload(result))
|
|
251
482
|
}
|
|
252
483
|
|
|
253
484
|
override fun onBatchScanResults(results: MutableList<ScanResult>) {
|
|
254
485
|
results.forEach { result ->
|
|
255
486
|
val device = result.device
|
|
256
487
|
discoveredDevices[device.address] = device
|
|
257
|
-
|
|
488
|
+
emitDeviceFound(buildScanPayload(result))
|
|
258
489
|
}
|
|
259
490
|
}
|
|
260
491
|
|
|
261
492
|
override fun onScanFailed(errorCode: Int) {
|
|
262
493
|
Log.e(TAG, "Scan failed: $errorCode")
|
|
263
494
|
isScanning = false
|
|
495
|
+
eventEmitter.emit(
|
|
496
|
+
"scanFailed",
|
|
497
|
+
mapOf(
|
|
498
|
+
"errorCode" to errorCode,
|
|
499
|
+
"message" to scanFailureMessage(errorCode)
|
|
500
|
+
)
|
|
501
|
+
)
|
|
264
502
|
}
|
|
265
503
|
}
|
|
266
504
|
|
|
@@ -278,6 +516,10 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
278
516
|
}
|
|
279
517
|
|
|
280
518
|
override fun connect(deviceId: String): Promise<Unit> {
|
|
519
|
+
if (!ensureBluetoothPermissions("connect to BLE device")) {
|
|
520
|
+
return Promise.rejected(IllegalStateException("Bluetooth permissions not granted"))
|
|
521
|
+
}
|
|
522
|
+
|
|
281
523
|
ensureBluetoothManager()
|
|
282
524
|
connectedDevices[deviceId]?.let { existingGatt ->
|
|
283
525
|
if (existingGatt.services != null) {
|
|
@@ -285,10 +527,10 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
285
527
|
}
|
|
286
528
|
}
|
|
287
529
|
|
|
288
|
-
val context = NitroModules.applicationContext
|
|
289
|
-
?: return Promise.rejected(IllegalStateException("React context unavailable"))
|
|
290
530
|
val adapter = bluetoothAdapter
|
|
291
531
|
?: return Promise.rejected(IllegalStateException("Bluetooth adapter unavailable"))
|
|
532
|
+
NitroModules.applicationContext
|
|
533
|
+
?: return Promise.rejected(IllegalStateException("React context unavailable"))
|
|
292
534
|
|
|
293
535
|
val device = discoveredDevices[deviceId] ?: run {
|
|
294
536
|
try {
|
|
@@ -300,20 +542,22 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
300
542
|
|
|
301
543
|
val promise = Promise<Unit>()
|
|
302
544
|
pendingConnections[deviceId] = promise
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
} else {
|
|
307
|
-
device.connectGatt(context, false, createGattCallback(deviceId))
|
|
308
|
-
}
|
|
309
|
-
connectedDevices[deviceId] = gatt
|
|
545
|
+
pendingConnectionAttempts[deviceId] = 0
|
|
546
|
+
scheduleConnectionTimeout(deviceId)
|
|
547
|
+
startGattConnection(deviceId, device)
|
|
310
548
|
return promise
|
|
311
549
|
}
|
|
312
550
|
|
|
313
551
|
override fun disconnect(deviceId: String) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
+
)
|
|
317
561
|
|
|
318
562
|
val gatt = connectedDevices.remove(deviceId)
|
|
319
563
|
gatt?.disconnect()
|
|
@@ -333,7 +577,11 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
333
577
|
|
|
334
578
|
val promise = Promise<Array<GATTService>>()
|
|
335
579
|
pendingServiceDiscoveries[deviceId] = promise
|
|
580
|
+
schedulePendingOperationTimeout("services|$deviceId", "Service discovery for $deviceId") {
|
|
581
|
+
pendingServiceDiscoveries.remove(deviceId)
|
|
582
|
+
}
|
|
336
583
|
if (!gatt.discoverServices()) {
|
|
584
|
+
cancelPendingOperationTimeout("services|$deviceId")
|
|
337
585
|
pendingServiceDiscoveries.remove(deviceId)
|
|
338
586
|
return Promise.rejected(IllegalStateException("Failed to start service discovery for $deviceId"))
|
|
339
587
|
}
|
|
@@ -355,14 +603,43 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
355
603
|
val promise = Promise<CharacteristicValue>()
|
|
356
604
|
val key = characteristicKey(deviceId, serviceUUID, characteristicUUID)
|
|
357
605
|
pendingReads[key] = promise
|
|
606
|
+
schedulePendingOperationTimeout("read|$key", "Characteristic read $key") {
|
|
607
|
+
pendingReads.remove(key)
|
|
608
|
+
}
|
|
358
609
|
|
|
359
610
|
if (!gatt.readCharacteristic(characteristic)) {
|
|
611
|
+
cancelPendingOperationTimeout("read|$key")
|
|
360
612
|
pendingReads.remove(key)
|
|
361
613
|
return Promise.rejected(IllegalStateException("Failed to start characteristic read"))
|
|
362
614
|
}
|
|
363
615
|
return promise
|
|
364
616
|
}
|
|
365
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
|
+
|
|
366
643
|
override fun writeCharacteristic(
|
|
367
644
|
deviceId: String,
|
|
368
645
|
serviceUUID: String,
|
|
@@ -379,20 +656,56 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
379
656
|
val data = hexStringToByteArray(value)
|
|
380
657
|
?: return Promise.rejected(IllegalArgumentException("Invalid hex string for characteristic write"))
|
|
381
658
|
|
|
382
|
-
|
|
383
|
-
characteristic.writeType = when (writeType) {
|
|
659
|
+
val resolvedWriteType = when (writeType) {
|
|
384
660
|
WriteType.WRITEWITHOUTRESPONSE -> BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
|
385
661
|
else -> BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
|
386
662
|
}
|
|
387
663
|
|
|
388
664
|
val promise = Promise<Unit>()
|
|
389
665
|
val key = characteristicKey(deviceId, serviceUUID, characteristicUUID)
|
|
390
|
-
|
|
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
|
+
}
|
|
391
672
|
|
|
392
|
-
if (!gatt
|
|
673
|
+
if (!writeGattCharacteristic(gatt, characteristic, data, resolvedWriteType)) {
|
|
674
|
+
cancelPendingOperationTimeout("write|$key")
|
|
393
675
|
pendingWrites.remove(key)
|
|
394
676
|
return Promise.rejected(IllegalStateException("Failed to start characteristic write"))
|
|
395
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
|
+
}
|
|
396
709
|
return promise
|
|
397
710
|
}
|
|
398
711
|
|
|
@@ -406,8 +719,14 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
406
719
|
gatt.setCharacteristicNotification(characteristic, true)
|
|
407
720
|
|
|
408
721
|
characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID)?.let { descriptor ->
|
|
409
|
-
|
|
410
|
-
|
|
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)
|
|
411
730
|
}
|
|
412
731
|
}
|
|
413
732
|
|
|
@@ -421,8 +740,7 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
421
740
|
gatt.setCharacteristicNotification(characteristic, false)
|
|
422
741
|
|
|
423
742
|
characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID)?.let { descriptor ->
|
|
424
|
-
descriptor
|
|
425
|
-
gatt.writeDescriptor(descriptor)
|
|
743
|
+
writeGattDescriptor(gatt, descriptor, BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)
|
|
426
744
|
}
|
|
427
745
|
}
|
|
428
746
|
|
|
@@ -440,148 +758,809 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
440
758
|
|
|
441
759
|
val promise = Promise<Double>()
|
|
442
760
|
pendingRssiReads[deviceId] = promise
|
|
761
|
+
schedulePendingOperationTimeout("rssi|$deviceId", "RSSI read for $deviceId") {
|
|
762
|
+
pendingRssiReads.remove(deviceId)
|
|
763
|
+
}
|
|
443
764
|
if (!gatt.readRemoteRssi()) {
|
|
765
|
+
cancelPendingOperationTimeout("rssi|$deviceId")
|
|
444
766
|
pendingRssiReads.remove(deviceId)
|
|
445
767
|
return Promise.rejected(IllegalStateException("Failed to start RSSI read"))
|
|
446
768
|
}
|
|
447
769
|
return promise
|
|
448
770
|
}
|
|
449
771
|
|
|
450
|
-
override fun
|
|
451
|
-
val
|
|
452
|
-
|
|
453
|
-
return
|
|
454
|
-
}
|
|
772
|
+
override fun requestMTU(deviceId: String, mtu: Double): Promise<Double> {
|
|
773
|
+
val gatt = connectedDevices[deviceId]
|
|
774
|
+
?: return Promise.rejected(IllegalStateException("Device not connected: $deviceId"))
|
|
455
775
|
|
|
456
|
-
val
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
)
|
|
462
|
-
putExtra(
|
|
463
|
-
MunimBluetoothBackgroundService.EXTRA_LOCAL_NAME,
|
|
464
|
-
options.localName
|
|
465
|
-
)
|
|
466
|
-
putExtra(
|
|
467
|
-
MunimBluetoothBackgroundService.EXTRA_ALLOW_DUPLICATES,
|
|
468
|
-
options.allowDuplicates ?: false
|
|
469
|
-
)
|
|
470
|
-
putExtra(
|
|
471
|
-
MunimBluetoothBackgroundService.EXTRA_SCAN_MODE,
|
|
472
|
-
options.scanMode?.name ?: ScanMode.LOWPOWER.name
|
|
473
|
-
)
|
|
474
|
-
putExtra(
|
|
475
|
-
MunimBluetoothBackgroundService.EXTRA_NOTIFICATION_CHANNEL_ID,
|
|
476
|
-
options.androidNotificationChannelId
|
|
477
|
-
?: MunimBluetoothBackgroundService.DEFAULT_CHANNEL_ID
|
|
478
|
-
)
|
|
479
|
-
putExtra(
|
|
480
|
-
MunimBluetoothBackgroundService.EXTRA_NOTIFICATION_CHANNEL_NAME,
|
|
481
|
-
options.androidNotificationChannelName
|
|
482
|
-
?: MunimBluetoothBackgroundService.DEFAULT_CHANNEL_NAME
|
|
483
|
-
)
|
|
484
|
-
putExtra(
|
|
485
|
-
MunimBluetoothBackgroundService.EXTRA_NOTIFICATION_TITLE,
|
|
486
|
-
options.androidNotificationTitle
|
|
487
|
-
?: MunimBluetoothBackgroundService.DEFAULT_NOTIFICATION_TITLE
|
|
488
|
-
)
|
|
489
|
-
putExtra(
|
|
490
|
-
MunimBluetoothBackgroundService.EXTRA_NOTIFICATION_TEXT,
|
|
491
|
-
options.androidNotificationText
|
|
492
|
-
?: MunimBluetoothBackgroundService.DEFAULT_NOTIFICATION_TEXT
|
|
493
|
-
)
|
|
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)
|
|
494
781
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
context.startService(intent)
|
|
782
|
+
if (!gatt.requestMtu(requestedMtu)) {
|
|
783
|
+
cancelPendingOperationTimeout("mtu|$deviceId")
|
|
784
|
+
pendingMtuRequests.remove(deviceId)
|
|
785
|
+
return Promise.rejected(IllegalStateException("Failed to start MTU request"))
|
|
500
786
|
}
|
|
787
|
+
return promise
|
|
501
788
|
}
|
|
502
789
|
|
|
503
|
-
override fun
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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")
|
|
507
798
|
}
|
|
508
|
-
context.startService(intent)
|
|
509
|
-
}
|
|
510
799
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
800
|
+
val gatt = connectedDevices[deviceId]
|
|
801
|
+
?: return Promise.rejected(IllegalStateException("Device not connected: $deviceId"))
|
|
514
802
|
|
|
515
|
-
|
|
516
|
-
|
|
803
|
+
gatt.setPreferredPhy(
|
|
804
|
+
phyToMask(txPhy),
|
|
805
|
+
phyToMask(rxPhy),
|
|
806
|
+
phyOptionToConstant(phyOption ?: BluetoothPhyOption.NONE)
|
|
807
|
+
)
|
|
808
|
+
return Promise.resolved(Unit)
|
|
517
809
|
}
|
|
518
810
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (adapter == null || !adapter.isEnabled) {
|
|
523
|
-
Log.e(TAG, "Bluetooth is not enabled or not available")
|
|
524
|
-
return
|
|
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")
|
|
525
814
|
}
|
|
526
815
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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)
|
|
530
823
|
}
|
|
824
|
+
gatt.readPhy()
|
|
825
|
+
return promise
|
|
826
|
+
}
|
|
531
827
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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))
|
|
832
|
+
}
|
|
536
833
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
Log.e(TAG, "Bluetooth LE advertiser is not available")
|
|
541
|
-
return@launch
|
|
542
|
-
}
|
|
834
|
+
override fun createBond(deviceId: String): Promise<BondState> {
|
|
835
|
+
val device = resolveBluetoothDevice(deviceId)
|
|
836
|
+
?: return Promise.rejected(IllegalArgumentException("Device not found: $deviceId"))
|
|
543
837
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
dataBuilder.addServiceUuid(ParcelUuid.fromString(uuid))
|
|
548
|
-
}
|
|
838
|
+
if (device.bondState == BluetoothDevice.BOND_BONDED) {
|
|
839
|
+
return Promise.resolved(BondState.BONDED)
|
|
840
|
+
}
|
|
549
841
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
842
|
+
return if (device.createBond()) {
|
|
843
|
+
Promise.resolved(bondStateFor(device))
|
|
844
|
+
} else {
|
|
845
|
+
Promise.rejected(IllegalStateException("Failed to start bond creation for $deviceId"))
|
|
846
|
+
}
|
|
847
|
+
}
|
|
556
848
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
849
|
+
override fun removeBond(deviceId: String): Promise<BondState> {
|
|
850
|
+
val device = resolveBluetoothDevice(deviceId)
|
|
851
|
+
?: return Promise.rejected(IllegalArgumentException("Device not found: $deviceId"))
|
|
561
852
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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"))
|
|
565
860
|
}
|
|
566
|
-
|
|
567
|
-
|
|
861
|
+
} catch (error: ReflectiveOperationException) {
|
|
862
|
+
Promise.rejected(UnsupportedOperationException("Removing bonds is unavailable on this Android build", error))
|
|
568
863
|
}
|
|
569
864
|
}
|
|
570
865
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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"))
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
ensureBluetoothManager()
|
|
875
|
+
val adapter = bluetoothAdapter
|
|
876
|
+
?: return Promise.rejected(IllegalStateException("Bluetooth adapter unavailable"))
|
|
877
|
+
if (!adapter.isLeExtendedAdvertisingSupported) {
|
|
878
|
+
return unsupportedPromise("BLE extended advertising is not supported by this device")
|
|
879
|
+
}
|
|
880
|
+
val advertiser = adapter.bluetoothLeAdvertiser
|
|
881
|
+
?: return Promise.rejected(IllegalStateException("Bluetooth LE advertiser is unavailable"))
|
|
882
|
+
|
|
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))
|
|
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()
|
|
911
|
+
|
|
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
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
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
|
+
}
|
|
948
|
+
|
|
949
|
+
override fun stopExtendedAdvertising(advertisingId: String) {
|
|
950
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
|
951
|
+
|
|
952
|
+
ensureBluetoothManager()
|
|
953
|
+
extendedAdvertisingSets.remove(advertisingId)
|
|
954
|
+
val callback = extendedAdvertisingCallbacks.remove(advertisingId) ?: return
|
|
955
|
+
bluetoothAdapter?.bluetoothLeAdvertiser?.stopAdvertisingSet(callback)
|
|
956
|
+
}
|
|
957
|
+
|
|
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
|
+
}
|
|
965
|
+
|
|
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()
|
|
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)
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return promise
|
|
999
|
+
}
|
|
1000
|
+
|
|
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)
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
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,
|
|
1551
|
+
requestId,
|
|
1552
|
+
BluetoothGatt.GATT_SUCCESS,
|
|
583
1553
|
offset,
|
|
584
|
-
|
|
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
|
+
)
|
|
585
1564
|
)
|
|
586
1565
|
}
|
|
587
1566
|
|
|
@@ -594,7 +1573,173 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
594
1573
|
offset: Int,
|
|
595
1574
|
value: ByteArray?
|
|
596
1575
|
) {
|
|
597
|
-
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
|
+
|
|
598
1743
|
if (responseNeeded) {
|
|
599
1744
|
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null)
|
|
600
1745
|
}
|
|
@@ -606,25 +1751,35 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
606
1751
|
return object : BluetoothGattCallback() {
|
|
607
1752
|
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
|
608
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)
|
|
609
1759
|
pendingConnections.remove(deviceId)?.reject(
|
|
610
1760
|
IllegalStateException("Failed to connect to $deviceId (status=$status)")
|
|
611
1761
|
)
|
|
612
|
-
connectedDevices.remove(deviceId)?.close()
|
|
1762
|
+
(pendingConnectionGatts.remove(deviceId) ?: connectedDevices.remove(deviceId))?.close()
|
|
613
1763
|
return
|
|
614
1764
|
}
|
|
615
1765
|
|
|
616
1766
|
when (newState) {
|
|
617
1767
|
BluetoothProfile.STATE_CONNECTED -> {
|
|
1768
|
+
pendingConnectionTimeouts.remove(deviceId)?.cancel()
|
|
1769
|
+
pendingConnectionAttempts.remove(deviceId)
|
|
1770
|
+
pendingConnectionGatts.remove(deviceId)
|
|
618
1771
|
connectedDevices[deviceId] = gatt
|
|
619
1772
|
pendingConnections.remove(deviceId)?.resolve(Unit)
|
|
620
1773
|
eventEmitter.emit("deviceConnected", mapOf("deviceId" to deviceId))
|
|
621
1774
|
}
|
|
622
1775
|
|
|
623
1776
|
BluetoothProfile.STATE_DISCONNECTED -> {
|
|
1777
|
+
pendingConnectionTimeouts.remove(deviceId)?.cancel()
|
|
1778
|
+
pendingConnectionAttempts.remove(deviceId)
|
|
624
1779
|
pendingConnections.remove(deviceId)?.reject(
|
|
625
1780
|
IllegalStateException("Disconnected from $deviceId")
|
|
626
1781
|
)
|
|
627
|
-
connectedDevices.remove(deviceId)?.close()
|
|
1782
|
+
(pendingConnectionGatts.remove(deviceId) ?: connectedDevices.remove(deviceId))?.close()
|
|
628
1783
|
rejectPendingOperationsForDevice(
|
|
629
1784
|
deviceId,
|
|
630
1785
|
IllegalStateException("Disconnected from $deviceId")
|
|
@@ -635,23 +1790,13 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
635
1790
|
}
|
|
636
1791
|
|
|
637
1792
|
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
|
1793
|
+
cancelPendingOperationTimeout("services|$deviceId")
|
|
638
1794
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
639
1795
|
val services = buildGattServices(gatt)
|
|
640
1796
|
pendingServiceDiscoveries.remove(deviceId)?.resolve(services)
|
|
641
1797
|
eventEmitter.emit(
|
|
642
1798
|
"servicesDiscovered",
|
|
643
|
-
mapOf("deviceId" to deviceId, "services" to services.map {
|
|
644
|
-
mapOf(
|
|
645
|
-
"uuid" to service.uuid,
|
|
646
|
-
"characteristics" to service.characteristics.map { characteristic ->
|
|
647
|
-
mapOf(
|
|
648
|
-
"uuid" to characteristic.uuid,
|
|
649
|
-
"properties" to characteristic.properties.toList(),
|
|
650
|
-
"value" to characteristic.value
|
|
651
|
-
)
|
|
652
|
-
}
|
|
653
|
-
)
|
|
654
|
-
})
|
|
1799
|
+
mapOf("deviceId" to deviceId, "services" to services.map { servicePayload(it) })
|
|
655
1800
|
)
|
|
656
1801
|
} else {
|
|
657
1802
|
pendingServiceDiscoveries.remove(deviceId)?.reject(
|
|
@@ -665,29 +1810,16 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
665
1810
|
characteristic: BluetoothGattCharacteristic,
|
|
666
1811
|
status: Int
|
|
667
1812
|
) {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
"characteristicValueChanged",
|
|
679
|
-
mapOf(
|
|
680
|
-
"deviceId" to deviceId,
|
|
681
|
-
"serviceUUID" to value.serviceUUID,
|
|
682
|
-
"characteristicUUID" to value.characteristicUUID,
|
|
683
|
-
"value" to value.value
|
|
684
|
-
)
|
|
685
|
-
)
|
|
686
|
-
} else {
|
|
687
|
-
pendingReads.remove(key)?.reject(
|
|
688
|
-
IllegalStateException("Failed to read characteristic $key (status=$status)")
|
|
689
|
-
)
|
|
690
|
-
}
|
|
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)
|
|
691
1823
|
}
|
|
692
1824
|
|
|
693
1825
|
override fun onCharacteristicWrite(
|
|
@@ -700,6 +1832,7 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
700
1832
|
characteristic.service.uuid.toString(),
|
|
701
1833
|
characteristic.uuid.toString()
|
|
702
1834
|
)
|
|
1835
|
+
cancelPendingOperationTimeout("write|$key")
|
|
703
1836
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
704
1837
|
pendingWrites.remove(key)?.resolve(Unit)
|
|
705
1838
|
} else {
|
|
@@ -713,21 +1846,87 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
713
1846
|
gatt: BluetoothGatt,
|
|
714
1847
|
characteristic: BluetoothGattCharacteristic
|
|
715
1848
|
) {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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()
|
|
727
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
|
+
}
|
|
728
1926
|
}
|
|
729
1927
|
|
|
730
1928
|
override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) {
|
|
1929
|
+
cancelPendingOperationTimeout("rssi|$deviceId")
|
|
731
1930
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
732
1931
|
val rssiValue = rssi.toDouble()
|
|
733
1932
|
lastRssiValues[deviceId] = rssiValue
|
|
@@ -745,15 +1944,213 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
745
1944
|
}
|
|
746
1945
|
}
|
|
747
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
|
+
|
|
748
2017
|
private fun rejectPendingOperationsForDevice(deviceId: String, error: Throwable) {
|
|
2018
|
+
pendingConnectionTimeouts.remove(deviceId)?.cancel()
|
|
2019
|
+
pendingConnectionAttempts.remove(deviceId)
|
|
2020
|
+
cancelPendingOperationTimeoutsForDevice(deviceId)
|
|
749
2021
|
pendingReads.keys
|
|
750
2022
|
.filter { it.startsWith("$deviceId|") }
|
|
751
2023
|
.forEach { key -> pendingReads.remove(key)?.reject(error) }
|
|
752
2024
|
pendingWrites.keys
|
|
753
2025
|
.filter { it.startsWith("$deviceId|") }
|
|
754
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) }
|
|
755
2033
|
pendingServiceDiscoveries.remove(deviceId)?.reject(error)
|
|
756
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
|
|
757
2154
|
}
|
|
758
2155
|
|
|
759
2156
|
private fun buildGattServices(gatt: BluetoothGatt): Array<GATTService> {
|
|
@@ -764,21 +2161,66 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
764
2161
|
GATTCharacteristic(
|
|
765
2162
|
uuid = characteristic.uuid.toString(),
|
|
766
2163
|
properties = propertiesToArray(characteristic.properties),
|
|
767
|
-
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()
|
|
768
2172
|
)
|
|
769
|
-
}.toTypedArray()
|
|
2173
|
+
}.toTypedArray(),
|
|
2174
|
+
includedServices = service.includedServices.map { it.uuid.toString() }.toTypedArray()
|
|
770
2175
|
)
|
|
771
2176
|
}.toTypedArray()
|
|
772
2177
|
}
|
|
773
2178
|
|
|
774
|
-
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 {
|
|
775
2204
|
return CharacteristicValue(
|
|
776
|
-
value =
|
|
2205
|
+
value = valueBytes?.toHexString() ?: "",
|
|
777
2206
|
serviceUUID = characteristic.service.uuid.toString(),
|
|
778
2207
|
characteristicUUID = characteristic.uuid.toString()
|
|
779
2208
|
)
|
|
780
2209
|
}
|
|
781
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
|
+
|
|
782
2224
|
private fun findCharacteristic(
|
|
783
2225
|
gatt: BluetoothGatt,
|
|
784
2226
|
serviceUUID: String,
|
|
@@ -791,6 +2233,68 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
791
2233
|
}
|
|
792
2234
|
}
|
|
793
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
|
+
|
|
794
2298
|
private fun characteristicKey(
|
|
795
2299
|
deviceId: String,
|
|
796
2300
|
serviceUUID: String,
|
|
@@ -799,6 +2303,38 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
799
2303
|
return "$deviceId|${serviceUUID.lowercase()}|${characteristicUUID.lowercase()}"
|
|
800
2304
|
}
|
|
801
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
|
+
|
|
802
2338
|
private fun buildScanPayload(result: ScanResult): Map<String, Any?> {
|
|
803
2339
|
val record = result.scanRecord
|
|
804
2340
|
val manufacturerData = extractManufacturerData(record)
|
|
@@ -810,14 +2346,10 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
810
2346
|
record?.deviceName?.let { advertisingData["completeLocalName"] = it }
|
|
811
2347
|
txPower?.let { advertisingData["txPowerLevel"] = it }
|
|
812
2348
|
manufacturerData?.let { advertisingData["manufacturerData"] = it }
|
|
813
|
-
serviceUUIDs?.let { advertisingData
|
|
2349
|
+
serviceUUIDs?.let { addServiceUuidBuckets(it, advertisingData) }
|
|
814
2350
|
serviceData?.takeIf { it.isNotEmpty() }?.let { entries ->
|
|
815
|
-
|
|
816
|
-
}
|
|
817
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
818
|
-
advertisingData["isConnectable"] = result.isConnectable
|
|
2351
|
+
addServiceDataBuckets(entries, advertisingData)
|
|
819
2352
|
}
|
|
820
|
-
advertisingData["rssi"] = result.rssi
|
|
821
2353
|
|
|
822
2354
|
return mapOf(
|
|
823
2355
|
"id" to result.device.address,
|
|
@@ -833,6 +2365,65 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
833
2365
|
)
|
|
834
2366
|
}
|
|
835
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
|
+
|
|
836
2427
|
private fun extractManufacturerData(record: ScanRecord?): String? {
|
|
837
2428
|
val data = record?.manufacturerSpecificData ?: return null
|
|
838
2429
|
if (data.size() == 0) return null
|
|
@@ -849,14 +2440,17 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
849
2440
|
|
|
850
2441
|
private fun processAdvertisingData(
|
|
851
2442
|
data: AdvertisingDataTypes,
|
|
852
|
-
dataBuilder: AdvertiseData.Builder
|
|
2443
|
+
dataBuilder: AdvertiseData.Builder,
|
|
2444
|
+
includeServiceUuids: Boolean = true
|
|
853
2445
|
) {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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
|
+
}
|
|
860
2454
|
|
|
861
2455
|
if (data.shortenedLocalName != null || data.completeLocalName != null) {
|
|
862
2456
|
dataBuilder.setIncludeDeviceName(true)
|
|
@@ -865,9 +2459,11 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
865
2459
|
dataBuilder.setIncludeTxPowerLevel(true)
|
|
866
2460
|
}
|
|
867
2461
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
2462
|
+
if (includeServiceUuids) {
|
|
2463
|
+
addServiceUUIDs(data.serviceSolicitationUUIDs16, dataBuilder)
|
|
2464
|
+
addServiceUUIDs(data.serviceSolicitationUUIDs32, dataBuilder)
|
|
2465
|
+
addServiceUUIDs(data.serviceSolicitationUUIDs128, dataBuilder)
|
|
2466
|
+
}
|
|
871
2467
|
addServiceData(data.serviceData16, dataBuilder)
|
|
872
2468
|
addServiceData(data.serviceData32, dataBuilder)
|
|
873
2469
|
addServiceData(data.serviceData128, dataBuilder)
|
|
@@ -953,6 +2549,273 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
953
2549
|
return result.toTypedArray()
|
|
954
2550
|
}
|
|
955
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
|
+
|
|
956
2819
|
private fun addServiceUUIDs(uuids: Array<String>?, dataBuilder: AdvertiseData.Builder) {
|
|
957
2820
|
uuids?.forEach { uuid ->
|
|
958
2821
|
dataBuilder.addServiceUuid(ParcelUuid.fromString(uuid))
|
|
@@ -970,6 +2833,29 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
970
2833
|
}
|
|
971
2834
|
}
|
|
972
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
|
+
|
|
973
2859
|
private fun hexStringToByteArray(hexString: String?): ByteArray? {
|
|
974
2860
|
if (hexString == null) return null
|
|
975
2861
|
|
|
@@ -1027,6 +2913,17 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
|
|
|
1027
2913
|
|
|
1028
2914
|
companion object {
|
|
1029
2915
|
private const val TAG = "HybridMunimBluetooth"
|
|
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")
|
|
1030
2927
|
private val CLIENT_CHARACTERISTIC_CONFIG_UUID =
|
|
1031
2928
|
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
|
|
1032
2929
|
}
|
|
@@ -1039,17 +2936,19 @@ private class NitroEventEmitter(private val tag: String) {
|
|
|
1039
2936
|
Log.w(tag, "Unable to emit $eventName: React context unavailable")
|
|
1040
2937
|
return
|
|
1041
2938
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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)
|
|
1046
2949
|
}
|
|
1047
|
-
|
|
1048
|
-
context
|
|
1049
|
-
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
1050
|
-
.emit(eventName, writable)
|
|
1051
2950
|
}
|
|
1052
|
-
|
|
2951
|
+
|
|
1053
2952
|
private fun writeValue(map: WritableMap, key: String, value: Any?) {
|
|
1054
2953
|
when (value) {
|
|
1055
2954
|
null -> map.putNull(key)
|
|
@@ -1064,7 +2963,7 @@ private class NitroEventEmitter(private val tag: String) {
|
|
|
1064
2963
|
else -> map.putString(key, value.toString())
|
|
1065
2964
|
}
|
|
1066
2965
|
}
|
|
1067
|
-
|
|
2966
|
+
|
|
1068
2967
|
private fun convertMap(map: Map<*, *>): WritableMap {
|
|
1069
2968
|
val writable = Arguments.createMap()
|
|
1070
2969
|
map.forEach { (key, value) ->
|
|
@@ -1074,7 +2973,7 @@ private class NitroEventEmitter(private val tag: String) {
|
|
|
1074
2973
|
}
|
|
1075
2974
|
return writable
|
|
1076
2975
|
}
|
|
1077
|
-
|
|
2976
|
+
|
|
1078
2977
|
private fun convertArray(list: List<*>): WritableArray {
|
|
1079
2978
|
val writable = Arguments.createArray()
|
|
1080
2979
|
list.forEach { value ->
|