ddcffa61b37568074b14fb843c49025ba87a8bb7
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b4a7fcb46e |
sync: what the on-device round found
Everything here came from running the account flow against a real Nextcloud rather than from reading the code. Discovery - A typed bare origin now gets the RFC 6764 well-known probe. It was returned as the only candidate, so `https://cloud.example.com` — what people actually type — was PROPFIND'd against the web UI, answered 405, and a working Nextcloud reported as "not a CalDAV server". - A same-host HTTPS→HTTP redirect is put back on TLS instead of refused (`dav` change 7). A Nextcloud behind a TLS-terminating proxy without `overwriteprotocol` builds every redirect with http://, including the /.well-known/caldav hop discovery depends on. Cross-host still throws. - Outcomes carry a `Cause` the UI translates, not the server's own words. "HTTP 405 Method Not Allowed" told someone entering an address nothing, in a language they may not read, from outside strings.xml. - An IPv6 origin keeps its brackets: `HttpUrl.host` returns "fd00::1", so the rebuilt origin did not parse and a homelab address came back as "not an address". Login Flow v2 - The poll response's scheme is coerced, never refused. Nextcloud returns the app password exactly once, so throwing there burned a live credential and left it dangling in the user's device list. The host mismatch already worked this way; the scheme now matches it. Accounts - The accounts screen observes Room and the sign-in state instead of taking a snapshot, so a sync landing — or a 401 stopping an account — reaches a screen that is already open. - A per-account detail screen, and provider identity (`CalDavProvider`) shared with the quirk table so one list drives both the icon and the warning. - The password field masks: floret-kit's `InlineTextField` gained a visual transformation, since `KeyboardType.Password` only tells the IME to drop suggestions. |
||
|
|
28b2423ad9 |
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. |
||
|
|
b25f8b231c |
sync(chunk 4): incremental sync and scheduling
RFC 6578 as an optimisation on top of chunk 3's full path, and sync that runs by itself. - :caldav gains sync-collection, with invalidation matched on DAV:valid-sync-token in the body on any 4xx rather than on a status code — 400, 403, 409 and 412 are all used in the wild, and matching the status is why Thunderbird never recovers from sabre's 403. No DAV:limit (Nextcloud regressed it to an HTML error page); 507 on our own href is truncation. - The engine persists the token per page, after the bodies. An iteration cap and a no-progress guard, since the RFC never requires the token to advance. The full path runs anyway every 24h: a token the server accepts over a pruned change log returns 207, zero changes and no error, and the protocol gives no other way to notice. - The three membership traps: an unknown removed href is a no-op, a delete-then-recreate is re-identified from the UID in the body, and a mass removal is refused in favour of a real listing, because ACL churn looks exactly like one. - Scheduling is a PeriodicWorkRequest with a network constraint, expedited only for the in-app button. getForegroundInfo is implemented unconditionally (setExpedited falls back to a foreground service below API 31 and the default throws) but declares no service type, which would have pulled back the Android 15 dataSync budget and a Play video-demo requirement. initialIncomplete turned out not to be needed: adopting a token only after a full reconciliation completes removes the hazard it guarded, so there is nothing to persist atomically with anything. /code-review high raised 8 findings, all fixed. The two that mattered: the cadence clock sat in the backed-up DataStore, so a restore would have made the engine trust a stale token for a day — both sync-state stores now have their own excluded file; and the incremental download path lacked the write-phase guard, overwriting local edits that had never reached the server. Reasoning in docs/SYNC-PLAN.md. Chunk 2's on-device review is still outstanding; none of this has run on a device or against a real server. |
||
|
|
b1189a4884 |
sync(chunk 3): the sync engine
Full bidirectional sync, correct but not yet clever: calendar-query with no time-range, calendar-multiget in batches matched against what was asked for, conditional writes, and per-resource quarantine so one bad task cannot stop a collection. - :caldav gains CalendarCollection (list/fetch/create/update/delete + the three-way 412 triage), ETag with its weak flag, vdirsyncer-style resource names, and the RemoteCalendar seam. - CalDavHttp now sends Accept-Encoding: identity and Prefer: handling=strict, the two headers that keep ETags strong and our bytes unrepaired. - :app gains CollectionSyncer (the reconciliation), SyncEngine, SyncStore, QuarantineStore, SyncReport and a working SyncWorker, plus an in-app sync trigger since ContentResolver.requestSync is gated at our targetSdk. - VTimeZones closes a chunk-1 gap: the mapper emitted TZID with no VTIMEZONE to resolve it, which handling=strict turns from a repair into a rejection. /code-review high raised 11 findings, all fixed. The three that mattered: an empty listing swept the whole list (a VTODO comp-filter some servers mishandle is not proof of deletion, so an empty listing now never sweeps); quarantine never covered creates, so a permanently rejected new task was re-PUT forever; and a RELATED-TO deleted on the server was re-uploaded on the next edit. Reasoning recorded in docs/SYNC-PLAN.md. Chunk 2's on-device review is still outstanding; nothing here has been run on a device or against a real server. |
||
|
|
48fc7261f0 |
sync(chunk 2d): the account-add flow
One flow, one back-stack entry, as a stepper rather than four destinations —
the steps are not independently reachable, and "back" from the browser step
abandons a server-side flow rather than popping a screen. It hangs off Settings
with the same sliding-section pattern Storage -> Export uses.
address -> discovery -> (Nextcloud browser approval | username + password)
-> pick lists -> add account
Provider warnings come before the attempt, not after: type a Fastmail or iCloud
address and the app password rule is stated while you type, which is the single
most common support ticket a CalDAV client inherits. Google is refused with the
reason. A login-flow host mismatch is shown, not refused — reverse proxies are
ordinary on self-hosted installs.
PreemptiveBasicInterceptor is deleted. The vendored BasicDigestAuthHandler
already sends Basic preemptively over HTTPS, also does Digest (Baikal defaults
to it, OkHttp has none), caches the working scheme, and scopes by registrable
domain — which is what iCloud's cross-host home set needs. Two implementations
of one job is the defect chunk 1 removed from ICalendarWriter.
⚠️ That handler compares its `domain` against the *registrable* domain, so
passing the full host meant credentials were withheld from every request to
every subdomain — i.e. every self-hosted Nextcloud, silently 401ing forever.
Pinned by CalDavHttpTest.
CalDavGateway and AccountCreator put the network and the database behind
interfaces so the sign-in state machine is testable without a server, a
database, a Keystore or an AccountManager. It had no tests, and the review
found eight issues in it.
Account creation is transactional and rolls its lists back explicitly:
account_id is ON DELETE SET NULL, so deleting the row alone leaves orphan
lists behind and every retry adds another set.
|
||
|
|
ec50e0998c |
sync(chunk 2c): Keystore credentials, AccountManager, stub sync adapter
The platform half of chunk 2; the account-add UI is 2d, since it is a design task and the piece that needs an on-device review. A stub ContentProvider turned out to be required and was not in the plan: a sync adapter registers against a content authority, and we publish no provider since :provider was deleted. Without one there is nothing for contentAuthority to name, nothing for requestSync to address, and hasAuthorityAccess() makes every ContentResolver sync call a silent no-op at targetSdk 34+. - CredentialStore: Keystore AES/GCM, blob in its own DataStore file. security-crypto is formally deprecated and terminal. Decryption failure means re-authenticate, never a crash — including ProviderException, which is a RuntimeException and escapes the obvious catches. - CalDavAccounts + SyncAuthenticator: no password reaches AccountManager, which stores them as plain TEXT. The authenticator never returns null — a null is the protocol for "answering asynchronously", and nothing here does, so Settings would wait forever. addAccount refuses with a readable message until 2d ships the screen, rather than opening the home screen and hanging. - SyncAdapterService: enqueue and wait on the unique work *name*, not the request id — enqueueUniqueWork is async so the id is unknown when the wait starts, and under KEEP it may never exist at all. Being deduplicated is not a failure. - Account type and authority are per build variant, so debug and release do not fight over ownership. SyncContractTest guards the Kotlin/resValue pair. - The credential blob is the only thing excluded from backup: Keystore keys are non-exportable, so a restored ciphertext can never be decrypted. Known trade-off recorded in network_security_config.xml and SYNC-PLAN.md: the user CA store is trusted for all traffic, which chunk 5's cert4android should replace rather than sit beside. The instrumented tests here compile but have not been run — device work waits for an explicit go-ahead. |
||
|
|
da42423fe2 |
sync(chunk 2b): CalDAV discovery and auth protocol
New :caldav module — MIT, plain JVM, api-depends on :dav. Separate from the vendored MPL tree so the licences stay unmixed, and so "no Android types" is a compile-time guarantee rather than a discipline. Chunk 2 split again: the Android account layer (Keystore, AccountManager, Custom Tabs, account-add UI) is 2c, with different verification and an on-device review. - ServiceDiscovery: the RFC 6764 ladder. SRV priority/weight, TXT path=, non-443 ports, "." targets, well-known then root. - CollectionClassifier: the two filters that are inversions of the obvious rule. An absent or empty supported-calendar-component-set means "supports everything", and classification is a positive test for CALDAV:calendar on an unordered set — excluding schedule-outbox would drop SOGo's main calendar. - CalDavDiscovery: OPTIONS gate, principal, every home set, Depth-1 by name. A failing home set does not fail the account, and every home set failing is reported as an error rather than as an account with no lists. - NextcloudLoginFlow: POST not GET, a User-Agent the user can recognise when revoking, 404-means-pending only, both URLs origin-checked, host mismatch carried rather than refused (reverse proxies are ordinary). - PreemptiveBasicInterceptor, ServerQuirks. dnsjava 3.6.3 (BSD-3) added: Android's DnsResolver is callback-only and cannot do the TXT path lookup, and JNDI's DNS provider does not exist on Android. Behind an interface, so every trap is tested with a fake and no network. :dav gains change 6 — <D:unauthenticated/> is parsed rather than inferred from a null href, which also fires on a merely non-conformant empty element. 52 tests here, 78 in :dav. SYNC.md's live-probed trap table is executable now. |
||
|
|
5a81d3c2f5 |
sync(chunk 2a): vendor dav4jvm 2.2.1 as :dav
JitPack-only publishing conflicts with FAIL_ON_PROJECT_REPOS, artifacts are unsigned and not immutable, and upstream shipped two breaking majors 19 days apart. 2.2.1 is the last OkHttp release: 3.0.0 moved to Ktor and 4.x needs Java 21, and SYNC.md's whole auth section is written in OkHttp terms, so 4.x would have invalidated it. A plain JVM module, not an Android library — the tree has no Android imports and keeping it that way keeps the floret-kit extraction a file move. Four changes from upstream, all in dav/PROVENANCE.md: - commons-lang3 dropped (one import, one call). The replacement requires the whole string to be consumed, as DateUtils did: pattern 1 ends in a literal 'GMT', so SimpleDateFormat would match "...GMT+02:00" as a prefix and throw the offset away. - HTTP dates were parsed and formatted in the device's local zone — the GMT in the format string is a quoted literal and timeZone was never set, so every getlastmodified was out by the local UTC offset. Upstream tests dates nowhere. The formatter is also no longer shared and mutable. - dav4jvm#209: permanent redirects now reach the caller via DavResource.permanentLocation, cleared per request so it never describes an earlier one. - xpp3 is compileOnly; Android supplies org.xmlpull.v1. SYNC.md's other claimed defect, "does not follow 303", is not true of 2.2.1 — pinned by a test rather than removed from the notes. CI names :dav:test explicitly; testDebugUnitTest is Android-only and would have run none of the vendored suite. lintDebug added to the per-chunk checks after it caught a literal byte-order mark left by chunk 1. |
||
|
|
a30114efbb |
sync(chunk 1): VTODO mapper with an unknown-property round-trip
A hand-rolled content-line model instead of ical4j: a raw (name, params, value) tree is what the round-trip requirement wants, and a typed model normalises away exactly what has to survive. lib-recur already does RRULE and java.time is native at minSdk 29, so the 2.2 MB of zone data and the registry shims buy nothing. Deviates from SYNC.md's library table — see SYNC-PLAN.md decision 4. - domain/ical: parser, serialiser, value codecs. No Android, no data types, so the floret-kit extraction stays a file move. - data/tasks/ical/VTodoMapper: VTODO <-> TaskEntity. Claims a property only when it can reproduce it exactly; everything else round-trips verbatim through TaskEntity.unknown_properties, which already existed at v1 — no migration needed, and BEGIN/END lines carry the nesting the plan thought needed a second table. - 19 fixtures as the specification, with the canonical comparison harness from SYNC.md: no property lost, modulo the enumerated allowlist. - ICalendarWriter now delegates folding and escaping rather than carrying its own copy. VALARM ownership settled: neither side writes the other's alarms. VALARMs round-trip in the residue, local reminders stay in task_alarms. The setAlarm collision was the provider's; the two stores are now disjoint. |
||
|
|
fd6c302c17 |
docs(sync): split the sync work into five chunks
SYNC.md decides; it doesn't sequence. SYNC-PLAN.md turns its ~40 scattered findings into five chunks — mapper, auth/discovery, engine core, incremental sync + scheduling, hardening — each with a done-when and the traps it must not get wrong. Closes SYNC.md's three open questions: vendor dav4jvm/cert4android at our Java 17 target, server-wins-and-report on 412, External mode stays. |