@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,33 @@
1
+ package dev.bota.sdk.reactnative
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+
9
+ public class BotaDeviceSDKPackage : BaseReactPackage() {
10
+ override fun getModule(
11
+ name: String,
12
+ reactContext: ReactApplicationContext,
13
+ ): NativeModule? =
14
+ if (name == BotaDeviceSDKModule.NAME) {
15
+ BotaDeviceSDKModule(reactContext)
16
+ } else {
17
+ null
18
+ }
19
+
20
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider =
21
+ ReactModuleInfoProvider {
22
+ mapOf(
23
+ BotaDeviceSDKModule.NAME to ReactModuleInfo(
24
+ name = BotaDeviceSDKModule.NAME,
25
+ className = BotaDeviceSDKModule::class.java.name,
26
+ canOverrideExistingModule = false,
27
+ needsEagerInit = false,
28
+ isCxxModule = false,
29
+ isTurboModule = true,
30
+ ),
31
+ )
32
+ }
33
+ }
@@ -0,0 +1,211 @@
1
+ package dev.bota.sdk.reactnative
2
+
3
+ import dev.bota.sdk.DeviceReconnectHint
4
+ import dev.bota.sdk.model.ConnectedDevice
5
+ import dev.bota.sdk.model.ConnectionState
6
+ import dev.bota.sdk.model.DeviceType
7
+ import dev.bota.sdk.model.DiscoveredDevice
8
+ import dev.bota.sdk.model.DeviceFlags
9
+ import dev.bota.sdk.model.DeviceState
10
+ import dev.bota.sdk.model.DeviceStatus
11
+ import dev.bota.sdk.model.LteStatus
12
+ import dev.bota.sdk.model.PairingState
13
+ import dev.bota.sdk.model.WifiRadioStatus
14
+ import dev.bota.sdk.model.WireValue
15
+ import java.time.Instant
16
+ import kotlinx.coroutines.CompletableDeferred
17
+ import kotlinx.coroutines.awaitCancellation
18
+ import kotlinx.coroutines.flow.Flow
19
+ import kotlinx.coroutines.flow.flow
20
+ import kotlinx.coroutines.test.runTest
21
+ import org.junit.Assert.assertEquals
22
+ import org.junit.Test
23
+
24
+ class BotaDeviceSDKAndroidDevicesTest {
25
+ @Test
26
+ fun unknownPairingStateUsesTheFrozenUnpairedFallback() {
27
+ assertEquals("unpaired", PairingState.Unknown(0xFFu).toBridgeValue())
28
+ }
29
+
30
+ @Test
31
+ fun scanAndConnectionsDelegateToTheAndroidFacade() = runTest {
32
+ val selected = DiscoveredDevice(
33
+ id = "selected",
34
+ name = "Bota Pin",
35
+ deviceType = DeviceType.BotaPin,
36
+ rssi = -42,
37
+ discoveredAt = Instant.ofEpochMilli(1_788_200_000_000),
38
+ )
39
+ val verified = ConnectedDevice(
40
+ id = "selected",
41
+ serialNumber = "EVFXXW67KP",
42
+ deviceType = DeviceType.BotaPin,
43
+ firmwareVersion = "1.0.11",
44
+ isProvisioned = false,
45
+ connectionState = ConnectionState.Connected,
46
+ mtu = 247,
47
+ )
48
+ val client = FakeDeviceClient(selected, verified)
49
+ val devices = BotaDeviceSDKAndroidDevices(client, backgroundScope)
50
+ val received = CompletableDeferred<DiscoveredDevice>()
51
+
52
+ devices.startScan(5_000u, allowDuplicates = true) { received.complete(it) }
53
+ assertEquals("selected", received.await().id)
54
+ val connected = devices.connect(selected)
55
+ val reconnected = devices.reconnect(
56
+ "EVFXXW67KP",
57
+ DeviceReconnectHint(
58
+ scanTimeoutMilliseconds = 7_000u,
59
+ connectionTimeoutMilliseconds = 8_000u,
60
+ ),
61
+ )
62
+ devices.disconnect()
63
+
64
+ assertEquals(listOf(5_000uL to true), client.scanOptions)
65
+ assertEquals(listOf("selected"), client.selectedIds)
66
+ assertEquals(listOf("EVFXXW67KP"), client.reconnectSerials)
67
+ assertEquals(1, client.cancelCount)
68
+ assertEquals(1, client.disconnectCount)
69
+ assertEquals("EVFXXW67KP", connected.serialNumber)
70
+ assertEquals("EVFXXW67KP", reconnected.serialNumber)
71
+ }
72
+
73
+ @Test
74
+ fun scanFailureIsReportedWithoutEscapingTheOwnedTask() = runTest {
75
+ val selected = DiscoveredDevice(
76
+ id = "selected",
77
+ name = "Bota Pin",
78
+ deviceType = DeviceType.BotaPin,
79
+ rssi = -42,
80
+ discoveredAt = Instant.ofEpochMilli(1_788_200_000_000),
81
+ )
82
+ val verified = ConnectedDevice(
83
+ id = "selected",
84
+ serialNumber = "EVFXXW67KP",
85
+ deviceType = DeviceType.BotaPin,
86
+ firmwareVersion = "1.0.11",
87
+ isProvisioned = false,
88
+ connectionState = ConnectionState.Connected,
89
+ mtu = 247,
90
+ )
91
+ val failure = IllegalStateException("scan failed")
92
+ val client = FakeDeviceClient(selected, verified, scanFailure = failure)
93
+ val devices = BotaDeviceSDKAndroidDevices(client, backgroundScope)
94
+ val reported = CompletableDeferred<Throwable>()
95
+
96
+ devices.startScan(
97
+ timeoutMilliseconds = 5_000u,
98
+ allowDuplicates = false,
99
+ onDevice = {},
100
+ onError = { reported.complete(it) },
101
+ )
102
+
103
+ assertEquals(failure, reported.await())
104
+ }
105
+
106
+ @Test
107
+ fun statusReadAndSubscriptionDelegateToTheAndroidFacade() = runTest {
108
+ val selected = DiscoveredDevice(id = "selected", name = "Bota Pin", rssi = -42)
109
+ val verified = ConnectedDevice(
110
+ id = "selected",
111
+ serialNumber = "EVFXXW67KP",
112
+ deviceType = DeviceType.BotaPin,
113
+ firmwareVersion = "1.0.11",
114
+ isProvisioned = false,
115
+ connectionState = ConnectionState.Connected,
116
+ mtu = 247,
117
+ )
118
+ val expected = testDeviceStatus()
119
+ val client = FakeDeviceClient(selected, verified, status = expected)
120
+ val devices = BotaDeviceSDKAndroidDevices(client, backgroundScope)
121
+ val received = CompletableDeferred<DeviceStatus>()
122
+
123
+ assertEquals(expected, devices.readStatus())
124
+ devices.startStatusUpdates { received.complete(it) }
125
+ assertEquals(expected, received.await())
126
+ devices.stopStatusUpdates()
127
+
128
+ assertEquals(1, client.statusReadCount)
129
+ assertEquals(1, client.statusCancelCount)
130
+ }
131
+
132
+ private class FakeDeviceClient(
133
+ private val discovered: DiscoveredDevice,
134
+ private val connected: ConnectedDevice,
135
+ private val scanFailure: Throwable? = null,
136
+ private val status: DeviceStatus = testDeviceStatus(),
137
+ ) : BotaDeviceSDKAndroidDeviceClient {
138
+ val scanOptions = mutableListOf<Pair<ULong, Boolean>>()
139
+ val selectedIds = mutableListOf<String>()
140
+ val reconnectSerials = mutableListOf<String>()
141
+ var cancelCount = 0
142
+ var disconnectCount = 0
143
+ var statusReadCount = 0
144
+ var statusCancelCount = 0
145
+
146
+ override suspend fun startScan(
147
+ timeoutMilliseconds: ULong,
148
+ allowDuplicates: Boolean,
149
+ ): Flow<DiscoveredDevice> = flow {
150
+ scanOptions += timeoutMilliseconds to allowDuplicates
151
+ scanFailure?.let { throw it }
152
+ emit(discovered)
153
+ awaitCancellation()
154
+ }
155
+
156
+ override suspend fun cancelCurrentOperation() {
157
+ cancelCount += 1
158
+ }
159
+
160
+ override suspend fun connect(device: DiscoveredDevice): ConnectedDevice {
161
+ selectedIds += device.id
162
+ return connected
163
+ }
164
+
165
+ override suspend fun reconnect(serialNumber: String, hint: DeviceReconnectHint): ConnectedDevice {
166
+ reconnectSerials += serialNumber
167
+ return connected
168
+ }
169
+
170
+ override suspend fun disconnect() {
171
+ disconnectCount += 1
172
+ }
173
+
174
+ override suspend fun readStatus(): DeviceStatus {
175
+ statusReadCount += 1
176
+ return status
177
+ }
178
+
179
+ override suspend fun statusUpdates(): Flow<DeviceStatus> = flow {
180
+ try {
181
+ emit(status)
182
+ awaitCancellation()
183
+ } finally {
184
+ statusCancelCount += 1
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ private fun testDeviceStatus(): DeviceStatus = DeviceStatus(
191
+ batteryLevel = 72,
192
+ batteryMv = 3_842,
193
+ storageTotalMb = 8_192,
194
+ storageUsedMb = 512,
195
+ state = WireValue.Known(DeviceState.Idle),
196
+ pendingRecordings = 2,
197
+ lastTimeSyncAt = Instant.ofEpochSecond(1_788_200_000),
198
+ signalStrength = 4,
199
+ flags = DeviceFlags(
200
+ charging = false,
201
+ lowBattery = false,
202
+ storageFull = false,
203
+ wifiConnected = true,
204
+ lteConnected = false,
205
+ syncActive = false,
206
+ ),
207
+ timestamp = 1_788_200_000u,
208
+ lteStatus = WireValue.Known(LteStatus.Off),
209
+ lteSignalQuality = 99,
210
+ wifiStatus = WireValue.Known(WifiRadioStatus.Connected),
211
+ )
@@ -0,0 +1,132 @@
1
+ package dev.bota.sdk.reactnative
2
+
3
+ import java.io.File
4
+ import kotlinx.coroutines.CompletableDeferred
5
+ import kotlinx.coroutines.async
6
+ import kotlinx.coroutines.launch
7
+ import kotlinx.coroutines.test.runTest
8
+ import org.junit.Assert.assertEquals
9
+ import org.junit.Assert.assertFalse
10
+ import org.junit.Assert.assertTrue
11
+ import org.junit.Test
12
+
13
+ class BotaDeviceSDKAndroidLifecycleTest {
14
+ @Test
15
+ fun initialStateAndCapabilitiesDescribeAndroidAdapter() = runTest {
16
+ val lifecycle = BotaDeviceSDKAndroidLifecycle(FakeClient())
17
+
18
+ assertEquals("uninitialized", lifecycle.state())
19
+ assertEquals(
20
+ BotaDeviceSDKAndroidCapabilities(
21
+ backgroundReconnect = false,
22
+ backgroundScan = false,
23
+ bluetooth = true,
24
+ nativeFileTransfer = true,
25
+ platform = "android",
26
+ ),
27
+ lifecycle.capabilities(),
28
+ )
29
+ }
30
+
31
+ @Test
32
+ fun configureForwardsExactStorageDirectoryAndBecomesReady() = runTest {
33
+ val client = FakeClient()
34
+ val lifecycle = BotaDeviceSDKAndroidLifecycle(client)
35
+ val directory = File("/tmp/bota-rn-android-lifecycle")
36
+
37
+ lifecycle.configure(directory)
38
+
39
+ assertEquals(listOf(directory), client.configuredDirectories)
40
+ assertEquals("ready", lifecycle.state())
41
+ }
42
+
43
+ @Test
44
+ fun concurrentConfigureCallsShareOneAndroidConfiguration() = runTest {
45
+ val client = FakeClient(blockConfigure = true)
46
+ val lifecycle = BotaDeviceSDKAndroidLifecycle(client)
47
+
48
+ val first = async { lifecycle.configure(null) }
49
+ client.configureStarted.await()
50
+ val second = async { lifecycle.configure(null) }
51
+
52
+ assertEquals("initializing", lifecycle.state())
53
+ client.allowConfigure.complete(Unit)
54
+ first.await()
55
+ second.await()
56
+
57
+ assertEquals(1, client.configureCalls)
58
+ assertEquals("ready", lifecycle.state())
59
+ }
60
+
61
+ @Test
62
+ fun configureFailureIsRecoverable() = runTest {
63
+ val client = FakeClient(failConfigure = true)
64
+ val lifecycle = BotaDeviceSDKAndroidLifecycle(client)
65
+
66
+ val first = runCatching { lifecycle.configure(null) }
67
+
68
+ assertTrue(first.isFailure)
69
+ assertEquals("error", lifecycle.state())
70
+ client.failConfigure = false
71
+ lifecycle.configure(null)
72
+ assertEquals(2, client.configureCalls)
73
+ assertEquals("ready", lifecycle.state())
74
+ }
75
+
76
+ @Test
77
+ fun destroyWaitsForConfigurationAndReturnsToUninitialized() = runTest {
78
+ val client = FakeClient(blockConfigure = true)
79
+ val lifecycle = BotaDeviceSDKAndroidLifecycle(client)
80
+
81
+ val configure = launch { lifecycle.configure(null) }
82
+ client.configureStarted.await()
83
+ val destroy = launch { lifecycle.destroy() }
84
+
85
+ assertFalse(client.destroyed.isCompleted)
86
+ client.allowConfigure.complete(Unit)
87
+ configure.join()
88
+ destroy.join()
89
+
90
+ assertEquals(1, client.destroyCalls)
91
+ assertTrue(client.destroyed.isCompleted)
92
+ assertEquals("uninitialized", lifecycle.state())
93
+ }
94
+
95
+ @Test
96
+ fun destroyIsIdempotent() = runTest {
97
+ val client = FakeClient()
98
+ val lifecycle = BotaDeviceSDKAndroidLifecycle(client)
99
+
100
+ lifecycle.configure(null)
101
+ lifecycle.destroy()
102
+ lifecycle.destroy()
103
+
104
+ assertEquals(1, client.destroyCalls)
105
+ assertEquals("uninitialized", lifecycle.state())
106
+ }
107
+
108
+ private class FakeClient(
109
+ private val blockConfigure: Boolean = false,
110
+ var failConfigure: Boolean = false,
111
+ ) : BotaDeviceSDKAndroidClient {
112
+ val allowConfigure = CompletableDeferred<Unit>()
113
+ val configureStarted = CompletableDeferred<Unit>()
114
+ val configuredDirectories = mutableListOf<File?>()
115
+ val destroyed = CompletableDeferred<Unit>()
116
+ var configureCalls = 0
117
+ var destroyCalls = 0
118
+
119
+ override suspend fun configure(storageDirectory: File?) {
120
+ configureCalls += 1
121
+ configuredDirectories += storageDirectory
122
+ configureStarted.complete(Unit)
123
+ if (blockConfigure) allowConfigure.await()
124
+ if (failConfigure) error("configure failed")
125
+ }
126
+
127
+ override suspend fun destroy() {
128
+ destroyCalls += 1
129
+ destroyed.complete(Unit)
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,57 @@
1
+ package dev.bota.sdk.reactnative
2
+
3
+ import dev.bota.sdk.model.ConnectedDevice
4
+ import dev.bota.sdk.model.ConnectionState
5
+ import dev.bota.sdk.model.DeviceLogLine
6
+ import dev.bota.sdk.model.DeviceType
7
+ import kotlinx.coroutines.CoroutineScope
8
+ import kotlinx.coroutines.ExperimentalCoroutinesApi
9
+ import kotlinx.coroutines.awaitCancellation
10
+ import kotlinx.coroutines.flow.Flow
11
+ import kotlinx.coroutines.flow.flow
12
+ import kotlinx.coroutines.test.runCurrent
13
+ import kotlinx.coroutines.test.runTest
14
+ import org.junit.Assert.assertEquals
15
+ import org.junit.Assert.assertTrue
16
+ import org.junit.Test
17
+
18
+ class BotaDeviceSDKAndroidLogsTest {
19
+ @OptIn(ExperimentalCoroutinesApi::class)
20
+ @Test
21
+ fun logStreamEmitsSanitizedLinesAndOwnsStop() = runTest {
22
+ val client = TestAndroidLogClient()
23
+ val logs = BotaDeviceSDKAndroidLogs(client, CoroutineScope(coroutineContext))
24
+ val lines = mutableListOf<DeviceLogLine>()
25
+
26
+ logs.start(
27
+ ConnectedDevice(
28
+ id = "selected",
29
+ serialNumber = "EVFXXW67KP",
30
+ deviceType = DeviceType.BotaPin,
31
+ firmwareVersion = "1.0.11",
32
+ isProvisioned = true,
33
+ connectionState = ConnectionState.Connected,
34
+ mtu = 247,
35
+ ),
36
+ onLine = lines::add,
37
+ )
38
+ runCurrent()
39
+
40
+ assertEquals(listOf(DeviceLogLine("boot pass", isBacklog = true)), lines)
41
+ logs.stop()
42
+ assertTrue(client.stopped)
43
+ }
44
+
45
+ private class TestAndroidLogClient : BotaDeviceSDKAndroidLogClient {
46
+ var stopped = false
47
+
48
+ override fun streamLogs(device: ConnectedDevice): Flow<DeviceLogLine> = flow {
49
+ emit(DeviceLogLine("boot pass", isBacklog = true))
50
+ awaitCancellation()
51
+ }
52
+
53
+ override suspend fun stop() {
54
+ stopped = true
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,75 @@
1
+ package dev.bota.sdk.reactnative
2
+
3
+ import dev.bota.sdk.FirmwareImage
4
+ import dev.bota.sdk.model.ConnectedDevice
5
+ import dev.bota.sdk.model.ConnectionState
6
+ import dev.bota.sdk.model.DeviceType
7
+ import dev.bota.sdk.model.FirmwareUpdatePhase
8
+ import dev.bota.sdk.model.FirmwareUpdateProgress
9
+ import kotlinx.coroutines.flow.Flow
10
+ import kotlinx.coroutines.flow.flowOf
11
+ import kotlinx.coroutines.test.runTest
12
+ import org.junit.Assert.assertEquals
13
+ import org.junit.Assert.assertTrue
14
+ import org.junit.Test
15
+
16
+ class BotaDeviceSDKAndroidOTATest {
17
+ @Test
18
+ fun firmwareDownloadAndTransferStayNative() = runTest {
19
+ val client = TestAndroidOTAClient()
20
+ val ota = BotaDeviceSDKAndroidOTA(client)
21
+ val progress = mutableListOf<FirmwareUpdateProgress>()
22
+
23
+ ota.updateFirmware(
24
+ device = ConnectedDevice(
25
+ id = "selected",
26
+ serialNumber = "EVFXXW67KP",
27
+ deviceType = DeviceType.BotaPin,
28
+ firmwareVersion = "1.0.11",
29
+ isProvisioned = true,
30
+ connectionState = ConnectionState.Connected,
31
+ mtu = 247,
32
+ ),
33
+ version = "1.0.12",
34
+ sizeBytes = 1_024_000u,
35
+ crc32 = 0x1234_5678u,
36
+ url = "https://firmware.bota.dev/update.ufw",
37
+ onProgress = progress::add,
38
+ )
39
+
40
+ val image = requireNotNull(client.image)
41
+ assertEquals("1.0.12", image.version)
42
+ assertEquals(1_024_000u, image.sizeBytes)
43
+ assertEquals(0x1234_5678u, image.crc32)
44
+ assertEquals("https://firmware.bota.dev/update.ufw", image.request.url.toString())
45
+ assertEquals(
46
+ listOf(
47
+ FirmwareUpdateProgress(FirmwareUpdatePhase.Downloading, 512_000u, 1_024_000u),
48
+ FirmwareUpdateProgress(FirmwareUpdatePhase.Complete, 1_024_000u, 1_024_000u),
49
+ ),
50
+ progress,
51
+ )
52
+ ota.cancelAll()
53
+ assertTrue(client.cancelled)
54
+ }
55
+
56
+ private class TestAndroidOTAClient : BotaDeviceSDKAndroidOTAClient {
57
+ var image: FirmwareImage? = null
58
+ var cancelled = false
59
+
60
+ override fun updateFirmware(
61
+ device: ConnectedDevice,
62
+ image: FirmwareImage,
63
+ ): Flow<FirmwareUpdateProgress> {
64
+ this.image = image
65
+ return flowOf(
66
+ FirmwareUpdateProgress(FirmwareUpdatePhase.Downloading, 512_000u, 1_024_000u),
67
+ FirmwareUpdateProgress(FirmwareUpdatePhase.Complete, 1_024_000u, 1_024_000u),
68
+ )
69
+ }
70
+
71
+ override suspend fun cancelCurrentOperation() {
72
+ cancelled = true
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,147 @@
1
+ package dev.bota.sdk.reactnative
2
+
3
+ import java.io.File
4
+ import java.nio.file.Files
5
+ import java.util.Collections
6
+ import kotlinx.coroutines.test.runTest
7
+ import okhttp3.OkHttpClient
8
+ import okhttp3.Protocol
9
+ import okhttp3.Response
10
+ import okhttp3.ResponseBody.Companion.toResponseBody
11
+ import org.junit.Assert.assertEquals
12
+ import org.junit.Assert.assertFalse
13
+ import org.junit.Test
14
+
15
+ class BotaDeviceSDKAndroidRecordingUploadsTest {
16
+ @Test
17
+ fun plaintextUploadUsesPutCompletesAndDeletesNativeFile() = runTest {
18
+ val fixture = UploadFixture()
19
+ val requests = Collections.synchronizedList(mutableListOf<CapturedRequest>())
20
+ val uploads = BotaDeviceSDKAndroidRecordingUploads(
21
+ fixture.queueFile,
22
+ stubbedClient(requests),
23
+ )
24
+ val progress = mutableListOf<BotaRecordingUploadProgress>()
25
+
26
+ uploads.upload(
27
+ fixture.request(
28
+ taskId = "task-1",
29
+ completeUrl = "https://api.example/complete",
30
+ uploadToken = "up-token",
31
+ ),
32
+ progress::add,
33
+ )
34
+
35
+ assertFalse(fixture.recordingFile.exists())
36
+ assertEquals(
37
+ listOf(
38
+ CapturedRequest("PUT", "https://s3.example/recording", null),
39
+ CapturedRequest(
40
+ "POST",
41
+ "https://api.example/complete",
42
+ "Bearer up-token",
43
+ ),
44
+ ),
45
+ requests,
46
+ )
47
+ assertEquals(listOf(0L, 4L), progress.map { it.completedBytes })
48
+ assertEquals(listOf(4L, 4L), progress.map { it.totalBytes })
49
+ }
50
+
51
+ @Test
52
+ fun encryptedUploadUsesRelayAndSkipsCompletion() = runTest {
53
+ val fixture = UploadFixture()
54
+ val requests = Collections.synchronizedList(mutableListOf<CapturedRequest>())
55
+ val uploads = BotaDeviceSDKAndroidRecordingUploads(
56
+ fixture.queueFile,
57
+ stubbedClient(requests),
58
+ )
59
+
60
+ uploads.upload(
61
+ fixture.request(
62
+ taskId = "task-2",
63
+ completeUrl = "https://api.example/unused",
64
+ uploadToken = "up-token",
65
+ relayUrl = "https://api.example/upload-relay",
66
+ relayBearerToken = "device-token",
67
+ ),
68
+ ) {}
69
+
70
+ assertEquals(
71
+ listOf(
72
+ CapturedRequest(
73
+ "POST",
74
+ "https://api.example/upload-relay",
75
+ "Bearer device-token",
76
+ ),
77
+ ),
78
+ requests,
79
+ )
80
+ }
81
+
82
+ @Test
83
+ fun queueSaveAtomicallyReplacesExistingMetadata() = runTest {
84
+ val fixture = UploadFixture()
85
+ val uploads = BotaDeviceSDKAndroidRecordingUploads(
86
+ fixture.queueFile,
87
+ stubbedClient(mutableListOf()),
88
+ )
89
+
90
+ uploads.saveQueue("[{\"id\":\"first\"}]")
91
+ uploads.saveQueue("[{\"id\":\"second\"}]")
92
+
93
+ assertEquals("[{\"id\":\"second\"}]", uploads.loadQueue())
94
+ }
95
+
96
+ private fun stubbedClient(requests: MutableList<CapturedRequest>): OkHttpClient =
97
+ OkHttpClient.Builder()
98
+ .addInterceptor { chain ->
99
+ val request = chain.request()
100
+ requests += CapturedRequest(
101
+ request.method,
102
+ request.url.toString(),
103
+ request.header("Authorization"),
104
+ )
105
+ Response.Builder()
106
+ .request(request)
107
+ .protocol(Protocol.HTTP_1_1)
108
+ .code(200)
109
+ .message("OK")
110
+ .body("".toResponseBody())
111
+ .build()
112
+ }
113
+ .build()
114
+ }
115
+
116
+ private data class CapturedRequest(
117
+ val method: String,
118
+ val url: String,
119
+ val authorization: String?,
120
+ )
121
+
122
+ private class UploadFixture {
123
+ private val directory = Files.createTempDirectory("bota-recording-upload").toFile()
124
+ val recordingFile = File(directory, "recording.bin").apply {
125
+ writeBytes(byteArrayOf(1, 2, 3, 4))
126
+ }
127
+ val queueFile = File(directory, "queue.json")
128
+
129
+ fun request(
130
+ taskId: String,
131
+ completeUrl: String? = null,
132
+ uploadToken: String? = null,
133
+ relayUrl: String? = null,
134
+ relayBearerToken: String? = null,
135
+ ): BotaRecordingUploadRequest = BotaRecordingUploadRequest(
136
+ taskId = taskId,
137
+ recordingId = "rec-1",
138
+ localPath = recordingFile.path,
139
+ uploadUrl = "https://s3.example/recording",
140
+ uploadToken = uploadToken,
141
+ completeUrl = completeUrl,
142
+ contentType = "audio/ogg",
143
+ contentSha256 = "abc123",
144
+ relayUrl = relayUrl,
145
+ relayBearerToken = relayBearerToken,
146
+ )
147
+ }