docs: draft the Photo and video permissions declaration answer

The Console flags READ_MEDIA_IMAGES as undeclared and asks a specific question per permission - why
the Android photo picker does not fully support the app, and why the permission is needed. The old
draft here answered only the second half, in general terms, so it is replaced with an answer in the
order the form asks, at two lengths (1 689 chars, and 682 for a small box).

The picker half is what a reviewer actually weighs, and the code makes it concrete instead of
rhetorical: a picker hands over the items the user selects, while the duplicates of a picked photo
are precisely the files it never hands over - and the app's similarity threshold is fitted per scan
from the library's own pairwise hash-distance histogram (AdaptiveThreshold will not fit at all below
200 pairs), which no hand-picked subset can supply. The selection is not a sample of the library,
it is the part the duplicates are not in.

Both claims sit in a table next to their source in the code, together with the other things the
answer asserts - no INTERNET, deletion only through createDeleteRequest/createTrashRequest, and
partial access implemented rather than merely declared - so the text can be defended if Play asks.

Also written down: the form asks about READ_MEDIA_IMAGES only, since
READ_MEDIA_VISUAL_USER_SELECTED is the narrow half of the pair and needs no justification; if a
video is asked for as well, the foreground-service demo is a fallback but was filmed for a different
question; and a refusal would not require code changes, because the partial mode already runs - what
would be lost is the promise.
This commit is contained in:
2026-10-14 13:38:33 +02:00
parent 68dcbc5db6
commit 23bbc4104b
+72 -10
View File
@@ -212,17 +212,79 @@ action except the items marked **[code]**.
(Match `<uses-permission` only. A bare `android.permission.` grep also picks up (Match `<uses-permission` only. A bare `android.permission.` grep also picks up
`android.permission.DUMP`, which AndroidX puts on its profile-installer *receiver* — `android.permission.DUMP`, which AndroidX puts on its profile-installer *receiver* —
a component restriction, not a permission the app requests.) a component restriction, not a permission the app requests.)
- [ ] Answer the **Photo and video permissions** declaration in the Console. The app - [ ] Answer the **Photo and video permissions** declaration (App content › Photo and video
requests `READ_MEDIA_IMAGES` (plus `READ_MEDIA_VISUAL_USER_SELECTED` for Android 14 permissions). It lists `READ_MEDIA_IMAGES` as *undeclared* and asks one question per
partial access) and deletes only user-selected files through the system dialog. permission:
Paste:
> **Read media images** — Describe why the Android photo picker or other system pickers do
> not fully support your app's core functionality, and why your app needs to use the
> `READ_MEDIA_IMAGES` permission.
Answer the *picker* half first and the *permission* half second — that is the order the
question asks in, and the picker half is the one a reviewer is actually weighing. Paste
(1 689 chars):
> Foto Dup Finder's core functionality is finding duplicate and visually similar photos
> across the user's whole photo library, and letting the user delete the copies. Reading the
> photo library is that entire operation.
>
> The Android photo picker cannot support it. The picker only grants access to the items the
> user selects in that session, and the duplicates are by definition outside that selection:
> the copies of a picked photo are other files, in other folders, that the picker never hands
> over. Comparing only the picked items tells the user nothing they did not already know;
> finding the groups requires comparing every photo in the library with every other photo.
>
> The app also needs the whole library to calibrate itself. It estimates the distance
> distribution of unrelated photos from the library's own pairwise perceptual-hash distances
> and sets the widest similarity threshold whose expected number of accidental matches stays
> in budget. Below a few hundred pairs that fit is impossible and the app falls back to a
> default; over a hand-picked subset it would be meaningless, because a selection is not a
> sample of the library — it is the part the duplicates are not in.
>
> Nothing is uploaded: the app declares no INTERNET permission and all hashing happens on the
> device. Photos are only read. Any deletion requires an explicit selection by the user and
> always goes through Android's own confirmation dialog (`MediaStore.createDeleteRequest` /
> `createTrashRequest`) — never silently — and the app never requests
> `MANAGE_EXTERNAL_STORAGE`. Partial access (`READ_MEDIA_VISUAL_USER_SELECTED`) is declared
> and supported as a reduced mode for users who prefer to hand over fewer photos.
If the box is smaller, this says the same in 682 chars:
> Foto Dup Finder's only function is finding duplicate and similar photos across the user's
> whole photo library and letting them delete the copies: reading the library is the feature.
> The Android photo picker grants access only to the items a user selects, while the
> duplicates of a picked photo are precisely the files the picker never hands over — a
> selection cannot answer "what is duplicated in my library". The app also calibrates its
> similarity threshold from the library's own pairwise distance distribution, which needs the
> whole library to mean anything. Nothing is uploaded (the app has no INTERNET permission)
> and deletion always goes through Android's confirmation dialog.
Nothing in either version is aspirational — every claim is a place in the code, so it can be
defended if Play comes back with questions:
| The answer claims … | … and this is where that is true |
|---|---|
| the app reads the whole library | `MediaStorePhotoSource.loadPhotos()` queries the image collection with a `null` selection — metadata only, pixels later and lazily |
| duplicate detection needs every pair | `DuplicateGrouper` clusters by exact SHA-256 *and* perceptual fingerprints, union-find over the whole scan |
| the threshold is fitted per library | `AdaptiveThreshold.choose()` estimates the unrelated-photo distribution from the scan's own pairwise histogram and returns the widest threshold keeping the *expected* number of accidental matches in budget; under `MIN_SAMPLE` = 200 pairs it falls back to the policy ceiling |
| a selection cannot stand in for a library | that histogram is built from every pair in the scan — the phone's 5,385-photo library fitted to *pHash ≤ 10 bits* and said so on screen, while the 22-photo tablet library was too small to fit and used its ceiling (`(default)`) |
| a constant threshold cannot work | `AdaptiveThreshold` KDoc and README § scan pipeline: the widest pHash threshold with zero false pairs falls from **13 (120 images) to 5 (2 880)** as a library grows |
| nothing leaves the device | no `INTERNET` in the merged release manifest — the `aapt2` audit in the item above |
| deletion is never silent | `MediaDeleter` → `MediaStore.createDeleteRequest` / `createTrashRequest`; no `MANAGE_EXTERNAL_STORAGE` |
| partial access is real, not a gesture | `AppPermissions.MediaAccess.PARTIAL` / `hasPartialAccess()`, with a banner and a prompt to upgrade to full access |
Three notes for filling the form in. (1) The declaration asks about `READ_MEDIA_IMAGES` only:
`READ_MEDIA_VISUAL_USER_SELECTED` is the *narrow* half of the pair and needs no justification,
so do not volunteer it as though it were the broad one. (2) If that screen also asks for a
**video**, the recorded foreground-service demo does show a full 5,385-photo library scan and
the groups it found, which is the functionality this declaration is about — but it was filmed
to answer the foreground-service question, so it is a fallback rather than the intended
answer, and a take focused on this question (open → grant → scan → groups) may be asked for.
(3) If the declaration were refused, the app already runs in the partial mode it implements
(banner + subset scan), so nothing would need rewriting — what would be lost is the promise
("find the duplicates you forgot about"), which is why the answer above is the one to stand
behind.
> Foto Dup Finder is a photo-cleanup tool. Reading the photo library **is** its core
> functionality: it compares photos on the device to find exact and near duplicates
> and lets the user remove the copies they select. Nothing is uploaded — the app
> declares no network permission. Deletion always goes through the system consent
> dialog (`MediaStore.createDeleteRequest`); the app never requests
> `MANAGE_EXTERNAL_STORAGE`.
- [ ] Declare the **foreground service type**. This is the release-blocking item for the - [ ] Declare the **foreground service type**. This is the release-blocking item for the
Android 14+ review. **[code]** The manifest already matches what you will declare: Android 14+ review. **[code]** The manifest already matches what you will declare: