Give a video row a frame of the video, and fix what R8 was hiding

A video row showed a film-strip icon, and a phone holds more video than anything else, so a column of identical icons said nothing about which clip was which. Images and videos now take the same path - EntryIcon hands Coil the file, and for a video the request asks for a frame - which cost exactly one dependency: io.coil-kt.coil3:coil-video, at the same version as the Coil already in use, whose own POM adds nothing that Coil did not already bring (coil-core, kotlin-stdlib, core-ktx). No ExoPlayer, no media3, and no new permission: the decoder is the platform's own MediaMetadataRetriever.

Two details are decisions rather than defaults. The frame is taken one second in, because the first frame of a recording is very often black or the middle of a fade-in, and a thumbnail of a black rectangle is no more useful than the icon it replaced. And a decode that fails - an unsupported codec, or a file only pretending to be a video - settles on the icon rather than leaving an empty square, which the row remembers so it is not retried on every recomposition.

The dependency also nearly shipped a feature that did not work. coil-video registers its decoder through a META-INF/services entry, and in a debug APK that is enough: the file and the class it names are both there, and frames decoded. In the release APK, R8 removes both, because a class mentioned only from a resource looks unused to it. So the feature worked in every build that had been looked at and would have failed silently in the one users install - the worst shape a bug can have, since it needs no code change to "fix" and would have been reported as "video thumbnails don't work" against a build nobody had run. The factory is now registered explicitly against an ImageLoader in FileManagerApplication, which makes it a code reference R8 cannot drop, and the release APK was installed and driven to the Videos library to confirm the frames decode there too. That also closes the gap noted two changes ago, that no ImageLoader was registered at all; its cache sizes are left to Coil deliberately, since they are derived from the device's heap and a number written here would only be a guess.

The same run found a smaller thing that no test could have: opening the search field from inside a library left the scope on "this folder" - the hint read "Search this folder" and a folder toggle sat beside it - while the search in fact ran everywhere, because the view model has always known that a library is not a folder and answered from the index instead. The behaviour was right and the screen was wrong, which is the same shape as the volume-list bug fixed earlier and the same root cause: the switch-on-open asked whether this was the root list rather than whether a folder was open. It now asks for a path, and the scope toggle appears only when there is a folder to choose about, because its two settings did the same thing otherwise. Every other use of isRootView was then checked for the same conflation and none was found. One test was added for the search behaviour; the scope mismatch itself is not testable, being a disagreement between two correct pieces.

324 JVM tests, lint clean. Verified on the phone in both builds: two clips made with ffmpeg (a testsrc2 pattern and SMPTE bars) drew their own patterns as thumbnails, beside a real 4.3 MB video from the device's own storage, and the demo library's three zero-filled stand-in .mp4s fell back to the icon - which exercised the failure path with material that happened to be lying around. The release APK is 2 294 021 bytes and still declares no INTERNET. Not done: the store screenshots still show the home screen from before the library views, and the release build running on a device is in nobody's checklist - which is exactly why the R8 problem survived until now, and is the first thing worth adding.
This commit is contained in:
2026-10-17 01:40:35 +02:00
parent 05a71f12bf
commit 47e48f054e
12 changed files with 226 additions and 36 deletions
+41
View File
@@ -26,6 +26,17 @@ upload needs a **higher `versionCode`** than every previous upload.
### Fixed
- **The search field told the truth everywhere except inside a library.** Opening the field from a
library left the scope on "this folder" — the hint read *Search this folder* and a folder toggle
sat beside it — while the search actually ran everywhere, because the view model has always known
that a library is not a folder and answered from the index instead. So the screen claimed a scope it
was not using, and offered a toggle whose two settings did the same thing. Switching on open now
asks whether there is a **path** rather than whether this is the root list — a library has none
either — and the toggle is shown only when there is a folder to choose about, because a control
that cannot change the outcome implies a choice the user does not have. Found by driving the app on
the phone rather than by reading it: the search itself was right, and it was the screen that was
wrong.
- **Searching needed a volume to be open first, and needed a second tap to be typed into.** Two
papercuts in the feature added above, both found by using it. `BrowserContent` decided between
the volume list and a folder listing on `path == null` alone, so on the "Storage" screen the
@@ -146,6 +157,36 @@ upload needs a **higher `versionCode`** than every previous upload.
### Added
- **A video shows a frame of itself instead of a film-strip icon.** A phone holds more video than
anything else, and a column of identical icons says nothing about which clip is which. Images and
videos now take the same path — `EntryIcon` hands Coil the file, and for a video the request asks
for a frame — which cost exactly one dependency: `io.coil-kt.coil3:coil-video`, at the same version
as the Coil already in use, whose own POM adds nothing that Coil did not already bring (coil-core,
kotlin-stdlib, core-ktx). No ExoPlayer and no media3, and **no new permission**: the decoder is the
platform's own `MediaMetadataRetriever`. The APK audit still shows the same permissions, with no
`READ_MEDIA_VIDEO` among them.
The decoder is registered **explicitly** in `FileManagerApplication`, which is where this nearly
went wrong. `coil-video` ships a `META-INF/services` entry that registers it on its own, and in a
debug build that works: both the entry and the class it names are in the APK. In the **release**
build R8 removes both, because a class mentioned only from a resource looks unused — so the feature
worked in every build that had been looked at and would have failed silently in the one users
install. Registering the factory against an `ImageLoader` makes it a code reference R8 cannot drop,
and the release APK was then re-run on the phone to confirm frames decode there too.
Two details are decisions rather than defaults. The frame is taken **one second in**, because the
first frame of a recording is very often black or the middle of a fade-in, and a thumbnail of a
black rectangle is no more useful than the icon it replaced. And a decode that **fails** — an
unsupported codec, or a file only pretending to be a video — settles on the icon rather than
leaving an empty square, which the row remembers so it is not retried on every recomposition.
Verified on the phone, which is the only place this could be verified: two clips made for the
purpose (`ffmpeg`, a testsrc2 pattern and SMPTE bars) were pushed into the demo folder first, and
both drew their own pattern as a thumbnail, next to a real 4.3 MB video from the device's own
storage. The demo library's three stand-in `.mp4`s are zero-filled and cannot decode at all, which
turned out to be the cheapest possible test of the other half: all three fell back to the icon. The
same four rows were then re-checked on the **release** APK, which is where the difference shows.
- **Libraries: the home screen now opens on what is on the device, not only on where it is.** The
volume list was the first thing a new user saw, so "where are my photos?" started with knowing
which folder they were in. The root screen now carries a **Libraries** section — Images, Videos,