You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

395 lines
14 KiB

/**
* 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);
}
}
}