Skip to content

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

Closed
jkmassel wants to merge 2 commits into
trunkfrom
jkmassel/block-picker-media
Closed

feat(android): add photos and camera media strip to block inserter#479
jkmassel wants to merge 2 commits into
trunkfrom
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-media branch 6 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 9 times, most recently from 34ddf54 to ed272e9 Compare May 4, 2026 17:56
@jkmassel jkmassel force-pushed the jkmassel/android-block-picker branch from ba77b89 to 9ae597c Compare May 11, 2026 21:37
jkmassel added a commit that referenced this pull request May 11, 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 340121d to d262201 Compare May 13, 2026 14:52
jkmassel added a commit that referenced this pull request May 13, 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 added a commit that referenced this pull request May 13, 2026
* feat(android): wire up showBlockInserter bridge

Adds the `@JavascriptInterface showBlockInserter(String)` entry point on
`GutenbergView` plus the `presentBlockInserter` / `insertBlock` /
`dismissBlockInserter` round-trip back to JS via
`window.blockInserter.insertBlock` / `onClose`. Bad payloads surface a
Toast (from `R.string.gbk_block_inserter_failure`) so a broken tap isn't
silent. Sets `resourcePrefix = "gbk_"` so AGP flags any unprefixed
library resource at lint time.

The dialog itself lands in #480; this commit only wires the bridge so
that PR can build atop it.

