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>
153 lines
10 KiB
Markdown
153 lines
10 KiB
Markdown
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
|
||
# Google Play technical compliance & console checklist (issue #17)
|
||
|
||
Verified 2026-07-01 against this repository (commit on `main` at time of writing). Companion
|
||
docs: [`PRIVACY.md`](../PRIVACY.md), [`play-data-safety.md`](play-data-safety.md),
|
||
[`play-permissions.md`](play-permissions.md).
|
||
|
||
## 1. Target API level — PASS
|
||
|
||
| Fact | Value | Source |
|
||
|---|---|---|
|
||
| `targetSdk` | **37** | `app/build.gradle.kts:52` |
|
||
| `compileSdk` | 37 | `app/build.gradle.kts:46` |
|
||
| `minSdk` | 29 (Android 10) | `app/build.gradle.kts:51` |
|
||
| Play requirement (new apps & updates, phones/tablets) | target API **35** (Android 15)+ since 2025-08-31 | [Play target-API policy](https://support.google.com/googleplay/android-developer/answer/11926878) |
|
||
|
||
Target 37 exceeds the requirement with two versions of headroom; no action needed. When Google
|
||
announces the 2026 deadline (expected: API 36 for the Aug 2026 window), 37 still passes.
|
||
|
||
## 2. 16 KB page-size support — PASS (verified empirically)
|
||
|
||
Play requires new apps and updates targeting Android 15+ to support 16 KB memory page sizes on
|
||
64-bit devices since 2025-11-01 ([Android developers blog](https://android-developers.googleblog.com/2025/05/prepare-play-apps-for-devices-with-16kb-page-size.html)).
|
||
Compliance = every `PT_LOAD` segment of every packaged 64-bit `.so` aligned to ≥ 0x4000 (16384).
|
||
|
||
The release AAB packages exactly three native libraries. All were extracted from
|
||
`app-release.aab` and their ELF program headers checked (same check as AOSP's
|
||
`check_elf_alignment.sh`); **every one reports `p_align = 0x4000` on every ABI**:
|
||
|
||
| Library | From dependency | arm64-v8a | x86_64 | armeabi-v7a / x86 (32-bit, not gated) |
|
||
|---|---|---|---|---|
|
||
| `libsqlcipher.so` | `net.zetetic:sqlcipher-android:4.16.0` | 0x4000 OK | 0x4000 OK | 0x4000 OK |
|
||
| `libandroidx.graphics.path.so` | Compose (BOM `2026.06.00`) | 0x4000 OK | 0x4000 OK | 0x4000 OK |
|
||
| `libdatastore_shared_counter.so` | `androidx.datastore:1.2.1` | 0x4000 OK | 0x4000 OK | 0x4000 OK |
|
||
|
||
SQLCipher — the dependency called out in issue #17 — has shipped 16 KB-aligned binaries since
|
||
well before 4.16.0, and the pinned version is confirmed aligned above. AGP 9.2 also emits 16
|
||
KB-zip-aligned uncompressed libraries by default (AGP ≥ 8.5.1 behavior), and Play regenerates
|
||
delivery APKs from the AAB anyway. Re-verify after any bump of `sqlcipher`, `datastore`, or
|
||
`composeBom` in `gradle/libs.versions.toml`: Play Console → **App bundle explorer** shows a
|
||
16 KB compliance verdict per upload.
|
||
|
||
## 3. App Bundle (AAB) — PASS, signing is a human step
|
||
|
||
- `./gradlew :app:bundleRelease` succeeds and produces
|
||
`app/build/outputs/bundle/release/app-release.aab` (~10.4 MB, R8-minified). Verified
|
||
2026-07-01 with JDK 21.
|
||
- **Signing:** without `secrets.properties` the release build intentionally falls back to the
|
||
**debug** keystore (`app/build.gradle.kts:86` — installable locally, not publishable). For
|
||
Play the maintainer must create an upload keystore, set `RELEASE_STORE_FILE` /
|
||
`RELEASE_STORE_PASSWORD` / `RELEASE_KEY_ALIAS` / `RELEASE_KEY_PASSWORD` in
|
||
`secrets.properties`, rebuild, and enroll in **Play App Signing** on first upload (Play holds
|
||
the app signing key; the local key becomes the upload key).
|
||
- `versionCode 1` / `versionName "0.1.0"` (`app/build.gradle.kts:53`) — bump per release.
|
||
|
||
## 4. OAuth / CASA — no Google verification applies
|
||
|
||
Verified in source, 2026-07-01:
|
||
|
||
- **Gmail onboarding uses an app password over IMAP/SMTP, not OAuth.** The Gmail preset
|
||
(`domain/model/MailProvider.kt:37`) is plain `imap.gmail.com:993` / `smtp.gmail.com:587`
|
||
authenticating with a user-created app password; the only Google URL in the app is the
|
||
`myaccount.google.com/apppasswords` help link opened in the browser. There is **no Google
|
||
OAuth client, no Google sign-in flow, and no Gmail API scope anywhere in the code** — so the
|
||
Google restricted-scope verification and **CASA security assessment do not apply** to
|
||
LibreMail. (This is deliberate — issue #9; do not "fix" Gmail back to OAuth.)
|
||
- **Outlook OAuth is Microsoft-side only.** `auth/OutlookAuthManager.kt` uses AppAuth (PKCE,
|
||
public client) against `login.microsoftonline.com` with Microsoft Graph
|
||
(`Mail.Send`) and Exchange Online (`IMAP.AccessAsUser.All`, `SMTP.Send`) scopes. Verification
|
||
of that client is governed by **Microsoft's** app-registration/publisher rules in Azure —
|
||
nothing on the Google side. Google Play itself imposes no OAuth review; only the data-safety
|
||
and permissions declarations above cover it.
|
||
- README follow-up: tracked as issue **#20** (a fuller README pass); `README.md`'s privacy
|
||
section now links `PRIVACY.md`.
|
||
|
||
## 5. Console checklist (human steps, in order)
|
||
|
||
Everything below happens in Play Console and cannot be done from the repo. Drafted answers are
|
||
ready to paste.
|
||
|
||
1. **Developer account** — one-time registration + identity verification.
|
||
2. **Create app** — name *LibreMail*, default language, **App** (not game), **Free**.
|
||
Free-to-paid can never be toggled later; LibreMail is GPL and free.
|
||
3. **Store listing** (assets required):
|
||
- App icon **512×512 PNG** (≤1 MB); feature graphic **1024×500**; **2–8 phone screenshots**
|
||
(16:9 or 9:16, 320–3840 px; onboarding, inbox, reader, compose, settings are good
|
||
candidates); optional 7"/10" tablet screenshots.
|
||
- Short description (≤80 chars), draft:
|
||
> Open-source email for Outlook, Gmail, Yahoo, iCloud and any IMAP provider.
|
||
- Full description (≤4000 chars), draft:
|
||
> LibreMail is a free and open-source (GPL-3.0) email client with a friendly Material You
|
||
> design. Add Outlook/Hotmail (OAuth sign-in), Gmail, Yahoo, iCloud (app password), or any
|
||
> IMAP/SMTP provider; read, search, and manage your mail offline-first; compose with rich
|
||
> text, signatures, attachments, and contact autocomplete; get instant new-mail
|
||
> notifications via IMAP IDLE push — no tracking, no ads, no analytics, and your mail
|
||
> never touches our servers because we don't have any. Optional extras: encrypted local
|
||
> cache (SQLCipher), unified inbox for multiple accounts, and full mail-history backfill
|
||
> with a retention cap.
|
||
- Category **Communication**; contact email (maintainer's); privacy policy URL
|
||
`https://github.com/JMR-dev/LibreMail/blob/main/PRIVACY.md`.
|
||
4. **App content declarations:**
|
||
- **Privacy policy** — URL above.
|
||
- **Ads** — *No, my app does not contain ads* (no ad SDK; see dependency audit in
|
||
[`play-data-safety.md`](play-data-safety.md)).
|
||
- **App access** — reviewers need a mail account to exercise the app. Provide either
|
||
"All functionality is available without special access" plus a note that any IMAP account
|
||
works, or (safer) supply a disposable test account (e.g. a throwaway IMAP mailbox) under
|
||
*Special access instructions*. Do **not** hand over a personal account.
|
||
- **Content rating (IARC questionnaire)** — draft answers: email/communication app; category
|
||
**Utility / Communication**; violence/sex/language/drugs/gambling: **No** to all;
|
||
user interaction: **Yes** (users exchange email — expect an "Interactive elements: Users
|
||
Interact" notice); shares user-provided location: **No**; digital purchases: **No**.
|
||
Expected rating: **Everyone / PEGI 3** with the Users-Interact disclosure.
|
||
- **Target audience** — **13 and over** (requires an email account; not directed at
|
||
children — do not select under-13, which triggers Families policy).
|
||
- **News app** — No. **COVID-19 app** — No. **Government app** — No.
|
||
- **Financial features** — None. **Health apps** — Not a health app.
|
||
- **Data safety** — answers and evidence in [`play-data-safety.md`](play-data-safety.md).
|
||
- **Foreground service permissions** (`FOREGROUND_SERVICE_DATA_SYNC`) — declaration text and
|
||
demo-video script in [`play-permissions.md`](play-permissions.md).
|
||
- **Account deletion** — Play's deletion-URL requirement applies to apps that let users
|
||
*create an account with the developer*. LibreMail creates no such accounts (users connect
|
||
their own third-party mailboxes), so answer the "App access/account creation" question
|
||
with **no account creation** and the deletion section does not apply. In-app truth, if a
|
||
free-text answer is wanted:
|
||
> LibreMail has no user accounts of its own and stores data only on the device. Removing
|
||
> an account inside the app deletes its saved credentials and its locally cached
|
||
> messages, folders, and settings (`AccountRepositoryImpl.deleteAccount`); uninstalling
|
||
> the app removes all app data. The user's mailbox at their email provider is unaffected.
|
||
5. **Upload** the properly signed release AAB to **Internal testing** first; check **App bundle
|
||
explorer** (16 KB verdict) and the **pre-launch report** (automated crawl on real devices;
|
||
supply the test-account credentials so it can get past onboarding).
|
||
6. **Countries/regions**, pricing (Free), then promote Internal → Closed/Open testing →
|
||
Production. Note: new personal developer accounts must run a closed test (12 testers /
|
||
14 days) before production access.
|
||
|
||
## 6. Findings for the maintainer (repo-side, discovered during verification)
|
||
|
||
1. **"Push mail" is on by default, not opt-in.** `SettingsRepository.kt:41` defaults
|
||
`pushIdle = true`, so the dataSync foreground service starts as soon as the first account is
|
||
added. The manifest comment (`AndroidManifest.xml:88` — "opt-in via Advanced Settings") and
|
||
README wording say opt-in. Either flip the default to `false` or fix the comments; the Play
|
||
FGS declaration drafted here describes the **actual** behavior (default-on, user-visible
|
||
toggle, persistent notification), which is acceptable to declare but must stay truthful.
|
||
2. **README tech-stack table says min SDK 33**; the build uses `minSdk 29`
|
||
(`app/build.gradle.kts:51`). Fix with the #20 README pass.
|
||
3. **README advertises an "app lock" (biometric/device-credential)** that does not exist in the
|
||
code yet (no biometric API usage anywhere in `app/src/main`). `PRIVACY.md` deliberately does
|
||
not claim it; remove or de-scope the README claim until implemented (#20), and update
|
||
`PRIVACY.md` when it ships.
|
||
4. **Release signing falls back to the debug key** without `secrets.properties` — fine for CI,
|
||
but the Play upload must be built with the real upload keystore (section 3).
|