Skip to content

feat(android): add photos and camera media strip to block inserter#479

Draft
jkmassel wants to merge 2 commits intojkmassel/android-block-pickerfrom
jkmassel/block-picker-media
Draft

feat(android): add photos and camera media strip to block inserter#479
jkmassel wants to merge 2 commits intojkmassel/android-block-pickerfrom
jkmassel/block-picker-media

Conversation

@jkmassel
Copy link
Copy Markdown
Contributor

@jkmassel jkmassel commented Apr 28, 2026

Summary

Adds the recent-photos / Photos / Camera strip between the inserter header and category tabs.

Three runtime states for the strip, resolved by resolveMediaStripView in PhotoAccessState.kt:

  1. Rationale card — shown when READ_MEDIA_IMAGES isn't granted and the user hasn't dismissed it. The card swaps body copy and primary-button label across three sub-states (Unasked / Denied / PermanentlyDenied); the transition to PermanentlyDenied is detected even after a synchronous second deny via a permission-result tick that drives recomposition. SharedPreferences tracks the first-prompt flag because shouldShowRequestPermissionRationale alone can't tell "never asked" from "permanently denied".
  2. Compact tiles — when the user has tapped Reject on the rationale (persisted in SharedPreferences, sticky across launches). Photos/Camera buttons render as a 64dp full-width row; the rationale card is hidden. A granted permission overrides this state — the strip flips back to thumbnails on the next resume.
  3. Full strip — once granted, queries MediaStore for the 64 most recent images and renders a horizontally-scrolling 2-row thumbnail grid. Lifecycle-resume aware, so revoking via system Settings → returning to the app updates the state without restart.

Photos / Camera tiles are usable in every state. Photos launches the system photo picker (permissionless via PickVisualMedia); Camera launches ACTION_IMAGE_CAPTURE against a cache-scoped FileProvider URI. Hand-off of the resulting URI into the editor is a follow-up — the tap-to-insert plumbing needs a WebViewAssetLoader path handler so the JS side can fetch() the content URI.

The library declares its own FileProvider keyed off ${applicationId} so it won't collide with one a host app already provides. Hosts that don't need photo access can tools:node="remove" the manifest permissions.

GutenbergView.resetBlockPickerPhotoPreferences(context) is exposed for host apps that want to clear the rationale-rejection / first-prompt flags from a settings screen — also wired into the demo app's menu as Reset Photo Permissions Prompts so reviewers can replay the rationale flow.

Test plan

  • First launch: rationale card shows with Allow + Reject alongside the Photos/Camera column
  • Tap Allow → system prompt → grant → strip shows recent photos, behind the same Photos/Camera column
  • Deny system prompt once → rationale switches to Try Again
  • Deny system prompt twice → rationale switches to Open Settings (system suppresses the prompt)
  • Tap Reject in rationale → rationale hides, Photos/Camera reflow into a 64dp full-width row; preference persists across dialog dismiss/relaunch and app cold-start
  • After Reject, grant permission via system Settings → return to the app → strip flips to thumbnails (lifecycle resume picks up the new grant)
  • Tap Photos tile → system photo picker opens (works in all three states)
  • Tap Camera tile → camera app opens with a cache-scoped output URI (works in all three states)
  • Vertical drag on the photo strip still drags the bottom sheet (nested-scroll relay)
  • Demo app Reset Photo Permissions Prompts clears SharedPreferences; reopening the inserter shows the rationale fresh
  • ./gradlew :Gutenberg:detekt :Gutenberg:assembleDebug :Gutenberg:testDebugUnitTest passes (includes PhotoAccessStateTest)

@github-actions github-actions Bot added the [Type] Enhancement A suggestion for improvement. label Apr 28, 2026
@jkmassel jkmassel marked this pull request as draft April 28, 2026 19:34
jkmassel added a commit that referenced this pull request Apr 28, 2026
Compose-based bottom sheet that replaces the legacy WebView block picker.
Variation B handoff: drag handle + header, tonal Material 3 palette
(dynamic on API 31+, brand-seeded fallback below), 5-column tile grid
with auto-shrinking labels, scrollable category-tab chips, and a rounded
search field.

Block tiles render plain tonal rounded-rect placeholders for now —
SVG icon rendering lands in #468 (which adds `SvgIconCache` and pipes
`iconForeground` through the JS payload + iOS/Android models). This PR
deliberately stops at the shell so #468 can be reviewed independently.

