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