M10: store assets - Plain Files naming, generated icon/feature graphic, listing copy

This commit is contained in:
2026-10-14 17:47:02 +02:00
parent e3de9a2fc0
commit d845a7646e
11 changed files with 313 additions and 13 deletions
+15
View File
@@ -31,6 +31,21 @@ upload needs a **higher `versionCode`** than every previous upload.
### Added
- **M10 — store assets.** The app is named **Plain Files** (one string; the `applicationId`
was already locked in M0). `tools/make-store-assets.sh` renders the 512×512 RGBA icon and
the 1024×500 RGB feature graphic from the app's own icon vectors, so the store icon *is*
the launcher icon rather than a lookalike that drifts after the next rebrand.
`tools/make-store-screenshots.sh` normalises `adb` captures to Play's 2:1 limit per form
factor — a phone at its native 1080×2400 is 2.22:1 and would be rejected outright.
[docs/STORE_LISTING.md](docs/STORE_LISTING.md) has the ready-to-paste copy, length-checked
against Play's limits, plus the permissions paragraph that the `MANAGE_EXTERNAL_STORAGE`
declaration requires. The feature graphic's copy was verified to survive Play's
centre-square crop by cropping the rendered PNG, not by trusting the arithmetic.
**Not finished:** the screenshots themselves. The generator and the raw-capture
directories are in place, but no captures have been taken, so `store-assets/screenshots/`
is still empty. The listing is publishable except for that.
- **M9 — monetisation.** Decided: a **one-time paid app at €4.49, with a 14-day launch price
of €2.29**, both set in the Play Console. No ads, no subscription and no in-app purchases —
which means no Billing client, no `com.android.vending.BILLING` permission and no purchase
+4 -4
View File
@@ -12,10 +12,10 @@ no advertising or tracking SDKs.
## Status
**M9 — monetisation: done.** One-time paid at €4.49 (€2.29 for the first 14 days), set in
the Play Console — so the APK carries no Billing client and no `BILLING` permission, which
keeps the offline claim true of the shipped artefact. See
[docs/MONETISATION.md](docs/MONETISATION.md).
**M10 — store assets: mostly done.** The app is named **Plain Files**, the icon and feature
graphic are generated from the app's own vectors, the screenshot normaliser is written, and
[docs/STORE_LISTING.md](docs/STORE_LISTING.md) holds length-checked listing copy. The
screenshots are still outstanding — the generator is ready but no captures exist yet.
## Tech stack
+4 -8
View File
@@ -1,14 +1,10 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!--
All user-visible text lives here. English is currently the only locale, but keeping the
text in resources is what makes a translation possible later at all — Play's automatic
translation covers the store listing, not the app UI. Plurals are declared properly so a
language with more than two plural forms needs no code change.
NOTE: `app_name` is a working title. The Play listing needs a distinctive name before
the first upload (it is changeable afterwards, unlike the applicationId).
The Play listing name. Deliberately not "File Manager": dozens of apps are literally
called that, and a distinctive name is what makes the listing findable and
rememberable. "Plain" carries the pitch — no ads, no account, no clutter.
-->
<string name="app_name">File Manager</string>
<string name="app_name">Plain Files</string>
<!-- Progress notification for long file operations -->
<string name="operation_copy_title">Copying files</string>
+1 -1
View File
@@ -19,7 +19,7 @@ Legend: ✅ done · 🚧 in progress · ⏳ not started · 💡 candidate (propo
| M7 | On-device validation & hardening | ✅ | Run on a real **Xiaomi M2101K7AG** (Android 12 / MIUI V140) — see [EDGE_CASE_TESTING](EDGE_CASE_TESTING.md). Verified on hardware: the first-run permission prompt, granting "All files access" while the app is alive (picked up on resume), browsing real storage, the text editor's read **and** write path (independently checked with `adb shell cat`), the storage-usage screen, landscape layout, and **graceful degradation when access is revoked mid-use**. Found and fixed two real defects: misleading first-run copy ("Full access was declined" on a device where nothing had been declined), and a usage screen that stayed blank for **~45–60 s** on a 55 GB volume — now one listing per directory instead of a `stat` per node, with rows streamed out as they are measured. **142 JVM tests** |
| M8 | Localisation groundwork | ✅ | Every user-visible string is in `res/values/strings.xml` with real `<plurals>`, and `text/TextRes.kt` maps **both** `OperationKind` and `FileKind` onto resources so the screen, the notification and the details sheet cannot drift. The audit found a genuine miss: the details sheet rendered `FileKind.name`, i.e. untranslatable English — now a resource. `isPseudoLocalesEnabled = true` on the debug build makes "did every string come from resources?" mechanically checkable; verified `en-rXA` is present in the debug APK and the flag is debug-only. Byte units and the timestamp pattern are deliberately unlocalised, and the README says why. **142 JVM tests** |
| M9 | Monetisation | ✅ | **Decided: one-time paid — €4.49 base with a 14-day launch price of €2.29**, both set in the Play Console (*Products › App pricing*), so there is **no Billing client, no `com.android.vending.BILLING` permission and no purchase code**. Rationale, market anchors and Console steps: [MONETISATION](MONETISATION.md). Verified against the release APK: zero billing and zero `INTERNET` references. **The price must be set before the first upload** — Play allows paid → free but never free → paid |
| M10 | Store assets | ⏳ | icon/feature-graphic generators, per-form-factor screenshots, [STORE_LISTING](STORE_LISTING.md); display name confirmed |
| M10 | Store assets | 🚧 | Name decided: **Plain Files** (one string; `applicationId` already locked). `tools/make-store-assets.sh` renders the 512×512 **RGBA** icon and the 1024×500 **RGB** feature graphic from the app's own vectors, so the store icon *is* the launcher icon; `tools/make-store-screenshots.sh` normalises `adb` captures to Play's 2:1 cap per form factor (phone crop default 93/130 px, tablets 0). [STORE_LISTING](STORE_LISTING.md) carries length-checked copy (name 11/30, short 67/80, full 1881/4000), the permissions paragraph the `MANAGE_EXTERNAL_STORAGE` declaration requires, and the graphics table. The feature graphic's copy was verified to survive Play's centre-square crop by cropping the rendered PNG. **Outstanding: screenshots** — the tooling and directories are in place but no captures exist yet |
## Key decisions
+120
View File
@@ -0,0 +1,120 @@
# Play Store listing (M10)
Ready-to-paste copy for the *Main store listing* page, plus the graphics that go with it.
Every graphic is generated from the app's own vectors, so a rebrand is a re-run rather than
a redraw.
See [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) § 6 for where this fits in the
release.
## App name
**Plain Files**
Deliberately not "File Manager": dozens of apps are literally called that, and a distinctive
name is what makes a listing findable and rememberable. It is one string
(`app_name` in `values/strings.xml`); the `applicationId` is separate and already locked.
## Graphics
```bash
./tools/make-store-assets.sh # icon + feature graphic -> store-assets/
./tools/make-store-screenshots.sh # phone -> screenshots/phone/
FORM_FACTOR=tablet-7in ./tools/make-store-screenshots.sh
FORM_FACTOR=tablet-10in ./tools/make-store-screenshots.sh
```
| Asset | Play's spec | Result | Source |
|---|---|---|---|
| App icon | 512×512, 32-bit PNG with alpha, ≤ 1 MB | `store-assets/icon-512.png` — 512×512, **RGBA**, ~5 KB | `tools/store-assets/icon.svg`, mirroring `ic_launcher_background.xml` + `ic_launcher_foreground.xml`. Background, paths and opacities are identical, so **the store icon is the launcher icon** rather than a lookalike that drifts. |
| Feature graphic | 1024×500, JPEG or 24-bit PNG **without** alpha | `store-assets/feature-graphic-1024x500.png` — 1024×500, **RGB** | `tools/store-assets/feature-graphic.svg` |
| Phone screenshots | no alpha, shorter side ≥ 320, longer side ≤ 3840, **longer side ≤ 2× shorter** | `store-assets/screenshots/phone/` (1080×2160) | `tools/make-store-screenshots.sh`, from `adb` captures in `store-assets/raw-screenshots/phone/` |
Two details worth keeping:
- **The icon is full bleed.** The background rect covers all 108 dp, so every pixel is
opaque and Play's mask can crop to any shape without showing a gap.
- **A phone at its native 1080×2400 is 2.22:1 and is therefore rejected.** The normaliser
crops the status and navigation bars and then clamps to 1080×2160 — system chrome only,
never app content. Tablets are already inside the 2:1 cap, so their crop defaults to 0.
### The feature graphic's safe area
Play crops the feature graphic to a **centre square** for some placements, so all of the copy
sits inside **x 262…762** and the motif — which does not survive — is decoration. Verified by
actually cropping the rendered PNG rather than by trusting the arithmetic:
```bash
magick store-assets/feature-graphic-1024x500.png -gravity Center -crop 500x500+0+0 +repage /tmp/square.png
```
## Short description (80 characters max)
```
Browse, organise and manage your files. Ad-free, offline, pay once.
```
## Full description (4000 characters max)
```
Plain Files is a straightforward file manager for Android — no ads, no account, no
subscription, and no network access at all. You buy it once and it is yours.
BROWSE ANYTHING
• Internal storage, SD cards and USB drives, side by side
• List or grid view, with sorting by name, size, date or type
• Breadcrumb navigation and an in-folder search
• Hidden files on request
MANAGE YOUR FILES
• Copy, move, rename and delete, one item or many at once
• Create folders and edit text files in the built-in editor
• See the details of anything: size, type, location, modified date
• Open a file in any app, or share it
FIND OUT WHERE THE SPACE WENT
• A storage-usage view that measures every folder and ranks it largest first
• Spot the folder that is actually eating your storage, then act on it
ARCHIVES
• Create ZIP archives, and extract ZIP, TAR, TAR.GZ, GZ, BZ2 and XZ
• RAR and 7z are not supported — they need native code this app does not ship, and it says
so rather than failing with a strange error
PRIVATE BY DEFAULT
• No INTERNET permission. The app cannot send your data anywhere, because it has no way to.
• No analytics, no advertising SDKs, no third-party trackers
• No account, no sign-in, nothing to remember
• Your files stay on your device, always
SOLD ONCE
• One purchase, every feature, no ads and nothing to buy inside the app
• No subscription
PERMISSIONS, EXPLAINED
Plain Files asks for "All files access" because that is what browsing and managing files
anywhere on a device requires — the app's core purpose. Google Play only grants this
permission to app types that genuinely need it, and a file manager is one of them. If you
would rather not grant it, the app still works: it can browse a folder you pick yourself
through Android's own folder picker, and it remembers that choice.
Read the privacy policy: https://example.invalid/plain-files/privacy
```
> Replace the privacy-policy URL with the real public URL before publishing — a repository
> file is not acceptable to Play. [PRIVACY.md](../PRIVACY.md) is the source text.
### Why the permission sentence matters
Play requires that an app's **core functionality be prominently documented and promoted in
the app's description** when it declares `MANAGE_EXTERNAL_STORAGE`. The "PERMISSIONS,
EXPLAINED" section is not marketing copy — it is the evidence the Permissions Declaration
Form asks for.
## Still to do before publishing
- **Screenshots.** `tools/make-store-screenshots.sh` is written and the raw-screenshot
directories are in place, but no captures have been taken yet, so
`store-assets/screenshots/` is empty. Capture against a **synthetic** library — never a
real one, since published screenshots would expose real file names.
- **Tablet screenshots.** Play has a 7-inch and a 10-inch slot, each wanting 4+.
Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
#
# Generates the Google Play store graphics from the app's own icon vectors.
#
# ./tools/make-store-assets.sh [output-dir] # default: ./store-assets
#
# Produces, matching Play's asset specs:
# icon-512.png 512 x 512 32-bit PNG *with* alpha (ours is full-bleed,
# so every pixel ends up opaque anyway)
# feature-graphic-1024x500.png 1024 x 500 24-bit PNG, no alpha
#
# Output lands in ./store-assets/ *inside* the repo, never in $HOME: the editable sources are
# the SVGs in tools/store-assets/ and these PNGs are the files that get uploaded to the Play
# Console, so they belong with the release they were made for. Pass a directory to override.
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
repo="$(dirname "$here")"
out="${1:-$repo/store-assets}"
mkdir -p "$out"
render() { # <svg> <width> <height> <filename> <keep-alpha|flatten>
local svg="$1" width="$2" height="$3" name="$4" mode="$5"
inkscape "$svg" \
--export-type=png --export-width="$width" --export-height="$height" \
--export-filename="$out/$name" >/dev/null 2>&1
if [ "$mode" = keep-alpha ]; then
# Play asks for a 32-bit PNG here. The SVG's background rect covers the whole
# canvas, so the alpha channel is fully opaque — no accidental transparency.
magick "$out/$name" -alpha on PNG32:"$out/$name"
else
magick "$out/$name" -background '#1565C0' -alpha remove -alpha off PNG24:"$out/$name"
fi
printf ' %s ' "$out/$name"
magick identify -format '%wx%h %[channels] %b\n' "$out/$name"
}
echo "Writing store assets to $out"
render "$here/store-assets/icon.svg" 512 512 icon-512.png keep-alpha
render "$here/store-assets/feature-graphic.svg" 1024 500 feature-graphic-1024x500.png flatten
echo "Done. Screenshots: ./tools/make-store-screenshots.sh # reads store-assets/raw-screenshots/phone/"
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
#
# Normalises `adb exec-out screencap` captures into the files that get uploaded to Play.
#
# ./tools/make-store-screenshots.sh # phone -> 1080x2160
# FORM_FACTOR=tablet-7in ./tools/make-store-screenshots.sh # 7-inch -> 1200x1920
# FORM_FACTOR=tablet-10in ./tools/make-store-screenshots.sh # 10-inch -> 1600x2560
#
# store-assets/raw-screenshots/<form>/ the raw captures (git-ignored)
# store-assets/screenshots/<form>/ what gets uploaded (committed)
#
# Play's rule that actually bites: JPEG or 24-bit PNG **without alpha**, shorter side >= 320,
# longer side <= 3840, and **longer side <= 2x the shorter side**. A phone at its native
# 1080x2400 is 2.22:1 and is therefore REJECTED, which is why the phone path crops system
# chrome (status + navigation bars) before clamping to 2:1. A tablet frame is already inside
# the 2:1 cap on every profile the SDK ships, so its crop defaults to 0 and is opt-in.
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
repo="$(dirname "$here")"
form="${FORM_FACTOR:-phone}"
case "$form" in
phone) target_w=1080; target_h=2160; default_status=93; default_nav=130 ;;
tablet-7in) target_w=1200; target_h=1920; default_status=0; default_nav=0 ;;
tablet-10in) target_w=1600; target_h=2560; default_status=0; default_nav=0 ;;
*) echo "Unknown FORM_FACTOR '$form' (phone|tablet-7in|tablet-10in)" >&2; exit 1 ;;
esac
status_px="${STATUS_BAR_PX:-$default_status}"
nav_px="${NAV_BAR_PX:-$default_nav}"
raw="$repo/store-assets/raw-screenshots/$form"
out="$repo/store-assets/screenshots/$form"
mkdir -p "$out"
shopt -s nullglob
captures=("$raw"/*.png)
if [ ${#captures[@]} -eq 0 ]; then
echo "No captures in $raw — take some with:" >&2
echo " adb exec-out screencap -p > $raw/01-roots.png" >&2
exit 1
fi
echo "Normalising ${#captures[@]} capture(s) from $raw -> $out"
for capture in "${captures[@]}"; do
name="$(basename "$capture")"
magick "$capture" \
-gravity North -chop 0x"$status_px" \
-gravity South -chop 0x"$nav_px" \
+repage \
-resize "${target_w}x${target_h}^" \
-gravity Center -extent "${target_w}x${target_h}" \
-alpha remove -alpha off PNG24:"$out/$name"
printf ' %s ' "$out/$name"
magick identify -format '%wx%h %[channels] %b\n' "$out/$name"
done
echo "Done."
+43
View File
@@ -0,0 +1,43 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="500" viewBox="0 0 1024 500">
<!--
Play feature graphic (1024 x 500, no transparency).
Same palette as the app icon (#1565C0) with the icon motif on the left and the store copy
on the right. Text stays inside the middle band because Play crops this graphic to a
square for some placements, and the outer edges are what get cut.
-->
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#0D3F7A" />
<stop offset="0.55" stop-color="#1565C0" />
<stop offset="1" stop-color="#2E7DD1" />
</linearGradient>
</defs>
<rect x="0" y="0" width="1024" height="500" fill="url(#bg)" />
<!--
The icon motif, on the left. The icon's own content spans roughly x 24..84 and y 38..77,
so it is centred on (54, 57.5).
-->
<g transform="translate(160,250) scale(3.4) translate(-54,-57.5)">
<path d="M34,38 h14 l5,5 h27 a4,4 0 0 1 4,4 v20 a4,4 0 0 1 -4,4 h-46 a4,4 0 0 1 -4,-4 v-25 a4,4 0 0 1 4,-4 z"
fill="#FFFFFF" fill-opacity="0.45" />
<path d="M28,44 h14 l5,5 h27 a4,4 0 0 1 4,4 v20 a4,4 0 0 1 -4,4 h-46 a4,4 0 0 1 -4,-4 v-25 a4,4 0 0 1 4,-4 z"
fill="#FFFFFF" />
<path d="M44,56 h20 a2,2 0 0 1 2,2 v12 a2,2 0 0 1 -2,2 h-20 a2,2 0 0 1 -2,-2 v-12 a2,2 0 0 1 2,-2 z"
fill="#1565C0" />
</g>
<!--
Store copy. Everything here stays inside x 262..762 — the centre square Play crops to for
some placements — so the name and the tagline survive the crop even though the motif does
not. Sizes were chosen by measuring the strings, not by eye.
-->
<text x="300" y="215" font-family="DejaVu Sans" font-weight="bold" font-size="50"
fill="#FFFFFF">Plain Files</text>
<text x="302" y="265" font-family="DejaVu Sans" font-size="20"
fill="#FFFFFF" fill-opacity="0.92">Browse and manage your files</text>
<text x="302" y="298" font-family="DejaVu Sans" font-size="20"
fill="#FFFFFF" fill-opacity="0.92">Ad-free, offline, no account. Pay once.</text>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+26
View File
@@ -0,0 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 108 108">
<!--
Play app icon (512 x 512, 32-bit PNG with alpha).
This mirrors the app's own launcher icon exactly: `ic_launcher_background.xml` plus the
three paths from `ic_launcher_foreground.xml`. Background, paths and opacities are
identical, so the store icon *is* the launcher icon rather than a lookalike that drifts
after the next rebrand.
The blue rect covers the whole 108 dp canvas, which is what makes the PNG full bleed:
Play's mask can crop to any shape without revealing a gap.
-->
<rect x="0" y="0" width="108" height="108" fill="#1565C0" />
<!-- Back folder, offset up and to the right -->
<path d="M34,38 h14 l5,5 h27 a4,4 0 0 1 4,4 v20 a4,4 0 0 1 -4,4 h-46 a4,4 0 0 1 -4,-4 v-25 a4,4 0 0 1 4,-4 z"
fill="#FFFFFF" fill-opacity="0.45" />
<!-- Front folder -->
<path d="M28,44 h14 l5,5 h27 a4,4 0 0 1 4,4 v20 a4,4 0 0 1 -4,4 h-46 a4,4 0 0 1 -4,-4 v-25 a4,4 0 0 1 4,-4 z"
fill="#FFFFFF" />
<!-- Document tab -->
<path d="M44,56 h20 a2,2 0 0 1 2,2 v12 a2,2 0 0 1 -2,2 h-20 a2,2 0 0 1 -2,-2 v-12 a2,2 0 0 1 2,-2 z"
fill="#1565C0" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB