Skip to content

Handle unresolvable attachments in picker#6285

Merged
VelikovPetar merged 1 commit intodevelopfrom
AND-1103-google-drive-attachment-issue
Mar 24, 2026
Merged

Handle unresolvable attachments in picker#6285
VelikovPetar merged 1 commit intodevelopfrom
AND-1103-google-drive-attachment-issue

Conversation

@andremion
Copy link
Copy Markdown
Contributor

@andremion andremion commented Mar 24, 2026

Goal

Fix a crash (FileNotFoundException) when users attach files from Google Drive or other cloud storage providers through the attachment picker. The content URI cannot always be resolved to a local input stream, causing an unhandled exception.

Implementation

  • Wrap contentResolver.openInputStream() in StorageHelper.getCachedFileFromUri with a try-catch so unresolvable URIs return null instead of crashing.
  • Propagate nullability upward: skip unresolvable attachments in both Compose (StorageHelperWrapper) and UI Components (AttachmentMetaDataMapper) paths.
  • Show a toast ("Some files could not be loaded and were skipped.") when attachments are skipped, in both Compose and UI Components pickers.

Testing

  1. Connect a Google Drive account on an Android device.
  2. Open a chat conversation and tap the attachment icon.
  3. Navigate to Google Drive and select a cloud-only file (not downloaded locally).
  4. Expected: The file is skipped, a toast message appears ("Some files could not be loaded and were skipped."), and the app does not crash.
  5. Select a mix of local files and cloud-only files.
  6. Expected: Only the local files are attached; the toast appears for the skipped ones.
  7. Select only local files.
  8. Expected: All files are attached normally, no toast shown.
  9. Repeat steps 3–8 using the UI Components attachment picker (AttachmentsPickerDialogFragment).

Summary by CodeRabbit

  • New Features

    • Added user-friendly notification when some attachment files cannot be loaded.
  • Bug Fixes

    • Improved handling of attachments that cannot be resolved or accessed.
    • Unresolvable files are now gracefully skipped instead of causing the picker to fail.
    • Enhanced error recovery for cloud-backed or inaccessible file URIs.
  • Tests

    • Added test coverage for unresolved attachment behavior.

@andremion andremion added the pr:bug Bug fix label Mar 24, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 24, 2026

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled.

🎉 Great job! This PR is ready for review.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 24, 2026

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.26 MB 5.26 MB 0.00 MB 🟢
stream-chat-android-offline 5.49 MB 5.49 MB 0.00 MB 🟢
stream-chat-android-ui-components 10.63 MB 10.63 MB 0.00 MB 🟢
stream-chat-android-compose 12.86 MB 12.86 MB 0.00 MB 🟢

- Update `StorageHelper` and `AttachmentMetaDataMapper` to safely handle cases where content URIs (e.g. cloud-backed files) cannot be opened.
- Introduce `hasUnresolvedAttachments` state in `AttachmentsPickerViewModel` to track failed attachment resolutions.
- Show a toast message in both View-based and Compose attachment pickers when files are unavailable and need to be downloaded to the device.
- Add `clearUnresolvedAttachments` to reset the error state after it has been consumed by the UI.
- Add unit tests for unresolved attachment scenarios in `AttachmentsPickerViewModelTest`.
@andremion andremion force-pushed the AND-1103-google-drive-attachment-issue branch from 40ac853 to 7678df3 Compare March 24, 2026 10:53
@andremion andremion marked this pull request as ready for review March 24, 2026 10:58
@andremion andremion requested a review from a team as a code owner March 24, 2026 10:58
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
34.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 24, 2026

Walkthrough

The changes introduce error handling for attachments that cannot be resolved during the attachment picker flow. When attachment resolution fails, the system sets a flag, displays a user-facing error toast, filters out unresolvable items, and clears the flag. Storage layer operations now gracefully return null for unresolvable attachments instead of propagating exceptions.

Changes

