From 6641a61b3ca7e41dadbe1d92f465bd5978caa07f Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 22 Mar 2025 19:06:27 +0100 Subject: [PATCH 1/4] Handle background downloads to update live activities --- App/App_iOS.swift | 48 ++++++++++++++++++++++- Model/Downloads/BackgroundDownloads.swift | 29 ++++++++++++++ Support/Info.plist | 2 +- Views/LiveActivity/ActivityService.swift | 23 ++++++++++- 4 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 Model/Downloads/BackgroundDownloads.swift diff --git a/App/App_iOS.swift b/App/App_iOS.swift index 50b8acdc..559d70f6 100644 --- a/App/App_iOS.swift +++ b/App/App_iOS.swift @@ -17,6 +17,8 @@ import SwiftUI import Combine import UserNotifications +import BackgroundTasks +import os @main struct Kiwix: App { @@ -33,7 +35,7 @@ struct Kiwix: App { // MARK: - live activities switch AppType.current { case .kiwix: - activityService = ActivityService() + activityService = ActivityService.shared() case .custom: activityService = nil } @@ -114,12 +116,56 @@ struct Kiwix: App { } private class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate { + /// Storing background download completion handler sent to application delegate func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { DownloadService.shared.backgroundCompletionHandler = completionHandler } + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + registerBackgroundTask() + return true + } + + // Background download task + func applicationDidFinishLaunching(_ application: UIApplication) { + registerBackgroundTask() + } + func applicationDidEnterBackground(_ application: UIApplication) { + registerBackgroundTask() + } + + private func registerBackgroundTask() { + guard case .kiwix = AppType.current else { return } + let isRegistered = BGTaskScheduler.shared.register( + forTaskWithIdentifier: BackgroundDownloads.identifier, + using: .main) { [self] task in + // update the live activities, if any + ActivityService.shared().start() + // reschedule + reScheduleBackgroundDownloadTask() + } + if isRegistered { + os_log("BackgroundDownloads registered", log: Log.DownloadService, type: .debug) + } else { + os_log("BackgroundDownloads registering failed: %s", log: Log.DownloadService, type: .error) + } + } + + private func reScheduleBackgroundDownloadTask() { + do { + let date = BackgroundDownloads.nextDate() + let request = BGAppRefreshTaskRequest(identifier: BackgroundDownloads.identifier) + request.earliestBeginDate = date + os_log("BackgroundDownloads task re-scheduled for: %s", log: Log.DownloadService, type: .debug, date.formatted()) + + try BGTaskScheduler.shared.submit(request) + } catch { + os_log("BackgroundDownloads re-schedule failed: %s", log: Log.DownloadService, type: .error, error.localizedDescription) + } + } /// Handling file download complete notification func userNotificationCenter(_ center: UNUserNotificationCenter, diff --git a/Model/Downloads/BackgroundDownloads.swift b/Model/Downloads/BackgroundDownloads.swift new file mode 100644 index 00000000..2c36b07d --- /dev/null +++ b/Model/Downloads/BackgroundDownloads.swift @@ -0,0 +1,29 @@ +// 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/. + +#if os(iOS) +import Foundation +import BackgroundTasks + +enum BackgroundDownloads { + static let identifier = "org.kiwix.downloads_to_liveactivity" + + static func nextDate() -> Date { + .now + 2 // after 2 seconds + } +} + + +#endif diff --git a/Support/Info.plist b/Support/Info.plist index 60f10513..3fedd627 100644 --- a/Support/Info.plist +++ b/Support/Info.plist @@ -6,7 +6,7 @@ id997079563 BGTaskSchedulerPermittedIdentifiers - org.kiwix.library_refresh + org.kiwix.downloads_to_liveactivity CFBundleDocumentTypes diff --git a/Views/LiveActivity/ActivityService.swift b/Views/LiveActivity/ActivityService.swift index 741e6ca9..981e2920 100644 --- a/Views/LiveActivity/ActivityService.swift +++ b/Views/LiveActivity/ActivityService.swift @@ -22,6 +22,7 @@ import QuartzCore @MainActor final class ActivityService { + private static var instance: ActivityService? private var cancellables = Set() private var activity: Activity? private var lastUpdate = CACurrentMediaTime() @@ -31,7 +32,27 @@ final class ActivityService { private var isStarted: Bool = false private var downloadTimes: [UUID: DownloadTime] = [:] - init( + public static func shared( + publisher: @MainActor @escaping () -> CurrentValueSubject<[UUID: DownloadState], Never> = { + DownloadService.shared.progress.publisher + }, + updateFrequency: Double = 2, + averageDownloadSpeedFromLastSeconds: Double = 30 + ) -> ActivityService { + if let instance = Self.instance { + return instance + } else { + let instance = ActivityService( + publisher: publisher, + updateFrequency: updateFrequency, + averageDownloadSpeedFromLastSeconds: averageDownloadSpeedFromLastSeconds + ) + Self.instance = instance + return instance + } + } + + private init( publisher: @MainActor @escaping () -> CurrentValueSubject<[UUID: DownloadState], Never> = { DownloadService.shared.progress.publisher }, From 8fd86f17693ede28376e20c4f05d77f19f68a55c Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 22 Mar 2025 19:08:25 +0100 Subject: [PATCH 2/4] Simplify init --- Views/LiveActivity/ActivityService.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Views/LiveActivity/ActivityService.swift b/Views/LiveActivity/ActivityService.swift index 981e2920..025499e5 100644 --- a/Views/LiveActivity/ActivityService.swift +++ b/Views/LiveActivity/ActivityService.swift @@ -53,11 +53,9 @@ final class ActivityService { } private init( - publisher: @MainActor @escaping () -> CurrentValueSubject<[UUID: DownloadState], Never> = { - DownloadService.shared.progress.publisher - }, - updateFrequency: Double = 2, - averageDownloadSpeedFromLastSeconds: Double = 30 + publisher: @MainActor @escaping () -> CurrentValueSubject<[UUID: DownloadState], Never>, + updateFrequency: Double, + averageDownloadSpeedFromLastSeconds: Double ) { assert(updateFrequency > 0) assert(averageDownloadSpeedFromLastSeconds > 0) From dee6b35b879e84c805cffeefede866b327af4fa7 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 22 Mar 2025 19:13:15 +0100 Subject: [PATCH 3/4] Fixlint --- App/App_iOS.swift | 23 ++++++++++++++++++----- Model/Downloads/BackgroundDownloads.swift | 1 - 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/App/App_iOS.swift b/App/App_iOS.swift index 559d70f6..3470a39d 100644 --- a/App/App_iOS.swift +++ b/App/App_iOS.swift @@ -124,7 +124,10 @@ struct Kiwix: App { DownloadService.shared.backgroundCompletionHandler = completionHandler } - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { registerBackgroundTask() return true } @@ -141,11 +144,11 @@ struct Kiwix: App { guard case .kiwix = AppType.current else { return } let isRegistered = BGTaskScheduler.shared.register( forTaskWithIdentifier: BackgroundDownloads.identifier, - using: .main) { [self] task in + using: .main) { [weak self] _ in // update the live activities, if any ActivityService.shared().start() // reschedule - reScheduleBackgroundDownloadTask() + self?.reScheduleBackgroundDownloadTask() } if isRegistered { os_log("BackgroundDownloads registered", log: Log.DownloadService, type: .debug) @@ -159,11 +162,21 @@ struct Kiwix: App { let date = BackgroundDownloads.nextDate() let request = BGAppRefreshTaskRequest(identifier: BackgroundDownloads.identifier) request.earliestBeginDate = date - os_log("BackgroundDownloads task re-scheduled for: %s", log: Log.DownloadService, type: .debug, date.formatted()) + os_log( + "BackgroundDownloads task re-scheduled for: %s", + log: Log.DownloadService, + type: .debug, + date.formatted() + ) try BGTaskScheduler.shared.submit(request) } catch { - os_log("BackgroundDownloads re-schedule failed: %s", log: Log.DownloadService, type: .error, error.localizedDescription) + os_log( + "BackgroundDownloads re-schedule failed: %s", + log: Log.DownloadService, + type: .error, + error.localizedDescription + ) } } diff --git a/Model/Downloads/BackgroundDownloads.swift b/Model/Downloads/BackgroundDownloads.swift index 2c36b07d..6df2201e 100644 --- a/Model/Downloads/BackgroundDownloads.swift +++ b/Model/Downloads/BackgroundDownloads.swift @@ -25,5 +25,4 @@ enum BackgroundDownloads { } } - #endif From 5c791e42771d162f1158e7a5cdf6d185a2c64114 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 22 Mar 2025 19:37:53 +0100 Subject: [PATCH 4/4] Use strong --- App/App_iOS.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/App/App_iOS.swift b/App/App_iOS.swift index 3470a39d..0c06c9a4 100644 --- a/App/App_iOS.swift +++ b/App/App_iOS.swift @@ -144,11 +144,11 @@ struct Kiwix: App { guard case .kiwix = AppType.current else { return } let isRegistered = BGTaskScheduler.shared.register( forTaskWithIdentifier: BackgroundDownloads.identifier, - using: .main) { [weak self] _ in + using: .main) { [self] _ in // update the live activities, if any ActivityService.shared().start() // reschedule - self?.reScheduleBackgroundDownloadTask() + reScheduleBackgroundDownloadTask() } if isRegistered { os_log("BackgroundDownloads registered", log: Log.DownloadService, type: .debug)