Step 3 of docs/STORAGE-AND-SYNC.md. Now that our own provider holds the data in the app's private storage, a Local-mode user's tasks exist in exactly one place and uninstalling deletes them — on Play, where most people will never have a sync engine, that is the majority case. So export is a v1 feature, not a nicety. One .ics per list, because a list is a CalDAV collection and that is the unit other clients understand; folding everything into one file would flatten the lists away, and list membership is not recoverable from a VTODO afterwards. ExportWriter can put them in a folder (ACTION_OPEN_DOCUMENT_TREE) or a single zip (ACTION_CREATE_DOCUMENT). No storage permission either way — SAF hands us a Uri the user picked. Two things needed care: Export reads the tasks table, not the instances view the rest of the app reads from. In the instances view a recurring task appears once per occurrence with its times resolved and no rule attached, so exporting from there would write the same task fifty times and lose the RRULE that generated them. And local tasks have no UID. The dmfs provider only lets a sync adapter assign one, so in Local mode every task arrives with _uid null — and a VTODO without a UID is both invalid and un-mergeable, meaning a re-imported backup would duplicate every task rather than match it. ICalendarWriter synthesises one from the row id, stable across exports and tagged so it is recognisable as synthetic. Times go out in UTC rather than with a TZID. Emitting TZID obliges us to emit a matching VTIMEZONE with its transition rules, and a TZID referencing an absent definition is what actually breaks importers. All-day values keep VALUE=DATE, the only form that survives a timezone change intact. The writer is pure Kotlin with no Android in it and is covered by 40 tests — line folding counted in octets and never splitting a UTF-8 sequence, TEXT escaping, forward references from a subtask to a parent later in the file, and CRLF endings. An export is only as good as its ability to be read back, and nothing about a malformed .ics is obvious until someone needs the backup. The SAF plumbing is marked in the storage doc as floret-kit material. Kept app-local for now on the kit's own stated principle of not extracting before a second consumer exists; the seam is in place, so moving it is a file move. Backend only — no UI yet; that comes with the frontend pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Agendula
A modern Material 3 Expressive task app for Android.
Reads, writes, and reminds — on top of an existing tasks provider, with no own
sync stack.
Agendula is the task-list sibling to Calendula.
Where Calendula is a pure front-end over Android's CalendarContract, Agendula is
a pure front-end over the OpenTasks TaskContract provider — the store that
DAVx5 (and SmoothSync, DecSync, …) syncs your CalDAV VTODO tasks into. No own
database, no reinvented sync.
The name rhymes with its sibling on purpose: Agendula is agenda — Latin for
“things to be done” — given Calendula's -ula ending. Calendula keeps your days;
Agendula keeps your to-dos. (A Calendula flower head is botanically a cluster of
many small florets — so the two apps are florets of one bloom.)
Status: data layer done, UI in progress. The full non-visual stack over the
TaskContractprovider — provider resolution, live-updating reads, writes, smart-list filtering, and a self-scheduled reminder engine — is built and unit-tested. The Material 3 Expressive screens are now being built on top, one at a time. Seedocs/ROADMAP.mdfor status,docs/ARCHITECTURE.mdfor how it's built, anddocs/PLAN.mdfor the A-now-B-later design rationale.
Sync sources (by design)
Agendula works with anything that writes to the tasks provider — DAVx5 (CalDAV), SmoothSync, CalDAV-Sync, DecSync CC, or any Android sync adapter — because it builds on the provider, not on any one sync app. Google Tasks / Microsoft To Do are out of scope by design (proprietary; they would mean owning a sync stack). Open standards — CalDAV / iCalendar / DecSync — are the lane.
Translations
Agendula ships in English so far, and would like not to. Translations are managed on a self-hosted Weblate, and partial ones are fine — an untranslated string simply falls back to English.
No coding needed: register on the Weblate server, pick (or request) a language, and translate the strings in your browser. You can also reach this link in the app from the top of Settings → App language.
License
MIT — see LICENSE.