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:
@@ -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 app’s `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. T−5 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 T−5.
|
||||
|
||||
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 T−5.
|
||||
- **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).
|
||||
Reference in New Issue
Block a user