@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.
Files changed (191) hide show
  1. package/README.md +72 -10
  2. package/android/build.gradle +15 -0
  3. package/android/lc3Lib/build.gradle +6 -1
  4. package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +368 -0
  5. package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocol.kt +156 -0
  6. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +63 -14
  7. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTracker.kt +55 -1
  8. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +4 -1
  9. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkInstallId.kt +23 -0
  10. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +8 -2
  11. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheck.kt +184 -0
  12. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTracker.kt +388 -0
  13. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
  14. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +133 -26
  15. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
  16. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +3 -2
  17. package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +6 -6
  18. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +227 -36
  19. package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +24 -0
  20. package/android/src/main/java/com/mentra/bluetoothsdk/NetworkDiagnostics.kt +25 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
  22. package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +3 -0
  23. package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +1 -1
  24. package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +33 -0
  25. package/android/src/main/java/com/mentra/bluetoothsdk/services/G2ConnectionRecovery.kt +47 -0
  26. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +4 -0
  27. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscovery.kt +70 -0
  28. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
  29. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +11 -32
  30. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
  31. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/LatinTextSanitizer.kt +39 -0
  32. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +194 -40
  33. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -8
  34. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +314 -395
  35. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodec.kt +247 -0
  36. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinator.kt +238 -0
  37. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasEncoder.kt +58 -0
  38. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSession.kt +180 -0
  39. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -0
  40. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddress.kt +17 -0
  41. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +5 -0
  42. package/android/src/main/java/com/mentra/bluetoothsdk/utils/G2Text.kt +7 -0
  43. package/android/src/release/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +20 -0
  44. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTrackerTest.kt +58 -3
  45. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheckTest.kt +124 -0
  46. package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTrackerTest.kt +263 -0
  47. package/android/src/test/java/com/mentra/bluetoothsdk/DashboardContentFormatterTest.kt +2 -2
  48. package/android/src/test/java/com/mentra/bluetoothsdk/DeviceIdentityPairingTest.kt +59 -0
  49. package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +666 -0
  50. package/android/src/test/java/com/mentra/bluetoothsdk/G2TextTest.kt +13 -0
  51. package/android/src/test/java/com/mentra/bluetoothsdk/NetworkDiagnosticsTest.kt +55 -0
  52. package/android/src/test/java/com/mentra/bluetoothsdk/ScanCompletionTest.kt +5 -1
  53. package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
  54. package/android/src/test/java/com/mentra/bluetoothsdk/debug/BleTraceLoggerTest.kt +69 -0
  55. package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
  56. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscoveryTest.kt +124 -0
  57. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
  58. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +29 -4
  59. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2DisplayThreadTest.kt +120 -0
  60. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2PairingTargetTest.kt +90 -0
  61. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveClassicDisconnectTest.kt +133 -0
  62. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveFileTransferSessionTest.kt +55 -0
  63. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +33 -0
  64. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveIncidentDispatchTest.kt +126 -0
  65. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingTargetTest.kt +65 -0
  66. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveWifiCredentialLogTest.kt +55 -0
  67. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraNexPairingTest.kt +151 -0
  68. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoBitmapAcceptanceTest.kt +105 -0
  69. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodecTest.kt +233 -0
  70. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinatorTest.kt +348 -0
  71. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +339 -0
  72. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoMicReconnectTest.kt +93 -0
  73. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +11 -0
  74. package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddressTest.kt +27 -0
  75. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnosticsTest.kt +287 -0
  76. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocolTest.kt +127 -0
  77. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoLifecycleTest.kt +288 -0
  78. package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +205 -0
  79. package/build/BluetoothSdk.types.d.ts +16 -0
  80. package/build/BluetoothSdk.types.d.ts.map +1 -1
  81. package/build/BluetoothSdk.types.js +1 -0
  82. package/build/BluetoothSdk.types.js.map +1 -1
  83. package/build/_private/BluetoothSdkModule.d.ts +4 -0
  84. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  85. package/build/_private/BluetoothSdkModule.js +5 -1
  86. package/build/_private/BluetoothSdkModule.js.map +1 -1
  87. package/build/_private/MentraLocalNetworkModule.d.ts +23 -0
  88. package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
  89. package/build/_private/MentraLocalNetworkModule.js.map +1 -1
  90. package/build/_private/nativeLogConsole.d.ts +19 -0
  91. package/build/_private/nativeLogConsole.d.ts.map +1 -1
  92. package/build/_private/nativeLogConsole.js +12 -0
  93. package/build/_private/nativeLogConsole.js.map +1 -1
  94. package/build/generated/changelogCatalog.d.ts +5 -2
  95. package/build/generated/changelogCatalog.d.ts.map +1 -1
  96. package/build/generated/changelogCatalog.js +6 -2
  97. package/build/generated/changelogCatalog.js.map +1 -1
  98. package/build/generated/releaseMetadata.js +6 -6
  99. package/build/generated/releaseMetadata.js.map +1 -1
  100. package/build/index.d.ts.map +1 -1
  101. package/build/index.js +1 -0
  102. package/build/index.js.map +1 -1
  103. package/build/ota-transport/index.d.ts +4 -0
  104. package/build/ota-transport/index.d.ts.map +1 -1
  105. package/build/ota-transport/index.js +2 -0
  106. package/build/ota-transport/index.js.map +1 -1
  107. package/ios/BluetoothSdkModule.swift +17 -4
  108. package/ios/MentraBluetoothSDK.podspec +3 -2
  109. package/ios/Source/BluetoothBackgroundMode.swift +32 -0
  110. package/ios/Source/BluetoothSdkDefaults.swift +7 -2
  111. package/ios/Source/Bridge.swift +15 -2
  112. package/ios/Source/DeviceManager.swift +102 -66
  113. package/ios/Source/DeviceStore.swift +26 -7
  114. package/ios/Source/GeneratedChangelogCatalog.swift +3 -2
  115. package/ios/Source/GeneratedReleaseMetadata.swift +6 -6
  116. package/ios/Source/MentraBluetoothSDK.swift +313 -54
  117. package/ios/Source/audio/GlassesMicWatchdog.swift +33 -0
  118. package/ios/Source/internal/BluetoothAvailability.swift +6 -0
  119. package/ios/Source/internal/BluetoothSdkAnalytics.swift +87 -13
  120. package/ios/Source/internal/BluetoothSdkAnalyticsTracker.swift +54 -1
  121. package/ios/Source/internal/BluetoothSdkInstallId.swift +21 -0
  122. package/ios/Source/internal/BluetoothSdkUpdateCheck.swift +223 -0
  123. package/ios/Source/internal/BluetoothSdkUsageTracker.swift +428 -0
  124. package/ios/Source/requests/DisplayRequests.swift +1 -1
  125. package/ios/Source/requests/VersionInfoResponseAccumulator.swift +11 -1
  126. package/ios/Source/sgcs/Ar99.swift +1 -2
  127. package/ios/Source/sgcs/G1.swift +68 -38
  128. package/ios/Source/sgcs/G1ConnectionTarget.swift +13 -0
  129. package/ios/Source/sgcs/G2.swift +261 -247
  130. package/ios/Source/sgcs/G2ConnectionSession.swift +101 -0
  131. package/ios/Source/sgcs/Mach1.swift +19 -7
  132. package/ios/Source/sgcs/MentraLive.swift +272 -197
  133. package/ios/Source/sgcs/MentraLiveFileTransferSession.swift +137 -0
  134. package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +47 -0
  135. package/ios/Source/sgcs/MentraNex.swift +8 -31
  136. package/ios/Source/sgcs/NexConnectionTarget.swift +15 -0
  137. package/ios/Source/sgcs/Nimo.swift +280 -408
  138. package/ios/Source/sgcs/NimoCanvasCodec.swift +272 -0
  139. package/ios/Source/sgcs/NimoCanvasCoordinator.swift +233 -0
  140. package/ios/Source/sgcs/NimoCanvasImage.swift +53 -0
  141. package/ios/Source/sgcs/NimoCanvasSession.swift +166 -0
  142. package/ios/Source/sgcs/NimoDiscovery.swift +49 -0
  143. package/ios/Source/sgcs/SGCManager.swift +25 -1
  144. package/ios/Source/sgcs/VuzixConnectionTarget.swift +15 -0
  145. package/ios/Source/status/DeviceStatus.swift +1 -0
  146. package/ios/Source/stt/SherpaOnnxTranscriber.swift +49 -189
  147. package/ios/Source/utils/AudioSessionMonitor.swift +44 -8
  148. package/ios/Source/utils/DemandDrivenAudioWorker.swift +150 -0
  149. package/ios/Source/utils/G1Text.swift +2 -24
  150. package/ios/Source/utils/G1TextSanitizer.swift +27 -0
  151. package/ios/Source/utils/G2Text.swift +11 -0
  152. package/ios/Source/utils/LatinTextSanitizer.swift +48 -0
  153. package/ios/Tests/BluetoothAudioRouteTests.swift +26 -0
  154. package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -0
  155. package/ios/Tests/BluetoothSdkAnalyticsTrackerTests.swift +54 -3
  156. package/ios/Tests/BluetoothSdkUpdateCheckTests.swift +153 -0
  157. package/ios/Tests/BluetoothSdkUsageTrackerTests.swift +296 -0
  158. package/ios/Tests/DashboardContentFormatterTests.swift +2 -2
  159. package/ios/Tests/DashboardRenderingTests.swift +31 -0
  160. package/ios/Tests/DemandDrivenAudioWorkerTests.swift +157 -0
  161. package/ios/Tests/DeviceIdentityPairingTests.swift +49 -0
  162. package/ios/Tests/DeviceManagerReconnectTests.swift +362 -0
  163. package/ios/Tests/G1PairingTests.swift +162 -0
  164. package/ios/Tests/G1TextSanitizerTests.swift +32 -0
  165. package/ios/Tests/G2ConnectionSessionTests.swift +172 -0
  166. package/ios/Tests/G2TextTests.swift +10 -0
  167. package/ios/Tests/IncidentLogDispatchTests.swift +111 -0
  168. package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
  169. package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
  170. package/ios/Tests/MentraLiveSessionReadinessTests.swift +163 -0
  171. package/ios/Tests/NimoCanvasCodecTests.swift +217 -0
  172. package/ios/Tests/NimoCanvasSessionTests.swift +181 -0
  173. package/ios/Tests/NimoCanvasTransportTests.swift +186 -0
  174. package/ios/Tests/NimoDeviceCommandsTests.swift +16 -0
  175. package/ios/Tests/NimoDiscoveryTests.swift +79 -0
  176. package/ios/Tests/SGCConnectionTargetTests.swift +48 -0
  177. package/ios/Tests/VersionInfoCommandTests.swift +194 -0
  178. package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
  179. package/ios/Tests/VideoRecordingCommandTests.swift +81 -0
  180. package/ios/Tests/WifiCredentialLoggingTests.swift +42 -0
  181. package/package.json +2 -2
  182. package/plugin/build/withIos.d.ts +2 -0
  183. package/plugin/build/withIos.js +12 -1
  184. package/src/BluetoothSdk.types.ts +15 -0
  185. package/src/_private/BluetoothSdkModule.ts +10 -1
  186. package/src/_private/MentraLocalNetworkModule.ts +28 -0
  187. package/src/_private/nativeLogConsole.ts +19 -0
  188. package/src/generated/changelogCatalog.ts +6 -2
  189. package/src/generated/releaseMetadata.ts +6 -6
  190. package/src/index.ts +1 -0
  191. 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
- assertThat(ScanCallback::class.java.declaredMethods.map { it.name })
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
+ }
@@ -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 bo")
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("Cafe\nDa Nang")
17
+ .isEqualTo("Café\nDa Nang")
18
18
  }
19
19
 
20
20
  @Test
21
- fun `expands Latin letters that do not decompose into combining marks`() {
21
+ fun `preserves mapped letters and expands only unmapped letters`() {
22
22
  assertThat(sanitizeG1DisplayText("Øresund Łódź Æsir Œuvre Straße Ðingvellir Þingvellir"))
23
- .isEqualTo("Oresund Lodz AEsir OEuvre Strasse Dingvellir THingvellir")
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
  }