/** * 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 scheduledAlarms; /** * 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<>(); } /** * Schedule a notification for delivery * * @param content Notification content to schedule * @return true if scheduling was successful */ public boolean scheduleNotification(NotificationContent content) { try { Log.d(TAG, "Scheduling notification: " + content.getId()); // Cancel any existing alarm for this notification cancelNotification(content.getId()); // Create intent for the notification Intent intent = new Intent(context, DailyNotificationReceiver.class); intent.setAction(ACTION_NOTIFICATION); intent.putExtra(EXTRA_NOTIFICATION_ID, content.getId()); // Create pending intent with unique request code int requestCode = content.getId().hashCode(); PendingIntent pendingIntent = PendingIntent.getBroadcast( context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE ); // Store the pending intent scheduledAlarms.put(content.getId(), pendingIntent); // Schedule the alarm long triggerTime = content.getScheduledTime(); 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 { // Check if we can use exact alarms if (canUseExactAlarms()) { return scheduleExactAlarm(pendingIntent, triggerTime); } else { return scheduleInexactAlarm(pendingIntent, triggerTime); } } catch (Exception e) { Log.e(TAG, "Error scheduling alarm", e); return false; } } /** * Schedule an exact alarm for precise timing * * @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 { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { alarmManager.setExactAndAllowWhileIdle( AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent ); } else { alarmManager.setExact( AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent ); } Log.d(TAG, "Exact alarm scheduled for " + formatTime(triggerTime)); return true; } catch (Exception e) { Log.e(TAG, "Error scheduling exact alarm", e); return false; } } /** * 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 * * @return true if exact alarms are permitted */ private boolean canUseExactAlarms() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { return alarmManager.canScheduleExactAlarms(); } return true; // Pre-Android 12 always allowed exact alarms } /** * 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) { alarmManager.cancel(pendingIntent); pendingIntent.cancel(); Log.d(TAG, "Cancelled 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(); 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 * * @param hour Hour of day (0-23) * @param minute Minute of hour (0-59) * @return Timestamp of next occurrence */ public long calculateNextOccurrence(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); } return calendar.getTimeInMillis(); } }