@mentra/bluetooth-sdk 3.2.1-beta.319 → 3.2.1-beta.355
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 +1 -1
- package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +368 -0
- package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocol.kt +156 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +67 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
- 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 +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +33 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/G2ConnectionRecovery.kt +47 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +64 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +73 -3
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +257 -395
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodec.kt +245 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinator.kt +225 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasEncoder.kt +58 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSession.kt +168 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/G2Text.kt +7 -0
- package/android/src/release/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +20 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +557 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/G2TextTest.kt +13 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2DisplayThreadTest.kt +120 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoBitmapAcceptanceTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodecTest.kt +220 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinatorTest.kt +308 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +270 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnosticsTest.kt +287 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocolTest.kt +127 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoLifecycleTest.kt +194 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +199 -0
- package/build/BluetoothSdk.types.d.ts +11 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +2 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +3 -0
- 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.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +5 -0
- package/ios/MentraBluetoothSDK.podspec +3 -2
- package/ios/Source/BluetoothSdkDefaults.swift +6 -1
- package/ios/Source/Bridge.swift +15 -2
- package/ios/Source/DeviceManager.swift +23 -10
- package/ios/Source/DeviceStore.swift +26 -7
- package/ios/Source/GeneratedChangelogCatalog.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +4 -0
- package/ios/Source/requests/VersionInfoResponseAccumulator.swift +11 -1
- package/ios/Source/sgcs/G2.swift +2 -2
- package/ios/Source/sgcs/MentraLive.swift +66 -3
- package/ios/Source/sgcs/Nimo.swift +182 -371
- package/ios/Source/sgcs/NimoCanvasCodec.swift +269 -0
- package/ios/Source/sgcs/NimoCanvasCoordinator.swift +219 -0
- package/ios/Source/sgcs/NimoCanvasImage.swift +53 -0
- package/ios/Source/sgcs/NimoCanvasSession.swift +150 -0
- package/ios/Source/sgcs/SGCManager.swift +24 -0
- package/ios/Source/utils/G2Text.swift +11 -0
- package/ios/Tests/DashboardRenderingTests.swift +31 -0
- package/ios/Tests/G2TextTests.swift +10 -0
- package/ios/Tests/NimoCanvasCodecTests.swift +204 -0
- package/ios/Tests/NimoCanvasSessionTests.swift +112 -0
- package/ios/Tests/NimoCanvasTransportTests.swift +161 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
- package/package.json +2 -2
- package/src/BluetoothSdk.types.ts +11 -0
- package/src/_private/BluetoothSdkModule.ts +6 -0
- package/src/generated/changelogCatalog.ts +1 -1
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +1 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import androidx.test.core.app.ApplicationProvider
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
import com.mentra.bluetoothsdk.sgcs.Nimo
|
|
6
|
+
import com.mentra.bluetoothsdk.sgcs.SGCManager
|
|
7
|
+
import com.mentra.bluetoothsdk.sgcs.SceneFrame
|
|
8
|
+
import com.mentra.bluetoothsdk.utils.DeviceTypes
|
|
9
|
+
import org.junit.Assert.*
|
|
10
|
+
import org.junit.Before
|
|
11
|
+
import org.junit.Test
|
|
12
|
+
import org.junit.runner.RunWith
|
|
13
|
+
import org.robolectric.RobolectricTestRunner
|
|
14
|
+
import org.robolectric.Shadows
|
|
15
|
+
import org.robolectric.annotation.Config
|
|
16
|
+
import org.robolectric.annotation.LooperMode
|
|
17
|
+
import java.util.concurrent.CopyOnWriteArrayList
|
|
18
|
+
import java.util.concurrent.CountDownLatch
|
|
19
|
+
import java.util.concurrent.ExecutorService
|
|
20
|
+
import java.util.concurrent.TimeUnit
|
|
21
|
+
|
|
22
|
+
@RunWith(RobolectricTestRunner::class)
|
|
23
|
+
@Config(sdk = [33])
|
|
24
|
+
class DeviceManagerSceneHandoffTest {
|
|
25
|
+
@Before fun setup() { Bridge.initialize(ApplicationProvider.getApplicationContext()) }
|
|
26
|
+
|
|
27
|
+
@Test @LooperMode(LooperMode.Mode.PAUSED)
|
|
28
|
+
fun nimoBrightnessChangesDoNotReplaceTheSelectedScene() {
|
|
29
|
+
for (headUp in listOf(false, true)) for ((key, value) in brightnessChanges()) {
|
|
30
|
+
withSharedRecordingDevice { manager, device ->
|
|
31
|
+
DeviceStore.set("glasses", "headUp", headUp)
|
|
32
|
+
manager.displayEvent(scene("main", "main-app"))
|
|
33
|
+
manager.displayEvent(scene("dashboard", "dashboard-app"))
|
|
34
|
+
device.calls.clear()
|
|
35
|
+
|
|
36
|
+
DeviceStore.apply("bluetooth", key, value)
|
|
37
|
+
Shadows.shadowOf(Looper.getMainLooper()).idleFor(1, TimeUnit.SECONDS)
|
|
38
|
+
|
|
39
|
+
assertEquals(listOf(if (key == "brightness") "65:true" else "50:false"), device.brightnessCalls)
|
|
40
|
+
assertTrue("Changing $key must not replace or clear the NIMO canvas: ${device.calls}", device.calls.isEmpty())
|
|
41
|
+
manager.sendCurrentState()
|
|
42
|
+
assertEquals(listOf("scene:${if (headUp) "dashboard-app" else "main-app"}:true"), device.calls)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Test @LooperMode(LooperMode.Mode.PAUSED)
|
|
48
|
+
fun repeatedNimoBrightnessChangesDoNotLeaveTimersThatEraseNewerFrames() {
|
|
49
|
+
withSharedRecordingDevice { manager, device ->
|
|
50
|
+
DeviceStore.set("glasses", "headUp", false)
|
|
51
|
+
manager.displayEvent(scene("main", "old"))
|
|
52
|
+
device.calls.clear()
|
|
53
|
+
DeviceStore.apply("bluetooth", "brightness", 65)
|
|
54
|
+
Shadows.shadowOf(Looper.getMainLooper()).idleFor(400, TimeUnit.MILLISECONDS)
|
|
55
|
+
manager.displayEvent(scene("main", "latest"))
|
|
56
|
+
DeviceStore.apply("bluetooth", "auto_brightness", false)
|
|
57
|
+
Shadows.shadowOf(Looper.getMainLooper()).idleFor(400, TimeUnit.MILLISECONDS)
|
|
58
|
+
DeviceStore.apply("bluetooth", "auto_brightness", true)
|
|
59
|
+
Shadows.shadowOf(Looper.getMainLooper()).idleFor(1, TimeUnit.SECONDS)
|
|
60
|
+
|
|
61
|
+
assertEquals(listOf("65:true", "65:false", "65:true"), device.brightnessCalls)
|
|
62
|
+
assertEquals(listOf("scene:latest:true"), device.calls)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Test @LooperMode(LooperMode.Mode.PAUSED)
|
|
67
|
+
fun nimoBrightnessChangesDoNotWakeSuppressedDisplays() {
|
|
68
|
+
for (gate in listOf("screen-disabled", "not-ready", "no-device")) {
|
|
69
|
+
for ((key, value) in brightnessChanges()) withSharedRecordingDevice { manager, device ->
|
|
70
|
+
when (gate) {
|
|
71
|
+
"screen-disabled" -> DeviceStore.set("bluetooth", "screen_disabled", true)
|
|
72
|
+
"not-ready" -> DeviceStore.set("glasses", "fullyBooted", false)
|
|
73
|
+
"no-device" -> manager.sgc = null
|
|
74
|
+
}
|
|
75
|
+
DeviceStore.apply("bluetooth", key, value)
|
|
76
|
+
Shadows.shadowOf(Looper.getMainLooper()).idleFor(1, TimeUnit.SECONDS)
|
|
77
|
+
assertTrue("Unexpected display mutation with $gate", device.calls.isEmpty())
|
|
78
|
+
assertEquals(if (gate == "no-device") 0 else 1, device.brightnessCalls.size)
|
|
79
|
+
manager.sgc = device
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Test @LooperMode(LooperMode.Mode.PAUSED)
|
|
85
|
+
fun defaultDevicesKeepTheirBrightnessConfirmation() {
|
|
86
|
+
for ((key, value) in brightnessChanges()) {
|
|
87
|
+
withSharedRecordingDevice(RecordingSGC(true)) { _, device ->
|
|
88
|
+
DeviceStore.apply("bluetooth", key, value)
|
|
89
|
+
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
|
90
|
+
val confirmation = if (key == "brightness") "Set brightness to 65%" else "Disabled auto brightness"
|
|
91
|
+
assertEquals(listOf("text:$confirmation"), device.calls)
|
|
92
|
+
Shadows.shadowOf(Looper.getMainLooper()).idleFor(799, TimeUnit.MILLISECONDS)
|
|
93
|
+
assertEquals(listOf("text:$confirmation"), device.calls)
|
|
94
|
+
Shadows.shadowOf(Looper.getMainLooper()).idleFor(1, TimeUnit.MILLISECONDS)
|
|
95
|
+
assertEquals(listOf("text:$confirmation", "clear"), device.calls)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@Test @LooperMode(LooperMode.Mode.PAUSED)
|
|
101
|
+
fun oldBrightnessConfirmationDoesNotClearAReplacementDevice() {
|
|
102
|
+
for ((key, value) in brightnessChanges()) {
|
|
103
|
+
withSharedRecordingDevice(RecordingSGC(true)) { manager, device ->
|
|
104
|
+
DeviceStore.apply("bluetooth", key, value)
|
|
105
|
+
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
|
106
|
+
assertEquals(1, device.calls.size)
|
|
107
|
+
val replacement = NimoRecordingSGC()
|
|
108
|
+
manager.sgc = replacement
|
|
109
|
+
Shadows.shadowOf(Looper.getMainLooper()).idleFor(1, TimeUnit.SECONDS)
|
|
110
|
+
assertEquals("The disconnected device must not be cleared either", 1, device.calls.size)
|
|
111
|
+
assertTrue("A previous device's timer must not touch NIMO", replacement.calls.isEmpty())
|
|
112
|
+
manager.sgc = device
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@Test @LooperMode(LooperMode.Mode.PAUSED)
|
|
118
|
+
fun unchangedBrightnessSettingsDoNotDispatchForEitherPolicy() {
|
|
119
|
+
for (device in listOf(RecordingSGC(true), NimoRecordingSGC())) {
|
|
120
|
+
withSharedRecordingDevice(device) { _, current ->
|
|
121
|
+
DeviceStore.apply("bluetooth", "brightness", 50)
|
|
122
|
+
DeviceStore.apply("bluetooth", "auto_brightness", true)
|
|
123
|
+
Shadows.shadowOf(Looper.getMainLooper()).idleFor(1, TimeUnit.SECONDS)
|
|
124
|
+
assertTrue(current.brightnessCalls.isEmpty())
|
|
125
|
+
assertTrue(current.calls.isEmpty())
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@Test fun nimoOptsOutOfBrightnessConfirmationIndependentlyOfSceneHandoffPolicy() {
|
|
131
|
+
val nimo = Nimo()
|
|
132
|
+
try {
|
|
133
|
+
assertFalse(nimo.showBrightnessConfirmation)
|
|
134
|
+
assertTrue(RecordingSGC(false).showBrightnessConfirmation)
|
|
135
|
+
assertTrue(RecordingSGC(true).showBrightnessConfirmation)
|
|
136
|
+
} finally { nimo.cleanup() }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private fun brightnessChanges(): List<Pair<String, Any>> =
|
|
140
|
+
listOf("brightness" to 65, "auto_brightness" to false)
|
|
141
|
+
|
|
142
|
+
@Test @LooperMode(LooperMode.Mode.PAUSED)
|
|
143
|
+
fun nimoReconnectDoesNotEraseReplayedMainSceneAfterReady() {
|
|
144
|
+
assertNimoReconnectPreservesReplayedScene(headUp = false)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@Test @LooperMode(LooperMode.Mode.PAUSED)
|
|
148
|
+
fun nimoReconnectDoesNotEraseReplayedDashboardSceneAfterReady() {
|
|
149
|
+
assertNimoReconnectPreservesReplayedScene(headUp = true)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@Test @LooperMode(LooperMode.Mode.PAUSED)
|
|
153
|
+
fun nimoReadyWithoutASceneDoesNotScheduleAWelcomeOrClear() {
|
|
154
|
+
withReadyRecordingDevice { manager, _, executor ->
|
|
155
|
+
manager.disconnect()
|
|
156
|
+
val replacement = NimoRecordingSGC()
|
|
157
|
+
manager.sgc = replacement
|
|
158
|
+
DeviceStore.apply("glasses", "fullyBooted", true)
|
|
159
|
+
executor.submit {}.get(5, TimeUnit.SECONDS)
|
|
160
|
+
Shadows.shadowOf(Looper.getMainLooper()).idleFor(4, TimeUnit.SECONDS)
|
|
161
|
+
assertTrue(replacement.calls.isEmpty())
|
|
162
|
+
assertEquals(false, DeviceStore.get("bluetooth", "shouldSendBootingMessage"))
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@Test @LooperMode(LooperMode.Mode.PAUSED)
|
|
167
|
+
fun defaultDeviceReadyKeepsItsThreeSecondWelcome() {
|
|
168
|
+
withReadyRecordingDevice { manager, _, executor ->
|
|
169
|
+
manager.disconnect()
|
|
170
|
+
val replacement = RecordingSGC(true)
|
|
171
|
+
manager.sgc = replacement
|
|
172
|
+
DeviceStore.apply("glasses", "fullyBooted", true)
|
|
173
|
+
executor.submit {}.get(5, TimeUnit.SECONDS)
|
|
174
|
+
Shadows.shadowOf(Looper.getMainLooper()).idleFor(4, TimeUnit.SECONDS)
|
|
175
|
+
assertEquals(listOf("text:// MentraOS Connected", "clear"), replacement.calls)
|
|
176
|
+
assertTrue("Welcome must remain for at least three seconds",
|
|
177
|
+
replacement.lastClearNanos - replacement.lastTextNanos >= TimeUnit.SECONDS.toNanos(3))
|
|
178
|
+
assertEquals(false, DeviceStore.get("bluetooth", "shouldSendBootingMessage"))
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@Test @LooperMode(LooperMode.Mode.PAUSED)
|
|
183
|
+
fun queuedWelcomeDoesNotReachAReplacementCommunicator() {
|
|
184
|
+
assertTrue("The welcome executor must observe communicator replacement writes",
|
|
185
|
+
java.lang.reflect.Modifier.isVolatile(DeviceManager::class.java.getDeclaredField("sgc").modifiers))
|
|
186
|
+
withReadyRecordingDevice { manager, _, executor ->
|
|
187
|
+
manager.disconnect()
|
|
188
|
+
val original = RecordingSGC(true)
|
|
189
|
+
manager.sgc = original
|
|
190
|
+
val executorEntered = CountDownLatch(1)
|
|
191
|
+
val releaseExecutor = CountDownLatch(1)
|
|
192
|
+
executor.execute { executorEntered.countDown(); releaseExecutor.await(5, TimeUnit.SECONDS) }
|
|
193
|
+
assertTrue(executorEntered.await(1, TimeUnit.SECONDS))
|
|
194
|
+
try {
|
|
195
|
+
DeviceStore.apply("glasses", "fullyBooted", true)
|
|
196
|
+
val replacement = NimoRecordingSGC()
|
|
197
|
+
manager.sgc = replacement
|
|
198
|
+
releaseExecutor.countDown()
|
|
199
|
+
executor.submit {}.get(5, TimeUnit.SECONDS)
|
|
200
|
+
Shadows.shadowOf(Looper.getMainLooper()).idleFor(4, TimeUnit.SECONDS)
|
|
201
|
+
assertTrue("The retired communicator must not receive a queued welcome", original.calls.isEmpty())
|
|
202
|
+
assertTrue("The replacement must not receive another communicator's welcome", replacement.calls.isEmpty())
|
|
203
|
+
} finally { releaseExecutor.countDown() }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
@Test @LooperMode(LooperMode.Mode.PAUSED)
|
|
208
|
+
fun welcomeClearDoesNotEraseAReplacementCommunicatorsScene() {
|
|
209
|
+
withReadyRecordingDevice { manager, _, executor ->
|
|
210
|
+
manager.disconnect()
|
|
211
|
+
val original = RecordingSGC(true)
|
|
212
|
+
manager.sgc = original
|
|
213
|
+
DeviceStore.apply("glasses", "fullyBooted", true)
|
|
214
|
+
assertTrue(original.textSent.await(1, TimeUnit.SECONDS))
|
|
215
|
+
val replacement = NimoRecordingSGC()
|
|
216
|
+
manager.sgc = replacement
|
|
217
|
+
manager.displayEvent(scene("main", "replacement-app", replay = true))
|
|
218
|
+
executor.submit {}.get(5, TimeUnit.SECONDS)
|
|
219
|
+
Shadows.shadowOf(Looper.getMainLooper()).idleFor(4, TimeUnit.SECONDS)
|
|
220
|
+
assertEquals(listOf("text:// MentraOS Connected"), original.calls)
|
|
221
|
+
assertEquals(listOf("scene:replacement-app:true"), replacement.calls)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@Test fun nimoOptsOutOfConnectionConfirmationIndependentlyOfOtherPolicies() {
|
|
226
|
+
val nimo = Nimo()
|
|
227
|
+
try {
|
|
228
|
+
assertFalse(nimo.showConnectionConfirmation)
|
|
229
|
+
assertTrue(RecordingSGC(false).showConnectionConfirmation)
|
|
230
|
+
assertTrue(RecordingSGC(true).showConnectionConfirmation)
|
|
231
|
+
assertTrue(object : RecordingSGC(false) {
|
|
232
|
+
override val showBrightnessConfirmation = false
|
|
233
|
+
}.showConnectionConfirmation)
|
|
234
|
+
} finally { nimo.cleanup() }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private fun assertNimoReconnectPreservesReplayedScene(headUp: Boolean) {
|
|
238
|
+
withReadyRecordingDevice { manager, original, executor ->
|
|
239
|
+
DeviceStore.set("glasses", "headUp", headUp)
|
|
240
|
+
manager.displayEvent(scene("main", "main-app"))
|
|
241
|
+
manager.displayEvent(scene("dashboard", "dashboard-app"))
|
|
242
|
+
val selectedView = if (headUp) "dashboard" else "main"
|
|
243
|
+
val selectedApp = "$selectedView-app"
|
|
244
|
+
val selectedScene = "scene:$selectedApp:true"
|
|
245
|
+
assertEquals("scene:$selectedApp:false", original.calls.last())
|
|
246
|
+
original.calls.clear()
|
|
247
|
+
|
|
248
|
+
// Use the public disconnect, not a manually armed boot flag.
|
|
249
|
+
manager.disconnect()
|
|
250
|
+
assertEquals(listOf("clear", "disconnect"), original.calls)
|
|
251
|
+
assertNull(manager.sgc)
|
|
252
|
+
assertEquals(true, DeviceStore.get("bluetooth", "shouldSendBootingMessage"))
|
|
253
|
+
val replacement = NimoRecordingSGC()
|
|
254
|
+
manager.sgc = replacement
|
|
255
|
+
// Disconnect resets headUp; restore the selected orientation
|
|
256
|
+
// before readiness without injecting a new miniapp frame.
|
|
257
|
+
DeviceStore.set("glasses", "headUp", headUp)
|
|
258
|
+
DeviceStore.apply("glasses", "fullyBooted", true)
|
|
259
|
+
// Model LocalDisplayManager's connected-edge replay through
|
|
260
|
+
// its actual native ingress. The miniapp content is unchanged;
|
|
261
|
+
// the host sends a fresh epoch with all elements created.
|
|
262
|
+
manager.displayEvent(scene(selectedView, selectedApp, replay = true, epoch = 2))
|
|
263
|
+
|
|
264
|
+
// The old welcome slept on this real executor. A submitted
|
|
265
|
+
// barrier waits for any such task; paused Main alone cannot.
|
|
266
|
+
executor.submit {}.get(5, TimeUnit.SECONDS)
|
|
267
|
+
Shadows.shadowOf(Looper.getMainLooper()).idleFor(4, TimeUnit.SECONDS)
|
|
268
|
+
val readyCalls = replacement.calls.toList()
|
|
269
|
+
|
|
270
|
+
// Positive control: the scene remains retained for later replay.
|
|
271
|
+
replacement.calls.clear()
|
|
272
|
+
manager.sendCurrentState()
|
|
273
|
+
assertEquals(listOf(selectedScene), replacement.calls)
|
|
274
|
+
assertEquals("Reconnect must not erase the host replay: $readyCalls", selectedScene, readyCalls.last())
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private fun withReadyRecordingDevice(block: (DeviceManager, RecordingSGC, ExecutorService) -> Unit) {
|
|
279
|
+
// Disconnect and readiness also mutate identity/mic state. Restore both
|
|
280
|
+
// categories, including keys that were absent before this test.
|
|
281
|
+
val saved = listOf("bluetooth", "glasses").associateWith { DeviceStore.store.getCategory(it) }
|
|
282
|
+
try {
|
|
283
|
+
withSharedRecordingDevice { manager, original ->
|
|
284
|
+
val executor = DeviceManager::class.java.getDeclaredField("executor").run {
|
|
285
|
+
isAccessible = true
|
|
286
|
+
get(manager) as ExecutorService
|
|
287
|
+
}
|
|
288
|
+
try { block(manager, original, executor) } finally {
|
|
289
|
+
executor.shutdownNow()
|
|
290
|
+
assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS))
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} finally {
|
|
294
|
+
saved.forEach { (category, values) ->
|
|
295
|
+
(DeviceStore.store.getCategory(category).keys - values.keys).forEach {
|
|
296
|
+
DeviceStore.store.remove(category, it)
|
|
297
|
+
}
|
|
298
|
+
values.forEach { (key, value) -> DeviceStore.set(category, key, value) }
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
@Test @LooperMode(LooperMode.Mode.PAUSED)
|
|
304
|
+
fun contextualDashboardToggleReplaysSelectedViewWithoutNewMiniappEvent() {
|
|
305
|
+
withSharedRecordingDevice { manager, device ->
|
|
306
|
+
manager.displayEvent(scene("main", "main-app"))
|
|
307
|
+
manager.displayEvent(scene("dashboard", "dashboard-app"))
|
|
308
|
+
device.calls.clear()
|
|
309
|
+
|
|
310
|
+
// Settings arrive from Expo's worker queue, not necessarily Main.
|
|
311
|
+
val worker = Thread { DeviceStore.apply("bluetooth", "contextual_dashboard", false) }
|
|
312
|
+
worker.start(); worker.join(1000)
|
|
313
|
+
assertFalse(worker.isAlive)
|
|
314
|
+
assertTrue("Dispatch must wait for Main", device.calls.isEmpty())
|
|
315
|
+
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
|
316
|
+
assertEquals(listOf("scene:main-app:true"), device.calls)
|
|
317
|
+
|
|
318
|
+
device.calls.clear()
|
|
319
|
+
DeviceStore.apply("bluetooth", "contextual_dashboard", true)
|
|
320
|
+
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
|
321
|
+
assertEquals(listOf("scene:dashboard-app:true"), device.calls)
|
|
322
|
+
device.calls.clear()
|
|
323
|
+
DeviceStore.apply("bluetooth", "contextual_dashboard", true)
|
|
324
|
+
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
|
325
|
+
assertTrue("An unchanged setting must not redraw", device.calls.isEmpty())
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
@Test @LooperMode(LooperMode.Mode.PAUSED)
|
|
330
|
+
fun contextualDashboardToggleRetainsHeadDownAndDisplaySuppressionGuards() {
|
|
331
|
+
for (gate in listOf("head-down", "screen-disabled", "not-ready", "no-device")) {
|
|
332
|
+
withSharedRecordingDevice { manager, device ->
|
|
333
|
+
manager.displayEvent(scene("main", "main-app"))
|
|
334
|
+
manager.displayEvent(scene("dashboard", "dashboard-app"))
|
|
335
|
+
device.calls.clear()
|
|
336
|
+
when (gate) {
|
|
337
|
+
"head-down" -> DeviceStore.set("glasses", "headUp", false)
|
|
338
|
+
"screen-disabled" -> DeviceStore.set("bluetooth", "screen_disabled", true)
|
|
339
|
+
"not-ready" -> DeviceStore.set("glasses", "fullyBooted", false)
|
|
340
|
+
"no-device" -> manager.sgc = null
|
|
341
|
+
}
|
|
342
|
+
DeviceStore.apply("bluetooth", "contextual_dashboard", false)
|
|
343
|
+
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
|
344
|
+
assertTrue("Unexpected dispatch with $gate", device.calls.isEmpty())
|
|
345
|
+
manager.sgc = device
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
@Test @LooperMode(LooperMode.Mode.PAUSED)
|
|
351
|
+
fun deferredDashboardHandoffRespectsFullFramePolicy() {
|
|
352
|
+
for (requiresClear in listOf(false, true)) {
|
|
353
|
+
withSharedRecordingDevice(RecordingSGC(requiresClear)) { manager, device ->
|
|
354
|
+
manager.displayEvent(scene("dashboard", "dashboard-app"))
|
|
355
|
+
device.calls.clear()
|
|
356
|
+
manager.setDashboardContent("new dashboard")
|
|
357
|
+
assertEquals(requiresClear, device.calls.any { it.startsWith("remove:") })
|
|
358
|
+
assertTrue(device.calls.last().endsWith("new dashboard"))
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private fun withSharedRecordingDevice(
|
|
364
|
+
device: RecordingSGC = NimoRecordingSGC(),
|
|
365
|
+
block: (DeviceManager, RecordingSGC) -> Unit
|
|
366
|
+
) {
|
|
367
|
+
val singleton = DeviceManager::class.java.getDeclaredField("_instance").apply { isAccessible = true }
|
|
368
|
+
val previous = singleton.get(null)
|
|
369
|
+
val keys = listOf("glasses" to "fullyBooted", "glasses" to "headUp",
|
|
370
|
+
"bluetooth" to "contextual_dashboard", "bluetooth" to "screen_disabled",
|
|
371
|
+
"bluetooth" to "brightness", "bluetooth" to "auto_brightness")
|
|
372
|
+
val saved = keys.associateWith { DeviceStore.get(it.first, it.second)!! }
|
|
373
|
+
val manager = DeviceManager(initializeHardware = false)
|
|
374
|
+
manager.sgc = device
|
|
375
|
+
singleton.set(null, manager)
|
|
376
|
+
DeviceStore.set("glasses", "fullyBooted", true)
|
|
377
|
+
DeviceStore.set("glasses", "headUp", true)
|
|
378
|
+
DeviceStore.set("bluetooth", "contextual_dashboard", true)
|
|
379
|
+
DeviceStore.set("bluetooth", "screen_disabled", false)
|
|
380
|
+
DeviceStore.set("bluetooth", "brightness", 50)
|
|
381
|
+
DeviceStore.set("bluetooth", "auto_brightness", true)
|
|
382
|
+
try { block(manager, device) } finally {
|
|
383
|
+
DeviceStore.set("glasses", "fullyBooted", false)
|
|
384
|
+
manager.cleanup()
|
|
385
|
+
singleton.set(null, previous)
|
|
386
|
+
saved.forEach { (key, value) -> DeviceStore.set(key.first, key.second, value) }
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
@Test fun actualDisplayEventHandoffsRespectBothViewsAndDevicePolicy() {
|
|
391
|
+
for (clearRequired in listOf(false, true)) for (headUp in listOf(false, true)) {
|
|
392
|
+
for (contextual in listOf(false, true)) for (view in listOf("main", "dashboard")) {
|
|
393
|
+
val manager = DeviceManager(initializeHardware = false)
|
|
394
|
+
val device = RecordingSGC(clearRequired)
|
|
395
|
+
manager.sgc = device
|
|
396
|
+
DeviceStore.set("glasses", "fullyBooted", true)
|
|
397
|
+
DeviceStore.set("glasses", "headUp", headUp)
|
|
398
|
+
DeviceStore.set("bluetooth", "contextual_dashboard", contextual)
|
|
399
|
+
DeviceStore.set("bluetooth", "screen_disabled", false)
|
|
400
|
+
val visible = (view == "dashboard") == (headUp && contextual)
|
|
401
|
+
|
|
402
|
+
manager.displayEvent(legacy(view, "legacy-a"))
|
|
403
|
+
device.calls.clear()
|
|
404
|
+
manager.displayEvent(scene(view, "one"))
|
|
405
|
+
assertEquals(if (!visible) emptyList() else
|
|
406
|
+
(if (clearRequired) listOf("clear") else emptyList()) + "scene:one:false", device.calls)
|
|
407
|
+
|
|
408
|
+
device.calls.clear()
|
|
409
|
+
manager.displayEvent(scene(view, "two"))
|
|
410
|
+
assertEquals(if (!visible) emptyList() else
|
|
411
|
+
(if (clearRequired) listOf("remove:one-label") else emptyList()) + "scene:two:true", device.calls)
|
|
412
|
+
|
|
413
|
+
device.calls.clear()
|
|
414
|
+
manager.displayEvent(legacy(view, "legacy-b"))
|
|
415
|
+
assertEquals(if (!visible) emptyList() else
|
|
416
|
+
(if (clearRequired) listOf("remove:two-label") else emptyList()) + "text:legacy-b", device.calls)
|
|
417
|
+
|
|
418
|
+
// Hidden events update their real replay slot, not the visible display.
|
|
419
|
+
device.calls.clear()
|
|
420
|
+
DeviceStore.set("glasses", "headUp", view == "dashboard")
|
|
421
|
+
DeviceStore.set("bluetooth", "contextual_dashboard", true)
|
|
422
|
+
manager.sendCurrentState()
|
|
423
|
+
assertEquals(listOf("text:legacy-b"), device.calls)
|
|
424
|
+
DeviceStore.set("glasses", "fullyBooted", false)
|
|
425
|
+
manager.cleanup()
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
@Test fun suppressedDisplaysNeitherClearNorDispatchButRetainSceneReplay() {
|
|
431
|
+
for (suppression in listOf("screen", "boot", "simulated")) {
|
|
432
|
+
val manager = DeviceManager(initializeHardware = false)
|
|
433
|
+
val device = RecordingSGC(true)
|
|
434
|
+
manager.sgc = device
|
|
435
|
+
DeviceStore.set("glasses", "fullyBooted", suppression != "boot")
|
|
436
|
+
DeviceStore.set("glasses", "headUp", false)
|
|
437
|
+
DeviceStore.set("bluetooth", "contextual_dashboard", true)
|
|
438
|
+
DeviceStore.set("bluetooth", "screen_disabled", suppression == "screen")
|
|
439
|
+
if (suppression == "simulated") device.type = DeviceTypes.SIMULATED
|
|
440
|
+
manager.displayEvent(legacy("main", "legacy"))
|
|
441
|
+
manager.displayEvent(scene("main", "one"))
|
|
442
|
+
manager.displayEvent(scene("main", "two"))
|
|
443
|
+
assertTrue(device.calls.isEmpty())
|
|
444
|
+
DeviceStore.set("glasses", "fullyBooted", true)
|
|
445
|
+
DeviceStore.set("bluetooth", "screen_disabled", false)
|
|
446
|
+
device.type = "Clear-required glasses"
|
|
447
|
+
manager.sendCurrentState()
|
|
448
|
+
assertEquals(listOf("scene:two:true"), device.calls)
|
|
449
|
+
DeviceStore.set("glasses", "fullyBooted", false)
|
|
450
|
+
manager.cleanup()
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
@Test fun clearViewDoesNotSweepSceneTwiceAndOtherDeviceDisconnectStaysUnchanged() {
|
|
455
|
+
val manager = DeviceManager(initializeHardware = false)
|
|
456
|
+
val device = RecordingSGC(true)
|
|
457
|
+
manager.sgc = device
|
|
458
|
+
DeviceStore.set("glasses", "fullyBooted", true)
|
|
459
|
+
DeviceStore.set("glasses", "headUp", false)
|
|
460
|
+
DeviceStore.set("bluetooth", "screen_disabled", false)
|
|
461
|
+
manager.displayEvent(scene("main", "one"))
|
|
462
|
+
device.calls.clear()
|
|
463
|
+
manager.displayEvent(mapOf("view" to "main", "layout" to mapOf("layoutType" to "clear_view")))
|
|
464
|
+
assertEquals(listOf("clear"), device.calls)
|
|
465
|
+
DeviceStore.set("glasses", "fullyBooted", false)
|
|
466
|
+
device.calls.clear()
|
|
467
|
+
manager.disconnect()
|
|
468
|
+
assertEquals(listOf("clear", "disconnect"), device.calls)
|
|
469
|
+
assertNull(manager.sgc)
|
|
470
|
+
manager.cleanup()
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
@Test fun ordinaryNimoDisconnectStopsItsEncoderThread() {
|
|
474
|
+
val manager = DeviceManager(initializeHardware = false)
|
|
475
|
+
val before = Thread.getAllStackTraces().keys
|
|
476
|
+
val nimo = Nimo()
|
|
477
|
+
manager.sgc = nimo
|
|
478
|
+
val encoder = Thread.getAllStackTraces().keys.single { it.name == "NimoCanvasEncoder" && it !in before }
|
|
479
|
+
try {
|
|
480
|
+
DeviceStore.set("glasses", "fullyBooted", false)
|
|
481
|
+
manager.disconnect()
|
|
482
|
+
encoder.join(1000)
|
|
483
|
+
assertFalse("Discarded communicator left an encoder thread alive", encoder.isAlive)
|
|
484
|
+
assertNull(manager.sgc)
|
|
485
|
+
} finally { nimo.cleanup(); manager.cleanup() }
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private fun legacy(view: String, text: String): Map<String, Any> =
|
|
489
|
+
mapOf("view" to view, "layout" to mapOf("layoutType" to "text_wall", "text" to text))
|
|
490
|
+
|
|
491
|
+
private fun scene(view: String, app: String, replay: Boolean = false, epoch: Int = 1): Map<String, Any> = mapOf("view" to view, "scene" to mapOf(
|
|
492
|
+
"appId" to app, "sceneEpoch" to epoch, "replay" to replay, "elements" to listOf(mapOf(
|
|
493
|
+
"id" to "$app-label", "type" to "text", "text" to app, "change" to "created",
|
|
494
|
+
"box" to mapOf("x" to 0, "y" to 0, "w" to 100, "h" to 20)))))
|
|
495
|
+
|
|
496
|
+
private class NimoRecordingSGC : RecordingSGC(false) {
|
|
497
|
+
override val showBrightnessConfirmation = false
|
|
498
|
+
override val showConnectionConfirmation = false
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private open class RecordingSGC(override val sceneHandoffRequiresClear: Boolean) : SGCManager() {
|
|
502
|
+
val calls = CopyOnWriteArrayList<String>()
|
|
503
|
+
val brightnessCalls = mutableListOf<String>()
|
|
504
|
+
val textSent = CountDownLatch(1)
|
|
505
|
+
var lastTextNanos = 0L
|
|
506
|
+
var lastClearNanos = 0L
|
|
507
|
+
init { type = if (sceneHandoffRequiresClear) "Clear-required glasses" else DeviceTypes.NIMO }
|
|
508
|
+
override fun clearDisplay() { lastClearNanos = System.nanoTime(); calls += "clear" }
|
|
509
|
+
override fun clearSceneElements(elementIds: List<String>) { calls += "remove:${elementIds.joinToString()}" }
|
|
510
|
+
override fun applySceneFrame(frame: SceneFrame) { calls += "scene:${frame.appId}:${frame.replay}" }
|
|
511
|
+
override fun sendTextWall(text: String) { lastTextNanos = System.nanoTime(); calls += "text:$text"; textSent.countDown() }
|
|
512
|
+
override fun disconnect() { calls += "disconnect" }
|
|
513
|
+
override fun cleanup() { calls += "cleanup" }
|
|
514
|
+
override fun setMicEnabled(enabled: Boolean) {}
|
|
515
|
+
override fun sortMicRanking(list: MutableList<String>) = list
|
|
516
|
+
override fun requestPhoto(request: PhotoRequest) {}
|
|
517
|
+
override fun startStream(message: MutableMap<String, Any>) {}
|
|
518
|
+
override fun stopStream() {}
|
|
519
|
+
override fun sendStreamKeepAlive(message: MutableMap<String, Any>) {}
|
|
520
|
+
override fun startVideoRecording(requestId: String, save: Boolean, sound: Boolean) {}
|
|
521
|
+
override fun stopVideoRecording(requestId: String) {}
|
|
522
|
+
override fun sendButtonPhotoSettings() {}
|
|
523
|
+
override fun sendButtonVideoRecordingSettings() {}
|
|
524
|
+
override fun sendButtonMaxRecordingTime() {}
|
|
525
|
+
override fun sendCameraFovSetting() {}
|
|
526
|
+
override fun setBrightness(level: Int, autoMode: Boolean) { brightnessCalls += "$level:$autoMode" }
|
|
527
|
+
override fun sendText(text: String) {}
|
|
528
|
+
override fun sendDoubleTextWall(top: String, bottom: String) {}
|
|
529
|
+
override fun displayBitmap(base64ImageData: String, x: Int?, y: Int?, width: Int?, height: Int?) = true
|
|
530
|
+
override fun showDashboard() {}
|
|
531
|
+
override fun setDashboardPosition(height: Int, depth: Int) {}
|
|
532
|
+
override fun setHeadUpAngle(angle: Int) {}
|
|
533
|
+
override fun getBatteryStatus() {}
|
|
534
|
+
override fun setSilentMode(enabled: Boolean) {}
|
|
535
|
+
override fun exit() {}
|
|
536
|
+
override fun sendShutdown() {}
|
|
537
|
+
override fun sendReboot() {}
|
|
538
|
+
override fun sendRgbLedControl(requestId: String, packageName: String?, action: String, color: String?, onDurationMs: Int, offDurationMs: Int, count: Int) {}
|
|
539
|
+
override fun forget() {}
|
|
540
|
+
override fun findCompatibleDevices() {}
|
|
541
|
+
override fun stopScan() {}
|
|
542
|
+
override fun connectById(id: String) {}
|
|
543
|
+
override fun getConnectedBluetoothName() = ""
|
|
544
|
+
override fun ping() {}
|
|
545
|
+
override fun dbg1() {}
|
|
546
|
+
override fun dbg2() {}
|
|
547
|
+
override fun requestWifiScan(scanId: String?) {}
|
|
548
|
+
override fun sendWifiCredentials(ssid: String, password: String) {}
|
|
549
|
+
override fun forgetWifiNetwork(ssid: String) {}
|
|
550
|
+
override fun sendHotspotState(enabled: Boolean) {}
|
|
551
|
+
override fun sendUserEmailToGlasses(email: String) {}
|
|
552
|
+
override fun sendIncidentId(incidentId: String, apiBaseUrl: String?) {}
|
|
553
|
+
override fun queryGalleryStatus() {}
|
|
554
|
+
override fun sendGalleryMode() {}
|
|
555
|
+
override fun requestVersionInfo() {}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import com.mentra.bluetoothsdk.utils.G2Text
|
|
4
|
+
import org.junit.Assert.assertEquals
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
class G2TextTest {
|
|
8
|
+
@Test fun blankRowsAreEncodedOnlyAtTheDeviceBoundary() {
|
|
9
|
+
assertEquals("hello\n\u200B\nworld\n\u200B", G2Text.containerContent("hello\n\nworld\n"))
|
|
10
|
+
assertEquals(" ", G2Text.containerContent(""))
|
|
11
|
+
assertEquals("hello\nworld", G2Text.containerContent("hello\nworld"))
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -102,6 +102,77 @@ class VersionInfoResponseAccumulatorTest {
|
|
|
102
102
|
.isEqualTo(VersionInfoAccumulatorOutcome.Waiting)
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
@Test
|
|
106
|
+
fun factoryAsg27CompletesAfterItsTwoChunks() {
|
|
107
|
+
// Exact January wire shape after Bridge normalization; firmware can be unknown.
|
|
108
|
+
for (firmware in listOf("", "17.26.1.13")) {
|
|
109
|
+
val accumulator = VersionInfoResponseAccumulator("request-1")
|
|
110
|
+
accumulator.accept(chunk("version_info_1", null, "buildNumber" to "27", "appVersion" to "27.0"))
|
|
111
|
+
val complete = accumulator.accept(chunk("version_info_2", null,
|
|
112
|
+
"buildNumber" to "", "otaVersionUrl" to "https://ota.example/live_version.json", "firmwareVersion" to firmware,
|
|
113
|
+
)) as VersionInfoAccumulatorOutcome.Complete
|
|
114
|
+
assertThat(complete.result.buildNumber).isEqualTo("27")
|
|
115
|
+
assertThat(complete.result.otaVersionUrl).isEqualTo("https://ota.example/live_version.json")
|
|
116
|
+
assertThat(complete.result.firmwareVersion).isEqualTo(firmware)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@Test
|
|
121
|
+
fun otherLegacyBuildsStillRequireTheirThirdChunk() {
|
|
122
|
+
for (build in listOf("", "26", "28", "31", "37", "42")) {
|
|
123
|
+
val accumulator = VersionInfoResponseAccumulator("request-1")
|
|
124
|
+
accumulator.accept(chunk("version_info_1", null, "buildNumber" to build))
|
|
125
|
+
assertThat(accumulator.accept(chunk("version_info_2", null,
|
|
126
|
+
"otaVersionUrl" to "https://ota.example/version.json",
|
|
127
|
+
))).isEqualTo(VersionInfoAccumulatorOutcome.Waiting)
|
|
128
|
+
assertThat(accumulator.accept(chunk("version_info_3", null, "besFirmwareVersion" to "new")))
|
|
129
|
+
.isInstanceOf(VersionInfoAccumulatorOutcome.Complete::class.java)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@Test
|
|
134
|
+
fun factorySecondChunkCannotCompleteWithoutFirstChunkAndOtaUrl() {
|
|
135
|
+
val accumulator = VersionInfoResponseAccumulator("request-1")
|
|
136
|
+
val second = chunk("version_info_2", null, "otaVersionUrl" to "https://ota.example/version.json")
|
|
137
|
+
assertThat(accumulator.accept(second)).isEqualTo(VersionInfoAccumulatorOutcome.Ignored)
|
|
138
|
+
accumulator.accept(chunk("version_info_1", null, "buildNumber" to "27"))
|
|
139
|
+
for (values in listOf(emptyMap(), mapOf("otaVersionUrl" to ""), mapOf("otaVersionUrl" to " \n"))) {
|
|
140
|
+
assertThat(accumulator.accept(chunk("version_info_2", null) + values))
|
|
141
|
+
.isEqualTo(VersionInfoAccumulatorOutcome.Waiting)
|
|
142
|
+
}
|
|
143
|
+
assertThat(accumulator.accept(second)).isInstanceOf(VersionInfoAccumulatorOutcome.Complete::class.java)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@Test
|
|
147
|
+
fun secondChunkCannotChangeBuildIntoFactoryProtocol() {
|
|
148
|
+
val accumulator = VersionInfoResponseAccumulator("request-1")
|
|
149
|
+
accumulator.accept(chunk("version_info_1", null, "buildNumber" to "31"))
|
|
150
|
+
assertThat(accumulator.accept(chunk("version_info_2", null,
|
|
151
|
+
"buildNumber" to "27", "otaVersionUrl" to "https://ota.example/version.json",
|
|
152
|
+
))).isEqualTo(VersionInfoAccumulatorOutcome.Waiting)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@Test
|
|
156
|
+
fun repeatedFirstChunkClearsFactoryCompletionRule() {
|
|
157
|
+
val accumulator = VersionInfoResponseAccumulator("request-1")
|
|
158
|
+
accumulator.accept(chunk("version_info_1", null, "buildNumber" to "27"))
|
|
159
|
+
accumulator.accept(chunk("version_info_1", null, "buildNumber" to "31"))
|
|
160
|
+
assertThat(accumulator.accept(chunk("version_info_2", null,
|
|
161
|
+
"otaVersionUrl" to "https://ota.example/version.json",
|
|
162
|
+
))).isEqualTo(VersionInfoAccumulatorOutcome.Waiting)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@Test
|
|
166
|
+
fun factoryBuildCannotBypassModernCorrelation() {
|
|
167
|
+
val accumulator = VersionInfoResponseAccumulator("request-1")
|
|
168
|
+
accumulator.accept(chunk("version_info_1", "request-1", "buildNumber" to "27"))
|
|
169
|
+
assertThat(accumulator.accept(chunk("version_info_2", null,
|
|
170
|
+
"otaVersionUrl" to "https://ota.example/version.json",
|
|
171
|
+
))).isEqualTo(VersionInfoAccumulatorOutcome.Ignored)
|
|
172
|
+
assertThat(accumulator.accept(chunk("version_info_3", "request-1", "besFirmwareVersion" to "new")))
|
|
173
|
+
.isInstanceOf(VersionInfoAccumulatorOutcome.Complete::class.java)
|
|
174
|
+
}
|
|
175
|
+
|
|
105
176
|
@Test
|
|
106
177
|
fun modernFinalChunkWaitsForMissingFirstChunk() {
|
|
107
178
|
val accumulator = VersionInfoResponseAccumulator("request-1")
|