Skip to content

Improve the behaviour of StreamCallActivity when the call has been declined - #1478

Merged
aleksandar-apostolov merged 25 commits into
developfrom
feature/rahullohra/group-calls
Jul 28, 2025
Merged

Improve the behaviour of StreamCallActivity when the call has been declined#1478
aleksandar-apostolov merged 25 commits into
developfrom
feature/rahullohra/group-calls

Conversation

@rahul-lohra

@rahul-lohra rahul-lohra commented Jul 22, 2025

Copy link
Copy Markdown
Contributor

🎯 Goal

  1. Improve Inconsistent Call Screen Behavior on Decline
  2. Avoid double finish of StreamCallActivity
  3. Improve reject api call lifecycle scope
  4. Allow the client to handle when there is an incoming call while the user is already in a call

1. Improve Inconsistent Call Screen Behavior on Decline

Setup:
iOS user (A) initiates a group call to two Android users: (B) and (C).
The call enters the Ringing state on both Android devices.
Issue:
User B declines the call.
❌ Problem: Despite declining, User B remains stuck on the clling screen, with the progress bar still visible.
✅ Expected behavior: User B should be navigated back to the main activity immediately after declining.
Later, User C also declines the call.
✅ Now, User B is finally redirected back to the main screen, even though they had already declined earlier.
Conclusion:
There seems to be a UI or state handling issue where the call screen doesn't dismiss immediately after a decline unless all recipients decline.

🛠️ Problem

The existing approach for loading StreamCallActivityConfiguration was flawed.

  • The configuration was being initialized via a public val:

    open val configuration: StreamCallActivityConfiguration
  • This was accessed during the activity's construction phase, before onCreate was invoked.

  • As a result, intent was null when the configuration was read, leading to incorrect or default behavior.


✅ Solution

To resolve this issue:

  • Configuration is now initialized explicitly inside onCreate() by reading from the intent.
  • To support singleTop or singleTask launch modes, configuration is also re-initialized in onNewIntent().

⚠️ Deprecated API

@Deprecated(
    message = "Accessing configuration before onCreate may lead to unintended behavior. Use config instead.",
    replaceWith = ReplaceWith("getConfiguration()")
)
open val configuration: StreamCallActivityConfiguration

 @Deprecated("Use configurationMap instead",
        replaceWith = ReplaceWith("configurationMap"),
        level = DeprecationLevel.WARNING)
    protected lateinit var config: StreamCallActivityConfiguration
        private set
        

🆕 Updated API

Use this variable instead:

/**
     * Map of a call id with StreamCallActivityConfiguration
     * You can get Call id via
     * `intent?.streamCallId(NotificationHandler.INTENT_EXTRA_CALL_CID)?.id`
     */
    private val configurationMap: HashMap<String, StreamCallActivityConfiguration> =
        HashMap()
  • Must be called after onCreate() or after calling initializeConfig(intent).

Additionally:

protected fun initializeConfig(intent: Intent?) {
    config = loadConfigFromIntent(intent)
    
    val streamCallId = intent?.streamCallId(NotificationHandler.INTENT_EXTRA_CALL_CID)
        streamCallId?.let {
            configurationMap[it.id] = config
        }
}

And

protected fun loadConfigFromIntent(intent: Intent?): StreamCallActivityConfiguration

🔁 Migration Notes

  • Replace direct access to configuration & config with configurationMap implementations.
  • If overriding behavior, call initializeConfig(intent) in your onCreate() and onNewIntent() methods.

2. Avoid double finish of StreamCallActivity

We introduce a new API named safeFinish which should be invoked when we want to finish the StreamCallActivity activity

Class Name Method Name Method Description
StreamCallActivity safeFinish Avoids double finish() invocation

3. Improve reject api call lifecycle scope

The reject API, when invoked from StreamCallActivity, operates in a broader scope than the activity itself. This is because there are scenarios where the activity is finished regardless of whether the network call succeeds or fails.

4. Allow the client to handle when there is an incoming call while the user is already in a call

We introduce an interface named IncomingCallHandlerDelegate which will allow the integrators to handle this case

/**
 * Interface to allow customization of call handling behavior
 */
