@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,55 @@
1
+ package com.mentra.bluetoothsdk.sgcs
2
+
3
+ import android.os.Looper
4
+ import androidx.test.core.app.ApplicationProvider
5
+ import com.mentra.bluetoothsdk.Bridge
6
+ import java.nio.charset.StandardCharsets
7
+ import java.time.Duration
8
+ import java.util.UUID
9
+ import org.junit.Assert.assertFalse
10
+ import org.junit.Assert.assertTrue
11
+ import org.junit.Test
12
+ import org.junit.runner.RunWith
13
+ import org.robolectric.RobolectricTestRunner
14
+ import org.robolectric.Shadows.shadowOf
15
+ import org.robolectric.annotation.Config
16
+ import org.robolectric.annotation.LooperMode
17
+ import org.robolectric.shadows.ShadowLog
18
+
19
+ @RunWith(RobolectricTestRunner::class)
20
+ @Config(sdk = [33], shadows = [G1PairingTest.ShadowLc3Cpp::class], instrumentedPackages = ["com.mentra.lc3Lib"])
21
+ @LooperMode(LooperMode.Mode.PAUSED)
22
+ class MentraLiveWifiCredentialLogTest {
23
+ @Test fun `wifi credentials reach the BLE queue without being logged`() {
24
+ Bridge.initialize(ApplicationProvider.getApplicationContext())
25
+ val ssid = "synthetic-${UUID.randomUUID()}"
26
+ val password = UUID.randomUUID().toString()
27
+ val events = mutableListOf<String>()
28
+ val sink = Bridge.addEventSink { type, body -> if (type == "log") events.add(body["message"].toString()) }
29
+ val live = MentraLive()
30
+ try {
31
+ ShadowLog.clear()
32
+ live.sendWifiCredentials(ssid, password)
33
+
34
+ val logs = events + ShadowLog.getLogs().map { it.msg }
35
+ assertFalse("password logged", logs.any { it.contains(password) })
36
+ assertTrue(events.any { it.endsWith("LIVE: Sending data to glasses: <set_wifi_credentials with credentials omitted>") })
37
+ // The glasses still receive the unchanged credentials.
38
+ val wire = queuedWrites(live).joinToString("") { String(it, StandardCharsets.UTF_8) }
39
+ assertTrue(wire.contains("set_wifi_credentials"))
40
+ assertTrue(wire.contains(ssid))
41
+ assertTrue(wire.contains(password))
42
+ } finally {
43
+ Bridge.removeEventSink(sink)
44
+ live.destroy()
45
+ shadowOf(Looper.getMainLooper()).idleFor(Duration.ofSeconds(2))
46
+ }
47
+ }
48
+
49
+ private fun queuedWrites(live: MentraLive): List<ByteArray> {
50
+ val queue = MentraLive::class.java.getDeclaredField("sendQueue").apply { isAccessible = true }.get(live)
51
+ return (queue as Iterable<*>).map { write ->
52
+ write!!.javaClass.getDeclaredField("data").apply { isAccessible = true }.get(write) as ByteArray
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,151 @@
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.DeviceManager
13
+ import com.mentra.bluetoothsdk.DeviceStore
14
+ import com.mentra.bluetoothsdk.utils.DeviceTypes
15
+ import org.junit.Assert.assertEquals
16
+ import org.junit.Assert.assertTrue
17
+ import org.junit.Before
18
+ import org.junit.Test
19
+ import org.junit.runner.RunWith
20
+ import org.robolectric.RobolectricTestRunner
21
+ import org.robolectric.Shadows.shadowOf
22
+ import org.robolectric.annotation.Config
23
+ import org.robolectric.annotation.Implementation
24
+ import org.robolectric.annotation.Implements
25
+ import org.robolectric.annotation.LooperMode
26
+ import org.robolectric.shadows.ShadowBluetoothGatt
27
+
28
+ @RunWith(RobolectricTestRunner::class)
29
+ @Config(sdk = [33], shadows = [G1PairingTest.ShadowLc3Cpp::class, MentraNexPairingTest.ShadowNexGatt::class], instrumentedPackages = ["com.mentra.lc3Lib"])
30
+ @LooperMode(LooperMode.Mode.PAUSED)
31
+ class MentraNexPairingTest {
32
+ // Robolectric implements connection callbacks but not the PHY binder request.
33
+ @Implements(BluetoothGatt::class)
34
+ class ShadowNexGatt : ShadowBluetoothGatt() {
35
+ @Implementation fun setPreferredPhy(txPhy: Int, rxPhy: Int, phyOptions: Int) {}
36
+ }
37
+
38
+ @Before fun setup() {
39
+ Bridge.initialize(ApplicationProvider.getApplicationContext())
40
+ shadowOf(BluetoothAdapter.getDefaultAdapter()).setState(BluetoothAdapter.STATE_ON)
41
+ DeviceStore.set("bluetooth", "pending_device_name", "Nex1-2")
42
+ DeviceStore.set("bluetooth", "pending_device_address", "")
43
+ DeviceStore.set("bluetooth", "device_address", "")
44
+ }
45
+
46
+ @Test fun `fresh pairing selects only the requested Nex`() {
47
+ assertScansSelectedTarget("", "")
48
+ }
49
+
50
+ @Test fun `simulated pairing selects only the requested Nex`() {
51
+ assertScansSelectedTarget("Simulated Glasses", "")
52
+ }
53
+
54
+ @Test fun `a different saved Nex name and address cannot redirect selection`() {
55
+ assertScansSelectedTarget("Nex1-1", "00:00:00:00:00:01")
56
+ }
57
+
58
+ @Test fun `pending address targets the new Nex instead of the saved pair`() {
59
+ assertDirectTarget("Nex1-1", "00:00:00:00:00:01", "00:00:00:00:00:02")
60
+ }
61
+
62
+ @Test fun `saved Nex reconnect retains its address`() {
63
+ assertDirectTarget("Nex1-2", "00:00:00:00:00:02", "")
64
+ }
65
+
66
+ @Test fun `name-only pairing readiness cannot associate the old MAC with the new Nex`() {
67
+ DeviceStore.set("bluetooth", "default_wearable", DeviceTypes.NEX)
68
+ DeviceStore.set("bluetooth", "pending_wearable", DeviceTypes.NEX)
69
+ DeviceStore.set("bluetooth", "device_name", "Nex1-1")
70
+ DeviceStore.set("bluetooth", "device_address", "00:00:00:00:00:01")
71
+ DeviceStore.set("bluetooth", "shouldSendBootingMessage", false)
72
+ DeviceStore.set("glasses", "fullyBooted", false)
73
+ val old = device("00:00:00:00:00:01", "Nex1-1")
74
+ val selected = device("00:00:00:00:00:02", "Nex1-2")
75
+ val singleton = DeviceManager::class.java.getDeclaredField("_instance").apply { isAccessible = true }
76
+ val previous = singleton.get(null)
77
+ val manager = DeviceManager(initializeHardware = false)
78
+ singleton.set(null, manager)
79
+ try {
80
+ var nex = MentraNex()
81
+ manager.sgc = nex
82
+ nex.connectById("Nex1-2")
83
+ val scanner = shadowOf(BluetoothAdapter.getDefaultAdapter().bluetoothLeScanner)
84
+ scanner.scanCallbacks.single().onScanResult(
85
+ ScanSettings.CALLBACK_TYPE_ALL_MATCHES, ScanResult(selected, null, -50, 0),
86
+ )
87
+ shadowOf(Looper.getMainLooper()).idle()
88
+ // Drive the actual ready callback and DeviceManager's identity promotion.
89
+ shadowOf(selected).simulateGattConnectionChange(BluetoothGatt.GATT_SUCCESS, BluetoothProfile.STATE_CONNECTED)
90
+ assertEquals("Nex1-2", DeviceStore.get("bluetooth", "device_name"))
91
+ assertEquals("", DeviceStore.get("bluetooth", "device_address"))
92
+ assertEquals("", DeviceStore.get("bluetooth", "pending_device_name"))
93
+ manager.disconnect()
94
+
95
+ nex = MentraNex()
96
+ manager.sgc = nex
97
+ nex.connectById(DeviceStore.get("bluetooth", "device_name") as String)
98
+ assertTrue(shadowOf(old).bluetoothGatts.isEmpty())
99
+ scanner.scanCallbacks.single().onScanResult(
100
+ ScanSettings.CALLBACK_TYPE_ALL_MATCHES, ScanResult(selected, null, -50, 0),
101
+ )
102
+ shadowOf(Looper.getMainLooper()).idle()
103
+ assertEquals(2, shadowOf(selected).bluetoothGatts.size)
104
+ } finally {
105
+ manager.disconnect()
106
+ manager.cleanup()
107
+ singleton.set(null, previous)
108
+ }
109
+ }
110
+
111
+ private fun assertScansSelectedTarget(savedName: String, savedAddress: String) {
112
+ DeviceStore.set("bluetooth", "device_name", savedName)
113
+ DeviceStore.set("bluetooth", "device_address", savedAddress)
114
+ val old = device("00:00:00:00:00:01", "Nex1-1")
115
+ val prefixCollision = device("00:00:00:00:00:03", "Nex1-23")
116
+ val selected = device("00:00:00:00:00:02", "Nex1-2")
117
+ val nex = MentraNex()
118
+ try {
119
+ nex.connectById("Nex1-2")
120
+ val scanner = shadowOf(BluetoothAdapter.getDefaultAdapter().bluetoothLeScanner)
121
+ val callback = scanner.scanCallbacks.single()
122
+ for (found in listOf(old, prefixCollision, selected)) {
123
+ callback.onScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES, ScanResult(found, null, -50, 0))
124
+ shadowOf(Looper.getMainLooper()).idle()
125
+ }
126
+ assertTrue(shadowOf(old).bluetoothGatts.isEmpty())
127
+ assertTrue(shadowOf(prefixCollision).bluetoothGatts.isEmpty())
128
+ assertEquals(1, shadowOf(selected).bluetoothGatts.size)
129
+ assertEquals(savedName, DeviceStore.get("bluetooth", "device_name"))
130
+ } finally { nex.disconnect() }
131
+ }
132
+
133
+ private fun assertDirectTarget(savedName: String, savedAddress: String, pendingAddress: String) {
134
+ DeviceStore.set("bluetooth", "device_name", savedName)
135
+ DeviceStore.set("bluetooth", "device_address", savedAddress)
136
+ DeviceStore.set("bluetooth", "pending_device_address", pendingAddress)
137
+ val old = device("00:00:00:00:00:01", "Nex1-1")
138
+ val selected = device("00:00:00:00:00:02", "Nex1-2")
139
+ val nex = MentraNex()
140
+ try {
141
+ nex.connectById("Nex1-2")
142
+ shadowOf(Looper.getMainLooper()).idle()
143
+ assertTrue(shadowOf(old).bluetoothGatts.isEmpty())
144
+ assertEquals(1, shadowOf(selected).bluetoothGatts.size)
145
+ assertEquals(savedName, DeviceStore.get("bluetooth", "device_name"))
146
+ } finally { nex.disconnect() }
147
+ }
148
+
149
+ private fun device(address: String, name: String): BluetoothDevice =
150
+ BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address).also { shadowOf(it).setName(name) }
151
+ }
@@ -0,0 +1,105 @@
1
+ package com.mentra.bluetoothsdk.sgcs
2
+
3
+ import android.graphics.Bitmap
4
+ import androidx.test.core.app.ApplicationProvider
5
+ import com.mentra.bluetoothsdk.Bridge
6
+ import java.io.ByteArrayOutputStream
7
+ import java.util.Base64
8
+ import org.junit.After
9
+ import org.junit.Assert.*
10
+ import org.junit.Before
11
+ import org.junit.Test
12
+ import org.junit.runner.RunWith
13
+ import org.robolectric.RobolectricTestRunner
14
+ import org.robolectric.annotation.Config
15
+ import org.robolectric.annotation.GraphicsMode
16
+
17
+ @RunWith(RobolectricTestRunner::class)
18
+ @Config(sdk = [33])
19
+ @GraphicsMode(GraphicsMode.Mode.NATIVE)
20
+ class NimoBitmapAcceptanceTest {
21
+ private lateinit var nimo: Nimo
22
+
23
+ @Before fun setup() {
24
+ Bridge.initialize(ApplicationProvider.getApplicationContext())
25
+ nimo = Nimo()
26
+ }
27
+
28
+ @After fun dispose() { nimo.cleanup() }
29
+
30
+ @Test fun malformedInputsAreRejectedBeforeReturning() {
31
+ val invalid = listOf(
32
+ "not base64!",
33
+ "data:image/jpeg;base64,${png()}",
34
+ "data:image/bmp;base64,${png()}",
35
+ Base64.getEncoder().encodeToString(byteArrayOf(0x89.toByte(), 80, 78, 71, 13, 10, 26, 10) + ByteArray(40)),
36
+ Base64.getEncoder().encodeToString(byteArrayOf(66, 77) + ByteArray(60)),
37
+ "A".repeat(NimoCanvasCodec.MAX_IMAGE_BASE64_CHARS + 1)
38
+ )
39
+ for (input in invalid) {
40
+ assertFalse("Malformed bitmap was accepted: ${input.take(50)}", nimo.displayBitmap(input))
41
+ }
42
+ }
43
+
44
+ @Test fun validPngAcceptsNullableGeometryAndRejectsOutOfBoundsGeometry() {
45
+ val image = png()
46
+ assertTrue(nimo.displayBitmap(image))
47
+ assertTrue(nimo.displayBitmap(image, 499, 219, null, null))
48
+ assertFalse(nimo.displayBitmap(image, 500, 0, null, null))
49
+ assertFalse(nimo.displayBitmap(image, 0, 0, 0, 1))
50
+ assertFalse(nimo.displayBitmap(image, -1, 0, 10, 10))
51
+ }
52
+
53
+ @Test fun jpegPixelsAreDecodedForRawAndDataUriInputs() {
54
+ val bitmap = Bitmap.createBitmap(2, 2, Bitmap.Config.ARGB_8888)
55
+ val bytes = ByteArrayOutputStream()
56
+ try {
57
+ bitmap.eraseColor(android.graphics.Color.WHITE)
58
+ assertTrue(bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bytes))
59
+ } finally { bitmap.recycle() }
60
+ val raw = Base64.getEncoder().encodeToString(bytes.toByteArray())
61
+ val decode = Nimo::class.java.getDeclaredMethod(
62
+ "decodeCanvasImage", String::class.java, Int::class.javaPrimitiveType, Int::class.javaPrimitiveType
63
+ ).apply { isAccessible = true }
64
+ for (input in listOf(raw, "data:image/jpeg;base64,$raw")) {
65
+ assertTrue(nimo.displayBitmap(input))
66
+ assertArrayEquals(ByteArray(4) { 255.toByte() }, decode.invoke(nimo, input, 2, 2) as ByteArray)
67
+ }
68
+ assertFalse(nimo.displayBitmap("data:image/png;base64,$raw"))
69
+ }
70
+
71
+ @Test fun sourceRasterBudgetsRejectOversizeAndAcceptBoundaries() {
72
+ for ((width, height) in listOf(4097 to 1, 1 to 4097, 2049 to 2000)) {
73
+ assertFalse("Oversize source $width x $height", nimo.displayBitmap(png(width, height)))
74
+ }
75
+ for ((width, height) in listOf(4096 to 1, 1 to 4096, 2000 to 2000)) {
76
+ assertTrue("Valid boundary $width x $height", nimo.displayBitmap(png(width, height)))
77
+ }
78
+ }
79
+
80
+ @Test fun coloredRasterQuantizationMatchesIosAtEveryTwoBitBoundary() {
81
+ // Identical opaque 8x1 24-bit BMP in NimoCanvasSceneTests; no scaling.
82
+ val bmp = byteArrayOf(
83
+ 66, 77, 78, 0, 0, 0, 0, 0, 0, 0, 54, 0, 0, 0,
84
+ 40, 0, 0, 0, 8, 0, 0, 0, 1, 0, 0, 0, 1, 0, 24, 0,
85
+ 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
86
+ 0, 72, 0, 0, 73, 0, 0, 217.toByte(), 0, 0, 218.toByte(), 0,
87
+ 0, 231.toByte(), 255.toByte(), 0, 232.toByte(), 255.toByte(), 0, 0, 0, -1, -1, -1
88
+ )
89
+ val decode = Nimo::class.java.getDeclaredMethod(
90
+ "decodeCanvasImage", String::class.java, Int::class.javaPrimitiveType, Int::class.javaPrimitiveType
91
+ ).apply { isAccessible = true }
92
+ val gray = decode.invoke(nimo, Base64.getEncoder().encodeToString(bmp), 8, 1) as ByteArray
93
+ assertArrayEquals(byteArrayOf(42, 43, 127, 128.toByte(), 212.toByte(), 213.toByte(), 0, -1), gray)
94
+ assertArrayEquals(byteArrayOf(0xE9.toByte(), 0x4C), NimoCanvasCodec.pack2(gray))
95
+ }
96
+
97
+ private fun png(width: Int = 2, height: Int = 2): String {
98
+ val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
99
+ return try {
100
+ val bytes = ByteArrayOutputStream()
101
+ assertTrue(bitmap.compress(Bitmap.CompressFormat.PNG, 100, bytes))
102
+ Base64.getEncoder().encodeToString(bytes.toByteArray())
103
+ } finally { bitmap.recycle() }
104
+ }
105
+ }
@@ -0,0 +1,233 @@
1
+ package com.mentra.bluetoothsdk.sgcs
2
+
3
+ import org.junit.Assert.*
4
+ import org.junit.Test
5
+
6
+ class NimoCanvasCodecTest {
7
+ @Test fun LatinFallbackPreservesOtherScriptsAndTheirMarks() {
8
+ val otherScripts = "ありがとう が パ カ\u3099 한국어 한\u302E Α\u0301 مُرَحَّبًا शि ❤️"
9
+ val label = NimoCanvasCodec.label("$otherScripts café—Æ\u0301", 0, 0, 500, 220)
10
+ val expected = "$otherScripts cafe-AE".toByteArray(Charsets.UTF_8)
11
+ assertArrayEquals(expected, label.payload.copyOfRange(14, label.payload.size))
12
+ assertEquals(expected.size, label.textBytes)
13
+ }
14
+
15
+ @Test fun imageSourcesAcceptRawAndMatchingDataUris() {
16
+ // ImageIO/BitmapFactory validate the complete raster; this boundary validates file kind and size.
17
+ val png = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 13, 10, 26, 10) + ByteArray(24)
18
+ val bmp = byteArrayOf(0x42, 0x4D) + ByteArray(56)
19
+ val jpeg = byteArrayOf(0xFF.toByte(), 0xD8.toByte(), 0xFF.toByte()) + ByteArray(24)
20
+ for ((mime, bytes) in listOf("png" to png, "bmp" to bmp, "jpeg" to jpeg)) {
21
+ val raw = java.util.Base64.getEncoder().encodeToString(bytes)
22
+ assertArrayEquals(bytes, NimoCanvasCodec.imageBytes(raw))
23
+ assertArrayEquals(bytes, NimoCanvasCodec.imageBytes("data:image/$mime;base64,$raw"))
24
+ val wrongMime = if (mime == "png") "bmp" else "png"
25
+ assertThrows(IllegalArgumentException::class.java) {
26
+ NimoCanvasCodec.imageBytes("data:image/$wrongMime;base64,$raw")
27
+ }
28
+ }
29
+ }
30
+
31
+ @Test fun unsupportedAndOversizedImageSourcesFailBeforeRasterDecoding() {
32
+ val encoder = java.util.Base64.getEncoder()
33
+ assertThrows(IllegalStateException::class.java) { NimoCanvasCodec.imageBytes("data:image/gif;base64,AAAA") }
34
+ assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.imageBytes(encoder.encodeToString(ByteArray(54))) }
35
+ assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.imageBytes("a".repeat(2_800_001)) }
36
+ assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.imageBytes(encoder.encodeToString(ByteArray(2_000_001))) }
37
+ assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.imageBytes("not valid base64!") }
38
+ assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.imageBytes("Qk0") }
39
+ }
40
+
41
+ @Test fun guideGoldenPacket() {
42
+ val frame = NimoCanvasCodec.replace(listOf(
43
+ NimoCanvasCodec.rectangle(20, 20, 120, 50, 2, 8),
44
+ NimoCanvasCodec.label("Hello Glass", 160, 30, 300, 40, font = 2)
45
+ ))
46
+ val actual = NimoCanvasCodec.frames(4, frame, 512).single()
47
+ val expected = "BF003600F72D000007043200FD000000020001010C0014001400780032000208FF00031900A0001E002C012800000002FF0B0048656C6C6F20476C617373"
48
+ assertEquals(expected, actual.joinToString("") { "%02X".format(it.toInt() and 255) })
49
+ }
50
+
51
+ @Test fun fourTonePolarityAndOddPadding() {
52
+ assertArrayEquals(byteArrayOf(0xE4.toByte(), 0x3F),
53
+ NimoCanvasCodec.pack2(byteArrayOf(0, 85, 170.toByte(), 255.toByte(), 255.toByte())))
54
+ }
55
+
56
+ @Test fun launchAndExitGoldenPackets() {
57
+ fun hex(bytes: ByteArray) = bytes.joinToString("") { "%02X".format(it.toInt() and 255) }
58
+ assertEquals("BF000600F5E4000007010200FD00", hex(NimoCanvasCodec.frames(1, writeCapacity = 512).single()))
59
+ assertEquals("BF00050086E4000007030100FD", hex(NimoCanvasCodec.frames(3, writeCapacity = 512).single()))
60
+ }
61
+
62
+ @Test fun primitivesValidateGeometryAndUseCompactCircle() {
63
+ assertEquals(10, NimoCanvasCodec.line(0, 0, 499, 219).payload.size)
64
+ assertEquals(12, NimoCanvasCodec.rectangle(0, 0, 500, 220).payload.size)
65
+ assertEquals(9, NimoCanvasCodec.circle(10, 10, 10).payload.size)
66
+ assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.line(0, 0, 500, 219) }
67
+ assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.rectangle(Int.MAX_VALUE, 0, 10, 1) }
68
+ assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.rectangle(0, 0, 501, 220) }
69
+ assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.circle(0, 10, 10) }
70
+ }
71
+
72
+ @Test fun labelUtf8LengthAndErrors() {
73
+ val dashes = NimoCanvasCodec.label("—éÆß— - –", 0, 0, 100, 20)
74
+ val expectedText = "-eAEss- - –".toByteArray(Charsets.UTF_8)
75
+ assertArrayEquals(expectedText, dashes.payload.copyOfRange(14, dashes.payload.size))
76
+ assertEquals(expectedText.size, dashes.textBytes)
77
+ assertEquals(expectedText.size, dashes.payload[12].toInt())
78
+ val label = NimoCanvasCodec.label("😀界", 0, 0, 100, 20)
79
+ assertEquals(7, label.textBytes)
80
+ assertEquals(7, label.payload[12].toInt())
81
+ assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.label("a\u0000b", 0, 0, 100, 20) }
82
+ assertThrows(java.nio.charset.CharacterCodingException::class.java) { NimoCanvasCodec.label("\uD800", 0, 0, 100, 20) }
83
+ assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.label("界".repeat(683), 0, 0, 100, 20) }
84
+ assertEquals(2046, NimoCanvasCodec.label("界".repeat(682), 0, 0, 100, 20).textBytes)
85
+ }
86
+
87
+ @Test fun totalBudgetsAndClear() {
88
+ assertArrayEquals(byteArrayOf(0, 0, 1), NimoCanvasCodec.replace(emptyList()))
89
+ val line = NimoCanvasCodec.line(0, 0, 1, 1)
90
+ NimoCanvasCodec.replace(List(64) { line })
91
+ assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.replace(List(65) { line }) }
92
+ val text = NimoCanvasCodec.label("a".repeat(2048), 0, 0, 500, 220)
93
+ NimoCanvasCodec.replace(List(4) { text })
94
+ assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.replace(List(5) { text }) }
95
+ val image = NimoCanvasCodec.bitmap(0, 0, 500, 220, ByteArray(110000))
96
+ NimoCanvasCodec.replace(listOf(image))
97
+ assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.replace(listOf(image, image)) }
98
+ assertThrows(IllegalArgumentException::class.java) {
99
+ NimoCanvasCodec.replace(listOf(NimoCanvasCodec.Object(4, ByteArray(12278))))
100
+ }
101
+ }
102
+
103
+ @Test fun hostFrameReservationBoundsNativeEncoding() {
104
+ val random = java.util.Random(42)
105
+ for (size in listOf(1, 5, 80, 160, 200)) {
106
+ val gray = ByteArray(size * size).also { random.nextBytes(it) }
107
+ val image = NimoCanvasCodec.bitmap(0, 0, size, size, gray)
108
+ val packed = (gray.size + 3) / 4
109
+ val reserved = 32 + packed + (packed + 126) / 127
110
+ assertTrue(3 + image.payload.size <= reserved)
111
+ }
112
+ // These are the retained prefixes of the host's over-budget regression scenes.
113
+ val rows = NimoCanvasCodec.textRows(List(11) { "row" }.joinToString("\n"), 0, 0, 500, 220)
114
+ assertTrue(NimoCanvasCodec.replace(List(5) { rows }.flatten()).size <= 12280)
115
+ val images = listOf(200, 80).map { size ->
116
+ NimoCanvasCodec.bitmap(0, 0, size, size, ByteArray(size * size).also { random.nextBytes(it) })
117
+ }
118
+ assertTrue(NimoCanvasCodec.replace(images).size <= 12280)
119
+ }
120
+
121
+ @Test fun explicitNewlinesRetainRowsWithoutRewrapping() {
122
+ val rows = NimoCanvasCodec.textRows("first\n\nthird", 12, 15, 400, 100)
123
+ assertEquals(2, rows.size)
124
+ assertEquals(15, rows[0].payload[2].toInt())
125
+ assertEquals(55, rows[1].payload[2].toInt())
126
+ assertEquals(20, rows[1].payload[6].toInt())
127
+ val single = NimoCanvasCodec.textRows("single", 12, 15, 400, 100).single()
128
+ assertEquals(20, single.payload[6].toInt())
129
+ }
130
+
131
+ @Test fun rleBoundaryAndLiteralRoundtrip() {
132
+ fun decode(data: ByteArray): ByteArray {
133
+ val out = java.io.ByteArrayOutputStream(); var i = 0
134
+ while (i < data.size) {
135
+ val tag = data[i++].toInt() and 255
136
+ val count = tag and 127
137
+ assertTrue(count > 0)
138
+ if (tag and 128 != 0) { out.write(data, i, count); i += count }
139
+ else { repeat(count) { out.write(data[i].toInt() and 255) }; i++ }
140
+ }
141
+ return out.toByteArray()
142
+ }
143
+ for (size in listOf(1, 2, 3, 126, 127, 128, 254, 27500)) {
144
+ for (input in listOf(ByteArray(size) { 33 }, ByteArray(size) { (it % 251).toByte() })) {
145
+ assertArrayEquals(input, decode(NimoCanvasCodec.rle(input)))
146
+ }
147
+ }
148
+ assertArrayEquals(byteArrayOf(3, 1, 0x82.toByte(), 2, 3), NimoCanvasCodec.rle(byteArrayOf(1, 1, 1, 2, 3)))
149
+ }
150
+
151
+ @Test fun bitmapIsCompleteCompressed2BppAndUsesExactOddSize() {
152
+ val bitmap = NimoCanvasCodec.bitmap(0, 0, 5, 1, byteArrayOf(0, 85, 170.toByte(), 255.toByte(), 255.toByte()))
153
+ assertEquals(1, bitmap.payload[10].toInt()) // total_chunks
154
+ assertEquals(2, bitmap.payload[19].toInt()) // image bpp
155
+ assertEquals(3, bitmap.payload[20].toInt()) // RLE is smallest here
156
+ assertEquals(2, bitmap.payload[21].toInt()) // original_size ceil(5/4)
157
+ assertArrayEquals(byteArrayOf(0x82.toByte(), 0xE4.toByte(), 0x3F), bitmap.payload.copyOfRange(29, bitmap.payload.size))
158
+ }
159
+
160
+ @Test fun fragmentationAtNegotiatedCapacityReassemblesWholeAppStream() {
161
+ val content = NimoCanvasCodec.replace(listOf(NimoCanvasCodec.label("X".repeat(2048), 0, 0, 500, 220)))
162
+ val baseline = NimoCanvasCodec.frames(4, content, 512).flatMap { it.drop(8) }.toByteArray()
163
+ for (capacity in listOf(20, 64, 185, 244, 512)) {
164
+ val frames = NimoCanvasCodec.frames(4, content, capacity)
165
+ frames.forEachIndexed { index, packet ->
166
+ assertTrue(packet.size <= capacity)
167
+ assertEquals(0, packet[1].toInt())
168
+ val expectedIndex = if (index == frames.lastIndex) 0 else index + 1
169
+ assertEquals(expectedIndex, (packet[6].toInt() and 255) or ((packet[7].toInt() and 255) shl 8))
170
+ assertEquals(NimoCanvasCodec.crc16(packet.copyOfRange(8, packet.size)),
171
+ (packet[4].toInt() and 255) or ((packet[5].toInt() and 255) shl 8))
172
+ }
173
+ assertArrayEquals(baseline, frames.flatMap { it.drop(8) }.toByteArray())
174
+ }
175
+ assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.frames(4, content, 19) }
176
+ assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.frames(4, ByteArray(12281), 512) }
177
+ }
178
+
179
+ @Test fun strictResponseParserAcceptsStatusOnlyErrors() {
180
+ fun packet(hex: String) = hex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
181
+ val good = packet("BF000900D87900000704050000FD000000")
182
+ assertArrayEquals(byteArrayOf(0, 0xFD.toByte(), 0, 0, 0), NimoCanvasCodec.response(good)!!.second)
183
+ assertNull(NimoCanvasCodec.response(good + 0))
184
+ assertNull(NimoCanvasCodec.response(good.copyOf().also { it[10] = 4 }))
185
+ assertNull(NimoCanvasCodec.response(good.copyOf().also { it[1] = 2 }))
186
+ val body = byteArrayOf(7, 4, 1, 0, 7)
187
+ val crc = NimoCanvasCodec.crc16(body)
188
+ val statusOnly = byteArrayOf(0xBF.toByte(), 0, 5, 0, crc.toByte(), (crc shr 8).toByte(), 0, 0) + body
189
+ assertArrayEquals(byteArrayOf(7), NimoCanvasCodec.response(statusOnly)!!.second)
190
+ }
191
+
192
+ @Test fun sceneCompilerKeepsGeometryOrderAndBorderBeforeText() {
193
+ val text = NimoCanvasCodec.Element("text", 10, 20, 100, 40, "héllo—Æsir", border = 2, radius = 3)
194
+ val rectangle = NimoCanvasCodec.Element("rect", 100, 100, 50, 20)
195
+ val actual = NimoCanvasCodec.scene(listOf(text, rectangle)) { _, _, _ -> error("Not an image") }
196
+ val expected = NimoCanvasCodec.replace(listOf(
197
+ NimoCanvasCodec.rectangle(10, 20, 100, 40, 2, 3),
198
+ NimoCanvasCodec.label("hello-AEsir", 10, 20, 100, 20),
199
+ NimoCanvasCodec.rectangle(100, 100, 50, 20, 1, 0)
200
+ ))
201
+ assertArrayEquals(expected, actual)
202
+ assertFalse(actual.contentEquals(NimoCanvasCodec.scene(listOf(rectangle, text)) { _, _, _ -> error("Not an image") }))
203
+ assertFalse(actual.contentEquals(NimoCanvasCodec.scene(listOf(text.copy(x = 11), rectangle)) { _, _, _ -> error("Not an image") }))
204
+ }
205
+
206
+ @Test fun scenePreflightsInvalidImageBudgetBeforeDecodingAnyPng() {
207
+ val image = NimoCanvasCodec.Element("image", 0, 0, 500, 220, data = "unused")
208
+ var decoded = 0
209
+ assertThrows(IllegalArgumentException::class.java) {
210
+ NimoCanvasCodec.scene(listOf(image, image)) { _, _, _ -> decoded++; ByteArray(110000) }
211
+ }
212
+ assertEquals(0, decoded)
213
+ assertThrows(IllegalArgumentException::class.java) {
214
+ NimoCanvasCodec.scene(listOf(image.copy(type = "html"))) { _, _, _ -> decoded++; ByteArray(110000) }
215
+ }
216
+ assertEquals(0, decoded)
217
+ }
218
+
219
+ @Test fun zlibCompressesDenseRepeatingImagesAndInflatesToPackedPixels() {
220
+ val gray = ByteArray(32768) { (((it / 17) xor (it / 43)) % 4 * 85).toByte() }
221
+ val bitmap = NimoCanvasCodec.bitmap(0, 0, 256, 128, gray)
222
+ assertEquals(7, bitmap.payload[20].toInt())
223
+ val inflater = java.util.zip.Inflater()
224
+ try {
225
+ inflater.setInput(bitmap.payload.copyOfRange(29, bitmap.payload.size))
226
+ val output = ByteArray((gray.size + 3) / 4)
227
+ assertEquals(output.size, inflater.inflate(output))
228
+ assertTrue(inflater.finished())
229
+ assertArrayEquals(NimoCanvasCodec.pack2(gray), output)
230
+ } finally { inflater.end() }
231
+ NimoCanvasCodec.replace(listOf(bitmap))
232
+ }
233
+ }