Skip to content

Conversation

@VelikovPetar
Copy link
Contributor

@VelikovPetar VelikovPetar commented Jan 12, 2026

🎯 Goal

Deferring the IO operations done in the AudioRecordingController to the IO dispatcher (to resolve the StrictMode violations) introduced some interesting race conditions, mainly:

  • Calling cancelRecording in quick succession after calling startRecording is ignored
  • Calling lockRecording in quick succession after calling startRecording is ignored
    The cause for these issues is because the creation of the recording file is now deferred to the IO dispatcher, meaning that it no longer blocks the UI thread. However, this also means that the UI could submit requests to cancelRecording/lockRecording before the recording file is created, and the recorder state is not ready to handle those events. In this PR we introduce a mechanism to 'schedule' such requests, done after startRecording was called, but also before it completes.

Note: This PR covers only Compose. I will create a separate one for XML.

🛠 Implementation details

  • Add 'scheduling' mechanism for cancelRecording/lockRecording to await execution after the IO work is done.

🎨 UI Changes

Case 1: Quick tap on record button (cancelRecording). Currently, the UI gets stuck in 'hold' state

Before After
short_tap_before.mp4
short_tap_after.mp4

Case 2: Impossible to call lockRecording immediately after startRecording (currently it is ignored)

To test this, you can update the AudioRecordingActions to call startRecording and lockRecording subsequently (use attached patch):

After
auto_lock_after.mp4

🧪 Testing

Case 1: Tap on the recording button - it shouldn't be stuck in 'held' state

Case 2:

  1. Apply the given patch
  2. Hold the recording button
  3. It should be locked immediately
