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.
		
		
		
		
		
			
		
			
				
					
					
						
							403 lines
						
					
					
						
							14 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							403 lines
						
					
					
						
							14 KiB
						
					
					
				
								/**
							 | 
						|
								 * DailyNotificationRollingWindow.swift
							 | 
						|
								 * 
							 | 
						|
								 * iOS Rolling window safety for notification scheduling
							 | 
						|
								 * Ensures today's notifications are always armed and tomorrow's are armed within iOS caps
							 | 
						|
								 * 
							 | 
						|
								 * @author Matthew Raymer
							 | 
						|
								 * @version 1.0.0
							 | 
						|
								 */
							 | 
						|
								
							 | 
						|
								import Foundation
							 | 
						|
								import UserNotifications
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Manages rolling window safety for notification scheduling on iOS
							 | 
						|
								 * 
							 | 
						|
								 * This class implements the critical rolling window logic:
							 | 
						|
								 * - Today's remaining notifications are always armed
							 | 
						|
								 * - Tomorrow's notifications are armed only if within iOS capacity limits
							 | 
						|
								 * - Automatic window maintenance as time progresses
							 | 
						|
								 * - iOS-specific capacity management
							 | 
						|
								 */
							 | 
						|
								class DailyNotificationRollingWindow {
							 | 
						|
								    
							 | 
						|
								    // MARK: - Constants
							 | 
						|
								    
							 | 
						|
								    private static let TAG = "DailyNotificationRollingWindow"
							 | 
						|
								    
							 | 
						|
								    // iOS notification limits
							 | 
						|
								    private static let IOS_MAX_PENDING_NOTIFICATIONS = 64
							 | 
						|
								    private static let IOS_MAX_DAILY_NOTIFICATIONS = 20
							 | 
						|
								    
							 | 
						|
								    // Window maintenance intervals
							 | 
						|
								    private static let WINDOW_MAINTENANCE_INTERVAL_SECONDS: TimeInterval = 15 * 60 // 15 minutes
							 | 
						|
								    
							 | 
						|
								    // MARK: - Properties
							 | 
						|
								    
							 | 
						|
								    private let ttlEnforcer: DailyNotificationTTLEnforcer
							 | 
						|
								    private let database: DailyNotificationDatabase?
							 | 
						|
								    private let useSharedStorage: Bool
							 | 
						|
								    
							 | 
						|
								    // Window state
							 | 
						|
								    private var lastMaintenanceTime: Date = Date.distantPast
							 | 
						|
								    private var currentPendingCount: Int = 0
							 | 
						|
								    private var currentDailyCount: Int = 0
							 | 
						|
								    
							 | 
						|
								    // MARK: - Initialization
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Initialize rolling window manager
							 | 
						|
								     * 
							 | 
						|
								     * @param ttlEnforcer TTL enforcement instance
							 | 
						|
								     * @param database SQLite database (nil if using UserDefaults)
							 | 
						|
								     * @param useSharedStorage Whether to use SQLite or UserDefaults
							 | 
						|
								     */
							 | 
						|
								    init(ttlEnforcer: DailyNotificationTTLEnforcer, 
							 | 
						|
								         database: DailyNotificationDatabase?, 
							 | 
						|
								         useSharedStorage: Bool) {
							 | 
						|
								        self.ttlEnforcer = ttlEnforcer
							 | 
						|
								        self.database = database
							 | 
						|
								        self.useSharedStorage = useSharedStorage
							 | 
						|
								        
							 | 
						|
								        print("\(Self.TAG): Rolling window initialized for iOS")
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    // MARK: - Window Maintenance
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Maintain the rolling window by ensuring proper notification coverage
							 | 
						|
								     * 
							 | 
						|
								     * This method should be called periodically to maintain the rolling window:
							 | 
						|
								     * - Arms today's remaining notifications
							 | 
						|
								     * - Arms tomorrow's notifications if within capacity limits
							 | 
						|
								     * - Updates window state and statistics
							 | 
						|
								     */
							 | 
						|
								    func maintainRollingWindow() {
							 | 
						|
								        do {
							 | 
						|
								            let currentTime = Date()
							 | 
						|
								            
							 | 
						|
								            // Check if maintenance is needed
							 | 
						|
								            if currentTime.timeIntervalSince(lastMaintenanceTime) < Self.WINDOW_MAINTENANCE_INTERVAL_SECONDS {
							 | 
						|
								                print("\(Self.TAG): Window maintenance not needed yet")
							 | 
						|
								                return
							 | 
						|
								            }
							 | 
						|
								            
							 | 
						|
								            print("\(Self.TAG): Starting rolling window maintenance")
							 | 
						|
								            
							 | 
						|
								            // Update current state
							 | 
						|
								            updateWindowState()
							 | 
						|
								            
							 | 
						|
								            // Arm today's remaining notifications
							 | 
						|
								            armTodaysRemainingNotifications()
							 | 
						|
								            
							 | 
						|
								            // Arm tomorrow's notifications if within capacity
							 | 
						|
								            armTomorrowsNotificationsIfWithinCapacity()
							 | 
						|
								            
							 | 
						|
								            // Update maintenance time
							 | 
						|
								            lastMaintenanceTime = currentTime
							 | 
						|
								            
							 | 
						|
								            print("\(Self.TAG): Rolling window maintenance completed: pending=\(currentPendingCount), daily=\(currentDailyCount)")
							 | 
						|
								            
							 | 
						|
								        } catch {
							 | 
						|
								            print("\(Self.TAG): Error during rolling window maintenance: \(error)")
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Arm today's remaining notifications
							 | 
						|
								     * 
							 | 
						|
								     * Ensures all notifications for today that haven't fired yet are armed
							 | 
						|
								     */
							 | 
						|
								    private func armTodaysRemainingNotifications() {
							 | 
						|
								        do {
							 | 
						|
								            print("\(Self.TAG): Arming today's remaining notifications")
							 | 
						|
								            
							 | 
						|
								            // Get today's date
							 | 
						|
								            let today = Date()
							 | 
						|
								            let todayDate = formatDate(today)
							 | 
						|
								            
							 | 
						|
								            // Get all notifications for today
							 | 
						|
								            let todaysNotifications = getNotificationsForDate(todayDate)
							 | 
						|
								            
							 | 
						|
								            var armedCount = 0
							 | 
						|
								            var skippedCount = 0
							 | 
						|
								            
							 | 
						|
								            for notification in todaysNotifications {
							 | 
						|
								                // Check if notification is in the future
							 | 
						|
								                let scheduledTime = Date(timeIntervalSince1970: notification.scheduledTime / 1000)
							 | 
						|
								                if scheduledTime > Date() {
							 | 
						|
								                    
							 | 
						|
								                    // Check TTL before arming
							 | 
						|
								                    if !ttlEnforcer.validateBeforeArming(notification) {
							 | 
						|
								                        print("\(Self.TAG): Skipping today's notification due to TTL: \(notification.id)")
							 | 
						|
								                        skippedCount += 1
							 | 
						|
								                        continue
							 | 
						|
								                    }
							 | 
						|
								                    
							 | 
						|
								                    // Arm the notification
							 | 
						|
								                    let armed = armNotification(notification)
							 | 
						|
								                    if armed {
							 | 
						|
								                        armedCount += 1
							 | 
						|
								                        currentPendingCount += 1
							 | 
						|
								                    } else {
							 | 
						|
								                        print("\(Self.TAG): Failed to arm today's notification: \(notification.id)")
							 | 
						|
								                    }
							 | 
						|
								                }
							 | 
						|
								            }
							 | 
						|
								            
							 | 
						|
								            print("\(Self.TAG): Today's notifications: armed=\(armedCount), skipped=\(skippedCount)")
							 | 
						|
								            
							 | 
						|
								        } catch {
							 | 
						|
								            print("\(Self.TAG): Error arming today's remaining notifications: \(error)")
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Arm tomorrow's notifications if within capacity limits
							 | 
						|
								     * 
							 | 
						|
								     * Only arms tomorrow's notifications if we're within iOS capacity limits
							 | 
						|
								     */
							 | 
						|
								    private func armTomorrowsNotificationsIfWithinCapacity() {
							 | 
						|
								        do {
							 | 
						|
								            print("\(Self.TAG): Checking capacity for tomorrow's notifications")
							 | 
						|
								            
							 | 
						|
								            // Check if we're within capacity limits
							 | 
						|
								            if !isWithinCapacityLimits() {
							 | 
						|
								                print("\(Self.TAG): At capacity limit, skipping tomorrow's notifications")
							 | 
						|
								                return
							 | 
						|
								            }
							 | 
						|
								            
							 | 
						|
								            // Get tomorrow's date
							 | 
						|
								            let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date()) ?? Date()
							 | 
						|
								            let tomorrowDate = formatDate(tomorrow)
							 | 
						|
								            
							 | 
						|
								            // Get all notifications for tomorrow
							 | 
						|
								            let tomorrowsNotifications = getNotificationsForDate(tomorrowDate)
							 | 
						|
								            
							 | 
						|
								            var armedCount = 0
							 | 
						|
								            var skippedCount = 0
							 | 
						|
								            
							 | 
						|
								            for notification in tomorrowsNotifications {
							 | 
						|
								                // Check TTL before arming
							 | 
						|
								                if !ttlEnforcer.validateBeforeArming(notification) {
							 | 
						|
								                    print("\(Self.TAG): Skipping tomorrow's notification due to TTL: \(notification.id)")
							 | 
						|
								                    skippedCount += 1
							 | 
						|
								                    continue
							 | 
						|
								                }
							 | 
						|
								                
							 | 
						|
								                // Arm the notification
							 | 
						|
								                let armed = armNotification(notification)
							 | 
						|
								                if armed {
							 | 
						|
								                    armedCount += 1
							 | 
						|
								                    currentPendingCount += 1
							 | 
						|
								                    currentDailyCount += 1
							 | 
						|
								                } else {
							 | 
						|
								                    print("\(Self.TAG): Failed to arm tomorrow's notification: \(notification.id)")
							 | 
						|
								                }
							 | 
						|
								                
							 | 
						|
								                // Check capacity after each arm
							 | 
						|
								                if !isWithinCapacityLimits() {
							 | 
						|
								                    print("\(Self.TAG): Reached capacity limit while arming tomorrow's notifications")
							 | 
						|
								                    break
							 | 
						|
								                }
							 | 
						|
								            }
							 | 
						|
								            
							 | 
						|
								            print("\(Self.TAG): Tomorrow's notifications: armed=\(armedCount), skipped=\(skippedCount)")
							 | 
						|
								            
							 | 
						|
								        } catch {
							 | 
						|
								            print("\(Self.TAG): Error arming tomorrow's notifications: \(error)")
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Check if we're within iOS capacity limits
							 | 
						|
								     * 
							 | 
						|
								     * @return true if within limits
							 | 
						|
								     */
							 | 
						|
								    private func isWithinCapacityLimits() -> Bool {
							 | 
						|
								        let withinPendingLimit = currentPendingCount < Self.IOS_MAX_PENDING_NOTIFICATIONS
							 | 
						|
								        let withinDailyLimit = currentDailyCount < Self.IOS_MAX_DAILY_NOTIFICATIONS
							 | 
						|
								        
							 | 
						|
								        print("\(Self.TAG): Capacity check: pending=\(currentPendingCount)/\(Self.IOS_MAX_PENDING_NOTIFICATIONS), daily=\(currentDailyCount)/\(Self.IOS_MAX_DAILY_NOTIFICATIONS), within=\(withinPendingLimit && withinDailyLimit)")
							 | 
						|
								        
							 | 
						|
								        return withinPendingLimit && withinDailyLimit
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Update window state by counting current notifications
							 | 
						|
								     */
							 | 
						|
								    private func updateWindowState() {
							 | 
						|
								        do {
							 | 
						|
								            print("\(Self.TAG): Updating window state")
							 | 
						|
								            
							 | 
						|
								            // Count pending notifications
							 | 
						|
								            currentPendingCount = countPendingNotifications()
							 | 
						|
								            
							 | 
						|
								            // Count today's notifications
							 | 
						|
								            let today = Date()
							 | 
						|
								            let todayDate = formatDate(today)
							 | 
						|
								            currentDailyCount = countNotificationsForDate(todayDate)
							 | 
						|
								            
							 | 
						|
								            print("\(Self.TAG): Window state updated: pending=\(currentPendingCount), daily=\(currentDailyCount)")
							 | 
						|
								            
							 | 
						|
								        } catch {
							 | 
						|
								            print("\(Self.TAG): Error updating window state: \(error)")
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    // MARK: - Notification Management
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Arm a notification using UNUserNotificationCenter
							 | 
						|
								     * 
							 | 
						|
								     * @param notification Notification to arm
							 | 
						|
								     * @return true if successfully armed
							 | 
						|
								     */
							 | 
						|
								    private func armNotification(_ notification: NotificationContent) -> Bool {
							 | 
						|
								        do {
							 | 
						|
								            let content = UNMutableNotificationContent()
							 | 
						|
								            content.title = notification.title ?? "Daily Notification"
							 | 
						|
								            content.body = notification.body ?? "Your daily notification is ready"
							 | 
						|
								            content.sound = UNNotificationSound.default
							 | 
						|
								            
							 | 
						|
								            // Create trigger for scheduled time
							 | 
						|
								            let scheduledTime = Date(timeIntervalSince1970: notification.scheduledTime / 1000)
							 | 
						|
								            let trigger = UNCalendarNotificationTrigger(dateMatching: Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: scheduledTime), repeats: false)
							 | 
						|
								            
							 | 
						|
								            // Create request
							 | 
						|
								            let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: trigger)
							 | 
						|
								            
							 | 
						|
								            // Schedule notification
							 | 
						|
								            UNUserNotificationCenter.current().add(request) { error in
							 | 
						|
								                if let error = error {
							 | 
						|
								                    print("\(Self.TAG): Failed to arm notification \(notification.id): \(error)")
							 | 
						|
								                } else {
							 | 
						|
								                    print("\(Self.TAG): Successfully armed notification: \(notification.id)")
							 | 
						|
								                }
							 | 
						|
								            }
							 | 
						|
								            
							 | 
						|
								            return true
							 | 
						|
								            
							 | 
						|
								        } catch {
							 | 
						|
								            print("\(Self.TAG): Error arming notification \(notification.id): \(error)")
							 | 
						|
								            return false
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    // MARK: - Data Access
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Count pending notifications
							 | 
						|
								     * 
							 | 
						|
								     * @return Number of pending notifications
							 | 
						|
								     */
							 | 
						|
								    private func countPendingNotifications() -> Int {
							 | 
						|
								        do {
							 | 
						|
								            // This would typically query the storage for pending notifications
							 | 
						|
								            // For now, we'll use a placeholder implementation
							 | 
						|
								            return 0 // TODO: Implement actual counting logic
							 | 
						|
								            
							 | 
						|
								        } catch {
							 | 
						|
								            print("\(Self.TAG): Error counting pending notifications: \(error)")
							 | 
						|
								            return 0
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Count notifications for a specific date
							 | 
						|
								     * 
							 | 
						|
								     * @param date Date in YYYY-MM-DD format
							 | 
						|
								     * @return Number of notifications for the date
							 | 
						|
								     */
							 | 
						|
								    private func countNotificationsForDate(_ date: String) -> Int {
							 | 
						|
								        do {
							 | 
						|
								            // This would typically query the storage for notifications on a specific date
							 | 
						|
								            // For now, we'll use a placeholder implementation
							 | 
						|
								            return 0 // TODO: Implement actual counting logic
							 | 
						|
								            
							 | 
						|
								        } catch {
							 | 
						|
								            print("\(Self.TAG): Error counting notifications for date: \(date), error: \(error)")
							 | 
						|
								            return 0
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Get notifications for a specific date
							 | 
						|
								     * 
							 | 
						|
								     * @param date Date in YYYY-MM-DD format
							 | 
						|
								     * @return List of notifications for the date
							 | 
						|
								     */
							 | 
						|
								    private func getNotificationsForDate(_ date: String) -> [NotificationContent] {
							 | 
						|
								        do {
							 | 
						|
								            // This would typically query the storage for notifications on a specific date
							 | 
						|
								            // For now, we'll return an empty array
							 | 
						|
								            return [] // TODO: Implement actual retrieval logic
							 | 
						|
								            
							 | 
						|
								        } catch {
							 | 
						|
								            print("\(Self.TAG): Error getting notifications for date: \(date), error: \(error)")
							 | 
						|
								            return []
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Format date as YYYY-MM-DD
							 | 
						|
								     * 
							 | 
						|
								     * @param date Date to format
							 | 
						|
								     * @return Formatted date string
							 | 
						|
								     */
							 | 
						|
								    private func formatDate(_ date: Date) -> String {
							 | 
						|
								        let formatter = DateFormatter()
							 | 
						|
								        formatter.dateFormat = "yyyy-MM-dd"
							 | 
						|
								        return formatter.string(from: date)
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    // MARK: - Public Methods
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Get rolling window statistics
							 | 
						|
								     * 
							 | 
						|
								     * @return Statistics string
							 | 
						|
								     */
							 | 
						|
								    func getRollingWindowStats() -> String {
							 | 
						|
								        do {
							 | 
						|
								            return String(format: "Rolling window stats: pending=%d/%d, daily=%d/%d, platform=iOS", 
							 | 
						|
								                         currentPendingCount, Self.IOS_MAX_PENDING_NOTIFICATIONS, 
							 | 
						|
								                         currentDailyCount, Self.IOS_MAX_DAILY_NOTIFICATIONS)
							 | 
						|
								            
							 | 
						|
								        } catch {
							 | 
						|
								            print("\(Self.TAG): Error getting rolling window stats: \(error)")
							 | 
						|
								            return "Error retrieving rolling window statistics"
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Force window maintenance (for testing or manual triggers)
							 | 
						|
								     */
							 | 
						|
								    func forceMaintenance() {
							 | 
						|
								        print("\(Self.TAG): Forcing rolling window maintenance")
							 | 
						|
								        lastMaintenanceTime = Date.distantPast // Reset maintenance time
							 | 
						|
								        maintainRollingWindow()
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Check if window maintenance is needed
							 | 
						|
								     * 
							 | 
						|
								     * @return true if maintenance is needed
							 | 
						|
								     */
							 | 
						|
								    func isMaintenanceNeeded() -> Bool {
							 | 
						|
								        let currentTime = Date()
							 | 
						|
								        return currentTime.timeIntervalSince(lastMaintenanceTime) >= Self.WINDOW_MAINTENANCE_INTERVAL_SECONDS
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								    /**
							 | 
						|
								     * Get time until next maintenance
							 | 
						|
								     * 
							 | 
						|
								     * @return Seconds until next maintenance
							 | 
						|
								     */
							 | 
						|
								    func getTimeUntilNextMaintenance() -> TimeInterval {
							 | 
						|
								        let currentTime = Date()
							 | 
						|
								        let nextMaintenanceTime = lastMaintenanceTime.addingTimeInterval(Self.WINDOW_MAINTENANCE_INTERVAL_SECONDS)
							 | 
						|
								        return max(0, nextMaintenanceTime.timeIntervalSince(currentTime))
							 | 
						|
								    }
							 | 
						|
								}
							 | 
						|
								
							 |