munim-bluetooth 0.3.27 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +476 -74
- package/android/gradle.properties +2 -2
- package/android/src/main/AndroidManifest.xml +3 -1
- package/android/src/main/cpp/cpp-adapter.cpp +4 -1
- package/android/src/main/java/com/munimbluetooth/HybridMunimBluetooth.kt +2006 -209
- package/android/src/main/java/com/munimbluetooth/MunimBluetoothBackgroundService.kt +561 -53
- package/app.plugin.js +155 -0
- package/ios/HybridMunimBluetooth.swift +2123 -298
- package/ios/MunimBluetoothEventEmitter.swift +68 -8
- package/lib/commonjs/index.js +272 -11
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +243 -11
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/index.d.ts +310 -7
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/munim-bluetooth.nitro.d.ts +219 -5
- package/lib/typescript/src/specs/munim-bluetooth.nitro.d.ts.map +1 -1
- package/nitro.json +9 -3
- package/nitrogen/generated/android/c++/JAdvertisingDataTypes.hpp +96 -96
- package/nitrogen/generated/android/c++/JAdvertisingOptions.hpp +8 -8
- package/nitrogen/generated/android/c++/JBackgroundSessionOptions.hpp +8 -8
- package/nitrogen/generated/android/c++/JBluetoothCapabilities.hpp +105 -0
- package/nitrogen/generated/android/c++/JBluetoothPhy.hpp +61 -0
- package/nitrogen/generated/android/c++/JBluetoothPhyOption.hpp +61 -0
- package/nitrogen/generated/android/c++/JBondState.hpp +64 -0
- package/nitrogen/generated/android/c++/JDescriptorValue.hpp +69 -0
- package/nitrogen/generated/android/c++/JExtendedAdvertisingOptions.hpp +131 -0
- package/nitrogen/generated/android/c++/JGATTCharacteristic.hpp +35 -11
- package/nitrogen/generated/android/c++/JGATTDescriptor.hpp +85 -0
- package/nitrogen/generated/android/c++/JGATTService.hpp +33 -9
- package/nitrogen/generated/android/c++/JHybridMunimBluetoothSpec.cpp +422 -12
- package/nitrogen/generated/android/c++/JHybridMunimBluetoothSpec.hpp +29 -0
- package/nitrogen/generated/android/c++/JL2CAPChannel.hpp +66 -0
- package/nitrogen/generated/android/c++/JMultipeerDiscoveryInfoEntry.hpp +61 -0
- package/nitrogen/generated/android/c++/JMultipeerEncryptionPreference.hpp +61 -0
- package/nitrogen/generated/android/c++/JMultipeerPeer.hpp +93 -0
- package/nitrogen/generated/android/c++/JMultipeerPeerState.hpp +61 -0
- package/nitrogen/generated/android/c++/JMultipeerSessionOptions.hpp +105 -0
- package/nitrogen/generated/android/c++/JPhyStatus.hpp +62 -0
- package/nitrogen/generated/android/c++/JScanOptions.hpp +8 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/AdvertisingDataTypes.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/AdvertisingOptions.kt +19 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BackgroundSessionOptions.kt +27 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BluetoothCapabilities.kt +111 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BluetoothPhy.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BluetoothPhyOption.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/BondState.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/CharacteristicValue.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/DescriptorValue.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/ExtendedAdvertisingOptions.kt +111 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/GATTCharacteristic.kt +25 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/GATTDescriptor.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/GATTService.kt +23 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/HybridMunimBluetoothSpec.kt +138 -22
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/L2CAPChannel.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/MultipeerDiscoveryInfoEntry.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/MultipeerEncryptionPreference.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/MultipeerPeer.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/MultipeerPeerState.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/MultipeerSessionOptions.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/PhyStatus.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/ScanOptions.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/munimbluetooth/ServiceDataEntry.kt +15 -0
- package/nitrogen/generated/ios/MunimBluetooth+autolinking.rb +2 -0
- package/nitrogen/generated/ios/MunimBluetooth-Swift-Cxx-Bridge.cpp +61 -5
- package/nitrogen/generated/ios/MunimBluetooth-Swift-Cxx-Bridge.hpp +494 -49
- package/nitrogen/generated/ios/MunimBluetooth-Swift-Cxx-Umbrella.hpp +42 -0
- package/nitrogen/generated/ios/c++/HybridMunimBluetoothSpecSwift.hpp +254 -0
- package/nitrogen/generated/ios/swift/BluetoothCapabilities.swift +89 -0
- package/nitrogen/generated/ios/swift/BluetoothPhy.swift +44 -0
- package/nitrogen/generated/ios/swift/BluetoothPhyOption.swift +44 -0
- package/nitrogen/generated/ios/swift/BondState.swift +48 -0
- package/nitrogen/generated/ios/swift/DescriptorValue.swift +44 -0
- package/nitrogen/generated/ios/swift/ExtendedAdvertisingOptions.swift +243 -0
- package/nitrogen/generated/ios/swift/Func_void_BluetoothCapabilities.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_BondState.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_DescriptorValue.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_L2CAPChannel.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_PhyStatus.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_MultipeerPeer_.swift +46 -0
- package/nitrogen/generated/ios/swift/GATTCharacteristic.swift +25 -1
- package/nitrogen/generated/ios/swift/GATTDescriptor.swift +71 -0
- package/nitrogen/generated/ios/swift/GATTService.swift +25 -1
- package/nitrogen/generated/ios/swift/HybridMunimBluetoothSpec.swift +29 -0
- package/nitrogen/generated/ios/swift/HybridMunimBluetoothSpec_cxx.swift +556 -23
- package/nitrogen/generated/ios/swift/L2CAPChannel.swift +52 -0
- package/nitrogen/generated/ios/swift/MultipeerDiscoveryInfoEntry.swift +34 -0
- package/nitrogen/generated/ios/swift/MultipeerEncryptionPreference.swift +44 -0
- package/nitrogen/generated/ios/swift/MultipeerPeer.swift +63 -0
- package/nitrogen/generated/ios/swift/MultipeerPeerState.swift +44 -0
- package/nitrogen/generated/ios/swift/MultipeerSessionOptions.swift +136 -0
- package/nitrogen/generated/ios/swift/PhyStatus.swift +34 -0
- package/nitrogen/generated/shared/c++/BluetoothCapabilities.hpp +131 -0
- package/nitrogen/generated/shared/c++/BluetoothPhy.hpp +80 -0
- package/nitrogen/generated/shared/c++/BluetoothPhyOption.hpp +80 -0
- package/nitrogen/generated/shared/c++/BondState.hpp +84 -0
- package/nitrogen/generated/shared/c++/DescriptorValue.hpp +95 -0
- package/nitrogen/generated/shared/c++/ExtendedAdvertisingOptions.hpp +138 -0
- package/nitrogen/generated/shared/c++/GATTCharacteristic.hpp +9 -3
- package/nitrogen/generated/shared/c++/GATTDescriptor.hpp +93 -0
- package/nitrogen/generated/shared/c++/GATTService.hpp +7 -2
- package/nitrogen/generated/shared/c++/HybridMunimBluetoothSpec.cpp +29 -0
- package/nitrogen/generated/shared/c++/HybridMunimBluetoothSpec.hpp +61 -2
- package/nitrogen/generated/shared/c++/L2CAPChannel.hpp +92 -0
- package/nitrogen/generated/shared/c++/MultipeerDiscoveryInfoEntry.hpp +87 -0
- package/nitrogen/generated/shared/c++/MultipeerEncryptionPreference.hpp +80 -0
- package/nitrogen/generated/shared/c++/MultipeerPeer.hpp +102 -0
- package/nitrogen/generated/shared/c++/MultipeerPeerState.hpp +80 -0
- package/nitrogen/generated/shared/c++/MultipeerSessionOptions.hpp +114 -0
- package/nitrogen/generated/shared/c++/PhyStatus.hpp +88 -0
- package/package.json +22 -11
- package/src/index.ts +416 -31
- package/src/specs/munim-bluetooth.nitro.ts +298 -14
|
@@ -5,6 +5,13 @@ import android.app.NotificationChannel
|
|
|
5
5
|
import android.app.NotificationManager
|
|
6
6
|
import android.app.Service
|
|
7
7
|
import android.bluetooth.BluetoothAdapter
|
|
8
|
+
import android.bluetooth.BluetoothDevice
|
|
9
|
+
import android.bluetooth.BluetoothGatt
|
|
10
|
+
import android.bluetooth.BluetoothGattCharacteristic
|
|
11
|
+
import android.bluetooth.BluetoothGattDescriptor
|
|
12
|
+
import android.bluetooth.BluetoothGattServer
|
|
13
|
+
import android.bluetooth.BluetoothGattServerCallback
|
|
14
|
+
import android.bluetooth.BluetoothGattService
|
|
8
15
|
import android.bluetooth.BluetoothManager
|
|
9
16
|
import android.bluetooth.le.AdvertiseCallback
|
|
10
17
|
import android.bluetooth.le.AdvertiseData
|
|
@@ -22,18 +29,37 @@ import android.os.IBinder
|
|
|
22
29
|
import android.os.ParcelUuid
|
|
23
30
|
import android.util.Log
|
|
24
31
|
import com.margelo.nitro.munimbluetooth.ScanMode
|
|
32
|
+
import org.json.JSONArray
|
|
25
33
|
import java.util.Locale
|
|
34
|
+
import java.util.UUID
|
|
26
35
|
|
|
27
36
|
class MunimBluetoothBackgroundService : Service() {
|
|
37
|
+
private data class SessionConfig(
|
|
38
|
+
val serviceUUIDs: Array<String>,
|
|
39
|
+
val localName: String?,
|
|
40
|
+
val allowDuplicates: Boolean,
|
|
41
|
+
val scanMode: ScanMode,
|
|
42
|
+
val gattServicesJson: String?,
|
|
43
|
+
val restoreGattOnStart: Boolean,
|
|
44
|
+
val notificationChannelId: String,
|
|
45
|
+
val notificationChannelName: String,
|
|
46
|
+
val notificationTitle: String,
|
|
47
|
+
val notificationText: String
|
|
48
|
+
)
|
|
49
|
+
|
|
28
50
|
private var bluetoothManager: BluetoothManager? = null
|
|
29
51
|
private var bluetoothAdapter: BluetoothAdapter? = null
|
|
30
52
|
private var advertiser: BluetoothLeAdvertiser? = null
|
|
31
53
|
private var advertiseCallback: AdvertiseCallback? = null
|
|
32
54
|
private var scanner: BluetoothLeScanner? = null
|
|
33
55
|
private var scanCallback: ScanCallback? = null
|
|
56
|
+
private var gattServer: BluetoothGattServer? = null
|
|
34
57
|
private var previousAdapterName: String? = null
|
|
35
58
|
|
|
36
59
|
private val discoveredDeviceIds = linkedSetOf<String>()
|
|
60
|
+
private val characteristicValues = mutableMapOf<String, ByteArray>()
|
|
61
|
+
private val descriptorValues = mutableMapOf<String, ByteArray>()
|
|
62
|
+
private val subscribedDevices = mutableMapOf<UUID, MutableSet<BluetoothDevice>>()
|
|
37
63
|
private var notificationChannelId = DEFAULT_CHANNEL_ID
|
|
38
64
|
private var notificationChannelName = DEFAULT_CHANNEL_NAME
|
|
39
65
|
private var notificationTitle = DEFAULT_NOTIFICATION_TITLE
|
|
@@ -42,51 +68,34 @@ class MunimBluetoothBackgroundService : Service() {
|
|
|
42
68
|
override fun onBind(intent: Intent?): IBinder? = null
|
|
43
69
|
|
|
44
70
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
ACTION_START -> {
|
|
52
|
-
notificationChannelId =
|
|
53
|
-
intent.getStringExtra(EXTRA_NOTIFICATION_CHANNEL_ID)
|
|
54
|
-
?: DEFAULT_CHANNEL_ID
|
|
55
|
-
notificationChannelName =
|
|
56
|
-
intent.getStringExtra(EXTRA_NOTIFICATION_CHANNEL_NAME)
|
|
57
|
-
?: DEFAULT_CHANNEL_NAME
|
|
58
|
-
notificationTitle =
|
|
59
|
-
intent.getStringExtra(EXTRA_NOTIFICATION_TITLE)
|
|
60
|
-
?: DEFAULT_NOTIFICATION_TITLE
|
|
61
|
-
notificationText =
|
|
62
|
-
intent.getStringExtra(EXTRA_NOTIFICATION_TEXT)
|
|
63
|
-
?: DEFAULT_NOTIFICATION_TEXT
|
|
64
|
-
|
|
65
|
-
startForeground(
|
|
66
|
-
NOTIFICATION_ID,
|
|
67
|
-
buildNotification(neighborCount = discoveredDeviceIds.size)
|
|
68
|
-
)
|
|
71
|
+
if (intent?.action == ACTION_STOP) {
|
|
72
|
+
clearPersistedSession()
|
|
73
|
+
stopSelf()
|
|
74
|
+
return START_NOT_STICKY
|
|
75
|
+
}
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
intent.getBooleanExtra(EXTRA_ALLOW_DUPLICATES, false)
|
|
75
|
-
val scanMode = parseScanMode(
|
|
76
|
-
intent.getStringExtra(EXTRA_SCAN_MODE) ?: ScanMode.LOWPOWER.name
|
|
77
|
-
)
|
|
77
|
+
val config = when (intent?.action) {
|
|
78
|
+
ACTION_START -> sessionConfigFromIntent(intent).also(::persistSession)
|
|
79
|
+
else -> readPersistedSession()
|
|
80
|
+
}
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
allowDuplicates = allowDuplicates,
|
|
83
|
-
scanMode = scanMode
|
|
84
|
-
)
|
|
85
|
-
return START_STICKY
|
|
86
|
-
}
|
|
82
|
+
if (config == null) {
|
|
83
|
+
Log.w(TAG, "No persisted background BLE session to restart")
|
|
84
|
+
return START_NOT_STICKY
|
|
87
85
|
}
|
|
88
86
|
|
|
89
|
-
|
|
87
|
+
notificationChannelId = config.notificationChannelId
|
|
88
|
+
notificationChannelName = config.notificationChannelName
|
|
89
|
+
notificationTitle = config.notificationTitle
|
|
90
|
+
notificationText = config.notificationText
|
|
91
|
+
|
|
92
|
+
startForeground(
|
|
93
|
+
NOTIFICATION_ID,
|
|
94
|
+
buildNotification(neighborCount = discoveredDeviceIds.size)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
startBleSession(config)
|
|
98
|
+
return START_STICKY
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
override fun onDestroy() {
|
|
@@ -94,12 +103,9 @@ class MunimBluetoothBackgroundService : Service() {
|
|
|
94
103
|
super.onDestroy()
|
|
95
104
|
}
|
|
96
105
|
|
|
97
|
-
private fun startBleSession(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
allowDuplicates: Boolean,
|
|
101
|
-
scanMode: ScanMode
|
|
102
|
-
) {
|
|
106
|
+
private fun startBleSession(config: SessionConfig) {
|
|
107
|
+
stopBleSession()
|
|
108
|
+
|
|
103
109
|
if (!BluetoothPermissionUtils.hasRequiredPermissions(applicationContext)) {
|
|
104
110
|
Log.w(TAG, "Unable to start background BLE session: missing runtime permissions")
|
|
105
111
|
stopSelf()
|
|
@@ -124,7 +130,7 @@ class MunimBluetoothBackgroundService : Service() {
|
|
|
124
130
|
return
|
|
125
131
|
}
|
|
126
132
|
|
|
127
|
-
if (!localName.isNullOrBlank() && previousAdapterName == null) {
|
|
133
|
+
if (!config.localName.isNullOrBlank() && previousAdapterName == null) {
|
|
128
134
|
previousAdapterName = try {
|
|
129
135
|
adapter.name
|
|
130
136
|
} catch (error: SecurityException) {
|
|
@@ -133,14 +139,17 @@ class MunimBluetoothBackgroundService : Service() {
|
|
|
133
139
|
}
|
|
134
140
|
|
|
135
141
|
try {
|
|
136
|
-
adapter.name = localName
|
|
142
|
+
adapter.name = config.localName
|
|
137
143
|
} catch (error: SecurityException) {
|
|
138
144
|
Log.w(TAG, "Unable to set adapter name for background advertising", error)
|
|
139
145
|
}
|
|
140
146
|
}
|
|
141
147
|
|
|
142
|
-
|
|
143
|
-
|
|
148
|
+
if (config.restoreGattOnStart) {
|
|
149
|
+
startGattServer(config.gattServicesJson)
|
|
150
|
+
}
|
|
151
|
+
startAdvertising(adapter, config.serviceUUIDs, !config.localName.isNullOrBlank())
|
|
152
|
+
startScan(adapter, config.serviceUUIDs, config.allowDuplicates, config.scanMode)
|
|
144
153
|
}
|
|
145
154
|
|
|
146
155
|
private fun stopBleSession() {
|
|
@@ -154,6 +163,12 @@ class MunimBluetoothBackgroundService : Service() {
|
|
|
154
163
|
scanCallback = null
|
|
155
164
|
scanner = null
|
|
156
165
|
|
|
166
|
+
gattServer?.close()
|
|
167
|
+
gattServer = null
|
|
168
|
+
characteristicValues.clear()
|
|
169
|
+
descriptorValues.clear()
|
|
170
|
+
subscribedDevices.clear()
|
|
171
|
+
|
|
157
172
|
advertiseCallback?.let { callback ->
|
|
158
173
|
try {
|
|
159
174
|
advertiser?.stopAdvertising(callback)
|
|
@@ -189,12 +204,14 @@ class MunimBluetoothBackgroundService : Service() {
|
|
|
189
204
|
advertiser = activeAdvertiser
|
|
190
205
|
|
|
191
206
|
val data = AdvertiseData.Builder()
|
|
192
|
-
.setIncludeDeviceName(includeDeviceName)
|
|
193
207
|
|
|
194
208
|
serviceUUIDs.forEach { uuid ->
|
|
195
209
|
runCatching { ParcelUuid.fromString(uuid) }.getOrNull()?.let(data::addServiceUuid)
|
|
196
210
|
}
|
|
197
211
|
|
|
212
|
+
val scanResponse = AdvertiseData.Builder()
|
|
213
|
+
.setIncludeDeviceName(includeDeviceName)
|
|
214
|
+
|
|
198
215
|
val settings = AdvertiseSettings.Builder()
|
|
199
216
|
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_POWER)
|
|
200
217
|
.setConnectable(true)
|
|
@@ -213,12 +230,277 @@ class MunimBluetoothBackgroundService : Service() {
|
|
|
213
230
|
}
|
|
214
231
|
|
|
215
232
|
try {
|
|
216
|
-
activeAdvertiser.startAdvertising(
|
|
233
|
+
activeAdvertiser.startAdvertising(
|
|
234
|
+
settings,
|
|
235
|
+
data.build(),
|
|
236
|
+
scanResponse.build(),
|
|
237
|
+
advertiseCallback
|
|
238
|
+
)
|
|
217
239
|
} catch (error: SecurityException) {
|
|
218
240
|
Log.w(TAG, "Unable to start background advertising", error)
|
|
219
241
|
}
|
|
220
242
|
}
|
|
221
243
|
|
|
244
|
+
private fun startGattServer(gattServicesJson: String?) {
|
|
245
|
+
if (gattServicesJson.isNullOrBlank()) {
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
val manager = bluetoothManager ?: return
|
|
250
|
+
gattServer = try {
|
|
251
|
+
manager.openGattServer(applicationContext, buildGattServerCallback())
|
|
252
|
+
} catch (error: SecurityException) {
|
|
253
|
+
Log.w(TAG, "Unable to open background GATT server", error)
|
|
254
|
+
null
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
val server = gattServer ?: return
|
|
258
|
+
runCatching {
|
|
259
|
+
val services = JSONArray(gattServicesJson)
|
|
260
|
+
val nativeServices = linkedMapOf<String, BluetoothGattService>()
|
|
261
|
+
for (index in 0 until services.length()) {
|
|
262
|
+
val serviceJson = services.getJSONObject(index)
|
|
263
|
+
val service = BluetoothGattService(
|
|
264
|
+
UUID.fromString(serviceJson.getString("uuid")),
|
|
265
|
+
BluetoothGattService.SERVICE_TYPE_PRIMARY
|
|
266
|
+
)
|
|
267
|
+
val characteristics = serviceJson.optJSONArray("characteristics") ?: JSONArray()
|
|
268
|
+
for (characteristicIndex in 0 until characteristics.length()) {
|
|
269
|
+
val characteristicJson = characteristics.getJSONObject(characteristicIndex)
|
|
270
|
+
val characteristic = BluetoothGattCharacteristic(
|
|
271
|
+
UUID.fromString(characteristicJson.getString("uuid")),
|
|
272
|
+
propertiesFromJson(characteristicJson.optJSONArray("properties")),
|
|
273
|
+
BluetoothGattCharacteristic.PERMISSION_READ or
|
|
274
|
+
BluetoothGattCharacteristic.PERMISSION_WRITE
|
|
275
|
+
)
|
|
276
|
+
val characteristicInitialValue = optionalString(characteristicJson, "value")
|
|
277
|
+
?.let { value ->
|
|
278
|
+
hexStringToByteArray(value) ?: value.toByteArray()
|
|
279
|
+
}
|
|
280
|
+
val descriptorInitialValues =
|
|
281
|
+
mutableListOf<Pair<BluetoothGattDescriptor, ByteArray>>()
|
|
282
|
+
characteristicInitialValue?.let { value ->
|
|
283
|
+
@Suppress("DEPRECATION")
|
|
284
|
+
characteristic.value = value
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
val descriptors = characteristicJson.optJSONArray("descriptors") ?: JSONArray()
|
|
288
|
+
for (descriptorIndex in 0 until descriptors.length()) {
|
|
289
|
+
val descriptorJson = descriptors.getJSONObject(descriptorIndex)
|
|
290
|
+
val descriptor = BluetoothGattDescriptor(
|
|
291
|
+
UUID.fromString(descriptorJson.getString("uuid")),
|
|
292
|
+
descriptorPermissionsFromJson(descriptorJson.optJSONArray("permissions"))
|
|
293
|
+
)
|
|
294
|
+
optionalString(descriptorJson, "value")?.let { value ->
|
|
295
|
+
val bytes = hexStringToByteArray(value) ?: value.toByteArray()
|
|
296
|
+
@Suppress("DEPRECATION")
|
|
297
|
+
descriptor.value = bytes
|
|
298
|
+
descriptorInitialValues.add(descriptor to bytes)
|
|
299
|
+
}
|
|
300
|
+
characteristic.addDescriptor(descriptor)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
val hasClientConfigDescriptor = characteristic.descriptors.any {
|
|
304
|
+
it.uuid == CLIENT_CHARACTERISTIC_CONFIG_UUID
|
|
305
|
+
}
|
|
306
|
+
if (supportsNotifyOrIndicate(characteristic) && !hasClientConfigDescriptor) {
|
|
307
|
+
characteristic.addDescriptor(
|
|
308
|
+
BluetoothGattDescriptor(
|
|
309
|
+
CLIENT_CHARACTERISTIC_CONFIG_UUID,
|
|
310
|
+
BluetoothGattDescriptor.PERMISSION_READ or
|
|
311
|
+
BluetoothGattDescriptor.PERMISSION_WRITE
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
service.addCharacteristic(characteristic)
|
|
317
|
+
characteristicInitialValue?.let { value ->
|
|
318
|
+
setCharacteristicValue(characteristic, value)
|
|
319
|
+
}
|
|
320
|
+
descriptorInitialValues.forEach { (descriptor, value) ->
|
|
321
|
+
setDescriptorValue(descriptor, value)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
nativeServices[serviceJson.getString("uuid").lowercase(Locale.US)] = service
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
for (index in 0 until services.length()) {
|
|
328
|
+
val serviceJson = services.getJSONObject(index)
|
|
329
|
+
val service = nativeServices[
|
|
330
|
+
serviceJson.getString("uuid").lowercase(Locale.US)
|
|
331
|
+
] ?: continue
|
|
332
|
+
val includedServices = serviceJson.optJSONArray("includedServices") ?: continue
|
|
333
|
+
for (includedIndex in 0 until includedServices.length()) {
|
|
334
|
+
nativeServices[
|
|
335
|
+
includedServices.optString(includedIndex).lowercase(Locale.US)
|
|
336
|
+
]?.let(service::addService)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
nativeServices.values.forEach { service ->
|
|
341
|
+
server.addService(service)
|
|
342
|
+
}
|
|
343
|
+
}.onFailure { error ->
|
|
344
|
+
Log.w(TAG, "Unable to restore background GATT services", error)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private fun buildGattServerCallback(): BluetoothGattServerCallback {
|
|
349
|
+
return object : BluetoothGattServerCallback() {
|
|
350
|
+
override fun onCharacteristicReadRequest(
|
|
351
|
+
device: BluetoothDevice,
|
|
352
|
+
requestId: Int,
|
|
353
|
+
offset: Int,
|
|
354
|
+
characteristic: BluetoothGattCharacteristic
|
|
355
|
+
) {
|
|
356
|
+
val value = characteristicValues[characteristicKey(characteristic)] ?: ByteArray(0)
|
|
357
|
+
if (offset > value.size) {
|
|
358
|
+
gattServer?.sendResponse(
|
|
359
|
+
device,
|
|
360
|
+
requestId,
|
|
361
|
+
BluetoothGatt.GATT_INVALID_OFFSET,
|
|
362
|
+
offset,
|
|
363
|
+
null
|
|
364
|
+
)
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
gattServer?.sendResponse(
|
|
369
|
+
device,
|
|
370
|
+
requestId,
|
|
371
|
+
BluetoothGatt.GATT_SUCCESS,
|
|
372
|
+
offset,
|
|
373
|
+
value.copyOfRange(offset, value.size)
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
override fun onCharacteristicWriteRequest(
|
|
378
|
+
device: BluetoothDevice,
|
|
379
|
+
requestId: Int,
|
|
380
|
+
characteristic: BluetoothGattCharacteristic,
|
|
381
|
+
preparedWrite: Boolean,
|
|
382
|
+
responseNeeded: Boolean,
|
|
383
|
+
offset: Int,
|
|
384
|
+
value: ByteArray
|
|
385
|
+
) {
|
|
386
|
+
val canWrite = characteristic.properties and
|
|
387
|
+
(BluetoothGattCharacteristic.PROPERTY_WRITE or
|
|
388
|
+
BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0
|
|
389
|
+
if (!canWrite) {
|
|
390
|
+
if (responseNeeded) {
|
|
391
|
+
gattServer?.sendResponse(
|
|
392
|
+
device,
|
|
393
|
+
requestId,
|
|
394
|
+
BluetoothGatt.GATT_WRITE_NOT_PERMITTED,
|
|
395
|
+
offset,
|
|
396
|
+
null
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
val key = characteristicKey(characteristic)
|
|
403
|
+
val previous = characteristicValues[key] ?: ByteArray(0)
|
|
404
|
+
if (offset > previous.size) {
|
|
405
|
+
if (responseNeeded) {
|
|
406
|
+
gattServer?.sendResponse(
|
|
407
|
+
device,
|
|
408
|
+
requestId,
|
|
409
|
+
BluetoothGatt.GATT_INVALID_OFFSET,
|
|
410
|
+
offset,
|
|
411
|
+
null
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
val updated = if (offset == 0) {
|
|
418
|
+
value
|
|
419
|
+
} else {
|
|
420
|
+
val merged = previous.copyOf(maxOf(previous.size, offset + value.size))
|
|
421
|
+
System.arraycopy(value, 0, merged, offset, value.size)
|
|
422
|
+
merged
|
|
423
|
+
}
|
|
424
|
+
setCharacteristicValue(characteristic, updated)
|
|
425
|
+
|
|
426
|
+
if (responseNeeded) {
|
|
427
|
+
gattServer?.sendResponse(
|
|
428
|
+
device,
|
|
429
|
+
requestId,
|
|
430
|
+
BluetoothGatt.GATT_SUCCESS,
|
|
431
|
+
offset,
|
|
432
|
+
updated
|
|
433
|
+
)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
notifySubscribers(characteristic, updated)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
override fun onDescriptorReadRequest(
|
|
440
|
+
device: BluetoothDevice,
|
|
441
|
+
requestId: Int,
|
|
442
|
+
offset: Int,
|
|
443
|
+
descriptor: BluetoothGattDescriptor
|
|
444
|
+
) {
|
|
445
|
+
val value = descriptorValues[descriptorKey(descriptor)] ?: ByteArray(0)
|
|
446
|
+
if (offset > value.size) {
|
|
447
|
+
gattServer?.sendResponse(
|
|
448
|
+
device,
|
|
449
|
+
requestId,
|
|
450
|
+
BluetoothGatt.GATT_INVALID_OFFSET,
|
|
451
|
+
offset,
|
|
452
|
+
null
|
|
453
|
+
)
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
gattServer?.sendResponse(
|
|
458
|
+
device,
|
|
459
|
+
requestId,
|
|
460
|
+
BluetoothGatt.GATT_SUCCESS,
|
|
461
|
+
offset,
|
|
462
|
+
value.copyOfRange(offset, value.size)
|
|
463
|
+
)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
override fun onDescriptorWriteRequest(
|
|
467
|
+
device: BluetoothDevice,
|
|
468
|
+
requestId: Int,
|
|
469
|
+
descriptor: BluetoothGattDescriptor,
|
|
470
|
+
preparedWrite: Boolean,
|
|
471
|
+
responseNeeded: Boolean,
|
|
472
|
+
offset: Int,
|
|
473
|
+
value: ByteArray
|
|
474
|
+
) {
|
|
475
|
+
setDescriptorValue(descriptor, value)
|
|
476
|
+
|
|
477
|
+
if (descriptor.uuid == CLIENT_CHARACTERISTIC_CONFIG_UUID) {
|
|
478
|
+
val characteristic = descriptor.characteristic
|
|
479
|
+
val subscribers = subscribedDevices.getOrPut(characteristic.uuid) {
|
|
480
|
+
mutableSetOf()
|
|
481
|
+
}
|
|
482
|
+
val enabled = value.contentEquals(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) ||
|
|
483
|
+
value.contentEquals(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE)
|
|
484
|
+
if (enabled) {
|
|
485
|
+
subscribers.add(device)
|
|
486
|
+
} else {
|
|
487
|
+
subscribers.remove(device)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (responseNeeded) {
|
|
492
|
+
gattServer?.sendResponse(
|
|
493
|
+
device,
|
|
494
|
+
requestId,
|
|
495
|
+
BluetoothGatt.GATT_SUCCESS,
|
|
496
|
+
offset,
|
|
497
|
+
value
|
|
498
|
+
)
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
222
504
|
private fun startScan(
|
|
223
505
|
adapter: BluetoothAdapter,
|
|
224
506
|
serviceUUIDs: Array<String>,
|
|
@@ -292,6 +574,7 @@ class MunimBluetoothBackgroundService : Service() {
|
|
|
292
574
|
)
|
|
293
575
|
}
|
|
294
576
|
|
|
577
|
+
@Suppress("DEPRECATION")
|
|
295
578
|
private fun buildNotification(neighborCount: Int): Notification {
|
|
296
579
|
ensureNotificationChannel()
|
|
297
580
|
|
|
@@ -347,6 +630,218 @@ class MunimBluetoothBackgroundService : Service() {
|
|
|
347
630
|
return runCatching { ScanMode.valueOf(rawValue) }.getOrElse { ScanMode.LOWPOWER }
|
|
348
631
|
}
|
|
349
632
|
|
|
633
|
+
private fun sessionConfigFromIntent(intent: Intent): SessionConfig {
|
|
634
|
+
return SessionConfig(
|
|
635
|
+
serviceUUIDs = intent.getStringArrayExtra(EXTRA_SERVICE_UUIDS) ?: emptyArray(),
|
|
636
|
+
localName = intent.getStringExtra(EXTRA_LOCAL_NAME),
|
|
637
|
+
allowDuplicates = intent.getBooleanExtra(EXTRA_ALLOW_DUPLICATES, false),
|
|
638
|
+
scanMode = parseScanMode(
|
|
639
|
+
intent.getStringExtra(EXTRA_SCAN_MODE) ?: ScanMode.LOWPOWER.name
|
|
640
|
+
),
|
|
641
|
+
gattServicesJson = intent.getStringExtra(EXTRA_GATT_SERVICES_JSON),
|
|
642
|
+
restoreGattOnStart = false,
|
|
643
|
+
notificationChannelId = intent.getStringExtra(EXTRA_NOTIFICATION_CHANNEL_ID)
|
|
644
|
+
?: DEFAULT_CHANNEL_ID,
|
|
645
|
+
notificationChannelName = intent.getStringExtra(EXTRA_NOTIFICATION_CHANNEL_NAME)
|
|
646
|
+
?: DEFAULT_CHANNEL_NAME,
|
|
647
|
+
notificationTitle = intent.getStringExtra(EXTRA_NOTIFICATION_TITLE)
|
|
648
|
+
?: DEFAULT_NOTIFICATION_TITLE,
|
|
649
|
+
notificationText = intent.getStringExtra(EXTRA_NOTIFICATION_TEXT)
|
|
650
|
+
?: DEFAULT_NOTIFICATION_TEXT
|
|
651
|
+
)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private fun persistSession(config: SessionConfig) {
|
|
655
|
+
getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
|
|
656
|
+
.edit()
|
|
657
|
+
.putString(PREF_SERVICE_UUIDS, config.serviceUUIDs.joinToString("\n"))
|
|
658
|
+
.putString(PREF_LOCAL_NAME, config.localName)
|
|
659
|
+
.putBoolean(PREF_ALLOW_DUPLICATES, config.allowDuplicates)
|
|
660
|
+
.putString(PREF_SCAN_MODE, config.scanMode.name)
|
|
661
|
+
.putString(PREF_GATT_SERVICES_JSON, config.gattServicesJson)
|
|
662
|
+
.putString(PREF_NOTIFICATION_CHANNEL_ID, config.notificationChannelId)
|
|
663
|
+
.putString(PREF_NOTIFICATION_CHANNEL_NAME, config.notificationChannelName)
|
|
664
|
+
.putString(PREF_NOTIFICATION_TITLE, config.notificationTitle)
|
|
665
|
+
.putString(PREF_NOTIFICATION_TEXT, config.notificationText)
|
|
666
|
+
.apply()
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
private fun readPersistedSession(): SessionConfig? {
|
|
670
|
+
val preferences = getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
|
|
671
|
+
if (!preferences.contains(PREF_SERVICE_UUIDS)) {
|
|
672
|
+
return null
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return SessionConfig(
|
|
676
|
+
serviceUUIDs = preferences.getString(PREF_SERVICE_UUIDS, "")
|
|
677
|
+
?.split('\n')
|
|
678
|
+
?.filter { it.isNotBlank() }
|
|
679
|
+
?.toTypedArray()
|
|
680
|
+
?: emptyArray(),
|
|
681
|
+
localName = preferences.getString(PREF_LOCAL_NAME, null),
|
|
682
|
+
allowDuplicates = preferences.getBoolean(PREF_ALLOW_DUPLICATES, false),
|
|
683
|
+
scanMode = parseScanMode(
|
|
684
|
+
preferences.getString(PREF_SCAN_MODE, ScanMode.LOWPOWER.name)
|
|
685
|
+
?: ScanMode.LOWPOWER.name
|
|
686
|
+
),
|
|
687
|
+
gattServicesJson = preferences.getString(PREF_GATT_SERVICES_JSON, null),
|
|
688
|
+
restoreGattOnStart = true,
|
|
689
|
+
notificationChannelId = preferences.getString(
|
|
690
|
+
PREF_NOTIFICATION_CHANNEL_ID,
|
|
691
|
+
DEFAULT_CHANNEL_ID
|
|
692
|
+
) ?: DEFAULT_CHANNEL_ID,
|
|
693
|
+
notificationChannelName = preferences.getString(
|
|
694
|
+
PREF_NOTIFICATION_CHANNEL_NAME,
|
|
695
|
+
DEFAULT_CHANNEL_NAME
|
|
696
|
+
) ?: DEFAULT_CHANNEL_NAME,
|
|
697
|
+
notificationTitle = preferences.getString(
|
|
698
|
+
PREF_NOTIFICATION_TITLE,
|
|
699
|
+
DEFAULT_NOTIFICATION_TITLE
|
|
700
|
+
) ?: DEFAULT_NOTIFICATION_TITLE,
|
|
701
|
+
notificationText = preferences.getString(
|
|
702
|
+
PREF_NOTIFICATION_TEXT,
|
|
703
|
+
DEFAULT_NOTIFICATION_TEXT
|
|
704
|
+
) ?: DEFAULT_NOTIFICATION_TEXT
|
|
705
|
+
)
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
private fun clearPersistedSession() {
|
|
709
|
+
getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
|
|
710
|
+
.edit()
|
|
711
|
+
.clear()
|
|
712
|
+
.apply()
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
private fun propertiesFromJson(properties: JSONArray?): Int {
|
|
716
|
+
var result = 0
|
|
717
|
+
forEachString(properties) { property ->
|
|
718
|
+
when (property) {
|
|
719
|
+
"read" -> result = result or BluetoothGattCharacteristic.PROPERTY_READ
|
|
720
|
+
"write" -> result = result or BluetoothGattCharacteristic.PROPERTY_WRITE
|
|
721
|
+
"notify" -> result = result or BluetoothGattCharacteristic.PROPERTY_NOTIFY
|
|
722
|
+
"indicate" -> result = result or BluetoothGattCharacteristic.PROPERTY_INDICATE
|
|
723
|
+
"writeWithoutResponse" -> {
|
|
724
|
+
result = result or BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return result
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
private fun descriptorPermissionsFromJson(permissions: JSONArray?): Int {
|
|
732
|
+
if (permissions == null || permissions.length() == 0) {
|
|
733
|
+
return BluetoothGattDescriptor.PERMISSION_READ or
|
|
734
|
+
BluetoothGattDescriptor.PERMISSION_WRITE
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
var result = 0
|
|
738
|
+
forEachString(permissions) { permission ->
|
|
739
|
+
when (permission) {
|
|
740
|
+
"read" -> result = result or BluetoothGattDescriptor.PERMISSION_READ
|
|
741
|
+
"write" -> result = result or BluetoothGattDescriptor.PERMISSION_WRITE
|
|
742
|
+
"readEncrypted" -> result = result or
|
|
743
|
+
BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED
|
|
744
|
+
"writeEncrypted" -> result = result or
|
|
745
|
+
BluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED
|
|
746
|
+
"readEncryptedMitm" -> result = result or
|
|
747
|
+
BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED_MITM
|
|
748
|
+
"writeEncryptedMitm" -> result = result or
|
|
749
|
+
BluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED_MITM
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return result
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
private fun forEachString(array: JSONArray?, block: (String) -> Unit) {
|
|
756
|
+
if (array == null) return
|
|
757
|
+
for (index in 0 until array.length()) {
|
|
758
|
+
block(array.optString(index))
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
private fun optionalString(json: org.json.JSONObject, key: String): String? {
|
|
763
|
+
return if (json.has(key) && !json.isNull(key)) json.getString(key) else null
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
private fun supportsNotifyOrIndicate(characteristic: BluetoothGattCharacteristic): Boolean {
|
|
767
|
+
return characteristic.properties and
|
|
768
|
+
(BluetoothGattCharacteristic.PROPERTY_NOTIFY or
|
|
769
|
+
BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
private fun setCharacteristicValue(
|
|
773
|
+
characteristic: BluetoothGattCharacteristic,
|
|
774
|
+
value: ByteArray
|
|
775
|
+
) {
|
|
776
|
+
characteristicValues[characteristicKey(characteristic)] = value
|
|
777
|
+
@Suppress("DEPRECATION")
|
|
778
|
+
characteristic.value = value
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
private fun setDescriptorValue(descriptor: BluetoothGattDescriptor, value: ByteArray) {
|
|
782
|
+
descriptorValues[descriptorKey(descriptor)] = value
|
|
783
|
+
@Suppress("DEPRECATION")
|
|
784
|
+
descriptor.value = value
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
private fun notifySubscribers(
|
|
788
|
+
characteristic: BluetoothGattCharacteristic,
|
|
789
|
+
value: ByteArray
|
|
790
|
+
) {
|
|
791
|
+
val subscribers = subscribedDevices[characteristic.uuid]?.toList().orEmpty()
|
|
792
|
+
subscribers.forEach { device ->
|
|
793
|
+
try {
|
|
794
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
795
|
+
gattServer?.notifyCharacteristicChanged(
|
|
796
|
+
device,
|
|
797
|
+
characteristic,
|
|
798
|
+
characteristic.properties and
|
|
799
|
+
BluetoothGattCharacteristic.PROPERTY_INDICATE != 0,
|
|
800
|
+
value
|
|
801
|
+
)
|
|
802
|
+
} else {
|
|
803
|
+
@Suppress("DEPRECATION")
|
|
804
|
+
characteristic.value = value
|
|
805
|
+
@Suppress("DEPRECATION")
|
|
806
|
+
gattServer?.notifyCharacteristicChanged(
|
|
807
|
+
device,
|
|
808
|
+
characteristic,
|
|
809
|
+
characteristic.properties and
|
|
810
|
+
BluetoothGattCharacteristic.PROPERTY_INDICATE != 0
|
|
811
|
+
)
|
|
812
|
+
}
|
|
813
|
+
} catch (error: SecurityException) {
|
|
814
|
+
Log.w(TAG, "Unable to notify background GATT subscriber", error)
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
private fun characteristicKey(characteristic: BluetoothGattCharacteristic): String {
|
|
820
|
+
return "${characteristic.service?.uuid}|${characteristic.uuid}".lowercase(Locale.US)
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
private fun descriptorKey(descriptor: BluetoothGattDescriptor): String {
|
|
824
|
+
val characteristic = descriptor.characteristic
|
|
825
|
+
?: return "unknown|${descriptor.uuid}".lowercase(Locale.US)
|
|
826
|
+
return "${characteristicKey(characteristic)}|${descriptor.uuid}"
|
|
827
|
+
.lowercase(Locale.US)
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
private fun hexStringToByteArray(hex: String): ByteArray? {
|
|
831
|
+
val cleanHex = hex.removePrefix("0x")
|
|
832
|
+
if (cleanHex.isEmpty() || cleanHex.length % 2 != 0) {
|
|
833
|
+
return null
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return try {
|
|
837
|
+
ByteArray(cleanHex.length / 2) { index ->
|
|
838
|
+
cleanHex.substring(index * 2, index * 2 + 2).toInt(16).toByte()
|
|
839
|
+
}
|
|
840
|
+
} catch (_: NumberFormatException) {
|
|
841
|
+
null
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
350
845
|
companion object {
|
|
351
846
|
const val ACTION_START = "com.munimbluetooth.action.START_BACKGROUND_SESSION"
|
|
352
847
|
const val ACTION_STOP = "com.munimbluetooth.action.STOP_BACKGROUND_SESSION"
|
|
@@ -355,6 +850,7 @@ class MunimBluetoothBackgroundService : Service() {
|
|
|
355
850
|
const val EXTRA_LOCAL_NAME = "localName"
|
|
356
851
|
const val EXTRA_ALLOW_DUPLICATES = "allowDuplicates"
|
|
357
852
|
const val EXTRA_SCAN_MODE = "scanMode"
|
|
853
|
+
const val EXTRA_GATT_SERVICES_JSON = "gattServicesJson"
|
|
358
854
|
const val EXTRA_NOTIFICATION_CHANNEL_ID = "notificationChannelId"
|
|
359
855
|
const val EXTRA_NOTIFICATION_CHANNEL_NAME = "notificationChannelName"
|
|
360
856
|
const val EXTRA_NOTIFICATION_TITLE = "notificationTitle"
|
|
@@ -367,5 +863,17 @@ class MunimBluetoothBackgroundService : Service() {
|
|
|
367
863
|
|
|
368
864
|
private const val NOTIFICATION_ID = 48231
|
|
369
865
|
private const val TAG = "MunimBluetoothBgSvc"
|
|
866
|
+
private const val PREFERENCES_NAME = "munim-bluetooth-background"
|
|
867
|
+
private const val PREF_SERVICE_UUIDS = "serviceUUIDs"
|
|
868
|
+
private const val PREF_LOCAL_NAME = "localName"
|
|
869
|
+
private const val PREF_ALLOW_DUPLICATES = "allowDuplicates"
|
|
870
|
+
private const val PREF_SCAN_MODE = "scanMode"
|
|
871
|
+
private const val PREF_GATT_SERVICES_JSON = "gattServicesJson"
|
|
872
|
+
private const val PREF_NOTIFICATION_CHANNEL_ID = "notificationChannelId"
|
|
873
|
+
private const val PREF_NOTIFICATION_CHANNEL_NAME = "notificationChannelName"
|
|
874
|
+
private const val PREF_NOTIFICATION_TITLE = "notificationTitle"
|
|
875
|
+
private const val PREF_NOTIFICATION_TEXT = "notificationText"
|
|
876
|
+
private val CLIENT_CHARACTERISTIC_CONFIG_UUID =
|
|
877
|
+
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
|
|
370
878
|
}
|
|
371
879
|
}
|