fix(android): support static reminder notifications and ensure channel exists
Static reminders scheduled via scheduleDailyNotification() with isStaticReminder=true were being skipped because they don't have content in storage - title/body are in Intent extras. Fixed by: - DailyNotificationReceiver: Extract static reminder extras from Intent and pass to WorkManager as input data - DailyNotificationWorker: Check for static reminder flag in input data and create NotificationContent from input data instead of loading from storage - DailyNotificationWorker: Ensure notification channel exists before displaying (fixes "No Channel found" errors) Also updated prefetch timing from 5 minutes to 2 minutes before notification time in plugin code and web UI.
This commit is contained in:
@@ -1286,9 +1286,9 @@ open class DailyNotificationPlugin : Plugin() {
|
||||
reminderId = scheduleId
|
||||
)
|
||||
|
||||
// Always schedule prefetch 5 minutes before notification
|
||||
// Always schedule prefetch 2 minutes before notification
|
||||
// (URL is optional - native fetcher will be used if registered)
|
||||
val fetchTime = nextRunTime - (5 * 60 * 1000L) // 5 minutes before
|
||||
val fetchTime = nextRunTime - (2 * 60 * 1000L) // 2 minutes before
|
||||
val delayMs = fetchTime - System.currentTimeMillis()
|
||||
|
||||
if (delayMs > 0) {
|
||||
|
||||
@@ -68,7 +68,8 @@ public class DailyNotificationReceiver extends BroadcastReceiver {
|
||||
}
|
||||
|
||||
// Enqueue work immediately - don't block receiver
|
||||
enqueueNotificationWork(context, notificationId);
|
||||
// Pass the full intent to extract static reminder extras
|
||||
enqueueNotificationWork(context, notificationId, intent);
|
||||
Log.d(TAG, "DN|RECEIVE_OK enqueued=" + notificationId);
|
||||
|
||||
} else if ("com.timesafari.daily.DISMISS".equals(action)) {
|
||||
@@ -99,17 +100,42 @@ public class DailyNotificationReceiver extends BroadcastReceiver {
|
||||
*
|
||||
* @param context Application context
|
||||
* @param notificationId ID of notification to process
|
||||
* @param intent Intent containing notification data (may include static reminder extras)
|
||||
*/
|
||||
private void enqueueNotificationWork(Context context, String notificationId) {
|
||||
private void enqueueNotificationWork(Context context, String notificationId, Intent intent) {
|
||||
try {
|
||||
// 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;
|
||||
|
||||
Data inputData = new Data.Builder()
|
||||
// Extract static reminder extras from intent if present
|
||||
// Static reminders have title/body in Intent extras, not in storage
|
||||
boolean isStaticReminder = intent.getBooleanExtra("is_static_reminder", false);
|
||||
String title = intent.getStringExtra("title");
|
||||
String body = intent.getStringExtra("body");
|
||||
boolean sound = intent.getBooleanExtra("sound", true);
|
||||
boolean vibration = intent.getBooleanExtra("vibration", true);
|
||||
String priority = intent.getStringExtra("priority");
|
||||
if (priority == null) {
|
||||
priority = "normal";
|
||||
}
|
||||
|
||||
Data.Builder dataBuilder = new Data.Builder()
|
||||
.putString("notification_id", notificationId)
|
||||
.putString("action", "display")
|
||||
.build();
|
||||
.putBoolean("is_static_reminder", isStaticReminder);
|
||||
|
||||
// Add static reminder data if present
|
||||
if (isStaticReminder && title != null && body != null) {
|
||||
dataBuilder.putString("title", title)
|
||||
.putString("body", body)
|
||||
.putBoolean("sound", sound)
|
||||
.putBoolean("vibration", vibration)
|
||||
.putString("priority", priority);
|
||||
Log.d(TAG, "DN|WORK_ENQUEUE static_reminder id=" + notificationId);
|
||||
}
|
||||
|
||||
Data inputData = dataBuilder.build();
|
||||
|
||||
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(DailyNotificationWorker.class)
|
||||
.setInputData(inputData)
|
||||
|
||||
@@ -127,25 +127,60 @@ public class DailyNotificationWorker extends Worker {
|
||||
try {
|
||||
Log.d(TAG, "DN|DISPLAY_START id=" + notificationId);
|
||||
|
||||
// Prefer Room storage; fallback to legacy SharedPreferences storage
|
||||
NotificationContent content = getContentFromRoomOrLegacy(notificationId);
|
||||
// Check if this is a static reminder (title/body in input data, not storage)
|
||||
Data inputData = getInputData();
|
||||
boolean isStaticReminder = inputData.getBoolean("is_static_reminder", false);
|
||||
NotificationContent content;
|
||||
|
||||
if (content == null) {
|
||||
// Content not found - likely removed during deduplication or cleanup
|
||||
// Return success instead of failure to prevent retries for intentionally removed notifications
|
||||
Log.w(TAG, "DN|DISPLAY_SKIP content_not_found id=" + notificationId + " (likely removed during deduplication)");
|
||||
return Result.success(); // Success prevents retry loops for removed notifications
|
||||
if (isStaticReminder) {
|
||||
// Static reminder: create NotificationContent from input data
|
||||
String title = inputData.getString("title");
|
||||
String body = inputData.getString("body");
|
||||
boolean sound = inputData.getBoolean("sound", true);
|
||||
boolean vibration = inputData.getBoolean("vibration", true);
|
||||
String priority = inputData.getString("priority");
|
||||
if (priority == null) {
|
||||
priority = "normal";
|
||||
}
|
||||
|
||||
if (title == null || body == null) {
|
||||
Log.w(TAG, "DN|DISPLAY_SKIP static_reminder_missing_data id=" + notificationId);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
// Create NotificationContent from input data
|
||||
// Use current time as scheduled time for static reminders
|
||||
long scheduledTime = System.currentTimeMillis();
|
||||
content = new NotificationContent(title, body, scheduledTime);
|
||||
content.setId(notificationId);
|
||||
content.setSound(sound);
|
||||
content.setPriority(priority);
|
||||
// Note: fetchedAt is automatically set to current time in NotificationContent constructor
|
||||
// Note: vibration is handled in displayNotification() method, not stored in NotificationContent
|
||||
|
||||
Log.d(TAG, "DN|DISPLAY_STATIC_REMINDER id=" + notificationId + " title=" + title);
|
||||
} else {
|
||||
// Regular notification: load from storage
|
||||
// Prefer Room storage; fallback to legacy SharedPreferences storage
|
||||
content = getContentFromRoomOrLegacy(notificationId);
|
||||
|
||||
if (content == null) {
|
||||
// Content not found - likely removed during deduplication or cleanup
|
||||
// Return success instead of failure to prevent retries for intentionally removed notifications
|
||||
Log.w(TAG, "DN|DISPLAY_SKIP content_not_found id=" + notificationId + " (likely removed during deduplication)");
|
||||
return Result.success(); // Success prevents retry loops for removed notifications
|
||||
}
|
||||
|
||||
// Check if notification is ready to display
|
||||
if (!content.isReadyToDisplay()) {
|
||||
Log.d(TAG, "DN|DISPLAY_SKIP not_ready id=" + notificationId);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
// JIT Freshness Re-check (Soft TTL) - skip for static reminders
|
||||
content = performJITFreshnessCheck(content);
|
||||
}
|
||||
|
||||
// Check if notification is ready to display
|
||||
if (!content.isReadyToDisplay()) {
|
||||
Log.d(TAG, "DN|DISPLAY_SKIP not_ready id=" + notificationId);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
// JIT Freshness Re-check (Soft TTL)
|
||||
content = performJITFreshnessCheck(content);
|
||||
|
||||
// Display the notification
|
||||
boolean displayed = displayNotification(content);
|
||||
|
||||
@@ -356,6 +391,13 @@ public class DailyNotificationWorker extends Worker {
|
||||
try {
|
||||
Log.d(TAG, "DN|DISPLAY_NOTIF_START id=" + content.getId());
|
||||
|
||||
// Ensure notification channel exists before displaying
|
||||
ChannelManager channelManager = new ChannelManager(getApplicationContext());
|
||||
if (!channelManager.ensureChannelExists()) {
|
||||
Log.w(TAG, "DN|DISPLAY_NOTIF_ERR channel_blocked id=" + content.getId());
|
||||
return false;
|
||||
}
|
||||
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@
|
||||
console.log('Testing notification scheduling...');
|
||||
const now = new Date();
|
||||
const notificationTime = new Date(now.getTime() + 600000); // 10 minutes from now
|
||||
const prefetchTime = new Date(now.getTime() + 300000); // 5 minutes from now
|
||||
const prefetchTime = new Date(now.getTime() + 120000); // 2 minutes from now
|
||||
const notificationTimeString = notificationTime.getHours().toString().padStart(2, '0') + ':' +
|
||||
notificationTime.getMinutes().toString().padStart(2, '0');
|
||||
const prefetchTimeString = prefetchTime.getHours().toString().padStart(2, '0') + ':' +
|
||||
@@ -256,10 +256,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule notification for 10 minutes from now (allows 5 min prefetch to fire)
|
||||
// Schedule notification for 10 minutes from now (allows 2 min prefetch to fire)
|
||||
const now = new Date();
|
||||
const notificationTime = new Date(now.getTime() + 600000); // 10 minutes from now
|
||||
const prefetchTime = new Date(now.getTime() + 300000); // 5 minutes from now
|
||||
const prefetchTime = new Date(now.getTime() + 120000); // 2 minutes from now
|
||||
const notificationTimeString = notificationTime.getHours().toString().padStart(2, '0') + ':' +
|
||||
notificationTime.getMinutes().toString().padStart(2, '0');
|
||||
const prefetchTimeString = prefetchTime.getHours().toString().padStart(2, '0') + ':' +
|
||||
|
||||
@@ -432,8 +432,8 @@
|
||||
scheduledTime.setDate(scheduledTime.getDate() + 1);
|
||||
}
|
||||
|
||||
// Calculate prefetch time (5 minutes before notification)
|
||||
const prefetchTime = new Date(scheduledTime.getTime() - 300000); // 5 minutes
|
||||
// Calculate prefetch time (2 minutes before notification)
|
||||
const prefetchTime = new Date(scheduledTime.getTime() - 120000); // 2 minutes
|
||||
const prefetchTimeReadable = prefetchTime.toLocaleTimeString();
|
||||
const notificationTimeReadable = scheduledTime.toLocaleTimeString();
|
||||
const prefetchTimeString = prefetchTime.getHours().toString().padStart(2, '0') + ':' +
|
||||
|
||||
Reference in New Issue
Block a user