@bota.dev/react-native-app-sdk 2.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/BotaDeviceSDK.podspec +55 -0
  2. package/LICENSE +21 -0
  3. package/README.md +101 -0
  4. package/android/build.gradle +76 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDevices.kt +172 -0
  7. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLifecycle.kt +100 -0
  8. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogs.kt +82 -0
  9. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidModels.kt +476 -0
  10. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidOTA.kt +58 -0
  11. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploads.kt +180 -0
  12. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordings.kt +751 -0
  13. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidSecurity.kt +509 -0
  14. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidWiFi.kt +125 -0
  15. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKEncryptedUploadV2Materials.kt +46 -0
  16. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKModule.kt +723 -0
  17. package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKPackage.kt +33 -0
  18. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDevicesTest.kt +211 -0
  19. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLifecycleTest.kt +132 -0
  20. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogsTest.kt +57 -0
  21. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidOTATest.kt +75 -0
  22. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploadsTest.kt +147 -0
  23. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingsTest.kt +355 -0
  24. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidSecurityTest.kt +523 -0
  25. package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidWiFiTest.kt +100 -0
  26. package/generated/codegen-contract.json +3593 -0
  27. package/ios/BotaDeviceSDK.mm +1300 -0
  28. package/ios/BotaDeviceSDKAppleBridge.swift +2260 -0
  29. package/ios/BotaDeviceSDKAppleDevices.swift +191 -0
  30. package/ios/BotaDeviceSDKAppleLifecycle.swift +167 -0
  31. package/ios/BotaDeviceSDKAppleLogs.swift +79 -0
  32. package/ios/BotaDeviceSDKAppleOTA.swift +73 -0
  33. package/ios/BotaDeviceSDKAppleRecordingUploads.swift +178 -0
  34. package/ios/BotaDeviceSDKAppleRecordings.swift +670 -0
  35. package/ios/BotaDeviceSDKAppleSecurity.swift +586 -0
  36. package/ios/BotaDeviceSDKAppleWiFi.swift +128 -0
  37. package/ios/BotaDeviceSDKEncryptedUploadV2Materials.swift +99 -0
  38. package/lib/commonjs/BotaClient.js +194 -0
  39. package/lib/commonjs/BotaClient.js.map +1 -0
  40. package/lib/commonjs/ble/constants.js +10 -0
  41. package/lib/commonjs/ble/constants.js.map +1 -0
  42. package/lib/commonjs/ble/deviceLogs.js +50 -0
  43. package/lib/commonjs/ble/deviceLogs.js.map +1 -0
  44. package/lib/commonjs/cache/DeviceStateCache.js +6 -0
  45. package/lib/commonjs/cache/DeviceStateCache.js.map +1 -0
  46. package/lib/commonjs/client.js +746 -0
  47. package/lib/commonjs/client.js.map +1 -0
  48. package/lib/commonjs/compatibility/runtime.js +44 -0
  49. package/lib/commonjs/compatibility/runtime.js.map +1 -0
  50. package/lib/commonjs/index.js +123 -0
  51. package/lib/commonjs/index.js.map +1 -0
  52. package/lib/commonjs/managers/DeviceManager.js +647 -0
  53. package/lib/commonjs/managers/DeviceManager.js.map +1 -0
  54. package/lib/commonjs/managers/OTAManager.js +140 -0
  55. package/lib/commonjs/managers/OTAManager.js.map +1 -0
  56. package/lib/commonjs/managers/RecordingManager.js +444 -0
  57. package/lib/commonjs/managers/RecordingManager.js.map +1 -0
  58. package/lib/commonjs/managers/StreamingSession.js +175 -0
  59. package/lib/commonjs/managers/StreamingSession.js.map +1 -0
  60. package/lib/commonjs/managers/types.js +6 -0
  61. package/lib/commonjs/managers/types.js.map +1 -0
  62. package/lib/commonjs/models/Device.js +2 -0
  63. package/lib/commonjs/models/Device.js.map +1 -0
  64. package/lib/commonjs/models/Recording.js +2 -0
  65. package/lib/commonjs/models/Recording.js.map +1 -0
  66. package/lib/commonjs/models/Status.js +6 -0
  67. package/lib/commonjs/models/Status.js.map +1 -0
  68. package/lib/commonjs/package.json +1 -0
  69. package/lib/commonjs/specs/NativeBotaDeviceSDK.js +9 -0
  70. package/lib/commonjs/specs/NativeBotaDeviceSDK.js.map +1 -0
  71. package/lib/commonjs/sync/syncStatus.js +178 -0
  72. package/lib/commonjs/sync/syncStatus.js.map +1 -0
  73. package/lib/commonjs/utils/errors.js +194 -0
  74. package/lib/commonjs/utils/errors.js.map +1 -0
  75. package/lib/commonjs/utils/logger.js +84 -0
  76. package/lib/commonjs/utils/logger.js.map +1 -0
  77. package/lib/module/BotaClient.js +189 -0
  78. package/lib/module/BotaClient.js.map +1 -0
  79. package/lib/module/ble/constants.js +6 -0
  80. package/lib/module/ble/constants.js.map +1 -0
  81. package/lib/module/ble/deviceLogs.js +45 -0
  82. package/lib/module/ble/deviceLogs.js.map +1 -0
  83. package/lib/module/cache/DeviceStateCache.js +4 -0
  84. package/lib/module/cache/DeviceStateCache.js.map +1 -0
  85. package/lib/module/client.js +740 -0
  86. package/lib/module/client.js.map +1 -0
  87. package/lib/module/compatibility/runtime.js +35 -0
  88. package/lib/module/compatibility/runtime.js.map +1 -0
  89. package/lib/module/index.js +17 -0
  90. package/lib/module/index.js.map +1 -0
  91. package/lib/module/managers/DeviceManager.js +641 -0
  92. package/lib/module/managers/DeviceManager.js.map +1 -0
  93. package/lib/module/managers/OTAManager.js +134 -0
  94. package/lib/module/managers/OTAManager.js.map +1 -0
  95. package/lib/module/managers/RecordingManager.js +438 -0
  96. package/lib/module/managers/RecordingManager.js.map +1 -0
  97. package/lib/module/managers/StreamingSession.js +168 -0
  98. package/lib/module/managers/StreamingSession.js.map +1 -0
  99. package/lib/module/managers/types.js +4 -0
  100. package/lib/module/managers/types.js.map +1 -0
  101. package/lib/module/models/Device.js +2 -0
  102. package/lib/module/models/Device.js.map +1 -0
  103. package/lib/module/models/Recording.js +2 -0
  104. package/lib/module/models/Recording.js.map +1 -0
  105. package/lib/module/models/Status.js +4 -0
  106. package/lib/module/models/Status.js.map +1 -0
  107. package/lib/module/specs/NativeBotaDeviceSDK.js +5 -0
  108. package/lib/module/specs/NativeBotaDeviceSDK.js.map +1 -0
  109. package/lib/module/sync/syncStatus.js +174 -0
  110. package/lib/module/sync/syncStatus.js.map +1 -0
  111. package/lib/module/utils/errors.js +183 -0
  112. package/lib/module/utils/errors.js.map +1 -0
  113. package/lib/module/utils/logger.js +80 -0
  114. package/lib/module/utils/logger.js.map +1 -0
  115. package/lib/typescript/BotaClient.d.ts +40 -0
  116. package/lib/typescript/BotaClient.d.ts.map +1 -0
  117. package/lib/typescript/ble/constants.d.ts +4 -0
  118. package/lib/typescript/ble/constants.d.ts.map +1 -0
  119. package/lib/typescript/ble/deviceLogs.d.ts +10 -0
  120. package/lib/typescript/ble/deviceLogs.d.ts.map +1 -0
  121. package/lib/typescript/cache/DeviceStateCache.d.ts +14 -0
  122. package/lib/typescript/cache/DeviceStateCache.d.ts.map +1 -0
  123. package/lib/typescript/client.d.ts +208 -0
  124. package/lib/typescript/client.d.ts.map +1 -0
  125. package/lib/typescript/compatibility/runtime.d.ts +11 -0
  126. package/lib/typescript/compatibility/runtime.d.ts.map +1 -0
  127. package/lib/typescript/index.d.ts +19 -0
  128. package/lib/typescript/index.d.ts.map +1 -0
  129. package/lib/typescript/managers/DeviceManager.d.ts +117 -0
  130. package/lib/typescript/managers/DeviceManager.d.ts.map +1 -0
  131. package/lib/typescript/managers/OTAManager.d.ts +25 -0
  132. package/lib/typescript/managers/OTAManager.d.ts.map +1 -0
  133. package/lib/typescript/managers/RecordingManager.d.ts +54 -0
  134. package/lib/typescript/managers/RecordingManager.d.ts.map +1 -0
  135. package/lib/typescript/managers/StreamingSession.d.ts +35 -0
  136. package/lib/typescript/managers/StreamingSession.d.ts.map +1 -0
  137. package/lib/typescript/managers/types.d.ts +19 -0
  138. package/lib/typescript/managers/types.d.ts.map +1 -0
  139. package/lib/typescript/models/Device.d.ts +392 -0
  140. package/lib/typescript/models/Device.d.ts.map +1 -0
  141. package/lib/typescript/models/Recording.d.ts +259 -0
  142. package/lib/typescript/models/Recording.d.ts.map +1 -0
  143. package/lib/typescript/models/Status.d.ts +106 -0
  144. package/lib/typescript/models/Status.d.ts.map +1 -0
  145. package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts +393 -0
  146. package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts.map +1 -0
  147. package/lib/typescript/sync/syncStatus.d.ts +123 -0
  148. package/lib/typescript/sync/syncStatus.d.ts.map +1 -0
  149. package/lib/typescript/utils/errors.d.ts +83 -0
  150. package/lib/typescript/utils/errors.d.ts.map +1 -0
  151. package/lib/typescript/utils/logger.d.ts +36 -0
  152. package/lib/typescript/utils/logger.d.ts.map +1 -0
  153. package/package.json +100 -0
  154. package/scripts/bota_device_sdk_spm_workaround.rb +53 -0
  155. package/src/BotaClient.ts +231 -0
  156. package/src/ble/constants.ts +3 -0
  157. package/src/ble/deviceLogs.ts +61 -0
  158. package/src/cache/DeviceStateCache.ts +20 -0
  159. package/src/client.ts +1410 -0
  160. package/src/compatibility/runtime.ts +55 -0
  161. package/src/index.ts +146 -0
  162. package/src/managers/DeviceManager.ts +927 -0
  163. package/src/managers/OTAManager.ts +195 -0
  164. package/src/managers/RecordingManager.ts +578 -0
  165. package/src/managers/StreamingSession.ts +212 -0
  166. package/src/managers/types.ts +36 -0
  167. package/src/models/Device.ts +484 -0
  168. package/src/models/Recording.ts +281 -0
  169. package/src/models/Status.ts +128 -0
  170. package/src/specs/NativeBotaDeviceSDK.ts +542 -0
  171. package/src/sync/syncStatus.ts +243 -0
  172. package/src/utils/errors.ts +327 -0
  173. package/src/utils/logger.ts +122 -0
