Improve ringing state transition from Outgoing to Active in joinAndRing flow#1617
Conversation
…ctive when we perform joinAndRing and WS is not connected To set WS connection to null set the return of waitForConnectionId to null
PR checklist ✅All required conditions are satisfied:
🎉 Great job! This PR is ready for review. |
SDK Size Comparison 📏
|
# Conflicts: # stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt
…issues' into bugfix/rahullohra/join-and-ring-issues # Conflicts: # stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt
WalkthroughThis PR customizes the Checkbox component in DirectCallJoinScreen with styling and offset, and refactors CallState.kt to use AtomicBoolean for thread-safe ringing state management, adds structured logging, and introduces new logic for handling join-and-ring scenarios. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes 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)
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.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt (1)
1026-1030:⚠️ Potential issue | 🟡 MinorChange
acceptedByCallee = truetoacceptedByCallee = falseat lines 1026–1030.At the time
JoinCallResponseEventis processed in thejoinAndRingflow, the callee has not yet accepted—settingacceptedByCallee = trueis semantically inaccurate. WhileactiveCallis set synchronously early in the join process (line 668 in_join), the flag's semantic meaning should reflect reality: the callee acceptance comes later viaCallAcceptedEvent. The transition toActivefor thejoinAndRingscenario is handled independently at line 1283 inupdateRingingStateand does not depend on this flag.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt` around lines 1026 - 1030, The current branch sets _ringingState.value = RingingState.Outgoing(acceptedByCallee = true) when ringingStateUpdatesStopped is true; change that boolean to false because at the time JoinCallResponseEvent is handled in the joinAndRing flow the callee has not accepted yet — leave acceptedByCallee = false so the semantic matches the real acceptance event (CallAcceptedEvent) which is processed later in _join/CallAcceptedEvent handling and updateRingingState transitions to Active independently.
🧹 Nitpick comments (5)
demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt (2)
177-179: Prefer using the lambda parameter instead of manually inverting state.
onCheckedChangeprovides the new checked value asit; ignoring it in favour of!callerJoinsFirstis non-idiomatic.♻️ Proposed fix
- onCheckedChange = { - callerJoinsFirst = !callerJoinsFirst - }, + onCheckedChange = { callerJoinsFirst = it },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt` around lines 177 - 179, The onCheckedChange handler currently ignores the provided new value and flips callerJoinsFirst manually; update the handler to use the lambda parameter directly (set callerJoinsFirst to the provided boolean) by replacing the body of the onCheckedChange lambda so it assigns the incoming parameter to callerJoinsFirst (referencing the onCheckedChange callback and the callerJoinsFirst state variable).
207-207: Remove commented-out debug code.This leftover comment adds noise with no clear intent to re-enable it.
♻️ Proposed fix
-// StreamCallId("default", UUID.randomUUID().toString()),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt` at line 207, Remove the leftover commented-out debug line StreamCallId("default", UUID.randomUUID().toString()) from DirectCallJoinScreen.kt; locate the commented line in the DirectCallJoinScreen composable or surrounding setup (where StreamCallId is referenced) and delete the commented code so the file contains only active, meaningful code without commented debug artifacts.stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt (3)
1316-1320: Add a comment explaining whyringingStateUpdatesStoppedis reset here.This reset enables the standard
hasActiveCall && !stopped → Activepath for all subsequentupdateRingingState()calls once the user is confirmed as a session participant. Without a comment, the intent is opaque and the line is easy to accidentally remove during future refactoring.♻️ Proposed fix
} else { // call is accepted and we are already in the call + // Re-enable normal ringing-state transitions: the user is already + // an SFU participant, so joinAndRing's suppression is no longer needed. ringingStateUpdatesStopped.set(false) cancelTimeout() ringingStateLogger.d { "RingingState.Active source 3" } RingingState.Active🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt` around lines 1316 - 1320, Add a brief inline comment above the ringingStateUpdatesStopped.set(false) call in the updateRingingState / RingingState.Active branch explaining that we reset this flag because once the call is accepted and the user is confirmed as a session participant we must re-enable the normal "hasActiveCall && !stopped → Active" transition for all subsequent updateRingingState() invocations; mention that this prevents future refactors from accidentally removing the reset and that cancelTimeout() is called to clear any pending fallback before returning RingingState.Active.
1242-1242: MoveringingStateLoggerto class scope — allocating a new logger delegate on everyupdateRingingState()call is wasteful.
updateRingingState()is invoked on virtually every call event; instantiating a freshtaggedLoggerdelegate each time adds unnecessary allocation overhead. Declare it alongside the existingloggerat class level.♻️ Proposed fix
private val logger by taggedLogger("CallState") +private val ringingStateLogger by taggedLogger("RingingState")private fun updateRingingState(rejectReason: RejectReason? = null) { - val ringingStateLogger by taggedLogger("RingingState") if (ringingState.value == RingingState.RejectedByAll) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt` at line 1242, The local declaration val ringingStateLogger by taggedLogger("RingingState") inside updateRingingState() should be moved to the class scope alongside the existing logger to avoid allocating a new delegate on every updateRingingState() call; add a class-level property (e.g., private val ringingStateLogger by taggedLogger("RingingState")) near the existing logger declaration and remove the local ringingStateLogger declaration from updateRingingState(), updating any usages to reference the class-scoped ringingStateLogger.
1317-1320: Inconsistent logger: line 1319 usesloggerwhile the rest ofupdateRingingStateusesringingStateLogger.♻️ Proposed fix
- logger.d { "RingingState.Active source 3" } + ringingStateLogger.d { "RingingState.Active source 3" }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt` around lines 1317 - 1320, The logging call in updateRingingState is inconsistent: it uses logger.d in the branch returning RingingState.Active while the rest of the function uses ringingStateLogger; update that call to use ringingStateLogger.d (or the same logging API used elsewhere in updateRingingState) so all log statements are uniform—modify the line that currently calls logger.d { "RingingState.Active source 3" } to ringgingStateLogger.d (matching the existing logging style) while keeping the surrounding logic (ringingStateUpdatesStopped.set(false), cancelTimeout(), returning RingingState.Active) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt`:
- Around line 1026-1030: The current branch sets _ringingState.value =
RingingState.Outgoing(acceptedByCallee = true) when ringingStateUpdatesStopped
is true; change that boolean to false because at the time JoinCallResponseEvent
is handled in the joinAndRing flow the callee has not accepted yet — leave
acceptedByCallee = false so the semantic matches the real acceptance event
(CallAcceptedEvent) which is processed later in _join/CallAcceptedEvent handling
and updateRingingState transitions to Active independently.
---
Nitpick comments:
In
`@demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt`:
- Around line 177-179: The onCheckedChange handler currently ignores the
provided new value and flips callerJoinsFirst manually; update the handler to
use the lambda parameter directly (set callerJoinsFirst to the provided boolean)
by replacing the body of the onCheckedChange lambda so it assigns the incoming
parameter to callerJoinsFirst (referencing the onCheckedChange callback and the
callerJoinsFirst state variable).
- Line 207: Remove the leftover commented-out debug line StreamCallId("default",
UUID.randomUUID().toString()) from DirectCallJoinScreen.kt; locate the commented
line in the DirectCallJoinScreen composable or surrounding setup (where
StreamCallId is referenced) and delete the commented code so the file contains
only active, meaningful code without commented debug artifacts.
In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt`:
- Around line 1316-1320: Add a brief inline comment above the
ringingStateUpdatesStopped.set(false) call in the updateRingingState /
RingingState.Active branch explaining that we reset this flag because once the
call is accepted and the user is confirmed as a session participant we must
re-enable the normal "hasActiveCall && !stopped → Active" transition for all
subsequent updateRingingState() invocations; mention that this prevents future
refactors from accidentally removing the reset and that cancelTimeout() is
called to clear any pending fallback before returning RingingState.Active.
- Line 1242: The local declaration val ringingStateLogger by
taggedLogger("RingingState") inside updateRingingState() should be moved to the
class scope alongside the existing logger to avoid allocating a new delegate on
every updateRingingState() call; add a class-level property (e.g., private val
ringingStateLogger by taggedLogger("RingingState")) near the existing logger
declaration and remove the local ringingStateLogger declaration from
updateRingingState(), updating any usages to reference the class-scoped
ringingStateLogger.
- Around line 1317-1320: The logging call in updateRingingState is inconsistent:
it uses logger.d in the branch returning RingingState.Active while the rest of
the function uses ringingStateLogger; update that call to use
ringingStateLogger.d (or the same logging API used elsewhere in
updateRingingState) so all log statements are uniform—modify the line that
currently calls logger.d { "RingingState.Active source 3" } to
ringgingStateLogger.d (matching the existing logging style) while keeping the
surrounding logic (ringingStateUpdatesStopped.set(false), cancelTimeout(),
returning RingingState.Active) unchanged.
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt
Outdated
Show resolved
Hide resolved
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt
Show resolved
Hide resolved
- Renamed `ringingStateUpdatesStopped` to `isJoinAndRingInProgress` in `CallState` for better clarity. - Updated `updateRingingState` to properly handle `RingingState.Active` transitions during normal joins vs. `joinAndRing` flows. - Modified `updateFromJoinResponse` to ensure `acceptedBy` list correctly merges server-side data with local state. - Refined `ClientState` transition logic to prevent premature call acceptance states when a `joinAndRing` operation is in progress. - Renamed `toggleRingingStateUpdates` to `toggleJoinAndRingProgress`.
# Conflicts: # stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt
|
|
🚀 Available in v1.20.0 |




Goal
Improve ringing state transition from Outgoing to Active in
joinAndRingflowWhen initiating an outgoing call via
joinAndRing, the call gets stuck inRingingState.Outgoingand never transitions toRingingState.Activeafter the callee accepts the call. This was traced to two independent race conditions and an unrelated readability issue with a poorly named flag.We have 2 major issues
Issue 1: _acceptedBy is cleared by CallSessionStartedEvent
_acceptedBygets overwritten duringCallSessionStartedEventhandling viaupdateFromResponse, even though a priorCallAcceptedEventhad already populated it.Issue 2: Failure to transition to RingingState.Active
The caller fails to transition from
RingingState.OutgoingtoRingingState.ActivebecauseringingCallis being set to null prematurely.The following logic is responsible for transitioning to RingingState.Active during joinAndRing:
However,
hasRingingCallbecomesfalsebecauseringingCallis cleared when thejoin()operation completes:Implementation
_acceptedByfrom video events instead of overwriting it with session snapshots.transitionToAcceptCallfrom clearing_ringingCallduringjoinAndRing, by guarding onisJoinAndRingInProgress.ringingStateUpdatesStoppedtoisJoinAndRingInProgressto better reflect its purpose and reduce ambiguity.🎨 UI Changes
None
Testing
Make 1-to-1 calls and verify that the caller transitions correctly to RingingState.Active.
This issue was caused by a race condition, which makes it difficult to reproduce reliably.
Summary by CodeRabbit
New Features
Improvements