package org.thoughtcrime.securesms.components.webrtc import android.content.Context import androidx.annotation.PluralsRes import androidx.annotation.StringRes import com.annimon.stream.OptionalLong import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.webrtc.WebRtcControls.FoldableState import org.thoughtcrime.securesms.events.CallParticipant import org.thoughtcrime.securesms.events.CallParticipant.Companion.createLocal import org.thoughtcrime.securesms.events.WebRtcViewModel import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.ringrtc.CameraState import org.thoughtcrime.securesms.service.webrtc.collections.ParticipantCollection import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState import java.util.concurrent.TimeUnit /** * Represents the state of all participants, remote and local, combined with view state * needed to properly render the participants. The view state primarily consists of * if we are in System PIP mode and if we should show our video for an outgoing call. */ data class CallParticipantsState( val callState: WebRtcViewModel.State = WebRtcViewModel.State.CALL_DISCONNECTED, val groupCallState: WebRtcViewModel.GroupCallState = WebRtcViewModel.GroupCallState.IDLE, private val remoteParticipants: ParticipantCollection = ParticipantCollection(SMALL_GROUP_MAX), val localParticipant: CallParticipant = createLocal(CameraState.UNKNOWN, BroadcastVideoSink(), false), val focusedParticipant: CallParticipant = CallParticipant.EMPTY, val localRenderState: WebRtcLocalRenderState = WebRtcLocalRenderState.GONE, val isInPipMode: Boolean = false, private val showVideoForOutgoing: Boolean = false, val isViewingFocusedParticipant: Boolean = false, val remoteDevicesCount: OptionalLong = OptionalLong.empty(), private val foldableState: FoldableState = FoldableState.flat(), val isInOutgoingRingingMode: Boolean = false, val ringGroup: Boolean = false, val ringerRecipient: Recipient = Recipient.UNKNOWN, val groupMembers: List = emptyList() ) { val allRemoteParticipants: List = remoteParticipants.allParticipants val isFolded: Boolean = foldableState.isFolded val isLargeVideoGroup: Boolean = allRemoteParticipants.size > SMALL_GROUP_MAX val isIncomingRing: Boolean = callState == WebRtcViewModel.State.CALL_INCOMING val gridParticipants: List get() { return remoteParticipants.gridParticipants } val listParticipants: List get() { val listParticipants: MutableList = mutableListOf() if (isViewingFocusedParticipant && allRemoteParticipants.size > 1) { listParticipants.addAll(allRemoteParticipants) listParticipants.remove(focusedParticipant) } else { listParticipants.addAll(remoteParticipants.listParticipants) } if (foldableState.isFlat) { listParticipants.add(CallParticipant.EMPTY) } listParticipants.reverse() return listParticipants } val participantCount: OptionalLong get() { val includeSelf = groupCallState == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED return remoteDevicesCount.map { l: Long -> l + if (includeSelf) 1L else 0L } .or { if (includeSelf) OptionalLong.of(1L) else OptionalLong.empty() } } fun getPreJoinGroupDescription(context: Context): String? { if (callState != WebRtcViewModel.State.CALL_PRE_JOIN || groupCallState.isIdle) { return null } return if (remoteParticipants.isEmpty) { describeGroupMembers( context = context, oneParticipant = if (ringGroup) R.string.WebRtcCallView__signal_will_ring_s else R.string.WebRtcCallView__s_will_be_notified, twoParticipants = if (ringGroup) R.string.WebRtcCallView__signal_will_ring_s_and_s else R.string.WebRtcCallView__s_and_s_will_be_notified, multipleParticipants = if (ringGroup) R.plurals.WebRtcCallView__signal_will_ring_s_s_and_d_others else R.plurals.WebRtcCallView__s_s_and_d_others_will_be_notified, members = groupMembers ) } else { when (remoteParticipants.size()) { 0 -> context.getString(R.string.WebRtcCallView__no_one_else_is_here) 1 -> context.getString(if (remoteParticipants[0].isSelf) R.string.WebRtcCallView__s_are_in_this_call else R.string.WebRtcCallView__s_is_in_this_call, remoteParticipants[0].getShortRecipientDisplayName(context)) 2 -> context.getString( R.string.WebRtcCallView__s_and_s_are_in_this_call, remoteParticipants[0].getShortRecipientDisplayName(context), remoteParticipants[1].getShortRecipientDisplayName(context) ) else -> { val others = remoteParticipants.size() - 2 context.resources.getQuantityString( R.plurals.WebRtcCallView__s_s_and_d_others_are_in_this_call, others, remoteParticipants[0].getShortRecipientDisplayName(context), remoteParticipants[1].getShortRecipientDisplayName(context), others ) } } } } fun getOutgoingRingingGroupDescription(context: Context): String? { if (callState == WebRtcViewModel.State.CALL_CONNECTED && groupCallState == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED && isInOutgoingRingingMode ) { return describeGroupMembers( context = context, oneParticipant = R.string.WebRtcCallView__ringing_s, twoParticipants = R.string.WebRtcCallView__ringing_s_and_s, multipleParticipants = R.plurals.WebRtcCallView__ringing_s_s_and_d_others, members = groupMembers ) } return null } fun getIncomingRingingGroupDescription(context: Context): String? { if (callState == WebRtcViewModel.State.CALL_INCOMING && groupCallState == WebRtcViewModel.GroupCallState.RINGING && ringerRecipient.hasServiceId() ) { val ringerName = ringerRecipient.getShortDisplayName(context) val membersWithoutYouOrRinger: List = groupMembers.filterNot { it.member.isSelf || ringerRecipient.requireServiceId() == it.member.serviceId.orElse(null) } return when (membersWithoutYouOrRinger.size) { 0 -> context.getString(R.string.WebRtcCallView__s_is_calling_you, ringerName) 1 -> context.getString( R.string.WebRtcCallView__s_is_calling_you_and_s, ringerName, membersWithoutYouOrRinger[0].member.getShortDisplayName(context) ) 2 -> context.getString( R.string.WebRtcCallView__s_is_calling_you_s_and_s, ringerName, membersWithoutYouOrRinger[0].member.getShortDisplayName(context), membersWithoutYouOrRinger[1].member.getShortDisplayName(context) ) else -> { val others = membersWithoutYouOrRinger.size - 2 context.resources.getQuantityString( R.plurals.WebRtcCallView__s_is_calling_you_s_s_and_d_others, others, ringerName, membersWithoutYouOrRinger[0].member.getShortDisplayName(context), membersWithoutYouOrRinger[1].member.getShortDisplayName(context), others ) } } } return null } fun needsNewRequestSizes(): Boolean { return if (groupCallState.isNotIdle) { allRemoteParticipants.any { it.videoSink.needsNewRequestingSize() } } else { false } } companion object { private const val SMALL_GROUP_MAX = 6 @JvmField val MAX_OUTGOING_GROUP_RING_DURATION = TimeUnit.MINUTES.toMillis(1) @JvmField val STARTING_STATE = CallParticipantsState() @JvmStatic fun update( oldState: CallParticipantsState, webRtcViewModel: WebRtcViewModel, enableVideo: Boolean ): CallParticipantsState { var newShowVideoForOutgoing: Boolean = oldState.showVideoForOutgoing if (enableVideo) { newShowVideoForOutgoing = webRtcViewModel.state == WebRtcViewModel.State.CALL_OUTGOING } else if (webRtcViewModel.state != WebRtcViewModel.State.CALL_OUTGOING) { newShowVideoForOutgoing = false } val isInOutgoingRingingMode = if (oldState.isInOutgoingRingingMode) { webRtcViewModel.callConnectedTime + MAX_OUTGOING_GROUP_RING_DURATION > System.currentTimeMillis() && webRtcViewModel.remoteParticipants.size == 0 } else { oldState.ringGroup && webRtcViewModel.callConnectedTime + MAX_OUTGOING_GROUP_RING_DURATION > System.currentTimeMillis() && webRtcViewModel.remoteParticipants.size == 0 && oldState.callState == WebRtcViewModel.State.CALL_OUTGOING && webRtcViewModel.state == WebRtcViewModel.State.CALL_CONNECTED } val localRenderState: WebRtcLocalRenderState = determineLocalRenderMode( oldState = oldState, localParticipant = webRtcViewModel.localParticipant, showVideoForOutgoing = newShowVideoForOutgoing, isNonIdleGroupCall = webRtcViewModel.groupState.isNotIdle, callState = webRtcViewModel.state, numberOfRemoteParticipants = webRtcViewModel.remoteParticipants.size ) return oldState.copy( callState = webRtcViewModel.state, groupCallState = webRtcViewModel.groupState, remoteParticipants = oldState.remoteParticipants.getNext(webRtcViewModel.remoteParticipants), localParticipant = webRtcViewModel.localParticipant, focusedParticipant = getFocusedParticipant(webRtcViewModel.remoteParticipants), localRenderState = localRenderState, showVideoForOutgoing = newShowVideoForOutgoing, remoteDevicesCount = webRtcViewModel.remoteDevicesCount, ringGroup = webRtcViewModel.ringGroup, isInOutgoingRingingMode = isInOutgoingRingingMode, ringerRecipient = webRtcViewModel.ringerRecipient ) } @JvmStatic fun update(oldState: CallParticipantsState, isInPip: Boolean): CallParticipantsState { val localRenderState: WebRtcLocalRenderState = determineLocalRenderMode(oldState = oldState, isInPip = isInPip) return oldState.copy(localRenderState = localRenderState, isInPipMode = isInPip) } @JvmStatic fun setExpanded(oldState: CallParticipantsState, expanded: Boolean): CallParticipantsState { val localRenderState: WebRtcLocalRenderState = determineLocalRenderMode(oldState = oldState, isExpanded = expanded) return oldState.copy(localRenderState = localRenderState) } @JvmStatic fun update(oldState: CallParticipantsState, selectedPage: SelectedPage): CallParticipantsState { val localRenderState: WebRtcLocalRenderState = determineLocalRenderMode(oldState = oldState, isViewingFocusedParticipant = selectedPage == SelectedPage.FOCUSED) return oldState.copy(localRenderState = localRenderState, isViewingFocusedParticipant = selectedPage == SelectedPage.FOCUSED) } @JvmStatic fun update(oldState: CallParticipantsState, foldableState: FoldableState): CallParticipantsState { val localRenderState: WebRtcLocalRenderState = determineLocalRenderMode(oldState = oldState) return oldState.copy(localRenderState = localRenderState, foldableState = foldableState) } @JvmStatic fun update(oldState: CallParticipantsState, groupMembers: List): CallParticipantsState { return oldState.copy(groupMembers = groupMembers) } @JvmStatic fun update(oldState: CallParticipantsState, ephemeralState: WebRtcEphemeralState): CallParticipantsState { return oldState.copy( remoteParticipants = oldState.remoteParticipants.map { p -> p.copy(audioLevel = ephemeralState.remoteAudioLevels[p.callParticipantId]) }, localParticipant = oldState.localParticipant.copy(audioLevel = ephemeralState.localAudioLevel), focusedParticipant = oldState.focusedParticipant.copy(audioLevel = ephemeralState.remoteAudioLevels[oldState.focusedParticipant.callParticipantId]) ) } private fun determineLocalRenderMode( oldState: CallParticipantsState, localParticipant: CallParticipant = oldState.localParticipant, isInPip: Boolean = oldState.isInPipMode, showVideoForOutgoing: Boolean = oldState.showVideoForOutgoing, isNonIdleGroupCall: Boolean = oldState.groupCallState.isNotIdle, callState: WebRtcViewModel.State = oldState.callState, numberOfRemoteParticipants: Int = oldState.allRemoteParticipants.size, isViewingFocusedParticipant: Boolean = oldState.isViewingFocusedParticipant, isExpanded: Boolean = oldState.localRenderState == WebRtcLocalRenderState.EXPANDED ): WebRtcLocalRenderState { val displayLocal: Boolean = (numberOfRemoteParticipants == 0 || !isInPip) && (isNonIdleGroupCall || localParticipant.isVideoEnabled) var localRenderState: WebRtcLocalRenderState = WebRtcLocalRenderState.GONE if (!isInPip && isExpanded && (localParticipant.isVideoEnabled || isNonIdleGroupCall)) { return WebRtcLocalRenderState.EXPANDED } else if (displayLocal || showVideoForOutgoing) { if (callState == WebRtcViewModel.State.CALL_CONNECTED || callState == WebRtcViewModel.State.CALL_RECONNECTING) { localRenderState = if (isViewingFocusedParticipant || numberOfRemoteParticipants > 1) { WebRtcLocalRenderState.SMALLER_RECTANGLE } else if (numberOfRemoteParticipants == 1) { WebRtcLocalRenderState.SMALL_RECTANGLE } else { if (localParticipant.isVideoEnabled) WebRtcLocalRenderState.LARGE else WebRtcLocalRenderState.LARGE_NO_VIDEO } } else if (callState != WebRtcViewModel.State.CALL_INCOMING && callState != WebRtcViewModel.State.CALL_DISCONNECTED) { localRenderState = if (localParticipant.isVideoEnabled) WebRtcLocalRenderState.LARGE else WebRtcLocalRenderState.LARGE_NO_VIDEO } } else if (callState == WebRtcViewModel.State.CALL_PRE_JOIN) { localRenderState = WebRtcLocalRenderState.LARGE_NO_VIDEO } return localRenderState } private fun getFocusedParticipant(participants: List): CallParticipant { val participantsByLastSpoke: List = participants.sortedByDescending(CallParticipant::lastSpoke) return if (participantsByLastSpoke.isEmpty()) { CallParticipant.EMPTY } else { participantsByLastSpoke.firstOrNull(CallParticipant::isScreenSharing) ?: participantsByLastSpoke[0] } } private fun describeGroupMembers( context: Context, @StringRes oneParticipant: Int, @StringRes twoParticipants: Int, @PluralsRes multipleParticipants: Int, members: List ): String { val eligibleMembers: List = members.filterNot { it.member.isSelf || it.member.isBlocked } return when (eligibleMembers.size) { 0 -> "" 1 -> context.getString( oneParticipant, eligibleMembers[0].member.getShortDisplayName(context) ) 2 -> context.getString( twoParticipants, eligibleMembers[0].member.getShortDisplayName(context), eligibleMembers[1].member.getShortDisplayName(context) ) else -> { val others = eligibleMembers.size - 2 context.resources.getQuantityString( multipleParticipants, others, eligibleMembers[0].member.getShortDisplayName(context), eligibleMembers[1].member.getShortDisplayName(context), others ) } } } } enum class SelectedPage { GRID, FOCUSED } }