@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,355 @@
1
+ package dev.bota.sdk.reactnative
2
+
3
+ import dev.bota.sdk.EncryptedUploadV2Capabilities
4
+ import dev.bota.sdk.EncryptedUploadV2CapabilitySnapshot
5
+ import dev.bota.sdk.EncryptedUploadV2Checkpoint
6
+ import dev.bota.sdk.EncryptedUploadV2Material
7
+ import dev.bota.sdk.EncryptedUploadV2ProfileProvider
8
+ import dev.bota.sdk.EncryptedUploadV2ProviderContext
9
+ import dev.bota.sdk.EncryptedUploadV2Recording
10
+ import dev.bota.sdk.EncryptedUploadV2SecurityPolicy
11
+ import dev.bota.sdk.BotaErrorCode
12
+ import dev.bota.sdk.BotaOperation
13
+ import dev.bota.sdk.BotaSDKError
14
+ import dev.bota.sdk.RecordingSyncEvent
15
+ import dev.bota.sdk.UploadOwnershipEvent
16
+ import dev.bota.sdk.UploadOwnershipResult
17
+ import dev.bota.sdk.model.AudioCodec
18
+ import dev.bota.sdk.model.ConnectedDevice
19
+ import dev.bota.sdk.model.ConnectionState
20
+ import dev.bota.sdk.model.DeviceRecording
21
+ import dev.bota.sdk.model.DeviceType
22
+ import dev.bota.sdk.model.RecordingTransferProgress
23
+ import dev.bota.sdk.model.StreamingChunkDestinationProvider
24
+ import dev.bota.sdk.model.StreamingChunkRequest
25
+ import dev.bota.sdk.model.StreamingFinalizeHandler
26
+ import dev.bota.sdk.model.StreamingFinalizeMetadata
27
+ import dev.bota.sdk.model.StreamingRecordingEvent
28
+ import dev.bota.sdk.model.WireValue
29
+ import java.nio.file.Path
30
+ import java.time.Instant
31
+ import java.util.UUID
32
+ import kotlinx.coroutines.flow.Flow
33
+ import kotlinx.coroutines.flow.flowOf
34
+ import kotlinx.coroutines.flow.flow
35
+ import kotlinx.coroutines.test.runTest
36
+ import okhttp3.Request
37
+ import org.junit.Assert.assertEquals
38
+ import org.junit.Assert.assertNull
39
+ import org.junit.Assert.assertTrue
40
+ import org.junit.Test
41
+
42
+ class BotaDeviceSDKAndroidRecordingsTest {
43
+ @Test
44
+ fun encryptedUploadV2UsesOneShotNativeMaterialAndSafeBridgeValues() = runTest {
45
+ val recordingUuid = "00112233-4455-6677-8899-aabbccddeeff"
46
+ val uploadSessionId = UUID.fromString("10213243-5465-7687-98a9-bacbdcedfe0f")
47
+ val recording = EncryptedUploadV2Recording(
48
+ recordingUuid,
49
+ 4u,
50
+ 4_096u,
51
+ ByteArray(32) { 0x5a },
52
+ )
53
+ val context = EncryptedUploadV2ProviderContext(
54
+ recording,
55
+ EncryptedUploadV2CapabilitySnapshot(
56
+ byteArrayOf(
57
+ 0x01, 0x02, 0x18, 0x00, 0x7f, 0x00, 0x00, 0x00,
58
+ 0x00, 0x04, 0x00, 0x04, 0x00, 0x10, 0x00, 0x08,
59
+ 0x00, 0x01, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00,
60
+ ),
61
+ ByteArray(32) { 0x6b },
62
+ EncryptedUploadV2Capabilities(0x7fu, 1_024u, 1_024u, 4_096u, 8u, 256u, 16u),
63
+ ),
64
+ EncryptedUploadV2Checkpoint(
65
+ uploadSessionId,
66
+ 2u,
67
+ 3u,
68
+ 2_048u,
69
+ ByteArray(32) { 0x7c },
70
+ 7u,
71
+ 72_623_859_790_382_856u,
72
+ "20314253-6475-8697-a8b9-cadbecfd0e1f",
73
+ 8u,
74
+ 512u,
75
+ ),
76
+ )
77
+ val material = EncryptedUploadV2Material(
78
+ "native-material-1",
79
+ recordingUuid,
80
+ uploadSessionId,
81
+ 2u,
82
+ EncryptedUploadV2SecurityPolicy.V2Required,
83
+ ByteArray(32) { 0xa5.toByte() },
84
+ { Request.Builder().url("https://native.invalid/stage").build() },
85
+ { _, _ -> },
86
+ { _ -> },
87
+ { ByteArray(16) { 0x3c } },
88
+ )
89
+ val registrationId = BotaDeviceSDKEncryptedUploadV2Materials.register(material)
90
+ val client = TestAndroidRecordingClient(recording(), context)
91
+ val recordings = BotaDeviceSDKAndroidRecordings(client)
92
+ val requests = mutableListOf<BotaEncryptedUploadV2ProfileRequest>()
93
+ val progress = mutableListOf<BotaEncryptedUploadV2Progress>()
94
+
95
+ recordings.syncEncryptedRecordingV2(
96
+ connectedDevice(),
97
+ recording,
98
+ "operation-1",
99
+ onProfileRequest = { request ->
100
+ requests += request
101
+ recordings.resolveEncryptedUploadV2Profile(
102
+ request.requestId,
103
+ "encrypted_upload_v2",
104
+ uploadSessionId.toString(),
105
+ 2u,
106
+ "v2_required",
107
+ registrationId,
108
+ )
109
+ },
110
+ onProgress = progress::add,
111
+ )
112
+
113
+ assertEquals(1, requests.size)
114
+ assertEquals(recordingUuid, requests.single().recording.uuid)
115
+ assertEquals("2048", requests.single().checkpoint?.nextCiphertextOffset)
116
+ assertEquals("native-material-1", client.encryptedMaterialId)
117
+ assertEquals(
118
+ listOf("profile_requested", "transferring", "completed"),
119
+ progress.map(BotaEncryptedUploadV2Progress::phase),
120
+ )
121
+ assertNull(BotaDeviceSDKEncryptedUploadV2Materials.remove(registrationId))
122
+ }
123
+
124
+ @Test
125
+ fun encryptedUploadV2MapsStableNativeFailureProgress() = runTest {
126
+ val failure = BotaSDKError.Core(
127
+ BotaErrorCode.ProtocolRejected,
128
+ BotaOperation.TransferRecording,
129
+ true,
130
+ 0x22u,
131
+ "device rejected the selected policy",
132
+ )
133
+ val client = TestAndroidRecordingClient(recording(), encryptedUploadV2Error = failure)
134
+ val recordings = BotaDeviceSDKAndroidRecordings(client)
135
+ val progress = mutableListOf<BotaEncryptedUploadV2Progress>()
136
+
137
+ val actual = runCatching {
138
+ recordings.syncEncryptedRecordingV2(
139
+ connectedDevice(),
140
+ EncryptedUploadV2Recording(
141
+ "00112233-4455-6677-8899-aabbccddeeff",
142
+ 4u,
143
+ 4_096u,
144
+ ByteArray(32) { 0x5a },
145
+ ),
146
+ "operation-1",
147
+ onProfileRequest = {},
148
+ onProgress = progress::add,
149
+ )
150
+ }.exceptionOrNull()
151
+
152
+ assertEquals(failure, actual)
153
+ assertEquals("failed", progress.last().phase)
154
+ assertEquals("protocol_rejected", progress.last().errorCode)
155
+ assertEquals(true, progress.last().retryable)
156
+ assertEquals(0x22u.toUShort(), progress.last().protocolStatus)
157
+ }
158
+
159
+ @Test
160
+ fun recordingListAndSyncKeepTransferBytesInNativeFile() = runTest {
161
+ val connected = ConnectedDevice(
162
+ id = "selected",
163
+ serialNumber = "EVFXXW67KP",
164
+ deviceType = DeviceType.BotaPin,
165
+ firmwareVersion = "1.0.11",
166
+ isProvisioned = true,
167
+ connectionState = ConnectionState.Connected,
168
+ mtu = 247,
169
+ )
170
+ val recording = DeviceRecording(
171
+ uuid = "recording-1",
172
+ startedAt = Instant.ofEpochMilli(1_788_200_000_000),
173
+ durationMs = 12_000u,
174
+ fileSizeBytes = 48_000u,
175
+ codec = WireValue.Known(AudioCodec.Opus16k),
176
+ isEncrypted = true,
177
+ )
178
+ val client = TestAndroidRecordingClient(recording)
179
+ val recordings = BotaDeviceSDKAndroidRecordings(client)
180
+ val progress = mutableListOf<RecordingTransferProgress>()
181
+
182
+ assertEquals(listOf(recording), recordings.listRecordings(connected))
183
+ assertEquals(
184
+ BotaDeviceSDKAndroidRecordings.BotaRecordingFile(
185
+ "/tmp/bota-recordings/recording-1.ogg",
186
+ true,
187
+ "5a".repeat(32),
188
+ ),
189
+ recordings.syncRecording(connected, recording, "sink-1", progress::add),
190
+ )
191
+ assertEquals(listOf("sink-1"), client.sinkIds)
192
+ assertEquals(
193
+ listOf(RecordingTransferProgress(24_000u, 48_000u)),
194
+ progress,
195
+ )
196
+ recordings.cancelAll()
197
+ assertTrue(client.cancelled)
198
+ }
199
+
200
+ @Test
201
+ fun uploadOwnershipReturnsNativeFallbackDecisionAndProgress() = runTest {
202
+ val connected = connectedDevice()
203
+ val client = TestAndroidRecordingClient(recording())
204
+ val recordings = BotaDeviceSDKAndroidRecordings(client)
205
+ val progress = mutableListOf<RecordingTransferProgress>()
206
+
207
+ assertEquals(
208
+ UploadOwnershipResult.BluetoothFallback(
209
+ "recording-1",
210
+ "upload-1",
211
+ "destination-1",
212
+ ),
213
+ recordings.observeUploadOwnership(
214
+ connected,
215
+ "recording-1",
216
+ "upload-1",
217
+ "destination-1",
218
+ progress::add,
219
+ ),
220
+ )
221
+ assertEquals(
222
+ listOf(RecordingTransferProgress(32_000u, 48_000u)),
223
+ progress,
224
+ )
225
+ }
226
+
227
+ @Test
228
+ fun streamingResolvesOneShotRequestsAndMapsProgress() = runTest {
229
+ val client = TestAndroidRecordingClient(recording())
230
+ val recordings = BotaDeviceSDKAndroidRecordings(client)
231
+ val states = mutableListOf<String>()
232
+ var sequence: Double? = null
233
+ var finalizedChunks: Double? = null
234
+
235
+ val total = recordings.streamRecording(
236
+ device = connectedDevice(),
237
+ recordingUuid = "recording-1",
238
+ sessionId = UUID.randomUUID().toString(),
239
+ chunkSizeBytes = 64 * 1_024,
240
+ flushIntervalMilliseconds = 1_000u,
241
+ onProgress = { states += it.state },
242
+ onDestinationRequest = {
243
+ sequence = it.sequence.toDouble()
244
+ recordings.resolveStreamingDestination(
245
+ it.requestId,
246
+ "https://example.test/chunk/1",
247
+ "PUT",
248
+ "audio/ogg",
249
+ null,
250
+ )
251
+ },
252
+ onFinalizeRequest = {
253
+ finalizedChunks = it.totalChunks.toDouble()
254
+ recordings.resolveStreamingFinalize(it.requestId)
255
+ },
256
+ )
257
+
258
+ assertEquals(96uL, total)
259
+ assertEquals(1.0, sequence)
260
+ assertEquals(2.0, finalizedChunks)
261
+ assertEquals(listOf("streaming", "paused", "streaming", "completing"), states)
262
+ }
263
+
264
+ private fun connectedDevice(): ConnectedDevice = ConnectedDevice(
265
+ id = "selected",
266
+ serialNumber = "EVFXXW67KP",
267
+ deviceType = DeviceType.BotaPin,
268
+ firmwareVersion = "1.0.11",
269
+ isProvisioned = true,
270
+ connectionState = ConnectionState.Connected,
271
+ mtu = 247,
272
+ )
273
+
274
+ private fun recording(): DeviceRecording = DeviceRecording(
275
+ uuid = "recording-1",
276
+ startedAt = Instant.ofEpochMilli(1_788_200_000_000),
277
+ durationMs = 12_000u,
278
+ fileSizeBytes = 48_000u,
279
+ codec = WireValue.Known(AudioCodec.Opus16k),
280
+ isEncrypted = true,
281
+ )
282
+
283
+ private class TestAndroidRecordingClient(
284
+ private val recording: DeviceRecording,
285
+ private val encryptedUploadV2Context: EncryptedUploadV2ProviderContext? = null,
286
+ private val encryptedUploadV2Error: BotaSDKError.Core? = null,
287
+ ) : BotaDeviceSDKAndroidRecordingClient {
288
+ var cancelled = false
289
+ val sinkIds = mutableListOf<String>()
290
+ var encryptedMaterialId: String? = null
291
+
292
+ override suspend fun listRecordings(device: ConnectedDevice): List<DeviceRecording> =
293
+ listOf(recording)
294
+
295
+ override fun syncRecording(
296
+ device: ConnectedDevice,
297
+ recording: DeviceRecording,
298
+ sinkId: String,
299
+ ): Flow<RecordingSyncEvent> = flowOf(
300
+ RecordingSyncEvent.Progress(RecordingTransferProgress(24_000u, 48_000u)),
301
+ RecordingSyncEvent.Completed(Path.of("/tmp/bota-recordings/recording-1.ogg")),
302
+ ).also { sinkIds += sinkId }
303
+
304
+ override fun transferMetadata(sinkId: String): dev.bota.sdk.RecordingTransferMetadata =
305
+ dev.bota.sdk.RecordingTransferMetadata(true, "5a".repeat(32))
306
+
307
+ override suspend fun syncEncryptedRecordingV2(
308
+ device: ConnectedDevice,
309
+ recording: EncryptedUploadV2Recording,
310
+ provider: EncryptedUploadV2ProfileProvider,
311
+ ) {
312
+ encryptedUploadV2Error?.let { throw it }
313
+ val context = requireNotNull(encryptedUploadV2Context)
314
+ encryptedMaterialId = provider.select(context).materialId
315
+ }
316
+
317
+ override suspend fun confirmRecording(device: ConnectedDevice, recordingUuid: String) = Unit
318
+
319
+ override fun observeUploadOwnership(
320
+ device: ConnectedDevice,
321
+ recordingUuid: String,
322
+ uploadId: String,
323
+ destinationId: String,
324
+ ): Flow<UploadOwnershipEvent> = flowOf(
325
+ UploadOwnershipEvent.Progress(RecordingTransferProgress(32_000u, 48_000u)),
326
+ UploadOwnershipEvent.Result(
327
+ UploadOwnershipResult.BluetoothFallback(
328
+ recordingUuid,
329
+ uploadId,
330
+ destinationId,
331
+ ),
332
+ ),
333
+ )
334
+
335
+ override fun streamRecording(
336
+ device: ConnectedDevice,
337
+ recordingUuid: String,
338
+ sinkId: String,
339
+ chunkSizeBytes: Int,
340
+ flushIntervalMilliseconds: ULong,
341
+ destinationProvider: StreamingChunkDestinationProvider,
342
+ finalize: StreamingFinalizeHandler,
343
+ ): Flow<StreamingRecordingEvent> = flow {
344
+ destinationProvider.destination(StreamingChunkRequest(1u, false))
345
+ finalize.finalize(StreamingFinalizeMetadata(2u, 500u, 96u, false))
346
+ emit(StreamingRecordingEvent.Paused(32u))
347
+ emit(StreamingRecordingEvent.Resumed)
348
+ emit(StreamingRecordingEvent.Completed(96u, 2u, false))
349
+ }
350
+
351
+ override suspend fun cancelCurrentOperation() {
352
+ cancelled = true
353
+ }
354
+ }
355
+ }