a461be2a66
The last commit shipped the screenshots and the two-pane layout but left the roadmap and README saying otherwise. Also corrects the stated reason 7z is unsupported: RAR really does need native code, but SevenZFile is already inside the Commons Compress jar this app ships.
175 lines
7.6 KiB
Markdown
175 lines
7.6 KiB
Markdown
# File Manager
|
||
|
||
An **ad-free**, **one-time-paid** Android file manager. It browses and manages the
|
||
files on your device — locally, with no ads, no account and no analytics.
|
||
|
||
Nothing ever leaves the device: the app declares no network permission and contains
|
||
no advertising or tracking SDKs.
|
||
|
||
> **Working title.** The display name and the store listing are decided before the
|
||
> first Play upload (see [docs/ROADMAP.md](docs/ROADMAP.md), M10). The `applicationId`
|
||
> is already fixed and can never change after the first release.
|
||
|
||
## Status
|
||
|
||
**M10 — store assets: done.** The app is named **Plain Files**, the icon and feature graphic
|
||
are generated from the app's own vectors, and [docs/STORE_LISTING.md](docs/STORE_LISTING.md)
|
||
holds length-checked listing copy. The screenshots are captured: 5 phone (1080×2160), 4 on a
|
||
7-inch tablet (1200×1920) and 4 on a 10-inch tablet (2560×1600).
|
||
|
||
## Tech stack
|
||
|
||
| Area | Choice |
|
||
|---|---|
|
||
| Language | Kotlin 2.4.10 (using AGP 9 *built-in Kotlin*) |
|
||
| UI | Jetpack Compose, BOM 2026.06.01, Material 3 |
|
||
| Build | Gradle 9.3.1, Android Gradle Plugin 9.1.0 |
|
||
| Storage access | `MANAGE_EXTERNAL_STORAGE` ("All files access") with a Storage Access Framework fallback |
|
||
| Archives | Apache Commons Compress (pure Java — no NDK) |
|
||
| SDK levels | `compileSdk`/`targetSdk` 36, `minSdk` 26 |
|
||
|
||
`targetSdk` is 36 because Google Play requires new apps and updates to target
|
||
Android 16 (API 36) from 31 August 2026.
|
||
|
||
## Project layout
|
||
|
||
```
|
||
app/src/main/java/de/softwareapp_hb/filemanager/
|
||
├── core/ Pure-Kotlin engine (no Android imports!) — paths, sorting, formatting,
|
||
│ search matching, copy/move planning, conflict policy
|
||
├── data/ Storage backends (all-files access + SAF), volumes, bookmarks,
|
||
│ file operations, archives, deletion
|
||
├── domain/ Browser + operation controllers, models, permission state
|
||
├── service/ Foreground service for long-running file operations
|
||
├── ui/ Compose theme, browser screen, components, ViewModels
|
||
└── text/ Domain enum → string-resource mapping
|
||
app/src/test/java/… JVM unit tests for the core engine
|
||
```
|
||
|
||
`core/` deliberately has **no Android dependencies**, so the trickiest logic is
|
||
covered by fast JVM tests.
|
||
|
||
## Building
|
||
|
||
Prerequisites:
|
||
|
||
- JDK 17+ (JDK 25 verified)
|
||
- Android SDK with `platforms;android-36` and `build-tools;36.0.0`
|
||
|
||
```bash
|
||
# Tell Gradle where the SDK lives (this file is git-ignored)
|
||
echo "sdk.dir=$HOME/Android/Sdk" > local.properties
|
||
|
||
./gradlew :app:assembleDebug # debug APK
|
||
./gradlew :app:testDebugUnitTest # JVM unit tests for the engine
|
||
./gradlew :app:lintDebug # Android Lint
|
||
./gradlew :app:bundleRelease # Play upload bundle (.aab)
|
||
```
|
||
|
||
Release signing is optional and driven by a git-ignored `keystore.properties`
|
||
(see `keystore.properties.example`); without it the release build still runs
|
||
R8/minification but produces an unsigned artefact.
|
||
|
||
## Tracked in git
|
||
|
||
- Source, resources and Gradle build files.
|
||
- `docs/` — roadmap and release documentation.
|
||
- `PRIVACY.md`, `CHANGELOG.md`, and (from M10) `store-assets/`.
|
||
|
||
Not tracked: `local.properties`, `keystore.properties`, `*.jks`, build outputs.
|
||
|
||
## Releasing
|
||
|
||
Release signing is driven by the git-ignored `keystore.properties` (see
|
||
`keystore.properties.example`). Without that file the release build still runs R8 and
|
||
resource shrinking, but produces an unsigned artefact that cannot be uploaded.
|
||
|
||
```bash
|
||
./gradlew :app:bundleRelease # Play upload bundle (.aab)
|
||
./gradlew :app:assembleRelease # APK, for local checks
|
||
|
||
# Audit what is actually shipped — in particular that there is no INTERNET permission:
|
||
$ANDROID_HOME/build-tools/36.0.0/aapt2 dump permissions \
|
||
app/build/outputs/apk/release/app-release.apk
|
||
```
|
||
|
||
Read [docs/PLAY_RELEASE_CHECKLIST.md](docs/PLAY_RELEASE_CHECKLIST.md) before uploading.
|
||
Step 4 is the one that matters most for this app: a file manager must declare **"All files
|
||
access"** through Play's Permissions Declaration Form, and have the core purpose documented
|
||
in its store description. [PRIVACY.md](PRIVACY.md) is written to match the shipped
|
||
permissions.
|
||
|
||
## Localisation
|
||
|
||
All user-visible text lives in `app/src/main/res/values/strings.xml`, with real `<plurals>`
|
||
wherever a count can be one, and the screen and the progress notification resolve their
|
||
labels through `text/TextRes.kt` so the two cannot drift apart. **English is currently the
|
||
only locale.**
|
||
|
||
Adding a language is then a file, not a refactor:
|
||
|
||
```
|
||
app/src/main/res/values-de/strings.xml # same keys, translated values
|
||
```
|
||
|
||
Deliberately **not** localised, and why:
|
||
|
||
- **Byte units** ("KB", "MB") — symbols, not words, and every file manager and the system
|
||
Settings app show them the same way.
|
||
- **The timestamp pattern** (`yyyy-MM-dd HH:mm`) — a fixed, sortable format; the zone is
|
||
passed in explicitly rather than assumed.
|
||
|
||
### Checking that nothing was missed
|
||
|
||
Two things are easy to get wrong: a literal typed into a composable, and showing an enum
|
||
constant directly (`FileKind.name` puts untranslatable English on screen — the details
|
||
sheet used to do exactly that). The mechanical check is the pseudo-locale:
|
||
|
||
```bash
|
||
./gradlew :app:assembleDebug
|
||
# Settings > System > Languages > English (XA), then launch the debug build.
|
||
```
|
||
|
||
The debug build type sets `isPseudoLocalesEnabled = true`, so every string that came from
|
||
`strings.xml` renders accented and bracketed (`[Îmàgé]`). **Anything that looks normal is a
|
||
string that never went through resources.** The release build is unaffected.
|
||
|
||
One caveat: **Google Play's automatic translation translates your store listing, not the app
|
||
UI.** Translated UI text has to be supplied in `values-<locale>/`.
|
||
|
||
## Known limitations
|
||
|
||
- **"Open with" and Share only work in the all-files mode.** A SAF path is relative to the
|
||
granted tree rather than a filesystem path, so it cannot be handed to `FileProvider`.
|
||
- **RAR and 7z are refused**, not supported. The stated reason is only half right: RAR
|
||
genuinely needs native code (the Commons Compress jar this app ships contains no RAR
|
||
classes at all), but **7z does not** — `SevenZFile`/`SevenZOutputFile` are already inside
|
||
that same jar, so 7z read, and AES-256-encrypted 7z write, is an unpack away.
|
||
- **Archive extraction does not restore original timestamps or permissions**, and there is
|
||
no archive encryption or password support.
|
||
- **Cancellation happens between entries**, so a single very large file finishes before a
|
||
stop is honoured.
|
||
- **View state is not persisted across process death** (`SavedStateHandle` is not wired up).
|
||
- **Long-press multi-select could not be driven from adb** on the MIUI test device, so the
|
||
compress/extract buttons want a manual pass (see
|
||
[docs/EDGE_CASE_TESTING.md](docs/EDGE_CASE_TESTING.md)).
|
||
- The display name is still the working title — decided before the first upload (M10).
|
||
|
||
## Monetisation
|
||
|
||
**One-time paid: €4.49 base, with a €2.29 launch price for the first 14 days.** No ads, no
|
||
subscription, no in-app purchases, no free tier.
|
||
|
||
The price lives on the Play listing, not in the build, which is why there is no Billing
|
||
client in the dependencies and no `com.android.vending.BILLING` permission in the APK —
|
||
that is what keeps "no network permission, nothing leaves your device" true of the
|
||
*shipped artefact* rather than just the source. Rationale, market anchors, the rejected
|
||
alternatives and the Console steps: [docs/MONETISATION.md](docs/MONETISATION.md).
|
||
|
||
⚠️ **The price must be set before the first upload** — Play allows paid → free but never
|
||
free → paid.
|
||
|
||
## Licence
|
||
|
||
Proprietary — all rights reserved.
|