Fixed critical compilation errors preventing iOS plugin build: - Updated logger API calls from logger.debug(TAG, msg) to logger.log(.debug, msg) across all iOS plugin files to match DailyNotificationLogger interface - Fixed async/await concurrency in makeConditionalRequest using semaphore pattern - Fixed NotificationContent immutability by creating new instances instead of mutation - Changed private access control to internal for extension-accessible methods - Added iOS 15.0+ availability checks for interruptionLevel property - Fixed static member references using Self.MEMBER_NAME syntax - Added missing .scheduling case to exhaustive switch statement - Fixed variable initialization in retry state closures Added DailyNotificationStorage.swift implementation matching Android pattern. Updated build scripts with improved error reporting and full log visibility. iOS plugin now compiles successfully. All build errors resolved.
446 lines
16 KiB
Swift
446 lines
16 KiB
Swift
/**
|
||
* 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]
|
||
|
||
// Create new notification instance with updated content
|
||
let updatedNotification = NotificationContent(
|
||
id: notification.id,
|
||
title: notification.title,
|
||
body: notification.body,
|
||
scheduledTime: notification.scheduledTime,
|
||
fetchedAt: Date().timeIntervalSince1970 * 1000,
|
||
url: notification.url,
|
||
payload: newContent,
|
||
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 {
|
||
let updatedNotification = NotificationContent(
|
||
id: notification.id,
|
||
title: notification.title,
|
||
body: notification.body,
|
||
scheduledTime: notification.scheduledTime,
|
||
fetchedAt: notification.fetchedAt,
|
||
url: notification.url,
|
||
payload: notification.payload,
|
||
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
|
||
]
|
||
}
|
||
}
|