@@ -0,0 +1,751 @@
1
+ package dev.bota.sdk.reactnative
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+ import dev.bota.sdk.BotaErrorCode
6
+ import dev.bota.sdk.BotaDeviceClient
7
+ import dev.bota.sdk.BotaSDKError
8
+ import dev.bota.sdk.EncryptedUploadV2CapabilitySnapshot
9
+ import dev.bota.sdk.EncryptedUploadV2Checkpoint
10
+ import dev.bota.sdk.EncryptedUploadV2Material
11
+ import dev.bota.sdk.EncryptedUploadV2ProfileProvider
12
+ import dev.bota.sdk.EncryptedUploadV2ProviderContext
13
+ import dev.bota.sdk.EncryptedUploadV2Recording
14
+ import dev.bota.sdk.RecordingSyncEvent
15
+ import dev.bota.sdk.RecordingTransferMetadata
16
+ import dev.bota.sdk.UploadOwnershipEvent
17
+ import dev.bota.sdk.UploadOwnershipResult
18
+ import dev.bota.sdk.model.ConnectedDevice
19
+ import dev.bota.sdk.model.DeviceRecording
20
+ import dev.bota.sdk.model.RecordingTransferProgress
21
+ import dev.bota.sdk.model.StreamingChunkDestinationProvider
22
+ import dev.bota.sdk.model.StreamingChunkRequest
23
+ import dev.bota.sdk.model.StreamingFinalizeHandler
24
+ import dev.bota.sdk.model.StreamingFinalizeMetadata
25
+ import dev.bota.sdk.model.StreamingRecordingEvent
26
+ import dev.bota.sdk.model.StreamingUploadDestination
27
+ import dev.bota.sdk.model.StreamingUploadMethod
28
+ import java.util.UUID
29
+ import java.util.concurrent.ConcurrentHashMap
30
+ import kotlinx.coroutines.CompletableDeferred
31
+ import kotlinx.coroutines.flow.Flow
32
+ import kotlinx.coroutines.flow.collect
33
+
34
+ internal interface BotaDeviceSDKAndroidRecordingClient {
35
+ suspend fun listRecordings(device: ConnectedDevice): List<DeviceRecording>
36
+
37
+ fun syncRecording(
38
+ device: ConnectedDevice,
39
+ recording: DeviceRecording,
40
+ sinkId: String,
41
+ ): Flow<RecordingSyncEvent>
42
+
43
+ fun transferMetadata(sinkId: String): RecordingTransferMetadata?
44
+
45
+ suspend fun syncEncryptedRecordingV2(
46
+ device: ConnectedDevice,
47
+ recording: EncryptedUploadV2Recording,
48
+ provider: EncryptedUploadV2ProfileProvider,
49
+ )
50
+
51
+ suspend fun confirmRecording(device: ConnectedDevice, recordingUuid: String)
52
+
53
+ fun observeUploadOwnership(
54
+ device: ConnectedDevice,
55
+ recordingUuid: String,
56
+ uploadId: String,
57
+ destinationId: String,
58
+ ): Flow<UploadOwnershipEvent>
59
+
60
+ fun streamRecording(
61
+ device: ConnectedDevice,
62
+ recordingUuid: String,
63
+ sinkId: String,
64
+ chunkSizeBytes: Int,
65
+ flushIntervalMilliseconds: ULong,
66
+ destinationProvider: StreamingChunkDestinationProvider,
67
+ finalize: StreamingFinalizeHandler,
68
+ ): Flow<StreamingRecordingEvent>
69
+
70
+ suspend fun cancelCurrentOperation()
71
+ }
72
+
73
+ internal fun BotaDeviceSDKAndroidRecordings.BotaRecordingFile.toWritableMap(): WritableMap =
74
+ Arguments.createMap().apply {
75
+ putString("localPath", localPath)
76
+ putBoolean("e2eEncrypted", isE2EEncrypted)
77
+ contentSha256Hex?.let { putString("contentSha256", it) }
78
+ }
79
+
80
+ internal data class BotaStreamingProgress(
81
+ val sessionId: String,
82
+ val state: String,
83
+ val bytesReceived: ULong,
84
+ val chunksUploaded: UInt,
85
+ )
86
+
87
+ internal data class BotaEncryptedUploadV2Recording(
88
+ val uuid: String,
89
+ val generation: UInt,
90
+ val ciphertextLength: String,
91
+ val ciphertextSha256: String,
92
+ )
93
+
94
+ internal data class BotaEncryptedUploadV2Capability(
95
+ val encodingVersion: Int,
96
+ val transferProfileVersion: Int,
97
+ val rawValueHex: String,
98
+ val sha256Hex: String,
99
+ val flags: UInt,
100
+ val maximumSignedBlobBytes: UShort,
101
+ val maximumManifestBytes: UShort,
102
+ val maximumDataPayloadBytes: UShort,
103
+ val maximumWindowPackets: UShort,
104
+ val durableCheckpointIntervalBlocks: UInt,
105
+ val maximumMissingSequences: UShort,
106
+ )
107
+
108
+ internal data class BotaEncryptedUploadV2Checkpoint(
109
+ val version: Int,
110
+ val uploadSessionId: String,
111
+ val ownerRevision: UInt,
112
+ val revision: UInt,
113
+ val nextCiphertextOffset: String,
114
+ val prefixSha256: String,
115
+ val highestContiguousSequence: UInt?,
116
+ val transportSessionId: String,
117
+ val sinkRegistrationId: String,
118
+ val windowPackets: UShort,
119
+ val dataPayloadBytes: UShort,
120
+ )
121
+
122
+ internal data class BotaEncryptedUploadV2ProfileRequest(
123
+ val requestId: String,
124
+ val operationId: String,
125
+ val recording: BotaEncryptedUploadV2Recording,
126
+ val capability: BotaEncryptedUploadV2Capability,
127
+ val checkpoint: BotaEncryptedUploadV2Checkpoint?,
128
+ )
129
+
130
+ internal data class BotaEncryptedUploadV2Progress(
131
+ val operationId: String,
132
+ val recordingUuid: String,
133
+ val phase: String,
134
+ val completedBytes: String,
135
+ val totalBytes: String,
136
+ val checkpointRevision: UInt? = null,
137
+ val errorCode: String? = null,
138
+ val retryable: Boolean? = null,
139
+ val protocolStatus: UShort? = null,
140
+ )
141
+
142
+ internal data class BotaStreamingDestinationRequest(
143
+ val requestId: String,
144
+ val sessionId: String,
145
+ val sequence: UInt,
146
+ val encrypted: Boolean,
147
+ )
148
+
149
+ internal data class BotaStreamingFinalizeRequest(
150
+ val requestId: String,
151
+ val sessionId: String,
152
+ val totalChunks: UInt,
153
+ val durationMilliseconds: ULong,
154
+ val fileSizeBytes: ULong,
155
+ val encrypted: Boolean,
156
+ )
157
+
158
+ internal fun BotaStreamingProgress.toWritableMap(): WritableMap = Arguments.createMap().apply {
159
+ putString("sessionId", sessionId)
160
+ putString("state", state)
161
+ putDouble("bytesReceived", bytesReceived.toDouble())
162
+ putDouble("chunksUploaded", chunksUploaded.toDouble())
163
+ }
164
+
165
+ internal fun BotaStreamingDestinationRequest.toWritableMap(): WritableMap = Arguments.createMap().apply {
166
+ putString("requestId", requestId)
167
+ putString("sessionId", sessionId)
168
+ putDouble("sequence", sequence.toDouble())
169
+ putBoolean("encrypted", encrypted)
170
+ }
171
+
172
+ internal fun BotaStreamingFinalizeRequest.toWritableMap(): WritableMap = Arguments.createMap().apply {
173
+ putString("requestId", requestId)
174
+ putString("sessionId", sessionId)
175
+ putDouble("totalChunks", totalChunks.toDouble())
176
+ putDouble("durationMs", durationMilliseconds.toDouble())
177
+ putDouble("fileSizeBytes", fileSizeBytes.toDouble())
178
+ putBoolean("encrypted", encrypted)
179
+ }
180
+
181
+ internal fun BotaEncryptedUploadV2ProfileRequest.toWritableMap(): WritableMap =
182
+ Arguments.createMap().apply {
183
+ putString("requestId", requestId)
184
+ putString("operationId", operationId)
185
+ putMap("recording", recording.toWritableMap())
186
+ putMap("capability", capability.toWritableMap())
187
+ checkpoint?.let { putMap("checkpoint", it.toWritableMap()) }
188
+ }
189
+
190
+ internal fun BotaEncryptedUploadV2Progress.toWritableMap(): WritableMap = Arguments.createMap().apply {
191
+ putString("operationId", operationId)
192
+ putString("recordingUuid", recordingUuid)
193
+ putString("phase", phase)
194
+ putString("completedBytes", completedBytes)
195
+ putString("totalBytes", totalBytes)
196
+ checkpointRevision?.let { putDouble("checkpointRevision", it.toDouble()) }
197
+ errorCode?.let { putString("errorCode", it) }
198
+ retryable?.let { putBoolean("retryable", it) }
199
+ protocolStatus?.let { putDouble("protocolStatus", it.toDouble()) }
200
+ }
201
+
202
+ private fun BotaEncryptedUploadV2Recording.toWritableMap(): WritableMap = Arguments.createMap().apply {
203
+ putString("uuid", uuid)
204
+ putDouble("generation", generation.toDouble())
205
+ putString("ciphertextLength", ciphertextLength)
206
+ putString("ciphertextSha256", ciphertextSha256)
207
+ }
208
+
209
+ private fun BotaEncryptedUploadV2Capability.toWritableMap(): WritableMap = Arguments.createMap().apply {
210
+ putDouble("encodingVersion", encodingVersion.toDouble())
211
+ putDouble("transferProfileVersion", transferProfileVersion.toDouble())
212
+ putString("rawValueHex", rawValueHex)
213
+ putString("sha256Hex", sha256Hex)
214
+ putDouble("flags", flags.toDouble())
215
+ putDouble("maximumSignedBlobBytes", maximumSignedBlobBytes.toDouble())
216
+ putDouble("maximumManifestBytes", maximumManifestBytes.toDouble())
217
+ putDouble("maximumDataPayloadBytes", maximumDataPayloadBytes.toDouble())
218
+ putDouble("maximumWindowPackets", maximumWindowPackets.toDouble())
219
+ putDouble("durableCheckpointIntervalBlocks", durableCheckpointIntervalBlocks.toDouble())
220
+ putDouble("maximumMissingSequences", maximumMissingSequences.toDouble())
221
+ }
222
+
223
+ private fun BotaEncryptedUploadV2Checkpoint.toWritableMap(): WritableMap = Arguments.createMap().apply {
224
+ putDouble("version", version.toDouble())
225
+ putString("uploadSessionId", uploadSessionId)
226
+ putDouble("ownerRevision", ownerRevision.toDouble())
227
+ putDouble("revision", revision.toDouble())
228
+ putString("nextCiphertextOffset", nextCiphertextOffset)
229
+ putString("prefixSha256", prefixSha256)
230
+ highestContiguousSequence?.let { putDouble("highestContiguousSequence", it.toDouble()) }
231
+ putString("transportSessionId", transportSessionId)
232
+ putString("sinkRegistrationId", sinkRegistrationId)
233
+ putDouble("windowPackets", windowPackets.toDouble())
234
+ putDouble("dataPayloadBytes", dataPayloadBytes.toDouble())
235
+ }
236
+
237
+ internal class BotaDeviceSDKSharedAndroidRecordingClient(
238
+ private val client: BotaDeviceClient = BotaDeviceClient.shared,
239
+ ) : BotaDeviceSDKAndroidRecordingClient {
240
+ override suspend fun listRecordings(device: ConnectedDevice): List<DeviceRecording> =
241
+ client.recordings.listRecordings(device)
242
+
243
+ override fun syncRecording(
244
+ device: ConnectedDevice,
245
+ recording: DeviceRecording,
246
+ sinkId: String,
247
+ ): Flow<RecordingSyncEvent> = client.recordings.syncRecording(
248
+ device,
249
+ recording,
250
+ sinkId,
251
+ confirmOnCompletion = false,
252
+ )
253
+
254
+ override fun transferMetadata(sinkId: String): RecordingTransferMetadata? =
255
+ client.recordings.transferMetadata(sinkId)
256
+
257
+ override suspend fun syncEncryptedRecordingV2(
258
+ device: ConnectedDevice,
259
+ recording: EncryptedUploadV2Recording,
260
+ provider: EncryptedUploadV2ProfileProvider,
261
+ ) {
262
+ client.recordings.syncEncryptedRecordingV2(device, recording, provider)
263
+ }
264
+
265
+ override suspend fun confirmRecording(device: ConnectedDevice, recordingUuid: String) {
266
+ client.recordings.confirmRecording(device, recordingUuid)
267
+ }
268
+
269
+ override fun observeUploadOwnership(
270
+ device: ConnectedDevice,
271
+ recordingUuid: String,
272
+ uploadId: String,
273
+ destinationId: String,
274
+ ): Flow<UploadOwnershipEvent> = client.recordings.observeUploadOwnership(
275
+ device,
276
+ recordingUuid,
277
+ uploadId,
278
+ destinationId,
279
+ )
280
+
281
+ override fun streamRecording(
282
+ device: ConnectedDevice,
283
+ recordingUuid: String,
284
+ sinkId: String,
285
+ chunkSizeBytes: Int,
286
+ flushIntervalMilliseconds: ULong,
287
+ destinationProvider: StreamingChunkDestinationProvider,
288
+ finalize: StreamingFinalizeHandler,
289
+ ): Flow<StreamingRecordingEvent> = client.recordings.streamRecording(
290
+ device,
291
+ recordingUuid,
292
+ sinkId,
293
+ chunkSizeBytes,
294
+ flushIntervalMilliseconds,
295
+ destinationProvider,
296
+ finalize,
297
+ )
298
+
299
+ override suspend fun cancelCurrentOperation() {
300
+ client.recordings.cancelCurrentOperation()
301
+ }
302
+ }
303
+
304
+ internal class BotaDeviceSDKAndroidRecordings(
305
+ private val client: BotaDeviceSDKAndroidRecordingClient =
306
+ BotaDeviceSDKSharedAndroidRecordingClient(),
307
+ ) {
308
+ private val destinationRequests = ConcurrentHashMap<
309
+ String,
310
+ CompletableDeferred<StreamingUploadDestination>
311
+ >()
312
+ private data class EncryptedUploadV2Request(
313
+ val recording: EncryptedUploadV2Recording,
314
+ val material: CompletableDeferred<EncryptedUploadV2Material>,
315
+ )
316
+ private val encryptedUploadV2Requests =
317
+ ConcurrentHashMap<String, EncryptedUploadV2Request>()
318
+ private val finalizeRequests = ConcurrentHashMap<String, CompletableDeferred<Unit>>()
319
+ @Volatile private var activeStreamingSessionId: String? = null
320
+
321
+ internal data class BotaRecordingFile(
322
+ val localPath: String,
323
+ val isE2EEncrypted: Boolean,
324
+ val contentSha256Hex: String?,
325
+ )
326
+
327
+ suspend fun listRecordings(device: ConnectedDevice): List<DeviceRecording> =
328
+ client.listRecordings(device)
329
+
330
+ suspend fun syncRecording(
331
+ device: ConnectedDevice,
332
+ recording: DeviceRecording,
333
+ sinkId: String,
334
+ onProgress: (RecordingTransferProgress) -> Unit,
335
+ ): BotaRecordingFile {
336
+ var path: String? = null
337
+ client.syncRecording(device, recording, sinkId).collect { event ->
338
+ when (event) {
339
+ is RecordingSyncEvent.Progress -> onProgress(event.progress)
340
+ is RecordingSyncEvent.Completed -> path = event.path.toString()
341
+ }
342
+ }
343
+ val localPath = path ?: error("recording transfer completed without a native file")
344
+ val metadata = client.transferMetadata(sinkId)
345
+ return BotaRecordingFile(
346
+ localPath,
347
+ metadata?.isE2EEncrypted ?: false,
348
+ metadata?.contentSha256Hex,
349
+ )
350
+ }
351
+
352
+ suspend fun syncEncryptedRecordingV2(
353
+ device: ConnectedDevice,
354
+ recording: EncryptedUploadV2Recording,
355
+ operationId: String,
356
+ onProfileRequest: (BotaEncryptedUploadV2ProfileRequest) -> Unit,
357
+ onProgress: (BotaEncryptedUploadV2Progress) -> Unit,
358
+ ) {
359
+ try {
360
+ client.syncEncryptedRecordingV2(
361
+ device,
362
+ recording,
363
+ EncryptedUploadV2ProfileProvider { context ->
364
+ val completedBytes = context.checkpoint?.nextCiphertextOffset?.toString() ?: "0"
365
+ val checkpointRevision = context.checkpoint?.revision
366
+ onProgress(BotaEncryptedUploadV2Progress(
367
+ operationId,
368
+ recording.uuid,
369
+ "profile_requested",
370
+ completedBytes,
371
+ recording.ciphertextLength.toString(),
372
+ checkpointRevision,
373
+ ))
374
+ val material = requestEncryptedUploadV2Profile(
375
+ operationId,
376
+ context,
377
+ onProfileRequest,
378
+ )
379
+ onProgress(BotaEncryptedUploadV2Progress(
380
+ operationId,
381
+ recording.uuid,
382
+ "transferring",
383
+ completedBytes,
384
+ recording.ciphertextLength.toString(),
385
+ checkpointRevision,
386
+ ))
387
+ material
388
+ },
389
+ )
390
+ onProgress(BotaEncryptedUploadV2Progress(
391
+ operationId,
392
+ recording.uuid,
393
+ "completed",
394
+ recording.ciphertextLength.toString(),
395
+ recording.ciphertextLength.toString(),
396
+ ))
397
+ } catch (error: Throwable) {
398
+ val failure = error.encryptedUploadV2Failure()
399
+ onProgress(BotaEncryptedUploadV2Progress(
400
+ operationId,
401
+ recording.uuid,
402
+ "failed",
403
+ "0",
404
+ recording.ciphertextLength.toString(),
405
+ errorCode = failure.code,
406
+ retryable = failure.retryable,
407
+ protocolStatus = failure.protocolStatus,
408
+ ))
409
+ throw error
410
+ }
411
+ }
412
+
413
+ fun resolveEncryptedUploadV2Profile(
414
+ requestId: String,
415
+ profile: String,
416
+ uploadSessionId: String,
417
+ ownerRevision: UInt,
418
+ securityPolicy: String,
419
+ materialRegistrationId: String,
420
+ ) {
421
+ val request = encryptedUploadV2Requests.remove(requestId)
422
+ ?: error("encrypted upload v2 profile request is no longer pending")
423
+ runCatching {
424
+ require(profile == "encrypted_upload_v2") {
425
+ "encrypted upload v2 requires an explicit matching profile"
426
+ }
427
+ BotaDeviceSDKEncryptedUploadV2Materials.consume(
428
+ materialRegistrationId,
429
+ request.recording,
430
+ UUID.fromString(uploadSessionId),
431
+ ownerRevision,
432
+ securityPolicy,
433
+ )
434
+ }.onSuccess { request.material.complete(it) }
435
+ .onFailure { request.material.completeExceptionally(it) }
436
+ .getOrThrow()
437
+ }
438
+
439
+ fun rejectEncryptedUploadV2Profile(requestId: String, errorCode: String) {
440
+ val request = encryptedUploadV2Requests.remove(requestId)
441
+ ?: error("encrypted upload v2 profile request is no longer pending")
442
+ val error = if (errorCode == "application_material_rejected") {
443
+ IllegalStateException("encrypted upload v2 material was rejected")
444
+ } else {
445
+ IllegalArgumentException("encrypted upload v2 rejection code is unsupported")
446
+ }
447
+ request.material.completeExceptionally(error)
448
+ if (errorCode != "application_material_rejected") throw error
449
+ }
450
+
451
+ suspend fun observeUploadOwnership(
452
+ device: ConnectedDevice,
453
+ recordingUuid: String,
454
+ uploadId: String,
455
+ destinationId: String,
456
+ onProgress: (RecordingTransferProgress) -> Unit,
457
+ ): UploadOwnershipResult {
458
+ var result: UploadOwnershipResult? = null
459
+ client.observeUploadOwnership(device, recordingUuid, uploadId, destinationId).collect { event ->
460
+ when (event) {
461
+ is UploadOwnershipEvent.Progress -> onProgress(event.progress)
462
+ is UploadOwnershipEvent.Result -> result = event.result
463
+ }
464
+ }
465
+ return result ?: error("upload ownership completed without a result")
466
+ }
467
+
468
+ suspend fun confirmRecording(device: ConnectedDevice, recordingUuid: String) {
469
+ client.confirmRecording(device, recordingUuid)
470
+ }
471
+
472
+ suspend fun streamRecording(
473
+ device: ConnectedDevice,
474
+ recordingUuid: String,
475
+ sessionId: String,
476
+ chunkSizeBytes: Int,
477
+ flushIntervalMilliseconds: ULong,
478
+ onProgress: (BotaStreamingProgress) -> Unit,
479
+ onDestinationRequest: (BotaStreamingDestinationRequest) -> Unit,
480
+ onFinalizeRequest: (BotaStreamingFinalizeRequest) -> Unit,
481
+ ): ULong {
482
+ activeStreamingSessionId = sessionId
483
+ var bytesReceived = 0uL
484
+ var chunksUploaded = 0u
485
+ onProgress(streamingProgress(sessionId, "streaming", bytesReceived, chunksUploaded))
486
+ try {
487
+ client.streamRecording(
488
+ device,
489
+ recordingUuid,
490
+ sessionId,
491
+ chunkSizeBytes,
492
+ flushIntervalMilliseconds,
493
+ StreamingChunkDestinationProvider { request ->
494
+ requestDestination(sessionId, request, onDestinationRequest)
495
+ },
496
+ StreamingFinalizeHandler { metadata ->
497
+ requestFinalize(sessionId, metadata, onFinalizeRequest)
498
+ },
499
+ ).collect { event ->
500
+ when (event) {
501
+ is StreamingRecordingEvent.Paused -> {
502
+ bytesReceived = event.completedBytes
503
+ onProgress(streamingProgress(
504
+ sessionId,
505
+ "paused",
506
+ bytesReceived,
507
+ chunksUploaded,
508
+ ))
509
+ }
510
+ StreamingRecordingEvent.Resumed -> onProgress(streamingProgress(
511
+ sessionId,
512
+ "streaming",
513
+ bytesReceived,
514
+ chunksUploaded,
515
+ ))
516
+ is StreamingRecordingEvent.Completed -> {
517
+ bytesReceived = event.totalBytes
518
+ chunksUploaded = event.uploadedChunks
519
+ onProgress(streamingProgress(
520
+ sessionId,
521
+ "completing",
522
+ bytesReceived,
523
+ chunksUploaded,
524
+ ))
525
+ }
526
+ }
527
+ }
528
+ return bytesReceived
529
+ } finally {
530
+ if (activeStreamingSessionId == sessionId) activeStreamingSessionId = null
531
+ }
532
+ }
533
+
534
+ fun resolveStreamingDestination(
535
+ requestId: String,
536
+ url: String,
537
+ method: String,
538
+ contentType: String,
539
+ bearerToken: String?,
540
+ ) {
541
+ val destination = StreamingUploadDestination(
542
+ url,
543
+ StreamingUploadMethod.entries.firstOrNull { it.wireValue == method }
544
+ ?: error("unsupported streaming upload method: $method"),
545
+ contentType,
546
+ bearerToken,
547
+ )
548
+ destinationRequests.remove(requestId)?.complete(destination)
549
+ }
550
+
551
+ fun rejectStreamingDestination(requestId: String, message: String) {
552
+ destinationRequests.remove(requestId)?.completeExceptionally(IllegalStateException(message))
553
+ }
554
+
555
+ fun resolveStreamingFinalize(requestId: String) {
556
+ finalizeRequests.remove(requestId)?.complete(Unit)
557
+ }
558
+
559
+ fun rejectStreamingFinalize(requestId: String, message: String) {
560
+ finalizeRequests.remove(requestId)?.completeExceptionally(IllegalStateException(message))
561
+ }
562
+
563
+ suspend fun abortStreaming(sessionId: String) {
564
+ if (activeStreamingSessionId != sessionId) return
565
+ rejectPendingRequests("streaming session was aborted")
566
+ runCatching { client.cancelCurrentOperation() }
567
+ }
568
+
569
+ suspend fun cancelAll() {
570
+ rejectPendingRequests("recording operations were cancelled")
571
+ runCatching { client.cancelCurrentOperation() }
572
+ }
573
+
574
+ private suspend fun requestDestination(
575
+ sessionId: String,
576
+ request: StreamingChunkRequest,
577
+ onRequest: (BotaStreamingDestinationRequest) -> Unit,
578
+ ): StreamingUploadDestination {
579
+ val requestId = UUID.randomUUID().toString()
580
+ val deferred = CompletableDeferred<StreamingUploadDestination>()
581
+ destinationRequests[requestId] = deferred
582
+ onRequest(BotaStreamingDestinationRequest(
583
+ requestId,
584
+ sessionId,
585
+ request.sequence,
586
+ request.isEncrypted,
587
+ ))
588
+ return try {
589
+ deferred.await()
590
+ } finally {
591
+ destinationRequests.remove(requestId, deferred)
592
+ }
593
+ }
594
+
595
+ private suspend fun requestEncryptedUploadV2Profile(
596
+ operationId: String,
597
+ context: EncryptedUploadV2ProviderContext,
598
+ onRequest: (BotaEncryptedUploadV2ProfileRequest) -> Unit,
599
+ ): EncryptedUploadV2Material {
600
+ val requestId = UUID.randomUUID().toString()
601
+ val deferred = CompletableDeferred<EncryptedUploadV2Material>()
602
+ val request = EncryptedUploadV2Request(
603
+ context.recording,
604
+ deferred,
605
+ )
606
+ encryptedUploadV2Requests[requestId] = request
607
+ onRequest(BotaEncryptedUploadV2ProfileRequest(
608
+ requestId,
609
+ operationId,
610
+ context.recording.toBridgeValue(),
611
+ context.capability.toBridgeValue(),
612
+ context.checkpoint?.toBridgeValue(),
613
+ ))
614
+ return try {
615
+ deferred.await()
616
+ } finally {
617
+ encryptedUploadV2Requests.remove(requestId, request)
618
+ }
619
+ }
620
+
621
+ private suspend fun requestFinalize(
622
+ sessionId: String,
623
+ metadata: StreamingFinalizeMetadata,
624
+ onRequest: (BotaStreamingFinalizeRequest) -> Unit,
625
+ ) {
626
+ val requestId = UUID.randomUUID().toString()
627
+ val deferred = CompletableDeferred<Unit>()
628
+ finalizeRequests[requestId] = deferred
629
+ onRequest(BotaStreamingFinalizeRequest(
630
+ requestId,
631
+ sessionId,
632
+ metadata.totalChunks,
633
+ metadata.durationMilliseconds,
634
+ metadata.fileSizeBytes,
635
+ metadata.isEncrypted,
636
+ ))
637
+ try {
638
+ deferred.await()
639
+ } finally {
640
+ finalizeRequests.remove(requestId, deferred)
641
+ }
642
+ }
643
+
644
+ private fun rejectPendingRequests(message: String) {
645
+ val error = IllegalStateException(message)
646
+ encryptedUploadV2Requests.values.forEach { it.material.completeExceptionally(error) }
647
+ destinationRequests.values.forEach { it.completeExceptionally(error) }
648
+ finalizeRequests.values.forEach { it.completeExceptionally(error) }
649
+ destinationRequests.clear()
650
+ finalizeRequests.clear()
651
+ encryptedUploadV2Requests.clear()
652
+ }
653
+ }
654
+
655
+ private fun EncryptedUploadV2Recording.toBridgeValue(): BotaEncryptedUploadV2Recording =
656
+ BotaEncryptedUploadV2Recording(
657
+ uuid,
658
+ generation,
659
+ ciphertextLength.toString(),
660
+ ciphertextSha256.toHex(),
661
+ )
662
+
663
+ private fun EncryptedUploadV2CapabilitySnapshot.toBridgeValue(): BotaEncryptedUploadV2Capability {
664
+ val values = capabilities
665
+ return BotaEncryptedUploadV2Capability(
666
+ rawValue.getOrElse(0) { 0 }.toUByte().toInt(),
667
+ rawValue.getOrElse(1) { 0 }.toUByte().toInt(),
668
+ rawValue.toHex(),
669
+ sha256.toHex(),
670
+ values.flags,
671
+ values.maximumSignedBlobBytes,
672
+ values.maximumManifestBytes,
673
+ values.maximumDataPayloadBytes,
674
+ values.maximumWindowPackets,
675
+ values.durableCheckpointIntervalBlocks,
676
+ values.maximumMissingSequences,
677
+ )
678
+ }
679
+
680
+ private fun EncryptedUploadV2Checkpoint.toBridgeValue(): BotaEncryptedUploadV2Checkpoint =
681
+ BotaEncryptedUploadV2Checkpoint(
682
+ 1,
683
+ uploadSessionId.toString(),
684
+ ownerRevision,
685
+ revision,
686
+ nextCiphertextOffset.toString(),
687
+ prefixSha256.toHex(),
688
+ highestContiguousSequence,
689
+ transportSessionId.toString(),
690
+ sinkId,
691
+ windowPackets,
692
+ dataPayloadBytes,
693
+ )
694
+
695
+ private data class EncryptedUploadV2Failure(
696
+ val code: String,
697
+ val retryable: Boolean,
698
+ val protocolStatus: UShort?,
699
+ )
700
+
701
+ private fun Throwable.encryptedUploadV2Failure(): EncryptedUploadV2Failure = when (this) {
702
+ is BotaSDKError.Core -> EncryptedUploadV2Failure(
703
+ code.stableName(),
704
+ retryable,
705
+ protocolStatus,
706
+ )
707
+ is BotaSDKError.AuthorizationRequired ->
708
+ EncryptedUploadV2Failure("authorization_required", false, null)
709
+ is kotlinx.coroutines.CancellationException ->
710
+ EncryptedUploadV2Failure("cancelled", false, null)
711
+ else -> EncryptedUploadV2Failure("application_material_rejected", false, null)
712
+ }
713
+
714
+ private fun BotaErrorCode.stableName(): String = when (this) {
715
+ BotaErrorCode.InvalidInput -> "invalid_input"
716
+ BotaErrorCode.TruncatedPacket -> "truncated_packet"
717
+ BotaErrorCode.UnknownPacket -> "unknown_packet"
718
+ BotaErrorCode.PayloadTooLarge -> "payload_too_large"
719
+ BotaErrorCode.UnsupportedCapability -> "unsupported_capability"
720
+ BotaErrorCode.UnsupportedOperation -> "unsupported_operation"
721
+ BotaErrorCode.FeatureUnavailable -> "feature_unavailable"
722
+ BotaErrorCode.OperationInProgress -> "operation_in_progress"
723
+ BotaErrorCode.UnexpectedEvent -> "unexpected_event"
724
+ BotaErrorCode.DeviceNotFound -> "device_not_found"
725
+ BotaErrorCode.IdentityMismatch -> "identity_mismatch"
726
+ BotaErrorCode.ConnectionFailed -> "connection_failed"
727
+ BotaErrorCode.PersistenceFailed -> "persistence_failed"
728
+ BotaErrorCode.NotConnected -> "not_connected"
729
+ BotaErrorCode.Timeout -> "timeout"
730
+ BotaErrorCode.Cancelled -> "cancelled"
731
+ BotaErrorCode.ProtocolRejected -> "protocol_rejected"
732
+ BotaErrorCode.IntegrityFailed -> "integrity_failed"
733
+ BotaErrorCode.UploadOwnershipUnknown -> "upload_ownership_unknown"
734
+ BotaErrorCode.DownloadFailed -> "download_failed"
735
+ BotaErrorCode.Internal -> "internal"
736
+ is BotaErrorCode.Unknown -> "unknown"
737
+ }
738
+
739
+ private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it.toInt() and 0xff) }
740
+
741
+ private fun streamingProgress(
742
+ sessionId: String,
743
+ state: String,
744
+ bytesReceived: ULong,
745
+ chunksUploaded: UInt,
746
+ ): BotaStreamingProgress = BotaStreamingProgress(
747
+ sessionId,
748
+ state,
749
+ bytesReceived,
750
+ chunksUploaded,
751
+ )