@mentra/bluetooth-sdk 3.3.0-dev.421 → 3.3.0-dev.422
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/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +0 -7
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +3 -17
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +0 -11
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +0 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +31 -213
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +2 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +6 -41
- package/build/BluetoothSdk.types.d.ts +0 -10
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +0 -9
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +0 -2
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/package.json +2 -2
- package/src/BluetoothSdk.types.ts +0 -10
- package/src/_private/BluetoothSdkModule.ts +0 -13
- package/src/generated/releaseMetadata.ts +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleEvidenceLog.kt +0 -454
- package/android/src/test/java/com/mentra/bluetoothsdk/WifiScanEvidenceTest.kt +0 -117
- package/android/src/test/java/com/mentra/bluetoothsdk/debug/BleEvidenceLogTest.kt +0 -282
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveEvidenceTest.kt +0 -603
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/MessageChunkReassemblerOriginTest.kt +0 -51
- package/build/_private/diagnosticRender.d.ts +0 -12
- package/build/_private/diagnosticRender.d.ts.map +0 -1
- package/build/_private/diagnosticRender.js +0 -27
- package/build/_private/diagnosticRender.js.map +0 -1
- package/src/_private/diagnosticRender.ts +0 -35
|
@@ -1,603 +0,0 @@
|
|
|
1
|
-
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
-
|
|
3
|
-
import android.bluetooth.BluetoothAdapter
|
|
4
|
-
import android.bluetooth.BluetoothGatt
|
|
5
|
-
import android.bluetooth.BluetoothGattCharacteristic
|
|
6
|
-
import android.bluetooth.BluetoothGattService
|
|
7
|
-
import android.bluetooth.BluetoothProfile
|
|
8
|
-
import android.os.Looper
|
|
9
|
-
import androidx.test.core.app.ApplicationProvider
|
|
10
|
-
import com.mentra.bluetoothsdk.Bridge
|
|
11
|
-
import com.mentra.bluetoothsdk.DeviceStore
|
|
12
|
-
import com.mentra.bluetoothsdk.debug.BleEvidenceLog
|
|
13
|
-
import com.mentra.bluetoothsdk.utils.K900ProtocolUtils
|
|
14
|
-
import com.mentra.bluetoothsdk.utils.MessageChunker
|
|
15
|
-
import java.nio.charset.StandardCharsets
|
|
16
|
-
import java.time.Duration
|
|
17
|
-
import java.util.UUID
|
|
18
|
-
import org.json.JSONArray
|
|
19
|
-
import org.json.JSONObject
|
|
20
|
-
import org.junit.After
|
|
21
|
-
import org.junit.Assert.assertEquals
|
|
22
|
-
import org.junit.Assert.assertFalse
|
|
23
|
-
import org.junit.Assert.assertNotEquals
|
|
24
|
-
import org.junit.Assert.assertNotNull
|
|
25
|
-
import org.junit.Assert.assertTrue
|
|
26
|
-
import org.junit.Before
|
|
27
|
-
import org.junit.Test
|
|
28
|
-
import org.junit.runner.RunWith
|
|
29
|
-
import org.robolectric.RobolectricTestRunner
|
|
30
|
-
import org.robolectric.Shadows.shadowOf
|
|
31
|
-
import org.robolectric.annotation.Config
|
|
32
|
-
import org.robolectric.annotation.Implementation
|
|
33
|
-
import org.robolectric.annotation.Implements
|
|
34
|
-
import org.robolectric.annotation.LooperMode
|
|
35
|
-
import org.robolectric.shadow.api.Shadow
|
|
36
|
-
import org.robolectric.shadows.ShadowBluetoothGatt
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Drives the production MentraLive GATT callback, decoder, send queue and Bridge with a fake
|
|
40
|
-
* GATT transport (Robolectric shadows) and checks the provenance the evidence log records.
|
|
41
|
-
*/
|
|
42
|
-
@RunWith(RobolectricTestRunner::class)
|
|
43
|
-
@Config(
|
|
44
|
-
sdk = [33],
|
|
45
|
-
shadows = [G1PairingTest.ShadowLc3Cpp::class, MentraLiveEvidenceTest.FakeGattWrites::class],
|
|
46
|
-
instrumentedPackages = ["com.mentra.lc3Lib"],
|
|
47
|
-
)
|
|
48
|
-
@LooperMode(LooperMode.Mode.PAUSED)
|
|
49
|
-
class MentraLiveEvidenceTest {
|
|
50
|
-
/** Fake GATT write acceptance at the Android API boundary. */
|
|
51
|
-
@Implements(BluetoothGatt::class)
|
|
52
|
-
class FakeGattWrites : ShadowBluetoothGatt() {
|
|
53
|
-
companion object {
|
|
54
|
-
@JvmStatic var accept = false
|
|
55
|
-
@JvmStatic val written = mutableListOf<String>()
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
@Suppress("DEPRECATION")
|
|
59
|
-
@Implementation
|
|
60
|
-
protected fun writeCharacteristic(characteristic: BluetoothGattCharacteristic): Boolean {
|
|
61
|
-
if (accept) written.add(String(characteristic.value, StandardCharsets.ISO_8859_1))
|
|
62
|
-
return accept
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
private val serviceUuid = UUID.fromString("00004860-0000-1000-8000-00805f9b34fb")
|
|
67
|
-
private val rxUuid = UUID.fromString("000070FF-0000-1000-8000-00805f9b34fb")
|
|
68
|
-
private val txUuid = UUID.fromString("000071FF-0000-1000-8000-00805f9b34fb")
|
|
69
|
-
private val peerA = "AA:BB:CC:DD:EE:01"
|
|
70
|
-
private val peerB = "AA:BB:CC:DD:EE:02"
|
|
71
|
-
private val events = mutableListOf<Pair<String, Map<String, Any>>>()
|
|
72
|
-
private lateinit var sinkId: String
|
|
73
|
-
private val managers = mutableListOf<MentraLive>()
|
|
74
|
-
|
|
75
|
-
@Before fun setup() {
|
|
76
|
-
Bridge.initialize(ApplicationProvider.getApplicationContext())
|
|
77
|
-
shadowOf(BluetoothAdapter.getDefaultAdapter()).setState(BluetoothAdapter.STATE_ON)
|
|
78
|
-
BleEvidenceLog.resetForTest()
|
|
79
|
-
FakeGattWrites.accept = false
|
|
80
|
-
FakeGattWrites.written.clear()
|
|
81
|
-
sinkId = Bridge.addEventSink { type, body -> events.add(type to HashMap(body)) }
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
@After fun teardown() {
|
|
85
|
-
Bridge.removeEventSink(sinkId)
|
|
86
|
-
managers.forEach { it.destroy() }
|
|
87
|
-
// Robolectric does not deliver the disconnect callback; finish the production timeout.
|
|
88
|
-
shadowOf(Looper.getMainLooper()).idleFor(Duration.ofSeconds(3))
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ---- helpers -------------------------------------------------------------------------
|
|
92
|
-
|
|
93
|
-
private fun idle() = shadowOf(Looper.getMainLooper()).idle()
|
|
94
|
-
|
|
95
|
-
private fun idleFor(ms: Long) = shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(ms))
|
|
96
|
-
|
|
97
|
-
private fun manager() = MentraLive().also { managers.add(it) }
|
|
98
|
-
|
|
99
|
-
/** The send rate limiter reads wall-clock time, which Robolectric does not advance. */
|
|
100
|
-
private fun drainSends() {
|
|
101
|
-
Thread.sleep(250)
|
|
102
|
-
idleFor(1_000)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
private fun callback(gatt: BluetoothGatt) = Shadow.extract<ShadowBluetoothGatt>(gatt).gattCallback
|
|
106
|
-
|
|
107
|
-
/** Connects [live] to [address] through connectById and the production GATT callback. */
|
|
108
|
-
private fun connect(live: MentraLive, address: String): BluetoothGatt {
|
|
109
|
-
val name = "MENTRA_LIVE_" + address.takeLast(2)
|
|
110
|
-
val device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address)
|
|
111
|
-
shadowOf(device).setName(name)
|
|
112
|
-
DeviceStore.set("bluetooth", "pending_device_name", name)
|
|
113
|
-
DeviceStore.set("bluetooth", "device_name", name)
|
|
114
|
-
DeviceStore.set("bluetooth", "pending_device_address", "")
|
|
115
|
-
DeviceStore.set("bluetooth", "device_address", address)
|
|
116
|
-
val before = shadowOf(device).bluetoothGatts.size
|
|
117
|
-
live.connectById(name)
|
|
118
|
-
idle()
|
|
119
|
-
// A replaced connection waits for its teardown (timeout without a Robolectric callback).
|
|
120
|
-
if (shadowOf(device).bluetoothGatts.size == before) idleFor(3_000)
|
|
121
|
-
val gatts = shadowOf(device).bluetoothGatts
|
|
122
|
-
assertEquals(before + 1, gatts.size)
|
|
123
|
-
val gatt = gatts.last()
|
|
124
|
-
val service = BluetoothGattService(serviceUuid, BluetoothGattService.SERVICE_TYPE_PRIMARY)
|
|
125
|
-
service.addCharacteristic(BluetoothGattCharacteristic(rxUuid, BluetoothGattCharacteristic.PROPERTY_NOTIFY, 0))
|
|
126
|
-
service.addCharacteristic(BluetoothGattCharacteristic(txUuid, BluetoothGattCharacteristic.PROPERTY_WRITE, 0))
|
|
127
|
-
Shadow.extract<ShadowBluetoothGatt>(gatt).addDiscoverableService(service)
|
|
128
|
-
callback(gatt).onConnectionStateChange(gatt, BluetoothGatt.GATT_SUCCESS, BluetoothProfile.STATE_CONNECTED)
|
|
129
|
-
idle()
|
|
130
|
-
callback(gatt).onServicesDiscovered(gatt, BluetoothGatt.GATT_SUCCESS)
|
|
131
|
-
idle()
|
|
132
|
-
return gatt
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
private fun notifyRaw(gatt: BluetoothGatt, bytes: ByteArray) {
|
|
136
|
-
callback(gatt).onCharacteristicChanged(gatt, BluetoothGattCharacteristic(rxUuid, 0, 0), bytes)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
private fun notifyJson(gatt: BluetoothGatt, json: String) {
|
|
140
|
-
notifyRaw(gatt, json.toByteArray(StandardCharsets.UTF_8))
|
|
141
|
-
idle()
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
private fun notifyK900(gatt: BluetoothGatt, json: String) {
|
|
145
|
-
notifyRaw(gatt, K900ProtocolUtils.packDataCommand(json.toByteArray(StandardCharsets.UTF_8), K900ProtocolUtils.CMD_TYPE_STRING))
|
|
146
|
-
idle()
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
private fun records(target: String? = null): List<JSONObject> {
|
|
150
|
-
val array = BleEvidenceLog.snapshot(0, target).getJSONArray("records")
|
|
151
|
-
return (0 until array.length()).map { array.getJSONObject(it) }
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
private fun kind(kind: String, target: String? = null) = records(target).filter { it.getString("kind") == kind }
|
|
155
|
-
|
|
156
|
-
private fun batteryEvents() = events.filter { it.first == "battery_status" }.map { it.second }
|
|
157
|
-
|
|
158
|
-
private fun origin(record: JSONObject): Pair<Long, Long>? =
|
|
159
|
-
if (record.isNull("origin")) null
|
|
160
|
-
else record.getJSONObject("origin").let { it.getLong("connection") to it.getLong("wire") }
|
|
161
|
-
|
|
162
|
-
// ---- receive provenance ----------------------------------------------------------------
|
|
163
|
-
|
|
164
|
-
@Test
|
|
165
|
-
fun `battery notification keeps its accepted origin through decoder and bridge`() {
|
|
166
|
-
val live = manager()
|
|
167
|
-
val gatt = connect(live, peerA)
|
|
168
|
-
notifyJson(gatt, """{"type":"battery_status","percent":57}""")
|
|
169
|
-
|
|
170
|
-
val event = batteryEvents().single()
|
|
171
|
-
assertEquals(57, event["level"])
|
|
172
|
-
val eventId = event["eventId"] as String
|
|
173
|
-
val connection = kind("connection_accepted").single()
|
|
174
|
-
assertEquals(peerA, connection.getString("peerMac"))
|
|
175
|
-
val battery = kind("battery").single()
|
|
176
|
-
assertEquals(BleEvidenceLog.eventId(battery.getLong("seq")), eventId)
|
|
177
|
-
assertEquals(connection.getLong("seq") to connection.getLong("seq"), origin(battery))
|
|
178
|
-
assertEquals("battery_status", battery.getString("source"))
|
|
179
|
-
assertFalse("percent-only source never claims PMU charging", battery.has("pmuCharging"))
|
|
180
|
-
val dispatch = kind("bridge_dispatch").single()
|
|
181
|
-
assertEquals(eventId, dispatch.getString("eventId"))
|
|
182
|
-
assertTrue(dispatch.getInt("sinks") > 0)
|
|
183
|
-
assertTrue(dispatch.getLong("seq") > battery.getLong("seq"))
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
@Test
|
|
187
|
-
fun `only the sr_hrt PMU bit is charging provenance`() {
|
|
188
|
-
val gatt = connect(manager(), peerA)
|
|
189
|
-
notifyK900(gatt, """{"C":"sr_hrt","B":{"pt":64,"ready":1,"charg":1}}""")
|
|
190
|
-
notifyK900(gatt, """{"C":"sr_batv","B":{"vt":4010,"pt":63}}""")
|
|
191
|
-
notifyJson(gatt, """{"type":"battery_status","percent":62}""")
|
|
192
|
-
|
|
193
|
-
val batteries = kind("battery")
|
|
194
|
-
assertEquals(listOf(64, 63, 62), batteries.map { it.getInt("percent") })
|
|
195
|
-
assertEquals(listOf("k900_sr_hrt", "k900_sr_batv", "battery_status"), batteries.map { it.getString("source") })
|
|
196
|
-
assertTrue(batteries[0].getBoolean("pmuCharging"))
|
|
197
|
-
// The app still shows the inherited charging state, but it is not PMU evidence.
|
|
198
|
-
assertEquals(listOf(true, true, true), batteryEvents().map { it["charging"] })
|
|
199
|
-
assertFalse(batteries[1].has("pmuCharging"))
|
|
200
|
-
assertFalse(batteries[2].has("pmuCharging"))
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
@Test
|
|
204
|
-
fun `an unchanged percentage is two events with the same value, not a change`() {
|
|
205
|
-
val gatt = connect(manager(), peerA)
|
|
206
|
-
notifyJson(gatt, """{"type":"battery_status","percent":57}""")
|
|
207
|
-
notifyJson(gatt, """{"type":"battery_status","percent":57}""")
|
|
208
|
-
|
|
209
|
-
val batteries = kind("battery")
|
|
210
|
-
assertEquals(listOf(57, 57), batteries.map { it.getInt("percent") })
|
|
211
|
-
assertNotEquals(batteryEvents()[0]["eventId"], batteryEvents()[1]["eventId"])
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
@Test
|
|
215
|
-
fun `queued notification bytes are copied before the owner decodes them`() {
|
|
216
|
-
val gatt = connect(manager(), peerA)
|
|
217
|
-
val bytes = """{"type":"battery_status","percent":41}""".toByteArray(StandardCharsets.UTF_8)
|
|
218
|
-
notifyRaw(gatt, bytes)
|
|
219
|
-
bytes[bytes.indexOf('4'.code.toByte())] = '9'.code.toByte()
|
|
220
|
-
idle()
|
|
221
|
-
assertEquals(listOf(41), kind("battery").map { it.getInt("percent") })
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// ---- connection identity -----------------------------------------------------------------
|
|
225
|
-
|
|
226
|
-
@Test
|
|
227
|
-
fun `wrong peer and same-peer reconnect get distinct connection identities`() {
|
|
228
|
-
// Switching glasses disconnects (destroys) the manager and creates another.
|
|
229
|
-
for ((peer, percent) in listOf(peerA to 50, peerB to 51, peerA to 52)) {
|
|
230
|
-
managers.lastOrNull()?.disconnect()
|
|
231
|
-
idleFor(3_000)
|
|
232
|
-
notifyJson(connect(manager(), peer), """{"type":"battery_status","percent":$percent}""")
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
val connections = kind("connection_accepted")
|
|
236
|
-
assertEquals(listOf(peerA, peerB, peerA), connections.map { it.getString("peerMac") })
|
|
237
|
-
assertEquals(3, connections.map { it.getLong("seq") }.distinct().size)
|
|
238
|
-
// Each value is bound to the connection that received it, never the later current peer.
|
|
239
|
-
assertEquals(connections.map { it.getLong("seq") }, kind("battery").map { origin(it)!!.first })
|
|
240
|
-
assertEquals(listOf("teardown", "teardown"), kind("connection_closed").map { it.getString("reason") })
|
|
241
|
-
val current = BleEvidenceLog.snapshot(0, null).getJSONObject("current")
|
|
242
|
-
assertEquals(peerA, current.getString("peerMac"))
|
|
243
|
-
assertEquals(connections.last().getLong("seq"), current.getLong("connection"))
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
@Test
|
|
247
|
-
fun `remote disconnect closes the identity and later callbacks from that GATT are rejected`() {
|
|
248
|
-
val gatt = connect(manager(), peerA)
|
|
249
|
-
callback(gatt).onConnectionStateChange(gatt, BluetoothGatt.GATT_SUCCESS, BluetoothProfile.STATE_DISCONNECTED)
|
|
250
|
-
idle()
|
|
251
|
-
notifyJson(gatt, """{"type":"battery_status","percent":33}""")
|
|
252
|
-
|
|
253
|
-
assertTrue(kind("connection_closed").any { it.getString("reason") == "remote_disconnect" })
|
|
254
|
-
assertTrue(kind("battery").isEmpty())
|
|
255
|
-
assertEquals("stale_gatt", kind("receive_rejected").last().getString("reason"))
|
|
256
|
-
assertTrue(BleEvidenceLog.snapshot(0, null).getJSONObject("current").isNull("connection"))
|
|
257
|
-
|
|
258
|
-
// High-rate audio from the dead link is not evidence and must not flood the bounded log.
|
|
259
|
-
val before = kind("receive_rejected").size
|
|
260
|
-
val lc3 = BluetoothGattCharacteristic(UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"), 0, 0)
|
|
261
|
-
repeat(20) { callback(gatt).onCharacteristicChanged(gatt, lc3, byteArrayOf(0xA0.toByte(), 1, 2)) }
|
|
262
|
-
idle()
|
|
263
|
-
assertEquals(before, kind("receive_rejected").size)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
@Test
|
|
267
|
-
fun `manager recreation in one process never reuses a connection identity`() {
|
|
268
|
-
val first = manager()
|
|
269
|
-
val oldGatt = connect(first, peerA)
|
|
270
|
-
notifyJson(oldGatt, """{"type":"battery_status","percent":70}""")
|
|
271
|
-
first.destroy()
|
|
272
|
-
idleFor(3_000)
|
|
273
|
-
val second = manager()
|
|
274
|
-
notifyJson(connect(second, peerA), """{"type":"battery_status","percent":71}""")
|
|
275
|
-
// A late callback for the destroyed manager's GATT cannot produce a value.
|
|
276
|
-
notifyJson(oldGatt, """{"type":"battery_status","percent":99}""")
|
|
277
|
-
|
|
278
|
-
val connections = kind("connection_accepted").map { it.getLong("seq") }
|
|
279
|
-
assertEquals(2, connections.distinct().size)
|
|
280
|
-
assertEquals(listOf(70, 71), kind("battery").map { it.getInt("percent") })
|
|
281
|
-
assertEquals(connections, kind("battery").map { origin(it)!!.first })
|
|
282
|
-
assertEquals("stale_gatt", kind("receive_rejected").last().getString("reason"))
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
@Test
|
|
286
|
-
fun `a notification queued behind teardown is rejected explicitly`() {
|
|
287
|
-
val live = manager()
|
|
288
|
-
val gatt = connect(live, peerA)
|
|
289
|
-
live.disconnect()
|
|
290
|
-
idle()
|
|
291
|
-
notifyJson(gatt, """{"type":"battery_status","percent":12}""")
|
|
292
|
-
|
|
293
|
-
assertTrue(kind("battery").isEmpty())
|
|
294
|
-
assertTrue(batteryEvents().isEmpty())
|
|
295
|
-
assertEquals("teardown", kind("receive_rejected").last().getString("reason"))
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
@Test
|
|
299
|
-
fun `wire reset without disconnect starts a new wire epoch and drops straddling chunks`() {
|
|
300
|
-
val gatt = connect(manager(), peerA)
|
|
301
|
-
val padded = """{"type":"battery_status","percent":44,"pad":"${"x".repeat(400)}"}"""
|
|
302
|
-
val chunks = MessageChunker.createChunks(padded, 0L)
|
|
303
|
-
assertTrue(chunks.size > 1)
|
|
304
|
-
notifyJson(gatt, chunks[0].toString())
|
|
305
|
-
notifyJson(gatt, """{"type":"glasses_ready"}""")
|
|
306
|
-
chunks.drop(1).forEach { notifyJson(gatt, it.toString()) }
|
|
307
|
-
assertTrue("a message straddling the reset must not be decoded", kind("battery").isEmpty())
|
|
308
|
-
|
|
309
|
-
notifyJson(gatt, """{"type":"battery_status","percent":45}""")
|
|
310
|
-
val connection = kind("connection_accepted").single().getLong("seq")
|
|
311
|
-
val wireReset = kind("wire_reset").single()
|
|
312
|
-
assertEquals(connection, wireReset.getLong("connection"))
|
|
313
|
-
assertEquals(connection to wireReset.getLong("seq"), origin(kind("battery").single()))
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// ---- Wi-Fi scan send and chunks -------------------------------------------------------
|
|
317
|
-
|
|
318
|
-
@Test
|
|
319
|
-
fun `scan command counts as sent only after GATT accepts and acknowledges the write`() {
|
|
320
|
-
FakeGattWrites.accept = true
|
|
321
|
-
val gatt = connect(manager(), peerA)
|
|
322
|
-
live(gatt).requestWifiScan("scan-accepted")
|
|
323
|
-
drainSends()
|
|
324
|
-
val tx = BluetoothGattCharacteristic(txUuid, BluetoothGattCharacteristic.PROPERTY_WRITE, 0)
|
|
325
|
-
callback(gatt).onCharacteristicWrite(gatt, tx, BluetoothGatt.GATT_SUCCESS)
|
|
326
|
-
idle()
|
|
327
|
-
|
|
328
|
-
val sends = kind("scan_send").filter { it.getString("scanId") == "scan-accepted" }
|
|
329
|
-
assertEquals(listOf("queued", "gatt_accepted", "write_ok"), sends.map { it.getString("outcome") })
|
|
330
|
-
assertEquals(kind("connection_accepted").single().getLong("seq"), sends.last().getLong("connection"))
|
|
331
|
-
assertTrue(FakeGattWrites.written.any { it.contains("scan-accepted") })
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
@Test
|
|
335
|
-
fun `refused or failed scan writes never become an accepted send`() {
|
|
336
|
-
val gatt = connect(manager(), peerA)
|
|
337
|
-
live(gatt).requestWifiScan("scan-refused")
|
|
338
|
-
drainSends()
|
|
339
|
-
assertEquals(
|
|
340
|
-
listOf("queued", "gatt_refused"),
|
|
341
|
-
kind("scan_send").filter { it.getString("scanId") == "scan-refused" }.map { it.getString("outcome") },
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
FakeGattWrites.accept = true
|
|
345
|
-
live(gatt).requestWifiScan("scan-failed")
|
|
346
|
-
drainSends()
|
|
347
|
-
callback(gatt).onCharacteristicWrite(
|
|
348
|
-
gatt,
|
|
349
|
-
BluetoothGattCharacteristic(txUuid, BluetoothGattCharacteristic.PROPERTY_WRITE, 0),
|
|
350
|
-
133,
|
|
351
|
-
)
|
|
352
|
-
idle()
|
|
353
|
-
assertEquals(
|
|
354
|
-
listOf("queued", "gatt_accepted", "write_failed"),
|
|
355
|
-
kind("scan_send").filter { it.getString("scanId") == "scan-failed" }.map { it.getString("outcome") },
|
|
356
|
-
)
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
@Test
|
|
360
|
-
fun `scan requested while disconnected is refused, not sent`() {
|
|
361
|
-
FakeGattWrites.accept = true
|
|
362
|
-
val live = manager()
|
|
363
|
-
val gatt = connect(live, peerA)
|
|
364
|
-
callback(gatt).onConnectionStateChange(gatt, BluetoothGatt.GATT_SUCCESS, BluetoothProfile.STATE_DISCONNECTED)
|
|
365
|
-
idle()
|
|
366
|
-
live.requestWifiScan("scan-offline")
|
|
367
|
-
drainSends()
|
|
368
|
-
|
|
369
|
-
// Queued is intent only; it never reaches GATT without a connection.
|
|
370
|
-
val outcomes = kind("scan_send").filter { it.getString("scanId") == "scan-offline" }.map { it.getString("outcome") }
|
|
371
|
-
assertEquals(listOf("queued"), outcomes)
|
|
372
|
-
assertFalse(FakeGattWrites.written.any { it.contains("scan-offline") })
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
@Test
|
|
376
|
-
fun `scan chunks carry origin and expose only the protected target`() {
|
|
377
|
-
val gatt = connect(manager(), peerA)
|
|
378
|
-
val chunk = JSONObject()
|
|
379
|
-
.put("type", "wifi_scan_result")
|
|
380
|
-
.put("scanId", "scan-1")
|
|
381
|
-
.put("scan_complete", true)
|
|
382
|
-
.put(
|
|
383
|
-
"networks_neo",
|
|
384
|
-
JSONArray()
|
|
385
|
-
.put(JSONObject().put("ssid", "LabAP").put("requiresPassword", true))
|
|
386
|
-
.put(JSONObject().put("ssid", "Neighbor").put("requiresPassword", false)),
|
|
387
|
-
)
|
|
388
|
-
notifyJson(gatt, chunk.toString())
|
|
389
|
-
|
|
390
|
-
val event = events.single { it.first == "wifi_scan_result" }.second
|
|
391
|
-
val eventId = event["eventId"] as String
|
|
392
|
-
val target = BleEvidenceLog.sha256Hex("LabAP")
|
|
393
|
-
val record = kind("scan_chunk", target).single()
|
|
394
|
-
assertEquals(BleEvidenceLog.eventId(record.getLong("seq")), eventId)
|
|
395
|
-
assertEquals("scan-1", record.getString("scanId"))
|
|
396
|
-
assertTrue(record.getBoolean("complete"))
|
|
397
|
-
assertEquals(kind("connection_accepted").single().getLong("seq"), origin(record)!!.first)
|
|
398
|
-
assertTrue(record.getBoolean("targetSeen"))
|
|
399
|
-
assertTrue(record.getBoolean("targetRequiresPassword"))
|
|
400
|
-
val serialized = BleEvidenceLog.snapshot(0, target).toString()
|
|
401
|
-
assertFalse(serialized.contains("LabAP"))
|
|
402
|
-
assertFalse(serialized.contains("Neighbor"))
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
@Test
|
|
406
|
-
fun `a decoded scan with more networks than retained digests never certifies target absence`() {
|
|
407
|
-
val gatt = connect(manager(), peerA)
|
|
408
|
-
val networks = JSONArray()
|
|
409
|
-
(1..70).forEach { networks.put(JSONObject().put("ssid", "Net$it").put("requiresPassword", it == 70)) }
|
|
410
|
-
notifyJson(
|
|
411
|
-
gatt,
|
|
412
|
-
JSONObject().put("type", "wifi_scan_result").put("scanId", "scan-many").put("scan_complete", true)
|
|
413
|
-
.put("networks_neo", networks).toString(),
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
// The normal consumer still receives every network unchanged.
|
|
417
|
-
@Suppress("UNCHECKED_CAST")
|
|
418
|
-
val delivered = events.single { it.first == "wifi_scan_result" }.second["networks"] as List<Map<String, Any>>
|
|
419
|
-
assertEquals(70, delivered.size)
|
|
420
|
-
assertEquals(true, delivered.last()["requiresPassword"])
|
|
421
|
-
|
|
422
|
-
val beyond = kind("scan_chunk", BleEvidenceLog.sha256Hex("Net70")).single()
|
|
423
|
-
assertTrue("target beyond retained digests must not be reported absent", beyond.isNull("targetSeen"))
|
|
424
|
-
assertEquals("incomplete", beyond.getString("targetCoverage"))
|
|
425
|
-
val covered = kind("scan_chunk", BleEvidenceLog.sha256Hex("Net1")).single()
|
|
426
|
-
assertTrue(covered.getBoolean("targetSeen"))
|
|
427
|
-
assertFalse(covered.getBoolean("targetRequiresPassword"))
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
@Test
|
|
431
|
-
fun `a scan write queued before a send-queue reset keeps its generation and is never sent`() {
|
|
432
|
-
FakeGattWrites.accept = true
|
|
433
|
-
val live = manager()
|
|
434
|
-
val gatt = connect(live, peerA)
|
|
435
|
-
val tx = BluetoothGattCharacteristic(txUuid, BluetoothGattCharacteristic.PROPERTY_WRITE, 0)
|
|
436
|
-
live.requestWifiScan("scan-first")
|
|
437
|
-
drainSends()
|
|
438
|
-
callback(gatt).onCharacteristicWrite(gatt, tx, BluetoothGatt.GATT_SUCCESS)
|
|
439
|
-
// Without real time passing, the rate limiter keeps the next write queued.
|
|
440
|
-
live.requestWifiScan("scan-pending")
|
|
441
|
-
idle()
|
|
442
|
-
callback(gatt).onConnectionStateChange(gatt, BluetoothGatt.GATT_SUCCESS, BluetoothProfile.STATE_DISCONNECTED)
|
|
443
|
-
idle()
|
|
444
|
-
drainSends()
|
|
445
|
-
|
|
446
|
-
val generations = kind("ble_generation")
|
|
447
|
-
assertEquals(listOf(0L, 1L), generations.map { it.getLong("generation") })
|
|
448
|
-
val before = generations[0].getLong("seq")
|
|
449
|
-
val after = generations[1].getLong("seq")
|
|
450
|
-
assertTrue(kind("connection_accepted").single().getLong("seq") > before)
|
|
451
|
-
val first = kind("scan_send").filter { it.getString("scanId") == "scan-first" }
|
|
452
|
-
assertEquals(listOf("queued", "gatt_accepted", "write_ok"), first.map { it.getString("outcome") })
|
|
453
|
-
assertTrue(first.all { it.getLong("writeGeneration") == before })
|
|
454
|
-
val pending = kind("scan_send").filter { it.getString("scanId") == "scan-pending" }
|
|
455
|
-
assertEquals(listOf("queued"), pending.map { it.getString("outcome") })
|
|
456
|
-
assertEquals(before, pending.single().getLong("writeGeneration"))
|
|
457
|
-
assertTrue(after > pending.single().getLong("seq"))
|
|
458
|
-
assertEquals(after, BleEvidenceLog.snapshot(0, null).getJSONObject("current").getLong("writeGeneration"))
|
|
459
|
-
assertFalse(FakeGattWrites.written.any { it.contains("scan-pending") })
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
private fun scanJson(networksNeo: String?) =
|
|
463
|
-
"{\"type\":\"wifi_scan_result\",\"scanId\":\"scan-raw\",\"scan_complete\":true" +
|
|
464
|
-
(networksNeo?.let { ",\"networks_neo\":$it" } ?: "") + "}"
|
|
465
|
-
|
|
466
|
-
private fun scanDelivered(): List<String> {
|
|
467
|
-
@Suppress("UNCHECKED_CAST")
|
|
468
|
-
val networks = events.last { it.first == "wifi_scan_result" }.second["networks"] as List<Map<String, Any>>
|
|
469
|
-
return networks.map { it["ssid"] as String }
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
@Test
|
|
473
|
-
fun `an unparsed scan entry leaves later targets unknown, not absent`() {
|
|
474
|
-
val gatt = connect(manager(), peerA)
|
|
475
|
-
val other = "{\"ssid\":\"Other\",\"requiresPassword\":false}"
|
|
476
|
-
val lab = "{\"ssid\":\"LabAP\",\"requiresPassword\":true}"
|
|
477
|
-
for (bad in listOf("null", "42", "\"text\"", "[1]")) {
|
|
478
|
-
events.clear()
|
|
479
|
-
BleEvidenceLog.resetForTest()
|
|
480
|
-
notifyJson(gatt, scanJson("[$other,$bad,$lab]"))
|
|
481
|
-
// The normal payload keeps its existing behavior: entries before the bad one.
|
|
482
|
-
assertEquals(listOf("Other"), scanDelivered())
|
|
483
|
-
val lab70 = kind("scan_chunk", BleEvidenceLog.sha256Hex("LabAP")).single()
|
|
484
|
-
assertTrue("[$bad] must not certify LabAP absent", lab70.isNull("targetSeen"))
|
|
485
|
-
assertEquals("incomplete", lab70.getString("targetCoverage"))
|
|
486
|
-
// A parsed target is present, but unparsed entries could repeat it with other security.
|
|
487
|
-
val seen = kind("scan_chunk", BleEvidenceLog.sha256Hex("Other")).single()
|
|
488
|
-
assertTrue(seen.getBoolean("targetSeen"))
|
|
489
|
-
assertTrue("[$bad] security must stay uncertified", seen.isNull("targetRequiresPassword"))
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
@Test
|
|
494
|
-
fun `a malformed or missing network array is never a certified empty scan`() {
|
|
495
|
-
val gatt = connect(manager(), peerA)
|
|
496
|
-
for (form in listOf("{\"ssid\":\"LabAP\"}", "\"LabAP\"", "null", null)) {
|
|
497
|
-
events.clear()
|
|
498
|
-
BleEvidenceLog.resetForTest()
|
|
499
|
-
notifyJson(gatt, scanJson(form))
|
|
500
|
-
assertEquals(emptyList<String>(), scanDelivered())
|
|
501
|
-
val chunk = kind("scan_chunk", BleEvidenceLog.sha256Hex("LabAP")).single()
|
|
502
|
-
assertTrue("networks_neo=$form must not certify absence", chunk.isNull("targetSeen"))
|
|
503
|
-
assertEquals("incomplete", chunk.getString("targetCoverage"))
|
|
504
|
-
}
|
|
505
|
-
// Control: a well-formed empty list is a complete, certified empty chunk.
|
|
506
|
-
BleEvidenceLog.resetForTest()
|
|
507
|
-
notifyJson(gatt, scanJson("[]"))
|
|
508
|
-
val empty = kind("scan_chunk", BleEvidenceLog.sha256Hex("LabAP")).single()
|
|
509
|
-
assertEquals("complete", empty.getString("targetCoverage"))
|
|
510
|
-
assertFalse(empty.getBoolean("targetSeen"))
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
@Test
|
|
514
|
-
fun `fallback battery values are displayed without fresh provenance`() {
|
|
515
|
-
val gatt = connect(manager(), peerA)
|
|
516
|
-
notifyJson(gatt, """{"type":"battery_status","percent":57}""")
|
|
517
|
-
for (bad in listOf("""{"type":"battery_status"}""", """{"type":"battery_status","percent":"abc"}""",
|
|
518
|
-
"""{"type":"battery_status","percent":null}""", """{"type":"battery_status","percent":57.5}""",
|
|
519
|
-
"""{"type":"battery_status","percent":150}""")) {
|
|
520
|
-
notifyJson(gatt, bad)
|
|
521
|
-
}
|
|
522
|
-
val events = batteryEvents()
|
|
523
|
-
// Display behavior is unchanged: missing/invalid fields fall back as before.
|
|
524
|
-
assertEquals(listOf(57, 57, 57, 57, 57, 150), events.map { it["level"] })
|
|
525
|
-
assertTrue(events.first().containsKey("eventId"))
|
|
526
|
-
assertTrue("fallback values must not carry an event id", events.drop(1).none { it.containsKey("eventId") })
|
|
527
|
-
assertEquals(listOf(57), kind("battery").map { it.getInt("percent") })
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
@Test
|
|
531
|
-
fun `K900 battery fields are validated before provenance`() {
|
|
532
|
-
val gatt = connect(manager(), peerA)
|
|
533
|
-
notifyK900(gatt, """{"C":"sr_hrt","B":{"pt":64,"ready":1,"charg":1}}""")
|
|
534
|
-
notifyK900(gatt, """{"C":"sr_batv","B":{"vt":4010,"pt":63}}""")
|
|
535
|
-
notifyK900(gatt, """{"C":"sr_batv","B":{"vt":4010}}""")
|
|
536
|
-
notifyK900(gatt, """{"C":"sr_batv","B":{"vt":4010,"pt":"x"}}""")
|
|
537
|
-
notifyK900(gatt, """{"C":"sr_hrt","B":{"pt":61,"ready":1,"charg":2}}""")
|
|
538
|
-
|
|
539
|
-
val events = batteryEvents()
|
|
540
|
-
// sr_batv without a valid pt keeps its existing 0 display value, with no id.
|
|
541
|
-
assertEquals(listOf(64, 63, 0, 0, 61), events.map { it["level"] })
|
|
542
|
-
assertEquals(listOf(true, true, false, false, true), events.map { it.containsKey("eventId") })
|
|
543
|
-
val batteries = kind("battery")
|
|
544
|
-
assertEquals(listOf(64, 63, 61), batteries.map { it.getInt("percent") })
|
|
545
|
-
assertTrue(batteries[0].getBoolean("pmuCharging"))
|
|
546
|
-
assertFalse(batteries[1].has("pmuCharging"))
|
|
547
|
-
// charg=2 is not a PMU bit: the percentage is measured, the charging claim is not.
|
|
548
|
-
assertEquals("k900_sr_hrt", batteries[2].getString("source"))
|
|
549
|
-
assertFalse(batteries[2].has("pmuCharging"))
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
@Test
|
|
553
|
-
fun `only an exact 0 or 1 PMU charg bit is charging provenance`() {
|
|
554
|
-
val gatt = connect(manager(), peerA)
|
|
555
|
-
val chargValues = listOf("1", "0", "0.5", "1.5", "1.0", "0.0", "\"1\"")
|
|
556
|
-
for (charg in chargValues) {
|
|
557
|
-
notifyK900(gatt, """{"C":"sr_hrt","B":{"pt":60,"ready":1,"charg":$charg}}""")
|
|
558
|
-
}
|
|
559
|
-
// A non-numeric charg keeps the existing behavior of skipping the update entirely.
|
|
560
|
-
notifyK900(gatt, """{"C":"sr_hrt","B":{"pt":60,"ready":1,"charg":true}}""")
|
|
561
|
-
|
|
562
|
-
val batteries = kind("battery")
|
|
563
|
-
// Each percentage is measured; only a value exactly equal to 0 or 1 is PMU evidence.
|
|
564
|
-
assertEquals(List(chargValues.size) { 60 }, batteries.map { it.getInt("percent") })
|
|
565
|
-
assertEquals(
|
|
566
|
-
listOf<Boolean?>(true, false, null, null, true, false, null),
|
|
567
|
-
batteries.map { if (it.has("pmuCharging")) it.getBoolean("pmuCharging") else null },
|
|
568
|
-
)
|
|
569
|
-
// Display/charging semantics are unchanged: optInt truncation still drives isCharging.
|
|
570
|
-
assertEquals(
|
|
571
|
-
listOf(true, false, false, true, true, false, true),
|
|
572
|
-
batteryEvents().map { it["charging"] },
|
|
573
|
-
)
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
@Test
|
|
577
|
-
fun `sr_hrt without a measured pt issues no fresh battery id`() {
|
|
578
|
-
val gatt = connect(manager(), peerA)
|
|
579
|
-
notifyK900(gatt, """{"C":"sr_hrt","B":{"pt":64,"ready":1,"charg":1}}""")
|
|
580
|
-
for (pt in listOf(null, "\"x\"", "\"57\"", "57.5", "150", "-3")) {
|
|
581
|
-
val field = pt?.let { ",\"pt\":$it" } ?: ""
|
|
582
|
-
notifyK900(gatt, """{"C":"sr_hrt","B":{"ready":1,"charg":1$field}}""")
|
|
583
|
-
}
|
|
584
|
-
// Missing/non-numeric pt keeps the previous no-update behavior; numeric fallbacks are
|
|
585
|
-
// displayed as before, but none of them carries a fresh id or record.
|
|
586
|
-
val events = batteryEvents()
|
|
587
|
-
assertEquals(listOf(64, 57, 57, 150), events.map { it["level"] })
|
|
588
|
-
assertEquals(listOf(true, false, false, false), events.map { it.containsKey("eventId") })
|
|
589
|
-
assertEquals(listOf(64), kind("battery").map { it.getInt("percent") })
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
@Test
|
|
593
|
-
fun `events without native provenance carry no id`() {
|
|
594
|
-
// e.g. another glasses model or a synthesized status: nothing to adopt.
|
|
595
|
-
Bridge.sendBatteryStatus(80, false)
|
|
596
|
-
val event = batteryEvents().single()
|
|
597
|
-
assertFalse(event.containsKey("eventId"))
|
|
598
|
-
assertTrue(kind("bridge_dispatch").isEmpty())
|
|
599
|
-
assertNotNull(event["level"])
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
private fun live(gatt: BluetoothGatt): MentraLive = managers.last()
|
|
603
|
-
}
|
package/android/src/test/java/com/mentra/bluetoothsdk/utils/MessageChunkReassemblerOriginTest.kt
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
package com.mentra.bluetoothsdk.utils
|
|
2
|
-
|
|
3
|
-
import com.mentra.bluetoothsdk.debug.BleEvidenceLog
|
|
4
|
-
import org.junit.Assert.assertEquals
|
|
5
|
-
import org.junit.Assert.assertNull
|
|
6
|
-
import org.junit.Test
|
|
7
|
-
import org.junit.runner.RunWith
|
|
8
|
-
import org.robolectric.RobolectricTestRunner
|
|
9
|
-
import org.robolectric.annotation.Config
|
|
10
|
-
|
|
11
|
-
@RunWith(RobolectricTestRunner::class)
|
|
12
|
-
@Config(sdk = [33])
|
|
13
|
-
class MessageChunkReassemblerOriginTest {
|
|
14
|
-
private val first = BleEvidenceLog.Origin(connection = 3, wire = 3)
|
|
15
|
-
private val afterWireReset = BleEvidenceLog.Origin(connection = 3, wire = 9)
|
|
16
|
-
|
|
17
|
-
@Test
|
|
18
|
-
fun `chunks from one origin reassemble`() {
|
|
19
|
-
val reassembler = MessageChunkReassembler()
|
|
20
|
-
assertNull(reassembler.addChunk("c1", 0, 2, "{\"a\":", first))
|
|
21
|
-
assertEquals("{\"a\":1}", reassembler.addChunk("c1", 1, 2, "1}", first))
|
|
22
|
-
assertEquals(0L, reassembler.mixedOriginDrops)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
@Test
|
|
26
|
-
fun `a chunk from another origin discards the session instead of mixing`() {
|
|
27
|
-
val reassembler = MessageChunkReassembler()
|
|
28
|
-
assertNull(reassembler.addChunk("c1", 0, 2, "{\"a\":", first))
|
|
29
|
-
// Completing index 1 from a later wire epoch must not produce a message.
|
|
30
|
-
assertNull(reassembler.addChunk("c1", 1, 2, "1}", afterWireReset))
|
|
31
|
-
assertEquals(1L, reassembler.mixedOriginDrops)
|
|
32
|
-
// The discarded session's first chunk is gone; only fragments from the new origin combine.
|
|
33
|
-
assertEquals("{\"b\":1}", reassembler.addChunk("c1", 0, 2, "{\"b\":", afterWireReset))
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
@Test
|
|
37
|
-
fun `binary fragments from another origin are discarded`() {
|
|
38
|
-
val reassembler = MessageChunkReassembler()
|
|
39
|
-
assertNull(reassembler.addBinaryFragment(7, 0, 2, byteArrayOf(1), first))
|
|
40
|
-
assertNull(reassembler.addBinaryFragment(7, 1, 2, byteArrayOf(2), afterWireReset))
|
|
41
|
-
assertEquals(1L, reassembler.mixedOriginDrops)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
@Test
|
|
45
|
-
fun `callers without an origin keep the previous behavior`() {
|
|
46
|
-
val reassembler = MessageChunkReassembler()
|
|
47
|
-
assertNull(reassembler.addChunk("c1", 0, 2, "{\"a\":"))
|
|
48
|
-
assertEquals("{\"a\":1}", reassembler.addChunk("c1", 1, 2, "1}"))
|
|
49
|
-
assertEquals(0L, reassembler.mixedOriginDrops)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export type DiagnosticRenderSurface = "glasses_battery" | "wifi_scan";
|
|
2
|
-
export type ReportDiagnosticRender = (surface: DiagnosticRenderSurface, eventIds: string[], value?: number | null) => boolean;
|
|
3
|
-
/**
|
|
4
|
-
* Wraps the Android-only native render marker. An empty id list is forwarded because it is
|
|
5
|
-
* meaningful: it invalidates the surface's earlier marker. A displayed value the evidence
|
|
6
|
-
* cannot represent (not an integer, or outside the surface's range) is sent as a withdrawal
|
|
7
|
-
* (no ids, null value) rather than dropped or passed through, so an earlier marker never stays
|
|
8
|
-
* attributed to it. Returns false (and does nothing) on other platforms, on native builds
|
|
9
|
-
* without the function, or when the native call throws.
|
|
10
|
-
*/
|
|
11
|
-
export declare function createReportDiagnosticRender(native: Record<string, unknown>, platformOs: string): ReportDiagnosticRender;
|
|
12
|
-
//# sourceMappingURL=diagnosticRender.d.ts.map
|