sync(chunk 5): attribution, revocation, compliance
The parts of chunk 5 that a build can verify. What is left needs a device or a live server, and is listed in docs/SYNC-PLAN.md rather than guessed at. - Attribution screen in Settings. dav4jvm is vendored, which makes MPL-2.0 §3.2(a) ours rather than a dependency's, so its row points at PROVENANCE.md next to upstream. Hand-maintained: generators read POM metadata, which routinely names a non-SPDX licence and a licence URL that 404s. - Revocation both ways. A 401 marks the account, stops it before the next request reaches the network, and takes it off the schedule from outside the worker — Nextcloud throttles then 429s per source IP, so a timer on a dead app password degrades the user's other clients. On removal, a bounded best-effort DELETE of the app password, or uninstalling never revokes it. - Play compliance: docs/PRIVACY.md linked in the app, declaring Collected and not Shared; an option to delete the account's tasks from the device too; REQUEST_IGNORE_BATTERY_OPTIMIZATIONS confirmed absent. - The server trap matrix as far as a protocol mock reaches, with four tests left @Ignore'd and their reasons written out. - docs/SYNC-PLAN.md records what moves to floret-kit, so that branch is a file move rather than a rediscovery. /code-review high raised 9 findings, all fixed. Three were serious: app-password revocation was aimed at the principal URL and revoked nothing; opening the app put accounts a 401 had stopped back on the timer, because KEEP does not keep cancelled work; and the incremental path advanced the sync token past bodies a failed multiget never applied. Also: four scalars were emitted twice whenever their residue copy survived, which the round-trip corpus could not see. Not done, and needing you: the live server matrix, releaseTest on device, the restore-onto-a-fresh-device check, cert4android, and MKCALENDAR feature detection. Chunk 2's on-device review is still outstanding.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
# Agendula — privacy policy
|
||||
|
||||
_Last updated: 2026-09-07._
|
||||
|
||||
Agendula is a task app for Android, published by Jean-Luc Makiola. This policy
|
||||
describes what happens to your data. It is short because very little happens to
|
||||
it.
|
||||
|
||||
## The short version
|
||||
|
||||
Agendula has no servers. There is no Agendula account, no analytics, no
|
||||
advertising, no tracking and no third-party SDK that reports anything anywhere.
|
||||
Your tasks live on your device, and — only if you set that up yourself — on a
|
||||
CalDAV server **you** choose.
|
||||
|
||||
## What is stored on your device
|
||||
|
||||
- Your task lists, tasks, reminders and app settings.
|
||||
- If you add a CalDAV account: the server address, your username, and your
|
||||
password or app password. The password is encrypted with a key held in the
|
||||
Android Keystore, which cannot be exported from the device.
|
||||
|
||||
## What leaves your device
|
||||
|
||||
**Only if you add a CalDAV account**, and only to the server you entered:
|
||||
|
||||
- Your tasks in those lists, as iCalendar data, and the credentials needed to
|
||||
authenticate.
|
||||
- Requests are made over HTTPS. Cleartext HTTP is refused unless you explicitly
|
||||
opt in for a specific account.
|
||||
|
||||
Nothing is sent anywhere else. In particular, nothing is sent to the developer.
|
||||
|
||||
Under Google Play's Data Safety definitions this counts as **collected** — Play
|
||||
defines collection as transmitting data off the device, regardless of who
|
||||
receives it — and **not shared**, because the only recipient is the server you
|
||||
nominated. Data is encrypted in transit.
|
||||
|
||||
Your CalDAV provider has its own privacy policy, and your data on their server is
|
||||
governed by it. Agendula has no relationship with them.
|
||||
|
||||
## Crash reports
|
||||
|
||||
If the app crashes, it can show you the report and ask whether to send it. It is
|
||||
never sent without you choosing to send it, and you can read the whole report
|
||||
first.
|
||||
|
||||
## Backups
|
||||
|
||||
If Android Auto Backup is enabled on your device, your tasks and settings may be
|
||||
backed up to your own Google account. Two things are deliberately excluded: your
|
||||
stored CalDAV password, and Agendula's per-device sync bookkeeping.
|
||||
|
||||
## Deleting your data
|
||||
|
||||
- **Remove a CalDAV account** from Settings → Accounts. This deletes the stored
|
||||
credential and, where the server supports it, revokes the app password. Task
|
||||
lists become device-only lists rather than being destroyed.
|
||||
- **Remove an account and delete its local data** removes the lists and tasks as
|
||||
well.
|
||||
- **Uninstalling the app** removes everything Agendula stored on the device.
|
||||
|
||||
Deleting data from your CalDAV server is done on that server.
|
||||
|
||||
## Children
|
||||
|
||||
Agendula is not directed at children and collects nothing about anyone.
|
||||
|
||||
## Changes
|
||||
|
||||
Material changes will be noted here with a new date at the top. The history of
|
||||
this file is public in the repository.
|
||||
|
||||
## Contact
|
||||
|
||||
mail@jeanlucmakiola.de
|
||||
@@ -756,6 +756,125 @@ so the follow-up branch is a file move and not a rediscovery.
|
||||
**Done when:** the matrix is green where a server exists to run it against, the
|
||||
attribution screen ships, and a `releaseTest` build syncs on a real device.
|
||||
|
||||
### What shipped, and what is still owed
|
||||
|
||||
**Shipped in the chunk-5 commit** — everything verifiable without a device or a
|
||||
live server:
|
||||
|
||||
- **Attribution.** `Settings → Open source licenses`, hand-maintained. Not a
|
||||
generated list: POM metadata routinely names a non-SPDX licence and points at a
|
||||
URL that 404s, so generators produce confidently empty output. dav4jvm is
|
||||
vendored, which makes MPL-2.0 §3.2(a) *ours* rather than a dependency's — the
|
||||
row names `dav/PROVENANCE.md` alongside upstream, because §3.2(a) is about
|
||||
where a recipient of the binary can obtain the source of what we modified.
|
||||
- **Revocation, both directions.** A 401 escalates from the collection to the
|
||||
account, marks it, **cancels its periodic work**, and stops. Cancelling is the
|
||||
part that matters: Nextcloud throttles and then 429s per source IP, so a timer
|
||||
on a dead app password degrades every other Nextcloud client on that network.
|
||||
On removal, `DELETE /ocs/v2.php/core/apppassword` with `OCS-APIRequest: true`,
|
||||
best effort — without it, uninstalling never revokes anything and the password
|
||||
outlives the app in the user's device list.
|
||||
- **Play compliance.** A privacy policy in `docs/PRIVACY.md`, linked **in the
|
||||
app** (the half that is usually missed) and declaring *Collected, not Shared* —
|
||||
"not collected" is indefensible when Play defines collection as transmission
|
||||
off-device regardless of recipient. A "delete this account's tasks from this
|
||||
device too" option on removal. `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` was
|
||||
already absent and stays absent.
|
||||
- **The server matrix**, as far as a protocol mock can carry it: Nextcloud's
|
||||
`no-uid-conflict`, its trashbin 403 on a reused href, and share detection;
|
||||
SOGo's second-granularity opaque tokens and its triple resource type; Radicale
|
||||
advertising `sync-collection` without implementing it.
|
||||
|
||||
**Still owed, and blocked on things a loop cannot do:**
|
||||
|
||||
1. **The live matrix.** Four tests are `@Ignore`d with their reasons written out
|
||||
rather than deleted: Baïkal on Digest, a `CLASS:CONFIDENTIAL` task on a shared
|
||||
Nextcloud calendar, an ETag-unchanged body change on SOGo, and Nextcloud's
|
||||
MKCALENDAR rate limit. Each names why a mock cannot stand in for it.
|
||||
2. **`releaseTest` on device**, and the R8 outages `proguard-rules.pro` already
|
||||
documents.
|
||||
3. **The restore path**, which needs a restore onto a fresh device — the one
|
||||
verification `SYNC.md` calls for explicitly and the one that cannot be faked.
|
||||
4. **cert4android**, which replaces the current blanket user-CA trust and is a
|
||||
UI-bearing dependency.
|
||||
5. **Collection creation feature detection** (OPTIONS → MKCALENDAR / extended
|
||||
MKCOL) and disabling the "new task list" affordance where neither exists.
|
||||
Deferred because the app has no CalDAV-side list-creation UI yet, so there is
|
||||
nothing to disable — it becomes real the moment that UI does.
|
||||
|
||||
### Found by the review, and worth naming
|
||||
|
||||
**⚠️ Revocation was aimed at the principal URL and revoked nothing.** The OCS
|
||||
endpoint sits at the *server root*; the principal is
|
||||
`…/remote.php/dav/principals/users/alice/`, so appending to it produced a path
|
||||
that 404s on every server, every time — and the failure was swallowed as
|
||||
best-effort. The whole feature was a no-op. The root is now derived by cutting at
|
||||
`remote.php`, which is right for a subpath install too, with the origin as the
|
||||
fallback.
|
||||
|
||||
**⚠️ Opening the app resurrected accounts a 401 had just stopped.**
|
||||
`ExistingPeriodicWorkPolicy.KEEP` only keeps work that is *unfinished*, and
|
||||
CANCELLED counts as finished — so `rescheduleAll()` re-enqueued the very request
|
||||
the stop had removed, and put a dead app password back on a four-hour loop
|
||||
against a server that throttles by source IP. Exactly what the stop exists to
|
||||
prevent.
|
||||
|
||||
**⚠️ The cursor advanced past bodies that were never applied.** A network drop
|
||||
mid-multiget set `report.failure` and returned, and the incremental path stored
|
||||
the new token anyway — a permanent hole in the collection, invisible until the
|
||||
next full reconciliation a day later. `runFull` had this guard from the start;
|
||||
the incremental path did not.
|
||||
|
||||
**Stopping an account cancelled the worker doing the stopping.** `syncTrigger.cancel`
|
||||
kills both unique names, one of which is the WorkSpec currently executing —
|
||||
WorkManager interrupts the coroutine, so `NeedsSignIn` was never returned and the
|
||||
adapter saw CANCELLED rather than FAILED. The flag now does the work: `sync()`
|
||||
refuses before touching the network, and the schedule is cancelled from outside
|
||||
any worker.
|
||||
|
||||
**"Sign in again" was a dead end.** `create` is the only path that writes a
|
||||
credential and it rejected the duplicate name, so a user with a revoked app
|
||||
password could only remove the account — discarding the choice to keep its lists.
|
||||
It now re-authenticates in place, but *only* for an account already marked as
|
||||
needing sign-in, so a healthy credential can never be silently overwritten.
|
||||
|
||||
**Four scalars were emitted twice.** `SEQUENCE`, `PRIORITY`, `PERCENT-COMPLETE`
|
||||
and `CLASS` reach the residue verbatim when unparseable, and `write` authors them
|
||||
from their columns unconditionally — so a task read from `SEQUENCE:x` round-tripped
|
||||
carrying both `SEQUENCE:0` and `SEQUENCE:x`. Invisible to the corpus, which
|
||||
filters `SEQUENCE` out of comparison entirely. Now suppressed, with a test that
|
||||
counts occurrences rather than comparing canonically.
|
||||
|
||||
**A solidus-prefixed `TZID` was rejected as undefined** — the rule's own comment
|
||||
says §3.2.19 exempts it. Thunderbird writes
|
||||
`/mozilla.org/20050126_1/Europe/Berlin` on every zoned task, so the validator
|
||||
would have quarantined every Lightning-authored task permanently.
|
||||
|
||||
**Two smaller ones:** a resource the query lists but the multiget will not return
|
||||
was re-requested for ever with nothing counting it, and account removal blocked
|
||||
on a 30-second connect timeout before anything visible happened.
|
||||
|
||||
## What moves to floret-kit
|
||||
|
||||
Recorded here so the follow-up branch is a file move rather than a rediscovery.
|
||||
The rule that made it a move rather than a rewrite held: **no task-domain type
|
||||
and no Android UI type crossed into the DAV or CalDAV layers.**
|
||||
|
||||
| Moves | Why it generalises |
|
||||
|---|---|
|
||||
| `:dav` (vendored dav4jvm) → `core-dav` | Not app-specific in any way; Calendula needs the identical tree |
|
||||
| `:caldav` — discovery, `CalendarCollection`, `sync-collection`, quirks | Pure protocol, plain JVM, no Android types |
|
||||
| `NextcloudLoginFlow` | Nothing about it is task-shaped |
|
||||
| `CredentialStore`, `CalDavAccounts`, `SyncAuthenticator` | Keystore + `AccountManager` plumbing, identical for any CalDAV client |
|
||||
| `LicencesScreen` / `Attribution` | Every app in the family owes the same notices |
|
||||
|
||||
| Stays app-local | Why |
|
||||
|---|---|
|
||||
| `VTodoMapper`, `CalendarResource`, `ResourceValidator` | VTODO ↔ *our* Room schema; the residue contract is ours |
|
||||
| `CollectionSyncer`, `SyncReport`, conflict policy and its UI | Encodes decision 2, which is a product decision |
|
||||
| `SyncEngine`, `SyncWorker`, `SyncTrigger` | Bound to our entities and our scheduling choices |
|
||||
| Storage modes, External mode | Agendula-specific by construction |
|
||||
|
||||
---
|
||||
|
||||
## Definition of done, every chunk
|
||||
|
||||
Reference in New Issue
Block a user