/** * DailyNotificationStorage.java * * Storage management for notification content and settings * Implements tiered storage: Key-Value (quick) + DB (structured) + Files (large assets) * * @author Matthew Raymer * @version 1.0.0 */ package com.timesafari.dailynotification; import android.content.Context; import android.content.SharedPreferences; import android.util.Log; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.io.File; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.concurrent.ConcurrentHashMap; /** * Manages storage for notification content and settings * * This class implements the tiered storage approach: * - Tier 1: SharedPreferences for quick access to settings and recent data * - Tier 2: In-memory cache for structured notification content * - Tier 3: File system for large assets (future use) */ public class DailyNotificationStorage { private static final String TAG = "DailyNotificationStorage"; private static final String PREFS_NAME = "DailyNotificationPrefs"; private static final String KEY_NOTIFICATIONS = "notifications"; private static final String KEY_SETTINGS = "settings"; private static final String KEY_LAST_FETCH = "last_fetch"; private static final String KEY_ADAPTIVE_SCHEDULING = "adaptive_scheduling"; private static final int MAX_CACHE_SIZE = 100; // Maximum notifications to keep in memory private static final long CACHE_CLEANUP_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours private final Context context; private final SharedPreferences prefs; private final Gson gson; private final ConcurrentHashMap notificationCache; private final List notificationList; /** * Constructor * * @param context Application context */ public DailyNotificationStorage(Context context) { this.context = context; this.prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); this.gson = new Gson(); this.notificationCache = new ConcurrentHashMap<>(); this.notificationList = Collections.synchronizedList(new ArrayList<>()); loadNotificationsFromStorage(); cleanupOldNotifications(); } /** * Save notification content to storage * * @param content Notification content to save */ public void saveNotificationContent(NotificationContent content) { try { Log.d(TAG, "Saving notification: " + content.getId()); // Add to cache notificationCache.put(content.getId(), content); // Add to list and sort by scheduled time synchronized (notificationList) { notificationList.removeIf(n -> n.getId().equals(content.getId())); notificationList.add(content); Collections.sort(notificationList, Comparator.comparingLong(NotificationContent::getScheduledTime)); } // Persist to SharedPreferences saveNotificationsToStorage(); Log.d(TAG, "Notification saved successfully"); } catch (Exception e) { Log.e(TAG, "Error saving notification content", e); } } /** * Get notification content by ID * * @param id Notification ID * @return Notification content or null if not found */ public NotificationContent getNotificationContent(String id) { return notificationCache.get(id); } /** * Get the last notification that was delivered * * @return Last notification or null if none exists */ public NotificationContent getLastNotification() { synchronized (notificationList) { if (notificationList.isEmpty()) { return null; } // Find the most recent delivered notification long currentTime = System.currentTimeMillis(); for (int i = notificationList.size() - 1; i >= 0; i--) { NotificationContent notification = notificationList.get(i); if (notification.getScheduledTime() <= currentTime) { return notification; } } return null; } } /** * Get all notifications * * @return List of all notifications */ public List getAllNotifications() { synchronized (notificationList) { return new ArrayList<>(notificationList); } } /** * Get notifications that are ready to be displayed * * @return List of ready notifications */ public List getReadyNotifications() { List readyNotifications = new ArrayList<>(); long currentTime = System.currentTimeMillis(); synchronized (notificationList) { for (NotificationContent notification : notificationList) { if (notification.isReadyToDisplay()) { readyNotifications.add(notification); } } } return readyNotifications; } /** * Get the next scheduled notification * * @return Next notification or null if none scheduled */ public NotificationContent getNextNotification() { synchronized (notificationList) { long currentTime = System.currentTimeMillis(); for (NotificationContent notification : notificationList) { if (notification.getScheduledTime() > currentTime) { return notification; } } return null; } } /** * Remove notification by ID * * @param id Notification ID to remove */ public void removeNotification(String id) { try { Log.d(TAG, "Removing notification: " + id); notificationCache.remove(id); synchronized (notificationList) { notificationList.removeIf(n -> n.getId().equals(id)); } saveNotificationsToStorage(); Log.d(TAG, "Notification removed successfully"); } catch (Exception e) { Log.e(TAG, "Error removing notification", e); } } /** * Clear all notifications */ public void clearAllNotifications() { try { Log.d(TAG, "Clearing all notifications"); notificationCache.clear(); synchronized (notificationList) { notificationList.clear(); } saveNotificationsToStorage(); Log.d(TAG, "All notifications cleared successfully"); } catch (Exception e) { Log.e(TAG, "Error clearing notifications", e); } } /** * Get notification count * * @return Number of notifications */ public int getNotificationCount() { return notificationCache.size(); } /** * Check if storage is empty * * @return true if no notifications exist */ public boolean isEmpty() { return notificationCache.isEmpty(); } /** * Set sound enabled setting * * @param enabled true to enable sound */ public void setSoundEnabled(boolean enabled) { SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean("sound_enabled", enabled); editor.apply(); Log.d(TAG, "Sound setting updated: " + enabled); } /** * Get sound enabled setting * * @return true if sound is enabled */ public boolean isSoundEnabled() { return prefs.getBoolean("sound_enabled", true); } /** * Set notification priority * * @param priority Priority string (high, default, low) */ public void setPriority(String priority) { SharedPreferences.Editor editor = prefs.edit(); editor.putString("priority", priority); editor.apply(); Log.d(TAG, "Priority setting updated: " + priority); } /** * Get notification priority * * @return Priority string */ public String getPriority() { return prefs.getString("priority", "default"); } /** * Set timezone setting * * @param timezone Timezone identifier */ public void setTimezone(String timezone) { SharedPreferences.Editor editor = prefs.edit(); editor.putString("timezone", timezone); editor.apply(); Log.d(TAG, "Timezone setting updated: " + timezone); } /** * Get timezone setting * * @return Timezone identifier */ public String getTimezone() { return prefs.getString("timezone", "UTC"); } /** * Set adaptive scheduling enabled * * @param enabled true to enable adaptive scheduling */ public void setAdaptiveSchedulingEnabled(boolean enabled) { SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean(KEY_ADAPTIVE_SCHEDULING, enabled); editor.apply(); Log.d(TAG, "Adaptive scheduling setting updated: " + enabled); } /** * Check if adaptive scheduling is enabled * * @return true if adaptive scheduling is enabled */ public boolean isAdaptiveSchedulingEnabled() { return prefs.getBoolean(KEY_ADAPTIVE_SCHEDULING, true); } /** * Set last fetch timestamp * * @param timestamp Last fetch time in milliseconds */ public void setLastFetchTime(long timestamp) { SharedPreferences.Editor editor = prefs.edit(); editor.putLong(KEY_LAST_FETCH, timestamp); editor.apply(); Log.d(TAG, "Last fetch time updated: " + timestamp); } /** * Get last fetch timestamp * * @return Last fetch time in milliseconds */ public long getLastFetchTime() { return prefs.getLong(KEY_LAST_FETCH, 0); } /** * Check if it's time to fetch new content * * @return true if fetch is needed */ public boolean shouldFetchNewContent() { long lastFetch = getLastFetchTime(); long currentTime = System.currentTimeMillis(); long timeSinceLastFetch = currentTime - lastFetch; // Fetch if more than 12 hours have passed return timeSinceLastFetch > 12 * 60 * 60 * 1000; } /** * Load notifications from persistent storage */ private void loadNotificationsFromStorage() { try { String notificationsJson = prefs.getString(KEY_NOTIFICATIONS, "[]"); Type type = new TypeToken>(){}.getType(); List notifications = gson.fromJson(notificationsJson, type); if (notifications != null) { for (NotificationContent notification : notifications) { notificationCache.put(notification.getId(), notification); notificationList.add(notification); } // Sort by scheduled time Collections.sort(notificationList, Comparator.comparingLong(NotificationContent::getScheduledTime)); Log.d(TAG, "Loaded " + notifications.size() + " notifications from storage"); } } catch (Exception e) { Log.e(TAG, "Error loading notifications from storage", e); } } /** * Save notifications to persistent storage */ private void saveNotificationsToStorage() { try { List notifications; synchronized (notificationList) { notifications = new ArrayList<>(notificationList); } String notificationsJson = gson.toJson(notifications); SharedPreferences.Editor editor = prefs.edit(); editor.putString(KEY_NOTIFICATIONS, notificationsJson); editor.apply(); Log.d(TAG, "Saved " + notifications.size() + " notifications to storage"); } catch (Exception e) { Log.e(TAG, "Error saving notifications to storage", e); } } /** * Clean up old notifications to prevent memory bloat */ private void cleanupOldNotifications() { try { long currentTime = System.currentTimeMillis(); long cutoffTime = currentTime - (7 * 24 * 60 * 60 * 1000); // 7 days ago synchronized (notificationList) { notificationList.removeIf(notification -> notification.getScheduledTime() < cutoffTime); } // Update cache to match notificationCache.clear(); for (NotificationContent notification : notificationList) { notificationCache.put(notification.getId(), notification); } // Limit cache size if (notificationCache.size() > MAX_CACHE_SIZE) { List sortedNotifications = new ArrayList<>(notificationList); Collections.sort(sortedNotifications, Comparator.comparingLong(NotificationContent::getScheduledTime)); int toRemove = sortedNotifications.size() - MAX_CACHE_SIZE; for (int i = 0; i < toRemove; i++) { NotificationContent notification = sortedNotifications.get(i); notificationCache.remove(notification.getId()); } notificationList.clear(); notificationList.addAll(sortedNotifications.subList(toRemove, sortedNotifications.size())); } saveNotificationsToStorage(); Log.d(TAG, "Cleanup completed. Cache size: " + notificationCache.size()); } catch (Exception e) { Log.e(TAG, "Error during cleanup", e); } } /** * Get storage statistics * * @return Storage statistics as a string */ public String getStorageStats() { return String.format("Notifications: %d, Cache size: %d, Last fetch: %d", notificationList.size(), notificationCache.size(), getLastFetchTime()); } }