Skip to content

feat(mobile): review remote-trashed assets before syncing deletions on Android#28280

Open
PeterOmbodi wants to merge 264 commits into
immich-app:mainfrom
PeterOmbodi:feat/review-page
Open

feat(mobile): review remote-trashed assets before syncing deletions on Android#28280
PeterOmbodi wants to merge 264 commits into
immich-app:mainfrom
PeterOmbodi:feat/review-page

Conversation

@PeterOmbodi
Copy link
Copy Markdown
Contributor

@PeterOmbodi PeterOmbodi commented May 7, 2026

Description

This PR adds a new Review remote deletions option for Android remote deletion sync.

It is based on the work from the previous review-related PRs:

The existing Android remote deletion sync behavior is preserved. The new option is added next to the existing modes:

  • Sync remote deletions: existing automatic behavior. Matching local assets in backup-selected albums are moved to the device trash when MANAGE_MEDIA is available.
  • Review remote deletions: new behavior. Matching local assets in backup-selected albums are shown for review, and the user decides whether to keep the local asset or move it to trash.
  • Off: existing behavior. The app does not automatically modify local media.

Behavior

On Android, when a remote asset is moved to trash or permanently deleted remotely, the mobile app can detect matching local assets in backup-selected albums.

With Review remote deletions enabled, matching local assets that are currently in backup-selected albums are shown in a review queue instead of being moved automatically. The user can then:

  • keep the local asset on the device
  • move the local asset to the device trash

If album backup selection changes after a remote trash intent was recorded, the review page only shows and applies decisions to matching local assets that are still in backup-selected albums.

Remote restore events can restore matching local trashed assets when the selected mode allows it and the app has the required Android media permission.

Technical Notes

Trash sync flow

The implementation separates remote trash intent handling from local trash snapshot reconciliation:

  • SyncStreamService owns remote trash/restore events from the sync stream.
  • LocalSyncService owns Android local trash snapshot reconciliation.
  • trashSyncEntity stores current review decisions for remote trash intents.
  • trashedLocalAssetEntity stores current known local trash state.

Remote trash events

SyncStreamService handles remote trash intents while remote ids and deleted timestamps are still available.

For remote asset trash/delete events:

  1. Remote asset rows are updated or prepared for deletion.
  2. Matching local assets are looked up in currently backup-selected albums.
  3. Assets with an existing approved/rejected review decision are ignored.
  4. In automatic sync mode, local asset ids are passed to AssetMediaRepository.deleteAll when MANAGE_MEDIA is available.
  5. Only ids returned by deleteAll are recorded as locally trashed.
  6. Pending/rejected review rows for successfully moved checksums are removed.
  7. Unresolved candidates are written to trashSyncEntity as review candidates so they can be surfaced in review mode.

Review decisions

ActionService.resolveRemoteTrash applies explicit user decisions:

  • Reject: marks the remote trash intent as rejected and keeps the local asset.
  • Approve: moves matching local asset ids from currently backup-selected albums through AssetMediaRepository.deleteAll, records successfully moved assets in local trash state, then marks those checksums as approved.

The approval update happens after local trash state is written, because approved rows are excluded from future candidate lookup.

Local trash snapshot sync

LocalSyncService does not derive remote trash candidates.

It reconciles the Android local trash snapshot:

  1. Read current Android trashed assets.
  2. Update trashedLocalAssetEntity.
  3. Remove pending/rejected review rows that are already locally resolved.
  4. Restore local trashed assets for remote restore events when mode and permission allow it.
  5. Run throttled cleanup for stale rows.

Cleanup rules

trashSyncEntity is a current review queue, not a history table.

Rows are cleaned when:

  • the remote asset is restored/alive again
  • the local asset no longer exists
  • the local asset is already in local trash
  • an approved local trash row is no longer backed by local trash state

Approved/rejected decisions suppress repeated remote delete or permanent-delete events until a remote restore invalidates the decision.

Testing

Tested with updated unit/repository coverage for:

  • remote trash candidate lookup
  • review timeline backed by local assets
  • existing automatic remote deletion sync success and partial success
  • review candidate creation
  • user approve/reject actions
  • local trash snapshot reconciliation
  • cleanup of stale/resolved review rows

Manual test path:

  1. Use an Android 12+ device.
  2. Select a local album for backup.
  3. Use an asset from that backup-selected album.
  4. Enable Review remote deletions in Settings -> Advanced.
  5. Grant Manage Media Access when prompted.
  6. From the web app, either move the backed-up asset to trash or permanently delete it.
  7. Run or wait for remote sync on the mobile app.
  8. Verify that the matching local asset appears as pending review.

Review decision branches:

  1. Choose Keep on device.
  2. Verify that the local asset stays on device and is removed from the review queue.
  3. Repeat the remote deletion flow with another asset.
  4. Run or wait for remote sync on the mobile app.
  5. Choose Move to trash.
  6. Verify that the local asset moves to device trash and is removed from the review queue.

Album selection branch:

  1. Repeat the remote deletion flow with another asset.
  2. Run or wait for remote sync on the mobile app.
  3. Verify that the asset appears as pending review.
  4. Remove the asset's local album from backup selection.
  5. Verify that the asset is no longer shown as actionable in the review queue.
  6. Re-select the album for backup and verify that the pending review item becomes visible/actionable again if the review decision was not resolved.

