Browse Source
- Add DailyNotificationExactAlarmManager with SCHEDULE_EXACT_ALARM permission handling - Add DailyNotificationRebootRecoveryManager for system reboot and time-change recovery - Update DailyNotificationScheduler with exact alarm manager integration - Add exact alarm status checking and permission request methods - Add windowed alarm fallback (±10m) when exact alarms are denied - Add deep-link to exact alarm settings for user guidance - Add reboot recovery with broadcast receiver registration - Update TypeScript interface with new exact alarm and recovery methods - Update web implementations with placeholder methods - Add phase2-2-android-fallback.ts usage examples This completes Phase 2.2 Android fallback implementation: - Exact alarm permission handling with graceful fallback - Windowed alarm support (±10m) for battery optimization - Reboot and time-change recovery with broadcast receivers - Deep-link to exact alarm settings for user enablement - Integration with existing TTL enforcement and rolling window - Comprehensive fallback scenarios and error handling Files: 7 changed, 1200+ insertions(+)research/notification-plugin-enhancement
8 changed files with 1430 additions and 1 deletions
@ -0,0 +1,321 @@ |
|||
/** |
|||
* Phase 2.2 Android Fallback Completion Usage Example |
|||
* |
|||
* Demonstrates Android exact alarm fallback functionality |
|||
* Shows permission handling, windowed alarms, and reboot recovery |
|||
* |
|||
* @author Matthew Raymer |
|||
* @version 1.0.0 |
|||
*/ |
|||
|
|||
import { DailyNotification } from '@timesafari/daily-notification-plugin'; |
|||
|
|||
/** |
|||
* Example: Configure Android exact alarm fallback |
|||
*/ |
|||
async function configureAndroidExactAlarmFallback() { |
|||
try { |
|||
console.log('Configuring Android exact alarm fallback...'); |
|||
|
|||
// Configure with fallback settings
|
|||
await DailyNotification.configure({ |
|||
storage: 'shared', |
|||
ttlSeconds: 1800, // 30 minutes TTL
|
|||
prefetchLeadMinutes: 15, |
|||
maxNotificationsPerDay: 50 // Android limit
|
|||
}); |
|||
|
|||
console.log('✅ Android exact alarm fallback configured'); |
|||
|
|||
// The plugin will now:
|
|||
// - Request SCHEDULE_EXACT_ALARM permission
|
|||
// - Fall back to windowed alarms (±10m) if denied
|
|||
// - Handle reboot and time-change recovery
|
|||
// - Provide deep-link to enable exact alarms
|
|||
|
|||
} catch (error) { |
|||
console.error('❌ Android exact alarm fallback configuration failed:', error); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Example: Check exact alarm status |
|||
*/ |
|||
async function checkExactAlarmStatus() { |
|||
try { |
|||
console.log('Checking exact alarm status...'); |
|||
|
|||
// Get exact alarm status
|
|||
const status = await DailyNotification.getExactAlarmStatus(); |
|||
|
|||
console.log('📱 Exact Alarm Status:'); |
|||
console.log(` Supported: ${status.supported}`); |
|||
console.log(` Enabled: ${status.enabled}`); |
|||
console.log(` Can Schedule: ${status.canSchedule}`); |
|||
console.log(` Fallback Window: ${status.fallbackWindow}`); |
|||
|
|||
// Example output:
|
|||
// Supported: true
|
|||
// Enabled: false
|
|||
// Can Schedule: false
|
|||
// Fallback Window: ±10 minutes
|
|||
|
|||
if (!status.enabled && status.supported) { |
|||
console.log('⚠️ Exact alarms are supported but not enabled'); |
|||
console.log('💡 Consider requesting permission or opening settings'); |
|||
} |
|||
|
|||
} catch (error) { |
|||
console.error('❌ Exact alarm status check failed:', error); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Example: Request exact alarm permission |
|||
*/ |
|||
async function requestExactAlarmPermission() { |
|||
try { |
|||
console.log('Requesting exact alarm permission...'); |
|||
|
|||
// Request exact alarm permission
|
|||
await DailyNotification.requestExactAlarmPermission(); |
|||
|
|||
console.log('✅ Exact alarm permission request initiated'); |
|||
|
|||
// This will:
|
|||
// - Open the exact alarm settings screen
|
|||
// - Allow user to enable exact alarms
|
|||
// - Fall back to windowed alarms if denied
|
|||
|
|||
} catch (error) { |
|||
console.error('❌ Exact alarm permission request failed:', error); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Example: Open exact alarm settings |
|||
*/ |
|||
async function openExactAlarmSettings() { |
|||
try { |
|||
console.log('Opening exact alarm settings...'); |
|||
|
|||
// Open exact alarm settings
|
|||
await DailyNotification.openExactAlarmSettings(); |
|||
|
|||
console.log('✅ Exact alarm settings opened'); |
|||
|
|||
// This will:
|
|||
// - Navigate to exact alarm settings
|
|||
// - Allow user to enable exact alarms
|
|||
// - Provide fallback information if needed
|
|||
|
|||
} catch (error) { |
|||
console.error('❌ Opening exact alarm settings failed:', error); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Example: Schedule notification with fallback handling |
|||
*/ |
|||
async function scheduleWithFallbackHandling() { |
|||
try { |
|||
console.log('Scheduling notification with fallback handling...'); |
|||
|
|||
// Configure fallback
|
|||
await configureAndroidExactAlarmFallback(); |
|||
|
|||
// Check status first
|
|||
const status = await DailyNotification.getExactAlarmStatus(); |
|||
|
|||
if (!status.canSchedule) { |
|||
console.log('⚠️ Exact alarms not available, will use windowed fallback'); |
|||
} |
|||
|
|||
// Schedule notification
|
|||
await DailyNotification.scheduleDailyNotification({ |
|||
url: 'https://api.example.com/daily-content', |
|||
time: '09:00', |
|||
title: 'Daily Update', |
|||
body: 'Your daily notification is ready', |
|||
sound: true |
|||
}); |
|||
|
|||
console.log('✅ Notification scheduled with fallback handling'); |
|||
|
|||
// The plugin will:
|
|||
// - Use exact alarms if available
|
|||
// - Fall back to windowed alarms (±10m) if not
|
|||
// - Handle permission changes gracefully
|
|||
// - Provide appropriate user feedback
|
|||
|
|||
} catch (error) { |
|||
console.error('❌ Scheduling with fallback handling failed:', error); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Example: Check reboot recovery status |
|||
*/ |
|||
async function checkRebootRecoveryStatus() { |
|||
try { |
|||
console.log('Checking reboot recovery status...'); |
|||
|
|||
// Get reboot recovery status
|
|||
const status = await DailyNotification.getRebootRecoveryStatus(); |
|||
|
|||
console.log('🔄 Reboot Recovery Status:'); |
|||
console.log(` In Progress: ${status.inProgress}`); |
|||
console.log(` Last Recovery Time: ${new Date(status.lastRecoveryTime)}`); |
|||
console.log(` Time Since Last Recovery: ${status.timeSinceLastRecovery}ms`); |
|||
console.log(` Recovery Needed: ${status.recoveryNeeded}`); |
|||
|
|||
// Example output:
|
|||
// In Progress: false
|
|||
// Last Recovery Time: Mon Sep 08 2025 10:30:00 GMT+0000
|
|||
// Time Since Last Recovery: 120000ms
|
|||
// Recovery Needed: false
|
|||
|
|||
if (status.recoveryNeeded) { |
|||
console.log('⚠️ Recovery is needed - system may have rebooted'); |
|||
} |
|||
|
|||
} catch (error) { |
|||
console.error('❌ Reboot recovery status check failed:', error); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Example: Demonstrate fallback scenarios |
|||
*/ |
|||
async function demonstrateFallbackScenarios() { |
|||
try { |
|||
console.log('Demonstrating fallback scenarios...'); |
|||
|
|||
// Configure fallback
|
|||
await configureAndroidExactAlarmFallback(); |
|||
|
|||
// Check initial status
|
|||
const initialStatus = await DailyNotification.getExactAlarmStatus(); |
|||
console.log('📱 Initial Status:', initialStatus); |
|||
|
|||
// Schedule notification
|
|||
await DailyNotification.scheduleDailyNotification({ |
|||
url: 'https://api.example.com/daily-content', |
|||
time: '09:00', |
|||
title: 'Daily Update', |
|||
body: 'Your daily notification is ready' |
|||
}); |
|||
|
|||
console.log('✅ Notification scheduled'); |
|||
|
|||
// Check status after scheduling
|
|||
const afterStatus = await DailyNotification.getExactAlarmStatus(); |
|||
console.log('📱 Status After Scheduling:', afterStatus); |
|||
|
|||
// The plugin will handle:
|
|||
// - Exact alarms if permission granted
|
|||
// - Windowed alarms (±10m) if permission denied
|
|||
// - Graceful degradation based on Android version
|
|||
// - Appropriate user feedback and guidance
|
|||
|
|||
} catch (error) { |
|||
console.error('❌ Fallback scenarios demonstration failed:', error); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Example: Monitor exact alarm changes |
|||
*/ |
|||
async function monitorExactAlarmChanges() { |
|||
try { |
|||
console.log('Monitoring exact alarm changes...'); |
|||
|
|||
// Configure fallback
|
|||
await configureAndroidExactAlarmFallback(); |
|||
|
|||
// Monitor changes over time
|
|||
const monitorInterval = setInterval(async () => { |
|||
try { |
|||
const status = await DailyNotification.getExactAlarmStatus(); |
|||
console.log('📱 Exact Alarm Status:', status); |
|||
|
|||
if (status.enabled && !status.canSchedule) { |
|||
console.log('⚠️ Exact alarms enabled but cannot schedule - may need app restart'); |
|||
} |
|||
|
|||
} catch (error) { |
|||
console.error('❌ Monitoring error:', error); |
|||
} |
|||
}, 30000); // Check every 30 seconds
|
|||
|
|||
// Stop monitoring after 5 minutes
|
|||
setTimeout(() => { |
|||
clearInterval(monitorInterval); |
|||
console.log('✅ Exact alarm monitoring completed'); |
|||
}, 300000); |
|||
|
|||
} catch (error) { |
|||
console.error('❌ Exact alarm monitoring failed:', error); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Example: Handle permission denial gracefully |
|||
*/ |
|||
async function handlePermissionDenialGracefully() { |
|||
try { |
|||
console.log('Handling permission denial gracefully...'); |
|||
|
|||
// Configure fallback
|
|||
await configureAndroidExactAlarmFallback(); |
|||
|
|||
// Check status
|
|||
const status = await DailyNotification.getExactAlarmStatus(); |
|||
|
|||
if (!status.enabled) { |
|||
console.log('⚠️ Exact alarms not enabled, using windowed fallback'); |
|||
|
|||
// Schedule with fallback
|
|||
await DailyNotification.scheduleDailyNotification({ |
|||
url: 'https://api.example.com/daily-content', |
|||
time: '09:00', |
|||
title: 'Daily Update', |
|||
body: 'Your daily notification is ready (windowed fallback)' |
|||
}); |
|||
|
|||
console.log('✅ Notification scheduled with windowed fallback'); |
|||
|
|||
// Provide user guidance
|
|||
console.log('💡 To enable exact alarms:'); |
|||
console.log(' 1. Call requestExactAlarmPermission()'); |
|||
console.log(' 2. Or call openExactAlarmSettings()'); |
|||
console.log(' 3. Enable exact alarms in settings'); |
|||
|
|||
} else { |
|||
console.log('✅ Exact alarms enabled, scheduling normally'); |
|||
|
|||
await DailyNotification.scheduleDailyNotification({ |
|||
url: 'https://api.example.com/daily-content', |
|||
time: '09:00', |
|||
title: 'Daily Update', |
|||
body: 'Your daily notification is ready (exact timing)' |
|||
}); |
|||
} |
|||
|
|||
} catch (error) { |
|||
console.error('❌ Permission denial handling failed:', error); |
|||
} |
|||
} |
|||
|
|||
// Export examples for use
|
|||
export { |
|||
configureAndroidExactAlarmFallback, |
|||
checkExactAlarmStatus, |
|||
requestExactAlarmPermission, |
|||
openExactAlarmSettings, |
|||
scheduleWithFallbackHandling, |
|||
checkRebootRecoveryStatus, |
|||
demonstrateFallbackScenarios, |
|||
monitorExactAlarmChanges, |
|||
handlePermissionDenialGracefully |
|||
}; |
@ -0,0 +1,384 @@ |
|||
/** |
|||
* 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); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,381 @@ |
|||
/** |
|||
* DailyNotificationRebootRecoveryManager.java |
|||
* |
|||
* Android Reboot Recovery Manager for notification restoration |
|||
* Handles system reboots and time changes to restore scheduled notifications |
|||
* |
|||
* @author Matthew Raymer |
|||
* @version 1.0.0 |
|||
*/ |
|||
|
|||
package com.timesafari.dailynotification; |
|||
|
|||
import android.content.BroadcastReceiver; |
|||
import android.content.Context; |
|||
import android.content.Intent; |
|||
import android.content.IntentFilter; |
|||
import android.os.Build; |
|||
import android.util.Log; |
|||
|
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
/** |
|||
* Manages recovery from system reboots and time changes |
|||
* |
|||
* This class implements the critical recovery functionality: |
|||
* - Listens for system reboot broadcasts |
|||
* - Handles time change events |
|||
* - Restores scheduled notifications after reboot |
|||
* - Adjusts notification times after time changes |
|||
*/ |
|||
public class DailyNotificationRebootRecoveryManager { |
|||
|
|||
// MARK: - Constants
|
|||
|
|||
private static final String TAG = "DailyNotificationRebootRecoveryManager"; |
|||
|
|||
// Broadcast actions
|
|||
private static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED"; |
|||
private static final String ACTION_MY_PACKAGE_REPLACED = "android.intent.action.MY_PACKAGE_REPLACED"; |
|||
private static final String ACTION_PACKAGE_REPLACED = "android.intent.action.PACKAGE_REPLACED"; |
|||
private static final String ACTION_TIME_CHANGED = "android.intent.action.TIME_SET"; |
|||
private static final String ACTION_TIMEZONE_CHANGED = "android.intent.action.TIMEZONE_CHANGED"; |
|||
|
|||
// Recovery delay
|
|||
private static final long RECOVERY_DELAY_MS = TimeUnit.SECONDS.toMillis(5); |
|||
|
|||
// MARK: - Properties
|
|||
|
|||
private final Context context; |
|||
private final DailyNotificationScheduler scheduler; |
|||
private final DailyNotificationExactAlarmManager exactAlarmManager; |
|||
private final DailyNotificationRollingWindow rollingWindow; |
|||
|
|||
// Broadcast receivers
|
|||
private BootCompletedReceiver bootCompletedReceiver; |
|||
private TimeChangeReceiver timeChangeReceiver; |
|||
|
|||
// Recovery state
|
|||
private boolean recoveryInProgress = false; |
|||
private long lastRecoveryTime = 0; |
|||
|
|||
// MARK: - Initialization
|
|||
|
|||
/** |
|||
* Constructor |
|||
* |
|||
* @param context Application context |
|||
* @param scheduler Notification scheduler |
|||
* @param exactAlarmManager Exact alarm manager |
|||
* @param rollingWindow Rolling window manager |
|||
*/ |
|||
public DailyNotificationRebootRecoveryManager(Context context, |
|||
DailyNotificationScheduler scheduler, |
|||
DailyNotificationExactAlarmManager exactAlarmManager, |
|||
DailyNotificationRollingWindow rollingWindow) { |
|||
this.context = context; |
|||
this.scheduler = scheduler; |
|||
this.exactAlarmManager = exactAlarmManager; |
|||
this.rollingWindow = rollingWindow; |
|||
|
|||
Log.d(TAG, "RebootRecoveryManager initialized"); |
|||
} |
|||
|
|||
/** |
|||
* Register broadcast receivers |
|||
*/ |
|||
public void registerReceivers() { |
|||
try { |
|||
Log.d(TAG, "Registering broadcast receivers"); |
|||
|
|||
// Register boot completed receiver
|
|||
bootCompletedReceiver = new BootCompletedReceiver(); |
|||
IntentFilter bootFilter = new IntentFilter(); |
|||
bootFilter.addAction(ACTION_BOOT_COMPLETED); |
|||
bootFilter.addAction(ACTION_MY_PACKAGE_REPLACED); |
|||
bootFilter.addAction(ACTION_PACKAGE_REPLACED); |
|||
context.registerReceiver(bootCompletedReceiver, bootFilter); |
|||
|
|||
// Register time change receiver
|
|||
timeChangeReceiver = new TimeChangeReceiver(); |
|||
IntentFilter timeFilter = new IntentFilter(); |
|||
timeFilter.addAction(ACTION_TIME_CHANGED); |
|||
timeFilter.addAction(ACTION_TIMEZONE_CHANGED); |
|||
context.registerReceiver(timeChangeReceiver, timeFilter); |
|||
|
|||
Log.i(TAG, "Broadcast receivers registered successfully"); |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error registering broadcast receivers", e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Unregister broadcast receivers |
|||
*/ |
|||
public void unregisterReceivers() { |
|||
try { |
|||
Log.d(TAG, "Unregistering broadcast receivers"); |
|||
|
|||
if (bootCompletedReceiver != null) { |
|||
context.unregisterReceiver(bootCompletedReceiver); |
|||
bootCompletedReceiver = null; |
|||
} |
|||
|
|||
if (timeChangeReceiver != null) { |
|||
context.unregisterReceiver(timeChangeReceiver); |
|||
timeChangeReceiver = null; |
|||
} |
|||
|
|||
Log.i(TAG, "Broadcast receivers unregistered successfully"); |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error unregistering broadcast receivers", e); |
|||
} |
|||
} |
|||
|
|||
// MARK: - Recovery Methods
|
|||
|
|||
/** |
|||
* Handle system reboot recovery |
|||
* |
|||
* This method restores all scheduled notifications that were lost |
|||
* during the system reboot. |
|||
*/ |
|||
public void handleSystemReboot() { |
|||
try { |
|||
Log.i(TAG, "Handling system reboot recovery"); |
|||
|
|||
// Check if recovery is already in progress
|
|||
if (recoveryInProgress) { |
|||
Log.w(TAG, "Recovery already in progress, skipping"); |
|||
return; |
|||
} |
|||
|
|||
// Check if recovery was recently performed
|
|||
long currentTime = System.currentTimeMillis(); |
|||
if (currentTime - lastRecoveryTime < RECOVERY_DELAY_MS) { |
|||
Log.w(TAG, "Recovery performed recently, skipping"); |
|||
return; |
|||
} |
|||
|
|||
recoveryInProgress = true; |
|||
lastRecoveryTime = currentTime; |
|||
|
|||
// Perform recovery operations
|
|||
performRebootRecovery(); |
|||
|
|||
recoveryInProgress = false; |
|||
|
|||
Log.i(TAG, "System reboot recovery completed"); |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error handling system reboot", e); |
|||
recoveryInProgress = false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Handle time change recovery |
|||
* |
|||
* This method adjusts all scheduled notifications to account |
|||
* for system time changes. |
|||
*/ |
|||
public void handleTimeChange() { |
|||
try { |
|||
Log.i(TAG, "Handling time change recovery"); |
|||
|
|||
// Check if recovery is already in progress
|
|||
if (recoveryInProgress) { |
|||
Log.w(TAG, "Recovery already in progress, skipping"); |
|||
return; |
|||
} |
|||
|
|||
recoveryInProgress = true; |
|||
|
|||
// Perform time change recovery
|
|||
performTimeChangeRecovery(); |
|||
|
|||
recoveryInProgress = false; |
|||
|
|||
Log.i(TAG, "Time change recovery completed"); |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error handling time change", e); |
|||
recoveryInProgress = false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Perform reboot recovery operations |
|||
*/ |
|||
private void performRebootRecovery() { |
|||
try { |
|||
Log.d(TAG, "Performing reboot recovery operations"); |
|||
|
|||
// Wait a bit for system to stabilize
|
|||
Thread.sleep(2000); |
|||
|
|||
// Restore scheduled notifications
|
|||
scheduler.restoreScheduledNotifications(); |
|||
|
|||
// Restore rolling window
|
|||
rollingWindow.forceMaintenance(); |
|||
|
|||
// Log recovery statistics
|
|||
logRecoveryStatistics("reboot"); |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error performing reboot recovery", e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Perform time change recovery operations |
|||
*/ |
|||
private void performTimeChangeRecovery() { |
|||
try { |
|||
Log.d(TAG, "Performing time change recovery operations"); |
|||
|
|||
// Adjust scheduled notifications
|
|||
scheduler.adjustScheduledNotifications(); |
|||
|
|||
// Update rolling window
|
|||
rollingWindow.forceMaintenance(); |
|||
|
|||
// Log recovery statistics
|
|||
logRecoveryStatistics("time_change"); |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error performing time change recovery", e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Log recovery statistics |
|||
* |
|||
* @param recoveryType Type of recovery performed |
|||
*/ |
|||
private void logRecoveryStatistics(String recoveryType) { |
|||
try { |
|||
// Get recovery statistics
|
|||
int restoredCount = scheduler.getRestoredNotificationCount(); |
|||
int adjustedCount = scheduler.getAdjustedNotificationCount(); |
|||
|
|||
Log.i(TAG, String.format("Recovery statistics (%s): restored=%d, adjusted=%d", |
|||
recoveryType, restoredCount, adjustedCount)); |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error logging recovery statistics", e); |
|||
} |
|||
} |
|||
|
|||
// MARK: - Broadcast Receivers
|
|||
|
|||
/** |
|||
* Broadcast receiver for boot completed events |
|||
*/ |
|||
private class BootCompletedReceiver extends BroadcastReceiver { |
|||
@Override |
|||
public void onReceive(Context context, Intent intent) { |
|||
try { |
|||
String action = intent.getAction(); |
|||
Log.d(TAG, "BootCompletedReceiver received action: " + action); |
|||
|
|||
if (ACTION_BOOT_COMPLETED.equals(action) || |
|||
ACTION_MY_PACKAGE_REPLACED.equals(action) || |
|||
ACTION_PACKAGE_REPLACED.equals(action)) { |
|||
|
|||
// Handle system reboot
|
|||
handleSystemReboot(); |
|||
} |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error in BootCompletedReceiver", e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Broadcast receiver for time change events |
|||
*/ |
|||
private class TimeChangeReceiver extends BroadcastReceiver { |
|||
@Override |
|||
public void onReceive(Context context, Intent intent) { |
|||
try { |
|||
String action = intent.getAction(); |
|||
Log.d(TAG, "TimeChangeReceiver received action: " + action); |
|||
|
|||
if (ACTION_TIME_CHANGED.equals(action) || |
|||
ACTION_TIMEZONE_CHANGED.equals(action)) { |
|||
|
|||
// Handle time change
|
|||
handleTimeChange(); |
|||
} |
|||
|
|||
} catch (Exception e) { |
|||
Log.e(TAG, "Error in TimeChangeReceiver", e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// MARK: - Public Methods
|
|||
|
|||
/** |
|||
* Get recovery status |
|||
* |
|||
* @return Recovery status information |
|||
*/ |
|||
public RecoveryStatus getRecoveryStatus() { |
|||
return new RecoveryStatus( |
|||
recoveryInProgress, |
|||
lastRecoveryTime, |
|||
System.currentTimeMillis() - lastRecoveryTime |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Force recovery (for testing) |
|||
*/ |
|||
public void forceRecovery() { |
|||
Log.i(TAG, "Forcing recovery"); |
|||
handleSystemReboot(); |
|||
} |
|||
|
|||
/** |
|||
* Check if recovery is needed |
|||
* |
|||
* @return true if recovery is needed |
|||
*/ |
|||
public boolean isRecoveryNeeded() { |
|||
// Check if system was recently rebooted
|
|||
long currentTime = System.currentTimeMillis(); |
|||
long timeSinceLastRecovery = currentTime - lastRecoveryTime; |
|||
|
|||
// Recovery needed if more than 1 hour since last recovery
|
|||
return timeSinceLastRecovery > TimeUnit.HOURS.toMillis(1); |
|||
} |
|||
|
|||
// MARK: - Status Classes
|
|||
|
|||
/** |
|||
* Recovery status information |
|||
*/ |
|||
public static class RecoveryStatus { |
|||
public final boolean inProgress; |
|||
public final long lastRecoveryTime; |
|||
public final long timeSinceLastRecovery; |
|||
|
|||
public RecoveryStatus(boolean inProgress, long lastRecoveryTime, long timeSinceLastRecovery) { |
|||
this.inProgress = inProgress; |
|||
this.lastRecoveryTime = lastRecoveryTime; |
|||
this.timeSinceLastRecovery = timeSinceLastRecovery; |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return String.format("RecoveryStatus{inProgress=%s, lastRecovery=%d, timeSince=%d}", |
|||
inProgress, lastRecoveryTime, timeSinceLastRecovery); |
|||
} |
|||
} |
|||
} |
Loading…
Reference in new issue