Fix rememberCaptureMediaLauncher not surviving Activity death#6177
Conversation
PR checklist ✅All required conditions are satisfied:
🎉 Great job! This PR is ready for review. |
SDK Size Comparison 📏
|
…rviving_process_death
WalkthroughThe 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
🧹 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)vsrememberSaveable: intentional asymmetry worth documenting.The contract is held in
remember(mode)(lost on process death), while file paths userememberSaveable(survives). This is correct — a newCaptureMediaContractis 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 theAttachmentsPickerViewModelentry remains in thefactoriesmap.The map entry (no
SavedStateHandle) is now effectively a legacy fallback, since the preferred creation path goes throughcreate(modelClass, extras)at line 192. A brief comment would clarify that this entry serves backward-compatible directcreate(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).
andremion
left a comment
There was a problem hiding this comment.
Just a non-blocker small suggestion
...rc/main/kotlin/io/getstream/chat/android/ui/common/contract/internal/CaptureMediaContract.kt
Outdated
Show resolved
Hide resolved
|
|
🚀 Available in v6.32.4 |


Goal
Fix the attachment picker's media capture flow not surviving
Activitydeath (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
Activitymay be destroyed while the camera app is in the foreground. On return, the captured media was lost because:AttachmentsPickerViewModel.isShowingAttachmentswas a plainmutableStateOf— reset tofalseon recreation — so the picker composable tree was never recomposed, andrememberLauncherForActivityResultcallbacks were never re-registered to receive the pending result.CaptureMediaContract.pictureFile/videoFilewere private in-memory fields lost on process death, causingparseResultto returnnulleven though the camera wrote a valid file to disk.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 deathSavedStateHandleas a constructor parameter (with@JvmOverloadsfor backward compatibility).isShowingAttachmentswith bothMutableState(for Compose recomposition) andSavedStateHandle(for persistence). On recreation, the saved value is restored so the picker composable tree is recomposed and activity-result launchers can re-register.MessagesViewModelFactory— supplySavedStateHandleto the ViewModelcreate(Class<T>, CreationExtras)to obtain aSavedStateHandleviaCreationExtras.createSavedStateHandle()when constructingAttachmentsPickerViewModel. Falls back to the existingcreate(Class<T>)for all other ViewModels.CaptureMediaContract— expose file references and add a creation callbackpictureFileandvideoFilepublic (annotated@InternalStreamChatApi) so they can be restored externally after process death.onFilesCreatedcallback parameter, invoked insidecreateIntentafter destination files are created, giving callers a chance to persist the file paths before the external camera activity starts.CaptureMediaLauncher— persist destination file paths viarememberSaveablerememberCaptureMediaLauncherInternalwhich storespictureFilePathandvideoFilePathviarememberSaveable. On recreation, these paths are restored onto theCaptureMediaContractbeforerememberLauncherForActivityResultre-registers and dispatches the pending result.rememberCancelAwareCaptureMediaLauncherinternal variant that forwardsnullon cancellation so callers can react (e.g. dismiss the picker).AttachmentsPickerMediaCaptureTabFactory— guard against re-launching the camerarememberCancelAwareCaptureMediaLauncherinstead of manually creating the contract.hasLaunchedviarememberSaveableto preventLaunchedEffectfrom re-launching the camera after activity recreation.UI Changes
default-before.mp4
default-after.mp4
system-before.mp4
system-after.mp4
Testing
This can be tested by enabling "Don't keep activities" in Android Developer Options:
Additionally, verify the normal (non-process-death) flow still works:
Summary by CodeRabbit
Release Notes
Bug Fixes
Chores