Skip to content

Fix rememberCaptureMediaLauncher not surviving Activity death#6177

Merged
VelikovPetar merged 6 commits intodevelopfrom
bug/AND-1067_fix_attachment_picker_not_surviving_process_death
Feb 23, 2026
Merged

Fix rememberCaptureMediaLauncher not surviving Activity death#6177
VelikovPetar merged 6 commits intodevelopfrom
bug/AND-1067_fix_attachment_picker_not_surviving_process_death

Conversation

@VelikovPetar
Copy link
Contributor

@VelikovPetar VelikovPetar commented Feb 20, 2026

Goal

Fix the attachment picker's media capture flow not surviving Activity death (e.g. when the "Don't keep activities" developer option is enabled or the system kills the activity under memory pressure).

When the user opens the attachment picker and launches the device camera, the hosting Activity may be destroyed while the camera app is in the foreground. On return, the captured media was lost because:

  1. AttachmentsPickerViewModel.isShowingAttachments was a plain mutableStateOf — reset to false on recreation — so the picker composable tree was never recomposed, and rememberLauncherForActivityResult callbacks were never re-registered to receive the pending result.
  2. CaptureMediaContract.pictureFile / videoFile were private in-memory fields lost on process death, causing parseResult to return null even though the camera wrote a valid file to disk.
  3. The camera was launched unconditionally via LaunchedEffect(Unit), so on activity recreation it would attempt to re-launch the camera instead of waiting for the pending result from the previous launch.

Note: I will create a separate PR for the XML UI SDK.

Implementation

AttachmentsPickerViewModel — persist picker visibility across process death

  • Accept a SavedStateHandle as a constructor parameter (with @JvmOverloads for backward compatibility).
  • Back isShowingAttachments with both MutableState (for Compose recomposition) and SavedStateHandle (for persistence). On recreation, the saved value is restored so the picker composable tree is recomposed and activity-result launchers can re-register.

MessagesViewModelFactory — supply SavedStateHandle to the ViewModel

  • Override create(Class<T>, CreationExtras) to obtain a SavedStateHandle via CreationExtras.createSavedStateHandle() when constructing AttachmentsPickerViewModel. Falls back to the existing create(Class<T>) for all other ViewModels.

CaptureMediaContract — expose file references and add a creation callback

  • Make pictureFile and videoFile public (annotated @InternalStreamChatApi) so they can be restored externally after process death.
  • Add an onFilesCreated callback parameter, invoked inside createIntent after destination files are created, giving callers a chance to persist the file paths before the external camera activity starts.

CaptureMediaLauncher — persist destination file paths via rememberSaveable

  • Introduce rememberCaptureMediaLauncherInternal which stores pictureFilePath and videoFilePath via rememberSaveable. On recreation, these paths are restored onto the CaptureMediaContract before rememberLauncherForActivityResult re-registers and dispatches the pending result.
  • Add rememberCancelAwareCaptureMediaLauncher internal variant that forwards null on cancellation so callers can react (e.g. dismiss the picker).

AttachmentsPickerMediaCaptureTabFactory — guard against re-launching the camera

  • Switch to the new rememberCancelAwareCaptureMediaLauncher instead of manually creating the contract.
  • Track hasLaunched via rememberSaveable to prevent LaunchedEffect from re-launching the camera after activity recreation.

UI Changes

Before After
Default picker
default-before.mp4
default-after.mp4
System picker
system-before.mp4
system-after.mp4

Testing

This can be tested by enabling "Don't keep activities" in Android Developer Options:

  1. Open a channel and tap the attachment picker.
  2. Switch to the media capture tab and launch the camera.
  3. Capture a photo or video and confirm.
  4. Verify the captured media is correctly attached to the message composer (instead of being lost or the camera re-launching).

