Four milestones from the survey's remaining gaps
M17 - the mark of a change. A dot on the rows an operation produced, spent by the next read of that folder: in memory, per folder, no timer and no watcher. Copy, move, compress, extract, rename (single and batch) and new folders all mark what they made, so "what did that 4 GB copy do?" has an answer even when the copy went to a folder the user was not looking at. The dot is drawn by MarkOfChange in the list and the grid, with a content description, and `changed` is set by the same state update that announces the folder. M18 - store-only archives. Compressing now asks for the name and whether to compress, because the survey's most enthusiastic review is a bulk copy to a computer and that file is better stored than squeezed. Deflater.NO_COMPRESSION keeps it one pass; a STORED entry would need size and CRC before the header, i.e. reading every file twice. M19 - the trash says what it does. The screen states the 24-hour window, offers Empty trash behind one question, and opening the trash sweeps what has expired - the window used to run only after the next delete, so an old batch could stay on the card indefinitely. Trash.emptyAll leaves anything in that folder that is not one of ours alone. M20 - launcher shortcuts. Images, Recent and Trash as static shortcuts carrying a route extra; core/ShortcutRoute answers null for anything it does not know, so an old shortcut cannot crash a new build. Favourites was dropped from the plan: it is a section of the home screen, not a view, so a shortcut to it would promise something that may not be there. M21 (Android TV) is specified in the roadmap and deliberately not started: its acceptance needs a TV device pass this machine cannot provide. 18 new tests (427 in total). Detekt's baseline was regenerated after two signatures gained a parameter; the diff is three findings with new signatures and their KDoc moved above the annotations so the baseline stays readable. Verified by the gate only so far - the device pass for these four is next, and the terminal here has been losing command output for the last hour.
This commit is contained in:
+114
@@ -26,6 +26,12 @@ Legend: ✅ done · 🚧 in progress · ⏳ not started · 💡 candidate (propo
|
||||
| M13 | Video thumbnails | ✅ | A video row shows **a frame of the video** instead of a film icon: images and videos take the same path through Coil, with the video request asking for a frame a second in. One dependency — `coil-video` at the Coil version already pinned, whose POM adds only coil-core/stdlib/core-ktx — the platform's own `MediaMetadataRetriever` behind it, **no new permission** (no `READ_MEDIA_VIDEO`), and the decoder registered **explicitly** against an `ImageLoader` in `FileManagerApplication` — because R8 strips `coil-video`'s ServiceLoader entry from a release build, which would otherwise have left frames decoding in debug and quietly falling back to icons for real users. A decode that fails settles on the icon rather than an empty square. Verified on the phone against two purpose-made clips and a real video of the device's own, with the demo library's stand-ins falling back as intended, and **re-verified on the release APK**. **324 JVM tests** |
|
||||
| M14 | Batch rename with sequence continuation | ✅ | Selecting more than one file and choosing Rename opens a **pattern** dialog: a name, a start number, and a live preview of the names it will produce. `core/BatchRename` plans the whole batch before a single file moves and returns one of three answers — **Ready**, **Blocked** (naming the target the folder already holds) or **NothingToDo** — with the deliberate absence of a fourth: a batch that renamed nine of twelve and stopped would leave the user to work out which nine. The numbering **continues the folder's own series** `nextNumber` from the highest `base (n)` it finds, which is the market survey's most-helpful complaint answered by name; the extension is kept per file, the series is counted across extensions, and continuing beats filling the first gap. `SelectionBar` now offers Rename to any selection (one file keeps the name dialog, several get the pattern one), and the batch's renames are **one** `Undo.Relocate` step. The same commit hardens the single rename: `core/FileNames.isUsableName` refuses a name that is not one path segment, because `notes/final.txt` was being resolved as a path and **moved** the file instead of renaming it. **350 JVM tests** (26 added) |
|
||||
| M15 | Favourites | ✅ | A star in the app bar of any folder, and a **Favourites** section on the home screen above the libraries — absent while empty, so a first-time user still meets the libraries first. `data/BookmarkStore` + `SqliteBookmarkStore` keep one path per row in their own database: a **reference, never a copy**, so the list costs one query and no filesystem work. `BrowserUiState.Bookmark` carries an `exists` flag, and a folder that has gone is **kept and marked** rather than deleted — an unmounted card and a deleted folder are the same thing from here — with opening re-checked at that moment so the answer is a message instead of an empty listing. The row's filled star is the removal (the mark that says "favourite" is the thing you tap to stop it being one), there is no star in a library or on the root screen, and no `Undo` for a bookmark because that type is "the last change to the filesystem" and a favourite is not one. **371 JVM tests** (16 added) |
|
||||
| M16 | Copy to… / Move to… | ✅ | A destination picker behind the selection bar's overflow — volumes, then folders one level at a time, with Up and the trail above them — that copies or moves **without leaving the folder you are in**. The selection survives a copy, so a second destination is two taps; a move clears it. Conflicts ask what a paste asks, naming the picked folder; a copy answers "Copied to *folder*" with **Show**. Detailed below |
|
||||
| M17 | The mark of a change | ✅ | A dot on the rows an operation produced, spent by the next read of that folder. In memory, per folder: no timer, no watcher, no badge. `BrowserUiState.changed` + `markChanged` on every operation that creates something; the dot is drawn by `MarkOfChange` in both list and grid, with a content description TalkBack reads. **7 tests** (4 view model, 3 Compose) |
|
||||
| M18 | Store-only archives | ✅ | Compressing asks two things: the name, and whether to compress. Off stores the files as they are — `Deflater.NO_COMPRESSION`, one pass — which is what a bulk copy off the phone actually wants. A `STORED` entry would need each file's size and CRC before its header, i.e. reading everything twice. **4 tests** |
|
||||
| M19 | The trash says what it does | ✅ | The trash states its window ("Deleted files are kept for 24 hours") and offers **Empty trash** behind one confirmation; opening the trash sweeps expired batches, so the 24-hour retention no longer waits for the next delete to run. **4 tests** |
|
||||
| M20 | Launcher shortcuts | ✅ | Images, Recent and Trash as static shortcuts — three intents with a route extra, `core/ShortcutRoute` answering `null` for anything it does not know. Favourites was dropped from the plan: it is a section of the home screen, not a view. **3 tests** |
|
||||
| M21 | Android TV | ⏳ | Specified below and deliberately not started: its acceptance needs a TV device pass this machine cannot provide. |
|
||||
|
||||
## Key decisions
|
||||
|
||||
@@ -69,6 +75,10 @@ scaling numbers (which turned into progressive listing), Copy to… / Move to…
|
||||
left is the baseline profile's *measurement*, which needs a physical API 33+ device and cannot be finished
|
||||
on this machine; everything else in the list says what it produced.
|
||||
|
||||
**M17–M20** come from the same survey's remaining gaps and are implemented (the change mark, store-only
|
||||
archives, a trash that says what it does, launcher shortcuts). **M21** (Android TV) is specified and
|
||||
deliberately not started: its acceptance needs a TV device pass this machine cannot provide.
|
||||
|
||||
1. **Baseline profile** — **generated and committed**: `app/src/main/baseline-prof.txt` is bundled in the
|
||||
release APK, written by the `:baselineprofile` module. What remains is its *measurement*: collection
|
||||
needs a device with API 33+, and the phone is Android 12, while the API 35 emulator here is too noisy to
|
||||
@@ -131,3 +141,107 @@ destination picker.
|
||||
|
||||
**Not in scope**: drag and drop, a two-pane phone layout, remembering recent destinations.
|
||||
|
||||
### M17 — The mark of a change
|
||||
|
||||
**What**: the rows an operation created carry a dot until the folder is read again, in both the list and the
|
||||
grid.
|
||||
|
||||
**Why this shape**: the survey's 3.6.5 ("temporarily highlight new files with a dot after file operations")
|
||||
answers an obvious question — *what did that 4 GB copy just do?* — and it got sharper with M16, because a copy
|
||||
can now land in a folder the user is not looking at. The dot is a **set of paths the view model marks when an
|
||||
operation lands**, not a timestamp on every row: no timer, no background job, and "temporarily" means "until
|
||||
you see the folder again". The marks live in memory, so a restart forgets them — the same lifetime as the
|
||||
clipboard, and for the same reason.
|
||||
|
||||
**Acceptance criteria**
|
||||
|
||||
- Copying, moving, compressing, extracting, renaming and creating a folder or file mark what they produced.
|
||||
- The dot is drawn in the list and in the grid and carries a content description, so TalkBack says it and a
|
||||
Compose test can find it.
|
||||
- Opening the folder again clears its marks; opening a *different* folder does not disturb them.
|
||||
- Tests: a copy into the current folder marks the copy and no untouched row; a copy into another folder marks
|
||||
it there; re-opening clears.
|
||||
|
||||
**Not in scope**: catching files that appear while the folder is open (that needs a watcher), a notification
|
||||
or a badge on the home screen.
|
||||
|
||||
### M18 — Store-only archives
|
||||
|
||||
**What**: compressing asks two things — the name, and whether to compress — because the survey's most
|
||||
enthusiastic review is a 150 GB push to a PC, and that push is faster as one file that was cheap to build.
|
||||
|
||||
**Why this shape**: we refuse `INTERNET`, so their FTP-server answer is out; what is left is to make the file
|
||||
that leaves as cheap as possible. `Deflater.NO_COMPRESSION` does that in one pass. A `STORED` zip entry would
|
||||
be more literal and needs each file's size and CRC *before* the entry header is written, i.e. reading every
|
||||
file twice — slower than the deflate it avoids.
|
||||
|
||||
**Acceptance criteria**
|
||||
|
||||
- `ArchiveEngine.createZip` takes the choice; the archive is a valid zip either way and extracts to identical
|
||||
bytes.
|
||||
- The dialog opens on "compress contents" and does not remember the last answer.
|
||||
- Tests: both variants round-trip; the uncompressed one is measurably larger; the view model passes the flag.
|
||||
|
||||
**Not in scope**: a remembered preference, an estimated time, choosing a compression *level*.
|
||||
|
||||
### M19 — The trash says what it does
|
||||
|
||||
**What**: the trash screen states the window (24 hours) and offers **Empty trash**; the expiry sweep runs when
|
||||
the trash is opened, not only after the next delete.
|
||||
|
||||
**Why this shape**: the retention already exists (`Trash.RETENTION_HOURS`) and is invisible, and
|
||||
`purgeExpired` runs *after staging* — so a user who deletes once and never again keeps a day-old batch
|
||||
forever. Those are the two halves of "why is my storage full" that the app itself can answer. Making the
|
||||
window configurable would need a settings surface this app does not have; saying what it is costs nothing.
|
||||
|
||||
**Acceptance criteria**
|
||||
|
||||
- Opening the trash sweeps expired batches first, and the screen says how long items are kept.
|
||||
- **Empty trash** asks once, then deletes every batch for good — it is the one destructive action in the app
|
||||
with no undo, because that is what emptying means.
|
||||
- Tests: an expired batch is gone after opening, a fresh one survives, emptying removes all and leaves the
|
||||
screen saying it is empty.
|
||||
|
||||
**Not in scope**: a per-item permanent delete, a configurable window, a size cap.
|
||||
|
||||
### M20 — Launcher shortcuts
|
||||
|
||||
**What**: long-press the app icon → **Images**, **Recent**, **Trash**.
|
||||
|
||||
**Why this shape**: static shortcuts need no permission and no database — three intents with an extra, mapped
|
||||
to views that are otherwise two or three taps deep. It is the cheapest route to the screens the survey says
|
||||
people install the app for.
|
||||
|
||||
**Recent, not Favourites** — the one change from the proposal above. Favourites is a *section of the home
|
||||
screen* rather than a view of its own, so a shortcut to it would open the home screen and promise a section
|
||||
that is absent when nothing is starred. Recent is a library with its own name and its own state, so the
|
||||
shortcut opens exactly what it says.
|
||||
|
||||
**Acceptance criteria**
|
||||
|
||||
- Three shortcuts in `res/xml/shortcuts.xml`; each opens the app directly at that view, and back from there
|
||||
behaves like any other entry.
|
||||
- An unknown or absent extra is ignored, so an old shortcut cannot crash a new build.
|
||||
- Tests: the route parser maps the three names and returns `null` for anything else; each route opens the
|
||||
right view.
|
||||
|
||||
**Not in scope**: dynamic shortcuts built from favourites, deep links to a path.
|
||||
|
||||
### M21 — Android TV (specified, not started)
|
||||
|
||||
**What**: a leanback-capable build — banner, launcher entry, and the whole browser and every dialog reachable
|
||||
with a d-pad.
|
||||
|
||||
**Why this shape, and why it is not done**: Android TV is the only genuinely *new market* the survey names that
|
||||
costs no permission, and it is also the one milestone whose acceptance cannot be met on this machine — there
|
||||
is no TV AVD here and no KVM to run one at a usable speed. A half-tested TV build reviews worse than no TV
|
||||
build, which is the survey's own lesson about the competitor's timeouts, so the plan is written down and the
|
||||
work waits for a device.
|
||||
|
||||
**Acceptance criteria (when it starts)**: `LEANBACK_LAUNCHER` intent filter and a banner; focus traversal
|
||||
through the browser, the selection bar, the transfer picker and every dialog; no touch-only affordance (long
|
||||
press and double tap need a d-pad equivalent); a TV emulator pass recorded in [EDGE_CASE_TESTING](EDGE_CASE_TESTING.md).
|
||||
|
||||
**Not in scope**: TV-specific layouts, gamepad support, a screensaver.
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user