64 lines
2.8 KiB
Swift
64 lines
2.8 KiB
Swift
#if os(iOS)
|
|
import ActivityKit
|
|
import Foundation
|
|
import os.log
|
|
|
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "ABS", category: "LiveActivity")
|
|
|
|
/// Manages one `Activity<DownloadActivityAttributes>` 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<DownloadActivityAttributes>] = [:]
|
|
|
|
/// 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
|