Skip to content

Comments

Ability to reject an incoming call when you aleady have a ringing or active call#1614

Open
rahul-lohra wants to merge 14 commits intodevelopfrom
feature/rahullohra/reject-call-when-busy
Open

Ability to reject an incoming call when you aleady have a ringing or active call#1614
rahul-lohra wants to merge 14 commits intodevelopfrom
feature/rahullohra/reject-call-when-busy

Conversation

@rahul-lohra
Copy link
Contributor

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

Goal

Ability to reject an incoming call when you already have a ringing or active call

Implementation

Introduce a new parameter rejectCallWhenBusy in StreamVideoBuilder which can used as following

StreamVideoBuilder(context, rejectCallWhenBusy = true).build

Setting rejectCallWhenBusy to true will do the followings

  1. Automatically rejects incoming calls when the user is already in another call.
  2. The SDK suppresses incoming call notifications
  3. CallRingEvent will not be propagated if there is an active or ongoing ringing call.

🎨 UI Changes

None

Testing

  • Set StreamVideoBuilder.rejectWhenBusy to true
  • Incoming calls should get automatic reject when we are already either in ongoing call or if we have an incoming call
  • Incoming call notification should not appear

Summary by CodeRabbit

  • New Features

    • Added a configuration option to automatically reject incoming calls when the user is already on another call
    • Incoming call notifications can now be suppressed when the user is busy with another call
  • Tests

    • Added comprehensive test coverage for the new busy-call handling functionality

@rahul-lohra rahul-lohra self-assigned this Feb 12, 2026
@rahul-lohra rahul-lohra added the pr:new-feature Adds new functionality label Feb 12, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Feb 12, 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 [AND-1037] [Wip] Ability to reject an incoming call when you aleady have a ringing or active call [Wip] Ability to reject an incoming call when you aleady have a ringing or active call Feb 12, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Feb 12, 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 🟢

@rahul-lohra rahul-lohra changed the title [Wip] Ability to reject an incoming call when you aleady have a ringing or active call Ability to reject an incoming call when you aleady have a ringing or active call Feb 18, 2026
@rahul-lohra rahul-lohra marked this pull request as ready for review February 18, 2026 11:15
@rahul-lohra rahul-lohra requested a review from a team as a code owner February 18, 2026 11:15
@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

Walkthrough

This PR introduces a rejectCallWhenBusy configuration flag to the Stream Video SDK that automatically rejects incoming calls and suppresses related notifications when the user is already in another call, along with refactored event propagation logic and comprehensive test coverage.

Changes

Cohort / File(s) Summary
Configuration & Builder
demo-app/src/main/kotlin/.../StreamVideoInitHelper.kt, stream-video-android-core/src/main/kotlin/.../StreamVideoBuilder.kt, stream-video-android-core/api/stream-video-android-core.api
Adds rejectCallWhenBusy parameter (defaults to false) to StreamVideoBuilder, updates constructor signatures and public API signatures to include the new Boolean parameter, and forwards it to StreamVideoClient initialization.
Busy Call State Management
stream-video-android-core/src/main/kotlin/.../CallBusyHandler.kt, stream-video-android-core/src/main/kotlin/.../EventPropagationPolicy.kt
Introduces CallBusyHandler to coordinate busy-call detection and rejection, and EventPropagationPolicy to gate event propagation based on busy state. CallBusyHandler checks if client is busy with another call and optionally rejects via API with RejectReason.Busy.
Client State & Event Processing
stream-video-android-core/src/main/kotlin/.../ClientState.kt, stream-video-android-core/src/main/kotlin/.../StreamVideoClient.kt
Adds rejectCallWhenBusy property to ClientState. Refactors StreamVideoClient.fireEvent into staged helper methods (shouldProcessEvent, propagateEventToClientState, resolveSelectedCid, notifyClientSubscriptions, forwardToCallSubscriptions, propagateEventToCall, deliverToDestroyedCalls) for improved testability and separation of concerns.
Notification Handling
stream-video-android-core/src/main/kotlin/.../StreamDefaultNotificationHandler.kt
Adds shouldShowIncomingCallNotification helper that gates incoming call notifications based on busy state, preventing notification display when user is already in a call.
UI Layer
stream-video-android-ui-core/src/main/kotlin/.../StreamCallActivity.kt
Updates shouldAcceptNewCall logic to return rejectCallWhenBusy flag instead of always accepting new calls, conditioning acceptance on the configuration setting.
Unit Tests
stream-video-android-core/src/test/kotlin/.../EventPropagationPolicyTest.kt, stream-video-android-core/src/test/kotlin/.../StreamVideoClientTest.kt, stream-video-android-core/src/test/kotlin/.../CallBusyHandlerTest.kt, stream-video-android-core/src/test/kotlin/.../StreamDefaultNotificationHandlerTest.kt, stream-video-android-core/src/test/kotlin/.../RingTest.kt
Adds comprehensive test coverage for EventPropagationPolicy (delegation logic), StreamVideoClient (event processing flow), and CallBusyHandler (busy state detection and rejection). Updates existing notification handler and ring tests to support new busy-state scenarios. Disables one existing test.

