@mentra/bluetooth-sdk 3.2.1-dev.311 → 3.2.1
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 +4 -4
- 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 +6 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +129 -25
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +2 -2
- 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/requests/DisplayRequests.kt +1 -1
- 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/G1.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +101 -22
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +314 -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 +238 -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 +180 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddress.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +5 -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/DashboardContentFormatterTest.kt +2 -2
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceIdentityPairingTest.kt +59 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +666 -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/G1PairingTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2DisplayThreadTest.kt +120 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2PairingTargetTest.kt +90 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveFileTransferSessionTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +33 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingTargetTest.kt +65 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraNexPairingTest.kt +151 -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 +348 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +339 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoMicReconnectTest.kt +93 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +11 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddressTest.kt +27 -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 +288 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +205 -0
- package/build/BluetoothSdk.types.d.ts +13 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +1 -0
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -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 +2 -2
- package/build/generated/changelogCatalog.js +2 -2
- 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 +9 -0
- package/ios/MentraBluetoothSDK.podspec +3 -2
- package/ios/Source/BluetoothBackgroundMode.swift +32 -0
- package/ios/Source/BluetoothSdkDefaults.swift +6 -1
- package/ios/Source/Bridge.swift +15 -2
- package/ios/Source/DeviceManager.swift +96 -63
- package/ios/Source/DeviceStore.swift +26 -7
- package/ios/Source/GeneratedChangelogCatalog.swift +2 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +18 -0
- package/ios/Source/audio/GlassesMicWatchdog.swift +33 -0
- package/ios/Source/requests/DisplayRequests.swift +1 -1
- package/ios/Source/requests/VersionInfoResponseAccumulator.swift +11 -1
- package/ios/Source/sgcs/Ar99.swift +1 -2
- package/ios/Source/sgcs/G1.swift +68 -38
- package/ios/Source/sgcs/G1ConnectionTarget.swift +13 -0
- package/ios/Source/sgcs/G2.swift +261 -247
- package/ios/Source/sgcs/G2ConnectionSession.swift +101 -0
- package/ios/Source/sgcs/Mach1.swift +19 -7
- package/ios/Source/sgcs/MentraLive.swift +201 -156
- package/ios/Source/sgcs/MentraLiveFileTransferSession.swift +137 -0
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +47 -0
- package/ios/Source/sgcs/MentraNex.swift +8 -31
- package/ios/Source/sgcs/NexConnectionTarget.swift +15 -0
- package/ios/Source/sgcs/Nimo.swift +280 -408
- package/ios/Source/sgcs/NimoCanvasCodec.swift +269 -0
- package/ios/Source/sgcs/NimoCanvasCoordinator.swift +233 -0
- package/ios/Source/sgcs/NimoCanvasImage.swift +53 -0
- package/ios/Source/sgcs/NimoCanvasSession.swift +166 -0
- package/ios/Source/sgcs/NimoDiscovery.swift +49 -0
- package/ios/Source/sgcs/SGCManager.swift +24 -0
- package/ios/Source/sgcs/VuzixConnectionTarget.swift +15 -0
- package/ios/Source/status/DeviceStatus.swift +1 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +49 -189
- package/ios/Source/utils/DemandDrivenAudioWorker.swift +150 -0
- package/ios/Source/utils/G2Text.swift +11 -0
- package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -0
- package/ios/Tests/DashboardContentFormatterTests.swift +2 -2
- package/ios/Tests/DashboardRenderingTests.swift +31 -0
- package/ios/Tests/DemandDrivenAudioWorkerTests.swift +157 -0
- package/ios/Tests/DeviceIdentityPairingTests.swift +49 -0
- package/ios/Tests/DeviceManagerReconnectTests.swift +362 -0
- package/ios/Tests/G1PairingTests.swift +162 -0
- package/ios/Tests/G2ConnectionSessionTests.swift +172 -0
- package/ios/Tests/G2TextTests.swift +10 -0
- package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
- package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
- package/ios/Tests/NimoCanvasCodecTests.swift +204 -0
- package/ios/Tests/NimoCanvasSessionTests.swift +181 -0
- package/ios/Tests/NimoCanvasTransportTests.swift +186 -0
- package/ios/Tests/NimoDeviceCommandsTests.swift +16 -0
- package/ios/Tests/NimoDiscoveryTests.swift +79 -0
- package/ios/Tests/SGCConnectionTargetTests.swift +48 -0
- package/ios/Tests/VersionInfoCommandTests.swift +53 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
- package/ios/Tests/VideoRecordingCommandTests.swift +81 -0
- package/package.json +2 -2
- package/plugin/build/withIos.d.ts +2 -0
- package/plugin/build/withIos.js +12 -1
- package/src/BluetoothSdk.types.ts +14 -0
- package/src/_private/BluetoothSdkModule.ts +8 -0
- package/src/generated/changelogCatalog.ts +2 -2
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +1 -0
|
@@ -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")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.services
|
|
2
|
+
|
|
3
|
+
import com.mentra.bluetoothsdk.utils.DeviceTypes
|
|
4
|
+
import org.junit.Assert.*
|
|
5
|
+
import org.junit.Before
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
import org.junit.runner.RunWith
|
|
8
|
+
import org.robolectric.RobolectricTestRunner
|
|
9
|
+
import org.robolectric.RuntimeEnvironment
|
|
10
|
+
import org.robolectric.annotation.Config
|
|
11
|
+
|
|
12
|
+
@RunWith(RobolectricTestRunner::class)
|
|
13
|
+
@Config(sdk = [28])
|
|
14
|
+
class G2ConnectionRecoveryTest {
|
|
15
|
+
private fun recovery() = G2ConnectionRecovery(RuntimeEnvironment.getApplication())
|
|
16
|
+
private fun settings() = mapOf<String, Any>(
|
|
17
|
+
"default_wearable" to DeviceTypes.G2, "device_name" to "S200-test",
|
|
18
|
+
"brightness" to 75, "auto_brightness" to false,
|
|
19
|
+
"core_token" to "must-not-persist", "micEnabled" to true,
|
|
20
|
+
"searching" to true, "pending_device_name" to "unpaired",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
@Before fun reset() { recovery().clear() }
|
|
24
|
+
|
|
25
|
+
@Test fun `a fresh recovery owner restores identity and settings but not runtime or auth state`() {
|
|
26
|
+
recovery().save(settings())
|
|
27
|
+
val restored = recovery().read()!!
|
|
28
|
+
assertEquals("S200-test", restored["device_name"])
|
|
29
|
+
assertEquals(75, restored["brightness"])
|
|
30
|
+
assertEquals(false, restored["auto_brightness"])
|
|
31
|
+
assertFalse(restored.containsKey("core_token"))
|
|
32
|
+
assertFalse(restored.containsKey("micEnabled"))
|
|
33
|
+
assertFalse(restored.containsKey("searching"))
|
|
34
|
+
assertFalse(restored.containsKey("pending_device_name"))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Test fun `explicit disconnect clears recovery across owner recreation`() {
|
|
38
|
+
recovery().save(settings())
|
|
39
|
+
recovery().clear()
|
|
40
|
+
assertNull(recovery().read())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Test fun `forget or switching models cannot resurrect old G2 pairing`() {
|
|
44
|
+
recovery().save(settings())
|
|
45
|
+
recovery().save(settings() + ("device_name" to ""))
|
|
46
|
+
assertNull(recovery().read())
|
|
47
|
+
recovery().save(settings())
|
|
48
|
+
recovery().save(settings() + ("default_wearable" to DeviceTypes.G1))
|
|
49
|
+
assertNull(recovery().read())
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import android.bluetooth.BluetoothAdapter
|
|
4
|
+
import android.bluetooth.BluetoothDevice
|
|
5
|
+
import android.bluetooth.BluetoothGatt
|
|
6
|
+
import android.bluetooth.BluetoothProfile
|
|
7
|
+
import android.bluetooth.le.ScanResult
|
|
8
|
+
import android.bluetooth.le.ScanSettings
|
|
9
|
+
import android.os.Looper
|
|
10
|
+
import androidx.test.core.app.ApplicationProvider
|
|
11
|
+
import com.mentra.bluetoothsdk.Bridge
|
|
12
|
+
import com.mentra.bluetoothsdk.DeviceStore
|
|
13
|
+
import com.mentra.lc3Lib.Lc3Cpp
|
|
14
|
+
import org.junit.Assert.assertEquals
|
|
15
|
+
import org.junit.Assert.assertTrue
|
|
16
|
+
import org.junit.Before
|
|
17
|
+
import org.junit.Test
|
|
18
|
+
import org.junit.runner.RunWith
|
|
19
|
+
import org.robolectric.RobolectricTestRunner
|
|
20
|
+
import org.robolectric.Shadows.shadowOf
|
|
21
|
+
import org.robolectric.annotation.Config
|
|
22
|
+
import org.robolectric.annotation.Implementation
|
|
23
|
+
import org.robolectric.annotation.Implements
|
|
24
|
+
import org.robolectric.annotation.LooperMode
|
|
25
|
+
import java.util.concurrent.TimeUnit
|
|
26
|
+
|
|
27
|
+
@RunWith(RobolectricTestRunner::class)
|
|
28
|
+
@Config(sdk = [33], shadows = [G1PairingTest.ShadowLc3Cpp::class], instrumentedPackages = ["com.mentra.lc3Lib"])
|
|
29
|
+
@LooperMode(LooperMode.Mode.PAUSED)
|
|
30
|
+
class G1PairingTest {
|
|
31
|
+
@Before
|
|
32
|
+
fun setup() {
|
|
33
|
+
Bridge.initialize(ApplicationProvider.getApplicationContext())
|
|
34
|
+
shadowOf(BluetoothAdapter.getDefaultAdapter()).setState(BluetoothAdapter.STATE_ON)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Test
|
|
38
|
+
fun `fresh pairing connects the selected G1 without a saved identity`() {
|
|
39
|
+
assertSelectedGlassesConnect("")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Test
|
|
43
|
+
fun `pairing from simulated glasses connects the selected G1`() {
|
|
44
|
+
assertSelectedGlassesConnect("Simulated Glasses")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Test
|
|
48
|
+
fun `switching G1 pairs ignores the previously saved pair`() {
|
|
49
|
+
assertSelectedGlassesConnect("12")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Test
|
|
53
|
+
fun `reconnecting the saved G1 still connects both halves`() {
|
|
54
|
+
assertSelectedGlassesConnect("47")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private fun assertSelectedGlassesConnect(savedName: String) {
|
|
58
|
+
DeviceStore.set("bluetooth", "device_name", savedName)
|
|
59
|
+
DeviceStore.set("bluetooth", "pending_device_name", "47")
|
|
60
|
+
val adapter = BluetoothAdapter.getDefaultAdapter()
|
|
61
|
+
val scanner = shadowOf(adapter.bluetoothLeScanner)
|
|
62
|
+
val glasses = G1()
|
|
63
|
+
try {
|
|
64
|
+
glasses.connectById("47")
|
|
65
|
+
val callback = scanner.scanCallbacks.single()
|
|
66
|
+
val unrelated = device("00:00:00:00:12:01", "Even G1_12_L_000001")
|
|
67
|
+
val left = device("00:00:00:00:47:01", "Even G1_47_L_000001")
|
|
68
|
+
val right = device("00:00:00:00:47:02", "Even G1_47_R_000002")
|
|
69
|
+
|
|
70
|
+
for (device in listOf(unrelated, left, right)) {
|
|
71
|
+
callback.onScanResult(
|
|
72
|
+
ScanSettings.CALLBACK_TYPE_ALL_MATCHES,
|
|
73
|
+
ScanResult(device, null, -50, 0),
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
shadowOf(Looper.getMainLooper()).idle()
|
|
77
|
+
assertTrue("The previously saved pair must not receive a GATT connection", shadowOf(unrelated).bluetoothGatts.isEmpty())
|
|
78
|
+
assertEquals("Connect the selected left half", 1, shadowOf(left).bluetoothGatts.size)
|
|
79
|
+
shadowOf(left).simulateGattConnectionChange(BluetoothGatt.GATT_SUCCESS, BluetoothProfile.STATE_CONNECTED)
|
|
80
|
+
shadowOf(Looper.getMainLooper()).idleFor(2, TimeUnit.SECONDS)
|
|
81
|
+
assertEquals("Connect the selected right half", 1, shadowOf(right).bluetoothGatts.size)
|
|
82
|
+
assertTrue("Stop scanning once both selected halves are found", scanner.scanCallbacks.isEmpty())
|
|
83
|
+
assertEquals("Scanning must not promote an unready pairing", savedName, DeviceStore.get("bluetooth", "device_name"))
|
|
84
|
+
} finally {
|
|
85
|
+
glasses.disconnect()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private fun device(address: String, name: String): BluetoothDevice =
|
|
90
|
+
BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address).also {
|
|
91
|
+
shadowOf(it).setName(name)
|
|
92
|
+
shadowOf(it).setAlias(name)
|
|
93
|
+
shadowOf(it).setBondState(BluetoothDevice.BOND_BONDED)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Pairing tests exercise the real G1 driver; only its unrelated native audio codec is stubbed.
|
|
97
|
+
@Implements(Lc3Cpp::class)
|
|
98
|
+
class ShadowLc3Cpp {
|
|
99
|
+
companion object {
|
|
100
|
+
@JvmStatic @Implementation fun __staticInitializer__() {}
|
|
101
|
+
@JvmStatic @Implementation fun initDecoder(): Long = 1L
|
|
102
|
+
@JvmStatic @Implementation fun freeDecoder(decoderPtr: Long) {}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import android.os.Looper
|
|
4
|
+
import android.bluetooth.BluetoothGatt
|
|
5
|
+
import android.bluetooth.BluetoothGattCharacteristic
|
|
6
|
+
import java.util.UUID
|
|
7
|
+
import java.util.concurrent.ExecutorService
|
|
8
|
+
import java.util.concurrent.TimeUnit
|
|
9
|
+
import org.junit.Assert.assertTrue
|
|
10
|
+
import org.robolectric.annotation.Implementation
|
|
11
|
+
import org.robolectric.annotation.Implements
|
|
12
|
+
import org.robolectric.shadow.api.Shadow
|
|
13
|
+
import com.mentra.bluetoothsdk.Bridge
|
|
14
|
+
import org.junit.Assert.assertEquals
|
|
15
|
+
import org.junit.Test
|
|
16
|
+
import org.junit.runner.RunWith
|
|
17
|
+
import org.robolectric.RobolectricTestRunner
|
|
18
|
+
import org.robolectric.RuntimeEnvironment
|
|
19
|
+
import org.robolectric.Shadows.shadowOf
|
|
20
|
+
import org.robolectric.annotation.Config
|
|
21
|
+
import org.robolectric.annotation.LooperMode
|
|
22
|
+
|
|
23
|
+
@RunWith(RobolectricTestRunner::class)
|
|
24
|
+
@Config(sdk = [28], shadows = [RecordingGatt::class])
|
|
25
|
+
@LooperMode(LooperMode.Mode.PAUSED)
|
|
26
|
+
class G2DisplayThreadTest {
|
|
27
|
+
@Test fun `worker clear cannot remove containers under the main-thread reconcile loop`() {
|
|
28
|
+
Bridge.initialize(RuntimeEnvironment.getApplication())
|
|
29
|
+
val g2 = G2()
|
|
30
|
+
val add = G2::class.java.declaredMethods.single { it.name == "addTextContainer" }
|
|
31
|
+
add.isAccessible = true
|
|
32
|
+
add.invoke(g2, 0, 0, 576, 288, "captions", 0, 0, 0, 0)
|
|
33
|
+
add.invoke(g2, 10, 10, 100, 40, "HUD", 0, 0, 0, 0)
|
|
34
|
+
val field = G2::class.java.getDeclaredField("textContainers").apply { isAccessible = true }
|
|
35
|
+
val containers = field.get(g2) as List<*>
|
|
36
|
+
// This is the heartbeat's captured index range at the crash site.
|
|
37
|
+
val indices = containers.indices
|
|
38
|
+
val worker = Thread { g2.clearDisplay() }
|
|
39
|
+
worker.start()
|
|
40
|
+
worker.join()
|
|
41
|
+
assertEquals(2, containers.size)
|
|
42
|
+
indices.forEach { containers[it] } // crashed here when clear ran on the worker
|
|
43
|
+
shadowOf(Looper.getMainLooper()).idle()
|
|
44
|
+
assertEquals(1, containers.size) // only the positioned HUD husk is purged
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Test fun `closing positioned captions clears the live page without waiting for new speech`() {
|
|
48
|
+
Bridge.initialize(RuntimeEnvironment.getApplication())
|
|
49
|
+
val g2 = G2()
|
|
50
|
+
val add = G2::class.java.declaredMethods.single { it.name == "addTextContainer" }
|
|
51
|
+
add.isAccessible = true
|
|
52
|
+
add.invoke(g2, 10, 10, 500, 100, "old captions", 0, 0, 0, 0)
|
|
53
|
+
val page = G2::class.java.getDeclaredField("pageCreated").apply { isAccessible = true }
|
|
54
|
+
page.setBoolean(g2, true)
|
|
55
|
+
|
|
56
|
+
g2.clearDisplay()
|
|
57
|
+
shadowOf(Looper.getMainLooper()).idle()
|
|
58
|
+
|
|
59
|
+
val field = G2::class.java.getDeclaredField("textContainers").apply { isAccessible = true }
|
|
60
|
+
assertEquals(0, (field.get(g2) as List<*>).size)
|
|
61
|
+
// No shutdown or deferred recreation: the replacement page is already live,
|
|
62
|
+
// even if the user remains silent and no new caption frame ever arrives.
|
|
63
|
+
assertEquals(true, page.getBoolean(g2))
|
|
64
|
+
g2.clearDisplay()
|
|
65
|
+
shadowOf(Looper.getMainLooper()).idle()
|
|
66
|
+
assertEquals(true, page.getBoolean(g2))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@Test fun `clear transmits an erase before dropping the old caption container and retries busy writes`() {
|
|
71
|
+
Bridge.initialize(RuntimeEnvironment.getApplication())
|
|
72
|
+
val g2 = G2()
|
|
73
|
+
val gatt = Shadow.newInstanceOf(BluetoothGatt::class.java)
|
|
74
|
+
val recorder = Shadow.extract<RecordingGatt>(gatt)
|
|
75
|
+
recorder.rejectWrites = 1
|
|
76
|
+
G2::class.java.getDeclaredField("rightGatt").apply { isAccessible = true }.set(g2, gatt)
|
|
77
|
+
G2::class.java.getDeclaredField("rightWriteChar").apply { isAccessible = true }.set(
|
|
78
|
+
g2, BluetoothGattCharacteristic(UUID.randomUUID(), 4, 16)
|
|
79
|
+
)
|
|
80
|
+
val add = G2::class.java.declaredMethods.single { it.name == "addTextContainer" }
|
|
81
|
+
add.isAccessible = true
|
|
82
|
+
add.invoke(g2, 10, 10, 500, 100, "old captions", 0, 0, 0, 0)
|
|
83
|
+
G2::class.java.getDeclaredField("pageCreated").apply { isAccessible = true }.setBoolean(g2, true)
|
|
84
|
+
|
|
85
|
+
g2.clearDisplay()
|
|
86
|
+
shadowOf(Looper.getMainLooper()).idle()
|
|
87
|
+
val executor = G2::class.java.getDeclaredField("bleWriteExecutor").apply { isAccessible = true }
|
|
88
|
+
.get(g2) as ExecutorService
|
|
89
|
+
executor.submit {}.get(5, TimeUnit.SECONDS)
|
|
90
|
+
|
|
91
|
+
// Transport header is eight bytes; protobuf field 1 is the EvenHub command.
|
|
92
|
+
val commands = recorder.accepted.map { it[9].toInt() }
|
|
93
|
+
assertEquals(5, commands.first()) // UPDATE_TEXT_DATA, before REBUILD_PAGE (7)
|
|
94
|
+
assertTrue(commands.indexOf(7) > 0)
|
|
95
|
+
assertTrue(String(recorder.accepted.first(), Charsets.UTF_8).contains(" ".repeat(12)))
|
|
96
|
+
assertTrue(recorder.attempted[0].contentEquals(recorder.attempted[1]))
|
|
97
|
+
assertEquals(recorder.accepted.size + 1, recorder.attempted.size)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@Implements(BluetoothGatt::class)
|
|
104
|
+
class RecordingGatt {
|
|
105
|
+
var rejectWrites = 0
|
|
106
|
+
val attempted = mutableListOf<ByteArray>()
|
|
107
|
+
val accepted = mutableListOf<ByteArray>()
|
|
108
|
+
|
|
109
|
+
@Implementation
|
|
110
|
+
fun writeCharacteristic(characteristic: BluetoothGattCharacteristic): Boolean {
|
|
111
|
+
val packet = characteristic.value.copyOf()
|
|
112
|
+
attempted.add(packet)
|
|
113
|
+
if (rejectWrites > 0) {
|
|
114
|
+
rejectWrites--
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
accepted.add(packet)
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import android.bluetooth.BluetoothAdapter
|
|
4
|
+
import android.bluetooth.BluetoothGatt
|
|
5
|
+
import android.bluetooth.BluetoothProfile
|
|
6
|
+
import android.bluetooth.le.ScanRecord
|
|
7
|
+
import android.bluetooth.le.ScanResult
|
|
8
|
+
import android.bluetooth.le.ScanSettings
|
|
9
|
+
import android.content.Context
|
|
10
|
+
import android.os.Looper
|
|
11
|
+
import com.mentra.bluetoothsdk.Bridge
|
|
12
|
+
import org.junit.Assert.assertEquals
|
|
13
|
+
import org.junit.Assert.assertTrue
|
|
14
|
+
import org.junit.Before
|
|
15
|
+
import org.junit.Test
|
|
16
|
+
import org.junit.runner.RunWith
|
|
17
|
+
import org.robolectric.RobolectricTestRunner
|
|
18
|
+
import org.robolectric.RuntimeEnvironment
|
|
19
|
+
import org.robolectric.Shadows.shadowOf
|
|
20
|
+
import org.robolectric.annotation.Config
|
|
21
|
+
import org.robolectric.annotation.LooperMode
|
|
22
|
+
import org.robolectric.util.ReflectionHelpers
|
|
23
|
+
import org.robolectric.util.ReflectionHelpers.ClassParameter
|
|
24
|
+
|
|
25
|
+
@RunWith(RobolectricTestRunner::class)
|
|
26
|
+
@Config(sdk = [33])
|
|
27
|
+
@LooperMode(LooperMode.Mode.PAUSED)
|
|
28
|
+
class G2PairingTargetTest {
|
|
29
|
+
@Before fun setup() {
|
|
30
|
+
Bridge.initialize(RuntimeEnvironment.getApplication())
|
|
31
|
+
shadowOf(BluetoothAdapter.getDefaultAdapter()).setState(BluetoothAdapter.STATE_ON)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Test fun `unscoped legacy addresses cannot connect a different G2 pair`() {
|
|
35
|
+
RuntimeEnvironment.getApplication().getSharedPreferences("G2Prefs", Context.MODE_PRIVATE).edit()
|
|
36
|
+
.putString("g2_leftGlassAddress", "00:00:00:00:01:01")
|
|
37
|
+
.putString("g2_rightGlassAddress", "00:00:00:00:01:02").apply()
|
|
38
|
+
val g2 = G2()
|
|
39
|
+
try {
|
|
40
|
+
g2.connectById("S2NEW000000001")
|
|
41
|
+
val adapter = BluetoothAdapter.getDefaultAdapter()
|
|
42
|
+
assertTrue(shadowOf(adapter.getRemoteDevice("00:00:00:00:01:01")).bluetoothGatts.isEmpty())
|
|
43
|
+
assertTrue(shadowOf(adapter.getRemoteDevice("00:00:00:00:01:02")).bluetoothGatts.isEmpty())
|
|
44
|
+
assertEquals(1, shadowOf(adapter.bluetoothLeScanner).scanCallbacks.size)
|
|
45
|
+
} finally { g2.disconnect() }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Test fun `learned addresses reconnect only their own serial across driver recreation`() {
|
|
49
|
+
val serial = "S2NEW000000001"
|
|
50
|
+
val adapter = BluetoothAdapter.getDefaultAdapter()
|
|
51
|
+
val left = adapter.getRemoteDevice("00:00:00:00:02:01")
|
|
52
|
+
val right = adapter.getRemoteDevice("00:00:00:00:02:02")
|
|
53
|
+
shadowOf(left).setName("Even G2_47_L_000001")
|
|
54
|
+
shadowOf(right).setName("Even G2_47_R_000002")
|
|
55
|
+
var g2 = G2()
|
|
56
|
+
try {
|
|
57
|
+
g2.connectById(serial)
|
|
58
|
+
val callback = shadowOf(adapter.bluetoothLeScanner).scanCallbacks.single()
|
|
59
|
+
// Manufacturer record: company ID 0x4552, followed by the advertised serial.
|
|
60
|
+
val payload = byteArrayOf(0x52, 0x45) + serial.toByteArray(Charsets.US_ASCII)
|
|
61
|
+
val record = ReflectionHelpers.callStaticMethod<ScanRecord>(
|
|
62
|
+
ScanRecord::class.java, "parseFromBytes",
|
|
63
|
+
ClassParameter.from(ByteArray::class.java, byteArrayOf((payload.size + 1).toByte(), 0xff.toByte()) + payload + byteArrayOf(0)),
|
|
64
|
+
)
|
|
65
|
+
for (device in listOf(left, right)) {
|
|
66
|
+
callback.onScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES, ScanResult(device, record, -50, 0))
|
|
67
|
+
}
|
|
68
|
+
shadowOf(Looper.getMainLooper()).idle()
|
|
69
|
+
for (device in listOf(left, right)) {
|
|
70
|
+
assertEquals(1, shadowOf(device).bluetoothGatts.size)
|
|
71
|
+
shadowOf(device).simulateGattConnectionChange(BluetoothGatt.GATT_SUCCESS, BluetoothProfile.STATE_CONNECTED)
|
|
72
|
+
}
|
|
73
|
+
shadowOf(Looper.getMainLooper()).idle()
|
|
74
|
+
g2.disconnect()
|
|
75
|
+
|
|
76
|
+
g2 = G2()
|
|
77
|
+
g2.connectById("S2OTHER0000001")
|
|
78
|
+
assertEquals(1, shadowOf(left).bluetoothGatts.size)
|
|
79
|
+
assertEquals(1, shadowOf(right).bluetoothGatts.size)
|
|
80
|
+
assertEquals(1, shadowOf(adapter.bluetoothLeScanner).scanCallbacks.size)
|
|
81
|
+
g2.disconnect()
|
|
82
|
+
|
|
83
|
+
g2 = G2()
|
|
84
|
+
g2.connectById(serial)
|
|
85
|
+
assertEquals(2, shadowOf(left).bluetoothGatts.size)
|
|
86
|
+
assertEquals(2, shadowOf(right).bluetoothGatts.size)
|
|
87
|
+
assertTrue(shadowOf(adapter.bluetoothLeScanner).scanCallbacks.isEmpty())
|
|
88
|
+
} finally { g2.disconnect() }
|
|
89
|
+
}
|
|
90
|
+
}
|
package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveFileTransferSessionTest.kt
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.*
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
import org.junit.runner.RunWith
|
|
6
|
+
import org.robolectric.RobolectricTestRunner
|
|
7
|
+
import org.robolectric.annotation.Config
|
|
8
|
+
|
|
9
|
+
@RunWith(RobolectricTestRunner::class)
|
|
10
|
+
@Config(sdk = [33])
|
|
11
|
+
class MentraLiveFileTransferSessionTest {
|
|
12
|
+
@Test fun dynamicPayloadUsesTrueSizeEvenWhenDivisibleBy400() {
|
|
13
|
+
for (pack in listOf(244, 474, 800)) {
|
|
14
|
+
val session = MentraLive.FileTransferSession("photo", 80000, 2)
|
|
15
|
+
session.recalculateTotalPackets(pack)
|
|
16
|
+
assertEquals((80000 + pack - 1) / pack, session.totalPackets)
|
|
17
|
+
for (index in 0 until session.totalPackets) {
|
|
18
|
+
assertTrue(session.addPacket(index, ByteArray(minOf(pack, 80000 - index * pack))))
|
|
19
|
+
}
|
|
20
|
+
assertTrue(session.isComplete)
|
|
21
|
+
assertEquals(80000, session.assembleFile()!!.size)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Test fun legacyInflatedSizeAndLargeUnflaggedPayloads() {
|
|
26
|
+
// Flag 1 enables batched ACKs but still uses legacy size semantics.
|
|
27
|
+
for ((pack, headerSize, actualSize, packets) in listOf(
|
|
28
|
+
listOf(221, 80000, 43982, 200),
|
|
29
|
+
listOf(400, 80000, 79777, 200),
|
|
30
|
+
listOf(800, 80000, 80000, 100))) {
|
|
31
|
+
val session = MentraLive.FileTransferSession("photo", headerSize, 1)
|
|
32
|
+
session.recalculateTotalPackets(pack)
|
|
33
|
+
assertEquals(packets, session.totalPackets)
|
|
34
|
+
val source = ByteArray(actualSize) { (it % 251).toByte() }
|
|
35
|
+
for (index in (0 until packets).reversed()) {
|
|
36
|
+
val start = index * pack
|
|
37
|
+
assertTrue(session.addPacket(index, source.copyOfRange(start, minOf(start + pack, actualSize))))
|
|
38
|
+
}
|
|
39
|
+
assertTrue(session.isComplete)
|
|
40
|
+
assertArrayEquals(source, session.assembleFile())
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test fun malformedOrDuplicatePacketsCannotCompleteFile() {
|
|
45
|
+
val session = MentraLive.FileTransferSession("photo", 1000, 2)
|
|
46
|
+
session.recalculateTotalPackets(800)
|
|
47
|
+
assertFalse(session.addPacket(0, ByteArray(799)))
|
|
48
|
+
assertFalse(session.addPacket(2, ByteArray(200)))
|
|
49
|
+
assertTrue(session.addPacket(1, ByteArray(200)))
|
|
50
|
+
assertFalse(session.isComplete)
|
|
51
|
+
assertFalse(session.addPacket(1, ByteArray(200)))
|
|
52
|
+
assertTrue(session.addPacket(0, ByteArray(800)))
|
|
53
|
+
assertEquals(1000, session.assembleFile()!!.size)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -78,6 +78,39 @@ class MentraLiveGattCallbackTest {
|
|
|
78
78
|
assertArrayEquals(byteArrayOf(3, 2), callback.packets[1])
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
@Test
|
|
82
|
+
@Config(sdk = [33])
|
|
83
|
+
@Suppress("DEPRECATION")
|
|
84
|
+
fun `modern and legacy callbacks for one notification deliver one packet`() {
|
|
85
|
+
val queue = ArrayDeque<() -> Unit>()
|
|
86
|
+
val callback = RecordingCallback({ work -> queue.addLast(work) })
|
|
87
|
+
val gatt = gatt()
|
|
88
|
+
val characteristic = BluetoothGattCharacteristic(UUID.randomUUID(), 0, 0)
|
|
89
|
+
val value = byteArrayOf(0xf1.toByte(), 7, 1, 2)
|
|
90
|
+
characteristic.value = value
|
|
91
|
+
callback.onCharacteristicChanged(gatt, characteristic, value)
|
|
92
|
+
callback.onCharacteristicChanged(gatt, characteristic)
|
|
93
|
+
value[2] = 9
|
|
94
|
+
while (queue.isNotEmpty()) queue.removeFirst().invoke()
|
|
95
|
+
assertEquals(1, callback.packets.size)
|
|
96
|
+
assertArrayEquals(byteArrayOf(0xf1.toByte(), 7, 1, 2), callback.packets.single())
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@Test
|
|
100
|
+
@Config(sdk = [33])
|
|
101
|
+
fun `separate modern notifications retain identical payloads`() {
|
|
102
|
+
val queue = ArrayDeque<() -> Unit>()
|
|
103
|
+
val callback = RecordingCallback({ work -> queue.addLast(work) })
|
|
104
|
+
val gatt = gatt()
|
|
105
|
+
val characteristic = BluetoothGattCharacteristic(UUID.randomUUID(), 0, 0)
|
|
106
|
+
val value = byteArrayOf(1, 2)
|
|
107
|
+
repeat(2) { callback.onCharacteristicChanged(gatt, characteristic, value) }
|
|
108
|
+
value[0] = 9
|
|
109
|
+
while (queue.isNotEmpty()) queue.removeFirst().invoke()
|
|
110
|
+
assertEquals(2, callback.packets.size)
|
|
111
|
+
callback.packets.forEach { assertArrayEquals(byteArrayOf(1, 2), it) }
|
|
112
|
+
}
|
|
113
|
+
|
|
81
114
|
@Test
|
|
82
115
|
fun `replacement invalidates callbacks already waiting on the lifecycle queue`() {
|
|
83
116
|
val queue = ArrayDeque<() -> Unit>()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import android.bluetooth.BluetoothAdapter
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
import androidx.test.core.app.ApplicationProvider
|
|
6
|
+
import com.mentra.bluetoothsdk.Bridge
|
|
7
|
+
import com.mentra.bluetoothsdk.DeviceStore
|
|
8
|
+
import java.time.Duration
|
|
9
|
+
import org.junit.Assert.assertEquals
|
|
10
|
+
import org.junit.Assert.assertTrue
|
|
11
|
+
import org.junit.Before
|
|
12
|
+
import org.junit.Test
|
|
13
|
+
import org.junit.runner.RunWith
|
|
14
|
+
import org.robolectric.RobolectricTestRunner
|
|
15
|
+
import org.robolectric.Shadows.shadowOf
|
|
16
|
+
import org.robolectric.annotation.Config
|
|
17
|
+
import org.robolectric.annotation.LooperMode
|
|
18
|
+
|
|
19
|
+
@RunWith(RobolectricTestRunner::class)
|
|
20
|
+
@Config(sdk = [33], shadows = [G1PairingTest.ShadowLc3Cpp::class], instrumentedPackages = ["com.mentra.lc3Lib"])
|
|
21
|
+
@LooperMode(LooperMode.Mode.PAUSED)
|
|
22
|
+
class MentraLivePairingTargetTest {
|
|
23
|
+
@Before fun setup() {
|
|
24
|
+
Bridge.initialize(ApplicationProvider.getApplicationContext())
|
|
25
|
+
shadowOf(BluetoothAdapter.getDefaultAdapter()).setState(BluetoothAdapter.STATE_ON)
|
|
26
|
+
DeviceStore.set("bluetooth", "pending_device_name", "MENTRA_LIVE_NEW")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test fun `name-only selection scans instead of connecting the previous Live`() {
|
|
30
|
+
assertTarget("MENTRA_LIVE_OLD", "00:00:00:00:00:01", "", scanExpected = true)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Test fun `normal pairing connects the pending Live address`() {
|
|
34
|
+
assertTarget("MENTRA_LIVE_OLD", "00:00:00:00:00:01", "00:00:00:00:00:02")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Test fun `normal saved Live reconnect keeps the matching address`() {
|
|
38
|
+
assertTarget("MENTRA_LIVE_NEW", "00:00:00:00:00:02", "")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private fun assertTarget(savedName: String, savedAddress: String, pendingAddress: String, scanExpected: Boolean = false) {
|
|
42
|
+
DeviceStore.set("bluetooth", "device_name", savedName)
|
|
43
|
+
DeviceStore.set("bluetooth", "device_address", savedAddress)
|
|
44
|
+
DeviceStore.set("bluetooth", "pending_device_address", pendingAddress)
|
|
45
|
+
val adapter = BluetoothAdapter.getDefaultAdapter()
|
|
46
|
+
val old = adapter.getRemoteDevice("00:00:00:00:00:01")
|
|
47
|
+
val selected = adapter.getRemoteDevice("00:00:00:00:00:02")
|
|
48
|
+
shadowOf(old).setName("MENTRA_LIVE_OLD")
|
|
49
|
+
shadowOf(selected).setName("MENTRA_LIVE_NEW")
|
|
50
|
+
val live = MentraLive()
|
|
51
|
+
try {
|
|
52
|
+
live.connectById("MENTRA_LIVE_NEW")
|
|
53
|
+
shadowOf(Looper.getMainLooper()).idle()
|
|
54
|
+
assertTrue(shadowOf(old).bluetoothGatts.isEmpty())
|
|
55
|
+
assertEquals(if (scanExpected) 0 else 1, shadowOf(selected).bluetoothGatts.size)
|
|
56
|
+
assertEquals(if (scanExpected) 1 else 0, shadowOf(adapter.bluetoothLeScanner).scanCallbacks.size)
|
|
57
|
+
assertEquals(savedName, DeviceStore.get("bluetooth", "device_name"))
|
|
58
|
+
} finally {
|
|
59
|
+
live.destroy()
|
|
60
|
+
// Robolectric does not deliver the disconnect callback automatically.
|
|
61
|
+
// Complete the production teardown timeout before another test connects.
|
|
62
|
+
shadowOf(Looper.getMainLooper()).idleFor(Duration.ofSeconds(2))
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|