/** * DailyNotificationFetcher.java * * Handles background content fetching for daily notifications * Implements the prefetch step of the prefetch → cache → schedule → display pipeline * * @author Matthew Raymer * @version 1.0.0 */ package com.timesafari.dailynotification; import android.content.Context; import android.util.Log; import androidx.work.Data; import androidx.work.OneTimeWorkRequest; import androidx.work.WorkManager; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.util.concurrent.TimeUnit; /** * Manages background content fetching for daily notifications * * This class implements the prefetch step of the offline-first pipeline. * It schedules background work to fetch content before it's needed, * with proper timeout handling and fallback mechanisms. */ public class DailyNotificationFetcher { private static final String TAG = "DailyNotificationFetcher"; private static final String WORK_TAG_FETCH = "daily_notification_fetch"; private static final String WORK_TAG_MAINTENANCE = "daily_notification_maintenance"; private static final int NETWORK_TIMEOUT_MS = 30000; // 30 seconds private static final int MAX_RETRY_ATTEMPTS = 3; private static final long RETRY_DELAY_MS = 60000; // 1 minute private final Context context; private final DailyNotificationStorage storage; private final WorkManager workManager; // ETag manager for efficient fetching private final DailyNotificationETagManager etagManager; /** * Constructor * * @param context Application context * @param storage Storage instance for saving fetched content */ public DailyNotificationFetcher(Context context, DailyNotificationStorage storage) { this.context = context; this.storage = storage; this.workManager = WorkManager.getInstance(context); this.etagManager = new DailyNotificationETagManager(storage); Log.d(TAG, "DailyNotificationFetcher initialized with ETag support"); } /** * Schedule a background fetch for content * * @param scheduledTime When the notification is scheduled for */ public void scheduleFetch(long scheduledTime) { try { Log.d(TAG, "Scheduling background fetch for " + scheduledTime); // Calculate fetch time (1 hour before notification) long fetchTime = scheduledTime - TimeUnit.HOURS.toMillis(1); if (fetchTime > System.currentTimeMillis()) { // Create work data Data inputData = new Data.Builder() .putLong("scheduled_time", scheduledTime) .putLong("fetch_time", fetchTime) .putInt("retry_count", 0) .build(); // Create one-time work request OneTimeWorkRequest fetchWork = new OneTimeWorkRequest.Builder( DailyNotificationFetchWorker.class) .setInputData(inputData) .addTag(WORK_TAG_FETCH) .setInitialDelay(fetchTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS) .build(); // Enqueue the work workManager.enqueue(fetchWork); Log.i(TAG, "Background fetch scheduled successfully"); } else { Log.w(TAG, "Fetch time has already passed, scheduling immediate fetch"); scheduleImmediateFetch(); } } catch (Exception e) { Log.e(TAG, "Error scheduling background fetch", e); // Fallback to immediate fetch scheduleImmediateFetch(); } } /** * Schedule an immediate fetch (fallback) */ public void scheduleImmediateFetch() { try { Log.d(TAG, "Scheduling immediate fetch"); Data inputData = new Data.Builder() .putLong("scheduled_time", System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)) .putLong("fetch_time", System.currentTimeMillis()) .putInt("retry_count", 0) .putBoolean("immediate", true) .build(); OneTimeWorkRequest fetchWork = new OneTimeWorkRequest.Builder( DailyNotificationFetchWorker.class) .setInputData(inputData) .addTag(WORK_TAG_FETCH) .build(); workManager.enqueue(fetchWork); Log.i(TAG, "Immediate fetch scheduled successfully"); } catch (Exception e) { Log.e(TAG, "Error scheduling immediate fetch", e); } } /** * Fetch content immediately (synchronous) * * @return Fetched notification content or null if failed */ public NotificationContent fetchContentImmediately() { try { Log.d(TAG, "Fetching content immediately"); // Check if we should fetch new content if (!storage.shouldFetchNewContent()) { Log.d(TAG, "Content fetch not needed yet"); return storage.getLastNotification(); } // Attempt to fetch from network NotificationContent content = fetchFromNetwork(); if (content != null) { // Save to storage storage.saveNotificationContent(content); storage.setLastFetchTime(System.currentTimeMillis()); Log.i(TAG, "Content fetched and saved successfully"); return content; } else { // Fallback to cached content Log.w(TAG, "Network fetch failed, using cached content"); return getFallbackContent(); } } catch (Exception e) { Log.e(TAG, "Error during immediate content fetch", e); return getFallbackContent(); } } /** * Fetch content from network with ETag support * * @return Fetched content or null if failed */ private NotificationContent fetchFromNetwork() { try { Log.d(TAG, "Fetching content from network with ETag support"); // Get content endpoint URL String contentUrl = getContentEndpoint(); // Make conditional request with ETag DailyNotificationETagManager.ConditionalRequestResult result = etagManager.makeConditionalRequest(contentUrl); if (result.success) { if (result.isFromCache) { Log.d(TAG, "Content not modified (304) - using cached content"); return storage.getLastNotification(); } else { Log.d(TAG, "New content available (200) - parsing response"); return parseNetworkResponse(result.content); } } else { Log.w(TAG, "Conditional request failed: " + result.error); return null; } } catch (Exception e) { Log.e(TAG, "Error during network fetch with ETag", e); return null; } } /** * Parse network response into notification content * * @param connection HTTP connection with response * @return Parsed notification content or null if parsing failed */ private NotificationContent parseNetworkResponse(HttpURLConnection connection) { try { // This is a simplified parser - in production you'd use a proper JSON parser // For now, we'll create a placeholder content NotificationContent content = new NotificationContent(); content.setTitle("Daily Update"); content.setBody("Your daily notification is ready"); content.setScheduledTime(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)); content.setFetchTime(System.currentTimeMillis()); return content; } catch (Exception e) { Log.e(TAG, "Error parsing network response", e); return null; } } /** * Parse network response string into notification content * * @param responseString Response content as string * @return Parsed notification content or null if parsing failed */ private NotificationContent parseNetworkResponse(String responseString) { try { Log.d(TAG, "Parsing network response string"); // This is a simplified parser - in production you'd use a proper JSON parser // For now, we'll create a placeholder content NotificationContent content = new NotificationContent(); content.setTitle("Daily Update"); content.setBody("Your daily notification is ready"); content.setScheduledTime(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)); content.setFetchTime(System.currentTimeMillis()); Log.d(TAG, "Network response parsed successfully"); return content; } catch (Exception e) { Log.e(TAG, "Error parsing network response string", e); return null; } } /** * Get fallback content when network fetch fails * * @return Fallback notification content */ private NotificationContent getFallbackContent() { try { // Try to get last known good content NotificationContent lastContent = storage.getLastNotification(); if (lastContent != null && !lastContent.isStale()) { Log.d(TAG, "Using last known good content as fallback"); return lastContent; } // Create emergency fallback content Log.w(TAG, "Creating emergency fallback content"); return createEmergencyFallbackContent(); } catch (Exception e) { Log.e(TAG, "Error getting fallback content", e); return createEmergencyFallbackContent(); } } /** * Create emergency fallback content * * @return Emergency notification content */ private NotificationContent createEmergencyFallbackContent() { NotificationContent content = new NotificationContent(); content.setTitle("Daily Update"); content.setBody("🌅 Good morning! Ready to make today amazing?"); content.setScheduledTime(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)); content.setFetchTime(System.currentTimeMillis()); content.setPriority("default"); content.setSound(true); return content; } /** * Get the content endpoint URL * * @return Content endpoint URL */ private String getContentEndpoint() { // This would typically come from configuration // For now, return a placeholder return "https://api.timesafari.com/daily-content"; } /** * Schedule maintenance work */ public void scheduleMaintenance() { try { Log.d(TAG, "Scheduling maintenance work"); Data inputData = new Data.Builder() .putLong("maintenance_time", System.currentTimeMillis()) .build(); OneTimeWorkRequest maintenanceWork = new OneTimeWorkRequest.Builder( DailyNotificationMaintenanceWorker.class) .setInputData(inputData) .addTag(WORK_TAG_MAINTENANCE) .setInitialDelay(TimeUnit.HOURS.toMillis(2), TimeUnit.MILLISECONDS) .build(); workManager.enqueue(maintenanceWork); Log.i(TAG, "Maintenance work scheduled successfully"); } catch (Exception e) { Log.e(TAG, "Error scheduling maintenance work", e); } } /** * Cancel all scheduled fetch work */ public void cancelAllFetchWork() { try { Log.d(TAG, "Cancelling all fetch work"); workManager.cancelAllWorkByTag(WORK_TAG_FETCH); workManager.cancelAllWorkByTag(WORK_TAG_MAINTENANCE); Log.i(TAG, "All fetch work cancelled"); } catch (Exception e) { Log.e(TAG, "Error cancelling fetch work", e); } } /** * Check if fetch work is scheduled * * @return true if fetch work is scheduled */ public boolean isFetchWorkScheduled() { // This would check WorkManager for pending work // For now, return a placeholder return false; } /** * Get fetch statistics * * @return Fetch statistics as a string */ public String getFetchStats() { return String.format("Last fetch: %d, Fetch work scheduled: %s", storage.getLastFetchTime(), isFetchWorkScheduled() ? "yes" : "no"); } /** * Get ETag manager for external access * * @return ETag manager instance */ public DailyNotificationETagManager getETagManager() { return etagManager; } /** * Get network efficiency metrics * * @return Network metrics */ public DailyNotificationETagManager.NetworkMetrics getNetworkMetrics() { return etagManager.getMetrics(); } /** * Get ETag cache statistics * * @return Cache statistics */ public DailyNotificationETagManager.CacheStatistics getCacheStatistics() { return etagManager.getCacheStatistics(); } /** * Clean expired ETags */ public void cleanExpiredETags() { etagManager.cleanExpiredETags(); } /** * Reset network metrics */ public void resetNetworkMetrics() { etagManager.resetMetrics(); } }