# Working in this repository **Plain Files** is a file manager for Android: Kotlin and Jetpack Compose, one Gradle module (`app`), `minSdk` 26, `targetSdk` 36, and **no `INTERNET` permission** — "nothing leaves the device" is a shipping requirement of this app, not a slogan. It is sold once (no ads, no subscription, no billing code), and the prose in `docs/` is treated as part of the product. Two habits shape everything here. **The app is driven on a real device** for anything a user can see, and what that proved is written down. **A test name is a sentence** about behaviour, so the suite doubles as the specification. ## Read these two first, and only then the source | file | why | |---|---| | `docs/CODE_MAP.md` | Generated index of the source tree: what each file holds, and every test name in the suite. Start here for "which file is this in?" | | `docs/ROADMAP.md` | Which milestone is done, the key decisions, and the deliberate deferrals | Do **not** read `CHANGELOG.md` front to back — it is long, and it is meant to be grepped by feature name once you know what you are changing. ## Commands ```bash ./tools/verify.sh # the gate: map check, detekt, tests, lint ./tools/verify.sh --release # ...and an R8 release build (what CI runs nightly) GRADLE_FLAGS=--offline ./tools/verify.sh # when the network is unavailable ./gradlew :app:assembleDebug # then: adb install -r app/build/outputs/apk/debug/app-debug.apk ./tools/measure-startup.sh 5 # cold start, median of five, on the installed device ./gradlew :benchmark:connectedDebugAndroidTest # the four macrobenchmark tests (needs a device) # add -PbenchmarkBuild: it targets the benchmark build ./gradlew :app:detektBaseline # re-freezes the findings that exist now — think before running it python3 tools/make-code-map.py # regenerate docs/CODE_MAP.md after touching source ``` CI (`.github/workflows/verify.yml`) runs the same script on every push and pull request, plus a nightly `--release` build, because R8 is the one check nothing else performs and it has hidden a real defect here before. `git config core.hooksPath .githooks` runs it before every push as well. The detekt gap it leaves — the findings in `config/detekt/baseline.xml` — is a **ratchet**: new findings fail the build, existing ones are parked until the file they live in is touched for another reason. `tools/` holds the gate and the generators; `config/detekt/` holds the rules and that baseline. ## Where things live, and the one rule about direction ``` core/ pure Kotlin, no Android imports: paths, names, kinds, sorting, planning data/ storage backends (all-files access, SAF), volumes, shares, SQLite search/trash/favourites domain/ operations over a source: copy/move/delete/rename, archives, search, duplicates service/ the foreground service that keeps a long operation alive ui/ Compose: browser/, viewer/, usage/, trash/, duplicates/, theme/ text/ domain enum to string resource ``` The dependency direction is `ui → domain → data → core`, and `core/` importing `android.*` or `androidx.*` is a test failure, not a matter of taste: it is what keeps the trickiest logic testable without Robolectric. `docs/CODE_MAP.md` has the per-file detail. ## Non-negotiables Each of these is enforced somewhere, so breaking one fails a test, lint, or a reviewer's first comment. `app/src/test/java/…/ProjectInvariantsTest.kt` holds the source-level ones. - **No new permission, and never `INTERNET`.** The manifest's permission set is asserted exactly; adding one means deciding it, explaining it in the manifest comment and the README, and updating that list deliberately. - **Every user-visible string comes from `strings.xml`.** The debug build enables a pseudo-locale (`values-en-XA`), so untranslated text is visible on screen as unaccented text. - **A name is one path segment.** `core/FileNames.isUsableName` is the only implementation; never inspect `/` yourself. `StoragePath.child("a/b")` resolves *into a subfolder*, so an unguarded call renames or creates something the dialog never offered. - **Nothing is overwritten silently.** Operations refuse rather than destroy; deletes go through the trash (24-hour window) and offer undo; a batch that cannot succeed is refused as a whole rather than performed until it hits the problem. - **Comments explain why, not what.** The house style is a KDoc paragraph per file and type that says what it is for and what was rejected — decisions and limits, not restatements of the code. - **A test name is a sentence about behaviour** (`a batch rename is a single undo step`), because the names are read as the spec. Logic gets behaviour tests, rules about the shape of the project get source-level tests, and **what a user sees gets a Compose rendering test** — those run on the JVM through Robolectric, so they are part of the same gate rather than needing a device. - **The docs are part of the change.** `CHANGELOG.md` (Added/Changed/Fixed, in the house voice), the `ROADMAP.md` milestone table, the README if a claim changed, and `docs/EDGE_CASE_TESTING.md` for anything only a device could confirm. ## Traps that have cost real time here - **MIUI blocks `adb install` twice over.** Developer options needs *Install via USB*, and each install then waits for a confirmation dialog that times out on its own. A bare `INSTALL_FAILED_USER_RESTRICTED` means the dialog was answered late, not that the APK is bad. - **Never run `assembleDebug` and `adb install` in one parallel batch.** They race, the install takes the previous APK, and the feature you just wrote appears to be missing. - **R8 has hidden real bugs.** A class reached only by reflection, or named only from a resource, is stripped from the release build while debug works perfectly — this happened with `coil-video`, where video frames decoded in debug and quietly fell back to icons for real users. When debug and release disagree, assume the shrinker removed something. - **`adb input text` does not deliver a `/`.** Send `input keyevent 76` instead. The slash is worth testing at all because the field *accepts* it and the dialog is what refuses it. - **Long-press multi-select via `input swipe x y x y 900` works, but not every time** — it has also landed as a plain tap. A flaky gesture is a reason to look at the screen, not to distrust the code. - **The debug build is a separate package** (`…filemanager.debug`), so it installs beside a release build and the two can be compared directly. - **detekt 1.23 cannot run on JDK 25.** It parses the version string of the JVM it runs on and throws `25.0.4.1` as the entire error message, which is why `:app:detekt` drives the CLI on a 21 launcher. A bare version number as a build failure is this, not the code — and a **JDK 21 has to be installed** for the toolchain to find: Gradle does not download one here. - **A detekt baseline has to exist before it can be read.** It starts as an empty document on purpose; `--create-baseline` cannot conjure the path it was told to consult. ## Documentation map — read on demand | file | what it is for | |---|---| | `README.md` | The tour: features, layout, build, and the honest **known limitations** | | `CHANGELOG.md` | The reasoning behind each change. Grep it; do not read it end to end | | `docs/ROADMAP.md` | Milestones with their evidence, the key decisions, the deferrals | | `docs/EDGE_CASE_TESTING.md` | What was verified, on which device or AVD, and what was not | | `docs/PERFORMANCE.md` | The numbers that exist, and the ones that deliberately do not yet | | `docs/PLAY_RELEASE_CHECKLIST.md` | Release only — work it in order | | `docs/RELEASE_NOTES.md` | The Play **What's new** copy, at 500 characters, and the longer announcement | | `docs/PERMISSION_VIDEO.md` | The declaration's 23-second video: what it shows, and how to re-shoot it | | `docs/FGS_VIDEO.md` | The foreground-service video: the local copy, what was cut, and how to re-shoot it | | `docs/COMPETITIVE_ANALYSIS.md`, `docs/MONETISATION.md`, `docs/STORE_LISTING.md` | Product context, pricing, and length-checked listing copy | | `PRIVACY.md` | The policy text the store listing points at | ## Definition of done 1. `./tools/verify.sh` green — the map current, detekt, the unit tests and lint in one command. 2. `python3 tools/make-code-map.py` run, so that `--check` passes. 3. A `CHANGELOG.md` entry in the house voice: what changed, why, what was rejected, and what was verified rather than assumed. 4. Anything a user can see driven on a device or emulator, with the result written into `docs/EDGE_CASE_TESTING.md` — including what was deliberately left untested, and why. 5. A commit whose subject is a sentence about the change and whose body explains the reasoning. The diff already says what changed; the message is there for why it is this way.