Skip to content

Commit 1e19fd0

Browse files
committed
Create BGContinuedProcessingTask to upload media
1 parent 51d8072 commit 1e19fd0

5 files changed

Lines changed: 260 additions & 11 deletions

File tree

Sources/Jetpack/Info.plist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<array>
77
<string>org.wordpress.bgtask.weeklyroundup</string>
88
<string>org.wordpress.bgtask.weeklyroundup.processing</string>
9+
<string>$(PRODUCT_BUNDLE_IDENTIFIER).mediaUpload</string>
910
</array>
1011
<key>CFBundleDevelopmentRegion</key>
1112
<string>en</string>

Sources/WordPress/Info.plist

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<array>
77
<string>org.wordpress.bgtask.weeklyroundup</string>
88
<string>org.wordpress.bgtask.weeklyroundup.processing</string>
9+
<string>$(PRODUCT_BUNDLE_IDENTIFIER).mediaUpload</string>
910
</array>
1011
<key>CFBundleDevelopmentRegion</key>
1112
<string>en</string>
@@ -191,13 +192,13 @@
191192
<key>UIPrerenderedIcon</key>
192193
<true/>
193194
</dict>
194-
<key>Spectrum &apos;22</key>
195+
<key>Spectrum '22</key>
195196
<dict>
196197
<key>CFBundleIconFiles</key>
197198
<array>
198-
<string>spectrum-&apos;22-icon-app-60x60</string>
199-
<string>spectrum-&apos;22-icon-app-76x76</string>
200-
<string>spectrum-&apos;22-icon-app-83.5x83.5</string>
199+
<string>spectrum-'22-icon-app-60x60</string>
200+
<string>spectrum-'22-icon-app-76x76</string>
201+
<string>spectrum-'22-icon-app-83.5x83.5</string>
201202
</array>
202203
<key>UIPrerenderedIcon</key>
203204
<true/>
@@ -406,13 +407,13 @@
406407
<key>UIPrerenderedIcon</key>
407408
<true/>
408409
</dict>
409-
<key>Spectrum &apos;22</key>
410+
<key>Spectrum '22</key>
410411
<dict>
411412
<key>CFBundleIconFiles</key>
412413
<array>
413-
<string>spectrum-&apos;22-icon-app-60x60</string>
414-
<string>spectrum-&apos;22-icon-app-76x76</string>
415-
<string>spectrum-&apos;22-icon-app-83.5x83.5</string>
414+
<string>spectrum-'22-icon-app-60x60</string>
415+
<string>spectrum-'22-icon-app-76x76</string>
416+
<string>spectrum-'22-icon-app-83.5x83.5</string>
416417
</array>
417418
<key>UIPrerenderedIcon</key>
418419
<true/>

