feat(android): support Android 14 partial photo access#511
Draft
jkmassel wants to merge 3 commits into
Draft
Conversation
Adds a Photos / Camera quick-launch row between the inserter's header
and category tabs:
- Photos tile uses the permissionless system photo picker
(`ActivityResultContracts.PickVisualMedia`) — no manifest permission
required.
- Camera tile uses `ACTION_IMAGE_CAPTURE` against a cache-scoped
FileProvider URI; no `CAMERA` permission required since we delegate
to the system camera app.
The library declares its own FileProvider keyed off
`${applicationId}.gutenberg.fileprovider` so it can't collide with one
a host app already declares. No `uses-permission` lines added at this
stage — the recent-photos thumbnail strip that needs
`READ_MEDIA_IMAGES` lands in a follow-up.
The picker / camera result callbacks are intentionally inert until the
WebViewAssetLoader-based URI hand-off lands; the sheet is gated behind
the demo's "Enable Native Inserter" toggle in the meantime.
Layers on the Photos / Camera tiles introduced in the previous PR with
a full permission state machine, a rationale card, and a recent-photos
thumbnail strip.
- Three media-strip states resolved at runtime by `resolveMediaStripView`
in `PhotoAccessState.kt`:
1. **Rationale card** — shown when `READ_MEDIA_IMAGES` isn't granted
and the user hasn't dismissed it. Body copy and primary-button
label switch across three sub-states (`Unasked` / `Denied` /
`PermanentlyDenied`); SharedPreferences tracks the first-prompt
flag because `shouldShowRequestPermissionRationale` alone can't
tell "never asked" from "permanently denied".
2. **Compact tiles** — once the rationale is rejected, the Photos /
Camera column flattens into a full-width 88dp row. The rejection
auto-clears when permission is later granted, so a future
revocation surfaces the rationale again.
3. **Full strip** — once granted, queries MediaStore for the 64 most
recent images and renders them as a horizontally-scrolling 2-row
thumbnail grid. LazyRow keeps off-screen thumbnails out of memory.
- Observes the host Activity's lifecycle (not the BottomSheetDialog's
own) for `ON_RESUME`, so grants made via system Settings update the
strip on return without restart.
- `GutenbergView.resetBlockPickerPhotoPreferences(context)` exposed for
host apps that want to clear the rationale-rejection / first-prompt
flags from a settings screen — also wired into the demo's `⋮` menu
as **Reset Photo Permissions Prompts**.
- Library declares `READ_MEDIA_IMAGES` and `READ_EXTERNAL_STORAGE`
(max SDK 32). Host apps can opt out via `tools:node="remove"`.
- Demo's "Enable Native Inserter" toggle now defaults to on so
reviewers see the new strip without flipping a setting.
Touch targets on the rationale buttons meet the Material 48dp minimum
via a wrapper that keeps the visual 32dp pill while inflating the click
area; a shared `MutableInteractionSource` so the ripple still draws
inside the pill instead of as a square halo.
Layers on the rationale + recent-photos strip with proper handling for Android 14's "Select photos and videos" partial-access flow. - Declares `READ_MEDIA_VISUAL_USER_SELECTED` in the manifest so the system surfaces the three-way prompt (Allow / Select / Don't allow) and grants the new permission on partial selections. - Switches from `RequestPermission` to `RequestMultiplePermissions` and requests both `READ_MEDIA_IMAGES` and `READ_MEDIA_VISUAL_USER_SELECTED` together. On subsequent calls when partial is already granted, the system reopens the photo picker so the user can update the selection. - `hasPhotosPermission` accepts either full or partial-access reads. - `isPartialPhotoAccess` distinguishes "selected photos only" so the strip can surface a **Manage** tile, sitting right after the Photos/Camera column. The tile re-launches the multi-permission request, which reopens the system picker. - `refreshTick` keys the MediaStore re-query so a selection update (where `granted` stays true but the visible set changes) refreshes the strip. Without this PR, users on Android 14+ who pick "Select photos and videos" see a working strip but have no in-app affordance to update the selection — they'd have to leave for system Settings.
10 tasks
11 tasks
XCFramework BuildThis PR's XCFramework is available for testing. Add the following to your .package(url: "https://github.com/wordpress-mobile/GutenbergKit", branch: "pr-build/511")Built from 06bb7a9 |
7ab5b51 to
47a22e0
Compare
jkmassel
added a commit
that referenced
this pull request
May 14, 2026
… strip
`hasPhotosPermission` previously only checked `READ_MEDIA_IMAGES`. On
Android 14+ (targetSdk >= 34), the system shows a three-option dialog
("Allow all" / "Select photos" / "Don't allow") whenever an app
requests `READ_MEDIA_IMAGES` — empirically verified on Pixel 9 Pro XL
running Android 16 with this PR's manifest, which does *not* declare
`READ_MEDIA_VISUAL_USER_SELECTED`. The system still surfaces the
"Select photos" option without an explicit opt-in.
When the user picked "Select photos", `READ_MEDIA_IMAGES` stayed denied,
our state machine landed on `PermanentlyDenied`, and the rationale told
the user to open Settings — for a permission they had just granted.
This made the partial-grant experience strictly worse than full denial,
since the user knew they'd granted access and saw the app gaslight them.
Also check `READ_MEDIA_VISUAL_USER_SELECTED` (API 34+) when
`READ_MEDIA_IMAGES` is denied. MediaStore automatically scopes our
recent-images query to the user-selected items, so the existing query
path produces the right strip with no further changes.
Declaring `READ_MEDIA_VISUAL_USER_SELECTED` in the library manifest —
which enables the "Select more photos" re-prompt affordance and the
fuller partial-access UX — is the next PR in the stack (#511).
- [x] Verified on Pixel 9 Pro XL / Android 16: fresh app data → tap
Allow → three-option dialog → "Select photos" → choose 3 → strip
renders those 3 thumbnails instead of falling through to the
rationale's "Open Settings" state.
15 tasks
764b1ab to
64a055f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Layers on top of #510 with proper handling for Android 14's "Select photos and videos" partial-access flow.
Android 14 added a third option to the photo-permission dialog: users can pick Allow, Select photos and videos, or Don't allow. Without opting in to the partial-access permission, the system still shows the three-way prompt, but the app sees a partial grant as full access — and the user has no in-app way to update which photos they've shared. The strip would show the same 3 photos forever, with the only path to add more being a trip to system Settings.
This PR closes that gap.
Changes
Declare
READ_MEDIA_VISUAL_USER_SELECTEDin the manifest. This is the partial-access opt-in that signals to the system that we understand and handle partial grants.READ_MEDIA_VISUAL_USER_SELECTEDSwitch the launcher from
RequestPermissiontoRequestMultiplePermissions, requesting bothREAD_MEDIA_IMAGESandREAD_MEDIA_VISUAL_USER_SELECTEDtogether viaphotosPermissions(): Array<String>(API-tiered). When partial is already granted, calling the launcher again reopens the photo picker so the user can update the selection — that's the in-app "Manage" path.hasPhotosPermissionnow accepts either full or partial reads.isPartialPhotoAccessdistinguishes "selected photos only" so the strip can surface a Manage affordance.PhotoAccess.PartialAccessdata class carriesonManageSelection.PhotoAccess.Grantedgains an optionalpartialAccess: PartialAccess?field — non-null only when the user picked partial.ManageSelectionTilesits right after the Photos/Camera column when partial access is active. Uses theTuneicon andsecondaryContainercolour so it reads as an editorial control distinct from Photos/Camera. Visible without scrolling so partial-access users find it immediately.refreshTickkeys the MediaStore re-query so a selection update (wheregrantedstays true but the visible set changes) refreshes the strip —granted/limitalone wouldn't trigger that.Test plan
Requires an Android 14+ device.
READ_MEDIA_VISUAL_USER_SELECTEDavailable) → partial-access path is unreachable. GrantingREAD_MEDIA_IMAGESworks as before.PhotoAccessStateTest::partial access still shows full strippasses../gradlew :Gutenberg:detekt :Gutenberg:assembleDebug :Gutenberg:testDebugUnitTestpasses.