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.
This commit is contained in:
Jose Olarte III
2026-03-24 22:05:30 +08:00
parent d4cdee0698
commit aaee3bbbd2

View File

@@ -0,0 +1,126 @@
# 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 **stale**`useCache=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.kt``object ScheduleHelper`, `suspend fun scheduleDualNotification(...)`
- Calls `scheduleFetch(context, contentFetchConfig)` which resolves to `FetchWorker.scheduleFetchForDual``enqueueFetch` **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 ignored**`useCache=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.
---
## 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` — optional `timeout` / `retry*` JSON parsing (already addressed on the plugin side).