Skip to content

refactor(core): decompose Call into focused internal components#1747

Open
PratimMallick wants to merge 7 commits into
developfrom
refactor/call-class-decomposition
Open

refactor(core): decompose Call into focused internal components#1747
PratimMallick wants to merge 7 commits into
developfrom
refactor/call-class-decomposition

Conversation

@PratimMallick

@PratimMallick PratimMallick commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Goal

closes AND-1318
Call.kt had grown to ~2,260 lines and mixed many unrelated responsibilities (REST API calls, media, session/RTC lifecycle, reconnect state machine, connectivity/ICE monitoring, events, stats, rendering, teardown). This made it hard to read, test, and evolve.

This PR decomposes Call into focused internal collaborators while keeping Call as a thin, binary-compatible public facade — no public API changes.

Implementation

  • Extracted 11 internal components under core/call/components/, each owning one concern; Call delegates to them and keeps its existing member names as delegating accessors so RtcSession, CallState, and existing callers keep working:
    • CallApiClient — coordinator REST wrappers
    • CallEventManagerevents flow, subscriptions, dispatch
    • CallStatsReporter — stats collection/reporting, statsReport/statLatencyHistory
    • CallRenderer — renderer binding, visibility, screenshots, incoming media overrides
    • CallMediaManager — peer-connection factory, media manager, audio pipeline, screen sharing
    • CallSessionManager — RTC session StateFlow, session ids, location, reconnect counters
    • CallIceConnectionMonitor — publisher/subscriber ICE-restart monitoring
    • CallConnectivityMonitor — network listener + leave-on-disconnect timeout
    • CallJoinCoordinator — join flow, retry loop, RtcSession creation/connection
    • CallReconnector — FAST/REJOIN/MIGRATE reconnect state machine, failed-SFU tracking
    • CallLifecycleManager — leave/end/cleanup, leave guard, destroyed flag
  • Call remains the orchestrator for a few cross-component flows (e.g. monitorSession).
  • Merged latest develop and reconciled its Call changes into the new components: the typed SFU connect-failure recovery (SfuConnectionResult.Success/Failure + SfuConnectFailureCause) now lives in CallJoinCoordinator; the unitTestRtcSessionFactory test hook replaces TestInstanceProvider.rtcSessionCreator.
  • Updated white-box reflection tests to target the new component owners after internals moved out of Call.

Testing

  • ./gradlew :stream-video-android-core:testDebugUnitTest — pass (894 tests, 0 failures)
  • ./gradlew :stream-video-android-core:apiCheck — pass (public API unchanged / binary-compatible)
  • ./gradlew :stream-video-android-core:spotlessCheck — pass
  • ./gradlew assembleDebug — pass (all modules incl. demo-app)

☑️Contributor Checklist

General

  • I have signed the Stream CLA (required)
  • Assigned a person / code owner group (required)
  • Thread with the PR link started in a respective Slack channel (required internally)
  • PR targets the develop branch
  • PR is linked to the GitHub issue it resolves

Code & documentation

  • Changelog is updated with client-facing changes (N/A — no client-facing/API changes)
  • New code is covered by unit tests (existing suite retained; reflection tests updated)
  • Comparison screenshots added for visual changes (N/A — no UI change)
  • Affected documentation updated (KDocs, docusaurus, tutorial)
  • Tutorial starter kit updated
  • Examples/guides starter kits updated (stream-video-examples)

☑️Reviewer Checklist

  • XML sample runs & works
  • Compose sample runs & works
  • Tutorial starter kit
  • Example starter kits work
  • UI Changes correct (before & after images)
  • Bugs validated (bugfixes)
  • New feature tested and works
  • Release notes and docs clearly describe changes
  • All code we touched has new or updated KDocs
  • Check the SDK Size Comparison table in the CI logs

🎉 GIF

Skipped — internal refactor with no user-facing change.

Made with Cursor

