Preview files inside an archive, and search every volume from an index

The archive preview could say what a zip held but not show any of it, and the search could only answer "is it here?" for the folder tree the user was standing in. Both are now answered.

ArchiveEngine gains a single-entry reader: a ZIP the backend can seek jumps straight at the entry's data through the central directory, 7z scans its index, a streaming container decompresses past whatever is in front of the entry, and a compression wrapper has exactly one entry whose name is derived from the archive's own. extractEntry is rewritten on top of it, so there is one implementation of "reach this entry" rather than two that could disagree, and the now-dead extractFromZip and only= parameters are removed.

A tap on a row now previews and the row's action extracts, which is what a row means everywhere else in this app. Text and markup are read into memory, so the commonest question - what does this readme say - writes nothing at all and works in both storage modes. Everything else needs a file a decoder can open, so the entry is copied into the app's own cache, one folder per preview and under its own name so the viewer's title bar does not read copy-0.png, and deleted when the preview closes with a non-recursive delete. The copy is a real path, so image, PDF and media previews work even in the SAF fallback, where the browsed files cannot be previewed at all. A copy has no file to save back to, so the text editor shows no Save and markup offers no editor; closing returns to the archive rather than out of it.

SearchIndex is a small port over SQLite beside the trash index, inheriting that one's doctrine that an index is not the truth: a reader drops rows whose file has gone and forgets them, freshness is reported rather than assumed, and a volume's rows are replaced wholesale so an interrupted pass leaves the previous listing rather than half of this one. Timestamps are written only when a pass finishes, so a walk cut short by a new keystroke reads as older than it is, which is the safe direction to be wrong in. Names are matched with NameMatcher's semantics translated into LIKE - substring, anchored glob, and a literal % or _ escaped - because a search that quietly widens what was typed is what an index does badly.

The search field gains a scope toggle, and every search everywhere carries the age of the index and a way to rebuild it, because a search that silently omits what arrived since the last walk is worse than one that spends a minute being sure. Rows are written in batches of 500 and the counter reported every 100, since a row reading "0 files" for the whole first second reads as nothing happening. Deliberately not a foreground service: a pass runs while the screen is up and stops when it is not.

Verified on the API 35 phone rather than inferred: a .txt and a .png inside a real zip, both previewed from the archive list with their own names, the copy gone on close and the archive list restored behind it; then the first search building the index with a full walk, and a search after an app restart answering instantly and finding a file in Download/cline-zip while browsing Internal storage. Watching it caught two lies, both fixed here: the empty state claimed "Nothing matches" while the index was still being written, and two progress bars stacked on each other.

268 JVM tests, lint clean, release APK builds and installs. The store listing gains the archive preview and the scoped search and is re-measured at 2730/4000.
This commit is contained in:
2026-10-16 14:33:18 +02:00
parent cd0fdff31b
commit ae44cdb994
35 changed files with 4519 additions and 148 deletions
+35 -2
View File
@@ -88,7 +88,7 @@ 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
• Breadcrumb navigation, and a search that can look in one folder or everywhere
• Hidden files on request
ANY SIZE OF SCREEN
@@ -100,7 +100,8 @@ ANY SIZE OF SCREEN
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 a whole folder tree, not just the folder you are looking at
• 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
@@ -115,6 +116,8 @@ FIND OUT WHERE THE SPACE WENT
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
@@ -148,6 +151,36 @@ the app's description** when it declares `MANAGE_EXTERNAL_STORAGE`. The "PERMISS
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