Tab filter, search filter, photo/camera tiles, and recent-photo strip
ship in #478 / #479 — the chips and search bar are intentionally
non-functional in this PR so the visual shell can be reviewed in
isolation.
@jkmassel jkmassel force-pushed the jkmassel/block-picker-organize branch from 7333438 to 43be5d0 Compare April 28, 2026 19:37
@jkmassel jkmassel force-pushed the jkmassel/block-picker-media branch from 9d824ee to d3a1169 Compare April 28, 2026 19:40
@jkmassel jkmassel force-pushed the jkmassel/block-picker-organize branch from 43be5d0 to bfc85d9 Compare April 28, 2026 19:43
@jkmassel jkmassel changed the base branch from jkmassel/block-picker-organize to jkmassel/block-picker-organize-impl April 28, 2026 19:44
jkmassel added a commit that referenced this pull request Apr 28, 2026
Compose-based bottom sheet that replaces the legacy WebView block picker.
Variation B handoff: drag handle + header, tonal Material 3 palette
(dynamic on API 31+, brand-seeded fallback below), 5-column tile grid
with auto-shrinking labels, scrollable category-tab chips, and a rounded
search field.

Block tiles render plain tonal rounded-rect placeholders for now —
SVG icon rendering lands in #468 (which adds `SvgIconCache` and pipes
`iconForeground` through the JS payload + iOS/Android models). This PR
deliberately stops at the shell so #468 can be reviewed independently.

Tab filter, search filter, photo/camera tiles, and recent-photo strip
ship in #478 / #479 — the chips and search bar are intentionally
non-functional in this PR so the visual shell can be reviewed in
isolation.
@jkmassel jkmassel force-pushed the jkmassel/block-picker-organize-impl branch from 43be5d0 to d2a4a4c Compare April 28, 2026 19:58
@jkmassel jkmassel force-pushed the jkmassel/block-picker-media branch from d3a1169 to c966d1c Compare April 28, 2026 19:58
@jkmassel jkmassel force-pushed the jkmassel/block-picker-organize-impl branch from d2a4a4c to b067ac2 Compare April 28, 2026 20:03
jkmassel added a commit that referenced this pull request Apr 28, 2026
Compose-based bottom sheet that replaces the legacy WebView block picker.
Variation B handoff: drag handle + header, tonal Material 3 palette
(dynamic on API 31+, brand-seeded fallback below), 5-column tile grid
with auto-shrinking labels, scrollable category-tab chips, and a rounded
search field.

Block tiles render plain tonal rounded-rect placeholders for now —
SVG icon rendering lands in #468 (which adds `SvgIconCache` and pipes
`iconForeground` through the JS payload + iOS/Android models). This PR
deliberately stops at the shell so #468 can be reviewed independently.

Tab filter, search filter, photo/camera tiles, and recent-photo strip
ship in #478 / #479 — the chips and search bar are intentionally
non-functional in this PR so the visual shell can be reviewed in
isolation.
@jkmassel jkmassel force-pushed the jkmassel/block-picker-media branch from c966d1c to 4cc843e Compare April 28, 2026 20:09
jkmassel added a commit that referenced this pull request Apr 28, 2026
Compose-based bottom sheet that replaces the legacy WebView block picker.
Variation B handoff: drag handle + header, tonal Material 3 palette
(dynamic on API 31+, brand-seeded fallback below), 5-column tile grid
with auto-shrinking labels, scrollable category-tab chips, and a rounded
search field.

Block tiles render plain tonal rounded-rect placeholders for now —
SVG icon rendering lands in #468 (which adds `SvgIconCache` and pipes
`iconForeground` through the JS payload + iOS/Android models). This PR
deliberately stops at the shell so #468 can be reviewed independently.

Tab filter, search filter, photo/camera tiles, and recent-photo strip
ship in #478 / #479 — the chips and search bar are intentionally
non-functional in this PR so the visual shell can be reviewed in
isolation.
@jkmassel jkmassel force-pushed the jkmassel/block-picker-organize-impl branch from b067ac2 to 180a9b7 Compare April 28, 2026 20:12
@jkmassel jkmassel force-pushed the jkmassel/block-picker-media branch from 4cc843e to 5dbe8f3 Compare April 28, 2026 20:13
jkmassel added a commit that referenced this pull request Apr 29, 2026
Compose-based bottom sheet that replaces the legacy WebView block picker.
Variation B handoff: drag handle + header, tonal Material 3 palette
(dynamic on API 31+, brand-seeded fallback below), 5-column tile grid
with auto-shrinking labels, scrollable category-tab chips, and a rounded
search field.

Block tiles render plain tonal rounded-rect placeholders for now —
SVG icon rendering lands in #468 (which adds `SvgIconCache` and pipes
`iconForeground` through the JS payload + iOS/Android models). This PR
deliberately stops at the shell so #468 can be reviewed independently.