Summary by CodeRabbit

  • Improvements
    • Improved call joining and recovery when network connectivity is interrupted.
    • Added more reliable handling for reconnects, rejoining, and server migrations.
    • Improved detection and recovery from WebRTC connection interruptions.
    • Enhanced media, rendering, audio-level monitoring, and device connectivity handling.
    • Improved call statistics reporting and event delivery.
    • Streamlined call lifecycle cleanup and resource management.
  • Tests
    • Updated coverage for reconnect behavior, session handling, network changes, and call initialization.

PratimMallick and others added 2 commits June 25, 2026 13:06
Break the ~2,260-line Call class into 11 internal collaborators under
call/components (CallApiClient, CallStatsReporter, CallRenderer,
CallEventManager, CallMediaManager, CallSessionManager,
CallIceConnectionMonitor, CallConnectivityMonitor, CallJoinCoordinator,
CallReconnector, CallLifecycleManager). Call remains a thin, binary-compatible
public facade that delegates to them; public API is unchanged (apiCheck passes).

Update white-box reflection tests to target the new component owners after
internals moved out of Call.

Co-authored-by: Cursor <cursoragent@cursor.com>
…nto refactor/call-class-decomposition

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt
@PratimMallick
PratimMallick requested a review from a team as a code owner July 17, 2026 08:18
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

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, or the PR is bot-authored.
  • An issue is linked (Linear ticket or GitHub issue), or the PR is bot-authored.

🎉 Great job! This PR is ready for review.

@PratimMallick PratimMallick added the pr:improvement Enhances an existing feature or code label Jul 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

SDK Size Comparison 📏

SDK Before After Difference Status
stream-video-android-core 12.27 MB 12.29 MB 0.02 MB 🟢
stream-video-android-ui-xml 5.68 MB 5.68 MB 0.00 MB 🟢
stream-video-android-ui-compose 6.20 MB 6.20 MB 0.00 MB 🟢

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Call responsibility decomposition

Layer / File(s) Summary
Call wiring and shared state
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt, stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallSessionManager.kt, stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallEventManager.kt
Call now exposes collaborator-backed session, event, media, statistics, and lifecycle state, with initialization-order coverage updated.
REST and call operation façade
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallApiClient.kt, stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt
Coordinator REST calls, member actions, lifecycle actions, recordings, broadcasts, reactions, permissions, and transcription APIs are routed through CallApiClient.
Media, rendering, and statistics ownership
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallMediaManager.kt, CallRenderer.kt, CallStatsReporter.kt
Media setup, audio levels, peer connection factory recreation, screen sharing, rendering, screenshots, incoming track controls, and periodic statistics are delegated to dedicated components.
Join, connectivity, and reconnection flows
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallJoinCoordinator.kt, CallReconnector.kt, CallConnectivityMonitor.kt, CallIceConnectionMonitor.kt
Join retries, SFU session creation, network recovery, ICE recovery, reconnect strategy escalation, migration, and failed-SFU tracking are moved into coordinators and monitors.
Lifecycle teardown and cleanup
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallLifecycleManager.kt, stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt
Guarded leave, end-call handling, analytics, media shutdown, statistics stopping, session cleanup, and scope disposal are centralized in CallLifecycleManager.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Call
  participant CallJoinCoordinator
  participant RtcSession
  participant CallReconnector
  participant CallLifecycleManager

  Call->>CallJoinCoordinator: join()
  CallJoinCoordinator->>RtcSession: connect to SFU
  RtcSession-->>CallJoinCoordinator: connection result
  Call->>CallReconnector: reconnect(strategy, reason)
  CallReconnector->>CallJoinCoordinator: rejoin or migrate
  CallJoinCoordinator->>RtcSession: create replacement session
  CallReconnector-->>Call: connected or exhausted
  CallReconnector->>CallLifecycleManager: leave after exhausted recovery
Loading

Possibly related PRs

