Merge remote-tracking branch 'origin/release/v1.0.0' into feat/caldav-sync

# Conflicts:
#	app/build.gradle.kts
#	app/src/main/java/de/jeanlucmakiola/agendula/data/di/DataModule.kt
#	app/src/main/java/de/jeanlucmakiola/agendula/data/di/Qualifiers.kt
#	app/src/main/java/de/jeanlucmakiola/agendula/ui/export/ExportScreen.kt
#	app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/SettingsScreen.kt
#	docs/PRIVACY.md
#	gradle/libs.versions.toml
This commit is contained in:
2026-09-23 13:52:33 +02:00
45 changed files with 1343 additions and 174 deletions
+43
View File
@@ -332,6 +332,17 @@ gain.
## Migrating existing users
> ⚠️ **Corrected, 2026-09-21.** The premise below is wrong, and it was wrong when
> it was written: **no release ever bundled the provider.** It was added and
> deleted inside this one unreleased cycle, so `databases/tasks.db` exists on no
> published install and `OneShotImport` finds nothing to do for every real user.
> Anyone on 0.3.x or 0.4.0 keeps their tasks in OpenTasks or tasks.org, which
> `ProviderResolver.autoMode()` correctly keeps them on. The path onto the new
> store for those users is `ExternalImport` — see
> [Copying from an external provider](#copying-from-an-external-provider) below.
> Everything in this section still applies to a device that ran a dev build of
> this branch, which is why the import, its fixture and its tests stay.
Anyone on v0.3.x has their tasks inside the bundled provider's SQLite file at
`/data/data/de.jeanlucmakiola.agendula/databases/tasks.db` (dmfs schema
version 23). Removing the Gradle module does **not** remove that file — an app
@@ -384,6 +395,38 @@ still exist:
3. Only after a release with no import defects reported does a subsequent
version delete `tasks.db.imported`.
A failure is no longer invisible, either. `runIfNeeded` used to hand back an
`ImportResult.Failed` that `StartupGate` discarded — an upgrading user got an
empty app, no message, and their tasks in a file only a developer could name. The
outcome is now recorded in DataStore and logged, and Settings → Storage offers the
retry, which is what makes `reimportFromArchive()` reachable at all.
### Copying from an external provider
The migration that actually matters for 1.0.0, since the one above serves nobody.
`ExternalImport` (`data/tasks/transfer/`) reads the external provider through the
existing `exportTasks` seam and writes lists, tasks and alarms into Room in one
transaction with verified counts — the same shape as `OneShotImport`, for the
same reason. Task `uid`s are preserved, so the rows can be attached to a CalDAV
collection once sync lands rather than duplicating server-side.
- **User-initiated, once.** Settings → Storage → *Copy tasks from …*, behind a
confirm that names the real task count. Offered only while a provider is
installed and permitted and no copy has succeeded; a second run would duplicate
everything, because what it writes is indistinguishable from hand-typed tasks
the moment it finishes.
- **A copy, not a sync.** The source is untouched, whatever syncs it keeps
syncing it, and the two sets drift from that moment. Switching stores now asks
first for the same reason: neither store hands its rows to the other.
- **One-directional.** 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**, because the read seam is shaped for iCalendar
output: per-occurrence `RECURRENCE-ID` overrides (a series arrives as master +
rule), `EXDATE`, `CLASS`, `DURATION`, the per-task timezone, and a task's exact
`PRIORITY` digit (`Priority` buckets 1–4 as HIGH). Nothing the app itself
displays is lost.
This is the reason phase 5 (deleting `:provider`) ships *after* phase 4 rather
than with it — and the reason the deletion is its own release.
+16 -7
View File
@@ -1,7 +1,7 @@
---
title: Privacy Policy — Agendula
description: What Agendula does with your data. No servers, no account, no analytics — your tasks stay on your device unless you add a CalDAV server yourself.
updated: 2026-09-09
updated: 2026-09-21
---
<!--
@@ -21,7 +21,7 @@ updated: 2026-09-09
render on the published page.
-->
**Last updated:** 9 September 2026
**Last updated:** 21 September 2026
Applies to the Android app **Agendula** (package `de.jeanlucmakiola.agendula`),
all versions and all distribution channels.
@@ -37,7 +37,7 @@ nothing and no one else. Nothing is ever sent to the developer.
IT-Dienstleister | Jean-Luc Makiola
Mahlerstraße 10
14772 Brandenburg an der Havel
Email: [business@jeanlucmakiola.de](mailto:business@jeanlucmakiola.de)
Email: [support@jeanlucmakiola.de](mailto:support@jeanlucmakiola.de)
## 2. No data collection by the developer
@@ -144,9 +144,13 @@ stored locally on your device and are removed when you uninstall the app.
If Android Auto Backup is enabled on your device, your tasks and settings may
be backed up to your own Google account, under Google's terms — the developer
has no access to it. Two things are deliberately excluded from that backup:
your stored CalDAV password, and Agendula's per-device sync bookkeeping. After
restoring onto a new device you therefore sign in to your server again.
has no access to it. What travels is Agendula's own task database and your
settings, and nothing else: the backup rules name those explicitly, which makes
everything not named — including the archived copy the app keeps of an older
version's database — excluded by default. Two things are deliberately kept out
of that backup: your stored CalDAV password, and Agendula's per-device sync
bookkeeping. After restoring onto a new device you therefore sign in to your
server again.
## 7. Crash reports
@@ -183,6 +187,10 @@ process — it only opens the address.
## 9. Permissions and why they exist
This is the complete list the released app declares — you can check it against
the app's entry in F-Droid, or against `app/src/main/AndroidManifest.xml` in the
source:
- `INTERNET`, `ACCESS_NETWORK_STATE` — CalDAV sync with the server you
configure, and checking whether a connection exists before trying. Without a
CalDAV account, no connection is made.
@@ -195,7 +203,8 @@ process — it only opens the address.
- `org.dmfs.permission.READ_TASKS` / `WRITE_TASKS` and
`org.tasks.permission.READ_TASKS` / `WRITE_TASKS` — optional, requested only
if you choose the external-provider storage mode, and only for the provider
you selected (OpenTasks or tasks.org).
you selected (OpenTasks or tasks.org). All four are declared in the manifest
because a manifest is static, but none is requested until you pick that mode.
- `WAKE_LOCK`, `FOREGROUND_SERVICE` — required by the Android system component
used for scheduled background work (WorkManager); on older Android versions
it needs them to run an expedited sync.
+74 -15
View File
@@ -126,10 +126,14 @@ The engine exists (M1: `ReminderScheduler` + boot / provider-change re-sync,
- ✅ 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`.
add-a-subtask-row opt-out — plus the **App language** picker, which landed with
the 0.4.0 translations.
- ⬜ Glance task-list widget — still just an idea, and now without a foothold:
the `glance-appwidget` / `glance-material3` dependencies were declared but never
used by a single line, so they were shipping dead weight in the APK and have
been removed. Re-add them with the implementation, not before.
- ✅ Translations — German and Brazilian Portuguese, via Weblate, shipped in
0.4.0 (`res/values-de`, `res/values-pt-rBR`, `res/xml/locales_config.xml`).
- ⬜ Finalize F-Droid metadata, confirm CI release flow.
### ✅ Posture B — our own task store
@@ -206,10 +210,13 @@ most of it broken, absent or unusable. Reasoning in
`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.
postscript in `STORAGE-DECISION.md`. **Not a breaking change for anyone, as it
turns out:** the `de.jeanlucmakiola.agendula.tasks` authority and its two custom
permissions were added and deleted inside this same unreleased cycle
(`git tag --contains` on the commit that added `:provider` comes back empty), so
no published version ever carried them and nobody could have pointed DAVx5 at
one. The release notes must *not* warn about losing something that never
shipped.
- ✅ 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
@@ -229,13 +236,65 @@ most of it broken, absent or unusable. Reasoning in
stopped reading. `setCompletedInstance` now forks a `RECURRENCE-ID` override
the way `updateInstance` does — phase 2 always specified this, only the edit
half had it.
- ⬜ **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.
- ✅ **Ran the instrumented suite on a device** — 52 tests, 0 failures, Pixel 10
/ API 36, 13 Aug 2026; all six classes (the Room seam, the DAOs, the import, the
migration harness, the restore path, the performance check). Mind the ARM64
`aapt` trap: the task can exit non-zero on a fully green run, so read
`app/build/outputs/androidTest-results/connected/debug/*.xml` before believing
the exit code.
- ✅ **Re-ran it against the branch tip** — 66 tests, 0 failures, Pixel 10 /
API 37, 22 Sep 2026, now including `ExternalImportTest`. Worth having done:
the first run failed one test.
`RoomTasksDataSourceTest.alarmsRoundTripAndReplaceRatherThanAccumulate`
asserted `alarms()[id] == 30`, from before the seam returned a `TaskReminder`
rather than a bare minute count — the production value was right
(`TaskReminder(minutesBefore=30, fromStart=false)`) and the assertion was
stale. It compiled because Truth's `isEqualTo` takes `Any?`, so nothing but
executing it could have caught it. Exactly the defect class this item
existed to find.
- ⬜ Verify on a device — and note that the upgrade path this phase was designed
around is **not** the one real users are on. `OneShotImport` reads a bundled
dmfs provider's `databases/tasks.db`, and no release ever bundled one, so every
existing install's tasks sit in OpenTasks or tasks.org instead. What has to be
checked is therefore:
1. a fresh install landing on the Room store with nothing installed;
2. an upgrade from the 0.4.0 APK **with OpenTasks installed and permitted** —
`autoMode()` must keep them on External and show their tasks unchanged;
3. Settings → Storage → *Copy tasks from …* moving those tasks into the Room
store, then the switch to *On this device* showing them, reminders included;
4. `OneShotImport` itself, which on a real device is only reachable by
side-loading a dev build of this branch first.
- ⬜ Release notes for 1.0.0. Nothing to say about the dropped authority or
permissions (see Phase 5); what needs saying is the own store, the copy path out
of an external provider, iCalendar export and list management.
### ✅ A way onto the new store for the people already using Agendula
The migration this branch was planned around turned out to serve nobody: the
vendored provider it reads from never shipped, so no install has the file
`OneShotImport` looks for. Everyone on 0.4.0 keeps their tasks in OpenTasks or
tasks.org, which `autoMode()` correctly keeps them on — and until now the only
route to the new store was to retype everything.
- ✅ `ExternalImport` (`data/tasks/transfer/`) copies the external provider's
lists, tasks and alarms into Room in one transaction with verified counts, the
same discipline as the legacy import. Task `uid`s survive, so these rows can be
attached to a CalDAV collection once sync lands instead of duplicating.
- ✅ Offered as **Settings → Storage → Copy tasks from …**, behind a confirm that
names the real number of tasks, and only while a provider is installed,
permitted, and no copy has succeeded yet — a second run would leave two of
everything, since what it writes is indistinguishable from hand-typed tasks
afterwards.
- ✅ One-directional by design. 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:
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.
- ✅ A failed `OneShotImport` is no longer silent. It used to return an
`ImportResult.Failed` that every caller dropped, leaving an upgrading user an
empty app and no explanation; it is now recorded, logged, and surfaced in
Settings → Storage as a retry — which is also what finally makes
`reimportFromArchive()` reachable from the app.
### ✅ Managing lists in the app
Owning the store made this mandatory: there is no longer a provider app to
+3 -1
View File
@@ -8,7 +8,9 @@
> [`OWN-STORE.md`](OWN-STORE.md) for what replaces it. The permissions,
> distribution, storage-mode and dead-end sections below remain accurate; treat
> the "our own provider" sections as the historical record of a decision that was
> made, shipped, and then costed properly.
> made, built, and then costed properly. It never reached a release: the
> `:provider` module was added and deleted inside this same unreleased cycle, so
> no published version of Agendula ever carried an authority of its own.
> Decided direction, captured 2026-08-01. Supersedes the earlier "Posture B =
> bundle OpenTasks" working notes, which are withdrawn (see