diff --git a/kDrive/AppDelegate+BGAppRefresh.swift b/kDrive/AppDelegate+BGAppRefresh.swift deleted file mode 100644 index d69094dd3..000000000 --- a/kDrive/AppDelegate+BGAppRefresh.swift +++ /dev/null @@ -1,139 +0,0 @@ -/* - Infomaniak kDrive - iOS App - Copyright (C) 2023 Infomaniak Network SA - - This program 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 - (at your option) any later version. - - This program 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 this program. If not, see . - */ - -import BackgroundTasks -import CocoaLumberjackSwift -import Foundation -import InfomaniakDI -import kDriveCore - -extension AppDelegate { - /* To debug background tasks: - Launch -> - e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.infomaniak.background.refresh"] - OR - e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.infomaniak.background.long-refresh"] - - Force early termination -> - e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.infomaniak.background.refresh"] - OR - e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.infomaniak.background.long-refresh"] - */ - - /// schedule background tasks - func scheduleBackgroundRefresh() { - Log.bgTaskScheduling("scheduleBackgroundRefresh") - // List pictures + upload files (+pictures) / photoKit - let backgroundRefreshRequest = BGAppRefreshTaskRequest(identifier: Constants.backgroundRefreshIdentifier) - #if DEBUG - // Required for debugging - backgroundRefreshRequest.earliestBeginDate = Date() - #else - backgroundRefreshRequest.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60) - #endif - - // Upload files (+pictures) / photokit - let longBackgroundRefreshRequest = BGProcessingTaskRequest(identifier: Constants.longBackgroundRefreshIdentifier) - #if DEBUG - // Required for debugging - longBackgroundRefreshRequest.earliestBeginDate = Date() - #else - longBackgroundRefreshRequest.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60) - #endif - longBackgroundRefreshRequest.requiresNetworkConnectivity = true - longBackgroundRefreshRequest.requiresExternalPower = true - do { - try backgroundTaskScheduler.submit(backgroundRefreshRequest) - Log.bgTaskScheduling("scheduled task: \(backgroundRefreshRequest)") - try backgroundTaskScheduler.submit(longBackgroundRefreshRequest) - Log.bgTaskScheduling("scheduled task: \(longBackgroundRefreshRequest)") - - } catch { - Log.bgTaskScheduling("Error scheduling background task: \(error)", level: .error) - } - } - - /// Register BackgroundTasks in scheduler for later - func registerBackgroundTasks() { - Log.bgTaskScheduling("registerBackgroundTasks") - var registered = backgroundTaskScheduler.register( - forTaskWithIdentifier: Constants.backgroundRefreshIdentifier, - using: nil - ) { task in - self.scheduleBackgroundRefresh() - @InjectService var uploadQueue: UploadQueue - task.expirationHandler = { - Log.bgTaskScheduling("Task \(Constants.backgroundRefreshIdentifier) EXPIRED", level: .error) - uploadQueue.suspendAllOperations() - uploadQueue.rescheduleRunningOperations() - task.setTaskCompleted(success: false) - } - - self.handleBackgroundRefresh { _ in - Log.bgTaskScheduling("Task \(Constants.backgroundRefreshIdentifier) completed with SUCCESS") - task.setTaskCompleted(success: true) - } - } - Log.bgTaskScheduling("Task \(Constants.backgroundRefreshIdentifier) registered ? \(registered)") - - registered = backgroundTaskScheduler.register( - forTaskWithIdentifier: Constants.longBackgroundRefreshIdentifier, - using: nil - ) { task in - self.scheduleBackgroundRefresh() - @InjectService var uploadQueue: UploadQueue - task.expirationHandler = { - Log.bgTaskScheduling("Task \(Constants.longBackgroundRefreshIdentifier) EXPIRED", level: .error) - uploadQueue.suspendAllOperations() - uploadQueue.rescheduleRunningOperations() - task.setTaskCompleted(success: false) - } - - self.handleBackgroundRefresh { _ in - Log.bgTaskScheduling("Task \(Constants.longBackgroundRefreshIdentifier) completed with SUCCESS") - task.setTaskCompleted(success: true) - } - } - Log.bgTaskScheduling("Task \(Constants.longBackgroundRefreshIdentifier) registered ? \(registered)") - } - - func handleBackgroundRefresh(completion: @escaping (Bool) -> Void) { - Log.bgTaskScheduling("handleBackgroundRefresh") - // User installed the app but never logged in - if accountManager.accounts.isEmpty { - completion(false) - return - } - - Log.bgTaskScheduling("Enqueue new pictures") - @InjectService var photoUploader: PhotoLibraryUploader - photoUploader.scheduleNewPicturesForUpload() - - Log.bgTaskScheduling("Clean errors for all uploads") - @InjectService var uploadQueue: UploadQueue - uploadQueue.cleanNetworkAndLocalErrorsForAllOperations() - - Log.bgTaskScheduling("Reload operations in queue") - uploadQueue.rebuildUploadQueueFromObjectsInRealm() - - Log.bgTaskScheduling("waitForCompletion") - uploadQueue.waitForCompletion { - completion(true) - } - } -} diff --git a/kDrive/AppDelegate.swift b/kDrive/AppDelegate.swift index e31bb955f..0fcdcce52 100644 --- a/kDrive/AppDelegate.swift +++ b/kDrive/AppDelegate.swift @@ -49,9 +49,9 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg @LazyInjectService var backgroundUploadSessionManager: BackgroundUploadSessionManager @LazyInjectService var backgroundDownloadSessionManager: BackgroundDownloadSessionManager @LazyInjectService var photoLibraryUploader: PhotoLibraryUploader - @LazyInjectService var backgroundTaskScheduler: BGTaskScheduler @LazyInjectService var notificationHelper: NotificationsHelpable @LazyInjectService var accountManager: AccountManageable + @LazyInjectService var backgroundTasksService: BackgroundTasksServiceable // MARK: - UIApplicationDelegate @@ -71,7 +71,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg SentryDebug.capture(error: error) } - registerBackgroundTasks() + backgroundTasksService.registerBackgroundTasks() // In some cases the application can show the old Nextcloud import notification badge UIApplication.shared.applicationIconBadgeNumber = 0 @@ -167,8 +167,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg func applicationDidEnterBackground(_ application: UIApplication) { Log.appDelegate("applicationDidEnterBackground") + backgroundTasksService.scheduleBackgroundRefresh() - scheduleBackgroundRefresh() if UserDefaults.shared.isAppLockEnabled, !(window?.rootViewController?.isKind(of: LockedAppViewController.self) ?? false) { lockHelper.setTime() @@ -183,19 +183,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, AccountManagerDeleg shortcutItemToProcess = shortcutItem } - func application(_ application: UIApplication, - performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - Log.appDelegate("application performFetchWithCompletionHandler") - - handleBackgroundRefresh { newData in - if newData { - completionHandler(.newData) - } else { - completionHandler(.noData) - } - } - } - func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { Log.appDelegate("application app open url\(url)") diff --git a/kDriveCore/DI/FactoryService.swift b/kDriveCore/DI/FactoryService.swift index f6465660c..9f46da234 100644 --- a/kDriveCore/DI/FactoryService.swift +++ b/kDriveCore/DI/FactoryService.swift @@ -133,6 +133,9 @@ public enum FactoryService { }, Factory(type: PhotoLibrarySavable.self) { _, _ in PhotoLibrarySaver() + }, + Factory(type: BackgroundTasksServiceable.self) { _, _ in + BackgroundTasksService() } ] return services diff --git a/kDriveCore/Services/BackgroundTasksService.swift b/kDriveCore/Services/BackgroundTasksService.swift new file mode 100644 index 000000000..c0a00ed3b --- /dev/null +++ b/kDriveCore/Services/BackgroundTasksService.swift @@ -0,0 +1,142 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2023 Infomaniak Network SA + + This program 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 + (at your option) any later version. + + This program 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 this program. If not, see . + */ + +import BackgroundTasks +import CocoaLumberjackSwift +import Foundation +import InfomaniakDI +import kDriveCore + +/* To debug background tasks: + Launch -> + e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.infomaniak.background.refresh"] + OR + e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.infomaniak.background.long-refresh"] + + Force early termination -> + e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.infomaniak.background.refresh"] + OR + e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.infomaniak.background.long-refresh"] + */ + +/// Service to ask the system to do some work in the background later. +public protocol BackgroundTasksServiceable { + /// Ask the system to handle the app's background refresh + func registerBackgroundTasks() + + /// Schedule next refresh with the system + func scheduleBackgroundRefresh() +} + +struct BackgroundTasksService: BackgroundTasksServiceable { + @LazyInjectService private var scheduler: BGTaskScheduler + @LazyInjectService private var accountManager: AccountManageable + @LazyInjectService private var uploadQueue: UploadQueue + @LazyInjectService private var photoUploader: PhotoLibraryUploader + + public init() { + // META: keep SonarCloud happy + } + + public func registerBackgroundTasks() { + Log.backgroundTaskScheduling("registerBackgroundTasks") + registerBackgroundTask(identifier: Constants.backgroundRefreshIdentifier) + registerBackgroundTask(identifier: Constants.longBackgroundRefreshIdentifier) + } + + public func buildBackgroundTask(_ task: BGTask, identifier: String) { + scheduleBackgroundRefresh() + + handleBackgroundRefresh { _ in + Log.backgroundTaskScheduling("Task \(identifier) completed with SUCCESS") + task.setTaskCompleted(success: true) + } + + task.expirationHandler = { + Log.backgroundTaskScheduling("Task \(identifier) EXPIRED", level: .error) + uploadQueue.suspendAllOperations() + uploadQueue.rescheduleRunningOperations() + task.setTaskCompleted(success: false) + } + } + + func registerBackgroundTask(identifier: String) { + let registered = scheduler.register( + forTaskWithIdentifier: identifier, + using: nil + ) { task in + buildBackgroundTask(task, identifier: identifier) + } + Log.backgroundTaskScheduling("Task \(identifier) registered ? \(registered)") + } + + func handleBackgroundRefresh(completion: @escaping (Bool) -> Void) { + Log.backgroundTaskScheduling("handleBackgroundRefresh") + // User installed the app but never logged in + if accountManager.accounts.isEmpty { + completion(false) + return + } + + Log.backgroundTaskScheduling("Enqueue new pictures") + photoUploader.scheduleNewPicturesForUpload() + + Log.backgroundTaskScheduling("Clean errors for all uploads") + uploadQueue.cleanNetworkAndLocalErrorsForAllOperations() + + Log.backgroundTaskScheduling("Reload operations in queue") + uploadQueue.rebuildUploadQueueFromObjectsInRealm() + + Log.backgroundTaskScheduling("waitForCompletion") + uploadQueue.waitForCompletion { + completion(true) + } + } + + func scheduleBackgroundRefresh() { + Log.backgroundTaskScheduling("scheduleBackgroundRefresh") + // List pictures + upload files (+pictures) / photoKit + let backgroundRefreshRequest = BGAppRefreshTaskRequest(identifier: Constants.backgroundRefreshIdentifier) + #if DEBUG + // Required for debugging + backgroundRefreshRequest.earliestBeginDate = Date() + #else + backgroundRefreshRequest.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60) + #endif + + // Upload files (+pictures) / photokit + let longBackgroundRefreshRequest = BGProcessingTaskRequest(identifier: Constants.longBackgroundRefreshIdentifier) + #if DEBUG + // Required for debugging + longBackgroundRefreshRequest.earliestBeginDate = Date() + #else + longBackgroundRefreshRequest.earliestBeginDate = Date(timeIntervalSinceNow: 30 * 60) + #endif + longBackgroundRefreshRequest.requiresNetworkConnectivity = true + longBackgroundRefreshRequest.requiresExternalPower = true + do { + try scheduler.submit(backgroundRefreshRequest) + Log.backgroundTaskScheduling("scheduled task: \(backgroundRefreshRequest)") + try scheduler.submit(longBackgroundRefreshRequest) + Log.backgroundTaskScheduling("scheduled task: \(longBackgroundRefreshRequest)") + + } catch { + Log.backgroundTaskScheduling("Error scheduling background task: \(error)", level: .error) + } + } +} diff --git a/kDriveCore/Utils/AbstractLog+Category.swift b/kDriveCore/Utils/AbstractLog+Category.swift index d8c1fe3c5..7cafd1bed 100644 --- a/kDriveCore/Utils/AbstractLog+Category.swift +++ b/kDriveCore/Utils/AbstractLog+Category.swift @@ -90,13 +90,13 @@ public enum Log { /// /// In system console, visualize them with `subsystem:com.infomaniak.drive category:BGTaskScheduling` /// - public static func bgTaskScheduling(_ message: @autoclosure () -> Any, - level: AbstractLogLevel = .debug, - context: Int = 0, - file: StaticString = #file, - function: StaticString = #function, - line: UInt = #line, - tag: Any? = nil) { + public static func backgroundTaskScheduling(_ message: @autoclosure () -> Any, + level: AbstractLogLevel = .debug, + context: Int = 0, + file: StaticString = #file, + function: StaticString = #function, + line: UInt = #line, + tag: Any? = nil) { let category = "BGTaskScheduling" defaultLogHandler(message(), category: category,