@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,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
+ }
@@ -0,0 +1,133 @@
1
+ package com.mentra.bluetoothsdk.sgcs
2
+
3
+ import android.bluetooth.BluetoothA2dp
4
+ import android.bluetooth.BluetoothAdapter
5
+ import android.bluetooth.BluetoothDevice
6
+ import android.bluetooth.BluetoothGatt
7
+ import android.bluetooth.BluetoothHeadset
8
+ import android.bluetooth.BluetoothProfile
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 java.time.Duration
14
+ import org.junit.Assert.assertEquals
15
+ import org.junit.Assert.assertNull
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 org.robolectric.shadow.api.Shadow
26
+ import org.robolectric.shadows.ShadowBluetoothA2dp
27
+ import org.robolectric.shadows.ShadowBluetoothHeadset
28
+ import org.robolectric.util.ReflectionHelpers
29
+
30
+ @RunWith(RobolectricTestRunner::class)
31
+ @Config(sdk = [33], shadows = [G1PairingTest.ShadowLc3Cpp::class,
32
+ MentraLiveClassicDisconnectTest.RecordingA2dp::class,
33
+ MentraLiveClassicDisconnectTest.RecordingHeadset::class],
34
+ instrumentedPackages = ["com.mentra.lc3Lib"])
35
+ @LooperMode(LooperMode.Mode.PAUSED)
36
+ class MentraLiveClassicDisconnectTest {
37
+ private val adapter = BluetoothAdapter.getDefaultAdapter()
38
+ private val target = adapter.getRemoteDevice("AA:BB:CC:DD:EE:FF")
39
+ private val other = adapter.getRemoteDevice("11:22:33:44:55:66")
40
+ private val a2dp = Shadow.newInstanceOf(BluetoothA2dp::class.java)
41
+ private val headset = Shadow.newInstanceOf(BluetoothHeadset::class.java)
42
+ private val audio: RecordingA2dp = Shadow.extract(a2dp)
43
+ private val handsfree: RecordingHeadset = Shadow.extract(headset)
44
+
45
+ @Before fun setup() {
46
+ Bridge.initialize(ApplicationProvider.getApplicationContext())
47
+ shadowOf(adapter).setState(BluetoothAdapter.STATE_ON)
48
+ shadowOf(target).setName("MENTRA_LIVE_TEST")
49
+ shadowOf(target).setBondState(BluetoothDevice.BOND_BONDED)
50
+ DeviceStore.set("bluetooth", "device_name", "MENTRA_LIVE_TEST")
51
+ DeviceStore.set("bluetooth", "device_address", target.address)
52
+ DeviceStore.set("bluetooth", "pending_device_name", "MENTRA_LIVE_TEST")
53
+ DeviceStore.set("bluetooth", "pending_device_address", target.address)
54
+ }
55
+
56
+ @Test fun `BLE loss followed by explicit disconnect releases both Classic profiles and keeps bond`() {
57
+ assertDisconnect(dropBle = true)
58
+ }
59
+
60
+ @Test fun `explicit disconnect with BLE still connected releases both Classic profiles`() {
61
+ assertDisconnect(dropBle = false)
62
+ }
63
+
64
+ private fun assertDisconnect(dropBle: Boolean) {
65
+ val live = MentraLive()
66
+ try {
67
+ live.connectById("MENTRA_LIVE_TEST")
68
+ shadowOf(Looper.getMainLooper()).idle()
69
+ shadowOf(target).simulateGattConnectionChange(BluetoothGatt.GATT_SUCCESS, BluetoothProfile.STATE_CONNECTED)
70
+ shadowOf(Looper.getMainLooper()).idle()
71
+ if (dropBle) {
72
+ shadowOf(target).simulateGattConnectionChange(BluetoothGatt.GATT_SUCCESS, BluetoothProfile.STATE_DISCONNECTED)
73
+ shadowOf(Looper.getMainLooper()).idle()
74
+ assertNull(ReflectionHelpers.getField<BluetoothDevice?>(live, "connectedDevice"))
75
+ }
76
+ installConnectedProfiles()
77
+ live.disconnect()
78
+ shadowOf(Looper.getMainLooper()).idle()
79
+ assertEquals(listOf(target), audio.disconnected)
80
+ assertEquals(listOf(target), handsfree.disconnected)
81
+ assertEquals(BluetoothProfile.STATE_CONNECTED, a2dp.getConnectionState(other))
82
+ assertEquals(BluetoothProfile.STATE_CONNECTED, headset.getConnectionState(other))
83
+ assertEquals(BluetoothDevice.BOND_BONDED, target.bondState)
84
+ } finally {
85
+ live.destroy()
86
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofSeconds(2))
87
+ }
88
+ }
89
+
90
+ @Test fun `a manager with no connection session does not disconnect an unrelated bonded device`() {
91
+ val live = MentraLive()
92
+ try {
93
+ installConnectedProfiles()
94
+ live.disconnect()
95
+ shadowOf(Looper.getMainLooper()).idle()
96
+ assertEquals(emptyList<BluetoothDevice>(), audio.disconnected)
97
+ assertEquals(emptyList<BluetoothDevice>(), handsfree.disconnected)
98
+ } finally {
99
+ live.destroy()
100
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofSeconds(2))
101
+ }
102
+ }
103
+
104
+ private fun installConnectedProfiles() {
105
+ for (device in listOf(target, other)) {
106
+ audio.addDevice(device, BluetoothProfile.STATE_CONNECTED)
107
+ handsfree.addDevice(device, BluetoothProfile.STATE_CONNECTED)
108
+ }
109
+ shadowOf(adapter).setProfileProxy(BluetoothProfile.A2DP, a2dp)
110
+ shadowOf(adapter).setProfileProxy(BluetoothProfile.HEADSET, headset)
111
+ }
112
+
113
+ // The real MentraLive lifecycle runs; only Android's audio profile services are simulated.
114
+ @Implements(BluetoothA2dp::class)
115
+ class RecordingA2dp : ShadowBluetoothA2dp() {
116
+ val disconnected = mutableListOf<BluetoothDevice>()
117
+ @Implementation fun disconnect(device: BluetoothDevice): Boolean {
118
+ disconnected.add(device)
119
+ removeDevice(device)
120
+ return true
121
+ }
122
+ }
123
+
124
+ @Implements(BluetoothHeadset::class)
125
+ class RecordingHeadset : ShadowBluetoothHeadset() {
126
+ val disconnected = mutableListOf<BluetoothDevice>()
127
+ @Implementation fun disconnect(device: BluetoothDevice): Boolean {
128
+ disconnected.add(device)
129
+ removeDevice(device)
130
+ return true
131
+ }
132
+ }
133
+ }
@@ -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,126 @@
1
+ package com.mentra.bluetoothsdk.sgcs
2
+
3
+ import android.bluetooth.BluetoothAdapter
4
+ import android.bluetooth.BluetoothGattCallback
5
+ import android.bluetooth.BluetoothGattCharacteristic
6
+ import android.os.Looper
7
+ import androidx.test.core.app.ApplicationProvider
8
+ import com.mentra.bluetoothsdk.BluetoothSdkException
9
+ import com.mentra.bluetoothsdk.Bridge
10
+ import com.mentra.bluetoothsdk.DeviceStore
11
+ import java.nio.charset.StandardCharsets
12
+ import java.time.Duration
13
+ import java.util.UUID
14
+ import java.util.concurrent.atomic.AtomicLong
15
+ import org.junit.Assert.*
16
+ import org.junit.Test
17
+ import org.junit.runner.RunWith
18
+ import org.robolectric.RobolectricTestRunner
19
+ import org.robolectric.Shadows.shadowOf
20
+ import org.robolectric.annotation.Config
21
+ import org.robolectric.annotation.LooperMode
22
+ import org.robolectric.shadows.ShadowLog
23
+
24
+ @RunWith(RobolectricTestRunner::class)
25
+ @Config(sdk = [33], shadows = [G1PairingTest.ShadowLc3Cpp::class], instrumentedPackages = ["com.mentra.lc3Lib"])
26
+ @LooperMode(LooperMode.Mode.PAUSED)
27
+ class MentraLiveIncidentDispatchTest {
28
+ private val incident = "rep_01M4HBVT88DFSN8TYNZ1WTV4PB"
29
+ private val apiBase = "https://core.dev.us-west-2.mentraglass.com"
30
+
31
+ @Test fun `missing native BLE link rejects even when cached status is connected`() = withLive { live ->
32
+ DeviceStore.set("glasses", "connected", true)
33
+ expectCode("glasses_not_connected") { live.sendIncidentId(incident, apiBase) }
34
+ assertTrue(queuedWrites(live).isEmpty())
35
+ }
36
+
37
+ @Test fun `missing current token does not fall back to old stored credentials`() = withLive { live ->
38
+ ready(live)
39
+ DeviceStore.set("bluetooth", "core_token", "")
40
+ val context = ApplicationProvider.getApplicationContext<android.content.Context>()
41
+ val prefs = context.getSharedPreferences("augmentos_auth_prefs", android.content.Context.MODE_PRIVATE)
42
+ prefs.edit().putString("core_token", "stale-synthetic-token").commit()
43
+ expectCode("core_token_unavailable") { live.sendIncidentId(incident, apiBase) }
44
+ assertTrue(queuedWrites(live).isEmpty())
45
+ prefs.edit().remove("core_token").commit()
46
+ }
47
+
48
+ @Test fun `current token is queued before upload and never logged`() = withLive { live ->
49
+ ready(live)
50
+ val token = "synthetic-${UUID.randomUUID()}"
51
+ DeviceStore.set("bluetooth", "core_token", token)
52
+ ShadowLog.clear()
53
+ live.sendIncidentId(incident, apiBase)
54
+ val writes = queuedWrites(live)
55
+ val wire = writes.joinToString("") { String(it, StandardCharsets.UTF_8) }
56
+ assertEquals(2, writes.size)
57
+ assertTrue(wire.indexOf("auth_token") < wire.indexOf("upload_incident_logs"))
58
+ assertTrue(wire.contains(token))
59
+ assertTrue(wire.contains(incident))
60
+ assertFalse("token logged", ShadowLog.getLogs().any { it.msg.contains(token) })
61
+ }
62
+
63
+ @Test fun `link losing TX characteristic rejects instead of retaining incident relay`() = withLive { live ->
64
+ ready(live)
65
+ DeviceStore.set("bluetooth", "core_token", "synthetic")
66
+ set(live, "txCharacteristic", null)
67
+ expectCode("glasses_not_connected") { live.sendIncidentId(incident, apiBase) }
68
+ assertTrue(queuedWrites(live).isEmpty())
69
+ val relays = get(live, "bleIncidentLogRelays") as Map<*, *>
70
+ assertTrue(relays.isEmpty())
71
+ }
72
+
73
+ @Test fun `session loss during token serialization cannot queue an upload into the next session`() = withLive { live ->
74
+ ready(live)
75
+ DeviceStore.set("bluetooth", "core_token", "synthetic-token")
76
+ val generation = get(live, "bleSessionGeneration") as AtomicLong
77
+ val sink = Bridge.addEventSink { type, body ->
78
+ if (type == "log" && body["message"].toString().contains("<auth_token with credentials omitted>")) {
79
+ generation.incrementAndGet()
80
+ }
81
+ }
82
+ try {
83
+ expectCode("incident_dispatch_failed") { live.sendIncidentId(incident, apiBase) }
84
+ val wire = queuedWrites(live).joinToString("") { String(it, StandardCharsets.UTF_8) }
85
+ assertFalse(wire.contains("upload_incident_logs"))
86
+ assertTrue((get(live, "bleIncidentLogRelays") as Map<*, *>).isEmpty())
87
+ } finally { Bridge.removeEventSink(sink) }
88
+ }
89
+
90
+ private fun ready(live: MentraLive) {
91
+ val context = ApplicationProvider.getApplicationContext<android.content.Context>()
92
+ val gatt = BluetoothAdapter.getDefaultAdapter().getRemoteDevice("AA:BB:CC:DD:EE:FF")
93
+ .connectGatt(context, false, object : BluetoothGattCallback() {})
94
+ set(live, "isConnected", true)
95
+ set(live, "bluetoothGatt", gatt)
96
+ set(live, "txCharacteristic", BluetoothGattCharacteristic(UUID.randomUUID(), 0, 0))
97
+ }
98
+
99
+ private fun withLive(body: (MentraLive) -> Unit) {
100
+ Bridge.initialize(ApplicationProvider.getApplicationContext())
101
+ val live = MentraLive()
102
+ val token = DeviceStore.get("bluetooth", "core_token")
103
+ val connected = DeviceStore.get("glasses", "connected")
104
+ try { body(live) } finally {
105
+ DeviceStore.set("bluetooth", "core_token", token ?: "")
106
+ DeviceStore.set("glasses", "connected", connected ?: false)
107
+ live.destroy()
108
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofSeconds(2))
109
+ }
110
+ }
111
+
112
+ private fun expectCode(code: String, body: () -> Unit) {
113
+ try { body(); fail("Expected $code") } catch (error: BluetoothSdkException) { assertEquals(code, error.code) }
114
+ }
115
+
116
+ private fun get(live: MentraLive, name: String): Any? =
117
+ MentraLive::class.java.getDeclaredField(name).apply { isAccessible = true }.get(live)
118
+
119
+ private fun set(live: MentraLive, name: String, value: Any?) {
120
+ MentraLive::class.java.getDeclaredField(name).apply { isAccessible = true }.set(live, value)
121
+ }
122
+
123
+ private fun queuedWrites(live: MentraLive): List<ByteArray> = (get(live, "sendQueue") as Iterable<*>).map { write ->
124
+ write!!.javaClass.getDeclaredField("data").apply { isAccessible = true }.get(write) as ByteArray
125
+ }
126
+ }
@@ -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
+ }