WordPress/Classes/Services/MediaCoordinator.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ class MediaCoordinator: NSObject {
142142
/// - parameter origin: The location in the app where the upload was initiated (optional).
143143
///
144144
func addMedia(from asset: ExportableAsset, to blog: Blog, analyticsInfo: MediaAnalyticsInfo? = nil) {
145-
addMedia(from: asset, blog: blog, post: nil, coordinator: mediaLibraryProgressCoordinator, analyticsInfo: analyticsInfo)
145+
addMedia(from: asset, blog: blog, coordinator: mediaLibraryProgressCoordinator, analyticsInfo: analyticsInfo)
146146
}
147147

148148
/// Adds the specified media asset to the specified post. The upload process
@@ -192,14 +192,14 @@ class MediaCoordinator: NSObject {
192192
/// Create a `Media` instance and upload the asset to the Media Library.
193193
///
194194
/// - SeeAlso: `MediaImportService.createMedia(with:blog:post:receiveUpdate:thumbnailCallback:completion:)`
195-
private func addMedia(from asset: ExportableAsset, blog: Blog, post: AbstractPost?, coordinator: MediaProgressCoordinator, analyticsInfo: MediaAnalyticsInfo? = nil) {
195+
private func addMedia(from asset: ExportableAsset, blog: Blog, coordinator: MediaProgressCoordinator, analyticsInfo: MediaAnalyticsInfo? = nil) {
196196
coordinator.track(numberOfItems: 1)
197197
let service = MediaImportService(coreDataStack: coreDataStack)
198198
let totalProgress = Progress.discreteProgress(totalUnitCount: MediaExportProgressUnits.done)
199199
let creationProgress = service.createMedia(
200200
with: asset,
201201
blog: blog,
202-
post: post,
202+
post: nil,
203203
receiveUpdate: { [weak self] media in
204204
self?.processing(media)
205205
coordinator.track(progress: totalProgress, of: media, withIdentifier: media.uploadID)

WordPress/Classes/ViewRelated/Aztec/Media/MediaProgressCoordinator.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public class MediaProgressCoordinator: NSObject {
3434

3535
private static var mediaProgressObserverContext = 0
3636

37+
private lazy var bgTaskTracker: MediaUploadBackgroundTracker? = mediaUploadBackgroundTracker()
38+
3739
deinit {
3840
mediaGlobalProgress?.removeObserver(self, forKeyPath: #keyPath(Progress.fractionCompleted))
3941
}
@@ -72,6 +74,11 @@ public class MediaProgressCoordinator: NSObject {
7274
progress.setUserInfoObject(media, forKey: .mediaObject)
7375
mediaGlobalProgress?.addChild(progress, withPendingUnitCount: 1)
7476
mediaInProgress[mediaID] = progress
77+
78+
let objectID = TaggedManagedObjectID(media)
79+
Task {
80+
await bgTaskTracker?.track(progress: progress, media: objectID)
81+
}
7582
}
7683

7784
/// Finish one of the tasks.
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import Foundation
2+
import BackgroundTasks
3+
import Combine
4+
import WordPressShared
5+
6+
// This protocol is used to hide the `@available(iOS 26.0, *)` check.
7+
protocol MediaUploadBackgroundTracker {
8+
9+
func track(progress: Progress, media: TaggedManagedObjectID<Media>) async
10+
11+
}
12+
13+
func mediaUploadBackgroundTracker() -> MediaUploadBackgroundTracker? {
14+
if #available(iOS 26.0, *) {
15+
ConcreteMediaUploadBackgroundTracker.shared
16+
} else {
17+
nil
18+
}
19+
}
20+
21+
@available(iOS 26.0, *)
22+
/// Utilize `BGContinuedProcessingTask` to show the uploading media activity.
23+
private actor ConcreteMediaUploadBackgroundTracker: MediaUploadBackgroundTracker {
24+
struct Item {
25+
var media: TaggedManagedObjectID<Media>
26+
var progress: Progress
27+
}
28+
29+
enum BGTaskState {
30+
struct Accepted {
31+
let task: BGContinuedProcessingTask
32+
var items = [Item]()
33+
var observers: [AnyCancellable] = []
34+
35+
init(task: BGContinuedProcessingTask) {
36+
self.task = task
37+
}
38+
}
39+
40+
// No uploading. No `BGContinuedProcessingTask`.
41+
case idle
42+
// Waiting for the OS to response to the creating `BGContinuedProcessingTask` request.
43+
case pending([Item])
44+
// OS has created a `BGContinuedProcessingTask` instance.
45+
case accepted(Accepted)
46+
}
47+
48+
// Since this type works with `BGTaskScheduler.shared`, it only makes sense for the type to also be a singleton.
49+
static let shared = ConcreteMediaUploadBackgroundTracker()
50+
51+
// We only use one `BGContinuedProcessingTask` for all uploads. When adding new media during uploading, the new ones
52+
// will be added to the existing task.
53+
private let taskId: String
54+
55+
// State transtion: idle -> pending -> accepted -> [accepted...] -> idle.
56+
private var state: BGTaskState = .idle
57+
58+
private init?() {
59+
let taskId = (Bundle.main.infoDictionary?["BGTaskSchedulerPermittedIdentifiers"] as? [String])?.first {
60+
$0.hasSuffix(".mediaUpload")
61+
}
62+
guard let taskId else {
63+
wpAssertionFailure("media upload task id not found in the Info.plist")
64+
return nil
65+
}
66+
67+
self.taskId = taskId
68+
BGTaskScheduler.shared.register(forTaskWithIdentifier: self.taskId, using: nil) { [weak self] task in
69+
guard let task = task as? BGContinuedProcessingTask else {
70+
wpAssertionFailure("Unexpected task instance")
71+
return
72+
}
73+
74+
Task {
75+
await self?.taskCreated(task)
76+
}
77+
}
78+
}
79+
80+
func track(progress: Progress, media: TaggedManagedObjectID<Media>) async {
81+
let item = Item(media: media, progress: progress)
82+
switch state {
83+
case .idle:
84+
state = .pending([item])
85+
86+
let request = BGContinuedProcessingTaskRequest(identifier: taskId, title: Strings.uploadingMediaTitle, subtitle: "")
87+
request.strategy = .queue
88+
do {
89+
try BGTaskScheduler.shared.submit(request)
90+
} catch {
91+
DDLogError("Failed to submit a background task: \(error)")
92+
}
93+
case var .pending(items):
94+
items.removeAll {
95+
$0.media == media
96+
}
97+
items.append(item)
98+
self.state = .pending(items)
99+
case var .accepted(accepted):
100+
observe(item, accepted: &accepted)
101+
self.state = .accepted(accepted)
102+
}
103+
}
104+
105+
private func taskCreated(_ task: BGContinuedProcessingTask) {
106+
task.progress.totalUnitCount = 100
107+
task.expirationHandler = { [weak self] in
108+
Task {
109+
await self?.setTaskCompleted(success: false)
110+
}
111+
}
112+
113+
var accepted = BGTaskState.Accepted(task: task)
114+
switch state {
115+
case .idle, .accepted:
116+
wpAssertionFailure("Unexpected background task state")
117+
case let .pending(items):
118+
for item in items {
119+
observe(item, accepted: &accepted)
120+
}
121+
}
122+
123+
self.state = .accepted(accepted)
124+
}
125+
126+
private func observe(_ item: Item, accepted: inout BGTaskState.Accepted) {
127+
accepted.items.append(item)
128+
129+
let progress = item.progress.publisher(for: \.fractionCompleted).sink { [weak self] _ in
130+
Task {
131+
await self?.handleProgressUpdates()
132+
}
133+
}
134+
accepted.observers.append(progress)
135+
136+
Task { @MainActor in
137+
guard let media = try? ContextManager.shared.mainContext.existingObject(with: item.media) else { return }
138+
139+
let completion = media.publisher(for: \.remoteStatusNumber).sink { [weak self] _ in
140+
Task {
141+
await self?.handleStatusUpdates()
142+
}
143+
}
144+
await self.addObserver(completion)
145+
}
146+
}
147+
148+
private func addObserver(_ cancellable: AnyCancellable) {
149+
guard case var .accepted(accepted) = state else { return }
150+
accepted.observers.append(cancellable)
151+
self.state = .accepted(accepted)
152+
}
153+
154+
private func handleProgressUpdates() {
155+
guard case let .accepted(accepted) = state else { return }
156+
157+
let fractionCompleted = accepted.items.map(\.progress.fractionCompleted).reduce(0, +) / Double(accepted.items.count)
158+
accepted.task.progress.completedUnitCount = Int64(fractionCompleted * Double(accepted.task.progress.totalUnitCount))
159+
}
160+
161+
private func handleStatusUpdates() async {
162+
await updateMessaging()
163+
await updateResult()
164+
}
165+
166+
@MainActor
167+
private func updateMessaging() async {
168+
guard case let .accepted(accepted) = await self.state else { return }
169+
170+
let context = ContextManager.shared.mainContext
171+
let mediaItems = accepted.items.compactMap { try? context.existingObject(with: $0.media) }
172+
173+
let failed = mediaItems.count { $0.remoteStatus == .failed }
174+
let success = mediaItems.count { $0.remoteStatus == .sync }
175+
let total = mediaItems.count
176+
177+
var subtitle = [String]()
178+
if total - success - failed > 0 {
179+
subtitle.append(String.localizedStringWithFormat(Strings.uploadingStatus, total - success - failed))
180+
}
181+
if success > 0 {
182+
subtitle.append(String.localizedStringWithFormat(Strings.successStatus, success))
183+
}
184+
if failed > 0 {
185+
subtitle.append(String.localizedStringWithFormat(Strings.failedStatus, failed))
186+
}
187+
188+
accepted.task.updateTitle(Strings.uploadingMediaTitle, subtitle: ListFormatter.localizedString(byJoining: subtitle))
189+
}
190+
191+
@MainActor
192+
private func updateResult() async {
193+
guard case let .accepted(accepted) = await self.state else { return }
194+
195+
let context = ContextManager.shared.mainContext
196+
let mediaItems = accepted.items.compactMap { try? context.existingObject(with: $0.media) }
197+
198+
let completed = mediaItems.allSatisfy { $0.remoteStatus == .sync || $0.remoteStatus == .failed }
199+
guard completed else {
200+
return
201+
}
202+
203+
let success = mediaItems.allSatisfy { $0.remoteStatus == .sync }
204+
await setTaskCompleted(success: success)
205+
}
206+
207+
private func setTaskCompleted(success: Bool) {
208+
guard case let .accepted(accepted) = self.state else { return }
209+
DDLogInfo("BGTask completed with success? \(success)")
210+
211+
accepted.task.setTaskCompleted(success: success)
212+
self.state = .idle
213+
}
214+
}
215+
216+
private enum Strings {
217+
static let uploadingMediaTitle = NSLocalizedString(
218+
"BGTask.mediaUpload.title",
219+
value: "Uploading media",
220+
comment: "Title shown in background task when uploading media files"
221+
)
222+
223+
static let uploadingStatus = NSLocalizedString(
224+
"BGTask.mediaUpload.uploading",
225+
value: "%1$d uploading",
226+
comment: "Status message showing number of files currently uploading. %1$d is the count of uploading files."
227+
)
228+
229+
static let successStatus = NSLocalizedString(
230+
"BGTask.mediaUpload.successful",
231+
value: "%1$d successful",
232+
comment: "Status message showing number of files uploaded successfully. %1$d is the count of successful uploads."
233+
)
234+
235+
static let failedStatus = NSLocalizedString(
236+
"BGTask.mediaUpload.failed",
237+
value: "%1$d failed",
238+
comment: "Status message showing number of files that failed to upload. %1$d is the count of failed uploads."
239+
)
240+
}

0 commit comments

Comments
 (0)