Skip to content

Media sync slice 9: limited photo access and origin re-resolution - #2313

Merged
ericgriffin merged 13 commits into
mainfrom
ericgriffin/media-sync-s9-android-limited
Sep 24, 2026
Merged

ericgriffin merged 13 commits into
mainfrom
ericgriffin/media-sync-s9-android-limited

Conversation

@ericgriffin

Copy link
Copy Markdown
Member

Closes #2121
Refs #1625
Part of #2090

Slice 9 of the media sync program (spec section 6.3): on the device that linked a photo, nothing is called missing merely because the app could not see it. Harness scenario S7 is unskipped and green.

notFound is the one verdict that orphans a row, and the orphan flag syncs to every device, so a wrong one on the linking device spreads everywhere. Three paths produced it without evidence of absence: a limited photo selection, a gallery query that threw, and a lost Android read grant. Each is now inconclusive (accessDenied), which writes nothing.

The change

  • Resolution never prompts. AssetResolutionService called checkPermission, which on mobile is a request and could show the OS prompt from a thumbnail render. It now reads through a new, non-prompting PhotoPickerService.currentPermission; the prompt comes only from the picker and the new "Allow full access" action. A permission read or gallery query that throws is accessDenied (the query used to be unavailable, which read as notFound on the linking device), logged with its stack trace under the media category.
  • Limited access is inconclusive. A limited selection was admitted as full access, so a photo outside it went unfound, was cached as unresolved and orphaned. A miss under limited access is now accessDenied flagged limitedAccess (on ResolutionResult and UnavailableData) and caches nothing.
  • A lost Android grant searches first. The native handler already reports a lost grant as PERMISSION_DENIED; LocalFileResolver ignored the code. On the linking device a content URI that stops reading now searches the photo library by the metadata tiers (new AssetResolutionService.findInLibrary, which needs no stored asset id); a lost grant the search cannot recover is accessDenied, and verify reports it as such. Another device's URI is not searched. The branch is now injectable, so it runs in the Linux test shards.
  • The ways back (decided 2026-09-23). Grid tiles read "Not in your allowed photos". The full-screen viewer and the media info panel offer "Allow full access" (system settings) and "Choose photo again" (the system's limited-selection sheet, PhotoManager.presentLimited; the row keeps its link), and look for the photo again when the user returns. Strings in all 11 locales.

#1625 stays open: per spec 10 it closes once the reporter or the hardware pass confirms the fix on a device.

Spec 6.3 records the decisions; the plan (docs/superpowers/plans/2026-09-23-media-sync-phase2-android-limited.md) records where execution differed from it. Main (with slices 8 and 11) is merged in.

Verification

  • dart format .: no changes. flutter analyze: no issues.
  • Full suite on the merged tree: 31,976 passed, 21 skipped, 0 failed. No two-device scenario remains skipped.
  • New tests for the non-prompting read, the query-failure verdict, both limited exits, findInLibrary, the content-URI branch (recovered, lost grant, failed read, peer row, verify), the placeholder, the actions widget, the viewer and tile gating, and the info panel, plus S7.
  • 12 mutations, each compiling and failing its named test: the non-prompting read, the query-failure verdict, both limited exits (S7 and the tier-3 test), the resolver's pass-through, the grant-lost classification, the peer-row skip, serving a recovered photo, verify's mapping, the viewer-only actions gate, the panel's gallery-only gate, and the limited placeholder message.

Resolution called checkPermission, which on mobile is a request and shows
the OS prompt from a thumbnail render. It now reads the state through a
new PhotoPickerService.currentPermission. A gallery query that throws is
accessDenied rather than unavailable, which on the linking device read as
notFound and orphaned the row, and both failures log their stack trace
under the media category.
Resolution admitted a limited selection as full access, so a photo the
user had not selected went unfound, was cached unresolved, and on the
device that linked it read as notFound, which orphans the row everywhere.
Under limited access a miss is now accessDenied, flagged limitedAccess
through to UnavailableData, and caches nothing. Turns S7 green.
…thing is missing

On the device that linked a file, a content URI that stopped reading was
notFound whatever the reason, which orphans the row. The native handler
already reports a lost grant as PERMISSION_DENIED; the resolver now
searches the photo library by the metadata tiers first (a new
AssetResolutionService.findInLibrary that needs no stored asset id), and a
lost grant the search cannot recover is accessDenied, which verify reports
as such. The branch is now injectable, so it runs in the test shards.
…is out of reach

A photo outside the user's limited selection now reads 'Not in your
allowed photos' on its tile. The full-screen viewer and the info panel
offer 'Allow full access', which opens the system settings, and 'Choose
photo again', which opens the system's limited-selection sheet, and look
for the photo again when the user returns. Grid tiles show the
placeholder alone. Strings in all 11 locales.
…rmission

Slice 8's provider chose its non-prompting read by platform type; the
interface now carries currentPermission, as the origin backfill uses.
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

📦 Build artifacts for this PR · commit 73bc5ce

Platform Download
Android (APK) android-apk
macOS macos-build
Windows windows-build
Linux linux-build

Artifacts expire in 7 days. Downloading requires being signed in to GitHub. macOS needs two extractions: unzip the downloaded artifact, then unzip the submersion-macos.zip inside it to get a runnable submersion.app. The build is ad-hoc signed — right-click → Open on first launch.

Updated automatically on each push.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved access-denied propagation, cache invalidation, settings-return refresh, and site viewer action issues remain.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 2 Medium severity

Open (3)
What changed in this PR

Implements media sync slice 9 by making photo resolution non-prompting, treating limited or failed access as inconclusive, and adding recovery paths for Android grants.

Changes:

  • Adds accessDenied handling and origin-device library re-resolution.
  • Adds limited-access recovery actions and localized messaging.
  • Expands automated coverage, documentation, and S7 scenario validation.
File Summary
test/​helpers/​fake_photo_picker_service.dart Adds permission and query-failure controls.
test/​features/​trips/​presentation/​helpers/​trip_scan_actions_test.dart Updates picker test doubles.
test/​features/​media/​two_device/​resolution_scenarios_test.dart Enables S7 coverage.
test/​features/​media/​presentation/​widgets/​unavailable_media_placeholder_test.dart Tests limited-access messaging.
test/​features/​media/​presentation/​widgets/​media_item_view_test.dart Tests viewer and tile actions.
test/​features/​media/​presentation/​widgets/​media_info_panel_test.dart Tests panel actions.
test/​features/​media/​presentation/​widgets/​limited_access_actions_test.dart Tests access recovery actions.
test/​features/​media/​presentation/​providers/​photo_picker_providers_test.dart Updates picker fake usage.
test/​features/​media/​presentation/​pages/​photo_picker_page_tab_shell_test.dart Updates picker fake usage.
test/​features/​media/​presentation/​pages/​photo_picker_page_session_test.dart Updates picker fake usage.
test/​features/​media/​data/​services/​trip_media_scanner_test.dart Updates picker fake usage.
test/​features/​media/​data/​services/​trip_media_scanner_boundary_test.dart Updates picker fake usage.
test/​features/​media/​data/​services/​asset_resolution_service_test.dart Tests non-prompting resolution.
test/​features/​media/​data/​services/​asset_resolution_permission_test.dart Tests permissions and library recovery.
test/​features/​media/​data/​resolvers/​platform_gallery_resolver_test.dart Tests access verdict propagation.
test/​features/​media/​data/​resolvers/​platform_gallery_resolver_provenance_test.dart Updates picker fake usage.
test/​features/​media/​data/​resolvers/​platform_gallery_resolver_extra_test.dart Updates picker fake usage.
test/​features/​media/​data/​resolvers/​local_file_resolver_content_uri_test.dart Tests content-URI recovery.
test/​features/​media/​data/​repair/​photo_library_candidate_source_test.dart Updates picker fake usage.
lib/​l10n/​arb/​app_zh.arb Adds Chinese translations.
lib/​l10n/​arb/​app_pt.arb Adds Portuguese translations.
lib/​l10n/​arb/​app_nl.arb Adds Dutch translations.
lib/​l10n/​arb/​app_localizations.dart Updates localization interface.
lib/​l10n/​arb/​app_localizations_zh.dart Updates generated Chinese localization.
lib/​l10n/​arb/​app_localizations_pt.dart Updates generated Portuguese localization.
lib/​l10n/​arb/​app_localizations_nl.dart Updates generated Dutch localization.
lib/​l10n/​arb/​app_localizations_it.dart Updates generated Italian localization.
lib/​l10n/​arb/​app_localizations_hu.dart Updates generated Hungarian localization.
lib/​l10n/​arb/​app_localizations_he.dart Updates generated Hebrew localization.
lib/​l10n/​arb/​app_localizations_fr.dart Updates generated French localization.
lib/​l10n/​arb/​app_localizations_es.dart Updates generated Spanish localization.
lib/​l10n/​arb/​app_localizations_en.dart Updates generated English localization.
lib/​l10n/​arb/​app_localizations_de.dart Updates generated German localization.
lib/​l10n/​arb/​app_localizations_ar.dart Updates generated Arabic localization.
lib/​l10n/​arb/​app_it.arb Adds Italian translations.
lib/​l10n/​arb/​app_hu.arb Adds Hungarian translations.
lib/​l10n/​arb/​app_he.arb Adds Hebrew translations.
lib/​l10n/​arb/​app_fr.arb Adds French translations.
lib/​l10n/​arb/​app_es.arb Adds Spanish translations.
lib/​l10n/​arb/​app_en.arb Adds English translations.
lib/​l10n/​arb/​app_de.arb Adds German translations.
lib/​l10n/​arb/​app_ar.arb Adds Arabic translations.
lib/​features/​media/​presentation/​widgets/​unavailable_media_placeholder.dart Displays limited-access state.
lib/​features/​media/​presentation/​widgets/​media_item_view.dart Adds viewer recovery actions.
lib/​features/​media/​presentation/​widgets/​media_info_panel.dart Adds panel recovery actions.
lib/​features/​media/​presentation/​widgets/​limited_access_actions.dart Implements recovery buttons.
lib/​features/​media/​presentation/​providers/​photo_access_providers.dart Provides access state and actions.
lib/​features/​media/​presentation/​providers/​media_resolver_providers.dart Wires library recovery.
lib/​features/​media/​presentation/​providers/​gallery_origin_backfill_provider.dart Uses non-prompting permission reads.
lib/​features/​media/​presentation/​providers/​gallery_cloud_id_backfill_provider.dart Uses non-prompting permission reads.
lib/​features/​media/​presentation/​pages/​media_viewer_page.dart Enables viewer recovery actions.
lib/​features/​media/​domain/​value_objects/​media_source_data.dart Adds limited-access metadata.
lib/​features/​media/​data/​services/​photo_picker_service.dart Adds current permission API.
lib/​features/​media/​data/​services/​photo_picker_service_mobile.dart Implements mobile permission reads.
lib/​features/​media/​data/​services/​photo_picker_service_desktop.dart Implements desktop permission reads.
lib/​features/​media/​data/​services/​photo_access_actions.dart Wraps system access actions.
lib/​features/​media/​data/​services/​asset_resolution_service.dart Handles limited access and library search.
lib/​features/​media/​data/​resolvers/​platform_gallery_resolver.dart Propagates access-denied verdicts.
lib/​features/​media/​data/​resolvers/​local_file_resolver.dart Handles lost content-URI grants.
docs/​superpowers/​specs/​2026-09-18-media-sync-program-design.md Records slice decisions.
docs/​superpowers/​plans/​2026-09-23-media-sync-phase2-android-limited.md Documents implementation execution.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/features/media/presentation/providers/media_resolver_providers.dart Outdated
Comment thread lib/features/media/data/resolvers/platform_gallery_resolver.dart
Comment thread lib/features/media/presentation/pages/media_viewer_page.dart
A cached gallery mapping whose asset stopped reading (dropped from a
limited selection, or re-indexed) fell straight to the missing verdict;
resolve and verify now search again first and keep an accessDenied
answer. The lost-grant search reduced a search that could not look to
null, so a failed read became notFound; librarySearchOutcome keeps it
accessDenied. The site media viewer offers the access actions too, and
Allow full access refreshes when the app resumes, since opening the
settings returns at once.
findInLibrary returned a cached asset id unproven, and a mapping gone
stale (a second re-index) was then served as nothing on every render,
which on the linking device reads as notFound and never clears, since
resolved entries do not expire. It runs only after a read has failed, so
it now checks the mapping still loads and searches again if not. Also
moves _afterFailedUriRead out of _volumeOnlineOrAssumed's doc comment,
which the insertion had split.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved cache invalidation, stale-ID recovery, and permission-error logging findings remain.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
Resolved since last review (3)

Comment thread lib/features/media/data/services/asset_resolution_service.dart
resolveAssetId and findInLibrary honoured an unexpired unresolved entry
before reading permission, so a miss cached under limited access (which
every older build wrote) or before the user narrowed access still read as
notFound on the linking device. The backoff now reads permission without
prompting: it stands under full access, and is inconclusive otherwise,
flagged limited under a limited selection.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Address resolver backoff, stale gallery-query caching after access changes, and locale-dependent widget assertions.

Review effort: Lite
Findings: None

Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Gallery query cache ignores permission and selection changes

lib/​features/​media/​data/​services/​asset_resolution_service.dart:357

The limited-access decision can use stale gallery results: _galleryQueryCache is keyed only by time range and lives for 30 seconds, so this search may reuse a result captured under full access (or before the user changes the limited selection). After "Choose photo again", that can keep the newly allowed asset invisible; in the opposite direction, a cached full-access candidate can be matched and cached even though it is now hidden, then the resolver falls through to notFound. Make the query cache permission/selection-aware or invalidate it when access changes before applying this verdict.

…taken under

Gallery queries are shared for 30 seconds, keyed only by time window, so
a limited search could reuse one taken under full access and match a
photo that is now hidden, and a photo just added through Choose photo
again stayed invisible until the entry expired. The key now carries the
permission, and coming back from either access action drops the shared
queries, since a changed selection keeps the same permission.
@codecov

codecov Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

The limited-access provider's no-library and failed-read fallbacks, the
desktop currentPermission, findInLibrary with no photo library, a backoff
whose permission read throws, a library search that throws on a failed
content-URI read, verify re-finding a photo under a new id, and the info
panel re-reading access after an action.
@ericgriffin
ericgriffin merged commit 3764842 into main Sep 24, 2026
36 checks passed
@ericgriffin
ericgriffin deleted the ericgriffin/media-sync-s9-android-limited branch September 24, 2026 04:36
@github-project-automation github-project-automation Bot moved this from In review to Done in Submersion Release Tracker Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Media sync slice 9: Android limited photo access and origin-device re-resolution

2 participants