Write the release notes, the permission answers, and the video
Play asks three separate things before a file manager may keep All files access: the What's new copy, a justification for why SAF and MediaStore cannot carry it, and a short video of the permission in use. Writing them exposed two stale claims and one privacy problem, so those are fixed here rather than repeated. - docs/RELEASE_NOTES.md holds the Console field at 450/500 characters, measured, and the longer announcement. The first release's copy leads with the claim no competitor makes (no ads, no INTERNET permission) and then proves the app is still a file manager. - docs/PLAY_RELEASE_CHECKLIST.md section 5 holds the declaration answer at 490/500 characters. It argues from what the Storage Access Framework and MediaStore cannot do - one picked tree, no volume enumeration or whole-device hierarchy, no arbitrary non-media files - and never from performance, which Google's wording rejects. Both APIs are named as already used where they can be, so the argument is not that they are useless. - docs/PERMISSION_VIDEO.md and store-assets/video hold a 19.7 s captioned video recorded on the release build. The home-screen segment is cut because its library counts are the owner's real totals; the published copy resumes on the internal-storage listing. Also recorded: the emulator captures one frame in three seconds, and only appops set --uid ... deny brings the prompt back after Settings has granted the app-op. - README and ROADMAP caught up with the build: the status stopped at M15, and the trash limitation still said there was no Empty trash and that expiry waited for the next delete, both changed in M19. Verified: ./tools/verify.sh green, the code map current, the Play field measured at 450 characters and the form answer at 490, and the video decoded as H.264 1080x2400, 19.666 s, no audio, with frames checked at the cut and the end state.
This commit is contained in:
@@ -118,6 +118,8 @@ comment. `app/src/test/java/…/ProjectInvariantsTest.kt` holds the source-level
|
||||
| `docs/EDGE_CASE_TESTING.md` | What was verified, on which device or AVD, and what was not |
|
||||
| `docs/PERFORMANCE.md` | The numbers that exist, and the ones that deliberately do not yet |
|
||||
| `docs/PLAY_RELEASE_CHECKLIST.md` | Release only — work it in order |
|
||||
| `docs/RELEASE_NOTES.md` | The Play **What's new** copy, at 500 characters, and the longer announcement |
|
||||
| `docs/PERMISSION_VIDEO.md` | The declaration's 23-second video: what it shows, and how to re-shoot it |
|
||||
| `docs/COMPETITIVE_ANALYSIS.md`, `docs/MONETISATION.md`, `docs/STORE_LISTING.md` | Product context, pricing, and length-checked listing copy |
|
||||
| `PRIVACY.md` | The policy text the store listing points at |
|
||||
|
||||
|
||||
@@ -12,6 +12,42 @@ upload needs a **higher `versionCode`** than every previous upload.
|
||||
|
||||
### Added
|
||||
|
||||
- **The release notes exist before the release does, and their length is measured.** `docs/RELEASE_NOTES.md`
|
||||
holds the copy for the Play Console's **What's new** field — **450 characters**, against Play's 500
|
||||
limit, counted rather than estimated — plus a longer announcement for a website or GitHub release.
|
||||
The Console field renders neither Markdown nor bullets, so the copy is plain sentences; the first
|
||||
release's text leads with the claim the survey says no competitor makes (no ads, no `INTERNET`
|
||||
permission) and then proves it is still a file manager, because "private" without the verbs reads as
|
||||
a promise rather than a product. The file also says how the next one is written: move `[Unreleased]`
|
||||
into a version heading **first**, so the changelog and the notes cannot describe different builds.
|
||||
|
||||
Two stale claims were found while writing it and fixed rather than repeated in the announcement: the
|
||||
README's known limitations still said the trash had no "empty it now" action and that expiry waited
|
||||
for the next delete (both changed in M19), and its status paragraph still stopped at M15.
|
||||
|
||||
- **The Permissions Declaration Form's answer is written down, at 490 characters.** Section 5 of
|
||||
[PLAY_RELEASE_CHECKLIST](docs/PLAY_RELEASE_CHECKLIST.md) now holds the justification for
|
||||
`MANAGE_EXTERNAL_STORAGE`, measured against the form's 500-character field. It argues from what the
|
||||
Storage Access Framework and `MediaStore` cannot do — a grant covers one picked tree, neither
|
||||
enumerates volumes or presents a whole-device hierarchy, and `MediaStore` exposes media collections
|
||||
rather than arbitrary files and folders — and never from speed, which Google's own wording rejects.
|
||||
The app's existing SAF fallback and its single best-effort delete are named, so neither API is
|
||||
dismissed as useless; it is only unable to carry the core purpose.
|
||||
|
||||
- **The permission-use video is recorded, captioned and cut for privacy.** Play's form also asks for a
|
||||
short video of the permission in use; `store-assets/video/permission-use.mp4` is **19.7 s** at
|
||||
1080×2400 with six burned-in English captions (no audio), beside an uncaptioned twin and the `.srt`.
|
||||
It shows the app's prompt, Android's grant screen (the phone's system UI is German, translated in the
|
||||
captions), internal storage, non-media documents and a folder created through the same permission.
|
||||
The raw take also showed the home screen, whose library counts are the owner's real totals, so the
|
||||
published copy **cuts that segment** and resumes on the internal-storage listing. That is a privacy
|
||||
redaction, not an edit to the evidence, and both copies share it.
|
||||
|
||||
Two traps are written down with it: the emulator under software emulation captures **one frame in
|
||||
three seconds**, so the video has to come from the phone; and after Settings grants the app-op, a
|
||||
package-level `appops set … deny` leaves the UID mode allowed, so only
|
||||
`appops set --uid … deny` brings the prompt back.
|
||||
|
||||
- **A dot marks what an operation just produced.** Copy something, extract an archive, rename a file or make a
|
||||
folder, and the rows it created carry a small dot until you open that folder again — so "what did that 4 GB
|
||||
copy do?" has an answer, including when the copy went to a folder you were not looking at. The mark lives in
|
||||
|
||||
@@ -12,25 +12,23 @@ no advertising or tracking SDKs.
|
||||
|
||||
## Status
|
||||
|
||||
**M15 — favourites: done.** A star in any folder's app bar puts that folder on the home screen, above
|
||||
the libraries, where a folder that has gone is marked *not found* rather than silently dropped and
|
||||
the row's own star removes it. Before it: **M14 — renaming a batch** (selecting several files and
|
||||
choosing Rename opens a **pattern** dialog instead of the single-file one: one name, the number to
|
||||
start at, and a preview of the names
|
||||
it will produce — and the numbering **carries on from the folder's own**, so `series (1)` and
|
||||
`series (2)` already there make the next batch start at 3. A batch that cannot be done is refused
|
||||
before anything moves, naming the file in the way, and the whole batch is one undo step. Before it:
|
||||
**M13 — video thumbnails** (a video row shows a frame of the video itself rather than a film icon —
|
||||
the platform's own retriever, through Coil, with a frame taken a second in and the icon kept as the
|
||||
fallback when a file cannot be decoded), **M12 — duplicate finder** (size, then the head of every
|
||||
candidate, then the survivors in full), **M11 — library views** (a **Libraries** section on the
|
||||
root screen — Images, Videos, Audio, Documents, Archives and Recent — whose rows open
|
||||
ordinary listings of those files, each carrying a count from the search index), and **M10 — store
|
||||
assets**: the app is named **Plain Files**, the icon and feature graphic come from the app's own
|
||||
vectors, and [docs/STORE_LISTING.md](docs/STORE_LISTING.md) holds length-checked listing copy.
|
||||
Screenshots exist for 5 phone (1080×2160), 4 on a 7-inch tablet (1200×1920) and 4 on a 10-inch
|
||||
tablet (2560×1600) — but they **predate M11**, so the home screen in them is the old volume list and
|
||||
they will need re-shooting, which the library views make a privacy question as well as a cosmetic one
|
||||
**M20 — launcher shortcuts: done.** Long-press the app icon for **Images**, **Recent** and **Trash**.
|
||||
Since the last release build: **M19** gave the trash its 24-hour window, **Empty trash** and a sweep on
|
||||
open; **M18** let compression store files instead of squeezing them; **M17** put a dot on the rows an
|
||||
operation produced; and **M16** added **Copy to… / Move to…** with a destination picker that does not
|
||||
take you out of the folder you are in. Before them: **M15 — favourites** (a star puts a folder on the
|
||||
home screen), **M14 — batch rename** (one pattern, a start number, and a series that carries on from
|
||||
the folder's own), **M13 — video thumbnails** (a frame of the video itself rather than a film icon),
|
||||
**M12 — duplicate finder** (size, then the head of every candidate, then the survivors in full),
|
||||
**M11 — library views** (Images, Videos, Audio, Documents, Archives and Recent, counted from the
|
||||
search index), and **M10 — store assets**: the app is named **Plain Files**, the icon and feature
|
||||
graphic come from the app's own vectors, [docs/STORE_LISTING.md](docs/STORE_LISTING.md) holds
|
||||
length-checked listing copy, and [docs/RELEASE_NOTES.md](docs/RELEASE_NOTES.md) holds the copy for the
|
||||
Console's **What's new** field.
|
||||
|
||||
Screenshots exist for 8 phone (1080×2160), 4 on a 7-inch tablet (1200×1920) and 4 on a 10-inch
|
||||
tablet (2560×1600). The phone set was re-shot on 2026-10-18 and covers M16-M20; the tablet sets
|
||||
**predate M11** and should be regenerated on a machine with KVM before the listing goes live
|
||||
([docs/STORE_LISTING.md](docs/STORE_LISTING.md)).
|
||||
|
||||
## Tech stack
|
||||
@@ -179,10 +177,10 @@ UI.** Translated UI text has to be supplied in `values-<locale>/`.
|
||||
|
||||
- **The trash is a time window, not a bin you manage.** A delete stages the item in a hidden
|
||||
`.plainfiles-trash` folder on the same volume and keeps it for 24 hours; the **Trash** entry
|
||||
in the overflow menu lists what is still there and puts a whole batch back. There is no
|
||||
"empty it now" action — expired batches are swept the next time something is deleted — and a
|
||||
name taken again in the meantime is never overwritten, so a restored item can arrive beside
|
||||
the new one rather than replacing it.
|
||||
in the overflow menu lists what is still there, puts a whole batch back, and offers **Empty
|
||||
trash** for when the window is not what you want. Opening the trash sweeps what has expired,
|
||||
so retention no longer waits for the next delete. A name taken again in the meantime is never
|
||||
overwritten, so a restored item can arrive beside the new one rather than replacing it.
|
||||
- **Where a trashed item came from is remembered in the app's own database**, not on the
|
||||
volume. The staged files are the truth and the database is only an index, so the two are
|
||||
reconciled in both directions: an entry whose file has gone is not offered back, and a
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# The permission-use video
|
||||
|
||||
The Permissions Declaration Form asks for a link to a short video that shows
|
||||
`MANAGE_EXTERNAL_STORAGE` being used — 30 seconds or fewer, with a voice-over or captions where the
|
||||
UI does not make the use obvious. This file is the record of the video that answers it, and the
|
||||
instructions for making another.
|
||||
|
||||
## The file
|
||||
|
||||
| file | length | what it is |
|
||||
|---|---|---|
|
||||
| `store-assets/video/permission-use.mp4` | **19.7 s** | The submission copy: 1080×2400, H.264, **no audio**, English captions burned in. The home-screen segment is deliberately cut (below) |
|
||||
| `store-assets/video/permission-use-uncaptioned.mp4` | 19.7 s | The same cut without captions, for a voice-over instead |
|
||||
| `store-assets/video/permission-use.srt` | — | The caption text and timings, for a re-upload or translation |
|
||||
|
||||
**The file is not a link.** Play asks for a URL, so upload the MP4 — YouTube **Unlisted**, or Google
|
||||
Drive shared with "anyone with the link" — and paste that URL into the form. A private video or a
|
||||
sign-in-gated Drive link cannot be reviewed. Unlisted is enough; the video does not need to be
|
||||
public or searchable.
|
||||
|
||||
## What it shows
|
||||
|
||||
| time | on screen | what it proves |
|
||||
|---|---|---|
|
||||
| 0:00 | The app's **Choose a folder** prompt, with **Grant full access** | The app asks for the permission rather than assuming it |
|
||||
| 0:01–0:08 | Android's **All files access** screen; the toggle is off, then on | The user grants it through Android's own UI, not an in-app shortcut |
|
||||
| 0:08–0:12 | *(after a deliberate cut)* Internal storage, then the **AADemo** folder | The permission is what makes a whole-volume listing possible |
|
||||
| 0:12–0:14 | Documents: `budget.csv`, `itinerary.md`, `offsite-notes.txt`, `quarterly-report.pdf`, `service-agreement.docx` | The use is not media: these are files `MediaStore` does not manage |
|
||||
| 0:14–0:15 | **New folder** dialog | A write is about to be attempted |
|
||||
| 0:15–0:19 | **New folder** appears with the change dot; snackbar **Folder created** | The same permission is used to write, not only to read |
|
||||
|
||||
**The cut at 0:08 is a privacy redaction, not an edit to the evidence.** The raw take returns to the
|
||||
app's home screen between the grant and the folder listing, and that screen shows the phone's own
|
||||
library counts — **6 238 images and 20 248 documents**. [STORE_LISTING](STORE_LISTING.md) forbids
|
||||
publishing those counts, because they describe the owner's device rather than the app. The published
|
||||
video removes the home-screen segment and resumes on the internal-storage listing, which is the
|
||||
first frame that proves the grant took effect. The uncaptioned file is cut at the same two points,
|
||||
so both copies tell the same story.
|
||||
|
||||
The captions, in order:
|
||||
|
||||
1. *Plain Files needs All files access*
|
||||
2. *Android asks to allow all-file management*
|
||||
3. *Switching on All files access*
|
||||
4. *Granted: browsing internal storage*
|
||||
5. *Documents: CSV, MD, TXT, PDF, DOCX*
|
||||
6. *Writable too: a new folder is created*
|
||||
|
||||
## Why the system UI is German
|
||||
|
||||
The test phone (Xiaomi M2101K7AG) runs MIUI in German; the app itself is English. Changing the phone's
|
||||
language to flatter a recording would not change what the permission does, and the two system strings
|
||||
that matter are translated by the captions: *Zugriff auf alle Dateien* ("All files access") and
|
||||
*Zugriff zum Verwalten aller Dateien zulassen* ("Allow access to manage all files"). The reviewer sees
|
||||
the toggle move and the app change state, which is the evidence; the language is incidental.
|
||||
|
||||
## Privacy: what is not in the video
|
||||
|
||||
- The library is **generated** by `tools/make-demo-library.py`, not taken from the phone. It was
|
||||
copied to `/sdcard/AADemo` so it sorts first and the recording never has to open `Download`, whose
|
||||
listing on this device holds the owner's real PDFs.
|
||||
- Every filename on screen — `budget.csv`, `itinerary.md`, `quarterly-report.pdf`, `AADemo` — is
|
||||
synthetic. The published cut removes the home screen entirely, because its library counts
|
||||
(**6 238 images, 20 248 documents**) are the owner's real totals; the uncaptioned file is cut at the
|
||||
same points. What remains shows synthetic names and Android's own screens.
|
||||
- Frames were inspected with a contact sheet before and after the cut, and the status bar holds
|
||||
nothing but the clock, signal, battery and the screen-record indicator.
|
||||
|
||||
## Re-shooting it
|
||||
|
||||
The take was made on the release build, because that is the artefact the declaration is about:
|
||||
|
||||
```bash
|
||||
./gradlew :app:assembleRelease
|
||||
adb install -r app/build/outputs/apk/release/app-release.apk
|
||||
adb shell cp -r /sdcard/Download/PlainFiles-Demo /sdcard/AADemo # only if absent
|
||||
adb shell appops set --uid de.softwareapp_hb.filemanager MANAGE_EXTERNAL_STORAGE deny
|
||||
adb shell am force-stop de.softwareapp_hb.filemanager
|
||||
adb shell am start -n de.softwareapp_hb.filemanager/.MainActivity
|
||||
```
|
||||
|
||||
⚠️ **The `--uid` is the whole trap.** After Android's Settings screen grants the permission, a
|
||||
package-level `appops set de.softwareapp_hb.filemanager MANAGE_EXTERNAL_STORAGE deny` leaves the
|
||||
**UID mode** at `allow`, the app keeps `isExternalStorageManager() == true`, and no prompt appears.
|
||||
The revoke only works with `--uid`, and `appops get` shows both lines so the state can be checked
|
||||
before recording.
|
||||
|
||||
Then, with the prompt verified on screen, record and drive it:
|
||||
|
||||
```bash
|
||||
adb shell rm -f /sdcard/permission-use.mp4
|
||||
adb shell screenrecord --size 1080x2400 --bit-rate 8000000 --time-limit 30 /sdcard/permission-use.mp4 &
|
||||
sleep 1
|
||||
adb shell input tap 540 1358 # Grant full access
|
||||
sleep 2.5
|
||||
# If the runtime storage dialog appears, tap ERLAUBEN at (540,1321). It does not when
|
||||
# READ_EXTERNAL_STORAGE is already granted, which is the normal case on this phone.
|
||||
sleep 2.5
|
||||
adb shell input tap 931 526 # All files access toggle
|
||||
sleep 2
|
||||
adb shell input keyevent KEYCODE_BACK
|
||||
sleep 2
|
||||
adb shell input tap 540 1600 # Internal storage
|
||||
sleep 2
|
||||
adb shell input tap 540 644 # AADemo
|
||||
sleep 2
|
||||
adb shell input tap 540 644 # Documents
|
||||
sleep 2
|
||||
adb shell input tap 959 2146 # New folder (FAB)
|
||||
sleep 1.5
|
||||
adb shell input tap 824 1368 # Create; the name is prefilled, so no keyboard appears
|
||||
sleep 4
|
||||
adb shell pkill -INT screenrecord
|
||||
adb pull /sdcard/permission-use.mp4
|
||||
```
|
||||
|
||||
Then cut out the home screen. In this take the last Settings frame is at **8.25 s** and the first
|
||||
internal-storage frame is at **10.5 s**; re-check those two frames before reusing the numbers, because
|
||||
the exact times depend on how fast the taps landed:
|
||||
|
||||
```bash
|
||||
ffmpeg -y -i permission-use.mp4 -filter_complex \
|
||||
"[0:v]trim=0:8.25,setpts=PTS-STARTPTS[a];[0:v]trim=10.5,setpts=PTS-STARTPTS[b];[a][b]concat=n=2:v=1[v]" \
|
||||
-map "[v]" -c:v libx264 -crf 20 -pix_fmt yuv420p -an permission-use-cut.mp4
|
||||
```
|
||||
|
||||
Do not try to record this on the AVD. Under software emulation `screenrecord` captured **one frame in
|
||||
three seconds** — a three-second test produced a 1-frame, duration-less file — so the clean emulator
|
||||
cannot produce video. The phone records properly; the emulator is only useful for checking the demo
|
||||
library and the permission screens.
|
||||
|
||||
Two details are deliberate:
|
||||
|
||||
- **Never tap the name field.** On this MIUI build it raises the keyboard, the dialog shifts up, and
|
||||
the next tap lands on the keyboard instead of **Create** — the first attempt created the folder but
|
||||
ended on an empty screen. The prefilled *New folder* is enough to show a write.
|
||||
- **The coordinates assume 1080×2400 at 440 dpi** (the Xiaomi phone). On another device, map each
|
||||
screen with `adb shell uiautomator dump` and use the bounds rather than scaling these.
|
||||
|
||||
The captions were burned in from six transparent PNG overlays with `ffmpeg overlay` (Pillow, DejaVu
|
||||
Sans Bold 40 px, black 80 % box, bottom-centred above the snackbar). The text and timings are in
|
||||
`store-assets/video/permission-use.srt`, so a re-upload can carry the same captions without burning
|
||||
them in.
|
||||
@@ -152,6 +152,28 @@ This is the step that can block a file manager, and the one to get right.
|
||||
- [ ] Keep the permission set honest: declaring a permission the app does not use is itself
|
||||
a policy violation.
|
||||
|
||||
**The declaration answer (490 / 500 characters, measured):**
|
||||
|
||||
```
|
||||
Plain Files' core purpose is browsing and managing every file and folder on internal storage, SD cards and USB drives: documents, archives, APKs, hidden files and folders created by other apps. The Storage Access Framework grants one user-picked tree at a time and cannot enumerate volumes or present a whole-device hierarchy. MediaStore exposes indexed media collections, not arbitrary files or folder trees. SAF remains a fallback for a chosen folder, but cannot deliver the core purpose.
|
||||
```
|
||||
|
||||
This is a **functional** argument, which is the only kind the form accepts — Google's own wording
|
||||
says improving performance is not an acceptable reason, so speed is not mentioned. Each claim is true
|
||||
of this build: `data/SafSource` already provides the folder-tree fallback, and the app's only
|
||||
`MediaStore` use is the best-effort delete request (section 5's permission audit), so neither API is
|
||||
being dismissed as unnecessary. What they cannot do is *enumerate every volume*, *present one
|
||||
hierarchy spanning internal storage and removable media*, or *manage arbitrary non-media files and
|
||||
folders* — which is what "file manager" means, and why the listing copy leads with it.
|
||||
|
||||
If a future form allows fewer characters, cut the last sentence first: the fallback is a point of
|
||||
honesty, not the justification.
|
||||
|
||||
- [ ] Upload `store-assets/video/permission-use.mp4` (**19.7 s**, captions burned in) to YouTube
|
||||
**Unlisted** or Drive shared with anyone who has the link, then paste that URL into the form's
|
||||
video field. The repository file is not a link, and a private video cannot be reviewed. What it
|
||||
shows, why the system UI is German, and how to re-shoot it: [PERMISSION_VIDEO](PERMISSION_VIDEO.md).
|
||||
|
||||
**Verify the shipped permission set** after every release build:
|
||||
|
||||
```bash
|
||||
@@ -197,6 +219,10 @@ Full copy and the generated graphics land in **M10** (`docs/STORE_LISTING.md`).
|
||||
fields needed: app name, short description, full description, app icon 512×512, feature
|
||||
graphic 1024×500, and the screenshots.
|
||||
|
||||
The per-release **What's new** field is separate from the listing and is capped at **500 characters**:
|
||||
its copy, measured, is in [RELEASE_NOTES](RELEASE_NOTES.md), together with the longer announcement
|
||||
for a website or GitHub release.
|
||||
|
||||
- [ ] Upload the screenshots from `store-assets/screenshots/`: **phone** 1080×2160 (8),
|
||||
**7-inch tablet** 1200×1920 (4), **10-inch tablet** 2560×1600 (4). Play wants 4+ per
|
||||
large-screen form factor. All three sets clear its rules — 24-bit PNG with no alpha,
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
# Release notes
|
||||
|
||||
The copy a buyer reads *before* installing, as opposed to [CHANGELOG](../CHANGELOG.md), which is the
|
||||
reasoning for whoever maintains the app. Two lengths live here:
|
||||
|
||||
- **Play Console → What's new** — plain text, **500 characters per language**, no Markdown and no
|
||||
bullets (Play renders neither), measured rather than estimated.
|
||||
- **The longer announcement** — for a website, a forum post or a GitHub release, where the limit is
|
||||
the reader's patience rather than a Console field.
|
||||
|
||||
The source of truth for *what* changed is the changelog, and the source of truth for *what the app
|
||||
claims* is the [README](../README.md) and [STORE_LISTING](STORE_LISTING.md). This file only chooses
|
||||
which of those facts a new user should hear first.
|
||||
|
||||
## Writing the next one
|
||||
|
||||
1. **Version.** `versionName` and `versionCode` are in `app/build.gradle.kts`; the latter must be
|
||||
higher than every previous upload. The first release is `1.0.0` / `1`.
|
||||
2. **Changelog first.** Move the entries out of `## [Unreleased]` into a
|
||||
`## [1.0.0] - YYYY-MM-DD` heading, so this file and the changelog cannot describe different
|
||||
builds.
|
||||
3. **Then the Play field.** Say what a user can now do that they could not before — never what the
|
||||
code did. A first release describes the app instead of a diff.
|
||||
4. **Measure it.** Play counts characters, line breaks included:
|
||||
|
||||
```bash
|
||||
python3 -c "print(len(open('/tmp/whats-new.txt').read().strip()))"
|
||||
```
|
||||
|
||||
5. **Quote nothing that a device has not shown.** A limitation belongs in the announcement only if
|
||||
it is already in the README's *Known limitations*; if it is not, the README is what needs fixing.
|
||||
|
||||
---
|
||||
|
||||
## 1.0.0 — first release
|
||||
|
||||
### Play Console → What's new (500 characters max)
|
||||
|
||||
```
|
||||
Plain Files is here: a file manager with no ads, no account and no internet permission, so nothing leaves your device.
|
||||
|
||||
Browse internal storage, SD cards and USB drives. Copy, move, rename and delete files; undo a delete, rename or move; search everywhere; find duplicates; unpack ZIP, TAR, GZ, BZ2, XZ and 7z; star the folders you use; and read images, video, PDFs and text inside the app.
|
||||
|
||||
One purchase, every feature. No tracking, no subscription.
|
||||
```
|
||||
|
||||
**Measured: 450 characters** (including the two blank lines between paragraphs). The remaining
|
||||
headroom is deliberate: it leaves room for a sentence about a fix without rewriting the whole field.
|
||||
|
||||
Why this shape: the first two sentences are the claim no competitor in the survey makes — no ads and
|
||||
**no `INTERNET` permission** — and the middle one is the proof that it is still a file manager. The
|
||||
verbs are the ones people use ("copy, move, rename, delete"), not the app's internal names. Nothing
|
||||
here is a superlative, an emoji, or the word *free*: the app is paid, and a headline implying
|
||||
otherwise is both misleading and against Play's editorial policy.
|
||||
|
||||
### The longer announcement
|
||||
|
||||
> Plain Files 1.0.0 is available for Android 8.0 and later. One purchase, no subscription, no ads,
|
||||
> and no way for the app to send anything off the device — because it does not hold the network
|
||||
> permission at all.
|
||||
|
||||
#### What it does
|
||||
|
||||
**Browsing.** Internal storage, SD cards and USB drives side by side. List or grid, sorted by
|
||||
name — `file2` before `file10` — size, date or type; hidden files on request; breadcrumbs; and a
|
||||
search that either stays in the folder you are in or looks everywhere through an on-device index
|
||||
which says how current its answer is.
|
||||
|
||||
**Moving files around.** Copy, move, rename and delete one item or many. **Copy to…** and **Move
|
||||
to…** open a folder picker — volumes first, then one level at a time — and start the transfer into
|
||||
that folder without taking you out of the one you are standing in. A finished copy says
|
||||
*Copied to folder* and offers **Show**. A name that already exists asks what Paste asks instead of
|
||||
overwriting, and a batch that cannot succeed is refused as a whole rather than performed until it
|
||||
hits the problem. The rows an operation produced carry a dot until you next open that folder, so
|
||||
"what did that 4 GB copy do?" has an answer even when the destination was elsewhere.
|
||||
|
||||
**Undo, and a trash that explains itself.** A delete is a move into a hidden `.plainfiles-trash`
|
||||
folder on the same volume, kept for **24 hours**; the Trash screen states that window, offers
|
||||
**Empty trash** behind one question, and sweeps expired batches when you open it. Rename and move
|
||||
offer undo as well.
|
||||
|
||||
**Renaming a batch.** Select several files and Rename opens a pattern rather than the single-file
|
||||
dialog: one name, the number to start at, and a live preview of the names it will produce. The
|
||||
numbering **continues the folder's own series** — `Holiday (1)` and `Holiday (2)` already there make
|
||||
the next batch start at 3 — because a batch rename that always starts at 1 is useless the second
|
||||
time it is used. A name in the way blocks the batch and is named as it is typed.
|
||||
|
||||
**Finding things.** The duplicate finder compares size, then the head of every candidate, then the
|
||||
survivors in full — never a name or a date. The storage-usage view measures every folder and ranks
|
||||
it largest first, so the folder eating the card is the one at the top.
|
||||
|
||||
**Looking at files.** Image, text, PDF, media and archive viewers, and an APK's own details read
|
||||
without installing it. Thumbnails show the picture, and a **video shows a frame of itself** rather
|
||||
than a film icon. An archive can be read without extracting it first, with entry names checked
|
||||
against zip-slip.
|
||||
|
||||
**Archives.** Create ZIP; extract ZIP, TAR, TAR.GZ, TAR.BZ2, TAR.XZ, GZ, BZ2, XZ and 7z. When
|
||||
compressing, you choose the name and whether to compress at all: for copying a collection to a
|
||||
computer, storing the files as they are takes one pass and is still an ordinary archive.
|
||||
|
||||
**Working with other apps.** Plain Files can receive a share from anywhere and appear in another
|
||||
app's file chooser. Neither costs a permission.
|
||||
|
||||
**Tablets and large windows.** At expanded width the screen splits into two panes: the folder list
|
||||
on the left, and the contents — or a preview and the details — of the folder or file you pick on the
|
||||
right.
|
||||
|
||||
**Shortcuts.** Long-press the app icon for **Images**, **Recent** and **Trash**.
|
||||
|
||||
#### What it deliberately does not do
|
||||
|
||||
- **No cloud storage, network shares, FTP/SFTP/WebDAV or web server.** Every one of those needs
|
||||
`INTERNET`, which is the single permission this app refuses to hold. That is the product, not a
|
||||
gap in the schedule.
|
||||
- **RAR is not supported**, because it needs native code this app does not ship — and it says so
|
||||
rather than failing with a strange error. 7z can be read but not created.
|
||||
- **Extraction does not restore timestamps or permissions**, and archive passwords are not
|
||||
supported.
|
||||
- **Cancelling happens between entries**, so one very large file finishes before a stop is honoured.
|
||||
- **English is the only language so far.** Every string is in `strings.xml`, so a translation is a
|
||||
file rather than a refactor.
|
||||
- **Android TV is specified but not shipped.** Its acceptance needs a TV device pass this machine
|
||||
cannot provide, and a half-tested TV build reviews worse than none.
|
||||
- **A library is not restored across process death.** Images or Recent returns to the root screen
|
||||
after Android kills the process, because a library has no path to persist.
|
||||
|
||||
#### Privacy, in one paragraph
|
||||
|
||||
The app declares no `INTERNET` permission, contains no advertising or analytics SDK, and has no
|
||||
account or sign-in. It keeps three things on the device — an index of files for search, an index of
|
||||
staged deletes for the trash, and the list of starred folders — and none of them leaves it. The
|
||||
permission set and why "All files access" is the one sensitive declaration are in the
|
||||
[store listing](STORE_LISTING.md); the policy text is [PRIVACY.md](../PRIVACY.md).
|
||||
|
||||
#### Price
|
||||
|
||||
**€4.49**, with a **€2.29 launch price for the first 14 days**. One purchase, every feature; no
|
||||
subscription and nothing to buy inside the app. There is no Billing client in the build — the price
|
||||
lives on the listing — which is part of how "nothing leaves the device" stays true of the shipped
|
||||
artefact rather than only of the source.
|
||||
|
||||
#### What comes next
|
||||
|
||||
Android TV is specified and waits for a device; the post-1.0 queue and the deliberate deferrals are
|
||||
in [ROADMAP](ROADMAP.md). What will not change is the permission set: features arrive without a new
|
||||
permission, or they do not arrive.
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
# Roadmap
|
||||
|
||||
File Manager is built against a fixed milestone plan (**M0–M15**). This file is the
|
||||
single source of truth for what is done and what remains. **M16** (Copy to… / Move to…) has since been
|
||||
built and is specified below; the queue records what is left.
|
||||
File Manager is built against a fixed milestone plan. **M0–M20 are built**; **M21** (Android TV) is
|
||||
specified and deliberately waits for a device. This file is the single source of truth for what is
|
||||
done and what remains, and the queue records what is left.
|
||||
|
||||
Legend: ✅ done · 🚧 in progress · ⏳ not started · 💡 candidate (proposed, not scheduled)
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,23 @@
|
||||
1
|
||||
00:00:00,200 --> 00:00:03,400
|
||||
Plain Files needs All files access
|
||||
|
||||
2
|
||||
00:00:03,400 --> 00:00:06,200
|
||||
Android asks to allow all-file management
|
||||
|
||||
3
|
||||
00:00:06,200 --> 00:00:08,250
|
||||
Switching on All files access
|
||||
|
||||
4
|
||||
00:00:08,250 --> 00:00:11,750
|
||||
Granted: browsing internal storage
|
||||
|
||||
5
|
||||
00:00:11,750 --> 00:00:13,750
|
||||
Documents: CSV, MD, TXT, PDF, DOCX
|
||||
|
||||
6
|
||||
00:00:13,750 --> 00:00:19,600
|
||||
Writable too: a new folder is created
|
||||
Reference in New Issue
Block a user