diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index fab80d6..a1a4759 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -26,19 +26,26 @@ + android:exported="false"> + + + + + android:exported="true" + android:directBootAware="true"> + + + + - - diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/BootReceiver.java b/android/plugin/src/main/java/com/timesafari/dailynotification/BootReceiver.java new file mode 100644 index 0000000..483e538 --- /dev/null +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/BootReceiver.java @@ -0,0 +1,167 @@ +/** + * BootReceiver.java + * + * Android Boot Receiver for DailyNotification plugin + * Handles system boot events 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.util.Log; + +/** + * Broadcast receiver for system boot events + * + * This receiver is triggered when: + * - Device boots up (BOOT_COMPLETED) + * - App is updated (MY_PACKAGE_REPLACED) + * - Any package is updated (PACKAGE_REPLACED) + * + * It ensures that scheduled notifications are restored after system events + * that might have cleared the alarm manager. + */ +public class BootReceiver extends BroadcastReceiver { + + private static final String TAG = "BootReceiver"; + + // Broadcast actions we handle + private static final String ACTION_LOCKED_BOOT_COMPLETED = "android.intent.action.LOCKED_BOOT_COMPLETED"; + 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"; + + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null || intent.getAction() == null) { + Log.w(TAG, "Received null intent or action"); + return; + } + + String action = intent.getAction(); + Log.d(TAG, "Received broadcast: " + action); + + try { + switch (action) { + case ACTION_LOCKED_BOOT_COMPLETED: + handleLockedBootCompleted(context); + break; + + case ACTION_BOOT_COMPLETED: + handleBootCompleted(context); + break; + + case ACTION_MY_PACKAGE_REPLACED: + handlePackageReplaced(context, intent); + break; + + default: + Log.w(TAG, "Unknown action: " + action); + break; + } + } catch (Exception e) { + Log.e(TAG, "Error handling broadcast: " + action, e); + } + } + + /** + * Handle locked boot completion (before user unlock) + * + * @param context Application context + */ + private void handleLockedBootCompleted(Context context) { + Log.i(TAG, "Locked boot completed - preparing for recovery"); + + try { + // Use device protected storage context for Direct Boot + Context deviceProtectedContext = context; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + deviceProtectedContext = context.createDeviceProtectedStorageContext(); + } + + // Minimal work here - just log that we're ready + // Full recovery will happen on BOOT_COMPLETED when storage is available + Log.i(TAG, "Locked boot completed - ready for full recovery on unlock"); + + } catch (Exception e) { + Log.e(TAG, "Error during locked boot completion", e); + } + } + + /** + * Handle device boot completion (after user unlock) + * + * @param context Application context + */ + private void handleBootCompleted(Context context) { + Log.i(TAG, "Device boot completed - restoring notifications"); + + try { + // Initialize storage to load saved notifications + DailyNotificationStorage storage = new DailyNotificationStorage(context); + + // Get all saved notifications + java.util.List notifications = storage.getAllNotifications(); + + if (notifications.isEmpty()) { + Log.i(TAG, "No notifications to recover"); + return; + } + + Log.i(TAG, "Found " + notifications.size() + " notifications to recover"); + + // Initialize scheduler for rescheduling + android.app.AlarmManager alarmManager = (android.app.AlarmManager) + context.getSystemService(android.content.Context.ALARM_SERVICE); + DailyNotificationScheduler scheduler = new DailyNotificationScheduler(context, alarmManager); + + // Reschedule each notification + int recoveredCount = 0; + for (NotificationContent notification : notifications) { + try { + // Only reschedule future notifications + if (notification.getScheduledTime() > System.currentTimeMillis()) { + boolean scheduled = scheduler.scheduleNotification(notification); + if (scheduled) { + recoveredCount++; + Log.d(TAG, "Recovered notification: " + notification.getId()); + } else { + Log.w(TAG, "Failed to recover notification: " + notification.getId()); + } + } else { + Log.d(TAG, "Skipping past notification: " + notification.getId()); + } + } catch (Exception e) { + Log.e(TAG, "Error recovering notification: " + notification.getId(), e); + } + } + + Log.i(TAG, "Notification recovery completed: " + recoveredCount + "/" + notifications.size() + " recovered"); + + } catch (Exception e) { + Log.e(TAG, "Error during boot recovery", e); + } + } + + /** + * Handle package replacement (app update) + * + * @param context Application context + * @param intent Broadcast intent + */ + private void handlePackageReplaced(Context context, Intent intent) { + Log.i(TAG, "Package replaced - restoring notifications"); + + try { + // Use the same recovery logic as boot + handleBootCompleted(context); + + } catch (Exception e) { + Log.e(TAG, "Error during package replacement recovery", e); + } + } +}