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

/**
* DailyNotificationBackgroundTaskManager.swift
*
* iOS Background Task Manager for Tlead 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 Tlead prefetch functionality
*
* This class implements the critical iOS background execution:
* - Schedules BGTaskScheduler tasks for Tlead 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 Tlead 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 Tlead time
let tLeadTime = scheduledTime.addingTimeInterval(-TimeInterval(prefetchLeadMinutes * 60))
// Only schedule if Tlead 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 Tlead 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 Tlead 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 Tlead 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
]
}
}