Remember what is in the trash, and add a screen to restore from it

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.
This commit is contained in:
2026-10-15 10:25:37 +02:00
parent d1bc61e717
commit 667a85f80c
16 changed files with 758 additions and 22 deletions
+21
View File
@@ -38,6 +38,27 @@ upload needs a **higher `versionCode`** than every previous upload.
### Added
- **A trash you can actually recover from, after the fact.** The staged-delete work left a
hole: the undo handle lived in memory, so a delete from a previous session was unreachable
even though the files were sitting right there 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 any confidence. There is now a **Trash** entry in the overflow menu that lists what is
still there and puts a whole batch back.
Backing it is `data/`'s first persistence: a SQLite index of staged path, original path,
name, type, size and timestamp. Deliberately SQLite rather than Room — an annotation
processor would be a large addition to this build for one table and three queries — and
deliberately behind a `TrashIndex` port, so `domain/Trash` stays free of a `Context` and
the tests drive it with a map while the real one is tested against real SQLite.
The index is an **index, not the truth**: the staged files are the truth, and the two can
disagree — a user can delete `.plainfiles-trash` themselves, or pull the card. So it is
reconciled both ways. `restorableFrom` drops entries whose file has gone, `purgeExpired`
forgets entries whose folder has gone, and a row is written *after* the move, so a crash
between the two leaves an unindexed folder — harmless, and swept — rather than an entry
that could never restore anything. A `TrashScreen` row is one *batch*, not one file,
because a batch is what the user actually did and what the index records.
- **The app can receive a share, and act as a file picker.** The manifest had nothing but a
`LAUNCHER` filter, so no other app could hand Plain Files anything at all. It now registers
for `ACTION_SEND`/`SEND_MULTIPLE` — a bar appears over the browser, you navigate to the