Skip to content

[WIP] Improve Foreground Service orchestration when transitioning fromn outgoing call to active call#1624

Draft
rahul-lohra wants to merge 8 commits intodevelopfrom
bugfix/rahullohra/transition-to-active-call-service-exception
Draft

[WIP] Improve Foreground Service orchestration when transitioning fromn outgoing call to active call#1624
rahul-lohra wants to merge 8 commits intodevelopfrom
bugfix/rahullohra/transition-to-active-call-service-exception

Conversation

@rahul-lohra
Copy link
Contributor

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

This is WIP, please do not review

Goal

Previously, during the transition from an outgoing call to an active call, the Foreground Service (FGS) was started twice.

When initiating an outgoing call, we started the FGS with a minimal foreground service permission type. Upon transitioning to an active call, we then restarted the FGS with the full foreground service permission types (camera/microphone).

However, the transition from an outgoing call to an active call can occur while the app is in the background, which caused an exception because restarting a Foreground Service is not allowed when the app is in the background.

Solution -
To resolve this, we now start the Foreground Service only once, at the time of initiating the outgoing call, after all required permissions are granted.

By starting the FGS upfront with all necessary foreground service permission types, we eliminate the need to restart the service during the state transition, thereby avoiding the background execution exception.

image

Implementation

Key changes

  1. Since we want to create an outgoing-call only when we have all the required permission so we introduced a permission checker in code-flow of making an outgoing-call in StreamCallActivity
class StreamCallActivity {

@StreamCallActivityDelicateApi
    override fun create( call, ring, members, onSuccess, onError) {
       //1. Prepare call-capabilities ONLY for outgoing call
       //2. If has call related permission
       //3. If yes, create call
       //4. Do nothing
    }
  1. Our Permission rely on Call capabilities which is fetched from coordinator endpoing which is call.get()
  • So we created a local capabilities only for outgoing-call
  1. To create outgoing call capabilites and use it in StreamCallActivity - we need a way to know if we are making a video outgoing call or audio outgoing call
  • So we must inform StreamCallActivity explicitly about this

So we created a param to pass through intent

public fun <T : StreamCallActivity> callIntent(
             ...
            outgoingCallType: OutgoingCallType = OutgoingCallType.Unknown, // new type
        ) {
        ...
        putExtra(EXTRA_OUTGOING_CALL_TYPE, outgoingCallType.name)
        ...
        }
        
private const val EXTRA_OUTGOING_CALL_TYPE: String = "io.getstream.video.extra_outgoing_call_type"

public sealed class OutgoingCallType private constructor(public val name: String) {
            public companion object {
                public fun toOutgoingCallType(name: String): OutgoingCallType {
                    return when (name) {
                        Video.name -> Video
                        Audio.name -> Audio
                        else -> Unknown
                    }
                }
            }
            override fun toString(): String {
                return name
            }
            public object Video : OutgoingCallType("video")
            public object Audio : OutgoingCallType("audio")
            public object Unknown : OutgoingCallType("unknown")
        }

🎨 UI Changes

Add relevant screenshots

Before After
img img

Add relevant videos

Testing

Explain how this change can be tested (or why it can't be tested)

Provide a patch below if it is necessary for testing

Provide the patch summary here

Summary by CodeRabbit

  • New Features

    • Added support for audio and video outgoing call type differentiation
    • Enhanced permission management for outgoing calls based on call type
  • Refactor

    • Improved internal trigger handling system with type-safe enums replacing string-based constants

Introduces `OutgoingCallType` (Video, Audio, Unknown) to `StreamCallActivity.callIntent`. This allows specifying whether an outgoing call should start with audio-only or with video enabled.

The permission check for starting a call is now aware of the outgoing call type to request the correct permissions (camera and/or microphone) before the call is created.
@rahul-lohra rahul-lohra self-assigned this Feb 24, 2026
@rahul-lohra rahul-lohra requested a review from a team as a code owner February 24, 2026 10:57
@rahul-lohra rahul-lohra added the pr:bug Fixes a bug label Feb 24, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Feb 24, 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-1079] Improve Foreground Service orchesttation when transitioning fromn outgoing call to active call [WIP] mprove Foreground Service orchesttation when transitioning fromn outgoing call to active call Feb 24, 2026
@rahul-lohra rahul-lohra changed the title [WIP] mprove Foreground Service orchesttation when transitioning fromn outgoing call to active call [WIP] Improve Foreground Service orchesttation when transitioning fromn outgoing call to active call Feb 24, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

Walkthrough

The PR introduces a typed Trigger enum in CallService replacing string-based trigger constants, adds OutgoingCallType parameter propagation through navigation and activity configuration for distinguishing audio/video outgoing calls, and enhances permission checks to validate required capabilities based on call type.

Changes

Cohort / File(s) Summary
Outgoing Call Type Navigation & Configuration
demo-app/src/main/kotlin/io/getstream/video/android/ui/DogfoodingNavHost.kt, demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt
Added OutgoingCallType parameter to DirectCallJoinScreen navigation callback and propagated through to StreamCallActivityConfiguration. Updated call sites to pass Audio/Video type variants based on call path.
Trigger Type System Introduction
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt
Introduced new sealed class Trigger with variants (IncomingCall, OutgoingCall, OnGoingCall, RemoveIncomingCall, ShareScreen, None), replacing previous string constants. Added toTrigger() mapping utility and TRIGGER_KEY constant. Updated method signatures and call-handling flows to use typed Trigger instead of strings.
Trigger Type Conversion & Propagation
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt, stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt
Updated maybeStartForegroundService() to accept Trigger type instead of string. Added serviceTrigger state flow and updateServiceTriggers() method in CallState. Modified all call sites to use Trigger.OnGoingCall and Trigger.OutgoingCall instead of string constants.
Service Intent & Notification Building
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceIntentBuilder.kt, stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceNotificationRetriever.kt
Updated intent building to use Trigger.TRIGGER_KEY and trigger name. Modified notification retrieval methods to accept Trigger type with enum-based branch matching instead of string comparisons.
Service Parameter & Intent Models
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceParam.kt, stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/models/CallIntentParams.kt
Changed StartServiceParam.trigger and CallIntentParams.trigger field types from String to CallService.Companion.Trigger.
Service Launcher & Observers
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceLauncher.kt, stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/observers/CallServiceNotificationUpdateObserver.kt
Updated method signatures for showOnGoingCall(), showOutgoingCall(), and onStartService() callback to use Trigger type. Updated all trigger constant references to use Trigger.* variants.
Additional Trigger Type Updates
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/IncomingCallPresenter.kt, stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/screenshare/StreamScreenShareService.kt, stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/AndroidUtils.kt, stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManager.kt
Updated startForegroundWithServiceType() and related methods to accept Trigger type instead of string. Replaced TRIGGER_SHARE_SCREEN constant with Trigger.ShareScreen. Updated all trigger-based logic to use enum values.
OutgoingCallType Public API & Permission Management
stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt, stream-video-android-ui-core/api/stream-video-android-ui-core.api, stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/permission/PermissionManager.kt
Added public sealed class OutgoingCallType with variants (Audio, Video, Unknown) including toOutgoingCallType() mapper. Extended callIntent() signature with outgoingCallType parameter. Added EXTRA_OUTGOING_CALL_TYPE intent extra. Enhanced hasRequiredCallPermissions() to accept outgoingCallCapabilities parameter for capability-based permission validation in outgoing call scenarios.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant UI as DirectCallJoinScreen
    participant Nav as DogfoodingNavHost
    participant Activity as StreamCallActivity
    participant PermMgr as PermissionManager
    participant CallSvc as CallService
    
    User->>UI: Click Audio/Video Call
    activate UI
    UI->>UI: Determine OutgoingCallType
    UI->>Nav: navigateToDirectCall(cid, members, joinAndRing, OutgoingCallType)
    deactivate UI
    activate Nav
    Nav->>Activity: callIntent(..., outgoingCallType)
    deactivate Nav
    
    Activity->>Activity: Extract OutgoingCallType from intent
    activate Activity
    Activity->>Activity: Compute outgoing capabilities (Audio/Video)
    Activity->>PermMgr: hasRequiredCallPermissions(call, capabilities)
    activate PermMgr
    PermMgr->>PermMgr: Check RECORD_AUDIO & CAMERA perms
    PermMgr-->>Activity: Boolean result
    deactivate PermMgr
    
    alt Permissions Granted
        Activity->>CallSvc: Create call with Trigger.OutgoingCall
        activate CallSvc
        CallSvc->>CallSvc: Show outgoing notification
        CallSvc->>CallSvc: Start foreground service
        CallSvc-->>Activity: Call created
        deactivate CallSvc
    else Permissions Denied
        Activity->>Activity: No-op (TODO)
    end
    deactivate Activity
Loading
sequenceDiagram
    participant Service as CallService
    participant Intent as Intent Extra
    participant Trigger as Trigger Type
    participant Notification as NotificationManager
    
