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.
 
 
 
 
 
 

377 lines
12 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;
/**
* 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();
}
}