// // BackgroundDownloadTracker.swift // AtProtoBackup // // Created by Corey Alexander on 8/29/25. // import Foundation #if os(iOS) import ActivityKit #endif @MainActor class BackgroundDownloadTracker { static let shared = BackgroundDownloadTracker() private var downloadProgress: [String: DownloadProgress] = [:] #if os(iOS) private var liveActivities: [String: Activity] = [:] private var lastUpdateCounts: [String: Int] = [:] private let updateBatchSize = 10 #endif private struct DownloadProgress { var downloadedBlobs: Int var totalBlobs: Int var lastUpdated: Date } private init() {} func registerDownload(accountDid: String, totalBlobs: Int) { downloadProgress[accountDid] = DownloadProgress( downloadedBlobs: 0, totalBlobs: totalBlobs, lastUpdated: Date() ) #if os(iOS) lastUpdateCounts[accountDid] = 0 #endif } #if os(iOS) func registerLiveActivity(_ activity: Activity, for accountDid: String) { liveActivities[accountDid] = activity } #endif func incrementProgress(for accountDid: String) async { guard var progress = downloadProgress[accountDid] else { return } progress.downloadedBlobs += 1 progress.lastUpdated = Date() downloadProgress[accountDid] = progress #if os(iOS) // Check if we should update the Live Activity (every 10 downloads) let currentCount = lastUpdateCounts[accountDid] ?? 0 let newCount = currentCount + 1 lastUpdateCounts[accountDid] = newCount if newCount >= updateBatchSize || progress.downloadedBlobs == progress.totalBlobs { // Reset counter and update Live Activity lastUpdateCounts[accountDid] = 0 await updateLiveActivityIfNeeded(accountDid: accountDid, progress: progress) } #endif } #if os(iOS) private func updateLiveActivityIfNeeded(accountDid: String, progress: DownloadProgress) async { guard let activity = liveActivities[accountDid] else { return } let progressPercent = Double(progress.downloadedBlobs) / Double(progress.totalBlobs) let status: DownloadActivityAttributes.ContentState.DownloadStatus = progress.downloadedBlobs == progress.totalBlobs ? .completed : .downloading let updatedState = DownloadActivityAttributes.ContentState( progress: progressPercent, downloadedBlobs: progress.downloadedBlobs, totalBlobs: progress.totalBlobs, accountHandle: activity.attributes.accountHandle, isPaused: false, status: status ) await activity.update(using: updatedState) print("[BackgroundTracker] Updated activity for \(accountDid) - Blobs: \(progress.downloadedBlobs)/\(progress.totalBlobs)") // If completed, end the activity after a delay if status == .completed { Task { try? await Task.sleep(nanoseconds: 3_000_000_000) // 3 seconds await activity.end(dismissalPolicy: .immediate) liveActivities.removeValue(forKey: accountDid) downloadProgress.removeValue(forKey: accountDid) lastUpdateCounts.removeValue(forKey: accountDid) } } } #endif func getProgress(for accountDid: String) -> (downloaded: Int, total: Int)? { guard let progress = downloadProgress[accountDid] else { return nil } return (progress.downloadedBlobs, progress.totalBlobs) } func cleanup(for accountDid: String) { downloadProgress.removeValue(forKey: accountDid) #if os(iOS) liveActivities.removeValue(forKey: accountDid) lastUpdateCounts.removeValue(forKey: accountDid) #endif } }