@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,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
|
+
}
|
package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveFileTransferSessionTest.kt
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.*
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
import org.junit.runner.RunWith
|
|
6
|
+
import org.robolectric.RobolectricTestRunner
|
|
7
|
+
import org.robolectric.annotation.Config
|
|
8
|
+
|
|
9
|
+
@RunWith(RobolectricTestRunner::class)
|
|
10
|
+
@Config(sdk = [33])
|
|
11
|
+
class MentraLiveFileTransferSessionTest {
|
|
12
|
+
@Test fun dynamicPayloadUsesTrueSizeEvenWhenDivisibleBy400() {
|
|
13
|
+
for (pack in listOf(244, 474, 800)) {
|
|
14
|
+
val session = MentraLive.FileTransferSession("photo", 80000, 2)
|
|
15
|
+
session.recalculateTotalPackets(pack)
|
|
16
|
+
assertEquals((80000 + pack - 1) / pack, session.totalPackets)
|
|
17
|
+
for (index in 0 until session.totalPackets) {
|
|
18
|
+
assertTrue(session.addPacket(index, ByteArray(minOf(pack, 80000 - index * pack))))
|
|
19
|
+
}
|
|
20
|
+
assertTrue(session.isComplete)
|
|
21
|
+
assertEquals(80000, session.assembleFile()!!.size)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Test fun legacyInflatedSizeAndLargeUnflaggedPayloads() {
|
|
26
|
+
// Flag 1 enables batched ACKs but still uses legacy size semantics.
|
|
27
|
+
for ((pack, headerSize, actualSize, packets) in listOf(
|
|
28
|
+
listOf(221, 80000, 43982, 200),
|
|
29
|
+
listOf(400, 80000, 79777, 200),
|
|
30
|
+
listOf(800, 80000, 80000, 100))) {
|
|
31
|
+
val session = MentraLive.FileTransferSession("photo", headerSize, 1)
|
|
32
|
+
session.recalculateTotalPackets(pack)
|
|
33
|
+
assertEquals(packets, session.totalPackets)
|
|
34
|
+
val source = ByteArray(actualSize) { (it % 251).toByte() }
|
|
35
|
+
for (index in (0 until packets).reversed()) {
|
|
36
|
+
val start = index * pack
|
|
37
|
+
assertTrue(session.addPacket(index, source.copyOfRange(start, minOf(start + pack, actualSize))))
|
|
38
|
+
}
|
|
39
|
+
assertTrue(session.isComplete)
|
|
40
|
+
assertArrayEquals(source, session.assembleFile())
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test fun malformedOrDuplicatePacketsCannotCompleteFile() {
|
|
45
|
+
val session = MentraLive.FileTransferSession("photo", 1000, 2)
|
|
46
|
+
session.recalculateTotalPackets(800)
|
|
47
|
+
assertFalse(session.addPacket(0, ByteArray(799)))
|
|
48
|
+
assertFalse(session.addPacket(2, ByteArray(200)))
|
|
49
|
+
assertTrue(session.addPacket(1, ByteArray(200)))
|
|
50
|
+
assertFalse(session.isComplete)
|
|
51
|
+
assertFalse(session.addPacket(1, ByteArray(200)))
|
|
52
|
+
assertTrue(session.addPacket(0, ByteArray(800)))
|
|
53
|
+
assertEquals(1000, session.assembleFile()!!.size)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -78,6 +78,39 @@ class MentraLiveGattCallbackTest {
|
|
|
78
78
|
assertArrayEquals(byteArrayOf(3, 2), callback.packets[1])
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
@Test
|
|
82
|
+
@Config(sdk = [33])
|
|
83
|
+
@Suppress("DEPRECATION")
|
|
84
|
+
fun `modern and legacy callbacks for one notification deliver one packet`() {
|
|
85
|
+
val queue = ArrayDeque<() -> Unit>()
|
|
86
|
+
val callback = RecordingCallback({ work -> queue.addLast(work) })
|
|
87
|
+
val gatt = gatt()
|
|
88
|
+
val characteristic = BluetoothGattCharacteristic(UUID.randomUUID(), 0, 0)
|
|
89
|
+
val value = byteArrayOf(0xf1.toByte(), 7, 1, 2)
|
|
90
|
+
characteristic.value = value
|
|
91
|
+
callback.onCharacteristicChanged(gatt, characteristic, value)
|
|
92
|
+
callback.onCharacteristicChanged(gatt, characteristic)
|
|
93
|
+
value[2] = 9
|
|
94
|
+
while (queue.isNotEmpty()) queue.removeFirst().invoke()
|
|
95
|
+
assertEquals(1, callback.packets.size)
|
|
96
|
+
assertArrayEquals(byteArrayOf(0xf1.toByte(), 7, 1, 2), callback.packets.single())
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@Test
|
|
100
|
+
@Config(sdk = [33])
|
|
101
|
+
fun `separate modern notifications retain identical payloads`() {
|
|
102
|
+
val queue = ArrayDeque<() -> Unit>()
|
|
103
|
+
val callback = RecordingCallback({ work -> queue.addLast(work) })
|
|
104
|
+
val gatt = gatt()
|
|
105
|
+
val characteristic = BluetoothGattCharacteristic(UUID.randomUUID(), 0, 0)
|
|
106
|
+
val value = byteArrayOf(1, 2)
|
|
107
|
+
repeat(2) { callback.onCharacteristicChanged(gatt, characteristic, value) }
|
|
108
|
+
value[0] = 9
|
|
109
|
+
while (queue.isNotEmpty()) queue.removeFirst().invoke()
|
|
110
|
+
assertEquals(2, callback.packets.size)
|
|
111
|
+
callback.packets.forEach { assertArrayEquals(byteArrayOf(1, 2), it) }
|
|
112
|
+
}
|
|
113
|
+
|
|
81
114
|
@Test
|
|
82
115
|
fun `replacement invalidates callbacks already waiting on the lifecycle queue`() {
|
|
83
116
|
val queue = ArrayDeque<() -> Unit>()
|
|
@@ -0,0 +1,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
|
+
}
|