- Add DailyNotificationPlugin main class with Capacitor integration - Implement NotificationContent model following project directive schema - Create DailyNotificationStorage with tiered storage approach - Add DailyNotificationScheduler with exact/inexact alarm support - Implement DailyNotificationFetcher for background content retrieval - Create DailyNotificationReceiver for alarm handling - Add WorkManager workers for background tasks and maintenance - Implement prefetch → cache → schedule → display pipeline - Add comprehensive error handling and logging - Support battery optimization and adaptive scheduling
477 lines
15 KiB
Java
477 lines
15 KiB
Java
/**
|
|
* 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<String, NotificationContent> notificationCache;
|
|
private final List<NotificationContent> 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<NotificationContent> getAllNotifications() {
|
|
synchronized (notificationList) {
|
|
return new ArrayList<>(notificationList);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get notifications that are ready to be displayed
|
|
*
|
|
* @return List of ready notifications
|
|
*/
|
|
public List<NotificationContent> getReadyNotifications() {
|
|
List<NotificationContent> 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<ArrayList<NotificationContent>>(){}.getType();
|
|
List<NotificationContent> 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<NotificationContent> 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<NotificationContent> 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());
|
|
}
|
|
}
|