/** * DailyNotificationFetchWorker.java * * WorkManager worker for background content fetching * Implements the prefetch step with timeout handling and retry logic * * @author Matthew Raymer * @version 1.0.0 */ package com.timesafari.dailynotification; import android.content.Context; import android.util.Log; import androidx.annotation.NonNull; import androidx.work.Data; import androidx.work.Worker; import androidx.work.WorkerParameters; import java.util.concurrent.TimeUnit; /** * Background worker for fetching daily notification content * * This worker implements the prefetch step of the offline-first pipeline. * It runs in the background to fetch content before it's needed, * with proper timeout handling and retry mechanisms. */ public class DailyNotificationFetchWorker extends Worker { private static final String TAG = "DailyNotificationFetchWorker"; private static final String KEY_SCHEDULED_TIME = "scheduled_time"; private static final String KEY_FETCH_TIME = "fetch_time"; private static final String KEY_RETRY_COUNT = "retry_count"; private static final String KEY_IMMEDIATE = "immediate"; private static final int MAX_RETRY_ATTEMPTS = 3; private static final long WORK_TIMEOUT_MS = 8 * 60 * 1000; // 8 minutes total private static final long FETCH_TIMEOUT_MS = 30 * 1000; // 30 seconds for fetch private final Context context; private final DailyNotificationStorage storage; private final DailyNotificationFetcher fetcher; /** * Constructor * * @param context Application context * @param params Worker parameters */ public DailyNotificationFetchWorker(@NonNull Context context, @NonNull WorkerParameters params) { super(context, params); this.context = context; this.storage = new DailyNotificationStorage(context); this.fetcher = new DailyNotificationFetcher(context, storage); } /** * Main work method - fetch content with timeout and retry logic * * @return Result indicating success, failure, or retry */ @NonNull @Override public Result doWork() { try { Log.d(TAG, "Starting background content fetch"); // Get input data Data inputData = getInputData(); long scheduledTime = inputData.getLong(KEY_SCHEDULED_TIME, 0); long fetchTime = inputData.getLong(KEY_FETCH_TIME, 0); int retryCount = inputData.getInt(KEY_RETRY_COUNT, 0); boolean immediate = inputData.getBoolean(KEY_IMMEDIATE, false); Log.d(TAG, String.format("Fetch parameters - Scheduled: %d, Fetch: %d, Retry: %d, Immediate: %s", scheduledTime, fetchTime, retryCount, immediate)); // Check if we should proceed with fetch if (!shouldProceedWithFetch(scheduledTime, fetchTime)) { Log.d(TAG, "Skipping fetch - conditions not met"); return Result.success(); } // Attempt to fetch content with timeout NotificationContent content = fetchContentWithTimeout(); if (content != null) { // Success - save content and schedule notification handleSuccessfulFetch(content); return Result.success(); } else { // Fetch failed - handle retry logic return handleFailedFetch(retryCount, scheduledTime); } } catch (Exception e) { Log.e(TAG, "Unexpected error during background fetch", e); return handleFailedFetch(0, 0); } } /** * Check if we should proceed with the fetch * * @param scheduledTime When notification is scheduled for * @param fetchTime When fetch was originally scheduled for * @return true if fetch should proceed */ private boolean shouldProceedWithFetch(long scheduledTime, long fetchTime) { long currentTime = System.currentTimeMillis(); // If this is an immediate fetch, always proceed if (fetchTime == 0) { return true; } // Check if fetch time has passed if (currentTime < fetchTime) { Log.d(TAG, "Fetch time not yet reached"); return false; } // Check if notification time has passed if (currentTime >= scheduledTime) { Log.d(TAG, "Notification time has passed, fetch not needed"); return false; } // Check if we already have recent content if (!storage.shouldFetchNewContent()) { Log.d(TAG, "Recent content available, fetch not needed"); return false; } return true; } /** * Fetch content with timeout handling * * @return Fetched content or null if failed */ private NotificationContent fetchContentWithTimeout() { try { Log.d(TAG, "Fetching content with timeout: " + FETCH_TIMEOUT_MS + "ms"); // Use a simple timeout mechanism // In production, you might use CompletableFuture with timeout long startTime = System.currentTimeMillis(); // Attempt fetch NotificationContent content = fetcher.fetchContentImmediately(); long fetchDuration = System.currentTimeMillis() - startTime; if (content != null) { Log.d(TAG, "Content fetched successfully in " + fetchDuration + "ms"); return content; } else { Log.w(TAG, "Content fetch returned null after " + fetchDuration + "ms"); return null; } } catch (Exception e) { Log.e(TAG, "Error during content fetch", e); return null; } } /** * Handle successful content fetch * * @param content Successfully fetched content */ private void handleSuccessfulFetch(NotificationContent content) { try { Log.d(TAG, "Handling successful content fetch: " + content.getId()); // Content is already saved by the fetcher // Update last fetch time storage.setLastFetchTime(System.currentTimeMillis()); // Schedule notification if not already scheduled scheduleNotificationIfNeeded(content); Log.i(TAG, "Successful fetch handling completed"); } catch (Exception e) { Log.e(TAG, "Error handling successful fetch", e); } } /** * Handle failed content fetch with retry logic * * @param retryCount Current retry attempt * @param scheduledTime When notification is scheduled for * @return Result indicating retry or failure */ private Result handleFailedFetch(int retryCount, long scheduledTime) { try { Log.d(TAG, "Handling failed fetch - Retry: " + retryCount); if (retryCount < MAX_RETRY_ATTEMPTS) { // Schedule retry scheduleRetry(retryCount + 1, scheduledTime); Log.i(TAG, "Scheduled retry attempt " + (retryCount + 1)); return Result.retry(); } else { // Max retries reached - use fallback content Log.w(TAG, "Max retries reached, using fallback content"); useFallbackContent(scheduledTime); return Result.success(); } } catch (Exception e) { Log.e(TAG, "Error handling failed fetch", e); return Result.failure(); } } /** * Schedule a retry attempt * * @param retryCount New retry attempt number * @param scheduledTime When notification is scheduled for */ private void scheduleRetry(int retryCount, long scheduledTime) { try { Log.d(TAG, "Scheduling retry attempt " + retryCount); // Calculate retry delay with exponential backoff long retryDelay = calculateRetryDelay(retryCount); // Create retry work request Data retryData = new Data.Builder() .putLong(KEY_SCHEDULED_TIME, scheduledTime) .putLong(KEY_FETCH_TIME, System.currentTimeMillis()) .putInt(KEY_RETRY_COUNT, retryCount) .build(); androidx.work.OneTimeWorkRequest retryWork = new androidx.work.OneTimeWorkRequest.Builder(DailyNotificationFetchWorker.class) .setInputData(retryData) .setInitialDelay(retryDelay, TimeUnit.MILLISECONDS) .build(); androidx.work.WorkManager.getInstance(context).enqueue(retryWork); Log.d(TAG, "Retry scheduled for " + retryDelay + "ms from now"); } catch (Exception e) { Log.e(TAG, "Error scheduling retry", e); } } /** * Calculate retry delay with exponential backoff * * @param retryCount Current retry attempt * @return Delay in milliseconds */ private long calculateRetryDelay(int retryCount) { // Base delay: 1 minute, exponential backoff: 2^retryCount long baseDelay = 60 * 1000; // 1 minute long exponentialDelay = baseDelay * (long) Math.pow(2, retryCount - 1); // Cap at 1 hour long maxDelay = 60 * 60 * 1000; // 1 hour return Math.min(exponentialDelay, maxDelay); } /** * Use fallback content when all retries fail * * @param scheduledTime When notification is scheduled for */ private void useFallbackContent(long scheduledTime) { try { Log.d(TAG, "Using fallback content for scheduled time: " + scheduledTime); // Get fallback content from storage or create emergency content NotificationContent fallbackContent = getFallbackContent(scheduledTime); if (fallbackContent != null) { // Save fallback content storage.saveNotificationContent(fallbackContent); // Schedule notification scheduleNotificationIfNeeded(fallbackContent); Log.i(TAG, "Fallback content applied successfully"); } else { Log.e(TAG, "Failed to get fallback content"); } } catch (Exception e) { Log.e(TAG, "Error using fallback content", e); } } /** * Get fallback content for the scheduled time * * @param scheduledTime When notification is scheduled for * @return Fallback notification content */ private NotificationContent getFallbackContent(long scheduledTime) { 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"); // Create new content based on last good content NotificationContent fallbackContent = new NotificationContent(); fallbackContent.setTitle(lastContent.getTitle()); fallbackContent.setBody(lastContent.getBody() + " (from " + lastContent.getAgeString() + ")"); fallbackContent.setScheduledTime(scheduledTime); fallbackContent.setSound(lastContent.isSound()); fallbackContent.setPriority(lastContent.getPriority()); fallbackContent.setUrl(lastContent.getUrl()); fallbackContent.setFetchTime(System.currentTimeMillis()); return fallbackContent; } // Create emergency fallback content Log.w(TAG, "Creating emergency fallback content"); return createEmergencyFallbackContent(scheduledTime); } catch (Exception e) { Log.e(TAG, "Error getting fallback content", e); return createEmergencyFallbackContent(scheduledTime); } } /** * Create emergency fallback content * * @param scheduledTime When notification is scheduled for * @return Emergency notification content */ private NotificationContent createEmergencyFallbackContent(long scheduledTime) { NotificationContent content = new NotificationContent(); content.setTitle("Daily Update"); content.setBody("🌅 Good morning! Ready to make today amazing?"); content.setScheduledTime(scheduledTime); content.setFetchTime(System.currentTimeMillis()); content.setPriority("default"); content.setSound(true); return content; } /** * Schedule notification if not already scheduled * * @param content Notification content to schedule */ private void scheduleNotificationIfNeeded(NotificationContent content) { try { Log.d(TAG, "Checking if notification needs scheduling: " + content.getId()); // Check if notification is already scheduled DailyNotificationScheduler scheduler = new DailyNotificationScheduler( context, (android.app.AlarmManager) context.getSystemService(Context.ALARM_SERVICE) ); if (!scheduler.isNotificationScheduled(content.getId())) { Log.d(TAG, "Scheduling notification: " + content.getId()); boolean scheduled = scheduler.scheduleNotification(content); if (scheduled) { Log.i(TAG, "Notification scheduled successfully"); } else { Log.e(TAG, "Failed to schedule notification"); } } else { Log.d(TAG, "Notification already scheduled: " + content.getId()); } } catch (Exception e) { Log.e(TAG, "Error checking/scheduling notification", e); } } }