Files
crowd-funder-for-time-pwa/doc/plugin-feedback-android-duplicate-reminder-notification.md

7.6 KiB
Raw Blame History

Plugin feedback: Android duplicate reminder notification on first-time setup

Date: 2026-02-18
Generated: 2026-02-18 17:47:06 PST
Target repo: daily-notification-plugin (local copy at daily-notification-plugin_test)
Consuming app: crowd-funder-for-time-pwa (TimeSafari)
Platform: Android

Summary

When the user sets a Reminder Notification for the first time (toggle on → set message and time in PushNotificationPermission), two notifications fire at the scheduled time:

  1. Correct one: Users chosen title/message, from the static reminder alarm (scheduleId = daily_timesafari_reminder).
  2. Extra one: Fallback message (“Daily Update” / “🌅 Good morning! Ready to make today amazing?”), from a second alarm that uses a UUID as notification_id.

When the user edits an existing reminder (Edit Notification Details), only one notification fires. The duplicate only happens on initial setup.

The app calls scheduleDailyNotification once per user action in both flows (first-time and edit). The duplicate is caused inside the plugin by the prefetch worker scheduling a second alarm via the legacy DailyNotificationScheduler.


Evidence from Logcat

Filter: DNP-SCHEDULE, DailyNotificationWorker, DailyNotificationReceiver.

  • 17:42:34 Single call from app: plugin schedules the static reminder alarm (scheduleId=daily_timesafari_reminder, source=INITIAL_SETUP). One OS alarm is scheduled.
  • 17:45:00 Two RECEIVE_START events:
    • First: display=5e373fd1-0f08-4e8f-b166-cfd46d694d82 (UUID).
    • Second: static_reminder id=daily_timesafari_reminder.
  • Both run in parallel: Worker for UUID shows DN|JIT_FRESH skip=true and displays; Worker for daily_timesafari_reminder shows DN|DISPLAY_STATIC_REMINDER and displays. So two notifications are shown.

Conclusion: two different PendingIntents fire at the same time: one with notification_id = UUID, one with notification_id = daily_timesafari_reminder.


Root cause (plugin side)

  1. ScheduleHelper.scheduleDailyNotification (e.g. in DailyNotificationPlugin.kt):

    • Cancels existing alarm for scheduleId.
    • Schedules one alarm via NotifyReceiver.scheduleExactNotification with reminderId = scheduleId, scheduleId = scheduleId, isStaticReminder = true (INITIAL_SETUP). That alarm carries title/body in the intent and is the “correct” notification.
    • Enqueues DailyNotificationFetchWorker (prefetch) to run 2 minutes before the same time.
  2. DailyNotificationFetchWorker runs ~2 minutes before the display time:

    • Tries to fetch content (e.g. native fetcher). For a static-reminder-only app (no URL, no fetcher returning content), the fetch returns empty/null.
    • Goes to handleFailedFetchuseFallbackContentgetFallbackContentcreateEmergencyFallbackContent(scheduledTime).
    • createEmergencyFallbackContent builds a NotificationContent() (default constructor), which assigns a random UUID as id, and sets title “Daily Update” and body “🌅 Good morning! Ready to make today amazing?”.
    • useFallbackContent then calls scheduleNotificationIfNeeded(fallbackContent).
  3. scheduleNotificationIfNeeded uses the legacy DailyNotificationScheduler (AlarmManager) to schedule another alarm at the same scheduledTime, with notification_id = that UUID.

So at fire time there are two alarms:

  • NotifyReceivers alarm: notification_id = daily_timesafari_reminder, is_static_reminder = true → correct user message.
  • DailyNotificationSchedulers alarm: notification_id = UUID → fallback message.

The prefetch path is intended for “fetch content then display” flows. For static reminder schedules, the display is already fully handled by the single NotifyReceiver alarm; the prefetch worker should not schedule a second alarm.


Why edit doesnt show the duplicate (in observed behavior)

On edit, the app still calls the plugin once and the plugin again enqueues the prefetch worker. Possible reasons the duplicate is less obvious on edit:

  • Different timing (e.g. user sets a time further out, or doesnt wait for the second notification).
  • Or the first-time run leaves the prefetch/legacy path in a state where the duplicate only appears on first setup.

Regardless, the correct fix is to ensure that for static-reminder schedules the prefetch worker never schedules a second alarm.


Option A (recommended): Do not enqueue prefetch for static reminder schedules

In ScheduleHelper.scheduleDailyNotification (or equivalent), when scheduling a static reminder (title/body from app, no URL, display already in the intent), do not enqueue DailyNotificationFetchWorker for that run. The prefetch is for “fetch content then show”; for static reminders there is nothing to fetch and the only alarm should be the one from NotifyReceiver.

  • No new inputData flags needed.
  • No change to DailyNotificationFetchWorker semantics for other flows.

Option B: Prefetch worker skips scheduling when display is already scheduled

  • When enqueueing the prefetch work for a static-reminder schedule, pass an input flag (e.g. display_already_scheduled or is_static_reminder_schedule = true).
  • In DailyNotificationFetchWorker, in useFallbackContent (and anywhere else that calls scheduleNotificationIfNeeded for this work item), if that flag is set, do not call scheduleNotificationIfNeeded.
  • Ensures only the NotifyReceiver alarm fires for that time.

Option A is simpler and matches the semantics: static reminder = one alarm, no prefetch.


App-side behavior (no change required)

  • First-time reminder: Account view opens PushNotificationPermission without skipSchedule. User sets time/message and confirms. Dialogs turnOnNativeNotifications calls NotificationService.scheduleDailyNotification(...) once and then the callback saves settings. No second schedule from the app.
  • Edit reminder: Account view opens the dialog with skipSchedule: true. Only the parents callback runs; it calls cancelDailyNotification() (on iOS) then scheduleDailyNotification(...) once. No double schedule from the app.

So the duplicate is entirely due to the plugins prefetch worker scheduling an extra alarm via the legacy scheduler; fixing it in the plugin as above will resolve the issue.


Files to consider in the plugin

  • ScheduleHelper.scheduleDailyNotification (e.g. in DailyNotificationPlugin.kt): where the single NotifyReceiver alarm and the prefetch work are enqueued. Either skip enqueueing prefetch for static reminder (Option A), or add inputData for “display already scheduled” (Option B).
  • DailyNotificationFetchWorker: useFallbackContentscheduleNotificationIfNeeded; if using Option B, skip scheduleNotificationIfNeeded when the new flag is set.
  • DailyNotificationScheduler (legacy): used by scheduleNotificationIfNeeded to add the second (UUID) alarm; no change required if the worker simply stops calling it for static-reminder schedules.

Verification

After the fix:

  1. First-time: Turn on Reminder Notification, set message and time (e.g. 23 minutes ahead). Wait until the scheduled time. Only one notification should appear, with the users message.
  2. Logcat should show a single RECEIVE_START at that time (e.g. static_reminder id=daily_timesafari_reminder), and no second display=<uuid> for the same time.

You can reuse the same Logcat filter as above to confirm a single receiver run per scheduled time.