Skip to content

Comments

Correctly update Ringing State on CallEnded event#1621

Open
rahul-lohra wants to merge 2 commits intodevelopfrom
bugfix/rahullohra/incoming-call-declined-via-end-call
Open

Correctly update Ringing State on CallEnded event#1621
rahul-lohra wants to merge 2 commits intodevelopfrom
bugfix/rahullohra/incoming-call-declined-via-end-call

Conversation

@rahul-lohra
Copy link
Contributor

@rahul-lohra rahul-lohra commented Feb 20, 2026

Goal

Correctly update RingingState on CallEnded event

Background: When an incoming call is declined by the caller via call.end() rather than call.reject(), the callee currently transitions from RingingState.Incoming to RingingState.Idle. This change corrects the transition to RingingState.RejectedByAll.

Implementation

Used call.endedAt to determine if call has been ended

🎨 UI Changes

Before

Screen.Recording.2026-02-20.at.6.39.14.PM.mov

After

Screen.Recording.2026-02-20.at.6.34.22.PM.mov

Testing

  1. Replace call.reject() in StreamActivity.cancel() with call.end to reproduce this behaviour.
  2. Once the call is ended - we should go back to the home screen in demo-app

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of call state management during ended calls to ensure proper ringing state transitions.
    • Enhanced robustness of state transitions to prevent edge cases when calls conclude.

@rahul-lohra rahul-lohra self-assigned this Feb 20, 2026
@rahul-lohra rahul-lohra requested a review from a team as a code owner February 20, 2026 12:55
@rahul-lohra rahul-lohra added the pr:bug Fixes a bug 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.

@rahul-lohra rahul-lohra changed the title Correctly update Ringing State on CallEnded event [AND-1076] Correctly update Ringing State on CallEnded event Feb 20, 2026
@rahul-lohra rahul-lohra changed the title [AND-1076] Correctly update Ringing State on CallEnded event Correctly update Ringing State on CallEnded event Feb 20, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

Walkthrough

Refactoring of the CallState class to consolidate logging through a structured per-module logger (ringingLogger) and augment ringing state logic with call-ended state awareness, enabling proper state transitions when calls end.

Changes

Cohort / File(s) Summary
Logging & Ringing State Enhancement
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt
Replaced direct Log calls with structured ringingLogger, introduced isCallEnded computed flag to track ended state, updated CallEndedEvent handling to refresh ringing state, and added isCallEnded branch to transition to RingingState.RejectedByAll when call terminates.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A single file gets a logging glow,
With ringing states that now shall know,
When calls have ended, clean and bright,
The bunny hops—all logs done right! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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 clearly and specifically summarizes the main change: updating ringing state correctly when a CallEnded event occurs.
Description check ✅ Passed The pull request description includes all required sections: Goal, Implementation, UI Changes, Testing, and Contributor/Reviewer Checklists. While UI change videos are linked and testing steps are provided, the description adequately explains the bug fix for RingingState transitions.

✏️ 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 bugfix/rahullohra/incoming-call-declined-via-end-call

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: 1

🧹 Nitpick comments (1)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt (1)

1261-1262: Move ringingLogger to a class-level property to avoid recreating the delegate wrapper on every updateRingingState() call.

The local delegated val ringingLogger by taggedLogger("RingingState") at line 1261 creates a new delegate wrapper object on each invocation. Since updateRingingState() is called frequently (at least 7+ times across state changes: accepted/rejected events, member updates, session changes, etc.), this incurs unnecessary allocation overhead.

Follow the existing pattern used for logger at line 226:

 private val logger by taggedLogger("CallState")
+private val ringingLogger by taggedLogger("RingingState")

Then remove the local declaration at line 1261.

🤖 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 1261 - 1262, Declare a class-level property for the logger instead
of recreating it inside updateRingingState(): move the local delegated val
ringingLogger by taggedLogger("RingingState") to a top-level property in the
class (mirroring the existing logger declaration used at line 226) and then
remove the local declaration inside updateRingingState(); update usages in
updateRingingState() to reference the new class-level ringingLogger to avoid
repeated delegate-wrapper allocations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt`:
- Around line 1280-1282: The isCallEnded branch in updateRingingState()
transitions to RingingState.RejectedByAll but omits call.leave(), causing
resources to leak when updateRingingState() is invoked from other paths
(updateFromResponse, getOrCreateMembers, CallSessionParticipantJoinedEvent);
modify the isCallEnded branch in updateRingingState() to call call.leave(...)
with a clear reason (e.g., "updateRingingState-ended") prior to cancelTimeout(),
mirroring the other rejection branches so call.leave() is always invoked before
returning RingingState.RejectedByAll.

---

Nitpick comments:
In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt`:
- Around line 1261-1262: Declare a class-level property for the logger instead
of recreating it inside updateRingingState(): move the local delegated val
ringingLogger by taggedLogger("RingingState") to a top-level property in the
class (mirroring the existing logger declaration used at line 226) and then
remove the local declaration inside updateRingingState(); update usages in
updateRingingState() to reference the new class-level ringingLogger to avoid
repeated delegate-wrapper allocations.

