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.
This commit is contained in:
114
docs/plugin-feedback-android-duplicate-reminder-notification.md
Normal file
114
docs/plugin-feedback-android-duplicate-reminder-notification.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# 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:** User’s 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:
|
||||
|
||||
- NotifyReceiver’s alarm: `notification_id` = `daily_timesafari_reminder`, `is_static_reminder` = true → correct user message.
|
||||
- DailyNotificationScheduler’s 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 doesn’t 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 doesn’t 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. Dialog’s `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 parent’s callback runs; it calls `cancelDailyNotification()` (on iOS) then `scheduleDailyNotification(...)` **once**. No double schedule from the app.
|
||||
|
||||
So the duplicate is entirely due to the plugin’s 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. 2–3 minutes ahead). Wait until the scheduled time. **Only one** notification should appear, with the user’s 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.
|
||||
Reference in New Issue
Block a user