/** * DailyNotificationRebootRecoveryManager.java * * Android Reboot Recovery Manager for notification restoration * Handles system reboots and time changes to restore scheduled notifications * * @author Matthew Raymer * @version 1.0.0 */ package com.timesafari.dailynotification; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Build; import android.util.Log; import java.util.concurrent.TimeUnit; /** * Manages recovery from system reboots and time changes * * This class implements the critical recovery functionality: * - Listens for system reboot broadcasts * - Handles time change events * - Restores scheduled notifications after reboot * - Adjusts notification times after time changes */ public class DailyNotificationRebootRecoveryManager { // MARK: - Constants private static final String TAG = "DailyNotificationRebootRecoveryManager"; // Broadcast actions private static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED"; private static final String ACTION_MY_PACKAGE_REPLACED = "android.intent.action.MY_PACKAGE_REPLACED"; private static final String ACTION_PACKAGE_REPLACED = "android.intent.action.PACKAGE_REPLACED"; private static final String ACTION_TIME_CHANGED = "android.intent.action.TIME_SET"; private static final String ACTION_TIMEZONE_CHANGED = "android.intent.action.TIMEZONE_CHANGED"; // Recovery delay private static final long RECOVERY_DELAY_MS = TimeUnit.SECONDS.toMillis(5); // MARK: - Properties private final Context context; private final DailyNotificationScheduler scheduler; private final DailyNotificationExactAlarmManager exactAlarmManager; private final DailyNotificationRollingWindow rollingWindow; // Broadcast receivers private BootCompletedReceiver bootCompletedReceiver; private TimeChangeReceiver timeChangeReceiver; // Recovery state private boolean recoveryInProgress = false; private long lastRecoveryTime = 0; // MARK: - Initialization /** * Constructor * * @param context Application context * @param scheduler Notification scheduler * @param exactAlarmManager Exact alarm manager * @param rollingWindow Rolling window manager */ public DailyNotificationRebootRecoveryManager(Context context, DailyNotificationScheduler scheduler, DailyNotificationExactAlarmManager exactAlarmManager, DailyNotificationRollingWindow rollingWindow) { this.context = context; this.scheduler = scheduler; this.exactAlarmManager = exactAlarmManager; this.rollingWindow = rollingWindow; Log.d(TAG, "RebootRecoveryManager initialized"); } /** * Register broadcast receivers */ public void registerReceivers() { try { Log.d(TAG, "Registering broadcast receivers"); // Register boot completed receiver bootCompletedReceiver = new BootCompletedReceiver(); IntentFilter bootFilter = new IntentFilter(); bootFilter.addAction(ACTION_BOOT_COMPLETED); bootFilter.addAction(ACTION_MY_PACKAGE_REPLACED); bootFilter.addAction(ACTION_PACKAGE_REPLACED); context.registerReceiver(bootCompletedReceiver, bootFilter); // Register time change receiver timeChangeReceiver = new TimeChangeReceiver(); IntentFilter timeFilter = new IntentFilter(); timeFilter.addAction(ACTION_TIME_CHANGED); timeFilter.addAction(ACTION_TIMEZONE_CHANGED); context.registerReceiver(timeChangeReceiver, timeFilter); Log.i(TAG, "Broadcast receivers registered successfully"); } catch (Exception e) { Log.e(TAG, "Error registering broadcast receivers", e); } } /** * Unregister broadcast receivers */ public void unregisterReceivers() { try { Log.d(TAG, "Unregistering broadcast receivers"); if (bootCompletedReceiver != null) { context.unregisterReceiver(bootCompletedReceiver); bootCompletedReceiver = null; } if (timeChangeReceiver != null) { context.unregisterReceiver(timeChangeReceiver); timeChangeReceiver = null; } Log.i(TAG, "Broadcast receivers unregistered successfully"); } catch (Exception e) { Log.e(TAG, "Error unregistering broadcast receivers", e); } } // MARK: - Recovery Methods /** * Handle system reboot recovery * * This method restores all scheduled notifications that were lost * during the system reboot. */ public void handleSystemReboot() { try { Log.i(TAG, "Handling system reboot recovery"); // Check if recovery is already in progress if (recoveryInProgress) { Log.w(TAG, "Recovery already in progress, skipping"); return; } // Check if recovery was recently performed long currentTime = System.currentTimeMillis(); if (currentTime - lastRecoveryTime < RECOVERY_DELAY_MS) { Log.w(TAG, "Recovery performed recently, skipping"); return; } recoveryInProgress = true; lastRecoveryTime = currentTime; // Perform recovery operations performRebootRecovery(); recoveryInProgress = false; Log.i(TAG, "System reboot recovery completed"); } catch (Exception e) { Log.e(TAG, "Error handling system reboot", e); recoveryInProgress = false; } } /** * Handle time change recovery * * This method adjusts all scheduled notifications to account * for system time changes. */ public void handleTimeChange() { try { Log.i(TAG, "Handling time change recovery"); // Check if recovery is already in progress if (recoveryInProgress) { Log.w(TAG, "Recovery already in progress, skipping"); return; } recoveryInProgress = true; // Perform time change recovery performTimeChangeRecovery(); recoveryInProgress = false; Log.i(TAG, "Time change recovery completed"); } catch (Exception e) { Log.e(TAG, "Error handling time change", e); recoveryInProgress = false; } } /** * Perform reboot recovery operations */ private void performRebootRecovery() { try { Log.d(TAG, "Performing reboot recovery operations"); // Wait a bit for system to stabilize Thread.sleep(2000); // Restore scheduled notifications scheduler.restoreScheduledNotifications(); // Restore rolling window rollingWindow.forceMaintenance(); // Log recovery statistics logRecoveryStatistics("reboot"); } catch (Exception e) { Log.e(TAG, "Error performing reboot recovery", e); } } /** * Perform time change recovery operations */ private void performTimeChangeRecovery() { try { Log.d(TAG, "Performing time change recovery operations"); // Adjust scheduled notifications scheduler.adjustScheduledNotifications(); // Update rolling window rollingWindow.forceMaintenance(); // Log recovery statistics logRecoveryStatistics("time_change"); } catch (Exception e) { Log.e(TAG, "Error performing time change recovery", e); } } /** * Log recovery statistics * * @param recoveryType Type of recovery performed */ private void logRecoveryStatistics(String recoveryType) { try { // Get recovery statistics int restoredCount = scheduler.getRestoredNotificationCount(); int adjustedCount = scheduler.getAdjustedNotificationCount(); Log.i(TAG, String.format("Recovery statistics (%s): restored=%d, adjusted=%d", recoveryType, restoredCount, adjustedCount)); } catch (Exception e) { Log.e(TAG, "Error logging recovery statistics", e); } } // MARK: - Broadcast Receivers /** * Broadcast receiver for boot completed events */ private class BootCompletedReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { try { String action = intent.getAction(); Log.d(TAG, "BootCompletedReceiver received action: " + action); if (ACTION_BOOT_COMPLETED.equals(action) || ACTION_MY_PACKAGE_REPLACED.equals(action) || ACTION_PACKAGE_REPLACED.equals(action)) { // Handle system reboot handleSystemReboot(); } } catch (Exception e) { Log.e(TAG, "Error in BootCompletedReceiver", e); } } } /** * Broadcast receiver for time change events */ private class TimeChangeReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { try { String action = intent.getAction(); Log.d(TAG, "TimeChangeReceiver received action: " + action); if (ACTION_TIME_CHANGED.equals(action) || ACTION_TIMEZONE_CHANGED.equals(action)) { // Handle time change handleTimeChange(); } } catch (Exception e) { Log.e(TAG, "Error in TimeChangeReceiver", e); } } } // MARK: - Public Methods /** * Get recovery status * * @return Recovery status information */ public RecoveryStatus getRecoveryStatus() { return new RecoveryStatus( recoveryInProgress, lastRecoveryTime, System.currentTimeMillis() - lastRecoveryTime ); } /** * Force recovery (for testing) */ public void forceRecovery() { Log.i(TAG, "Forcing recovery"); handleSystemReboot(); } /** * Check if recovery is needed * * @return true if recovery is needed */ public boolean isRecoveryNeeded() { // Check if system was recently rebooted long currentTime = System.currentTimeMillis(); long timeSinceLastRecovery = currentTime - lastRecoveryTime; // Recovery needed if more than 1 hour since last recovery return timeSinceLastRecovery > TimeUnit.HOURS.toMillis(1); } // MARK: - Status Classes /** * Recovery status information */ public static class RecoveryStatus { public final boolean inProgress; public final long lastRecoveryTime; public final long timeSinceLastRecovery; public RecoveryStatus(boolean inProgress, long lastRecoveryTime, long timeSinceLastRecovery) { this.inProgress = inProgress; this.lastRecoveryTime = lastRecoveryTime; this.timeSinceLastRecovery = timeSinceLastRecovery; } @Override public String toString() { return String.format("RecoveryStatus{inProgress=%s, lastRecovery=%d, timeSince=%d}", inProgress, lastRecoveryTime, timeSinceLastRecovery); } } }