Files
crowd-funder-for-time-pwa/doc/plugin-feedback-android-dual-schedule-native-fetch-and-timing.md
Jose Olarte III aaee3bbbd2 Add plugin feedback doc for Android dual schedule native fetch and timing
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.
2026-03-24 22:05:30 +08:00

8.2 KiB
Raw Blame History

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:

  1. Prefetch does not call NativeNotificationContentFetcher.
    ScheduleHelper.scheduleDualNotification delegates fetch to FetchWorker (HTTP GET to optional url, or mock JSON when url is absent). The host apps TimeSafariNativeFetcher is never invoked. Logcat shows DNP-FETCH: Starting content fetch from: null, notificationTime=0 and no TimeSafariNativeFetcher fetchContent lines.

  2. Fetch is not scheduled at contentFetch.schedule (e.g. T5 minutes).
    FetchWorker.enqueueFetch enqueues immediate OneTimeWorkRequest work (no setInitialDelay aligned to the fetch cron). The notify alarm is scheduled correctly for dual_notify_*, but there is no corresponding alarm/work at the fetch cron time. A dual_fetch_* row may exist in the DB with nextRunAt, but the actual fetch runs at enable/setup time, not at T5.

  3. Cache vs DualScheduleHelper / contentTimeout.
    DualScheduleHelper.resolveDualContentBlocking only uses contentCache when the latest fetch is within relationship.contentTimeout (e.g. 5 minutes). If fetch runs once at setup and notify fires ~9+ minutes later, cache is staleuseCache=false → default title/body from userNotification, even when mock payload was stored.

Recommended direction (plugin):

  • For dual schedule when no HTTP url is configured (or when a flag indicates native mode), run NativeNotificationContentFetcher.fetchContent(FetchContext) (same path as DailyNotificationFetchWorker uses), persist results into the same contentCache / pipeline DualScheduleHelper expects.
  • 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 app timeToCronFiveMinutesBefore).
  • 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 / body from buildDualScheduleConfig), not API-derived or native “No updates” copy.
  • Logcat: DNP-DUAL: Resolved dual content: useCache=false at notify time.
  • Logcat: DNP-FETCH: Starting content fetch from: null, notificationTime=0 followed by Content fetch completed successfully at schedule/setup time, not at T5.
  • No TimeSafariNativeFetcher fetchContent START / POST …/plansLastUpdatedBetween during prefetch window (host registers NativeNotificationContentFetcher and 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: true
  • contentFetch.schedule: cron 5 minutes before userNotification.schedule (e.g. "25 19 * * *" for notify "30 19 * * *").
  • No contentFetch.url — intended to use native Endorser API via configureNativeFetcher + 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

  • enqueueFetch passes config.url into InputData; doWork logs Starting content fetch from: $url.
  • fetchContent(url, …) when url is null/blank returns generateMockContent() — never calls DailyNotificationPlugin.getNativeFetcherStatic().fetchContent(...).

2. scheduleDualNotification runs fetch work immediately, not at fetch cron

android/src/main/java/org/timesafari/dailynotification/DailyNotificationPlugin.ktobject ScheduleHelper, suspend fun scheduleDualNotification(...)

  • Calls scheduleFetch(context, contentFetchConfig) which resolves to FetchWorker.scheduleFetchForDualenqueueFetch without delay tied to contentFetchConfig.schedule.
  • Schedules notify via NotifyReceiver.scheduleExactNotification for dual_notify_* at calculateNextRunTime(userNotificationConfig.schedule).
  • Persists dual_fetch_* with nextRunAt = 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 TimeSafari contentFetch payload.

4. DualScheduleHelper behavior is consistent with “wrong fetch time”

android/src/main/java/org/timesafari/dailynotification/DualScheduleHelper.kt

  • Uses latest contentCache only if (now - fetchedAt) <= contentTimeoutMs. If fetch ran at setup and notify is later than contentTimeout, cache is ignoreduseCache=false in logs.

Acceptance criteria (plugin)

After a fix, on a device with:

  • configureNativeFetcher + updateStarredPlans called (host app),
  • scheduleDualNotification with contentFetch.enabled: true, no url, cron 5 min before notify,

then:

  1. At or before the notify fire time, within contentTimeout, the cache used by DualScheduleHelper reflects native fetch results when the API returns data (or empty), not only mock JSON.
  2. Logcat includes host tag TimeSafariNativeFetcher with fetchContent START (or equivalent) when prefetch runs, or plugin logs an explicit NativeNotificationContentFetcher invocation.
  3. 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).
  4. Optional: If url is set, preserve HTTP GET behavior; if url is 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.url is present; TimeSafari intentionally omits it for native API.
  • Reuse the same FetchContext / timeout semantics as DailyNotificationFetchWorker where possible to avoid two divergent native fetch implementations.
  • After changing timing, verify WorkManager unique work name fetch_dual / cancelDualSchedule still cancel only dual fetch and do not break daily reminder.

  • doc/notification-from-api-call.md — integration plan for API-driven New Activity.
  • doc/plugin-feedback-android-scheduleDualNotification-contentFetch-json.md — optional timeout / retry* JSON parsing (already addressed on the plugin side).