docs: bring the docs in line with what shipped
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>
This commit is contained in:
@@ -4,9 +4,13 @@
|
||||
> bundle OpenTasks" working notes, which are withdrawn (see
|
||||
> [Dead ends](#dead-ends--do-not-revisit)). This is the detailed companion to
|
||||
> `ARCHITECTURE.md` §7 and the `ProviderResolver` comments, **and it redefines
|
||||
> what Posture B means** — those two need a follow-up edit.
|
||||
> `ROADMAP.md` / `PLAN.md` remain known-stale and are due a deliberate pass;
|
||||
> this document does not attempt it.
|
||||
> what Posture B means.**
|
||||
>
|
||||
> **Status, 2026-08-02: steps 1–3 are done** — see
|
||||
> [Sequencing](#sequencing). `ARCHITECTURE.md` and `ROADMAP.md` have had their
|
||||
> follow-up pass and now match what shipped; `PLAN.md` is the original design
|
||||
> document and is left as the historical record. What is built, and what is still
|
||||
> only described here, is marked step by step below.
|
||||
|
||||
## The plan, in short
|
||||
|
||||
@@ -24,13 +28,13 @@
|
||||
|
||||
**In what order**
|
||||
|
||||
| # | Step | Why now |
|
||||
|---|---|---|
|
||||
| 1 | Merge `fix/provider-interaction-review` | unmerged and rotting; touches the same permission flow as step 2 |
|
||||
| 2 | Vendor `:provider` under our own authority | the identity, done once — and it ships a complete local-first app |
|
||||
| 3 | Export / backup | our data now lives only in our app's private storage |
|
||||
| 4 | File the DAVx5 issue | cheap, non-blocking, serves F-Droid users |
|
||||
| 5 | Sync adapter | the 1.x arc; design discussion pending |
|
||||
| # | Step | Why now | Status |
|
||||
|---|---|---|---|
|
||||
| 1 | Merge `fix/provider-interaction-review` | unmerged and rotting; touches the same permission flow as step 2 | ✅ done |
|
||||
| 2 | Vendor `:provider` under our own authority | the identity, done once — and it ships a complete local-first app | ✅ done |
|
||||
| 3 | Export / backup | our data now lives only in our app's private storage | ✅ backend done; no UI yet |
|
||||
| 4 | File the DAVx5 issue | cheap, non-blocking, serves F-Droid users | ⬜ |
|
||||
| 5 | Sync adapter | the 1.x arc; design discussion pending | ⬜ |
|
||||
|
||||
Everything below is the reasoning behind those choices, the alternatives that
|
||||
were rejected, and the constraints they have to survive.
|
||||
@@ -64,8 +68,8 @@ Plus a standing rule: **anything that isn't task-domain goes to floret-kit.**
|
||||
|
||||
### The vocabulary, redefined
|
||||
|
||||
`ARCHITECTURE.md` §7 and `ProviderResolver`'s KDoc still describe Posture B as
|
||||
"bundle OpenTasks and find `org.dmfs.tasks` first." Replace with:
|
||||
`ARCHITECTURE.md` §7 and `ProviderResolver`'s KDoc used to describe Posture B as
|
||||
"bundle OpenTasks and find `org.dmfs.tasks` first." Both now read as below:
|
||||
|
||||
- **Posture A** — front-end over an *external* provider (OpenTasks, tasks.org).
|
||||
Still fully supported; it stops being the default and becomes a **user
|
||||
@@ -173,19 +177,36 @@ feature — see [Storage modes](#storage-modes--the-users-choice).
|
||||
Local and Synced are the same store — Synced is Local with an account attached,
|
||||
so switching on sync is not a migration.
|
||||
|
||||
**Resolver ordering needs deciding.** Today `ProviderResolver.CANDIDATES` is a
|
||||
fixed priority list and the first hit wins. Once we bundle our own provider,
|
||||
"first hit" is the wrong rule: someone who used Agendula locally and *later*
|
||||
installs DAVx5 + OpenTasks would see an external candidate outrank the provider
|
||||
that actually holds their data. Options: rank ours first whenever it's
|
||||
non-empty, or make the mode an explicit Settings choice (it's user-visible
|
||||
either way, so probably both — auto-pick a sane default, let Settings override).
|
||||
**Resolver ordering — decided, and it went both ways as expected.**
|
||||
`ProviderResolver` now takes an explicit `StorageMode` from Settings when there
|
||||
is one, and otherwise calls `autoMode()`. The auto rule turned out to be sharper
|
||||
than "rank ours first whenever it's non-empty", and needs no database probe:
|
||||
|
||||
**Export/backup is a v1 feature.** Not, as previously framed, a migration safety
|
||||
net for "uninstall OpenTasks" — that scenario no longer exists. It's data
|
||||
portability for Local-mode users, whose tasks otherwise exist in exactly one
|
||||
place with no second copy. On Play, where most users won't have a sync engine,
|
||||
that's the majority.
|
||||
> **External if we already hold an external provider's runtime permission,
|
||||
> otherwise Local.**
|
||||
|
||||
That permission is dangerous-level, so it can only be there because an earlier
|
||||
version asked and the user agreed — which is exactly what "existing Posture A
|
||||
user" means. A fresh install holds nothing and gets local-first. ⬜ The Settings
|
||||
override the rule assumes is not built yet.
|
||||
|
||||
**Note on the mode vocabulary.** The code has two modes, not three:
|
||||
`StorageMode.LOCAL` and `StorageMode.EXTERNAL`. As this document says two
|
||||
paragraphs up, Synced *is* Local with an account attached — so it is derived
|
||||
state, and giving it its own constant would imply switching sync on is a
|
||||
migration when the whole point is that it isn't.
|
||||
|
||||
**Export/backup is a v1 feature.** ✅ Backend built (no UI yet). Not, as
|
||||
previously framed, a migration safety net for "uninstall OpenTasks" — that
|
||||
scenario no longer exists. It's data portability for Local-mode users, whose
|
||||
tasks otherwise exist in exactly one place with no second copy. On Play, where
|
||||
most users won't have a sync engine, that's the majority.
|
||||
|
||||
One `.ics` per list — a list is a CalDAV collection, and that's the unit other
|
||||
clients understand — written through SAF to either a folder or a single zip.
|
||||
Local tasks have no `_uid` (only a sync adapter may assign one), so the writer
|
||||
synthesises a stable UID per task; without it a re-imported backup would
|
||||
duplicate every task instead of matching it.
|
||||
|
||||
---
|
||||
|
||||
@@ -241,10 +262,11 @@ mechanisms, and conflating them is how apps end up over-permissioned:
|
||||
| `INTERNET` | **don't declare it until sync ships** |
|
||||
| `GET_ACCOUNTS` | never — stripped from the vendored provider; our own account type doesn't need it to see its own accounts |
|
||||
|
||||
**Work item:** the permission gate in `RootScreen` / `PermissionViewModel` /
|
||||
`ProviderResolver.hasPermission` currently assumes an external provider always
|
||||
needs a grant. It needs a bypass for our own provider. Modest, but it's the
|
||||
exact flow `fix/provider-interaction-review` just touched — merge that first.
|
||||
~~**Work item:** the permission gate … needs a bypass for our own provider.~~
|
||||
✅ Done. `ProviderResolver.hasPermission` short-circuits to `true` when
|
||||
`TaskProvider.isOwn`, so `ProviderStatus.NEEDS_PERMISSION` cannot fire in
|
||||
Local/Synced mode, and `PermissionViewModel` never offers our own permissions to
|
||||
the request launcher. Covered by `ProviderResolverTest`.
|
||||
|
||||
---
|
||||
|
||||
@@ -259,7 +281,7 @@ task-domain, it goes to the kit.
|
||||
| ContentProvider seam — `ColumnReader`, failures, observer→Flow | `core-provider` | **already on the kit's deferred list**, blocked on migrating Calendula to the name-based reader. Bundling our own provider is the forcing function that makes this worth doing. |
|
||||
| Runtime-permission staging — request/state machine, rationale plumbing, "ask at point of use" | new, e.g. `core-permissions` | pure mechanics, and Calendula has the identical problem |
|
||||
| DAV client + iCalendar parse/serialize | new, e.g. `core-dav` | **the big one.** Calendula is a calendar app; it needs the same primitives. Worth designing for two consumers from the start rather than extracting later |
|
||||
| Export/backup plumbing — SAF, file writing, share-out | kit | the *serialization* of tasks is domain; the plumbing isn't |
|
||||
| Export/backup plumbing — SAF, file writing, share-out | kit | the *serialization* of tasks is domain; the plumbing isn't. **Built app-local for now** (`data/export/ExportWriter`), on the kit's own principle of not extracting before a second consumer exists — the seam is in place, so moving it is a file move. `ICalendarWriter` stays app-local permanently: it's domain. |
|
||||
| Sync-adapter/account scaffolding | kit, probably | the `AbstractThreadedSyncAdapter` + authenticator boilerplate is identical everywhere; the delta logic is domain |
|
||||
|
||||
**Stays app-local:** the vendored `:provider` module (task-specific, and
|
||||
@@ -337,14 +359,33 @@ the roadmap should say so rather than inheriting the old estimate.
|
||||
## Open questions
|
||||
|
||||
1. **Sync protocol coverage**, account model, conflict resolution — the next
|
||||
discussion.
|
||||
2. **Resolver ordering / mode selection UX** once our provider coexists with
|
||||
external ones (see [Storage modes](#storage-modes--the-users-choice)).
|
||||
3. **Does the vendored provider work with no account at all?** Local-only mode
|
||||
depends on it entirely. First thing the vendoring work should prove.
|
||||
4. **`ical4android` licensing** vs our MIT.
|
||||
discussion. Still open.
|
||||
2. **Resolver ordering** — ✅ decided, see
|
||||
[Storage modes](#storage-modes--the-users-choice). The **mode-selection UX**
|
||||
is still open: `autoMode()` picks a default, but the Settings override it
|
||||
assumes does not exist yet.
|
||||
3. **Does the vendored provider work with no account at all?** ✅ Answered, with
|
||||
a caveat about *how* it was answered.
|
||||
|
||||
By construction: `cleanUpLists` exempts local lists explicitly (upstream's own
|
||||
rule), and our rework restricts pruning to account types this package
|
||||
authenticates — currently none — so nothing can be pruned at all.
|
||||
`ProviderAccountCleanupTest` creates a local list and a task in it with zero
|
||||
accounts present and reads both back.
|
||||
|
||||
⚠️ **But that test is Robolectric, and it skips on ARM64**, where Robolectric
|
||||
has no SQLite backend in either mode. It runs on x86_64 CI. It is not a
|
||||
substitute for a device, and this remains on the device-verification list.
|
||||
4. **`ical4android` licensing** vs our MIT. Still open — and note the export path
|
||||
does *not* depend on it: `ICalendarWriter` is our own ~200 lines, no library.
|
||||
The question is really about the sync adapter's iCalendar *parsing*.
|
||||
5. **jtx Board** as an additional External-mode candidate — richer contract,
|
||||
later. (`PLAN.md` decision #3, still open.)
|
||||
6. **The vendored provider's timezone-change behaviour** — upstream's receiver
|
||||
has a comment describing `break`s that were never written. We preserved the
|
||||
observed behaviour and wrote it out explicitly; which of code or comment is the
|
||||
bug wants a device to settle. Change 3 in
|
||||
[`provider/PROVENANCE.md`](../provider/PROVENANCE.md).
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user