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.
 
 
 
 
 
 

384 lines
14 KiB

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