Browse Source

fix(android): implement proper boot receiver with Direct Boot support

- Add android:exported="true" for API 31+ compatibility
- Add android:directBootAware="true" for Direct Boot handling
- Add LOCKED_BOOT_COMPLETED action for early boot recovery
- Remove PACKAGE_REPLACED action (not needed for our use case)
- Implement handleLockedBootCompleted() for Direct Boot safety
- Use device protected storage context for Direct Boot operations
- Add comprehensive logging for boot receiver events

This fixes Android 10+ boot receiver restrictions and ensures
notifications are restored after device reboots and app updates.
master
Matthew Raymer 1 week ago
parent
commit
c42814e60b
  1. 17
      android/app/src/main/AndroidManifest.xml
  2. 167
      android/plugin/src/main/java/com/timesafari/dailynotification/BootReceiver.java

17
android/app/src/main/AndroidManifest.xml

@ -26,19 +26,26 @@
<!-- Daily Notification Plugin Receivers --> <!-- Daily Notification Plugin Receivers -->
<receiver <receiver
android:name="com.timesafari.dailynotification.NotifyReceiver" android:name="com.timesafari.dailynotification.DailyNotificationReceiver"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false">
<intent-filter>
<action android:name="com.timesafari.daily.NOTIFICATION" />
</intent-filter>
</receiver>
<receiver <receiver
android:name="com.timesafari.dailynotification.BootReceiver" android:name="com.timesafari.dailynotification.BootReceiver"
android:enabled="true" android:enabled="true"
android:exported="true"> android:exported="true"
android:directBootAware="true">
<intent-filter android:priority="1000"> <intent-filter android:priority="1000">
<!-- Delivered very early after reboot (before unlock) -->
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<!-- Delivered after the user unlocks / credential-encrypted storage is available -->
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
<!-- Delivered after app update; great for rescheduling alarms without reboot -->
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" /> <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<data android:scheme="package" />
</intent-filter> </intent-filter>
</receiver> </receiver>

167
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<NotificationContent> 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);
}
}
}
Loading…
Cancel
Save