feat(ios): enhance background task handlers and documentation
Enhance background task handlers with recovery logic and comprehensive
code documentation:
Background Task Handlers (Section 3.3):
- Enhance handleBackgroundFetch with recovery logic:
- Verify scheduled notifications after fetch
- Schedule next background task automatically
- Improved expiration handling with graceful cleanup
- Enhance handleBackgroundNotify with recovery logic:
- Verify scheduled notifications state
- Prepare for next task scheduling
- Improved expiration handling with graceful cleanup
- Add getNextScheduledNotificationTime() helper method
- Wraps scheduler.getNextNotificationTime() with timeout
- Used for automatic next task scheduling
Code Documentation (Section 10.1):
- Add comprehensive file-level documentation to ReactivationManager:
- Purpose, features, architecture overview
- Recovery scenarios supported
- Error handling approach
- Thread safety notes
- Cross-references to requirements docs
- Add detailed method-level documentation:
- performRecovery(): process, scenarios, error handling
- detectScenario(): detection logic, error handling
- performColdStartRecovery(): steps, return values
- detectMissedNotifications(): criteria, error handling
- verifyFutureNotifications(): verification process
- rescheduleMissingNotification(): process, throws
- handleTerminationRecovery(): comprehensive recovery
- performBootRecovery(): boot recovery process
- recordRecoveryHistory(): history recording
- recordRecoveryFailure(): failure recording
- detectBootScenario(): detection logic
- updateLastLaunchTime(): storage details
- verifyBGTaskRegistration(): diagnostic method
- Add @param, @return, @throws tags to all methods
- Document error handling behavior for all methods
Implementation Status (Section 10.2):
- Update ios/Plugin/README.md with current status:
- Mark all completed features as ✅
- Add new components (DAO classes, ReactivationManager)
- Update version to 1.1.0
- Add recovery scenarios supported
- Update architecture overview
- Add last updated date (2025-12-08)
Completes sections 3.3, 10.1, and 10.2 of iOS implementation checklist.
This commit is contained in:
@@ -12,15 +12,49 @@ import BackgroundTasks
|
||||
import CoreData
|
||||
|
||||
/**
|
||||
* DailyNotificationReactivationManager.swift
|
||||
*
|
||||
* Manages recovery of notifications on app launch
|
||||
* Phase 1: Cold start recovery only
|
||||
*
|
||||
* This class implements comprehensive recovery logic for iOS app lifecycle scenarios:
|
||||
* - Cold Start Recovery: Detects and recovers missed notifications after app termination
|
||||
* - Termination Recovery: Full recovery when app was terminated by system
|
||||
* - Boot Recovery: Recovery after device reboot
|
||||
* - Warm Start: Optimized path when no recovery needed
|
||||
*
|
||||
* Features:
|
||||
* - Scenario detection (none, cold start, warm start, termination, boot)
|
||||
* - Missed notification detection and marking
|
||||
* - Future notification verification and rescheduling
|
||||
* - Comprehensive error handling (non-fatal, graceful degradation)
|
||||
* - Execution time tracking and metrics recording
|
||||
* - History persistence via Core Data
|
||||
*
|
||||
* Implements:
|
||||
* - [Plugin Requirements §3.1.2 - App Cold Start](../docs/alarms/03-plugin-requirements.md#312-app-cold-start) (iOS equivalent)
|
||||
* Platform Reference: [iOS §3.1.1](../docs/alarms/01-platform-capability-reference.md#311-notifications-survive-app-termination)
|
||||
* - [Plugin Requirements §3.1.3 - App Termination](../docs/alarms/03-plugin-requirements.md#313-app-termination) (iOS equivalent)
|
||||
* - [Plugin Requirements §3.1.4 - Device Boot](../docs/alarms/03-plugin-requirements.md#314-device-boot) (iOS equivalent)
|
||||
*
|
||||
* Platform Reference:
|
||||
* - [iOS §3.1.1](../docs/alarms/01-platform-capability-reference.md#311-notifications-survive-app-termination)
|
||||
* - [iOS Recovery Scenario Mapping](../docs/ios-recovery-scenario-mapping.md)
|
||||
*
|
||||
* Error Handling:
|
||||
* - All database errors are caught and handled gracefully (non-fatal)
|
||||
* - All notification center errors are caught and handled gracefully (non-fatal)
|
||||
* - All scheduling errors are caught and handled gracefully (non-fatal)
|
||||
* - Partial results returned when some operations fail
|
||||
* - App never crashes due to recovery errors
|
||||
*
|
||||
* Thread Safety:
|
||||
* - All operations are async/await based
|
||||
* - Recovery runs in background Task to avoid blocking app startup
|
||||
* - Timeout protection (2 seconds default) prevents hanging
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0 - Phase 1: Cold start recovery
|
||||
* @version 1.0.0
|
||||
* @created 2025-12-08
|
||||
* @lastUpdated 2025-12-08
|
||||
*/
|
||||
class DailyNotificationReactivationManager {
|
||||
|
||||
@@ -62,19 +96,47 @@ class DailyNotificationReactivationManager {
|
||||
|
||||
/**
|
||||
* Perform recovery on app launch
|
||||
* Phase 3: Includes boot detection and recovery
|
||||
*
|
||||
* Scenario detection implemented:
|
||||
* - .none: Empty database (first launch)
|
||||
* - .coldStart: Notifications exist, may need verification
|
||||
* - .warmStart: Notifications match DB state (optimization, no recovery)
|
||||
* - .termination: App terminated, notifications cleared
|
||||
* This is the main entry point for recovery operations. Called automatically
|
||||
* when the plugin loads via DailyNotificationPlugin.load().
|
||||
*
|
||||
* Phase 3: Boot detection added
|
||||
* Recovery Process:
|
||||
* 1. Detects boot scenario (if device rebooted)
|
||||
* 2. Detects recovery scenario (none, cold start, warm start, termination)
|
||||
* 3. Performs appropriate recovery actions based on scenario
|
||||
* 4. Records recovery metrics in Core Data history
|
||||
*
|
||||
* Runs asynchronously with timeout to avoid blocking app startup
|
||||
* Scenario Detection:
|
||||
* - `.none`: Empty database (first launch) - no recovery needed
|
||||
* - `.coldStart`: Notifications exist, may need verification - performs recovery
|
||||
* - `.warmStart`: Notifications match DB state - no recovery needed (optimization)
|
||||
* - `.termination`: App terminated, notifications cleared - full recovery
|
||||
* - `.boot`: Device rebooted - full recovery
|
||||
*
|
||||
* Rollback Safety: If recovery fails, app continues normally
|
||||
* Error Handling:
|
||||
* - All errors are caught and logged (non-fatal)
|
||||
* - Recovery failures are recorded in history
|
||||
* - App continues normally even if recovery fails
|
||||
* - Partial results returned when some operations fail
|
||||
*
|
||||
* Performance:
|
||||
* - Runs asynchronously in background Task
|
||||
* - Timeout protection (2 seconds default) prevents hanging
|
||||
* - Non-blocking: does not delay app startup
|
||||
*
|
||||
* Thread Safety:
|
||||
* - Safe to call from any thread
|
||||
* - All operations are async/await based
|
||||
*
|
||||
* @note This method is called automatically on app launch. Manual calls are
|
||||
* generally not needed unless testing recovery scenarios.
|
||||
*
|
||||
* @throws Never throws - all errors are caught and handled internally
|
||||
*
|
||||
* @see detectScenario() for scenario detection logic
|
||||
* @see performColdStartRecovery() for cold start recovery
|
||||
* @see handleTerminationRecovery() for termination recovery
|
||||
* @see performBootRecovery() for boot recovery
|
||||
*/
|
||||
func performRecovery() {
|
||||
Task {
|
||||
@@ -243,13 +305,35 @@ class DailyNotificationReactivationManager {
|
||||
/**
|
||||
* Perform cold start recovery
|
||||
*
|
||||
* Steps:
|
||||
* 1. Detect missed notifications (scheduled_time < now, not delivered)
|
||||
* 2. Mark missed notifications in database
|
||||
* 3. Verify future notifications are scheduled
|
||||
* 4. Reschedule missing future notifications
|
||||
* Handles recovery when app was terminated but notifications may still exist
|
||||
* in UNUserNotificationCenter. This is the most common recovery scenario.
|
||||
*
|
||||
* @return RecoveryResult with counts
|
||||
* Recovery Steps:
|
||||
* 1. Detect missed notifications (scheduled_time < now, not delivered)
|
||||
* 2. Mark missed notifications in database (update delivery status)
|
||||
* 3. Verify future notifications are scheduled in UNUserNotificationCenter
|
||||
* 4. Reschedule any missing future notifications
|
||||
*
|
||||
* Error Handling:
|
||||
* - Individual notification errors are caught and counted
|
||||
* - Partial results returned if some operations fail
|
||||
* - All errors logged but don't stop recovery process
|
||||
*
|
||||
* Performance:
|
||||
* - Processes notifications in batches
|
||||
* - Non-blocking async operations
|
||||
*
|
||||
* @return RecoveryResult containing:
|
||||
* - missedCount: Number of missed notifications marked
|
||||
* - rescheduledCount: Number of notifications rescheduled
|
||||
* - verifiedCount: Number of notifications verified as scheduled
|
||||
* - errors: Number of errors encountered during recovery
|
||||
*
|
||||
* @throws Never throws - all errors are caught and counted in result
|
||||
*
|
||||
* @see detectMissedNotifications() for missed notification detection
|
||||
* @see verifyFutureNotifications() for future notification verification
|
||||
* @see RecoveryResult for result structure
|
||||
*/
|
||||
private func performColdStartRecovery() async throws -> RecoveryResult {
|
||||
let currentTime = Date()
|
||||
@@ -328,10 +412,24 @@ class DailyNotificationReactivationManager {
|
||||
/**
|
||||
* Detect missed notifications
|
||||
*
|
||||
* @param currentTime Current time for comparison
|
||||
* @return Array of missed notifications
|
||||
* Identifies notifications that were scheduled to fire but haven't been delivered.
|
||||
* A notification is considered "missed" if:
|
||||
* - scheduledTime < currentTime (notification time has passed)
|
||||
* - deliveryStatus != 'delivered' (not yet marked as delivered)
|
||||
*
|
||||
* Note: Internal for testing
|
||||
* Error Handling:
|
||||
* - Storage errors: Returns empty array (non-fatal)
|
||||
* - All errors logged but don't crash app
|
||||
*
|
||||
* @param currentTime Current time for comparison (typically Date())
|
||||
* @return Array of NotificationContent that are considered missed
|
||||
*
|
||||
* @throws Never throws - all errors are caught and handled internally
|
||||
*
|
||||
* @note Internal visibility for unit testing. External code should use
|
||||
* performRecovery() which calls this method internally.
|
||||
*
|
||||
* @see NotificationContent for notification structure
|
||||
*/
|
||||
internal func detectMissedNotifications(currentTime: Date) async throws -> [NotificationContent] {
|
||||
// Get all notifications from storage
|
||||
@@ -388,9 +486,32 @@ class DailyNotificationReactivationManager {
|
||||
/**
|
||||
* Verify future notifications are scheduled
|
||||
*
|
||||
* @return VerificationResult with comparison details
|
||||
* Compares notifications in storage (scheduled for future) with pending
|
||||
* notifications in UNUserNotificationCenter to identify any missing ones.
|
||||
*
|
||||
* Note: Internal for testing
|
||||
* Verification Process:
|
||||
* 1. Get all pending notifications from UNUserNotificationCenter
|
||||
* 2. Get all future notifications from storage (scheduledTime >= now)
|
||||
* 3. Compare IDs to find missing notifications
|
||||
* 4. Return verification result with counts and missing IDs
|
||||
*
|
||||
* Error Handling:
|
||||
* - Notification center errors: Returns partial result (assumes all missing)
|
||||
* - Storage errors: Returns partial result (assumes none found)
|
||||
* - All errors logged but don't crash app
|
||||
*
|
||||
* @return VerificationResult containing:
|
||||
* - totalSchedules: Total future notifications in storage
|
||||
* - notificationsFound: Number found in UNUserNotificationCenter
|
||||
* - notificationsMissing: Number missing from UNUserNotificationCenter
|
||||
* - missingIds: Array of notification IDs that need rescheduling
|
||||
*
|
||||
* @throws Never throws - all errors are caught and handled internally
|
||||
*
|
||||
* @note Internal visibility for unit testing. External code should use
|
||||
* performRecovery() which calls this method internally.
|
||||
*
|
||||
* @see VerificationResult for result structure
|
||||
*/
|
||||
internal func verifyFutureNotifications() async throws -> VerificationResult {
|
||||
// Get pending notifications from UNUserNotificationCenter
|
||||
@@ -453,7 +574,22 @@ class DailyNotificationReactivationManager {
|
||||
/**
|
||||
* Reschedule missing notification
|
||||
*
|
||||
* Retrieves notification content from storage and reschedules it using
|
||||
* the scheduler. This is called when verifyFutureNotifications() identifies
|
||||
* a notification that should be scheduled but isn't in UNUserNotificationCenter.
|
||||
*
|
||||
* Error Handling:
|
||||
* - Storage errors: Throws ReactivationError.notificationNotFound
|
||||
* - Scheduling errors: Throws ReactivationError.rescheduleFailed
|
||||
* - Errors are caught by caller and counted in RecoveryResult.errors
|
||||
*
|
||||
* @param id Notification ID to reschedule
|
||||
*
|
||||
* @throws ReactivationError.notificationNotFound if notification not found in storage
|
||||
* @throws ReactivationError.rescheduleFailed if scheduling fails
|
||||
*
|
||||
* @see verifyFutureNotifications() for identification of missing notifications
|
||||
* @see DailyNotificationScheduler.scheduleNotification() for scheduling logic
|
||||
*/
|
||||
private func rescheduleMissingNotification(id: String) async throws {
|
||||
// Get notification content from storage
|
||||
@@ -733,12 +869,32 @@ class DailyNotificationReactivationManager {
|
||||
// MARK: - History Recording
|
||||
|
||||
/**
|
||||
* Record recovery history
|
||||
* Record recovery history in Core Data
|
||||
*
|
||||
* @param result Recovery result
|
||||
* @param scenario Recovery scenario
|
||||
* @param startTime When recovery started
|
||||
* @param endTime When recovery ended
|
||||
* Persists recovery metrics to Core Data History entity for observability
|
||||
* and debugging. Records execution time, counts, and scenario information.
|
||||
*
|
||||
* History Record Contains:
|
||||
* - Scenario type (cold start, termination, boot)
|
||||
* - Missed notification count
|
||||
* - Rescheduled notification count
|
||||
* - Verified notification count
|
||||
* - Error count
|
||||
* - Execution duration (milliseconds)
|
||||
*
|
||||
* Error Handling:
|
||||
* - Core Data errors are logged but don't fail recovery
|
||||
* - Best effort: if history recording fails, recovery still succeeds
|
||||
*
|
||||
* @param result Recovery result with metrics
|
||||
* @param scenario Recovery scenario that was executed
|
||||
* @param startTime When recovery started (for duration calculation)
|
||||
* @param endTime When recovery ended (for duration calculation)
|
||||
*
|
||||
* @throws Never throws - all errors are caught and logged internally
|
||||
*
|
||||
* @see HistoryDAO.recordRecovery() for Core Data persistence
|
||||
* @see RecoveryResult for result structure
|
||||
*/
|
||||
private func recordRecoveryHistory(_ result: RecoveryResult, scenario: RecoveryScenario, startTime: Date, endTime: Date) async throws {
|
||||
// Log recovery metrics
|
||||
@@ -776,10 +932,28 @@ class DailyNotificationReactivationManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Record recovery failure
|
||||
* Record recovery failure in Core Data
|
||||
*
|
||||
* @param error Error that occurred
|
||||
* @param scenario Optional recovery scenario (if known)
|
||||
* Persists error information to Core Data History entity when recovery
|
||||
* fails. Records error details, type, and optional scenario information.
|
||||
*
|
||||
* Error Record Contains:
|
||||
* - Error message (localizedDescription)
|
||||
* - Error type (Swift type name)
|
||||
* - NSError domain and code (if applicable)
|
||||
* - NSError userInfo (if applicable)
|
||||
* - Scenario (if known)
|
||||
*
|
||||
* Error Handling:
|
||||
* - Core Data errors are logged but don't fail recovery
|
||||
* - Best effort: if history recording fails, error is still logged
|
||||
*
|
||||
* @param error Error that occurred during recovery
|
||||
* @param scenario Optional recovery scenario (if known before failure)
|
||||
*
|
||||
* @throws Never throws - all errors are caught and logged internally
|
||||
*
|
||||
* @see HistoryDAO.recordRecoveryFailure() for Core Data persistence
|
||||
*/
|
||||
private func recordRecoveryFailure(_ error: Error, scenario: String? = nil) async throws {
|
||||
// Enhanced error logging
|
||||
|
||||
Reference in New Issue
Block a user