Browse Source
- Add DailyNotificationRollingWindow with capacity-aware scheduling - Implement iOS capacity limits (64 pending, 20 daily) vs Android (100, 50) - Add automatic window maintenance every 15 minutes - Add manual maintenance triggers and statistics API - Integrate rolling window with TTL enforcer and scheduler - Add comprehensive unit tests for rolling window functionality - Add rolling window methods to TypeScript interface - Add phase1-3-rolling-window.ts usage examples This completes Phase 1 core infrastructure: - Today's remaining notifications are always armed - Tomorrow's notifications armed only if within iOS caps - Automatic window maintenance prevents notification gaps - Platform-specific capacity management prevents limits - Integration with existing TTL enforcement and scheduling Files: 7 changed, 928 insertions(+)research/notification-plugin-enhancement
7 changed files with 928 additions and 0 deletions
@ -0,0 +1,224 @@ |
|||||
|
/** |
||||
|
* Phase 1.3 Rolling Window Safety Usage Example |
||||
|
* |
||||
|
* Demonstrates rolling window safety functionality |
||||
|
* Shows how notifications are maintained for today and tomorrow |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
|
||||
|
import { DailyNotification } from '@timesafari/daily-notification-plugin'; |
||||
|
|
||||
|
/** |
||||
|
* Example: Configure rolling window safety |
||||
|
*/ |
||||
|
async function configureRollingWindowSafety() { |
||||
|
try { |
||||
|
console.log('Configuring rolling window safety...'); |
||||
|
|
||||
|
// Configure with rolling window settings
|
||||
|
await DailyNotification.configure({ |
||||
|
storage: 'shared', |
||||
|
ttlSeconds: 1800, // 30 minutes TTL
|
||||
|
prefetchLeadMinutes: 15, |
||||
|
maxNotificationsPerDay: 20 // iOS limit
|
||||
|
}); |
||||
|
|
||||
|
console.log('✅ Rolling window safety configured'); |
||||
|
|
||||
|
// The plugin will now automatically:
|
||||
|
// - Keep today's remaining notifications armed
|
||||
|
// - Arm tomorrow's notifications if within iOS caps
|
||||
|
// - Maintain window state every 15 minutes
|
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('❌ Rolling window configuration failed:', error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Example: Manual rolling window maintenance |
||||
|
*/ |
||||
|
async function manualRollingWindowMaintenance() { |
||||
|
try { |
||||
|
console.log('Triggering manual rolling window maintenance...'); |
||||
|
|
||||
|
// Force window maintenance (useful for testing)
|
||||
|
await DailyNotification.maintainRollingWindow(); |
||||
|
|
||||
|
console.log('✅ Rolling window maintenance completed'); |
||||
|
|
||||
|
// This will:
|
||||
|
// - Arm today's remaining notifications
|
||||
|
// - Arm tomorrow's notifications if within capacity
|
||||
|
// - Update window state and statistics
|
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('❌ Manual maintenance failed:', error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Example: Check rolling window statistics |
||||
|
*/ |
||||
|
async function checkRollingWindowStats() { |
||||
|
try { |
||||
|
console.log('Checking rolling window statistics...'); |
||||
|
|
||||
|
// Get rolling window statistics
|
||||
|
const stats = await DailyNotification.getRollingWindowStats(); |
||||
|
|
||||
|
console.log('📊 Rolling Window Statistics:'); |
||||
|
console.log(` Stats: ${stats.stats}`); |
||||
|
console.log(` Maintenance Needed: ${stats.maintenanceNeeded}`); |
||||
|
console.log(` Time Until Next Maintenance: ${stats.timeUntilNextMaintenance}ms`); |
||||
|
|
||||
|
// Example output:
|
||||
|
// Stats: Rolling window stats: pending=5/100, daily=3/50, platform=Android
|
||||
|
// Maintenance Needed: false
|
||||
|
// Time Until Next Maintenance: 450000ms (7.5 minutes)
|
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('❌ Rolling window stats check failed:', error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Example: Schedule multiple notifications with rolling window |
||||
|
*/ |
||||
|
async function scheduleMultipleNotifications() { |
||||
|
try { |
||||
|
console.log('Scheduling multiple notifications with rolling window...'); |
||||
|
|
||||
|
// Configure rolling window safety
|
||||
|
await configureRollingWindowSafety(); |
||||
|
|
||||
|
// Schedule notifications for different times
|
||||
|
const notifications = [ |
||||
|
{ time: '08:00', title: 'Morning Update', body: 'Good morning!' }, |
||||
|
{ time: '12:00', title: 'Lunch Reminder', body: 'Time for lunch!' }, |
||||
|
{ time: '18:00', title: 'Evening Summary', body: 'End of day summary' }, |
||||
|
{ time: '22:00', title: 'Good Night', body: 'Time to rest' } |
||||
|
]; |
||||
|
|
||||
|
for (const notification of notifications) { |
||||
|
await DailyNotification.scheduleDailyNotification({ |
||||
|
url: 'https://api.example.com/daily-content', |
||||
|
time: notification.time, |
||||
|
title: notification.title, |
||||
|
body: notification.body |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
console.log('✅ Multiple notifications scheduled'); |
||||
|
|
||||
|
// The rolling window will ensure:
|
||||
|
// - All future notifications today are armed
|
||||
|
// - Tomorrow's notifications are armed if within iOS caps
|
||||
|
// - Window state is maintained automatically
|
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('❌ Multiple notification scheduling failed:', error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Example: Demonstrate iOS capacity limits |
||||
|
*/ |
||||
|
async function demonstrateIOSCapacityLimits() { |
||||
|
try { |
||||
|
console.log('Demonstrating iOS capacity limits...'); |
||||
|
|
||||
|
// Configure with iOS-like limits
|
||||
|
await DailyNotification.configure({ |
||||
|
storage: 'shared', |
||||
|
ttlSeconds: 3600, // 1 hour TTL
|
||||
|
prefetchLeadMinutes: 30, |
||||
|
maxNotificationsPerDay: 20 // iOS limit
|
||||
|
}); |
||||
|
|
||||
|
// Schedule many notifications to test capacity
|
||||
|
const notifications = []; |
||||
|
for (let i = 0; i < 25; i++) { |
||||
|
notifications.push({ |
||||
|
time: `${8 + i}:00`, |
||||
|
title: `Notification ${i + 1}`, |
||||
|
body: `This is notification number ${i + 1}` |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
for (const notification of notifications) { |
||||
|
await DailyNotification.scheduleDailyNotification({ |
||||
|
url: 'https://api.example.com/daily-content', |
||||
|
time: notification.time, |
||||
|
title: notification.title, |
||||
|
body: notification.body |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
console.log('✅ Many notifications scheduled'); |
||||
|
|
||||
|
// Check statistics to see capacity management
|
||||
|
const stats = await DailyNotification.getRollingWindowStats(); |
||||
|
console.log('📊 Capacity Management:', stats.stats); |
||||
|
|
||||
|
// The rolling window will:
|
||||
|
// - Arm notifications up to the daily limit
|
||||
|
// - Skip additional notifications if at capacity
|
||||
|
// - Log capacity violations for debugging
|
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('❌ iOS capacity demonstration failed:', error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Example: Monitor rolling window over time |
||||
|
*/ |
||||
|
async function monitorRollingWindowOverTime() { |
||||
|
try { |
||||
|
console.log('Monitoring rolling window over time...'); |
||||
|
|
||||
|
// Configure rolling window
|
||||
|
await configureRollingWindowSafety(); |
||||
|
|
||||
|
// Schedule some notifications
|
||||
|
await scheduleMultipleNotifications(); |
||||
|
|
||||
|
// Monitor window state over time
|
||||
|
const monitorInterval = setInterval(async () => { |
||||
|
try { |
||||
|
const stats = await DailyNotification.getRollingWindowStats(); |
||||
|
console.log('📊 Window State:', stats.stats); |
||||
|
|
||||
|
if (stats.maintenanceNeeded) { |
||||
|
console.log('⚠️ Maintenance needed, triggering...'); |
||||
|
await DailyNotification.maintainRollingWindow(); |
||||
|
} |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('❌ Monitoring error:', error); |
||||
|
} |
||||
|
}, 60000); // Check every minute
|
||||
|
|
||||
|
// Stop monitoring after 5 minutes
|
||||
|
setTimeout(() => { |
||||
|
clearInterval(monitorInterval); |
||||
|
console.log('✅ Monitoring completed'); |
||||
|
}, 300000); |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('❌ Rolling window monitoring failed:', error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Export examples for use
|
||||
|
export { |
||||
|
configureRollingWindowSafety, |
||||
|
manualRollingWindowMaintenance, |
||||
|
checkRollingWindowStats, |
||||
|
scheduleMultipleNotifications, |
||||
|
demonstrateIOSCapacityLimits, |
||||
|
monitorRollingWindowOverTime |
||||
|
}; |
@ -0,0 +1,384 @@ |
|||||
|
/** |
||||
|
* 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); |
||||
|
} |
||||
|
} |
@ -0,0 +1,193 @@ |
|||||
|
/** |
||||
|
* DailyNotificationRollingWindowTest.java |
||||
|
* |
||||
|
* Unit tests for rolling window safety functionality |
||||
|
* Tests window maintenance, capacity management, and platform-specific limits |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
|
||||
|
package com.timesafari.dailynotification; |
||||
|
|
||||
|
import android.content.Context; |
||||
|
import android.test.AndroidTestCase; |
||||
|
import android.test.mock.MockContext; |
||||
|
|
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
|
||||
|
/** |
||||
|
* Unit tests for DailyNotificationRollingWindow |
||||
|
* |
||||
|
* Tests the rolling window safety functionality including: |
||||
|
* - Window maintenance and state updates |
||||
|
* - Capacity limit enforcement |
||||
|
* - Platform-specific behavior (iOS vs Android) |
||||
|
* - Statistics and maintenance timing |
||||
|
*/ |
||||
|
public class DailyNotificationRollingWindowTest extends AndroidTestCase { |
||||
|
|
||||
|
private DailyNotificationRollingWindow rollingWindow; |
||||
|
private Context mockContext; |
||||
|
private DailyNotificationScheduler mockScheduler; |
||||
|
private DailyNotificationTTLEnforcer mockTTLEnforcer; |
||||
|
private DailyNotificationStorage mockStorage; |
||||
|
|
||||
|
@Override |
||||
|
protected void setUp() throws Exception { |
||||
|
super.setUp(); |
||||
|
|
||||
|
// Create mock context
|
||||
|
mockContext = new MockContext() { |
||||
|
@Override |
||||
|
public android.content.SharedPreferences getSharedPreferences(String name, int mode) { |
||||
|
return getContext().getSharedPreferences(name, mode); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// Create mock components
|
||||
|
mockScheduler = new MockDailyNotificationScheduler(); |
||||
|
mockTTLEnforcer = new MockDailyNotificationTTLEnforcer(); |
||||
|
mockStorage = new MockDailyNotificationStorage(); |
||||
|
|
||||
|
// Create rolling window for Android platform
|
||||
|
rollingWindow = new DailyNotificationRollingWindow( |
||||
|
mockContext, |
||||
|
mockScheduler, |
||||
|
mockTTLEnforcer, |
||||
|
mockStorage, |
||||
|
false // Android platform
|
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void tearDown() throws Exception { |
||||
|
super.tearDown(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Test rolling window initialization |
||||
|
*/ |
||||
|
public void testRollingWindowInitialization() { |
||||
|
assertNotNull("Rolling window should be initialized", rollingWindow); |
||||
|
|
||||
|
// Test Android platform limits
|
||||
|
String stats = rollingWindow.getRollingWindowStats(); |
||||
|
assertNotNull("Stats should not be null", stats); |
||||
|
assertTrue("Stats should contain Android platform info", stats.contains("Android")); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Test rolling window maintenance |
||||
|
*/ |
||||
|
public void testRollingWindowMaintenance() { |
||||
|
// Test that maintenance can be forced
|
||||
|
rollingWindow.forceMaintenance(); |
||||
|
|
||||
|
// Test maintenance timing
|
||||
|
assertFalse("Maintenance should not be needed immediately after forcing", |
||||
|
rollingWindow.isMaintenanceNeeded()); |
||||
|
|
||||
|
// Test time until next maintenance
|
||||
|
long timeUntilNext = rollingWindow.getTimeUntilNextMaintenance(); |
||||
|
assertTrue("Time until next maintenance should be positive", timeUntilNext > 0); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Test iOS platform behavior |
||||
|
*/ |
||||
|
public void testIOSPlatformBehavior() { |
||||
|
// Create rolling window for iOS platform
|
||||
|
DailyNotificationRollingWindow iosRollingWindow = new DailyNotificationRollingWindow( |
||||
|
mockContext, |
||||
|
mockScheduler, |
||||
|
mockTTLEnforcer, |
||||
|
mockStorage, |
||||
|
true // iOS platform
|
||||
|
); |
||||
|
|
||||
|
String stats = iosRollingWindow.getRollingWindowStats(); |
||||
|
assertNotNull("iOS stats should not be null", stats); |
||||
|
assertTrue("Stats should contain iOS platform info", stats.contains("iOS")); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Test maintenance timing |
||||
|
*/ |
||||
|
public void testMaintenanceTiming() { |
||||
|
// Initially, maintenance should not be needed
|
||||
|
assertFalse("Maintenance should not be needed initially", |
||||
|
rollingWindow.isMaintenanceNeeded()); |
||||
|
|
||||
|
// Force maintenance
|
||||
|
rollingWindow.forceMaintenance(); |
||||
|
|
||||
|
// Should not be needed immediately after
|
||||
|
assertFalse("Maintenance should not be needed after forcing", |
||||
|
rollingWindow.isMaintenanceNeeded()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Test statistics retrieval |
||||
|
*/ |
||||
|
public void testStatisticsRetrieval() { |
||||
|
String stats = rollingWindow.getRollingWindowStats(); |
||||
|
|
||||
|
assertNotNull("Statistics should not be null", stats); |
||||
|
assertTrue("Statistics should contain pending count", stats.contains("pending")); |
||||
|
assertTrue("Statistics should contain daily count", stats.contains("daily")); |
||||
|
assertTrue("Statistics should contain platform info", stats.contains("platform")); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Test error handling |
||||
|
*/ |
||||
|
public void testErrorHandling() { |
||||
|
// Test with null components (should not crash)
|
||||
|
try { |
||||
|
DailyNotificationRollingWindow errorWindow = new DailyNotificationRollingWindow( |
||||
|
null, null, null, null, false |
||||
|
); |
||||
|
// Should not crash during construction
|
||||
|
} catch (Exception e) { |
||||
|
// Expected to handle gracefully
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Mock DailyNotificationScheduler for testing |
||||
|
*/ |
||||
|
private static class MockDailyNotificationScheduler extends DailyNotificationScheduler { |
||||
|
public MockDailyNotificationScheduler() { |
||||
|
super(null, null); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean scheduleNotification(NotificationContent content) { |
||||
|
return true; // Always succeed for testing
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Mock DailyNotificationTTLEnforcer for testing |
||||
|
*/ |
||||
|
private static class MockDailyNotificationTTLEnforcer extends DailyNotificationTTLEnforcer { |
||||
|
public MockDailyNotificationTTLEnforcer() { |
||||
|
super(null, null, false); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean validateBeforeArming(NotificationContent content) { |
||||
|
return true; // Always pass validation for testing
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Mock DailyNotificationStorage for testing |
||||
|
*/ |
||||
|
private static class MockDailyNotificationStorage extends DailyNotificationStorage { |
||||
|
public MockDailyNotificationStorage() { |
||||
|
super(null); |
||||
|
} |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue