feat(backup): opt-in Android Backup for settings only #41

Merged
JMR-dev merged 2 commits from feat-backup-optin into main 2026-07-01 14:59:40 +00:00
JMR-dev commented 2026-07-01 04:56:50 +00:00 (Migrated from github.com)

Closes #21.

What

Adds an opt-in, off-by-default toggle to include app data in system Android Backup (Auto Backup / cloud + device-to-device transfer). When enabled, only re-creatable user preferences are backed up; secrets and the re-syncable mail cache are never included.

How

allowBackup is a manifest flag, not a runtime toggle, so a BackupAgent enforces the runtime opt-in:

  • AndroidManifest.xml — allowBackup="true", fullBackupOnly="true", backupAgent=".backup.LibreMailBackupAgent", plus fullBackupContent for API 29–30 alongside the existing dataExtractionRules (API 31+).
  • LibreMailBackupAgent (BackupAgentHelper) — onFullBackup calls super only when the user has opted in; otherwise it ships nothing. The flag is read straight from the shared settingsDataStore singleton (fail-safe to "off"), so it doesn't depend on Hilt or Application.onCreate in the framework's restricted backup mode. Sharing the one DataStore delegate avoids a "multiple DataStores active for the same file" crash.
  • data_extraction_rules.xml / backup_rules.xml — strict <include> allowlists containing only datastore/libremail_settings.preferences_pb. Everything else is excluded by omission (Android lint's FullBackupContent rule forbids <exclude> paths outside an <include>).
  • Settings — new includeInBackup preference + a "Backup" section with F-Droid-honest copy (off by default; states it uses Google infrastructure and that mail/accounts/passwords/keys are never backed up). The setter nudges BackupManager.dataChanged() so the change takes effect on the next backup pass.
  • BackupPolicy — single source of truth for the eligible/excluded paths.

What is NOT backed up (excluded by not being on the allowlist)

  • datastore/libremail_dbkey.preferences_pb — the Keystore-sealed SQLCipher passphrase (device-bound; ciphertext is useless off-device).
  • libremail.db (+ -wal/-shm/-journal) — encrypted IMAP/OAuth credentials and the mail cache (re-downloads on next sync).

Testing

  • Fast CI gate green: assembleDebug, testDebugUnitTest, lintDebug, ktlintCheck, detekt (JDK 21).
  • New JVM unit tests:
    • BackupPolicyTest — opt-in default is off; shouldBackUp only true when enabled; secret paths are on the excluded lists.
    • DataExtractionRulesTest — parses the shipped data_extraction_rules.xml (cloud-backup + device-transfer) and backup_rules.xml and asserts the include set is exactly the settings DataStore and that the DB key / credentials DB are never eligible — so the rules can't silently drift.
  • On-device backup/restore was verified by inspection only (no emulator in this environment): the manifest + both XML allowlists include only the settings DataStore and exclude the DB key and credentials DB. Left to CI/manual.

🤖 Generated with Claude Code

Closes #21. ## What Adds an **opt-in, off-by-default** toggle to include app data in system Android Backup (Auto Backup / cloud + device-to-device transfer). When enabled, **only re-creatable user preferences** are backed up; secrets and the re-syncable mail cache are never included. ## How `allowBackup` is a manifest flag, not a runtime toggle, so a `BackupAgent` enforces the runtime opt-in: - **`AndroidManifest.xml`** — `allowBackup="true"`, `fullBackupOnly="true"`, `backupAgent=".backup.LibreMailBackupAgent"`, plus `fullBackupContent` for API 29–30 alongside the existing `dataExtractionRules` (API 31+). - **`LibreMailBackupAgent`** (`BackupAgentHelper`) — `onFullBackup` calls `super` **only when the user has opted in**; otherwise it ships nothing. The flag is read straight from the shared `settingsDataStore` singleton (fail-safe to "off"), so it doesn't depend on Hilt or `Application.onCreate` in the framework's restricted backup mode. Sharing the one DataStore delegate avoids a "multiple DataStores active for the same file" crash. - **`data_extraction_rules.xml` / `backup_rules.xml`** — strict `<include>` allowlists containing only `datastore/libremail_settings.preferences_pb`. Everything else is excluded by omission (Android lint's `FullBackupContent` rule forbids `<exclude>` paths outside an `<include>`). - **Settings** — new `includeInBackup` preference + a "Backup" section with F-Droid-honest copy (off by default; states it uses Google infrastructure and that mail/accounts/passwords/keys are never backed up). The setter nudges `BackupManager.dataChanged()` so the change takes effect on the next backup pass. - **`BackupPolicy`** — single source of truth for the eligible/excluded paths. ## What is NOT backed up (excluded by not being on the allowlist) - `datastore/libremail_dbkey.preferences_pb` — the Keystore-sealed SQLCipher passphrase (device-bound; ciphertext is useless off-device). - `libremail.db` (+ `-wal`/`-shm`/`-journal`) — encrypted IMAP/OAuth credentials and the mail cache (re-downloads on next sync). ## Testing - Fast CI gate green: `assembleDebug`, `testDebugUnitTest`, `lintDebug`, `ktlintCheck`, `detekt` (JDK 21). - New JVM unit tests: - `BackupPolicyTest` — opt-in default is off; `shouldBackUp` only true when enabled; secret paths are on the excluded lists. - `DataExtractionRulesTest` — parses the shipped `data_extraction_rules.xml` (cloud-backup + device-transfer) and `backup_rules.xml` and asserts the include set is **exactly** the settings DataStore and that the DB key / credentials DB are never eligible — so the rules can't silently drift. - On-device backup/restore was verified by inspection only (no emulator in this environment): the manifest + both XML allowlists include only the settings DataStore and exclude the DB key and credentials DB. Left to CI/manual. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.