From 23bbc4104b0d8d9add7a16db30c3d5f2a053dba4 Mon Sep 17 00:00:00 2001 From: Hadrian Burkhardt Date: Wed, 14 Oct 2026 13:38:33 +0200 Subject: [PATCH] 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. --- docs/PLAY_RELEASE_CHECKLIST.md | 82 +++++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 10 deletions(-) diff --git a/docs/PLAY_RELEASE_CHECKLIST.md b/docs/PLAY_RELEASE_CHECKLIST.md index 180d339..fee4303 100644 --- a/docs/PLAY_RELEASE_CHECKLIST.md +++ b/docs/PLAY_RELEASE_CHECKLIST.md @@ -212,17 +212,79 @@ action except the items marked **[code]**. (Match ` **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 Android 14+ review. **[code]** The manifest already matches what you will declare: