#if os(iOS) import ActivityKit import Foundation import os.log private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "ABS", category: "LiveActivity") /// Manages one `Activity` per active download key. /// All methods must be called from the MainActor (same isolation as DownloadManager). @MainActor final class DownloadActivityManager { // Keyed by downloadKey (matches DownloadManager's key scheme). private var activities: [String: Activity] = [:] /// Requests a new Live Activity for a download that is starting. /// Logs the reason to the console (visible in Xcode) if the request fails. func startActivity(downloadKey: String, itemTitle: String) { let authInfo = ActivityAuthorizationInfo() guard authInfo.areActivitiesEnabled else { logger.warning("Live Activities disabled (areActivitiesEnabled=false) — skipping for '\(itemTitle)'") return } let attributes = DownloadActivityAttributes(itemTitle: itemTitle) let initial = DownloadActivityAttributes.ContentState( progress: 0, bytesDownloaded: 0 ) let content = ActivityContent(state: initial, staleDate: nil) do { let activity = try Activity.request(attributes: attributes, content: content) activities[downloadKey] = activity logger.info("Live Activity started: id=\(activity.id) title='\(itemTitle)'") } catch { // Most common reason: no widget extension is installed/embedded that // provides an ActivityConfiguration for DownloadActivityAttributes. logger.error("Activity.request() failed: \(error.localizedDescription)") } } /// Pushes an incremental progress update to the running Live Activity. func updateActivity(downloadKey: String, progress: Double, bytesDownloaded: Int64) { guard let activity = activities[downloadKey] else { return } let state = DownloadActivityAttributes.ContentState( progress: progress, bytesDownloaded: bytesDownloaded ) let content = ActivityContent(state: state, staleDate: nil) // activity.update() is async; fire-and-forget is intentional — a skipped // frame doesn't hurt the download and avoids blocking the download loop. Task { await activity.update(content) } } /// Ends the Live Activity with `.immediate` dismissal so the Lock Screen /// badge disappears right away instead of lingering for the default 4 hours. func endActivity(downloadKey: String) { guard let activity = activities.removeValue(forKey: downloadKey) else { return } Task { await activity.end(nil, dismissalPolicy: .immediate) } } } #endif