@mentra/bluetooth-sdk 3.2.0-dev.195 → 3.2.0-dev.203
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -45
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +5 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +77 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +12 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +318 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/MessageAckPolicy.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +88 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/WifiRequestResolution.kt +246 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +50 -16
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +104 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +626 -120
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattLifecycle.kt +64 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +76 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +16 -3
- package/android/src/test/java/com/mentra/bluetoothsdk/GlassesStatusClassicAudioTest.kt +14 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/MessageAckPolicyTest.kt +16 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoBridgeTest.kt +33 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +169 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/WifiRequestResolutionTest.kt +271 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceManifestTest.kt +57 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +52 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTrackerTest.kt +131 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +94 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattLifecycleTest.kt +112 -0
- package/build/BluetoothSdk.types.d.ts +62 -1
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +3 -2
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +1 -1
- package/build/generated/changelogCatalog.js +1 -1
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +8 -2
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +78 -1
- package/ios/Source/DeviceManager.swift +13 -6
- package/ios/Source/GeneratedChangelogCatalog.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +397 -99
- package/ios/Source/WifiRequestResolution.swift +251 -0
- package/ios/Source/requests/VersionInfoResponseAccumulator.swift +101 -0
- package/ios/Source/sgcs/MentraLive.swift +179 -37
- package/ios/Source/sgcs/SGCManager.swift +13 -1
- package/ios/Source/status/DeviceStatus.swift +30 -3
- package/ios/Tests/VersionInfoBridgeTests.swift +32 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +181 -0
- package/ios/Tests/WifiRequestResolutionTests.swift +320 -0
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +76 -1
- package/src/_private/BluetoothSdkModule.ts +4 -1
- package/src/generated/changelogCatalog.ts +1 -1
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +11 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Process-wide barrier between Mentra Live GATT teardown and the next connection attempt.
|
|
5
|
+
*
|
|
6
|
+
* Android can keep the physical link alive after a disconnect request until it delivers the
|
|
7
|
+
* disconnected callback. Starting another GATT session before that callback can reuse the old link,
|
|
8
|
+
* including its already-consumed MTU exchange. Callers queue connection work here and release it
|
|
9
|
+
* only after every outstanding teardown has completed or timed out. A newer deferred connection
|
|
10
|
+
* replaces the previous one so reconnect retries cannot start multiple GATT sessions together.
|
|
11
|
+
*/
|
|
12
|
+
internal class MentraLiveGattTeardownBarrier {
|
|
13
|
+
private val activeTeardowns = mutableSetOf<Long>()
|
|
14
|
+
private var waitingConnection: (() -> Unit)? = null
|
|
15
|
+
private var nextToken = 1L
|
|
16
|
+
|
|
17
|
+
@Synchronized
|
|
18
|
+
fun beginTeardown(): Long {
|
|
19
|
+
val token = nextToken++
|
|
20
|
+
activeTeardowns.add(token)
|
|
21
|
+
return token
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Admission and execution are one operation; production callers all use the main looper. */
|
|
25
|
+
@Synchronized
|
|
26
|
+
fun runWhenIdle(work: () -> Unit) {
|
|
27
|
+
if (activeTeardowns.isEmpty()) work() else waitingConnection = work
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Synchronized
|
|
31
|
+
fun completeTeardown(token: Long) {
|
|
32
|
+
if (!activeTeardowns.remove(token) || activeTeardowns.isNotEmpty()) return
|
|
33
|
+
val ready = waitingConnection.also { waitingConnection = null }
|
|
34
|
+
ready?.invoke()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One-shot guard shared by the MTU callback and its watchdog fallback. */
|
|
39
|
+
internal class MentraLiveMtuSetupGate {
|
|
40
|
+
private var nextToken = 1L
|
|
41
|
+
private var pendingToken: Long? = null
|
|
42
|
+
|
|
43
|
+
@Synchronized
|
|
44
|
+
fun begin(): Long {
|
|
45
|
+
val token = nextToken++
|
|
46
|
+
pendingToken = token
|
|
47
|
+
return token
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Returns true exactly once for the current setup operation. */
|
|
51
|
+
@Synchronized
|
|
52
|
+
fun complete(token: Long): Boolean {
|
|
53
|
+
if (pendingToken != token) {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
pendingToken = null
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@Synchronized
|
|
61
|
+
fun cancel() {
|
|
62
|
+
pendingToken = null
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -313,8 +313,10 @@ abstract class SGCManager {
|
|
|
313
313
|
|
|
314
314
|
// Network Management
|
|
315
315
|
abstract fun requestWifiScan(scanId: String?)
|
|
316
|
+
open fun requestSavedWifiNetworks(requestId: String, sid: String): Boolean = false
|
|
316
317
|
abstract fun sendWifiCredentials(ssid: String, password: String)
|
|
317
318
|
abstract fun forgetWifiNetwork(ssid: String)
|
|
319
|
+
open fun forgetWifiNetwork(ssid: String, requestId: String?, sid: String?): Boolean = false
|
|
318
320
|
abstract fun sendHotspotState(enabled: Boolean)
|
|
319
321
|
|
|
320
322
|
/** Set glasses system clock (Mentra Live and G2; no-op on other devices). */
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import android.bluetooth.BluetoothGatt
|
|
4
|
+
import android.bluetooth.BluetoothGattCallback
|
|
5
|
+
import android.bluetooth.BluetoothGattCharacteristic
|
|
6
|
+
import android.bluetooth.BluetoothGattDescriptor
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Binder callbacks only capture their arguments. Session validation and all callback effects run
|
|
10
|
+
* on the lifecycle owner, in the same queue as connect and teardown. Notification bytes must be
|
|
11
|
+
* copied before dispatch: Android reuses the mutable characteristic between notifications.
|
|
12
|
+
*/
|
|
13
|
+
internal abstract class SerializedGattCallback(
|
|
14
|
+
private val enqueue: (() -> Unit) -> Unit,
|
|
15
|
+
private val isCurrent: (BluetoothGatt) -> Boolean,
|
|
16
|
+
) : BluetoothGattCallback() {
|
|
17
|
+
private fun dispatch(gatt: BluetoothGatt, work: () -> Unit) {
|
|
18
|
+
enqueue { if (isCurrent(gatt)) work() }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
final override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) =
|
|
22
|
+
dispatch(gatt) { handleConnectionStateChange(gatt, status, newState) }
|
|
23
|
+
|
|
24
|
+
final override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) =
|
|
25
|
+
dispatch(gatt) { handleServicesDiscovered(gatt, status) }
|
|
26
|
+
|
|
27
|
+
final override fun onPhyUpdate(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) =
|
|
28
|
+
dispatch(gatt) { handlePhyUpdate(gatt, txPhy, rxPhy, status) }
|
|
29
|
+
|
|
30
|
+
final override fun onPhyRead(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) =
|
|
31
|
+
dispatch(gatt) { handlePhyRead(gatt, txPhy, rxPhy, status) }
|
|
32
|
+
|
|
33
|
+
final override fun onCharacteristicRead(
|
|
34
|
+
gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int,
|
|
35
|
+
) = dispatch(gatt) { handleCharacteristicRead(gatt, characteristic, status) }
|
|
36
|
+
|
|
37
|
+
final override fun onReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int) =
|
|
38
|
+
dispatch(gatt) { handleReadRemoteRssi(gatt, rssi, status) }
|
|
39
|
+
|
|
40
|
+
final override fun onCharacteristicWrite(
|
|
41
|
+
gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int,
|
|
42
|
+
) = dispatch(gatt) { handleCharacteristicWrite(gatt, characteristic, status) }
|
|
43
|
+
|
|
44
|
+
@Suppress("DEPRECATION")
|
|
45
|
+
final override fun onCharacteristicChanged(
|
|
46
|
+
gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic,
|
|
47
|
+
) {
|
|
48
|
+
val value = characteristic.value?.copyOf() ?: return
|
|
49
|
+
dispatch(gatt) { handleCharacteristicChanged(gatt, characteristic, value) }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
final override fun onCharacteristicChanged(
|
|
53
|
+
gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray,
|
|
54
|
+
) {
|
|
55
|
+
val snapshot = value.copyOf()
|
|
56
|
+
dispatch(gatt) { handleCharacteristicChanged(gatt, characteristic, snapshot) }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
final override fun onDescriptorWrite(
|
|
60
|
+
gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int,
|
|
61
|
+
) = dispatch(gatt) { handleDescriptorWrite(gatt, descriptor, status) }
|
|
62
|
+
|
|
63
|
+
final override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) =
|
|
64
|
+
dispatch(gatt) { handleMtuChanged(gatt, mtu, status) }
|
|
65
|
+
|
|
66
|
+
protected abstract fun handleConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int)
|
|
67
|
+
protected abstract fun handleServicesDiscovered(gatt: BluetoothGatt, status: Int)
|
|
68
|
+
protected abstract fun handlePhyUpdate(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int)
|
|
69
|
+
protected abstract fun handlePhyRead(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int)
|
|
70
|
+
protected abstract fun handleCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int)
|
|
71
|
+
protected abstract fun handleReadRemoteRssi(gatt: BluetoothGatt, rssi: Int, status: Int)
|
|
72
|
+
protected abstract fun handleCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int)
|
|
73
|
+
protected abstract fun handleCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, data: ByteArray)
|
|
74
|
+
protected abstract fun handleDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int)
|
|
75
|
+
protected abstract fun handleMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int)
|
|
76
|
+
}
|
|
@@ -189,6 +189,10 @@ data class VersionInfoResult(
|
|
|
189
189
|
val appVersion: String,
|
|
190
190
|
val packageName: String,
|
|
191
191
|
val hotspotOtaVersion: Int,
|
|
192
|
+
val wifiForgetResultVersion: Int? = null,
|
|
193
|
+
val savedWifiNetworksVersion: Int? = null,
|
|
194
|
+
val versionInfoType: String? = null,
|
|
195
|
+
val sid: String? = null,
|
|
192
196
|
) {
|
|
193
197
|
internal fun toMap(): Map<String, Any> =
|
|
194
198
|
buildMap {
|
|
@@ -200,13 +204,16 @@ data class VersionInfoResult(
|
|
|
200
204
|
systemTimeMs?.let { put("systemTimeMs", it) }
|
|
201
205
|
put("otaVersionUrl", otaVersionUrl)
|
|
202
206
|
put("appVersion", appVersion)
|
|
203
|
-
// Only when known
|
|
204
|
-
//
|
|
205
|
-
// overwrite a known identity with "absent", which the OTA guard reads as stock.
|
|
207
|
+
// Only when known: per-chunk events may omit package identity. Emitting an empty
|
|
208
|
+
// value would overwrite a known identity, which the OTA guard uses to identify stock.
|
|
206
209
|
if (packageName.isNotEmpty()) {
|
|
207
210
|
put("packageName", packageName)
|
|
208
211
|
}
|
|
209
212
|
put("hotspotOtaVersion", hotspotOtaVersion)
|
|
213
|
+
wifiForgetResultVersion?.let { put("wifiForgetResultVersion", it) }
|
|
214
|
+
savedWifiNetworksVersion?.let { put("savedWifiNetworksVersion", it) }
|
|
215
|
+
versionInfoType?.let { put("versionInfoType", it) }
|
|
216
|
+
sid?.let { put("sid", it) }
|
|
210
217
|
}
|
|
211
218
|
|
|
212
219
|
companion object {
|
|
@@ -237,6 +244,12 @@ data class VersionInfoResult(
|
|
|
237
244
|
packageName = stringValue(values, "packageName", "package_name") ?: "",
|
|
238
245
|
hotspotOtaVersion =
|
|
239
246
|
numberValue(values, "hotspotOtaVersion", "hotspot_ota_version") ?: 0,
|
|
247
|
+
wifiForgetResultVersion =
|
|
248
|
+
numberValue(values, "wifiForgetResultVersion", "wifi_forget_result_version"),
|
|
249
|
+
savedWifiNetworksVersion =
|
|
250
|
+
numberValue(values, "savedWifiNetworksVersion", "saved_wifi_networks_version"),
|
|
251
|
+
versionInfoType = stringValue(values, "versionInfoType", "version_info_type"),
|
|
252
|
+
sid = stringValue(values, "sid"),
|
|
240
253
|
)
|
|
241
254
|
}
|
|
242
255
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class GlassesStatusClassicAudioTest {
|
|
7
|
+
@Test
|
|
8
|
+
fun `serializes connected Classic audio state for React Native`() {
|
|
9
|
+
val serialized =
|
|
10
|
+
GlassesStatus.fromMap(mapOf("bluetoothClassicConnected" to true)).toMap()
|
|
11
|
+
|
|
12
|
+
assertThat(serialized["bluetoothClassicConnected"]).isEqualTo(true)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertNull
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
class MessageAckPolicyTest {
|
|
8
|
+
@Test
|
|
9
|
+
fun `every non-ack message id is acknowledged regardless of type`() {
|
|
10
|
+
assertEquals(17L, incomingGlassesMessageAckId("wifi_forget_result", 17L))
|
|
11
|
+
assertEquals(18L, incomingGlassesMessageAckId("saved_wifi_networks", 18L))
|
|
12
|
+
assertEquals(19L, incomingGlassesMessageAckId("future_terminal_type", 19L))
|
|
13
|
+
assertNull(incomingGlassesMessageAckId("msg_ack", 20L))
|
|
14
|
+
assertNull(incomingGlassesMessageAckId("wifi_forget_result", null))
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertTrue
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
import org.junit.runner.RunWith
|
|
7
|
+
import org.robolectric.RobolectricTestRunner
|
|
8
|
+
import org.robolectric.annotation.Config
|
|
9
|
+
|
|
10
|
+
@RunWith(RobolectricTestRunner::class)
|
|
11
|
+
@Config(sdk = [28])
|
|
12
|
+
class VersionInfoBridgeTest {
|
|
13
|
+
@Test
|
|
14
|
+
fun wireMetadataSurvivesBridgeNormalizationAndCompletesOnlyAfterBothChunks() {
|
|
15
|
+
val accumulator = VersionInfoResponseAccumulator("request-1")
|
|
16
|
+
val outcomes = mutableListOf<VersionInfoAccumulatorOutcome>()
|
|
17
|
+
val sink = Bridge.addEventSink { type, body ->
|
|
18
|
+
if (type == "version_info") outcomes.add(accumulator.accept(body))
|
|
19
|
+
}
|
|
20
|
+
val common = mapOf<String, Any>("request_id" to "request-1", "sid" to "asg-1", "chunkCount" to 2)
|
|
21
|
+
try {
|
|
22
|
+
Bridge.sendVersionInfo(common + mapOf("chunkIndex" to 1, "final" to false, "build_number" to "42"), "version_info_1")
|
|
23
|
+
Bridge.sendVersionInfo(common + mapOf("chunkIndex" to 2, "final" to true, "bes_fw_version" to "new"), "version_info_3")
|
|
24
|
+
assertEquals(VersionInfoAccumulatorOutcome.Waiting, outcomes[0])
|
|
25
|
+
val complete = outcomes[1] as VersionInfoAccumulatorOutcome.Complete
|
|
26
|
+
assertEquals("42", complete.result.buildNumber)
|
|
27
|
+
assertEquals("new", complete.result.besFirmwareVersion)
|
|
28
|
+
assertTrue(complete.result.toMap().keys.none { it.startsWith("_response") })
|
|
29
|
+
} finally {
|
|
30
|
+
Bridge.removeEventSink(sink)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class VersionInfoResponseAccumulatorTest {
|
|
7
|
+
@Test
|
|
8
|
+
fun mergesCurrentChunksAndCompletesOnFirmwareChunk() {
|
|
9
|
+
val accumulator = VersionInfoResponseAccumulator("request-1")
|
|
10
|
+
|
|
11
|
+
assertThat(accumulator.accept(chunk("version_info_1", "request-1", "buildNumber" to "42")))
|
|
12
|
+
.isEqualTo(VersionInfoAccumulatorOutcome.Waiting)
|
|
13
|
+
|
|
14
|
+
val complete =
|
|
15
|
+
accumulator.accept(
|
|
16
|
+
chunk(
|
|
17
|
+
"version_info_3",
|
|
18
|
+
"request-1",
|
|
19
|
+
"buildNumber" to "",
|
|
20
|
+
"besFirmwareVersion" to "26.8.27.0",
|
|
21
|
+
"mtkFirmwareVersion" to "MentraLive_20260709",
|
|
22
|
+
)
|
|
23
|
+
) as VersionInfoAccumulatorOutcome.Complete
|
|
24
|
+
|
|
25
|
+
assertThat(complete.result.buildNumber).isEqualTo("42")
|
|
26
|
+
assertThat(complete.result.besFirmwareVersion).isEqualTo("26.8.27.0")
|
|
27
|
+
assertThat(complete.result.mtkFirmwareVersion).isEqualTo("MentraLive_20260709")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Test
|
|
31
|
+
fun ignoresMismatchedAndTrailingStaleChunks() {
|
|
32
|
+
val accumulator = VersionInfoResponseAccumulator("request-1")
|
|
33
|
+
|
|
34
|
+
assertThat(accumulator.accept(chunk("version_info_1", "other", "buildNumber" to "old")))
|
|
35
|
+
.isEqualTo(VersionInfoAccumulatorOutcome.Ignored)
|
|
36
|
+
assertThat(accumulator.accept(chunk("version_info_3", null, "besFirmwareVersion" to "stale")))
|
|
37
|
+
.isEqualTo(VersionInfoAccumulatorOutcome.Ignored)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@Test
|
|
41
|
+
fun doesNotMixCorrelatedAndUncorrelatedSequences() {
|
|
42
|
+
val accumulator = VersionInfoResponseAccumulator("request-1")
|
|
43
|
+
accumulator.accept(chunk("version_info_1", "request-1", "buildNumber" to "42"))
|
|
44
|
+
|
|
45
|
+
assertThat(accumulator.accept(chunk("version_info_3", null, "besFirmwareVersion" to "stale")))
|
|
46
|
+
.isEqualTo(VersionInfoAccumulatorOutcome.Ignored)
|
|
47
|
+
|
|
48
|
+
val complete =
|
|
49
|
+
accumulator.accept(
|
|
50
|
+
chunk("version_info_3", "request-1", "besFirmwareVersion" to "current")
|
|
51
|
+
) as VersionInfoAccumulatorOutcome.Complete
|
|
52
|
+
assertThat(complete.result.besFirmwareVersion).isEqualTo("current")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
fun staleUncorrelatedResponsesCannotReplaceCorrelatedSequence() {
|
|
57
|
+
val accumulator = VersionInfoResponseAccumulator("request-1")
|
|
58
|
+
accumulator.accept(chunk("version_info_1", "request-1", "buildNumber" to "42"))
|
|
59
|
+
|
|
60
|
+
assertThat(accumulator.accept(chunk("version_info_1", null, "buildNumber" to "stale")))
|
|
61
|
+
.isEqualTo(VersionInfoAccumulatorOutcome.Ignored)
|
|
62
|
+
assertThat(accumulator.accept(chunk("version_info", null, "buildNumber" to "legacy")))
|
|
63
|
+
.isEqualTo(VersionInfoAccumulatorOutcome.Ignored)
|
|
64
|
+
|
|
65
|
+
val complete =
|
|
66
|
+
accumulator.accept(
|
|
67
|
+
chunk("version_info_3", "request-1", "besFirmwareVersion" to "current")
|
|
68
|
+
) as VersionInfoAccumulatorOutcome.Complete
|
|
69
|
+
assertThat(complete.result.buildNumber).isEqualTo("42")
|
|
70
|
+
assertThat(complete.result.besFirmwareVersion).isEqualTo("current")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Test
|
|
74
|
+
fun repeatedFirstChunkResetsRatherThanMixingResponses() {
|
|
75
|
+
val accumulator = VersionInfoResponseAccumulator("request-1")
|
|
76
|
+
accumulator.accept(chunk("version_info_1", null, "appVersion" to "old"))
|
|
77
|
+
accumulator.accept(chunk("version_info_1", null, "buildNumber" to "43"))
|
|
78
|
+
|
|
79
|
+
val complete = (accumulator.accept(chunk("version_info_3", null, "besFirmwareVersion" to "new"))
|
|
80
|
+
as VersionInfoAccumulatorOutcome.Complete).result
|
|
81
|
+
|
|
82
|
+
assertThat(complete.appVersion).isEmpty()
|
|
83
|
+
assertThat(complete.buildNumber).isEqualTo("43")
|
|
84
|
+
assertThat(complete.besFirmwareVersion).isEqualTo("new")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@Test
|
|
88
|
+
fun legacySingleMessageCompletesImmediately() {
|
|
89
|
+
val accumulator = VersionInfoResponseAccumulator("request-1")
|
|
90
|
+
|
|
91
|
+
val complete =
|
|
92
|
+
accumulator.accept(chunk("version_info", null, "buildNumber" to "7"))
|
|
93
|
+
as VersionInfoAccumulatorOutcome.Complete
|
|
94
|
+
|
|
95
|
+
assertThat(complete.result.buildNumber).isEqualTo("7")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@Test
|
|
99
|
+
fun legacyFirstChunkDoesNotCompleteWithoutFinalChunk() {
|
|
100
|
+
val accumulator = VersionInfoResponseAccumulator("request-1")
|
|
101
|
+
assertThat(accumulator.accept(chunk("version_info_1", null, "buildNumber" to "8")))
|
|
102
|
+
.isEqualTo(VersionInfoAccumulatorOutcome.Waiting)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@Test
|
|
106
|
+
fun modernFinalChunkWaitsForMissingFirstChunk() {
|
|
107
|
+
val accumulator = VersionInfoResponseAccumulator("request-1")
|
|
108
|
+
assertThat(accumulator.accept(chunk("version_info_3", "request-1", "besFirmwareVersion" to "new")))
|
|
109
|
+
.isEqualTo(VersionInfoAccumulatorOutcome.Waiting)
|
|
110
|
+
val result = accumulator.accept(chunk("version_info_1", "request-1", "buildNumber" to "42"))
|
|
111
|
+
as VersionInfoAccumulatorOutcome.Complete
|
|
112
|
+
assertThat(result.result.buildNumber).isEqualTo("42")
|
|
113
|
+
assertThat(result.result.besFirmwareVersion).isEqualTo("new")
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@Test
|
|
117
|
+
fun duplicateChunkDoesNotCountAsMissingChunk() {
|
|
118
|
+
val accumulator = VersionInfoResponseAccumulator("request-1")
|
|
119
|
+
val first = chunk("version_info_1", "request-1", "buildNumber" to "42")
|
|
120
|
+
assertThat(accumulator.accept(first)).isEqualTo(VersionInfoAccumulatorOutcome.Waiting)
|
|
121
|
+
assertThat(accumulator.accept(first)).isEqualTo(VersionInfoAccumulatorOutcome.Waiting)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@Test
|
|
125
|
+
fun rejectsMalformedModernMetadataAndMixedProcess() {
|
|
126
|
+
val accumulator = VersionInfoResponseAccumulator("request-1")
|
|
127
|
+
val first = chunk("version_info_1", "request-1", "buildNumber" to "42")
|
|
128
|
+
for ((key, value) in listOf(
|
|
129
|
+
VersionInfoResponseAccumulator.RESPONSE_COUNT_KEY to 2.5,
|
|
130
|
+
VersionInfoResponseAccumulator.RESPONSE_INDEX_KEY to true,
|
|
131
|
+
VersionInfoResponseAccumulator.RESPONSE_FINAL_KEY to true,
|
|
132
|
+
VersionInfoResponseAccumulator.RESPONSE_REQUEST_ID_KEY to "",
|
|
133
|
+
)) {
|
|
134
|
+
assertThat(accumulator.accept(first + (key to value))).isEqualTo(VersionInfoAccumulatorOutcome.Ignored)
|
|
135
|
+
}
|
|
136
|
+
accumulator.accept(first)
|
|
137
|
+
val last = chunk("version_info_3", "request-1", "besFirmwareVersion" to "new")
|
|
138
|
+
assertThat(accumulator.accept(last + (VersionInfoResponseAccumulator.RESPONSE_SID_KEY to "process-2")))
|
|
139
|
+
.isEqualTo(VersionInfoAccumulatorOutcome.Ignored)
|
|
140
|
+
assertThat(accumulator.accept(last)).isInstanceOf(VersionInfoAccumulatorOutcome.Complete::class.java)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@Test
|
|
144
|
+
fun correlatedResponseWithoutCompletenessMetadataCannotComplete() {
|
|
145
|
+
val accumulator = VersionInfoResponseAccumulator("request-1")
|
|
146
|
+
val event = mapOf<String, Any>(
|
|
147
|
+
VersionInfoResponseAccumulator.RESPONSE_CHUNK_KEY to "version_info_3",
|
|
148
|
+
VersionInfoResponseAccumulator.RESPONSE_REQUEST_ID_KEY to "request-1",
|
|
149
|
+
)
|
|
150
|
+
assertThat(accumulator.accept(event)).isEqualTo(VersionInfoAccumulatorOutcome.Ignored)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private fun chunk(
|
|
154
|
+
type: String,
|
|
155
|
+
requestId: String?,
|
|
156
|
+
vararg values: Pair<String, Any>,
|
|
157
|
+
): Map<String, Any> =
|
|
158
|
+
buildMap {
|
|
159
|
+
put(VersionInfoResponseAccumulator.RESPONSE_CHUNK_KEY, type)
|
|
160
|
+
requestId?.let {
|
|
161
|
+
put(VersionInfoResponseAccumulator.RESPONSE_REQUEST_ID_KEY, it)
|
|
162
|
+
put(VersionInfoResponseAccumulator.RESPONSE_INDEX_KEY, if (type == "version_info_1") 1 else 2)
|
|
163
|
+
put(VersionInfoResponseAccumulator.RESPONSE_COUNT_KEY, 2)
|
|
164
|
+
put(VersionInfoResponseAccumulator.RESPONSE_FINAL_KEY, type == "version_info_3")
|
|
165
|
+
put(VersionInfoResponseAccumulator.RESPONSE_SID_KEY, "process-1")
|
|
166
|
+
}
|
|
167
|
+
putAll(values)
|
|
168
|
+
}
|
|
169
|
+
}
|