diff --git a/CHANGELOG.md b/CHANGELOG.md index d4c1c86..231ae9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 4d2c7ed..9bf50dc 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0b3fcc6..435ef91 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,14 +1,10 @@ - File Manager + Plain Files Copying files diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 0c36f1a..bfac368 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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 ``, 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 diff --git a/docs/STORE_LISTING.md b/docs/STORE_LISTING.md new file mode 100644 index 0000000..406d913 --- /dev/null +++ b/docs/STORE_LISTING.md @@ -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+. diff --git a/store-assets/feature-graphic-1024x500.png b/store-assets/feature-graphic-1024x500.png new file mode 100644 index 0000000..80d6aeb Binary files /dev/null and b/store-assets/feature-graphic-1024x500.png differ diff --git a/store-assets/icon-512.png b/store-assets/icon-512.png new file mode 100644 index 0000000..1666fbf Binary files /dev/null and b/store-assets/icon-512.png differ diff --git a/tools/make-store-assets.sh b/tools/make-store-assets.sh new file mode 100755 index 0000000..2634b5c --- /dev/null +++ b/tools/make-store-assets.sh @@ -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() { # + 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/" diff --git a/tools/make-store-screenshots.sh b/tools/make-store-screenshots.sh new file mode 100755 index 0000000..9a34dba --- /dev/null +++ b/tools/make-store-screenshots.sh @@ -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/
/ the raw captures (git-ignored) +# store-assets/screenshots/
/ 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." diff --git a/tools/store-assets/feature-graphic.svg b/tools/store-assets/feature-graphic.svg new file mode 100644 index 0000000..972c8c5 --- /dev/null +++ b/tools/store-assets/feature-graphic.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + Plain Files + Browse and manage your files + Ad-free, offline, no account. Pay once. + diff --git a/tools/store-assets/icon.svg b/tools/store-assets/icon.svg new file mode 100644 index 0000000..51736d3 --- /dev/null +++ b/tools/store-assets/icon.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + +