fix(reminders): arm reminders again in our own store
Regression from deleting the provider. sync() gated on providerResolver.resolve() != null, and OWN resolves to no provider by design — so from that commit no due reminder was ever armed in what had just become the default mode, and clearAll() cancelled any that survived the upgrade. The gate is now ProviderResolver.canReadStore(): OWN is always readable, and only EXTERNAL can fail, for the two reasons it ever could. Putting the decision on the resolver rather than inside the scheduler is what makes it testable at all — ReminderScheduler needs Context and AlarmManager, which is why nothing caught this. Also brings ARCHITECTURE.md and ROADMAP.md in line with the branch: one module, OWN/EXTERNAL, the four Room tables, expansion at read time, the import and startup gate, and the manifest surface that no longer declares a provider or any permission of its own.
This commit is contained in:
145
docs/ROADMAP.md
145
docs/ROADMAP.md
@@ -10,18 +10,19 @@ 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.
|
||||
Agendula now **carries its own task store, and it is one we wrote**: a Room
|
||||
database designed against `VTODO`, with recurrence expanded at read time. The
|
||||
vendored dmfs provider and the `:provider` module that held it are deleted, a
|
||||
v0.3.x install's tasks are imported on first launch, and an external provider
|
||||
(OpenTasks / tasks.org) is a user choice rather than a requirement. 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
|
||||
hardening the new store (`OWN-STORE.md` phase 6), the **frontend surfaces for
|
||||
what has landed** (a storage-mode picker, an export screen), then M6 (Glance
|
||||
widget, translations, F-Droid release) and the sync adapter.
|
||||
|
||||
---
|
||||
|
||||
@@ -133,33 +134,86 @@ The engine exists (M1: `ReminderScheduler` + boot / provider-change re-sync,
|
||||
### ✅ 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).
|
||||
what Posture B means (our own store, 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 56 JVM tests pass.
|
||||
- ✅ Step 2, first pass — the Apache-2.0 dmfs provider 1.4.2 (DB 23) vendored
|
||||
in-tree as `:provider`, under `de.jeanlucmakiola.agendula.tasks` and our own
|
||||
permission namespace. Shipped in v0.3.x and **since superseded**: see "our own
|
||||
store" below.
|
||||
- ✅ 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
|
||||
can no longer fire in our own 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
|
||||
- ✅ Export to iCalendar (step 3) — a v1 feature now that own-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.
|
||||
Note it now means "sync into an app that has no provider", so the ask has
|
||||
changed shape.
|
||||
- ⬜ Sync adapter (step 5) — the 1.x arc. **Designed in [`SYNC.md`](SYNC.md)**,
|
||||
not started: mapper → auth → engine → hardening, ~8–9 weeks. The account model
|
||||
is settled (`AccountManager`) and `ical4android` is closed out (superseded by
|
||||
`synctools`, GPLv3, so we write the mapper in-house); what's still open is
|
||||
dav4jvm's JitPack-only distribution, conflict policy, and whether External mode
|
||||
survives the milestone.
|
||||
- ⬜ Verify on a device: the local path with no account, and the vendored
|
||||
provider's timezone-change behaviour (change 3 in `PROVENANCE.md`).
|
||||
not started: mapper → auth → engine → hardening, ~8–9 weeks, minus the 2.5–4
|
||||
weeks owning the store deletes from it (`OWN-STORE.md`, "Effects on the sync
|
||||
plan"). The account model is settled (`AccountManager`) and `ical4android` is
|
||||
closed out (superseded by `synctools`, GPLv3, so we write the mapper
|
||||
in-house); what's still open is dav4jvm's JitPack-only distribution, conflict
|
||||
policy, and whether External mode survives the milestone.
|
||||
|
||||
### ✅ Our own store — Room, and the provider deleted
|
||||
The vendored provider was kept because it appeared to hand us the sync
|
||||
bookkeeping for free; the phase-1 sync audit measured that bookkeeping and found
|
||||
most of it broken, absent or unusable. Reasoning in
|
||||
[`STORAGE-DECISION.md`](STORAGE-DECISION.md), architecture and six-phase plan in
|
||||
[`OWN-STORE.md`](OWN-STORE.md).
|
||||
- ✅ Phase 0 — occurrences addressed by `(taskId, occurrenceStart)`. `Task.id`
|
||||
(the materialised instance row id) dropped for `occurrenceStart`, lazy-list
|
||||
keys moved to `Task.occurrenceKey`, `updateInstance` re-signed, `domain/`
|
||||
stopped importing `TasksContract`. Done while the provider was still the store,
|
||||
so the External path exercised the new seam first.
|
||||
- ✅ Phase 1 — the schema: `task_lists`, `tasks`, `task_alarms`, `accounts`, a
|
||||
DAO per table, the v1 schema JSON committed for migration testing. Masters and
|
||||
`RECURRENCE-ID` overrides share the `tasks` table, so the unique index is
|
||||
`(list_id, uid, recurrence_id)`.
|
||||
- ✅ Phase 2 — `RecurrenceExpander` over `lib-recur` 0.12.2: a series expanded in
|
||||
memory at read time, bounded by a window (1 year back, 2 forward) and a hard
|
||||
per-series ceiling. No materialised instances table, so none of its staleness
|
||||
bugs. 38 tests against RFC 5545 directly, since the provider only ever
|
||||
materialised one occurrence to compare against.
|
||||
- ✅ Phase 3 — `RoomTasksDataSource` implements all 14 seam methods, picked per
|
||||
call by `ModeRoutingTasksDataSource`. Editing one occurrence writes a
|
||||
`RECURRENCE-ID` override sharing the master's UID (RFC 5545 model (a)), where
|
||||
the provider forked a new task with a new UID (model (d)). Completion rules are
|
||||
stated directly rather than worked around, so a task can no longer strand
|
||||
itself "done at 75%".
|
||||
- ✅ Phase 4 — `OneShotImport` moves a v0.3.x install's `databases/tasks.db` into
|
||||
Room on first launch, archiving the source as `tasks.db.imported`; `OWN` is the
|
||||
default; `StartupGate` holds the first store read until the mode has landed and
|
||||
the import has run; backup rules take the database with its WAL sidecars and
|
||||
the app checkpoints on `ON_STOP`.
|
||||
- ✅ Phase 5 — `:provider` deleted: 84 Java files, 14,555 lines, its `<provider>`,
|
||||
its two custom permissions and its three dmfs runtime dependencies.
|
||||
`StorageMode.LOCAL` is gone (a stored `LOCAL` reads as `OWN`); `ProviderResolver`
|
||||
narrows to discovering external providers; `ProviderChangeReceiver` filters on
|
||||
the two external authorities only. `provider/PROVENANCE.md` is replaced by a
|
||||
postscript in `STORAGE-DECISION.md`. **Breaking:** the
|
||||
`de.jeanlucmakiola.agendula.tasks` authority and both custom permissions no
|
||||
longer exist — anyone who pointed DAVx5 at that authority loses it, and the
|
||||
release notes have to say so.
|
||||
- ✅ Phase 6 — harden: `MigrationTestHelper` wired against the committed v1
|
||||
schema so v1 → v2 is cheap when sync adds columns, an Auto Backup restore test
|
||||
covering the WAL case in both directions, and a performance check at 5,000
|
||||
tasks with 20 recurring series.
|
||||
- ✅ Fallout: `ReminderScheduler.sync()` gated on `resolve() != null`, which is
|
||||
what `OWN` returns, so no due reminder armed in the default mode. It now gates
|
||||
on `ProviderResolver.canReadStore()`, with tests.
|
||||
- ⬜ **Run the instrumented suite on a device.** Six classes — the Room seam, the
|
||||
DAOs, the import, the migration harness, the restore path and the performance
|
||||
check — all compile and none has ever executed. Everything load-bearing about
|
||||
this migration is verified only by tests that have not run.
|
||||
- ⬜ Verify on a device: a fresh install on the Room store, and an upgrade from a
|
||||
v0.3.2 APK with seeded data landing every task, list and reminder.
|
||||
- ⬜ Per-locale release notes for the dropped authority and permissions.
|
||||
|
||||
---
|
||||
|
||||
@@ -174,15 +228,15 @@ These carry over from [`PLAN.md`](PLAN.md) §9; resolved ones are struck through
|
||||
(Not in the candidate list today.) Note this is now downstream of
|
||||
[`SYNC.md`](SYNC.md) open question 3: if External mode is retired once we sync
|
||||
ourselves, the question disappears with it.
|
||||
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.
|
||||
4. ~~**Posture B authority choice**~~ moot: Agendula publishes no provider and
|
||||
holds no authority at all. The question was live while the store was a
|
||||
vendored provider, and squatting `org.dmfs.tasks` was a dead end even then —
|
||||
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~~
|
||||
resolved: our own store expands a series at read time and writes an edit to
|
||||
one occurrence as a `RECURRENCE-ID` override sharing the master's UID; in
|
||||
External mode the edit still goes through the instances URI.
|
||||
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.
|
||||
@@ -196,7 +250,12 @@ These carry over from [`PLAN.md`](PLAN.md) §9; resolved ones are struck through
|
||||
|
||||
- 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.
|
||||
- Instrumented tests: `./gradlew :app:connectedDebugAndroidTest` — the Room
|
||||
schema, `RoomTasksDataSource` and `OneShotImport` (the last against
|
||||
`app/src/androidTest/assets/tasks-v23.db`, regenerated by
|
||||
`scripts/make_import_fixture.py`).
|
||||
- Any device or emulator will do for the default path: the store is ours and
|
||||
needs nothing installed. Debug builds seed an "Agendula Demo" list via
|
||||
`DemoSeeder` unless it already exists. To exercise **External** mode, use a
|
||||
device with **OpenTasks**
|
||||
or **tasks.org** installed, and ideally DAVx5 syncing a CalDAV task list.
|
||||
|
||||
Reference in New Issue
Block a user