diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt b/demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt index 2052ce2061..00e4e8bebc 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/ui/outgoing/DirectCallJoinScreen.kt @@ -26,11 +26,13 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.Checkbox +import androidx.compose.material.CheckboxDefaults import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.RadioButton import androidx.compose.material.RadioButtonDefaults @@ -135,7 +137,7 @@ private fun Body( toggleUserSelection: (Int) -> Unit, onStartCallClick: (cid: StreamCallId, membersList: String, joinAndRing: Boolean) -> Unit, ) { - var callerJoinsFirst by rememberSaveable { mutableStateOf(false) } + var callerJoinsFirst by rememberSaveable { mutableStateOf(true) } Box( modifier = Modifier @@ -164,9 +166,18 @@ private fun Body( horizontalArrangement = Arrangement.SpaceBetween, ) { Text("Join First", color = Color.White) - Checkbox(callerJoinsFirst, onCheckedChange = { - callerJoinsFirst = !callerJoinsFirst - }) + Checkbox( + callerJoinsFirst, + modifier = Modifier.offset(x = 10.dp), + colors = CheckboxDefaults.colors( + uncheckedColor = Color.White, // Border color when unchecked + checkedColor = Color.White, // Fill color when checked + checkmarkColor = VideoTheme.colors.buttonBrandDefault, // Tick color + ), + onCheckedChange = { + callerJoinsFirst = !callerJoinsFirst + }, + ) } UserList( entries = users, diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt index 4b0ed353ae..d9094f50b8 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt @@ -574,7 +574,7 @@ public class Call( video: Boolean = isVideoEnabled(), ): Result { logger.d { "[joinAndRing] #ringing; #track; members: $members, video: $video" } - state.toggleRingingStateUpdates(true) + state.toggleJoinAndRingProgress(true) return join(ring = false, createOptions = createOptions).flatMap { rtcSession -> logger.d { "[joinAndRing] Joined #ringing; #track; ring: $members" } ring(RingCallRequest(isVideoEnabled(), members)).map { @@ -583,7 +583,7 @@ public class Call( rtcSession }.onError { logger.e { "[joinAndRing] Ring failed #ringing; #track; error: $it" } - state.toggleRingingStateUpdates(false) + state.toggleJoinAndRingProgress(false) leave("ring-failed (${it.message})") } } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt index d6ec114a75..2bc0c0e7cf 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt @@ -18,7 +18,6 @@ package io.getstream.video.android.core 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 @@ -160,6 +159,7 @@ import java.util.Locale import java.util.SortedMap import java.util.UUID import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference import kotlin.time.Duration import kotlin.time.DurationUnit @@ -724,6 +724,7 @@ public class CallState( internal var jetpackTelecomRepository: JetpackTelecomRepository? = null internal var incomingNotificationData = IncomingNotificationData(emptyMap()) + private val ringingLogger by taggedLogger("RingingState") fun handleEvent(event: VideoEvent) { logger.d { "[handleEvent] ${event::class.java.name.split(".").last()}" } @@ -1023,7 +1024,7 @@ public class CallState( is JoinCallResponseEvent -> { // time to update call state based on the join response updateFromJoinResponse(event) - if (!ringingStateUpdatesStopped) { + if (!isJoinAndRingInProgress.get()) { updateRingingState() } else { _ringingState.value = RingingState.Outgoing(acceptedByCallee = true) @@ -1256,8 +1257,9 @@ public class CallState( val userIsParticipant = _session.value?.participants?.find { it.user.id == client.userId } != null val outgoingMembersCount = _members.value.filter { it.value.user.id != client.userId }.size + val createdBySelf = createdBy?.id == client.userId - Log.d("RingingState", "Current: ${_ringingState.value}, call_id: ${call.cid}") + ringingLogger.d { "Current: ${_ringingState.value}, call_id: ${call.cid}" } val ringingStateLogs = arrayListOf( ("acceptedByMe: $isAcceptedByMe"), @@ -1268,13 +1270,13 @@ public class CallState( ("userIsParticipant: $userIsParticipant"), ).joinToString("") { it + "\n" } - Log.d( - "RingingState", - "call_id: ${call.cid}, Flags: $ringingStateLogs", - ) + ringingLogger.d { "call_id: ${call.cid}, Flags: $ringingStateLogs" } // no members - call is empty, we can join - val state: RingingState = if (hasActiveCall && !ringingStateUpdatesStopped) { + val state: RingingState = if (hasActiveCall && !isJoinAndRingInProgress.get()) { + /** + * Normal join, not joinAndRing + */ cancelTimeout() RingingState.Active } else if (isRejectedByMe) { @@ -1303,7 +1305,7 @@ public class CallState( } } else if (hasRingingCall && createdBy?.id == client.userId) { // The call is created by us - logger.d { "acceptedBy: $acceptedBy, userIsParticipant: $userIsParticipant" } + ringingLogger.d { "acceptedBy: $acceptedBy, userIsParticipant: $userIsParticipant" } if (acceptedBy.isEmpty()) { // no one accepted the call RingingState.Outgoing(acceptedByCallee = false) @@ -1311,8 +1313,11 @@ public class CallState( // someone already accepted the call, but it's not us (client needs to do call.join) RingingState.Outgoing(acceptedByCallee = true) } else { - // call is accepted and we are already in the call - ringingStateUpdatesStopped = false + /** + * Executed when the callee accepts a join-and-ring call. + * Call is accepted and we are already in the call + */ + isJoinAndRingInProgress.set(false) cancelTimeout() RingingState.Active } @@ -1325,7 +1330,7 @@ public class CallState( } if (_ringingState.value != state) { - logger.d { "Updating ringing state ${_ringingState.value} -> $state" } + ringingLogger.d { "Updating ringing state ${_ringingState.value} -> $state" } // handle the auto-cancel for outgoing ringing calls if (state is RingingState.Outgoing && !state.acceptedByCallee) { @@ -1338,7 +1343,7 @@ public class CallState( // stop the call ringing timer if it's running } - Log.d("RingingState", "Update: $state") + ringingLogger.d { "Update: $state" } _ringingState.value = state } @@ -1394,7 +1399,7 @@ public class CallState( // double check that we are still in Outgoing call state and call is not active if (_ringingState.value is RingingState.Outgoing || _ringingState.value is RingingState.Incoming && client.state.activeCall.value == null) { - ringingStateUpdatesStopped = false + isJoinAndRingInProgress.set(false) call.reject(reason = RejectReason.Custom(alias = REJECT_REASON_TIMEOUT)) call.leave("start-ringing-timeout") } @@ -1520,7 +1525,8 @@ public class CallState( _broadcasting.value = response.egress.broadcasting _session.value = response.session _rejectedBy.value = response.session?.rejectedBy?.keys?.toSet() ?: emptySet() - _acceptedBy.value = response.session?.acceptedBy?.keys?.toSet() ?: emptySet() + val serverAcceptedBy = response.session?.acceptedBy?.keys?.toSet() ?: emptySet() + _acceptedBy.value = acceptedBy.value + serverAcceptedBy _createdAt.value = response.createdAt _updatedAt.value = response.updatedAt _endedAt.value = response.endedAt @@ -1643,10 +1649,10 @@ public class CallState( _broadcasting.value = true } - private var ringingStateUpdatesStopped = false + internal var isJoinAndRingInProgress = AtomicBoolean(false) - internal fun toggleRingingStateUpdates(stopped: Boolean) { - ringingStateUpdatesStopped = stopped + internal fun toggleJoinAndRingProgress(stopped: Boolean) { + isJoinAndRingInProgress.set(stopped) } /** diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt index 7455e64e37..96dfa6ce8f 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt @@ -170,15 +170,17 @@ class ClientState(private val client: StreamVideo) { val ringingState = call.state.ringingState.value when (ringingState) { is RingingState.Incoming -> { + transitionToAcceptCall(call) call.scope.launch { - transitionToAcceptCall(call) delay(serviceTransitionDelayMs) maybeStartForegroundService(call, CallService.TRIGGER_ONGOING_CALL) } } is RingingState.Outgoing -> { - call.scope.launch { + if (!call.state.isJoinAndRingInProgress.get()) { transitionToAcceptCall(call) + } + call.scope.launch { delay(serviceTransitionDelayMs) maybeStartForegroundService(call, CallService.TRIGGER_ONGOING_CALL) }