/** * DailyNotificationExactAlarmManager.java * * Android Exact Alarm Manager with fallback to windowed alarms * Implements SCHEDULE_EXACT_ALARM permission handling and fallback logic * * @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.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.provider.Settings; import android.util.Log; import java.util.concurrent.TimeUnit; /** * Manages Android exact alarms with fallback to windowed alarms * * This class implements the critical Android alarm management: * - Requests SCHEDULE_EXACT_ALARM permission * - Falls back to windowed alarms (±10m) if exact permission denied * - Provides deep-link to enable exact alarms in settings * - Handles reboot and time-change recovery */ public class DailyNotificationExactAlarmManager { // MARK: - Constants private static final String TAG = "DailyNotificationExactAlarmManager"; // Permission constants private static final String PERMISSION_SCHEDULE_EXACT_ALARM = "android.permission.SCHEDULE_EXACT_ALARM"; // Fallback window settings private static final long FALLBACK_WINDOW_START_MS = TimeUnit.MINUTES.toMillis(-10); // 10 minutes before private static final long FALLBACK_WINDOW_LENGTH_MS = TimeUnit.MINUTES.toMillis(20); // 20 minutes total // Deep-link constants private static final String EXACT_ALARM_SETTINGS_ACTION = "android.settings.REQUEST_SCHEDULE_EXACT_ALARM"; private static final String EXACT_ALARM_SETTINGS_PACKAGE = "com.android.settings"; // MARK: - Properties private final Context context; private final AlarmManager alarmManager; private final DailyNotificationScheduler scheduler; // Alarm state private boolean exactAlarmsEnabled = false; private boolean exactAlarmsSupported = false; // MARK: - Initialization /** * Constructor * * @param context Application context * @param alarmManager System AlarmManager service * @param scheduler Notification scheduler */ public DailyNotificationExactAlarmManager(Context context, AlarmManager alarmManager, DailyNotificationScheduler scheduler) { this.context = context; this.alarmManager = alarmManager; this.scheduler = scheduler; // Check exact alarm support and status checkExactAlarmSupport(); checkExactAlarmStatus(); Log.d(TAG, "ExactAlarmManager initialized: supported=" + exactAlarmsSupported + ", enabled=" + exactAlarmsEnabled); } // MARK: - Exact Alarm Support /** * Check if exact alarms are supported on this device */ private void checkExactAlarmSupport() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { exactAlarmsSupported = true; Log.d(TAG, "Exact alarms supported on Android S+"); } else { exactAlarmsSupported = false; Log.d(TAG, "Exact alarms not supported on Android " + Build.VERSION.SDK_INT); } } /** * Check current exact alarm status */ private void checkExactAlarmStatus() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { exactAlarmsEnabled = alarmManager.canScheduleExactAlarms(); Log.d(TAG, "Exact alarm status: " + (exactAlarmsEnabled ? "enabled" : "disabled")); } else { exactAlarmsEnabled = true; // Always available on older Android versions Log.d(TAG, "Exact alarms always available on Android " + Build.VERSION.SDK_INT); } } /** * Get exact alarm status * * @return Status information */ public ExactAlarmStatus getExactAlarmStatus() { return new ExactAlarmStatus( exactAlarmsSupported, exactAlarmsEnabled, canScheduleExactAlarms(), getFallbackWindowInfo() ); } /** * Check if exact alarms can be scheduled * * @return true if exact alarms can be scheduled */ public boolean canScheduleExactAlarms() { if (!exactAlarmsSupported) { return false; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { return alarmManager.canScheduleExactAlarms(); } return true; } /** * Get fallback window information * * @return Fallback window info */ public FallbackWindowInfo getFallbackWindowInfo() { return new FallbackWindowInfo( FALLBACK_WINDOW_START_MS, FALLBACK_WINDOW_LENGTH_MS, "±10 minutes" ); } // MARK: - Alarm Scheduling /** * Schedule alarm with exact or fallback logic * * @param pendingIntent PendingIntent to trigger * @param triggerTime Exact trigger time * @return true if scheduling was successful */ public boolean scheduleAlarm(PendingIntent pendingIntent, long triggerTime) { try { Log.d(TAG, "Scheduling alarm for " + triggerTime); if (canScheduleExactAlarms()) { return scheduleExactAlarm(pendingIntent, triggerTime); } else { return scheduleWindowedAlarm(pendingIntent, triggerTime); } } catch (Exception e) { Log.e(TAG, "Error scheduling alarm", e); return false; } } /** * Schedule exact alarm * * @param pendingIntent PendingIntent to trigger * @param triggerTime Exact trigger time * @return true if scheduling was successful */ private boolean scheduleExactAlarm(PendingIntent pendingIntent, long triggerTime) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); Log.i(TAG, "Exact alarm scheduled for " + triggerTime); return true; } else { alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); Log.i(TAG, "Exact alarm scheduled for " + triggerTime + " (pre-M)"); return true; } } catch (Exception e) { Log.e(TAG, "Error scheduling exact alarm", e); return false; } } /** * Schedule windowed alarm as fallback * * @param pendingIntent PendingIntent to trigger * @param triggerTime Target trigger time * @return true if scheduling was successful */ private boolean scheduleWindowedAlarm(PendingIntent pendingIntent, long triggerTime) { try { // Calculate window start time (10 minutes before target) long windowStartTime = triggerTime + FALLBACK_WINDOW_START_MS; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { alarmManager.setWindow(AlarmManager.RTC_WAKEUP, windowStartTime, FALLBACK_WINDOW_LENGTH_MS, pendingIntent); Log.i(TAG, "Windowed alarm scheduled: target=" + triggerTime + ", window=" + windowStartTime + " to " + (windowStartTime + FALLBACK_WINDOW_LENGTH_MS)); return true; } else { // Fallback to inexact alarm on older versions alarmManager.set(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); Log.i(TAG, "Inexact alarm scheduled for " + triggerTime + " (pre-KitKat)"); return true; } } catch (Exception e) { Log.e(TAG, "Error scheduling windowed alarm", e); return false; } } // MARK: - Permission Management /** * Request exact alarm permission * * @return true if permission request was initiated */ public boolean requestExactAlarmPermission() { if (!exactAlarmsSupported) { Log.w(TAG, "Exact alarms not supported on this device"); return false; } if (exactAlarmsEnabled) { Log.d(TAG, "Exact alarms already enabled"); return true; } try { // Open exact alarm settings Intent intent = new Intent(EXACT_ALARM_SETTINGS_ACTION); intent.setPackage(EXACT_ALARM_SETTINGS_PACKAGE); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); Log.i(TAG, "Exact alarm permission request initiated"); return true; } catch (Exception e) { Log.e(TAG, "Error requesting exact alarm permission", e); return false; } } /** * Open exact alarm settings * * @return true if settings were opened */ public boolean openExactAlarmSettings() { try { Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); Log.i(TAG, "Exact alarm settings opened"); return true; } catch (Exception e) { Log.e(TAG, "Error opening exact alarm settings", e); return false; } } /** * Check if exact alarm permission is granted * * @return true if permission is granted */ public boolean hasExactAlarmPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { return context.checkSelfPermission(PERMISSION_SCHEDULE_EXACT_ALARM) == PackageManager.PERMISSION_GRANTED; } return true; // Always available on older versions } // MARK: - Reboot and Time Change Recovery /** * Handle system reboot * * This method should be called when the system boots to restore * scheduled alarms that were lost during reboot. */ public void handleSystemReboot() { try { Log.i(TAG, "Handling system reboot - restoring scheduled alarms"); // Re-schedule all pending notifications scheduler.restoreScheduledNotifications(); Log.i(TAG, "System reboot handling completed"); } catch (Exception e) { Log.e(TAG, "Error handling system reboot", e); } } /** * Handle time change * * This method should be called when the system time changes * to adjust scheduled alarms accordingly. */ public void handleTimeChange() { try { Log.i(TAG, "Handling time change - adjusting scheduled alarms"); // Re-schedule all pending notifications with adjusted times scheduler.adjustScheduledNotifications(); Log.i(TAG, "Time change handling completed"); } catch (Exception e) { Log.e(TAG, "Error handling time change", e); } } // MARK: - Status Classes /** * Exact alarm status information */ public static class ExactAlarmStatus { public final boolean supported; public final boolean enabled; public final boolean canSchedule; public final FallbackWindowInfo fallbackWindow; public ExactAlarmStatus(boolean supported, boolean enabled, boolean canSchedule, FallbackWindowInfo fallbackWindow) { this.supported = supported; this.enabled = enabled; this.canSchedule = canSchedule; this.fallbackWindow = fallbackWindow; } @Override public String toString() { return String.format("ExactAlarmStatus{supported=%s, enabled=%s, canSchedule=%s, fallbackWindow=%s}", supported, enabled, canSchedule, fallbackWindow); } } /** * Fallback window information */ public static class FallbackWindowInfo { public final long startMs; public final long lengthMs; public final String description; public FallbackWindowInfo(long startMs, long lengthMs, String description) { this.startMs = startMs; this.lengthMs = lengthMs; this.description = description; } @Override public String toString() { return String.format("FallbackWindowInfo{start=%dms, length=%dms, description='%s'}", startMs, lengthMs, description); } } }