@bota.dev/react-native-app-sdk 2.0.0-beta.1 → 2.0.0-beta.10
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 +104 -4
- package/android/build.gradle +1 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDevices.kt +22 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDiagnostics.kt +53 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogs.kt +16 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploads.kt +144 -14
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordings.kt +150 -29
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKEncryptedUploadV2Materials.kt +27 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKModule.kt +86 -3
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDevicesTest.kt +35 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogsTest.kt +33 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploadsTest.kt +135 -6
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingsTest.kt +236 -10
- package/generated/codegen-contract.json +568 -8
- package/ios/BotaDeviceSDK.mm +114 -0
- package/ios/BotaDeviceSDKAppleBridge.swift +141 -4
- package/ios/BotaDeviceSDKAppleDevices.swift +11 -0
- package/ios/BotaDeviceSDKAppleDiagnostics.swift +40 -0
- package/ios/BotaDeviceSDKAppleLogs.swift +18 -0
- package/ios/BotaDeviceSDKAppleRecordingUploads.swift +138 -6
- package/ios/BotaDeviceSDKAppleRecordings.swift +113 -5
- package/ios/BotaDeviceSDKEncryptedUploadV2Materials.swift +40 -0
- package/lib/commonjs/BotaClient.js +16 -2
- package/lib/commonjs/BotaClient.js.map +1 -1
- package/lib/commonjs/ble/constants.js +5 -1
- package/lib/commonjs/ble/constants.js.map +1 -1
- package/lib/commonjs/ble/deviceDiagnostics.js +240 -0
- package/lib/commonjs/ble/deviceDiagnostics.js.map +1 -0
- package/lib/commonjs/client.js +183 -28
- package/lib/commonjs/client.js.map +1 -1
- package/lib/commonjs/clientPresence.js +26 -0
- package/lib/commonjs/clientPresence.js.map +1 -0
- package/lib/commonjs/index.js +29 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/managers/DeviceManager.js +23 -0
- package/lib/commonjs/managers/DeviceManager.js.map +1 -1
- package/lib/commonjs/managers/NativeUploadQueue.js +386 -0
- package/lib/commonjs/managers/NativeUploadQueue.js.map +1 -0
- package/lib/commonjs/managers/RecordingManager.js +83 -102
- package/lib/commonjs/managers/RecordingManager.js.map +1 -1
- package/lib/commonjs/managers/uploadRecoveryMetadata.js +83 -0
- package/lib/commonjs/managers/uploadRecoveryMetadata.js.map +1 -0
- package/lib/commonjs/sdkIdentity.js +10 -0
- package/lib/commonjs/sdkIdentity.js.map +1 -0
- package/lib/commonjs/specs/NativeBotaDeviceSDK.js.map +1 -1
- package/lib/commonjs/uploadRecovery.js +68 -0
- package/lib/commonjs/uploadRecovery.js.map +1 -0
- package/lib/module/BotaClient.js +16 -2
- package/lib/module/BotaClient.js.map +1 -1
- package/lib/module/ble/constants.js +4 -0
- package/lib/module/ble/constants.js.map +1 -1
- package/lib/module/ble/deviceDiagnostics.js +234 -0
- package/lib/module/ble/deviceDiagnostics.js.map +1 -0
- package/lib/module/client.js +183 -28
- package/lib/module/client.js.map +1 -1
- package/lib/module/clientPresence.js +22 -0
- package/lib/module/clientPresence.js.map +1 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/managers/DeviceManager.js +23 -0
- package/lib/module/managers/DeviceManager.js.map +1 -1
- package/lib/module/managers/NativeUploadQueue.js +380 -0
- package/lib/module/managers/NativeUploadQueue.js.map +1 -0
- package/lib/module/managers/RecordingManager.js +83 -102
- package/lib/module/managers/RecordingManager.js.map +1 -1
- package/lib/module/managers/uploadRecoveryMetadata.js +77 -0
- package/lib/module/managers/uploadRecoveryMetadata.js.map +1 -0
- package/lib/module/sdkIdentity.js +6 -0
- package/lib/module/sdkIdentity.js.map +1 -0
- package/lib/module/specs/NativeBotaDeviceSDK.js.map +1 -1
- package/lib/module/uploadRecovery.js +64 -0
- package/lib/module/uploadRecovery.js.map +1 -0
- package/lib/typescript/BotaClient.d.ts +2 -0
- package/lib/typescript/BotaClient.d.ts.map +1 -1
- package/lib/typescript/ble/constants.d.ts +4 -0
- package/lib/typescript/ble/constants.d.ts.map +1 -1
- package/lib/typescript/ble/deviceDiagnostics.d.ts +10 -0
- package/lib/typescript/ble/deviceDiagnostics.d.ts.map +1 -0
- package/lib/typescript/client.d.ts +24 -3
- package/lib/typescript/client.d.ts.map +1 -1
- package/lib/typescript/clientPresence.d.ts +26 -0
- package/lib/typescript/clientPresence.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +10 -4
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/managers/DeviceManager.d.ts +5 -1
- package/lib/typescript/managers/DeviceManager.d.ts.map +1 -1
- package/lib/typescript/managers/NativeUploadQueue.d.ts +41 -0
- package/lib/typescript/managers/NativeUploadQueue.d.ts.map +1 -0
- package/lib/typescript/managers/RecordingManager.d.ts +3 -8
- package/lib/typescript/managers/RecordingManager.d.ts.map +1 -1
- package/lib/typescript/managers/types.d.ts +6 -1
- package/lib/typescript/managers/types.d.ts.map +1 -1
- package/lib/typescript/managers/uploadRecoveryMetadata.d.ts +6 -0
- package/lib/typescript/managers/uploadRecoveryMetadata.d.ts.map +1 -0
- package/lib/typescript/models/Device.d.ts +42 -0
- package/lib/typescript/models/Device.d.ts.map +1 -1
- package/lib/typescript/models/Recording.d.ts +47 -0
- package/lib/typescript/models/Recording.d.ts.map +1 -1
- package/lib/typescript/models/Status.d.ts +5 -1
- package/lib/typescript/models/Status.d.ts.map +1 -1
- package/lib/typescript/sdkIdentity.d.ts +3 -0
- package/lib/typescript/sdkIdentity.d.ts.map +1 -0
- package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts +70 -0
- package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts.map +1 -1
- package/lib/typescript/uploadRecovery.d.ts +27 -0
- package/lib/typescript/uploadRecovery.d.ts.map +1 -0
- package/package.json +1 -1
- package/scripts/bota_device_sdk_spm_workaround.rb +26 -15
- package/src/BotaClient.ts +17 -3
- package/src/ble/constants.ts +5 -0
- package/src/ble/deviceDiagnostics.ts +238 -0
- package/src/client.ts +189 -18
- package/src/clientPresence.ts +33 -0
- package/src/index.ts +18 -0
- package/src/managers/DeviceManager.ts +23 -0
- package/src/managers/NativeUploadQueue.ts +330 -0
- package/src/managers/RecordingManager.ts +83 -116
- package/src/managers/types.ts +7 -1
- package/src/managers/uploadRecoveryMetadata.ts +59 -0
- package/src/models/Device.ts +43 -0
- package/src/models/Recording.ts +52 -0
- package/src/models/Status.ts +5 -1
- package/src/sdkIdentity.ts +3 -0
- package/src/specs/NativeBotaDeviceSDK.ts +66 -0
- package/src/uploadRecovery.ts +66 -0
|
@@ -11,6 +11,7 @@ import dev.bota.sdk.EncryptedUploadV2Material
|
|
|
11
11
|
import dev.bota.sdk.EncryptedUploadV2ProfileProvider
|
|
12
12
|
import dev.bota.sdk.EncryptedUploadV2ProviderContext
|
|
13
13
|
import dev.bota.sdk.EncryptedUploadV2Recording
|
|
14
|
+
import dev.bota.sdk.PendingRecording
|
|
14
15
|
import dev.bota.sdk.RecordingSyncEvent
|
|
15
16
|
import dev.bota.sdk.RecordingTransferMetadata
|
|
16
17
|
import dev.bota.sdk.UploadOwnershipEvent
|
|
@@ -28,11 +29,19 @@ import dev.bota.sdk.model.StreamingUploadMethod
|
|
|
28
29
|
import java.util.UUID
|
|
29
30
|
import java.util.concurrent.ConcurrentHashMap
|
|
30
31
|
import kotlinx.coroutines.CompletableDeferred
|
|
32
|
+
import kotlinx.coroutines.CancellationException
|
|
33
|
+
import kotlinx.coroutines.Job
|
|
34
|
+
import kotlinx.coroutines.NonCancellable
|
|
35
|
+
import kotlinx.coroutines.currentCoroutineContext
|
|
36
|
+
import kotlinx.coroutines.ensureActive
|
|
37
|
+
import kotlinx.coroutines.job
|
|
38
|
+
import kotlinx.coroutines.withContext
|
|
31
39
|
import kotlinx.coroutines.flow.Flow
|
|
32
40
|
import kotlinx.coroutines.flow.collect
|
|
33
41
|
|
|
34
42
|
internal interface BotaDeviceSDKAndroidRecordingClient {
|
|
35
43
|
suspend fun listRecordings(device: ConnectedDevice): List<DeviceRecording>
|
|
44
|
+
suspend fun listPendingRecordings(device: ConnectedDevice): List<PendingRecording>
|
|
36
45
|
|
|
37
46
|
fun syncRecording(
|
|
38
47
|
device: ConnectedDevice,
|
|
@@ -45,9 +54,12 @@ internal interface BotaDeviceSDKAndroidRecordingClient {
|
|
|
45
54
|
suspend fun syncEncryptedRecordingV2(
|
|
46
55
|
device: ConnectedDevice,
|
|
47
56
|
recording: EncryptedUploadV2Recording,
|
|
57
|
+
operationId: UUID,
|
|
48
58
|
provider: EncryptedUploadV2ProfileProvider,
|
|
49
59
|
)
|
|
50
60
|
|
|
61
|
+
suspend fun cancelEncryptedRecordingV2(operationId: UUID)
|
|
62
|
+
|
|
51
63
|
suspend fun confirmRecording(device: ConnectedDevice, recordingUuid: String)
|
|
52
64
|
|
|
53
65
|
fun observeUploadOwnership(
|
|
@@ -74,6 +86,7 @@ internal fun BotaDeviceSDKAndroidRecordings.BotaRecordingFile.toWritableMap(): W
|
|
|
74
86
|
Arguments.createMap().apply {
|
|
75
87
|
putString("localPath", localPath)
|
|
76
88
|
putBoolean("e2eEncrypted", isE2EEncrypted)
|
|
89
|
+
putDouble("fileSizeBytes", fileSizeBytes.toDouble())
|
|
77
90
|
contentSha256Hex?.let { putString("contentSha256", it) }
|
|
78
91
|
}
|
|
79
92
|
|
|
@@ -89,6 +102,10 @@ internal data class BotaEncryptedUploadV2Recording(
|
|
|
89
102
|
val generation: UInt,
|
|
90
103
|
val ciphertextLength: String,
|
|
91
104
|
val ciphertextSha256: String,
|
|
105
|
+
val startedAtMs: String,
|
|
106
|
+
val durationMs: String,
|
|
107
|
+
val plaintextLength: String,
|
|
108
|
+
val storageFormat: Int,
|
|
92
109
|
)
|
|
93
110
|
|
|
94
111
|
internal data class BotaEncryptedUploadV2Capability(
|
|
@@ -199,11 +216,15 @@ internal fun BotaEncryptedUploadV2Progress.toWritableMap(): WritableMap = Argume
|
|
|
199
216
|
protocolStatus?.let { putDouble("protocolStatus", it.toDouble()) }
|
|
200
217
|
}
|
|
201
218
|
|
|
202
|
-
|
|
219
|
+
internal fun BotaEncryptedUploadV2Recording.toWritableMap(): WritableMap = Arguments.createMap().apply {
|
|
203
220
|
putString("uuid", uuid)
|
|
204
221
|
putDouble("generation", generation.toDouble())
|
|
205
222
|
putString("ciphertextLength", ciphertextLength)
|
|
206
223
|
putString("ciphertextSha256", ciphertextSha256)
|
|
224
|
+
putString("startedAtMs", startedAtMs)
|
|
225
|
+
putString("durationMs", durationMs)
|
|
226
|
+
putString("plaintextLength", plaintextLength)
|
|
227
|
+
putInt("storageFormat", storageFormat)
|
|
207
228
|
}
|
|
208
229
|
|
|
209
230
|
private fun BotaEncryptedUploadV2Capability.toWritableMap(): WritableMap = Arguments.createMap().apply {
|
|
@@ -240,6 +261,9 @@ internal class BotaDeviceSDKSharedAndroidRecordingClient(
|
|
|
240
261
|
override suspend fun listRecordings(device: ConnectedDevice): List<DeviceRecording> =
|
|
241
262
|
client.recordings.listRecordings(device)
|
|
242
263
|
|
|
264
|
+
override suspend fun listPendingRecordings(device: ConnectedDevice): List<PendingRecording> =
|
|
265
|
+
client.recordings.listPendingRecordings(device)
|
|
266
|
+
|
|
243
267
|
override fun syncRecording(
|
|
244
268
|
device: ConnectedDevice,
|
|
245
269
|
recording: DeviceRecording,
|
|
@@ -257,9 +281,14 @@ internal class BotaDeviceSDKSharedAndroidRecordingClient(
|
|
|
257
281
|
override suspend fun syncEncryptedRecordingV2(
|
|
258
282
|
device: ConnectedDevice,
|
|
259
283
|
recording: EncryptedUploadV2Recording,
|
|
284
|
+
operationId: UUID,
|
|
260
285
|
provider: EncryptedUploadV2ProfileProvider,
|
|
261
286
|
) {
|
|
262
|
-
client.recordings.syncEncryptedRecordingV2(device, recording, provider)
|
|
287
|
+
client.recordings.syncEncryptedRecordingV2(device, recording, operationId, provider)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
override suspend fun cancelEncryptedRecordingV2(operationId: UUID) {
|
|
291
|
+
client.recordings.cancelEncryptedRecordingV2(operationId)
|
|
263
292
|
}
|
|
264
293
|
|
|
265
294
|
override suspend fun confirmRecording(device: ConnectedDevice, recordingUuid: String) {
|
|
@@ -304,17 +333,28 @@ internal class BotaDeviceSDKSharedAndroidRecordingClient(
|
|
|
304
333
|
internal class BotaDeviceSDKAndroidRecordings(
|
|
305
334
|
private val client: BotaDeviceSDKAndroidRecordingClient =
|
|
306
335
|
BotaDeviceSDKSharedAndroidRecordingClient(),
|
|
336
|
+
private val fileSize: (String) -> Long = { path ->
|
|
337
|
+
val file = java.nio.file.Paths.get(path)
|
|
338
|
+
check(java.nio.file.Files.isRegularFile(file)) { "recording transfer did not produce a regular file" }
|
|
339
|
+
java.nio.file.Files.size(file)
|
|
340
|
+
},
|
|
307
341
|
) {
|
|
308
342
|
private val destinationRequests = ConcurrentHashMap<
|
|
309
343
|
String,
|
|
310
344
|
CompletableDeferred<StreamingUploadDestination>
|
|
311
345
|
>()
|
|
312
346
|
private data class EncryptedUploadV2Request(
|
|
347
|
+
val operationId: String,
|
|
313
348
|
val recording: EncryptedUploadV2Recording,
|
|
314
349
|
val material: CompletableDeferred<EncryptedUploadV2Material>,
|
|
315
|
-
)
|
|
350
|
+
) {
|
|
351
|
+
@Volatile var selectedMaterial: EncryptedUploadV2Material? = null
|
|
352
|
+
}
|
|
316
353
|
private val encryptedUploadV2Requests =
|
|
317
354
|
ConcurrentHashMap<String, EncryptedUploadV2Request>()
|
|
355
|
+
private data class EncryptedUploadV2Operation(val id: UUID, val job: Job)
|
|
356
|
+
private val encryptedUploadV2Operations = ConcurrentHashMap<String, EncryptedUploadV2Operation>()
|
|
357
|
+
private val cancelledEncryptedUploadV2Operations = ConcurrentHashMap.newKeySet<String>()
|
|
318
358
|
private val finalizeRequests = ConcurrentHashMap<String, CompletableDeferred<Unit>>()
|
|
319
359
|
@Volatile private var activeStreamingSessionId: String? = null
|
|
320
360
|
|
|
@@ -322,11 +362,37 @@ internal class BotaDeviceSDKAndroidRecordings(
|
|
|
322
362
|
val localPath: String,
|
|
323
363
|
val isE2EEncrypted: Boolean,
|
|
324
364
|
val contentSha256Hex: String?,
|
|
365
|
+
val fileSizeBytes: Long,
|
|
325
366
|
)
|
|
326
367
|
|
|
327
368
|
suspend fun listRecordings(device: ConnectedDevice): List<DeviceRecording> =
|
|
328
369
|
client.listRecordings(device)
|
|
329
370
|
|
|
371
|
+
suspend fun listPendingRecordings(device: ConnectedDevice): List<PendingRecording> =
|
|
372
|
+
client.listPendingRecordings(device)
|
|
373
|
+
|
|
374
|
+
suspend fun cancelEncryptedRecordingV2(operationId: String) {
|
|
375
|
+
val caller = currentCoroutineContext().job
|
|
376
|
+
cancelledEncryptedUploadV2Operations.add(operationId)
|
|
377
|
+
val operation = encryptedUploadV2Operations[operationId]
|
|
378
|
+
operation?.job?.cancel()
|
|
379
|
+
encryptedUploadV2Requests.entries.forEach { (id, request) ->
|
|
380
|
+
if (request.operationId == operationId && encryptedUploadV2Requests.remove(id, request)) {
|
|
381
|
+
request.material.completeExceptionally(CancellationException())
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
BotaDeviceSDKEncryptedUploadV2Materials.removeContext(operationId)
|
|
385
|
+
if (operation != null) {
|
|
386
|
+
withContext(NonCancellable) {
|
|
387
|
+
try {
|
|
388
|
+
client.cancelEncryptedRecordingV2(operation.id)
|
|
389
|
+
} finally {
|
|
390
|
+
if (operation.job !== caller) operation.job.join()
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
330
396
|
suspend fun syncRecording(
|
|
331
397
|
device: ConnectedDevice,
|
|
332
398
|
recording: DeviceRecording,
|
|
@@ -346,6 +412,7 @@ internal class BotaDeviceSDKAndroidRecordings(
|
|
|
346
412
|
localPath,
|
|
347
413
|
metadata?.isE2EEncrypted ?: false,
|
|
348
414
|
metadata?.contentSha256Hex,
|
|
415
|
+
fileSize(localPath),
|
|
349
416
|
)
|
|
350
417
|
}
|
|
351
418
|
|
|
@@ -356,10 +423,16 @@ internal class BotaDeviceSDKAndroidRecordings(
|
|
|
356
423
|
onProfileRequest: (BotaEncryptedUploadV2ProfileRequest) -> Unit,
|
|
357
424
|
onProgress: (BotaEncryptedUploadV2Progress) -> Unit,
|
|
358
425
|
) {
|
|
426
|
+
val operation = EncryptedUploadV2Operation(UUID.randomUUID(), currentCoroutineContext().job)
|
|
427
|
+
check(encryptedUploadV2Operations.putIfAbsent(operationId, operation) == null) {
|
|
428
|
+
"encrypted upload v2 operation is already active"
|
|
429
|
+
}
|
|
359
430
|
try {
|
|
431
|
+
if (cancelledEncryptedUploadV2Operations.contains(operationId)) throw CancellationException()
|
|
360
432
|
client.syncEncryptedRecordingV2(
|
|
361
433
|
device,
|
|
362
434
|
recording,
|
|
435
|
+
operation.id,
|
|
363
436
|
EncryptedUploadV2ProfileProvider { context ->
|
|
364
437
|
val completedBytes = context.checkpoint?.nextCiphertextOffset?.toString() ?: "0"
|
|
365
438
|
val checkpointRevision = context.checkpoint?.revision
|
|
@@ -376,17 +449,24 @@ internal class BotaDeviceSDKAndroidRecordings(
|
|
|
376
449
|
context,
|
|
377
450
|
onProfileRequest,
|
|
378
451
|
)
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
452
|
+
try {
|
|
453
|
+
currentCoroutineContext().ensureActive()
|
|
454
|
+
onProgress(BotaEncryptedUploadV2Progress(
|
|
455
|
+
operationId,
|
|
456
|
+
recording.uuid,
|
|
457
|
+
"transferring",
|
|
458
|
+
completedBytes,
|
|
459
|
+
recording.ciphertextLength.toString(),
|
|
460
|
+
checkpointRevision,
|
|
461
|
+
))
|
|
462
|
+
material
|
|
463
|
+
} catch (error: Throwable) {
|
|
464
|
+
withContext(NonCancellable) { material.cancelPreparation() }
|
|
465
|
+
throw error
|
|
466
|
+
}
|
|
388
467
|
},
|
|
389
468
|
)
|
|
469
|
+
currentCoroutineContext().ensureActive()
|
|
390
470
|
onProgress(BotaEncryptedUploadV2Progress(
|
|
391
471
|
operationId,
|
|
392
472
|
recording.uuid,
|
|
@@ -407,10 +487,13 @@ internal class BotaDeviceSDKAndroidRecordings(
|
|
|
407
487
|
protocolStatus = failure.protocolStatus,
|
|
408
488
|
))
|
|
409
489
|
throw error
|
|
490
|
+
} finally {
|
|
491
|
+
BotaDeviceSDKEncryptedUploadV2Materials.removeContext(operationId)
|
|
492
|
+
encryptedUploadV2Operations.remove(operationId, operation)
|
|
410
493
|
}
|
|
411
494
|
}
|
|
412
495
|
|
|
413
|
-
fun resolveEncryptedUploadV2Profile(
|
|
496
|
+
suspend fun resolveEncryptedUploadV2Profile(
|
|
414
497
|
requestId: String,
|
|
415
498
|
profile: String,
|
|
416
499
|
uploadSessionId: String,
|
|
@@ -419,26 +502,41 @@ internal class BotaDeviceSDKAndroidRecordings(
|
|
|
419
502
|
materialRegistrationId: String,
|
|
420
503
|
) {
|
|
421
504
|
val request = encryptedUploadV2Requests.remove(requestId)
|
|
422
|
-
|
|
423
|
-
|
|
505
|
+
if (request == null) {
|
|
506
|
+
withContext(NonCancellable) { BotaDeviceSDKEncryptedUploadV2Materials.release(materialRegistrationId) }
|
|
507
|
+
error("encrypted upload v2 profile request is no longer pending")
|
|
508
|
+
}
|
|
509
|
+
BotaDeviceSDKEncryptedUploadV2Materials.removeContext(request.operationId)
|
|
510
|
+
var selected: EncryptedUploadV2Material? = null
|
|
511
|
+
try {
|
|
424
512
|
require(profile == "encrypted_upload_v2") {
|
|
425
513
|
"encrypted upload v2 requires an explicit matching profile"
|
|
426
514
|
}
|
|
427
|
-
BotaDeviceSDKEncryptedUploadV2Materials.consume(
|
|
515
|
+
selected = BotaDeviceSDKEncryptedUploadV2Materials.consume(
|
|
428
516
|
materialRegistrationId,
|
|
429
517
|
request.recording,
|
|
430
518
|
UUID.fromString(uploadSessionId),
|
|
431
519
|
ownerRevision,
|
|
432
520
|
securityPolicy,
|
|
433
521
|
)
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
522
|
+
request.selectedMaterial = selected
|
|
523
|
+
check(request.material.complete(selected)) {
|
|
524
|
+
"encrypted upload v2 profile request is no longer pending"
|
|
525
|
+
}
|
|
526
|
+
} catch (error: Throwable) {
|
|
527
|
+
request.material.completeExceptionally(error)
|
|
528
|
+
withContext(NonCancellable) {
|
|
529
|
+
if (selected != null) selected.cancelPreparation()
|
|
530
|
+
else BotaDeviceSDKEncryptedUploadV2Materials.release(materialRegistrationId)
|
|
531
|
+
}
|
|
532
|
+
throw error
|
|
533
|
+
}
|
|
437
534
|
}
|
|
438
535
|
|
|
439
536
|
fun rejectEncryptedUploadV2Profile(requestId: String, errorCode: String) {
|
|
440
537
|
val request = encryptedUploadV2Requests.remove(requestId)
|
|
441
538
|
?: error("encrypted upload v2 profile request is no longer pending")
|
|
539
|
+
BotaDeviceSDKEncryptedUploadV2Materials.removeContext(request.operationId)
|
|
442
540
|
val error = if (errorCode == "application_material_rejected") {
|
|
443
541
|
IllegalStateException("encrypted upload v2 material was rejected")
|
|
444
542
|
} else {
|
|
@@ -567,6 +665,9 @@ internal class BotaDeviceSDKAndroidRecordings(
|
|
|
567
665
|
}
|
|
568
666
|
|
|
569
667
|
suspend fun cancelAll() {
|
|
668
|
+
val operations = encryptedUploadV2Operations.toMap()
|
|
669
|
+
operations.values.forEach { it.job.cancel() }
|
|
670
|
+
operations.keys.forEach { runCatching { cancelEncryptedRecordingV2(it) } }
|
|
570
671
|
rejectPendingRequests("recording operations were cancelled")
|
|
571
672
|
runCatching { client.cancelCurrentOperation() }
|
|
572
673
|
}
|
|
@@ -600,21 +701,34 @@ internal class BotaDeviceSDKAndroidRecordings(
|
|
|
600
701
|
val requestId = UUID.randomUUID().toString()
|
|
601
702
|
val deferred = CompletableDeferred<EncryptedUploadV2Material>()
|
|
602
703
|
val request = EncryptedUploadV2Request(
|
|
704
|
+
operationId,
|
|
603
705
|
context.recording,
|
|
604
706
|
deferred,
|
|
605
707
|
)
|
|
708
|
+
BotaDeviceSDKEncryptedUploadV2Materials.registerContext(operationId, context)
|
|
606
709
|
encryptedUploadV2Requests[requestId] = request
|
|
607
|
-
|
|
608
|
-
requestId,
|
|
609
|
-
operationId,
|
|
610
|
-
context.recording.toBridgeValue(),
|
|
611
|
-
context.capability.toBridgeValue(),
|
|
612
|
-
context.checkpoint?.toBridgeValue(),
|
|
613
|
-
))
|
|
710
|
+
var delivered = false
|
|
614
711
|
return try {
|
|
615
|
-
|
|
712
|
+
if (cancelledEncryptedUploadV2Operations.contains(operationId)) throw CancellationException()
|
|
713
|
+
onRequest(BotaEncryptedUploadV2ProfileRequest(
|
|
714
|
+
requestId,
|
|
715
|
+
operationId,
|
|
716
|
+
context.recording.toBridgeValue(),
|
|
717
|
+
context.capability.toBridgeValue(),
|
|
718
|
+
context.checkpoint?.toBridgeValue(),
|
|
719
|
+
))
|
|
720
|
+
deferred.await().also {
|
|
721
|
+
currentCoroutineContext().ensureActive()
|
|
722
|
+
delivered = true
|
|
723
|
+
}
|
|
616
724
|
} finally {
|
|
617
725
|
encryptedUploadV2Requests.remove(requestId, request)
|
|
726
|
+
BotaDeviceSDKEncryptedUploadV2Materials.removeContext(operationId)
|
|
727
|
+
if (!delivered) {
|
|
728
|
+
// A completed deferred can still lose to prompt coroutine cancellation.
|
|
729
|
+
deferred.cancel()
|
|
730
|
+
withContext(NonCancellable) { request.selectedMaterial?.cancelPreparation() }
|
|
731
|
+
}
|
|
618
732
|
}
|
|
619
733
|
}
|
|
620
734
|
|
|
@@ -643,7 +757,10 @@ internal class BotaDeviceSDKAndroidRecordings(
|
|
|
643
757
|
|
|
644
758
|
private fun rejectPendingRequests(message: String) {
|
|
645
759
|
val error = IllegalStateException(message)
|
|
646
|
-
encryptedUploadV2Requests.values.forEach {
|
|
760
|
+
encryptedUploadV2Requests.values.forEach {
|
|
761
|
+
BotaDeviceSDKEncryptedUploadV2Materials.removeContext(it.operationId)
|
|
762
|
+
it.material.completeExceptionally(error)
|
|
763
|
+
}
|
|
647
764
|
destinationRequests.values.forEach { it.completeExceptionally(error) }
|
|
648
765
|
finalizeRequests.values.forEach { it.completeExceptionally(error) }
|
|
649
766
|
destinationRequests.clear()
|
|
@@ -652,12 +769,16 @@ internal class BotaDeviceSDKAndroidRecordings(
|
|
|
652
769
|
}
|
|
653
770
|
}
|
|
654
771
|
|
|
655
|
-
|
|
772
|
+
internal fun EncryptedUploadV2Recording.toBridgeValue(): BotaEncryptedUploadV2Recording =
|
|
656
773
|
BotaEncryptedUploadV2Recording(
|
|
657
774
|
uuid,
|
|
658
775
|
generation,
|
|
659
776
|
ciphertextLength.toString(),
|
|
660
777
|
ciphertextSha256.toHex(),
|
|
778
|
+
startedAtMs.toString(),
|
|
779
|
+
durationMs.toString(),
|
|
780
|
+
plaintextLength.toString(),
|
|
781
|
+
storageFormat.toInt(),
|
|
661
782
|
)
|
|
662
783
|
|
|
663
784
|
private fun EncryptedUploadV2CapabilitySnapshot.toBridgeValue(): BotaEncryptedUploadV2Capability {
|
package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKEncryptedUploadV2Materials.kt
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package dev.bota.sdk.reactnative
|
|
2
2
|
|
|
3
3
|
import dev.bota.sdk.EncryptedUploadV2Material
|
|
4
|
+
import dev.bota.sdk.EncryptedUploadV2ProviderContext
|
|
4
5
|
import dev.bota.sdk.EncryptedUploadV2Recording
|
|
5
6
|
import dev.bota.sdk.EncryptedUploadV2SecurityPolicy
|
|
6
7
|
import java.util.UUID
|
|
@@ -9,6 +10,32 @@ import java.util.concurrent.ConcurrentHashMap
|
|
|
9
10
|
/** Holds v2 application material in native memory and exposes only opaque IDs to JavaScript. */
|
|
10
11
|
public object BotaDeviceSDKEncryptedUploadV2Materials {
|
|
11
12
|
private val materials = ConcurrentHashMap<String, EncryptedUploadV2Material>()
|
|
13
|
+
private data class Context(val lease: UUID, val value: EncryptedUploadV2ProviderContext)
|
|
14
|
+
private val contexts = ConcurrentHashMap<String, Context>()
|
|
15
|
+
|
|
16
|
+
public suspend fun readAuthNonce(operationId: String): ByteArray {
|
|
17
|
+
val context = contexts[operationId]
|
|
18
|
+
?: error("encrypted upload v2 provider context is unavailable")
|
|
19
|
+
val nonce = context.value.readAuthNonce()
|
|
20
|
+
check(contexts[operationId]?.lease == context.lease) {
|
|
21
|
+
"encrypted upload v2 provider context is unavailable"
|
|
22
|
+
}
|
|
23
|
+
return nonce.copyOf()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public suspend fun release(registrationId: String) {
|
|
27
|
+
materials.remove(registrationId)?.cancelPreparation()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
internal fun registerContext(operationId: String, context: EncryptedUploadV2ProviderContext) {
|
|
31
|
+
check(contexts.putIfAbsent(operationId, Context(UUID.randomUUID(), context)) == null) {
|
|
32
|
+
"encrypted upload v2 provider context is already active"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
internal fun removeContext(operationId: String) {
|
|
37
|
+
contexts.remove(operationId)
|
|
38
|
+
}
|
|
12
39
|
|
|
13
40
|
public fun register(material: EncryptedUploadV2Material): String =
|
|
14
41
|
UUID.randomUUID().toString().also { materials[it] = material }
|
|
@@ -4,6 +4,7 @@ import com.facebook.react.bridge.Arguments
|
|
|
4
4
|
import com.facebook.react.bridge.Promise
|
|
5
5
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
6
|
import com.facebook.react.bridge.ReadableMap
|
|
7
|
+
import com.facebook.react.bridge.ReadableArray
|
|
7
8
|
import com.facebook.react.module.annotations.ReactModule
|
|
8
9
|
import dev.bota.sdk.DeviceReconnectHint
|
|
9
10
|
import dev.bota.sdk.DeviceApiEnvironment
|
|
@@ -42,7 +43,13 @@ internal class BotaDeviceSDKModule(
|
|
|
42
43
|
} else {
|
|
43
44
|
null
|
|
44
45
|
}
|
|
45
|
-
launch(promise) {
|
|
46
|
+
launch(promise) {
|
|
47
|
+
lifecycle.configure(storageDirectory)
|
|
48
|
+
recordingUploads.configure(storageDirectory)
|
|
49
|
+
devices.startConnectionUpdates {
|
|
50
|
+
emitOnDeviceDisconnected(Arguments.createMap())
|
|
51
|
+
}
|
|
52
|
+
}
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
override fun destroy(promise: Promise) {
|
|
@@ -223,6 +230,37 @@ internal class BotaDeviceSDKModule(
|
|
|
223
230
|
}
|
|
224
231
|
}
|
|
225
232
|
|
|
233
|
+
override fun listPendingRecordings(device: ReadableMap, promise: Promise) {
|
|
234
|
+
launchValue(promise) {
|
|
235
|
+
Arguments.createArray().apply {
|
|
236
|
+
recordings.listPendingRecordings(device.toConnectedDevice()).forEach { recording ->
|
|
237
|
+
pushMap(Arguments.createMap().apply {
|
|
238
|
+
when (recording) {
|
|
239
|
+
is dev.bota.sdk.PendingRecording.Legacy -> {
|
|
240
|
+
putString("profile", "legacy")
|
|
241
|
+
putMap("legacy", recording.recording.toWritableMap())
|
|
242
|
+
}
|
|
243
|
+
is dev.bota.sdk.PendingRecording.EncryptedV2 -> {
|
|
244
|
+
putString("profile", "encrypted_upload_v2")
|
|
245
|
+
putMap("encrypted", recording.recording.toBridgeValue().toWritableMap())
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
override fun cancelEncryptedRecordingV2(operationId: String, promise: Promise) {
|
|
255
|
+
launchEncryptedUploadV2(promise) { recordings.cancelEncryptedRecordingV2(operationId) }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
override fun releaseEncryptedUploadV2Material(materialRegistrationId: String, promise: Promise) {
|
|
259
|
+
launchEncryptedUploadV2(promise) {
|
|
260
|
+
BotaDeviceSDKEncryptedUploadV2Materials.release(materialRegistrationId)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
226
264
|
override fun syncRecording(
|
|
227
265
|
device: ReadableMap,
|
|
228
266
|
recording: ReadableMap,
|
|
@@ -264,7 +302,7 @@ internal class BotaDeviceSDKModule(
|
|
|
264
302
|
decision: ReadableMap,
|
|
265
303
|
promise: Promise,
|
|
266
304
|
) {
|
|
267
|
-
|
|
305
|
+
launchEncryptedUploadV2(promise) {
|
|
268
306
|
recordings.resolveEncryptedUploadV2Profile(
|
|
269
307
|
requestId,
|
|
270
308
|
decision.getString("profile")
|
|
@@ -285,7 +323,7 @@ internal class BotaDeviceSDKModule(
|
|
|
285
323
|
errorCode: String,
|
|
286
324
|
promise: Promise,
|
|
287
325
|
) {
|
|
288
|
-
|
|
326
|
+
launchEncryptedUploadV2(promise) { recordings.rejectEncryptedUploadV2Profile(requestId, errorCode) }
|
|
289
327
|
}
|
|
290
328
|
|
|
291
329
|
override fun startStreaming(
|
|
@@ -385,6 +423,10 @@ internal class BotaDeviceSDKModule(
|
|
|
385
423
|
launch(promise) { recordingUploads.cancel(taskId) }
|
|
386
424
|
}
|
|
387
425
|
|
|
426
|
+
override fun releaseRecordingFile(taskId: String, localPath: String, promise: Promise) {
|
|
427
|
+
launch(promise) { recordingUploads.release(taskId, localPath) }
|
|
428
|
+
}
|
|
429
|
+
|
|
388
430
|
override fun loadCompatibilityUploadQueue(promise: Promise) {
|
|
389
431
|
launchValue(promise) { recordingUploads.loadQueue() }
|
|
390
432
|
}
|
|
@@ -453,10 +495,36 @@ internal class BotaDeviceSDKModule(
|
|
|
453
495
|
launch(promise) { logs.stop() }
|
|
454
496
|
}
|
|
455
497
|
|
|
498
|
+
override fun readDiagnosticEvents(device: ReadableMap, promise: Promise) {
|
|
499
|
+
launchValue(promise) { logs.readDiagnosticEvents(device.toConnectedDevice()).toWritableMap() }
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
override fun acknowledgeDiagnosticEvents(device: ReadableMap, acceptedEventIds: ReadableArray, promise: Promise) {
|
|
503
|
+
launch(promise) {
|
|
504
|
+
val ids = (0 until acceptedEventIds.size()).map { acceptedEventIds.getString(it) ?: error("diagnostic event ID is required") }
|
|
505
|
+
logs.acknowledgeDiagnosticEvents(device.toConnectedDevice(), ids)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
456
509
|
override fun readStatus(promise: Promise) {
|
|
457
510
|
launchValue(promise) { devices.readStatus().toWritableMap() }
|
|
458
511
|
}
|
|
459
512
|
|
|
513
|
+
override fun nextClientPresence(deviceId: String, promise: Promise) {
|
|
514
|
+
launchValue(promise) {
|
|
515
|
+
devices.nextClientPresence(deviceId)?.let { report ->
|
|
516
|
+
Arguments.createMap().apply {
|
|
517
|
+
putInt("schemaVersion", report.schemaVersion)
|
|
518
|
+
putString("sessionId", report.sessionId)
|
|
519
|
+
putDouble("sequence", report.sequence.toDouble())
|
|
520
|
+
putString("platform", report.platform)
|
|
521
|
+
putString("sdkPackage", report.sdkPackage)
|
|
522
|
+
putString("sdkVersion", report.sdkVersion)
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
460
528
|
override fun startStatusUpdates(promise: Promise) {
|
|
461
529
|
launch(promise) {
|
|
462
530
|
devices.startStatusUpdates(
|
|
@@ -712,9 +780,24 @@ private fun ReadableMap.toEncryptedUploadV2Recording(): EncryptedUploadV2Recordi
|
|
|
712
780
|
getDouble("generation").toUnsignedInt(),
|
|
713
781
|
length,
|
|
714
782
|
digest,
|
|
783
|
+
optionalCatalogInteger("startedAtMs"),
|
|
784
|
+
optionalCatalogInteger("durationMs"),
|
|
785
|
+
optionalCatalogInteger("plaintextLength"),
|
|
786
|
+
if (hasKey("storageFormat") && !isNull("storageFormat")) {
|
|
787
|
+
getDouble("storageFormat").toUnsignedInt().also { require(it <= 255u) }.toUByte()
|
|
788
|
+
} else 3u,
|
|
715
789
|
)
|
|
716
790
|
}
|
|
717
791
|
|
|
792
|
+
private fun ReadableMap.optionalCatalogInteger(key: String): ULong {
|
|
793
|
+
if (!hasKey(key) || isNull(key)) return 0u
|
|
794
|
+
val value = getString(key) ?: error("encrypted upload v2 catalog integer is required")
|
|
795
|
+
require(value.matches(Regex("0|[1-9][0-9]*"))) {
|
|
796
|
+
"encrypted upload v2 catalog integer is invalid"
|
|
797
|
+
}
|
|
798
|
+
return value.toULongOrNull() ?: error("encrypted upload v2 catalog integer exceeds its bound")
|
|
799
|
+
}
|
|
800
|
+
|
|
718
801
|
private fun String.sha256Bytes(): ByteArray {
|
|
719
802
|
require(length == 64 && all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }) {
|
|
720
803
|
"encrypted upload v2 ciphertext SHA-256 must contain exactly 32 hexadecimal bytes"
|
|
@@ -22,6 +22,38 @@ import org.junit.Assert.assertEquals
|
|
|
22
22
|
import org.junit.Test
|
|
23
23
|
|
|
24
24
|
class BotaDeviceSDKAndroidDevicesTest {
|
|
25
|
+
@Test
|
|
26
|
+
fun transportLossIsForwardedWithoutAStatusSubscription() = runTest {
|
|
27
|
+
val verified = ConnectedDevice(
|
|
28
|
+
id = "selected", serialNumber = "EVFXXW67KP", deviceType = DeviceType.BotaPin,
|
|
29
|
+
firmwareVersion = "1.0.19", isProvisioned = false,
|
|
30
|
+
connectionState = ConnectionState.Connected, mtu = 247,
|
|
31
|
+
)
|
|
32
|
+
val stopped = CompletableDeferred<Unit>()
|
|
33
|
+
val client = FakeDeviceClient(
|
|
34
|
+
DiscoveredDevice("selected", rssi = -30), verified,
|
|
35
|
+
connections = flow {
|
|
36
|
+
try {
|
|
37
|
+
emit(null)
|
|
38
|
+
emit(verified)
|
|
39
|
+
emit(null)
|
|
40
|
+
emit(null)
|
|
41
|
+
awaitCancellation()
|
|
42
|
+
} finally {
|
|
43
|
+
stopped.complete(Unit)
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
)
|
|
47
|
+
val devices = BotaDeviceSDKAndroidDevices(client, backgroundScope)
|
|
48
|
+
var losses = 0
|
|
49
|
+
devices.startConnectionUpdates { losses += 1 }
|
|
50
|
+
assertEquals(1, losses)
|
|
51
|
+
assertEquals(0, client.statusReadCount)
|
|
52
|
+
devices.stopAll()
|
|
53
|
+
stopped.await()
|
|
54
|
+
assertEquals(1, losses)
|
|
55
|
+
}
|
|
56
|
+
|
|
25
57
|
@Test
|
|
26
58
|
fun unknownPairingStateUsesTheFrozenUnpairedFallback() {
|
|
27
59
|
assertEquals("unpaired", PairingState.Unknown(0xFFu).toBridgeValue())
|
|
@@ -134,7 +166,10 @@ class BotaDeviceSDKAndroidDevicesTest {
|
|
|
134
166
|
private val connected: ConnectedDevice,
|
|
135
167
|
private val scanFailure: Throwable? = null,
|
|
136
168
|
private val status: DeviceStatus = testDeviceStatus(),
|
|
169
|
+
private val connections: Flow<ConnectedDevice?> = kotlinx.coroutines.flow.emptyFlow(),
|
|
137
170
|
) : BotaDeviceSDKAndroidDeviceClient {
|
|
171
|
+
override fun connectionUpdates(): Flow<ConnectedDevice?> = connections
|
|
172
|
+
override suspend fun nextClientPresence(deviceId: String): dev.bota.sdk.SDKClientContext? = null
|
|
138
173
|
val scanOptions = mutableListOf<Pair<ULong, Boolean>>()
|
|
139
174
|
val selectedIds = mutableListOf<String>()
|
|
140
175
|
val reconnectSerials = mutableListOf<String>()
|
|
@@ -4,6 +4,8 @@ import dev.bota.sdk.model.ConnectedDevice
|
|
|
4
4
|
import dev.bota.sdk.model.ConnectionState
|
|
5
5
|
import dev.bota.sdk.model.DeviceLogLine
|
|
6
6
|
import dev.bota.sdk.model.DeviceType
|
|
7
|
+
import dev.bota.sdk.model.DeviceDiagnosticEvent
|
|
8
|
+
import dev.bota.sdk.model.DeviceDiagnosticsBatch
|
|
7
9
|
import kotlinx.coroutines.CoroutineScope
|
|
8
10
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
9
11
|
import kotlinx.coroutines.awaitCancellation
|
|
@@ -16,6 +18,23 @@ import org.junit.Assert.assertTrue
|
|
|
16
18
|
import org.junit.Test
|
|
17
19
|
|
|
18
20
|
class BotaDeviceSDKAndroidLogsTest {
|
|
21
|
+
@Test
|
|
22
|
+
fun diagnosticReadDoesNotAcknowledgeAndPreservesEventIDs() = runTest {
|
|
23
|
+
val client = TestAndroidLogClient()
|
|
24
|
+
val logs = BotaDeviceSDKAndroidLogs(client, CoroutineScope(coroutineContext))
|
|
25
|
+
val device = ConnectedDevice(
|
|
26
|
+
id = "selected", serialNumber = "EVFXXW67KP", deviceType = DeviceType.BotaPin,
|
|
27
|
+
firmwareVersion = "1.0.11", isProvisioned = true,
|
|
28
|
+
connectionState = ConnectionState.Connected, mtu = 247,
|
|
29
|
+
)
|
|
30
|
+
val batch = logs.readDiagnosticEvents(device)
|
|
31
|
+
assertTrue(client.acceptedIDs.isEmpty())
|
|
32
|
+
assertEquals("ffffffffffffffff", batch.events.single().eventId)
|
|
33
|
+
assertEquals(UInt.MAX_VALUE, batch.events.single().uptimeMs)
|
|
34
|
+
logs.acknowledgeDiagnosticEvents(device, listOf("ffffffffffffffff"))
|
|
35
|
+
assertEquals(listOf("ffffffffffffffff"), client.acceptedIDs)
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
@OptIn(ExperimentalCoroutinesApi::class)
|
|
20
39
|
@Test
|
|
21
40
|
fun logStreamEmitsSanitizedLinesAndOwnsStop() = runTest {
|
|
@@ -44,6 +63,20 @@ class BotaDeviceSDKAndroidLogsTest {
|
|
|
44
63
|
|
|
45
64
|
private class TestAndroidLogClient : BotaDeviceSDKAndroidLogClient {
|
|
46
65
|
var stopped = false
|
|
66
|
+
val acceptedIDs = mutableListOf<String>()
|
|
67
|
+
|
|
68
|
+
override suspend fun readDiagnosticEvents(device: ConnectedDevice) = DeviceDiagnosticsBatch(
|
|
69
|
+
schemaVersion = 1,
|
|
70
|
+
events = listOf(DeviceDiagnosticEvent(
|
|
71
|
+
eventId = "ffffffffffffffff", eventType = "crash", reasonCode = "exception",
|
|
72
|
+
uptimeMs = UInt.MAX_VALUE, signature = "0123456789abcdef", firmwareBuildId = "1.0.11",
|
|
73
|
+
subsystem = "system", stateBeforeEvent = "idle",
|
|
74
|
+
)),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
override suspend fun acknowledgeDiagnosticEvents(device: ConnectedDevice, acceptedEventIds: List<String>) {
|
|
78
|
+
acceptedIDs.addAll(acceptedEventIds)
|
|
79
|
+
}
|
|
47
80
|
|
|
48
81
|
override fun streamLogs(device: ConnectedDevice): Flow<DeviceLogLine> = flow {
|
|
49
82
|
emit(DeviceLogLine("boot pass", isBacklog = true))
|