@mentra/bluetooth-sdk 3.2.1-dev.314 → 3.3.0-beta.942
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 +72 -10
- package/android/build.gradle +15 -0
- package/android/lc3Lib/build.gradle +6 -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/BluetoothSdkAnalytics.kt +63 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTracker.kt +55 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +4 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkInstallId.kt +23 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +8 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheck.kt +184 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTracker.kt +388 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +133 -26
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +3 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +6 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +227 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +24 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/NetworkDiagnostics.kt +25 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +3 -0
- 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/ClassicAudioConnectionTracker.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscovery.kt +70 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +11 -32
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/LatinTextSanitizer.kt +39 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +194 -40
- 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 +247 -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/BluetoothSdkAnalyticsTrackerTest.kt +58 -3
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheckTest.kt +124 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTrackerTest.kt +263 -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/NetworkDiagnosticsTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ScanCompletionTest.kt +5 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/debug/BleTraceLoggerTest.kt +69 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscoveryTest.kt +124 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +29 -4
- 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/MentraLiveClassicDisconnectTest.kt +133 -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/MentraLiveIncidentDispatchTest.kt +126 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingTargetTest.kt +65 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveWifiCredentialLogTest.kt +55 -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 +233 -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 +16 -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 +5 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +23 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
- package/build/_private/MentraLocalNetworkModule.js.map +1 -1
- package/build/_private/nativeLogConsole.d.ts +19 -0
- package/build/_private/nativeLogConsole.d.ts.map +1 -1
- package/build/_private/nativeLogConsole.js +12 -0
- package/build/_private/nativeLogConsole.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +5 -2
- package/build/generated/changelogCatalog.d.ts.map +1 -1
- package/build/generated/changelogCatalog.js +6 -2
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +6 -6
- 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/build/ota-transport/index.d.ts +4 -0
- package/build/ota-transport/index.d.ts.map +1 -1
- package/build/ota-transport/index.js +2 -0
- package/build/ota-transport/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +17 -4
- package/ios/MentraBluetoothSDK.podspec +3 -2
- package/ios/Source/BluetoothBackgroundMode.swift +32 -0
- package/ios/Source/BluetoothSdkDefaults.swift +7 -2
- package/ios/Source/Bridge.swift +15 -2
- package/ios/Source/DeviceManager.swift +102 -66
- package/ios/Source/DeviceStore.swift +26 -7
- package/ios/Source/GeneratedChangelogCatalog.swift +3 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +6 -6
- package/ios/Source/MentraBluetoothSDK.swift +313 -54
- package/ios/Source/audio/GlassesMicWatchdog.swift +33 -0
- package/ios/Source/internal/BluetoothAvailability.swift +6 -0
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +87 -13
- package/ios/Source/internal/BluetoothSdkAnalyticsTracker.swift +54 -1
- package/ios/Source/internal/BluetoothSdkInstallId.swift +21 -0
- package/ios/Source/internal/BluetoothSdkUpdateCheck.swift +223 -0
- package/ios/Source/internal/BluetoothSdkUsageTracker.swift +428 -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 +272 -197
- 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 +272 -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 +25 -1
- 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/AudioSessionMonitor.swift +44 -8
- package/ios/Source/utils/DemandDrivenAudioWorker.swift +150 -0
- package/ios/Source/utils/G1Text.swift +2 -24
- package/ios/Source/utils/G1TextSanitizer.swift +27 -0
- package/ios/Source/utils/G2Text.swift +11 -0
- package/ios/Source/utils/LatinTextSanitizer.swift +48 -0
- package/ios/Tests/BluetoothAudioRouteTests.swift +26 -0
- package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -0
- package/ios/Tests/BluetoothSdkAnalyticsTrackerTests.swift +54 -3
- package/ios/Tests/BluetoothSdkUpdateCheckTests.swift +153 -0
- package/ios/Tests/BluetoothSdkUsageTrackerTests.swift +296 -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/G1TextSanitizerTests.swift +32 -0
- package/ios/Tests/G2ConnectionSessionTests.swift +172 -0
- package/ios/Tests/G2TextTests.swift +10 -0
- package/ios/Tests/IncidentLogDispatchTests.swift +111 -0
- package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
- package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
- package/ios/Tests/MentraLiveSessionReadinessTests.swift +163 -0
- package/ios/Tests/NimoCanvasCodecTests.swift +217 -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 +194 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
- package/ios/Tests/VideoRecordingCommandTests.swift +81 -0
- package/ios/Tests/WifiCredentialLoggingTests.swift +42 -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 +15 -0
- package/src/_private/BluetoothSdkModule.ts +10 -1
- package/src/_private/MentraLocalNetworkModule.ts +28 -0
- package/src/_private/nativeLogConsole.ts +19 -0
- package/src/generated/changelogCatalog.ts +6 -2
- package/src/generated/releaseMetadata.ts +6 -6
- package/src/index.ts +1 -0
- package/src/ota-transport/index.ts +10 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertFalse
|
|
5
|
+
import org.junit.Assert.assertNull
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
|
|
8
|
+
class NetworkDiagnosticsTest {
|
|
9
|
+
private val validatedInternet = mapOf("internet" to true, "validated" to true)
|
|
10
|
+
private val localOnly = mapOf("internet" to false, "validated" to false)
|
|
11
|
+
|
|
12
|
+
@Test
|
|
13
|
+
fun `distinguishes a process bound to glasses from a validated active network`() {
|
|
14
|
+
val snapshot = networkDiagnostics(123L, "private-glasses", "private-cellular", "private-glasses") {
|
|
15
|
+
if (it == "private-cellular") validatedInternet else localOnly
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
assertEquals(123L, snapshot["sampledAtMs"])
|
|
19
|
+
assertEquals(false, snapshot["processBoundMatchesActive"])
|
|
20
|
+
assertEquals(true, snapshot["processBoundMatchesGlassesLocal"])
|
|
21
|
+
assertEquals(false, snapshot["activeNetworkMatchesGlassesLocal"])
|
|
22
|
+
assertEquals(validatedInternet, observation(snapshot, "activeNetwork")["capabilities"])
|
|
23
|
+
assertFalse(snapshot.toString().contains("private-"))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Test
|
|
27
|
+
fun `keeps an unvalidated active network distinct from a disappeared network`() {
|
|
28
|
+
val unvalidated = networkDiagnostics(124L, null, "default", null) { localOnly }
|
|
29
|
+
assertEquals(localOnly, observation(unvalidated, "activeNetwork")["capabilities"])
|
|
30
|
+
val disappeared = networkDiagnostics(125L, null, "default", null) { null }
|
|
31
|
+
assertEquals(true, observation(disappeared, "activeNetwork")["present"])
|
|
32
|
+
assertNull(observation(disappeared, "activeNetwork")["capabilities"])
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Test
|
|
36
|
+
fun `no network does not imply equal routes or attempt a capability read`() {
|
|
37
|
+
var read = false
|
|
38
|
+
val snapshot = networkDiagnostics<String>(126L, null, null, null) {
|
|
39
|
+
read = true
|
|
40
|
+
validatedInternet
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
assertFalse(read)
|
|
44
|
+
listOf("processBound", "activeNetwork", "glassesLocal").forEach {
|
|
45
|
+
assertEquals(false, observation(snapshot, it)["present"])
|
|
46
|
+
assertNull(observation(snapshot, it)["capabilities"])
|
|
47
|
+
}
|
|
48
|
+
assertEquals(false, snapshot["processBoundMatchesActive"])
|
|
49
|
+
assertEquals(false, snapshot["processBoundMatchesGlassesLocal"])
|
|
50
|
+
assertEquals(false, snapshot["activeNetworkMatchesGlassesLocal"])
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Suppress("UNCHECKED_CAST")
|
|
54
|
+
private fun observation(snapshot: Map<String, Any?>, name: String) = snapshot[name] as Map<String, Any?>
|
|
55
|
+
}
|
|
@@ -37,7 +37,11 @@ class ScanCompletionTest {
|
|
|
37
37
|
error("Legacy callbacks must not query diagnostics")
|
|
38
38
|
}
|
|
39
39
|
assertThat(completed).isTrue()
|
|
40
|
-
|
|
40
|
+
// Instance methods only: Kotlin 2.2+ compiles bodies to Java default
|
|
41
|
+
// methods plus static `access$...$jd` bridges, which nothing implements.
|
|
42
|
+
assertThat(ScanCallback::class.java.declaredMethods
|
|
43
|
+
.filterNot { java.lang.reflect.Modifier.isStatic(it.modifiers) }
|
|
44
|
+
.map { it.name })
|
|
41
45
|
.containsExactlyInAnyOrder("onResults", "onComplete", "onError")
|
|
42
46
|
}
|
|
43
47
|
|
|
@@ -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,69 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.debug
|
|
2
|
+
|
|
3
|
+
import java.util.UUID
|
|
4
|
+
import org.json.JSONArray
|
|
5
|
+
import org.json.JSONObject
|
|
6
|
+
import org.junit.Assert.assertEquals
|
|
7
|
+
import org.junit.Assert.assertFalse
|
|
8
|
+
import org.junit.Test
|
|
9
|
+
import org.junit.runner.RunWith
|
|
10
|
+
import org.robolectric.RobolectricTestRunner
|
|
11
|
+
import org.robolectric.annotation.Config
|
|
12
|
+
import org.robolectric.shadows.ShadowLog
|
|
13
|
+
|
|
14
|
+
@RunWith(RobolectricTestRunner::class)
|
|
15
|
+
@Config(sdk = [28])
|
|
16
|
+
class BleTraceLoggerTest {
|
|
17
|
+
@Test
|
|
18
|
+
fun wifiScanPreservesBooleanSecurityWhileRedactingCredentials() {
|
|
19
|
+
val secret = UUID.randomUUID().toString()
|
|
20
|
+
val networks = JSONArray()
|
|
21
|
+
for (secured in listOf(true, false)) {
|
|
22
|
+
networks.put(
|
|
23
|
+
JSONObject()
|
|
24
|
+
.put("ssid", if (secured) "secured-fixture" else "open-fixture")
|
|
25
|
+
.put("requiresPassword", secured)
|
|
26
|
+
.put("signalStrength", -45)
|
|
27
|
+
.put("password", secret)
|
|
28
|
+
.put("token", secret)
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
ShadowLog.clear()
|
|
32
|
+
BleTraceLogger.logJson(
|
|
33
|
+
"glasses_to_phone",
|
|
34
|
+
"sdk_ble_event",
|
|
35
|
+
JSONObject().put("type", "wifi_scan_result").put("networks_neo", networks),
|
|
36
|
+
)
|
|
37
|
+
val message = ShadowLog.getLogsForTag("MentraBleTrace").single().msg
|
|
38
|
+
assertFalse(message.contains(secret))
|
|
39
|
+
val actual = JSONObject(message.substringAfter("payload=")).getJSONArray("networks_neo")
|
|
40
|
+
for ((index, secured) in listOf(true, false).withIndex()) {
|
|
41
|
+
val item = actual.getJSONObject(index)
|
|
42
|
+
assertEquals(secured, item.get("requiresPassword"))
|
|
43
|
+
assertEquals(-45, item.getInt("signalStrength"))
|
|
44
|
+
assertEquals("<redacted>", item.getString("password"))
|
|
45
|
+
assertEquals("<redacted>", item.getString("token"))
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Test
|
|
50
|
+
fun exceptionDoesNotExposeNonBooleanOrOtherPasswordFields() {
|
|
51
|
+
val secret = UUID.randomUUID().toString()
|
|
52
|
+
ShadowLog.clear()
|
|
53
|
+
BleTraceLogger.logJson(
|
|
54
|
+
"glasses_to_phone",
|
|
55
|
+
"sdk_ble_event",
|
|
56
|
+
JSONObject()
|
|
57
|
+
.put("type", "wifi_scan_result")
|
|
58
|
+
.put("requiresPassword", secret)
|
|
59
|
+
.put("password", true)
|
|
60
|
+
.put("RequiresPassword", false),
|
|
61
|
+
)
|
|
62
|
+
val message = ShadowLog.getLogsForTag("MentraBleTrace").single().msg
|
|
63
|
+
assertFalse(message.contains(secret))
|
|
64
|
+
val actual = JSONObject(message.substringAfter("payload="))
|
|
65
|
+
for (key in listOf("requiresPassword", "password", "RequiresPassword")) {
|
|
66
|
+
assertEquals("<redacted>", actual.getString(key))
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -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
|
+
}
|
package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscoveryTest.kt
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import android.bluetooth.BluetoothAdapter
|
|
4
|
+
import android.bluetooth.BluetoothDevice
|
|
5
|
+
import android.os.Handler
|
|
6
|
+
import android.os.Looper
|
|
7
|
+
import android.os.ParcelUuid
|
|
8
|
+
import java.time.Duration
|
|
9
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
10
|
+
import org.junit.Test
|
|
11
|
+
import org.junit.runner.RunWith
|
|
12
|
+
import org.robolectric.RobolectricTestRunner
|
|
13
|
+
import org.robolectric.Shadows.shadowOf
|
|
14
|
+
import org.robolectric.annotation.Config
|
|
15
|
+
import org.robolectric.annotation.LooperMode
|
|
16
|
+
|
|
17
|
+
@RunWith(RobolectricTestRunner::class)
|
|
18
|
+
@Config(sdk = [33])
|
|
19
|
+
@LooperMode(LooperMode.Mode.PAUSED)
|
|
20
|
+
class ClassicAudioServiceDiscoveryTest {
|
|
21
|
+
private val target = BluetoothAdapter.getDefaultAdapter().getRemoteDevice("AA:BB:CC:DD:EE:FF")
|
|
22
|
+
private val sink = ParcelUuid.fromString("0000110b-0000-1000-8000-00805f9b34fb")
|
|
23
|
+
private val zero = ParcelUuid.fromString("00000000-0000-0000-0000-000000000000")
|
|
24
|
+
private val messages = mutableListOf<String>()
|
|
25
|
+
private val discovery = ClassicAudioServiceDiscovery(Handler(Looper.getMainLooper()), 30000, messages::add)
|
|
26
|
+
private var connects = 0
|
|
27
|
+
|
|
28
|
+
private fun request(device: BluetoothDevice = target) {
|
|
29
|
+
discovery.connectWhenDiscovered(device) { connects++ }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Test fun `bonded Samsung zero UUID inventory waits for real Audio Sink discovery`() {
|
|
33
|
+
shadowOf(target).setBondState(BluetoothDevice.BOND_BONDED)
|
|
34
|
+
shadowOf(target).setUuids(arrayOf(zero))
|
|
35
|
+
shadowOf(target).setFetchUuidsWithSdpResult(true)
|
|
36
|
+
request()
|
|
37
|
+
assertThat(connects).isZero()
|
|
38
|
+
assertThat(shadowOf(target).fetchUuidsWithSdpCount).isEqualTo(1)
|
|
39
|
+
shadowOf(target).setUuids(arrayOf(sink))
|
|
40
|
+
discovery.onUuids(target)
|
|
41
|
+
discovery.onUuids(target)
|
|
42
|
+
assertThat(connects).isEqualTo(1)
|
|
43
|
+
shadowOf(Looper.getMainLooper()).idleFor(Duration.ofSeconds(30))
|
|
44
|
+
assertThat(messages.none { it.contains("timed out") }).isTrue()
|
|
45
|
+
assertThat(target.bondState).isEqualTo(BluetoothDevice.BOND_BONDED)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Test fun `valid cached Audio Sink needs no refresh`() {
|
|
49
|
+
shadowOf(target).setUuids(arrayOf(sink))
|
|
50
|
+
request()
|
|
51
|
+
assertThat(connects).isEqualTo(1)
|
|
52
|
+
assertThat(shadowOf(target).fetchUuidsWithSdpCount).isZero()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test fun `early empty UUID broadcast preserves the wait for the actual SDP result`() {
|
|
56
|
+
shadowOf(target).setUuids(arrayOf(zero))
|
|
57
|
+
shadowOf(target).setFetchUuidsWithSdpResult(true)
|
|
58
|
+
request()
|
|
59
|
+
discovery.onUuids(target)
|
|
60
|
+
assertThat(connects).isZero()
|
|
61
|
+
shadowOf(target).setUuids(arrayOf(sink))
|
|
62
|
+
discovery.onUuids(target)
|
|
63
|
+
assertThat(connects).isEqualTo(1)
|
|
64
|
+
assertThat(shadowOf(target).fetchUuidsWithSdpCount).isEqualTo(1)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Test fun `unrelated device and duplicate requests cannot complete or multiply discovery`() {
|
|
68
|
+
shadowOf(target).setFetchUuidsWithSdpResult(true)
|
|
69
|
+
request()
|
|
70
|
+
request()
|
|
71
|
+
val other = BluetoothAdapter.getDefaultAdapter().getRemoteDevice("11:22:33:44:55:66")
|
|
72
|
+
shadowOf(other).setUuids(arrayOf(sink))
|
|
73
|
+
discovery.onUuids(other)
|
|
74
|
+
assertThat(connects).isZero()
|
|
75
|
+
assertThat(shadowOf(target).fetchUuidsWithSdpCount).isEqualTo(1)
|
|
76
|
+
shadowOf(target).setUuids(arrayOf(sink))
|
|
77
|
+
discovery.onUuids(target)
|
|
78
|
+
assertThat(connects).isEqualTo(1)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Test fun `rejected SDP request does not connect on a later unsolicited event`() {
|
|
82
|
+
shadowOf(target).setFetchUuidsWithSdpResult(false)
|
|
83
|
+
request()
|
|
84
|
+
shadowOf(target).setUuids(arrayOf(sink))
|
|
85
|
+
discovery.onUuids(target)
|
|
86
|
+
assertThat(connects).isZero()
|
|
87
|
+
assertThat(messages.last()).contains("rejected")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@Test fun `missing UUID broadcast times out without connecting on a late event`() {
|
|
91
|
+
shadowOf(target).setFetchUuidsWithSdpResult(true)
|
|
92
|
+
request()
|
|
93
|
+
shadowOf(Looper.getMainLooper()).idleFor(Duration.ofSeconds(20))
|
|
94
|
+
discovery.onUuids(target)
|
|
95
|
+
shadowOf(Looper.getMainLooper()).idleFor(Duration.ofSeconds(10))
|
|
96
|
+
shadowOf(target).setUuids(arrayOf(sink))
|
|
97
|
+
discovery.onUuids(target)
|
|
98
|
+
assertThat(connects).isZero()
|
|
99
|
+
assertThat(messages.last()).contains("timed out")
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@Test fun `teardown cancellation invalidates callback and removes its timeout`() {
|
|
103
|
+
shadowOf(target).setFetchUuidsWithSdpResult(true)
|
|
104
|
+
request()
|
|
105
|
+
discovery.cancel()
|
|
106
|
+
shadowOf(target).setUuids(arrayOf(sink))
|
|
107
|
+
discovery.onUuids(target)
|
|
108
|
+
shadowOf(Looper.getMainLooper()).idleFor(Duration.ofSeconds(30))
|
|
109
|
+
assertThat(connects).isZero()
|
|
110
|
+
assertThat(messages.none { it.contains("timed out") }).isTrue()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@Test fun `previous session timeout cannot cancel rediscovery for the same glasses`() {
|
|
114
|
+
shadowOf(target).setFetchUuidsWithSdpResult(true)
|
|
115
|
+
request()
|
|
116
|
+
shadowOf(Looper.getMainLooper()).idleFor(Duration.ofSeconds(20))
|
|
117
|
+
discovery.cancel()
|
|
118
|
+
request()
|
|
119
|
+
shadowOf(Looper.getMainLooper()).idleFor(Duration.ofSeconds(10))
|
|
120
|
+
shadowOf(target).setUuids(arrayOf(sink))
|
|
121
|
+
discovery.onUuids(target)
|
|
122
|
+
assertThat(connects).isEqualTo(1)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -8,19 +8,19 @@ class G1TextSanitizerTest {
|
|
|
8
8
|
@Test
|
|
9
9
|
fun `strips Vietnamese diacritics unsupported by G1`() {
|
|
10
10
|
assertThat(sanitizeG1DisplayText("Tiếng Việt: Đặng, Nguyễn, phở bò"))
|
|
11
|
-
.isEqualTo("Tieng Viet: Dang, Nguyen, pho
|
|
11
|
+
.isEqualTo("Tieng Viet: Dang, Nguyen, pho bò")
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
@Test
|
|
15
15
|
fun `handles decomposed marks and preserves layout`() {
|
|
16
16
|
assertThat(sanitizeG1DisplayText("Cafe\u0301\nĐa Nang"))
|
|
17
|
-
.isEqualTo("
|
|
17
|
+
.isEqualTo("Café\nDa Nang")
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
@Test
|
|
21
|
-
fun `
|
|
21
|
+
fun `preserves mapped letters and expands only unmapped letters`() {
|
|
22
22
|
assertThat(sanitizeG1DisplayText("Øresund Łódź Æsir Œuvre Straße Ðingvellir Þingvellir"))
|
|
23
|
-
.isEqualTo("
|
|
23
|
+
.isEqualTo("Øresund Łódź Æsir OEuvre Straße Ðingvellir Þingvellir")
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
@Test
|
|
@@ -28,4 +28,29 @@ class G1TextSanitizerTest {
|
|
|
28
28
|
assertThat(sanitizeG1DisplayText("Hello, world! 123"))
|
|
29
29
|
.isEqualTo("Hello, world! 123")
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
@Test
|
|
33
|
+
fun `preserves Swedish accents case punctuation and spacing`() {
|
|
34
|
+
assertThat(sanitizeG1DisplayText("“Hallå, hallå”\nÅÄÖ åäö"))
|
|
35
|
+
.isEqualTo("“Hallå, hallå”\nÅÄÖ åäö")
|
|
36
|
+
assertThat(sanitizeG1DisplayText("Halla\u030A, halla\u030A"))
|
|
37
|
+
.isEqualTo("Hallå, hallå")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@Test
|
|
41
|
+
fun `preserves other scripts and is idempotent`() {
|
|
42
|
+
val otherScripts = "が カ\u3099 한\u302E Α\u0301 مُرَحَّبًا शि ❤️"
|
|
43
|
+
assertThat(sanitizeG1DisplayText(otherScripts)).isEqualTo(otherScripts)
|
|
44
|
+
val normalized = sanitizeG1DisplayText("Hallå Łódź Đặng Œuvre")
|
|
45
|
+
assertThat(normalized).isEqualTo("Hallå Łódź Dang OEuvre")
|
|
46
|
+
assertThat(sanitizeG1DisplayText(normalized)).isEqualTo(normalized)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Test
|
|
50
|
+
fun `preserves every embedded extended Latin glyph in composed and decomposed forms`() {
|
|
51
|
+
val glyphs = "ÀÂÇÈÉÊËÎÏÔÙÛÜàçèéêëîïôùûüÿŸÄäÖößẞâÁáÍíÑñúÓóÚⁱIJijŠšŽžÃÅÆÌÐÒÕØÝÞãåæìðòõøýþĄąĆćČčĎďĘęĚěĞğŁłŃńŇňŘřŚśŞşŤťŮůŹźŻżĂăıȘșȚțǖǘǚǜǎǐǒǔŐőŰű"
|
|
52
|
+
assertThat(sanitizeG1DisplayText(glyphs)).isEqualTo(glyphs)
|
|
53
|
+
val decomposed = java.text.Normalizer.normalize(glyphs, java.text.Normalizer.Form.NFD)
|
|
54
|
+
assertThat(sanitizeG1DisplayText(decomposed)).isEqualTo(glyphs)
|
|
55
|
+
}
|
|
31
56
|
}
|