Sequence Diagram

sequenceDiagram
    participant User as User (Incoming)
    participant Client as StreamVideoClient
    participant Policy as EventPropagationPolicy
    participant Handler as CallBusyHandler
    participant Notif as NotificationHandler
    participant State as ClientState

    User->>Client: CallRingEvent arrives
    Client->>Policy: shouldPropagate(event)
    Policy->>Handler: shouldPropagateEvent(event)
    Handler->>State: Check rejectCallWhenBusy
    Handler->>Handler: Check active/ringing calls
    alt User is busy with another call
        Handler-->>Policy: false
        Policy-->>Client: false
        Client->>Handler: isBusyWithAnotherCall(rejectViaApi=true)
        Handler->>Client: rejectCall(RejectReason.Busy)
        Handler-->>Policy: true (busy)
        Client->>Notif: Skip notification (busy)
    else User is not busy
        Handler-->>Policy: true (not busy)
        Policy-->>Client: true
        Client->>Client: Propagate event
        Client->>Notif: Show incoming call notification
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A busy bunny's call comes in,
But hop-hop, another's discipline begins,
Reject with grace when schedules clash,
Let notifications quietly pass,
One conversation at a time we'll dash! 📞✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.64% 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 reflects the main feature: ability to reject incoming calls when already busy with another call.
Description check ✅ Passed The description covers Goal and Implementation sections with code example and expected behavior, but is missing required sections like Testing details, UI Changes (even though stated as None), and Contributor/Reviewer checklists.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/rahullohra/reject-call-when-busy

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.

Actionable comments posted: 5

🧹 Nitpick comments (14)
stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/EventPropagationPolicyTest.kt (1)

28-28: Prefer org.junit.Test for consistency with the rest of the test suite.

This file mixes org.junit.Before (line 27) with kotlin.test.Test (line 28). Other test files in the module (RingTest.kt, StreamDefaultNotificationHandlerTest.kt, etc.) use org.junit.Test throughout. Keeping a consistent import avoids confusion about which runner applies.

♻️ Proposed fix
-import kotlin.test.Test
+import org.junit.Test
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/EventPropagationPolicyTest.kt`
at line 28, The test imports kotlin.test.Test while the suite uses
org.junit.Test; update the EventPropagationPolicyTest import by replacing
kotlin.test.Test with org.junit.Test (and remove any other kotlin.test.Test
usages) so the class EventPropagationPolicyTest and its test methods use
org.junit.Test consistently with the other tests (e.g., RingTest,
StreamDefaultNotificationHandlerTest).
stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/handlers/StreamDefaultNotificationHandlerTest.kt (2)

210-213: Redundant unmockkObject(StreamVideo) inside the test body — rely on @After tearDown.

@After fun tearDown() at line 184 already calls unmockkAll(), which clears every mock including StreamVideo. Calling unmockkObject(StreamVideo) mid-test is redundant and misleading — the parallel onRingingCall shows... not busy test doesn't do this, yet both are cleaned up the same way. Remove the redundant call for consistency.

♻️ Proposed fix
         // Then — NOTHING downstream should execute
         verify(exactly = 0) {
             mockIntentResolver.searchIncomingCallPendingIntent(any(), any())
         }
-        unmockkObject(StreamVideo)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/handlers/StreamDefaultNotificationHandlerTest.kt`
around lines 210 - 213, Remove the redundant mid-test unmockkObject(StreamVideo)
call in StreamDefaultNotificationHandlerTest: the verify block that asserts
mockIntentResolver.searchIncomingCallPendingIntent(...) is called exactly 0
times should remain, but delete the trailing unmockkObject(StreamVideo) line
because `@After` tearDown() already calls unmockkAll(), so this explicit
unmockkObject(StreamVideo) is unnecessary and inconsistent with the other test.