* feat(android): add native block inserter (#480)

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.

* feat(android): render branded block icons with contrast-aware tinting (#468)

* feat(android): render SVG icons in block inserter

Adopts AndroidSVG (com.caverock:androidsvg-aar:1.4) — the same rendering
engine Coil-SVG wraps, used directly to avoid pulling in Coil — and wires
it into the block inserter dialog introduced in #461. Mirrors
`BlockIconCache` on iOS: parse each block's inline SVG once, keyed by
`BlockType.id`, and cache the rendered bitmap so RecyclerView rebinds
don't re-render on scroll.

Three @wordpress/icons patterns the browser handles via CSS that
AndroidSVG does not:

1. **Missing `viewBox`** (e.g. `core/site-tagline`) — intrinsic
   width/height are declared but paths render at native coordinate size
   inside our larger viewport, so the icon appears tiny. Synthesise a
   viewBox from the intrinsic dimensions and set document width/height
   to 100%.
2. **`fill="none"` at root** (e.g. `core/icon`) — paths without an
   explicit fill inherit `none` and render invisibly. The web editor's
   `@wordpress/components` CSS injects `fill: currentColor`; we do the
   same via `RenderOptions.css` at render time.
3. **Multi-fill branded icons** (e.g. Pocket Casts, Animoto) — these
   rely on colour contrast between inner/outer paths. A monochrome
   PorterDuff SRC_IN tint flattens them into a silhouette. Detect hex
   fills in the raw SVG string and skip the tint when present, letting
   branded icons render as-is.

Pocket Casts still renders black-and-white rather than brand red — its
foreground colour lives in JS metadata (`icon.foreground`) that
`getBlockIcon` at `src/utils/blocks.js:44` drops. Fixing this properly
requires piping `foreground` through the bridge and is deferred to a
follow-up.

- [x] `./gradlew :Gutenberg:test detekt :Gutenberg:lintDebug :app:compileDebugKotlin` — BUILD SUCCESSFUL
- [x] Manually verified on Pixel 9 Pro XL with `enableNativeBlockInserter` on: open inserter, scroll through all sections, confirmed Site Tagline (no viewBox), Icon block (root `fill="none"`), and Pocket Casts/Animoto/Bluesky (branded colours) all render correctly.
- [ ] Reviewer: verify iOS behaviour unchanged.

* feat(android): contrast-aware tinting for branded block icons

Follow-up to #461 / 444c640 that addresses the "known limitation" called
out in that commit: branded icons (Pocket Casts, Animoto, Bluesky) and
single-colour brand foregrounds (X, Dailymotion, WordPress Embed) now
render correctly against either light or dark dialog surfaces.

`getBlockIcon` at `src/utils/blocks.js:44` previously dropped
`icon.foreground` — the JS metadata that the web editor applies as CSS
`color`, which paths inside the SVG pick up via `currentColor`. Adds
`getBlockIconForeground` and includes it in the serialised inserter
payload, with matching `iconForeground: String?` fields on the Android
`BlockType` data class and the iOS `BlockType` struct (iOS gets the
field for parity; no rendering change).

Wraps each icon in a 44dp `RoundedRectangle` chip filled with ~12% of
the theme's primary text colour, mirroring the iOS `BlockIconView`
treatment (`Color(.label).mask` over a `secondarySystemFill` chip).
Without this, low-contrast brand icons (X's `#000000`, Dailymotion's
`#333436`) rendered as near-invisible smudges on the dark bottom sheet.

`SvgIconCache.shouldTint` decides per-icon whether to apply the theme
tint:

1. **No declared colours** — pure monochrome; tint to text colour.
2. **Multiple declared colours** (Pocket Casts red+white, Animoto,
   Bluesky) — render as-is. A PorterDuff SRC_IN tint would flatten
   internal contrast into a silhouette.
3. **Single declared colour** — keep it if it has at least 3:1 contrast
   (WCAG 2.x SC 1.4.11 — minimum for UI graphics) against the surface
   the icon actually sits on; otherwise strip the brand colour and tint.

The contrast reference is the chip fill **composited over the dialog
surface**, not the bare surface. Measuring against bare black makes
marginal colours like WordPress blue (#0073AA) appear to pass at 3.16:1
while reading as dim against the actual ~`#3B3B3D` chip surrogate
(2.1:1). `resolveDialogSurface()` looks up `?attr/colorSurface` then
`?android:attr/colorBackground` so the surrogate matches what the user
sees.

- [x] `./gradlew :Gutenberg:test detekt :Gutenberg:lintDebug :app:compileDebugKotlin` — BUILD SUCCESSFUL
- [x] Manually verified on Pixel 9 Pro XL with `enableNativeBlockInserter` on, dark theme: Pocket Casts renders red+white, Animoto/Bluesky render branded, X / Dailymotion / WordPress Embed render white (tint fallback), Site Tagline / Icon block still render correctly.
- [ ] Reviewer: verify in light theme; verify iOS behaviour unchanged.

* refactor(android): address review feedback in BlockInserterDialog

Three fixes from #468 review:

- **resolveTextColorPrimary fallback** — `resolveAttribute` can return
  false (attribute absent) or leave `typed.resourceId == 0`, in which
  case `getColorStateList(0, …)` throws. Mirror the pattern already used
  by `resolveDialogSurface` and fall back to `Color.BLACK`.
- **BlockViewHolder positional access** — `(container.getChildAt(0) as
  FrameLayout).getChildAt(0) as ImageView` silently breaks if the view
  hierarchy is reordered. `buildBlockView` now returns a `BlockRowViews`
  struct holding direct references; the ViewHolder takes that struct
  rather than the root `View`.
- **rightMargin → marginEnd** — `rightMargin` is physical, `marginEnd`
  is layout-direction-aware so the chip leads correctly under RTL.

Verified manually on Pixel 9 Pro XL: most-used and embed sections still
render correctly with chip-backed icons.

* refactor(android): drop over-defensive fallback in resolveTextColorPrimary

The Color.BLACK fallback only triggered when theme.resolveAttribute
returned false — i.e. the host app's theme genuinely doesn't define
android.R.attr.textColorPrimary, which has been part of the framework
since API 1. Standard theme parents (AppCompat, Material*, even bare
@android:style/Theme) all define it. Bailing out a host that's already
broken in many other ways isn't our job.

Keep the typed.resourceId != 0 branch — that handles legitimate inline
literal themes (`<item name="...">#DD000000</item>`), which is real
real-world theme construction, not misconfiguration.

* feat(android): render icons and wrap labels in the block inserter (#481)

* feat(android): render block icons in the native inserter

Replaces the tonal placeholder Box in `BlockPickerDialog.BlockTile` with
the actual SVG icon for each block, sourced from `SvgIconCache` (added
in #461 alongside this dialog but never wired up). Without this change
the inserter renders rows of identical solid chips — every block is
visually indistinguishable apart from its label.

The cache lives at the `BlockGridContent` level and is keyed on
`(renderSizePx, chipColor)` so a font-scale or theme change recomputes
both the bitmap resolution and the contrast surrogate the tinting
decision in `SvgIconCache.shouldTint` is measured against. The render
size is the inner glyph (`BLOCK_TILE_ICON_GLYPH_SIZE_DP`, 32dp), not
the 56dp chip — the bitmap pixels need to be sharp at the displayed
size, not at the chip dimensions.

Branded icons (Pocket Casts, Pinterest, Reddit) flow through with no
tint; monochrome and low-contrast brand icons get
`ColorFilter.tint(onSurface)` so they read against the chip. Disabled
blocks dim via `Image.alpha` rather than baking the alpha into the
tint, so branded icons fade uniformly instead of going monochrome on
disable.

* feat(android): wrap two-line labels with tight line height in the inserter

`AutoShrinkTileLabel` previously shrank labels down to 9sp until they fit
on a single line. Three issues with the result:

1. **Single-word and multi-word were treated identically.** "Featured
   Image" was shrunk to fit on one line when it could have wrapped at
   the space at full size and stayed legible.
2. **Mid-word breaks on long single words.** Naively switching to
   `maxLines = 2` made Compose character-break "Preformatted" into
   "Preform" / "atted" rather than shrink it. The default soft-wrap
   will break inside a word to satisfy the line budget, so we now
   measure single-word labels with `maxLines = 1` and render with
   `softWrap = false` to force the shrink path.
3. **Loose vertical rhythm on wrapped labels.** With no explicit
   `lineHeight`, the gap between the two wrapped lines tracked the
   font's default leading and read as too tall. `Material 3`'s
   typography presets fix this with `LineHeightStyle.Trim.Both`, but
   custom `sp + lineHeight` doesn't pick that up — set it explicitly
   so labels sit centred against the reserved two-line height.

`minLines = 2` reserves two lines worth of vertical space at the
resolved size so tile heights stay consistent across the grid, even
when neighbouring labels need different line counts.

* fix(android): re-render block icons when the SVG cache is replaced

`rememberSvgIconCache` correctly returns a new `SvgIconCache` instance
when `sizePx` or `surfaceArgb` changes — i.e. on a theme or font-scale
switch. `BlockTile` was keying its `remember` only on `block.id`, so it
returned the previously-rendered `Icon` from the now-stale cache.
Visible result: icons keep using the old contrast surrogate (and the
old bitmap pixels) until the tile leaves and re-enters composition.

Adding `iconCache` to the key forces the lambda to re-run whenever the
cache reference changes, which re-renders each SVG against the new
surface and rebuilds the tinting decision. The new bitmap is the
correct one to display going forward.

Caught by @adalpari in #481 review.

* feat(android): wire up block inserter search and tab filtering (#478)

Activates the chips and search field shipped as a non-functional shell
in the previous PR. Tab selection narrows the grid to a fixed set of
block ids per category (Text/Media/Design/Widgets/Theme/Embeds), and
the Recent tab surfaces blocks from the payload's `gbk-most-used`
section when present, falling back to all blocks otherwise.

Search runs against the active tab's results with a 150ms debounce and
a fuzzy scorer that weights name/title/keyword/category matches. Empty
result sets render the No results / "No blocks match X" copy.

* fix(js): skip redux toggle when opening native block inserter (#487)

Tapping the `+` button currently calls `setIsInserterOpened(true)` in
the editor store, which momentarily renders Gutenberg's web inserter
before the native dialog covers it — visible as a flash on slower
devices. Call `prepareAndShowInserter()` directly instead; the redux
flag isn't useful when the inserter is rendered natively.
Base automatically changed from jkmassel/android-block-picker to trunk May 13, 2026 16:36
jkmassel added 2 commits May 13, 2026 10:40
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.
@jkmassel jkmassel force-pushed the jkmassel/block-picker-media branch from df4931c to a086223 Compare May 13, 2026 16:43
@wpmobilebot
Copy link
Copy Markdown

XCFramework Build

This PR's XCFramework is available for testing. Add the following to your Package.swift:

.package(url: "https://github.com/wordpress-mobile/GutenbergKit", branch: "pr-build/479")

Built from a086223

@jkmassel
Copy link
Copy Markdown
Contributor Author

Superseded by four smaller PRs that split the media-strip work into reviewable chunks:

Closing this in favour of the stack.

@jkmassel jkmassel closed this May 13, 2026
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.

2 participants