cd0fdff31b
formatOf matched .tar.gz/.tgz as a combined format and then fell through to the bare .bz2 and .xz wrappers, so a .tar.bz2 was read as one compressed file: extracting it decompressed a single layer and left a .tar to extract again, while the same archive named .tar.gz unpacked in one step. The store listing advertised "extract ZIP, TAR, TAR.GZ, GZ, BZ2, XZ and 7z", which reads as though they all behave alike. Found by extracting one archive of every supported format through the UI on the API 35 AVD rather than trusting the suite. A real z4.tar.bz2 left z4/z4.tar - raw ustar headers and all - beside z3.tar.gz fully unpacked to z3/a.txt and z3/sub/b.txt. TAR_BZIP2 and TAR_XZ now sit alongside TAR_GZIP, detection consumes the whole tar suffix (.tar.bz2/.tbz2, .tar.xz/.txz) before the bare compressor, and the engine gains the two readArchive branches. The bare spellings still mean a single compressed file. The suite had a gap that let this hide: 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 tar spellings have one now, built in memory with the same library that reads them. 212 JVM tests, lint clean. Also records the format sweep in EDGE_CASE_TESTING and re-measures the release artefacts (aab 4 838 235 / sha256 e1d23130..., apk 2 155 455 / sha256 37fb46cb...). The APK byte count happened to land on the same figure as the previous build, so the new literals were confirmed present in the shipped dex rather than trusted to the size.
139 lines
11 KiB
Markdown
139 lines
11 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.
|
||
|
||
## 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.
|
||
- **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.
|