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