@mentra/bluetooth-sdk 3.3.0-dev.421 → 3.3.0-dev.423
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,454 +0,0 @@
|
|
|
1
|
-
package com.mentra.bluetoothsdk.debug
|
|
2
|
-
|
|
3
|
-
import android.os.Handler
|
|
4
|
-
import android.os.Looper
|
|
5
|
-
import android.os.Process
|
|
6
|
-
import android.os.SystemClock
|
|
7
|
-
import java.nio.charset.StandardCharsets
|
|
8
|
-
import java.security.MessageDigest
|
|
9
|
-
import java.util.UUID
|
|
10
|
-
import java.util.concurrent.CountDownLatch
|
|
11
|
-
import java.util.concurrent.TimeUnit
|
|
12
|
-
import java.util.concurrent.atomic.AtomicReference
|
|
13
|
-
import org.json.JSONArray
|
|
14
|
-
import org.json.JSONObject
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Bounded provenance record for Mentra Live battery and Wi-Fi scan observations (schema v1).
|
|
18
|
-
*
|
|
19
|
-
* The record lets a device test prove that a value shown by the app came from a specific
|
|
20
|
-
* glasses notification on a specific, still-current BLE connection. It is not telemetry:
|
|
21
|
-
* it stores only lifecycle boundaries, battery percentages, scan request/send/chunk/terminal
|
|
22
|
-
* metadata and allowlisted render markers, in a fixed-size in-memory ring. Nothing is uploaded.
|
|
23
|
-
*
|
|
24
|
-
* Identities are derived from one random [streamId] per process plus a strictly increasing
|
|
25
|
-
* [Record.seq]:
|
|
26
|
-
* - a connection is the seq of its `connection_accepted` record (nonreused within the stream,
|
|
27
|
-
* and a manager recreated in the same process still gets a new seq);
|
|
28
|
-
* - a wire epoch is the seq of the latest `wire_reset` (or the connection record itself);
|
|
29
|
-
* - a BLE write generation is the seq of the `ble_generation` record naming the send-queue
|
|
30
|
-
* generation that each queued write captured when it was queued;
|
|
31
|
-
* - a logical event id is `"<streamId>:<seq>"` of its `battery`/`scan_chunk` record.
|
|
32
|
-
*
|
|
33
|
-
* SSIDs are never stored or serialized. A scan chunk keeps SHA-256 digests of up to
|
|
34
|
-
* [MAX_SCAN_SSIDS] of its SSIDs in memory so a snapshot can report only whether a
|
|
35
|
-
* caller-supplied target digest was present. A target that is not among the retained digests
|
|
36
|
-
* is reported as absent only when every network of the chunk was retained; otherwise unknown.
|
|
37
|
-
*
|
|
38
|
-
* Read access is [dump], reached through `dumpsys` on the non-exported foreground service
|
|
39
|
-
* (DUMP permission, shell/system only). It is read-only and runs on the owner looper so its
|
|
40
|
-
* fence is ordered after every callback that already ran there.
|
|
41
|
-
*/
|
|
42
|
-
object BleEvidenceLog {
|
|
43
|
-
const val SCHEMA_VERSION = 1
|
|
44
|
-
const val CAPACITY = 512
|
|
45
|
-
const val DUMP_COMMAND = "mentra-ble-evidence"
|
|
46
|
-
const val OUTPUT_PREFIX = "MENTRA_BLE_EVIDENCE "
|
|
47
|
-
const val SURFACE_BATTERY = "glasses_battery"
|
|
48
|
-
const val SURFACE_WIFI_SCAN = "wifi_scan"
|
|
49
|
-
private const val MAX_SCAN_SSIDS = 64
|
|
50
|
-
private const val MAX_RENDER_EVENT_IDS = 32
|
|
51
|
-
private const val DUMP_TIMEOUT_MS = 2_000L
|
|
52
|
-
private val SHA256_HEX = Regex("^[0-9a-f]{64}$")
|
|
53
|
-
|
|
54
|
-
/** Receive origin captured at the accepted notification, before decoding. */
|
|
55
|
-
data class Origin(val connection: Long, val wire: Long)
|
|
56
|
-
|
|
57
|
-
private class Record(
|
|
58
|
-
val seq: Long,
|
|
59
|
-
val elapsedRealtimeMs: Long,
|
|
60
|
-
val kind: String,
|
|
61
|
-
val fields: Map<String, Any?>,
|
|
62
|
-
/** SSID digest -> exact requiresPassword boolean (null when not a boolean). Never serialized. */
|
|
63
|
-
val ssidSecurity: Map<String, Boolean?>? = null,
|
|
64
|
-
/** False when some network of the chunk was not retained in [ssidSecurity]. */
|
|
65
|
-
val ssidCoverageComplete: Boolean = true,
|
|
66
|
-
/**
|
|
67
|
-
* False when some entry of the chunk could not be identified (unparsed or without a
|
|
68
|
-
* string SSID); such an entry could repeat a seen target with another security value.
|
|
69
|
-
*/
|
|
70
|
-
val ssidSecurityCertain: Boolean = true,
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
private val lock = Any()
|
|
74
|
-
private val records = ArrayDeque<Record>()
|
|
75
|
-
@Volatile var streamId: String = UUID.randomUUID().toString()
|
|
76
|
-
private set
|
|
77
|
-
private var lastSeq = 0L
|
|
78
|
-
private var droppedCount = 0L
|
|
79
|
-
private var currentConnection = 0L
|
|
80
|
-
private var currentPeerMac: String? = null
|
|
81
|
-
private var currentWire = 0L
|
|
82
|
-
private var currentWriteGeneration = 0L
|
|
83
|
-
internal var clock: () -> Long = { SystemClock.elapsedRealtime() }
|
|
84
|
-
|
|
85
|
-
/** Starts a new stream as a fresh process would. Tests only. */
|
|
86
|
-
internal fun resetForTest() {
|
|
87
|
-
synchronized(lock) {
|
|
88
|
-
records.clear()
|
|
89
|
-
streamId = UUID.randomUUID().toString()
|
|
90
|
-
lastSeq = 0L
|
|
91
|
-
droppedCount = 0L
|
|
92
|
-
currentConnection = 0L
|
|
93
|
-
currentPeerMac = null
|
|
94
|
-
currentWire = 0L
|
|
95
|
-
currentWriteGeneration = 0L
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
fun eventId(seq: Long): String = "$streamId:$seq"
|
|
100
|
-
|
|
101
|
-
/** Records an accepted connection for the actual callback peer and returns its origin. */
|
|
102
|
-
fun connectionAccepted(peerMac: String): Origin =
|
|
103
|
-
synchronized(lock) {
|
|
104
|
-
val seq = append("connection_accepted", mapOf("peerMac" to peerMac))
|
|
105
|
-
currentConnection = seq
|
|
106
|
-
currentPeerMac = peerMac
|
|
107
|
-
currentWire = seq
|
|
108
|
-
Origin(seq, seq)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
fun connectionClosed(connection: Long, reason: String) {
|
|
112
|
-
if (connection == 0L) return
|
|
113
|
-
synchronized(lock) {
|
|
114
|
-
append("connection_closed", mapOf("connection" to connection, "reason" to reason))
|
|
115
|
-
if (currentConnection == connection) {
|
|
116
|
-
currentConnection = 0L
|
|
117
|
-
currentPeerMac = null
|
|
118
|
-
currentWire = 0L
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Wire negotiation reset without a BLE disconnect (e.g. glasses_ready). Returns the new wire epoch. */
|
|
124
|
-
fun wireReset(connection: Long): Long =
|
|
125
|
-
synchronized(lock) {
|
|
126
|
-
val seq = append("wire_reset", mapOf("connection" to connection))
|
|
127
|
-
if (connection == currentConnection) currentWire = seq
|
|
128
|
-
seq
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* The BLE send queue moved to [generation] (manager creation or disconnect reset). Writes
|
|
133
|
-
* queued under an older generation are never transmitted. Returns the boundary seq that
|
|
134
|
-
* queued writes of this generation reference as `writeGeneration`.
|
|
135
|
-
*/
|
|
136
|
-
fun bleGeneration(connection: Long, generation: Long): Long =
|
|
137
|
-
synchronized(lock) {
|
|
138
|
-
val seq =
|
|
139
|
-
append(
|
|
140
|
-
"ble_generation",
|
|
141
|
-
mapOf("connection" to (if (connection == 0L) null else connection), "generation" to generation),
|
|
142
|
-
)
|
|
143
|
-
currentWriteGeneration = seq
|
|
144
|
-
seq
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
fun receiveRejected(reason: String, origin: Origin?) {
|
|
148
|
-
synchronized(lock) {
|
|
149
|
-
append("receive_rejected", mapOf("reason" to reason, "origin" to originJson(origin)))
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Records a battery value decoded from one notification. [pmuCharging] is only for the
|
|
155
|
-
* PMU `charg` bit of `sr_hrt`; percent-only sources pass null.
|
|
156
|
-
*/
|
|
157
|
-
fun battery(origin: Origin?, percent: Int, source: String, pmuCharging: Boolean?): String =
|
|
158
|
-
synchronized(lock) {
|
|
159
|
-
val fields = linkedMapOf<String, Any?>(
|
|
160
|
-
"origin" to originJson(origin),
|
|
161
|
-
"percent" to percent,
|
|
162
|
-
"source" to source,
|
|
163
|
-
)
|
|
164
|
-
if (pmuCharging != null) fields["pmuCharging"] = pmuCharging
|
|
165
|
-
eventId(append("battery", fields))
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
fun scanRequest(scanId: String, fresh: Boolean) {
|
|
169
|
-
synchronized(lock) {
|
|
170
|
-
append("scan_request", mapOf("scanId" to scanId, "mode" to if (fresh) "fresh" else "joined"))
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* One native write of a scan command. Outcomes: queued, not_queued, gatt_accepted,
|
|
176
|
-
* gatt_refused, write_ok, write_failed, stale_dropped. [connection] and [writeGeneration]
|
|
177
|
-
* are the values captured when the write was queued, never re-read later.
|
|
178
|
-
*/
|
|
179
|
-
fun scanSend(
|
|
180
|
-
scanId: String,
|
|
181
|
-
outcome: String,
|
|
182
|
-
fragment: Int,
|
|
183
|
-
fragments: Int,
|
|
184
|
-
connection: Long,
|
|
185
|
-
writeGeneration: Long?,
|
|
186
|
-
) {
|
|
187
|
-
synchronized(lock) {
|
|
188
|
-
append(
|
|
189
|
-
"scan_send",
|
|
190
|
-
mapOf(
|
|
191
|
-
"scanId" to scanId,
|
|
192
|
-
"outcome" to outcome,
|
|
193
|
-
"fragment" to fragment,
|
|
194
|
-
"fragments" to fragments,
|
|
195
|
-
"connection" to (if (connection == 0L) null else connection),
|
|
196
|
-
"writeGeneration" to writeGeneration,
|
|
197
|
-
),
|
|
198
|
-
)
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* [networks] are the entries the decoder parsed. [entriesParsed] is false when the raw chunk
|
|
204
|
-
* had entries (or a network list) that could not be parsed; they were not delivered either,
|
|
205
|
-
* so a target among them is unknown rather than absent.
|
|
206
|
-
*/
|
|
207
|
-
fun scanChunk(
|
|
208
|
-
origin: Origin?,
|
|
209
|
-
scanId: String?,
|
|
210
|
-
networks: List<Map<String, Any>>,
|
|
211
|
-
complete: Boolean,
|
|
212
|
-
entriesParsed: Boolean,
|
|
213
|
-
): String {
|
|
214
|
-
val security = LinkedHashMap<String, Boolean?>()
|
|
215
|
-
var identitiesKnown = entriesParsed
|
|
216
|
-
var coverageComplete = entriesParsed
|
|
217
|
-
for (network in networks) {
|
|
218
|
-
// An entry without a string SSID could be anything: coverage is no longer complete.
|
|
219
|
-
val ssid = network["ssid"] as? String
|
|
220
|
-
if (ssid == null) {
|
|
221
|
-
identitiesKnown = false
|
|
222
|
-
coverageComplete = false
|
|
223
|
-
continue
|
|
224
|
-
}
|
|
225
|
-
val digest = sha256Hex(ssid)
|
|
226
|
-
val requiresPassword = network["requiresPassword"] as? Boolean
|
|
227
|
-
if (security.containsKey(digest)) {
|
|
228
|
-
// Conflicting duplicates make the security value unknown.
|
|
229
|
-
if (security[digest] != requiresPassword) security[digest] = null
|
|
230
|
-
continue
|
|
231
|
-
}
|
|
232
|
-
if (security.size >= MAX_SCAN_SSIDS) {
|
|
233
|
-
coverageComplete = false
|
|
234
|
-
continue
|
|
235
|
-
}
|
|
236
|
-
security[digest] = requiresPassword
|
|
237
|
-
}
|
|
238
|
-
return synchronized(lock) {
|
|
239
|
-
val seq =
|
|
240
|
-
append(
|
|
241
|
-
"scan_chunk",
|
|
242
|
-
mapOf(
|
|
243
|
-
"origin" to originJson(origin),
|
|
244
|
-
"scanId" to scanId,
|
|
245
|
-
"networks" to networks.size,
|
|
246
|
-
"entriesParsed" to entriesParsed,
|
|
247
|
-
"complete" to complete,
|
|
248
|
-
),
|
|
249
|
-
security,
|
|
250
|
-
coverageComplete,
|
|
251
|
-
identitiesKnown,
|
|
252
|
-
)
|
|
253
|
-
eventId(seq)
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/** Result: complete, timeout_partial, timeout_empty, error, legacy_uncorrelated. */
|
|
258
|
-
fun scanTerminal(scanId: String?, result: String, networks: Int) {
|
|
259
|
-
synchronized(lock) {
|
|
260
|
-
append("scan_terminal", mapOf("scanId" to scanId, "result" to result, "networks" to networks))
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
fun bridgeDispatch(eventId: String, type: String, sinks: Int) {
|
|
265
|
-
synchronized(lock) {
|
|
266
|
-
append("bridge_dispatch", mapOf("eventId" to eventId, "type" to type, "sinks" to sinks))
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Records that app UI committed state derived from [eventIds]. Only allowlisted surfaces and
|
|
272
|
-
* bounded integer values are accepted; ids from another stream are counted, not stored.
|
|
273
|
-
* An out-of-range value is not accepted: it is recorded as a withdrawal of the surface (no
|
|
274
|
-
* ids, null value, the value itself not stored) and false is returned, so the surface's
|
|
275
|
-
* earlier marker never stays current for a value it does not describe.
|
|
276
|
-
*/
|
|
277
|
-
fun uiRender(surface: String, eventIds: List<String>, value: Int?): Boolean {
|
|
278
|
-
val maxValue =
|
|
279
|
-
when (surface) {
|
|
280
|
-
SURFACE_BATTERY -> 100
|
|
281
|
-
SURFACE_WIFI_SCAN -> 500
|
|
282
|
-
else -> return false
|
|
283
|
-
}
|
|
284
|
-
if (value != null && (value < 0 || value > maxValue)) {
|
|
285
|
-
synchronized(lock) {
|
|
286
|
-
append(
|
|
287
|
-
"ui_render",
|
|
288
|
-
mapOf(
|
|
289
|
-
"surface" to surface,
|
|
290
|
-
"eventIds" to JSONArray(),
|
|
291
|
-
"rejectedEventIds" to eventIds.size,
|
|
292
|
-
"truncated" to false,
|
|
293
|
-
"value" to null,
|
|
294
|
-
),
|
|
295
|
-
)
|
|
296
|
-
}
|
|
297
|
-
return false
|
|
298
|
-
}
|
|
299
|
-
return synchronized(lock) {
|
|
300
|
-
val prefix = "$streamId:"
|
|
301
|
-
val accepted = eventIds.filter { id ->
|
|
302
|
-
id.startsWith(prefix) &&
|
|
303
|
-
id.substring(prefix.length).toLongOrNull()?.let { it in 1..lastSeq } == true
|
|
304
|
-
}.distinct()
|
|
305
|
-
append(
|
|
306
|
-
"ui_render",
|
|
307
|
-
mapOf(
|
|
308
|
-
"surface" to surface,
|
|
309
|
-
"eventIds" to JSONArray(accepted.take(MAX_RENDER_EVENT_IDS)),
|
|
310
|
-
"rejectedEventIds" to (eventIds.size - accepted.size),
|
|
311
|
-
"truncated" to (accepted.size > MAX_RENDER_EVENT_IDS),
|
|
312
|
-
"value" to value,
|
|
313
|
-
),
|
|
314
|
-
)
|
|
315
|
-
true
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Returns every retained record after [afterSeq]. `outcome` is `ok` only when the cursor
|
|
321
|
-
* belongs to this stream and no record after it was evicted.
|
|
322
|
-
*/
|
|
323
|
-
fun snapshot(afterSeq: Long?, targetSha256: String?): JSONObject =
|
|
324
|
-
synchronized(lock) {
|
|
325
|
-
val firstRetained = records.firstOrNull()?.seq ?: (lastSeq + 1)
|
|
326
|
-
val target = targetSha256?.lowercase()
|
|
327
|
-
val outcome =
|
|
328
|
-
when {
|
|
329
|
-
afterSeq == null || afterSeq < 0 -> "invalid_request"
|
|
330
|
-
target != null && !SHA256_HEX.matches(target) -> "invalid_request"
|
|
331
|
-
afterSeq > lastSeq -> "stale_cursor"
|
|
332
|
-
afterSeq < firstRetained - 1 -> "gap"
|
|
333
|
-
else -> "ok"
|
|
334
|
-
}
|
|
335
|
-
val out = JSONObject()
|
|
336
|
-
out.put("schemaVersion", SCHEMA_VERSION)
|
|
337
|
-
out.put("streamId", streamId)
|
|
338
|
-
out.put("pid", Process.myPid())
|
|
339
|
-
out.put("outcome", outcome)
|
|
340
|
-
out.put("afterSeq", afterSeq ?: JSONObject.NULL)
|
|
341
|
-
out.put("fenceSeq", lastSeq)
|
|
342
|
-
out.put("firstRetainedSeq", firstRetained)
|
|
343
|
-
out.put("droppedCount", droppedCount)
|
|
344
|
-
out.put("nowElapsedRealtimeMs", clock())
|
|
345
|
-
out.put("targetFiltered", target != null && outcome != "invalid_request")
|
|
346
|
-
out.put(
|
|
347
|
-
"current",
|
|
348
|
-
JSONObject().apply {
|
|
349
|
-
put("connection", if (currentConnection == 0L) JSONObject.NULL else currentConnection)
|
|
350
|
-
put("peerMac", currentPeerMac ?: JSONObject.NULL)
|
|
351
|
-
put("wire", if (currentWire == 0L) JSONObject.NULL else currentWire)
|
|
352
|
-
put("writeGeneration", if (currentWriteGeneration == 0L) JSONObject.NULL else currentWriteGeneration)
|
|
353
|
-
},
|
|
354
|
-
)
|
|
355
|
-
val list = JSONArray()
|
|
356
|
-
if (outcome != "invalid_request" && outcome != "stale_cursor") {
|
|
357
|
-
for (record in records) {
|
|
358
|
-
if (record.seq > afterSeq!!) list.put(recordJson(record, target))
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
out.put("records", list)
|
|
362
|
-
out
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Handles `dumpsys ... mentra-ble-evidence afterSeq=<n> [targetSha256=<hex>]`. Returns null
|
|
367
|
-
* for any other dump request. The snapshot runs on [owner] so it is ordered after work that
|
|
368
|
-
* already ran there; if the owner does not answer in time the outcome is `unavailable`.
|
|
369
|
-
*/
|
|
370
|
-
fun dump(args: Array<out String>?, owner: Handler, timeoutMs: Long = DUMP_TIMEOUT_MS): String? {
|
|
371
|
-
if (args.isNullOrEmpty() || args[0] != DUMP_COMMAND) return null
|
|
372
|
-
var afterSeq: Long? = null
|
|
373
|
-
var target: String? = null
|
|
374
|
-
var valid = true
|
|
375
|
-
for (arg in args.drop(1)) {
|
|
376
|
-
when {
|
|
377
|
-
arg.startsWith("afterSeq=") -> afterSeq = arg.removePrefix("afterSeq=").toLongOrNull()
|
|
378
|
-
arg.startsWith("targetSha256=") -> target = arg.removePrefix("targetSha256=")
|
|
379
|
-
else -> valid = false
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
if (!valid) afterSeq = null
|
|
383
|
-
val result = AtomicReference<JSONObject?>()
|
|
384
|
-
if (Looper.myLooper() == owner.looper) {
|
|
385
|
-
result.set(snapshot(afterSeq, target))
|
|
386
|
-
} else {
|
|
387
|
-
val done = CountDownLatch(1)
|
|
388
|
-
val posted =
|
|
389
|
-
owner.post {
|
|
390
|
-
result.set(snapshot(afterSeq, target))
|
|
391
|
-
done.countDown()
|
|
392
|
-
}
|
|
393
|
-
if (posted) done.await(timeoutMs, TimeUnit.MILLISECONDS)
|
|
394
|
-
}
|
|
395
|
-
val snapshot =
|
|
396
|
-
result.get()
|
|
397
|
-
?: JSONObject().apply {
|
|
398
|
-
put("schemaVersion", SCHEMA_VERSION)
|
|
399
|
-
put("streamId", streamId)
|
|
400
|
-
put("pid", Process.myPid())
|
|
401
|
-
put("outcome", "unavailable")
|
|
402
|
-
}
|
|
403
|
-
return OUTPUT_PREFIX + snapshot
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
fun sha256Hex(value: String): String =
|
|
407
|
-
MessageDigest.getInstance("SHA-256")
|
|
408
|
-
.digest(value.toByteArray(StandardCharsets.UTF_8))
|
|
409
|
-
.joinToString("") { "%02x".format(it) }
|
|
410
|
-
|
|
411
|
-
private fun append(
|
|
412
|
-
kind: String,
|
|
413
|
-
fields: Map<String, Any?>,
|
|
414
|
-
ssidSecurity: Map<String, Boolean?>? = null,
|
|
415
|
-
ssidCoverageComplete: Boolean = true,
|
|
416
|
-
ssidSecurityCertain: Boolean = true,
|
|
417
|
-
): Long {
|
|
418
|
-
val seq = ++lastSeq
|
|
419
|
-
records.addLast(Record(seq, clock(), kind, fields, ssidSecurity, ssidCoverageComplete, ssidSecurityCertain))
|
|
420
|
-
while (records.size > CAPACITY) {
|
|
421
|
-
records.removeFirst()
|
|
422
|
-
droppedCount++
|
|
423
|
-
}
|
|
424
|
-
return seq
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
private fun originJson(origin: Origin?): Any =
|
|
428
|
-
origin?.let { JSONObject().put("connection", it.connection).put("wire", it.wire) } ?: JSONObject.NULL
|
|
429
|
-
|
|
430
|
-
private fun recordJson(record: Record, target: String?): JSONObject {
|
|
431
|
-
val json = JSONObject()
|
|
432
|
-
json.put("seq", record.seq)
|
|
433
|
-
json.put("elapsedRealtimeMs", record.elapsedRealtimeMs)
|
|
434
|
-
json.put("kind", record.kind)
|
|
435
|
-
for ((key, value) in record.fields) json.put(key, value ?: JSONObject.NULL)
|
|
436
|
-
val security = record.ssidSecurity
|
|
437
|
-
if (security != null && target != null) {
|
|
438
|
-
val seen = security.containsKey(target)
|
|
439
|
-
json.put("targetCoverage", if (record.ssidCoverageComplete) "complete" else "incomplete")
|
|
440
|
-
when {
|
|
441
|
-
seen -> {
|
|
442
|
-
json.put("targetSeen", true)
|
|
443
|
-
// Presence is certain; its security is only certain when no entry was unidentified.
|
|
444
|
-
val requiresPassword = if (record.ssidSecurityCertain) security[target] else null
|
|
445
|
-
json.put("targetRequiresPassword", requiresPassword ?: JSONObject.NULL)
|
|
446
|
-
}
|
|
447
|
-
// Absence is only known when every network of the chunk was retained.
|
|
448
|
-
record.ssidCoverageComplete -> json.put("targetSeen", false)
|
|
449
|
-
else -> json.put("targetSeen", JSONObject.NULL)
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
return json
|
|
453
|
-
}
|
|
454
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
package com.mentra.bluetoothsdk
|
|
2
|
-
|
|
3
|
-
import androidx.test.core.app.ApplicationProvider
|
|
4
|
-
import com.mentra.bluetoothsdk.debug.BleEvidenceLog
|
|
5
|
-
import com.mentra.bluetoothsdk.sgcs.G1PairingTest
|
|
6
|
-
import kotlinx.coroutines.CoroutineStart
|
|
7
|
-
import kotlinx.coroutines.async
|
|
8
|
-
import kotlinx.coroutines.runBlocking
|
|
9
|
-
import kotlinx.coroutines.yield
|
|
10
|
-
import org.json.JSONObject
|
|
11
|
-
import org.junit.After
|
|
12
|
-
import org.junit.Assert.assertEquals
|
|
13
|
-
import org.junit.Assert.assertFalse
|
|
14
|
-
import org.junit.Assert.assertTrue
|
|
15
|
-
import org.junit.Before
|
|
16
|
-
import org.junit.Test
|
|
17
|
-
import org.junit.runner.RunWith
|
|
18
|
-
import org.robolectric.RobolectricTestRunner
|
|
19
|
-
import org.robolectric.annotation.Config
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Runs the production MentraBluetoothSdk.requestWifiScan and its Bridge chunk handling. Chunks
|
|
23
|
-
* enter through Bridge.updateWifiScanResults, the same call MentraLive makes after decoding.
|
|
24
|
-
*/
|
|
25
|
-
@RunWith(RobolectricTestRunner::class)
|
|
26
|
-
@Config(sdk = [33], shadows = [G1PairingTest.ShadowLc3Cpp::class], instrumentedPackages = ["com.mentra.lc3Lib"])
|
|
27
|
-
class WifiScanEvidenceTest {
|
|
28
|
-
private lateinit var sdk: MentraBluetoothSdk
|
|
29
|
-
|
|
30
|
-
@Before fun setup() {
|
|
31
|
-
BleEvidenceLog.resetForTest()
|
|
32
|
-
sdk = MentraBluetoothSdk.create(ApplicationProvider.getApplicationContext(), object : MentraBluetoothSdkListener {})
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
@After fun close() = sdk.close()
|
|
36
|
-
|
|
37
|
-
private fun records(kind: String): List<JSONObject> {
|
|
38
|
-
val array = BleEvidenceLog.snapshot(0, null).getJSONArray("records")
|
|
39
|
-
return (0 until array.length()).map { array.getJSONObject(it) }.filter { it.getString("kind") == kind }
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
private fun freshScanId() = records("scan_request").single { it.getString("mode") == "fresh" }.getString("scanId")
|
|
43
|
-
|
|
44
|
-
private fun chunk(scanId: String?, ssid: String, complete: Boolean) =
|
|
45
|
-
Bridge.updateWifiScanResults(
|
|
46
|
-
listOf(mapOf("ssid" to ssid, "requiresPassword" to true, "signalStrength" to -40)),
|
|
47
|
-
complete,
|
|
48
|
-
scanId,
|
|
49
|
-
BleEvidenceLog.scanChunk(null, scanId, emptyList(), complete, entriesParsed = true),
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
@Test
|
|
53
|
-
fun `explicit completion of the fresh request is the only complete terminal`() = runBlocking {
|
|
54
|
-
val scan = async(start = CoroutineStart.UNDISPATCHED) { sdk.requestWifiScan() }
|
|
55
|
-
val scanId = freshScanId()
|
|
56
|
-
chunk(scanId, "LabAP", complete = false)
|
|
57
|
-
yield()
|
|
58
|
-
assertFalse(scan.isCompleted)
|
|
59
|
-
assertTrue("streamed chunks are not completion", records("scan_terminal").isEmpty())
|
|
60
|
-
|
|
61
|
-
chunk(scanId, "Other", complete = true)
|
|
62
|
-
assertEquals(listOf("LabAP", "Other"), scan.await().map { it.ssid })
|
|
63
|
-
val terminal = records("scan_terminal").single()
|
|
64
|
-
assertEquals(scanId, terminal.getString("scanId"))
|
|
65
|
-
assertEquals("complete", terminal.getString("result"))
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
@Test
|
|
69
|
-
fun `a joined caller is recorded as joining the older request, not a fresh scan`() = runBlocking {
|
|
70
|
-
val first = async(start = CoroutineStart.UNDISPATCHED) { sdk.requestWifiScan() }
|
|
71
|
-
val joined = async(start = CoroutineStart.UNDISPATCHED) { sdk.requestWifiScan() }
|
|
72
|
-
val scanId = freshScanId()
|
|
73
|
-
val requests = records("scan_request")
|
|
74
|
-
assertEquals(listOf("fresh", "joined"), requests.map { it.getString("mode") })
|
|
75
|
-
assertEquals(listOf(scanId, scanId), requests.map { it.getString("scanId") })
|
|
76
|
-
|
|
77
|
-
chunk(scanId, "LabAP", complete = true)
|
|
78
|
-
assertEquals(first.await(), joined.await())
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
@Test
|
|
82
|
-
fun `a chunk for another scan id neither resolves nor completes the fresh request`() = runBlocking {
|
|
83
|
-
val scan = async(start = CoroutineStart.UNDISPATCHED) { sdk.requestWifiScan() }
|
|
84
|
-
val scanId = freshScanId()
|
|
85
|
-
chunk("scan-older", "Stale", complete = true)
|
|
86
|
-
yield()
|
|
87
|
-
assertFalse(scan.isCompleted)
|
|
88
|
-
assertTrue(records("scan_terminal").isEmpty())
|
|
89
|
-
|
|
90
|
-
chunk(scanId, "LabAP", complete = true)
|
|
91
|
-
assertEquals(listOf("LabAP"), scan.await().map { it.ssid })
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
@Test
|
|
95
|
-
fun `legacy uncorrelated results are never a correlated completion`() = runBlocking {
|
|
96
|
-
val scan = async(start = CoroutineStart.UNDISPATCHED) { sdk.requestWifiScan() }
|
|
97
|
-
val scanId = freshScanId()
|
|
98
|
-
chunk(null, "Legacy", complete = true)
|
|
99
|
-
assertEquals(listOf("Legacy"), scan.await().map { it.ssid })
|
|
100
|
-
val terminal = records("scan_terminal").single()
|
|
101
|
-
assertEquals(scanId, terminal.getString("scanId"))
|
|
102
|
-
assertEquals("legacy_uncorrelated", terminal.getString("result"))
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
@Test
|
|
106
|
-
fun `a timeout that returns partial results is recorded as partial, not complete`() = runBlocking {
|
|
107
|
-
val scan = async(start = CoroutineStart.UNDISPATCHED) { sdk.requestWifiScan() }
|
|
108
|
-
val scanId = freshScanId()
|
|
109
|
-
chunk(scanId, "LabAP", complete = false)
|
|
110
|
-
// Customer behavior is unchanged: the promise still resolves with the partial list.
|
|
111
|
-
assertEquals(listOf("LabAP"), scan.await().map { it.ssid })
|
|
112
|
-
val terminal = records("scan_terminal").single()
|
|
113
|
-
assertEquals(scanId, terminal.getString("scanId"))
|
|
114
|
-
assertEquals("timeout_partial", terminal.getString("result"))
|
|
115
|
-
assertEquals(1, terminal.getInt("networks"))
|
|
116
|
-
}
|
|
117
|
-
}
|