Suggested labels: pr:internal

Suggested reviewers: rahul-lohra, aleksandar-apostolov

Poem

I’m a rabbit with delegates bright,
Sorting calls through day and night.
Join, reconnect, render, and leave,
Each has a burrow up its sleeve.
Less tangled code, more hops in flight! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: decomposing Call into focused internal components.
Description check ✅ Passed The description covers the goal, implementation, testing, and checklist sections required by the template; no UI section is acceptable for this refactor.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/call-class-decomposition

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 15

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt`:
- Around line 413-420: Remove the duplicated join preflight logic from Call’s
public join flow, including analytics, permission observation/checking, and
guest-token waiting around the shown joinCoordinator.join call. Keep that setup
exclusively in CallJoinCoordinator.join(), leaving Call’s join method as a
direct delegation with the existing arguments.
- Around line 759-765: Update Call.shutDownJobsGracefully so teardown does not
immediately cancel scope while supervisorJob children are draining. Complete the
supervisor to reject new children, await existing children within an owned
structured client scope, then perform final scope cancellation; handle
cancellation and child failures deterministically without relying on the
detached UserScope launch.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallConnectivityMonitor.kt`:
- Around line 73-92: Update the disconnect handling around
leaveTimeoutAfterDisconnect so the timeout coroutine is created only when
clientImpl.leaveAfterDisconnectSeconds is positive. Treat zero or negative
values as disabled, avoiding delay(0) and the subsequent call.leave flow;
preserve the existing reconnection check and timeout behavior for positive
values.
- Line 40: Remove or redact call.id from the logger tags used by
CallConnectivityMonitor, CallIceConnectionMonitor, CallJoinCoordinator, and
CallReconnector. Update the logger initialization in
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallConnectivityMonitor.kt:40-40,
CallIceConnectionMonitor.kt:35-35, CallJoinCoordinator.kt:52-52, and
CallReconnector.kt:69-69 while preserving non-sensitive call context such as
call.type.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallEventManager.kt`:
- Around line 77-95: The fireEvent method currently invokes listeners while
holding the subscriptions monitor and allows listener exceptions to abort
dispatch. Snapshot active subscriptions under synchronized(subscriptions),
release the lock, invoke each matching listener with per-listener failure
isolation so remaining callbacks continue, then always call
events.tryEmit(event) afterward.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallIceConnectionMonitor.kt`:
- Around line 69-83: Update startSubscriberMonitor to collect the session and
subscriber flows rather than reading call.session.value?.subscriber?.value once
before collecting iceState. Keep the monitor active when the subscriber is
initially null and automatically switch to observing each replacement
subscriber’s iceState, matching the publisher monitor’s flow-observation pattern
while preserving the existing restart and logging behavior.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallJoinCoordinator.kt`:
- Around line 163-184: Update the join flow in CallJoinCoordinator so a failed
join also resets the join-and-ring progress state. Handle the error on the outer
join result, not only inside the flatMap block, and call
state.toggleJoinAndRingProgress(false) while preserving the existing
ring-failure handling.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallLifecycleManager.kt`:
- Around line 63-80: Sanitize all raw call identifiers in component logging:
update CallLifecycleManager.leave and internalLeave to remove or hash call.cid
and call.id, and sanitize or remove call.id from logger tags in
CallLifecycleManager (39-39), CallMediaManager (47-47), CallRenderer (43-43),
and CallStatsReporter (35-35). Preserve the existing log context without
exposing call IDs or CIDs.
- Around line 107-118: Update the coroutine launched in the call-leave flow to
wrap the leave analytics, tracing, stats, and leave-event operations in a
try/finally block, with cleanup() in finally. Ensure cleanup always runs after
the leave markers are attempted, including when onCallLeave() throws or the
coroutine is cancelled.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallMediaManager.kt`:
- Around line 270-272: Update CallMediaManager.cleanup() to dispose the owned
peer-connection factory during final teardown, in addition to calling
mediaManager.cleanup(). Reuse the existing factory instance and disposal
mechanism already used during recreation, ensuring native WebRTC resources are
released.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallReconnector.kt`:
- Around line 254-271: Update the FAST reconnect failure handling in
CallReconnector’s ReconnectOutcome.Failed branch to consult
MAX_FAST_RECONNECT_ATTEMPTS and escalate to the REJOIN strategy once the
configured fast-retry limit is reached. Preserve the existing migration and
deadline escalation conditions, and apply the same logic to the corresponding
failure branch around the alternate referenced location.
- Around line 178-187: Update the reconnect loop in CallReconnector so the
network-availability wait is skipped when the active strategy is DISCONNECT,
allowing the server-requested disconnect path to execute offline and produce
SFU_DISCONNECT. Preserve the existing connectivity polling behavior for other
reconnect strategies.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallRenderer.kt`:
- Around line 156-178: Update takeScreenshot around suspendCancellableCoroutine
to register an invokeOnCancellation handler that asynchronously removes
screenshotSink through the same call.scope cleanup path used after the first
frame. Ensure cancellation cleanup is safe if no frame has arrived, while
preserving the existing first-frame removal behavior.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallStatsReporter.kt`:
- Around line 45-57: Update CallStatsReporter.start so the coroutine keeps the
initial reportingIntervalMs delay, then sends the first stats report immediately
upon entering the while (isActive) loop; retain the interval delay only for
subsequent reports.

