@mentra/bluetooth-sdk 3.2.1-dev.311 → 3.2.1
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 +4 -4
- 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/BluetoothSdkDefaults.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +6 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +129 -25
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
- 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/G1.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +101 -22
- 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 +245 -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/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/VersionInfoResponseAccumulatorTest.kt +71 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
- 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/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/MentraLivePairingTargetTest.kt +65 -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 +220 -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 +13 -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 +3 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +2 -2
- package/build/generated/changelogCatalog.js +2 -2
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- 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/ios/BluetoothSdkModule.swift +9 -0
- package/ios/MentraBluetoothSDK.podspec +3 -2
- package/ios/Source/BluetoothBackgroundMode.swift +32 -0
- package/ios/Source/BluetoothSdkDefaults.swift +6 -1
- package/ios/Source/Bridge.swift +15 -2
- package/ios/Source/DeviceManager.swift +96 -63
- package/ios/Source/DeviceStore.swift +26 -7
- package/ios/Source/GeneratedChangelogCatalog.swift +2 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +18 -0
- package/ios/Source/audio/GlassesMicWatchdog.swift +33 -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 +201 -156
- 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 +269 -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 +24 -0
- 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/DemandDrivenAudioWorker.swift +150 -0
- package/ios/Source/utils/G2Text.swift +11 -0
- package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -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/G2ConnectionSessionTests.swift +172 -0
- package/ios/Tests/G2TextTests.swift +10 -0
- package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
- package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
- package/ios/Tests/NimoCanvasCodecTests.swift +204 -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 +53 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
- package/ios/Tests/VideoRecordingCommandTests.swift +81 -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 +14 -0
- package/src/_private/BluetoothSdkModule.ts +8 -0
- package/src/generated/changelogCatalog.ts +2 -2
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +1 -0
|
@@ -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,220 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.*
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class NimoCanvasCodecTest {
|
|
7
|
+
@Test fun imageSourcesAcceptRawAndMatchingDataUris() {
|
|
8
|
+
// ImageIO/BitmapFactory validate the complete raster; this boundary validates file kind and size.
|
|
9
|
+
val png = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 13, 10, 26, 10) + ByteArray(24)
|
|
10
|
+
val bmp = byteArrayOf(0x42, 0x4D) + ByteArray(56)
|
|
11
|
+
val jpeg = byteArrayOf(0xFF.toByte(), 0xD8.toByte(), 0xFF.toByte()) + ByteArray(24)
|
|
12
|
+
for ((mime, bytes) in listOf("png" to png, "bmp" to bmp, "jpeg" to jpeg)) {
|
|
13
|
+
val raw = java.util.Base64.getEncoder().encodeToString(bytes)
|
|
14
|
+
assertArrayEquals(bytes, NimoCanvasCodec.imageBytes(raw))
|
|
15
|
+
assertArrayEquals(bytes, NimoCanvasCodec.imageBytes("data:image/$mime;base64,$raw"))
|
|
16
|
+
val wrongMime = if (mime == "png") "bmp" else "png"
|
|
17
|
+
assertThrows(IllegalArgumentException::class.java) {
|
|
18
|
+
NimoCanvasCodec.imageBytes("data:image/$wrongMime;base64,$raw")
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Test fun unsupportedAndOversizedImageSourcesFailBeforeRasterDecoding() {
|
|
24
|
+
val encoder = java.util.Base64.getEncoder()
|
|
25
|
+
assertThrows(IllegalStateException::class.java) { NimoCanvasCodec.imageBytes("data:image/gif;base64,AAAA") }
|
|
26
|
+
assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.imageBytes(encoder.encodeToString(ByteArray(54))) }
|
|
27
|
+
assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.imageBytes("a".repeat(2_800_001)) }
|
|
28
|
+
assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.imageBytes(encoder.encodeToString(ByteArray(2_000_001))) }
|
|
29
|
+
assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.imageBytes("not valid base64!") }
|
|
30
|
+
assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.imageBytes("Qk0") }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Test fun guideGoldenPacket() {
|
|
34
|
+
val frame = NimoCanvasCodec.replace(listOf(
|
|
35
|
+
NimoCanvasCodec.rectangle(20, 20, 120, 50, 2, 8),
|
|
36
|
+
NimoCanvasCodec.label("Hello Glass", 160, 30, 300, 40, font = 2)
|
|
37
|
+
))
|
|
38
|
+
val actual = NimoCanvasCodec.frames(4, frame, 512).single()
|
|
39
|
+
val expected = "BF003600F72D000007043200FD000000020001010C0014001400780032000208FF00031900A0001E002C012800000002FF0B0048656C6C6F20476C617373"
|
|
40
|
+
assertEquals(expected, actual.joinToString("") { "%02X".format(it.toInt() and 255) })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Test fun fourTonePolarityAndOddPadding() {
|
|
44
|
+
assertArrayEquals(byteArrayOf(0xE4.toByte(), 0x3F),
|
|
45
|
+
NimoCanvasCodec.pack2(byteArrayOf(0, 85, 170.toByte(), 255.toByte(), 255.toByte())))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Test fun launchAndExitGoldenPackets() {
|
|
49
|
+
fun hex(bytes: ByteArray) = bytes.joinToString("") { "%02X".format(it.toInt() and 255) }
|
|
50
|
+
assertEquals("BF000600F5E4000007010200FD00", hex(NimoCanvasCodec.frames(1, writeCapacity = 512).single()))
|
|
51
|
+
assertEquals("BF00050086E4000007030100FD", hex(NimoCanvasCodec.frames(3, writeCapacity = 512).single()))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Test fun primitivesValidateGeometryAndUseCompactCircle() {
|
|
55
|
+
assertEquals(10, NimoCanvasCodec.line(0, 0, 499, 219).payload.size)
|
|
56
|
+
assertEquals(12, NimoCanvasCodec.rectangle(0, 0, 500, 220).payload.size)
|
|
57
|
+
assertEquals(9, NimoCanvasCodec.circle(10, 10, 10).payload.size)
|
|
58
|
+
assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.line(0, 0, 500, 219) }
|
|
59
|
+
assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.rectangle(Int.MAX_VALUE, 0, 10, 1) }
|
|
60
|
+
assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.rectangle(0, 0, 501, 220) }
|
|
61
|
+
assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.circle(0, 10, 10) }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Test fun labelUtf8LengthAndErrors() {
|
|
65
|
+
val label = NimoCanvasCodec.label("😀é", 0, 0, 100, 20)
|
|
66
|
+
assertEquals(6, label.textBytes)
|
|
67
|
+
assertEquals(6, label.payload[12].toInt())
|
|
68
|
+
assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.label("a\u0000b", 0, 0, 100, 20) }
|
|
69
|
+
assertThrows(java.nio.charset.CharacterCodingException::class.java) { NimoCanvasCodec.label("\uD800", 0, 0, 100, 20) }
|
|
70
|
+
assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.label("é".repeat(1025), 0, 0, 100, 20) }
|
|
71
|
+
assertEquals(2048, NimoCanvasCodec.label("é".repeat(1024), 0, 0, 100, 20).textBytes)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Test fun totalBudgetsAndClear() {
|
|
75
|
+
assertArrayEquals(byteArrayOf(0, 0, 1), NimoCanvasCodec.replace(emptyList()))
|
|
76
|
+
val line = NimoCanvasCodec.line(0, 0, 1, 1)
|
|
77
|
+
NimoCanvasCodec.replace(List(64) { line })
|
|
78
|
+
assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.replace(List(65) { line }) }
|
|
79
|
+
val text = NimoCanvasCodec.label("a".repeat(2048), 0, 0, 500, 220)
|
|
80
|
+
NimoCanvasCodec.replace(List(4) { text })
|
|
81
|
+
assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.replace(List(5) { text }) }
|
|
82
|
+
val image = NimoCanvasCodec.bitmap(0, 0, 500, 220, ByteArray(110000))
|
|
83
|
+
NimoCanvasCodec.replace(listOf(image))
|
|
84
|
+
assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.replace(listOf(image, image)) }
|
|
85
|
+
assertThrows(IllegalArgumentException::class.java) {
|
|
86
|
+
NimoCanvasCodec.replace(listOf(NimoCanvasCodec.Object(4, ByteArray(12278))))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@Test fun hostFrameReservationBoundsNativeEncoding() {
|
|
91
|
+
val random = java.util.Random(42)
|
|
92
|
+
for (size in listOf(1, 5, 80, 160, 200)) {
|
|
93
|
+
val gray = ByteArray(size * size).also { random.nextBytes(it) }
|
|
94
|
+
val image = NimoCanvasCodec.bitmap(0, 0, size, size, gray)
|
|
95
|
+
val packed = (gray.size + 3) / 4
|
|
96
|
+
val reserved = 32 + packed + (packed + 126) / 127
|
|
97
|
+
assertTrue(3 + image.payload.size <= reserved)
|
|
98
|
+
}
|
|
99
|
+
// These are the retained prefixes of the host's over-budget regression scenes.
|
|
100
|
+
val rows = NimoCanvasCodec.textRows(List(11) { "row" }.joinToString("\n"), 0, 0, 500, 220)
|
|
101
|
+
assertTrue(NimoCanvasCodec.replace(List(5) { rows }.flatten()).size <= 12280)
|
|
102
|
+
val images = listOf(200, 80).map { size ->
|
|
103
|
+
NimoCanvasCodec.bitmap(0, 0, size, size, ByteArray(size * size).also { random.nextBytes(it) })
|
|
104
|
+
}
|
|
105
|
+
assertTrue(NimoCanvasCodec.replace(images).size <= 12280)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@Test fun explicitNewlinesRetainRowsWithoutRewrapping() {
|
|
109
|
+
val rows = NimoCanvasCodec.textRows("first\n\nthird", 12, 15, 400, 100)
|
|
110
|
+
assertEquals(2, rows.size)
|
|
111
|
+
assertEquals(15, rows[0].payload[2].toInt())
|
|
112
|
+
assertEquals(55, rows[1].payload[2].toInt())
|
|
113
|
+
assertEquals(20, rows[1].payload[6].toInt())
|
|
114
|
+
val single = NimoCanvasCodec.textRows("single", 12, 15, 400, 100).single()
|
|
115
|
+
assertEquals(20, single.payload[6].toInt())
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@Test fun rleBoundaryAndLiteralRoundtrip() {
|
|
119
|
+
fun decode(data: ByteArray): ByteArray {
|
|
120
|
+
val out = java.io.ByteArrayOutputStream(); var i = 0
|
|
121
|
+
while (i < data.size) {
|
|
122
|
+
val tag = data[i++].toInt() and 255
|
|
123
|
+
val count = tag and 127
|
|
124
|
+
assertTrue(count > 0)
|
|
125
|
+
if (tag and 128 != 0) { out.write(data, i, count); i += count }
|
|
126
|
+
else { repeat(count) { out.write(data[i].toInt() and 255) }; i++ }
|
|
127
|
+
}
|
|
128
|
+
return out.toByteArray()
|
|
129
|
+
}
|
|
130
|
+
for (size in listOf(1, 2, 3, 126, 127, 128, 254, 27500)) {
|
|
131
|
+
for (input in listOf(ByteArray(size) { 33 }, ByteArray(size) { (it % 251).toByte() })) {
|
|
132
|
+
assertArrayEquals(input, decode(NimoCanvasCodec.rle(input)))
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
assertArrayEquals(byteArrayOf(3, 1, 0x82.toByte(), 2, 3), NimoCanvasCodec.rle(byteArrayOf(1, 1, 1, 2, 3)))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@Test fun bitmapIsCompleteCompressed2BppAndUsesExactOddSize() {
|
|
139
|
+
val bitmap = NimoCanvasCodec.bitmap(0, 0, 5, 1, byteArrayOf(0, 85, 170.toByte(), 255.toByte(), 255.toByte()))
|
|
140
|
+
assertEquals(1, bitmap.payload[10].toInt()) // total_chunks
|
|
141
|
+
assertEquals(2, bitmap.payload[19].toInt()) // image bpp
|
|
142
|
+
assertEquals(3, bitmap.payload[20].toInt()) // RLE is smallest here
|
|
143
|
+
assertEquals(2, bitmap.payload[21].toInt()) // original_size ceil(5/4)
|
|
144
|
+
assertArrayEquals(byteArrayOf(0x82.toByte(), 0xE4.toByte(), 0x3F), bitmap.payload.copyOfRange(29, bitmap.payload.size))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@Test fun fragmentationAtNegotiatedCapacityReassemblesWholeAppStream() {
|
|
148
|
+
val content = NimoCanvasCodec.replace(listOf(NimoCanvasCodec.label("X".repeat(2048), 0, 0, 500, 220)))
|
|
149
|
+
val baseline = NimoCanvasCodec.frames(4, content, 512).flatMap { it.drop(8) }.toByteArray()
|
|
150
|
+
for (capacity in listOf(20, 64, 185, 244, 512)) {
|
|
151
|
+
val frames = NimoCanvasCodec.frames(4, content, capacity)
|
|
152
|
+
frames.forEachIndexed { index, packet ->
|
|
153
|
+
assertTrue(packet.size <= capacity)
|
|
154
|
+
assertEquals(0, packet[1].toInt())
|
|
155
|
+
val expectedIndex = if (index == frames.lastIndex) 0 else index + 1
|
|
156
|
+
assertEquals(expectedIndex, (packet[6].toInt() and 255) or ((packet[7].toInt() and 255) shl 8))
|
|
157
|
+
assertEquals(NimoCanvasCodec.crc16(packet.copyOfRange(8, packet.size)),
|
|
158
|
+
(packet[4].toInt() and 255) or ((packet[5].toInt() and 255) shl 8))
|
|
159
|
+
}
|
|
160
|
+
assertArrayEquals(baseline, frames.flatMap { it.drop(8) }.toByteArray())
|
|
161
|
+
}
|
|
162
|
+
assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.frames(4, content, 19) }
|
|
163
|
+
assertThrows(IllegalArgumentException::class.java) { NimoCanvasCodec.frames(4, ByteArray(12281), 512) }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@Test fun strictResponseParserAcceptsStatusOnlyErrors() {
|
|
167
|
+
fun packet(hex: String) = hex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
|
168
|
+
val good = packet("BF000900D87900000704050000FD000000")
|
|
169
|
+
assertArrayEquals(byteArrayOf(0, 0xFD.toByte(), 0, 0, 0), NimoCanvasCodec.response(good)!!.second)
|
|
170
|
+
assertNull(NimoCanvasCodec.response(good + 0))
|
|
171
|
+
assertNull(NimoCanvasCodec.response(good.copyOf().also { it[10] = 4 }))
|
|
172
|
+
assertNull(NimoCanvasCodec.response(good.copyOf().also { it[1] = 2 }))
|
|
173
|
+
val body = byteArrayOf(7, 4, 1, 0, 7)
|
|
174
|
+
val crc = NimoCanvasCodec.crc16(body)
|
|
175
|
+
val statusOnly = byteArrayOf(0xBF.toByte(), 0, 5, 0, crc.toByte(), (crc shr 8).toByte(), 0, 0) + body
|
|
176
|
+
assertArrayEquals(byteArrayOf(7), NimoCanvasCodec.response(statusOnly)!!.second)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@Test fun sceneCompilerKeepsGeometryOrderAndBorderBeforeText() {
|
|
180
|
+
val text = NimoCanvasCodec.Element("text", 10, 20, 100, 40, "hello", border = 2, radius = 3)
|
|
181
|
+
val rectangle = NimoCanvasCodec.Element("rect", 100, 100, 50, 20)
|
|
182
|
+
val actual = NimoCanvasCodec.scene(listOf(text, rectangle)) { _, _, _ -> error("Not an image") }
|
|
183
|
+
val expected = NimoCanvasCodec.replace(listOf(
|
|
184
|
+
NimoCanvasCodec.rectangle(10, 20, 100, 40, 2, 3),
|
|
185
|
+
NimoCanvasCodec.label("hello", 10, 20, 100, 20),
|
|
186
|
+
NimoCanvasCodec.rectangle(100, 100, 50, 20, 1, 0)
|
|
187
|
+
))
|
|
188
|
+
assertArrayEquals(expected, actual)
|
|
189
|
+
assertFalse(actual.contentEquals(NimoCanvasCodec.scene(listOf(rectangle, text)) { _, _, _ -> error("Not an image") }))
|
|
190
|
+
assertFalse(actual.contentEquals(NimoCanvasCodec.scene(listOf(text.copy(x = 11), rectangle)) { _, _, _ -> error("Not an image") }))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@Test fun scenePreflightsInvalidImageBudgetBeforeDecodingAnyPng() {
|
|
194
|
+
val image = NimoCanvasCodec.Element("image", 0, 0, 500, 220, data = "unused")
|
|
195
|
+
var decoded = 0
|
|
196
|
+
assertThrows(IllegalArgumentException::class.java) {
|
|
197
|
+
NimoCanvasCodec.scene(listOf(image, image)) { _, _, _ -> decoded++; ByteArray(110000) }
|
|
198
|
+
}
|
|
199
|
+
assertEquals(0, decoded)
|
|
200
|
+
assertThrows(IllegalArgumentException::class.java) {
|
|
201
|
+
NimoCanvasCodec.scene(listOf(image.copy(type = "html"))) { _, _, _ -> decoded++; ByteArray(110000) }
|
|
202
|
+
}
|
|
203
|
+
assertEquals(0, decoded)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@Test fun zlibCompressesDenseRepeatingImagesAndInflatesToPackedPixels() {
|
|
207
|
+
val gray = ByteArray(32768) { (((it / 17) xor (it / 43)) % 4 * 85).toByte() }
|
|
208
|
+
val bitmap = NimoCanvasCodec.bitmap(0, 0, 256, 128, gray)
|
|
209
|
+
assertEquals(7, bitmap.payload[20].toInt())
|
|
210
|
+
val inflater = java.util.zip.Inflater()
|
|
211
|
+
try {
|
|
212
|
+
inflater.setInput(bitmap.payload.copyOfRange(29, bitmap.payload.size))
|
|
213
|
+
val output = ByteArray((gray.size + 3) / 4)
|
|
214
|
+
assertEquals(output.size, inflater.inflate(output))
|
|
215
|
+
assertTrue(inflater.finished())
|
|
216
|
+
assertArrayEquals(NimoCanvasCodec.pack2(gray), output)
|
|
217
|
+
} finally { inflater.end() }
|
|
218
|
+
NimoCanvasCodec.replace(listOf(bitmap))
|
|
219
|
+
}
|
|
220
|
+
}
|