Automatic sync regression branch:

  1. Enable Sync remote deletions.
  2. From the web app, either move a backed-up asset to trash or permanently delete it.
  3. Run or wait for remote sync on the mobile app.
  4. Verify that the matching local asset is moved to device trash automatically when Manage Media Access is available.

Remote restore regression branch:

  1. Restore the asset remotely.
  2. Run or wait for remote sync on the mobile app.
  3. Verify that matching local trashed assets are restored when the selected mode allows it and Manage Media Access is available.

Checklist:

  • I have carefully read CONTRIBUTING.md
  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation if applicable
  • I have no unrelated changes in the PR.
  • I have confirmed that any new dependencies are strictly necessary.
  • I have written tests for new code (if applicable)
  • I have followed naming conventions/patterns in the surrounding code
  • All code in src/services/ uses repositories implementations for database calls, filesystem operations, etc.
  • All code in src/repositories/ is pretty basic/simple and does not have any immich specific logic (that belongs in src/services/)

Please describe to which degree, if any, an LLM was used in creating this pull request.

An LLM was used to review chages and draft this PR description.

Peter Ombodi and others added 30 commits August 11, 2025 09:51
- Refactor TrashSyncService and ActionService
- Respect isSyncApproved value in timeline.trashSyncReview
- Use i18n values instead of hardcoded strings
- Close review page when out-of-sync record count reaches 0
refactor code
refactor outOfSyncCountProvider
create appSettingStreamProvider
ImmichToast: add warning mode, add onTap callback
refactor code
allow/deny buttons on asset preview (draft)
…ssets_v2

# Conflicts:
#	i18n/en.json
#	mobile/drift_schemas/main/drift_schema_v7.json
#	mobile/lib/infrastructure/repositories/db.repository.dart
#	mobile/lib/infrastructure/repositories/db.repository.drift.dart
#	mobile/lib/infrastructure/repositories/db.repository.steps.dart
#	mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart
#	mobile/lib/routing/router.dart
#	mobile/test/drift/main/generated/schema.dart
#	mobile/test/drift/main/generated/schema_v7.dart
show warning mark on avatar instead
rework out-of-sync button
…ssets_v2

# Conflicts:
#	i18n/en.json
#	mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart
#	mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart
#	mobile/lib/widgets/common/immich_sliver_app_bar.dart
#	mobile/lib/widgets/settings/advanced_settings.dart
…sync_assets_v2

# Conflicts:
#	mobile/lib/domain/services/sync_stream.service.dart
#	mobile/lib/domain/services/trash_sync.service.dart
refactor trash_sync service, action service
add label for Deny/Allow actions buttons
delete LocalAssetEntity records when moved to trash
refactor code
…rashed_state

# Conflicts:
#	mobile/test/domain/services/sync_stream_service_test.dart
…n flow

- Add new `local_trashed_asset` table to store metadata of trashed assets
- Save trashed asset info into `local_trashed_asset` before deletion
- Use `local_trashed_asset` as source for asset restoration
- Implement file restoration by `mediaId`
…rashed_state

# Conflicts:
#	mobile/drift_schemas/main/drift_schema_v10.json
#	mobile/lib/infrastructure/repositories/db.repository.dart
#	mobile/lib/infrastructure/repositories/db.repository.drift.dart
#	mobile/lib/infrastructure/repositories/db.repository.steps.dart
#	mobile/test/drift/main/generated/schema_v10.dart
Peter Ombodi added 5 commits May 7, 2026 10:40
Rename the local asset lookup to clarify remote trash candidate behavior and exclude assets that already have accepted or rejected trash sync decisions. Defer approving trash sync entries until local trash processing completes, and avoid updating rejected entries during candidate upserts.
Add repository and service test coverage for the updated trash sync flow.
update action service tests for success, failure, and partial-delete cases.
use `AssetMediaRepository.deleteAll` for automatic remote trash handling
update sync stream tests
@PeterOmbodi PeterOmbodi marked this pull request as draft May 7, 2026 13:54
@PeterOmbodi PeterOmbodi changed the base branch from feat/review-page to main May 7, 2026 13:55
Peter Ombodi added 2 commits May 7, 2026 18:04
move manage media operations into `AssetMediaRepository` and remove the separate local files manager repository/service.
remove unused Pigeon methods.
# Conflicts:
#	mobile/drift_schemas/main/drift_schema_v25.json
#	mobile/lib/domain/models/store.model.dart
#	mobile/lib/infrastructure/repositories/db.repository.dart
#	mobile/lib/infrastructure/repositories/db.repository.drift.dart
#	mobile/lib/infrastructure/repositories/db.repository.steps.dart
#	mobile/lib/services/app_settings.service.dart
#	mobile/lib/widgets/settings/advanced_settings.dart
#	mobile/test/drift/main/generated/schema_v25.dart
@PeterOmbodi PeterOmbodi marked this pull request as ready for review May 7, 2026 16:13
@PeterOmbodi PeterOmbodi marked this pull request as draft May 8, 2026 14:18
@PeterOmbodi PeterOmbodi marked this pull request as ready for review May 8, 2026 16:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants