fix(android): restore user title/body after reboot so notification doesn't show fallback text

After device restart, PendingIntent extras (title, body, is_static_reminder) can be
missing when the alarm fires, so the worker took the Room/JIT path and showed
fallback text instead of the user's message.

- DailyNotificationReceiver: when intent has notification_id but missing title/body,
  load NotificationContentEntity from Room and pass title/body into Worker input
  with is_static_reminder=true.
- ReactivationManager: add getTitleBodyForSchedule(); use persisted title/body in
  rescheduleAlarm and rescheduleAlarmForBoot (and inner boot helper) instead of
  hardcoded "Daily Notification" / "Your daily update is ready".
- BootReceiver: use ReactivationManager.getTitleBodyForSchedule() when building
  UserNotificationConfig for notify schedules after boot.
- DailyNotificationWorker: when content from Room has both title and body, skip
  performJITFreshnessCheck so user text is not overwritten by fetcher placeholder.

Ref: plugin-feedback-android-post-reboot-fallback-text (crowd-funder-for-time-pwa)
This commit is contained in:
Jose Olarte III
2026-02-23 18:00:36 +08:00
parent c2b1a60804
commit 1157a0f1ef
4 changed files with 83 additions and 16 deletions

View File

@@ -76,11 +76,13 @@ class BootReceiver : BroadcastReceiver() {
// Reschedule AlarmManager notification
val nextRunTime = calculateNextRunTime(schedule)
if (nextRunTime > System.currentTimeMillis()) {
val (title, body) = ReactivationManager.getTitleBodyForSchedule(db, schedule)
?: Pair("Daily Notification", "Your daily update is ready")
val config = UserNotificationConfig(
enabled = schedule.enabled,
schedule = schedule.cron ?: schedule.clockTime ?: "0 9 * * *",
title = "Daily Notification",
body = "Your daily update is ready",
title = title,
body = body,
sound = true,
vibration = true,
priority = "normal"

View File

@@ -25,6 +25,8 @@ import androidx.work.ExistingWorkPolicy;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import com.timesafari.dailynotification.entities.NotificationContentEntity;
/**
* Broadcast receiver for daily notification alarms
*
@@ -107,9 +109,10 @@ public class DailyNotificationReceiver extends BroadcastReceiver {
// Create unique work name based on notification ID to prevent duplicates
// WorkManager will automatically skip if work with this name already exists
String workName = "display_" + notificationId;
// Extract static reminder extras from intent if present
// Static reminders have title/body in Intent extras, not in storage
// After reboot, Intent extras may be missing; restore from DB when possible
boolean isStaticReminder = intent.getBooleanExtra("is_static_reminder", false);
String title = intent.getStringExtra("title");
String body = intent.getStringExtra("body");
@@ -119,12 +122,34 @@ public class DailyNotificationReceiver extends BroadcastReceiver {
if (priority == null) {
priority = "normal";
}
// Post-reboot fallback: if we have notification_id but missing title/body, load from DB
// so the worker can display user-set text instead of fallback (see plugin-feedback-android-post-reboot-fallback-text)
if ((title == null || title.isEmpty() || body == null || body.isEmpty()) && notificationId != null) {
try {
com.timesafari.dailynotification.DailyNotificationDatabase db =
com.timesafari.dailynotification.DailyNotificationDatabase.getInstance(context);
NotificationContentEntity entity = db.notificationContentDao().getNotificationById(notificationId);
if (entity != null && entity.title != null && !entity.title.isEmpty()
&& entity.body != null && !entity.body.isEmpty()) {
title = entity.title;
body = entity.body;
isStaticReminder = true;
sound = entity.soundEnabled;
vibration = entity.vibrationEnabled;
priority = mapPriorityFromInt(entity.priority);
Log.d(TAG, "DN|WORK_ENQUEUE static_reminder_from_db id=" + notificationId);
}
} catch (Exception e) {
Log.w(TAG, "DN|WORK_ENQUEUE db_fallback_failed id=" + notificationId + " err=" + e.getMessage());
}
}
Data.Builder dataBuilder = new Data.Builder()
.putString("notification_id", notificationId)
.putString("action", "display")
.putBoolean("is_static_reminder", isStaticReminder);
// Add static reminder data if present
if (isStaticReminder && title != null && body != null) {
dataBuilder.putString("title", title)
@@ -134,7 +159,7 @@ public class DailyNotificationReceiver extends BroadcastReceiver {
.putString("priority", priority);
Log.d(TAG, "DN|WORK_ENQUEUE static_reminder id=" + notificationId);
}
Data inputData = dataBuilder.build();
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(DailyNotificationWorker.class)
@@ -195,6 +220,15 @@ public class DailyNotificationReceiver extends BroadcastReceiver {
Log.e(TAG, "DN|WORK_ENQUEUE_ERR dismiss=" + notificationId + " err=" + e.getMessage(), e);
}
}
/**
* Map Room priority int to string for Worker input.
*/
private static String mapPriorityFromInt(int p) {
if (p >= 2) return "high";
if (p <= -1) return "low";
return "normal";
}
/**
* Handle notification intent

View File

@@ -177,8 +177,15 @@ public class DailyNotificationWorker extends Worker {
return Result.success();
}
// JIT Freshness Re-check (Soft TTL) - skip for static reminders
content = performJITFreshnessCheck(content);
// JIT Freshness Re-check (Soft TTL) - skip when content has title/body from Room
// (e.g. post-reboot static reminder restored from DB so we don't overwrite with fetcher fallback)
boolean hasTitleBody = content.getTitle() != null && !content.getTitle().isEmpty()
&& content.getBody() != null && !content.getBody().isEmpty();
if (!hasTitleBody) {
content = performJITFreshnessCheck(content);
} else {
Log.d(TAG, "DN|DISPLAY_USE_ROOM_CONTENT id=" + notificationId + " (skip JIT)");
}
}
// Display the notification

View File

@@ -41,6 +41,26 @@ class ReactivationManager(private val context: Context) {
companion object {
private const val TAG = "DNP-REACTIVATION"
private const val RECOVERY_TIMEOUT_SECONDS = 2L
/**
* Load persisted title/body for a schedule from NotificationContentEntity (post-reboot recovery).
* Tries schedule.id then "daily_${schedule.id}" to match NotifyReceiver/ScheduleHelper id convention.
* Internal so BootReceiver can use when rescheduling after boot.
*/
internal fun getTitleBodyForSchedule(db: DailyNotificationDatabase, schedule: Schedule): Pair<String, String>? {
val entity = try {
db.notificationContentDao().getNotificationById(schedule.id)
} catch (_: Exception) {
null
} ?: try {
db.notificationContentDao().getNotificationById("daily_${schedule.id}")
} catch (_: Exception) {
null
} ?: return null
val t = entity.title?.takeIf { it.isNotBlank() } ?: return null
val b = entity.body?.takeIf { it.isNotBlank() } ?: return null
return Pair(t, b)
}
/**
* Run boot-time recovery
@@ -275,11 +295,13 @@ class ReactivationManager(private val context: Context) {
db: DailyNotificationDatabase
) {
try {
val (title, body) = getTitleBodyForSchedule(db, schedule)
?: Pair("Daily Notification", "Your daily update is ready")
val config = UserNotificationConfig(
enabled = schedule.enabled,
schedule = schedule.cron ?: schedule.clockTime ?: "0 9 * * *",
title = "Daily Notification",
body = "Your daily update is ready",
title = title,
body = body,
sound = true,
vibration = true,
priority = "normal"
@@ -816,13 +838,13 @@ class ReactivationManager(private val context: Context) {
db: DailyNotificationDatabase
) {
try {
// Use existing BootReceiver logic for calculating next run time
// For now, use schedule.nextRunAt directly
val (title, body) = ReactivationManager.getTitleBodyForSchedule(db, schedule)
?: Pair("Daily Notification", "Your daily update is ready")
val config = UserNotificationConfig(
enabled = schedule.enabled,
schedule = schedule.cron ?: schedule.clockTime ?: "0 9 * * *",
title = "Daily Notification",
body = "Your daily update is ready",
title = title,
body = body,
sound = true,
vibration = true,
priority = "normal"
@@ -1045,11 +1067,13 @@ class ReactivationManager(private val context: Context) {
db: DailyNotificationDatabase
) {
try {
val (title, body) = ReactivationManager.getTitleBodyForSchedule(db, schedule)
?: Pair("Daily Notification", "Your daily update is ready")
val config = UserNotificationConfig(
enabled = schedule.enabled,
schedule = schedule.cron ?: schedule.clockTime ?: "0 9 * * *",
title = "Daily Notification",
body = "Your daily update is ready",
title = title,
body = body,
sound = true,
vibration = true,
priority = "normal"