this repo has no description
1//
2// BackgroundDownloadTracker.swift
3// AtProtoBackup
4//
5// Created by Corey Alexander on 8/29/25.
6//
7
8import Foundation
9#if os(iOS)
10import ActivityKit
11#endif
12
13@MainActor
14class BackgroundDownloadTracker {
15 static let shared = BackgroundDownloadTracker()
16
17 private var downloadProgress: [String: DownloadProgress] = [:]
18 #if os(iOS)
19 private var liveActivities: [String: Activity<DownloadActivityAttributes>] = [:]
20 private var lastUpdateCounts: [String: Int] = [:]
21 private let updateBatchSize = 10
22 #endif
23
24 private struct DownloadProgress {
25 var downloadedBlobs: Int
26 var totalBlobs: Int
27 var lastUpdated: Date
28 }
29
30 private init() {}
31
32 func registerDownload(accountDid: String, totalBlobs: Int) {
33 downloadProgress[accountDid] = DownloadProgress(
34 downloadedBlobs: 0,
35 totalBlobs: totalBlobs,
36 lastUpdated: Date()
37 )
38 #if os(iOS)
39 lastUpdateCounts[accountDid] = 0
40 #endif
41 }
42
43 #if os(iOS)
44 func registerLiveActivity(_ activity: Activity<DownloadActivityAttributes>, for accountDid: String) {
45 liveActivities[accountDid] = activity
46 }
47 #endif
48
49 func incrementProgress(for accountDid: String) async {
50 guard var progress = downloadProgress[accountDid] else { return }
51
52 progress.downloadedBlobs += 1
53 progress.lastUpdated = Date()
54 downloadProgress[accountDid] = progress
55
56 #if os(iOS)
57 // Check if we should update the Live Activity (every 10 downloads)
58 let currentCount = lastUpdateCounts[accountDid] ?? 0
59 let newCount = currentCount + 1
60 lastUpdateCounts[accountDid] = newCount
61
62 if newCount >= updateBatchSize || progress.downloadedBlobs == progress.totalBlobs {
63 // Reset counter and update Live Activity
64 lastUpdateCounts[accountDid] = 0
65 await updateLiveActivityIfNeeded(accountDid: accountDid, progress: progress)
66 }
67 #endif
68 }
69
70 #if os(iOS)
71 private func updateLiveActivityIfNeeded(accountDid: String, progress: DownloadProgress) async {
72 guard let activity = liveActivities[accountDid] else { return }
73
74 let progressPercent = Double(progress.downloadedBlobs) / Double(progress.totalBlobs)
75 let status: DownloadActivityAttributes.ContentState.DownloadStatus =
76 progress.downloadedBlobs == progress.totalBlobs ? .completed : .downloading
77
78 let updatedState = DownloadActivityAttributes.ContentState(
79 progress: progressPercent,
80 downloadedBlobs: progress.downloadedBlobs,
81 totalBlobs: progress.totalBlobs,
82 accountHandle: activity.attributes.accountHandle,
83 isPaused: false,
84 status: status
85 )
86
87 await activity.update(using: updatedState)
88 print("[BackgroundTracker] Updated activity for \(accountDid) - Blobs: \(progress.downloadedBlobs)/\(progress.totalBlobs)")
89
90 // If completed, end the activity after a delay
91 if status == .completed {
92 Task {
93 try? await Task.sleep(nanoseconds: 3_000_000_000) // 3 seconds
94 await activity.end(dismissalPolicy: .immediate)
95 liveActivities.removeValue(forKey: accountDid)
96 downloadProgress.removeValue(forKey: accountDid)
97 lastUpdateCounts.removeValue(forKey: accountDid)
98 }
99 }
100 }
101 #endif
102
103 func getProgress(for accountDid: String) -> (downloaded: Int, total: Int)? {
104 guard let progress = downloadProgress[accountDid] else { return nil }
105 return (progress.downloadedBlobs, progress.totalBlobs)
106 }
107
108 func cleanup(for accountDid: String) {
109 downloadProgress.removeValue(forKey: accountDid)
110 #if os(iOS)
111 liveActivities.removeValue(forKey: accountDid)
112 lastUpdateCounts.removeValue(forKey: accountDid)
113 #endif
114 }
115}