Tab filter, search filter, photo/camera tiles, and recent-photo strip
ship in #478 / #479 — the chips and search bar are intentionally
non-functional in this PR so the visual shell can be reviewed in
isolation.
Base automatically changed from jkmassel/block-picker-organize-impl to jkmassel/android-block-picker April 29, 2026 16:18
jkmassel added a commit that referenced this pull request Apr 29, 2026
Compose-based bottom sheet that replaces the legacy WebView block picker.
Variation B handoff: drag handle + header, tonal Material 3 palette
(dynamic on API 31+, brand-seeded fallback below), 5-column tile grid
with auto-shrinking labels, scrollable category-tab chips, and a rounded
search field.

Block tiles render plain tonal rounded-rect placeholders for now —
SVG icon rendering lands in #468 (which adds `SvgIconCache` and pipes
`iconForeground` through the JS payload + iOS/Android models). This PR
deliberately stops at the shell so #468 can be reviewed independently.

Tab filter, search filter, photo/camera tiles, and recent-photo strip
ship in #478 / #479 — the chips and search bar are intentionally
non-functional in this PR so the visual shell can be reviewed in
isolation.
@jkmassel jkmassel force-pushed the jkmassel/android-block-picker branch from dfaf054 to 4067726 Compare April 29, 2026 17:50
@jkmassel jkmassel force-pushed the jkmassel/block-picker-media branch from 5dbe8f3 to a878277 Compare April 29, 2026 19:32
@jkmassel jkmassel changed the base branch from jkmassel/android-block-picker to jkmassel/block-picker-search April 29, 2026 22:15
@jkmassel jkmassel force-pushed the jkmassel/block-picker-media branch 7 times, most recently from f9ac5cd to 8a32386 Compare April 29, 2026 22:55
@jkmassel jkmassel force-pushed the jkmassel/block-picker-search branch from d8ae56e to 79c5380 Compare April 30, 2026 15:34
@jkmassel jkmassel force-pushed the jkmassel/block-picker-media branch 8 times, most recently from 7e42816 to 4a31671 Compare April 30, 2026 21:21
Base automatically changed from jkmassel/block-picker-search to jkmassel/android-block-picker May 4, 2026 15:12
@jkmassel jkmassel force-pushed the jkmassel/block-picker-media branch 8 times, most recently from 7833b4a to 34ddf54 Compare May 4, 2026 17:51
Adds the recent-photos / Photos / Camera strip between the inserter
header and category tabs. Three states, picked at runtime:

1. **Permission rationale card** — shown when the host app hasn't yet
   been granted READ_MEDIA_IMAGES. The card switches body copy and
   primary-button label based on whether we've never asked, were
   denied once, or were permanently denied. SharedPreferences tracks
   the first prompt because `shouldShowRequestPermissionRationale`
   alone can't distinguish "never asked" from "permanently denied".
2. **Recent photos strip** — once granted, queries MediaStore for the
   12 most recent images and renders them as 2-row thumbnail tiles.
3. **Photos / Camera tiles** — Photos launches the system photo
   picker (permissionless via `PickVisualMedia`); Camera launches
   `ACTION_IMAGE_CAPTURE` against a cache-scoped FileProvider URI.
   Hand-off of the picked URI to editor insertion is a follow-up.

The library declares its own FileProvider keyed off `${applicationId}`
so it won't collide with one a host app already provides. Host apps
that don't need photo access can `tools:node="remove"` the manifest
permissions.
- Observe the host Activity's lifecycle (not the BottomSheetDialog's)
  so the photo-access state refreshes when the user returns from system
  Settings. The dialog's own LifecycleRegistry only dispatches ON_RESUME
  from `show()` and never refires on activity resume.
- Switch the recent-photo strip to LazyRow so the 64 thumbnails aren't
  all decoded into memory upfront.
- Replace the `permissionTick` snapshot-read trick with a `canReprompt`
  state, written explicitly by the launcher callback and resume observer.
- Declare `READ_MEDIA_VISUAL_USER_SELECTED`, request both photo
  permissions together via `RequestMultiplePermissions`, detect partial
  grants, and surface a "Manage" tile in the strip when only partial is
  granted (Android 14+).
- Clear the rationale-rejected flag once the permission is observed
  granted, so a later revocation surfaces the rationale again instead of
  trapping the user in the compact-tiles state.
- Inflate touch targets on rationale buttons and category chips to the
  Material 48dp minimum without changing the visual heights, using a
  shared MutableInteractionSource so the ripple still draws inside the
  rounded pill.
- Drop the deprecated `androidx.compose.ui.platform.LocalLifecycleOwner`
  import.
- Add a TODO for cleaning up orphaned camera capture files when the
  editor URI hand-off lands.
- Default the demo's "Enable Native Inserter" toggle to on so reviewers
  see the new sheet without flipping a setting.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant