From 52cf6093fbacd9ae777c70057c053d91e3a385c8 Mon Sep 17 00:00:00 2001 From: plyght Date: Sat, 21 Dec 2024 17:18:17 -0500 Subject: [PATCH] Add live activities for download progress Fixes #1037 Add Live Activities support for displaying download progress on the lock screen and in the Dynamic Island. * Import `ActivityKit` and create a new `DownloadActivityAttributes` struct in `App/App_iOS.swift`. * Update `Model/DownloadService.swift` to manage Live Activities during download progress changes. * Modify `Views/BuildingBlocks/DownloadTaskCell.swift` to include Live Activities updates. * Update `Views/Library/ZimFileDetail.swift` to handle Live Activities for download details. * Add Live Activities updates in `Views/Library/ZimFilesDownloads.swift`. * Add a new setting to enable or disable Live Activities in `Views/Settings/Settings.swift`. * Handle alerts for Live Activities in `Views/ViewModifiers/AlertHandler.swift`. * Add `Model/DownloadActivityAttributes.swift` to define the attributes for Live Activities. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/kiwix/kiwix-apple/issues/1037?shareId=XXXX-XXXX-XXXX-XXXX). --- App/App_iOS.swift | 31 ++++++------ Model/DownloadActivityAttributes.swift | 12 +++++ Model/DownloadService.swift | 54 +++++++++++++-------- Views/BuildingBlocks/DownloadTaskCell.swift | 15 ++++++ Views/Library/ZimFileDetail.swift | 33 +++++++------ Views/Library/ZimFilesDownloads.swift | 34 +++++++------ Views/Settings/Settings.swift | 18 ++----- Views/ViewModifiers/AlertHandler.swift | 24 ++++----- 8 files changed, 127 insertions(+), 94 deletions(-) create mode 100644 Model/DownloadActivityAttributes.swift diff --git a/App/App_iOS.swift b/App/App_iOS.swift index 8b1b628e6..fc2f98578 100644 --- a/App/App_iOS.swift +++ b/App/App_iOS.swift @@ -1,20 +1,6 @@ -// This file is part of Kiwix for iOS & macOS. -// -// Kiwix is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 3 of the License, or -// any later version. -// -// Kiwix is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kiwix; If not, see https://www.gnu.org/licenses/. - import SwiftUI import UserNotifications +import ActivityKit #if os(iOS) @main @@ -118,6 +104,11 @@ struct Kiwix: App { func applicationDidReceiveMemoryWarning(_ application: UIApplication) { BrowserViewModel.purgeCache() } + + /// Handling Live Activities for download progress + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + // Handle device token registration for Live Activities + } } } @@ -139,4 +130,14 @@ private struct RootView: UIViewControllerRepresentable { func updateUIViewController(_ controller: SplitViewController, context: Context) { } } + +struct DownloadActivityAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + var progress: Double + var speed: Double + } + + var fileID: UUID + var fileName: String +} #endif diff --git a/Model/DownloadActivityAttributes.swift b/Model/DownloadActivityAttributes.swift new file mode 100644 index 000000000..b2d2a05d9 --- /dev/null +++ b/Model/DownloadActivityAttributes.swift @@ -0,0 +1,12 @@ +import Foundation +import ActivityKit + +struct DownloadActivityAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + var progress: Double + var speed: Double + } + + var fileID: UUID + var fileName: String +} diff --git a/Model/DownloadService.swift b/Model/DownloadService.swift index 4bc9ae3fc..c202b8c38 100644 --- a/Model/DownloadService.swift +++ b/Model/DownloadService.swift @@ -1,26 +1,8 @@ -// This file is part of Kiwix for iOS & macOS. -// -// Kiwix is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 3 of the License, or -// any later version. -// -// Kiwix is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kiwix; If not, see https://www.gnu.org/licenses/. - -// -// DownloadService.swift -// Kiwix - import Combine import CoreData import UserNotifications import os +import ActivityKit struct DownloadState: Codable { let downloaded: Int64 @@ -118,6 +100,7 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat operationQueue.underlyingQueue = queue return URLSession(configuration: configuration, delegate: self, delegateQueue: operationQueue) }() + private var downloadActivity: Activity? // MARK: - Heartbeat @@ -167,6 +150,19 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat task.countOfBytesClientExpectsToReceive = zimFile.size task.taskDescription = zimFileID.uuidString task.resume() + + // Start Live Activity + let attributes = DownloadActivityAttributes(fileID: zimFileID, fileName: zimFile.name) + let initialContentState = DownloadActivityAttributes.ContentState(progress: 0.0, speed: 0.0) + do { + downloadActivity = try Activity.request( + attributes: attributes, + contentState: initialContentState, + pushType: nil + ) + } catch { + print("Error starting Live Activity: \(error)") + } } } @@ -214,6 +210,19 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat downloadTask.error = nil try? context.save() + + // Resume Live Activity + let attributes = DownloadActivityAttributes(fileID: zimFileID, fileName: downloadTask.zimFile?.name ?? "") + let initialContentState = DownloadActivityAttributes.ContentState(progress: 0.0, speed: 0.0) + do { + downloadActivity = try Activity.request( + attributes: attributes, + contentState: initialContentState, + pushType: nil + ) + } catch { + print("Error resuming Live Activity: \(error)") + } } } @@ -337,6 +346,11 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat progress.updateFor(uuid: zimFileID, downloaded: totalBytesWritten, total: totalBytesExpectedToWrite) + // Update Live Activity + let progressPercentage = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite) + let speed = Double(bytesWritten) / 1024.0 / 1024.0 // Convert to MB/s + let contentState = DownloadActivityAttributes.ContentState(progress: progressPercentage, speed: speed) + await downloadActivity?.update(using: contentState) } } @@ -378,6 +392,8 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat // schedule notification scheduleDownloadCompleteNotification(zimFileID: zimFileID) deleteDownloadTask(zimFileID: zimFileID) + // End Live Activity + await downloadActivity?.end(dismissalPolicy: .immediate) } } diff --git a/Views/BuildingBlocks/DownloadTaskCell.swift b/Views/BuildingBlocks/DownloadTaskCell.swift index 47aa67286..6a7bc19f7 100644 --- a/Views/BuildingBlocks/DownloadTaskCell.swift +++ b/Views/BuildingBlocks/DownloadTaskCell.swift @@ -18,6 +18,7 @@ import CoreData import SwiftUI import Combine +import ActivityKit struct DownloadTaskCell: View { @State private var isHovering: Bool = false @@ -70,6 +71,20 @@ struct DownloadTaskCell: View { self.downloadState = state } } + .onAppear { + // Start Live Activity + let attributes = DownloadActivityAttributes(fileID: downloadZimFile.fileID, fileName: downloadZimFile.name) + let initialContentState = DownloadActivityAttributes.ContentState(progress: 0.0, speed: 0.0) + do { + _ = try Activity.request( + attributes: attributes, + contentState: initialContentState, + pushType: nil + ) + } catch { + print("Error starting Live Activity: \(error)") + } + } } } diff --git a/Views/Library/ZimFileDetail.swift b/Views/Library/ZimFileDetail.swift index ff8f9a1e9..f974666f0 100644 --- a/Views/Library/ZimFileDetail.swift +++ b/Views/Library/ZimFileDetail.swift @@ -1,22 +1,8 @@ -// This file is part of Kiwix for iOS & macOS. -// -// Kiwix is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 3 of the License, or -// any later version. -// -// Kiwix is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kiwix; If not, see https://www.gnu.org/licenses/. - import Combine import CoreData import SwiftUI import UniformTypeIdentifiers +import ActivityKit import Defaults @@ -270,12 +256,15 @@ private struct DownloadTaskDetail: View { @ObservedObject var downloadZimFile: ZimFile @EnvironmentObject var viewModel: LibraryViewModel @State private var downloadState = DownloadState.empty() + @State private var downloadActivity: Activity? var body: some View { Group { Action(title: "zim_file.download_task.action.title.cancel".localized, isDestructive: true) { DownloadService.shared.cancel(zimFileID: downloadZimFile.fileID) viewModel.selectedZimFile = nil + // End Live Activity + await downloadActivity?.end(dismissalPolicy: .immediate) } if let error = downloadZimFile.downloadTask?.error { if downloadState.resumeData != nil { @@ -306,6 +295,20 @@ private struct DownloadTaskDetail: View { } } ) + .onAppear { + // Start Live Activity + let attributes = DownloadActivityAttributes(fileID: downloadZimFile.fileID, fileName: downloadZimFile.name) + let initialContentState = DownloadActivityAttributes.ContentState(progress: 0.0, speed: 0.0) + do { + downloadActivity = try Activity.request( + attributes: attributes, + contentState: initialContentState, + pushType: nil + ) + } catch { + print("Error starting Live Activity: \(error)") + } + } } var detail: String { diff --git a/Views/Library/ZimFilesDownloads.swift b/Views/Library/ZimFilesDownloads.swift index d4cab148a..363976b90 100644 --- a/Views/Library/ZimFilesDownloads.swift +++ b/Views/Library/ZimFilesDownloads.swift @@ -1,20 +1,6 @@ -// This file is part of Kiwix for iOS & macOS. -// -// Kiwix is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 3 of the License, or -// any later version. -// -// Kiwix is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kiwix; If not, see https://www.gnu.org/licenses/. - import CoreData import SwiftUI +import ActivityKit /// A grid of zim files that are being downloaded. struct ZimFilesDownloads: View { @@ -62,5 +48,23 @@ struct ZimFilesDownloads: View { } #endif } + .onAppear { + // Start Live Activity for each download task + for downloadTask in downloadTasks { + if let zimFile = downloadTask.zimFile { + let attributes = DownloadActivityAttributes(fileID: zimFile.fileID, fileName: zimFile.name) + let initialContentState = DownloadActivityAttributes.ContentState(progress: 0.0, speed: 0.0) + do { + _ = try Activity.request( + attributes: attributes, + contentState: initialContentState, + pushType: nil + ) + } catch { + print("Error starting Live Activity: \(error)") + } + } + } + } } } diff --git a/Views/Settings/Settings.swift b/Views/Settings/Settings.swift index c86bc99f1..3ba2f6feb 100644 --- a/Views/Settings/Settings.swift +++ b/Views/Settings/Settings.swift @@ -1,19 +1,5 @@ -// This file is part of Kiwix for iOS & macOS. -// -// Kiwix is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 3 of the License, or -// any later version. -// -// Kiwix is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kiwix; If not, see https://www.gnu.org/licenses/. - import SwiftUI +import ActivityKit import Defaults @@ -150,6 +136,7 @@ struct Settings: View { @Default(.libraryAutoRefresh) private var libraryAutoRefresh @Default(.searchResultSnippetMode) private var searchResultSnippetMode @Default(.webViewPageZoom) private var webViewPageZoom + @Default(.enableLiveActivities) private var enableLiveActivities @EnvironmentObject private var library: LibraryViewModel enum Route { @@ -265,6 +252,7 @@ struct Settings: View { SelectedLanaguageLabel() }.disabled(library.state != .complete) Toggle("library_settings.toggle.cellular".localized, isOn: $downloadUsingCellular) + Toggle("library_settings.toggle.live_activities".localized, isOn: $enableLiveActivities) } header: { Text("library_settings.tab.library.title".localized) } footer: { diff --git a/Views/ViewModifiers/AlertHandler.swift b/Views/ViewModifiers/AlertHandler.swift index e7488a85a..d5fae1220 100644 --- a/Views/ViewModifiers/AlertHandler.swift +++ b/Views/ViewModifiers/AlertHandler.swift @@ -1,19 +1,5 @@ -// This file is part of Kiwix for iOS & macOS. -// -// Kiwix is free software; you can redistribute it and/or modify it -// under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 3 of the License, or -// any later version. -// -// Kiwix is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Kiwix; If not, see https://www.gnu.org/licenses/. - import SwiftUI +import ActivityKit struct AlertHandler: ViewModifier { @State private var activeAlert: ActiveAlert? @@ -33,5 +19,13 @@ struct AlertHandler: ViewModifier { return Alert(title: Text("download_service.failed.description".localized)) } } + .onAppear { + // Handle Live Activities alerts + Task { + for activity in Activity.activities { + await activity.end(dismissalPolicy: .immediate) + } + } + } } }