Comment on lines 1280 to 1282
} else if (isCallEnded) {
cancelTimeout()
RingingState.RejectedByAll
Copy link

@coderabbitai coderabbitai bot Feb 20, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

isCallEnded branch is missing call.leave(), unlike every other rejection branch

Every other branch that transitions to RejectedByAll in this function also calls call.leave():

} else if (isRejectedByMe) {
    call.leave("updateRingingState-rejected-self")   // ← always called
    cancelTimeout()
    RingingState.RejectedByAll
} else if (...rejectedBy...) {
    call.leave("updateRingingState-rejected")         // ← always called
    cancelTimeout()
    ...
    RingingState.RejectedByAll

The new isCallEnded branch does not:

} else if (isCallEnded) {
    cancelTimeout()
    RingingState.RejectedByAll   // no call.leave()

This works for the primary path (CallEndedEventcall.leave() is called explicitly at line 841). However, updateRingingState() is also invoked from updateFromResponse(CallResponse) (line 1540), getOrCreateMembers (line 1502), and CallSessionParticipantJoinedEvent (line 1195). If any of those paths execute with _endedAt != null — for example, a CallSessionEndedEvent or CallUpdatedEvent carrying event.call.endedAt != null in a race condition — the ringing state flips to RejectedByAll but call.leave() is never triggered, leaving call resources unreleased.

🐛 Proposed fix — align with other rejection branches
 } else if (isCallEnded) {
+    call.leave("updateRingingState-call-ended")
     cancelTimeout()
     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`
around lines 1280 - 1282, The isCallEnded branch in updateRingingState()
transitions to RingingState.RejectedByAll but omits call.leave(), causing
resources to leak when updateRingingState() is invoked from other paths
(updateFromResponse, getOrCreateMembers, CallSessionParticipantJoinedEvent);
modify the isCallEnded branch in updateRingingState() to call call.leave(...)
with a clear reason (e.g., "updateRingingState-ended") prior to cancelTimeout(),
mirroring the other rejection branches so call.leave() is always invoked before
returning RingingState.RejectedByAll.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Our scope is limited to correctly updating the RingingState when the call is ended. No additional changes are required.

Copy link

Choose a reason for hiding this comment

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

@rahul-lohra, understood! I'll respect the scope you've defined for this PR. The fix correctly addresses the ringing state transition when a call ends, which is the stated objective. Thank you for the clarification.


🧠 Learnings used
Learnt from: rahul-lohra
Repo: GetStream/stream-video-android PR: 1614
File: stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/CallBusyHandler.kt:40-44
Timestamp: 2026-02-18T12:33:02.111Z
Learning: In the `CallBusyHandler` class in `stream-video-android-core`, fire-and-forget rejection calls (using `streamVideo.scope.launch` to call `reject(RejectReason.Busy)`) without explicit error handling are acceptable for the busy call rejection feature.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 20, 2026

SDK Size Comparison 📏

SDK Before After Difference Status
stream-video-android-core 12.00 MB 12.00 MB 0.00 MB 🟢
stream-video-android-ui-xml 5.68 MB 5.68 MB 0.00 MB 🟢
stream-video-android-ui-compose 6.28 MB 6.28 MB 0.00 MB 🟢

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
B Maintainability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

val userIsParticipant =
_session.value?.participants?.find { it.user.id == client.userId } != null
val outgoingMembersCount = _members.value.filter { it.value.user.id != client.userId }.size
val isCallEnded: Boolean = _endedAt.value != null
Copy link
Contributor

@aleksandar-apostolov aleksandar-apostolov Feb 23, 2026

Choose a reason for hiding this comment

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

taggedLogger is a delegate — creating it inside updateRingingState() means a new instance allocates on every call. This method runs frequently (every state change, every event). Should be class-level: private val ringingLogger by taggedLogger("RingingState")

Same issue flagged on #1617

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:bug Fixes a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants