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>
8.0 KiB
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
(
OpenMultipleDocumentsinui/compose/ComposeScreen.kt:88) and aFileProviderfor 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.ktqueriesContactsContract.CommonDataKinds.Emailfor 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.searchreturns 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 viaVISIBILITY_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.ktNotificationPermissionEffect). If denied,MailNotifier.notifyNewMailno-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, manifestforegroundServiceType="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_SERVICEplus the typed permission are both declared and the service callsServiceCompat.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.