@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,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
+ }