# 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. | | 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. 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.** Now driven end-to-end on the API 35 AVD: a folder of three files compressed to a `.zip` that the host's `unzip` read back with the right relative paths and contents, and that `.zip` then selected for extraction. What is still untested is the exotic end of the format list — 7z, `.xz`, the tar wrappers — against real files rather than the fixtures the JVM tests build.