Skip to content

ADFA-5094: catalog freshness service (weekly publish + on-device refresh) - #404

Merged
luisguzman-adfa merged 8 commits into
mainfrom
feat/ADFA-5094-offline-catalog
Aug 14, 2026
Merged

ADFA-5094: catalog freshness service (weekly publish + on-device refresh)#404
luisguzman-adfa merged 8 commits into
mainfrom
feat/ADFA-5094-offline-catalog

Conversation

@luisguzman-adfa

@luisguzman-adfa luisguzman-adfa commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

What

First half of ADFA-5094: keep the Kolibri catalog current without an APK release. A weekly GitHub Actions workflow publishes a versioned catalog + manifest to Cloudflare; the app pulls it when online and overlays the APK-bundled asset, with a freshness label on the picker. The topic-tree depth (the ticket's other half) is a separate follow-up PR — nothing here depends on it.

  • CI: publish-catalogs.yml (weekly + manual) runs the existing generators once and uploads catalog + a fixed-key manifest to R2 under catalogs/, reusing the update.json pattern; served from k2go-download.appdevforall.org.
  • Domain: CatalogManifest + CatalogFreshness (TTL + hash), pure and unit-tested.
  • Data: manifest parser, HTTP client (ETag/304 + temp-file download + sha256 verify), refresh store, WorkManager worker + scheduler (weekly + opportunistic, network-constrained). Catalog-agnostic (input-data driven), so Kiwix reuses it.
  • Kolibri: BundledCatalogSource prefers the pulled overlay over the asset and self-invalidates on mtime change; CatalogRepositoryImpl schedules the refresh; a "Catalog updated on " label on the picker.

Why

The catalog refreshes only when a new APK ships (the Books catalog is ~2 months stale) and generation hit Studio from the build. This centralises the source fetch to one weekly run and lets the app stay current independently, at ~83 KB vs a ~30 MB OTA. Amends ADR-4954's "staleness rides the OTA" sub-decision (see ADR-5094).

Notes

Independent of the depth half: with only this merged, topics keep working exactly as today (live from Studio); the catalog just gains freshness. 5094 stays open for the depth PR.
Content stays on the Nginx mirror; only the catalog metadata moves to Cloudflare.
New strings translated across all 33 locales.

Verification

javalang parse on the changed Java; XML well-formedness on the layout + 34 strings files;
CatalogFreshness unit assertions. Not yet device-tested: run the workflow once
(workflow_dispatch) to publish the URLs, then confirm the app pulls/overlays on a device.

…n-device refresh

Amend ADR-4954's "staleness rides the APK OTA" sub-decision with the evidence it
lacked (Books catalog ~2 months stale; ~83 KB catalog pull vs ~30 MB APK OTA). Move
catalog generation off Gradle onto a weekly scheduled GitHub Actions workflow that
uploads versioned catalogs + a fixed-key manifest to Cloudflare R2, reusing the
update.json pipeline and the k2go-download.appdevforall.org base; the app pulls when
online (bundled snapshot as the offline floor, superseded by an overlay), with
TTL + ETag + hash for freshness. Content stays on the Nginx mirror; only the catalog
metadata moves to Cloudflare. One catalog-agnostic mechanism; Kolibri first.
…oudflare R2

New scheduled workflow (weekly + workflow_dispatch): run the existing generators once,
build a fixed-key manifest per catalog (version, generated, sha256 hash, url, and
per-channel {id,version} for jsonl), and upload catalog + manifest to R2 under
catalogs/, reusing the update.json R2 pattern (aws s3 cp, CLOUDFLARE_* vars). Served
from k2go-download.appdevforall.org/catalogs/. Never fails on a blocked source fetch;
skips that catalog's upload and keeps the last published one.
Pure, catalog-agnostic domain for the on-device refresh (ADR-5094): CatalogManifest
value type (catalog/version/generated/hash/url + per-item versions) and CatalogFreshness
rules — dueForCheck (TTL, with a backwards-clock guard) and changed (hash is the truth,
version only a label). Unit-tested. No Android; the data layer parses the manifest JSON
into these and the worker consumes them.
…store)

Data plumbing for the on-device catalog refresh (ADR-5094), catalog-agnostic:
- CatalogManifestParser: manifest JSON -> CatalogManifest (null on malformed).
- CatalogManifestClient: conditional GET with ETag/If-None-Match (304), plus a
  temp-file catalog download, mirroring the OTA update client's HTTP. Never throws.
- CatalogRefreshStore: per-catalog prefs (last check, active hash/generated, ETag).
Catalog-agnostic on-device refresh (ADR-5094): CatalogRefreshWorker (TTL gate ->
conditional manifest fetch -> if the hash changed, download + sha256-verify ->
write the overlay), CatalogRefreshScheduler (weekly periodic + opportunistic
one-shot, network-constrained, unique per catalog), CatalogOverlay (the overlay
file path both the worker and the source use). Catalog to refresh is passed as
WorkManager input data {name, manifestUrl, basename}. Mirrors OutboundSyncWorker.
BundledCatalogSource now prefers a pulled overlay (filesDir/catalogs/kolibri_catalog.jsonl)
over the APK asset when present, and self-invalidates when the overlay's mtime changes —
no explicit callback needed. CatalogRepositoryImpl(Context) schedules the weekly +
opportunistic refresh for the Kolibri catalog (the test constructor does not). With no
overlay, behaviour is unchanged; the bundled asset stays the offline floor.
Show "Catalog updated on <date>" on the Kolibri browse screen, from the catalog's
generated date (already carried by KolibriCatalogUiState). With the overlay-aware
source, it reflects the pulled catalog's date once refreshed, else the bundled one;
hidden when empty. New string k2go_kolibri_catalog_updated + 33 locales.
Review follow-up: BundledCatalogSource.ASSET and CatalogRepositoryImpl.BASENAME both
held "kolibri_catalog.jsonl" independently, so the source (reads the overlay) and the
worker (writes it) could silently drift. BASENAME now references ASSET.
@luisguzman-adfa luisguzman-adfa changed the title ADFA-5094: catalog service — ADR, weekly publish workflow, freshness domain ADFA-5094: catalog freshness service (weekly publish + on-device refresh) Aug 14, 2026
@luisguzman-adfa
luisguzman-adfa merged commit 4b38dd2 into main Aug 14, 2026
3 checks passed
@luisguzman-adfa
luisguzman-adfa deleted the feat/ADFA-5094-offline-catalog branch August 14, 2026 11:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant