Skip to content

Commit

Permalink
Add live activities for download progress
Browse files Browse the repository at this point in the history
Fixes kiwix#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).
  • Loading branch information
plyght committed Dec 21, 2024
1 parent e1631bb commit 52cf609
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 94 deletions.
31 changes: 16 additions & 15 deletions App/App_iOS.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}
}
}

Expand All @@ -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
12 changes: 12 additions & 0 deletions Model/DownloadActivityAttributes.swift
Original file line number Diff line number Diff line change
@@ -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
}
54 changes: 35 additions & 19 deletions Model/DownloadService.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<DownloadActivityAttributes>?

// MARK: - Heartbeat

Expand Down Expand Up @@ -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<DownloadActivityAttributes>.request(
attributes: attributes,
contentState: initialContentState,
pushType: nil
)
} catch {
print("Error starting Live Activity: \(error)")
}
}
}

Expand Down Expand Up @@ -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<DownloadActivityAttributes>.request(
attributes: attributes,
contentState: initialContentState,
pushType: nil
)
} catch {
print("Error resuming Live Activity: \(error)")
}
}
}

Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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)
}
}

Expand Down
15 changes: 15 additions & 0 deletions Views/BuildingBlocks/DownloadTaskCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import CoreData
import SwiftUI
import Combine
import ActivityKit

struct DownloadTaskCell: View {
@State private var isHovering: Bool = false
Expand Down Expand Up @@ -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<DownloadActivityAttributes>.request(
attributes: attributes,
contentState: initialContentState,
pushType: nil
)
} catch {
print("Error starting Live Activity: \(error)")
}
}
}
}

Expand Down
33 changes: 18 additions & 15 deletions Views/Library/ZimFileDetail.swift
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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<DownloadActivityAttributes>?

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 {
Expand Down Expand Up @@ -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<DownloadActivityAttributes>.request(
attributes: attributes,
contentState: initialContentState,
pushType: nil
)
} catch {
print("Error starting Live Activity: \(error)")
}
}
}

var detail: String {
Expand Down
34 changes: 19 additions & 15 deletions Views/Library/ZimFilesDownloads.swift
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<DownloadActivityAttributes>.request(
attributes: attributes,
contentState: initialContentState,
pushType: nil
)
} catch {
print("Error starting Live Activity: \(error)")
}
}
}
}
}
}
18 changes: 3 additions & 15 deletions Views/Settings/Settings.swift
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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: {
Expand Down
24 changes: 9 additions & 15 deletions Views/ViewModifiers/AlertHandler.swift
Original file line number Diff line number Diff line change
@@ -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?
Expand All @@ -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<DownloadActivityAttributes>.activities {
await activity.end(dismissalPolicy: .immediate)
}
}
}
}
}

0 comments on commit 52cf609

Please sign in to comment.