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:
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user