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