Files
crowd-funder-for-time-pwa/docs/plugin-feedback-android-duplicate-reminder-notification.md
Jose Olarte III c28c47a3c9 docs: add plugin feedback for Android duplicate reminder notification
Add docs/plugin-feedback-android-duplicate-reminder-notification.md
describing the duplicate notification on first-time reminder setup (one
correct + one fallback). Root cause: ScheduleHelper schedules the
static reminder alarm and also enqueues the prefetch worker, which
on fallback schedules a second alarm via DailyNotificationScheduler.

Suggested fix: for static-reminder schedules, do not enqueue prefetch
(or have prefetch skip scheduleNotificationIfNeeded). The suggested
plugin changes were applied and fixed the issue.
2026-02-18 19:52:41 +08:00

115 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 **handleFailedFetch****useFallbackContent****getFallbackContent****createEmergencyFallbackContent(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.
---
## Recommended fix (in the plugin)
**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**: `useFallbackContent``scheduleNotificationIfNeeded`; 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.