refactor(ios): clean up continued processing task handling (#1547)

This commit is contained in:
UltraGeek0102 authored and GitHub committed 2026-09-25 16:21:09 +08:00
1 parent 363153fa33
commit 8d1ae93b03
4 files changed
+172 -67

No files matched your search

@@ -18,6 +18,7 @@
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
3EF29839EA13551EB8144269 /* GopeedLiveActivityExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 76ED41E0163DFA97F018D1AC /* GopeedLiveActivityExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
5BF69191A426106A38B62F9F /* GopeedContinuedProcessingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3879B6DB5398A19281BA5F8F /* GopeedContinuedProcessingManager.swift */; };
B30000000000000000000001 /* GopeedTaskEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B30000000000000000000002 /* GopeedTaskEvent.swift */; };
6085D504396791024CEC496B /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8823E31DA135C410E362683E /* Pods_RunnerTests.framework */; };
6243BE8F995F5284DBDD7DE3 /* GopeedLiveActivityWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C9F8DE4B991051758F2BE1 /* GopeedLiveActivityWidget.swift */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
@@ -102,6 +103,7 @@
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
35C9F8DE4B991051758F2BE1 /* GopeedLiveActivityWidget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GopeedLiveActivityWidget.swift; sourceTree = "<group>"; };
3879B6DB5398A19281BA5F8F /* GopeedContinuedProcessingManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GopeedContinuedProcessingManager.swift; sourceTree = "<group>"; };
B30000000000000000000002 /* GopeedTaskEvent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GopeedTaskEvent.swift; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
52A13633B52EDC9D3F479DFD /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
55356FCC7E573C3FEEC2DF4D /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
@@ -286,6 +288,7 @@
09AC32C4BF90EAC54D048584 /* GopeedDownloadAttributes.swift */,
EB8C142A455C80367DDD68AE /* GopeedLiveActivityManager.swift */,
3879B6DB5398A19281BA5F8F /* GopeedContinuedProcessingManager.swift */,
B30000000000000000000002 /* GopeedTaskEvent.swift */,
);
path = Runner;
sourceTree = "<group>";
@@ -607,6 +610,7 @@
1E29080079AAFD1CF2C71262 /* GopeedDownloadAttributes.swift in Sources */,
032447D610CFAF8A75642212 /* GopeedLiveActivityManager.swift in Sources */,
5BF69191A426106A38B62F9F /* GopeedContinuedProcessingManager.swift in Sources */,
B30000000000000000000001 /* GopeedTaskEvent.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -22,8 +22,8 @@ final class GopeedContinuedProcessingManager: NSObject {
// MARK: - BGCPT state
private var registeredIdentifiers = Set<String>()
private var pendingTaskIDs = Set<String>()
private var foregroundStartTokens: [String: UUID] = [:]
private struct ExpiringTaskState {
let identifier: String
let generation: UUID
@@ -167,16 +167,11 @@ final class GopeedContinuedProcessingManager: NSObject {
// Pre-mark task.start so the Objective-C forwarder can
// immediately suppress the custom ActivityKit Live Activity.
if
let data = payload.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(
with: data
) as? [String: Any],
let type = json["type"] as? String,
type == "task.start",
let taskID = json["taskId"] as? String
let event = GopeedTaskEvent.decode(payload),
event.type == .start
{
setTaskOwnership(
taskID: taskID,
taskID: event.taskID,
handled: true
)
}
@@ -198,33 +193,19 @@ final class GopeedContinuedProcessingManager: NSObject {
return
}
guard
let data = payload.data(
using: .utf8
),
let json =
try? JSONSerialization
.jsonObject(
with: data
) as? [String: Any],
let type =
json["type"] as? String,
let taskID =
json["taskId"] as? String
else {
guard let event = GopeedTaskEvent.decode(payload) else {
print(
"ContinuedProcessing: invalid event"
)
return
}
let name =
json["name"] as? String
?? "Download"
let taskID = event.taskID
let name = event.name
switch type {
switch event.type {
case "task.start":
case .start:
progressRecoveryTokens.removeValue(
forKey: taskID
)
@@ -234,7 +215,7 @@ final class GopeedContinuedProcessingManager: NSObject {
name: name
)
case "task.progress":
case .progress:
if let generation =
taskGenerations[taskID] {
updateProgress(
@@ -249,7 +230,7 @@ final class GopeedContinuedProcessingManager: NSObject {
)
}
case "task.pause":
case .pause:
if let expiring =
expiringTasks[taskID] {
finishExpirationCleanup(
@@ -265,7 +246,7 @@ final class GopeedContinuedProcessingManager: NSObject {
)
}
case "task.done":
case .done:
finishTask(
taskID: taskID,
success: true,
@@ -273,7 +254,7 @@ final class GopeedContinuedProcessingManager: NSObject {
"Download complete"
)
case "task.error":
case .error:
finishTask(
taskID: taskID,
success: false,
@@ -281,7 +262,7 @@ final class GopeedContinuedProcessingManager: NSObject {
"Download failed"
)
case "task.delete":
case .delete:
finishTask(
taskID: taskID,
success: true,
@@ -289,12 +270,11 @@ final class GopeedContinuedProcessingManager: NSObject {
"Download removed"
)
default:
case .other:
break
}
}
// MARK: - Recover active task after setting changes
private func recoverTaskFromProgress(
@@ -307,6 +287,7 @@ final class GopeedContinuedProcessingManager: NSObject {
!pendingTaskIDs.contains(taskID),
activeTasks[taskID] == nil,
expiringTasks[taskID] == nil,
foregroundStartTokens[taskID] == nil,
progressRecoveryTokens[taskID] == nil
else {
return
@@ -395,7 +376,8 @@ final class GopeedContinuedProcessingManager: NSObject {
guard
!pendingTaskIDs.contains(taskID),
activeTasks[taskID] == nil
activeTasks[taskID] == nil,
foregroundStartTokens[taskID] == nil
else {
setTaskOwnership(
taskID: taskID,
@@ -404,20 +386,57 @@ final class GopeedContinuedProcessingManager: NSObject {
return
}
// BGCPT should originate from a foreground user action.
// This main-thread check happens only once per task start.
let appIsActive: Bool
// BGCPT must originate from a foreground user action.
// Never synchronously hop from workerQueue to the main queue:
// doing so can deadlock if the main thread is waiting on work
// that eventually needs workerQueue.
let foregroundToken = UUID()
foregroundStartTokens[taskID] =
foregroundToken
if Thread.isMainThread {
appIsActive =
DispatchQueue.main.async { [weak self] in
let appIsActive =
UIApplication.shared
.applicationState == .active
} else {
appIsActive =
DispatchQueue.main.sync {
UIApplication.shared
.applicationState == .active
}
guard let self else {
return
}
self.workerQueue.async {
self.continueBeginTaskAfterForegroundCheck(
taskID: taskID,
name: name,
foregroundToken: foregroundToken,
appIsActive: appIsActive
)
}
}
}
private func continueBeginTaskAfterForegroundCheck(
taskID: String,
name: String,
foregroundToken: UUID,
appIsActive: Bool
) {
guard
foregroundStartTokens[taskID]
== foregroundToken
else {
return
}
foregroundStartTokens.removeValue(
forKey: taskID
)
guard currentEnabledSnapshot() else {
setTaskOwnership(
taskID: taskID,
handled: false
)
return
}
guard appIsActive else {
@@ -434,6 +453,20 @@ final class GopeedContinuedProcessingManager: NSObject {
return
}
// Re-check after the asynchronous main-thread hop. A terminal
// event, disable, or another valid start may have changed the
// lifecycle while the foreground state was being read.
guard
!pendingTaskIDs.contains(taskID),
activeTasks[taskID] == nil
else {
setTaskOwnership(
taskID: taskID,
handled: true
)
return
}
let generation = UUID()
taskGenerations[taskID] = generation
@@ -498,8 +531,6 @@ final class GopeedContinuedProcessingManager: NSObject {
return
}
registeredIdentifiers.insert(identifier)
let request =
BGContinuedProcessingTaskRequest(
identifier: identifier,
@@ -558,7 +589,6 @@ final class GopeedContinuedProcessingManager: NSObject {
}
}
// MARK: - System launched task
private func activateTask(
@@ -901,6 +931,10 @@ final class GopeedContinuedProcessingManager: NSObject {
success: Bool,
finalSubtitle: String
) {
foregroundStartTokens.removeValue(
forKey: taskID
)
progressRecoveryTokens.removeValue(
forKey: taskID
)
@@ -1115,6 +1149,7 @@ final class GopeedContinuedProcessingManager: NSObject {
Set(
handledTaskIDsSnapshot()
+ Array(taskIdentifiers.keys)
+ Array(foregroundStartTokens.keys)
+ Array(pendingTaskIDs)
+ Array(activeTasks.keys)
+ Array(expiringTasks.keys)
@@ -1140,6 +1175,7 @@ final class GopeedContinuedProcessingManager: NSObject {
}
activeTasks.removeAll()
foregroundStartTokens.removeAll()
pendingTaskIDs.removeAll()
expiringTasks.removeAll()
taskGenerations.removeAll()
@@ -248,26 +248,22 @@ final class GopeedLiveActivityManager: NSObject {
return
}
guard
let data = payload.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data)
as? [String: Any],
let type = json["type"] as? String,
let taskID = json["taskId"] as? String
else {
guard let event = GopeedTaskEvent.decode(payload) else {
print("LiveActivity: invalid task event payload")
return
}
let taskID = event.taskID
guard !isTaskSuppressed(taskID) else {
return
}
let name = json["name"] as? String ?? "Download"
let name = event.name
switch type {
switch event.type {
case "task.start":
case .start:
let generation =
startGeneration(
taskID: taskID
@@ -282,7 +278,7 @@ final class GopeedLiveActivityManager: NSObject {
)
}
case "task.progress":
case .progress:
guard
let generation =
generationForProgress(
@@ -298,7 +294,7 @@ final class GopeedLiveActivityManager: NSObject {
generation: generation
)
case "task.pause":
case .pause:
if let generation =
invalidateGeneration(
taskID: taskID
@@ -312,7 +308,7 @@ final class GopeedLiveActivityManager: NSObject {
}
}
case "task.done":
case .done:
if let generation =
invalidateGeneration(
taskID: taskID
@@ -325,9 +321,9 @@ final class GopeedLiveActivityManager: NSObject {
}
}
case "task.error":
case .error:
let error =
json["error"] as? String
event.error
?? "Download failed"
if let generation =
@@ -343,7 +339,7 @@ final class GopeedLiveActivityManager: NSObject {
}
}
case "task.delete":
case .delete:
if let generation =
invalidateGeneration(
taskID: taskID
@@ -357,7 +353,7 @@ final class GopeedLiveActivityManager: NSObject {
}
}
default:
case .other:
break
}
}
@@ -0,0 +1,69 @@
import Foundation
enum GopeedTaskEventType: Equatable {
case start
case progress
case pause
case done
case error
case delete
case other(String)
init(rawValue: String) {
switch rawValue {
case "task.start":
self = .start
case "task.progress":
self = .progress
case "task.pause":
self = .pause
case "task.done":
self = .done
case "task.error":
self = .error
case "task.delete":
self = .delete
default:
self = .other(rawValue)
}
}
}
struct GopeedTaskEvent {
let type: GopeedTaskEventType
let taskID: String
let name: String
let error: String?
private struct Payload: Decodable {
let type: String
let taskId: String
let name: String?
let error: String?
}
static func decode(
_ payload: String
) -> GopeedTaskEvent? {
guard
let data = payload.data(using: .utf8),
let decoded =
try? JSONDecoder().decode(
Payload.self,
from: data
)
else {
return nil
}
return GopeedTaskEvent(
type:
GopeedTaskEventType(
rawValue: decoded.type
),
taskID: decoded.taskId,
name: decoded.name ?? "Download",
error: decoded.error
)
}
}