05a71f12bf
The root screen opened on a list of volumes, so "where are my photos?" began with knowing which folder they were in. It now opens on Libraries - Images, Videos, Audio, Documents, Archives and Recent - each row saying how many files the index knows of that kind, and each opening an ordinary listing with thumbnails, sorting, multi-select, details and the existing viewers, so a tap on a photo opens the image viewer and a tap on a PDF the PDF viewer. Downloads and New files are deliberately absent: one is a folder shortcut and the other a time window, and presenting three different things as one feature is how a listing starts lying about itself. Documents includes .txt, because a user looking for their documents means those too, and DIRECTORY, APK and OTHER belong to no library at all - a test asserts the three omissions so they stay decisions rather than oversights. A library has to ask for "every image" without reading a name, so IndexedFile gained a derived kind beside FileEntry's and the index stores a copy of it to filter on: schema v3, with a migration that rebuilds the files table and deletes the volume stamps with it, because a stamp means "the rows behind this are this current" and a rebuilt table has no rows behind it. Rebuilding rather than backfilling is legitimate only because this index is a cache of a walk that can be repeated; SqliteTrashIndex's rule is the opposite and its own documentation says why. A library then obeys the indexed search's two rules exactly: the answer comes first with the walk behind it, and every row is confirmed against the filesystem before it is shown, with the rows whose file has gone forgotten rather than displayed. On a device that has never been indexed the first tap on Images therefore starts a walk - which the screen admits to, using the progress row the search already had, instead of showing an empty library that reads as an empty device. "Find duplicates" in the overflow compares contents in three passes, cheapest first: size, which the index already knows, so the candidate list is one query and reads nothing at all; the first 64 KB of every candidate, a filter whose only power is to rule files out; and the whole file for whatever survives, which is the pass that decides. The separation is enforced by a test that builds two files sharing their first 64 KB and differing afterwards and asserts both were read in full - it would fail if the cheap pass were ever treated as an answer. The screen shows only what was measured: groups ordered by the space deleting the spare copies would give back, each header stating the copies and what they cost, every row carrying its folder as well as its size, and nothing preselected, because which copy to keep is the one decision the screen exists to hand over. A file whose bytes cannot be read is counted and reported rather than skipped, "nothing found" is never claimed without saying how many files were compared, and deleting closes the scan instead of patching it - its rows came from comparing bytes, and quietly dropping some of them would leave a list that still looks like a scan while its headers claim numbers nobody measured. The thumbnails went from 28 to 48 dp in a list row and 40 to 96 dp in a grid cell, with the grid's minimum column width raised to 144 dp to suit them; ListItem measures its leading slot rather than capping it, so a row grows to fit instead of clipping. That turned out to matter: the commonest complaint in the competitive survey is thumbnails that load only once scrolling stops, which is the failure mode a larger thumbnail invites. Two smaller corrections came out of the same work - a mutation now re-reads whatever is on screen, so a delete from inside a library removes the row instead of doing nothing at all because there was no path to reload, and returning to the app re-reads a library rather than dropping the user back to the roots. docs/COMPETITIVE_ANALYSIS.md records the market leader's features and what its users praise and complain about, with the sources that were blocked and the size of the review sample stated rather than glossed over; it is where both features came from, and the gaps that cost no permission are listed as candidates in the roadmap. 323 JVM tests (34 added across the two features), lint clean. Tested on the real phone - the same M2101K7AG, with the debug build installed past MIUI's two gates. The libraries on a cold index (no counts rather than a row of zeroes, "Indexing 300 files..." on the first tap, then "Index updated" and real thumbnails), the counts afterwards (Images 6238, Recent uncounted), a tap opening the viewer and BACK returning with the scroll position, and a duplicate scan that found four genuine groups in the device's own storage with nothing crashing. The duplicate delete was deliberately not pressed on a device that is not ours to empty, the store screenshots predate the new home screen, and the library views turn the M10 screenshot rule into a privacy question as well as a cosmetic one - all of that is recorded in docs/EDGE_CASE_TESTING.md rather than left implicit.
198 lines
9.7 KiB
Markdown
198 lines
9.7 KiB
Markdown
# 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
|
||
```
|
||
|
||
## Screenshots
|
||
|
||
Play's slots, and what each needs:
|
||
|
||
| Slot | How many | Target here | Notes |
|
||
|---|---|---|---|
|
||
| Phone | **2–8** | `screenshots/phone/` — 1080×2160 | A phone at its native 1080×2400 is 2.22:1 and is **rejected**; the normaliser crops chrome and clamps to 2:1 |
|
||
| 7-inch tablet | 4+ recommended | `screenshots/tablet-7in/` — 1200×1920 | A tablet frame is already inside the 2:1 cap, so 0 px of crop |
|
||
| 10-inch tablet | 4+ recommended | `screenshots/tablet-10in/` — 2560×1600 | Landscape profile (matches the `tablet10` AVD); Play accepts either orientation |
|
||
|
||
### The library the shots are taken over
|
||
|
||
**Never shoot over a real device's storage.** Published screenshots are public, and a file
|
||
list exposes real folder names and document titles. The library is generated instead:
|
||
|
||
```bash
|
||
python3 tools/make-demo-library.py # -> demo-files/ (git-ignored, ~161 MB)
|
||
adb push demo-files /sdcard/Download/PlainFiles-Demo
|
||
```
|
||
|
||
⚠️ **The home screen and the library views need this more than a folder listing does.** A library is
|
||
not a folder — it lists files from *everywhere* — so shooting `Images` on a real phone publishes the
|
||
owner's own filenames and thumbnails, and the home screen's counts publish how many files of each
|
||
kind the device holds. Both must be shot on a device, or an AVD, whose only storage is the demo
|
||
library. This was found by running the libraries on a real phone and seeing its owner's files in
|
||
them: [EDGE_CASE_TESTING](EDGE_CASE_TESTING.md).
|
||
|
||
`tools/make-demo-library.py` builds eight distinct sample images (so the grid view and
|
||
thumbnails have something to show), a handful of documents, sized stand-ins for video and
|
||
audio so the storage-usage screen has something worth ranking, and a real ZIP so the extract
|
||
action can be exercised. Every name and every pixel is fake by construction.
|
||
|
||
| 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 a search that can look in one folder or everywhere
|
||
• Hidden files on request
|
||
|
||
ANY SIZE OF SCREEN
|
||
• On a tablet or a wide window you get two panes: the folder list on the left, and what is
|
||
inside the folder you pick on the right — no jumping back and forth
|
||
• Pictures preview at full size beside the list, with their details
|
||
• The same layout on a phone, a 7-inch tablet and a 10-inch tablet
|
||
|
||
WORKS WITH YOUR OTHER APPS
|
||
• Share a file to Plain Files from anywhere, then pick the folder to drop it in
|
||
• Choose Plain Files when another app asks you for a file
|
||
• Search everywhere at once: an index means answers arrive right away instead of after a
|
||
long scan — and it always says how current it is
|
||
• Delete, rename or move by mistake? Undo it
|
||
|
||
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, TAR.BZ2, TAR.XZ, GZ, BZ2, XZ and 7z
|
||
• Look inside an archive before you extract anything — text, images, documents, audio and
|
||
video included, read straight out of the file
|
||
• RAR is not supported — it needs 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.
|
||
|
||
## Ad headlines (App campaign, 30 characters each)
|
||
|
||
Google App campaigns take **up to five headlines**, and the system mixes and matches them, so
|
||
each one has to stand on its own rather than continue the one before it. Counts include spaces
|
||
and are measured, not estimated:
|
||
|
||
| Headline | Chars | Angle |
|
||
|---|---|---|
|
||
| Ad-free file manager | 20 | The whole pitch, in three words |
|
||
| Pay once, keep it forever | 25 | One-time price, no subscription |
|
||
| Works offline. No network. | 26 | The privacy claim the APK actually backs |
|
||
| SD cards and USB drives | 23 | Reach beyond internal storage |
|
||
| See where your storage went | 27 | The storage-usage view |
|
||
|
||
Spares, to swap in when an asset group wants a different emphasis: `Two panes on your tablet`
|
||
(24), `Undo a delete you regret` (24), `Built for phones and tablets` (28).
|
||
|
||
Three rules these are written to:
|
||
|
||
- **Never "free".** This is a paid app, so a headline implying otherwise is misleading as well
|
||
as a policy problem. The launch price is the only discount, and it belongs in the price field,
|
||
not in a headline.
|
||
- **No superlatives.** "Best", "top" and "#1" are unverifiable claims, and they are not how this
|
||
app sells anyway — the differentiator is what it *does not* contain.
|
||
- **Sentence case, no shouting.** No all-caps text, no emoji, no stacked punctuation; Google's
|
||
editorial policy refuses all three, and it matches the voice of the listing copy above.
|
||
|
||
The app name is taken from the store listing (*App name* above) and is not repeated in a
|
||
headline.
|
||
|
||
## 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+.
|