Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
f3990a7
Refactor call service into smaller classes
rahul-lohra Dec 17, 2025
7b0942c
chore: add logging to notification and lifecycle managers
rahul-lohra Dec 18, 2025
dc7f077
feat: Add logging to CallEventObserver
rahul-lohra Dec 18, 2025
55f48bb
feat: Update createdBy on call rejected event
rahul-lohra Dec 18, 2025
6f190b7
chore: add createdByUserId in LocalCallMissedEvent
rahul-lohra Dec 22, 2025
67ffeae
chore: fix missed imports
rahul-lohra Dec 22, 2025
d164b57
chore: fix service cancellation from LocalCallMissedEvent
rahul-lohra Dec 22, 2025
9ae4455
chore: refactor
rahul-lohra Dec 22, 2025
2992616
chore: fix notification id for missed-call
rahul-lohra Dec 22, 2025
5cb2299
chore: add logic for time-sensitive services
rahul-lohra Dec 22, 2025
594cc89
chore: temp
rahul-lohra Dec 22, 2025
215fc02
feat: Introduce local call events for better state handling
rahul-lohra Dec 23, 2025
0998fac
Merge branch 'develop' into bugfix/rahullohra/refactor-call-service
rahul-lohra Dec 23, 2025
58afb52
feat: Add reason when leaving call from service
rahul-lohra Dec 23, 2025
b5b95e1
Merge branch 'develop' into bugfix/rahullohra/refactor-call-service
aleksandar-apostolov Dec 24, 2025
b0702cf
refactor: Rename CallService components and add tests
rahul-lohra Dec 29, 2025
3527dd2
refactor: Rename local call events
rahul-lohra Dec 29, 2025
5a14940
feat: Add notificationId to updateNotification
rahul-lohra Dec 29, 2025
776b4b5
feat: Add `updateNotification` without notification ID
rahul-lohra Dec 29, 2025
697b82a
feat: Make `transitionToAcceptCall` internal
rahul-lohra Dec 29, 2025
8346b9e
fix: Remove stopService call from ServiceLauncher
rahul-lohra Dec 29, 2025
94138cc
chore: Add a TODO comment
rahul-lohra Dec 29, 2025
c60b687
chore: Add a TODO comment
rahul-lohra Dec 29, 2025
cd1cf38
Merge branch 'develop' into bugfix/rahullohra/refactor-call-service
rahul-lohra Dec 29, 2025
8452835
test: Use RobolectricTestRunner in ServiceStateTest
rahul-lohra Dec 29, 2025
ab9b2be
chore: Improve documentation and make local events internal
rahul-lohra Dec 30, 2025
ed98b04
feat: Add support for outgoing call notifications
rahul-lohra Dec 30, 2025
db54cee
Refactor: Improve notification and service handling for calls
rahul-lohra Dec 31, 2025
53896eb
fix: Remove leftover comments and add clarity
rahul-lohra Jan 2, 2026
bbdd712
chore: Remove temporary logging
rahul-lohra Jan 2, 2026
bbbbe3f
fix: Correctly report service running state
rahul-lohra Jan 2, 2026
5d38f6f
refactor: Improve service running check
rahul-lohra Jan 2, 2026
9c7d498
refactor: Extract Throttler from Debouncer
rahul-lohra Jan 2, 2026
309e3d6
feat: Throttle call service stop requests
rahul-lohra Jan 2, 2026
e5641b5
chore: Re-enable graceful service stop
rahul-lohra Jan 2, 2026
d96cdef
chore: Remove TODO comment
rahul-lohra Jan 2, 2026
85dd868
chore: Use notificationId from call state
rahul-lohra Jan 2, 2026
6b073d8
refactor: Use Call specific notification ID in tests
rahul-lohra Jan 2, 2026
4068127
fix: Cancel correct notification ID for calls
rahul-lohra Jan 2, 2026
332a399
chore: Add logging and fix notification ID usage
rahul-lohra Jan 2, 2026
b52fe23
Refactor: Improve call service logic and logging
rahul-lohra Jan 2, 2026
1800f99
refactor: Improve incoming call and service stop logic
rahul-lohra Jan 2, 2026
3436f6d
Merge branch 'develop' into bugfix/rahullohra/refactor-call-service
rahul-lohra Jan 7, 2026
2d18d42
chore: Improve foreground service stability and test coverage
rahul-lohra Jan 7, 2026
d1f3843
Fix: Make `CallService` safer against null `StreamVideo` instance
rahul-lohra Jan 7, 2026
b0c6405
Refactor: Move debugPrintLastStackFrames to AndroidUtils
rahul-lohra Jan 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,10 @@ object StreamVideoInitHelper {
val callServiceConfigRegistry = CallServiceConfigRegistry()
callServiceConfigRegistry.apply {
register(DefaultCallConfigurations.getLivestreamGuestCallServiceConfig())
register(CallType.AudioCall.name) { enableTelecom(true) }
register(
CallType.AudioCall.name,
DefaultCallConfigurations.audioCall.copy(enableTelecom = true),
)
register(CallType.AnyMarker.name) {
setModerationConfig(
ModerationConfig(
Expand Down
9 changes: 6 additions & 3 deletions stream-video-android-core/api/stream-video-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -3558,13 +3558,15 @@ public final class io/getstream/android/video/generated/models/ListTranscription
}

public final class io/getstream/android/video/generated/models/LocalCallMissedEvent : io/getstream/android/video/generated/models/LocalEvent {
public fun <init> (Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;)Lio/getstream/android/video/generated/models/LocalCallMissedEvent;
public static synthetic fun copy$default (Lio/getstream/android/video/generated/models/LocalCallMissedEvent;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/android/video/generated/models/LocalCallMissedEvent;
public final fun component2 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/android/video/generated/models/LocalCallMissedEvent;
public static synthetic fun copy$default (Lio/getstream/android/video/generated/models/LocalCallMissedEvent;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/android/video/generated/models/LocalCallMissedEvent;
public fun equals (Ljava/lang/Object;)Z
public fun getCallCID ()Ljava/lang/String;
public final fun getCallCid ()Ljava/lang/String;
public final fun getCreatedById ()Ljava/lang/String;
public fun getEventType ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
Expand Down Expand Up @@ -7778,6 +7780,7 @@ public final class io/getstream/video/android/core/CallState {
public final fun updateFromResponse (Lio/getstream/android/video/generated/models/StartHLSBroadcastingResponse;)V
public final fun updateFromResponse (Lio/getstream/android/video/generated/models/StopLiveResponse;)V
public final fun updateFromResponse (Lio/getstream/android/video/generated/models/UpdateCallResponse;)V
public final fun updateNotification (ILandroid/app/Notification;)V
public final fun updateNotification (Landroid/app/Notification;)V
public final fun updateParticipant (Lio/getstream/video/android/core/ParticipantState;)V
public final fun updateParticipantSortingOrder (Ljava/util/Comparator;)V
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2014-2024 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.
*/

@file:Suppress(
"ArrayInDataClass",
"EnumEntryName",
"RemoveRedundantQualifierName",
"UnusedImport"
)

package io.getstream.android.video.generated.models

import com.squareup.moshi.Json
import org.threeten.bp.OffsetDateTime

/**
* This event is sent after [CallAcceptedEvent] is consumed in [io.getstream.video.android.core.CallState]
*/

internal data class LocalCallAcceptedPostEvent (
@Json(name = "call_cid")
val callCid: String,

@Json(name = "created_at")
val createdAt: OffsetDateTime,

@Json(name = "call")
val call: CallResponse,

@Json(name = "user")
val user: UserResponse,

@Json(name = "type")
val type: String
)
: VideoEvent(), WSCallEvent
{

override fun getEventType(): String {
return type
}

override fun getCallCID(): String {
return callCid
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2014-2024 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.
*/

@file:Suppress(
"ArrayInDataClass",
"EnumEntryName",
"RemoveRedundantQualifierName",
"UnusedImport"
)

package io.getstream.android.video.generated.models

import com.squareup.moshi.Json
import org.threeten.bp.OffsetDateTime

/**
* This event is sent after [CallRejectedEvent] is consumed in [io.getstream.video.android.core.CallState]
*/

internal data class LocalCallRejectedPostEvent (
@Json(name = "call_cid")
val callCid: String,

@Json(name = "created_at")
val createdAt: OffsetDateTime,

@Json(name = "call")
val call: CallResponse,

@Json(name = "user")
val user: UserResponse,

@Json(name = "type")
val type: String,

@Json(name = "reason")
val reason: String? = null
)
: VideoEvent(), WSCallEvent
{

override fun getEventType(): String {
return type
}

override fun getCallCID(): String {
return callCid
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,12 @@ public class Call(
}

response.onSuccess {
/**
* Because [CallState.updateFromResponse] reads the value of [ClientState.ringingCall]
*/
if (ring) {
client.state._ringingCall.value = this
}
state.updateFromResponse(it)
if (ring) {
client.state.addRingingCall(this, RingingState.Outgoing())
Expand Down Expand Up @@ -929,7 +935,6 @@ public class Call(
}

private fun internalLeave(disconnectionReason: Throwable?, reason: String) = atomicLeave {
val callId = id
monitorSubscriberPCStateJob?.cancel()
monitorPublisherPCStateJob?.cancel()
monitorPublisherPCStateJob = null
Expand Down Expand Up @@ -1535,8 +1540,7 @@ public class Call(
logger.d { "[accept] #ringing; no args, call_id:$id" }
state.acceptedOnThisDevice = true

clientImpl.state.removeRingingCall(this)
clientImpl.state.maybeStopForegroundService(call = this)
clientImpl.state.transitionToAcceptCall(this)
return clientImpl.accept(type, id)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import android.app.Notification
import android.os.Bundle
import android.util.Log
import androidx.compose.runtime.Stable
import androidx.core.app.NotificationManagerCompat
import io.getstream.android.video.generated.models.BlockedUserEvent
import io.getstream.android.video.generated.models.CallAcceptedEvent
import io.getstream.android.video.generated.models.CallClosedCaption
Expand All @@ -33,6 +34,7 @@ import io.getstream.android.video.generated.models.CallMemberAddedEvent
import io.getstream.android.video.generated.models.CallMemberRemovedEvent
import io.getstream.android.video.generated.models.CallMemberUpdatedEvent
import io.getstream.android.video.generated.models.CallMemberUpdatedPermissionEvent
import io.getstream.android.video.generated.models.CallMissedEvent
import io.getstream.android.video.generated.models.CallModerationBlurEvent
import io.getstream.android.video.generated.models.CallParticipantResponse
import io.getstream.android.video.generated.models.CallReactionEvent
Expand Down Expand Up @@ -63,7 +65,9 @@ import io.getstream.android.video.generated.models.GetOrCreateCallResponse
import io.getstream.android.video.generated.models.GoLiveResponse
import io.getstream.android.video.generated.models.HealthCheckEvent
import io.getstream.android.video.generated.models.JoinCallResponse
import io.getstream.android.video.generated.models.LocalCallAcceptedPostEvent
import io.getstream.android.video.generated.models.LocalCallMissedEvent
import io.getstream.android.video.generated.models.LocalCallRejectedPostEvent
import io.getstream.android.video.generated.models.MemberResponse
import io.getstream.android.video.generated.models.MuteUsersResponse
import io.getstream.android.video.generated.models.OwnCapability
Expand Down Expand Up @@ -109,6 +113,7 @@ import io.getstream.video.android.core.model.ScreenSharingSession
import io.getstream.video.android.core.model.VisibilityOnScreenState
import io.getstream.video.android.core.moderations.ModerationManager
import io.getstream.video.android.core.notifications.IncomingNotificationData
import io.getstream.video.android.core.notifications.NotificationType
import io.getstream.video.android.core.notifications.internal.service.CallServiceConfig
import io.getstream.video.android.core.notifications.internal.telecom.jetpack.JetpackTelecomRepository
import io.getstream.video.android.core.permission.PermissionRequest
Expand All @@ -122,6 +127,7 @@ import io.getstream.video.android.core.utils.TaskSchedulerWithDebounce
import io.getstream.video.android.core.utils.mapState
import io.getstream.video.android.core.utils.safeCall
import io.getstream.video.android.core.utils.toUser
import io.getstream.video.android.model.StreamCallId
import io.getstream.video.android.model.User
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
Expand Down Expand Up @@ -699,6 +705,7 @@ public class CallState(
*/
internal val atomicNotification: AtomicReference<Notification?> =
AtomicReference<Notification?>(null)
internal var notificationId: Int? = null

@InternalStreamVideoApi
internal var jetpackTelecomRepository: JetpackTelecomRepository? = null
Expand All @@ -707,6 +714,7 @@ public class CallState(

fun handleEvent(event: VideoEvent) {
logger.d { "[handleEvent] ${event::class.java.name.split(".").last()}" }

when (event) {
is BlockedUserEvent -> {
val newBlockedUsers = _blockedUsers.value.toMutableSet()
Expand Down Expand Up @@ -747,9 +755,23 @@ public class CallState(
// Then leave the call on this device
if (!acceptedOnThisDevice) call.leave("accepted-on-another-device")
}
call.fireEvent(
LocalCallAcceptedPostEvent(
event.callCid,
event.createdAt,
event.call,
event.user,
event.type,
),
)
}

is CallMissedEvent -> {
_createdBy.value = event.call.createdBy.toUser()
}

is CallRejectedEvent -> {
_createdBy.value = event.call.createdBy.toUser()
val new = _rejectedBy.value.toMutableSet()
new.add(event.user.id)
_rejectedBy.value = new.toSet()
Expand All @@ -763,6 +785,16 @@ public class CallState(
}
},
)
call.fireEvent(
LocalCallRejectedPostEvent(
event.callCid,
event.createdAt,
event.call,
event.user,
event.type,
event.reason,
),
)
}

is LocalCallMissedEvent -> {
Expand All @@ -774,6 +806,17 @@ public class CallState(
_rejectedBy.value = newRejectedBySet.toSet()
_ringingState.value = RingingState.RejectedByAll
call.leave("LocalCallMissedEvent")

val activeCallExists = client.state.activeCall.value != null
if (activeCallExists) {
// Another call is active - just remove incoming notification
val streamCallId = StreamCallId(call.type, call.id)
NotificationManagerCompat.from(client.context)
.cancel(streamCallId.getNotificationId(NotificationType.Incoming))
} else {
// No other call - stop service
client.state.maybeStopForegroundService(call)
}
}
}

Expand Down Expand Up @@ -1645,9 +1688,15 @@ public class CallState(
_rejectActionBundle.value = bundle
}

@Deprecated("Use updateNotification(Int, Notification) instead")
fun updateNotification(notification: Notification) {
atomicNotification.set(notification)
}

fun updateNotification(notificationId: Int, notification: Notification) {
this.notificationId = notificationId
this.atomicNotification.set(notification)
}
}

private fun MemberResponse.toMemberState(): MemberState {
Expand Down
Loading
Loading