1ec71381a0
Two fixes from driving Copy to… and Move to… on the phone. The dialog was nested inside the conflict-prompt block, so it could only appear when a conflict was already on screen - which is never. Nothing in the unit tests could see that: they test the view model, and the view model was right. And the picker named internal storage by the platform's own name for it, 0: as the volume row, and in the trail above it (0 / AADst). The roots screen has always shown Internal storage there; the picker now follows the same rule, through a label resource on PickerFolder rather than a second copy of the string logic. A test pins the label on both the row and the crumb. On the device: copy leaves the source and the selection in place, move empties the source, clears the selection and says Moved 1 item. Recorded in EDGE_CASE_TESTING.md.
330 lines
28 KiB
Markdown
330 lines
28 KiB
Markdown
# 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. |
|
||
| AOSP `phone35` AVD | **15 (API 35)**, x86_64 | Software emulation, same reason. Added for the Android 13+ surface the phone cannot reach: the permission model, mandatory edge-to-edge, predictive back. |
|
||
|
||
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
|
||
|
||
1. **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.
|
||
2. **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:
|
||
- `UsageAnalysis` now passes the `FileEntry` down the recursion instead of re-`stat`ing
|
||
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 `onProgress` callback, 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.
|
||
|
||
## The Android 15 (API 35) session
|
||
|
||
The phone is on Android 12, and the checks that matter most for a release are precisely the
|
||
ones Android 12 cannot answer. An API 35 AVD was installed for that reason
|
||
(`sdkmanager --install 'system-images;android-35;default;x86_64'`, AVD on the `pixel_6`
|
||
profile, 1080×2400 @ 420dpi — the phone's geometry), rather than waiting for a public test
|
||
track to report back.
|
||
|
||
| What | Result |
|
||
|---|---|
|
||
| Cold start and browse | Fine. Storage root, then "Internal storage / Download", with the real file and size shown. |
|
||
| Edge-to-edge, which API 35 enforces | **Correct.** The status and navigation bars draw over the app background and the app bar sits *below* the status bar, so the `Scaffold` insets are being applied. This was the single most likely Android 15 regression. |
|
||
| Predictive back | **Correct.** `BACK` from inside a folder returns to the root and the activity stays resumed, so the `BackHandler` composed after the overlay branch works under the `OnBackInvoked` dispatcher too. |
|
||
| Permission request | **Broke, fixed.** On the shipped build Android 13+ was asked for nothing at all; see below. |
|
||
| The progress notification | **Fixed, then verified.** After the fix, a real 220 MB copy posts notification `1001` on channel `file-operations` with `ONGOING_EVENT\|NO_CLEAR\|FOREGROUND_SERVICE` and `category=progress`, then removes it and stops the service when the copy finishes. Before the fix the app's notification importance was `NONE`. |
|
||
| Copy to completion | The staged copy landed at `/sdcard/bigfile.bin`, byte-count identical, via the overflow menu's "Paste here". |
|
||
| Compress and extract from the UI | **Found a bug, fixed.** Long-pressing a folder and choosing "Compress to ZIP" produced a `tocompress.zip` that the host's `unzip` read back with the right relative paths and contents. Selecting that `.zip` then exposed the selection bar clipping its own actions — see below. |
|
||
| One archive of **every** supported format | **Found a bug, fixed.** A real `.zip`, `.7z`, `.tar`, `.tar.gz`, `.tar.bz2`, `.txt.xz`, `.txt.gz` and `.txt.bz2` were built on the host and all eight extracted through the UI. Each landed with the right entry names and byte-for-byte content — except `.tar.bz2`, which produced a bare `.tar`, recorded below. |
|
||
| Long-press from adb | `input swipe x y x y 700` on a single point *did* open the selection bar and reach Copy. The same gesture has been flaky on the MIUI phone, so it still deserves a manual pass. |
|
||
|
||
### The bugs this session found
|
||
|
||
**On Android 13+ the app asked for no runtime permission at all, so the progress notification
|
||
was dead.** `StorageAccessPolicy.missingPermissions` returned an empty list from API 33 —
|
||
deliberately, with a test asserting it — on the reasoning that "all files access" is a special
|
||
permission and browsing therefore needs no runtime grant. True as far as browsing goes, but the
|
||
early return also dropped `POST_NOTIFICATIONS`, and nothing else requested it: the manifest
|
||
declared it and the service posted it, so it looked implemented, while `dumpsys` showed
|
||
`granted=false` and `importance=NONE`. No crash and all 203 tests green — the feature was
|
||
simply invisible, and unrecoverable, because Android will not let a user grant in Settings a
|
||
permission the app has never asked for. The four `READ_MEDIA_*` declarations were in the same
|
||
position, and the manifest comment claiming they backed thumbnails on Android 13+ was wrong:
|
||
Coil loads thumbnails and the viewers from a real filesystem path. Fixed by removing those
|
||
declarations and requesting the notification grant alongside the storage grant; a re-run on the
|
||
same AVD shows the dialog, `importance` `NONE → DEFAULT`, and the notification lifecycle above.
|
||
Details in [CHANGELOG](../CHANGELOG.md).
|
||
|
||
**The selection bar drew three of its own actions off the screen.** With a single archive selected
|
||
the bar holds ten icon buttons, which at the 48 dp minimum touch size need ~480 dp against the
|
||
411 dp this window has. `BottomAppBar` is a plain `Row` and does not scroll, so the surplus was
|
||
laid out past the right edge and clipped: the row measured Copy at x=167 through Compress at
|
||
923–980 — Compress already squeezed from 63 px to 57 — and then simply stopped, leaving "Extract
|
||
here", "Select all" and "Clear selection" unreachable. The same selection in a 2400 px landscape
|
||
window showed all ten, which isolates it to width: nothing was wrong with the logic,
|
||
`ArchiveFormats.isExtractable` had judged the `.zip` correctly all along, and the app's own
|
||
archive feature merely looked absent on the shape of phone most people hold. Wrapping the actions
|
||
in a scrollable row makes every one of them reachable again, verified by swiping the bar in
|
||
portrait and seeing all ten appear. The row still *scrolls* on a 411 dp phone rather than showing
|
||
everything at once, which is honest but not ideal — moving the two least-used actions (Select all,
|
||
Clear selection) into the overflow menu would be the better fix, and is left as deliberate
|
||
follow-up rather than bundled in here.
|
||
|
||
**`.tar.bz2` and `.tar.xz` only half-extracted.** `ArchiveFormats.formatOf` tested `.tar.gz`/`.tgz`
|
||
first, as a combined format, but then matched `.bz2` and `.xz` *before* it ever considered the
|
||
`.tar` in the middle of the name — so a `.tar.gz` unpacked to its files while a `.tar.bz2` was
|
||
read as one compressed file and yielded a `.tar`. Driving the sweep made it exact: extracting
|
||
`z4.tar.bz2` left `z4/z4.tar`, raw `ustar` headers and all, beside `z3.tar.gz`'s fully unpacked
|
||
`z3/a.txt` and `z3/sub/b.txt`. `TAR_BZIP2` and `TAR_XZ` now exist alongside `TAR_GZIP`, and
|
||
detection consumes the whole tar suffix before the bare compressor. The suite could not have
|
||
caught this: only ZIP, GZIP, XZ and 7z had round-trip tests, so neither plain `TAR` nor
|
||
`TAR_GZIP` did, and there was nothing for the asymmetry to contradict. All four have one now.
|
||
|
||
The lesson is the one the phone already taught twice: a green test suite says the code does
|
||
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 |
|
||
| Video thumbnails | **Correct.** Two clips made for the purpose (`ffmpeg`: a testsrc2 pattern, then SMPTE bars) were pushed into the demo folder and both drew **their own pattern** as the row's thumbnail — a frame from inside the file, not a poster and not the icon — beside a real 4.3 MB video from the device's own storage. The library's three zero-filled stand-in `.mp4`s cannot be decoded and **fell back to the film icon**, which exercised the failure path with material that happened to be lying around |
|
||
| The same rows on the **release** APK (R8) | **Correct, after a fix — and this is the one that mattered.** `coil-video` registers its decoder through a `META-INF/services` entry, and the release APK contained neither that file nor the class it named: frames would have decoded in debug and quietly fallen back to icons for anyone who installed the app. Registering the factory explicitly in `FileManagerApplication` fixed it, and the release build was installed and driven to the Videos library, where all four rows behaved as the debug ones had |
|
||
| The search field opened from a library | **Wrong, and fixed.** The hint read *Search this folder* and a folder toggle sat beside it, while the search in fact ran everywhere — the view model has always answered from the index when there is no folder. See the findings below |
|
||
| Crashes | **None.** `logcat -b crash` contained no entry for the package across the whole session |
|
||
|
||
### Four findings worth keeping
|
||
|
||
- **A ServiceLoader entry is not a contract, and R8 is the difference between a debug build and the
|
||
one people install.** `coil-video` registers its decoder with Coil through a `META-INF/services`
|
||
file. In the debug APK both that file and the class it names are present, and video thumbnails
|
||
worked. In the **release** APK neither was: R8 removes a class that is mentioned only from a
|
||
resource, because by its reckoning nothing uses it, and the resource goes with it. The feature was
|
||
therefore working in every build that had been looked at and would have failed silently in the one
|
||
that ships — the worst possible shape for a bug, 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. Registering the
|
||
decoder explicitly against an `ImageLoader` turns it into a code reference, and the release APK was
|
||
then installed and driven to confirm it. The lesson generalises: **audit the release build, not the
|
||
debug one**, and never trust a registration that happens by naming a class in a file. That lesson
|
||
is now a step rather than a note — [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) § 4 carries
|
||
the install commands, the paths to walk, and what to do when something differs.
|
||
|
||
- **The search field lied about its own scope inside a library, and only a run could show it.**
|
||
Opening the field from a library left the scope on "this folder": the hint said *Search this
|
||
folder* and offered a folder/public toggle, while the search actually answered from the index over
|
||
every volume. The behaviour was right and the *screen* was wrong — the same shape of bug the
|
||
volume-list fix had, and the same root cause: the switch-on-open asked whether this was the root
|
||
list instead of whether a folder was open, and a library is not the root list but has no folder
|
||
either. The toggle is now shown only when there is a folder to choose about, because its two
|
||
settings did the same thing otherwise. Nothing in the suite could have caught this: it was a
|
||
mismatch between two correct pieces, which is what a device pass is for.
|
||
|
||
- **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.
|
||
|
||
## The batch rename session (M14)
|
||
|
||
Driven on the Xiaomi phone against a purpose-made folder — `/sdcard/AAABatchRename`, holding
|
||
`alpha.txt`, `beta.txt`, `gamma.txt` and an existing series `series (1).txt` / `series (2).txt` —
|
||
because the one thing worth proving here is the **continuation**, and that needs a folder that
|
||
already holds a series. Every step went through the UI with `adb input`, and each claim below was
|
||
read back off the device rather than off the screen.
|
||
|
||
- **The dialog continues the series on its own.** Three files selected (long-press, then a tap each —
|
||
the M4 tap-to-toggles behaviour), Rename from the action bar, `series` typed into the name and the
|
||
**Start at** field moved from 1 to **3** by itself, with the preview reading *Will be renamed to
|
||
series (3).txt, series (4).txt, series (5).txt*. Nothing but the names in the folder was consulted,
|
||
and those are the listing on screen, so the dialog and the view model cannot disagree about them.
|
||
- **The work is exactly what the dialog promised.** `ls` afterwards showed `series (1) … (5)`, the
|
||
two originals untouched, and `cat` showed `series (3).txt` holding `alpha` and `series (4).txt`
|
||
holding `beta` — the renames *moved* the files rather than copying them.
|
||
- **One undo step, through the snackbar.** A second batch was renamed to `clip (1..3)`, the snackbar
|
||
read *Renamed 3 files* with **Undo**, and a single tap put all three names back, contents intact
|
||
through the round trip (`series (5).txt` still held `gamma`).
|
||
- **Both refusals are visible before anything can be pressed.** With `series` typed and the start
|
||
forced back to 1 over a folder holding `series (1)`/`series (2)`, the dialog read *series (1).txt is
|
||
already in this folder* — the file in the way, named — and the only `enabled="false"` node in the
|
||
whole dump was the Rename button. Renaming two files to the names they already hold read *Every
|
||
selected file already has that name* instead, which is the honest version of "nothing to do", and a
|
||
name containing `/` read *A name cannot contain a slash*, button disabled as well.
|
||
- **The test folder was removed afterwards.** It was created for this pass, in the phone's own
|
||
storage, and deleted through `adb` at the end: the M10 rule about not leaving artefacts on a device
|
||
that is not ours applies to a scratch folder as much as to screenshots.
|
||
|
||
Two things this session added to the tooling notes:
|
||
|
||
- **`adb input text` does not deliver a `/` reliably.** `'a/b'` arrived in the field mangled (`a!c`,
|
||
after the trailing character had to be deleted), so the slash case was sent as `input keyevent 76`
|
||
— worth knowing before concluding that a field filters separators: here the *field* accepted it and
|
||
the dialog is what refused it.
|
||
- **The dialog lifts once the keyboard appears.** Its fields sit ~200 px higher afterwards, so a
|
||
second field has to be aimed at the post-keyboard layout. Two dumps were needed to tell the two
|
||
positions apart, which is also why the numbers quoted above come from a dump rather than from
|
||
memory.
|
||
|
||
## The favourites session (M15)
|
||
|
||
Driven on the Xiaomi phone, starring the volume root — the smallest thing that can be starred, and
|
||
the one where the label matters most, because its own name is `0`.
|
||
|
||
- **The star is offered exactly where it means something.** The app bar of the volume root shows it
|
||
as *Add to favourites* (four actions now: star, search, sort, more); the home screen and a library
|
||
show none, because neither is a folder. Tapping it flipped the description to *Remove from
|
||
favourites* without leaving the screen, which means the state came back out of SQLite rather than
|
||
out of the button.
|
||
- **The home screen puts it where it belongs.** Back at the root view, a **Favourites** section
|
||
appeared above *Libraries* with the volume's own label — *Internal storage*, not `0` — and
|
||
`/storage/emulated/0` underneath, which is the path the details sheet would show as well.
|
||
- **The row opens what it says.** Tapping it opened the volume with the star already filled.
|
||
- **The row's star is the removal.** Tapping it on the home screen took the whole section away and
|
||
said *Removed from favourites*; the libraries moved back to the top, which is the empty case a new
|
||
user sees.
|
||
- **Nothing was left behind.** No files were created for this pass and the one bookmark made was
|
||
removed through the UI, so the device is as it was found.
|
||
|
||
Deliberately not done on the device: a favourite whose folder disappears. The case is covered by two
|
||
JVM tests, and staging it would have meant deleting a folder on someone else's phone; the missing
|
||
row's wording and the message are the same strings those tests assert.
|
||
|
||
## The thumbnail hardening session
|
||
|
||
The one screen that had never been pushed past what a photo album happens to contain: a folder that is
|
||
mostly *not* pictures, next to a folder that is nothing but pictures. Both built on the phone, both driven
|
||
by hand through `adb`.
|
||
|
||
| pass | result |
|
||
|---|---|
|
||
| **6 200 files, 300 of them real JPEGs** (`AAThumbs`) — opened and scrolled five times | **Correct and stable.** No crash, no `OutOfMemoryError`, no decode failure that left an empty square; text rows kept their document icon and the JPEGs further down would have had their frames. PSS 86.6 MB while scrolling the text rows. |
|
||
| **300 real JPEGs** (`AAImages`) — opened, scrolled eight swipes down and one back | **Correct.** Every row drew its own picture, not an icon and not a neighbour's picture — 300 copies of one photo, so a cache-key collision would have been invisible here and is covered by `ThumbnailKeyTest` instead. No crash, no OOM. PSS grew 83.9 MB → 149.6 MB as the memory cache filled, then stayed. |
|
||
|
||
Two things that pass is *not* evidence of:
|
||
|
||
- **It is not a jank measurement.** This device reports no frame durations at all (`docs/PERFORMANCE.md`),
|
||
so "it scrolled" is all that can be said here. The emulator path reports `frameDurationCpuMs`, so the
|
||
number that would matter can be taken there or on a newer phone.
|
||
- **It is not a 20 000 *image* folder.** The 6 200-file folder held 300 images, and 300 is where the memory
|
||
cache tops out. A folder of 20 000 pictures is the case that would test the cache bound rather than the
|
||
listing, and it is not staged — writing 20 000 files through MIUI's storage layer took five minutes for
|
||
6 000, so it is a fixture to build once and keep, not something to improvise.
|
||
|
||
What the pass did change, before it was run:
|
||
|
||
- thumbnails are keyed by path **and size and modification time** (`core/ThumbnailKey`), so a file that is
|
||
replaced or edited outside the app re-decodes instead of showing the previous picture — the one failure
|
||
a cache can cause that a user would mistake for data loss;
|
||
- the icon is drawn *under* the thumbnail rather than the row showing an empty square while decoding;
|
||
- the image loader's caches are decided explicitly: **memory only**, at 15 % of the heap, and no disk
|
||
cache — a disk cache would hold a second copy of pictures that are already on the device, and survive
|
||
the file changing.
|
||
|
||
## The copy-to session (M16)
|
||
|
||
Driven on the phone through the real UI (`adb` taps on `uiautomator` dumps), with `/sdcard/AASrc`,
|
||
`/sdcard/AADst` and `/sdcard/AAMv` standing in for two user folders.
|
||
|
||
- **Copy** into a picked folder: one file selected in `AASrc`, overflow → *Copy to…*, *Internal
|
||
storage* → `AADst` → *Copy here*. The file appeared in `AADst`, **stayed** in `AASrc`, the browser
|
||
came back to `AASrc`, and the bar still read *1 item* — the selection really does survive a copy.
|
||
- **Move** into a picked folder: the same walk into `AAMv`. `AASrc` ended up empty, `AAMv` held the
|
||
file, the snackbar said *Moved 1 item*, the listing said *This folder is empty*, and the selection bar
|
||
was gone.
|
||
- **The trail and the volume row.** Both first read the platform's own name for internal storage —
|
||
`0 / AADst` and a row labelled `0` — which is correct as a path and useless as a label. Fixed by
|
||
giving the picker the same label rule as the roots screen; the second pass reads *Internal storage /
|
||
AADst*. This is the kind of thing only a device shows: the JVM tests use paths, not labels.
|
||
|
||
## Not verified, and why
|
||
|
||
- **Long-press multi-select via adb: it works, but not every time.** `input swipe x y x y 900`
|
||
*does* 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. Driving it manually on the API 35 AVD then paid for the
|
||
caution: the compress/extract pass below found the selection bar clipping its own actions. The
|
||
archive engine itself is covered by end-to-end JVM tests, including a zip-slip archive.
|
||
- **Folder open, thumbnail paint and search timings.** Only cold start has been measured — see
|
||
[PERFORMANCE](PERFORMANCE.md), which also records why the rest needs a benchmark module and a
|
||
profileable build, and why that module does not exist yet.
|
||
- **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. **Now observed** on the API 35 AVD
|
||
(see above), including its teardown — so what is left unverified is a *physical* device, where
|
||
the system may defer or kill a background `dataSync` service on its own schedule.
|
||
- **Archive creation/extraction against real files.** Driven end-to-end on the API 35 AVD, one
|
||
archive of every supported format: `ZIP`, `7z`, `TAR`, `TAR.GZ`, `TAR.BZ2`, `TAR.XZ`, `GZ`,
|
||
`BZ2` and `XZ` all extracted through the UI with the right entry names and byte-for-byte
|
||
contents, and a folder compressed to a `.zip` that the host's `unzip` read back. That sweep is
|
||
what found the `.tar.bz2` asymmetry fixed above. Still untested: a *hostile* archive on a
|
||
device — the zip-slip and 7z-slip cases are JVM-only — and RAR/CAB/ISO/WIM, which the app
|
||
refuses by design.
|