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
|
@@ -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,34 +103,53 @@ class MunimBluetoothBackgroundService : Service() {
|
|
|
94
103
|
super.onDestroy()
|
|
95
104
|
}
|
|
96
105
|
|
|
97
|
-
private fun startBleSession(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
private fun startBleSession(config: SessionConfig) {
|
|
107
|
+
stopBleSession()
|
|
108
|
+
|
|
109
|
+
if (!BluetoothPermissionUtils.hasRequiredPermissions(applicationContext)) {
|
|
110
|
+
Log.w(TAG, "Unable to start background BLE session: missing runtime permissions")
|
|
111
|
+
stopSelf()
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
103
115
|
bluetoothManager =
|
|
104
116
|
applicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
|
|
105
117
|
bluetoothAdapter = bluetoothManager?.adapter
|
|
106
118
|
|
|
107
119
|
val adapter = bluetoothAdapter
|
|
108
|
-
|
|
120
|
+
val isEnabled = try {
|
|
121
|
+
adapter?.isEnabled == true
|
|
122
|
+
} catch (error: SecurityException) {
|
|
123
|
+
Log.w(TAG, "Unable to inspect Bluetooth adapter state for background session", error)
|
|
124
|
+
false
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (adapter == null || !isEnabled) {
|
|
109
128
|
Log.w(TAG, "Unable to start background BLE session: Bluetooth unavailable")
|
|
110
129
|
stopSelf()
|
|
111
130
|
return
|
|
112
131
|
}
|
|
113
132
|
|
|
114
|
-
if (!localName.isNullOrBlank() && previousAdapterName == null) {
|
|
115
|
-
previousAdapterName =
|
|
133
|
+
if (!config.localName.isNullOrBlank() && previousAdapterName == null) {
|
|
134
|
+
previousAdapterName = try {
|
|
135
|
+
adapter.name
|
|
136
|
+
} catch (error: SecurityException) {
|
|
137
|
+
Log.w(TAG, "Unable to read adapter name for background advertising", error)
|
|
138
|
+
null
|
|
139
|
+
}
|
|
140
|
+
|
|
116
141
|
try {
|
|
117
|
-
adapter.name = localName
|
|
142
|
+
adapter.name = config.localName
|
|
118
143
|
} catch (error: SecurityException) {
|
|
119
144
|
Log.w(TAG, "Unable to set adapter name for background advertising", error)
|
|
120
145
|
}
|
|
121
146
|
}
|
|
122
147
|
|
|
123
|
-
|
|
124
|
-
|
|
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)
|
|
125
153
|
}
|
|
126
154
|
|
|
127
155
|
private fun stopBleSession() {
|
|
@@ -135,6 +163,12 @@ class MunimBluetoothBackgroundService : Service() {
|
|
|
135
163
|
scanCallback = null
|
|
136
164
|
scanner = null
|
|
137
165
|
|
|
166
|
+
gattServer?.close()
|
|
167
|
+
gattServer = null
|
|
168
|
+
characteristicValues.clear()
|
|
169
|
+
descriptorValues.clear()
|
|
170
|
+
subscribedDevices.clear()
|
|
171
|
+
|
|
138
172
|
advertiseCallback?.let { callback ->
|
|
139
173
|
try {
|
|
140
174
|
advertiser?.stopAdvertising(callback)
|
|
@@ -170,12 +204,14 @@ class MunimBluetoothBackgroundService : Service() {
|
|
|
170
204
|
advertiser = activeAdvertiser
|
|
171
205
|
|
|
172
206
|
val data = AdvertiseData.Builder()
|
|
173
|
-
.setIncludeDeviceName(includeDeviceName)
|
|
174
207
|
|
|
175
208
|
serviceUUIDs.forEach { uuid ->
|
|
176
209
|
runCatching { ParcelUuid.fromString(uuid) }.getOrNull()?.let(data::addServiceUuid)
|
|
177
210
|
}
|
|
178
211
|
|
|
212
|
+
val scanResponse = AdvertiseData.Builder()
|
|
213
|
+
.setIncludeDeviceName(includeDeviceName)
|
|
214
|
+
|
|
179
215
|
val settings = AdvertiseSettings.Builder()
|
|
180
216
|
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_POWER)
|
|
181
217
|
.setConnectable(true)
|
|
@@ -193,7 +229,276 @@ class MunimBluetoothBackgroundService : Service() {
|
|
|
193
229
|
}
|
|
194
230
|
}
|
|
195
231
|
|
|
196
|
-
|
|
232
|
+
try {
|
|
233
|
+
activeAdvertiser.startAdvertising(
|
|
234
|
+
settings,
|
|
235
|
+
data.build(),
|
|
236
|
+
scanResponse.build(),
|
|
237
|
+
advertiseCallback
|
|
238
|
+
)
|
|
239
|
+
} catch (error: SecurityException) {
|
|
240
|
+
Log.w(TAG, "Unable to start background advertising", error)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
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
|
+
}
|
|
197
502
|
}
|
|
198
503
|
|
|
199
504
|
private fun startScan(
|
|
@@ -242,7 +547,11 @@ class MunimBluetoothBackgroundService : Service() {
|
|
|
242
547
|
}
|
|
243
548
|
}
|
|
244
549
|
|
|
245
|
-
|
|
550
|
+
try {
|
|
551
|
+
activeScanner.startScan(filters, settingsBuilder.build(), scanCallback)
|
|
552
|
+
} catch (error: SecurityException) {
|
|
553
|
+
Log.w(TAG, "Unable to start background scan", error)
|
|
554
|
+
}
|
|
246
555
|
}
|
|
247
556
|
|
|
248
557
|
private fun handleScanResult(result: ScanResult, allowDuplicates: Boolean) {
|
|
@@ -265,6 +574,7 @@ class MunimBluetoothBackgroundService : Service() {
|
|
|
265
574
|
)
|
|
266
575
|
}
|
|
267
576
|
|
|
577
|
+
@Suppress("DEPRECATION")
|
|
268
578
|
private fun buildNotification(neighborCount: Int): Notification {
|
|
269
579
|
ensureNotificationChannel()
|
|
270
580
|
|
|
@@ -320,6 +630,218 @@ class MunimBluetoothBackgroundService : Service() {
|
|
|
320
630
|
return runCatching { ScanMode.valueOf(rawValue) }.getOrElse { ScanMode.LOWPOWER }
|
|
321
631
|
}
|
|
322
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
|
+
|
|
323
845
|
companion object {
|
|
324
846
|
const val ACTION_START = "com.munimbluetooth.action.START_BACKGROUND_SESSION"
|
|
325
847
|
const val ACTION_STOP = "com.munimbluetooth.action.STOP_BACKGROUND_SESSION"
|
|
@@ -328,6 +850,7 @@ class MunimBluetoothBackgroundService : Service() {
|
|
|
328
850
|
const val EXTRA_LOCAL_NAME = "localName"
|
|
329
851
|
const val EXTRA_ALLOW_DUPLICATES = "allowDuplicates"
|
|
330
852
|
const val EXTRA_SCAN_MODE = "scanMode"
|
|
853
|
+
const val EXTRA_GATT_SERVICES_JSON = "gattServicesJson"
|
|
331
854
|
const val EXTRA_NOTIFICATION_CHANNEL_ID = "notificationChannelId"
|
|
332
855
|
const val EXTRA_NOTIFICATION_CHANNEL_NAME = "notificationChannelName"
|
|
333
856
|
const val EXTRA_NOTIFICATION_TITLE = "notificationTitle"
|
|
@@ -340,5 +863,17 @@ class MunimBluetoothBackgroundService : Service() {
|
|
|
340
863
|
|
|
341
864
|
private const val NOTIFICATION_ID = 48231
|
|
342
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")
|
|
343
878
|
}
|
|
344
879
|
}
|