Files
daily-notification-plugin/src/android/DailyNotificationStorage.java
Matthew Raymer 2d535b5d8f feat: Implement Android native plugin with offline-first pipeline
- 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
2025-08-12 09:39:06 +00:00

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());
}
}