Repo-actionable deliverables for the Google Play compliance work (issue #17), every claim verified against the code and the built release artifacts: - PRIVACY.md: user-facing privacy policy (device-local mail cache, optional SQLCipher encryption, traffic only to the user's own mail provider, on-device-only contacts autocomplete, strictly local opt-in debug reports, no ads/analytics/tracking SDKs). - docs/play-data-safety.md: Play Data safety questionnaire mapping -- answer 'no data collected/shared' with per-category code evidence, the policy exemptions relied on, a dependency audit, and a conservative fallback. - docs/play-permissions.md: merged-manifest permission audit (incl. the WorkManager-injected WAKE_LOCK / RECEIVE_BOOT_COMPLETED) with paste-ready Console justifications for READ_CONTACTS, POST_NOTIFICATIONS, and the FOREGROUND_SERVICE_DATA_SYNC declaration + demo-video script. - docs/play-compliance.md: verified targetSdk 37 (requirement: 35+), 16 KB page-size compliance (all packaged .so PT_LOAD p_align=0x4000, incl. sqlcipher-android 4.16.0), bundleRelease AAB check, the Gmail-app-password / no-CASA OAuth note, the console-steps checklist with drafted content-rating and listing answers, and repo findings (push-mail default vs docs, README minSdk/app-lock drift, debug-key release fallback). - README.md: link PRIVACY.md and note the no-Google-OAuth/no-CASA status (fuller README pass stays issue #20). Part of #17. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
113 lines
8.0 KiB
Markdown
113 lines
8.0 KiB
Markdown
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
|
|
# Permissions justification — merged manifest audit (issue #17)
|
|
|
|
Every permission in the **merged release manifest** (source of truth:
|
|
`app/build/intermediates/merged_manifests/release/processReleaseManifest/AndroidManifest.xml`
|
|
after `./gradlew :app:bundleRelease`; attribution from
|
|
`app/build/outputs/logs/manifest-merger-release-report.txt`), why it exists, where it is used,
|
|
and the text to paste into Play Console where a declaration is required.
|
|
|
|
## Complete merged-manifest permission list
|
|
|
|
| Permission | Declared by | Runtime prompt? | Purpose |
|
|
|---|---|---|---|
|
|
| `INTERNET` | app manifest | No | IMAP/SMTP/OAuth/Graph connections to the user's mail provider |
|
|
| `ACCESS_NETWORK_STATE` | app manifest | No | Connectivity checks so sync/WorkManager runs only when online |
|
|
| `READ_CONTACTS` | app manifest | **Yes** | On-device recipient autocomplete in the compose screen |
|
|
| `POST_NOTIFICATIONS` | app manifest | **Yes** (API 33+) | New-mail notifications + mandatory foreground-service status notification |
|
|
| `FOREGROUND_SERVICE` | app manifest | No | Prerequisite for running any foreground service (API 28+) |
|
|
| `FOREGROUND_SERVICE_DATA_SYNC` | app manifest | No | Type-specific permission for the IMAP IDLE push service (API 34+) |
|
|
| `WAKE_LOCK` | `androidx.work:work-runtime:2.11.2` | No | WorkManager keeps the CPU awake while a scheduled job (mail sync, outbox send) runs |
|
|
| `RECEIVE_BOOT_COMPLETED` | `androidx.work:work-runtime:2.11.2` | No | WorkManager reschedules pending jobs (periodic sync, queued outbox mail) after reboot |
|
|
| `org.libremail.app.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION` | `androidx.core:core:1.17.0` | No | Auto-generated app-signature permission guarding non-exported runtime receivers; not user-facing |
|
|
|
|
Nothing else. Notably **absent** (worth stating in any review exchange):
|
|
|
|
- **No `AD_ID`** — no ads or analytics SDKs at all.
|
|
- **No storage/media permissions** — attachments use the Storage Access Framework
|
|
(`OpenMultipleDocuments` in `ui/compose/ComposeScreen.kt:88`) and a `FileProvider` for viewing
|
|
(`AndroidManifest.xml:95`).
|
|
- **No `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS`** — deliberately avoided because Play restricts
|
|
it; the app deep-links to the system app-details screen instead
|
|
(`push/BatteryOptimizationManager.kt`, comment cites this issue).
|
|
- No location, camera, microphone, SMS, call-log, accessibility, or `QUERY_ALL_PACKAGES`.
|
|
|
|
## `READ_CONTACTS` (Play "sensitive" permission — scrutinized, no declaration form)
|
|
|
|
- **Feature:** recipient autocomplete while composing. `contacts/ContactsRepository.kt` queries
|
|
`ContactsContract.CommonDataKinds.Email` for at most 8 name/email matches of the typed text.
|
|
- **Data handling:** query and results are entirely **on-device** (results live in memory for
|
|
the suggestion dropdown). Nothing from the contacts provider is stored, logged, or
|
|
transmitted; an address reaches the network only if the user puts it on an email they send.
|
|
- **Request flow:** first composition of the compose screen (`ui/compose/ComposeScreen.kt:101`);
|
|
denial is handled gracefully — `ContactsRepository.search` returns empty and composing works
|
|
normally (manual address entry).
|
|
- **Play-Console justification text (if asked in review):**
|
|
> LibreMail is an email client. READ_CONTACTS powers recipient autocomplete on the compose
|
|
> screen only: the app queries the on-device contacts provider for names/email addresses
|
|
> matching what the user typed and shows up to 8 suggestions. Contact data is processed
|
|
> entirely on the device — it is never uploaded, stored outside the suggestion list, or shared.
|
|
> The permission is requested in context (first open of the compose screen) and the feature
|
|
> degrades gracefully if denied.
|
|
|
|
## `POST_NOTIFICATIONS`
|
|
|
|
- **Features:** (1) per-account new-mail notifications, generated on-device from synced mail —
|
|
`notifications/MailNotifier.kt` (no push/cloud-messaging service; lock-screen content
|
|
redacted via `VISIBILITY_PRIVATE`); (2) the persistent low-importance status notification
|
|
Android requires while the IMAP IDLE foreground service runs (`push/IdleService.kt:120`).
|
|
- **Request flow:** once at first launch, API 33+ only (`MainActivity.kt`
|
|
`NotificationPermissionEffect`). If denied, `MailNotifier.notifyNewMail` no-ops (permission
|
|
re-checked before every post, `MailNotifier.kt:134`); mail sync itself is unaffected.
|
|
- **Play-Console justification text (if asked):**
|
|
> Notifies the user of newly received email (per-account channels, generated on the device
|
|
> from the user's own mailbox — no push service) and shows the persistent status notification
|
|
> Android requires for the optional foreground IMAP IDLE connection. Requested once at first
|
|
> launch; all app functions except notifications work if declined.
|
|
|
|
## `FOREGROUND_SERVICE_DATA_SYNC` (requires the Play Console FGS declaration)
|
|
|
|
Play Console → App content → **Foreground service permissions** asks for the type's use case
|
|
and a demo video. Facts to declare, all verifiable in `push/IdleService.kt`:
|
|
|
|
- **What runs:** one foreground service (`.push.IdleService`, manifest
|
|
`foregroundServiceType="dataSync"`, `AndroidManifest.xml:89`) holding a long-lived IMAP IDLE
|
|
(RFC 2177) connection per configured account so the user's own mail server can push new mail
|
|
instantly. On server activity it triggers a normal sync into the local cache and a new-mail
|
|
notification.
|
|
- **Why a foreground service:** IMAP IDLE requires a continuously open TCP connection that
|
|
survives while the app is backgrounded; it cannot be modeled as deferrable work. WorkManager
|
|
**is** used for everything deferrable (periodic sync, outbox sending) — the service exists
|
|
only for the always-connected push case. There is no push-notification alternative (FCM)
|
|
because plain IMAP servers cannot address one, and the app deliberately uses no Google cloud
|
|
services.
|
|
- **User control / lifecycle:** starts only when at least one account exists **and** the "push
|
|
mail" setting is enabled (`LibreMailApplication.kt:70`); the setting is a visible toggle in
|
|
Settings (`ui/settings/SettingsScreen.kt:147`); the service stops reactively when the toggle
|
|
turns off or the last account is removed, and shows a persistent low-importance status
|
|
notification while running (`IdleService.startAsForeground`).
|
|
- **Declaration text to paste:**
|
|
> LibreMail is an email client. The dataSync foreground service maintains a long-lived IMAP
|
|
> IDLE (RFC 2177) connection to the user's own mail server so new mail arrives instantly.
|
|
> IMAP has no out-of-band push channel (such as FCM), so real-time delivery requires keeping
|
|
> this user-visible connection open; all deferrable transfers (periodic sync, sending queued
|
|
> mail) already use WorkManager instead. The service runs only while the user has an account
|
|
> configured and the "push mail" setting enabled, displays a persistent status notification,
|
|
> and stops immediately when the user disables the setting or removes their last account.
|
|
- **Demo video (human step):** screen-record: Settings → toggle "push mail" on → the status
|
|
notification appears → send the account a mail from elsewhere → the new-mail notification
|
|
arrives with the app backgrounded → toggle off → status notification disappears.
|
|
- **Note:** the app targets SDK 37, so the API-34 requirement to declare a type for every FGS
|
|
is in force; `FOREGROUND_SERVICE` plus the typed permission are both declared and the service
|
|
calls `ServiceCompat.startForeground(..., FOREGROUND_SERVICE_TYPE_DATA_SYNC)`
|
|
(`IdleService.kt:136`).
|
|
|
|
## Library-injected permissions (`WAKE_LOCK`, `RECEIVE_BOOT_COMPLETED`)
|
|
|
|
Injected by `androidx.work:work-runtime:2.11.2` for its own machinery: holding a partial wake
|
|
lock while an enqueued job executes, and re-registering scheduled jobs after a reboot. LibreMail
|
|
uses WorkManager for periodic mail sync (`data/sync/` workers), reliable outbox sending
|
|
(`SendWorker.kt`), and the opt-in debug-report upload (`ReportUploadWorker.kt`, dormant —
|
|
endpoint unconfigured). Neither permission needs a Play declaration; keep this attribution handy
|
|
for review questions.
|