Additionally, verify the normal (non-process-death) flow still works:

  1. Open the attachment picker, capture media, and confirm it attaches correctly.
  2. Open the attachment picker, launch the camera, then cancel — verify the picker dismisses properly.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved state preservation for the attachments picker when the app is interrupted or backgrounded.
    • Enhanced media capture functionality with better handling of cancellations and process interruptions.
  • Chores

    • Updated view model factory infrastructure for improved lifecycle management and state restoration.

@VelikovPetar VelikovPetar added the pr:bug Bug fix label Feb 20, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Feb 20, 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
Contributor

github-actions bot commented Feb 20, 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.48 MB 5.48 MB 0.00 MB 🟢
stream-chat-android-ui-components 10.63 MB 10.63 MB 0.00 MB 🟢
stream-chat-android-compose 12.85 MB 12.85 MB 0.00 MB 🟢

@VelikovPetar VelikovPetar marked this pull request as ready for review February 20, 2026 16:57
@VelikovPetar VelikovPetar requested a review from a team as a code owner February 20, 2026 16:57
@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

Walkthrough

The changes enhance process death resilience for media capture and attachments picker flows by persisting state via SavedStateHandle, restoring file paths across activity recreation, and introducing cancel-aware launcher variants to handle media capture cancellation properly.

Changes

Cohort / File(s) Summary
ViewModel & Factory State Persistence
AttachmentsPickerViewModel.kt, MessagesViewModelFactory.kt
Added SavedStateHandle integration to AttachmentsPickerViewModel constructor and factory method to persist UI visibility state across process death. Factory now creates SavedStateHandle from CreationExtras and passes it to ViewModel during construction.
Media Capture Launcher Resilience
CaptureMediaLauncher.kt, AttachmentsPickerMediaCaptureTabFactory.kt, CaptureMediaContract.kt
Introduced process-death recovery by persisting destination file paths using rememberSaveable state, exposing pictureFile and videoFile as public restorable properties on CaptureMediaContract, adding cancel-aware launcher variant, and implementing callback-based file path restoration after activity recreation.
API Contract Updates
stream-chat-android-compose.api
Extended public API signatures with new constructor accepting SavedStateHandle for AttachmentsPickerViewModel and overloaded create methods accepting CreationExtras for MessagesViewModelFactory and MessageListViewModelFactory.
Documentation
MessageComposer.kt
Reordered imports for androidx.compose.material.icons components to eliminate duplication.

Sequence Diagram

sequenceDiagram
    actor User as User
    participant Compose as Compose UI
    participant Factory as MessagesViewModelFactory
    participant ViewModel as AttachmentsPickerViewModel
    participant SavedState as SavedStateHandle
    participant Launcher as CaptureMediaLauncher
    participant Contract as CaptureMediaContract
    
    User->>Compose: Open attachments picker
    Compose->>Factory: Create ViewModel with CreationExtras
    Factory->>SavedState: Create SavedStateHandle from CreationExtras
    Factory->>ViewModel: Constructor(StorageHelper, ChannelState, SavedStateHandle)
    ViewModel->>SavedState: Restore isShowingAttachments from saved state
    ViewModel-->>Compose: Initialized with persisted state
    
    User->>Compose: Tap to capture media
    Compose->>Launcher: Configure & launch capture
    Launcher->>SavedState: Store file paths in rememberSaveable state
    Launcher->>Contract: Initialize contract with onFilesCreated callback
    Contract-->>User: System media capture activity starts
    
    rect rgba(255, 165, 0, 0.5)
    Note over User,Contract: Process death occurs
    end
    
    User-->>Contract: Return from media capture
    Contract->>Launcher: Callback with pictureFile, videoFile
    Launcher->>SavedState: Restore file paths from saved state
    Launcher->>Contract: Restore pictureFile & videoFile properties
    Launcher-->>ViewModel: onResult with File
    ViewModel->>SavedState: Persist isShowingAttachments = false
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • andremion

Poem

