@ -18,7 +18,13 @@ import androidx.work.Data;
import androidx.work.Worker ;
import androidx.work.WorkerParameters ;
import java.util.List ;
import java.util.Map ;
import java.util.HashMap ;
import java.util.concurrent.TimeUnit ;
import java.util.concurrent.TimeoutException ;
import java.util.concurrent.CompletableFuture ;
import java.util.Random ;
/ * *
* Background worker for fetching daily notification content
@ -37,11 +43,13 @@ public class DailyNotificationFetchWorker extends Worker {
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
// Legacy timeout (will be replaced by SchedulingPolicy)
private static final long FETCH_TIMEOUT_MS_DEFAULT = 30 * 1000 ; // 30 seconds
private final Context context ;
private final DailyNotificationStorage storage ;
private final DailyNotificationFetcher fetcher ;
private final DailyNotificationFetcher fetcher ; // Legacy fetcher (fallback only)
/ * *
* Constructor
@ -75,21 +83,8 @@ public class DailyNotificationFetchWorker extends Worker {
int retryCount = inputData . getInt ( KEY_RETRY_COUNT , 0 ) ;
boolean immediate = inputData . getBoolean ( KEY_IMMEDIATE , false ) ;
// Phase 3: Extract TimeSafari coordination data
boolean timesafariCoordination = inputData . getBoolean ( "timesafari_coordination" , false ) ;
long coordinationTimestamp = inputData . getLong ( "coordination_timestamp" , 0 ) ;
String activeDidTracking = inputData . getString ( "active_did_tracking" ) ;
Log . d ( TAG , String . format ( "Phase 3: Fetch parameters - Scheduled: %d, Fetch: %d, Retry: %d, Immediate: %s" ,
Log . d ( TAG , String . format ( "PR2: Fetch parameters - Scheduled: %d, Fetch: %d, Retry: %d, Immediate: %s" ,
scheduledTime , fetchTime , retryCount , immediate ) ) ;
Log . d ( TAG , String . format ( "Phase 3: TimeSafari coordination - Enabled: %s, Timestamp: %d, Tracking: %s" ,
timesafariCoordination , coordinationTimestamp , activeDidTracking ) ) ;
// Phase 3: Check TimeSafari coordination constraints
if ( timesafariCoordination & & ! shouldProceedWithTimeSafariCoordination ( coordinationTimestamp ) ) {
Log . d ( TAG , "Phase 3: Skipping fetch - TimeSafari coordination constraints not met" ) ;
return Result . success ( ) ;
}
// Check if we should proceed with fetch
if ( ! shouldProceedWithFetch ( scheduledTime , fetchTime ) ) {
@ -97,12 +92,12 @@ public class DailyNotificationFetchWorker extends Worker {
return Result . success ( ) ;
}
// Attempt to fetch content with timeout
NotificationContent content = fetchContentWithTimeout ( ) ;
// PR2: Attempt to fetch content using native fetcher SPI
List < NotificationContent > contents = fetchContentWithTimeout ( scheduledTime , fetchTime , immediate ) ;
if ( content ! = null ) {
// Success - save content and schedule notification
handleSuccessfulFetch ( content ) ;
if ( contents ! = null & & ! contents . isEmpty ( ) ) {
// Success - save contents and schedule notifications
handleSuccessfulFetch ( contents ) ;
return Result . success ( ) ;
} else {
@ -153,57 +148,138 @@ public class DailyNotificationFetchWorker extends Worker {
}
/ * *
* Fetch content with timeout handling
* Fetch content with timeout handling using native fetcher SPI ( PR2 )
*
* @return Fetched content or null if failed
* @param scheduledTime When notification is scheduled for
* @param fetchTime When fetch was triggered
* @param immediate Whether this is an immediate fetch
* @return List of fetched notification contents or null if failed
* /
private NotificationContent fetchContentWithTimeout ( ) {
private List < NotificationContent > fetchContentWithTimeout ( long scheduledTime , long fetchTime , boolean immediate ) {
try {
Log . d ( TAG , "Fetching content with timeout: " + FETCH_TIMEOUT_MS + "ms" ) ;
// Get SchedulingPolicy for timeout configuration
SchedulingPolicy policy = getSchedulingPolicy ( ) ;
long fetchTimeoutMs = policy . fetchTimeoutMs ! = null ?
policy . fetchTimeoutMs : FETCH_TIMEOUT_MS_DEFAULT ;
Log . d ( TAG , "PR2: Fetching content with native fetcher SPI, timeout: " + fetchTimeoutMs + "ms" ) ;
// Get native fetcher from static registry
NativeNotificationContentFetcher nativeFetcher = DailyNotificationPlugin . getNativeFetcherStatic ( ) ;
if ( nativeFetcher = = null ) {
Log . w ( TAG , "PR2: Native fetcher not registered, falling back to legacy fetcher" ) ;
// Fallback to legacy fetcher
NotificationContent content = fetcher . fetchContentImmediately ( ) ;
if ( content ! = null ) {
return java . util . Collections . singletonList ( content ) ;
}
return null ;
}
// Use a simple timeout mechanism
// In production, you might use CompletableFuture with timeout
long startTime = System . currentTimeMillis ( ) ;
// Attempt fetch
NotificationContent content = fetcher . fetchContentImmediately ( ) ;
// Create FetchContext
String trigger = immediate ? "manual" :
( fetchTime > 0 ? "prefetch" : "background_work" ) ;
Long scheduledTimeOpt = scheduledTime > 0 ? scheduledTime : null ;
Map < String , Object > metadata = new HashMap < > ( ) ;
metadata . put ( "retryCount" , 0 ) ;
metadata . put ( "immediate" , immediate ) ;
FetchContext context = new FetchContext (
trigger ,
scheduledTimeOpt ,
fetchTime > 0 ? fetchTime : System . currentTimeMillis ( ) ,
metadata
) ;
// Call native fetcher with timeout
CompletableFuture < List < NotificationContent > > future = nativeFetcher . fetchContent ( context ) ;
List < NotificationContent > contents ;
try {
contents = future . get ( fetchTimeoutMs , TimeUnit . MILLISECONDS ) ;
} catch ( TimeoutException e ) {
Log . e ( TAG , "PR2: Native fetcher timeout after " + fetchTimeoutMs + "ms" , e ) ;
return null ;
}
long fetchDuration = System . currentTimeMillis ( ) - startTime ;
if ( content ! = null ) {
Log . d ( TAG , "Content fetched successfully in " + fetchDuration + "ms" ) ;
return content ;
if ( contents ! = null & & ! contents . isEmpty ( ) ) {
Log . i ( TAG , "PR2: Content fetched successfully - " + contents . size ( ) +
" items in " + fetchDuration + "ms" ) ;
// TODO PR2: Record metrics (items_fetched, fetch_duration_ms, fetch_success)
return contents ;
} else {
Log . w ( TAG , "Content fetch returned null after " + fetchDuration + "ms" ) ;
Log . w ( TAG , "PR2: Native fetcher returned empty list after " + fetchDuration + "ms" ) ;
// TODO PR2: Record metrics (fetch_success=false)
return null ;
}
} catch ( Exception e ) {
Log . e ( TAG , "Error during content fetch" , e ) ;
Log . e ( TAG , "PR2: Error during native fetcher call" , e ) ;
// TODO PR2: Record metrics (fetch_fail_class=retryable)
return null ;
}
}
/ * *
* Handle successful content fetch
* Get SchedulingPolicy from SharedPreferences or return default
*
* @param content Successfully fetched content
* @return SchedulingPolicy instance
* /
private void handleSuccessfulFetch ( NotificationContent content ) {
private SchedulingPolicy getSchedulingPolicy ( ) {
try {
Log . d ( TAG , "Handling successful content fetch: " + content . getId ( ) ) ;
// Try to load from SharedPreferences (set via plugin's setPolicy method)
android . content . SharedPreferences prefs = context . getSharedPreferences (
"daily_notification_spi" , Context . MODE_PRIVATE ) ;
// For now, return default policy
// TODO: Deserialize from SharedPreferences in future enhancement
return SchedulingPolicy . createDefault ( ) ;
} catch ( Exception e ) {
Log . w ( TAG , "Error loading SchedulingPolicy, using default" , e ) ;
return SchedulingPolicy . createDefault ( ) ;
}
}
/ * *
* Handle successful content fetch ( PR2 : now handles List < NotificationContent > )
*
* @param contents List of successfully fetched notification contents
* /
private void handleSuccessfulFetch ( List < NotificationContent > contents ) {
try {
Log . d ( TAG , "PR2: Handling successful content fetch - " + contents . size ( ) + " items" ) ;
// Content is already saved by the fetcher
// Update last fetch time
storage . setLastFetchTime ( System . currentTimeMillis ( ) ) ;
// Schedule notification if not already scheduled
scheduleNotificationIfNeeded ( content ) ;
// Save all contents and schedule notifications
int scheduledCount = 0 ;
for ( NotificationContent content : contents ) {
try {
// Save content to storage
storage . saveNotificationContent ( content ) ;
// Schedule notification if not already scheduled
scheduleNotificationIfNeeded ( content ) ;
scheduledCount + + ;
} catch ( Exception e ) {
Log . e ( TAG , "PR2: Error processing notification content: " + content . getId ( ) , e ) ;
}
}
Log . i ( TAG , "Successful fetch handling completed" ) ;
Log . i ( TAG , "PR2: Successful fetch handling completed - " + scheduledCount + "/" +
contents . size ( ) + " notifications scheduled" ) ;
// TODO PR2: Record metrics (items_enqueued=scheduledCount)
} catch ( Exception e ) {
Log . e ( TAG , "Error handling successful fetch" , e ) ;
Log . e ( TAG , "PR2: Error handling successful fetch" , e ) ;
}
}
@ -216,29 +292,23 @@ public class DailyNotificationFetchWorker extends Worker {
* /
private Result handleFailedFetch ( int retryCount , long scheduledTime ) {
try {
Log . d ( TAG , "Phase 2: Handling failed fetch - Retry: " + retryCount ) ;
// Phase 2: Check for TimeSafari special retry triggers
if ( shouldRetryForActiveDidChange ( ) ) {
Log . d ( TAG , "Phase 2: ActiveDid change detected - extending retry quota" ) ;
retryCount = 0 ; // Reset retry count for activeDid change
}
Log . d ( TAG , "PR2: Handling failed fetch - Retry: " + retryCount ) ;
if ( retryCount < MAX_RETRIES_FOR_TIMESAFARI ( ) ) {
// Phase 2: Schedule enhanced retry with activeDid consideration
scheduleRetryWithActiveDidSupport ( retryCount + 1 , scheduledTime ) ;
Log . i ( TAG , "Phase 2: Scheduled retry attempt " + ( retryCount + 1 ) + " with TimeSafari support" ) ;
if ( retryCount < MAX_RETRY_ATTEMPTS ) {
// PR2: Schedule retry with SchedulingPolicy backoff
scheduleRetry ( retryCount + 1 , scheduledTime ) ;
Log . i ( TAG , "PR2: Scheduled retry attempt " + ( retryCount + 1 ) ) ;
return Result . retry ( ) ;
} else {
// Max retries reached - use fallback content
Log . w ( TAG , "Phase 2: Max retries reached, using fallback content" ) ;
useFallbackContentWithActiveDidSupport ( scheduledTime ) ;
Log . w ( TAG , "PR 2: Max retries reached, using fallback content" ) ;
useFallbackContent ( scheduledTime ) ;
return Result . success ( ) ;
}
} catch ( Exception e ) {
Log . e ( TAG , "Phase 2: Error handling failed fetch" , e ) ;
Log . e ( TAG , "PR2 metabolites Error handling failed fetch" , e ) ;
return Result . failure ( ) ;
}
}
@ -279,121 +349,34 @@ public class DailyNotificationFetchWorker extends Worker {
}
/ * *
* Calculate retry delay with exponential backoff
* Calculate retry delay with exponential backoff using SchedulingPolicy ( PR2 )
*
* @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 ) ;
SchedulingPolicy policy = getSchedulingPolicy ( ) ;
SchedulingPolicy . RetryBackoff backoff = policy . retryBackoff ;
// Cap at 1 hour
long maxDelay = 60 * 60 * 1000 ; // 1 hour
return Math . min ( exponentialDelay , maxDelay ) ;
}
// MARK: - Phase 2: TimeSafari ActiveDid Enhancement Methods
/ * *
* Phase 2 : Check if retry is needed due to activeDid change
* /
private boolean shouldRetryForActiveDidChange ( ) {
try {
// Check if activeDid has changed since last fetch attempt
android . content . SharedPreferences prefs = context . getSharedPreferences ( "daily_notification_timesafari" , android . content . Context . MODE_PRIVATE ) ;
long lastFetchAttempt = prefs . getLong ( "lastFetchAttempt" , 0 ) ;
long lastActiveDidChange = prefs . getLong ( "lastActiveDidChange" , 0 ) ;
boolean activeDidChanged = lastActiveDidChange > lastFetchAttempt ;
if ( activeDidChanged ) {
Log . d ( TAG , "Phase 2: ActiveDid change detected in retry logic" ) ;
return true ;
}
return false ;
} catch ( Exception e ) {
Log . e ( TAG , "Phase 2: Error checking activeDid change" , e ) ;
return false ;
}
}
/ * *
* Phase 2 : Get max retries with TimeSafari enhancements
* /
private int MAX_RETRIES_FOR_TIMESAFARI ( ) {
// Base retries + additional for activeDid changes
return MAX_RETRY_ATTEMPTS + 2 ; // Extra retries for TimeSafari integration
}
/ * *
* Phase 2 : Schedule retry with activeDid support
* /
private void scheduleRetryWithActiveDidSupport ( int retryCount , long scheduledTime ) {
try {
Log . d ( TAG , "Phase 2: Scheduling retry attempt " + retryCount + " with TimeSafari support" ) ;
// Store the last fetch attempt time for activeDid change detection
android . content . SharedPreferences prefs = context . getSharedPreferences ( "daily_notification_timesafari" , android . content . Context . MODE_PRIVATE ) ;
prefs . edit ( ) . putLong ( "lastFetchAttempt" , System . currentTimeMillis ( ) ) . apply ( ) ;
// Delegate to original retry logic
scheduleRetry ( retryCount , scheduledTime ) ;
} catch ( Exception e ) {
Log . e ( TAG , "Phase 2: Error scheduling enhanced retry" , e ) ;
// Fallback to original retry logic
scheduleRetry ( retryCount , scheduledTime ) ;
}
}
/ * *
* Phase 2 : Use fallback content with activeDid support
* /
private void useFallbackContentWithActiveDidSupport ( long scheduledTime ) {
try {
Log . d ( TAG , "Phase 2: Using fallback content with TimeSafari support" ) ;
// Generate TimeSafari-aware fallback content
NotificationContent fallbackContent = generateTimeSafariFallbackContent ( ) ;
if ( fallbackContent ! = null ) {
storage . saveNotificationContent ( fallbackContent ) ;
Log . i ( TAG , "Phase 2: TimeSafari fallback content saved" ) ;
} else {
// Fallback to original logic
useFallbackContent ( scheduledTime ) ;
}
} catch ( Exception e ) {
Log . e ( TAG , "Phase 2: Error using enhanced fallback content" , e ) ;
// Fallback to original logic
useFallbackContent ( scheduledTime ) ;
}
}
/ * *
* Phase 2 : Generate TimeSafari - aware fallback content
* /
private NotificationContent generateTimeSafariFallbackContent ( ) {
try {
// Generate fallback content specific to TimeSafari context
NotificationContent content = new NotificationContent ( ) ;
content . setId ( "timesafari_fallback_" + System . currentTimeMillis ( ) ) ;
content . setTitle ( "TimeSafari Update Available" ) ;
content . setBody ( "Your community updates are ready. Tap to view offers, projects, and connections." ) ;
// fetchedAt is set in constructor, no need to set it again
content . setScheduledTime ( System . currentTimeMillis ( ) + 30000 ) ; // 30 seconds from now
return content ;
} catch ( Exception e ) {
Log . e ( TAG , "Phase 2: Error generating TimeSafari fallback content" , e ) ;
return null ;
}
// Calculate exponential delay: minMs * (factor ^ (retryCount - 1))
long baseDelay = backoff . minMs ;
double exponentialMultiplier = Math . pow ( backoff . factor , retryCount - 1 ) ;
long exponentialDelay = ( long ) ( baseDelay * exponentialMultiplier ) ;
// Cap at maxMs
long cappedDelay = Math . min ( exponentialDelay , backoff . maxMs ) ;
// Add jitter: delay * (1 + jitterPct/100 * random(0-1))
Random random = new Random ( ) ;
double jitter = backoff . jitterPct / 100 . 0 * random . nextDouble ( ) ;
long finalDelay = ( long ) ( cappedDelay * ( 1 . 0 + jitter ) ) ;
Log . d ( TAG , "PR2: Calculated retry delay - attempt=" + retryCount +
", base=" + baseDelay + "ms, exponential=" + exponentialDelay + "ms, " +
"capped=" + cappedDelay + "ms, jitter=" + String . format ( "%.1f%%" , jitter * 100 ) +
", final=" + finalDelay + "ms" ) ;
return finalDelay ;
}
/ * *
@ -513,127 +496,4 @@ public class DailyNotificationFetchWorker extends Worker {
Log . e ( TAG , "Error checking/scheduling notification" , e ) ;
}
}
// MARK: - Phase 3: TimeSafari Coordination Methods
/ * *
* Phase 3 : Check if background work should proceed with TimeSafari coordination
* /
private boolean shouldProceedWithTimeSafariCoordination ( long coordinationTimestamp ) {
try {
Log . d ( TAG , "Phase 3: Checking TimeSafari coordination constraints" ) ;
// Check coordination freshness - must be within 5 minutes
long maxCoordinationAge = 5 * 60 * 1000 ; // 5 minutes
long coordinationAge = System . currentTimeMillis ( ) - coordinationTimestamp ;
if ( coordinationAge > maxCoordinationAge ) {
Log . w ( TAG , "Phase 3: Coordination data too old (" + coordinationAge + "ms) - allowing fetch" ) ;
return true ;
}
// Check if app coordination is proactively paused
android . content . SharedPreferences prefs = context . getSharedPreferences (
"daily_notification_timesafari" , Context . MODE_PRIVATE ) ;
boolean coordinationPaused = prefs . getBoolean ( "coordinationPaused" , false ) ;
long lastCoordinationPaused = prefs . getLong ( "lastCoordinationPaused" , 0 ) ;
boolean recentlyPaused = ( System . currentTimeMillis ( ) - lastCoordinationPaused ) < 30000 ; // 30 seconds
if ( coordinationPaused & & recentlyPaused ) {
Log . d ( TAG , "Phase 3: Coordination proactively paused by TimeSafari - deferring fetch" ) ;
return false ;
}
// Check if activeDid has changed since coordination
long lastActiveDidChange = prefs . getLong ( "lastActiveDidChange" , 0 ) ;
if ( lastActiveDidChange > coordinationTimestamp ) {
Log . d ( TAG , "Phase 3: ActiveDid changed after coordination - requiring re-coordination" ) ;
return false ;
}
// Check battery optimization status
if ( isDeviceInLowPowerMode ( ) ) {
Log . d ( TAG , "Phase 3: Device in low power mode - deferring fetch" ) ;
return false ;
}
Log . d ( TAG , "Phase 3: TimeSafari coordination constraints satisfied" ) ;
return true ;
} catch ( Exception e ) {
Log . e ( TAG , "Phase 3: Error checking TimeSafari coordination" , e ) ;
return true ; // Default to allowing fetch on error
}
}
/ * *
* Phase 3 : Check if device is in low power mode
* /
private boolean isDeviceInLowPowerMode ( ) {
try {
android . os . PowerManager powerManager =
( android . os . PowerManager ) context . getSystemService ( Context . POWER_SERVICE ) ;
if ( powerManager ! = null ) {
boolean isLowPowerMode = powerManager . isPowerSaveMode ( ) ;
Log . d ( TAG , "Phase 3: Device low power mode: " + isLowPowerMode ) ;
return isLowPowerMode ;
}
return false ;
} catch ( Exception e ) {
Log . e ( TAG , "Phase 3: Error checking low power mode" , e ) ;
return false ;
}
}
/ * *
* Phase 3 : Report coordination success to TimeSafari
* /
private void reportCoordinationSuccess ( String operation , long durationMs , boolean authUsed , String activeDid ) {
try {
Log . d ( TAG , "Phase 3: Reporting coordination success: " + operation ) ;
android . content . SharedPreferences prefs = context . getSharedPreferences (
"daily_notification_timesafari" , Context . MODE_PRIVATE ) ;
prefs . edit ( )
. putLong ( "lastCoordinationSuccess_" + operation , System . currentTimeMillis ( ) )
. putLong ( "lastCoordinationDuration_" + operation , durationMs )
. putBoolean ( "lastCoordinationUsed_" + operation , authUsed )
. putString ( "lastCoordinationActiveDid_" + operation , activeDid )
. apply ( ) ;
Log . d ( TAG , "Phase 3: Coordination success reported - " + operation + " in " + durationMs + "ms" ) ;
} catch ( Exception e ) {
Log . e ( TAG , "Phase 3: Error reporting coordination success" , e ) ;
}
}
/ * *
* Phase 3 : Report coordination failure to TimeSafari
* /
private void reportCoordinationFailed ( String operation , String error , long durationMs , boolean authUsed ) {
try {
Log . d ( TAG , "Phase 3: Reporting coordination failure: " + operation + " - " + error ) ;
android . content . SharedPreferences prefs = context . getSharedPreferences (
"daily_notification_timesafari" , Context . MODE_PRIVATE ) ;
prefs . edit ( )
. putLong ( "lastCoordinationFailure_" + operation , System . currentTimeMillis ( ) )
. putString ( "lastCoordinationError_" + operation , error )
. putLong ( "lastCoordinationFailureDuration_" + operation , durationMs )
. putBoolean ( "lastCoordinationFailedUsed_" + operation , authUsed )
. apply ( ) ;
Log . d ( TAG , "Phase 3: Coordination failure reported - " + operation ) ;
} catch ( Exception e ) {
Log . e ( TAG , "Phase 3: Error reporting coordination failure" , e ) ;
}
}
}