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
|
|
]
|
|
}
|
|
}
|
|
|