/** * 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; /** * 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); } /** * 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 timeout * * @return Fetched content or null if failed */ private NotificationContent fetchFromNetwork() { HttpURLConnection connection = null; try { // Create connection to content endpoint URL url = new URL(getContentEndpoint()); connection = (HttpURLConnection) url.openConnection(); // Set timeout connection.setConnectTimeout(NETWORK_TIMEOUT_MS); connection.setReadTimeout(NETWORK_TIMEOUT_MS); connection.setRequestMethod("GET"); // Add headers connection.setRequestProperty("User-Agent", "TimeSafari-DailyNotification/1.0"); connection.setRequestProperty("Accept", "application/json"); // Connect and check response int responseCode = connection.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK) { // Parse response and create notification content NotificationContent content = parseNetworkResponse(connection); if (content != null) { Log.d(TAG, "Content fetched from network successfully"); return content; } } else { Log.w(TAG, "Network request failed with response code: " + responseCode); } } catch (IOException e) { Log.e(TAG, "Network error during content fetch", e); } catch (Exception e) { Log.e(TAG, "Unexpected error during network fetch", e); } finally { if (connection != null) { connection.disconnect(); } } 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; } } /** * 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"); } }