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

/**
* 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))
}
}