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.
This commit is contained in:
2026-09-04 18:00:07 +02:00
parent da42423fe2
commit ec50e0998c
20 changed files with 1026 additions and 4 deletions
+11
View File
@@ -19,4 +19,15 @@
<include domain="database" path="agendula-tasks.db-wal" />
<include domain="database" path="agendula-tasks.db-shm" />
<include domain="file" path="datastore/" />
<!--
The one thing that must not travel. Keystore keys are non-exportable, so
a restored ciphertext can never be decrypted again — it would surface as
an account that silently stops syncing with no way to tell why. Excluding
it means the user signs in again on a new device, which is the honest
outcome. Note this is the *only* exclusion that is right here:
docs/SYNC.md is explicit that dropping the database or all of DataStore
from backup would trade a latent bug for a live one, since Auto Backup is
Local mode's only automatic safety net.
-->
<exclude domain="file" path="datastore/agendula_credentials.preferences_pb" />
</full-backup-content>