Browse Source
- Add DailyNotificationTTLEnforcer with freshness validation logic - Add TTL validation to scheduling path before arming notifications - Implement skip rule: if (T - fetchedAt) > ttlSeconds → skip arming - Add TTL violation logging with TTL_VIOLATION code - Add comprehensive unit tests for TTL enforcement - Add TTL enforcer integration to DailyNotificationPlugin - Add phase1-2-ttl-enforcement.ts usage examples This implements the critical Phase 1.2 gate for content freshness: - Notifications with stale content are automatically skipped - TTL violations are logged and tracked for analytics - Freshness validation prevents delivery of outdated content - Configurable TTL settings support different use cases - Integration with existing scheduling infrastructure Files: 5 changed, 878 insertions(+)research/notification-plugin-enhancement
5 changed files with 878 additions and 0 deletions
@ -0,0 +1,173 @@ |
|||
/** |
|||
* Phase 1.2 TTL-at-Fire Enforcement Usage Example |
|||
* |
|||
* Demonstrates TTL enforcement functionality |
|||
* Shows how stale notifications are automatically skipped |
|||
* |
|||
* @author Matthew Raymer |
|||
* @version 1.0.0 |
|||
*/ |
|||
|
|||
import { DailyNotification } from '@timesafari/daily-notification-plugin'; |
|||
|
|||
/** |
|||
* Example: Configure TTL enforcement |
|||
*/ |
|||
async function configureTTLEnforcement() { |
|||
try { |
|||
console.log('Configuring TTL enforcement...'); |
|||
|
|||
// Configure with TTL settings
|
|||
await DailyNotification.configure({ |
|||
storage: 'shared', |
|||
ttlSeconds: 1800, // 30 minutes TTL
|
|||
prefetchLeadMinutes: 15 |
|||
}); |
|||
|
|||
console.log('✅ TTL enforcement configured (30 minutes)'); |
|||
|
|||
// Now the plugin will automatically skip notifications with stale content
|
|||
// Content older than 30 minutes at fire time will not be armed
|
|||
|
|||
} catch (error) { |
|||
console.error('❌ TTL configuration failed:', error); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Example: Schedule notification with TTL enforcement |
|||
*/ |
|||
async function scheduleWithTTLEnforcement() { |
|||
try { |
|||
// Configure TTL enforcement first
|
|||
await configureTTLEnforcement(); |
|||
|
|||
// Schedule a notification
|
|||
await DailyNotification.scheduleDailyNotification({ |
|||
url: 'https://api.example.com/daily-content', |
|||
time: '09:00', |
|||
title: 'Daily Update', |
|||
body: 'Your daily notification is ready', |
|||
sound: true |
|||
}); |
|||
|
|||
console.log('✅ Notification scheduled with TTL enforcement'); |
|||
|
|||
// The plugin will now:
|
|||
// 1. Check content freshness before arming
|
|||
// 2. Skip notifications with stale content
|
|||
// 3. Log TTL violations for debugging
|
|||
// 4. Store violation statistics
|
|||
|
|||
} catch (error) { |
|||
console.error('❌ Scheduling with TTL enforcement failed:', error); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Example: Demonstrate TTL violation scenario |
|||
*/ |
|||
async function demonstrateTTLViolation() { |
|||
try { |
|||
console.log('Demonstrating TTL violation scenario...'); |
|||
|
|||
// Configure with short TTL for demonstration
|
|||
await DailyNotification.configure({ |
|||
storage: 'shared', |
|||
ttlSeconds: 300, // 5 minutes TTL (very short for demo)
|
|||
prefetchLeadMinutes: 2 |
|||
}); |
|||
|
|||
// Schedule notification
|
|||
await DailyNotification.scheduleDailyNotification({ |
|||
url: 'https://api.example.com/daily-content', |
|||
time: '09:00', |
|||
title: 'Daily Update', |
|||
body: 'Your daily notification is ready' |
|||
}); |
|||
|
|||
console.log('✅ Notification scheduled with 5-minute TTL'); |
|||
|
|||
// If content is fetched more than 5 minutes before 09:00,
|
|||
// the notification will be skipped due to TTL violation
|
|||
|
|||
console.log('ℹ️ If content is older than 5 minutes at 09:00, notification will be skipped'); |
|||
|
|||
} catch (error) { |
|||
console.error('❌ TTL violation demonstration failed:', error); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Example: Check TTL violation statistics |
|||
*/ |
|||
async function checkTTLStats() { |
|||
try { |
|||
console.log('Checking TTL violation statistics...'); |
|||
|
|||
// Configure TTL enforcement
|
|||
await DailyNotification.configure({ |
|||
storage: 'shared', |
|||
ttlSeconds: 1800, // 30 minutes
|
|||
prefetchLeadMinutes: 15 |
|||
}); |
|||
|
|||
// The plugin automatically tracks TTL violations
|
|||
// You can check the logs for TTL_VIOLATION entries
|
|||
// or implement a method to retrieve violation statistics
|
|||
|
|||
console.log('✅ TTL enforcement active - violations will be logged'); |
|||
console.log('ℹ️ Check logs for "TTL_VIOLATION" entries'); |
|||
|
|||
} catch (error) { |
|||
console.error('❌ TTL stats check failed:', error); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Example: Different TTL configurations for different use cases |
|||
*/ |
|||
async function configureDifferentTTLScenarios() { |
|||
try { |
|||
console.log('Configuring different TTL scenarios...'); |
|||
|
|||
// Scenario 1: Real-time notifications (short TTL)
|
|||
await DailyNotification.configure({ |
|||
storage: 'shared', |
|||
ttlSeconds: 300, // 5 minutes
|
|||
prefetchLeadMinutes: 2 |
|||
}); |
|||
|
|||
console.log('✅ Real-time notifications: 5-minute TTL'); |
|||
|
|||
// Scenario 2: Daily digest (longer TTL)
|
|||
await DailyNotification.configure({ |
|||
storage: 'shared', |
|||
ttlSeconds: 7200, // 2 hours
|
|||
prefetchLeadMinutes: 30 |
|||
}); |
|||
|
|||
console.log('✅ Daily digest: 2-hour TTL'); |
|||
|
|||
// Scenario 3: Weekly summary (very long TTL)
|
|||
await DailyNotification.configure({ |
|||
storage: 'shared', |
|||
ttlSeconds: 86400, // 24 hours
|
|||
prefetchLeadMinutes: 60 |
|||
}); |
|||
|
|||
console.log('✅ Weekly summary: 24-hour TTL'); |
|||
|
|||
} catch (error) { |
|||
console.error('❌ TTL scenario configuration failed:', error); |
|||
} |
|||
} |
|||
|
|||
// Export examples for use
|
|||
export { |
|||
configureTTLEnforcement, |
|||
scheduleWithTTLEnforcement, |
|||
demonstrateTTLViolation, |
|||
checkTTLStats, |
|||
configureDifferentTTLScenarios |
|||
}; |
@ -0,0 +1,438 @@ |
|||
/** |
|||
* DailyNotificationTTLEnforcer.java |
|||
* |
|||
* TTL-at-fire enforcement for notification freshness |
|||
* Implements the skip rule: if (T - fetchedAt) > ttlSeconds → skip arming |
|||
* |
|||
* @author Matthew Raymer |
|||
* @version 1.0.0 |
|||
*/ |
|||
|
|||
package com.timesafari.dailynotification; |
|||
|
|||
import android.content.Context; |
|||
import android.content.SharedPreferences; |
|||
import android.database.sqlite.SQLiteDatabase; |
|||
import android.util.Log; |
|||
|
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
/** |
|||
* Enforces TTL-at-fire rules for notification freshness |
|||
* |
|||
* This class implements the critical freshness enforcement: |
|||
* - Before arming for T, if (T − fetchedAt) > ttlSeconds → skip |
|||
* - Logs TTL violations for debugging |
|||
* - Supports both SQLite and SharedPreferences storage |
|||
* - Provides freshness validation before scheduling |
|||
*/ |
|||
public class DailyNotificationTTLEnforcer { |
|||
|
|||
private static final String TAG = "DailyNotificationTTLEnforcer"; |
|||
private static final String LOG_CODE_TTL_VIOLATION = "TTL_VIOLATION"; |
|||
|
|||
// Default TTL values
|
|||
private static final long DEFAULT_TTL_SECONDS = 3600; // 1 hour
|
|||
private static final long MIN_TTL_SECONDS = 60; // 1 minute
|
|||
private static final long MAX_TTL_SECONDS = 86400; // 24 hours
|
|||
|
|||
private final Context context; |
|||
private final DailyNotificationDatabase database; |
|||
private final boolean useSharedStorage; |
|||
|
|||
/** |
|||
* Constructor |
|||
* |
|||
* @param context Application context |
|||
* @param database SQLite database (null if using SharedPreferences) |
|||
* @param useSharedStorage Whether to use SQLite or SharedPreferences |
|||
*/ |
|||
public DailyNotificationTTLEnforcer(Context context, DailyNotificationDatabase database, boolean useSharedStorage) { |
|||
this.context = context; |
|||
this.database = database; |
|||
this.useSharedStorage = useSharedStorage; |
|||
} |
|||
|
|||
/** |
|||
* Check if notification content is fresh enough to arm |
|||
* |
|||
* @param slotId Notification slot ID |
|||
* @param scheduledTime T (slot time) - when notification should fire |
|||
* @param fetchedAt When content was fetched |
|||
* @return true if content is fresh enough to arm |
|||
*/ |
|||
public boolean isContentFresh(String slotId, long scheduledTime, long fetchedAt) { |
|||
try { |
|||
long ttlSeconds = getTTLSeconds(); |
|||
|
|||
// Calculate age at fire time
|
|||
long ageAtFireTime = scheduledTime - fetchedAt; |
|||
long ageAtFireSeconds = TimeUnit.MILLISECONDS.toSeconds(ageAtFireTime); |
|||
|
|||
boolean isFresh = ageAtFireSeconds <= ttlSeconds; |
|||
|
|||
if (!isFresh) { |
|||
logTTLViolation(slotId, scheduledTime, fetchedAt, ageAtFireSeconds, ttlSeconds); |
|||
} |
|||
|
|||
Log.d(TAG, String.format("TTL check for %s: age=%ds, ttl=%ds, fresh=%s", |
|||
slotId, ageAtFireSeconds, ttlSeconds, isFresh)); |
|||
|
|||
return isFresh; |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error checking content freshness", e); |
|||
// Default to allowing arming if check fails
|
|||
return true; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Check if notification content is fresh enough to arm (using stored fetchedAt) |
|||
* |
|||
* @param slotId Notification slot ID |
|||
* @param scheduledTime T (slot time) - when notification should fire |
|||
* @return true if content is fresh enough to arm |
|||
*/ |
|||
public boolean isContentFresh(String slotId, long scheduledTime) { |
|||
try { |
|||
long fetchedAt = getFetchedAt(slotId); |
|||
if (fetchedAt == 0) { |
|||
Log.w(TAG, "No fetchedAt found for slot: " + slotId); |
|||
return false; |
|||
} |
|||
|
|||
return isContentFresh(slotId, scheduledTime, fetchedAt); |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error checking content freshness for slot: " + slotId, e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Validate freshness before arming notification |
|||
* |
|||
* @param notificationContent Notification content to validate |
|||
* @return true if notification should be armed |
|||
*/ |
|||
public boolean validateBeforeArming(NotificationContent notificationContent) { |
|||
try { |
|||
String slotId = notificationContent.getId(); |
|||
long scheduledTime = notificationContent.getScheduledTime(); |
|||
long fetchedAt = notificationContent.getFetchedAt(); |
|||
|
|||
Log.d(TAG, String.format("Validating freshness before arming: slot=%s, scheduled=%d, fetched=%d", |
|||
slotId, scheduledTime, fetchedAt)); |
|||
|
|||
boolean isFresh = isContentFresh(slotId, scheduledTime, fetchedAt); |
|||
|
|||
if (!isFresh) { |
|||
Log.w(TAG, "Skipping arming due to TTL violation: " + slotId); |
|||
return false; |
|||
} |
|||
|
|||
Log.d(TAG, "Content is fresh, proceeding with arming: " + slotId); |
|||
return true; |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error validating freshness before arming", e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get TTL seconds from configuration |
|||
* |
|||
* @return TTL in seconds |
|||
*/ |
|||
private long getTTLSeconds() { |
|||
try { |
|||
if (useSharedStorage && database != null) { |
|||
return getTTLFromSQLite(); |
|||
} else { |
|||
return getTTLFromSharedPreferences(); |
|||
} |
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error getting TTL seconds", e); |
|||
return DEFAULT_TTL_SECONDS; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get TTL from SQLite database |
|||
* |
|||
* @return TTL in seconds |
|||
*/ |
|||
private long getTTLFromSQLite() { |
|||
try { |
|||
SQLiteDatabase db = database.getReadableDatabase(); |
|||
android.database.Cursor cursor = db.query( |
|||
DailyNotificationDatabase.TABLE_NOTIF_CONFIG, |
|||
new String[]{DailyNotificationDatabase.COL_CONFIG_V}, |
|||
DailyNotificationDatabase.COL_CONFIG_K + " = ?", |
|||
new String[]{"ttlSeconds"}, |
|||
null, null, null |
|||
); |
|||
|
|||
long ttlSeconds = DEFAULT_TTL_SECONDS; |
|||
if (cursor.moveToFirst()) { |
|||
ttlSeconds = Long.parseLong(cursor.getString(0)); |
|||
} |
|||
cursor.close(); |
|||
|
|||
// Validate TTL range
|
|||
ttlSeconds = Math.max(MIN_TTL_SECONDS, Math.min(MAX_TTL_SECONDS, ttlSeconds)); |
|||
|
|||
return ttlSeconds; |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error getting TTL from SQLite", e); |
|||
return DEFAULT_TTL_SECONDS; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get TTL from SharedPreferences |
|||
* |
|||
* @return TTL in seconds |
|||
*/ |
|||
private long getTTLFromSharedPreferences() { |
|||
try { |
|||
SharedPreferences prefs = context.getSharedPreferences("DailyNotificationPrefs", Context.MODE_PRIVATE); |
|||
long ttlSeconds = prefs.getLong("ttlSeconds", DEFAULT_TTL_SECONDS); |
|||
|
|||
// Validate TTL range
|
|||
ttlSeconds = Math.max(MIN_TTL_SECONDS, Math.min(MAX_TTL_SECONDS, ttlSeconds)); |
|||
|
|||
return ttlSeconds; |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error getting TTL from SharedPreferences", e); |
|||
return DEFAULT_TTL_SECONDS; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get fetchedAt timestamp for a slot |
|||
* |
|||
* @param slotId Notification slot ID |
|||
* @return FetchedAt timestamp in milliseconds |
|||
*/ |
|||
private long getFetchedAt(String slotId) { |
|||
try { |
|||
if (useSharedStorage && database != null) { |
|||
return getFetchedAtFromSQLite(slotId); |
|||
} else { |
|||
return getFetchedAtFromSharedPreferences(slotId); |
|||
} |
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error getting fetchedAt for slot: " + slotId, e); |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get fetchedAt from SQLite database |
|||
* |
|||
* @param slotId Notification slot ID |
|||
* @return FetchedAt timestamp in milliseconds |
|||
*/ |
|||
private long getFetchedAtFromSQLite(String slotId) { |
|||
try { |
|||
SQLiteDatabase db = database.getReadableDatabase(); |
|||
android.database.Cursor cursor = db.query( |
|||
DailyNotificationDatabase.TABLE_NOTIF_CONTENTS, |
|||
new String[]{DailyNotificationDatabase.COL_CONTENTS_FETCHED_AT}, |
|||
DailyNotificationDatabase.COL_CONTENTS_SLOT_ID + " = ?", |
|||
new String[]{slotId}, |
|||
null, null, |
|||
DailyNotificationDatabase.COL_CONTENTS_FETCHED_AT + " DESC", |
|||
"1" |
|||
); |
|||
|
|||
long fetchedAt = 0; |
|||
if (cursor.moveToFirst()) { |
|||
fetchedAt = cursor.getLong(0); |
|||
} |
|||
cursor.close(); |
|||
|
|||
return fetchedAt; |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error getting fetchedAt from SQLite", e); |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get fetchedAt from SharedPreferences |
|||
* |
|||
* @param slotId Notification slot ID |
|||
* @return FetchedAt timestamp in milliseconds |
|||
*/ |
|||
private long getFetchedAtFromSharedPreferences(String slotId) { |
|||
try { |
|||
SharedPreferences prefs = context.getSharedPreferences("DailyNotificationPrefs", Context.MODE_PRIVATE); |
|||
return prefs.getLong("last_fetch_" + slotId, 0); |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error getting fetchedAt from SharedPreferences", e); |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Log TTL violation with detailed information |
|||
* |
|||
* @param slotId Notification slot ID |
|||
* @param scheduledTime When notification was scheduled to fire |
|||
* @param fetchedAt When content was fetched |
|||
* @param ageAtFireSeconds Age of content at fire time |
|||
* @param ttlSeconds TTL limit in seconds |
|||
*/ |
|||
private void logTTLViolation(String slotId, long scheduledTime, long fetchedAt, |
|||
long ageAtFireSeconds, long ttlSeconds) { |
|||
try { |
|||
String violationMessage = String.format( |
|||
"TTL violation: slot=%s, scheduled=%d, fetched=%d, age=%ds, ttl=%ds", |
|||
slotId, scheduledTime, fetchedAt, ageAtFireSeconds, ttlSeconds |
|||
); |
|||
|
|||
Log.w(TAG, LOG_CODE_TTL_VIOLATION + ": " + violationMessage); |
|||
|
|||
// Store violation in database or SharedPreferences for analytics
|
|||
storeTTLViolation(slotId, scheduledTime, fetchedAt, ageAtFireSeconds, ttlSeconds); |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error logging TTL violation", e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Store TTL violation for analytics |
|||
*/ |
|||
private void storeTTLViolation(String slotId, long scheduledTime, long fetchedAt, |
|||
long ageAtFireSeconds, long ttlSeconds) { |
|||
try { |
|||
if (useSharedStorage && database != null) { |
|||
storeTTLViolationInSQLite(slotId, scheduledTime, fetchedAt, ageAtFireSeconds, ttlSeconds); |
|||
} else { |
|||
storeTTLViolationInSharedPreferences(slotId, scheduledTime, fetchedAt, ageAtFireSeconds, ttlSeconds); |
|||
} |
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error storing TTL violation", e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Store TTL violation in SQLite database |
|||
*/ |
|||
private void storeTTLViolationInSQLite(String slotId, long scheduledTime, long fetchedAt, |
|||
long ageAtFireSeconds, long ttlSeconds) { |
|||
try { |
|||
SQLiteDatabase db = database.getWritableDatabase(); |
|||
|
|||
// Insert into notif_deliveries with error status
|
|||
android.content.ContentValues values = new android.content.ContentValues(); |
|||
values.put(DailyNotificationDatabase.COL_DELIVERIES_SLOT_ID, slotId); |
|||
values.put(DailyNotificationDatabase.COL_DELIVERIES_FIRE_AT, scheduledTime); |
|||
values.put(DailyNotificationDatabase.COL_DELIVERIES_STATUS, DailyNotificationDatabase.STATUS_ERROR); |
|||
values.put(DailyNotificationDatabase.COL_DELIVERIES_ERROR_CODE, LOG_CODE_TTL_VIOLATION); |
|||
values.put(DailyNotificationDatabase.COL_DELIVERIES_ERROR_MESSAGE, |
|||
String.format("Content age %ds exceeds TTL %ds", ageAtFireSeconds, ttlSeconds)); |
|||
|
|||
db.insert(DailyNotificationDatabase.TABLE_NOTIF_DELIVERIES, null, values); |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error storing TTL violation in SQLite", e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Store TTL violation in SharedPreferences |
|||
*/ |
|||
private void storeTTLViolationInSharedPreferences(String slotId, long scheduledTime, long fetchedAt, |
|||
long ageAtFireSeconds, long ttlSeconds) { |
|||
try { |
|||
SharedPreferences prefs = context.getSharedPreferences("DailyNotificationPrefs", Context.MODE_PRIVATE); |
|||
SharedPreferences.Editor editor = prefs.edit(); |
|||
|
|||
String violationKey = "ttl_violation_" + slotId + "_" + scheduledTime; |
|||
String violationValue = String.format("%d,%d,%d,%d", fetchedAt, ageAtFireSeconds, ttlSeconds, System.currentTimeMillis()); |
|||
|
|||
editor.putString(violationKey, violationValue); |
|||
editor.apply(); |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error storing TTL violation in SharedPreferences", e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get TTL violation statistics |
|||
* |
|||
* @return Statistics string |
|||
*/ |
|||
public String getTTLViolationStats() { |
|||
try { |
|||
if (useSharedStorage && database != null) { |
|||
return getTTLViolationStatsFromSQLite(); |
|||
} else { |
|||
return getTTLViolationStatsFromSharedPreferences(); |
|||
} |
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error getting TTL violation stats", e); |
|||
return "Error retrieving TTL violation statistics"; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get TTL violation statistics from SQLite |
|||
*/ |
|||
private String getTTLViolationStatsFromSQLite() { |
|||
try { |
|||
SQLiteDatabase db = database.getReadableDatabase(); |
|||
android.database.Cursor cursor = db.rawQuery( |
|||
"SELECT COUNT(*) FROM " + DailyNotificationDatabase.TABLE_NOTIF_DELIVERIES + |
|||
" WHERE " + DailyNotificationDatabase.COL_DELIVERIES_ERROR_CODE + " = ?", |
|||
new String[]{LOG_CODE_TTL_VIOLATION} |
|||
); |
|||
|
|||
int violationCount = 0; |
|||
if (cursor.moveToFirst()) { |
|||
violationCount = cursor.getInt(0); |
|||
} |
|||
cursor.close(); |
|||
|
|||
return String.format("TTL violations: %d", violationCount); |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error getting TTL violation stats from SQLite", e); |
|||
return "Error retrieving TTL violation statistics"; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get TTL violation statistics from SharedPreferences |
|||
*/ |
|||
private String getTTLViolationStatsFromSharedPreferences() { |
|||
try { |
|||
SharedPreferences prefs = context.getSharedPreferences("DailyNotificationPrefs", Context.MODE_PRIVATE); |
|||
java.util.Map<String, ?> allPrefs = prefs.getAll(); |
|||
|
|||
int violationCount = 0; |
|||
for (String key : allPrefs.keySet()) { |
|||
if (key.startsWith("ttl_violation_")) { |
|||
violationCount++; |
|||
} |
|||
} |
|||
|
|||
return String.format("TTL violations: %d", violationCount); |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error getting TTL violation stats from SharedPreferences", e); |
|||
return "Error retrieving TTL violation statistics"; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,217 @@ |
|||
/** |
|||
* DailyNotificationTTLEnforcerTest.java |
|||
* |
|||
* Unit tests for TTL-at-fire enforcement functionality |
|||
* Tests freshness validation, TTL violation logging, and skip logic |
|||
* |
|||
* @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 DailyNotificationTTLEnforcer |
|||
* |
|||
* Tests the core TTL enforcement functionality including: |
|||
* - Freshness validation before arming |
|||
* - TTL violation detection and logging |
|||
* - Skip logic for stale content |
|||
* - Configuration retrieval from storage |
|||
*/ |
|||
public class DailyNotificationTTLEnforcerTest extends AndroidTestCase { |
|||
|
|||
private DailyNotificationTTLEnforcer ttlEnforcer; |
|||
private Context mockContext; |
|||
private DailyNotificationDatabase database; |
|||
|
|||
@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 database instance
|
|||
database = new DailyNotificationDatabase(mockContext); |
|||
|
|||
// Create TTL enforcer with SQLite storage
|
|||
ttlEnforcer = new DailyNotificationTTLEnforcer(mockContext, database, true); |
|||
} |
|||
|
|||
@Override |
|||
protected void tearDown() throws Exception { |
|||
if (database != null) { |
|||
database.close(); |
|||
} |
|||
super.tearDown(); |
|||
} |
|||
|
|||
/** |
|||
* Test freshness validation with fresh content |
|||
*/ |
|||
public void testFreshContentValidation() { |
|||
long currentTime = System.currentTimeMillis(); |
|||
long scheduledTime = currentTime + TimeUnit.MINUTES.toMillis(30); // 30 minutes from now
|
|||
long fetchedAt = currentTime - TimeUnit.MINUTES.toMillis(5); // 5 minutes ago
|
|||
|
|||
boolean isFresh = ttlEnforcer.isContentFresh("test_slot_1", scheduledTime, fetchedAt); |
|||
|
|||
assertTrue("Content should be fresh (5 min old, scheduled 30 min from now)", isFresh); |
|||
} |
|||
|
|||
/** |
|||
* Test freshness validation with stale content |
|||
*/ |
|||
public void testStaleContentValidation() { |
|||
long currentTime = System.currentTimeMillis(); |
|||
long scheduledTime = currentTime + TimeUnit.MINUTES.toMillis(30); // 30 minutes from now
|
|||
long fetchedAt = currentTime - TimeUnit.HOURS.toMillis(2); // 2 hours ago
|
|||
|
|||
boolean isFresh = ttlEnforcer.isContentFresh("test_slot_2", scheduledTime, fetchedAt); |
|||
|
|||
assertFalse("Content should be stale (2 hours old, exceeds 1 hour TTL)", isFresh); |
|||
} |
|||
|
|||
/** |
|||
* Test TTL violation detection |
|||
*/ |
|||
public void testTTLViolationDetection() { |
|||
long currentTime = System.currentTimeMillis(); |
|||
long scheduledTime = currentTime + TimeUnit.MINUTES.toMillis(30); |
|||
long fetchedAt = currentTime - TimeUnit.HOURS.toMillis(2); // 2 hours ago
|
|||
|
|||
// This should trigger a TTL violation
|
|||
boolean isFresh = ttlEnforcer.isContentFresh("test_slot_3", scheduledTime, fetchedAt); |
|||
|
|||
assertFalse("Should detect TTL violation", isFresh); |
|||
|
|||
// Check that violation was logged (we can't easily test the actual logging,
|
|||
// but we can verify the method returns false as expected)
|
|||
} |
|||
|
|||
/** |
|||
* Test validateBeforeArming with fresh content |
|||
*/ |
|||
public void testValidateBeforeArmingFresh() { |
|||
long currentTime = System.currentTimeMillis(); |
|||
long scheduledTime = currentTime + TimeUnit.MINUTES.toMillis(30); |
|||
long fetchedAt = currentTime - TimeUnit.MINUTES.toMillis(5); |
|||
|
|||
NotificationContent content = new NotificationContent(); |
|||
content.setId("test_slot_4"); |
|||
content.setScheduledTime(scheduledTime); |
|||
content.setFetchedAt(fetchedAt); |
|||
content.setTitle("Test Notification"); |
|||
content.setBody("Test body"); |
|||
|
|||
boolean shouldArm = ttlEnforcer.validateBeforeArming(content); |
|||
|
|||
assertTrue("Should arm fresh content", shouldArm); |
|||
} |
|||
|
|||
/** |
|||
* Test validateBeforeArming with stale content |
|||
*/ |
|||
public void testValidateBeforeArmingStale() { |
|||
long currentTime = System.currentTimeMillis(); |
|||
long scheduledTime = currentTime + TimeUnit.MINUTES.toMillis(30); |
|||
long fetchedAt = currentTime - TimeUnit.HOURS.toMillis(2); |
|||
|
|||
NotificationContent content = new NotificationContent(); |
|||
content.setId("test_slot_5"); |
|||
content.setScheduledTime(scheduledTime); |
|||
content.setFetchedAt(fetchedAt); |
|||
content.setTitle("Test Notification"); |
|||
content.setBody("Test body"); |
|||
|
|||
boolean shouldArm = ttlEnforcer.validateBeforeArming(content); |
|||
|
|||
assertFalse("Should not arm stale content", shouldArm); |
|||
} |
|||
|
|||
/** |
|||
* Test edge case: content fetched exactly at TTL limit |
|||
*/ |
|||
public void testTTLBoundaryCase() { |
|||
long currentTime = System.currentTimeMillis(); |
|||
long scheduledTime = currentTime + TimeUnit.MINUTES.toMillis(30); |
|||
long fetchedAt = currentTime - TimeUnit.HOURS.toMillis(1); // Exactly 1 hour ago (TTL limit)
|
|||
|
|||
boolean isFresh = ttlEnforcer.isContentFresh("test_slot_6", scheduledTime, fetchedAt); |
|||
|
|||
assertTrue("Content at TTL boundary should be considered fresh", isFresh); |
|||
} |
|||
|
|||
/** |
|||
* Test edge case: content fetched just over TTL limit |
|||
*/ |
|||
public void testTTLBoundaryCaseOver() { |
|||
long currentTime = System.currentTimeMillis(); |
|||
long scheduledTime = currentTime + TimeUnit.MINUTES.toMillis(30); |
|||
long fetchedAt = currentTime - TimeUnit.HOURS.toMillis(1) - TimeUnit.SECONDS.toMillis(1); // 1 hour + 1 second ago
|
|||
|
|||
boolean isFresh = ttlEnforcer.isContentFresh("test_slot_7", scheduledTime, fetchedAt); |
|||
|
|||
assertFalse("Content just over TTL limit should be considered stale", isFresh); |
|||
} |
|||
|
|||
/** |
|||
* Test TTL violation statistics |
|||
*/ |
|||
public void testTTLViolationStats() { |
|||
// Generate some TTL violations
|
|||
long currentTime = System.currentTimeMillis(); |
|||
long scheduledTime = currentTime + TimeUnit.MINUTES.toMillis(30); |
|||
long fetchedAt = currentTime - TimeUnit.HOURS.toMillis(2); |
|||
|
|||
// Trigger TTL violations
|
|||
ttlEnforcer.isContentFresh("test_slot_8", scheduledTime, fetchedAt); |
|||
ttlEnforcer.isContentFresh("test_slot_9", scheduledTime, fetchedAt); |
|||
|
|||
String stats = ttlEnforcer.getTTLViolationStats(); |
|||
|
|||
assertNotNull("TTL violation stats should not be null", stats); |
|||
assertTrue("Stats should contain violation count", stats.contains("violations")); |
|||
} |
|||
|
|||
/** |
|||
* Test error handling with invalid parameters |
|||
*/ |
|||
public void testErrorHandling() { |
|||
// Test with null slot ID
|
|||
boolean result = ttlEnforcer.isContentFresh(null, System.currentTimeMillis(), System.currentTimeMillis()); |
|||
assertFalse("Should handle null slot ID gracefully", result); |
|||
|
|||
// Test with invalid timestamps
|
|||
result = ttlEnforcer.isContentFresh("test_slot_10", 0, 0); |
|||
assertTrue("Should handle invalid timestamps gracefully", result); |
|||
} |
|||
|
|||
/** |
|||
* Test TTL configuration retrieval |
|||
*/ |
|||
public void testTTLConfiguration() { |
|||
// Test that TTL enforcer can retrieve configuration
|
|||
// This is indirectly tested through the freshness checks
|
|||
long currentTime = System.currentTimeMillis(); |
|||
long scheduledTime = currentTime + TimeUnit.MINUTES.toMillis(30); |
|||
long fetchedAt = currentTime - TimeUnit.MINUTES.toMillis(30); // 30 minutes ago
|
|||
|
|||
boolean isFresh = ttlEnforcer.isContentFresh("test_slot_11", scheduledTime, fetchedAt); |
|||
|
|||
// Should be fresh (30 min < 1 hour TTL)
|
|||
assertTrue("Should retrieve TTL configuration correctly", isFresh); |
|||
} |
|||
} |
Loading…
Reference in new issue