The staged-delete work left a hole. The undo handle lived in BrowserUiState, so a delete from an earlier session was unreachable even though the files were sitting on the volume; and because nothing on disk recorded which folder an item came from, not even the user could put one back by hand with confidence. Adding a Trash screen alone would not have fixed it: the recovery data did not survive the process. So the origin is now written at delete time. data/ gains its first persistence: a SQLite index (staged path, original path, name, type, size, timestamp). SQLite rather than Room because an annotation processor is a large addition to this build for one table and three queries, and behind a TrashIndex port so domain/Trash keeps no Context while the tests drive it with a map and the real one is still tested against real SQLite. New Trash entry in the overflow menu lists batches and restores a whole one. The index is an index, not the truth: the staged files are the truth, and the two can disagree if a user empties the folder by hand or pulls a card. Reconciled both ways - restorableFrom drops entries whose file has gone, purgeExpired forgets entries whose folder has gone and sweeps folders no entry mentions - and rows are written after the move, so a crash leaves an unindexed folder (harmless, swept) rather than an entry that could never restore. Verified on hardware: delete, force-stop, relaunch, restore. Also corrects the docs where they claimed input swipe x y x y 900 is delivered as a plain tap on MIUI. It does open the selection bar - it was driven that way - but it has also landed as a tap, so it is flaky rather than impossible.
4.5 KiB
On-device testing
What has actually been run on hardware, what it found, and what is still unverified. Keep this file honest: an untested claim is a bug waiting to be reported.
Devices used
| Device | Android | Notes |
|---|---|---|
Xiaomi M2101K7AG (Redmi, sunny), MIUI V140 |
12 (API 31), arm64-v8a | A real phone with a real 55 GB library. Auto-rotate was on, so runs happened in both orientations. |
AOSP phone AVD |
12 (API 31), x86_64 | Software emulation — the host exposes no /dev/kvm. Used from M0–M6. |
Connecting a phone to this dev container needs one fix: the USB node is created
root:plugdev 0660, and the container's user is not in plugdev, so adb cannot open it.
An udev rule widening the mode for Android vendors fixes it
(/etc/udev/rules.d/51-android.rules), after which adb devices sees the phone.
Verified on the real device
| What | Result |
|---|---|
| First run, no permission granted | The "Choose a folder" prompt appears with both routes offered: Grant full access and Choose folder. Correct — the app has no source yet, so the SAF fallback is the honest state. |
| Granting "All files access" while the app is alive | The app picks it up on resume and switches from the prompt to the roots view, showing "Internal storage · 50.0 GB free" (matching df). The lifecycle refresh works. |
| Browsing real storage | Real folders listed alphabetically with correct icons and breadcrumbs ("Internal storage / Download"). Titles render "Internal storage", not the raw volume name. |
| Text editor, read | Tapping a.txt opened the editor showing its real content. |
| Text editor, write | Editing and tapping Save persisted to the device — verified independently with adb shell cat. |
| Storage usage | DCIM 2.7 GB / 668 files, Android 1.2 GB / 5925 files, Music 1.2 GB / 43 files, Pictures 866.4 MB / 5849 files, … sorted largest first with per-row share bars and correct plurals. |
| Revoking "All files access" mid-use | Returned to the folder prompt without crashing; the app kept running. Graceful degradation confirmed. |
| Landscape | The whole UI adapts (the phone auto-rotated mid-session): app bar, list, breadcrumbs and dialogs all laid out correctly. |
BACK from the roots view |
Exits the app, which is the expected Android behaviour. |
Bugs this session found — both fixed
-
The first-run copy was wrong. The prompt read "Full access was declined, so pick a folder…" on a device where the user had never declined anything. Reworded to "Without full access you can still work inside one folder you choose here.", which is true whether the user declined or simply has not granted yet. Re-verified on device.
-
The storage-usage screen took ~45–60 seconds to show anything. Measuring the root of a real 55 GB volume blocked on the whole walk with only a spinner for company, which reads as a hang. Two changes:
UsageAnalysisnow passes theFileEntrydown the recursion instead of re-stating every node (the parent's listing already says whether each child is a folder), so the walk costs one listing per directory and nothing more.- Rows are published as they are measured through an
onProgresscallback, and the ViewModel appends them, re-sorting as it goes, so the biggest folders appear while the rest of the tree is still being walked.
The UI never froze — the progress bar kept animating throughout, which confirms the I/O stays off the main thread — but "slow and silent" is still a defect.
Not verified, and why
- Long-press multi-select via adb: it works, but not every time.
input swipe x y x y 900does open the selection action bar on this MIUI build — it was driven that way to reach Delete, Rename and Compress — but the same gesture has also landed as a plain tap, which opens the row instead of selecting it. So the bar is reachable from the host, and yet compress/extract from the UI is still worth a manual pass rather than trusted to a scripted gesture. The archive engine itself is covered by end-to-end JVM tests, including a zip-slip archive. - The SAF folder-picker round trip. Driving the system picker and granting a tree was
not attempted; the code path is unit-tested against a stand-in
DocumentsProvider. - Image viewer and thumbnails. Verified on the AVD in M4/M5; not repeated on the phone.
- The foreground-service notification during a long copy. Started in M3 and unit-tested; not observed on hardware.
- Archive creation/extraction against real files. Only exercised in-memory.