A review of every path through the OpenTasks/tasks.org ContentProvider, prompted by edited due times reverting. Four independent defects produced that one symptom, plus several unrelated ones alongside. Edits reverting - The edit form was bound from a LaunchedEffect in the nav host while its ViewModel survives on the back stack, and bindEdit replaced state wholesale. MainActivity declares no configChanges, so any Activity recreation (rotation, theme/font/display-size change, split-screen, unfolding) re-fired the effect and overwrote in-progress edits with the stored row. Guarded with a `bound` flag; picker state moved to rememberSaveable so an open picker also survives. All-day handling - All-day items are date-only in iCalendar and belong at UTC midnight with a null tz. The app wrote *local* midnight, so in Berlin an all-day task drifted back a day on every save cycle, corrupting anything synced. Rendering had the mirror bug, so the two cancelled out locally and hid each other. - Toggling the all-day switch flipped the flag but left the timestamp, so an all-day task toggled off read back as 02:00 — another apparent "time reset". - New domain/AllDayTime.kt owns the two conventions and the conversion between them; the picker, the write mapper and the toggle all go through it. Provider write contract - DUE and DURATION are mutually exclusive and the provider validates the merged row, so saving a due date onto a task that carried a duration threw IllegalArgumentException — the save simply failed. DURATION is now cleared alongside every time write. - A recurring task's start/due are read from the instances view, and writing them back to tasks/<id> re-anchored the whole series. Updates now go through instances/<id>, where the provider forks an override instead. - Recurrence is derived from rrule/rdate rather than the is_recurring column: that column only exists from OpenTasks 1.4.0 (DB 23) and is absent on tasks.org's bundled provider (DB 22), where it would report every recurring task as one-off and send its edits to the anchor. Reminders - The per-task Reminder field in the edit form was inert: never persisted, never read back, and REMINDER_WITHOUT_DUE could block a save over a value that was discarded regardless. Leads are now stored as Alarm property rows and preferred over the per-list/global setting. Written before the task update so a recurrence fork copies them onto the override. Note the provider fires nothing itself — ReminderScheduler still arms the alarm. - Reminders were keyed by task id over rows read from the instances view, so .toMap() collapsed a recurring task to one arbitrary occurrence (the query is unsorted). Now keyed per occurrence, with request codes and intent data to match. Missed reminders within 6h fire once on boot instead of being dropped. Robustness - Four terminal `catch`es killed their upstream on the first provider failure. SettingsViewModel is collected in setContent above the permission gate for the Activity's lifetime, so a pre-grant SecurityException left the list picker empty until the process restarted. Replaced with capped-backoff retry. - lazyChildren had no catch at all; an exception escaped stateIn past viewModelScope's SupervisorJob and crashed the process. - Observer registration is all-or-nothing (the second register throwing leaked the first), ProviderChangeReceiver validates action and authority and debounces, and the permission gate re-checks on resume. Also drops the unused DateTimeField composable and the stale INSTANCES projection, which omitted the recurrence columns the mapper now depends on. Bumps floret-kit to pick up the matching all-day formatting fix. Verified by unit tests (43 app, 15 core-time) and a clean assembleDebug; the provider interaction itself has not been exercised on a device. Co-Authored-By: Claude Opus 4.8 (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.
License
MIT — see LICENSE.