/** * DailyNotificationRollingWindow.java * * 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 */ package com.timesafari.dailynotification; import android.content.Context; import android.util.Log; import java.util.ArrayList; import java.util.Calendar; import java.util.List; import java.util.concurrent.TimeUnit; /** * Manages rolling window safety for notification scheduling * * 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 * - Platform-specific capacity management */ public class DailyNotificationRollingWindow { private static final String TAG = "DailyNotificationRollingWindow"; // iOS notification limits private static final int IOS_MAX_PENDING_NOTIFICATIONS = 64; private static final int IOS_MAX_DAILY_NOTIFICATIONS = 20; // Android has no hard limits, but we use reasonable defaults private static final int ANDROID_MAX_PENDING_NOTIFICATIONS = 100; private static final int ANDROID_MAX_DAILY_NOTIFICATIONS = 50; // Window maintenance intervals private static final long WINDOW_MAINTENANCE_INTERVAL_MS = TimeUnit.MINUTES.toMillis(15); private static final long WINDOW_MAINTENANCE_INTERVAL_MS = TimeUnit.MINUTES.toMillis(15); private final Context context; private final DailyNotificationScheduler scheduler; private final DailyNotificationTTLEnforcer ttlEnforcer; private final DailyNotificationStorage storage; private final boolean isIOSPlatform; // Window state private long lastMaintenanceTime = 0; private int currentPendingCount = 0; private int currentDailyCount = 0; /** * Constructor * * @param context Application context * @param scheduler Notification scheduler * @param ttlEnforcer TTL enforcement instance * @param storage Storage instance * @param isIOSPlatform Whether running on iOS platform */ public DailyNotificationRollingWindow(Context context, DailyNotificationScheduler scheduler, DailyNotificationTTLEnforcer ttlEnforcer, DailyNotificationStorage storage, boolean isIOSPlatform) { this.context = context; this.scheduler = scheduler; this.ttlEnforcer = ttlEnforcer; this.storage = storage; this.isIOSPlatform = isIOSPlatform; Log.d(TAG, "Rolling window initialized for " + (isIOSPlatform ? "iOS" : "Android")); } /** * 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 */ public void maintainRollingWindow() { try { long currentTime = System.currentTimeMillis(); // Check if maintenance is needed if (currentTime - lastMaintenanceTime < WINDOW_MAINTENANCE_INTERVAL_MS) { Log.d(TAG, "Window maintenance not needed yet"); return; } Log.d(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; Log.i(TAG, String.format("Rolling window maintenance completed: pending=%d, daily=%d", currentPendingCount, currentDailyCount)); } catch (Exception e) { Log.e(TAG, "Error during rolling window maintenance", e); } } /** * Arm today's remaining notifications * * Ensures all notifications for today that haven't fired yet are armed */ private void armTodaysRemainingNotifications() { try { Log.d(TAG, "Arming today's remaining notifications"); // Get today's date Calendar today = Calendar.getInstance(); String todayDate = formatDate(today); // Get all notifications for today List todaysNotifications = getNotificationsForDate(todayDate); int armedCount = 0; int skippedCount = 0; for (NotificationContent notification : todaysNotifications) { // Check if notification is in the future if (notification.getScheduledTime() > System.currentTimeMillis()) { // Check TTL before arming if (ttlEnforcer != null && !ttlEnforcer.validateBeforeArming(notification)) { Log.w(TAG, "Skipping today's notification due to TTL: " + notification.getId()); skippedCount++; continue; } // Arm the notification boolean armed = scheduler.scheduleNotification(notification); if (armed) { armedCount++; currentPendingCount++; } else { Log.w(TAG, "Failed to arm today's notification: " + notification.getId()); } } } Log.i(TAG, String.format("Today's notifications: armed=%d, skipped=%d", armedCount, skippedCount)); } catch (Exception e) { Log.e(TAG, "Error arming today's remaining notifications", e); } } /** * Arm tomorrow's notifications if within capacity limits * * Only arms tomorrow's notifications if we're within platform-specific limits */ private void armTomorrowsNotificationsIfWithinCapacity() { try { Log.d(TAG, "Checking capacity for tomorrow's notifications"); // Check if we're within capacity limits if (!isWithinCapacityLimits()) { Log.w(TAG, "At capacity limit, skipping tomorrow's notifications"); return; } // Get tomorrow's date Calendar tomorrow = Calendar.getInstance(); tomorrow.add(Calendar.DAY_OF_MONTH, 1); String tomorrowDate = formatDate(tomorrow); // Get all notifications for tomorrow List tomorrowsNotifications = getNotificationsForDate(tomorrowDate); int armedCount = 0; int skippedCount = 0; for (NotificationContent notification : tomorrowsNotifications) { // Check TTL before arming if (ttlEnforcer != null && !ttlEnforcer.validateBeforeArming(notification)) { Log.w(TAG, "Skipping tomorrow's notification due to TTL: " + notification.getId()); skippedCount++; continue; } // Arm the notification boolean armed = scheduler.scheduleNotification(notification); if (armed) { armedCount++; currentPendingCount++; currentDailyCount++; } else { Log.w(TAG, "Failed to arm tomorrow's notification: " + notification.getId()); } // Check capacity after each arm if (!isWithinCapacityLimits()) { Log.w(TAG, "Reached capacity limit while arming tomorrow's notifications"); break; } } Log.i(TAG, String.format("Tomorrow's notifications: armed=%d, skipped=%d", armedCount, skippedCount)); } catch (Exception e) { Log.e(TAG, "Error arming tomorrow's notifications", e); } } /** * Check if we're within platform-specific capacity limits * * @return true if within limits */ private boolean isWithinCapacityLimits() { int maxPending = isIOSPlatform ? IOS_MAX_PENDING_NOTIFICATIONS : ANDROID_MAX_PENDING_NOTIFICATIONS; int maxDaily = isIOSPlatform ? IOS_MAX_DAILY_NOTIFICATIONS : ANDROID_MAX_DAILY_NOTIFICATIONS; boolean withinPendingLimit = currentPendingCount < maxPending; boolean withinDailyLimit = currentDailyCount < maxDaily; Log.d(TAG, String.format("Capacity check: pending=%d/%d, daily=%d/%d, within=%s", currentPendingCount, maxPending, currentDailyCount, maxDaily, withinPendingLimit && withinDailyLimit)); return withinPendingLimit && withinDailyLimit; } /** * Update window state by counting current notifications */ private void updateWindowState() { try { Log.d(TAG, "Updating window state"); // Count pending notifications currentPendingCount = countPendingNotifications(); // Count today's notifications Calendar today = Calendar.getInstance(); String todayDate = formatDate(today); currentDailyCount = countNotificationsForDate(todayDate); Log.d(TAG, String.format("Window state updated: pending=%d, daily=%d", currentPendingCount, currentDailyCount)); } catch (Exception e) { Log.e(TAG, "Error updating window state", e); } } /** * Count pending notifications * * @return Number of pending notifications */ private int countPendingNotifications() { try { // 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 (Exception e) { Log.e(TAG, "Error counting pending notifications", e); return 0; } } /** * Count notifications for a specific date * * @param date Date in YYYY-MM-DD format * @return Number of notifications for the date */ private int countNotificationsForDate(String date) { try { // 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 (Exception e) { Log.e(TAG, "Error counting notifications for date: " + date, e); return 0; } } /** * Get notifications for a specific date * * @param date Date in YYYY-MM-DD format * @return List of notifications for the date */ private List getNotificationsForDate(String date) { try { // This would typically query the storage for notifications on a specific date // For now, we'll return an empty list return new ArrayList<>(); // TODO: Implement actual retrieval logic } catch (Exception e) { Log.e(TAG, "Error getting notifications for date: " + date, e); return new ArrayList<>(); } } /** * Format date as YYYY-MM-DD * * @param calendar Calendar instance * @return Formatted date string */ private String formatDate(Calendar calendar) { int year = calendar.get(Calendar.YEAR); int month = calendar.get(Calendar.MONTH) + 1; // Calendar months are 0-based int day = calendar.get(Calendar.DAY_OF_MONTH); return String.format("%04d-%02d-%02d", year, month, day); } /** * Get rolling window statistics * * @return Statistics string */ public String getRollingWindowStats() { try { int maxPending = isIOSPlatform ? IOS_MAX_PENDING_NOTIFICATIONS : ANDROID_MAX_PENDING_NOTIFICATIONS; int maxDaily = isIOSPlatform ? IOS_MAX_DAILY_NOTIFICATIONS : ANDROID_MAX_DAILY_NOTIFICATIONS; return String.format("Rolling window stats: pending=%d/%d, daily=%d/%d, platform=%s", currentPendingCount, maxPending, currentDailyCount, maxDaily, isIOSPlatform ? "iOS" : "Android"); } catch (Exception e) { Log.e(TAG, "Error getting rolling window stats", e); return "Error retrieving rolling window statistics"; } } /** * Force window maintenance (for testing or manual triggers) */ public void forceMaintenance() { Log.i(TAG, "Forcing rolling window maintenance"); lastMaintenanceTime = 0; // Reset maintenance time maintainRollingWindow(); } /** * Check if window maintenance is needed * * @return true if maintenance is needed */ public boolean isMaintenanceNeeded() { long currentTime = System.currentTimeMillis(); return currentTime - lastMaintenanceTime >= WINDOW_MAINTENANCE_INTERVAL_MS; } /** * Get time until next maintenance * * @return Milliseconds until next maintenance */ public long getTimeUntilNextMaintenance() { long currentTime = System.currentTimeMillis(); long nextMaintenanceTime = lastMaintenanceTime + WINDOW_MAINTENANCE_INTERVAL_MS; return Math.max(0, nextMaintenanceTime - currentTime); } }