M6: release signing, privacy policy and Play release checklist

This commit is contained in:
2026-10-14 16:44:58 +02:00
parent 3e39fc9b86
commit e2cfb5d9ca
5 changed files with 243 additions and 5 deletions
+9
View File
@@ -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
+80
View File
@@ -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**.
+24 -4
View File
@@ -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.
+129
View File
@@ -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/<abi>/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
<applicationId>.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.
+1 -1
View File
@@ -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 `<plurals>`; `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 |