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.
Phase 6 of docs/OWN-STORE.md. MigrationTestHelper is wired against the
committed v1 schema, so the first real migration only has to add its own
case; the class KDoc says where it goes. app/schemas/ is added to the
androidTest assets — the schema location comes from the KSP arg, not the
Room Gradle plugin, so nothing wired the test assets automatically.
The restore tests state the WAL premise directly rather than around it: a
backup of the .db alone must lose whatever is still in the -wal, carrying
the sidecars must keep it, and checkpointing first must make the .db
alone sufficient. If the premise is wrong the first test fails instead of
passing vacuously.
Performance: 5,000 tasks and 20 FREQ=DAILY series — daily on purpose, so
the per-series occurrence cap is the case being measured — through one
full smart-list read. The ceiling is loose and the numbers are printed,
because nobody has run this on hardware yet.
Also fixes a lint error I introduced in the backup rules two commits ago.
Naming any <include> makes everything else excluded by default, so the
<exclude> for tasks.db.imported sat under no included path and
FullBackupContent rejected it — lintDebug has been failing at HEAD since,
and CI runs it.
The same defect had a second, quieter half: those explicit includes had
silently stopped DataStore being backed up at all, since it was only ever
covered by the old file's "everything by default". Settings are listed
back in explicitly.
Phase 4 of docs/OWN-STORE.md. OneShotImport reads databases/tasks.db
directly — read-only, no provider, no ContentResolver — and writes it
into Room in one verified transaction. dmfs row ids are remapped in two
passes, because a parent can carry a higher _id than its child.
The archive happens before the import, not after, and the import always
replaces. That is what actually closes the crash window the plan's "flag
*and* rename" is meant to cover: renaming last leaves the flag unset with
tasks.db still in place, so the next launch imports a second copy. In
this order every kill point re-enters correctly.
Recurrence overrides are carried across as master_id/recurrence_id rather
than ignored. dmfs stores them as ordinary rows sharing their master's
_uid, so importing one as a second master would collide on the unique
index and abort the whole import.
autoMode now answers OWN, and a stored LOCAL reads as OWN — after the
import the dmfs file has been renamed away, so someone who chose local
storage explicitly must land on the store their data is now in.
StartupGate holds the first store read until the mode has landed and the
import has run; showing an upgrading user an empty app is the worst thing
this migration could do. The backup rules take the database with its WAL
sidecars and exclude the archive, and the app checkpoints on ON_STOP.
Sibling to Calendula. Pure front-end posture; TaskContract data layer,
task screens and reminder engine to follow (see docs/PLAN.md).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>