@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.
- package/BotaDeviceSDK.podspec +55 -0
- package/LICENSE +21 -0
- package/README.md +101 -0
- package/android/build.gradle +76 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDevices.kt +172 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLifecycle.kt +100 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogs.kt +82 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidModels.kt +476 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidOTA.kt +58 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploads.kt +180 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordings.kt +751 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidSecurity.kt +509 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidWiFi.kt +125 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKEncryptedUploadV2Materials.kt +46 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKModule.kt +723 -0
- package/android/src/main/java/dev/bota/sdk/reactnative/BotaDeviceSDKPackage.kt +33 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidDevicesTest.kt +211 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLifecycleTest.kt +132 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidLogsTest.kt +57 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidOTATest.kt +75 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingUploadsTest.kt +147 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidRecordingsTest.kt +355 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidSecurityTest.kt +523 -0
- package/android/src/test/java/dev/bota/sdk/reactnative/BotaDeviceSDKAndroidWiFiTest.kt +100 -0
- package/generated/codegen-contract.json +3593 -0
- package/ios/BotaDeviceSDK.mm +1300 -0
- package/ios/BotaDeviceSDKAppleBridge.swift +2260 -0
- package/ios/BotaDeviceSDKAppleDevices.swift +191 -0
- package/ios/BotaDeviceSDKAppleLifecycle.swift +167 -0
- package/ios/BotaDeviceSDKAppleLogs.swift +79 -0
- package/ios/BotaDeviceSDKAppleOTA.swift +73 -0
- package/ios/BotaDeviceSDKAppleRecordingUploads.swift +178 -0
- package/ios/BotaDeviceSDKAppleRecordings.swift +670 -0
- package/ios/BotaDeviceSDKAppleSecurity.swift +586 -0
- package/ios/BotaDeviceSDKAppleWiFi.swift +128 -0
- package/ios/BotaDeviceSDKEncryptedUploadV2Materials.swift +99 -0
- package/lib/commonjs/BotaClient.js +194 -0
- package/lib/commonjs/BotaClient.js.map +1 -0
- package/lib/commonjs/ble/constants.js +10 -0
- package/lib/commonjs/ble/constants.js.map +1 -0
- package/lib/commonjs/ble/deviceLogs.js +50 -0
- package/lib/commonjs/ble/deviceLogs.js.map +1 -0
- package/lib/commonjs/cache/DeviceStateCache.js +6 -0
- package/lib/commonjs/cache/DeviceStateCache.js.map +1 -0
- package/lib/commonjs/client.js +746 -0
- package/lib/commonjs/client.js.map +1 -0
- package/lib/commonjs/compatibility/runtime.js +44 -0
- package/lib/commonjs/compatibility/runtime.js.map +1 -0
- package/lib/commonjs/index.js +123 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/managers/DeviceManager.js +647 -0
- package/lib/commonjs/managers/DeviceManager.js.map +1 -0
- package/lib/commonjs/managers/OTAManager.js +140 -0
- package/lib/commonjs/managers/OTAManager.js.map +1 -0
- package/lib/commonjs/managers/RecordingManager.js +444 -0
- package/lib/commonjs/managers/RecordingManager.js.map +1 -0
- package/lib/commonjs/managers/StreamingSession.js +175 -0
- package/lib/commonjs/managers/StreamingSession.js.map +1 -0
- package/lib/commonjs/managers/types.js +6 -0
- package/lib/commonjs/managers/types.js.map +1 -0
- package/lib/commonjs/models/Device.js +2 -0
- package/lib/commonjs/models/Device.js.map +1 -0
- package/lib/commonjs/models/Recording.js +2 -0
- package/lib/commonjs/models/Recording.js.map +1 -0
- package/lib/commonjs/models/Status.js +6 -0
- package/lib/commonjs/models/Status.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/specs/NativeBotaDeviceSDK.js +9 -0
- package/lib/commonjs/specs/NativeBotaDeviceSDK.js.map +1 -0
- package/lib/commonjs/sync/syncStatus.js +178 -0
- package/lib/commonjs/sync/syncStatus.js.map +1 -0
- package/lib/commonjs/utils/errors.js +194 -0
- package/lib/commonjs/utils/errors.js.map +1 -0
- package/lib/commonjs/utils/logger.js +84 -0
- package/lib/commonjs/utils/logger.js.map +1 -0
- package/lib/module/BotaClient.js +189 -0
- package/lib/module/BotaClient.js.map +1 -0
- package/lib/module/ble/constants.js +6 -0
- package/lib/module/ble/constants.js.map +1 -0
- package/lib/module/ble/deviceLogs.js +45 -0
- package/lib/module/ble/deviceLogs.js.map +1 -0
- package/lib/module/cache/DeviceStateCache.js +4 -0
- package/lib/module/cache/DeviceStateCache.js.map +1 -0
- package/lib/module/client.js +740 -0
- package/lib/module/client.js.map +1 -0
- package/lib/module/compatibility/runtime.js +35 -0
- package/lib/module/compatibility/runtime.js.map +1 -0
- package/lib/module/index.js +17 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/managers/DeviceManager.js +641 -0
- package/lib/module/managers/DeviceManager.js.map +1 -0
- package/lib/module/managers/OTAManager.js +134 -0
- package/lib/module/managers/OTAManager.js.map +1 -0
- package/lib/module/managers/RecordingManager.js +438 -0
- package/lib/module/managers/RecordingManager.js.map +1 -0
- package/lib/module/managers/StreamingSession.js +168 -0
- package/lib/module/managers/StreamingSession.js.map +1 -0
- package/lib/module/managers/types.js +4 -0
- package/lib/module/managers/types.js.map +1 -0
- package/lib/module/models/Device.js +2 -0
- package/lib/module/models/Device.js.map +1 -0
- package/lib/module/models/Recording.js +2 -0
- package/lib/module/models/Recording.js.map +1 -0
- package/lib/module/models/Status.js +4 -0
- package/lib/module/models/Status.js.map +1 -0
- package/lib/module/specs/NativeBotaDeviceSDK.js +5 -0
- package/lib/module/specs/NativeBotaDeviceSDK.js.map +1 -0
- package/lib/module/sync/syncStatus.js +174 -0
- package/lib/module/sync/syncStatus.js.map +1 -0
- package/lib/module/utils/errors.js +183 -0
- package/lib/module/utils/errors.js.map +1 -0
- package/lib/module/utils/logger.js +80 -0
- package/lib/module/utils/logger.js.map +1 -0
- package/lib/typescript/BotaClient.d.ts +40 -0
- package/lib/typescript/BotaClient.d.ts.map +1 -0
- package/lib/typescript/ble/constants.d.ts +4 -0
- package/lib/typescript/ble/constants.d.ts.map +1 -0
- package/lib/typescript/ble/deviceLogs.d.ts +10 -0
- package/lib/typescript/ble/deviceLogs.d.ts.map +1 -0
- package/lib/typescript/cache/DeviceStateCache.d.ts +14 -0
- package/lib/typescript/cache/DeviceStateCache.d.ts.map +1 -0
- package/lib/typescript/client.d.ts +208 -0
- package/lib/typescript/client.d.ts.map +1 -0
- package/lib/typescript/compatibility/runtime.d.ts +11 -0
- package/lib/typescript/compatibility/runtime.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +19 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/managers/DeviceManager.d.ts +117 -0
- package/lib/typescript/managers/DeviceManager.d.ts.map +1 -0
- package/lib/typescript/managers/OTAManager.d.ts +25 -0
- package/lib/typescript/managers/OTAManager.d.ts.map +1 -0
- package/lib/typescript/managers/RecordingManager.d.ts +54 -0
- package/lib/typescript/managers/RecordingManager.d.ts.map +1 -0
- package/lib/typescript/managers/StreamingSession.d.ts +35 -0
- package/lib/typescript/managers/StreamingSession.d.ts.map +1 -0
- package/lib/typescript/managers/types.d.ts +19 -0
- package/lib/typescript/managers/types.d.ts.map +1 -0
- package/lib/typescript/models/Device.d.ts +392 -0
- package/lib/typescript/models/Device.d.ts.map +1 -0
- package/lib/typescript/models/Recording.d.ts +259 -0
- package/lib/typescript/models/Recording.d.ts.map +1 -0
- package/lib/typescript/models/Status.d.ts +106 -0
- package/lib/typescript/models/Status.d.ts.map +1 -0
- package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts +393 -0
- package/lib/typescript/specs/NativeBotaDeviceSDK.d.ts.map +1 -0
- package/lib/typescript/sync/syncStatus.d.ts +123 -0
- package/lib/typescript/sync/syncStatus.d.ts.map +1 -0
- package/lib/typescript/utils/errors.d.ts +83 -0
- package/lib/typescript/utils/errors.d.ts.map +1 -0
- package/lib/typescript/utils/logger.d.ts +36 -0
- package/lib/typescript/utils/logger.d.ts.map +1 -0
- package/package.json +100 -0
- package/scripts/bota_device_sdk_spm_workaround.rb +53 -0
- package/src/BotaClient.ts +231 -0
- package/src/ble/constants.ts +3 -0
- package/src/ble/deviceLogs.ts +61 -0
- package/src/cache/DeviceStateCache.ts +20 -0
- package/src/client.ts +1410 -0
- package/src/compatibility/runtime.ts +55 -0
- package/src/index.ts +146 -0
- package/src/managers/DeviceManager.ts +927 -0
- package/src/managers/OTAManager.ts +195 -0
- package/src/managers/RecordingManager.ts +578 -0
- package/src/managers/StreamingSession.ts +212 -0
- package/src/managers/types.ts +36 -0
- package/src/models/Device.ts +484 -0
- package/src/models/Recording.ts +281 -0
- package/src/models/Status.ts +128 -0
- package/src/specs/NativeBotaDeviceSDK.ts +542 -0
- package/src/sync/syncStatus.ts +243 -0
- package/src/utils/errors.ts +327 -0
- package/src/utils/logger.ts +122 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import BotaAppSDK
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
protocol BotaDeviceSDKAppleDeviceClient: Sendable {
|
|
5
|
+
func startScan(
|
|
6
|
+
timeoutMilliseconds: UInt64,
|
|
7
|
+
allowDuplicates: Bool
|
|
8
|
+
) async throws -> AsyncThrowingStream<DiscoveredDevice, Error>
|
|
9
|
+
func cancelCurrentOperation() async throws
|
|
10
|
+
func connect(device: DiscoveredDevice) async throws -> ConnectedDevice
|
|
11
|
+
func reconnect(
|
|
12
|
+
serialNumber: String,
|
|
13
|
+
hint: DeviceReconnectHint
|
|
14
|
+
) async throws -> ConnectedDevice
|
|
15
|
+
func disconnect() async throws
|
|
16
|
+
func readStatus() async throws -> DeviceStatus
|
|
17
|
+
func statusUpdates() async throws -> AsyncThrowingStream<DeviceStatus, Error>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
struct BotaDeviceSDKSharedAppleDeviceClient: BotaDeviceSDKAppleDeviceClient {
|
|
21
|
+
private let devices: DeviceManager
|
|
22
|
+
|
|
23
|
+
init(client: BotaDeviceClient = .shared) {
|
|
24
|
+
devices = client.devices
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func startScan(
|
|
28
|
+
timeoutMilliseconds: UInt64,
|
|
29
|
+
allowDuplicates: Bool
|
|
30
|
+
) async throws -> AsyncThrowingStream<DiscoveredDevice, Error> {
|
|
31
|
+
try await devices.startScan(
|
|
32
|
+
timeoutMilliseconds: timeoutMilliseconds,
|
|
33
|
+
allowDuplicates: allowDuplicates
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func cancelCurrentOperation() async throws {
|
|
38
|
+
try await devices.cancelCurrentOperation()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func connect(device: DiscoveredDevice) async throws -> ConnectedDevice {
|
|
42
|
+
try await devices.connect(device: device)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func reconnect(
|
|
46
|
+
serialNumber: String,
|
|
47
|
+
hint: DeviceReconnectHint
|
|
48
|
+
) async throws -> ConnectedDevice {
|
|
49
|
+
try await devices.reconnect(serialNumber: serialNumber, hint: hint)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func disconnect() async throws {
|
|
53
|
+
try await devices.disconnect()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func readStatus() async throws -> DeviceStatus {
|
|
57
|
+
try await devices.readStatus()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func statusUpdates() async throws -> AsyncThrowingStream<DeviceStatus, Error> {
|
|
61
|
+
try await devices.statusUpdates()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
actor BotaDeviceSDKAppleDevices {
|
|
66
|
+
private struct ActiveScan {
|
|
67
|
+
let id: UUID
|
|
68
|
+
let task: Task<Void, Never>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private struct ActiveStatusUpdates {
|
|
72
|
+
let id: UUID
|
|
73
|
+
let task: Task<Void, Never>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private let client: any BotaDeviceSDKAppleDeviceClient
|
|
77
|
+
private var activeScan: ActiveScan?
|
|
78
|
+
private var activeStatusUpdates: ActiveStatusUpdates?
|
|
79
|
+
|
|
80
|
+
init(client: any BotaDeviceSDKAppleDeviceClient = BotaDeviceSDKSharedAppleDeviceClient()) {
|
|
81
|
+
self.client = client
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func startScan(
|
|
85
|
+
timeoutMilliseconds: UInt64,
|
|
86
|
+
allowDuplicates: Bool,
|
|
87
|
+
onDevice: @escaping @Sendable (DiscoveredDevice) -> Void,
|
|
88
|
+
onError: @escaping @Sendable (Error) -> Void = { _ in }
|
|
89
|
+
) async throws {
|
|
90
|
+
await stopScan()
|
|
91
|
+
let stream = try await client.startScan(
|
|
92
|
+
timeoutMilliseconds: timeoutMilliseconds,
|
|
93
|
+
allowDuplicates: allowDuplicates
|
|
94
|
+
)
|
|
95
|
+
let id = UUID()
|
|
96
|
+
let task = Task {
|
|
97
|
+
do {
|
|
98
|
+
for try await device in stream {
|
|
99
|
+
try Task.checkCancellation()
|
|
100
|
+
onDevice(device)
|
|
101
|
+
}
|
|
102
|
+
} catch is CancellationError {
|
|
103
|
+
// Explicit stop is not a scan failure.
|
|
104
|
+
} catch {
|
|
105
|
+
onError(error)
|
|
106
|
+
}
|
|
107
|
+
scanFinished(id: id)
|
|
108
|
+
}
|
|
109
|
+
activeScan = ActiveScan(id: id, task: task)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
func stopScan() async {
|
|
113
|
+
guard let scan = activeScan else { return }
|
|
114
|
+
activeScan = nil
|
|
115
|
+
scan.task.cancel()
|
|
116
|
+
try? await client.cancelCurrentOperation()
|
|
117
|
+
await scan.task.value
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func connect(_ device: DiscoveredDevice) async throws -> ConnectedDevice {
|
|
121
|
+
await stopScan()
|
|
122
|
+
await stopStatusUpdates()
|
|
123
|
+
return try await client.connect(device: device)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func reconnect(
|
|
127
|
+
serialNumber: String,
|
|
128
|
+
hint: DeviceReconnectHint
|
|
129
|
+
) async throws -> ConnectedDevice {
|
|
130
|
+
await stopScan()
|
|
131
|
+
await stopStatusUpdates()
|
|
132
|
+
return try await client.reconnect(serialNumber: serialNumber, hint: hint)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
func disconnect() async throws {
|
|
136
|
+
await stopScan()
|
|
137
|
+
await stopStatusUpdates()
|
|
138
|
+
try await client.disconnect()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func readStatus() async throws -> DeviceStatus {
|
|
142
|
+
try await client.readStatus()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
func startStatusUpdates(
|
|
146
|
+
onStatus: @escaping @Sendable (DeviceStatus) -> Void,
|
|
147
|
+
onError: @escaping @Sendable (Error) -> Void = { _ in }
|
|
148
|
+
) async throws {
|
|
149
|
+
await stopStatusUpdates()
|
|
150
|
+
let stream = try await client.statusUpdates()
|
|
151
|
+
let id = UUID()
|
|
152
|
+
let task = Task {
|
|
153
|
+
do {
|
|
154
|
+
for try await status in stream {
|
|
155
|
+
try Task.checkCancellation()
|
|
156
|
+
onStatus(status)
|
|
157
|
+
}
|
|
158
|
+
} catch is CancellationError {
|
|
159
|
+
// Explicit stop is not a status subscription failure.
|
|
160
|
+
} catch {
|
|
161
|
+
onError(error)
|
|
162
|
+
}
|
|
163
|
+
statusUpdatesFinished(id: id)
|
|
164
|
+
}
|
|
165
|
+
activeStatusUpdates = ActiveStatusUpdates(id: id, task: task)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
func stopStatusUpdates() async {
|
|
169
|
+
guard let updates = activeStatusUpdates else { return }
|
|
170
|
+
activeStatusUpdates = nil
|
|
171
|
+
updates.task.cancel()
|
|
172
|
+
await updates.task.value
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
func stopAll() async {
|
|
176
|
+
await stopScan()
|
|
177
|
+
await stopStatusUpdates()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private func scanFinished(id: UUID) {
|
|
181
|
+
if activeScan?.id == id {
|
|
182
|
+
activeScan = nil
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private func statusUpdatesFinished(id: UUID) {
|
|
187
|
+
if activeStatusUpdates?.id == id {
|
|
188
|
+
activeStatusUpdates = nil
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import BotaAppSDK
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
protocol BotaDeviceSDKAppleClient: Sendable {
|
|
5
|
+
func configure(applicationSupportDirectory: URL?) async throws
|
|
6
|
+
func destroy() async
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
struct BotaDeviceSDKSharedAppleClient: BotaDeviceSDKAppleClient {
|
|
10
|
+
private let client: BotaDeviceClient
|
|
11
|
+
|
|
12
|
+
init(client: BotaDeviceClient = .shared) {
|
|
13
|
+
self.client = client
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func configure(applicationSupportDirectory: URL?) async throws {
|
|
17
|
+
try await client.configure(
|
|
18
|
+
BotaConfiguration(applicationSupportDirectory: applicationSupportDirectory)
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func destroy() async {
|
|
23
|
+
await client.destroy()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
struct BotaDeviceSDKAppleCapabilities: Equatable, Sendable {
|
|
28
|
+
let backgroundReconnect: Bool
|
|
29
|
+
let backgroundScan: Bool
|
|
30
|
+
let bluetooth: Bool
|
|
31
|
+
let nativeFileTransfer: Bool
|
|
32
|
+
let platform: String
|
|
33
|
+
|
|
34
|
+
static let current = Self(
|
|
35
|
+
backgroundReconnect: false,
|
|
36
|
+
backgroundScan: false,
|
|
37
|
+
bluetooth: true,
|
|
38
|
+
nativeFileTransfer: true,
|
|
39
|
+
platform: "ios"
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
actor BotaDeviceSDKAppleLifecycle {
|
|
44
|
+
private enum Phase {
|
|
45
|
+
case uninitialized
|
|
46
|
+
case configuring(UUID, Task<Void, Error>)
|
|
47
|
+
case ready
|
|
48
|
+
case error
|
|
49
|
+
case destroying(UUID, Task<Void, Never>)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private let client: any BotaDeviceSDKAppleClient
|
|
53
|
+
private var phase = Phase.uninitialized
|
|
54
|
+
private var destroyingWaiters: [CheckedContinuation<Void, Never>] = []
|
|
55
|
+
|
|
56
|
+
init(client: any BotaDeviceSDKAppleClient = BotaDeviceSDKSharedAppleClient()) {
|
|
57
|
+
self.client = client
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func configure(applicationSupportDirectory: URL?) async throws {
|
|
61
|
+
while true {
|
|
62
|
+
switch phase {
|
|
63
|
+
case .ready:
|
|
64
|
+
return
|
|
65
|
+
case let .configuring(id, task):
|
|
66
|
+
try await finishConfigure(id: id, task: task)
|
|
67
|
+
return
|
|
68
|
+
case let .destroying(id, task):
|
|
69
|
+
await task.value
|
|
70
|
+
finishDestroy(id: id)
|
|
71
|
+
case .uninitialized, .error:
|
|
72
|
+
let id = UUID()
|
|
73
|
+
let task = Task {
|
|
74
|
+
try await client.configure(
|
|
75
|
+
applicationSupportDirectory: applicationSupportDirectory
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
phase = .configuring(id, task)
|
|
79
|
+
try await finishConfigure(id: id, task: task)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func destroy() async {
|
|
86
|
+
while true {
|
|
87
|
+
switch phase {
|
|
88
|
+
case .uninitialized:
|
|
89
|
+
return
|
|
90
|
+
case let .configuring(_, configureTask):
|
|
91
|
+
let id = UUID()
|
|
92
|
+
let task = Task {
|
|
93
|
+
_ = try? await configureTask.value
|
|
94
|
+
await client.destroy()
|
|
95
|
+
}
|
|
96
|
+
beginDestroying(id: id, task: task)
|
|
97
|
+
await task.value
|
|
98
|
+
finishDestroy(id: id)
|
|
99
|
+
return
|
|
100
|
+
case let .destroying(id, task):
|
|
101
|
+
await task.value
|
|
102
|
+
finishDestroy(id: id)
|
|
103
|
+
return
|
|
104
|
+
case .ready, .error:
|
|
105
|
+
let id = UUID()
|
|
106
|
+
let task = Task { await client.destroy() }
|
|
107
|
+
beginDestroying(id: id, task: task)
|
|
108
|
+
await task.value
|
|
109
|
+
finishDestroy(id: id)
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func state() -> String {
|
|
116
|
+
switch phase {
|
|
117
|
+
case .uninitialized, .destroying:
|
|
118
|
+
"uninitialized"
|
|
119
|
+
case .configuring:
|
|
120
|
+
"initializing"
|
|
121
|
+
case .ready:
|
|
122
|
+
"ready"
|
|
123
|
+
case .error:
|
|
124
|
+
"error"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
func capabilities() -> BotaDeviceSDKAppleCapabilities {
|
|
129
|
+
.current
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
func waitUntilDestroying() async {
|
|
133
|
+
if case .destroying = phase {
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
await withCheckedContinuation { continuation in
|
|
137
|
+
destroyingWaiters.append(continuation)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private func finishConfigure(id: UUID, task: Task<Void, Error>) async throws {
|
|
142
|
+
do {
|
|
143
|
+
try await task.value
|
|
144
|
+
if case let .configuring(activeID, _) = phase, activeID == id {
|
|
145
|
+
phase = .ready
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
if case let .configuring(activeID, _) = phase, activeID == id {
|
|
149
|
+
phase = .error
|
|
150
|
+
}
|
|
151
|
+
throw error
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private func beginDestroying(id: UUID, task: Task<Void, Never>) {
|
|
156
|
+
phase = .destroying(id, task)
|
|
157
|
+
let waiters = destroyingWaiters
|
|
158
|
+
destroyingWaiters.removeAll()
|
|
159
|
+
waiters.forEach { $0.resume() }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private func finishDestroy(id: UUID) {
|
|
163
|
+
if case let .destroying(activeID, _) = phase, activeID == id {
|
|
164
|
+
phase = .uninitialized
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import BotaAppSDK
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
protocol BotaDeviceSDKAppleLogClient: Sendable {
|
|
5
|
+
func streamLogs(
|
|
6
|
+
_ device: ConnectedDevice
|
|
7
|
+
) async throws -> AsyncThrowingStream<DeviceLogLine, Error>
|
|
8
|
+
func stop() async throws
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
struct BotaDeviceSDKSharedAppleLogClient: BotaDeviceSDKAppleLogClient {
|
|
12
|
+
private let logs: DeviceLogManager
|
|
13
|
+
|
|
14
|
+
init(client: BotaDeviceClient = .shared) {
|
|
15
|
+
logs = client.logs
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func streamLogs(
|
|
19
|
+
_ device: ConnectedDevice
|
|
20
|
+
) async throws -> AsyncThrowingStream<DeviceLogLine, Error> {
|
|
21
|
+
try await logs.streamLogs(device)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func stop() async throws {
|
|
25
|
+
try await logs.stop()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
actor BotaDeviceSDKAppleLogs {
|
|
30
|
+
private struct ActiveStream {
|
|
31
|
+
let id: UUID
|
|
32
|
+
let task: Task<Void, Never>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private let client: any BotaDeviceSDKAppleLogClient
|
|
36
|
+
private var activeStream: ActiveStream?
|
|
37
|
+
|
|
38
|
+
init(client: any BotaDeviceSDKAppleLogClient = BotaDeviceSDKSharedAppleLogClient()) {
|
|
39
|
+
self.client = client
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func start(
|
|
43
|
+
_ device: ConnectedDevice,
|
|
44
|
+
onLine: @escaping @Sendable (DeviceLogLine) -> Void,
|
|
45
|
+
onError: @escaping @Sendable (Error) -> Void = { _ in }
|
|
46
|
+
) async throws {
|
|
47
|
+
await stop()
|
|
48
|
+
let stream = try await client.streamLogs(device)
|
|
49
|
+
let id = UUID()
|
|
50
|
+
let task = Task {
|
|
51
|
+
do {
|
|
52
|
+
for try await line in stream {
|
|
53
|
+
try Task.checkCancellation()
|
|
54
|
+
onLine(line)
|
|
55
|
+
}
|
|
56
|
+
} catch is CancellationError {
|
|
57
|
+
// Explicit stop is not a log-stream failure.
|
|
58
|
+
} catch {
|
|
59
|
+
onError(error)
|
|
60
|
+
}
|
|
61
|
+
streamFinished(id: id)
|
|
62
|
+
}
|
|
63
|
+
activeStream = ActiveStream(id: id, task: task)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func stop() async {
|
|
67
|
+
guard let stream = activeStream else { return }
|
|
68
|
+
activeStream = nil
|
|
69
|
+
stream.task.cancel()
|
|
70
|
+
try? await client.stop()
|
|
71
|
+
await stream.task.value
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private func streamFinished(id: UUID) {
|
|
75
|
+
if activeStream?.id == id {
|
|
76
|
+
activeStream = nil
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import BotaAppSDK
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
protocol BotaDeviceSDKAppleOTAClient: Sendable {
|
|
5
|
+
func updateFirmware(
|
|
6
|
+
_ device: ConnectedDevice,
|
|
7
|
+
image: FirmwareImage
|
|
8
|
+
) async throws -> AsyncThrowingStream<FirmwareUpdateProgress, Error>
|
|
9
|
+
func cancelCurrentOperation() async throws
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
struct BotaDeviceSDKSharedAppleOTAClient: BotaDeviceSDKAppleOTAClient {
|
|
13
|
+
private let ota: OTAManager
|
|
14
|
+
|
|
15
|
+
init(client: BotaDeviceClient = .shared) {
|
|
16
|
+
ota = client.ota
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func updateFirmware(
|
|
20
|
+
_ device: ConnectedDevice,
|
|
21
|
+
image: FirmwareImage
|
|
22
|
+
) async throws -> AsyncThrowingStream<FirmwareUpdateProgress, Error> {
|
|
23
|
+
try await ota.updateFirmware(device, image: image)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func cancelCurrentOperation() async throws {
|
|
27
|
+
try await ota.cancelCurrentOperation()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
actor BotaDeviceSDKAppleOTA {
|
|
32
|
+
private enum OTAError: LocalizedError {
|
|
33
|
+
case invalidURL
|
|
34
|
+
|
|
35
|
+
var errorDescription: String? {
|
|
36
|
+
"firmware URL is invalid"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private let client: any BotaDeviceSDKAppleOTAClient
|
|
41
|
+
|
|
42
|
+
init(
|
|
43
|
+
client: any BotaDeviceSDKAppleOTAClient = BotaDeviceSDKSharedAppleOTAClient()
|
|
44
|
+
) {
|
|
45
|
+
self.client = client
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func updateFirmware(
|
|
49
|
+
_ device: ConnectedDevice,
|
|
50
|
+
version: String,
|
|
51
|
+
sizeBytes: UInt32,
|
|
52
|
+
crc32: UInt32,
|
|
53
|
+
url: String,
|
|
54
|
+
onProgress: @escaping @Sendable (FirmwareUpdateProgress) -> Void
|
|
55
|
+
) async throws {
|
|
56
|
+
guard let url = URL(string: url) else { throw OTAError.invalidURL }
|
|
57
|
+
let image = FirmwareImage(
|
|
58
|
+
version: version,
|
|
59
|
+
sizeBytes: sizeBytes,
|
|
60
|
+
crc32: crc32,
|
|
61
|
+
downloadID: UInt64.random(in: 1 ... UInt64.max),
|
|
62
|
+
request: URLRequest(url: url)
|
|
63
|
+
)
|
|
64
|
+
let progress = try await client.updateFirmware(device, image: image)
|
|
65
|
+
for try await value in progress {
|
|
66
|
+
onProgress(value)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func cancelAll() async {
|
|
71
|
+
try? await client.cancelCurrentOperation()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
struct BotaDeviceSDKAppleRecordingUploadRequest: Sendable {
|
|
4
|
+
let taskID: String
|
|
5
|
+
let recordingID: String
|
|
6
|
+
let localPath: String
|
|
7
|
+
let uploadURL: String
|
|
8
|
+
let uploadToken: String?
|
|
9
|
+
let completeURL: String?
|
|
10
|
+
let contentType: String?
|
|
11
|
+
let contentSHA256: String?
|
|
12
|
+
let relayURL: String?
|
|
13
|
+
let relayBearerToken: String?
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
struct BotaDeviceSDKAppleRecordingUploadProgress: Equatable, Sendable {
|
|
17
|
+
let taskID: String
|
|
18
|
+
let completedBytes: Int64
|
|
19
|
+
let totalBytes: Int64
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
actor BotaDeviceSDKAppleRecordingUploads {
|
|
23
|
+
private enum UploadError: LocalizedError {
|
|
24
|
+
case alreadyActive(String)
|
|
25
|
+
case invalidResponse
|
|
26
|
+
case missingFile(String)
|
|
27
|
+
case requestFailed(Int)
|
|
28
|
+
|
|
29
|
+
var errorDescription: String? {
|
|
30
|
+
switch self {
|
|
31
|
+
case let .alreadyActive(taskID):
|
|
32
|
+
"recording upload is already active: \(taskID)"
|
|
33
|
+
case .invalidResponse:
|
|
34
|
+
"recording upload returned an invalid HTTP response"
|
|
35
|
+
case let .missingFile(path):
|
|
36
|
+
"recording file does not exist: \(path)"
|
|
37
|
+
case let .requestFailed(statusCode):
|
|
38
|
+
"recording upload failed with HTTP \(statusCode)"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private var queueFile: URL
|
|
44
|
+
private let session: URLSession
|
|
45
|
+
private var operations: [String: Task<Void, Error>] = [:]
|
|
46
|
+
|
|
47
|
+
init(
|
|
48
|
+
queueFile: URL = BotaDeviceSDKAppleRecordingUploads.defaultQueueFile(),
|
|
49
|
+
session: URLSession = .shared
|
|
50
|
+
) {
|
|
51
|
+
self.queueFile = queueFile
|
|
52
|
+
self.session = session
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func configure(applicationSupportDirectory: URL?) {
|
|
56
|
+
guard let applicationSupportDirectory else { return }
|
|
57
|
+
queueFile = applicationSupportDirectory
|
|
58
|
+
.appendingPathComponent("BotaDeviceSDK", isDirectory: true)
|
|
59
|
+
.appendingPathComponent("compat-upload-queue.json")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func upload(
|
|
63
|
+
_ upload: BotaDeviceSDKAppleRecordingUploadRequest,
|
|
64
|
+
onProgress: @escaping @Sendable (BotaDeviceSDKAppleRecordingUploadProgress) -> Void
|
|
65
|
+
) async throws {
|
|
66
|
+
guard operations[upload.taskID] == nil else {
|
|
67
|
+
throw UploadError.alreadyActive(upload.taskID)
|
|
68
|
+
}
|
|
69
|
+
let session = session
|
|
70
|
+
let operation = Task {
|
|
71
|
+
try await Self.performUpload(upload, session: session, onProgress: onProgress)
|
|
72
|
+
}
|
|
73
|
+
operations[upload.taskID] = operation
|
|
74
|
+
defer { operations.removeValue(forKey: upload.taskID) }
|
|
75
|
+
try await operation.value
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func cancel(taskID: String) {
|
|
79
|
+
operations.removeValue(forKey: taskID)?.cancel()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func cancelAll() {
|
|
83
|
+
operations.values.forEach { $0.cancel() }
|
|
84
|
+
operations.removeAll()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func loadQueue() throws -> String {
|
|
88
|
+
guard FileManager.default.fileExists(atPath: queueFile.path) else { return "[]" }
|
|
89
|
+
return try String(contentsOf: queueFile, encoding: .utf8)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func saveQueue(_ serializedTasks: String) throws {
|
|
93
|
+
try FileManager.default.createDirectory(
|
|
94
|
+
at: queueFile.deletingLastPathComponent(),
|
|
95
|
+
withIntermediateDirectories: true
|
|
96
|
+
)
|
|
97
|
+
try Data(serializedTasks.utf8).write(to: queueFile, options: .atomic)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private static func performUpload(
|
|
101
|
+
_ upload: BotaDeviceSDKAppleRecordingUploadRequest,
|
|
102
|
+
session: URLSession,
|
|
103
|
+
onProgress: @escaping @Sendable (BotaDeviceSDKAppleRecordingUploadProgress) -> Void
|
|
104
|
+
) async throws {
|
|
105
|
+
let fileURL = URL(fileURLWithPath: upload.localPath)
|
|
106
|
+
guard FileManager.default.fileExists(atPath: fileURL.path) else {
|
|
107
|
+
throw UploadError.missingFile(upload.localPath)
|
|
108
|
+
}
|
|
109
|
+
let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path)
|
|
110
|
+
let totalBytes = (attributes[.size] as? NSNumber)?.int64Value ?? 0
|
|
111
|
+
onProgress(.init(taskID: upload.taskID, completedBytes: 0, totalBytes: totalBytes))
|
|
112
|
+
|
|
113
|
+
let relay = upload.relayURL != nil
|
|
114
|
+
let destination = try Self.requiredURL(upload.relayURL ?? upload.uploadURL)
|
|
115
|
+
var request = URLRequest(url: destination)
|
|
116
|
+
request.httpMethod = relay ? "POST" : "PUT"
|
|
117
|
+
request.setValue(
|
|
118
|
+
relay ? "application/octet-stream" : (upload.contentType ?? "audio/opus"),
|
|
119
|
+
forHTTPHeaderField: "Content-Type"
|
|
120
|
+
)
|
|
121
|
+
if relay, let token = upload.relayBearerToken {
|
|
122
|
+
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
123
|
+
}
|
|
124
|
+
let (_, response) = try await session.upload(for: request, fromFile: fileURL)
|
|
125
|
+
try Self.requireSuccess(response)
|
|
126
|
+
|
|
127
|
+
if !relay,
|
|
128
|
+
let completeURL = upload.completeURL,
|
|
129
|
+
let uploadToken = upload.uploadToken
|
|
130
|
+
{
|
|
131
|
+
var completeRequest = URLRequest(url: try requiredURL(completeURL))
|
|
132
|
+
completeRequest.httpMethod = "POST"
|
|
133
|
+
completeRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
134
|
+
completeRequest.setValue(
|
|
135
|
+
"Bearer \(uploadToken)",
|
|
136
|
+
forHTTPHeaderField: "Authorization"
|
|
137
|
+
)
|
|
138
|
+
var body: [String: String] = ["recording_id": upload.recordingID]
|
|
139
|
+
if let contentSHA256 = upload.contentSHA256 {
|
|
140
|
+
body["content_sha256"] = contentSHA256
|
|
141
|
+
}
|
|
142
|
+
completeRequest.httpBody = try JSONSerialization.data(withJSONObject: body)
|
|
143
|
+
let (_, completeResponse) = try await session.data(for: completeRequest)
|
|
144
|
+
try requireSuccess(completeResponse)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
onProgress(.init(
|
|
148
|
+
taskID: upload.taskID,
|
|
149
|
+
completedBytes: totalBytes,
|
|
150
|
+
totalBytes: totalBytes
|
|
151
|
+
))
|
|
152
|
+
try FileManager.default.removeItem(at: fileURL)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private static func requiredURL(_ value: String) throws -> URL {
|
|
156
|
+
guard let url = URL(string: value) else { throw UploadError.invalidResponse }
|
|
157
|
+
return url
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private static func requireSuccess(_ response: URLResponse) throws {
|
|
161
|
+
guard let http = response as? HTTPURLResponse else {
|
|
162
|
+
throw UploadError.invalidResponse
|
|
163
|
+
}
|
|
164
|
+
guard (200 ... 299).contains(http.statusCode) else {
|
|
165
|
+
throw UploadError.requestFailed(http.statusCode)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private static func defaultQueueFile() -> URL {
|
|
170
|
+
let base = FileManager.default.urls(
|
|
171
|
+
for: .applicationSupportDirectory,
|
|
172
|
+
in: .userDomainMask
|
|
173
|
+
).first ?? FileManager.default.temporaryDirectory
|
|
174
|
+
return base
|
|
175
|
+
.appendingPathComponent("BotaDeviceSDK", isDirectory: true)
|
|
176
|
+
.appendingPathComponent("compat-upload-queue.json")
|
|
177
|
+
}
|
|
178
|
+
}
|