Skip to content

Commit

Permalink
feat: End SFT oneOnOne call on proteus protocol (WPB-7153) 🍒 🍒 (#2948)
Browse files Browse the repository at this point in the history
* Commit with unresolved merge conflicts

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

* chore: resolve conflicts

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Oussama Hassine <[email protected]>
  • Loading branch information
github-actions[bot] and ohassine authored Sep 2, 2024
1 parent 1549f4c commit b70070b
Show file tree
Hide file tree
Showing 5 changed files with 331 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,7 @@ class CallManagerImpl internal constructor(
callRepository = callRepository,
qualifiedIdMapper = qualifiedIdMapper,
participantMapper = ParticipantMapperImpl(videoStateChecker, callMapper, qualifiedIdMapper),
userConfigRepository = userConfigRepository,
mlsCallHelper = MLSCallHelperImpl(
callRepository = callRepository,
subconversationRepository = subconversationRepository,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,55 +22,62 @@ import com.sun.jna.Pointer
import com.wire.kalium.calling.callbacks.ParticipantChangedHandler
import com.wire.kalium.logger.obfuscateId
import com.wire.kalium.logic.callingLogger
import com.wire.kalium.logic.configuration.UserConfigRepository
import com.wire.kalium.logic.data.call.CallParticipants
import com.wire.kalium.logic.data.call.CallRepository
import com.wire.kalium.logic.data.call.MLSCallHelper
import com.wire.kalium.logic.data.call.ParticipantMinimized
import com.wire.kalium.logic.data.call.mapper.ParticipantMapper
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.id.QualifiedIdMapper
import com.wire.kalium.logic.functional.getOrElse
import com.wire.kalium.logic.kaliumLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json

// TODO: add tests for this class
@Suppress("LongParameterList")
class OnParticipantListChanged internal constructor(
private val callRepository: CallRepository,
private val qualifiedIdMapper: QualifiedIdMapper,
private val participantMapper: ParticipantMapper,
private val userConfigRepository: UserConfigRepository,
private val mlsCallHelper: MLSCallHelper,
private val endCall: suspend (conversationId: ConversationId) -> Unit,
private val callingScope: CoroutineScope
private val callingScope: CoroutineScope,
private val jsonDecoder: Json = Json
) : ParticipantChangedHandler {

override fun onParticipantChanged(remoteConversationId: String, data: String, arg: Pointer?) {

val participantsChange = Json.decodeFromString<CallParticipants>(data)
val participantsChange = jsonDecoder.decodeFromString<CallParticipants>(data)

callingScope.launch {
val participants = mutableListOf<ParticipantMinimized>()
val conversationIdWithDomain = qualifiedIdMapper.fromStringToQualifiedID(remoteConversationId)
val conversationIdWithDomain =
qualifiedIdMapper.fromStringToQualifiedID(remoteConversationId)

participantsChange.members.map { member ->
participants.add(participantMapper.fromCallMemberToParticipantMinimized(member))
}
val callProtocol = callRepository.currentCallProtocol(conversationIdWithDomain)

val currentCall = callRepository.establishedCallsFlow().first().firstOrNull()
currentCall?.let {
val shouldEndSFTOneOnOneCall = mlsCallHelper.shouldEndSFTOneOnOneCall(
conversationId = conversationIdWithDomain,
callProtocol = callProtocol,
conversationType = it.conversationType,
newCallParticipants = participants,
previousCallParticipants = it.participants
)
if (shouldEndSFTOneOnOneCall) {
kaliumLogger.i("[onParticipantChanged] - Ending MLS call due to participant leaving")
endCall(conversationIdWithDomain)
if (userConfigRepository.shouldUseSFTForOneOnOneCalls().getOrElse(false)) {
val callProtocol = callRepository.currentCallProtocol(conversationIdWithDomain)

val currentCall = callRepository.establishedCallsFlow().first().firstOrNull()
currentCall?.let {
val shouldEndSFTOneOnOneCall = mlsCallHelper.shouldEndSFTOneOnOneCall(
conversationId = conversationIdWithDomain,
callProtocol = callProtocol,
conversationType = it.conversationType,
newCallParticipants = participants,
previousCallParticipants = it.participants
)
if (shouldEndSFTOneOnOneCall) {
kaliumLogger.i("[onParticipantChanged] - Ending SFT one on one call due to participant leaving")
endCall(conversationIdWithDomain)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,18 @@ class MLSCallHelperImpl(
conversationType: Conversation.Type,
newCallParticipants: List<ParticipantMinimized>,
previousCallParticipants: List<Participant>
) = callProtocol is Conversation.ProtocolInfo.MLS &&
userConfigRepository.shouldUseSFTForOneOnOneCalls().getOrElse(false) &&
): Boolean {
return if (callProtocol is Conversation.ProtocolInfo.Proteus) {
conversationType == Conversation.Type.ONE_ON_ONE &&
newCallParticipants.size == TWO_PARTICIPANTS &&
previousCallParticipants.size == TWO_PARTICIPANTS &&
previousCallParticipants[1].hasEstablishedAudio && !newCallParticipants[1].hasEstablishedAudio
newCallParticipants.size == ONE_PARTICIPANTS &&
previousCallParticipants.size == TWO_PARTICIPANTS
} else {
conversationType == Conversation.Type.ONE_ON_ONE &&
newCallParticipants.size == TWO_PARTICIPANTS &&
previousCallParticipants.size == TWO_PARTICIPANTS &&
previousCallParticipants[1].hasEstablishedAudio && !newCallParticipants[1].hasEstablishedAudio
}
}

override suspend fun handleCallTermination(
conversationId: ConversationId,
Expand Down Expand Up @@ -119,5 +125,6 @@ class MLSCallHelperImpl(

companion object {
const val TWO_PARTICIPANTS = 2
const val ONE_PARTICIPANTS = 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,44 +47,34 @@ import kotlin.test.assertTrue
class MLSCallHelperTest {

@Test
fun givenMlsConferenceCall_whenShouldEndSFTOneOnOneCallIsCalled_thenReturnCorrectValue() =
fun givenMlsProtocol_whenShouldEndSFTOneOnOneCallIsCalled_thenReturnCorrectValue() =
runTest {
val (_, mLSCallHelper) = Arrangement()
.withShouldUseSFTForOneOnOneCallsReturning(Either.Right(true))
.arrange()

// Proteus protocol
val shouldEndSFTOneOnOneCall1 = mLSCallHelper.shouldEndSFTOneOnOneCall(
conversationId = conversationId,
callProtocol = Conversation.ProtocolInfo.Proteus,
conversationType = Conversation.Type.ONE_ON_ONE,
newCallParticipants = listOf(participantMinimized1, participantMinimized2),
previousCallParticipants = listOf(participant1, participant2)
)
assertFalse { shouldEndSFTOneOnOneCall1 }

// one participant in the call
val shouldEndSFTOneOnOneCall2 = mLSCallHelper.shouldEndSFTOneOnOneCall(
val shouldEndSFTOneOnOneCall1 = mLSCallHelper.shouldEndSFTOneOnOneCall(
conversationId = conversationId,
callProtocol = CONVERSATION_MLS_PROTOCOL_INFO,
conversationType = Conversation.Type.ONE_ON_ONE,
newCallParticipants = listOf(participantMinimized1),
previousCallParticipants = listOf(participant1)
)
assertFalse { shouldEndSFTOneOnOneCall2 }
assertFalse { shouldEndSFTOneOnOneCall1 }

// Audi not lost for the second participant
val shouldEndSFTOneOnOneCall3 = mLSCallHelper.shouldEndSFTOneOnOneCall(
// Audio not lost for the second participant
val shouldEndSFTOneOnOneCall2 = mLSCallHelper.shouldEndSFTOneOnOneCall(
conversationId = conversationId,
callProtocol = CONVERSATION_MLS_PROTOCOL_INFO,
conversationType = Conversation.Type.GROUP,
newCallParticipants = listOf(participantMinimized1, participantMinimized2),
previousCallParticipants = listOf(participant1, participant2)
)
assertFalse { shouldEndSFTOneOnOneCall3 }
assertFalse { shouldEndSFTOneOnOneCall2 }

// Audio lost for the second participant
val shouldEndSFTOneOnOneCall4 = mLSCallHelper.shouldEndSFTOneOnOneCall(
val shouldEndSFTOneOnOneCall3 = mLSCallHelper.shouldEndSFTOneOnOneCall(
conversationId = conversationId,
callProtocol = CONVERSATION_MLS_PROTOCOL_INFO,
conversationType = Conversation.Type.ONE_ON_ONE,
Expand All @@ -94,24 +84,36 @@ class MLSCallHelperTest {
participantMinimized2.copy(hasEstablishedAudio = false)
)
)
assertTrue { shouldEndSFTOneOnOneCall4 }
assertTrue { shouldEndSFTOneOnOneCall3 }
}

val (_, mLSCallHelper2) = Arrangement()
.withShouldUseSFTForOneOnOneCallsReturning(Either.Left(StorageFailure.DataNotFound))
@Test
fun givenProteusProtocol_whenShouldEndSFTOneOnOneCallIsCalled_thenReturnCorrectValue() =
runTest {

val (_, mLSCallHelper) = Arrangement()
.withShouldUseSFTForOneOnOneCallsReturning(Either.Right(true))
.arrange()

// ShouldUseSFTForOneOnOneCalls user config is not found
val shouldEndSFTOneOnOneCall5 = mLSCallHelper2.shouldEndSFTOneOnOneCall(
// participants list has 2 items for the new list and the previous list
val shouldEndSFTOneOnOneCall1 = mLSCallHelper.shouldEndSFTOneOnOneCall(
conversationId = conversationId,
callProtocol = CONVERSATION_MLS_PROTOCOL_INFO,
callProtocol = Conversation.ProtocolInfo.Proteus,
conversationType = Conversation.Type.ONE_ON_ONE,
previousCallParticipants = listOf(participant1, participant2),
newCallParticipants = listOf(
participantMinimized1,
participantMinimized2.copy(hasEstablishedAudio = false)
)
newCallParticipants = listOf(participantMinimized1, participantMinimized2),
previousCallParticipants = listOf(participant1, participant2)
)
assertFalse { shouldEndSFTOneOnOneCall5 }
assertFalse { shouldEndSFTOneOnOneCall1 }

// new participants list has 1 participant
val shouldEndSFTOneOnOneCall2 = mLSCallHelper.shouldEndSFTOneOnOneCall(
conversationId = conversationId,
callProtocol = Conversation.ProtocolInfo.Proteus,
conversationType = Conversation.Type.ONE_ON_ONE,
newCallParticipants = listOf(participantMinimized1),
previousCallParticipants = listOf(participant1, participant2)
)
assertTrue { shouldEndSFTOneOnOneCall2 }
}

@Test
Expand All @@ -129,7 +131,7 @@ class MLSCallHelperTest {

coVerify {
arrangement.subconversationRepository.deleteRemoteSubConversation(any(), any(), any())
}.wasInvoked(once)
}.wasInvoked(exactly = once)
}

@Test
Expand All @@ -149,11 +151,7 @@ class MLSCallHelperTest {
mLSCallHelper.handleCallTermination(conversationId, Conversation.Type.ONE_ON_ONE)

coVerify {
arrangement.subconversationRepository.deleteRemoteSubConversation(
eq(conversationId),
any(),
any()
)
arrangement.subconversationRepository.deleteRemoteSubConversation(any(), any(), any())
}.wasNotInvoked()
}

Expand All @@ -167,8 +165,8 @@ class MLSCallHelperTest {
mLSCallHelper.handleCallTermination(conversationId, Conversation.Type.GROUP)

coVerify {
arrangement.callRepository.leaveMlsConference(eq(conversationId))
}.wasInvoked(once)
arrangement.callRepository.leaveMlsConference(any())
}.wasInvoked(exactly = once)
}

@Test
Expand All @@ -182,7 +180,7 @@ class MLSCallHelperTest {

coVerify {
arrangement.callRepository.leaveMlsConference(eq(conversationId))
}.wasInvoked(once)
}.wasInvoked(exactly = once)
}

private class Arrangement {
Expand All @@ -206,18 +204,13 @@ class MLSCallHelperTest {

fun withShouldUseSFTForOneOnOneCallsReturning(result: Either<StorageFailure, Boolean>) =
apply {
every {
userConfigRepository.shouldUseSFTForOneOnOneCalls()
}.returns(result)
every { userConfigRepository.shouldUseSFTForOneOnOneCalls() }.returns(result)
}

suspend fun withFetchRemoteSubConversationDetailsReturning(result: Either<NetworkFailure, SubconversationResponse>) =
apply {
coEvery {
subconversationRepository.fetchRemoteSubConversationDetails(
conversationId,
CALL_SUBCONVERSATION_ID
)
subconversationRepository.fetchRemoteSubConversationDetails(eq(conversationId), eq(CALL_SUBCONVERSATION_ID))
}.returns(result)
}

Expand All @@ -238,32 +231,32 @@ class MLSCallHelperTest {
Instant.parse("2021-03-30T15:36:00.000Z"),
cipherSuite = CipherSuite.MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519
)
val participantMinimized1 = ParticipantMinimized(
val participant1 = Participant(
id = QualifiedID("participantId", "participantDomain"),
userId = QualifiedID("participantId", "participantDomain"),
clientId = "abcd",
name = "name",
isMuted = true,
isSpeaking = false,
isCameraOn = false,
avatarAssetId = null,
isSharingScreen = false,
hasEstablishedAudio = true
hasEstablishedAudio = true,
accentId = 0
)
val participantMinimized2 = participantMinimized1.copy(
val participant2 = participant1.copy(
id = QualifiedID("participantId2", "participantDomain2"),
clientId = "efgh"
)
val participant1 = Participant(
val participantMinimized1 = ParticipantMinimized(
id = QualifiedID("participantId", "participantDomain"),
userId = QualifiedID("participantId", "participantDomain"),
clientId = "abcd",
name = "name",
isMuted = true,
isSpeaking = false,
isCameraOn = false,
avatarAssetId = null,
isSharingScreen = false,
hasEstablishedAudio = true,
accentId = 0
hasEstablishedAudio = true
)
val participant2 = participant1.copy(
val participantMinimized2 = participantMinimized1.copy(
id = QualifiedID("participantId2", "participantDomain2"),
clientId = "efgh"
)
Expand Down
Loading

0 comments on commit b70070b

Please sign in to comment.