Two halves of the same problem: the migration this branch was built around serves nobody, and when it does run and fail it says nothing. **The copy.** `OneShotImport` reads a bundled dmfs provider's `databases/tasks.db`, and no release ever bundled that provider — `git tag --contains` on the commit that added `:provider` comes back empty, because it was added and deleted inside this same unreleased cycle. Every existing install therefore keeps its tasks in OpenTasks or tasks.org, which `autoMode()` correctly keeps them on, and the only route to the store this release is named after was to retype everything by hand. `ExternalImport` copies the external provider's lists, tasks and alarms into Room in one transaction with verified counts — the same discipline as the legacy import, for the same reason: a partial copy is worse than none, because the user cannot tell which half is missing. Task `uid`s survive, so these rows can be attached to a CalDAV collection once sync lands instead of duplicating server-side. Offered as Settings → Storage → "Copy tasks from …", behind a confirm that names the real task count, and only while a provider is installed and permitted and no copy has succeeded yet. A second run would leave two of everything: what it writes is indistinguishable from hand-typed tasks the moment it finishes, so there is nothing to reconcile against. One-directional on purpose. Writing a *list* into a third-party provider means impersonating its sync adapter, and the external store is already the one that can sync. What does not come across is documented on the class, because the read seam is `exportTasks` and that is shaped for iCalendar: per-occurrence overrides, EXDATE, CLASS, DURATION, the per-task timezone, and a task's exact PRIORITY digit. Switching stores at all now asks first. Neither store hands its rows to the other, so the app looks emptied to anyone who expected a move — the picker's hint said so in passing, which is not where someone reads it. **The failure.** `runIfNeeded` returned a fully-specified `ImportResult` — `Failed(cause)`, `Imported(counts)` — and the one caller threw it away. `StartupGate` called it inside a `runCatching` whose value it discarded, so an upgrading user whose import failed got an empty app, no message, and their tasks in a file only a developer could name. The class comment said as much: "reading before the import lands shows an upgrading user an empty app, which is the single worst thing this migration could do." It is now recorded and logged, the completion flag deliberately left unset so the next launch retries, and Settings → Storage offers the retry — which is what finally makes `reimportFromArchive()` reachable from the app rather than only "by a targeted fix release". Near-zero blast radius, given the above, but it is the one irreversible path in the app, and the contrast was hard to defend: the export path has a localized failure enum for a failure that costs nothing. `@ExternalStore` binds the external source so the copy can name a store other than the active one, and so it can be tested against a fake. Nine instrumented tests cover the write half: id remapping with a child before its parent, the duplicate-uid case that would otherwise abort on the unique index, the START alarm reference, device-only lists, the empty source, a mid-read failure rolling back with the guard left open, and preview with and without a provider.
Agendula
A modern Material 3 Expressive task app for Android.
Keeps your tasks on your device, or on top of a tasks provider you already use.
Open standards, no account required.
Agendula is the task-list sibling to Calendula.
Where Calendula is a pure front-end over Android's CalendarContract, Agendula
keeps its own store, designed around RFC 5545's VTODO — the same tasks DAVx5
(and SmoothSync, DecSync, …) sync out of your CalDAV server. It can also read and
write a tasks provider you already have, for anyone already syncing that way.
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.)
Where your tasks live — your choice
| Where | Sync | Needs | |
|---|---|---|---|
| On your device (default) | Agendula's own database | none yet — CalDAV sync of our own is planned | nothing. No account, no permissions, no other app |
| In a provider you already use | OpenTasks or tasks.org | whatever syncs it for you — DAVx5 and friends | that app installed, and its read/write permission |
Agendula's own store is an ordinary app database — nothing is published to other apps, so there is no authority to clash over and no permission to grant. It coexists with OpenTasks rather than replacing it: installing one never breaks the other, and if you already sync through a provider, that keeps working exactly as it did.
Recurring tasks are expanded per RFC 5545, and everything the schema does not model is round-tripped verbatim rather than dropped — so passing your tasks through Agendula does not quietly lose fields a server sent.
Your tasks are exportable as standard iCalendar .ics files at any time, because
data you can't take with you isn't really yours.
Status: backend complete, UI catching up. Storage, reads and writes, smart-list filtering, a self-scheduled reminder engine, and export are built and unit-tested. The Material 3 Expressive screens are being built on top, one at a time — the storage-mode picker and export screen are not there yet. See
docs/ROADMAP.mdfor status,docs/ARCHITECTURE.mdfor how it's built, anddocs/STORAGE-AND-SYNC.mdfor why storage works the way it does.
Sync sources (by design)
In external-provider mode Agendula works with anything that writes to that 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.