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
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);
|
|
}
|
|
}
|
|
|