From c38f23564734b9448bf58852e87983bd779608d5 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Wed, 4 Mar 2026 21:17:26 +0800 Subject: [PATCH] fix(android): apply rollover interval for daily_rollover_* and allow ROLLOVER_ON_FIRE updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bug 1: When the firing run used schedule_id daily_rollover_*, resolve the canonical notify schedule (first enabled with rolloverIntervalMinutes > 0) and use it to read the interval so the next run is current + interval instead of +24h. Add ScheduleHelper.getCanonicalRolloverScheduleBlocking(). - Bug 2: For ROLLOVER_ON_FIRE, do not skip scheduling when an existing PendingIntent is found for the same schedule id: cancel the existing alarm and set the new trigger time so the rollover chain (e.g. 21:10 → 21:20) is updated instead of treated as duplicate. --- .../DailyNotificationPlugin.kt | 18 ++++++++++++++++++ .../DailyNotificationWorker.java | 14 +++++++++++--- .../dailynotification/NotifyReceiver.kt | 16 +++++++++++----- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt index 587ca1c..11429cb 100644 --- a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt +++ b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt @@ -2767,6 +2767,24 @@ object ScheduleHelper { } } + /** + * Blocking: first enabled notify schedule with rolloverIntervalMinutes > 0 (canonical for rollover chain). + * Used when the firing run has schedule_id = daily_rollover_* so we can still apply the interval. + */ + @JvmStatic + fun getCanonicalRolloverScheduleBlocking(context: Context): Schedule? { + return kotlinx.coroutines.runBlocking { + try { + DailyNotificationDatabase.getDatabase(context).scheduleDao() + .getByKindAndEnabled("notify", true) + .firstOrNull { it.rolloverIntervalMinutes != null && it.rolloverIntervalMinutes > 0 } + } catch (e: Exception) { + Log.w("ScheduleHelper", "getCanonicalRolloverScheduleBlocking failed", e) + null + } + } + } + /** * Blocking update of schedule next run time (for use from Java Worker after rollover). */ diff --git a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java index b45b86d..e43ec77 100644 --- a/android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java +++ b/android/src/main/java/com/timesafari/dailynotification/DailyNotificationWorker.java @@ -547,12 +547,20 @@ public class DailyNotificationWorker extends Worker { scheduleIdForRollover = notificationId; } } + // When firing run used daily_rollover_* id, resolve canonical schedule so we still apply rolloverIntervalMinutes + String logicalScheduleIdForRollover = scheduleIdForRollover; + if (scheduleIdForRollover != null && scheduleIdForRollover.startsWith("daily_rollover_")) { + com.timesafari.dailynotification.Schedule canonical = com.timesafari.dailynotification.ScheduleHelper.getCanonicalRolloverScheduleBlocking(getApplicationContext()); + if (canonical != null) { + logicalScheduleIdForRollover = canonical.getId(); + } + } Integer rolloverMinutes = null; - if (scheduleIdForRollover != null && !scheduleIdForRollover.isEmpty()) { - com.timesafari.dailynotification.Schedule s = com.timesafari.dailynotification.ScheduleHelper.getScheduleBlocking(getApplicationContext(), scheduleIdForRollover); + if (logicalScheduleIdForRollover != null && !logicalScheduleIdForRollover.isEmpty()) { + com.timesafari.dailynotification.Schedule s = com.timesafari.dailynotification.ScheduleHelper.getScheduleBlocking(getApplicationContext(), logicalScheduleIdForRollover); if (s != null && s.getRolloverIntervalMinutes() != null && s.getRolloverIntervalMinutes() > 0) { rolloverMinutes = s.getRolloverIntervalMinutes(); - Log.d(TAG, "DN|ROLLOVER_INTERVAL scheduleId=" + scheduleIdForRollover + " minutes=" + rolloverMinutes); + Log.d(TAG, "DN|ROLLOVER_INTERVAL scheduleId=" + logicalScheduleIdForRollover + " minutes=" + rolloverMinutes); } } long nextScheduledTime; diff --git a/android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt b/android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt index fbe4c49..a4283d2 100644 --- a/android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt +++ b/android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt @@ -193,11 +193,17 @@ class NotifyReceiver : BroadcastReceiver() { } if (existingPendingIntent != null) { - val triggerTimeStr = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US) - .format(java.util.Date(triggerAtMillis)) - Log.w(SCHEDULE_TAG, "Skipping duplicate schedule: id=$stableScheduleId, nextRun=$triggerTimeStr, source=$source") - Log.w(SCHEDULE_TAG, "Existing PendingIntent found for requestCode=$requestCode - alarm already scheduled") - return + if (source == ScheduleSource.ROLLOVER_ON_FIRE) { + // Rollover chain: same schedule id, new trigger time — treat as update: cancel then set + Log.d(SCHEDULE_TAG, "ROLLOVER_ON_FIRE: cancelling existing alarm for id=$stableScheduleId to set new trigger") + alarmManager.cancel(existingPendingIntent) + } else { + val triggerTimeStr = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US) + .format(java.util.Date(triggerAtMillis)) + Log.w(SCHEDULE_TAG, "Skipping duplicate schedule: id=$stableScheduleId, nextRun=$triggerTimeStr, source=$source") + Log.w(SCHEDULE_TAG, "Existing PendingIntent found for requestCode=$requestCode - alarm already scheduled") + return + } } } else { Log.d(SCHEDULE_TAG, "Skipping PendingIntent idempotence (caller just cancelled scheduleId=$stableScheduleId)")