107-107: Rename setUp2 to setUp — the 2 suffix is confusing.

There is no other @Before method in this class, so the suffix implies a companion that doesn't exist. The @After/@Before conventions expect setUp/tearDown.

♻️ Proposed fix
-    fun setUp2() {
+    fun setUp() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/handlers/StreamDefaultNotificationHandlerTest.kt`
at line 107, Rename the test fixture method setUp2 to setUp in the
StreamDefaultNotificationHandlerTest class and keep its `@Before` annotation so it
follows standard lifecycle naming; update any internal references or test
utilities that call setUp2 to call setUp instead, and ensure there are no
duplicate `@Before` methods after the rename to avoid lifecycle conflicts.
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/handlers/StreamDefaultNotificationHandler.kt (2)

154-157: shouldShowIncomingCallNotification should be private, not internal.

This helper is only called from onRingingCall within the same class. Declaring it internal unnecessarily widens the module-visible API surface of a public open class, since subclasses and the entire module can now see and override the decision logic. The tests exercise it indirectly via onRingingCall (by mocking callBusyHandler), so no test needs internal access.

♻️ Proposed fix
-    internal fun shouldShowIncomingCallNotification(
+    private fun shouldShowIncomingCallNotification(
         callBusyHandler: CallBusyHandler,
         callCid: String,
     ) = !callBusyHandler.isBusyWithAnotherCall(callCid)
🤖 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/notifications/handlers/StreamDefaultNotificationHandler.kt`
around lines 154 - 157, Change the visibility of the helper function
shouldShowIncomingCallNotification in StreamDefaultNotificationHandler from
internal to private: locate the function signature "internal fun
shouldShowIncomingCallNotification(callBusyHandler: CallBusyHandler, callCid:
String)" and make it private so it is only callable within the class (ensure no
external references rely on it; it's used by onRingingCall). Keep the rest of
the method body unchanged.

167-173: Push-notification path suppresses the UI but never sends the busy-rejection API call.

shouldShowIncomingCallNotification calls isBusyWithAnotherCall(callCid) with the default rejectViaApi = false. This means when the SDK receives a push notification while the socket is disconnected (typical background scenario), the incoming-call notification is correctly suppressed, but the caller is never sent a RejectReason.Busy response — they continue ringing until timeout.

The rejection API is only fired from the WebSocket event path (EventPropagationPolicyCallBusyHandler.shouldPropagateEventisBusyWithAnotherCall(cid, rejectViaApi = true)). If the socket is not connected when the push arrives, that path is never reached.

Consider calling isBusyWithAnotherCall(callId.cid, rejectViaApi = true) here as well, or triggering the rejection through a separate helper, so the caller receives timely feedback even in background/push-only scenarios.

🤖 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/notifications/handlers/StreamDefaultNotificationHandler.kt`
around lines 167 - 173, When suppressing the incoming-call notification in
StreamDefaultNotificationHandler (inside the shouldShowIncomingCallNotification
check), ensure you also trigger the busy-rejection API by calling
isBusyWithAnotherCall(callId.cid, rejectViaApi = true) (or invoking the existing
helper that sends RejectReason.Busy) so the caller receives a timely Reject
response when the SDK is backgrounded and the socket is disconnected; update the
branch that returns early after shouldShowIncomingCallNotification to call that
method before returning.
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/CallBusyHandler.kt (1)

34-34: Use split(":", limit = 2) to guard against malformed CIDs.

"foo".split(":") returns a single-element list; the destructuring val (type, id) = ... then throws IndexOutOfBoundsException. Using limit = 2 is consistent with defensive coding, costs nothing, and avoids a potential crash if a server ever emits a CID without a colon.

♻️ Proposed fix
-        val (type, id) = callCid.split(":")
+        val parts = callCid.split(":", limit = 2)
+        if (parts.size < 2) return false
+        val (type, id) = parts
🤖 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/call/CallBusyHandler.kt`
at line 34, The destructuring in CallBusyHandler that does val (type, id) =
callCid.split(":") can throw IndexOutOfBoundsException for malformed CIDs;
change the split call to use a limit of 2 (i.e., split(":", limit = 2)) so it
returns at most two parts and then handle the case where the second part is
missing (e.g., provide a safe fallback or abort early) to avoid crashes when
callCid lacks a colon.
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt (1)

90-93: Redundant (client as StreamVideoClient) casts — use streamVideoClient.

streamVideoClient at line 72 already holds the result of client as StreamVideoClient. Casting again on lines 90 and 92 is noise and repeats a fallible unsafe cast.

♻️ Proposed fix
-    val rejectCallWhenBusy: Boolean = (client as StreamVideoClient).rejectCallWhenBusy
+    public val rejectCallWhenBusy: Boolean = streamVideoClient.rejectCallWhenBusy
 
-    internal val callBusyHandler = CallBusyHandler(this.client as StreamVideoClient)
+    internal val callBusyHandler = CallBusyHandler(streamVideoClient)
     internal val eventPropagationPolicy = EventPropagationPolicy(callBusyHandler)
🤖 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/ClientState.kt`
around lines 90 - 93, The redundant unsafe casts (client as StreamVideoClient)
used when initializing rejectCallWhenBusy, callBusyHandler, and
eventPropagationPolicy should be replaced with the already-computed
streamVideoClient property (defined earlier at line 72); update the initializers
for rejectCallWhenBusy, callBusyHandler, and eventPropagationPolicy to use
streamVideoClient instead of casting client again to avoid duplicate/unsafe
casts and improve clarity.
stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/StreamVideoClientTest.kt (3)

206-215: fireEvent full flow test only asserts one side effect.

The test name claims to verify the "full flow" but only checks that state.handleEvent is invoked. The intermediate steps — notifyClientSubscriptions, forwardToCallSubscriptions, and propagateEventToCall — are not verified. Consider either narrowing the name to match the single assertion, or adding verification of the additional stages.

🧪 Example extended assertions
     `@Test`
     fun `fireEvent full flow executes in order when allowed`() {
         val event = mockk<VideoEvent>()
+        val call = mockk<Call>(relaxed = true)
+        val listener = mockk<VideoEventListener<VideoEvent>>(relaxed = true)
+        val sub = EventSubscription(listener)
+        client::class.java.getDeclaredField("subscriptions").apply {
+            isAccessible = true
+            set(client, mutableSetOf(sub))
+        }
 
         every { policy.shouldPropagate(event) } returns true
 
         client.fireEvent(event)
 
         verify { state.handleEvent(event) }
+        verify { listener.onEvent(event) }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/StreamVideoClientTest.kt`
around lines 206 - 215, Test "fireEvent full flow executes in order when
allowed" only asserts state.handleEvent; either rename it to reflect that single
assertion or extend the test to verify the intermediate steps and ordering. If
extending, after calling client.fireEvent(event) add verifications for
policy.shouldPropagate(event) (already mocked),
notifyClientSubscriptions(event), forwardToCallSubscriptions(event) and
propagateEventToCall(event), and assert call order (e.g. using verifySequence or
verifyOrder) that policy check happens first then notifyClientSubscriptions,
forwardToCallSubscriptions, propagateEventToCall and finally state.handleEvent
to match the "full flow" behavior.

41-41: Consider extending TestBase for fast unit tests.

Per coding guidelines, fast unit tests should extend TestBase rather than bare JUnit classes, for consistent test infrastructure across the module.

Based on learnings: "Use TestBase for fast unit tests and IntegrationTestBase for end-to-end call flows."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/StreamVideoClientTest.kt`
at line 41, The test class StreamVideoClientTest is a fast unit test and should
extend TestBase instead of a bare JUnit class; update the class declaration for
StreamVideoClientTest to inherit from TestBase (and add the required import for
TestBase if missing) so it uses the shared fast-test infrastructure and
utilities provided by TestBase rather than a bare JUnit class.

148-167: Misleading test name for shouldProcessCallAcceptedEvent.

The test name reads "not for outgoing call" but the setup does have an outgoing ringing call (RingingState.Outgoing on CID video:123). The gate fires because the accepted event targets a different call (video:999). A name like `returns false when accepted event is for a different call while ringing state is outgoing` better describes the scenario.

♻️ Proposed rename
-    fun `shouldProcessCallAcceptedEvent returns false when accepted event not for outgoing call`() {
+    fun `shouldProcessCallAcceptedEvent returns false when accepted event targets a different call during outgoing ring`() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/StreamVideoClientTest.kt`
around lines 148 - 167, Rename the test function to accurately describe the
scenario: it should assert that shouldProcessCallAcceptedEvent returns false
when the accepted event targets a different call while there is an outgoing
ringing call; update the test method name (currently
`shouldProcessCallAcceptedEvent returns false when accepted event not for
outgoing call`) to something like `returns false when accepted event is for a
different call while ringing state is outgoing` so it clearly reflects the setup
using CallAcceptedEvent, ringingCall with RingingState.Outgoing, and the
mismatched callCid.
stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/call/CallBusyHandlerTest.kt (2)

43-44: Consider extending TestBase for fast unit tests.

Per coding guidelines, fast unit tests should use TestBase. Using TestBase can provide consistent test infrastructure across the module.

Based on learnings: "Use TestBase for fast unit tests and IntegrationTestBase for end-to-end call flows."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/call/CallBusyHandlerTest.kt`
around lines 43 - 44, Update the CallBusyHandlerTest class to extend TestBase to
adopt the module's fast-unit-test infrastructure: modify the class declaration
for CallBusyHandlerTest to inherit from TestBase (ensure the correct TestBase
import is added) and move or remove any duplicate setup/teardown that TestBase
already provides so tests reuse its shared coroutine dispatchers and helpers.

80-84: Blank line missing before mockCall helper, and ringingCall busy-path not tested.

Two minor issues:

  1. Style: mockCall immediately follows tearDown() with no blank line, deviating from the surrounding formatting convention.

  2. Coverage gap: All "busy" test cases only exercise the activeCall path. There's no test verifying that isBusyWithAnotherCall returns true when the user is busy with a ringing call (i.e., ringingCall is non-null with a different ID), nor a case where rejectCallWhenBusy = true but both activeCall and ringingCall are null (should return false).

🧪 Example missing test cases
+    `@Test`
+    fun `returns true when busy with an incoming ringing call`() {
+        every { clientState.rejectCallWhenBusy } returns true
+        every { clientState.activeCall } returns MutableStateFlow(null)
+        every { clientState.ringingCall } returns MutableStateFlow(mockCall("999"))
+
+        val result = handler.isBusyWithAnotherCall("video:123", rejectViaApi = false)
+
+        assertTrue(result)
+    }
+
+    `@Test`
+    fun `returns false when rejectCallWhenBusy is enabled but no active or ringing call`() {
+        every { clientState.rejectCallWhenBusy } returns true
+        every { clientState.activeCall } returns MutableStateFlow(null)
+        every { clientState.ringingCall } returns MutableStateFlow(null)
+
+        val result = handler.isBusyWithAnotherCall("video:123")
+
+        assertFalse(result)
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/call/CallBusyHandlerTest.kt`
around lines 80 - 84, Add a blank line before the mockCall helper to match file
formatting and add two unit tests for CallBusyHandler: one that sets ringingCall
to a Call with a different id (and activeCall null) and asserts
isBusyWithAnotherCall(...) returns true, and another that sets
rejectCallWhenBusy = true with both activeCall and ringingCall null and asserts
isBusyWithAnotherCall(...) returns false; reference the mockCall helper to
create ringingCall and exercise the isBusyWithAnotherCall logic alongside
existing activeCall tests and keep tearDown() unchanged.
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt (1)

611-613: deliverToDestroyedCalls is a one-liner wrapper with no value.

This trivial delegation to deliverIntentToDestroyedCalls adds indirection without abstraction benefit. Since deliverIntentToDestroyedCalls is already private, either inline the call at the fireEvent call site or remove the wrapper.

♻️ Proposed simplification
-    internal fun deliverToDestroyedCalls(event: VideoEvent) {
-        deliverIntentToDestroyedCalls(event)
-    }
-
     private fun shouldProcessDestroyedCall(event: VideoEvent, callCid: String): Boolean {

And in fireEvent:

-        deliverToDestroyedCalls(event)
+        deliverIntentToDestroyedCalls(event)

If the wrapper is kept for testability (to allow spying in StreamVideoClientTest), make deliverIntentToDestroyedCalls internal instead and remove the wrapper.

🤖 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/StreamVideoClient.kt`
around lines 611 - 613, deliverToDestroyedCalls is a redundant one-line wrapper
around private deliverIntentToDestroyedCalls; remove the indirection by deleting
deliverToDestroyedCalls and calling deliverIntentToDestroyedCalls directly from
fireEvent, or if the wrapper exists only to enable test spying in
StreamVideoClientTest, instead make deliverIntentToDestroyedCalls internal and
remove deliverToDestroyedCalls so tests can access it; update references
(deliverToDestroyedCalls -> deliverIntentToDestroyedCalls) and
StreamVideoClientTest accordingly.
demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt (1)

349-349: rejectCallWhenBusy = false is the parameter's default — explicit pass is redundant.

Since StreamVideoBuilder already defaults rejectCallWhenBusy to false, this line has no behavioral effect. It can be safely removed, or kept as-is for discoverability in the demo app.

♻️ Suggested simplification (demo app only)
-            rejectCallWhenBusy = false,
🤖 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/util/StreamVideoInitHelper.kt`
at line 349, The call to StreamVideoBuilder sets rejectCallWhenBusy = false
which is redundant because StreamVideoBuilder already defaults that parameter to
false; remove the explicit rejectCallWhenBusy = false argument from the
StreamVideoBuilder invocation in StreamVideoInitHelper to simplify the demo code
(or if you prefer discoverability, keep a commented note instead). Locate the
builder usage of StreamVideoBuilder in StreamVideoInitHelper and delete the
rejectCallWhenBusy = false entry (or replace it with a short commented
explanation) so behavior remains unchanged and the code is cleaner.
🤖 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/call/CallBusyHandler.kt`:
- Around line 40-44: The fire-and-forget in CallBusyHandler (the isBusy &&
rejectViaApi branch using streamVideo.scope.launch) swallows exceptions from
streamVideo.call(type, id).reject(RejectReason.Busy); change this to capture and
handle failures by wrapping the reject call in a try/catch (or use runCatching)
inside the coroutine, log the error via the appropriate logger, and surface the
failure upstream (e.g., return a Result or call a failure callback) instead of
silently dropping it; update the code around streamVideo.scope.launch and the
reject invocation to ensure errors are logged and propagated.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt`:
- Line 90: The property declaration for rejectCallWhenBusy in ClientState lacks
an explicit public visibility modifier; update the declaration of val
rejectCallWhenBusy in the ClientState class (the property that backs
getRejectCallWhenBusy()) to include the public visibility modifier so it matches
other client-facing properties and the public API surface.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt`:
- Around line 105-107: In the KDoc for the property rejectCallWhenBusy in
StreamVideoBuilder (the comment block that currently reads "CallRingEvent event
will not be propagated..."), remove the redundant word "event" so it reads
"CallRingEvent will not be propagated..." to avoid the duplicate noun; update
only the documentation text in the StreamVideoBuilder.kt KDoc for
rejectCallWhenBusy.

In
`@stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/stories/RingTest.kt`:
- Around line 115-116: The test function named `Reject a call` has its `@Test`
annotation commented out; restore test visibility by adding an `@Test`
annotation and mark it skipped using `@Ignore("reason")` to match the file's
convention (see other skipped tests in this file). Edit the `fun `Reject a
call`() = runTest { ... }` declaration in `RingTest` to use `@Test` together
with `@Ignore("temporarily disabled" or a short reason)` so the test appears in
reports but is skipped.

In
`@stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt`:
- Around line 297-298: The shouldAcceptNewCall implementation in
StreamCallActivity currently returns
StreamVideo.instanceOrNull()?.state?.rejectCallWhenBusy ?: true which inverts
the intended semantics; change it to return the negation of rejectCallWhenBusy
and use a safe false default: use
!(StreamVideo.instanceOrNull()?.state?.rejectCallWhenBusy ?: true) replaced with
!(... ?: true) or more clearly
(StreamVideo.instanceOrNull()?.state?.rejectCallWhenBusy == false) so that when
rejectCallWhenBusy is true we do not accept new calls, when false we accept, and
when null we default to false (do not switch).

---

Nitpick comments:
In
`@demo-app/src/main/kotlin/io/getstream/video/android/util/StreamVideoInitHelper.kt`:
- Line 349: The call to StreamVideoBuilder sets rejectCallWhenBusy = false which
is redundant because StreamVideoBuilder already defaults that parameter to
false; remove the explicit rejectCallWhenBusy = false argument from the
StreamVideoBuilder invocation in StreamVideoInitHelper to simplify the demo code
(or if you prefer discoverability, keep a commented note instead). Locate the
builder usage of StreamVideoBuilder in StreamVideoInitHelper and delete the
rejectCallWhenBusy = false entry (or replace it with a short commented
explanation) so behavior remains unchanged and the code is cleaner.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/CallBusyHandler.kt`:
- Line 34: The destructuring in CallBusyHandler that does val (type, id) =
callCid.split(":") can throw IndexOutOfBoundsException for malformed CIDs;
change the split call to use a limit of 2 (i.e., split(":", limit = 2)) so it
returns at most two parts and then handle the case where the second part is
missing (e.g., provide a safe fallback or abort early) to avoid crashes when
callCid lacks a colon.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt`:
- Around line 90-93: The redundant unsafe casts (client as StreamVideoClient)
used when initializing rejectCallWhenBusy, callBusyHandler, and
eventPropagationPolicy should be replaced with the already-computed
streamVideoClient property (defined earlier at line 72); update the initializers
for rejectCallWhenBusy, callBusyHandler, and eventPropagationPolicy to use
streamVideoClient instead of casting client again to avoid duplicate/unsafe
casts and improve clarity.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/handlers/StreamDefaultNotificationHandler.kt`:
- Around line 154-157: Change the visibility of the helper function
shouldShowIncomingCallNotification in StreamDefaultNotificationHandler from
internal to private: locate the function signature "internal fun
shouldShowIncomingCallNotification(callBusyHandler: CallBusyHandler, callCid:
String)" and make it private so it is only callable within the class (ensure no
external references rely on it; it's used by onRingingCall). Keep the rest of
the method body unchanged.
- Around line 167-173: When suppressing the incoming-call notification in
StreamDefaultNotificationHandler (inside the shouldShowIncomingCallNotification
check), ensure you also trigger the busy-rejection API by calling
isBusyWithAnotherCall(callId.cid, rejectViaApi = true) (or invoking the existing
helper that sends RejectReason.Busy) so the caller receives a timely Reject
response when the SDK is backgrounded and the socket is disconnected; update the
branch that returns early after shouldShowIncomingCallNotification to call that
method before returning.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt`:
- Around line 611-613: deliverToDestroyedCalls is a redundant one-line wrapper
around private deliverIntentToDestroyedCalls; remove the indirection by deleting
deliverToDestroyedCalls and calling deliverIntentToDestroyedCalls directly from
fireEvent, or if the wrapper exists only to enable test spying in
StreamVideoClientTest, instead make deliverIntentToDestroyedCalls internal and
remove deliverToDestroyedCalls so tests can access it; update references
(deliverToDestroyedCalls -> deliverIntentToDestroyedCalls) and
StreamVideoClientTest accordingly.

In
`@stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/call/CallBusyHandlerTest.kt`:
- Around line 43-44: Update the CallBusyHandlerTest class to extend TestBase to
adopt the module's fast-unit-test infrastructure: modify the class declaration
for CallBusyHandlerTest to inherit from TestBase (ensure the correct TestBase
import is added) and move or remove any duplicate setup/teardown that TestBase
already provides so tests reuse its shared coroutine dispatchers and helpers.
- Around line 80-84: Add a blank line before the mockCall helper to match file
formatting and add two unit tests for CallBusyHandler: one that sets ringingCall
to a Call with a different id (and activeCall null) and asserts
isBusyWithAnotherCall(...) returns true, and another that sets
rejectCallWhenBusy = true with both activeCall and ringingCall null and asserts
isBusyWithAnotherCall(...) returns false; reference the mockCall helper to
create ringingCall and exercise the isBusyWithAnotherCall logic alongside
existing activeCall tests and keep tearDown() unchanged.

In
`@stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/EventPropagationPolicyTest.kt`:
- Line 28: The test imports kotlin.test.Test while the suite uses
org.junit.Test; update the EventPropagationPolicyTest import by replacing
kotlin.test.Test with org.junit.Test (and remove any other kotlin.test.Test
usages) so the class EventPropagationPolicyTest and its test methods use
org.junit.Test consistently with the other tests (e.g., RingTest,
StreamDefaultNotificationHandlerTest).

In
`@stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/handlers/StreamDefaultNotificationHandlerTest.kt`:
- Around line 210-213: Remove the redundant mid-test unmockkObject(StreamVideo)
call in StreamDefaultNotificationHandlerTest: the verify block that asserts
mockIntentResolver.searchIncomingCallPendingIntent(...) is called exactly 0
times should remain, but delete the trailing unmockkObject(StreamVideo) line
because `@After` tearDown() already calls unmockkAll(), so this explicit
unmockkObject(StreamVideo) is unnecessary and inconsistent with the other test.
- Line 107: Rename the test fixture method setUp2 to setUp in the
StreamDefaultNotificationHandlerTest class and keep its `@Before` annotation so it
follows standard lifecycle naming; update any internal references or test
utilities that call setUp2 to call setUp instead, and ensure there are no
duplicate `@Before` methods after the rename to avoid lifecycle conflicts.

In
`@stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/StreamVideoClientTest.kt`:
- Around line 206-215: Test "fireEvent full flow executes in order when allowed"
only asserts state.handleEvent; either rename it to reflect that single
assertion or extend the test to verify the intermediate steps and ordering. If
extending, after calling client.fireEvent(event) add verifications for
policy.shouldPropagate(event) (already mocked),
notifyClientSubscriptions(event), forwardToCallSubscriptions(event) and
propagateEventToCall(event), and assert call order (e.g. using verifySequence or
verifyOrder) that policy check happens first then notifyClientSubscriptions,
forwardToCallSubscriptions, propagateEventToCall and finally state.handleEvent
to match the "full flow" behavior.
- Line 41: The test class StreamVideoClientTest is a fast unit test and should
extend TestBase instead of a bare JUnit class; update the class declaration for
StreamVideoClientTest to inherit from TestBase (and add the required import for
TestBase if missing) so it uses the shared fast-test infrastructure and
utilities provided by TestBase rather than a bare JUnit class.
- Around line 148-167: Rename the test function to accurately describe the
scenario: it should assert that shouldProcessCallAcceptedEvent returns false
when the accepted event targets a different call while there is an outgoing
ringing call; update the test method name (currently
`shouldProcessCallAcceptedEvent returns false when accepted event not for
outgoing call`) to something like `returns false when accepted event is for a
different call while ringing state is outgoing` so it clearly reflects the setup
using CallAcceptedEvent, ringingCall with RingingState.Outgoing, and the
mismatched callCid.

Comment on lines +40 to +44
if (isBusy && rejectViaApi) {
streamVideo.scope.launch {
streamVideo.call(type, id).reject(RejectReason.Busy)
}
}
Copy link

@coderabbitai coderabbitai bot Feb 18, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Silent rejection failure leaves the remote caller ringing indefinitely.

The fire-and-forget launch swallows any exception from reject(RejectReason.Busy). If the API call fails (network error, server error, etc.), the caller never receives a busy signal. At minimum, the failure should be logged; ideally it is also surfaced upstream.

🛡️ Proposed fix
         if (isBusy && rejectViaApi) {
             streamVideo.scope.launch {
-                streamVideo.call(type, id).reject(RejectReason.Busy)
+                streamVideo.call(type, id).reject(RejectReason.Busy)
+                    .onError { error ->
+                        // Log so operators can observe rejection failures
+                        streamLog(priority = StreamLog.WARN) {
+                            "[CallBusyHandler] Failed to reject busy call $callCid: ${error.message}"
+                        }
+                    }
             }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (isBusy && rejectViaApi) {
streamVideo.scope.launch {
streamVideo.call(type, id).reject(RejectReason.Busy)
}
}
if (isBusy && rejectViaApi) {
streamVideo.scope.launch {
streamVideo.call(type, id).reject(RejectReason.Busy)
.onError { error ->
// Log so operators can observe rejection failures
streamLog(priority = StreamLog.WARN) {
"[CallBusyHandler] Failed to reject busy call $callCid: ${error.message}"
}
}
}
}
🤖 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/call/CallBusyHandler.kt`
around lines 40 - 44, The fire-and-forget in CallBusyHandler (the isBusy &&
rejectViaApi branch using streamVideo.scope.launch) swallows exceptions from
streamVideo.call(type, id).reject(RejectReason.Busy); change this to capture and
handle failures by wrapping the reject call in a try/catch (or use runCatching)
inside the coroutine, log the error via the appropriate logger, and surface the
failure upstream (e.g., return a Result or call a failure callback) instead of
silently dropping it; update the code around streamVideo.scope.launch and the
reject invocation to ensure errors are logged and propagated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's okay

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 skip this suggestion for error handling in the busy call rejection flow.

   /)_/)
  (o.o)
  />📋  Noted!

✏️ Learnings added
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:32:54.490Z
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.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: rahul-lohra
Repo: GetStream/stream-video-android PR: 1588
File: stream-video-android-core/api/stream-video-android-core.api:3561-3569
Timestamp: 2026-01-07T09:52:52.695Z
Learning: The `LocalCallMissedEvent` class in `stream-video-android-core` is only instantiated internally by the library maintainers. Clients only read these instances and never construct them directly, so breaking changes to the constructor are acceptable.

@sonarqubecloud
Copy link

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

Labels

pr:new-feature Adds new functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants