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