Cohort / File(s) Summary
ViewModel State Management
stream-chat-android-compose/src/main/java/.../viewmodel/messages/AttachmentsPickerViewModel.kt, stream-chat-android-compose/src/test/kotlin/.../AttachmentsPickerViewModelTest.kt
Added hasUnresolvedAttachments observable state flag and clearUnresolvedAttachments() function. Modified getSelectedAttachments() and getAttachmentsFromMetadataAsync() to detect when attachment resolution produces fewer results than inputs and set the flag accordingly. Added four test cases covering initialization, resolution success/failure, and flag reset.
Attachment Resolution Layer
stream-chat-android-ui-common/src/main/kotlin/.../helper/internal/StorageHelper.kt, stream-chat-android-compose/src/main/java/.../ui/util/StorageHelperWrapper.kt, stream-chat-android-ui-components/src/main/kotlin/.../composer/internal/AttachmentMetaDataMapper.kt
StorageHelper.getCachedFileFromUri() now wraps stream opening in try/catch, logging exceptions and returning null instead of propagating errors. StorageHelperWrapper switched to mapNotNull() to skip unresolvable entries and logs warnings. AttachmentMetaDataMapper.toAttachment() changed return type to nullable and guards against missing cached files.
UI Error Feedback
stream-chat-android-compose/src/main/java/.../messages/attachments/AttachmentsPicker.kt, stream-chat-android-ui-components/src/main/kotlin/.../attachment/picker/AttachmentsPickerDialogFragment.kt, stream-chat-android-ui-common/src/main/res/values/strings.xml
AttachmentsPicker added LaunchedEffect observing hasUnresolvedAttachments to display a long-duration error toast and trigger flag reset. AttachmentsPickerDialogFragment uses mapNotNull() to filter unresolvable attachments and shows the same error toast when losses occur. New string resource added for the unresolvable attachments error message.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant AP as AttachmentsPicker
    participant VM as AttachmentsPickerViewModel
    participant SHW as StorageHelperWrapper
    participant SH as StorageHelper

    User->>AP: Select attachments
    AP->>VM: getSelectedAttachments()
    VM->>SHW: getAttachmentsForUpload(metadata)
    SHW->>SH: mapNotNull - getCachedFileFromUri(each metadata)
    SH-->>SHW: File or null (with error logging if failure)
    SHW-->>VM: Filtered Attachment list (smaller if any unresolvable)
    VM->>VM: Detect: resolved count < input count
    VM->>VM: Set hasUnresolvedAttachments = true
    VM-->>AP: Return filtered attachments
    AP->>AP: LaunchedEffect observes flag change
    AP->>User: Show error toast (unresolvable attachments)
    AP->>VM: clearUnresolvedAttachments()
    VM->>VM: Reset flag to false
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Hop through attachments with grace,
When files won't load, we log the case,
A gentle toast shows what went wrong,
Filter and skip to move along!
Error handling, fluffy and bright,

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Handle unresolvable attachments in picker' directly and clearly summarizes the main change: adding support for gracefully handling attachments that cannot be resolved, particularly from cloud storage providers.
Description check ✅ Passed The PR description provides Goal, Implementation, and Testing sections with clear details about the crash fix and how to test it. While UI Changes and full Contributor/Reviewer checklists are not completed, the core required information is present and substantive.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch AND-1103-google-drive-attachment-issue

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentsPicker.kt (1)

108-120: Minor simplification: Use resource ID directly in Toast.

Toast.makeText() accepts a resource ID directly, so context.getString() is unnecessary.

