Browse Source

feat(android): implement Phase 2.2 Android exact alarm fallback completion

- 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
Matthew Raymer 1 week ago
parent
commit
69aca905e1
  1. 321
      examples/phase2-2-android-fallback.ts
  2. 384
      src/android/DailyNotificationExactAlarmManager.java
  3. 171
      src/android/DailyNotificationPlugin.java
  4. 381
      src/android/DailyNotificationRebootRecoveryManager.java
  5. 80
      src/android/DailyNotificationScheduler.java
  6. 18
      src/definitions.ts
  7. 38
      src/web.ts
  8. 38
      src/web/index.ts

321
examples/phase2-2-android-fallback.ts

@ -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
};

384
src/android/DailyNotificationExactAlarmManager.java

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

171
src/android/DailyNotificationPlugin.java

@ -82,6 +82,12 @@ public class DailyNotificationPlugin extends Plugin {
// Rolling window management // Rolling window management
private DailyNotificationRollingWindow rollingWindow; private DailyNotificationRollingWindow rollingWindow;
// Exact alarm management
private DailyNotificationExactAlarmManager exactAlarmManager;
// Reboot recovery management
private DailyNotificationRebootRecoveryManager rebootRecoveryManager;
/** /**
* Initialize the plugin and create notification channel * Initialize the plugin and create notification channel
*/ */
@ -337,6 +343,12 @@ public class DailyNotificationPlugin extends Plugin {
isIOSPlatform isIOSPlatform
); );
// Initialize exact alarm manager
initializeExactAlarmManager();
// Initialize reboot recovery manager
initializeRebootRecoveryManager();
// Start initial window maintenance // Start initial window maintenance
rollingWindow.maintainRollingWindow(); rollingWindow.maintainRollingWindow();
@ -347,6 +359,55 @@ public class DailyNotificationPlugin extends Plugin {
} }
} }
/**
* Initialize exact alarm manager
*/
private void initializeExactAlarmManager() {
try {
Log.d(TAG, "Initializing exact alarm manager");
// Create exact alarm manager
exactAlarmManager = new DailyNotificationExactAlarmManager(
getContext(),
alarmManager,
scheduler
);
// Connect to scheduler
scheduler.setExactAlarmManager(exactAlarmManager);
Log.i(TAG, "Exact alarm manager initialized");
} catch (Exception e) {
Log.e(TAG, "Error initializing exact alarm manager", e);
}
}
/**
* Initialize reboot recovery manager
*/
private void initializeRebootRecoveryManager() {
try {
Log.d(TAG, "Initializing reboot recovery manager");
// Create reboot recovery manager
rebootRecoveryManager = new DailyNotificationRebootRecoveryManager(
getContext(),
scheduler,
exactAlarmManager,
rollingWindow
);
// Register broadcast receivers
rebootRecoveryManager.registerReceivers();
Log.i(TAG, "Reboot recovery manager initialized");
} catch (Exception e) {
Log.e(TAG, "Error initializing reboot recovery manager", e);
}
}
/** /**
* Schedule a daily notification with the specified options * Schedule a daily notification with the specified options
* *
@ -795,4 +856,114 @@ public class DailyNotificationPlugin extends Plugin {
call.reject("Error getting rolling window stats: " + e.getMessage()); call.reject("Error getting rolling window stats: " + e.getMessage());
} }
} }
/**
* Get exact alarm status
*
* @param call Plugin call
*/
@PluginMethod
public void getExactAlarmStatus(PluginCall call) {
try {
Log.d(TAG, "Exact alarm status requested");
if (exactAlarmManager != null) {
DailyNotificationExactAlarmManager.ExactAlarmStatus status = exactAlarmManager.getExactAlarmStatus();
JSObject result = new JSObject();
result.put("supported", status.supported);
result.put("enabled", status.enabled);
result.put("canSchedule", status.canSchedule);
result.put("fallbackWindow", status.fallbackWindow.description);
call.resolve(result);
} else {
call.reject("Exact alarm manager not initialized");
}
} catch (Exception e) {
Log.e(TAG, "Error getting exact alarm status", e);
call.reject("Error getting exact alarm status: " + e.getMessage());
}
}
/**
* Request exact alarm permission
*
* @param call Plugin call
*/
@PluginMethod
public void requestExactAlarmPermission(PluginCall call) {
try {
Log.d(TAG, "Exact alarm permission request");
if (exactAlarmManager != null) {
boolean success = exactAlarmManager.requestExactAlarmPermission();
if (success) {
call.resolve();
} else {
call.reject("Failed to request exact alarm permission");
}
} else {
call.reject("Exact alarm manager not initialized");
}
} catch (Exception e) {
Log.e(TAG, "Error requesting exact alarm permission", e);
call.reject("Error requesting exact alarm permission: " + e.getMessage());
}
}
/**
* Open exact alarm settings
*
* @param call Plugin call
*/
@PluginMethod
public void openExactAlarmSettings(PluginCall call) {
try {
Log.d(TAG, "Opening exact alarm settings");
if (exactAlarmManager != null) {
boolean success = exactAlarmManager.openExactAlarmSettings();
if (success) {
call.resolve();
} else {
call.reject("Failed to open exact alarm settings");
}
} else {
call.reject("Exact alarm manager not initialized");
}
} catch (Exception e) {
Log.e(TAG, "Error opening exact alarm settings", e);
call.reject("Error opening exact alarm settings: " + e.getMessage());
}
}
/**
* Get reboot recovery status
*
* @param call Plugin call
*/
@PluginMethod
public void getRebootRecoveryStatus(PluginCall call) {
try {
Log.d(TAG, "Reboot recovery status requested");
if (rebootRecoveryManager != null) {
DailyNotificationRebootRecoveryManager.RecoveryStatus status = rebootRecoveryManager.getRecoveryStatus();
JSObject result = new JSObject();
result.put("inProgress", status.inProgress);
result.put("lastRecoveryTime", status.lastRecoveryTime);
result.put("timeSinceLastRecovery", status.timeSinceLastRecovery);
result.put("recoveryNeeded", rebootRecoveryManager.isRecoveryNeeded());
call.resolve(result);
} else {
call.reject("Reboot recovery manager not initialized");
}
} catch (Exception e) {
Log.e(TAG, "Error getting reboot recovery status", e);
call.reject("Error getting reboot recovery status: " + e.getMessage());
}
}
} }

381
src/android/DailyNotificationRebootRecoveryManager.java

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

80
src/android/DailyNotificationScheduler.java

@ -39,6 +39,9 @@ public class DailyNotificationScheduler {
// TTL enforcement // TTL enforcement
private DailyNotificationTTLEnforcer ttlEnforcer; private DailyNotificationTTLEnforcer ttlEnforcer;
// Exact alarm management
private DailyNotificationExactAlarmManager exactAlarmManager;
/** /**
* Constructor * Constructor
* *
@ -61,6 +64,16 @@ public class DailyNotificationScheduler {
Log.d(TAG, "TTL enforcer set for freshness validation"); 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 * Schedule a notification for delivery
* *
@ -130,7 +143,12 @@ public class DailyNotificationScheduler {
*/ */
private boolean scheduleAlarm(PendingIntent pendingIntent, long triggerTime) { private boolean scheduleAlarm(PendingIntent pendingIntent, long triggerTime) {
try { try {
// Check if we can use exact alarms // Use exact alarm manager if available
if (exactAlarmManager != null) {
return exactAlarmManager.scheduleAlarm(pendingIntent, triggerTime);
}
// Fallback to legacy scheduling
if (canUseExactAlarms()) { if (canUseExactAlarms()) {
return scheduleExactAlarm(pendingIntent, triggerTime); return scheduleExactAlarm(pendingIntent, triggerTime);
} else { } else {
@ -397,4 +415,64 @@ public class DailyNotificationScheduler {
return calendar.getTimeInMillis(); 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;
}
} }

18
src/definitions.ts

@ -268,6 +268,24 @@ export interface DailyNotificationPlugin {
timeUntilNextMaintenance: number; timeUntilNextMaintenance: number;
}>; }>;
// Exact alarm management
getExactAlarmStatus(): Promise<{
supported: boolean;
enabled: boolean;
canSchedule: boolean;
fallbackWindow: string;
}>;
requestExactAlarmPermission(): Promise<void>;
openExactAlarmSettings(): Promise<void>;
// Reboot recovery management
getRebootRecoveryStatus(): Promise<{
inProgress: boolean;
lastRecoveryTime: number;
timeSinceLastRecovery: number;
recoveryNeeded: boolean;
}>;
// Existing methods // Existing methods
scheduleDailyNotification(options: NotificationOptions | ScheduleOptions): Promise<void>; scheduleDailyNotification(options: NotificationOptions | ScheduleOptions): Promise<void>;
getLastNotification(): Promise<NotificationResponse | null>; getLastNotification(): Promise<NotificationResponse | null>;

38
src/web.ts

@ -30,6 +30,44 @@ export class DailyNotificationWeb extends WebPlugin implements DailyNotification
timeUntilNextMaintenance: 0 timeUntilNextMaintenance: 0
}; };
} }
async getExactAlarmStatus(): Promise<{
supported: boolean;
enabled: boolean;
canSchedule: boolean;
fallbackWindow: string;
}> {
console.log('Get exact alarm status called on web platform');
return {
supported: false,
enabled: false,
canSchedule: false,
fallbackWindow: 'Not applicable on web'
};
}
async requestExactAlarmPermission(): Promise<void> {
console.log('Request exact alarm permission called on web platform');
}
async openExactAlarmSettings(): Promise<void> {
console.log('Open exact alarm settings called on web platform');
}
async getRebootRecoveryStatus(): Promise<{
inProgress: boolean;
lastRecoveryTime: number;
timeSinceLastRecovery: number;
recoveryNeeded: boolean;
}> {
console.log('Get reboot recovery status called on web platform');
return {
inProgress: false,
lastRecoveryTime: 0,
timeSinceLastRecovery: 0,
recoveryNeeded: false
};
}
async scheduleDailyNotification(_options: NotificationOptions | any): Promise<void> { async scheduleDailyNotification(_options: NotificationOptions | any): Promise<void> {
// Web implementation placeholder // Web implementation placeholder

38
src/web/index.ts

@ -40,6 +40,44 @@ export class DailyNotificationWeb implements DailyNotificationPlugin {
timeUntilNextMaintenance: 0 timeUntilNextMaintenance: 0
}; };
} }
async getExactAlarmStatus(): Promise<{
supported: boolean;
enabled: boolean;
canSchedule: boolean;
fallbackWindow: string;
}> {
console.log('Get exact alarm status called on web platform');
return {
supported: false,
enabled: false,
canSchedule: false,
fallbackWindow: 'Not applicable on web'
};
}
async requestExactAlarmPermission(): Promise<void> {
console.log('Request exact alarm permission called on web platform');
}
async openExactAlarmSettings(): Promise<void> {
console.log('Open exact alarm settings called on web platform');
}
async getRebootRecoveryStatus(): Promise<{
inProgress: boolean;
lastRecoveryTime: number;
timeSinceLastRecovery: number;
recoveryNeeded: boolean;
}> {
console.log('Get reboot recovery status called on web platform');
return {
inProgress: false,
lastRecoveryTime: 0,
timeSinceLastRecovery: 0,
recoveryNeeded: false
};
}
/** /**
* Schedule a daily notification * Schedule a daily notification

Loading…
Cancel
Save