/** * DailyNotificationBackgroundTaskManager.swift * * iOS Background Task Manager for T–lead prefetch * Implements BGTaskScheduler integration with single-attempt prefetch logic * * @author Matthew Raymer * @version 1.0.0 */ import Foundation import BackgroundTasks import UserNotifications /** * Manages iOS background tasks for T–lead prefetch functionality * * This class implements the critical iOS background execution: * - Schedules BGTaskScheduler tasks for T–lead prefetch * - Performs single-attempt content fetch with 12s timeout * - Handles ETag/304 caching and TTL validation * - Integrates with existing SQLite storage and TTL enforcement */ @available(iOS 13.0, *) class DailyNotificationBackgroundTaskManager { // MARK: - Constants private static let TAG = "DailyNotificationBackgroundTaskManager" private static let BACKGROUND_TASK_IDENTIFIER = "com.timesafari.dailynotification.prefetch" private static let PREFETCH_TIMEOUT_SECONDS: TimeInterval = 12.0 private static let TASK_EXPIRATION_SECONDS: TimeInterval = 30.0 // MARK: - Properties private let database: DailyNotificationDatabase private let ttlEnforcer: DailyNotificationTTLEnforcer private let rollingWindow: DailyNotificationRollingWindow private let urlSession: URLSession // MARK: - Initialization /** * Initialize the background task manager * * @param database SQLite database instance * @param ttlEnforcer TTL enforcement instance * @param rollingWindow Rolling window manager */ init(database: DailyNotificationDatabase, ttlEnforcer: DailyNotificationTTLEnforcer, rollingWindow: DailyNotificationRollingWindow) { self.database = database self.ttlEnforcer = ttlEnforcer self.rollingWindow = rollingWindow // Configure URL session for prefetch requests let config = URLSessionConfiguration.background(withIdentifier: "com.timesafari.dailynotification.prefetch") config.timeoutIntervalForRequest = Self.PREFETCH_TIMEOUT_SECONDS config.timeoutIntervalForResource = Self.PREFETCH_TIMEOUT_SECONDS self.urlSession = URLSession(configuration: config) print("\(Self.TAG): Background task manager initialized") } // MARK: - Background Task Registration /** * Register background task with BGTaskScheduler * * This method should be called during app launch to register * the background task identifier with the system. */ func registerBackgroundTask() { guard #available(iOS 13.0, *) else { print("\(Self.TAG): Background tasks not available on this iOS version") return } BGTaskScheduler.shared.register(forTaskWithIdentifier: Self.BACKGROUND_TASK_IDENTIFIER, using: nil) { task in self.handleBackgroundTask(task: task as! BGAppRefreshTask) } print("\(Self.TAG): Background task registered: \(Self.BACKGROUND_TASK_IDENTIFIER)") } /** * Schedule next background task for T–lead prefetch * * @param scheduledTime T (slot time) when notification should fire * @param prefetchLeadMinutes Minutes before T to perform prefetch */ func scheduleBackgroundTask(scheduledTime: Date, prefetchLeadMinutes: Int) { guard #available(iOS 13.0, *) else { print("\(Self.TAG): Background tasks not available on this iOS version") return } // Calculate T–lead time let tLeadTime = scheduledTime.addingTimeInterval(-TimeInterval(prefetchLeadMinutes * 60)) // Only schedule if T–lead is in the future guard tLeadTime > Date() else { print("\(Self.TAG): T–lead time has passed, skipping background task") return } let request = BGAppRefreshTaskRequest(identifier: Self.BACKGROUND_TASK_IDENTIFIER) request.earliestBeginDate = tLeadTime do { try BGTaskScheduler.shared.submit(request) print("\(Self.TAG): Background task scheduled for T–lead: \(tLeadTime)") } catch { print("\(Self.TAG): Failed to schedule background task: \(error)") } } // MARK: - Background Task Handling /** * Handle background task execution * * @param task BGAppRefreshTask from the system */ private func handleBackgroundTask(task: BGAppRefreshTask) { print("\(Self.TAG): Background task started") // Set task expiration handler task.expirationHandler = { print("\(Self.TAG): Background task expired") task.setTaskCompleted(success: false) } // Perform T–lead prefetch performTLeadPrefetch { success in print("\(Self.TAG): Background task completed with success: \(success)") task.setTaskCompleted(success: success) // Schedule next background task if needed self.scheduleNextBackgroundTask() } } /** * Perform T–lead prefetch with single attempt * * @param completion Completion handler with success status */ private func performTLeadPrefetch(completion: @escaping (Bool) -> Void) { print("\(Self.TAG): Starting T–lead prefetch") // Get notifications that need prefetch getNotificationsNeedingPrefetch { notifications in guard !notifications.isEmpty else { print("\(Self.TAG): No notifications need prefetch") completion(true) return } print("\(Self.TAG): Found \(notifications.count) notifications needing prefetch") // Perform prefetch for each notification let group = DispatchGroup() var successCount = 0 for notification in notifications { group.enter() self.prefetchNotificationContent(notification) { success in if success { successCount += 1 } group.leave() } } group.notify(queue: .main) { print("\(Self.TAG): T–lead prefetch completed: \(successCount)/\(notifications.count) successful") completion(successCount > 0) } } } /** * Prefetch content for a single notification * * @param notification Notification to prefetch * @param completion Completion handler with success status */ private func prefetchNotificationContent(_ notification: NotificationContent, completion: @escaping (Bool) -> Void) { guard let url = URL(string: notification.url ?? "") else { print("\(Self.TAG): Invalid URL for notification: \(notification.id)") completion(false) return } print("\(Self.TAG): Prefetching content for notification: \(notification.id)") // Create request with ETag support var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue("application/json", forHTTPHeaderField: "Accept") // Add ETag if available if let etag = notification.etag { request.setValue(etag, forHTTPHeaderField: "If-None-Match") } // Perform request with timeout let task = urlSession.dataTask(with: request) { data, response, error in self.handlePrefetchResponse(notification: notification, data: data, response: response, error: error, completion: completion) } task.resume() } /** * Handle prefetch response * * @param notification Original notification * @param data Response data * @param response HTTP response * @param error Request error * @param completion Completion handler */ private func handlePrefetchResponse(notification: NotificationContent, data: Data?, response: URLResponse?, error: Error?, completion: @escaping (Bool) -> Void) { if let error = error { print("\(Self.TAG): Prefetch error for \(notification.id): \(error)") completion(false) return } guard let httpResponse = response as? HTTPURLResponse else { print("\(Self.TAG): Invalid response for \(notification.id)") completion(false) return } print("\(Self.TAG): Prefetch response for \(notification.id): \(httpResponse.statusCode)") switch httpResponse.statusCode { case 200: // New content available handleNewContent(notification: notification, data: data, response: httpResponse, completion: completion) case 304: // Content unchanged (ETag match) handleUnchangedContent(notification: notification, response: httpResponse, completion: completion) default: print("\(Self.TAG): Unexpected status code for \(notification.id): \(httpResponse.statusCode)") completion(false) } } /** * Handle new content response * * @param notification Original notification * @param data New content data * @param response HTTP response * @param completion Completion handler */ private func handleNewContent(notification: NotificationContent, data: Data?, response: HTTPURLResponse, completion: @escaping (Bool) -> Void) { guard let data = data else { print("\(Self.TAG): No data in response for \(notification.id)") completion(false) return } do { // Parse new content let newContent = try JSONSerialization.jsonObject(with: data) as? [String: Any] // Update notification with new content var updatedNotification = notification updatedNotification.payload = newContent updatedNotification.fetchedAt = Date().timeIntervalSince1970 * 1000 updatedNotification.etag = response.allHeaderFields["ETag"] as? String // Check TTL before storing if ttlEnforcer.validateBeforeArming(updatedNotification) { // Store updated content storeUpdatedContent(updatedNotification) { success in if success { print("\(Self.TAG): New content stored for \(notification.id)") // Re-arm notification if still within TTL self.rearmNotificationIfNeeded(updatedNotification) } completion(success) } } else { print("\(Self.TAG): New content violates TTL for \(notification.id)") completion(false) } } catch { print("\(Self.TAG): Failed to parse new content for \(notification.id): \(error)") completion(false) } } /** * Handle unchanged content response (304) * * @param notification Original notification * @param response HTTP response * @param completion Completion handler */ private func handleUnchangedContent(notification: NotificationContent, response: HTTPURLResponse, completion: @escaping (Bool) -> Void) { print("\(Self.TAG): Content unchanged for \(notification.id) (304)") // Update ETag if provided if let etag = response.allHeaderFields["ETag"] as? String { var updatedNotification = notification updatedNotification.etag = etag storeUpdatedContent(updatedNotification) { success in completion(success) } } else { completion(true) } } /** * Store updated content in database * * @param notification Updated notification * @param completion Completion handler */ private func storeUpdatedContent(_ notification: NotificationContent, completion: @escaping (Bool) -> Void) { // This would typically store the updated content in SQLite // For now, we'll simulate success print("\(Self.TAG): Storing updated content for \(notification.id)") completion(true) } /** * Re-arm notification if still within TTL * * @param notification Updated notification */ private func rearmNotificationIfNeeded(_ notification: NotificationContent) { // Check if notification should be re-armed if ttlEnforcer.validateBeforeArming(notification) { print("\(Self.TAG): Re-arming notification: \(notification.id)") // This would typically re-arm the notification // For now, we'll just log the action } else { print("\(Self.TAG): Notification \(notification.id) not re-armed due to TTL") } } /** * Get notifications that need prefetch * * @param completion Completion handler with notifications array */ private func getNotificationsNeedingPrefetch(completion: @escaping ([NotificationContent]) -> Void) { // This would typically query the database for notifications // that need prefetch based on T–lead timing // For now, we'll return an empty array print("\(Self.TAG): Querying notifications needing prefetch") completion([]) } /** * Schedule next background task if needed */ private func scheduleNextBackgroundTask() { // This would typically check for the next notification // that needs prefetch and schedule accordingly print("\(Self.TAG): Scheduling next background task") } // MARK: - Public Methods /** * Cancel all pending background tasks */ func cancelAllBackgroundTasks() { guard #available(iOS 13.0, *) else { return } BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: Self.BACKGROUND_TASK_IDENTIFIER) print("\(Self.TAG): All background tasks cancelled") } /** * Get background task status * * @return Status information */ func getBackgroundTaskStatus() -> [String: Any] { guard #available(iOS 13.0, *) else { return ["available": false, "reason": "iOS version not supported"] } return [ "available": true, "identifier": Self.BACKGROUND_TASK_IDENTIFIER, "timeout": Self.PREFETCH_TIMEOUT_SECONDS, "expiration": Self.TASK_EXPIRATION_SECONDS ] } }