✨ Suggested simplification
     LaunchedEffect(hasUnresolvedAttachments) {
         if (hasUnresolvedAttachments) {
             Toast.makeText(
                 context,
-                context.getString(R.string.stream_ui_attachment_picker_error_unresolvable_attachments),
+                R.string.stream_ui_attachment_picker_error_unresolvable_attachments,
                 Toast.LENGTH_LONG,
             ).show()
             attachmentsPickerViewModel.clearUnresolvedAttachments()
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentsPicker.kt`
around lines 108 - 120, The Toast creation in the LaunchedEffect uses
context.getString(...) unnecessarily; update the Toast.makeText call in the
AttachmentsPicker composable (where LocalContext.current,
hasUnresolvedAttachments, and
attachmentsPickerViewModel.clearUnresolvedAttachments are used) to pass the
string resource ID
(R.string.stream_ui_attachment_picker_error_unresolvable_attachments) directly
instead of context.getString(...), leaving the context and Toast.LENGTH_LONG
parameters unchanged.
stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/internal/AttachmentMetaDataMapper.kt (1)

32-32: Consider caching or injecting the StorageHelper instance.

A new StorageHelper() is created on every call to toAttachment(). While likely not a performance concern for typical attachment counts, consider whether this mapper could receive a shared instance, especially if this method is called in a loop for many attachments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/internal/AttachmentMetaDataMapper.kt`
at line 32, AttachmentMetaDataMapper currently constructs a new StorageHelper on
every call to toAttachment (see the fileFromUri assignment), which can be
optimized by injecting or reusing a shared StorageHelper instance; modify
AttachmentMetaDataMapper to accept a StorageHelper via constructor (or provide a
lazily initialized/shared instance) and replace the inline new StorageHelper()
call in toAttachment with that injected/shared instance so repeated toAttachment
calls reuse the same StorageHelper.
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/AttachmentsPickerViewModel.kt (1)

237-247: Thread-safety consideration for hasUnresolvedAttachments modification.

getSelectedAttachments() is public and modifies hasUnresolvedAttachments. When called via getSelectedAttachmentsAsync(), this modification occurs on DispatcherProvider.IO. While Compose's mutableStateOf is thread-safe for reads, writes should typically occur on the main thread.

Consider moving the flag update to the main thread:

🔧 Suggested fix
     internal fun getSelectedAttachmentsAsync(onComplete: (List<Attachment>) -> Unit) {
         viewModelScope.launch {
-            val attachments = withContext(DispatcherProvider.IO) {
-                getSelectedAttachments()
+            val dataSet = if (attachmentsPickerMode == Files) files else images
+            val selectedMetaData = dataSet
+                .filter(AttachmentPickerItemState::isSelected)
+                .map(AttachmentPickerItemState::attachmentMetaData)
+            val attachments = withContext(DispatcherProvider.IO) {
+                storageHelper.getAttachmentsForUpload(selectedMetaData)
+            }
+            if (attachments.size < selectedMetaData.size) {
+                hasUnresolvedAttachments = true
             }
             onComplete(attachments)
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/AttachmentsPickerViewModel.kt`
around lines 237 - 247, getSelectedAttachments() computes attachments off the IO
dispatcher (via getSelectedAttachmentsAsync()) but directly writes to the
mutable state hasUnresolvedAttachments from that background context; move the
state mutation to the main thread by performing only the expensive work
(storageHelper.getAttachmentsForUpload and size comparison) on IO and then
posting the result to the main dispatcher—e.g., after computing attachments and
determining the boolean (unresolved = attachments.size < selectedMetaData.size),
dispatch a coroutine on the main dispatcher (using viewModelScope and
DispatcherProvider.main()/Dispatchers.Main) to set hasUnresolvedAttachments =
unresolved; keep the rest of getSelectedAttachments/getSelectedAttachmentsAsync
logic intact and only relocate the write to hasUnresolvedAttachments onto the
main thread.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentsPicker.kt`:
- Around line 108-120: The Toast creation in the LaunchedEffect uses
context.getString(...) unnecessarily; update the Toast.makeText call in the
AttachmentsPicker composable (where LocalContext.current,
hasUnresolvedAttachments, and
attachmentsPickerViewModel.clearUnresolvedAttachments are used) to pass the
string resource ID
(R.string.stream_ui_attachment_picker_error_unresolvable_attachments) directly
instead of context.getString(...), leaving the context and Toast.LENGTH_LONG
parameters unchanged.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/AttachmentsPickerViewModel.kt`:
- Around line 237-247: getSelectedAttachments() computes attachments off the IO
dispatcher (via getSelectedAttachmentsAsync()) but directly writes to the
mutable state hasUnresolvedAttachments from that background context; move the
state mutation to the main thread by performing only the expensive work
(storageHelper.getAttachmentsForUpload and size comparison) on IO and then
posting the result to the main dispatcher—e.g., after computing attachments and
determining the boolean (unresolved = attachments.size < selectedMetaData.size),
dispatch a coroutine on the main dispatcher (using viewModelScope and
DispatcherProvider.main()/Dispatchers.Main) to set hasUnresolvedAttachments =
unresolved; keep the rest of getSelectedAttachments/getSelectedAttachmentsAsync
logic intact and only relocate the write to hasUnresolvedAttachments onto the
main thread.

In
`@stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/internal/AttachmentMetaDataMapper.kt`:
- Line 32: AttachmentMetaDataMapper currently constructs a new StorageHelper on
every call to toAttachment (see the fileFromUri assignment), which can be
optimized by injecting or reusing a shared StorageHelper instance; modify
AttachmentMetaDataMapper to accept a StorageHelper via constructor (or provide a
lazily initialized/shared instance) and replace the inline new StorageHelper()
call in toAttachment with that injected/shared instance so repeated toAttachment
calls reuse the same StorageHelper.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: aee5ca77-d6f0-495d-8d07-35ff2bc36c5c

📥 Commits

Reviewing files that changed from the base of the PR and between cdccfd9 and 7678df3.

📒 Files selected for processing (8)
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/AttachmentsPicker.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StorageHelperWrapper.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/AttachmentsPickerViewModel.kt
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/messages/AttachmentsPickerViewModelTest.kt
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/helper/internal/StorageHelper.kt
  • stream-chat-android-ui-common/src/main/res/values/strings.xml
  • stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/AttachmentsPickerDialogFragment.kt
  • stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/internal/AttachmentMetaDataMapper.kt

@VelikovPetar VelikovPetar merged commit 4924fd1 into develop Mar 24, 2026
17 of 18 checks passed
@VelikovPetar VelikovPetar deleted the AND-1103-google-drive-attachment-issue branch March 24, 2026 17:11
@stream-public-bot stream-public-bot added the released Included in a release label Mar 26, 2026
@stream-public-bot
Copy link
Copy Markdown
Contributor

🚀 Available in v6.36.0

andremion added a commit that referenced this pull request Apr 7, 2026
* Improve `Message.createdLocallyAt` creation logic using estimated server time (#6199)

* Fix createdLocallyAt using NTP-style server clock offset estimation

Co-Authored-By: Claude <noreply@anthropic.com>

* Pr remarks

* Adjust thread message createdLocallyAt.

* Ensure exceedsSyncThreshold is compared against estimated server time (where applicable).

* Add max allowed offset.

---------

Co-authored-by: Claude <noreply@anthropic.com>

* [skip ci] Update SDK sizes

* Update README cover image (#6282)

* Fix XML image flicker caused by `interceptorCoroutineContext(Dispatchers.IO)` (#6284)

Co-authored-by: Claude <noreply@anthropic.com>

* [skip ci] Update SDK sizes

* AUTOMATION: Version Bump

* Fix race condition in plugin resolution during disconnect (#6269)

* Update `DependencyResolverTest` to verify error handling when dependency resolution races with disconnection.

* Prevent race conditions during disconnects in `ChatClient`.

* Handle unresolvable attachments in picker (#6285)

- Update `StorageHelper` and `AttachmentMetaDataMapper` to safely handle cases where content URIs (e.g. cloud-backed files) cannot be opened.
- Introduce `hasUnresolvedAttachments` state in `AttachmentsPickerViewModel` to track failed attachment resolutions.
- Show a toast message in both View-based and Compose attachment pickers when files are unavailable and need to be downloaded to the device.
- Add `clearUnresolvedAttachments` to reset the error state after it has been consumed by the UI.
- Add unit tests for unresolved attachment scenarios in `AttachmentsPickerViewModelTest`.

* [skip ci] Update SDK sizes

* Fix wrong message selected on quoted message long click (#6292)

* Use type-specific attachment URL fields and deprecate `imagePreviewUrl` (#6280)

* Deprecate imagePreviewUrl and use type-specific attachment URL fields

Co-Authored-By: Claude <noreply@anthropic.com>

* Extract common extensions.

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Expose optional completion callback for audio recording (#6290)

Co-authored-by: Claude <noreply@anthropic.com>

* AUTOMATION: Version Bump

* AUTOMATION: Clean Detekt Baseline Files (#6299)

Co-authored-by: adasiewiczr <17440581+adasiewiczr@users.noreply.github.com>

* Add support for intercepting CDN file requests (#6295)

* Add new CDN contract.

* Add CDN for document files.

* Add CDN support for downloading attachments.

* Deprecate current CDN methods.

* Add progress indicator snackbar.

* Add useDocumentGView config flag.

* Add file sharing cache handling.

* Add file sharing cache handling.

* Remove CDNResponse.kt

* Add tests

* PR remarks

* [skip ci] Update SDK sizes

* Post-merge clean-up.

* Post-merge clean-up.

* ApiDump.

* Improve attachment URI resolution and error handling in `AttachmentsPickerViewModel` and `AttachmentStorageHelper`.

- Add `isUriResolvable` to `StorageHelper` to verify if a content URI can be opened for reading.
- Implement `partitionResolvable` in `AttachmentStorageHelper` to separate metadata based on URI accessibility.
- Update `AttachmentsPickerViewModel.resolveAndSubmitUris` to exclude inaccessible URIs (e.g., undownloaded cloud files) from the submission.
- Ensure `hasUnresolvedAttachments` is correctly set when URIs are inaccessible, independent of file type support.
- Add unit tests in `AttachmentStorageHelperTest` and `AttachmentsPickerViewModelTest` to verify partitioning logic and view model state updates.

* Handle unresolvable attachments in XML

* apiDump.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: André Mion <andremion@gmail.com>
Co-authored-by: Gianmarco <47775302+gpunto@users.noreply.github.com>
Co-authored-by: stream-pr-merger[bot] <117762243+stream-pr-merger[bot]@users.noreply.github.com>
Co-authored-by: adasiewiczr <17440581+adasiewiczr@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:bug Bug fix released Included in a release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants