/** * 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; // TTL enforcement private DailyNotificationTTLEnforcer ttlEnforcer; // Exact alarm management private DailyNotificationExactAlarmManager exactAlarmManager; /** * 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<>(); } /** * Set TTL enforcer for freshness validation * * @param ttlEnforcer TTL enforcement instance */ public void setTTLEnforcer(DailyNotificationTTLEnforcer ttlEnforcer) { this.ttlEnforcer = ttlEnforcer; Log.d(TAG, "TTL enforcer set for freshness validation"); } /** * Set exact alarm manager for alarm scheduling * * @param exactAlarmManager Exact alarm manager instance */ public void setExactAlarmManager(DailyNotificationExactAlarmManager exactAlarmManager) { this.exactAlarmManager = exactAlarmManager; Log.d(TAG, "Exact alarm manager set for alarm scheduling"); } /** * Schedule a notification for delivery (Phase 3 enhanced) * * @param content Notification content to schedule * @return true if scheduling was successful */ public boolean scheduleNotification(NotificationContent content) { try { Log.d(TAG, "Phase 3: Scheduling notification: " + content.getId()); // Phase 3: TimeSafari coordination before scheduling if (!shouldScheduleWithTimeSafariCoordination(content)) { Log.w(TAG, "Phase 3: Scheduling blocked by TimeSafari coordination"); return false; } // TTL validation before arming if (ttlEnforcer != null) { if (!ttlEnforcer.validateBeforeArming(content)) { Log.w(TAG, "Skipping notification due to TTL violation: " + content.getId()); return false; } } else { Log.w(TAG, "TTL enforcer not set, proceeding without freshness validation"); } // 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()); // Check if this is a static reminder if (content.getId().startsWith("reminder_") || content.getId().contains("_reminder")) { intent.putExtra("is_static_reminder", true); intent.putExtra("reminder_id", content.getId()); intent.putExtra("title", content.getTitle()); intent.putExtra("body", content.getBody()); intent.putExtra("sound", content.isSound()); intent.putExtra("vibration", true); // Default to true for reminders intent.putExtra("priority", content.getPriority()); } // 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 { // Use exact alarm manager if available if (exactAlarmManager != null) { return exactAlarmManager.scheduleAlarm(pendingIntent, triggerTime); } // Fallback to legacy scheduling 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(); } /** * Restore scheduled notifications after reboot * * This method should be called after system reboot to restore * all scheduled notifications that were lost during reboot. */ public void restoreScheduledNotifications() { try { Log.i(TAG, "Restoring scheduled notifications after reboot"); // This would typically restore notifications from storage // For now, we'll just log the action Log.d(TAG, "Scheduled notifications restored"); } catch (Exception e) { Log.e(TAG, "Error restoring scheduled notifications", e); } } /** * Adjust scheduled notifications after time change * * This method should be called after system time changes to adjust * all scheduled notifications accordingly. */ public void adjustScheduledNotifications() { try { Log.i(TAG, "Adjusting scheduled notifications after time change"); // This would typically adjust notification times // For now, we'll just log the action Log.d(TAG, "Scheduled notifications adjusted"); } catch (Exception e) { Log.e(TAG, "Error adjusting scheduled notifications", e); } } /** * Get count of restored notifications * * @return Number of restored notifications */ public int getRestoredNotificationCount() { // This would typically return actual count // For now, we'll return a placeholder return 0; } /** * Get count of adjusted notifications * * @return Number of adjusted notifications */ public int getAdjustedNotificationCount() { // This would typically return actual count // For now, we'll return a placeholder return 0; } // MARK: - Phase 3: TimeSafari Coordination Methods /** * Phase 3: Check if scheduling should proceed with TimeSafari coordination */ private boolean shouldScheduleWithTimeSafariCoordination(NotificationContent content) { try { Log.d(TAG, "Phase 3: Checking TimeSafari coordination for notification: " + content.getId()); // Check app lifecycle state if (!isAppInForeground()) { Log.d(TAG, "Phase 3: App not in foreground - allowing scheduling"); return true; } // Check activeDid health if (hasActiveDidChangedRecently()) { Log.d(TAG, "Phase 3: ActiveDid changed recently - deferring scheduling"); return false; } // Check background task coordination if (!isBackgroundTaskCoordinated()) { Log.d(TAG, "Phase 3: Background tasks not coordinated - allowing scheduling"); return true; } // Check notification throttling if (isNotificationThrottled()) { Log.d(TAG, "Phase 3: Notification throttled - deferring scheduling"); return false; } Log.d(TAG, "Phase 3: TimeSafari coordination passed - allowing scheduling"); return true; } catch (Exception e) { Log.e(TAG, "Phase 3: Error checking TimeSafari coordination", e); return true; // Default to allowing scheduling on error } } /** * Phase 3: Check if app is currently in foreground */ private boolean isAppInForeground() { try { android.app.ActivityManager activityManager = (android.app.ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); if (activityManager != null) { java.util.List runningProcesses = activityManager.getRunningAppProcesses(); if (runningProcesses != null) { for (android.app.ActivityManager.RunningAppProcessInfo processInfo : runningProcesses) { if (processInfo.processName.equals(context.getPackageName())) { boolean inForeground = processInfo.importance == android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; Log.d(TAG, "Phase 3: App foreground state: " + inForeground); return inForeground; } } } } return false; } catch (Exception e) { Log.e(TAG, "Phase 3: Error checking app foreground state", e); return false; } } /** * Phase 3: Check if activeDid has changed recently */ private boolean hasActiveDidChangedRecently() { try { android.content.SharedPreferences prefs = context.getSharedPreferences( "daily_notification_timesafari", Context.MODE_PRIVATE); long lastActiveDidChange = prefs.getLong("lastActiveDidChange", 0); long gracefulPeriodMs = 30000; // 30 seconds grace period if (lastActiveDidChange > 0) { long timeSinceChange = System.currentTimeMillis() - lastActiveDidChange; boolean changedRecently = timeSinceChange < gracefulPeriodMs; Log.d(TAG, "Phase 3: ActiveDid change check - lastChange: " + lastActiveDidChange + ", timeSince: " + timeSinceChange + "ms, changedRecently: " + changedRecently); return changedRecently; } return false; } catch (Exception e) { Log.e(TAG, "Phase 3: Error checking activeDid change", e); return false; } } /** * Phase 3: Check if background tasks are properly coordinated */ private boolean isBackgroundTaskCoordinated() { try { android.content.SharedPreferences prefs = context.getSharedPreferences( "daily_notification_timesafari", Context.MODE_PRIVATE); boolean autoSync = prefs.getBoolean("autoSync", false); long lastFetchAttempt = prefs.getLong("lastFetchAttempt", 0); long coordinationTimeout = 60000; // 1 minute timeout if (!autoSync) { Log.d(TAG, "Phase 3: Auto-sync disabled - background coordination not needed"); return true; } if (lastFetchAttempt > 0) { long timeSinceLastFetch = System.currentTimeMillis() - lastFetchAttempt; boolean recentFetch = timeSinceLastFetch < coordinationTimeout; Log.d(TAG, "Phase 3: Background task coordination - timeSinceLastFetch: " + timeSinceLastFetch + "ms, recentFetch: " + recentFetch); return recentFetch; } return true; } catch (Exception e) { Log.e(TAG, "Phase 3: Error checking background task coordination", e); return true; } } /** * Phase 3: Check if notifications are currently throttled */ private boolean isNotificationThrottled() { try { android.content.SharedPreferences prefs = context.getSharedPreferences( "daily_notification_timesafari", Context.MODE_PRIVATE); long lastNotificationDelivered = prefs.getLong("lastNotificationDelivered", 0); long throttleIntervalMs = 10000; // 10 seconds between notifications if (lastNotificationDelivered > 0) { long timeSinceLastDelivery = System.currentTimeMillis() - lastNotificationDelivered; boolean isThrottled = timeSinceLastDelivery < throttleIntervalMs; Log.d(TAG, "Phase 3: Notification throttling - timeSinceLastDelivery: " + timeSinceLastDelivery + "ms, isThrottled: " + isThrottled); return isThrottled; } return false; } catch (Exception e) { Log.e(TAG, "Phase 3: Error checking notification throttle", e); return false; } } /** * Phase 3: Update notification delivery timestamp */ public void recordNotificationDelivery(String notificationId) { try { android.content.SharedPreferences prefs = context.getSharedPreferences( "daily_notification_timesafari", Context.MODE_PRIVATE); prefs.edit() .putLong("lastNotificationDelivered", System.currentTimeMillis()) .putString("lastDeliveredNotificationId", notificationId) .apply(); Log.d(TAG, "Phase 3: Notification delivery recorded: " + notificationId); } catch (Exception e) { Log.e(TAG, "Phase 3: Error recording notification delivery", e); } } /** * Phase 3: Coordinate with PlatformServiceMixin events */ public void coordinateWithPlatformServiceMixin() { try { Log.d(TAG, "Phase 3: Coordinating with PlatformServiceMixin events"); // This would integrate with TimeSafari's PlatformServiceMixin lifecycle events // For now, we'll implement a simplified coordination android.content.SharedPreferences prefs = context.getSharedPreferences( "daily_notification_timesafari", Context.MODE_PRIVATE); boolean autoSync = prefs.getBoolean("autoSync", false); if (autoSync) { // Schedule background content fetch coordination scheduleBackgroundContentFetchWithCoordination(); } Log.d(TAG, "Phase 3: PlatformServiceMixin coordination completed"); } catch (Exception e) { Log.e(TAG, "Phase 3: Error coordinating with PlatformServiceMixin", e); } } /** * Phase 3: Schedule background content fetch with coordination */ private void scheduleBackgroundContentFetchWithCoordination() { try { Log.d(TAG, "Phase 3: Scheduling background content fetch with coordination"); // This would coordinate with TimeSafari's background task management // For now, we'll update coordination timestamps android.content.SharedPreferences prefs = context.getSharedPreferences( "daily_notification_timesafari", Context.MODE_PRIVATE); prefs.edit() .putLong("lastBackgroundFetchCoordinated", System.currentTimeMillis()) .apply(); Log.d(TAG, "Phase 3: Background content fetch coordination completed"); } catch (Exception e) { Log.e(TAG, "Phase 3: Error scheduling background content fetch coordination", e); } } }