Document how the daily-notification-plugin dual path uses FetchWorker mock/URL fetch instead of NativeNotificationContentFetcher, schedules fetch immediately rather than at contentFetch cron, and why DualScheduleHelper shows useCache=false. Includes acceptance criteria and file pointers for maintainers fixing the plugin.
8.2 KiB
Plugin feedback: Android dual schedule — native fetcher not used; fetch timing wrong
Date: 2026-03-24 21:56 PST
Target repo: @timesafari/daily-notification-plugin (daily-notification-plugin)
Consuming app: crowd-funder-for-time-pwa (TimeSafari)
Platform: Android (Kotlin / Java)
Related: New Activity notifications (scheduleDualNotification / cancelDualSchedule)
Summary
On Android, the dual (New Activity) schedule path is not implementing the intended contract:
-
Prefetch does not call
NativeNotificationContentFetcher.
ScheduleHelper.scheduleDualNotificationdelegates fetch toFetchWorker(HTTP GET to optionalurl, or mock JSON whenurlis absent). The host app’sTimeSafariNativeFetcheris never invoked. Logcat showsDNP-FETCH: Starting content fetch from: null, notificationTime=0and noTimeSafariNativeFetcherfetchContentlines. -
Fetch is not scheduled at
contentFetch.schedule(e.g. T−5 minutes).
FetchWorker.enqueueFetchenqueues immediateOneTimeWorkRequestwork (nosetInitialDelayaligned to the fetch cron). The notify alarm is scheduled correctly fordual_notify_*, but there is no corresponding alarm/work at the fetch cron time. Adual_fetch_*row may exist in the DB withnextRunAt, but the actual fetch runs at enable/setup time, not at T−5. -
Cache vs
DualScheduleHelper/contentTimeout.
DualScheduleHelper.resolveDualContentBlockingonly usescontentCachewhen the latest fetch is withinrelationship.contentTimeout(e.g. 5 minutes). If fetch runs once at setup and notify fires ~9+ minutes later, cache is stale →useCache=false→ default title/body fromuserNotification, even when mock payload was stored.
Recommended direction (plugin):
- For dual schedule when no HTTP
urlis configured (or when a flag indicates native mode), runNativeNotificationContentFetcher.fetchContent(FetchContext)(same path asDailyNotificationFetchWorkeruses), persist results into the samecontentCache/ pipelineDualScheduleHelperexpects. - Schedule that work (or an alarm that enqueues it) at
calculateNextRunTime(contentFetch.schedule)— i.e. before the notify alarm, typically 5 minutes earlier per app cron (see consuming apptimeToCronFiveMinutesBefore). - Optionally align one scheduling mechanism: either exact alarm for fetch + notify, or WorkManager with initial delay to the next fetch instant (and reschedule after run).
Symptoms (consuming app + logcat)
- Notification shows default copy from
userNotification(title/bodyfrombuildDualScheduleConfig), not API-derived or native “No updates” copy. - Logcat:
DNP-DUAL: Resolved dual content: useCache=falseat notify time. - Logcat:
DNP-FETCH: Starting content fetch from: null, notificationTime=0followed byContent fetch completed successfullyat schedule/setup time, not at T−5. - No
TimeSafariNativeFetcherfetchContent START/POST …/plansLastUpdatedBetweenduring prefetch window (host registersNativeNotificationContentFetcherand logs on configure + fetch). - No activity at the prefetch cron time (e.g. 19:05 for notify at 19:10); only notify fires at T.
What the consuming app sends (contract)
File: src/services/notifications/dualScheduleConfig.ts
contentFetch.enabled: truecontentFetch.schedule: cron 5 minutes beforeuserNotification.schedule(e.g."25 19 * * *"for notify"30 19 * * *").- No
contentFetch.url— intended to use native Endorser API viaconfigureNativeFetcher+NativeNotificationContentFetcher. relationship.autoLink: true,relationship.contentTimeout: 5 * 60 * 1000,fallbackBehavior: "show_default".
Host app: android/.../TimeSafariNativeFetcher.java implements NativeNotificationContentFetcher and calls POST /api/v2/report/plansLastUpdatedBetween with starred plan IDs from updateStarredPlans.
Root cause (plugin code — paths to review)
These paths are from a local clone of daily-notification-plugin; line numbers may drift.
1. FetchWorker is URL/mock-only; does not call native fetcher
android/src/main/java/org/timesafari/dailynotification/FetchWorker.kt
enqueueFetchpassesconfig.urlintoInputData;doWorklogsStarting content fetch from: $url.fetchContent(url, …)whenurlis null/blank returnsgenerateMockContent()— never callsDailyNotificationPlugin.getNativeFetcherStatic().fetchContent(...).
2. scheduleDualNotification runs fetch work immediately, not at fetch cron
android/src/main/java/org/timesafari/dailynotification/DailyNotificationPlugin.kt — object ScheduleHelper, suspend fun scheduleDualNotification(...)
- Calls
scheduleFetch(context, contentFetchConfig)which resolves toFetchWorker.scheduleFetchForDual→enqueueFetchwithout delay tied tocontentFetchConfig.schedule. - Schedules notify via
NotifyReceiver.scheduleExactNotificationfordual_notify_*atcalculateNextRunTime(userNotificationConfig.schedule). - Persists
dual_fetch_*withnextRunAt = calculateNextRunTime(contentFetchConfig.schedule)but no matching alarm/work is scheduled for that instant in the current flow (as observed).
3. Native fetcher exists elsewhere
android/src/main/java/org/timesafari/dailynotification/DailyNotificationFetchWorker.java
- Contains logic to call
NativeNotificationContentFetcher.fetchContent(FetchContext)(with timeout). Dual schedule does not enqueue this worker for the TimeSafaricontentFetchpayload.
4. DualScheduleHelper behavior is consistent with “wrong fetch time”
android/src/main/java/org/timesafari/dailynotification/DualScheduleHelper.kt
- Uses latest
contentCacheonly if(now - fetchedAt) <= contentTimeoutMs. If fetch ran at setup and notify is later thancontentTimeout, cache is ignored →useCache=falsein logs.
Acceptance criteria (plugin)
After a fix, on a device with:
configureNativeFetcher+updateStarredPlanscalled (host app),scheduleDualNotificationwithcontentFetch.enabled: true, nourl, cron 5 min before notify,
then:
- At or before the notify fire time, within
contentTimeout, the cache used byDualScheduleHelperreflects native fetch results when the API returns data (or empty), not only mock JSON. - Logcat includes host tag
TimeSafariNativeFetcherwithfetchContent START(or equivalent) when prefetch runs, or plugin logs an explicitNativeNotificationContentFetcherinvocation. - Prefetch does not run only at INITIAL_SETUP; it runs at the next occurrence of
contentFetch.schedule(and reschedules for the following day after success, same as notify rollover). - Optional: If
urlis set, preserve HTTP GET behavior; ifurlis absent and native fetcher is registered, use native path.
References in consuming app
| Topic | Location |
|---|---|
| Dual config builder | src/services/notifications/dualScheduleConfig.ts |
scheduleDualNotification call |
src/views/AccountViewView.vue (scheduleNewActivityDualNotification, editNewActivityNotification) |
| Native fetcher | android/app/src/main/java/app/timesafari/TimeSafariNativeFetcher.java |
| Registration | MainActivity / plugin init (host registers DailyNotificationPlugin.setNativeFetcher) |
Notes for Cursor / implementers
- Do not assume
contentFetch.urlis present; TimeSafari intentionally omits it for native API. - Reuse the same
FetchContext/ timeout semantics asDailyNotificationFetchWorkerwhere possible to avoid two divergent native fetch implementations. - After changing timing, verify
WorkManagerunique work namefetch_dual/cancelDualSchedulestill cancel only dual fetch and do not break daily reminder.
Related docs in this repo
doc/notification-from-api-call.md— integration plan for API-driven New Activity.doc/plugin-feedback-android-scheduleDualNotification-contentFetch-json.md— optionaltimeout/retry*JSON parsing (already addressed on the plugin side).