diff --git a/CHANGELOG.md b/CHANGELOG.md index 4661039..7ca827f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,15 @@ upload needs a **higher `versionCode`** than every previous upload. ### Added +- **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 diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000..b55dc0b --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,80 @@ +# Privacy Policy — File Manager + +_Last updated: 2026-10-14_ + +## Summary + +File Manager lets you browse and manage the files **on your own device**, entirely +offline. The app does **not** collect, transmit, sell or share any of your data. It has no +network access at all: it declares no `INTERNET` permission and contains no analytics, no +advertising and no crash-reporting SDK. It is sold once through Google Play; the purchase +is handled entirely by Google Play, never by the app (see [Purchases](#purchases)). + +## What the app accesses + +| Data | Why | Where it goes | +|---|---|---| +| Your files and folders | To show them and to carry out the actions you ask for (open, copy, move, rename, delete, compress, extract) | Never leaves your device | +| File metadata (name, size, folder, modified date) | To sort, search, measure storage usage and show details | Never leaves your device | +| The folder you grant (only in the fallback mode) | To browse a folder you picked yourself, when "All files access" is declined | A permission reference is stored by Android on this device | + +## What the app does NOT do + +- It does **not** have the `INTERNET` permission, so it cannot send data anywhere. +- It does **not** collect analytics, crash reports, advertising identifiers or location + data. +- It does **not** contain advertising SDKs or third-party trackers. +- It does **not** create accounts and does **not** ask for your name, email or phone + number. +- It does **not** upload, back up or sync your files. + +## Permissions and what they mean + +| Permission | Purpose | +|---|---| +| `MANAGE_EXTERNAL_STORAGE` ("All files access") | Required for the app's core purpose: browsing and managing files anywhere on the device. Google Play only allows this permission for apps whose core purpose needs it — file managers are one of the accepted cases — and the app must be granted it by you in Android Settings. If you decline, the app still works, but only in a folder you pick. | +| Read media (`READ_MEDIA_IMAGES` / `READ_MEDIA_VIDEO` / `READ_MEDIA_AUDIO`, `READ_EXTERNAL_STORAGE` on older Android) | To show thumbnails and to open images, audio and video. | +| `READ_MEDIA_VISUAL_USER_SELECTED` | Supports Android 14's "selected photos only" partial access. | +| `WRITE_EXTERNAL_STORAGE` (Android 10 only) | Used only to write files you ask for on that older Android version. | +| Notifications (`POST_NOTIFICATIONS`) | Optional. Shows progress while a long copy, move, delete or extraction is running. Denying it changes nothing except that the progress bar is not shown. | +| Foreground service (`FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_DATA_SYNC`) | Keeps a long file operation running while the app is in the background, so a move is not cut in half. | + +## Deleting files + +Files are deleted only when you ask. Deleting through the app asks for confirmation first. +Where Android provides its own delete dialog for media files, the app hands the request to +the system and Android performs the deletion; otherwise the app deletes the files you +selected directly. The app never deletes anything you did not select. + +## Data retention and deletion + +The app stores a small amount of data in its own private storage: your view preferences +(list or grid, sort order), any folder permission you granted through the system picker, +and nothing else. This data never leaves your device. Uninstalling the app, or clearing its +storage in Android Settings, removes all of it. + +The app does not keep copies of your files. + +## Purchases + +The app is **sold once through Google Play**. There is no subscription and there is nothing +to buy inside the app. + +Google Play processes the payment and holds the payment details. The app contains no +payment code and never receives them, because it has no network access of any kind (see +above). Refunds, order problems and any question about a charge are handled by Google Play, +and the developer can be reached at the address below. + +## Children + +The app is a general-purpose utility and is not directed at children. It collects no +personal data from anyone. + +## Changes + +If this policy changes, the updated version will be published at this address with a new +"last updated" date. + +## Contact + +Questions about this policy: **softwareapp.hb@gmail.com**. diff --git a/README.md b/README.md index 9e53187..01ee1da 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,9 @@ no advertising or tracking SDKs. ## Status -**M5 — analysis, archives & viewers: done.** Recursive storage usage, ZIP creation and -ZIP/TAR/GZ/BZ2/XZ extraction (with zip-slip protection), Coil thumbnails, an image viewer -and a text editor. Covered by 141 JVM unit tests (26 added here). See -[docs/ROADMAP.md](docs/ROADMAP.md) for the full milestone plan. +**M6 — release config & Play readiness: done.** Signed, R8-minified `.aab` builds; the Play +release checklist (including the "All files access" declaration) and the privacy policy are +written. See [docs/ROADMAP.md](docs/ROADMAP.md) for the full milestone plan. ## Tech stack @@ -78,6 +77,27 @@ R8/minification but produces an unsigned artefact. 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. + ## Licence Proprietary — all rights reserved. diff --git a/docs/PLAY_RELEASE_CHECKLIST.md b/docs/PLAY_RELEASE_CHECKLIST.md new file mode 100644 index 0000000..e75a6d5 --- /dev/null +++ b/docs/PLAY_RELEASE_CHECKLIST.md @@ -0,0 +1,129 @@ +# Google Play release checklist + +Work top to bottom. Items marked **[code]** are already handled in this repository; the +rest are things you must do in the Play Console or with your own accounts. + +## 1. Identity and signing + +- [x] **Application id** chosen: `de.softwareapp_hb.filemanager` + (`applicationId` in `app/build.gradle.kts`). It can **never be changed after the first + release**, so treat it as locked. `namespace` and the Kotlin package of the same name + are aligned with it; debug builds append `.debug`. +- [x] **[code] Upload keystore generated** — `filemanager-upload.jks` in the project root + (git-ignored, `alias upload`, 4096-bit RSA, 10 000 days, `CN=Hadrian Burkhardt, + L=Potsdam, C=DE`). Certificate SHA-256 fingerprint, to confirm the right key is + registered when Play App Signing is enabled: + `49:A9:FC:13:F5:13:24:48:AE:74:5A:AE:7E:05:D9:2F:67:55:1F:EE:71:A5:64:A1:10:13:57:4D:0E:AA:0E:4E` +- [x] **[code] `keystore.properties` written** (git-ignored, mode `600`). The Gradle build + picks it up automatically and signs release builds; without it the release APK stays + unsigned. +- [ ] **Store the keystore and its password in a password manager and back it up.** Losing + the upload key means you cannot ship updates. +- [ ] Enable **Play App Signing** in the Play Console with the first upload. Google then + holds the app signing key and this one only signs uploads: + ```bash + keytool -exportcert -alias upload -keystore filemanager-upload.jks -rfc > upload_cert.pem + ``` + +## 2. Versioning + +- [x] `versionName` **[code]** — `1.0.0` in `app/build.gradle.kts`. +- [x] `versionCode` **[code]** — `1`. Play orders uploads by it, so every later upload must + be higher. + +## 3. Build the release artefacts + +```bash +./gradlew :app:bundleRelease # -> app/build/outputs/bundle/release/app-release.aab +./gradlew :app:assembleRelease # -> APK, for local checks +./gradlew :app:testDebugUnitTest # JVM unit tests +./gradlew :app:lintDebug # Android Lint +``` + +- [x] **[code] verified 2026-10-14.** `bundleRelease` and `assembleRelease` both succeed + with R8 minification and resource shrinking on. + - `app-release.aab` is **4 682 814 bytes** + (`sha256 9014627a8596397623404cb9d986af5019c5e33cf331c5d363427c140f121788`). + - `app-release.apk` is **2 081 955 bytes**, signed by the upload key above. + - Both report `versionCode=1 versionName=1.0.0 targetSdk=36` and exactly the + permission set in step 4 — **no `INTERNET`**, so "nothing leaves the device" holds + in the shipped artefact and not just in the source manifest. +- [ ] Upload the **`.aab`** (not the APK) to the Play Console. + ⚠️ **Expect a native-debug-symbols warning and ignore it.** Play finds + `base/lib//libandroidx.graphics.path.so`, which arrives transitively through + `androidx.compose.ui:ui-graphics` → `androidx.graphics:graphics-path`. It is a + recommendation, not an error: it does not block upload, review or rollout. This + project has **no** `ndk`/`externalNativeBuild`/`jniLibs` configuration and no native + sources of its own. +- [ ] Keep `app/build/outputs/mapping/release/mapping.txt` for every release so Play can + de-obfuscate crash reports. + +## 4. Sensitive permissions — the Permissions Declaration Form + +This is the step that can block a file manager, and the one to get right. + +- [ ] In the Play Console open **App content → Sensitive app permissions** (older UI: + *Permissions Declaration Form*) and declare **All files access** + (`MANAGE_EXTERNAL_STORAGE`). +- [ ] Select the **file manager** use case. Google's policy — *Use of All files access + (MANAGE_EXTERNAL_STORAGE) permission* — names file managers explicitly: + *"intended and permitted use includes file managers, backup and restore apps, + anti-virus apps, and document management apps."* +- [ ] Make sure the **store description states the core purpose** (browsing, organising and + managing files on the device). The policy requires the core functionality to be + "prominently documented and promoted in the app's description" — the listing copy in + `docs/STORE_LISTING.md` (M10) is written to satisfy that. +- [ ] **Do not** add `QUERY_ALL_PACKAGES`, `REQUEST_INSTALL_PACKAGES` or `INTERNET` without + declaring them. The app currently requests none of them. +- [ ] Provide the **privacy policy URL** ([PRIVACY.md](../PRIVACY.md) — it must be a public + URL, not a file in the repo). +- [ ] Keep the permission set honest: declaring a permission the app does not use is itself + a policy violation. + +**Verify the shipped permission set** after every release build: + +```bash +$ANDROID_HOME/build-tools/36.0.0/aapt2 dump permissions \ + app/build/outputs/apk/release/app-release.apk +``` + +Expected, and nothing else: + +``` +MANAGE_EXTERNAL_STORAGE +READ_EXTERNAL_STORAGE (maxSdkVersion=32) +READ_MEDIA_IMAGES +READ_MEDIA_VIDEO +READ_MEDIA_AUDIO +READ_MEDIA_VISUAL_USER_SELECTED +WRITE_EXTERNAL_STORAGE (maxSdkVersion=29) +FOREGROUND_SERVICE +FOREGROUND_SERVICE_DATA_SYNC +POST_NOTIFICATIONS +.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION (added by AndroidX) +``` + +## 5. Data safety, content rating, audience + +- [ ] **Data safety** form: the app collects **nothing** and shares **nothing**. Answer "No" + to collection, "No" to sharing, and state that data is not transmitted off the device. +- [ ] Confirm the answers match [PRIVACY.md](../PRIVACY.md). +- [ ] Content rating questionnaire: utility, no user-generated content, no ads. +- [ ] Target audience: not directed at children. + +## 6. Store listing + +Full copy and the generated graphics land in **M10** (`docs/STORE_LISTING.md`). Console +fields needed: app name, short description, full description, app icon 512×512, feature +graphic 1024×500, and at least 4 phone screenshots (Play asks for 4+ per large-screen form +factor too). + +## 7. Release tracks and pricing + +- [ ] Create the app, complete *App content*, then roll out + **internal → closed → production**, checking the pre-launch report on the way. +- [ ] ⚠️ **Set the price before the first upload** (M9). Once an app has been offered for + free it can never be made paid, and this app is a **one-time paid** app: no ads, no + subscription, no in-app purchases — and therefore no Billing code and no + `com.android.vending.BILLING` permission in the APK. The price lives on the listing, + not in the build. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 11d8312..b5ebadf 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -15,7 +15,7 @@ Legend: ✅ done · 🚧 in progress · ⏳ not started · 💡 candidate (propo | M3 | Operations engine + service | ✅ | `domain/FileOperations` composes the backend primitives into copy/move/delete/rename with recursive walking, an honest progress total, cooperative cancellation between entries and per-item failure reporting; conflicts are decided through the M1 `TransferPlanner` (`ASK` is surfaced for the UI to resolve). `service/FileOpService` is a `dataSync` foreground service mirroring progress into a notification (`FileOpNotifications` + `text/TextRes`). **99 JVM tests** total (23 added here) | | M4 | Compose UI | ✅ | `BrowserViewModel` + `BrowserUiState` over `StorageLocation`; browser screen with list/grid, breadcrumbs, in-folder search, a sort/hidden-files/view sheet, multi-select action bar, details sheet, and new-folder/rename/delete/conflict dialogs; a roots view and the "All files access" + SAF-folder prompts; `data/MediaStoreDeleter` routes a delete through the platform's own dialog where it can and falls back to a direct delete; a `FileProvider` backs "open with" and share. **115 JVM tests** total (16 added here) | | M5 | Analysis, archives, viewers | ✅ | `domain/UsageAnalysis` measures recursive folder sizes and finds the largest files, shown by a bar-per-row storage-usage screen; `domain/ArchiveEngine` creates ZIP and extracts ZIP/TAR/TAR.GZ/GZ/BZ2/XZ using `java.util.zip` + Commons Compress (no native code), with **every entry name filtered through `core/ArchivePath` against zip-slip**; a Coil-backed image viewer, thumbnails in the list and grid, a text editor, and compress/extract in the selection bar. **141 JVM tests** total (26 added here) | -| M6 | Release config & Play readiness | ⏳ | R8/minify, optional signing, `.aab`; [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) (incl. the **Permissions Declaration Form**), [PRIVACY.md](../PRIVACY.md), README | +| M6 | Release config & Play readiness | ✅ | Upload keystore generated (4096-bit RSA, `CN=Hadrian Burkhardt`) and wired through the git-ignored `keystore.properties`; `bundleRelease` + `assembleRelease` verified — **4 682 814-byte `.aab`** (`sha256 9014627a…`), 2 081 955-byte APK signed by the upload key, R8 mapping kept. [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) (incl. the **Permissions Declaration Form** for "All files access", the Data safety answers and the shipped-permission audit) and [PRIVACY.md](../PRIVACY.md) written for a restricted-permission app. Shipped permission set audited: **no `INTERNET`** | | M7 | On-device validation & hardening | ⏳ | AVD + real-device checks, per-operation error boundaries, edge-case fixtures | | M8 | Localisation groundwork | ⏳ | every user-visible string in `res/values/strings.xml` with real ``; `text/` mapping | | M9 | Monetisation | ⏳ | **one-time paid** — price set only in the Play Console before the first upload; no Billing code and no `BILLING` permission |