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

127 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).