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:
@@ -76,11 +76,13 @@ class BootReceiver : BroadcastReceiver() {
|
|||||||
// Reschedule AlarmManager notification
|
// Reschedule AlarmManager notification
|
||||||
val nextRunTime = calculateNextRunTime(schedule)
|
val nextRunTime = calculateNextRunTime(schedule)
|
||||||
if (nextRunTime > System.currentTimeMillis()) {
|
if (nextRunTime > System.currentTimeMillis()) {
|
||||||
|
val (title, body) = ReactivationManager.getTitleBodyForSchedule(db, schedule)
|
||||||
|
?: Pair("Daily Notification", "Your daily update is ready")
|
||||||
val config = UserNotificationConfig(
|
val config = UserNotificationConfig(
|
||||||
enabled = schedule.enabled,
|
enabled = schedule.enabled,
|
||||||
schedule = schedule.cron ?: schedule.clockTime ?: "0 9 * * *",
|
schedule = schedule.cron ?: schedule.clockTime ?: "0 9 * * *",
|
||||||
title = "Daily Notification",
|
title = title,
|
||||||
body = "Your daily update is ready",
|
body = body,
|
||||||
sound = true,
|
sound = true,
|
||||||
vibration = true,
|
vibration = true,
|
||||||
priority = "normal"
|
priority = "normal"
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import androidx.work.ExistingWorkPolicy;
|
|||||||
import androidx.work.OneTimeWorkRequest;
|
import androidx.work.OneTimeWorkRequest;
|
||||||
import androidx.work.WorkManager;
|
import androidx.work.WorkManager;
|
||||||
|
|
||||||
|
import com.timesafari.dailynotification.entities.NotificationContentEntity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcast receiver for daily notification alarms
|
* 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
|
// Create unique work name based on notification ID to prevent duplicates
|
||||||
// WorkManager will automatically skip if work with this name already exists
|
// WorkManager will automatically skip if work with this name already exists
|
||||||
String workName = "display_" + notificationId;
|
String workName = "display_" + notificationId;
|
||||||
|
|
||||||
// Extract static reminder extras from intent if present
|
// Extract static reminder extras from intent if present
|
||||||
// Static reminders have title/body in Intent extras, not in storage
|
// 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);
|
boolean isStaticReminder = intent.getBooleanExtra("is_static_reminder", false);
|
||||||
String title = intent.getStringExtra("title");
|
String title = intent.getStringExtra("title");
|
||||||
String body = intent.getStringExtra("body");
|
String body = intent.getStringExtra("body");
|
||||||
@@ -119,12 +122,34 @@ public class DailyNotificationReceiver extends BroadcastReceiver {
|
|||||||
if (priority == null) {
|
if (priority == null) {
|
||||||
priority = "normal";
|
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()
|
Data.Builder dataBuilder = new Data.Builder()
|
||||||
.putString("notification_id", notificationId)
|
.putString("notification_id", notificationId)
|
||||||
.putString("action", "display")
|
.putString("action", "display")
|
||||||
.putBoolean("is_static_reminder", isStaticReminder);
|
.putBoolean("is_static_reminder", isStaticReminder);
|
||||||
|
|
||||||
// Add static reminder data if present
|
// Add static reminder data if present
|
||||||
if (isStaticReminder && title != null && body != null) {
|
if (isStaticReminder && title != null && body != null) {
|
||||||
dataBuilder.putString("title", title)
|
dataBuilder.putString("title", title)
|
||||||
@@ -134,7 +159,7 @@ public class DailyNotificationReceiver extends BroadcastReceiver {
|
|||||||
.putString("priority", priority);
|
.putString("priority", priority);
|
||||||
Log.d(TAG, "DN|WORK_ENQUEUE static_reminder id=" + notificationId);
|
Log.d(TAG, "DN|WORK_ENQUEUE static_reminder id=" + notificationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Data inputData = dataBuilder.build();
|
Data inputData = dataBuilder.build();
|
||||||
|
|
||||||
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(DailyNotificationWorker.class)
|
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);
|
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
|
* Handle notification intent
|
||||||
|
|||||||
@@ -177,8 +177,15 @@ public class DailyNotificationWorker extends Worker {
|
|||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
// JIT Freshness Re-check (Soft TTL) - skip for static reminders
|
// JIT Freshness Re-check (Soft TTL) - skip when content has title/body from Room
|
||||||
content = performJITFreshnessCheck(content);
|
// (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
|
// Display the notification
|
||||||
|
|||||||
@@ -41,6 +41,26 @@ class ReactivationManager(private val context: Context) {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "DNP-REACTIVATION"
|
private const val TAG = "DNP-REACTIVATION"
|
||||||
private const val RECOVERY_TIMEOUT_SECONDS = 2L
|
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
|
* Run boot-time recovery
|
||||||
@@ -275,11 +295,13 @@ class ReactivationManager(private val context: Context) {
|
|||||||
db: DailyNotificationDatabase
|
db: DailyNotificationDatabase
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
val (title, body) = getTitleBodyForSchedule(db, schedule)
|
||||||
|
?: Pair("Daily Notification", "Your daily update is ready")
|
||||||
val config = UserNotificationConfig(
|
val config = UserNotificationConfig(
|
||||||
enabled = schedule.enabled,
|
enabled = schedule.enabled,
|
||||||
schedule = schedule.cron ?: schedule.clockTime ?: "0 9 * * *",
|
schedule = schedule.cron ?: schedule.clockTime ?: "0 9 * * *",
|
||||||
title = "Daily Notification",
|
title = title,
|
||||||
body = "Your daily update is ready",
|
body = body,
|
||||||
sound = true,
|
sound = true,
|
||||||
vibration = true,
|
vibration = true,
|
||||||
priority = "normal"
|
priority = "normal"
|
||||||
@@ -816,13 +838,13 @@ class ReactivationManager(private val context: Context) {
|
|||||||
db: DailyNotificationDatabase
|
db: DailyNotificationDatabase
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// Use existing BootReceiver logic for calculating next run time
|
val (title, body) = ReactivationManager.getTitleBodyForSchedule(db, schedule)
|
||||||
// For now, use schedule.nextRunAt directly
|
?: Pair("Daily Notification", "Your daily update is ready")
|
||||||
val config = UserNotificationConfig(
|
val config = UserNotificationConfig(
|
||||||
enabled = schedule.enabled,
|
enabled = schedule.enabled,
|
||||||
schedule = schedule.cron ?: schedule.clockTime ?: "0 9 * * *",
|
schedule = schedule.cron ?: schedule.clockTime ?: "0 9 * * *",
|
||||||
title = "Daily Notification",
|
title = title,
|
||||||
body = "Your daily update is ready",
|
body = body,
|
||||||
sound = true,
|
sound = true,
|
||||||
vibration = true,
|
vibration = true,
|
||||||
priority = "normal"
|
priority = "normal"
|
||||||
@@ -1045,11 +1067,13 @@ class ReactivationManager(private val context: Context) {
|
|||||||
db: DailyNotificationDatabase
|
db: DailyNotificationDatabase
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
val (title, body) = ReactivationManager.getTitleBodyForSchedule(db, schedule)
|
||||||
|
?: Pair("Daily Notification", "Your daily update is ready")
|
||||||
val config = UserNotificationConfig(
|
val config = UserNotificationConfig(
|
||||||
enabled = schedule.enabled,
|
enabled = schedule.enabled,
|
||||||
schedule = schedule.cron ?: schedule.clockTime ?: "0 9 * * *",
|
schedule = schedule.cron ?: schedule.clockTime ?: "0 9 * * *",
|
||||||
title = "Daily Notification",
|
title = title,
|
||||||
body = "Your daily update is ready",
|
body = body,
|
||||||
sound = true,
|
sound = true,
|
||||||
vibration = true,
|
vibration = true,
|
||||||
priority = "normal"
|
priority = "normal"
|
||||||
|
|||||||
Reference in New Issue
Block a user