// // BackgroundManager.swift // Reader // // Created by Shadowfacts on 6/20/22. // import Foundation import BackgroundTasks import OSLog import Persistence import Fervor private let logger = Logger(subsystem: "net.shadowfacts.Reader", category: "BackgroundManager") class BackgroundManager: NSObject { static let shared = BackgroundManager() private override init() {} static let refreshIdentifier = "net.shadowfacts.Reader.refresh" private var refreshTask: BGAppRefreshTask? private var completedRefreshRequests = 0 private var receivedData: [Int: Data] = [:] func scheduleRefresh() { // we schedule refreshes from sceneDidEnterBackground, but there may be multiple scenes, and we only want one refresh task BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundManager.refreshIdentifier) logger.debug("Scheduling background refresh task") let request = BGAppRefreshTaskRequest(identifier: BackgroundManager.refreshIdentifier) request.earliestBeginDate = Date(timeIntervalSinceNow: 2 * 60 * 60) // request.earliestBeginDate = Date(timeIntervalSinceNow: 60) do { try BGTaskScheduler.shared.submit(request) } catch { logger.error("Unable to schedule app refresh: \(String(describing: error), privacy: .public)") } } func handleBackgroundRefresh(task: BGAppRefreshTask) { logger.debug("Handling background refresh task") scheduleRefresh() guard !LocalData.accounts.isEmpty else { task.setTaskCompleted(success: true) return } self.refreshTask = task self.completedRefreshRequests = 0 doRefresh() task.expirationHandler = { task.setTaskCompleted(success: self.completedRefreshRequests == LocalData.accounts.count) } } func doRefresh() { let config = URLSessionConfiguration.background(withIdentifier: BackgroundManager.refreshIdentifier) config.sessionSendsLaunchEvents = true let backgroundSession = URLSession(configuration: config, delegate: self, delegateQueue: nil) Task { for account in LocalData.accounts { let fervorController = await FervorController(account: account, session: backgroundSession) let itemsRequest = await fervorController.client.itemsRequest(limit: 32) let task = backgroundSession.dataTask(with: itemsRequest) task.taskDescription = account.id.base64EncodedString() task.resume() } } } private func receivedData(_ data: Data, for task: URLSessionDataTask) { guard let taskDescription = task.taskDescription, let id = Data(base64Encoded: taskDescription), let account = LocalData.account(with: id) else { logger.error("Could not find account to handle request") return } Task { @MainActor in defer { if let refreshTask { refreshTask.setTaskCompleted(success: completedRefreshRequests == LocalData.accounts.count) } } let items: [Fervor.Item] do { items = try FervorClient.decoder.decode([Fervor.Item].self, from: data) } catch { logger.error("Unable to decode items: \(String(describing: error), privacy: .public)") return } let fervorController = await FervorController(account: account) do { try await fervorController.persistentContainer.upsertItems(items) logger.info("Upserted \(items.count) items during background refresh") await WidgetHelper.updateWidgetData(fervorController: fervorController) completedRefreshRequests += 1 } catch { logger.error("Unable to upsert items: \(String(describing: error), privacy: .public)") } } } } extension BackgroundManager: URLSessionDataDelegate { func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { if !receivedData.keys.contains(dataTask.taskIdentifier) { receivedData[dataTask.taskIdentifier] = Data(capacity: Int(dataTask.countOfBytesExpectedToReceive)) } receivedData[dataTask.taskIdentifier]!.append(data) logger.debug("Received chunk of \(data.count) bytes") if dataTask.countOfBytesReceived == dataTask.countOfBytesExpectedToReceive { logger.info("Received all data for task, handling update") receivedData(receivedData.removeValue(forKey: dataTask.taskIdentifier)!, for: dataTask) } } }