@bota.dev/react-native-app-sdk 2.0.0-beta.1 → 2.0.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -4
- package/android/build.gradle +1 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDevices.kt +22 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDiagnostics.kt +53 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogs.kt +16 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploads.kt +144 -14
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordings.kt +150 -29
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKEncryptedUploadV2Materials.kt +27 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKModule.kt +86 -3
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKPackage.kt +9 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/upload/BotaUploadV2BackendModule.kt +66 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/upload/NativeUpload.kt +400 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/upload/UploadHttp.kt +158 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/upload/UploadJournal.kt +112 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/upload/UploadModels.kt +171 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDevicesTest.kt +35 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogsTest.kt +33 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploadsTest.kt +135 -6
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingsTest.kt +236 -10
- package/generated/codegen-contract.json +751 -32
- package/ios/BotaDeviceSDK.mm +114 -0
- package/ios/BotaDeviceSDKAppleBridge.swift +141 -4
- package/ios/BotaDeviceSDKAppleDevices.swift +11 -0
- package/ios/BotaDeviceSDKAppleDiagnostics.swift +40 -0
- package/ios/BotaDeviceSDKAppleLogs.swift +18 -0
- package/ios/BotaDeviceSDKAppleRecordingUploads.swift +138 -6
- package/ios/BotaDeviceSDKAppleRecordings.swift +113 -5
- package/ios/BotaDeviceSDKEncryptedUploadV2Materials.swift +40 -0
- package/ios/BotaUploadV2Backend.mm +49 -0
- package/ios/UploadBackend/BotaUploadV2BackendBridge.swift +98 -0
- package/ios/UploadBackend/UploadCoordinator.swift +84 -0
- package/ios/UploadBackend/UploadHTTP.swift +82 -0
- package/ios/UploadBackend/UploadJournal.swift +187 -0
- package/ios/UploadBackend/UploadModels.swift +273 -0
- package/ios/UploadBackend/UploadOperation.swift +391 -0
- package/lib/commonjs/BotaClient.js +16 -2
- package/lib/commonjs/BotaClient.js.map +1 -1
- package/lib/commonjs/ble/constants.js +5 -1
- package/lib/commonjs/ble/constants.js.map +1 -1
- package/lib/commonjs/ble/deviceDiagnostics.js +240 -0
- package/lib/commonjs/ble/deviceDiagnostics.js.map +1 -0
- package/lib/commonjs/client.js +183 -28
- package/lib/commonjs/client.js.map +1 -1
- package/lib/commonjs/clientPresence.js +26 -0
- package/lib/commonjs/clientPresence.js.map +1 -0
- package/lib/commonjs/encryptedUploadV2Backend.js +200 -0
- package/lib/commonjs/encryptedUploadV2Backend.js.map +1 -0
- package/lib/commonjs/index.js +34 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/managers/DeviceManager.js +23 -0
- package/lib/commonjs/managers/DeviceManager.js.map +1 -1
- package/lib/commonjs/managers/NativeUploadQueue.js +386 -0
- package/lib/commonjs/managers/NativeUploadQueue.js.map +1 -0
- package/lib/commonjs/managers/RecordingManager.js +83 -102
- package/lib/commonjs/managers/RecordingManager.js.map +1 -1
- package/lib/commonjs/managers/uploadRecoveryMetadata.js +83 -0
- package/lib/commonjs/managers/uploadRecoveryMetadata.js.map +1 -0
- package/lib/commonjs/sdkIdentity.js +10 -0
- package/lib/commonjs/sdkIdentity.js.map +1 -0
- package/lib/commonjs/specs/NativeBotaDeviceSDK.js.map +1 -1
- package/lib/commonjs/specs/NativeBotaUploadV2Backend.js +11 -0
- package/lib/commonjs/specs/NativeBotaUploadV2Backend.js.map +1 -0
- package/lib/commonjs/uploadRecovery.js +68 -0
- package/lib/commonjs/uploadRecovery.js.map +1 -0
- package/lib/module/BotaClient.js +16 -2
- package/lib/module/BotaClient.js.map +1 -1
- package/lib/module/ble/constants.js +4 -0
- package/lib/module/ble/constants.js.map +1 -1
- package/lib/module/ble/deviceDiagnostics.js +234 -0
- package/lib/module/ble/deviceDiagnostics.js.map +1 -0
- package/lib/module/client.js +183 -28
- package/lib/module/client.js.map +1 -1
- package/lib/module/clientPresence.js +22 -0
- package/lib/module/clientPresence.js.map +1 -0
- package/lib/module/encryptedUploadV2Backend.js +196 -0
- package/lib/module/encryptedUploadV2Backend.js.map +1 -0
- package/lib/module/index.js +7 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/managers/DeviceManager.js +23 -0
- package/lib/module/managers/DeviceManager.js.map +1 -1
- package/lib/module/managers/NativeUploadQueue.js +380 -0
- package/lib/module/managers/NativeUploadQueue.js.map +1 -0
- package/lib/module/managers/RecordingManager.js +83 -102
- package/lib/module/managers/RecordingManager.js.map +1 -1
- package/lib/module/managers/uploadRecoveryMetadata.js +77 -0
- package/lib/module/managers/uploadRecoveryMetadata.js.map +1 -0
- package/lib/module/sdkIdentity.js +6 -0
- package/lib/module/sdkIdentity.js.map +1 -0
- package/lib/module/specs/NativeBotaDeviceSDK.js.map +1 -1
- package/lib/module/specs/NativeBotaUploadV2Backend.js +9 -0
- package/lib/module/specs/NativeBotaUploadV2Backend.js.map +1 -0
- package/lib/module/uploadRecovery.js +64 -0
- package/lib/module/uploadRecovery.js.map +1 -0
- package/lib/typescript/BotaClient.d.ts +2 -0
- package/lib/typescript/BotaClient.d.ts.map +1 -1
- package/lib/typescript/ble/constants.d.ts +4 -0
- package/lib/typescript/ble/constants.d.ts.map +1 -1
- package/lib/typescript/ble/deviceDiagnostics.d.ts +10 -0
- package/lib/typescript/ble/deviceDiagnostics.d.ts.map +1 -0
- package/lib/typescript/client.d.ts +24 -3
- package/lib/typescript/client.d.ts.map +1 -1
- package/lib/typescript/clientPresence.d.ts +26 -0
- package/lib/typescript/clientPresence.d.ts.map +1 -0
- package/lib/typescript/encryptedUploadV2Backend.d.ts +41 -0
- package/lib/typescript/encryptedUploadV2Backend.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +13 -4
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/managers/DeviceManager.d.ts +5 -1
- package/lib/typescript/managers/DeviceManager.d.ts.map +1 -1
- package/lib/typescript/managers/NativeUploadQueue.d.ts +41 -0
- package/lib/typescript/managers/NativeUploadQueue.d.ts.map +1 -0
- package/lib/typescript/managers/RecordingManager.d.ts +3 -8
- package/lib/typescript/managers/RecordingManager.d.ts.map +1 -1
- package/lib/typescript/managers/types.d.ts +6 -1
- package/lib/typescript/managers/types.d.ts.map +1 -1
- package/lib/typescript/managers/uploadRecoveryMetadata.d.ts +6 -0
- package/lib/typescript/managers/uploadRecoveryMetadata.d.ts.map +1 -0
- package/lib/typescript/models/Device.d.ts +42 -0
- package/lib/typescript/models/Device.d.ts.map +1 -1
- package/lib/typescript/models/Recording.d.ts +47 -0
- package/lib/typescript/models/Recording.d.ts.map +1 -1
- package/lib/typescript/models/Status.d.ts +5 -1
- package/lib/typescript/models/Status.d.ts.map +1 -1
- package/lib/typescript/sdkIdentity.d.ts +3 -0
- package/lib/typescript/sdkIdentity.d.ts.map +1 -0
- package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts +70 -0
- package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts.map +1 -1
- package/lib/typescript/specs/NativeBotaUploadV2Backend.d.ts +18 -0
- package/lib/typescript/specs/NativeBotaUploadV2Backend.d.ts.map +1 -0
- package/lib/typescript/uploadRecovery.d.ts +27 -0
- package/lib/typescript/uploadRecovery.d.ts.map +1 -0
- package/package.json +1 -1
- package/scripts/bota_device_sdk_spm_workaround.rb +26 -15
- package/src/BotaClient.ts +17 -3
- package/src/ble/constants.ts +5 -0
- package/src/ble/deviceDiagnostics.ts +238 -0
- package/src/client.ts +189 -18
- package/src/clientPresence.ts +33 -0
- package/src/encryptedUploadV2Backend.ts +171 -0
- package/src/index.ts +24 -0
- package/src/managers/DeviceManager.ts +23 -0
- package/src/managers/NativeUploadQueue.ts +330 -0
- package/src/managers/RecordingManager.ts +83 -116
- package/src/managers/types.ts +7 -1
- package/src/managers/uploadRecoveryMetadata.ts +59 -0
- package/src/models/Device.ts +43 -0
- package/src/models/Recording.ts +52 -0
- package/src/models/Status.ts +5 -1
- package/src/sdkIdentity.ts +3 -0
- package/src/specs/NativeBotaDeviceSDK.ts +66 -0
- package/src/specs/NativeBotaUploadV2Backend.ts +19 -0
- package/src/uploadRecovery.ts +66 -0
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Bota SDK for React Native
|
|
2
2
|
|
|
3
|
+
Source candidate `2.0.0-beta.11` keeps the five-platform version synchronized.
|
|
4
|
+
The managed HTTP helper is React Native only; other facade behavior is unchanged.
|
|
5
|
+
Beta.10 remains the published baseline until release gates pass.
|
|
6
|
+
|
|
3
7
|
Bluetooth device SDK for Bota Pin and Bota Note applications. The 2.x candidate
|
|
4
8
|
preserves the public TypeScript API of `@bota.dev/react-native-sdk@0.0.65`
|
|
5
9
|
while moving Bluetooth workflows and recording, streaming, and firmware bytes
|
|
@@ -7,6 +11,15 @@ into the native Apple and Android SDKs.
|
|
|
7
11
|
|
|
8
12
|
## Requirements
|
|
9
13
|
|
|
14
|
+
App SDK beta.6 and later expose `BotaDeviceSDK.clientPresence.nextReport(deviceId)`.
|
|
15
|
+
It returns `BotaClientPresenceContext | null` from the native verified connection,
|
|
16
|
+
preserving native session/sequence while identifying this React Native package
|
|
17
|
+
and its generated version. It makes no HTTP call or device command. Destroy
|
|
18
|
+
discards late metadata results. The host explicitly relays a report alongside
|
|
19
|
+
fresh status; see the root README. The frozen compatibility `BotaClient` and
|
|
20
|
+
compatibility `DeviceManager` APIs are unchanged. Rebuild native binaries when
|
|
21
|
+
adopting this source API; published-package availability is a separate gate.
|
|
22
|
+
|
|
10
23
|
- React Native `0.86.3` or newer with the New Architecture enabled
|
|
11
24
|
- iOS `15.1` or newer
|
|
12
25
|
- Android API `26` or newer
|
|
@@ -14,19 +27,31 @@ into the native Apple and Android SDKs.
|
|
|
14
27
|
|
|
15
28
|
## Install
|
|
16
29
|
|
|
17
|
-
|
|
18
|
-
publication
|
|
30
|
+
Published beta.9 remains available. Install this beta.10 source candidate with
|
|
31
|
+
the exact pin below only after publication is verified. Beta.10 retires the
|
|
32
|
+
exact Android connection when explicit disconnect times out or is cancelled
|
|
33
|
+
without a native callback, preserving replacement sessions. Reconnect explicitly.
|
|
34
|
+
The [preflight](../../release/evidence/2.0.0-beta.10-preflight.md) separates
|
|
35
|
+
automated cleanup regressions from publication and physical acceptance.
|
|
36
|
+
The intermittent GATT 8/133 reconnect issue remains unverified.
|
|
19
37
|
Remove `@bota.dev/react-native-sdk` before adding the replacement; do not
|
|
20
|
-
co-install both.
|
|
38
|
+
co-install both. Standalone maintenance ended September 29, 2026; follow the
|
|
39
|
+
[retirement and migration notice](../../docs/migrations/react-native-sdk-sunset.md).
|
|
21
40
|
|
|
22
41
|
```bash
|
|
23
|
-
npm install --save-exact @bota.dev/react-native-app-sdk@2.0.0-beta.
|
|
42
|
+
npm install --save-exact @bota.dev/react-native-app-sdk@2.0.0-beta.11
|
|
24
43
|
npx pod-install
|
|
25
44
|
```
|
|
26
45
|
|
|
27
46
|
Rebuild the native iOS and Android applications after installation. An Expo Go
|
|
28
47
|
runtime cannot load this native module; use a development or production build.
|
|
29
48
|
|
|
49
|
+
An application-native upload pod may depend on `BotaDeviceSDK`. With static
|
|
50
|
+
SwiftPM linkage, the SDK's CocoaPods workaround normalizes the module-map path
|
|
51
|
+
in both aggregate and dependent-pod cached build settings. This preserves the
|
|
52
|
+
path when Expo later writes macro flags. Do not work around missing module maps
|
|
53
|
+
by adding another copy of the Apple SDK.
|
|
54
|
+
|
|
30
55
|
Expo prebuild and EAS projects must set the Android minimum SDK explicitly. Add
|
|
31
56
|
the Expo-compatible `expo-build-properties` package and configure it in the app
|
|
32
57
|
config:
|
|
@@ -59,8 +84,21 @@ await BotaClient.waitForBluetooth();
|
|
|
59
84
|
The `BotaClient.devices`, `BotaClient.recordings`, and `BotaClient.ota`
|
|
60
85
|
managers become available after configuration completes.
|
|
61
86
|
|
|
87
|
+
Discovery exposes the manufacturer-advertised `macAddress` as uppercase,
|
|
88
|
+
colon-separated bytes (`AA:BB:CC:DD:EE:FF`), preserving the legacy React Native
|
|
89
|
+
registration contract. Compact native addresses are formatted at the RN
|
|
90
|
+
boundary; missing or malformed advertised values become `null`. The peripheral
|
|
91
|
+
`id` is unchanged and is never substituted for an absent advertised MAC.
|
|
92
|
+
|
|
62
93
|
## Encrypted upload v2
|
|
63
94
|
|
|
95
|
+
Current source includes `createEncryptedUploadV2Backend` for iOS/Android. Supply
|
|
96
|
+
your authenticated backend URL, fresh-token callback and scoped identity; the
|
|
97
|
+
SDK owns native session journaling, manifest replay, verification polling and
|
|
98
|
+
signed-receipt delivery. See the [managed integration](../../docs/parity/v2-managed-backend.md)
|
|
99
|
+
for the example, backend routes, restart trigger and release limits. Beta.10
|
|
100
|
+
does not contain this helper. The low-level provider below remains supported.
|
|
101
|
+
|
|
64
102
|
The target encrypted-upload-v2 runtime is an additive `BotaDeviceSDK` API; it
|
|
65
103
|
does not change the legacy `BotaClient` recording providers or events:
|
|
66
104
|
|
|
@@ -92,6 +130,57 @@ Its target-facade tests and the maintenance SDK `0.0.67` runtime tests are
|
|
|
92
130
|
cross-referenced by the same canonical v2 workflow evidence in CI and tagged
|
|
93
131
|
release verification. The frozen public compatibility surface remains `0.0.65`.
|
|
94
132
|
|
|
133
|
+
### Unpublished Native App Integration
|
|
134
|
+
|
|
135
|
+
Current source adds `BotaDeviceSDK.recordings.listPendingRecordings(device)`.
|
|
136
|
+
It returns legacy recordings and v2 entries tagged by `storageFormat: 3`, with
|
|
137
|
+
the full UUID/generation, ciphertext length/hash and catalog metadata. Catalog
|
|
138
|
+
errors do not silently downgrade to legacy listing. These changes require
|
|
139
|
+
matching native source and a newly built application; beta.1 is insufficient.
|
|
140
|
+
|
|
141
|
+
The v2 sync method accepts a fifth argument `{ signal, operationId }`. The
|
|
142
|
+
optional operation ID is a fresh UUID shared with an application-native
|
|
143
|
+
adapter; cancellation stops and awaits that exact operation. Native provider
|
|
144
|
+
contexts expose an operation-scoped `readAuthNonce` callback. An RN application's
|
|
145
|
+
Swift/Kotlin adapter can call
|
|
146
|
+
`BotaDeviceSDKEncryptedUploadV2Materials.readAuthNonce(operationId)` while the
|
|
147
|
+
profile request is pending; it must not make a competing public BLE read.
|
|
148
|
+
|
|
149
|
+
Native material must supply `uploadContext`, which relays opaque challenge/proof
|
|
150
|
+
documents through the application backend. The SDK owns the bounded device
|
|
151
|
+
handshake before authorization and the first completion receipt. Ordinary v2
|
|
152
|
+
requires capability mask `0x17f`; expired-session replacement requires `0x37f`.
|
|
153
|
+
Neither the app nor SDK verifies backend signatures or trusts the phone clock
|
|
154
|
+
as a substitute for device verification. Cancellation discards late native
|
|
155
|
+
material via `cancelPreparation`; it does not delete recoverable recordings.
|
|
156
|
+
Historical checkpoints without ciphertext identity may resume the same owner
|
|
157
|
+
but cannot authorize replacement. See `docs/parity/v2-demo-*.md` for evidence
|
|
158
|
+
and limitations. Hardware and publication gates remain unchanged.
|
|
159
|
+
|
|
160
|
+
## Unpublished Maintenance Additions
|
|
161
|
+
|
|
162
|
+
Current source follows the diagnostic and upload-recovery additions from
|
|
163
|
+
maintenance SDK commit `318974f925a573cf04b0d624978bee04784af09b`. These additions
|
|
164
|
+
are not part of the published `2.0.0-beta.1` package.
|
|
165
|
+
|
|
166
|
+
- `BotaClient.devices.readDiagnosticEvents(device)` returns a bounded batch.
|
|
167
|
+
Reading does not delete it. After the application durably accepts events,
|
|
168
|
+
call `acknowledgeDiagnosticEvents(device, acceptedEventIds)` with their exact
|
|
169
|
+
16-character lowercase hexadecimal IDs. The native `BotaDeviceSDK.logs` API
|
|
170
|
+
exposes the same methods. Firmware must support the diagnostics service.
|
|
171
|
+
- Configure `uploadRecoveryProvider` to refresh upload credentials for the
|
|
172
|
+
original recording and account/project scope after restart. Only non-secret
|
|
173
|
+
queue metadata persists; recording bytes stay in native files. Completion
|
|
174
|
+
acknowledgement is required before releasing recoverable files.
|
|
175
|
+
- Legacy `RecordingDataStore` JavaScript byte callbacks are not supported.
|
|
176
|
+
Supplying one fails configuration explicitly. Use the native file store and
|
|
177
|
+
credential/completion callbacks instead; this is a migration adaptation,
|
|
178
|
+
not byte-store API equivalence.
|
|
179
|
+
|
|
180
|
+
See the repository's `docs/parity/` notes for recovery contracts and test
|
|
181
|
+
coverage. This does not enable encrypted-v2 device support or complete the
|
|
182
|
+
separate physical-device and application rollout gates.
|
|
183
|
+
|
|
95
184
|
## Documentation
|
|
96
185
|
|
|
97
186
|
See [docs.bota.dev](https://docs.bota.dev) for pairing, provisioning,
|
|
@@ -100,3 +189,25 @@ recording transfer, WiFi, OTA, and device-management guides.
|
|
|
100
189
|
## License
|
|
101
190
|
|
|
102
191
|
MIT
|
|
192
|
+
|
|
193
|
+
## Standalone migration additions (beta.6)
|
|
194
|
+
|
|
195
|
+
`BotaClient.clientPresence.nextReport(deviceId)` and
|
|
196
|
+
`BotaClient.devices.clientPresence.nextReport(deviceId)` return the standalone
|
|
197
|
+
`SdkClientContext` shape (`schema_version`, `session_id`, `sequence`, `platform`,
|
|
198
|
+
`sdk_package`, `sdk_version`) or null. This is passive diagnostic metadata.
|
|
199
|
+
The existing modern facade still returns camelCase fields, now typed as
|
|
200
|
+
`BotaClientPresenceContext`; callers that imported its former `SdkClientContext`
|
|
201
|
+
type should rename that type import. No native wire fields changed.
|
|
202
|
+
|
|
203
|
+
Use `readClientPresenceObservation` around a fresh status read and an exact host
|
|
204
|
+
scope guard, then attach the report and the authorized `binding_generation` to
|
|
205
|
+
the existing authenticated BLE heartbeat. Skip during competing work; do not
|
|
206
|
+
add timers, replay offline reports, or treat metadata as device authority.
|
|
207
|
+
|
|
208
|
+
Use `createUploadRecoveryProvider` with an authenticated, disposable
|
|
209
|
+
`UploadRecoveryBackend` lease. The helper checks identity and evidence, obtains
|
|
210
|
+
fresh credentials, and completes the original backend recording before native
|
|
211
|
+
cleanup. Supply the initial `recoveryScope` and configure the provider before
|
|
212
|
+
starting batch sync. Do not call backend completion only after the sync generator
|
|
213
|
+
returns: device confirmation has already happened then.
|
package/android/build.gradle
CHANGED
|
@@ -72,5 +72,6 @@ dependencies {
|
|
|
72
72
|
implementation "${androidMetadata.mavenCoordinate}:${packageVersion}"
|
|
73
73
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${androidMetadata.coroutinesVersion}"
|
|
74
74
|
testImplementation "junit:junit:4.13.2"
|
|
75
|
+
testImplementation "org.json:json:20240303"
|
|
75
76
|
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:${androidMetadata.coroutinesVersion}"
|
|
76
77
|
}
|
|
@@ -28,8 +28,10 @@ internal interface BotaDeviceSDKAndroidDeviceClient {
|
|
|
28
28
|
suspend fun disconnect()
|
|
29
29
|
|
|
30
30
|
suspend fun readStatus(): DeviceStatus
|
|
31
|
+
suspend fun nextClientPresence(deviceId: String): dev.bota.sdk.SDKClientContext?
|
|
31
32
|
|
|
32
33
|
suspend fun statusUpdates(): Flow<DeviceStatus>
|
|
34
|
+
fun connectionUpdates(): Flow<ConnectedDevice?>
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
internal class BotaDeviceSDKSharedAndroidDeviceClient(
|
|
@@ -56,19 +58,37 @@ internal class BotaDeviceSDKSharedAndroidDeviceClient(
|
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
override suspend fun readStatus(): DeviceStatus = client.devices.readStatus()
|
|
61
|
+
override suspend fun nextClientPresence(deviceId: String): dev.bota.sdk.SDKClientContext? =
|
|
62
|
+
client.clientPresence.nextReport(deviceId)
|
|
59
63
|
|
|
60
64
|
override suspend fun statusUpdates(): Flow<DeviceStatus> = client.devices.statusUpdates()
|
|
65
|
+
override fun connectionUpdates(): Flow<ConnectedDevice?> = client.devices.connectionUpdates()
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
internal class BotaDeviceSDKAndroidDevices(
|
|
64
69
|
private val client: BotaDeviceSDKAndroidDeviceClient,
|
|
65
70
|
private val scope: CoroutineScope,
|
|
66
71
|
) {
|
|
72
|
+
suspend fun nextClientPresence(deviceId: String): dev.bota.sdk.SDKClientContext? = client.nextClientPresence(deviceId)
|
|
67
73
|
private val operations = Mutex()
|
|
68
74
|
private val scanLock = Any()
|
|
69
75
|
private val statusLock = Any()
|
|
70
76
|
private var activeScan: Job? = null
|
|
71
77
|
private var activeStatusUpdates: Job? = null
|
|
78
|
+
private var activeConnectionUpdates: Job? = null
|
|
79
|
+
|
|
80
|
+
suspend fun startConnectionUpdates(onDisconnected: () -> Unit) = operations.withLock {
|
|
81
|
+
activeConnectionUpdates?.cancelAndJoin()
|
|
82
|
+
val stream = client.connectionUpdates()
|
|
83
|
+
activeConnectionUpdates = scope.launch(start = CoroutineStart.UNDISPATCHED) {
|
|
84
|
+
var connected = false
|
|
85
|
+
stream.collect { device ->
|
|
86
|
+
val lost = connected && device == null
|
|
87
|
+
connected = device != null
|
|
88
|
+
if (lost) onDisconnected()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
72
92
|
|
|
73
93
|
suspend fun startScan(
|
|
74
94
|
timeoutMilliseconds: ULong,
|
|
@@ -151,6 +171,8 @@ internal class BotaDeviceSDKAndroidDevices(
|
|
|
151
171
|
}
|
|
152
172
|
|
|
153
173
|
suspend fun stopAll() = operations.withLock {
|
|
174
|
+
activeConnectionUpdates?.cancelAndJoin()
|
|
175
|
+
activeConnectionUpdates = null
|
|
154
176
|
stopScanOwned()
|
|
155
177
|
stopStatusUpdatesOwned()
|
|
156
178
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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.model.DeviceDiagnosticsBatch
|
|
6
|
+
import dev.bota.sdk.model.DeviceDiagnosticEvent
|
|
7
|
+
|
|
8
|
+
internal fun DeviceDiagnosticsBatch.toWritableMap(): WritableMap = Arguments.createMap().apply {
|
|
9
|
+
putInt("schema_version", schemaVersion)
|
|
10
|
+
putArray("events", Arguments.createArray().apply { events.forEach { pushMap(it.toWritableMap()) } })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private fun DeviceDiagnosticEvent.toWritableMap(): WritableMap = Arguments.createMap().apply {
|
|
14
|
+
putString("event_id", eventId)
|
|
15
|
+
putString("event_type", eventType)
|
|
16
|
+
putString("reason_code", reasonCode)
|
|
17
|
+
putDouble("uptime_ms", uptimeMs.toDouble())
|
|
18
|
+
putString("signature", signature)
|
|
19
|
+
putString("firmware_build_id", firmwareBuildId)
|
|
20
|
+
putString("subsystem", subsystem)
|
|
21
|
+
putString("state_before_event", stateBeforeEvent)
|
|
22
|
+
report?.let { detail ->
|
|
23
|
+
putMap("report", Arguments.createMap().apply {
|
|
24
|
+
putMap("fault", Arguments.createMap().apply {
|
|
25
|
+
putInt("cpu_id", detail.fault.cpuId)
|
|
26
|
+
putString("cpu_emu", detail.fault.cpuEmu)
|
|
27
|
+
putString("core_emu", detail.fault.coreEmu)
|
|
28
|
+
putString("hsb_emu", detail.fault.hsbEmu)
|
|
29
|
+
putString("audio_emu", detail.fault.audioEmu)
|
|
30
|
+
putString("wireless_emu", detail.fault.wirelessEmu)
|
|
31
|
+
})
|
|
32
|
+
putMap("execution", Arguments.createMap().apply {
|
|
33
|
+
detail.execution.task?.let { putString("task", it) }
|
|
34
|
+
detail.execution.reti?.let { putString("reti", it) }
|
|
35
|
+
detail.execution.rets?.let { putString("rets", it) }
|
|
36
|
+
putArray("pc_trace", Arguments.createArray().apply { detail.execution.pcTrace.forEach { pushString(it) } })
|
|
37
|
+
})
|
|
38
|
+
detail.runtime?.let { runtime ->
|
|
39
|
+
putMap("runtime", Arguments.createMap().apply {
|
|
40
|
+
runtime.heapFreeBytes?.let { putDouble("heap_free_bytes", it.toDouble()) }
|
|
41
|
+
runtime.taskStackRemainingBytes?.let { putDouble("task_stack_remaining_bytes", it.toDouble()) }
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
detail.breadcrumbs?.let { breadcrumbs ->
|
|
45
|
+
putArray("breadcrumbs", Arguments.createArray().apply {
|
|
46
|
+
breadcrumbs.forEach { pushMap(Arguments.createMap().apply {
|
|
47
|
+
putInt("delta_ms", it.deltaMs); putString("code", it.code); putInt("arg0", it.arg0)
|
|
48
|
+
}) }
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -3,6 +3,7 @@ package dev.bota.sdk.reactnative
|
|
|
3
3
|
import dev.bota.sdk.BotaDeviceClient
|
|
4
4
|
import dev.bota.sdk.model.ConnectedDevice
|
|
5
5
|
import dev.bota.sdk.model.DeviceLogLine
|
|
6
|
+
import dev.bota.sdk.model.DeviceDiagnosticsBatch
|
|
6
7
|
import kotlinx.coroutines.CancellationException
|
|
7
8
|
import kotlinx.coroutines.CoroutineScope
|
|
8
9
|
import kotlinx.coroutines.CoroutineStart
|
|
@@ -15,6 +16,8 @@ import kotlinx.coroutines.sync.Mutex
|
|
|
15
16
|
import kotlinx.coroutines.sync.withLock
|
|
16
17
|
|
|
17
18
|
internal interface BotaDeviceSDKAndroidLogClient {
|
|
19
|
+
suspend fun readDiagnosticEvents(device: ConnectedDevice): DeviceDiagnosticsBatch
|
|
20
|
+
suspend fun acknowledgeDiagnosticEvents(device: ConnectedDevice, acceptedEventIds: List<String>)
|
|
18
21
|
fun streamLogs(device: ConnectedDevice): Flow<DeviceLogLine>
|
|
19
22
|
|
|
20
23
|
suspend fun stop()
|
|
@@ -29,12 +32,25 @@ internal class BotaDeviceSDKSharedAndroidLogClient(
|
|
|
29
32
|
override suspend fun stop() {
|
|
30
33
|
client.logs.stop()
|
|
31
34
|
}
|
|
35
|
+
|
|
36
|
+
override suspend fun readDiagnosticEvents(device: ConnectedDevice): DeviceDiagnosticsBatch =
|
|
37
|
+
client.logs.readDiagnosticEvents(device)
|
|
38
|
+
|
|
39
|
+
override suspend fun acknowledgeDiagnosticEvents(device: ConnectedDevice, acceptedEventIds: List<String>) {
|
|
40
|
+
client.logs.acknowledgeDiagnosticEvents(device, acceptedEventIds)
|
|
41
|
+
}
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
internal class BotaDeviceSDKAndroidLogs(
|
|
35
45
|
private val client: BotaDeviceSDKAndroidLogClient,
|
|
36
46
|
private val scope: CoroutineScope,
|
|
37
47
|
) {
|
|
48
|
+
suspend fun readDiagnosticEvents(device: ConnectedDevice): DeviceDiagnosticsBatch =
|
|
49
|
+
client.readDiagnosticEvents(device)
|
|
50
|
+
|
|
51
|
+
suspend fun acknowledgeDiagnosticEvents(device: ConnectedDevice, acceptedEventIds: List<String>) {
|
|
52
|
+
client.acknowledgeDiagnosticEvents(device, acceptedEventIds)
|
|
53
|
+
}
|
|
38
54
|
private val operations = Mutex()
|
|
39
55
|
private val streamLock = Any()
|
|
40
56
|
private var activeStream: Job? = null
|
package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploads.kt
CHANGED
|
@@ -5,11 +5,18 @@ import com.facebook.react.bridge.Arguments
|
|
|
5
5
|
import com.facebook.react.bridge.ReadableMap
|
|
6
6
|
import com.facebook.react.bridge.WritableMap
|
|
7
7
|
import java.io.File
|
|
8
|
+
import java.io.FileOutputStream
|
|
8
9
|
import java.nio.file.AtomicMoveNotSupportedException
|
|
9
10
|
import java.nio.file.Files
|
|
10
11
|
import java.nio.file.StandardCopyOption
|
|
12
|
+
import java.nio.channels.FileChannel
|
|
13
|
+
import java.nio.file.StandardOpenOption
|
|
11
14
|
import java.util.concurrent.ConcurrentHashMap
|
|
12
15
|
import kotlinx.coroutines.Dispatchers
|
|
16
|
+
import kotlinx.coroutines.currentCoroutineContext
|
|
17
|
+
import kotlinx.coroutines.ensureActive
|
|
18
|
+
import kotlinx.coroutines.sync.Mutex
|
|
19
|
+
import kotlinx.coroutines.sync.withLock
|
|
13
20
|
import kotlinx.coroutines.withContext
|
|
14
21
|
import okhttp3.Call
|
|
15
22
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
@@ -17,6 +24,8 @@ import okhttp3.OkHttpClient
|
|
|
17
24
|
import okhttp3.Request
|
|
18
25
|
import okhttp3.RequestBody.Companion.asRequestBody
|
|
19
26
|
import okhttp3.RequestBody.Companion.toRequestBody
|
|
27
|
+
import org.json.JSONArray
|
|
28
|
+
import org.json.JSONObject
|
|
20
29
|
|
|
21
30
|
internal data class BotaRecordingUploadRequest(
|
|
22
31
|
val taskId: String,
|
|
@@ -29,6 +38,7 @@ internal data class BotaRecordingUploadRequest(
|
|
|
29
38
|
val contentSha256: String?,
|
|
30
39
|
val relayUrl: String?,
|
|
31
40
|
val relayBearerToken: String?,
|
|
41
|
+
val fileSizeBytes: Long? = null,
|
|
32
42
|
)
|
|
33
43
|
|
|
34
44
|
internal data class BotaRecordingUploadProgress(
|
|
@@ -40,21 +50,51 @@ internal data class BotaRecordingUploadProgress(
|
|
|
40
50
|
internal class BotaDeviceSDKAndroidRecordingUploads(
|
|
41
51
|
private val queueFile: File,
|
|
42
52
|
private val httpClient: OkHttpClient = OkHttpClient(),
|
|
53
|
+
private val defaultRecordingDirectory: File = File(queueFile.parentFile, "recordings"),
|
|
54
|
+
private val syncDirectory: (File) -> Unit = { directory ->
|
|
55
|
+
FileChannel.open(directory.toPath(), StandardOpenOption.READ).use { it.force(true) }
|
|
56
|
+
},
|
|
43
57
|
) {
|
|
44
58
|
constructor(
|
|
45
59
|
context: Context,
|
|
46
60
|
httpClient: OkHttpClient = OkHttpClient(),
|
|
47
|
-
) : this(File(context.filesDir, "bota-sdk/compat-upload-queue.json"), httpClient
|
|
61
|
+
) : this(File(context.filesDir, "bota-sdk/compat-upload-queue.json"), httpClient,
|
|
62
|
+
File(context.noBackupFilesDir, "bota-app-sdk/recordings"))
|
|
48
63
|
|
|
49
|
-
private
|
|
64
|
+
@Volatile private var recordingDirectory = defaultRecordingDirectory
|
|
65
|
+
|
|
66
|
+
fun configure(storageDirectory: File?) {
|
|
67
|
+
recordingDirectory = storageDirectory?.let { File(it, "recordings") } ?: defaultRecordingDirectory
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private class Operation {
|
|
71
|
+
private var cancelled = false
|
|
72
|
+
private var call: Call? = null
|
|
73
|
+
|
|
74
|
+
@Synchronized fun execute(next: Call) {
|
|
75
|
+
check(!cancelled) { "recording upload cancelled" }
|
|
76
|
+
call = next
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@Synchronized fun cancel() { cancelled = true; call?.cancel() }
|
|
80
|
+
@Synchronized fun checkActive() { check(!cancelled) { "recording upload cancelled" } }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private val operations = ConcurrentHashMap<String, Operation>()
|
|
84
|
+
private val cancelledAttempts = mutableSetOf<String>()
|
|
85
|
+
private val journalMutex = Mutex()
|
|
86
|
+
private var journalWriteUncertain = false
|
|
50
87
|
|
|
51
88
|
suspend fun upload(
|
|
52
89
|
upload: BotaRecordingUploadRequest,
|
|
53
90
|
onProgress: (BotaRecordingUploadProgress) -> Unit,
|
|
54
91
|
): Unit = withContext(Dispatchers.IO) {
|
|
55
92
|
val file = File(upload.localPath)
|
|
93
|
+
requireOwnedFile(upload.localPath)
|
|
56
94
|
require(file.isFile) { "recording file does not exist: ${upload.localPath}" }
|
|
57
95
|
val total = file.length()
|
|
96
|
+
require(upload.fileSizeBytes == null || upload.fileSizeBytes == total) { "stored recording length changed" }
|
|
97
|
+
currentCoroutineContext().ensureActive()
|
|
58
98
|
onProgress(BotaRecordingUploadProgress(upload.taskId, 0, total))
|
|
59
99
|
val relay = upload.relayUrl != null
|
|
60
100
|
val request = Request.Builder()
|
|
@@ -69,15 +109,22 @@ internal class BotaDeviceSDKAndroidRecordingUploads(
|
|
|
69
109
|
}
|
|
70
110
|
.build()
|
|
71
111
|
val call = httpClient.newCall(request)
|
|
72
|
-
|
|
73
|
-
|
|
112
|
+
val operation = Operation()
|
|
113
|
+
synchronized(operations) {
|
|
114
|
+
check(upload.taskId !in cancelledAttempts) { "recording upload cancelled" }
|
|
115
|
+
check(operations.putIfAbsent(upload.taskId, operation) == null) {
|
|
116
|
+
"recording upload is already active: ${upload.taskId}"
|
|
117
|
+
}
|
|
74
118
|
}
|
|
75
119
|
try {
|
|
120
|
+
operation.execute(call)
|
|
76
121
|
call.execute().use { response ->
|
|
77
122
|
check(response.isSuccessful) {
|
|
78
123
|
"recording upload failed with HTTP ${response.code}"
|
|
79
124
|
}
|
|
80
125
|
}
|
|
126
|
+
currentCoroutineContext().ensureActive()
|
|
127
|
+
operation.checkActive()
|
|
81
128
|
if (!relay && upload.completeUrl != null && upload.uploadToken != null) {
|
|
82
129
|
val checksum = upload.contentSha256?.let {
|
|
83
130
|
",\"content_sha256\":\"${it.jsonEscaped()}\""
|
|
@@ -89,39 +136,86 @@ internal class BotaDeviceSDKAndroidRecordingUploads(
|
|
|
89
136
|
.post(body.toRequestBody("application/json".toMediaTypeOrNull()))
|
|
90
137
|
.build()
|
|
91
138
|
val completionCall = httpClient.newCall(complete)
|
|
92
|
-
|
|
139
|
+
operation.execute(completionCall)
|
|
93
140
|
completionCall.execute().use { response ->
|
|
94
141
|
check(response.isSuccessful) {
|
|
95
142
|
"recording completion failed with HTTP ${response.code}"
|
|
96
143
|
}
|
|
97
144
|
}
|
|
98
145
|
}
|
|
146
|
+
currentCoroutineContext().ensureActive()
|
|
147
|
+
operation.checkActive()
|
|
99
148
|
onProgress(BotaRecordingUploadProgress(upload.taskId, total, total))
|
|
100
|
-
check(file.delete() || !file.exists()) {
|
|
101
|
-
"uploaded recording file could not be removed"
|
|
102
|
-
}
|
|
103
149
|
} finally {
|
|
104
|
-
|
|
150
|
+
operations.remove(upload.taskId, operation)
|
|
105
151
|
}
|
|
106
152
|
}
|
|
107
153
|
|
|
108
154
|
suspend fun cancel(taskId: String) = withContext(Dispatchers.IO) {
|
|
109
|
-
|
|
155
|
+
synchronized(operations) {
|
|
156
|
+
cancelledAttempts.add(taskId)
|
|
157
|
+
operations[taskId]?.cancel()
|
|
158
|
+
}
|
|
110
159
|
}
|
|
111
160
|
|
|
112
161
|
suspend fun cancelAll() = withContext(Dispatchers.IO) {
|
|
113
|
-
|
|
114
|
-
|
|
162
|
+
synchronized(operations) {
|
|
163
|
+
cancelledAttempts.addAll(operations.keys)
|
|
164
|
+
operations.values.forEach(Operation::cancel)
|
|
165
|
+
}
|
|
115
166
|
}
|
|
116
167
|
|
|
117
168
|
suspend fun loadQueue(): String = withContext(Dispatchers.IO) {
|
|
118
|
-
|
|
169
|
+
journalMutex.withLock {
|
|
170
|
+
if (!queueFile.isFile) return@withLock "[]"
|
|
171
|
+
queueFile.readText()
|
|
172
|
+
}
|
|
119
173
|
}
|
|
120
174
|
|
|
121
175
|
suspend fun saveQueue(serializedTasks: String): Unit = withContext(Dispatchers.IO) {
|
|
176
|
+
journalMutex.withLock { writeQueue(metadata(serializedTasks).toString()) }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
suspend fun release(taskId: String, localPath: String): Unit = withContext(Dispatchers.IO) {
|
|
180
|
+
journalMutex.withLock {
|
|
181
|
+
check(!journalWriteUncertain) { "recording release requires durable journal completion" }
|
|
182
|
+
val tasks = metadata(queueFile.readText())
|
|
183
|
+
check((0 until tasks.length()).any { index ->
|
|
184
|
+
val task = tasks.getJSONObject(index)
|
|
185
|
+
task.optString("id") == taskId && task.optString("localPath") == localPath &&
|
|
186
|
+
task.optString("status") == "completed"
|
|
187
|
+
}) { "recording release requires exact durable completion" }
|
|
188
|
+
removeCompletedFile(localPath)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private fun removeCompletedFile(path: String) {
|
|
193
|
+
requireOwnedFile(path)
|
|
194
|
+
val file = File(path)
|
|
195
|
+
require(!file.exists() || (file.isFile && !Files.isSymbolicLink(file.toPath()))) { "recording release requires a regular file" }
|
|
196
|
+
check(file.delete() || !file.exists()) { "completed recording cleanup deferred" }
|
|
197
|
+
// A missing file may be an earlier unlink whose directory sync failed.
|
|
198
|
+
syncDirectory(requireNotNull(file.parentFile))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private fun requireOwnedFile(path: String) {
|
|
202
|
+
val file = File(path)
|
|
203
|
+
require(file.isAbsolute && file.extension == "recording" &&
|
|
204
|
+
runCatching { java.util.UUID.fromString(file.nameWithoutExtension) }.isSuccess &&
|
|
205
|
+
file.canonicalFile.parentFile == recordingDirectory.canonicalFile &&
|
|
206
|
+
!Files.isSymbolicLink(file.toPath()) && (!file.exists() || file.isFile)
|
|
207
|
+
) { "recording file is not owned by the native recording store" }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private fun writeQueue(serializedTasks: String) {
|
|
211
|
+
journalWriteUncertain = true
|
|
122
212
|
queueFile.parentFile?.mkdirs()
|
|
213
|
+
queueFile.parentFile?.parentFile?.let(syncDirectory)
|
|
123
214
|
val temporary = File(queueFile.parentFile, "${queueFile.name}.tmp")
|
|
124
|
-
temporary.
|
|
215
|
+
FileOutputStream(temporary).use { stream ->
|
|
216
|
+
stream.write(serializedTasks.toByteArray(Charsets.UTF_8))
|
|
217
|
+
stream.fd.sync()
|
|
218
|
+
}
|
|
125
219
|
try {
|
|
126
220
|
Files.move(
|
|
127
221
|
temporary.toPath(),
|
|
@@ -136,6 +230,41 @@ internal class BotaDeviceSDKAndroidRecordingUploads(
|
|
|
136
230
|
StandardCopyOption.REPLACE_EXISTING,
|
|
137
231
|
)
|
|
138
232
|
}
|
|
233
|
+
queueFile.parentFile?.let(syncDirectory)
|
|
234
|
+
journalWriteUncertain = false
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private fun metadata(serialized: String): JSONArray {
|
|
238
|
+
val tasks = JSONArray(serialized)
|
|
239
|
+
val fields = setOf("id", "recordingId", "deviceId", "recordingUuid", "localPath", "status", "retryCount", "createdAt", "updatedAt", "recoveryScope", "fileSizeBytes", "nextAttemptAt", "contentType", "contentSha256", "relayUpload")
|
|
240
|
+
val ids = mutableSetOf<String>()
|
|
241
|
+
return JSONArray().apply {
|
|
242
|
+
for (index in 0 until tasks.length()) {
|
|
243
|
+
val task = tasks.getJSONObject(index)
|
|
244
|
+
require(task.get("id") is String && task.getString("id").isNotEmpty() && ids.add(task.getString("id"))) { "invalid upload recovery journal" }
|
|
245
|
+
for (key in listOf("recordingId", "deviceId", "localPath", "createdAt", "updatedAt")) require(task.get(key) is String) { "invalid upload recovery journal" }
|
|
246
|
+
require(task.getString("recordingId").isNotEmpty() && task.getString("deviceId").isNotEmpty()) { "invalid upload recovery journal" }
|
|
247
|
+
require(validInteger(task.get("retryCount")) && task.getString("status") in setOf("pending", "uploading", "completed", "failed")) { "invalid upload recovery journal" }
|
|
248
|
+
for (key in listOf("recordingUuid", "recoveryScope", "contentType", "contentSha256")) if (task.has(key)) require(task.get(key) is String) { "invalid upload recovery journal" }
|
|
249
|
+
for (key in listOf("fileSizeBytes", "nextAttemptAt")) if (task.has(key)) require(validInteger(task.get(key))) { "invalid upload recovery journal" }
|
|
250
|
+
if (task.has("relayUpload")) require(task.get("relayUpload") is Boolean) { "invalid upload recovery journal" }
|
|
251
|
+
for (key in listOf("createdAt", "updatedAt")) require(runCatching { java.time.Instant.parse(task.getString(key)) }.isSuccess) { "invalid upload recovery journal" }
|
|
252
|
+
put(JSONObject().apply {
|
|
253
|
+
for (key in fields) if (task.has(key)) {
|
|
254
|
+
val value = task.get(key)
|
|
255
|
+
require(value is String || value is Number || value is Boolean) { "invalid upload recovery journal" }
|
|
256
|
+
put(key, value)
|
|
257
|
+
}
|
|
258
|
+
put("relayUpload", if (task.has("relayUpload")) task.getBoolean("relayUpload") else task.optJSONObject("relay") != null)
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private fun validInteger(value: Any): Boolean {
|
|
265
|
+
if (value !is Number) return false
|
|
266
|
+
val number = value.toDouble()
|
|
267
|
+
return number.isFinite() && number >= 0 && number <= 9_007_199_254_740_991.0 && number % 1.0 == 0.0
|
|
139
268
|
}
|
|
140
269
|
}
|
|
141
270
|
|
|
@@ -151,6 +280,7 @@ internal fun ReadableMap.toRecordingUploadRequest(): BotaRecordingUploadRequest
|
|
|
151
280
|
contentSha256 = optionalString("contentSha256"),
|
|
152
281
|
relayUrl = optionalString("relayUrl"),
|
|
153
282
|
relayBearerToken = optionalString("relayBearerToken"),
|
|
283
|
+
fileSizeBytes = if (hasKey("fileSizeBytes") && !isNull("fileSizeBytes")) getDouble("fileSizeBytes").toLong() else null,
|
|
154
284
|
)
|
|
155
285
|
|
|
156
286
|
internal fun BotaRecordingUploadProgress.toWritableMap(): WritableMap =
|