Provide the patch summary here
Subject: [PATCH] Update CHANGELOG.md.
---
Index: stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/actions/AudioRecordingActions.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/actions/AudioRecordingActions.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/actions/AudioRecordingActions.kt
--- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/actions/AudioRecordingActions.kt	(revision c680030829aa0421817f4f08648b4ccf235686d2)
+++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/actions/AudioRecordingActions.kt	(date 1768291139099)
@@ -74,7 +74,10 @@
         public fun defaultActions(
             viewModel: MessageComposerViewModel,
         ): AudioRecordingActions = AudioRecordingActions(
-            onStartRecording = { viewModel.startRecording(it.toRestrictedCoordinates()) },
+            onStartRecording = {
+                viewModel.startRecording(it.toRestrictedCoordinates())
+                viewModel.lockRecording()
+            },
             onHoldRecording = { viewModel.holdRecording(it.toRestrictedCoordinates()) },
             onLockRecording = { viewModel.lockRecording() },
             onCancelRecording = { viewModel.cancelRecording() },

Summary by CodeRabbit

  • Bug Fixes
    • Fixed race conditions in audio recording that could cause unexpected behavior during recording interactions.
    • Improved handling of recording state to prevent cancellation or locking issues when state changes occur during user interactions.

✏️ Tip: You can customize this high-level summary in your review settings.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 12, 2026

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.25 MB 5.25 MB 0.00 MB 🟢
stream-chat-android-offline 5.48 MB 5.48 MB 0.00 MB 🟢
stream-chat-android-ui-components 10.62 MB 10.62 MB 0.00 MB 🟢
stream-chat-android-compose 12.84 MB 12.84 MB 0.00 MB 🟢

@VelikovPetar VelikovPetar changed the title Fix audio recording race conditions Fix audio recording race conditions (Compose) Jan 13, 2026
@VelikovPetar VelikovPetar marked this pull request as ready for review January 13, 2026 08:17
@VelikovPetar VelikovPetar requested a review from a team as a code owner January 13, 2026 08:17
@coderabbitai
Copy link

coderabbitai bot commented Jan 13, 2026

Walkthrough

The changes fix audio recording race conditions by introducing state guards and thread-safe flags. The UI component now checks for locked states during drag operations, while the audio controller uses pending flags to handle concurrent calls to startRecording, cancelRecording, and lockRecording.

Changes

Cohort / File(s) Summary
Changelog Update
CHANGELOG.md
Added entry documenting fix for audio recording race conditions.
UI State Guards
stream-chat-android-compose/.../DefaultMessageComposerRecordingContent.kt
Introduced rememberUpdatedState to track current state and added early-exit logic in drag handling when state transitions to Locked. Release actions now guarded against state changes during drag sequences.
Race Condition Handling
stream-chat-android-ui-common/.../AudioRecordingController.kt
Added thread-safe pendingCancel and pendingLock flags to handle concurrent calls during recorder startup. Methods now defer actions (cancel/lock) if called while idle, applying them after startRecording completes. clearData resets both flags.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit's whisper on the wind...

When buttons clash and threads collide,
Our pending flags become our guide,
State guards protect each drag and hold—
No race conditions left untold! 🎙️✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% 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 accurately summarizes the main change: fixing audio recording race conditions in the Compose module, which is the core objective of the PR.
Description check ✅ Passed The description covers the goal, implementation details, UI changes with videos, testing instructions, and includes a patch for testing. However, the Contributor Checklist items are not marked/completed.

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

✨ Finishing touches
  • 📝 Generate docstrings

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • UTF-8: Entity not found: Issue - Could not find referenced 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

@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.

Actionable comments posted: 0

🧹 Nitpick comments (2)
CHANGELOG.md (1)

71-73: Changelog entry looks correct and properly scoped to Compose.
The item is in the right section and links to the PR. Optionally, consider making the description slightly more specific (e.g., “...when lock/cancel is invoked immediately after start”) to aid release notes readers.

stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/AudioRecordingController.kt (1)

176-206: Consider adding unit tests for race condition scenarios.

The race condition handling logic is critical. Based on learnings, consider adding deterministic tests using runTest with virtual time to verify:

  1. cancelRecording called during startRecording's IO results in proper cleanup
  2. lockRecording called during startRecording's IO transitions to Locked state
  3. Multiple concurrent calls don't cause unexpected state transitions

Do you have unit tests covering these race condition scenarios? If not, would you like me to help draft test cases?

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a2d545b and c680030.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/DefaultMessageComposerRecordingContent.kt
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/AudioRecordingController.kt
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{kt,kts}

📄 CodeRabbit inference engine (AGENTS.md)

Format and apply Kotlin style with Spotless (4 spaces, no wildcard imports, licence headers)

Files:

  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/AudioRecordingController.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/DefaultMessageComposerRecordingContent.kt
**/*.kt

📄 CodeRabbit inference engine (AGENTS.md)

**/*.kt: Use @OptIn annotations explicitly; avoid suppressions unless documented
Document public APIs with KDoc, including thread expectations and state notes

Files:

  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/AudioRecordingController.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/DefaultMessageComposerRecordingContent.kt
**/stream-chat-android-compose/**/*.kt

📄 CodeRabbit inference engine (AGENTS.md)

**/stream-chat-android-compose/**/*.kt: Compose components should follow noun-based naming (e.g., MessageList, ChannelListHeader)
Compose previews should use @StreamPreview helpers

Files:

  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/DefaultMessageComposerRecordingContent.kt
🧠 Learnings (4)
📓 Common learnings
Learnt from: CR
Repo: GetStream/stream-chat-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-17T15:00:07.506Z
Learning: Applies to **/stream-chat-android-compose/**/*Test.kt : Add Paparazzi snapshots for Compose UI regressions and run `verifyPaparazziDebug`
📚 Learning: 2025-12-17T15:00:07.506Z
Learnt from: CR
Repo: GetStream/stream-chat-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-17T15:00:07.506Z
Learning: Applies to **/stream-chat-android-ui-components/**/*Test.kt : Record Shot baselines when behaviour changes in XML kit UI tests

Applied to files:

  • CHANGELOG.md
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/DefaultMessageComposerRecordingContent.kt
📚 Learning: 2025-12-17T15:00:07.506Z
Learnt from: CR
Repo: GetStream/stream-chat-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-17T15:00:07.506Z
Learning: Applies to **/stream-chat-android-compose/**/*Test.kt : Add Paparazzi snapshots for Compose UI regressions and run `verifyPaparazziDebug`

Applied to files:

  • CHANGELOG.md
📚 Learning: 2025-12-17T15:00:07.506Z
Learnt from: CR
Repo: GetStream/stream-chat-android PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-17T15:00:07.506Z
Learning: Applies to **/src/test/**/*.kt : Use deterministic tests with `runTest` + virtual time for concurrency-sensitive logic (uploads, sync, message state)

Applied to files:

  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/AudioRecordingController.kt
🧬 Code graph analysis (1)
stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/AudioRecordingController.kt (1)
stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt (1)
  • clearData (670-678)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Test compose (1)
  • GitHub Check: Test compose (2)
  • GitHub Check: Test compose (0)
🔇 Additional comments (8)
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/DefaultMessageComposerRecordingContent.kt (3)

51-51: Good use of rememberUpdatedState for gesture handling.

Using rememberUpdatedState(state) is the correct pattern here to capture the latest state value within the pointerInput block, which doesn't recompose when state changes. This ensures currentState always reflects the most recent recording state during the drag gesture.

Also applies to: 156-156


199-202: Correct early exit when recording is locked.

Breaking out of the drag loop when the state transitions to Locked prevents the UI from continuing to process drag events after locking, which aligns with the race condition fix.


206-217: Documented suppression for state guard is appropriate.

The @Suppress("KotlinConstantConditions") is correctly applied with an explanatory comment. The compiler can't know that currentState may change during the suspend call to awaitDragOrCancellation. The guard prevents sending/canceling when the recording was already locked via the pending mechanism in AudioRecordingController.

stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/AudioRecordingController.kt (5)

86-96: Well-documented thread-safe flags.

Good use of AtomicBoolean for thread-safe flag operations between the main thread (where cancelRecording/lockRecording are called) and the IO dispatcher (where startAudioRecording executes). The KDoc clearly explains the purpose.


184-205: Sound race condition handling in startRecording.

The implementation correctly:

  1. Resets flags before async work to only detect concurrent calls
  2. Uses getAndSet(false) for atomic read-and-clear
  3. Checks pendingCancel before pendingLock, giving cancellation precedence over locking

This ensures that quick cancel or lock requests during the IO work are honored.


222-230: Correct deferred locking mechanism.

When lockRecording is called while state is Idle (meaning startRecording's IO is in progress), setting pendingLock ensures the lock will be applied after the IO completes. This directly addresses the race condition described in the PR objectives.


239-246: Correct deferred cancellation mechanism.

Setting pendingCancel when state is Idle handles the quick-tap scenario where cancelRecording races with startRecording's IO work. This prevents the UI from getting stuck in the "hold" state as described in the PR objectives.


485-496: Correct cleanup of pending flags.

Resetting both pendingCancel and pendingLock in clearData ensures a clean slate for subsequent recording sessions, preventing stale flags from causing unexpected behavior.

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

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

See analysis details on SonarQube Cloud

@VelikovPetar VelikovPetar merged commit 252c414 into develop Jan 16, 2026
13 of 14 checks passed
@VelikovPetar VelikovPetar deleted the bug/AND-954_fix_audio_recording_stuck_in_locked branch January 16, 2026 09:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants