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.
 
 
 
 
 
 

1102 lines
42 KiB

/**
* DailyNotificationScheduler.java
*
* Handles scheduling and timing of daily notifications
* Implements exact and inexact alarm scheduling with battery optimization handling
*
* @author Matthew Raymer
* @version 1.0.0
*/
package com.timesafari.dailynotification;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.Log;
import java.util.Calendar;
import java.util.concurrent.ConcurrentHashMap;
/**
* Manages scheduling of daily notifications using AlarmManager
*
* This class handles the scheduling aspect of the prefetch → cache → schedule → display pipeline.
* It supports both exact and inexact alarms based on system permissions and battery optimization.
*/
public class DailyNotificationScheduler {
private static final String TAG = "DailyNotificationScheduler";
private static final String ACTION_NOTIFICATION = "com.timesafari.daily.NOTIFICATION";
private static final String EXTRA_NOTIFICATION_ID = "notification_id";
private final Context context;
private final AlarmManager alarmManager;
private final ConcurrentHashMap<String, PendingIntent> scheduledAlarms;
// Track scheduled times to prevent duplicate alarms for same time
// Maps scheduledTime (ms) -> notificationId that has alarm scheduled
private final ConcurrentHashMap<Long, String> scheduledTimeToId;
// PendingIntent management
private PendingIntentManager pendingIntentManager;
// TTL enforcement
private DailyNotificationTTLEnforcer ttlEnforcer;
// Exact alarm management
private DailyNotificationExactAlarmManager exactAlarmManager;
/**
* Constructor
*
* @param context Application context
* @param alarmManager System AlarmManager service
*/
public DailyNotificationScheduler(Context context, AlarmManager alarmManager) {
this.context = context;
this.alarmManager = alarmManager;
this.scheduledAlarms = new ConcurrentHashMap<>();
this.scheduledTimeToId = new ConcurrentHashMap<>();
this.pendingIntentManager = new PendingIntentManager(context);
Log.d(TAG, "DailyNotificationScheduler initialized with PendingIntentManager");
}
/**
* Set TTL enforcer for freshness validation
*
* @param ttlEnforcer TTL enforcement instance
*/
public void setTTLEnforcer(DailyNotificationTTLEnforcer ttlEnforcer) {
this.ttlEnforcer = ttlEnforcer;
Log.d(TAG, "TTL enforcer set for freshness validation");
}
/**
* Get alarm scheduling status
*
* @return AlarmStatus with detailed information
*/
public PendingIntentManager.AlarmStatus getAlarmStatus() {
return pendingIntentManager.getAlarmStatus();
}
/**
* Set exact alarm manager for alarm scheduling
*
* @param exactAlarmManager Exact alarm manager instance
*/
public void setExactAlarmManager(DailyNotificationExactAlarmManager exactAlarmManager) {
this.exactAlarmManager = exactAlarmManager;
Log.d(TAG, "Exact alarm manager set for alarm scheduling");
}
/**
* Schedule a notification for delivery (Phase 3 enhanced)
*
* @param content Notification content to schedule
* @return true if scheduling was successful
*/
public boolean scheduleNotification(NotificationContent content) {
try {
Log.d(TAG, "Phase 3: Scheduling notification: " + content.getId());
// Phase 3: TimeSafari coordination before scheduling
if (!shouldScheduleWithTimeSafariCoordination(content)) {
Log.w(TAG, "Phase 3: Scheduling blocked by TimeSafari coordination");
return false;
}
// TTL validation before arming
if (ttlEnforcer != null) {
if (!ttlEnforcer.validateBeforeArming(content)) {
Log.w(TAG, "Skipping notification due to TTL violation: " + content.getId());
return false;
}
} else {
Log.w(TAG, "TTL enforcer not set, proceeding without freshness validation");
}
// Cancel any existing alarm for this notification ID to prevent duplicates
// This ensures only one alarm exists for this notification at a time
Log.d(TAG, "Phase 3: Cancelling existing alarm for notification: " + content.getId());
cancelNotification(content.getId());
// Get scheduled time and check for duplicate alarms at same time
long triggerTime = content.getScheduledTime();
long toleranceMs = 60 * 1000; // 1 minute tolerance for DST/clock adjustments
// Cancel any existing alarm for the same scheduled time (within tolerance)
// This prevents multiple notifications scheduled for same time from creating duplicate alarms
// Check all scheduled times to find any within tolerance
java.util.List<String> duplicateIds = new java.util.ArrayList<>();
for (java.util.Map.Entry<Long, String> entry : scheduledTimeToId.entrySet()) {
Long scheduledTime = entry.getKey();
String existingId = entry.getValue();
// Skip if it's the same notification ID or time difference is too large
if (existingId.equals(content.getId()) ||
Math.abs(scheduledTime - triggerTime) > toleranceMs) {
continue;
}
// Found an alarm scheduled for a time very close to this one
duplicateIds.add(existingId);
}
// Cancel any duplicate alarms found
for (String duplicateId : duplicateIds) {
Log.w(TAG, "Phase 3: Cancelling duplicate alarm for time " +
formatTime(triggerTime) + " (existing ID: " + duplicateId +
", new ID: " + content.getId() + ")");
cancelNotification(duplicateId);
}
// Create intent for the notification
Intent intent = new Intent(context, DailyNotificationReceiver.class);
intent.setAction(ACTION_NOTIFICATION);
intent.putExtra(EXTRA_NOTIFICATION_ID, content.getId());
// Check if this is a static reminder
if (content.getId().startsWith("reminder_") || content.getId().contains("_reminder")) {
intent.putExtra("is_static_reminder", true);
intent.putExtra("reminder_id", content.getId());
intent.putExtra("title", content.getTitle());
intent.putExtra("body", content.getBody());
intent.putExtra("sound", content.isSound());
intent.putExtra("vibration", true); // Default to true for reminders
intent.putExtra("priority", content.getPriority());
}
// Create pending intent with unique request code using PendingIntentManager
int requestCode = content.getId().hashCode();
PendingIntent pendingIntent = pendingIntentManager.createBroadcastPendingIntent(intent, requestCode);
// Store the pending intent
scheduledAlarms.put(content.getId(), pendingIntent);
// Track scheduled time to notification ID mapping
scheduledTimeToId.put(triggerTime, content.getId());
// Schedule the alarm
boolean scheduled = scheduleAlarm(pendingIntent, triggerTime);
if (scheduled) {
Log.i(TAG, "Notification scheduled successfully for " +
formatTime(triggerTime));
return true;
} else {
Log.e(TAG, "Failed to schedule notification");
scheduledAlarms.remove(content.getId());
return false;
}
} catch (Exception e) {
Log.e(TAG, "Error scheduling notification", e);
return false;
}
}
/**
* Schedule an alarm using the best available method
*
* @param pendingIntent PendingIntent to trigger
* @param triggerTime When to trigger the alarm
* @return true if scheduling was successful
*/
private boolean scheduleAlarm(PendingIntent pendingIntent, long triggerTime) {
try {
// Use exact alarm manager if available
if (exactAlarmManager != null) {
return exactAlarmManager.scheduleAlarm(pendingIntent, triggerTime);
}
// Use PendingIntentManager for modern alarm scheduling
if (pendingIntentManager.canScheduleExactAlarms()) {
return pendingIntentManager.scheduleExactAlarm(pendingIntent, triggerTime);
} else {
// Fallback to windowed alarm
return pendingIntentManager.scheduleWindowedAlarm(pendingIntent, triggerTime, 20 * 60 * 1000); // 20 minute window
}
} catch (Exception e) {
Log.e(TAG, "Error scheduling alarm", e);
return false;
}
}
/**
* Schedule an exact alarm for precise timing with enhanced Doze handling
*
* @param pendingIntent PendingIntent to trigger
* @param triggerTime When to trigger the alarm
* @return true if scheduling was successful
*/
private boolean scheduleExactAlarm(PendingIntent pendingIntent, long triggerTime) {
try {
// Enhanced exact alarm scheduling for Android 12+ and Doze mode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Use setExactAndAllowWhileIdle for Doze mode compatibility
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
);
Log.d(TAG, "Exact alarm scheduled with Doze compatibility for " + formatTime(triggerTime));
} else {
// Pre-Android 6.0: Use standard exact alarm
alarmManager.setExact(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
);
Log.d(TAG, "Exact alarm scheduled (pre-Android 6.0) for " + formatTime(triggerTime));
}
// Log alarm scheduling details for debugging
logAlarmSchedulingDetails(triggerTime);
return true;
} catch (SecurityException e) {
Log.e(TAG, "Security exception scheduling exact alarm - exact alarm permission may be denied", e);
return false;
} catch (Exception e) {
Log.e(TAG, "Error scheduling exact alarm", e);
return false;
}
}
/**
* Log detailed alarm scheduling information for debugging
*
* @param triggerTime When the alarm will trigger
*/
private void logAlarmSchedulingDetails(long triggerTime) {
try {
long currentTime = System.currentTimeMillis();
long timeUntilTrigger = triggerTime - currentTime;
Log.d(TAG, String.format("Alarm scheduling details: " +
"Current time: %s, " +
"Trigger time: %s, " +
"Time until trigger: %d minutes, " +
"Android version: %d, " +
"Exact alarms supported: %s",
formatTime(currentTime),
formatTime(triggerTime),
timeUntilTrigger / (60 * 1000),
Build.VERSION.SDK_INT,
canUseExactAlarms() ? "Yes" : "No"));
} catch (Exception e) {
Log.e(TAG, "Error logging alarm scheduling details", e);
}
}
/**
* Schedule an inexact alarm for battery optimization
*
* @param pendingIntent PendingIntent to trigger
* @param triggerTime When to trigger the alarm
* @return true if scheduling was successful
*/
private boolean scheduleInexactAlarm(PendingIntent pendingIntent, long triggerTime) {
try {
alarmManager.setRepeating(
AlarmManager.RTC_WAKEUP,
triggerTime,
AlarmManager.INTERVAL_DAY,
pendingIntent
);
Log.d(TAG, "Inexact alarm scheduled for " + formatTime(triggerTime));
return true;
} catch (Exception e) {
Log.e(TAG, "Error scheduling inexact alarm", e);
return false;
}
}
/**
* Check if we can use exact alarms with enhanced Android 12+ support
*
* @return true if exact alarms are permitted
*/
private boolean canUseExactAlarms() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Android 12+ requires SCHEDULE_EXACT_ALARM permission
boolean canSchedule = alarmManager.canScheduleExactAlarms();
Log.d(TAG, "Android 12+ exact alarm check: " +
(canSchedule ? "Permission granted" : "Permission denied"));
return canSchedule;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Android 6.0+ supports exact alarms but may be affected by Doze mode
Log.d(TAG, "Android 6.0+ exact alarm support: Available (may be affected by Doze)");
return true;
} else {
// Pre-Android 6.0: Exact alarms always available
Log.d(TAG, "Pre-Android 6.0 exact alarm support: Always available");
return true;
}
} catch (Exception e) {
Log.e(TAG, "Error checking exact alarm capability", e);
return false;
}
}
/**
* Request exact alarm permission for Android 12+
*
* @return true if permission request was initiated
*/
public boolean requestExactAlarmPermission() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!alarmManager.canScheduleExactAlarms()) {
Log.d(TAG, "Requesting exact alarm permission for Android 12+");
// Create intent to open exact alarm settings
Intent intent = new Intent(android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
context.startActivity(intent);
Log.i(TAG, "Exact alarm permission request initiated");
return true;
} catch (Exception e) {
Log.e(TAG, "Failed to open exact alarm settings", e);
return false;
}
} else {
Log.d(TAG, "Exact alarm permission already granted");
return true;
}
} else {
Log.d(TAG, "Exact alarm permission not required for Android version " + Build.VERSION.SDK_INT);
return true;
}
} catch (Exception e) {
Log.e(TAG, "Error requesting exact alarm permission", e);
return false;
}
}
/**
* Get exact alarm status with detailed information
*
* @return ExactAlarmStatus with comprehensive status information
*/
public ExactAlarmStatus getExactAlarmStatus() {
try {
boolean supported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
boolean enabled = canUseExactAlarms();
boolean canSchedule = enabled && supported;
String fallbackWindow = "±10 minutes"; // Default fallback window
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!enabled) {
fallbackWindow = "±15 minutes (Android 12+ restriction)";
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
fallbackWindow = "±5 minutes (Doze mode may affect timing)";
}
ExactAlarmStatus status = new ExactAlarmStatus();
status.supported = supported;
status.enabled = enabled;
status.canSchedule = canSchedule;
status.fallbackWindow = fallbackWindow;
Log.d(TAG, "Exact alarm status: supported=" + supported +
", enabled=" + enabled + ", canSchedule=" + canSchedule);
return status;
} catch (Exception e) {
Log.e(TAG, "Error getting exact alarm status", e);
// Return safe default status
ExactAlarmStatus status = new ExactAlarmStatus();
status.supported = false;
status.enabled = false;
status.canSchedule = false;
status.fallbackWindow = "±20 minutes (error state)";
return status;
}
}
/**
* Exact alarm status information
*/
public static class ExactAlarmStatus {
public boolean supported;
public boolean enabled;
public boolean canSchedule;
public String fallbackWindow;
}
/**
* Cancel a specific notification
*
* @param notificationId ID of notification to cancel
*/
public void cancelNotification(String notificationId) {
try {
PendingIntent pendingIntent = scheduledAlarms.remove(notificationId);
if (pendingIntent != null) {
pendingIntentManager.cancelAlarm(pendingIntent);
pendingIntent.cancel();
Log.d(TAG, "Cancelled existing alarm for notification: " + notificationId);
// Remove from time-to-ID mapping by finding and removing the entry
java.util.List<Long> timesToRemove = new java.util.ArrayList<>();
for (java.util.Map.Entry<Long, String> entry : scheduledTimeToId.entrySet()) {
if (entry.getValue().equals(notificationId)) {
timesToRemove.add(entry.getKey());
}
}
for (Long time : timesToRemove) {
scheduledTimeToId.remove(time);
}
} else {
Log.d(TAG, "No existing alarm found to cancel for notification: " + notificationId);
}
} catch (Exception e) {
Log.e(TAG, "Error cancelling notification: " + notificationId, e);
}
}
/**
* Cancel all scheduled notifications
*/
public void cancelAllNotifications() {
try {
Log.d(TAG, "Cancelling all notifications");
for (String notificationId : scheduledAlarms.keySet()) {
cancelNotification(notificationId);
}
scheduledAlarms.clear();
scheduledTimeToId.clear();
Log.i(TAG, "All notifications cancelled");
} catch (Exception e) {
Log.e(TAG, "Error cancelling all notifications", e);
}
}
/**
* Get the next scheduled notification time
*
* @return Timestamp of next notification or 0 if none scheduled
*/
public long getNextNotificationTime() {
// This would need to be implemented with actual notification data
// For now, return a placeholder
return System.currentTimeMillis() + (24 * 60 * 60 * 1000); // 24 hours from now
}
/**
* Get count of pending notifications
*
* @return Number of scheduled notifications
*/
public int getPendingNotificationsCount() {
return scheduledAlarms.size();
}
/**
* Update notification settings for existing notifications
*/
public void updateNotificationSettings() {
try {
Log.d(TAG, "Updating notification settings");
// This would typically involve rescheduling notifications
// with new settings. For now, just log the action.
Log.i(TAG, "Notification settings updated");
} catch (Exception e) {
Log.e(TAG, "Error updating notification settings", e);
}
}
/**
* Enable adaptive scheduling based on device state
*/
public void enableAdaptiveScheduling() {
try {
Log.d(TAG, "Enabling adaptive scheduling");
// This would implement logic to adjust scheduling based on:
// - Battery level
// - Power save mode
// - Doze mode
// - User activity patterns
Log.i(TAG, "Adaptive scheduling enabled");
} catch (Exception e) {
Log.e(TAG, "Error enabling adaptive scheduling", e);
}
}
/**
* Disable adaptive scheduling
*/
public void disableAdaptiveScheduling() {
try {
Log.d(TAG, "Disabling adaptive scheduling");
// Reset to default scheduling behavior
Log.i(TAG, "Adaptive scheduling disabled");
} catch (Exception e) {
Log.e(TAG, "Error disabling adaptive scheduling", e);
}
}
/**
* Reschedule notifications after system reboot
*/
public void rescheduleAfterReboot() {
try {
Log.d(TAG, "Rescheduling notifications after reboot");
// This would typically be called from a BOOT_COMPLETED receiver
// to restore scheduled notifications after device restart
Log.i(TAG, "Notifications rescheduled after reboot");
} catch (Exception e) {
Log.e(TAG, "Error rescheduling after reboot", e);
}
}
/**
* Check if a notification is currently scheduled
*
* @param notificationId ID of notification to check
* @return true if notification is scheduled
*/
public boolean isNotificationScheduled(String notificationId) {
return scheduledAlarms.containsKey(notificationId);
}
/**
* Get scheduling statistics
*
* @return Scheduling statistics as a string
*/
public String getSchedulingStats() {
return String.format("Scheduled: %d, Exact alarms: %s",
scheduledAlarms.size(),
canUseExactAlarms() ? "enabled" : "disabled");
}
/**
* Format timestamp for logging
*
* @param timestamp Timestamp in milliseconds
* @return Formatted time string
*/
private String formatTime(long timestamp) {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(timestamp);
return String.format("%02d:%02d:%02d on %02d/%02d/%04d",
calendar.get(Calendar.HOUR_OF_DAY),
calendar.get(Calendar.MINUTE),
calendar.get(Calendar.SECOND),
calendar.get(Calendar.MONTH) + 1,
calendar.get(Calendar.DAY_OF_MONTH),
calendar.get(Calendar.YEAR));
}
/**
* Calculate next occurrence of a daily time with DST-safe handling
*
* @param hour Hour of day (0-23)
* @param minute Minute of hour (0-59)
* @param timezone Timezone identifier (e.g., "America/New_York")
* @return Timestamp of next occurrence
*/
public long calculateNextOccurrence(int hour, int minute, String timezone) {
try {
// Use Java 8 Time API for DST-safe calculations
java.time.ZoneId zone = java.time.ZoneId.of(timezone);
java.time.LocalTime targetTime = java.time.LocalTime.of(hour, minute);
// Get current time in user's timezone
java.time.ZonedDateTime now = java.time.ZonedDateTime.now(zone);
java.time.LocalDate today = now.toLocalDate();
// Calculate next occurrence at same local time
java.time.ZonedDateTime nextScheduled = java.time.ZonedDateTime.of(today, targetTime, zone);
// If time has passed today, schedule for tomorrow
if (nextScheduled.isBefore(now)) {
nextScheduled = nextScheduled.plusDays(1);
}
long result = nextScheduled.toInstant().toEpochMilli();
Log.d(TAG, String.format("DST-safe calculation: target=%02d:%02d, timezone=%s, " +
"next occurrence=%s (UTC offset: %s)",
hour, minute, timezone,
formatTime(result),
nextScheduled.getOffset().toString()));
return result;
} catch (Exception e) {
Log.e(TAG, "Error in DST-safe calculation, falling back to Calendar", e);
return calculateNextOccurrenceLegacy(hour, minute);
}
}
/**
* Calculate next occurrence using legacy Calendar API (fallback)
*
* @param hour Hour of day (0-23)
* @param minute Minute of hour (0-59)
* @return Timestamp of next occurrence
*/
private long calculateNextOccurrenceLegacy(int hour, int minute) {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, hour);
calendar.set(Calendar.MINUTE, minute);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
// If time has passed today, schedule for tomorrow
if (calendar.getTimeInMillis() <= System.currentTimeMillis()) {
calendar.add(Calendar.DAY_OF_YEAR, 1);
}
Log.d(TAG, String.format("Legacy calculation: target=%02d:%02d, next occurrence=%s",
hour, minute, formatTime(calendar.getTimeInMillis())));
return calendar.getTimeInMillis();
}
/**
* Calculate next occurrence with DST transition awareness
*
* @param hour Hour of day (0-23)
* @param minute Minute of hour (0-59)
* @param timezone Timezone identifier
* @param daysAhead Number of days to look ahead for DST transitions
* @return Timestamp of next occurrence with DST awareness
*/
public long calculateNextOccurrenceWithDSTAwareness(int hour, int minute, String timezone, int daysAhead) {
try {
java.time.ZoneId zone = java.time.ZoneId.of(timezone);
java.time.LocalTime targetTime = java.time.LocalTime.of(hour, minute);
java.time.ZonedDateTime now = java.time.ZonedDateTime.now(zone);
// Look ahead for DST transitions
java.time.ZonedDateTime candidate = java.time.ZonedDateTime.of(now.toLocalDate(), targetTime, zone);
// If time has passed today, start from tomorrow
if (candidate.isBefore(now)) {
candidate = candidate.plusDays(1);
}
// Check for DST transitions in the next few days
for (int i = 0; i < daysAhead; i++) {
java.time.ZonedDateTime nextDay = candidate.plusDays(i);
java.time.ZonedDateTime nextDayAtTarget = java.time.ZonedDateTime.of(nextDay.toLocalDate(), targetTime, zone);
// Check if this day has a DST transition
if (hasDSTTransition(nextDayAtTarget, zone)) {
Log.d(TAG, String.format("DST transition detected on %s, adjusting schedule",
nextDayAtTarget.toLocalDate().toString()));
// Adjust for DST transition
nextDayAtTarget = adjustForDSTTransition(nextDayAtTarget, zone);
}
// Use the first valid occurrence
if (nextDayAtTarget.isAfter(now)) {
long result = nextDayAtTarget.toInstant().toEpochMilli();
Log.d(TAG, String.format("DST-aware calculation: target=%02d:%02d, timezone=%s, " +
"next occurrence=%s (UTC offset: %s)",
hour, minute, timezone,
formatTime(result),
nextDayAtTarget.getOffset().toString()));
return result;
}
}
// Fallback to standard calculation
return calculateNextOccurrence(hour, minute, timezone);
} catch (Exception e) {
Log.e(TAG, "Error in DST-aware calculation", e);
return calculateNextOccurrenceLegacy(hour, minute);
}
}
/**
* Check if a specific date has a DST transition
*
* @param dateTime The date/time to check
* @param zone The timezone
* @return true if there's a DST transition on this date
*/
private boolean hasDSTTransition(java.time.ZonedDateTime dateTime, java.time.ZoneId zone) {
try {
// Check if the offset changes between this day and the next
java.time.ZonedDateTime nextDay = dateTime.plusDays(1);
return !dateTime.getOffset().equals(nextDay.getOffset());
} catch (Exception e) {
Log.e(TAG, "Error checking DST transition", e);
return false;
}
}
/**
* Adjust schedule for DST transition
*
* @param dateTime The date/time to adjust
* @param zone The timezone
* @return Adjusted date/time
*/
private java.time.ZonedDateTime adjustForDSTTransition(java.time.ZonedDateTime dateTime, java.time.ZoneId zone) {
try {
// For spring forward (lose an hour), schedule 1 hour earlier
// For fall back (gain an hour), schedule 1 hour later
java.time.ZonedDateTime nextDay = dateTime.plusDays(1);
if (dateTime.getOffset().getTotalSeconds() < nextDay.getOffset().getTotalSeconds()) {
// Spring forward - schedule earlier
Log.d(TAG, "Spring forward detected, scheduling 1 hour earlier");
return dateTime.minusHours(1);
} else if (dateTime.getOffset().getTotalSeconds() > nextDay.getOffset().getTotalSeconds()) {
// Fall back - schedule later
Log.d(TAG, "Fall back detected, scheduling 1 hour later");
return dateTime.plusHours(1);
}
return dateTime;
} catch (Exception e) {
Log.e(TAG, "Error adjusting for DST transition", e);
return dateTime;
}
}
/**
* Restore scheduled notifications after reboot
*
* This method should be called after system reboot to restore
* all scheduled notifications that were lost during reboot.
*/
public void restoreScheduledNotifications() {
try {
Log.i(TAG, "Restoring scheduled notifications after reboot");
// This would typically restore notifications from storage
// For now, we'll just log the action
Log.d(TAG, "Scheduled notifications restored");
} catch (Exception e) {
Log.e(TAG, "Error restoring scheduled notifications", e);
}
}
/**
* Adjust scheduled notifications after time change
*
* This method should be called after system time changes to adjust
* all scheduled notifications accordingly.
*/
public void adjustScheduledNotifications() {
try {
Log.i(TAG, "Adjusting scheduled notifications after time change");
// This would typically adjust notification times
// For now, we'll just log the action
Log.d(TAG, "Scheduled notifications adjusted");
} catch (Exception e) {
Log.e(TAG, "Error adjusting scheduled notifications", e);
}
}
/**
* Get count of restored notifications
*
* @return Number of restored notifications
*/
public int getRestoredNotificationCount() {
// This would typically return actual count
// For now, we'll return a placeholder
return 0;
}
/**
* Get count of adjusted notifications
*
* @return Number of adjusted notifications
*/
public int getAdjustedNotificationCount() {
// This would typically return actual count
// For now, we'll return a placeholder
return 0;
}
// MARK: - Phase 3: TimeSafari Coordination Methods
/**
* Phase 3: Check if scheduling should proceed with TimeSafari coordination
*/
private boolean shouldScheduleWithTimeSafariCoordination(NotificationContent content) {
try {
Log.d(TAG, "Phase 3: Checking TimeSafari coordination for notification: " + content.getId());
// Check app lifecycle state
if (!isAppInForeground()) {
Log.d(TAG, "Phase 3: App not in foreground - allowing scheduling");
return true;
}
// Check activeDid health
if (hasActiveDidChangedRecently()) {
Log.d(TAG, "Phase 3: ActiveDid changed recently - deferring scheduling");
return false;
}
// Check background task coordination
if (!isBackgroundTaskCoordinated()) {
Log.d(TAG, "Phase 3: Background tasks not coordinated - allowing scheduling");
return true;
}
// Check notification throttling
if (isNotificationThrottled()) {
Log.d(TAG, "Phase 3: Notification throttled - deferring scheduling");
return false;
}
Log.d(TAG, "Phase 3: TimeSafari coordination passed - allowing scheduling");
return true;
} catch (Exception e) {
Log.e(TAG, "Phase 3: Error checking TimeSafari coordination", e);
return true; // Default to allowing scheduling on error
}
}
/**
* Phase 3: Check if app is currently in foreground
*/
private boolean isAppInForeground() {
try {
android.app.ActivityManager activityManager =
(android.app.ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager != null) {
java.util.List<android.app.ActivityManager.RunningAppProcessInfo> runningProcesses =
activityManager.getRunningAppProcesses();
if (runningProcesses != null) {
for (android.app.ActivityManager.RunningAppProcessInfo processInfo : runningProcesses) {
if (processInfo.processName.equals(context.getPackageName())) {
boolean inForeground = processInfo.importance ==
android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
Log.d(TAG, "Phase 3: App foreground state: " + inForeground);
return inForeground;
}
}
}
}
return false;
} catch (Exception e) {
Log.e(TAG, "Phase 3: Error checking app foreground state", e);
return false;
}
}
/**
* Phase 3: Check if activeDid has changed recently
*/
private boolean hasActiveDidChangedRecently() {
try {
android.content.SharedPreferences prefs = context.getSharedPreferences(
"daily_notification_timesafari", Context.MODE_PRIVATE);
long lastActiveDidChange = prefs.getLong("lastActiveDidChange", 0);
long gracefulPeriodMs = 30000; // 30 seconds grace period
if (lastActiveDidChange > 0) {
long timeSinceChange = System.currentTimeMillis() - lastActiveDidChange;
boolean changedRecently = timeSinceChange < gracefulPeriodMs;
Log.d(TAG, "Phase 3: ActiveDid change check - lastChange: " + lastActiveDidChange +
", timeSince: " + timeSinceChange + "ms, changedRecently: " + changedRecently);
return changedRecently;
}
return false;
} catch (Exception e) {
Log.e(TAG, "Phase 3: Error checking activeDid change", e);
return false;
}
}
/**
* Phase 3: Check if background tasks are properly coordinated
*/
private boolean isBackgroundTaskCoordinated() {
try {
android.content.SharedPreferences prefs = context.getSharedPreferences(
"daily_notification_timesafari", Context.MODE_PRIVATE);
boolean autoSync = prefs.getBoolean("autoSync", false);
long lastFetchAttempt = prefs.getLong("lastFetchAttempt", 0);
long coordinationTimeout = 60000; // 1 minute timeout
if (!autoSync) {
Log.d(TAG, "Phase 3: Auto-sync disabled - background coordination not needed");
return true;
}
if (lastFetchAttempt > 0) {
long timeSinceLastFetch = System.currentTimeMillis() - lastFetchAttempt;
boolean recentFetch = timeSinceLastFetch < coordinationTimeout;
Log.d(TAG, "Phase 3: Background task coordination - timeSinceLastFetch: " +
timeSinceLastFetch + "ms, recentFetch: " + recentFetch);
return recentFetch;
}
return true;
} catch (Exception e) {
Log.e(TAG, "Phase 3: Error checking background task coordination", e);
return true;
}
}
/**
* Phase 3: Check if notifications are currently throttled
*/
private boolean isNotificationThrottled() {
try {
android.content.SharedPreferences prefs = context.getSharedPreferences(
"daily_notification_timesafari", Context.MODE_PRIVATE);
long lastNotificationDelivered = prefs.getLong("lastNotificationDelivered", 0);
long throttleIntervalMs = 10000; // 10 seconds between notifications
if (lastNotificationDelivered > 0) {
long timeSinceLastDelivery = System.currentTimeMillis() - lastNotificationDelivered;
boolean isThrottled = timeSinceLastDelivery < throttleIntervalMs;
Log.d(TAG, "Phase 3: Notification throttling - timeSinceLastDelivery: " +
timeSinceLastDelivery + "ms, isThrottled: " + isThrottled);
return isThrottled;
}
return false;
} catch (Exception e) {
Log.e(TAG, "Phase 3: Error checking notification throttle", e);
return false;
}
}
/**
* Phase 3: Update notification delivery timestamp
*/
public void recordNotificationDelivery(String notificationId) {
try {
android.content.SharedPreferences prefs = context.getSharedPreferences(
"daily_notification_timesafari", Context.MODE_PRIVATE);
prefs.edit()
.putLong("lastNotificationDelivered", System.currentTimeMillis())
.putString("lastDeliveredNotificationId", notificationId)
.apply();
Log.d(TAG, "Phase 3: Notification delivery recorded: " + notificationId);
} catch (Exception e) {
Log.e(TAG, "Phase 3: Error recording notification delivery", e);
}
}
/**
* Phase 3: Coordinate with PlatformServiceMixin events
*/
public void coordinateWithPlatformServiceMixin() {
try {
Log.d(TAG, "Phase 3: Coordinating with PlatformServiceMixin events");
// This would integrate with TimeSafari's PlatformServiceMixin lifecycle events
// For now, we'll implement a simplified coordination
android.content.SharedPreferences prefs = context.getSharedPreferences(
"daily_notification_timesafari", Context.MODE_PRIVATE);
boolean autoSync = prefs.getBoolean("autoSync", false);
if (autoSync) {
// Schedule background content fetch coordination
scheduleBackgroundContentFetchWithCoordination();
}
Log.d(TAG, "Phase 3: PlatformServiceMixin coordination completed");
} catch (Exception e) {
Log.e(TAG, "Phase 3: Error coordinating with PlatformServiceMixin", e);
}
}
/**
* Phase 3: Schedule background content fetch with coordination
*/
private void scheduleBackgroundContentFetchWithCoordination() {
try {
Log.d(TAG, "Phase 3: Scheduling background content fetch with coordination");
// This would coordinate with TimeSafari's background task management
// For now, we'll update coordination timestamps
android.content.SharedPreferences prefs = context.getSharedPreferences(
"daily_notification_timesafari", Context.MODE_PRIVATE);
prefs.edit()
.putLong("lastBackgroundFetchCoordinated", System.currentTimeMillis())
.apply();
Log.d(TAG, "Phase 3: Background content fetch coordination completed");
} catch (Exception e) {
Log.e(TAG, "Phase 3: Error scheduling background content fetch coordination", e);
}
}
}