In
`@stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/CallFieldDeclarationOrderTest.kt`:
- Around line 53-67: Update CallFieldDeclarationOrderTest to assert that
eventManagerLine precedes eventsLine, in addition to the existing ordering
checks. Keep the requirement that both eventManager and events are declared
before state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ce0c99a0-1916-4ef7-85a5-2b59619e7d82

📥 Commits

Reviewing files that changed from the base of the PR and between be52131 and 16b0487.

📒 Files selected for processing (17)
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallApiClient.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallConnectivityMonitor.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallEventManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallIceConnectionMonitor.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallJoinCoordinator.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallLifecycleManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallMediaManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallReconnector.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallRenderer.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallSessionManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/components/CallStatsReporter.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/CallFieldDeclarationOrderTest.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/reconnect/FailedSfuIdsTest.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/reconnect/ReconnectAttemptsCountTest.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/reconnect/ReconnectSessionIdTest.kt
  • stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/rtc/JoinRecoverableFailureTest.kt

Comment thread stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt Outdated
PratimMallick and others added 5 commits July 21, 2026 17:02
Call.join() ran join analytics, permission checks, and the guest-token
wait before delegating to CallJoinCoordinator.join(), which performed the
exact same preflight — so every join() executed it twice. Make the facade
a pure delegation so the preflight runs only once in the coordinator.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add JVM unit tests for the extracted Call collaborators (CallApiClient,
CallEventManager, CallSessionManager, CallRenderer, CallMediaManager) to
raise coverage on the refactor's new code toward the SonarCloud gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ents

Broaden new-code coverage for the extracted Call components: exercise the
CallJoinCoordinator retry loop and join-and-ring flow (via the RtcSession test
factory), the CallConnectivityMonitor reconnect/leave listener, the reachable
CallReconnector state-machine branches, the CallIceConnectionMonitor restart
paths, plus additional CallMediaManager (monitorHeadset, not-selected devices)
and CallApiClient (ring request, ringing create) cases.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add a unitTestRtcSessionFactory seam to CallReconnector's rejoin/migrate so the
session-swap, monitor and finalize paths are unit-testable, and add tests for
them (success + retry-until-exhausted). Also cover CallRenderer's incoming-audio
track walking for all/selected participants.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

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

See analysis details on SonarQube Cloud

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

Labels

pr:improvement Enhances an existing feature or code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant