From 0b61d33f21952b7bafd3f97991677d322845640c Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Mon, 16 Feb 2026 18:16:20 +0800 Subject: [PATCH] fix(android): avoid overwriting app schedule when rollover uses daily_rollover_ id NotifyReceiver's post-schedule DB update no longer uses the "first enabled notify schedule" fallback when stableScheduleId starts with "daily_rollover_". That fallback was updating the app's schedule row (e.g. daily_timesafari_reminder) with the rollover time and could leave the app's next alarm in a bad state after a notification fired. Add docs/CONSUMING_APP_ANDROID_NOTES.md with notes for consuming apps: debounce double scheduleDailyNotification calls, and include DailyNotificationReceiver in logcat when debugging alarms that are scheduled but do not fire. --- .../dailynotification/NotifyReceiver.kt | 8 ++-- docs/CONSUMING_APP_ANDROID_NOTES.md | 37 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 docs/CONSUMING_APP_ANDROID_NOTES.md diff --git a/android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt b/android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt index 5d23eb2..e291701 100644 --- a/android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt +++ b/android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt @@ -420,9 +420,11 @@ class NotifyReceiver : BroadcastReceiver() { // First, try to find schedule by the provided stableScheduleId var scheduleToUpdate = db.scheduleDao().getById(stableScheduleId) - // If not found by ID, find the existing enabled notify schedule (for rollover scenarios) - // getNotificationStatus() finds schedules with kind="notify" && enabled=true - if (scheduleToUpdate == null) { + // If not found by ID, only use "first enabled notify" fallback when this is NOT + // a rollover id (daily_rollover_*). Rollover work may use a different notification_id + // (e.g. from recovery); updating the app's schedule row here would overwrite + // nextRunAt with the rollover time and can leave the app's alarm in a bad state. + if (scheduleToUpdate == null && !stableScheduleId.startsWith("daily_rollover_")) { val allSchedules = db.scheduleDao().getAll() scheduleToUpdate = allSchedules.firstOrNull { it.kind == "notify" && it.enabled } } diff --git a/docs/CONSUMING_APP_ANDROID_NOTES.md b/docs/CONSUMING_APP_ANDROID_NOTES.md new file mode 100644 index 0000000..bb3ec68 --- /dev/null +++ b/docs/CONSUMING_APP_ANDROID_NOTES.md @@ -0,0 +1,37 @@ +# Consuming App Notes — Android Daily Notifications + +Brief notes for apps that integrate the daily notification plugin on Android. + +--- + +## Double schedule (rapid successive calls) + +If your app calls `scheduleDailyNotification` twice in quick succession (e.g. within a few hundred ms) for the same reminder, the second call cancels the alarm just set and reschedules. On some devices or OEMs this can contribute to the alarm not firing. + +**Recommendation:** Debounce or guard in the edit-reminder success path so you only call `scheduleDailyNotification` once per user action (e.g. wait for the first call to resolve before allowing another, or coalesce rapid calls). + +--- + +## Alarm scheduled but not firing (e.g. 6:04) + +When logs show "Scheduling OS alarm" and "Updated schedule in database" but the notification never appears: + +1. **Confirm the broadcast is delivered** + Run logcat including the receiver: + ```bash + adb logcat -v time -s DNP-SCHEDULE:V DailyNotificationWorker:V DailyNotificationReceiver:V + ``` + At the scheduled time, check whether `DailyNotificationReceiver` logs anything. If the Receiver runs, the issue is downstream (WorkManager / display). If it does not run, the OS did not deliver the alarm (Doze, OEM, or alarm replacement). + +2. **Avoid double schedule** + Ensure the app is not calling `scheduleDailyNotification` twice in quick succession for the same reminder (see above). + +3. **Plugin fix (v1.1.5+)** + The plugin no longer overwrites the app’s schedule row when handling rollover work that uses a `daily_rollover_*` id, so the app’s `nextRunAt` stays correct after a notification fires. + +--- + +## References + +- [ACTION_PLAN_INTEGRATION_FIXES.md](./ACTION_PLAN_INTEGRATION_FIXES.md) — plugin and app integration checklist +- [CONSUMING_APP_OPTIONAL_ANDROID_ID_CLEANUP.md](./CONSUMING_APP_OPTIONAL_ANDROID_ID_CLEANUP.md) — optional cleanup of stale schedule rows