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
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();
|
|
}
|
|
}
|
|
|