fix(android): apply rollover interval for daily_rollover_* and allow ROLLOVER_ON_FIRE updates

- 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.
This commit is contained in:
Jose Olarte III
2026-03-04 21:17:26 +08:00
parent 2714480070
commit c38f235647
3 changed files with 40 additions and 8 deletions

View File

@@ -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).
*/

View File

@@ -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;

View File

@@ -193,12 +193,18 @@ class NotifyReceiver : BroadcastReceiver() {
}
if (existingPendingIntent != null) {
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)")
}