🐰 Files dance through process death with grace,
SavedState holds UI in safe embrace,
File paths persist when systems restart,
Cancel-aware launchers know when to part,
Attachments picker, forever preserved! 📸

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% 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 clearly and specifically describes the main fix: preventing the media capture launcher from losing state when the Activity is destroyed.
Description check ✅ Passed The description includes all required sections: Goal (with clear problem statement), Implementation (with detailed breakdown of changes across multiple files), UI Changes (with before/after videos), Testing (with clear reproduction steps), and Contributor Checklist.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bug/AND-1067_fix_attachment_picker_not_surviving_process_death

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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

@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 (2)
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/media/CaptureMediaLauncher.kt (1)

105-110: remember(mode) vs rememberSaveable: intentional asymmetry worth documenting.

The contract is held in remember(mode) (lost on process death), while file paths use rememberSaveable (survives). This is correct — a new CaptureMediaContract is created on recreation and immediately gets its files restored from the saved paths. A brief inline comment noting this deliberate choice could help future maintainers, but the code is functionally sound.

🤖 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/media/CaptureMediaLauncher.kt`
around lines 105 - 110, Add a short inline comment near the CaptureMediaContract
creation explaining the deliberate asymmetry: the contract instance is stored
with remember(mode) (so it can be recreated after process death) while
pictureFilePath and videoFilePath are stored with rememberSaveable and will be
restored on recreation; note that the restored paths are applied to the newly
created CaptureMediaContract so this behavior is intentional and should not be
changed.
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessagesViewModelFactory.kt (1)

166-168: Consider adding a comment explaining why the AttachmentsPickerViewModel entry remains in the factories map.

The map entry (no SavedStateHandle) is now effectively a legacy fallback, since the preferred creation path goes through create(modelClass, extras) at line 192. A brief comment would clarify that this entry serves backward-compatible direct create(Class) callers.

🤖 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/MessagesViewModelFactory.kt`
around lines 166 - 168, The AttachmentsPickerViewModel entry in the factories
map is intentionally kept as a legacy fallback for direct create(Class) callers
even though the preferred creation path now uses create(modelClass, extras); add
a short comment above the AttachmentsPickerViewModel mapping in the factories
map explaining that this entry exists for backward compatibility (no
SavedStateHandle) and should be retained to support callers that invoke
create(Class) directly while new code should use create(modelClass, extras).
🤖 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/media/CaptureMediaLauncher.kt`:
- Around line 105-110: Add a short inline comment near the CaptureMediaContract
creation explaining the deliberate asymmetry: the contract instance is stored
with remember(mode) (so it can be recreated after process death) while
pictureFilePath and videoFilePath are stored with rememberSaveable and will be
restored on recreation; note that the restored paths are applied to the newly
created CaptureMediaContract so this behavior is intentional and should not be
changed.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessagesViewModelFactory.kt`:
- Around line 166-168: The AttachmentsPickerViewModel entry in the factories map
is intentionally kept as a legacy fallback for direct create(Class) callers even
though the preferred creation path now uses create(modelClass, extras); add a
short comment above the AttachmentsPickerViewModel mapping in the factories map
explaining that this entry exists for backward compatibility (no
SavedStateHandle) and should be retained to support callers that invoke
create(Class) directly while new code should use create(modelClass, extras).

Copy link
Contributor

@andremion andremion left a comment

Choose a reason for hiding this comment

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

Just a non-blocker small suggestion

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

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

See analysis details on SonarQube Cloud

@VelikovPetar VelikovPetar merged commit 0a76dd6 into develop Feb 23, 2026
14 of 15 checks passed
@VelikovPetar VelikovPetar deleted the bug/AND-1067_fix_attachment_picker_not_surviving_process_death branch February 23, 2026 16:38
@stream-public-bot stream-public-bot added the released Included in a release label Feb 24, 2026
@stream-public-bot
Copy link
Contributor

🚀 Available in v6.32.4

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