public interface IncomingCallHandlerDelegate {
    /**
     * Called when a new call comes in while there's an ongoing call
     * @param activeCall The current ongoing call
     * @param intent New [Intent] which has new call information
     * @return true to accept the new call, false to ignore it
     */
    public fun shouldAcceptNewCall(activeCall: Call, intent: Intent): Boolean

    /**
     * Called when accepting a new call
     * @param intent The [Intent] for the call
     */
    public fun onAcceptCall(intent: Intent)

    /**
     * Called when ignoring a call (same call or based on delegate decision)
     * @param intent New [Intent] which has new call information
     * @param reason The reason for ignoring [IgnoreReason.SameCall], [IgnoreReason.DelegateDeclined],
     * [IgnoreReason.Custom]
     */
    public fun onIgnoreCall(intent: Intent, reason: IgnoreReason)
}

/**
 * Use [IgnoreReason] when we want to ignore the incoming call while we are
 * already on a active call
 */
public sealed class IgnoreReason {
    public data object SameCall : IgnoreReason()
    public data object DelegateDeclined : IgnoreReason()
    public data class Custom(val message: String) : IgnoreReason()
}

Integrators can implement their handling of IncomingCallHandlerDelegate via

    protected var callHandlerDelegate: IncomingCallHandlerDelegate? = null

The SDK has default implementation of it in which, we are rejecting the current call first then accepting the incoming call within the same activity

Its implementation

private val defaultCallHandler = object : IncomingCallHandlerDelegate {
        override fun shouldAcceptNewCall(activeCall: Call, intent: Intent) = true

        override fun onAcceptCall(intent: Intent) {
            initializeCallOrFail(
                ....
            )
        }

        override fun onIgnoreCall(intent: Intent, reason: IgnoreReason) {
           ....
            }
        }
    }

We improved the error handling in StreamCallActivity as this activity can have more than 1 call (1 active call + 1 incoming call). So the Exception in onErrorFinish will be StreamCallActivityException from this version in a Non-breaking way)

    /**
     * The Exception is `StreamCallActivityException`. We will update the args in next major release
     */
    protected val onErrorFinish: suspend (Exception) -> Unit = { error ->
        logger.e(error) { "Something went wrong" }
        onFailed(error)

        if (error is StreamCallActivityException) {
            if (isCurrentAcceptedCall(error.call)) {
                val configuration = configurationMap[error.call.id]
                if (configuration?.closeScreenOnError == true) {
                    logger.e(error) { "Finishing the activity" }
                    safeFinish()
                }
            }
        } else {
            // older version
            if (config.closeScreenOnError) {
                logger.e(error) { "Finishing the activity" }
                safeFinish()
            }
        }
    }

🎨 UI Changes

Add relevant screenshots

Before After
img img

Add relevant videos

Before After

@rahul-lohra rahul-lohra self-assigned this Jul 22, 2025
@github-actions

github-actions Bot commented Jul 22, 2025

Copy link
Copy Markdown
Contributor

SDK Size Comparison 📏

SDK Before After Difference Status
stream-video-android-core 11.41 MB 11.41 MB 0.00 MB 🟢
stream-video-android-ui-xml 5.70 MB 5.70 MB 0.00 MB 🟢
stream-video-android-ui-compose 5.86 MB 5.88 MB 0.02 MB 🟢

@rahul-lohra
rahul-lohra marked this pull request as ready for review July 23, 2025 08:15
@rahul-lohra
rahul-lohra requested a review from a team as a code owner July 23, 2025 08:15
@rahul-lohra rahul-lohra added pr:new-feature Adds new functionality pr:bug Fixes a bug labels Jul 25, 2025
@rahul-lohra rahul-lohra added pr:improvement Enhances an existing feature or code and removed pr:new-feature Adds new functionality pr:bug Fixes a bug labels Jul 25, 2025
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

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

See analysis details on SonarQube Cloud

@aleksandar-apostolov
aleksandar-apostolov merged commit 4aa6d71 into develop Jul 28, 2025
10 of 11 checks passed
@aleksandar-apostolov
aleksandar-apostolov deleted the feature/rahullohra/group-calls branch July 28, 2025 09:26
@aleksandar-apostolov aleksandar-apostolov changed the title Improve Call Screen Behavior on Decline Improve the behaviour of StreamCallActivity when the call has been declined Jul 28, 2025
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.

2 participants