// // DailyNotificationBackgroundTaskTestHarness.swift // DailyNotificationPlugin // // Test harness for BGTaskScheduler prefetch functionality // Reference implementation demonstrating task registration, scheduling, and handling // // See: doc/test-app-ios/IOS_PREFETCH_TESTING.md for testing procedures // import Foundation import BackgroundTasks import UIKit /// Minimal BGTaskScheduler test harness for DailyNotificationPlugin prefetch testing /// /// This is a reference implementation demonstrating: /// - Task registration /// - Task scheduling /// - Task handler implementation /// - Expiration handling /// - Completion reporting /// /// **Usage:** /// - Reference this when implementing actual prefetch logic in `DailyNotificationBackgroundTaskManager.swift` /// - Use in test app for debugging BGTaskScheduler behavior /// - See `doc/test-app-ios/IOS_PREFETCH_TESTING.md` for comprehensive testing guide /// /// **Info.plist Requirements:** /// ```xml /// BGTaskSchedulerPermittedIdentifiers /// /// com.timesafari.dailynotification.fetch /// /// ``` /// /// **Background Modes (Xcode Capabilities):** /// - Background fetch /// - Background processing (if using BGProcessingTask) class DailyNotificationBackgroundTaskTestHarness { // MARK: - Constants static let prefetchTaskIdentifier = "com.timesafari.dailynotification.fetch" // MARK: - Registration /// Register BGTaskScheduler task handler /// /// Call this in AppDelegate.application(_:didFinishLaunchingWithOptions:) /// before app finishes launching. static func registerBackgroundTasks() { BGTaskScheduler.shared.register( forTaskWithIdentifier: prefetchTaskIdentifier, using: nil ) { task in // This closure is called when the task is launched by the system handlePrefetchTask(task: task as! BGAppRefreshTask) } print("[DNP-FETCH] Registered BGTaskScheduler with id=\(prefetchTaskIdentifier)") } // MARK: - Scheduling /// Schedule a BGAppRefreshTask for prefetch /// /// - Parameter earliestOffsetSeconds: Seconds from now when task can begin /// - Returns: true if scheduling succeeded, false otherwise @discardableResult static func schedulePrefetchTask(earliestOffsetSeconds: TimeInterval) -> Bool { let request = BGAppRefreshTaskRequest(identifier: prefetchTaskIdentifier) request.earliestBeginDate = Date(timeIntervalSinceNow: earliestOffsetSeconds) do { try BGTaskScheduler.shared.submit(request) print("[DNP-FETCH] BGAppRefreshTask scheduled (earliestBeginDate=\(String(describing: request.earliestBeginDate)))") return true } catch { print("[DNP-FETCH] Failed to schedule BGAppRefreshTask: \(error)") return false } } // MARK: - Handler /// Handle BGAppRefreshTask execution /// /// This is called by the system when the background task is launched. /// Replace PrefetchOperation with your actual prefetch logic. private static func handlePrefetchTask(task: BGAppRefreshTask) { print("[DNP-FETCH] BGTask handler invoked (task.identifier=\(task.identifier))") // Schedule the next one early, so that there's always a pending task // In real implementation, calculate next schedule based on notification time schedulePrefetchTask(earliestOffsetSeconds: 60 * 30) // 30 minutes later, for example // Define the work let queue = OperationQueue() queue.maxConcurrentOperationCount = 1 let operation = PrefetchOperation() // Set expiration handler // Called if iOS decides to end the task early (typically ~30 seconds) task.expirationHandler = { print("[DNP-FETCH] Task expired") operation.cancel() } // Set completion handler operation.completionBlock = { let success = !operation.isCancelled print("[DNP-FETCH] Task completionBlock (success=\(success))") task.setTaskCompleted(success: success) } queue.addOperation(operation) } } // MARK: - Prefetch Operation /// Simple Operation example for testing /// /// Replace this with your actual prefetch logic: /// - HTTP fetch from TimeSafari API /// - JWT signing /// - ETag validation /// - Content caching /// - Error handling class PrefetchOperation: Operation { override func main() { if isCancelled { return } print("[DNP-FETCH] PrefetchOperation: starting fake fetch...") // Simulate some work // In real implementation, this would be: // - Make HTTP request // - Parse response // - Cache content // - Update database Thread.sleep(forTimeInterval: 2) if isCancelled { return } print("[DNP-FETCH] PrefetchOperation: finished fake fetch.") } } // MARK: - AppDelegate Integration Example /* Example integration in AppDelegate.swift: import UIKit import BackgroundTasks @main class AppDelegate: UIResponder, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // Register background tasks BEFORE app finishes launching DailyNotificationBackgroundTaskTestHarness.registerBackgroundTasks() // Schedule initial task (for testing) DailyNotificationBackgroundTaskTestHarness.schedulePrefetchTask(earliestOffsetSeconds: 5 * 60) // 5 minutes return true } } */ // MARK: - Testing in Simulator /* To test in simulator: 1. Run app in Xcode 2. Background the app (Home button / Cmd+Shift+H) 3. In Xcode menu: - Debug → Simulate Background Fetch, or - Debug → Simulate Background Refresh 4. Check console logs for [DNP-FETCH] messages Expected logs: - [DNP-FETCH] Registered BGTaskScheduler with id=... - [DNP-FETCH] BGAppRefreshTask scheduled (earliestBeginDate=...) - [DNP-FETCH] BGTask handler invoked (task.identifier=...) - [DNP-FETCH] PrefetchOperation: starting fake fetch... - [DNP-FETCH] PrefetchOperation: finished fake fetch. - [DNP-FETCH] Task completionBlock (success=true) */ // MARK: - Testing on Real Device /* To test on real device: 1. Install app on iPhone 2. Enable Background App Refresh in Settings → [Your App] 3. Schedule a notification with prefetch 4. Lock device and leave idle (plugged in for best results) 5. Monitor logs via: - Xcode → Devices & Simulators → device → open console - Or os_log aggregator / remote logging Note: Real device timing is heuristic, not deterministic. iOS will run the task when it determines it's appropriate, not necessarily at the exact earliestBeginDate. */