STORAGE-AND-SYNC.md asked for a follow-up pass on ARCHITECTURE.md §7 and the
ProviderResolver KDoc, which still defined Posture B as "bundle OpenTasks and
find org.dmfs.tasks first" — the plan that was withdrawn as a dead end. That pass,
plus the status the doc left open.
ARCHITECTURE.md now describes the app as built: two modules, the storage-mode
table with the permission each needs, the autoMode rule and why it keys on
holding an external provider's permission, the two-not-three mode vocabulary, and
a manifest section that says what :provider contributes and what is deliberately
absent (GET_ACCOUNTS, INTERNET). §7 records squatting the dmfs authority as a
dead end rather than a road not yet taken, so it doesn't get re-proposed.
ROADMAP.md turns "Posture B, later" into what actually landed and lists what
didn't: the frontend surfaces, the DAVx5 issue, the sync adapter, and device
verification. Two open decisions resolved and struck through — the authority
choice, and recurrence-aware editing, which fix/provider-interaction-review made
stale.
STORAGE-AND-SYNC.md gets per-step status. Open question 3 ("does it work with no
account?") is answered, with the caveat that the test proving it is Robolectric
and skips on ARM64 — answered by construction, not yet on a device.
PLAN.md gets a banner. It's the original design document and still holds the
reasoning behind the layering, but two of its premises are overturned and it
should not be read as current.
README.md was telling users they need a tasks provider installed. They don't, and
that's the headline feature: a table of where tasks can live, that our provider
coexists with OpenTasks rather than replacing it, and that everything exports as
standard .ics.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
197 lines
11 KiB
Markdown
197 lines
11 KiB
Markdown
# Agendula — roadmap
|
||
|
||
Where the project is and where it's going. This is the **status** view; the
|
||
design rationale behind each milestone lives in [`PLAN.md`](PLAN.md), and how the
|
||
pieces fit together is in [`ARCHITECTURE.md`](ARCHITECTURE.md).
|
||
|
||
Status legend: ✅ done · 🚧 in progress · ⬜ not started
|
||
|
||
---
|
||
|
||
## Current state (one line)
|
||
|
||
Agendula now **carries its own task store**: the `:provider` module ships the
|
||
vendored dmfs provider under our authority, so the app is complete and local-first
|
||
with nothing else installed, and an external provider (OpenTasks / tasks.org) is a
|
||
user choice rather than a requirement. The non-visual stack over `TaskContract` is
|
||
done and unit-tested, export to iCalendar has landed, and the Material 3
|
||
Expressive UI is built through **M5**: lists → task list (swipe gestures, inline
|
||
add, smart-list section headers) → detail / edit with full CRUD, date-time
|
||
pickers, priority, percent-complete, conflict-safe saves, per-task reminders, and
|
||
subtask create + reparent — plus a one-time reminder onboarding step and a
|
||
Settings screen. Remaining work is the **frontend surfaces for what just landed**
|
||
(a storage-mode picker, an export screen), then M6 (Glance widget, translations,
|
||
F-Droid release) and the sync adapter.
|
||
|
||
---
|
||
|
||
## Milestones
|
||
|
||
### ✅ M0 — Skeleton
|
||
Project scaffolding copied from Calendula (Gradle, version catalog, Hilt,
|
||
Material 3 Expressive theme, Gitea CI/release workflows, F-Droid metadata),
|
||
reseeded to a warm-mauve fallback palette. Builds, shows a themed placeholder.
|
||
|
||
### ✅ M1 — Read path + logic (the "backoffice")
|
||
The complete non-visual stack:
|
||
- Vendored `TaskContract` subset, `ProviderResolver` (runtime authority
|
||
detection), `ColumnReader`, mappers, `AndroidTasksDataSource` (Instances
|
||
query + `ContentObserver`), and `TasksRepository` exposing live Flows of
|
||
lists / tasks / detail.
|
||
- Domain models, smart-list filtering (Today / Upcoming / Overdue / No-date /
|
||
All / Completed), sorting, form validation, subtasks via `parentId`.
|
||
- Self-scheduled due-reminder engine (AlarmManager + boot / provider-change
|
||
re-sync), notifications, DataStore prefs.
|
||
- Render-only ViewModels + UiState for **every** screen, so the UI is build-only
|
||
from here.
|
||
- JVM unit tests across mappers, filtering, sorting, form, value mapping, and
|
||
day windows.
|
||
|
||
### ✅ M2 — Screens: lists, task list, complete & CRUD
|
||
The real Material 3 Expressive UI, built screen by screen against the M1
|
||
ViewModels.
|
||
- ✅ Provider/permission onboarding gate (`RootScreen`).
|
||
- ✅ Lists overview (`ListsScreen`) — smart lists + user lists grouped by account.
|
||
- ✅ Navigation host wiring (`AgendulaNavHost` + `Dest` route table: lists → task
|
||
list → detail / edit, each binding its M1 ViewModel from route args).
|
||
- ✅ Task list screen — checkbox rows + toggle-complete, swipe-to-complete /
|
||
-delete (`SwipeToDismissBox`), inline add field (`InlineAdd` → `quickAdd`),
|
||
smart-list section headers (`TaskSections`).
|
||
- ✅ Detail + edit screens — detail shows title / description / subtasks with
|
||
edit+delete; edit has the full CRUD form (title, description, save) wired to a
|
||
list picker.
|
||
|
||
### ✅ M3 — Detail / edit polish
|
||
- ✅ Due / start date-time pickers (`DateTimePickerFlow` in `TaskEditScreen`).
|
||
- ✅ Priority — coloured by level: green / amber / red pastels (`priorityFill`;
|
||
M3 has no priority role, only `error`). A shared `ui/common/PriorityChip` on the
|
||
list and detail screens, and the edit form's M3 segmented selector tints its
|
||
active segment in the chosen level's hue.
|
||
- ✅ Smart-list section presentation (`TaskSections` headers on the task list).
|
||
- ✅ Percent-complete field — optional "Progress" slider (5% detents) on the edit
|
||
form; writes `Tasks.PERCENT_COMPLETE` (clamped 0–100, status left to the
|
||
complete toggle).
|
||
- ✅ Conflict-safe saves — `updateTask` re-checks `last_modified` against the
|
||
value captured when the form loaded and throws `TaskConflictException`; the
|
||
editor offers overwrite-or-cancel instead of clobbering an external change.
|
||
|
||
### ✅ M4 — Subtasks (UI)
|
||
`RELATED-TO` hierarchy in the UI. The data layer already reads `parentId`.
|
||
- ✅ Create subtask (`AddSubtaskField` → `TaskDetailViewModel.addSubtask` sets
|
||
`parentId`); subtasks render as a grouped section in the detail screen. Tapping
|
||
a subtask opens its own detail (a new `TaskDetail` entry, so it can have
|
||
children too), and the subtask's detail shows a tappable "Part of …" parent
|
||
card so it never reads as a stray standalone task.
|
||
- ✅ Inline expansion on the task list — a parent row has a dedicated expand
|
||
button (trailing chevron, separate from the count chip). Each section flattens
|
||
into one grouped run (parents + their expanded children) and corners are chosen
|
||
per-edge (`cornerPosition`): top-level tasks keep their own run, an expanded
|
||
parent opens its bottom to its children, and the child group rounds off on its
|
||
last segment while the next top-level task stays mid-run. Children are
|
||
full-width, set a step down in tone (`surfaceContainer` vs the parents'
|
||
`surfaceContainerHigh`) and with no colour bar (checkbox stays aligned). An expanded group ends with an inline "add a subtask"
|
||
row (on by default; opt out in Settings → Tasks since M5).
|
||
Offered only where the list holds all the children (a real list; smart lists
|
||
that omit off-day children stay collapsed). `TaskDetail.parent` carries the
|
||
parent for the detail card.
|
||
- ✅ Reparent — a full-width, searchable "Parent task" sheet on the edit form
|
||
groups active candidates by due-date section (Overdue / Today / Upcoming / No
|
||
date) and files a task under any top-level task in its list (or "None" to
|
||
promote it); switching list clears the now-invalid parent. Candidates stay
|
||
active + top-level to keep nesting one level deep, matching the detail screen.
|
||
|
||
### ✅ M5 — Reminders onboarding & polish
|
||
The engine exists (M1: `ReminderScheduler` + boot / provider-change re-sync,
|
||
`DueReminderReceiver`, `TaskNotifier`).
|
||
- ✅ Per-task reminder-offset UI (`ReminderPickerDialog` → `TaskForm`
|
||
`reminderMinutesBeforeDue`).
|
||
- ✅ `POST_NOTIFICATIONS` onboarding flow — a one-time `ReminderOnboardingScreen`
|
||
(Calendula's shell + copy adapted for tasks) gated in `RootScreen` after the
|
||
provider/permission grant; it requests the runtime permission (API 33+) and
|
||
records the choice (`reminderOnboardingDone`). Re-requestable from Settings.
|
||
- ✅ Exact-alarm surface — a status row in Settings → Reminders, shown only on
|
||
Android 12 (where `SCHEDULE_EXACT_ALARM` is revocable), deep-linking to the
|
||
system grant screen; on 13+ the app holds `USE_EXACT_ALARM` (always granted).
|
||
- ✅ Default reminder-offset settings UI — exposed as "When to remind" in
|
||
Settings → Reminders (`reminderLeadMinutes`), behind a master `remindersEnabled`
|
||
switch that gates the entire engine (`ReminderScheduler` clears all alarms when
|
||
off; `DueReminderReceiver` suppresses any in-flight fire).
|
||
- ⬜ End-to-end verification on device (build + unit tests green; not yet run on
|
||
a device with a live provider).
|
||
|
||
### 🚧 M6 — Settings, widget, i18n, release
|
||
- ✅ F-Droid metadata scaffolded (`fdroid-metadata/`).
|
||
- ✅ Settings screen — landed early with M5 (`SettingsScreen` in the nav graph,
|
||
reached by the gear on the lists overview). Covers theme, dynamic colour, due
|
||
reminders (toggle + default offset + exact-alarm status), default list, and the
|
||
add-a-subtask-row opt-out. Still ⬜ a **language** entry (deferred until there
|
||
are translations to switch to).
|
||
- ⬜ Glance task-list widget — deps present in `build.gradle.kts`, zero impl.
|
||
- ⬜ Translations — only `res/values/` (English); no `values-XX`.
|
||
- ⬜ Finalize F-Droid metadata, confirm CI release flow.
|
||
|
||
### ✅ Posture B — our own task store
|
||
Agendula stopped depending on a provider app being installed. Direction and
|
||
reasoning in [`STORAGE-AND-SYNC.md`](STORAGE-AND-SYNC.md); note it **redefined**
|
||
what Posture B means (our own authority, coexisting with everything — *not*
|
||
squatting `org.dmfs.tasks`, which is a dead end).
|
||
- ✅ `fix/provider-interaction-review` merged (step 1).
|
||
- ✅ `:provider` — the Apache-2.0 dmfs provider 1.4.2 (DB 23) vendored in-tree
|
||
under `de.jeanlucmakiola.agendula.tasks` and our own permission namespace.
|
||
`GET_ACCOUNTS` dropped, with the account-cleanup path reworked so it can only
|
||
prune account types we authenticate ourselves — the deletion is unsafe without
|
||
that rework. Modernized to minSdk 29 / targetSdk 36 / Java 17.
|
||
[`provider/PROVENANCE.md`](../provider/PROVENANCE.md) records every deviation,
|
||
each also marked `AGENDULA CHANGE` at the site. Upstream's 51 JVM tests pass.
|
||
- ✅ Storage modes + the permission-gate bypass — `ProviderStatus.NEEDS_PERMISSION`
|
||
can no longer fire in Local mode, and an upgrading Posture A user stays on the
|
||
provider that holds their data (`ProviderResolver.autoMode`).
|
||
- ✅ Export to iCalendar (step 3) — a v1 feature now that Local-mode data lives
|
||
only in our app's private storage. One `.ics` per list, to a folder or a zip,
|
||
via SAF. Backend only.
|
||
- ⬜ **Frontend surfaces for the above** — a storage-mode picker in Settings and
|
||
an export screen. The backend is done and unused until these exist.
|
||
- ⬜ File the DAVx5 issue (step 4) — non-blocking, cheap, serves F-Droid users.
|
||
- ⬜ Sync adapter (step 5) — the 1.x arc. Design discussion still open: protocol
|
||
coverage, account model, conflict resolution, and `ical4android`'s licence
|
||
against our MIT.
|
||
- ⬜ Verify on a device: the local path with no account, and the vendored
|
||
provider's timezone-change behaviour (change 3 in `PROVENANCE.md`).
|
||
|
||
---
|
||
|
||
## Open decisions / to verify
|
||
|
||
These carry over from [`PLAN.md`](PLAN.md) §9; resolved ones are struck through.
|
||
|
||
1. ~~**Name** — `Agendula`~~ confirmed (appId `de.jeanlucmakiola.agendula`).
|
||
2. ~~**tasks.org provider authority**~~ verified on device:
|
||
`org.tasks.opentasks` + `org.tasks.permission.*`.
|
||
3. **jtx Board** — support its richer contract later, or stay OpenTasks-only?
|
||
(Not in the candidate list today.)
|
||
4. ~~**Posture B authority choice**~~ resolved: **our own**
|
||
`de.jeanlucmakiola.agendula.tasks`. Squatting `org.dmfs.tasks` is a dead end,
|
||
not merely a trade-off — two apps cannot declare the same authority or
|
||
permission name, so anyone with OpenTasks installed could not have installed
|
||
Agendula at all.
|
||
5. ~~**Recurring tasks** — recurrence-aware editing out of scope for v1~~ stale:
|
||
`fix/provider-interaction-review` routes edits to a recurring task through the
|
||
instances URI, so the provider forks an override instead of re-anchoring the
|
||
series.
|
||
6. **Resolver ordering / mode-selection UX** — `autoMode()` picks a sane default
|
||
today (see [`ARCHITECTURE.md`](ARCHITECTURE.md) §4.1); the Settings override it
|
||
assumes is not built yet.
|
||
7. **Sync protocol coverage**, account model, conflict resolution — the next
|
||
design discussion.
|
||
|
||
---
|
||
|
||
## How to contribute / verify
|
||
|
||
- Build: `./gradlew :app:assembleDebug`
|
||
- Unit tests: `./gradlew :app:testDebugUnitTest`
|
||
- Run on a device/emulator that has **OpenTasks** or **tasks.org** installed (and
|
||
ideally DAVx5 syncing a CalDAV task list) so the read/write paths have real
|
||
data. Debug builds use `DemoSeeder` for sample data when no provider data is
|
||
present.
|