# Changelog All notable changes to File Manager are documented in this file. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). `versionCode` and `versionName` are defined in `app/build.gradle.kts`. Each Play upload needs a **higher `versionCode`** than every previous upload. ## [Unreleased] ### Fixed - **The storage-usage screen stayed blank for ~45–60 s** on a real 55 GB volume, which reads as a hang even though the work was off the main thread. `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. It also reports each child through an `onProgress` callback, and the ViewModel appends and re-sorts as they arrive, so the largest folders appear while the rest of the tree is still being measured. Found by running the app on a real phone. - **Misleading first-run copy.** The permission prompt said "Full access was declined" on a device where the user had never declined anything. Reworded so it is true whether access was declined or simply never granted. - **The details sheet put untranslatable English on screen.** It rendered `FileKind.name` directly, so "Type" read `directory`, `archive`, `apk` — enum constants rather than labels, and impossible to translate. Every category now has a string resource and a `FileKind.labelRes()` mapping alongside the existing `OperationKind.titleRes()`. ### Added - **M10 — store assets.** The app is named **Plain Files** (one string; the `applicationId` was already locked in M0). `tools/make-store-assets.sh` renders the 512×512 RGBA icon and the 1024×500 RGB feature graphic from the app's own icon vectors, so the store icon *is* the launcher icon rather than a lookalike that drifts after the next rebrand. `tools/make-store-screenshots.sh` normalises `adb` captures to Play's 2:1 limit per form factor — a phone at its native 1080×2400 is 2.22:1 and would be rejected outright. [docs/STORE_LISTING.md](docs/STORE_LISTING.md) has the ready-to-paste copy, length-checked against Play's limits, plus the permissions paragraph that the `MANAGE_EXTERNAL_STORAGE` declaration requires. The feature graphic's copy was verified to survive Play's centre-square crop by cropping the rendered PNG, not by trusting the arithmetic. **Not finished:** the screenshots themselves. The generator and the raw-capture directories are in place, but no captures have been taken, so `store-assets/screenshots/` is still empty. The listing is publishable except for that. - **M9 — monetisation.** Decided: a **one-time paid app at €4.49, with a 14-day launch price of €2.29**, both set in the Play Console. No ads, no subscription and no in-app purchases — which means no Billing client, no `com.android.vending.BILLING` permission and no purchase code in the APK at all; the price lives on the listing, not in the build. That is what keeps the "no network permission, nothing leaves your device" claim true of the *shipped artefact* rather than just the source. The rationale, the market anchors and the rejected alternatives are in [docs/MONETISATION.md](docs/MONETISATION.md). Verified against the rebuilt release APK: zero billing references, zero `INTERNET`, and the debug-only pseudo-locale correctly absent. - **M8 — localisation groundwork.** Every user-visible string lives in `res/values/strings.xml` with real ``, and an audit of the Compose sources found exactly one genuine miss (above). The debug build type enables `isPseudoLocalesEnabled`, so running it in English (XA) renders resource-backed text accented and bracketed while anything hardcoded looks normal — a mechanical way to catch the next one. Confirmed the pseudo-locale reaches the debug APK and not the release. Byte units and the timestamp pattern stay unlocalised on purpose, documented in the README along with the pseudo-locale check. - **M7 — on-device validation.** The app was run on a real Xiaomi M2101K7AG (Android 12, MIUI V140) rather than only emulator images. Verified on hardware: the first-run permission prompt, "All files access" granted while the app is alive (picked up on resume), browsing real storage, the text editor's read *and* write path (the write checked independently with `adb shell cat`), the storage-usage screen, landscape layout, and **graceful degradation when access is revoked mid-use**. The findings live in [docs/EDGE_CASE_TESTING.md](docs/EDGE_CASE_TESTING.md), including what could *not* be driven from the host — long-press multi-select is delivered as a plain tap by this MIUI build, so the compress/extract buttons still want a manual pass. - **M6 — release config and Play readiness.** An upload keystore (4096-bit RSA, `CN=Hadrian Burkhardt, L=Potsdam, C=DE`) is generated and wired through the git-ignored `keystore.properties`, so `bundleRelease` produces a signed, R8-minified, resource-shrunk `.aab` (4 682 814 bytes) and `assembleRelease` a signed APK. `docs/PLAY_RELEASE_CHECKLIST.md` walks the Play Console work, with step 4 covering the **Permissions Declaration Form** for "All files access" — the declaration a file manager must make, and the one that can block a release. `PRIVACY.md` is written to match the shipped permissions: no collection, no sharing, no `INTERNET`. The shipped permission set is audited from the release APK. - **M5 — analysis, archives and viewers.** `domain/UsageAnalysis` rolls up recursive folder sizes and finds the largest files below a folder, which the new storage-usage screen shows as a size and a share bar per row. `domain/ArchiveEngine` creates ZIP archives and extracts ZIP, TAR, TAR.GZ, GZ, BZ2 and XZ using `java.util.zip` and Commons Compress — all pure Java, so the app still ships no native code; RAR and 7z are recognised and refused with a clear message rather than half-supported. Every archive entry name is passed through `core/ArchivePath` before it becomes a path, so a crafted `../` entry cannot write outside the destination. The browser gains Coil thumbnails in the list and grid, a built-in image viewer and text editor, and compress/extract actions. 26 new JVM tests (141 total), including a zip-slip archive that is refused while the same archive's safe entry still extracts. - **M4 — the browser UI.** The app is now usable end to end. `StorageLocation` (with a real `StorageProvider` and a fake for tests) decides which backend is in effect; `BrowserViewModel` owns navigation, sorting, hidden files, list/grid, search, selection and the clipboard, and turns an `ASK` conflict into a prompt rather than a guess. The screen offers roots and folder lists with breadcrumbs, a details sheet, new-folder / rename / delete / conflict dialogs, and the "All files access" and SAF-folder prompts — including an upgrade banner so declining full access never blocks browsing. Deletes go through the platform's own confirmation when `MediaStore` can build one and fall back to a direct delete otherwise; a `FileProvider` lets other apps open and receive files without a `file://` URI ever leaving the process. 16 new JVM tests (115 total). - **M3 — operations engine and foreground service.** `domain/FileOperations` turns the backend primitives into copy, move, delete and rename: it walks folders, counts the work up front so the progress bar has an honest denominator, checks for cancellation between entries (never swallowing `CancellationException`), and reports per-item failures instead of abandoning a batch because one file was unreadable. Conflicts are resolved through the M1 `TransferPlanner` — `ASK` is deliberately surfaced to the caller so the UI can prompt. A move tries the backend's atomic rename first and only falls back to copy-then-delete when the backend cannot do it (the SAF path across directories); the source is never deleted when its copy failed. `service/FileOpService` is a `dataSync` foreground service that mirrors the engine's progress into an ongoing notification, so a long operation survives the app being backgrounded. 23 new JVM tests (99 total), including Robolectric coverage of the notification and the service entering the foreground. - **M2 — storage layer.** The two storage backends behind one interface. `AllFilesSource` is `java.io.File` over every mounted volume (the "All files access" path); `SafSource` is the Storage Access Framework fallback, reading a persisted tree URI through `DocumentsContract` with `SafDocumentIds` doing the tree↔path mapping and `SafGrant` owning the folder picker and its persistence. `VolumeRepository` enumerates volumes via `StorageManager`/`StorageVolume` and reports free/total space through `StatFs`; `StorageAccess` and `StorageAccessPolicy` decide between the two backends. Both sources confine their blocking I/O to `Dispatchers.IO` and expose a `suspend` contract. 32 new JVM tests (76 total), including a Robolectric run of `SafSource` against a stand-in `DocumentsProvider`. - **M1 — pure-Kotlin core engine.** An Android-free `core/` package holding the logic every other layer builds on: `StoragePath` (normalised absolute paths that can never escape the root), `FileEntry` with extension-based `FileKind` classification and a `MimeTypes` lookup for "open with", natural-order `SortSpec` (directories always first, `file2` before `file10`), human-readable `FileSize`, `NameMatcher` (case-insensitive substring plus `*`/`?` globs), `NameAllocator` ("name (1)") and `TransferPlanner` (conflict policies and move-into-self detection). Covered by **44 JVM unit tests** across 10 classes. - **M0 — project scaffold.** Gradle 9.3.1 / AGP 9.1.0 build with a version catalog, `de.softwareapp_hb.filemanager` app module, a Compose Material 3 shell with the branded blue theme (dynamic colour on Android 12+), an adaptive launcher icon, and the storage-access permission set (all files access + SAF fallback). Debug APK builds, installs and runs on an AOSP Android 12 AVD.