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.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user