fix(ios): correct next notification time and improve rollover UI refresh

- Fix getNextNotificationTime() to find earliest scheduled notification
  instead of using first request (pendingNotificationRequests doesn't
  guarantee order)
- Add comprehensive logging for rollover tracking with DNP-ROLLOVER
  prefix for Xcode console filtering
- Reset all notifications and rollover state when scheduling new
  notification via scheduleDailyNotification() to ensure clean test
  state
- Fix userInfo scope error in handleNotificationDelivery error handler
- Update test app UI to refresh status every 5-10 seconds and
  immediately after notification delivery to reflect rollover changes
- Add console logging in UI to debug getNotificationStatus() results

This ensures the UI correctly displays the next notification time after
rollover completes, and test notifications start with a clean slate.
This commit is contained in:
Jose Olarte III
2025-12-15 21:42:48 +08:00
parent 527c075941
commit 0a2cbf24f7
12 changed files with 2281 additions and 12 deletions

View File

@@ -72,6 +72,14 @@ public class DailyNotificationPlugin: CAPPlugin {
// Perform recovery on app launch (async, non-blocking)
reactivationManager?.performRecovery()
// Register for notification delivery events (Notification Center pattern)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleNotificationDelivery(_:)),
name: NSNotification.Name("DailyNotificationDelivered"),
object: nil
)
NSLog("DNP-DEBUG: DailyNotificationPlugin.load() completed - initialization done")
print("DNP-PLUGIN: Daily Notification Plugin loaded on iOS")
}
@@ -1068,6 +1076,38 @@ public class DailyNotificationPlugin: CAPPlugin {
// Store notification content via state actor (thread-safe)
Task {
// Reset: Cancel all existing notifications and clear rollover state
// This ensures clicking "Test Notification" starts fresh
// Cancel all pending notifications (including rollovers)
await scheduler.cancelAllNotifications()
NSLog("DNP-PLUGIN: Cleared all pending notifications for fresh schedule")
print("DNP-PLUGIN: Cleared all pending notifications for fresh schedule")
// Clear all stored notification content
if let storage = self.storage {
storage.clearAllNotifications()
NSLog("DNP-PLUGIN: Cleared all stored notification content")
print("DNP-PLUGIN: Cleared all stored notification content")
}
// Clear rollover state from UserDefaults
// Clear global rollover time
if let storage = self.storage {
storage.saveLastRolloverTime(0)
}
// Clear per-notification rollover times
// We need to clear all rollover_* keys from UserDefaults
let userDefaults = UserDefaults.standard
let allKeys = userDefaults.dictionaryRepresentation().keys
for key in allKeys {
if key.hasPrefix("rollover_") {
userDefaults.removeObject(forKey: key)
}
}
userDefaults.synchronize()
NSLog("DNP-PLUGIN: Cleared all rollover state")
print("DNP-PLUGIN: Cleared all rollover state")
if #available(iOS 13.0, *) {
if let stateActor = await self.stateActor {
await stateActor.saveNotificationContent(content)
@@ -1226,12 +1266,17 @@ public class DailyNotificationPlugin: CAPPlugin {
// Calculate next notification time
let nextNotificationTime = await scheduler.getNextNotificationTime() ?? 0
// Get rollover status
let lastRolloverTime = storage?.getLastRolloverTime() ?? 0
var result: [String: Any] = [
"isEnabled": isEnabled,
"isScheduled": pendingCount > 0,
"lastNotificationTime": lastNotification?.scheduledTime ?? 0,
"nextNotificationTime": nextNotificationTime,
"pending": pendingCount,
"rolloverEnabled": true, // Indicate rollover is active
"lastRolloverTime": lastRolloverTime, // When last rollover occurred
"settings": settings
]
@@ -1241,6 +1286,93 @@ public class DailyNotificationPlugin: CAPPlugin {
}
}
/**
* Handle notification delivery event (from Notification Center)
*
* This is called when AppDelegate posts notification delivery event
* Matches Android's scheduleNextNotification() behavior
*
* @param notification NSNotification with userInfo containing notification_id and scheduled_time
*/
@objc private func handleNotificationDelivery(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let notificationId = userInfo["notification_id"] as? String,
let scheduledTime = userInfo["scheduled_time"] as? Int64 else {
NSLog("DNP-ROLLOVER: INVALID_DATA userInfo=\(String(describing: notification.userInfo))")
print("DNP-ROLLOVER: INVALID_DATA userInfo=\(String(describing: notification.userInfo))")
return
}
let scheduledTimeStr = formatTime(scheduledTime)
NSLog("DNP-ROLLOVER: DELIVERY_DETECTED id=\(notificationId) scheduled_time=\(scheduledTimeStr)")
print("DNP-ROLLOVER: DELIVERY_DETECTED id=\(notificationId) scheduled_time=\(scheduledTimeStr)")
Task {
await processRollover(notificationId: notificationId, scheduledTime: scheduledTime)
}
}
/**
* Process rollover for delivered notification
*
* @param notificationId ID of notification that was delivered
* @param scheduledTime Scheduled time of delivered notification
*/
private func processRollover(notificationId: String, scheduledTime: Int64) async {
let scheduledTimeStr = formatTime(scheduledTime)
NSLog("DNP-ROLLOVER: PROCESS_START id=\(notificationId) scheduled_time=\(scheduledTimeStr)")
print("DNP-ROLLOVER: PROCESS_START id=\(notificationId) scheduled_time=\(scheduledTimeStr)")
guard let scheduler = scheduler, let storage = storage else {
NSLog("DNP-ROLLOVER: ERROR id=\(notificationId) plugin_not_initialized")
print("DNP-ROLLOVER: ERROR id=\(notificationId) plugin_not_initialized")
return
}
// Get the notification content that was delivered
guard let content = storage.getNotificationContent(id: notificationId) else {
NSLog("DNP-ROLLOVER: ERROR id=\(notificationId) content_not_found")
print("DNP-ROLLOVER: ERROR id=\(notificationId) content_not_found")
return
}
let contentTimeStr = formatTime(content.scheduledTime)
NSLog("DNP-ROLLOVER: CONTENT_FOUND id=\(notificationId) content_scheduled_time=\(contentTimeStr)")
print("DNP-ROLLOVER: CONTENT_FOUND id=\(notificationId) content_scheduled_time=\(contentTimeStr)")
// Schedule next notification
// Note: DailyNotificationFetcher integration deferred to Phase 2
let scheduled = await scheduler.scheduleNextNotification(
content,
storage: storage,
fetcher: nil // TODO: Phase 2 - Add fetcher instance
)
if scheduled {
NSLog("DNP-ROLLOVER: PROCESS_SUCCESS id=\(notificationId) next_notification_scheduled")
print("DNP-ROLLOVER: PROCESS_SUCCESS id=\(notificationId) next_notification_scheduled")
// Log success (non-fatal, background operation)
} else {
NSLog("DNP-ROLLOVER: PROCESS_FAILED id=\(notificationId) next_notification_not_scheduled")
print("DNP-ROLLOVER: PROCESS_FAILED id=\(notificationId) next_notification_not_scheduled")
// Log failure but continue (recovery will handle on next launch)
}
}
/**
* Format time for logging
*
* @param timestamp Timestamp in milliseconds
* @return Formatted time string
*/
private func formatTime(_ timestamp: Int64) -> String {
let date = Date(timeIntervalSince1970: Double(timestamp) / 1000.0)
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter.string(from: date)
}
/**
* Check permission status
* Returns boolean flags for each permission type

View File

@@ -393,6 +393,10 @@ class DailyNotificationReactivationManager {
}
}
// Step 4.5: Check for delivered notifications and trigger rollover
// This handles notifications that were delivered while app was not running
await checkAndProcessDeliveredNotifications()
// Record recovery in history
let result = RecoveryResult(
missedCount: missedCount,
@@ -1004,6 +1008,94 @@ class DailyNotificationReactivationManager {
// Don't throw - this is best effort
}
}
/**
* Check for delivered notifications and trigger rollover
*
* This ensures rollover happens on app launch if notifications were delivered
* while the app was not running
*/
private func checkAndProcessDeliveredNotifications() async {
NSLog("DNP-ROLLOVER: RECOVERY_CHECK_START")
print("DNP-ROLLOVER: RECOVERY_CHECK_START")
// Get delivered notifications from system
let deliveredNotifications = await notificationCenter.deliveredNotifications()
NSLog("DNP-ROLLOVER: RECOVERY_FOUND delivered_count=\(deliveredNotifications.count)")
print("DNP-ROLLOVER: RECOVERY_FOUND delivered_count=\(deliveredNotifications.count)")
// Get last processed rollover time from storage
let lastProcessedTime = storage.getLastRolloverTime()
let lastProcessedTimeStr = formatTime(lastProcessedTime)
NSLog("DNP-ROLLOVER: RECOVERY_LAST_PROCESSED time=\(lastProcessedTimeStr)")
print("DNP-ROLLOVER: RECOVERY_LAST_PROCESSED time=\(lastProcessedTimeStr)")
var processedCount = 0
var skippedCount = 0
for notification in deliveredNotifications {
let userInfo = notification.request.content.userInfo
guard let notificationId = userInfo["notification_id"] as? String,
let scheduledTime = userInfo["scheduled_time"] as? Int64 else {
continue
}
let scheduledTimeStr = formatTime(scheduledTime)
// Only process if this notification hasn't been processed yet
if scheduledTime > lastProcessedTime {
NSLog("DNP-ROLLOVER: RECOVERY_PROCESS id=\(notificationId) scheduled_time=\(scheduledTimeStr)")
print("DNP-ROLLOVER: RECOVERY_PROCESS id=\(notificationId) scheduled_time=\(scheduledTimeStr)")
// Get notification content
guard let content = storage.getNotificationContent(id: notificationId) else {
NSLog("DNP-ROLLOVER: RECOVERY_ERROR id=\(notificationId) content_not_found")
print("DNP-ROLLOVER: RECOVERY_ERROR id=\(notificationId) content_not_found")
continue
}
// Trigger rollover
let scheduled = await scheduler.scheduleNextNotification(
content,
storage: storage,
fetcher: nil // TODO: Phase 2 - Add fetcher
)
if scheduled {
NSLog("DNP-ROLLOVER: RECOVERY_SUCCESS id=\(notificationId)")
print("DNP-ROLLOVER: RECOVERY_SUCCESS id=\(notificationId)")
// Update last processed time
storage.saveLastRolloverTime(scheduledTime)
processedCount += 1
} else {
NSLog("DNP-ROLLOVER: RECOVERY_FAILED id=\(notificationId)")
print("DNP-ROLLOVER: RECOVERY_FAILED id=\(notificationId)")
}
} else {
skippedCount += 1
NSLog("DNP-ROLLOVER: RECOVERY_SKIP id=\(notificationId) already_processed scheduled_time=\(scheduledTimeStr)")
print("DNP-ROLLOVER: RECOVERY_SKIP id=\(notificationId) already_processed scheduled_time=\(scheduledTimeStr)")
}
}
NSLog("DNP-ROLLOVER: RECOVERY_COMPLETE processed=\(processedCount) skipped=\(skippedCount)")
print("DNP-ROLLOVER: RECOVERY_COMPLETE processed=\(processedCount) skipped=\(skippedCount)")
}
/**
* Format time for logging
*
* @param timestamp Timestamp in milliseconds
* @return Formatted time string
*/
private func formatTime(_ timestamp: Int64) -> String {
let date = Date(timeIntervalSince1970: Double(timestamp) / 1000.0)
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter.string(from: date)
}
}
// MARK: - Supporting Types

View File

@@ -314,12 +314,251 @@ class DailyNotificationScheduler {
func getNextNotificationTime() async -> Int64? {
let requests = await notificationCenter.pendingNotificationRequests()
guard let trigger = requests.first?.trigger as? UNCalendarNotificationTrigger,
let nextDate = trigger.nextTriggerDate() else {
// Find the earliest scheduled notification by checking all requests
var earliestDate: Date? = nil
var earliestRequestId: String? = nil
var allTimes: [(String, String)] = []
for request in requests {
var requestTime: Date? = nil
if let trigger = request.trigger as? UNCalendarNotificationTrigger,
let nextDate = trigger.nextTriggerDate() {
requestTime = nextDate
} else if let trigger = request.trigger as? UNTimeIntervalNotificationTrigger,
let nextDate = trigger.nextTriggerDate() {
requestTime = nextDate
}
if let time = requestTime {
let timeStr = formatTime(Int64(time.timeIntervalSince1970 * 1000))
allTimes.append((request.identifier, timeStr))
if earliestDate == nil || time < earliestDate! {
earliestDate = time
earliestRequestId = request.identifier
}
}
}
guard let nextDate = earliestDate else {
NSLog("DNP-ROLLOVER: GET_NEXT_TIME no_pending_requests")
print("DNP-ROLLOVER: GET_NEXT_TIME no_pending_requests")
return nil
}
return Int64(nextDate.timeIntervalSince1970 * 1000)
let nextTime = Int64(nextDate.timeIntervalSince1970 * 1000)
let nextTimeStr = formatTime(nextTime)
let allTimesStr = allTimes.map { "\($0.0):\($0.1)" }.joined(separator: ", ")
NSLog("DNP-ROLLOVER: GET_NEXT_TIME found=\(nextTimeStr) id=\(earliestRequestId ?? "unknown") from \(requests.count) pending: [\(allTimesStr)]")
print("DNP-ROLLOVER: GET_NEXT_TIME found=\(nextTimeStr) id=\(earliestRequestId ?? "unknown") from \(requests.count) pending: [\(allTimesStr)]")
return nextTime
}
/**
* Calculate next scheduled time from current scheduled time (24 hours later, DST-safe)
*
* Matches Android calculateNextScheduledTime() functionality
* Handles DST transitions automatically using Calendar
*
* @param currentScheduledTime Current scheduled time in milliseconds
* @return Next scheduled time in milliseconds (24 hours later)
*/
func calculateNextScheduledTime(_ currentScheduledTime: Int64) -> Int64 {
let calendar = Calendar.current
let currentDate = Date(timeIntervalSince1970: Double(currentScheduledTime) / 1000.0)
let currentTimeStr = formatTime(currentScheduledTime)
// Add 24 hours (handles DST transitions automatically)
guard let nextDate = calendar.date(byAdding: .hour, value: 24, to: currentDate) else {
// Fallback to simple 24-hour addition if calendar calculation fails
let fallbackTime = currentScheduledTime + (24 * 60 * 60 * 1000)
let fallbackTimeStr = formatTime(fallbackTime)
NSLog("DNP-ROLLOVER: DST_CALC_FAILED current=\(currentTimeStr) using_fallback=\(fallbackTimeStr)")
print("DNP-ROLLOVER: DST_CALC_FAILED current=\(currentTimeStr) using_fallback=\(fallbackTimeStr)")
return fallbackTime
}
let nextTime = Int64(nextDate.timeIntervalSince1970 * 1000)
let nextTimeStr = formatTime(nextTime)
// Validate: Log DST transitions for debugging
let currentHour = calendar.component(.hour, from: currentDate)
let currentMinute = calendar.component(.minute, from: currentDate)
let nextHour = calendar.component(.hour, from: nextDate)
let nextMinute = calendar.component(.minute, from: nextDate)
if currentHour != nextHour || currentMinute != nextMinute {
NSLog("DNP-ROLLOVER: DST_TRANSITION current=\(currentHour):\(String(format: "%02d", currentMinute)) next=\(nextHour):\(String(format: "%02d", nextMinute))")
print("DNP-ROLLOVER: DST_TRANSITION current=\(currentHour):\(String(format: "%02d", currentMinute)) next=\(nextHour):\(String(format: "%02d", nextMinute))")
}
// Log the calculation result
let timeDiffMs = nextTime - currentScheduledTime
let timeDiffHours = Double(timeDiffMs) / 1000.0 / 60.0 / 60.0
NSLog("DNP-ROLLOVER: CALC_NEXT current=\(currentTimeStr) next=\(nextTimeStr) diff_hours=\(String(format: "%.2f", timeDiffHours))")
print("DNP-ROLLOVER: CALC_NEXT current=\(currentTimeStr) next=\(nextTimeStr) diff_hours=\(String(format: "%.2f", timeDiffHours))")
return nextTime
}
/**
* Schedule next notification after current one fires (rollover)
*
* Matches Android scheduleNextNotification() functionality
* Implements multi-level duplicate prevention
*
* @param content Current notification content that just fired
* @param storage Storage instance for duplicate checking
* @param fetcher Optional fetcher for scheduling prefetch (Phase 2)
* @return true if next notification was scheduled successfully
*/
func scheduleNextNotification(
_ content: NotificationContent,
storage: DailyNotificationStorage?,
fetcher: Any? = nil
) async -> Bool {
let currentTime = Int64(Date().timeIntervalSince1970 * 1000)
let currentTimeStr = formatTime(currentTime)
let currentScheduledTimeStr = formatTime(content.scheduledTime)
NSLog("DNP-ROLLOVER: START id=\(content.id) current_time=\(currentTimeStr) scheduled_time=\(currentScheduledTimeStr)")
print("DNP-ROLLOVER: START id=\(content.id) current_time=\(currentTimeStr) scheduled_time=\(currentScheduledTimeStr)")
// Check 1: Rollover state tracking (prevent duplicate rollover attempts)
if let storage = storage {
let lastRolloverTime = await storage.getLastRolloverTime(for: content.id)
// If rollover was processed recently (< 1 hour ago), skip
if let lastTime = lastRolloverTime,
(currentTime - lastTime) < (60 * 60 * 1000) {
let lastTimeStr = formatTime(lastTime)
let timeSinceRollover = (currentTime - lastTime) / 1000 / 60 // minutes
NSLog("DNP-ROLLOVER: SKIP id=\(content.id) already_processed last_rollover=\(lastTimeStr) \(timeSinceRollover)min_ago")
print("DNP-ROLLOVER: SKIP id=\(content.id) already_processed last_rollover=\(lastTimeStr) \(timeSinceRollover)min_ago")
return false
}
}
// Calculate next occurrence using DST-safe calculation
let nextScheduledTime = calculateNextScheduledTime(content.scheduledTime)
let nextScheduledTimeStr = formatTime(nextScheduledTime)
let hoursUntilNext = Double(nextScheduledTime - currentTime) / 1000.0 / 60.0 / 60.0
NSLog("DNP-ROLLOVER: CALCULATED id=\(content.id) next_time=\(nextScheduledTimeStr) hours_until=\(String(format: "%.2f", hoursUntilNext))")
print("DNP-ROLLOVER: CALCULATED id=\(content.id) next_time=\(nextScheduledTimeStr) hours_until=\(String(format: "%.2f", hoursUntilNext))")
// Check 2: Storage-level duplicate check (prevent duplicate notifications)
if let storage = storage {
let existingNotifications = storage.getAllNotifications()
let toleranceMs: Int64 = 60 * 1000 // 1 minute tolerance for DST shifts
for existing in existingNotifications {
if abs(existing.scheduledTime - nextScheduledTime) <= toleranceMs {
let existingTimeStr = formatTime(existing.scheduledTime)
let timeDiffMs = abs(existing.scheduledTime - nextScheduledTime)
NSLog("DNP-ROLLOVER: DUPLICATE_STORAGE id=\(content.id) existing_id=\(existing.id) existing_time=\(existingTimeStr) time_diff_ms=\(timeDiffMs)")
print("DNP-ROLLOVER: DUPLICATE_STORAGE id=\(content.id) existing_id=\(existing.id) existing_time=\(existingTimeStr) time_diff_ms=\(timeDiffMs)")
return false // Skip rescheduling to prevent duplicate
}
}
}
// Check 3: System-level duplicate check (query UNUserNotificationCenter)
let pendingNotifications = await notificationCenter.pendingNotificationRequests()
NSLog("DNP-ROLLOVER: CHECK_SYSTEM id=\(content.id) pending_count=\(pendingNotifications.count)")
print("DNP-ROLLOVER: CHECK_SYSTEM id=\(content.id) pending_count=\(pendingNotifications.count)")
for pending in pendingNotifications {
if let trigger = pending.trigger as? UNCalendarNotificationTrigger,
let nextDate = trigger.nextTriggerDate() {
let pendingTime = Int64(nextDate.timeIntervalSince1970 * 1000)
let toleranceMs: Int64 = 60 * 1000
if abs(pendingTime - nextScheduledTime) <= toleranceMs {
let pendingTimeStr = formatTime(pendingTime)
let timeDiffMs = abs(pendingTime - nextScheduledTime)
NSLog("DNP-ROLLOVER: DUPLICATE_SYSTEM id=\(content.id) system_pending_id=\(pending.identifier) pending_time=\(pendingTimeStr) time_diff_ms=\(timeDiffMs)")
print("DNP-ROLLOVER: DUPLICATE_SYSTEM id=\(content.id) system_pending_id=\(pending.identifier) pending_time=\(pendingTimeStr) time_diff_ms=\(timeDiffMs)")
return false
}
}
}
// Extract hour:minute from current scheduled time for logging
let calendar = Calendar.current
let scheduledDate = content.getScheduledTimeAsDate()
let hour = calendar.component(.hour, from: scheduledDate)
let minute = calendar.component(.minute, from: scheduledDate)
// Create new notification content for next occurrence
// Note: Content will be refreshed by prefetch, but we need placeholder
let nextId = "daily_rollover_\(Int64(Date().timeIntervalSince1970 * 1000))"
let nextContent = NotificationContent(
id: nextId,
title: content.title, // Will be updated by prefetch
body: content.body, // Will be updated by prefetch
scheduledTime: nextScheduledTime,
fetchedAt: Int64(Date().timeIntervalSince1970 * 1000),
url: content.url,
payload: content.payload,
etag: content.etag
)
// Schedule the next notification
NSLog("DNP-ROLLOVER: SCHEDULING id=\(content.id) next_id=\(nextId) next_time=\(nextScheduledTimeStr)")
print("DNP-ROLLOVER: SCHEDULING id=\(content.id) next_id=\(nextId) next_time=\(nextScheduledTimeStr)")
let scheduled = await scheduleNotification(nextContent)
if scheduled {
// Verify the notification was actually scheduled
let pendingCount = await getPendingNotificationCount()
let isScheduled = await isNotificationScheduled(id: nextId)
NSLog("DNP-ROLLOVER: SUCCESS id=\(content.id) next_id=\(nextId) next_time=\(nextScheduledTimeStr) pending_count=\(pendingCount) is_scheduled=\(isScheduled)")
print("DNP-ROLLOVER: SUCCESS id=\(content.id) next_id=\(nextId) next_time=\(nextScheduledTimeStr) pending_count=\(pendingCount) is_scheduled=\(isScheduled)")
// Log time difference verification
let timeDiffMs = nextScheduledTime - content.scheduledTime
let timeDiffHours = Double(timeDiffMs) / 1000.0 / 60.0 / 60.0
NSLog("DNP-ROLLOVER: TIME_VERIFY id=\(content.id) current=\(currentScheduledTimeStr) next=\(nextScheduledTimeStr) diff_hours=\(String(format: "%.2f", timeDiffHours))")
print("DNP-ROLLOVER: TIME_VERIFY id=\(content.id) current=\(currentScheduledTimeStr) next=\(nextScheduledTimeStr) diff_hours=\(String(format: "%.2f", timeDiffHours))")
// Schedule background fetch for next notification (5 minutes before scheduled time)
// Note: DailyNotificationFetcher integration deferred to Phase 2
if fetcher != nil {
let fetchTime = nextScheduledTime - (5 * 60 * 1000) // 5 minutes before
let currentTime = Int64(Date().timeIntervalSince1970 * 1000)
if fetchTime > currentTime {
// TODO: Phase 2 - Implement fetcher.scheduleFetch(fetchTime)
NSLog("DNP-ROLLOVER: PREFETCH_SCHEDULED id=\(content.id) next_fetch=\(fetchTime) next_notification=\(nextScheduledTime)")
print("DNP-ROLLOVER: PREFETCH_SCHEDULED id=\(content.id) next_fetch=\(fetchTime) next_notification=\(nextScheduledTime)")
} else {
// TODO: Phase 2 - Implement fetcher.scheduleImmediateFetch()
NSLog("DNP-ROLLOVER: PREFETCH_PAST id=\(content.id) fetch_time=\(fetchTime) current=\(currentTime)")
print("DNP-ROLLOVER: PREFETCH_PAST id=\(content.id) fetch_time=\(fetchTime) current=\(currentTime)")
}
} else {
NSLog("DNP-ROLLOVER: PREFETCH_SKIP id=\(content.id) fetcher_not_available")
print("DNP-ROLLOVER: PREFETCH_SKIP id=\(content.id) fetcher_not_available")
}
// Mark rollover as processed
let rolloverProcessedTime = Int64(Date().timeIntervalSince1970 * 1000)
await storage?.saveLastRolloverTime(for: content.id, time: rolloverProcessedTime)
NSLog("DNP-ROLLOVER: COMPLETE id=\(content.id) next_id=\(nextId) next_time=\(nextScheduledTimeStr)")
print("DNP-ROLLOVER: COMPLETE id=\(content.id) next_id=\(nextId) next_time=\(nextScheduledTimeStr)")
return true
} else {
NSLog("DNP-ROLLOVER: ERROR id=\(content.id) scheduling_failed next_time=\(nextScheduledTimeStr)")
print("DNP-ROLLOVER: ERROR id=\(content.id) scheduling_failed next_time=\(nextScheduledTimeStr)")
return false
}
}
}

View File

@@ -151,6 +151,51 @@ class DailyNotificationStorage {
}
}
/**
* Get last rollover time for a notification ID
*
* @param notificationId Notification ID
* @return Last rollover time in milliseconds, or nil if never rolled over
*/
func getLastRolloverTime(for notificationId: String) async -> Int64? {
let key = "rollover_\(notificationId)"
let lastTime = userDefaults.object(forKey: key) as? Int64
return lastTime
}
/**
* Save last rollover time for a notification ID
*
* @param notificationId Notification ID
* @param time Rollover time in milliseconds
*/
func saveLastRolloverTime(for notificationId: String, time: Int64) async {
let key = "rollover_\(notificationId)"
userDefaults.set(time, forKey: key)
userDefaults.synchronize()
}
/**
* Get last rollover time (any notification)
*
* @return Last rollover time in milliseconds, or 0 if never rolled over
*/
func getLastRolloverTime() -> Int64 {
let key = "rollover_last"
return Int64(userDefaults.integer(forKey: key))
}
/**
* Save last rollover time (any notification)
*
* @param time Rollover time in milliseconds
*/
func saveLastRolloverTime(_ time: Int64) {
let key = "rollover_last"
userDefaults.set(time, forKey: key)
userDefaults.synchronize()
}
/**
* Get notifications that are ready to be displayed
*