From 2288ba84fee563a295d95c4ac3e0d32140059e3a Mon Sep 17 00:00:00 2001 From: Thomas Kroll Date: Fri, 13 Nov 2026 18:03:49 +0100 Subject: [PATCH 1/3] docs: store listing assets and texts ready for Google Play Screenshots: render at 1078x1918 (9:16). Google Play rejects a longer side of more than twice the shorter one, which the old 1078x2399 size broke; F-Droid has no such limit, so one size serves both. All six re-rendered from the current origin/main; site/index.html gets the new image height. Graphics: icon.png is now a 32-bit PNG with alpha, rendered from icon.svg; featureGraphic.png (1024x500, no alpha) is new, drawn from featureGraphic.svg with the launcher mark, the website's colours and its Geist font (SIL OFL) and two app screenshots. scripts/render-store-graphics.sh renders both. Texts and declarations under docs/store/: Play title, short and full description (Lime Technology attribution verbatim, kept apart from F-Droid's fastlane texts), Data safety answers derived from the store flavor with file:line evidence, content rating, target audience and ads, and the App access text with a marked placeholder until the in-app demo mode exists. ADR-0047 records Google Play as a third channel (store flavor, existing release key via Play App Signing with a separate upload key, demo mode for reviewers); ADR-0040 gets a dated amendment. Refs #328 Co-Authored-By: Claude Sonnet 5.5 --- .../nova/ui/screenshots/StoreScreenshots.kt | 6 +- .../0040-fdroid-play-distribution-flavors.md | 4 ++ ...-google-play-third-distribution-channel.md | 54 +++++++++++++++ docs/adr/README.md | 1 + docs/local-build.md | 10 ++- docs/store/README.md | 62 ++++++++++++++++++ docs/store/app-access.md | 29 ++++++++ docs/store/app-content.md | 21 ++++++ docs/store/content-rating.md | 20 ++++++ docs/store/data-safety.md | 34 ++++++++++ docs/store/play-listing/full_description.txt | 24 +++++++ docs/store/play-listing/short_description.txt | 1 + docs/store/play-listing/title.txt | 1 + .../android/en-US/images/featureGraphic.png | Bin 0 -> 112625 bytes .../android/en-US/images/featureGraphic.svg | 31 +++++++++ .../metadata/android/en-US/images/icon.png | Bin 4525 -> 5125 bytes .../metadata/android/en-US/images/icon.svg | 6 ++ .../en-US/images/phoneScreenshots/1.png | Bin 146704 -> 112300 bytes .../en-US/images/phoneScreenshots/2.png | Bin 146919 -> 112960 bytes .../en-US/images/phoneScreenshots/3.png | Bin 171983 -> 137559 bytes .../en-US/images/phoneScreenshots/4.png | Bin 145766 -> 142948 bytes .../en-US/images/phoneScreenshots/5.png | Bin 132590 -> 122055 bytes .../en-US/images/phoneScreenshots/6.png | Bin 148665 -> 117167 bytes scripts/render-store-graphics.sh | 50 ++++++++++++++ site/index.html | 12 ++-- 25 files changed, 356 insertions(+), 10 deletions(-) create mode 100644 docs/adr/0047-google-play-third-distribution-channel.md create mode 100644 docs/store/README.md create mode 100644 docs/store/app-access.md create mode 100644 docs/store/app-content.md create mode 100644 docs/store/content-rating.md create mode 100644 docs/store/data-safety.md create mode 100644 docs/store/play-listing/full_description.txt create mode 100644 docs/store/play-listing/short_description.txt create mode 100644 docs/store/play-listing/title.txt create mode 100644 fastlane/metadata/android/en-US/images/featureGraphic.png create mode 100644 fastlane/metadata/android/en-US/images/featureGraphic.svg create mode 100644 fastlane/metadata/android/en-US/images/icon.svg create mode 100755 scripts/render-store-graphics.sh diff --git a/app/src/test/kotlin/io/github/nofuturekid/nova/ui/screenshots/StoreScreenshots.kt b/app/src/test/kotlin/io/github/nofuturekid/nova/ui/screenshots/StoreScreenshots.kt index c8fd36ee..55939624 100644 --- a/app/src/test/kotlin/io/github/nofuturekid/nova/ui/screenshots/StoreScreenshots.kt +++ b/app/src/test/kotlin/io/github/nofuturekid/nova/ui/screenshots/StoreScreenshots.kt @@ -87,8 +87,10 @@ import org.robolectric.annotation.GraphicsMode */ @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) -// Pixel-8-class phone: 411x914 dp at 420 dpi = ~1080x2400 px. -@Config(sdk = [35], qualifiers = "w411dp-h914dp-night-420dpi") +// 411x731 dp at 420 dpi = 1078x1918 px (9:16). Google Play rejects phone +// screenshots whose longer side is more than twice the shorter one, which +// the earlier 20:9 size (1078x2399) broke; F-Droid has no such limit. +@Config(sdk = [35], qualifiers = "w411dp-h731dp-night-420dpi") class StoreScreenshots { @get:Rule diff --git a/docs/adr/0040-fdroid-play-distribution-flavors.md b/docs/adr/0040-fdroid-play-distribution-flavors.md index a7d02d89..166d7699 100644 --- a/docs/adr/0040-fdroid-play-distribution-flavors.md +++ b/docs/adr/0040-fdroid-play-distribution-flavors.md @@ -103,3 +103,7 @@ The Signing paragraph says the `store` flavor is "unsigned in CI output". `app/b ## Amendment 2026-11-13 — no install permission in `direct` any more [ADR-0046](./0046-update-via-browser-download.md) supersedes the parts that say the `direct` flavor declares `REQUEST_INSTALL_PACKAGES` and carries the in-app installer: `app/src/direct/AndroidManifest.xml` is gone and no flavor declares the permission. The `direct`/`store` split, `BuildConfig.HAS_UPDATER` (the update _check_ stays `direct`-only) and the signing-key consequences are unchanged. The text above stays as written. + +## Amendment 2026-11-13 — Play signing and the migration claim + +[ADR-0047](./0047-google-play-third-distribution-channel.md) corrects two statements above for Google Play. The developer holds the Play _upload_ key and Google holds the app signing key (the "Signing" paragraph has this the wrong way round). And the Play build carries the existing release key, given to Google through Play App Signing, so GitHub and Play installs update each other in place; only F-Droid, which signs with its own key, still needs an uninstall and reinstall. The text above stays as written. diff --git a/docs/adr/0047-google-play-third-distribution-channel.md b/docs/adr/0047-google-play-third-distribution-channel.md new file mode 100644 index 00000000..00d94e45 --- /dev/null +++ b/docs/adr/0047-google-play-third-distribution-channel.md @@ -0,0 +1,54 @@ +# ADR-0047: Google Play as a third distribution channel + +- **Status**: Accepted (2026-11-13) +- **Date**: 2026-11-13 +- **Tags**: release, distribution, signing, legal + +## Context + +NOVA ships through two channels: the signed APK on GitHub Releases (`direct` flavor) and F-Droid (`store` flavor, built and signed by F-Droid). ADR-0040 split the two flavors and left Google Play as "a separate later track reusing the same `store` flavor". On 2026-11-13 the maintainer decided that NOVA goes to Google Play (issue #328). The plan behind this ADR (Google's policies and pages read on the same day) found: + +- **Signing.** For a new app Google enrols Play App Signing and generates the app signing key by default. A developer "can change this default before a release is rolled out in the open testing or production track" and choose "Provide a copy of your app signing key"; the transfer goes through Google's PEPK tool, and custom keys "must be RSA 2048-bit or higher". The upload key is a separate key that the developer keeps and can reset if lost; "your upload key and app signing key should be different". A self-managed app signing key "cannot be reset if you manage it yourself and lose it" (). +- **Updates across channels.** Android accepts an update only from the same signing certificate, and a `versionCode` that is higher or equal (). Both flavors share the `versionCode`, so builds with one signer can update each other in place. +- **Policy.** Play forbids an app that "may not modify, replace, or update itself using any method other than Google Play's update mechanism" (). The `store` flavor has no updater (`BuildConfig.HAS_UPDATER` false, `app/build.gradle.kts:31-33`) and no flavor declares `REQUEST_INSTALL_PACKAGES` (ADR-0046). +- **Review.** Google requires "an active demo account, login information, and all other resources needed for Google Play to review your app" (). NOVA is useless without an Unraid server and shows only "No server configured" (`SnapshotStates.kt:119`). +- **Format.** New apps must publish as an Android App Bundle (). Nothing builds one yet. + +ADR-0040 contains two statements that this does not match. Its "Signing" paragraph says Play "re-signs via Play App Signing (Google holds the upload key once enrolled)": the developer holds the upload key, Google holds the app signing key. And it accepts that a GitHub-direct user must uninstall and reinstall to switch to a store, because "both stores re-sign the APK with a different key". That is true of F-Droid, and of Play only if Google generates the key. + +## Decision + +1. **NOVA is listed on Google Play.** +2. **The `store` flavor is the Play build,** published as an Android App Bundle (`:app:bundleStoreRelease`). Same `applicationId` (`io.github.nofuturekid.nova`), same `versionCode` as the other channels. No third flavor. +3. **The Play build carries the existing release signing key.** The maintainer gives Google a copy of the release key (the one whose certificate SHA-256 starts with `9f86baa1`, the pin in `.github/scripts/release-verify-apk.sh`) through Play App Signing, **before any open-testing or production rollout** (after that the choice cannot be made any more). A **separate upload key**, new and kept in the `release` environment, signs the bundles that are uploaded. The upload key is registered with Play; the private key of the release key stays with the maintainer, who keeps an offline backup. +4. **Reviewers get an in-app demo mode** instead of credentials: an entry on the "No server configured" screen that shows the fictional data the store screenshots already use, without any network request. It changes the APK in every flavor, including F-Droid, and is built and released as an ordinary beta cycle (separate issue). Until it ships, the App-access answer cannot be written (`docs/store/app-access.md` holds the text with placeholders). +5. **The listing material lives in this repository.** Images in `fastlane/metadata/android/en-US/images/` (shared with F-Droid), Play-only texts and the declarations in `docs/store/` (see `docs/store/README.md`). The Play title is `NOVA for Unraid®`; the attribution of Lime Technology's trademark policy sits in the description. +6. **Not decided here:** the app-bundle CI job and its signing (it must fit the tag-time signing design of #295 and needs a spike), the Play account and trader declaration (the maintainer's), the demo mode's design, and whether to register F-Droid's signing key for developer verification. + +## Corrections to ADR-0040 + +- "Google holds the upload key once enrolled" is wrong: the developer holds the upload key, Google the app signing key. Under this decision Google holds a copy of the existing release key as app signing key. +- The "uninstall and reinstall once" migration no longer applies between **GitHub and Play**: both carry the same signer. It still applies between **F-Droid and either of the others**, because F-Droid signs with its own key. + +An amendment line in ADR-0040 points here; its text is not rewritten. + +## Consequences + +- **Positive.** GitHub users who meet the Play Protect warning (ADR-0046) can move to Play without losing their servers, and the other way round, in place. One signer for GitHub and Play keeps developer verification to one identity for those two. If the Play account is ever terminated, Play users can move to the GitHub build in place. +- **Negative / trade-offs.** Google holds a copy of the key that also signs GitHub builds; the export is a one-time operation on the maintainer's machine that handles the private key. Losing that key cannot be undone, and it would also block the registration of packages (developer verification, ): an offline backup is required. Whether quantum-ready hybrid signing applies to an uploaded key is not stated by Google. The demo mode is an APK change in every flavor and holds the Play review until it ships. A personal Play account must first run a closed test with at least 12 testers for 14 days before it can apply for production (). +- **Trigger to revisit.** (a) Google rejects the demo mode as a substitute for credentials: add a permanently reachable reviewer server or amend this ADR. (b) Google's requirements for app signing or bundle uploads change before the first rollout. (c) The Play account is suspended or the key is compromised: the signing key must then be rotated, which breaks in-place updates in every channel at once. + +## Alternatives considered + +- **Google-generated app signing key (the default).** Safest for the key (Google holds it, quantum-ready hybrid signing), but Play installs get a different signer from GitHub builds: switching between GitHub and Play means uninstalling and losing app data, and developer verification would need the GitHub key registered separately. Rejected because NOVA's users move between GitHub and the stores. +- **Google-generated key, plus distributing Play's universal APK on GitHub.** Would change the signer of the GitHub build and break every existing GitHub install, because updates need the same certificate. Not viable. +- **A permanently reachable reviewer Unraid server instead of a demo mode.** No code change, but permanent internet exposure and upkeep, and a failing login is a rejection reason. Kept as the fallback; the demo mode also helps new users. +- **Organisation account.** Needs a D-U-N-S number and is meant for a business; a private person without one uses a personal account. + +## References + +- Issue hellwand/nova#328 (decision and plan). +- ADR-0038 and ADR-0039 (trademark compliance, name `NOVA for Unraid®`), ADR-0040 (flavors; amended), ADR-0046 (no install permission). +- Google: Play App Signing ; listing assets ; metadata policy ; App access ; closed-test requirement . +- Lime Technology, Third-Party App Naming and Trademark Use Policy (effective 2025-10-01): . +- [Store listing material](../store/README.md). diff --git a/docs/adr/README.md b/docs/adr/README.md index fbdfb909..318ac5fa 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -81,3 +81,4 @@ ADRs live under `docs/` so the CI docs-only bypass applies — pure-ADR PRs comp | [0044](./0044-container-detail-live-stats-overlay.md) | Surface live CPU/Mem/Net/Disk in the container detail sheet from the docker stats overlay | Accepted | | [0045](./0045-array-disk-display.md) | Array disk display — sleeping disks, per-disk thresholds, errors | Accepted (updated beta10 — 2026-05-30) | | [0046](./0046-update-via-browser-download.md) | Offer updates of the GitHub build as a browser download, without an in-app installer | Accepted | +| [0047](./0047-google-play-third-distribution-channel.md) | Google Play as a third distribution channel | Accepted | diff --git a/docs/local-build.md b/docs/local-build.md index 68600051..76db28cc 100644 --- a/docs/local-build.md +++ b/docs/local-build.md @@ -83,9 +83,11 @@ and must not change it. ## Store screenshots -The phone screenshots for F-Droid +The phone screenshots for F-Droid and Google Play (`fastlane/metadata/android/en-US/images/phoneScreenshots/`) are rendered -from the app's real composables and theme (dark, Pixel-class 1080x2400) +from the app's real composables and theme (dark, 411x731 dp at 420 dpi = +1078x1918 px; Google Play rejects a longer side of more than twice the +shorter one, see `docs/store/README.md`) by a JVM test, `StoreScreenshots`, using Robolectric (native graphics) and a fictional demo server ("Tower", RFC 5737 addresses). No device or server is involved. Regenerate them with: @@ -104,6 +106,10 @@ pill itself, in the insets the app already reserves. Edit the demo data and the `app/src/test/kotlin/io/github/nofuturekid/nova/ui/screenshots/StoreScreenshots.kt`. Robolectric downloads its Android jar from Maven Central on first use. +The store graphics (`icon.png`, `featureGraphic.png`) are rendered from SVG +sources by `./scripts/render-store-graphics.sh`; run it after the screenshots +(the feature graphic embeds two of them). Details: `docs/store/README.md`. + ## Out of scope This environment is **headless build + lint + Apollo codegen + unit diff --git a/docs/store/README.md b/docs/store/README.md new file mode 100644 index 00000000..e2f4a06c --- /dev/null +++ b/docs/store/README.md @@ -0,0 +1,62 @@ +# Store listings + +Everything the project prepares for its app-store listings that is not app code. Two stores read from here and from `fastlane/`: + +| Store | Reads | Notes | +| -------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| F-Droid | `fastlane/metadata/android/en-US/` (texts, `images/`, `changelogs/`), from the tag it builds | Unchanged by the Google Play work except that the images now also contain `featureGraphic.png` (F-Droid shows it). | +| Google Play (not live yet) | the texts in [`play-listing/`](./play-listing/) and the declarations in this folder; images from `fastlane/` | Entered by hand in Play Console. [ADR-0047](../adr/0047-google-play-third-distribution-channel.md) records the decision. | + +## What lives where + +| File | Content | +| ----------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| `fastlane/metadata/android/en-US/images/icon.png`, `icon.svg` | 512x512 32-bit PNG with alpha (Play) and its source | +| `fastlane/metadata/android/en-US/images/featureGraphic.png`, `featureGraphic.svg` | 1024x500 24-bit PNG, no alpha (Play) and its source | +| `fastlane/metadata/android/en-US/images/phoneScreenshots/1.png` to `6.png` | 1078x1918 phone screenshots (F-Droid and Play) | +| [`play-listing/title.txt`](./play-listing/title.txt), [`short_description.txt`](./play-listing/short_description.txt), [`full_description.txt`](./play-listing/full_description.txt) | Texts for Play Console | +| [`data-safety.md`](./data-safety.md) | Data safety form answers, derived from the `store` flavor's code | +| [`content-rating.md`](./content-rating.md) | Content rating questionnaire answers | +| [`app-content.md`](./app-content.md) | Target audience, ads and the other App content declarations | +| [`app-access.md`](./app-access.md) | Instructions for Google's reviewers (blocked on the demo mode) | + +## Why Play gets its own texts + +F-Droid's build recipe for NOVA (`metadata/io.github.nofuturekid.nova.yml` in fdroiddata, read 2026-11-13) has `AutoName: NOVA` and no `Summary` or `Description`, so F-Droid takes the listing from `fastlane/metadata/android/en-US/` of the tag it builds. Texts for Play cannot live there without changing F-Droid's listing. They differ in three ways: + +- **Title.** Play: `NOVA for Unraid®` (16 characters; the `[YourApp] for Unraid®` form of ADR-0038 and ADR-0039). F-Droid keeps `NOVA`, which is the app's name there. +- **Attribution.** Lime Technology's policy (section 6, , read 2026-11-13) wants the attribution near the first mention of "Unraid" in each medium, and lets the listing description carry the short form. The Play full description opens with the short form and ends with the long form, both verbatim from the policy. The F-Droid description still carries the older wording ("... endorsed by, or supported by Lime Technology"), as do the app's About screen (`SettingsScreen.kt`) and `docs/privacy/POLICY.md`; aligning them is a separate change (the app one needs an APK build). +- **Claims.** The F-Droid text says "no third-party services involved". That is true of NOVA's own traffic, but container icons come from hosts the user's server names (ADR-0040, amendment 2026-11-10), so the Play text says "no NOVA account, no analytics and no advertising" instead. + +Limits (Google, read 2026-11-13): title 30 characters and short description 80 characters are stated on the [metadata policy](https://support.google.com/googleplay/android-developer/answer/9898842) and the [listing assets page](https://support.google.com/googleplay/android-developer/answer/9866151). The 4000-character limit for the full description is Play Console's own field limit and is **not** printed on either page. The metadata policy only asks for a "succinct and straightforward" description with no keyword stuffing, no rankings, no "best" or promotional claims, no emojis or repeated special characters. Check the lengths and the banned words with: + +```bash +cd docs/store/play-listing +for f in title short_description full_description; do printf '%s: ' "$f"; python3 -c "print(len(open('$f.txt', encoding='utf-8').read().rstrip('\n')))"; done +grep -n -i -E '\b(free|best|#1|top|number one)\b' *.txt # must print nothing +``` + +Current values: title 16, short description 74, full description 1747. + +When the Play listing goes live, add one line to `full_description.txt` that names the demo mode (see `app-access.md`), and change the future-tense mention of Google Play in `docs/privacy/POLICY.md` (line "and, in the future, the Google Play Store"). + +## Rendering the graphics + +```bash +./scripts/store-screenshots.sh # re-render the phone screenshots (Docker, as local-ci does) +./scripts/render-store-graphics.sh # icon.png and featureGraphic.png from their SVG sources +``` + +The second script needs `rsvg-convert`, `magick` and `woff2_decompress`, uses the website's Geist font (SIL OFL, `site/fonts/`) through a private fontconfig file, and embeds `phoneScreenshots/1.png` and `3.png` into the feature graphic, so run it after the screenshots. + +### Google's numbers (read 2026-11-13) + +- Icon: 512x512, "32-bit PNG (with alpha)", at most 1024 KB. The existing icon was RGB without alpha and is now RGBA. +- Feature graphic: 1024x500, "JPEG or 24-bit PNG (no alpha)". +- Phone screenshots: JPEG or 24-bit PNG (no alpha), 2 to 8 per device type, each side 320 to 3840 px, and "the maximum dimension of your screenshot can't be more than twice as long as the minimum dimension". The earlier 1078x2399 screenshots (ratio 2.23) broke that rule. +- Screenshot size, chosen: **1078x1918**, 411x731 dp at 420 dpi (ratio 1.78, the 9:16 portrait shape Google recommends for featuring). 1080x2160 would pass the rule at exactly 2:1 with no margin and is not Google's recommended shape; F-Droid has no size limit, so one size serves both. The screenshots are 24-bit RGB without alpha. + +### Still to do before the listing goes live + +- Re-render the screenshots once the Overview "needs attention" card and the container search land (the Docker screenshot already shows the search field; the Overview does not yet show a needs-attention card), then run `render-store-graphics.sh` again. +- `featureGraphic.png` is new in the F-Droid image folder. F-Droid's `fdroid update` ignores the two `.svg` sources next to it (not tested here: no F-Droid tooling on this machine). diff --git a/docs/store/app-access.md b/docs/store/app-access.md new file mode 100644 index 00000000..880b2553 --- /dev/null +++ b/docs/store/app-access.md @@ -0,0 +1,29 @@ +# Google Play App access: instructions for reviewers + +Play Console, App content, App access. Google's requirement: "Provide an active demo account, login information, and all other resources needed for Google Play to review your app" (, requirement 3.3). Credentials and resources "must be accessible at all times, reusable, and valid regardless of user location", and instructions must be in English (). Up to five sets of instructions fit into the form; "Any other instructions" takes free text (). + +## Status: blocked on the demo mode + +NOVA is a client for the user's own Unraid server. Without a server it shows only "No server configured" and "Add server" (`SnapshotStates.kt:119,130`), and adding a server needs a host and an API key. A reviewer has neither. ADR-0047 decides to give reviewers a **demo mode** inside the app, backed by the fictional data the store screenshots already use (`StoreScreenshots.kt`, server "Tower"). **That demo mode does not exist yet** (APK change in every flavor, tracked separately). Until it ships and is on the track Google reviews, the text below cannot be entered: a placeholder in the form, or a promise of something the build does not have, is a rejection reason ("If the provided password expires ... the app may be rejected", Google, answer 15748846). + +If a review is rejected because Google does not accept a demo mode instead of credentials, the fallback is a permanently reachable read-only Unraid server for reviewers (no code change, permanent exposure and upkeep); it needs its own decision. + +## Text to enter (English; fill the placeholders when the demo mode ships) + +```text +NOVA is a remote client for a user's own Unraid NAS server. It has no accounts, no sign-in and no in-app purchases. No credentials are needed to review it. + +To look at the app with sample data: +1. Open NOVA. The first screen says "No server configured". +2. [PLACEHOLDER, demo mode not built yet: tap "