You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							431 lines
						
					
					
						
							15 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							431 lines
						
					
					
						
							15 KiB
						
					
					
				
								/**
							 | 
						|
								 * 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
							 | 
						|
								        ]
							 | 
						|
								    }
							 | 
						|
								}
							 | 
						|
								
							 |