Browse the device by what a file is, and find the ones that hold the same bytes

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.
This commit is contained in:
2026-10-17 01:15:41 +02:00
parent 90806fa3cc
commit 05a71f12bf
25 changed files with 2608 additions and 73 deletions
+50
View File
@@ -115,6 +115,56 @@ The lesson is the one the phone already taught twice: a green test suite says th
what the tests describe, not that the app does anything on a device. Both times the gap was a
user-visible promise the code no longer kept — once in a dialog, once in a permission.
## The library and duplicate session (M11)
Run **2026-10-17** on the same **Xiaomi M2101K7AG** (Android 12, MIUI), with the debug build
(`de.softwareapp_hb.filemanager.debug`, installed with `adb install`). The device was **cold** for
the index: the app had never run on it, so "the first tap on a library starts a walk" was tested as
a first run rather than as a re-run. First-run permissions were granted from the host
(`appops set … MANAGE_EXTERNAL_STORAGE allow`, `pm grant … READ_EXTERNAL_STORAGE`) rather than
through the prompt, because the prompt was already covered in the M7 session.
| What | Result |
|---|---|
| Home screen, before any index exists | **Correct, and the important part is what is absent.** The **Libraries** section renders above the volume with all six rows and their icons, and **no counts at all** — not a row of zeroes. That is the design: an index that has never run cannot say "you have no photos" |
| Opening a library on a cold index | **Correct.** Title `Images`, no breadcrumbs, no new-folder action, an empty listing, and the honest status row: **"Indexing 300 files…"** with a progress bar and Rebuild beside it. The answer came first and the walk ran behind it |
| The walk finishing | **Correct.** The status row became **"Index updated 2026-10-17 01:01"**, and the library filled with real files and real thumbnails |
| Counts after the walk | **Correct.** Images **6238**, Videos 169, Audio 196, Documents 160, Archives 5, and **Recent uncounted** — the one library that is a window rather than a set, which is what the code says it should be |
| Tapping a file inside a library | **Correct.** A tap opened the image viewer with that file's name in its bar, and `BACK` returned to the library listing with the index row and the scroll position intact |
| The duplicate scan | **Correct.** Tapping *Find duplicates* in the overflow ran the scan and produced real groups: `DOC-20251121-WA0001.zip` and `Schwarzhofen.zip` at 10.5 MB each (identical content in `Android/media/…` and `Download/`), a PDF duplicated at 5.3 MB, another PDF present **three** times at 761.3 KB. Group headers read "2 copies · 10.5 MB" above "10.5 MB could be freed", each row carried its **folder path**, and the groups were ordered by reclaimable space |
| Ticking a copy | **Correct.** The row highlighted and the bar action appeared as **"Delete 1 copy"** — the plural resource agrees with the count |
| Closing the scan | **Correct.** `X` returned to the browser with nothing changed |
| Crashes | **None.** `logcat -b crash` contained no entry for the package across the whole session |
### Two findings worth keeping
- **The library views are device-wide, so they cannot be photographed on a real phone.** The
`Images` list showed the owner's own files — names and thumbnails included — because a library
is not a folder and does not respect one. The M10 rule ("never shoot over a real device's
storage") now applies to the *home screen too*, since its counts expose how many files of each
kind the device holds. See [STORE_LISTING](STORE_LISTING.md).
- **MIUI blocks `adb install` twice over, and both gates are on the device, not in the tooling.**
Developer options needs *Installieren über USB* enabled, and each install then waits for a
confirmation dialog (`com.miui.securitycenter/…AdbInstallActivity`) that times out on its own —
which is what the bare `INSTALL_FAILED_USER_RESTRICTED: Install canceled by user` means. Worth
knowing before blaming a build. A second, cheaper lesson came from this session: **do not run
`assembleDebug` and `adb install` in the same parallel batch.** They raced, the install took the
previous APK, and the new menu entry was missing while the host APK contained it — a confusing
twenty minutes caused entirely by tooling, not by code.
### Deliberately not done on this device
- **The duplicate *delete* was never pressed.** The scan is read-only and safe to run on someone
else's phone; deleting files is not, and the delete itself is the ordinary, already-covered
delete path (trash + undo + the platform dialog where it applies). The selection UI was driven
to the point of the action label and then left untouched.
- **The walk was not timed end to end** on a large volume. It finished inside the 20 seconds this
session waited, but that says nothing about a phone with a far larger library; the M7 note about
a 45–60 s usage walk is the honest precedent.
- **The 7-inch and 10-inch tablet layouts with the new section** were not re-shot or re-checked:
the AVDs have no `/dev/kvm` here, and the two-pane root list is where the libraries now appear
first.
## Not verified, and why
- **Long-press multi-select via adb: it works, but not every time.** `input swipe x y x y 900`