diff --git a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt index 49dc010..073c875 100644 --- a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt +++ b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt @@ -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) { diff --git a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java index f6ca728..e548de8 100644 --- a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java +++ b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java @@ -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) diff --git a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java index 139855e..18f19ae 100644 --- a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java +++ b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java @@ -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); diff --git a/test-apps/android-test-app/app/src/main/assets/public/index.html b/test-apps/android-test-app/app/src/main/assets/public/index.html index c788751..ec38ac1 100644 --- a/test-apps/android-test-app/app/src/main/assets/public/index.html +++ b/test-apps/android-test-app/app/src/main/assets/public/index.html @@ -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') + ':' + diff --git a/www/index.html b/www/index.html index be40a1c..48908a4 100644 --- a/www/index.html +++ b/www/index.html @@ -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') + ':' +