    Service->>Intent: Read EXTRA_OUTGOING_CALL_TYPE
    activate Service
    Intent-->>Service: String value (e.g., "video")
    Service->>Trigger: toTrigger(stringValue)
    activate Trigger
    Trigger-->>Service: Trigger.OutgoingCall
    deactivate Trigger
    Service->>Notification: handleNotification(Trigger.OutgoingCall)
    activate Notification
    Notification->>Notification: Match on Trigger type
    Notification->>Notification: Show appropriate notification
    deactivate Notification
    deactivate Service
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A trigger once lived as a string, unclear,
But now it's a type, so perfectly clear!
With OutgoingCallType leading the way,
Audio or video, we know what to play!
Permissions in hand, the flows now unite,
Type safety brings calls to their height! 🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.98% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The pull request description is incomplete and labeled 'WIP, please do not review.' While it explains the goal and provides implementation details, it lacks required sections and proper structure. Complete all required sections: add UI changes (screenshots/videos), provide comprehensive testing instructions, verify all contributor checklist items, and remove the WIP status before final review.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the primary objective: improving foreground service orchestration during the transition from outgoing to active calls, which is the core focus of this PR.

✏️ 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/transition-to-active-call-service-exception

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/permission/PermissionManager.kt (2)

1-33: ⚠️ Potential issue | 🟡 Minor

Duplicate license header block.

Lines 1–15 and 19–33 contain two separate license headers. The second block (with the 2014-2022 date) should be removed.

Proposed fix
 package io.getstream.video.android.ui.common.permission
 
-/*
- * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved.
- *
- * Licensed under the Stream License;
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    https://github.com/GetStream/stream-video-android/blob/main/LICENSE
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
 import android.Manifest
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/permission/PermissionManager.kt`
around lines 1 - 33, Remove the duplicated license header in
PermissionManager.kt by deleting the second block (the 2014-2022 license) that
appears after the first header (2014-2026); keep only the topmost license
comment and ensure no other code/comments are altered so the remaining file
header and package declaration remain intact.

101-127: ⚠️ Potential issue | 🔴 Critical

The outgoingCallCapabilities parameter is ineffective—ringingState is not Outgoing when hasRequiredCallPermissions is invoked.

In StreamCallActivity.create(), this function is called before call.create(...), but RingingState.Outgoing is only set inside Call.create() after the server responds. The check at line 114 will never be true, and outgoingCallCapabilities will always be empty, silently falling through to the call.get() path.

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

In
`@stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/permission/PermissionManager.kt`
around lines 101 - 127, hasRequiredCallPermissions currently ignores the
outgoingCallCapabilities because RingingState.Outgoing isn't set yet; fix it in
hasRequiredCallPermissions by using the outgoingCallCapabilities parameter as
the first fallback when ownCapabilities is empty (i.e., if
call.state.ownCapabilities.value is empty and
outgoingCallCapabilities.isNotEmpty() then set capabilities =
outgoingCallCapabilities) before calling call.get() or checking ringingState;
update references in hasRequiredCallPermissions and keep the existing call.get()
failure behavior.
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceLauncher.kt (1)

1-33: ⚠️ Potential issue | 🟡 Minor

Duplicate license header block.

Same as in PermissionManager.kt — lines 1–15 and 19–33 contain duplicate license headers (with different years: 2026 vs 2024). Remove the second block.

🤖 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/internal/service/ServiceLauncher.kt`
around lines 1 - 33, Remove the duplicated license header comment block that
appears above the package declaration in ServiceLauncher.kt (the second
redundant /* ... */ block with the 2014-2024 header) so only a single, correct
license header remains; locate the duplicate by finding the package line
"package io.getstream.video.android.core.notifications.internal.service" and
delete the extra comment block immediately preceding it.
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/AndroidUtils.kt (1)

183-186: ⚠️ Potential issue | 🟡 Minor

Stale KDoc – update @param trigger description.

The parameter description still references the removed TRIGGER_* string constants. It should reference CallService.Companion.Trigger enum values instead.

📝 Proposed fix
- * `@param` trigger The trigger that started the service: [TRIGGER_ONGOING_CALL], [TRIGGER_OUTGOING_CALL], [TRIGGER_INCOMING_CALL], [TRIGGER_SHARE_SCREEN]
+ * `@param` trigger The trigger that started the service: [CallService.Companion.Trigger.OnGoingCall], [CallService.Companion.Trigger.OutgoingCall], [CallService.Companion.Trigger.IncomingCall], [CallService.Companion.Trigger.ShareScreen]
🤖 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/utils/AndroidUtils.kt`
around lines 183 - 186, Update the stale KDoc for the parameter named "trigger"
to reference the enum values on CallService.Companion.Trigger instead of the
removed TRIGGER_* string constants; locate the KDoc for the function that
documents parameters notificationId, notification and trigger in AndroidUtils.kt
and replace the `@param` trigger description with a brief mention of the
CallService.Companion.Trigger enum (e.g., list or refer to its enum members) so
the documentation points to the correct type and values.
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceNotificationRetriever.kt (1)

40-42: ⚠️ Potential issue | 🟡 Minor

Stale KDoc – update @param trigger description.

The example references CallService.TRIGGER_INCOMING_CALL etc., which are removed string constants. Update to reference the CallService.Companion.Trigger enum values.

📝 Proposed fix
- *                eg. [CallService.TRIGGER_INCOMING_CALL], [CallService.TRIGGER_ONGOING_CALL], [CallService.TRIGGER_OUTGOING_CALL]
+ *                e.g. [CallService.Companion.Trigger.IncomingCall], [CallService.Companion.Trigger.OnGoingCall], [CallService.Companion.Trigger.OutgoingCall]
🤖 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/internal/service/ServiceNotificationRetriever.kt`
around lines 40 - 42, Update the stale KDoc for the parameter 'trigger' in
ServiceNotificationRetriever (in ServiceNotificationRetriever.kt) so it
references the CallService.Companion.Trigger enum values instead of the removed
string constants; specifically change the example and description to mention
CallService.Companion.Trigger.INCOMING_CALL, .ONGOING_CALL, .OUTGOING_CALL (or
the actual enum member names) and ensure the text explains that 'trigger' is a
stringified/enum-backed trigger coming from CallService.Companion.Trigger.
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt (1)

581-610: ⚠️ Potential issue | 🟡 Minor

Remove or scope the no-op serviceTrigger collector.

The new collectLatest block has an empty body and is launched in call.scope, which can outlive the service. Consider removing it until there’s real handling, or scope it to serviceScope to keep teardown deterministic.

Suggested cleanup (remove until implemented)
-        call.scope.launch {
-            call.state.serviceTrigger.collectLatest {
-                /**
-                 * TODO Rahul, what to write
-                 * Notifications are already updated via notification update-triggers
-                 */
-            }
-        }
As per coding guidelines: Ensure cleanup/teardown paths handle cancellation and failure (important for sockets, queues, retries).
🤖 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/internal/service/CallService.kt`
around lines 581 - 610, The empty collector launched with call.scope.launch over
call.state.serviceTrigger is a no-op that may outlive the service; either remove
this collectLatest block until real handling is implemented, or change its
coroutine scope to the service-bound serviceScope so it is cancelled with the
service; locate the collectLatest call in CallService.kt (the call.scope.launch
{ call.state.serviceTrigger.collectLatest { ... } } block) and either delete it
or replace call.scope with serviceScope and ensure proper cancellation/teardown
consistent with other observers such as CallServiceNotificationUpdateObserver
and the enableCallNotificationUpdates branch.
🧹 Nitpick comments (6)
demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt (2)

142-147: Misleading parameter name isVideo for type OutgoingCallType.

The parameter name isVideo implies a Boolean, but the type is OutgoingCallType. This is inconsistent with the same callback in DirectCallJoinScreen (line 75) where it's correctly named outgoingCallType.

Proposed fix
     onStartCallClick: (
         cid: StreamCallId,
         membersList: String,
         joinAndRing: Boolean,
-        isVideo: StreamCallActivity.Companion.OutgoingCallType,
+        outgoingCallType: StreamCallActivity.Companion.OutgoingCallType,
     ) -> Unit,
🤖 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 142 - 147, Rename the misleading parameter name isVideo in the
onStartCallClick callback to match the correct descriptive name outgoingCallType
(type StreamCallActivity.Companion.OutgoingCallType) so the signature aligns
with the other DirectCallJoinScreen declaration; update any call sites using
onStartCallClick to pass and destructure/outgoingCallType accordingly to avoid
treating a non-Boolean as a Boolean.

206-213: Remove commented-out code.

Line 207 has a commented-out StreamCallId("default", ...) line that looks like a debugging artifact.

Proposed fix
                         onStartCallClick(
                             StreamCallId("audio_call", UUID.randomUUID().toString()),
-//                                StreamCallId("default", UUID.randomUUID().toString()),
                             users
🤖 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 206 - 213, Remove the leftover commented-out debug line containing
StreamCallId("default", ...) in DirectCallJoinScreen.kt so only the intended
StreamCallId("audio_call", UUID.randomUUID().toString()) is used; locate the
StreamCallId construction in the outgoing call invocation inside the
DirectCallJoinScreen component (around the block building the call parameters
and passing callerJoinsFirst and OutgoingCallType.Audio) and delete the
commented line to clean up the code.
stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt (2)

149-165: Consider using a Kotlin enum class instead of a sealed class for OutgoingCallType.

OutgoingCallType has a fixed set of singleton variants with a name string, a toString(), and a companion mapper — this is exactly what enum class provides out of the box (with valueOf() and entries). The sealed class approach is more boilerplate for no additional flexibility.

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

In
`@stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt`
around lines 149 - 165, Replace the sealed class OutgoingCallType with a Kotlin
enum class to reduce boilerplate: define enum class OutgoingCallType(val name:
String) with entries Video, Audio, Unknown and remove the manual toString();
replace the companion's toOutgoingCallType(name: String) with a lookup using
values().firstOrNull { it.name == name } ?: OutgoingCallType.Unknown (or use
valueOf with safe handling) and update any usages of
OutgoingCallType.Video/Audio/Unknown and the toOutgoingCallType call sites
accordingly.

864-890: create() reads outgoingCallType from the activity intent — implicit coupling.

The public create() method (inherited from ActivityCallOperations) reads EXTRA_OUTGOING_CALL_TYPE directly from this.intent. This makes create() silently depend on the activity's intent state, which is fragile — subclasses or different call flows might not set this extra. Consider passing outgoingCallType as an explicit parameter or extracting it once in onIntentAction and passing it down.

Additionally, when outgoingCallType is Unknown, the else -> {} at line 888 silently skips capability setup without logging, so no capabilities are added and the call falls through to call.get().

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

In
`@stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt`
around lines 864 - 890, The create() method in StreamCallActivity currently
reads EXTRA_OUTGOING_CALL_TYPE from this.intent (implicit coupling); instead,
extract and normalize the outgoingCallType once (e.g., in onIntentAction) and
pass it explicitly into create() (or store it in a clearly-named property set by
onIntentAction) so create() no longer depends on intent state; also handle the
OutgoingCallType.Unknown branch in the capability-building logic
(outgoingCallCapabilities / OwnCapability) by adding a default capability set or
at minimum logging a warning before falling through so missing extras are
visible.
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/AndroidUtils.kt (1)

204-209: Redundant when branches – simplify to a single condition.

OnGoingCall, OutgoingCall, IncomingCall, and else all evaluate to foregroundServiceType; only ShareScreen differs. The three explicit arms add no value and will silently drift if new Trigger values are introduced.

♻️ Proposed simplification
-            when (trigger) {
-                CallService.Companion.Trigger.OnGoingCall -> foregroundServiceType
-                CallService.Companion.Trigger.OutgoingCall, CallService.Companion.Trigger.IncomingCall -> foregroundServiceType
-                CallService.Companion.Trigger.ShareScreen -> ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
-                else -> foregroundServiceType
-            },
+            when (trigger) {
+                CallService.Companion.Trigger.ShareScreen -> ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
+                else -> foregroundServiceType
+            },
🤖 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/utils/AndroidUtils.kt`
around lines 204 - 209, The when-expression in AndroidUtils.kt that checks
CallService.Companion.Trigger (OnGoingCall, OutgoingCall, IncomingCall, else) is
redundant because all cases except ShareScreen return foregroundServiceType;
simplify it to a single conditional that returns
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION only when trigger ==
CallService.Companion.Trigger.ShareScreen and returns foregroundServiceType for
all other Trigger values (use either an if/else or a two-branch when with
ShareScreen and an else) to avoid duplicated branches and accidental drift.
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt (1)

235-266: Consider delaying updateCallState until after permission checks.

Right now the trigger is emitted even when verifyPermissions fails and the service stops immediately. Moving the update below the permission gate keeps serviceTrigger consistent with actual service start.

Suggested adjustment
-        updateCallState(params)
         maybeHandleMediaIntent(intent, params.callId)
         val notificationId = serviceNotificationRetriever.getNotificationId(
             params.trigger,
             params.streamVideo,
             params.callId,
         )
@@
         if (params.trigger != Trigger.IncomingCall) {
             if (!verifyPermissions(
                     params.streamVideo,
                     call,
                     params.callId,
                     params.trigger,
                 )
             ) {
                 stopServiceGracefully()
                 return START_NOT_STICKY
             }
         }
+
+        updateCallState(params)
 
         val (notification, _) = getNotificationPair(
             params.trigger,
             params.streamVideo,
             params.callId,
🤖 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/internal/service/CallService.kt`
around lines 235 - 266, Move the call state update so the serviceTrigger isn't
emitted when permissions fail: only call updateCallState(params) after the
permission gate succeeds (i.e., after verifyPermissions(...) returns true) or
immediately when params.trigger == Trigger.IncomingCall (since incoming calls
don't need permissions); keep maybeHandleMediaIntent(params, params.callId) and
notificationId retrieval as-is but ensure updateCallState is invoked after the
verifyPermissions check (and before promoteToFgServiceIfNoActiveCall where the
service is promoted) and remove the original early updateCallState(params) call.
🤖 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 727-730: Add an explicit StateFlow type annotation to the exposed
property `serviceTrigger`: change the declaration of `serviceTrigger` so it is
typed as StateFlow<CallService.Companion.Trigger> that wraps the backing
MutableStateFlow `_serviceTrigger` (i.e., make `serviceTrigger` a StateFlow of
CallService.Companion.Trigger and assign `_serviceTrigger.asStateFlow()` to it);
this keeps the API consistent with other exposed StateFlow properties like
`_pinnedParticipants`.

In
`@stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt`:
- Around line 858-861: Remove the developer-facing TODO block in
StreamCallActivity (the comment referencing call-related permission checks
before starting the call service) — either implement the permission check in the
relevant method (e.g., where startCallService or similar call-start logic exists
in class StreamCallActivity) or delete/replace the note with a tracked task
reference (issue/PR ID) and a clear comment stating the resolution; do not leave
informal discussion-style TODOs in the code.
- Around line 896-913: In StreamCallActivity, when hasCallPermission is false
the else branch is currently silent; instead call the provided onError callback
with a descriptive exception (e.g., SecurityException or IllegalStateException)
so callers (like onIntentAction) can handle the failure; update the else branch
that surrounds the call.create(...) / result.onOutcome(...) logic to invoke
onError(Throwable("Missing call permission: user must grant CALL/RECORD
permission")) (or similar), referencing the hasCallPermission check, the
call.create(...) flow and the existing onError parameter to locate where to add
this.

---

Outside diff comments:
In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt`:
- Around line 581-610: The empty collector launched with call.scope.launch over
call.state.serviceTrigger is a no-op that may outlive the service; either remove
this collectLatest block until real handling is implemented, or change its
coroutine scope to the service-bound serviceScope so it is cancelled with the
service; locate the collectLatest call in CallService.kt (the call.scope.launch
{ call.state.serviceTrigger.collectLatest { ... } } block) and either delete it
or replace call.scope with serviceScope and ensure proper cancellation/teardown
consistent with other observers such as CallServiceNotificationUpdateObserver
and the enableCallNotificationUpdates branch.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceLauncher.kt`:
- Around line 1-33: Remove the duplicated license header comment block that
appears above the package declaration in ServiceLauncher.kt (the second
redundant /* ... */ block with the 2014-2024 header) so only a single, correct
license header remains; locate the duplicate by finding the package line
"package io.getstream.video.android.core.notifications.internal.service" and
delete the extra comment block immediately preceding it.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceNotificationRetriever.kt`:
- Around line 40-42: Update the stale KDoc for the parameter 'trigger' in
ServiceNotificationRetriever (in ServiceNotificationRetriever.kt) so it
references the CallService.Companion.Trigger enum values instead of the removed
string constants; specifically change the example and description to mention
CallService.Companion.Trigger.INCOMING_CALL, .ONGOING_CALL, .OUTGOING_CALL (or
the actual enum member names) and ensure the text explains that 'trigger' is a
stringified/enum-backed trigger coming from CallService.Companion.Trigger.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/AndroidUtils.kt`:
- Around line 183-186: Update the stale KDoc for the parameter named "trigger"
to reference the enum values on CallService.Companion.Trigger instead of the
removed TRIGGER_* string constants; locate the KDoc for the function that
documents parameters notificationId, notification and trigger in AndroidUtils.kt
and replace the `@param` trigger description with a brief mention of the
CallService.Companion.Trigger enum (e.g., list or refer to its enum members) so
the documentation points to the correct type and values.

In
`@stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/permission/PermissionManager.kt`:
- Around line 1-33: Remove the duplicated license header in PermissionManager.kt
by deleting the second block (the 2014-2022 license) that appears after the
first header (2014-2026); keep only the topmost license comment and ensure no
other code/comments are altered so the remaining file header and package
declaration remain intact.
- Around line 101-127: hasRequiredCallPermissions currently ignores the
outgoingCallCapabilities because RingingState.Outgoing isn't set yet; fix it in
hasRequiredCallPermissions by using the outgoingCallCapabilities parameter as
the first fallback when ownCapabilities is empty (i.e., if
call.state.ownCapabilities.value is empty and
outgoingCallCapabilities.isNotEmpty() then set capabilities =
outgoingCallCapabilities) before calling call.get() or checking ringingState;
update references in hasRequiredCallPermissions and keep the existing call.get()
failure behavior.

---

Nitpick comments:
In
`@demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt`:
- Around line 142-147: Rename the misleading parameter name isVideo in the
onStartCallClick callback to match the correct descriptive name outgoingCallType
(type StreamCallActivity.Companion.OutgoingCallType) so the signature aligns
with the other DirectCallJoinScreen declaration; update any call sites using
onStartCallClick to pass and destructure/outgoingCallType accordingly to avoid
treating a non-Boolean as a Boolean.
- Around line 206-213: Remove the leftover commented-out debug line containing
StreamCallId("default", ...) in DirectCallJoinScreen.kt so only the intended
StreamCallId("audio_call", UUID.randomUUID().toString()) is used; locate the
StreamCallId construction in the outgoing call invocation inside the
DirectCallJoinScreen component (around the block building the call parameters
and passing callerJoinsFirst and OutgoingCallType.Audio) and delete the
commented line to clean up the code.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt`:
- Around line 235-266: Move the call state update so the serviceTrigger isn't
emitted when permissions fail: only call updateCallState(params) after the
permission gate succeeds (i.e., after verifyPermissions(...) returns true) or
immediately when params.trigger == Trigger.IncomingCall (since incoming calls
don't need permissions); keep maybeHandleMediaIntent(params, params.callId) and
notificationId retrieval as-is but ensure updateCallState is invoked after the
verifyPermissions check (and before promoteToFgServiceIfNoActiveCall where the
service is promoted) and remove the original early updateCallState(params) call.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/AndroidUtils.kt`:
- Around line 204-209: The when-expression in AndroidUtils.kt that checks
CallService.Companion.Trigger (OnGoingCall, OutgoingCall, IncomingCall, else) is
redundant because all cases except ShareScreen return foregroundServiceType;
simplify it to a single conditional that returns
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION only when trigger ==
CallService.Companion.Trigger.ShareScreen and returns foregroundServiceType for
all other Trigger values (use either an if/else or a two-branch when with
ShareScreen and an else) to avoid duplicated branches and accidental drift.

In
`@stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt`:
- Around line 149-165: Replace the sealed class OutgoingCallType with a Kotlin
enum class to reduce boilerplate: define enum class OutgoingCallType(val name:
String) with entries Video, Audio, Unknown and remove the manual toString();
replace the companion's toOutgoingCallType(name: String) with a lookup using
values().firstOrNull { it.name == name } ?: OutgoingCallType.Unknown (or use
valueOf with safe handling) and update any usages of
OutgoingCallType.Video/Audio/Unknown and the toOutgoingCallType call sites
accordingly.
- Around line 864-890: The create() method in StreamCallActivity currently reads
EXTRA_OUTGOING_CALL_TYPE from this.intent (implicit coupling); instead, extract
and normalize the outgoingCallType once (e.g., in onIntentAction) and pass it
explicitly into create() (or store it in a clearly-named property set by
onIntentAction) so create() no longer depends on intent state; also handle the
OutgoingCallType.Unknown branch in the capability-building logic
(outgoingCallCapabilities / OwnCapability) by adding a default capability set or
at minimum logging a warning before falling through so missing extras are
visible.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fffac9c and 81057bd.

📒 Files selected for processing (18)
  • demo-app/src/main/kotlin/io/getstream/video/android/ui/DogfoodingNavHost.kt
  • demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/IncomingCallPresenter.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceIntentBuilder.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceLauncher.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceNotificationRetriever.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceParam.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/models/CallIntentParams.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/observers/CallServiceNotificationUpdateObserver.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/ForegroundServicePermissionManager.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/screenshare/StreamScreenShareService.kt
  • stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/AndroidUtils.kt
  • stream-video-android-ui-core/api/stream-video-android-ui-core.api
  • stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt
  • stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/permission/PermissionManager.kt

Comment on lines 727 to 730
private val _serviceTrigger =
MutableStateFlow<CallService.Companion.Trigger>(CallService.Companion.Trigger.None)
internal val serviceTrigger = _serviceTrigger.asStateFlow()

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd -e kt -e java | xargs grep -l "class CallService" | head -5

Repository: GetStream/stream-video-android

Length of output: 730


🏁 Script executed:

grep -n "enum\|sealed\|object Trigger\|case.*None" stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt | head -50

Repository: GetStream/stream-video-android

Length of output: 146


🏁 Script executed:

sed -n '117,150p' stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt

Repository: GetStream/stream-video-android

Length of output: 1608


🏁 Script executed:

sed -n '727,732p' stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt

Repository: GetStream/stream-video-android

Length of output: 346


🏁 Script executed:

# Check if there are similar patterns in CallState.kt with explicit StateFlow type annotations
grep -n "internal val.*= .*asStateFlow()" stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt | head -10

Repository: GetStream/stream-video-android

Length of output: 141


🏁 Script executed:

# Check other internal StateFlow properties in CallState.kt to see the pattern
grep -n "internal val.*=" stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt | grep -i "stateflow\|flow" | head -15

Repository: GetStream/stream-video-android

Length of output: 836


🏁 Script executed:

sed -n '313,316p' stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt

Repository: GetStream/stream-video-android

Length of output: 314


Add explicit StateFlow type annotation to serviceTrigger for consistency.

Trigger.None is correctly defined in the enum. However, serviceTrigger should have an explicit type annotation like other exposed StateFlow properties in this file (see line 313: internal val _pinnedParticipants: StateFlow<...>). Update line 729 to:

internal val serviceTrigger: StateFlow<CallService.Companion.Trigger> = _serviceTrigger.asStateFlow()
🤖 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 727 - 730, Add an explicit StateFlow type annotation to the exposed
property `serviceTrigger`: change the declaration of `serviceTrigger` so it is
typed as StateFlow<CallService.Companion.Trigger> that wraps the backing
MutableStateFlow `_serviceTrigger` (i.e., make `serviceTrigger` a StateFlow of
CallService.Companion.Trigger and assign `_serviceTrigger.asStateFlow()` to it);
this keeps the API consistent with other exposed StateFlow properties like
`_pinnedParticipants`.

Comment on lines 858 to 861
/**
* Todo Rahul we need to check call-related permission here - as this is going to start the callservice
* Is this the right place
*/
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove TODO comments before merging.

Lines 858–861 contain a developer-facing TODO that reads like internal discussion notes. These should be resolved or removed before merge.

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

In
`@stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt`
around lines 858 - 861, Remove the developer-facing TODO block in
StreamCallActivity (the comment referencing call-related permission checks
before starting the call service) — either implement the permission check in the
relevant method (e.g., where startCallService or similar call-start logic exists
in class StreamCallActivity) or delete/replace the note with a tracked task
reference (issue/PR ID) and a clear comment stating the resolution; do not leave
informal discussion-style TODOs in the code.

Comment on lines 896 to 913
)
result.onOutcome(call, onSuccess, onError)
if (hasCallPermission) {
val instance = StreamVideo.instance()
val result = call.create(
// List of all users, containing the caller also
memberIds = members + instance.userId,
// If other users will get push notification.
ring = ring,
)
result.onOutcome(call, onSuccess, onError)
} else {
/**
* TODO Rahul, maybe just do nothing, at this moment we should render get permission from setting
* Once we have the permission from setting, we re-open the our active - restarts the processing of
* last intent.
*/
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Silent failure when permission check fails — onError is not invoked.

When hasCallPermission is false, the else branch (lines 906–912) does nothing. The onError callback passed to create() is never invoked, and the caller (e.g., onIntentAction) has no way to know the operation failed. This can leave the user staring at a loading screen indefinitely.

At minimum, invoke onError with a descriptive exception so the activity can react.

Proposed fix
             if (hasCallPermission) {
                 val instance = StreamVideo.instance()
                 val result = call.create(
                     // List of all users, containing the caller also
                     memberIds = members + instance.userId,
                     // If other users will get push notification.
                     ring = ring,
                 )
                 result.onOutcome(call, onSuccess, onError)
             } else {
-                /**
-                 * TODO Rahul, maybe just do nothing, at this moment we should render get permission from setting
-                 * Once we have the permission from setting, we re-open the our active - restarts the processing of
-                 * last intent.
-                 */
+                onError?.invoke(
+                    StreamCallActivityException(
+                        call,
+                        "Required call permissions (camera/microphone) are not granted.",
+                        null,
+                    ),
+                )
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt`
around lines 896 - 913, In StreamCallActivity, when hasCallPermission is false
the else branch is currently silent; instead call the provided onError callback
with a descriptive exception (e.g., SecurityException or IllegalStateException)
so callers (like onIntentAction) can handle the failure; update the else branch
that surrounds the call.create(...) / result.onOutcome(...) logic to invoke
onError(Throwable("Missing call permission: user must grant CALL/RECORD
permission")) (or similar), referencing the hasCallPermission check, the
call.create(...) flow and the existing onError parameter to locate where to add
this.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 24, 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.27 MB 6.27 MB 0.00 MB 🟢

@rahul-lohra rahul-lohra changed the title [WIP] Improve Foreground Service orchesttation when transitioning fromn outgoing call to active call [WIP] Improve Foreground Service orchestation when transitioning fromn outgoing call to active call Feb 25, 2026
@rahul-lohra rahul-lohra changed the title [WIP] Improve Foreground Service orchestation when transitioning fromn outgoing call to active call [WIP] Improve Foreground Service orchestration when transitioning fromn outgoing call to active call Feb 25, 2026
…ll-service-exception

# Conflicts:
#	stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt
…o-active-call-service-exception

# Conflicts:
#	stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

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

See analysis details on SonarQube Cloud

@rahul-lohra rahul-lohra marked this pull request as draft February 26, 2026 08:12
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.

1 participant