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.
 
 
 
 
 
 

364 lines
12 KiB

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