@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,523 @@
1
+ package dev.bota.sdk.reactnative
2
+
3
+ import dev.bota.sdk.model.ConnectedDevice
4
+ import dev.bota.sdk.DeviceApiEnvironment
5
+ import dev.bota.sdk.model.ConnectionState
6
+ import dev.bota.sdk.model.DeviceType
7
+ import dev.bota.sdk.model.DeviceConnectionSettings
8
+ import dev.bota.sdk.model.DeprovisionResult
9
+ import dev.bota.sdk.model.FactoryResetCompletion
10
+ import dev.bota.sdk.model.FactoryResetGrantRequest
11
+ import dev.bota.sdk.model.FactoryResetPersistenceResult
12
+ import dev.bota.sdk.model.ProvisioningMaterial
13
+ import dev.bota.sdk.model.ProvisioningMaterialRequest
14
+ import dev.bota.sdk.model.RecordingControlError
15
+ import dev.bota.sdk.model.RecordingControlResult
16
+ import dev.bota.sdk.model.RecordingInitiator
17
+ import dev.bota.sdk.model.RecordingState
18
+ import kotlinx.coroutines.CompletableDeferred
19
+ import kotlinx.coroutines.ExperimentalCoroutinesApi
20
+ import kotlinx.coroutines.awaitCancellation
21
+ import kotlinx.coroutines.async
22
+ import kotlinx.coroutines.flow.Flow
23
+ import kotlinx.coroutines.flow.flow
24
+ import kotlinx.coroutines.test.runCurrent
25
+ import kotlinx.coroutines.supervisorScope
26
+ import kotlinx.coroutines.test.runTest
27
+ import org.junit.Assert.assertArrayEquals
28
+ import org.junit.Assert.assertEquals
29
+ import org.junit.Assert.assertTrue
30
+ import org.junit.Test
31
+
32
+ class BotaDeviceSDKAndroidSecurityTest {
33
+ @Test
34
+ fun deviceControlsDelegateTypedValuesToAndroidFacade() = runTest {
35
+ val connected = ConnectedDevice(
36
+ id = "selected",
37
+ serialNumber = "EVFXXW67KP",
38
+ deviceType = DeviceType.BotaPin,
39
+ firmwareVersion = "1.0.11",
40
+ isProvisioned = true,
41
+ connectionState = ConnectionState.Connected,
42
+ mtu = 247,
43
+ )
44
+ val client = TestAndroidSecurityClient()
45
+ val security = BotaDeviceSDKAndroidSecurity(client)
46
+
47
+ assertTrue(security.isProvisioned(connected))
48
+ assertEquals("public-key", security.readPublicKey(connected))
49
+ assertEquals("nonce", security.readAuthNonce(connected))
50
+ security.setApiEndpoint(DeviceApiEnvironment.Gamma, connected)
51
+ security.deliverCertificate("cert", "key", connected)
52
+ security.deliverBackendPublicKey(byteArrayOf(1, 2, 3), connected)
53
+ security.writeGrant("AQID", connected)
54
+ security.syncTime(connected)
55
+
56
+ assertEquals(DeviceApiEnvironment.Gamma, client.environment)
57
+ assertEquals("cert", client.certificate)
58
+ assertEquals("key", client.privateKey)
59
+ assertArrayEquals(byteArrayOf(1, 2, 3), client.backendPublicKey)
60
+ assertEquals("AQID", client.grantBlob)
61
+ assertTrue(client.timeSynced)
62
+ }
63
+
64
+ @Test
65
+ @OptIn(ExperimentalCoroutinesApi::class)
66
+ fun recordingControlsAndStateStreamDelegateToAndroidFacade() = runTest {
67
+ val connected = ConnectedDevice(
68
+ id = "selected",
69
+ serialNumber = "EVFXXW67KP",
70
+ deviceType = DeviceType.BotaPin,
71
+ firmwareVersion = "1.0.11",
72
+ isProvisioned = true,
73
+ connectionState = ConnectionState.Connected,
74
+ mtu = 247,
75
+ )
76
+ val client = TestAndroidSecurityClient()
77
+ val security = BotaDeviceSDKAndroidSecurity(client, backgroundScope)
78
+ val update = CompletableDeferred<RecordingState>()
79
+
80
+ assertEquals(
81
+ RecordingControlResult(success = true),
82
+ security.requestStartRecording(connected, "c3RhcnQ="),
83
+ )
84
+ assertEquals(
85
+ RecordingControlResult(success = false, error = RecordingControlError.NotRecording),
86
+ security.requestStopRecording(connected, "c3RvcA=="),
87
+ )
88
+ assertEquals(
89
+ RecordingState(true, "recording-1", RecordingInitiator.Remote),
90
+ security.readRecordingState(connected),
91
+ )
92
+
93
+ security.startRecordingStateUpdates(connected) { update.complete(it) }
94
+ runCurrent()
95
+ assertEquals(
96
+ RecordingState(false, initiatedBy = RecordingInitiator.Local),
97
+ update.await(),
98
+ )
99
+ security.stopRecordingStateUpdates()
100
+ security.stopRecordingStateUpdates()
101
+ client.recordingStateTerminated.await()
102
+
103
+ assertEquals("c3RhcnQ=", client.startRecordingGrant)
104
+ assertEquals("c3RvcA==", client.stopRecordingGrant)
105
+ assertEquals(1, client.recordingStateTerminationCount)
106
+ }
107
+
108
+ @Test
109
+ fun provisioningMaterialRoundTripAndDeprovisionDelegateToAndroidFacade() = runTest {
110
+ val connected = ConnectedDevice(
111
+ id = "selected",
112
+ serialNumber = "EVFXXW67KP",
113
+ deviceType = DeviceType.BotaPin,
114
+ firmwareVersion = "1.0.11",
115
+ isProvisioned = false,
116
+ connectionState = ConnectionState.Connected,
117
+ mtu = 247,
118
+ )
119
+ val client = TestAndroidSecurityClient()
120
+ val security = BotaDeviceSDKAndroidSecurity(client)
121
+ val requests = CompletableDeferred<BotaDeviceSDKAndroidProvisioningRequest>()
122
+
123
+ val operation = async {
124
+ security.provision(connected) { requests.complete(it) }
125
+ }
126
+ val request = requests.await()
127
+ assertEquals(connected.serialNumber, request.serialNumber)
128
+ assertEquals("00112233", request.nonce)
129
+ assertEquals("aabbccdd", request.devicePublicKey)
130
+
131
+ security.resolveProvisioningMaterial(
132
+ requestId = request.requestId,
133
+ apiEndpoint = "https://api.bota.dev",
134
+ deviceToken = "dtok_example",
135
+ mtu = 247u,
136
+ )
137
+ operation.await()
138
+ val deprovision = security.deprovision(connected, "AQID")
139
+
140
+ assertArrayEquals("https://api.bota.dev".encodeToByteArray(), client.material?.apiEndpoint)
141
+ assertArrayEquals("dtok_example".encodeToByteArray(), client.material?.deviceToken)
142
+ assertEquals(247uL, client.material?.mtu)
143
+ assertEquals(listOf(connected.serialNumber), client.deprovisionedSerials)
144
+ assertEquals("AQID", client.deprovisionGrant)
145
+ assertEquals(DeprovisionResult(success = true), deprovision)
146
+ }
147
+
148
+ @Test
149
+ fun factoryResetGrantRoundTripAndExactGenerationResumeDelegateToAndroidFacade() = runTest {
150
+ val connected = ConnectedDevice(
151
+ id = "selected",
152
+ serialNumber = "EVFXXW67KP",
153
+ deviceType = DeviceType.BotaPin,
154
+ firmwareVersion = "1.0.11",
155
+ isProvisioned = true,
156
+ connectionState = ConnectionState.Connected,
157
+ mtu = 247,
158
+ )
159
+ val client = TestAndroidSecurityClient()
160
+ val security = BotaDeviceSDKAndroidSecurity(client)
161
+ val requests = CompletableDeferred<BotaDeviceSDKAndroidFactoryResetRequest>()
162
+
163
+ val operation = async {
164
+ security.factoryReset(
165
+ connected,
166
+ commandId = "reset-command-1",
167
+ bindingGeneration = 9u,
168
+ onGrantRequest = { requests.complete(it) },
169
+ onPersistenceRequest = null,
170
+ )
171
+ }
172
+ val request = requests.await()
173
+ assertEquals(connected.serialNumber, request.serialNumber)
174
+ assertEquals("44556677", request.nonce)
175
+ assertEquals("reset-command-1", request.commandId)
176
+ assertEquals(9uL, request.bindingGeneration)
177
+
178
+ security.resolveFactoryResetGrant(request.requestId, "Z3JhbnQ=")
179
+ assertEquals(
180
+ FactoryResetCompletion("reset-command-1", 9u),
181
+ operation.await(),
182
+ )
183
+ assertEquals(
184
+ FactoryResetCompletion("reset-command-1", 9u),
185
+ security.resumePendingFactoryReset(connected, 9u, null),
186
+ )
187
+ assertArrayEquals("grant".encodeToByteArray(), client.factoryResetGrant)
188
+ assertEquals(listOf(9uL), client.resumedBindingGenerations)
189
+ }
190
+
191
+ @Test
192
+ fun factoryResetWaitsForApplicationPersistenceResolution() = runTest {
193
+ val connected = ConnectedDevice(
194
+ id = "selected",
195
+ serialNumber = "EVFXXW67KP",
196
+ deviceType = DeviceType.BotaPin,
197
+ firmwareVersion = "1.0.11",
198
+ isProvisioned = true,
199
+ connectionState = ConnectionState.Connected,
200
+ mtu = 247,
201
+ )
202
+ val client = TestAndroidSecurityClient()
203
+ val security = BotaDeviceSDKAndroidSecurity(client)
204
+ val grantRequests = CompletableDeferred<BotaDeviceSDKAndroidFactoryResetRequest>()
205
+ val persistenceRequests = CompletableDeferred<
206
+ BotaDeviceSDKAndroidFactoryResetPersistenceRequest
207
+ >()
208
+
209
+ val operation = async {
210
+ security.factoryReset(
211
+ connected,
212
+ commandId = "reset-command-1",
213
+ bindingGeneration = 9u,
214
+ onGrantRequest = { grantRequests.complete(it) },
215
+ onPersistenceRequest = { persistenceRequests.complete(it) },
216
+ )
217
+ }
218
+ val grantRequest = grantRequests.await()
219
+ security.resolveFactoryResetGrant(grantRequest.requestId, "Z3JhbnQ=")
220
+ val persistenceRequest = persistenceRequests.await()
221
+ assertEquals(7u.toUShort(), persistenceRequest.localRecordingsDeleted)
222
+ assertEquals(0, client.factoryResetPersistenceCompletions)
223
+
224
+ security.resolveFactoryResetResultPersistence(persistenceRequest.requestId)
225
+ operation.await()
226
+
227
+ assertEquals(1, client.factoryResetPersistenceCompletions)
228
+ }
229
+
230
+ @Test
231
+ fun factoryResetResumeFallsBackToFirmwareReplayAfterReinstall() = runTest {
232
+ val connected = ConnectedDevice(
233
+ id = "selected",
234
+ serialNumber = "EVFXXW67KP",
235
+ deviceType = DeviceType.BotaPin,
236
+ firmwareVersion = "1.0.11",
237
+ isProvisioned = true,
238
+ connectionState = ConnectionState.Connected,
239
+ mtu = 247,
240
+ )
241
+ val client = TestAndroidSecurityClient().apply { hasPendingFactoryReset = false }
242
+ val security = BotaDeviceSDKAndroidSecurity(client)
243
+ val persistenceRequests = CompletableDeferred<
244
+ BotaDeviceSDKAndroidFactoryResetPersistenceRequest
245
+ >()
246
+
247
+ val operation = async {
248
+ security.resumePendingFactoryReset(
249
+ connected,
250
+ currentBindingGeneration = 0u,
251
+ onPersistenceRequest = { persistenceRequests.complete(it) },
252
+ )
253
+ }
254
+ val request = persistenceRequests.await()
255
+ assertEquals(7u.toUShort(), request.localRecordingsDeleted)
256
+ security.resolveFactoryResetResultPersistence(request.requestId)
257
+
258
+ assertEquals(0uL, operation.await()?.bindingGeneration)
259
+ assertEquals(listOf(0uL), client.unjournaledResumeBindingGenerations)
260
+ }
261
+
262
+ @Test
263
+ fun connectionSettingsDelegateToAndroidFacade() = runTest {
264
+ val connected = ConnectedDevice(
265
+ id = "selected",
266
+ serialNumber = "EVFXXW67KP",
267
+ deviceType = DeviceType.BotaNote,
268
+ firmwareVersion = "1.0.11",
269
+ isProvisioned = true,
270
+ connectionState = ConnectionState.Connected,
271
+ mtu = 247,
272
+ )
273
+ val client = TestAndroidSecurityClient()
274
+ val security = BotaDeviceSDKAndroidSecurity(client)
275
+ val settings = DeviceConnectionSettings(
276
+ enabledConnections = DeviceConnectionSettings.EnabledConnections(true, true),
277
+ heartbeatEnabledConnections = DeviceConnectionSettings.EnabledConnections(true, true),
278
+ uploadNetworkPreference = listOf(
279
+ DeviceConnectionSettings.ConnectionType.Wifi,
280
+ DeviceConnectionSettings.ConnectionType.Ble,
281
+ DeviceConnectionSettings.ConnectionType.Cellular,
282
+ ),
283
+ powerManagement = DeviceConnectionSettings.PowerManagement(0, -1),
284
+ streamingEnabled = false,
285
+ streamingFlushIntervalSeconds = 30,
286
+ )
287
+
288
+ security.writeConnectionSettings(settings, connected)
289
+
290
+ assertEquals(settings, client.connectionSettings)
291
+ }
292
+
293
+ @Test
294
+ fun connectionSettingsReadDelegatesToAndroidFacade() = runTest {
295
+ val connected = ConnectedDevice(
296
+ id = "selected",
297
+ serialNumber = "EVFXXW67KP",
298
+ deviceType = DeviceType.BotaNote,
299
+ firmwareVersion = "1.0.11",
300
+ isProvisioned = true,
301
+ connectionState = ConnectionState.Connected,
302
+ mtu = 247,
303
+ )
304
+ val expected = DeviceConnectionSettings(
305
+ enabledConnections = DeviceConnectionSettings.EnabledConnections(true, false),
306
+ uploadNetworkPreference = listOf(
307
+ DeviceConnectionSettings.ConnectionType.Wifi,
308
+ DeviceConnectionSettings.ConnectionType.Ble,
309
+ ),
310
+ )
311
+ val client = TestAndroidSecurityClient().apply { connectionSettingsReadResult = expected }
312
+ val security = BotaDeviceSDKAndroidSecurity(client)
313
+
314
+ val settings = security.readConnectionSettings(connected)
315
+
316
+ assertEquals(expected, settings)
317
+ }
318
+
319
+ @Test
320
+ fun factoryResetRejectsMalformedGrantAndCancelsPendingRequestsOnDestroy() = runTest {
321
+ val connected = ConnectedDevice(
322
+ id = "selected",
323
+ serialNumber = "EVFXXW67KP",
324
+ deviceType = DeviceType.BotaPin,
325
+ firmwareVersion = "1.0.11",
326
+ isProvisioned = true,
327
+ connectionState = ConnectionState.Connected,
328
+ mtu = 247,
329
+ )
330
+ supervisorScope {
331
+ val malformedClient = TestAndroidSecurityClient()
332
+ val malformedSecurity = BotaDeviceSDKAndroidSecurity(malformedClient)
333
+ val malformedRequests = CompletableDeferred<BotaDeviceSDKAndroidFactoryResetRequest>()
334
+ val malformedOperation = async {
335
+ malformedSecurity.factoryReset(
336
+ connected,
337
+ commandId = "reset-command-1",
338
+ bindingGeneration = 9u,
339
+ onGrantRequest = { malformedRequests.complete(it) },
340
+ onPersistenceRequest = null,
341
+ )
342
+ }
343
+ val malformedRequest = malformedRequests.await()
344
+
345
+ malformedSecurity.resolveFactoryResetGrant(malformedRequest.requestId, "not-encoded")
346
+ assertEquals(
347
+ "factory reset grant is not valid encoded data",
348
+ runCatching { malformedOperation.await() }.exceptionOrNull()?.message,
349
+ )
350
+
351
+ val cancelledClient = TestAndroidSecurityClient()
352
+ val cancelledSecurity = BotaDeviceSDKAndroidSecurity(cancelledClient)
353
+ val cancelledRequests = CompletableDeferred<BotaDeviceSDKAndroidFactoryResetRequest>()
354
+ val cancelledOperation = async {
355
+ cancelledSecurity.factoryReset(
356
+ connected,
357
+ commandId = "reset-command-2",
358
+ bindingGeneration = 10u,
359
+ onGrantRequest = { cancelledRequests.complete(it) },
360
+ onPersistenceRequest = null,
361
+ )
362
+ }
363
+ cancelledRequests.await()
364
+ cancelledSecurity.cancelAll()
365
+
366
+ assertTrue(runCatching { cancelledOperation.await() }.isFailure)
367
+ assertTrue(cancelledClient.factoryResetCancelled)
368
+ }
369
+ }
370
+
371
+ private class TestAndroidSecurityClient : BotaDeviceSDKAndroidSecurityClient {
372
+ var material: ProvisioningMaterial? = null
373
+ val deprovisionedSerials = mutableListOf<String>()
374
+ var deprovisionGrant: String? = null
375
+ var factoryResetGrant: ByteArray? = null
376
+ var factoryResetCancelled = false
377
+ var factoryResetPersistenceCompletions = 0
378
+ val resumedBindingGenerations = mutableListOf<ULong>()
379
+ val unjournaledResumeBindingGenerations = mutableListOf<ULong>()
380
+ var hasPendingFactoryReset = true
381
+ var connectionSettings: DeviceConnectionSettings? = null
382
+ var connectionSettingsReadResult = DeviceConnectionSettings(
383
+ enabledConnections = DeviceConnectionSettings.EnabledConnections(true, false),
384
+ uploadNetworkPreference = listOf(DeviceConnectionSettings.ConnectionType.Wifi),
385
+ )
386
+ var environment: DeviceApiEnvironment? = null
387
+ var certificate: String? = null
388
+ var privateKey: String? = null
389
+ var backendPublicKey: ByteArray? = null
390
+ var grantBlob: String? = null
391
+ var timeSynced = false
392
+ var startRecordingGrant: String? = null
393
+ var stopRecordingGrant: String? = null
394
+ val recordingStateTerminated = CompletableDeferred<Unit>()
395
+ var recordingStateTerminationCount = 0
396
+
397
+ override suspend fun isProvisioned(device: ConnectedDevice): Boolean = true
398
+ override suspend fun readPublicKey(device: ConnectedDevice): String? = "public-key"
399
+ override suspend fun readAuthNonce(device: ConnectedDevice): String? = "nonce"
400
+ override suspend fun setApiEndpoint(environment: DeviceApiEnvironment, device: ConnectedDevice) {
401
+ this.environment = environment
402
+ }
403
+ override suspend fun deliverCertificate(
404
+ certificatePem: String,
405
+ privateKeyPem: String,
406
+ device: ConnectedDevice,
407
+ ) {
408
+ certificate = certificatePem
409
+ privateKey = privateKeyPem
410
+ }
411
+ override suspend fun deliverBackendPublicKey(publicKey: ByteArray, device: ConnectedDevice) {
412
+ backendPublicKey = publicKey
413
+ }
414
+ override suspend fun writeGrant(grantBlob: String, device: ConnectedDevice) {
415
+ this.grantBlob = grantBlob
416
+ }
417
+ override suspend fun syncTime(device: ConnectedDevice) { timeSynced = true }
418
+ override suspend fun requestStartRecording(
419
+ device: ConnectedDevice,
420
+ grantBlob: String,
421
+ ): RecordingControlResult {
422
+ startRecordingGrant = grantBlob
423
+ return RecordingControlResult(success = true)
424
+ }
425
+ override suspend fun requestStopRecording(
426
+ device: ConnectedDevice,
427
+ grantBlob: String,
428
+ ): RecordingControlResult {
429
+ stopRecordingGrant = grantBlob
430
+ return RecordingControlResult(success = false, error = RecordingControlError.NotRecording)
431
+ }
432
+ override suspend fun readRecordingState(device: ConnectedDevice): RecordingState =
433
+ RecordingState(true, "recording-1", RecordingInitiator.Remote)
434
+ override fun recordingStateUpdates(device: ConnectedDevice): Flow<RecordingState> = flow {
435
+ try {
436
+ emit(RecordingState(false, initiatedBy = RecordingInitiator.Local))
437
+ awaitCancellation()
438
+ } finally {
439
+ recordingStateTerminationCount += 1
440
+ recordingStateTerminated.complete(Unit)
441
+ }
442
+ }
443
+
444
+ override suspend fun provision(
445
+ device: ConnectedDevice,
446
+ provider: suspend (ProvisioningMaterialRequest) -> ProvisioningMaterial,
447
+ ) {
448
+ material = provider(
449
+ ProvisioningMaterialRequest(
450
+ serialNumber = device.serialNumber,
451
+ nonce = byteArrayOf(0x00, 0x11, 0x22, 0x33),
452
+ devicePublicKey = byteArrayOf(0xAA.toByte(), 0xBB.toByte(), 0xCC.toByte(), 0xDD.toByte()),
453
+ ),
454
+ )
455
+ }
456
+
457
+ override suspend fun deprovision(
458
+ device: ConnectedDevice,
459
+ grantBlob: String,
460
+ ): DeprovisionResult {
461
+ deprovisionedSerials += device.serialNumber
462
+ deprovisionGrant = grantBlob
463
+ return DeprovisionResult(success = true)
464
+ }
465
+
466
+ override suspend fun writeConnectionSettings(
467
+ settings: DeviceConnectionSettings,
468
+ device: ConnectedDevice,
469
+ ) {
470
+ connectionSettings = settings
471
+ }
472
+
473
+ override suspend fun readConnectionSettings(device: ConnectedDevice): DeviceConnectionSettings =
474
+ connectionSettingsReadResult
475
+
476
+ override suspend fun cancelCurrentOperation() = Unit
477
+
478
+ override suspend fun factoryReset(
479
+ device: ConnectedDevice,
480
+ commandId: String,
481
+ bindingGeneration: ULong,
482
+ persistResult: (suspend (FactoryResetPersistenceResult) -> Unit)?,
483
+ provider: suspend (FactoryResetGrantRequest) -> ByteArray,
484
+ ): FactoryResetCompletion {
485
+ factoryResetGrant = provider(
486
+ FactoryResetGrantRequest(
487
+ serialNumber = device.serialNumber,
488
+ nonce = byteArrayOf(0x44, 0x55, 0x66, 0x77),
489
+ commandId = commandId,
490
+ bindingGeneration = bindingGeneration,
491
+ ),
492
+ )
493
+ persistResult?.invoke(FactoryResetPersistenceResult(7u))
494
+ if (persistResult != null) factoryResetPersistenceCompletions += 1
495
+ return FactoryResetCompletion(commandId, bindingGeneration)
496
+ }
497
+
498
+ override suspend fun resumePendingFactoryReset(
499
+ device: ConnectedDevice,
500
+ currentBindingGeneration: ULong,
501
+ persistResult: (suspend (FactoryResetPersistenceResult) -> Unit)?,
502
+ ): FactoryResetCompletion? {
503
+ resumedBindingGenerations += currentBindingGeneration
504
+ if (!hasPendingFactoryReset) return null
505
+ return FactoryResetCompletion("reset-command-1", currentBindingGeneration)
506
+ }
507
+
508
+ override suspend fun resumeUnjournaledFactoryReset(
509
+ device: ConnectedDevice,
510
+ commandId: String,
511
+ bindingGeneration: ULong,
512
+ persistResult: suspend (FactoryResetPersistenceResult) -> Unit,
513
+ ): FactoryResetCompletion {
514
+ unjournaledResumeBindingGenerations += bindingGeneration
515
+ persistResult(FactoryResetPersistenceResult(7u))
516
+ return FactoryResetCompletion(commandId, bindingGeneration)
517
+ }
518
+
519
+ override suspend fun cancelFactoryReset() {
520
+ factoryResetCancelled = true
521
+ }
522
+ }
523
+ }
@@ -0,0 +1,100 @@
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.DeviceType
6
+ import dev.bota.sdk.model.DeviceWiFiScanResult
7
+ import dev.bota.sdk.model.WiFiConfigResult
8
+ import dev.bota.sdk.model.WiFiConnectionStatus
9
+ import dev.bota.sdk.model.WiFiScanNetwork
10
+ import dev.bota.sdk.model.WiFiStatusInfo
11
+ import kotlinx.coroutines.CoroutineScope
12
+ import kotlinx.coroutines.ExperimentalCoroutinesApi
13
+ import kotlinx.coroutines.awaitCancellation
14
+ import kotlinx.coroutines.flow.Flow
15
+ import kotlinx.coroutines.flow.flow
16
+ import kotlinx.coroutines.test.runCurrent
17
+ import kotlinx.coroutines.test.runTest
18
+ import org.junit.Assert.assertEquals
19
+ import org.junit.Test
20
+
21
+ class BotaDeviceSDKAndroidWiFiTest {
22
+ @Test
23
+ fun configurationAndScanDelegateToNativeWiFiFacade() = runTest {
24
+ val client = TestAndroidWiFiClient()
25
+ val wifi = BotaDeviceSDKAndroidWiFi(client, CoroutineScope(coroutineContext))
26
+ val device = wifiTestDevice()
27
+
28
+ val configured = wifi.configure(device, "Bota", "secret", "grant.test")
29
+ val scan = wifi.scanNetworks(device)
30
+
31
+ assertEquals(WiFiConfigResult.Success, configured)
32
+ assertEquals("Bota", scan.currentSsid)
33
+ assertEquals(ConfigurationInput("Bota", "secret", "grant.test"), client.configurationInput)
34
+ }
35
+
36
+ @OptIn(ExperimentalCoroutinesApi::class)
37
+ @Test
38
+ fun statusStreamOwnsExactlyOneNativeSubscription() = runTest {
39
+ val client = TestAndroidWiFiClient()
40
+ val wifi = BotaDeviceSDKAndroidWiFi(client, CoroutineScope(coroutineContext))
41
+ val values = mutableListOf<WiFiStatusInfo>()
42
+
43
+ wifi.startStatusUpdates(wifiTestDevice(), onStatus = values::add)
44
+ runCurrent()
45
+ wifi.stopStatusUpdates()
46
+ wifi.stopStatusUpdates()
47
+
48
+ assertEquals(listOf(WiFiStatusInfo(WiFiConnectionStatus.Connected, 87u, "Bota")), values)
49
+ assertEquals(1, client.subscriptionTerminations)
50
+ }
51
+ }
52
+
53
+ private data class ConfigurationInput(val ssid: String, val password: String, val grantBlob: String)
54
+
55
+ private class TestAndroidWiFiClient : BotaDeviceSDKAndroidWiFiClient {
56
+ var configurationInput: ConfigurationInput? = null
57
+ var subscriptionTerminations = 0
58
+
59
+ override suspend fun configure(
60
+ device: ConnectedDevice,
61
+ ssid: String,
62
+ password: String,
63
+ grantBlob: String,
64
+ ): WiFiConfigResult {
65
+ configurationInput = ConfigurationInput(ssid, password, grantBlob)
66
+ return WiFiConfigResult.Success
67
+ }
68
+
69
+ override suspend fun disconnect(device: ConnectedDevice): WiFiConfigResult = WiFiConfigResult.Success
70
+
71
+ override suspend fun readStatus(device: ConnectedDevice): WiFiStatusInfo =
72
+ WiFiStatusInfo(WiFiConnectionStatus.Connected, 87u, "Bota")
73
+
74
+ override fun statusUpdates(device: ConnectedDevice): Flow<WiFiStatusInfo> = flow {
75
+ try {
76
+ emit(WiFiStatusInfo(WiFiConnectionStatus.Connected, 87u, "Bota"))
77
+ awaitCancellation()
78
+ } finally {
79
+ subscriptionTerminations += 1
80
+ }
81
+ }
82
+
83
+ override suspend fun scanNetworks(device: ConnectedDevice): DeviceWiFiScanResult =
84
+ DeviceWiFiScanResult(
85
+ listOf(WiFiScanNetwork("Bota", 100u, isCurrent = true, isOpen = false)),
86
+ "Bota",
87
+ )
88
+
89
+ override suspend fun cancelCurrentOperation() = Unit
90
+ }
91
+
92
+ private fun wifiTestDevice(): ConnectedDevice = ConnectedDevice(
93
+ id = "selected",
94
+ serialNumber = "EVFXXW67KP",
95
+ deviceType = DeviceType.BotaNote,
96
+ firmwareVersion = "1.0.17",
97
+ isProvisioned = true,
98
+ connectionState = ConnectionState.Connected,
99
+ mtu = 247,
100
+ )