Files
file-manager/docs/EDGE_CASE_TESTING.md
T
SoftwareApp bb1187881b Make the Android 13+ pass, and fix what it found
The phone runs Android 12, so the questions that matter most for a release were exactly the ones it could not answer. Installed an API 35 AVD (sdkmanager was already present and dl.google.com reachable) rather than waiting for a public test track to report back. It found two user-visible defects, neither of which any test could have caught.

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 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 in the app requested it. startForeground still ran, so nothing crashed and all 203 tests passed, while dumpsys showed granted=false and importance=NONE. Settings could not fix that either, because Android will not let a user grant a permission the app has never asked for. The notification grant is now requested alongside the storage grant, which is the only moment Android accepts it.

The four READ_MEDIA_* declarations were in the same position, and the manifest comment claiming they back thumbnails and viewers on Android 13+ was wrong. Coil loads both from a real filesystem path — the code comments already say "all-files mode only" — and the one MediaStore use is a best-effort delete request that "all files access" already covers. They were never requested, so they were never granted, so nothing could have depended on them. Removed, which also settles the checklist rule that declaring an unused permission is itself a policy violation.

The selection bar drew its last three actions off the screen on a phone. With one archive selected it holds ten icon buttons, ~480 dp at the 48 dp minimum touch size against the 411 dp a phone has, and BottomAppBar is a plain Row that does not scroll. Compose laid the surplus out past the right edge: measured on a 1080 px screen the row ran Copy(167) to Compress(923-980, itself squeezed to 57 px) and stopped, so Extract here, Select all and Clear selection were unreachable, while the same selection in a 2400 px landscape window showed all ten. The logic was never wrong — ArchiveFormats.isExtractable had judged the .zip correctly — the app just looked like it could not extract. The action row now scrolls, verified by swiping it in portrait and seeing all ten appear.

Verified on the AVD rather than only built: the notification dialog now appears and importance goes NONE -> DEFAULT, a real 220 MB copy posts notification 1001 on channel file-operations with ONGOING_EVENT|FOREGROUND_SERVICE and tears it down plus stops the service when done, edge-to-edge is correct under API 35s enforcement, predictive back returns from a folder and keeps the activity resumed, and Compress to ZIP produced an archive the host unzip read back with the right relative paths and contents. 204 JVM tests, lint clean, release artefacts re-measured.
2026-10-15 11:39:48 +02:00

10 KiB
Raw Blame History

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-stating 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.

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.