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.
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.
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.
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.
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.