From 08ffe8a152ab51f3c54d6e3580ed5d8feb1ca9fd Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Fri, 26 Jul 2024 11:09:07 +0200 Subject: [PATCH 1/9] chore: resolve conflicts --- .../logic/feature/call/CallManagerTest.kt | 8 + .../call/scenario/OnIncomingCallTest.kt | 7 +- .../logic/feature/call/CallManagerImpl.kt | 2 + .../logic/feature/call/GlobalCallManager.kt | 2 + .../logic/feature/call/CallManagerImpl.kt | 18 +- .../logic/feature/call/GlobalCallManager.kt | 3 + .../feature/call/scenario/OnIncomingCall.kt | 4 +- .../kalium/logic/data/call/CallRepository.kt | 4 +- ...tionType.kt => ConversationTypeForCall.kt} | 2 +- .../logic/data/call/mapper/CallMapper.kt | 72 +++-- .../data/conversation/ConversationMapper.kt | 2 + .../conversation/ConversationRepository.kt | 2 + .../data/featureConfig/FeatureConfigMapper.kt | 2 +- .../kalium/logic/feature/UserSessionScope.kt | 36 ++- .../kalium/logic/feature/call/CallManager.kt | 2 + .../kalium/logic/feature/call/CallsScope.kt | 6 +- .../logic/feature/call/GlobalCallManager.kt | 3 + .../GetCallConversationTypeProvider.kt | 71 +++++ .../feature/call/usecase/StartCallUseCase.kt | 6 + .../kalium/logic/data/call/CallMapperTest.kt | 24 +- .../logic/data/call/CallRepositoryTest.kt | 20 +- .../GetCallConversationTypeProviderTest.kt | 259 ++++++++++++++++++ .../call/usecase/StartCallUseCaseTest.kt | 24 +- .../featureConfigs/FeatureConfigResponse.kt | 2 +- .../wire/kalium/persistence/Conversations.sq | 3 + .../dao/conversation/ConversationDAO.kt | 1 + .../dao/conversation/ConversationDAOImpl.kt | 5 + 27 files changed, 510 insertions(+), 80 deletions(-) rename logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/{ConversationType.kt => ConversationTypeForCall.kt} (95%) create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/GetCallConversationTypeProvider.kt create mode 100644 logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/GetCallConversationTypeProviderTest.kt diff --git a/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt b/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt index 0cf2dca1731..cd126782a03 100644 --- a/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt +++ b/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt @@ -49,6 +49,10 @@ import kotlin.test.BeforeTest import kotlin.test.Ignore import kotlin.test.Test import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater +import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider +import com.wire.kalium.network.NetworkStateObserver +import com.wire.kalium.util.DateTimeUtil.toIsoDateTimeString +import kotlinx.datetime.Instant class CallManagerTest { @@ -85,6 +89,9 @@ class CallManagerTest { @Mock private val videoStateChecker = mock(classOf()) + @Mock + private val getCallConversationType = mock(GetCallConversationTypeProvider::class) + private val dispatcher = TestKaliumDispatcher private lateinit var callManagerImpl: CallManagerImpl @@ -108,6 +115,7 @@ class CallManagerTest { qualifiedIdMapper = qualifiedIdMapper, videoStateChecker = videoStateChecker, callMapper = callMapper, + getCallConversationType = getCallConversationType, conversationClientsInCallUpdater = conversationClientsInCallUpdater, kaliumConfigs = kaliumConfigs ) diff --git a/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCallTest.kt b/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCallTest.kt index df38d77048c..964694f3a20 100644 --- a/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCallTest.kt +++ b/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCallTest.kt @@ -20,7 +20,7 @@ package com.wire.kalium.logic.feature.call.scenario import com.wire.kalium.calling.types.Uint32_t import com.wire.kalium.calling.ConversationTypeCalling import com.wire.kalium.logic.data.call.CallRepository -import com.wire.kalium.logic.data.call.ConversationType +import com.wire.kalium.logic.data.call.ConversationTypeForCall import com.wire.kalium.logic.data.call.mapper.CallMapperImpl import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl import com.wire.kalium.logic.data.call.CallStatus @@ -33,7 +33,6 @@ import io.mockative.configure import io.mockative.eq import io.mockative.mock import io.mockative.once -import io.mockative.verify import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest @@ -64,7 +63,7 @@ class OnIncomingCallTest { .suspendFunction(arrangement.callRepository::createCall) .with( eq(TestConversation.CONVERSATION.id), - eq(ConversationType.Conference), + eq(ConversationTypeForCall.Conference), eq(CallStatus.INCOMING), eq(TestUser.USER_ID.toString()), eq(true), @@ -95,7 +94,7 @@ class OnIncomingCallTest { .suspendFunction(arrangement.callRepository::createCall) .with( eq(TestConversation.CONVERSATION.id), - eq(ConversationType.Conference), + eq(ConversationTypeForCall.Conference), eq(CallStatus.STILL_ONGOING), eq(TestUser.USER_ID.toString()), eq(true), diff --git a/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt b/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt index 59dd7230f91..a136495738a 100644 --- a/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt +++ b/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt @@ -18,6 +18,7 @@ package com.wire.kalium.logic.feature.call +import com.wire.kalium.calling.ConversationTypeCalling import com.wire.kalium.logic.data.call.CallClientList import com.wire.kalium.logic.data.call.CallType import com.wire.kalium.logic.data.call.EpochInfo @@ -35,6 +36,7 @@ class CallManagerImpl : CallManager { override suspend fun startCall( conversationId: ConversationId, callType: CallType, + conversationTypeCalling: ConversationTypeCalling, isAudioCbr: Boolean ) { TODO("Not yet implemented") diff --git a/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt b/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt index 8ba800004ba..ac3af705113 100644 --- a/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt +++ b/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt @@ -30,6 +30,7 @@ import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater +import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.featureFlags.KaliumConfigs @@ -48,6 +49,7 @@ actual class GlobalCallManager { qualifiedIdMapper: QualifiedIdMapper, videoStateChecker: VideoStateChecker, conversationClientsInCallUpdater: ConversationClientsInCallUpdater, + getCallConversationType: GetCallConversationTypeProvider, kaliumConfigs: KaliumConfigs ): CallManager { return CallManagerImpl() diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt index 0b32be4cb27..507397c67db 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt @@ -21,6 +21,7 @@ package com.wire.kalium.logic.feature.call import com.sun.jna.Pointer import com.wire.kalium.calling.CallTypeCalling import com.wire.kalium.calling.Calling +import com.wire.kalium.calling.ConversationTypeCalling import com.wire.kalium.calling.callbacks.ConstantBitRateStateChangeHandler import com.wire.kalium.calling.callbacks.MetricsHandler import com.wire.kalium.calling.callbacks.ReadyHandler @@ -34,7 +35,6 @@ import com.wire.kalium.logic.data.call.CallClientList import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.call.CallStatus import com.wire.kalium.logic.data.call.CallType -import com.wire.kalium.logic.data.call.ConversationType import com.wire.kalium.logic.data.call.EpochInfo import com.wire.kalium.logic.data.call.VideoState import com.wire.kalium.logic.data.call.VideoStateChecker @@ -67,6 +67,7 @@ import com.wire.kalium.logic.feature.call.scenario.OnRequestNewEpoch import com.wire.kalium.logic.feature.call.scenario.OnSFTRequest import com.wire.kalium.logic.feature.call.scenario.OnSendOTR import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater +import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.featureFlags.KaliumConfigs import com.wire.kalium.logic.functional.fold @@ -102,6 +103,8 @@ class CallManagerImpl internal constructor( private val qualifiedIdMapper: QualifiedIdMapper, private val videoStateChecker: VideoStateChecker, private val conversationClientsInCallUpdater: ConversationClientsInCallUpdater, + private val networkStateObserver: NetworkStateObserver, + private val getCallConversationType: GetCallConversationTypeProvider, private val kaliumConfigs: KaliumConfigs, private val json: Json = Json { ignoreUnknownKeys = true }, private val shouldRemoteMuteChecker: ShouldRemoteMuteChecker = ShouldRemoteMuteCheckerImpl(), @@ -232,9 +235,8 @@ class CallManagerImpl internal constructor( message.conversationId } - val type = conversationRepository.getConversationById(targetConversationId)?.let { - callMapper.fromConversationToConversationType(it) - } ?: ConversationType.Unknown + val callConversationType = getCallConversationType(targetConversationId) + val type = callMapper.toConversationType(callConversationType) wcall_recv_msg( inst = deferredHandle.await(), @@ -254,6 +256,7 @@ class CallManagerImpl internal constructor( override suspend fun startCall( conversationId: ConversationId, callType: CallType, + conversationTypeCalling: ConversationTypeCalling, isAudioCbr: Boolean ) { callingLogger.d( @@ -261,9 +264,7 @@ class CallManagerImpl internal constructor( "${conversationId.toLogString()}.." ) val isCameraOn = callType == CallType.VIDEO - val type = conversationRepository.getConversationById(conversationId)?.let { - callMapper.fromConversationToConversationType(it) - } ?: ConversationType.Unknown + val type = callMapper.toConversationType(conversationTypeCalling) callRepository.createCall( conversationId = conversationId, @@ -277,13 +278,12 @@ class CallManagerImpl internal constructor( withCalling { val avsCallType = callMapper.toCallTypeCalling(callType) - val avsConversationType = callMapper.toConversationTypeCalling(type) // TODO: Handle response. Possible failure? wcall_start( deferredHandle.await(), federatedIdMapper.parseToFederatedId(conversationId), avsCallType.avsValue, - avsConversationType.avsValue, + conversationTypeCalling.avsValue, isAudioCbr.toInt() ) diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt index fcc30c6e080..090de749688 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt @@ -35,6 +35,7 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater +import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.featureFlags.KaliumConfigs import com.wire.kalium.logic.util.CurrentPlatform @@ -84,6 +85,7 @@ actual class GlobalCallManager( qualifiedIdMapper: QualifiedIdMapper, videoStateChecker: VideoStateChecker, conversationClientsInCallUpdater: ConversationClientsInCallUpdater, + getCallConversationType: GetCallConversationTypeProvider, kaliumConfigs: KaliumConfigs ): CallManager { return callManagerHolder.computeIfAbsent(userId) { @@ -100,6 +102,7 @@ actual class GlobalCallManager( qualifiedIdMapper = qualifiedIdMapper, videoStateChecker = videoStateChecker, conversationClientsInCallUpdater = conversationClientsInCallUpdater, + getCallConversationType = getCallConversationType, kaliumConfigs = kaliumConfigs ) } diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCall.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCall.kt index b626d045bb9..4c23e8fe868 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCall.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCall.kt @@ -25,7 +25,7 @@ import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.callingLogger import com.wire.kalium.logic.data.call.mapper.CallMapper import com.wire.kalium.logic.data.call.CallRepository -import com.wire.kalium.logic.data.call.ConversationType +import com.wire.kalium.logic.data.call.ConversationTypeForCall import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.call.CallStatus import com.wire.kalium.logic.featureFlags.KaliumConfigs @@ -55,7 +55,7 @@ class OnIncomingCall( " | UserId: ${userId.obfuscateId()} | shouldRing: $shouldRing | type: $conversationType" ) val mappedConversationType = callMapper.fromIntToConversationType(conversationType) - val isMuted = setOf(ConversationType.Conference, ConversationType.ConferenceMls).contains(mappedConversationType) + val isMuted = setOf(ConversationTypeForCall.Conference, ConversationTypeForCall.ConferenceMls).contains(mappedConversationType) val status = if (shouldRing) CallStatus.INCOMING else CallStatus.STILL_ONGOING val qualifiedConversationId = qualifiedIdMapper.fromStringToQualifiedID(conversationId) scope.launch { diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt index 52ca28b5867..d8f02129023 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt @@ -106,7 +106,7 @@ interface CallRepository { @Suppress("LongParameterList") suspend fun createCall( conversationId: ConversationId, - type: ConversationType, + type: ConversationTypeForCall, status: CallStatus, callerId: String, isMuted: Boolean, @@ -185,7 +185,7 @@ internal class CallDataSource( @Suppress("LongMethod", "NestedBlockDepth") override suspend fun createCall( conversationId: ConversationId, - type: ConversationType, + type: ConversationTypeForCall, status: CallStatus, callerId: String, isMuted: Boolean, diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/ConversationType.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/ConversationTypeForCall.kt similarity index 95% rename from logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/ConversationType.kt rename to logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/ConversationTypeForCall.kt index ca39a3d820c..8f010c91fbd 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/ConversationType.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/ConversationTypeForCall.kt @@ -22,7 +22,7 @@ package com.wire.kalium.logic.data.call * [OneOnOne] for a 1:1 call * [Conference] for a group cal */ -enum class ConversationType { +enum class ConversationTypeForCall { OneOnOne, Conference, ConferenceMls, diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/CallMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/CallMapper.kt index 4693b6483fa..d9f8cf80c02 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/CallMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/CallMapper.kt @@ -24,7 +24,7 @@ import com.wire.kalium.calling.VideoStateCalling import com.wire.kalium.logic.data.call.CallClientList import com.wire.kalium.logic.data.call.CallMetadata import com.wire.kalium.logic.data.call.CallType -import com.wire.kalium.logic.data.call.ConversationType +import com.wire.kalium.logic.data.call.ConversationTypeForCall import com.wire.kalium.logic.data.call.VideoState import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.conversation.Conversation @@ -41,17 +41,21 @@ import com.wire.kalium.persistence.dao.conversation.ConversationEntity interface CallMapper { fun toCallTypeCalling(callType: CallType): CallTypeCalling - fun toConversationTypeCalling(conversationType: ConversationType): ConversationTypeCalling - fun fromIntToConversationType(conversationType: Int): ConversationType + fun toConversationTypeCalling(conversationTypeForCall: ConversationTypeForCall): ConversationTypeCalling + fun toConversationType(conversationTypeCalling: ConversationTypeCalling): ConversationTypeForCall + fun fromIntToConversationType(conversationType: Int): ConversationTypeForCall fun fromIntToCallingVideoState(videStateInt: Int): VideoStateCalling fun toVideoStateCalling(videoState: VideoState): VideoStateCalling - fun fromConversationToConversationType(conversation: Conversation): ConversationType + fun fromConversationTypeToConversationTypeForCall( + conversationType: Conversation.Type, + conversationProtocol: Conversation.ProtocolInfo + ): ConversationTypeForCall @Suppress("LongParameterList") fun toCallEntity( conversationId: ConversationId, id: String, - type: ConversationType, + type: ConversationTypeForCall, status: CallStatus, conversationType: Conversation.Type, callerId: UserId @@ -81,21 +85,30 @@ class CallMapperImpl( } } - override fun toConversationTypeCalling(conversationType: ConversationType): ConversationTypeCalling { - return when (conversationType) { - ConversationType.OneOnOne -> ConversationTypeCalling.OneOnOne - ConversationType.Conference -> ConversationTypeCalling.Conference - ConversationType.ConferenceMls -> ConversationTypeCalling.ConferenceMls + override fun toConversationTypeCalling(conversationTypeForCall: ConversationTypeForCall): ConversationTypeCalling { + return when (conversationTypeForCall) { + ConversationTypeForCall.OneOnOne -> ConversationTypeCalling.OneOnOne + ConversationTypeForCall.Conference -> ConversationTypeCalling.Conference + ConversationTypeForCall.ConferenceMls -> ConversationTypeCalling.ConferenceMls else -> ConversationTypeCalling.Unknown } } - override fun fromIntToConversationType(conversationType: Int): ConversationType { + override fun toConversationType(conversationTypeCalling: ConversationTypeCalling): ConversationTypeForCall { + return when (conversationTypeCalling) { + ConversationTypeCalling.OneOnOne -> ConversationTypeForCall.OneOnOne + ConversationTypeCalling.Conference -> ConversationTypeForCall.Conference + ConversationTypeCalling.ConferenceMls -> ConversationTypeForCall.ConferenceMls + else -> ConversationTypeForCall.Unknown + } + } + + override fun fromIntToConversationType(conversationType: Int): ConversationTypeForCall { return when (conversationType) { - ConversationTypeCalling.OneOnOne.avsValue -> ConversationType.OneOnOne - ConversationTypeCalling.Conference.avsValue -> ConversationType.Conference - ConversationTypeCalling.ConferenceMls.avsValue -> ConversationType.ConferenceMls - else -> ConversationType.Unknown + ConversationTypeCalling.OneOnOne.avsValue -> ConversationTypeForCall.OneOnOne + ConversationTypeCalling.Conference.avsValue -> ConversationTypeForCall.Conference + ConversationTypeCalling.ConferenceMls.avsValue -> ConversationTypeForCall.ConferenceMls + else -> ConversationTypeForCall.Unknown } } @@ -120,23 +133,26 @@ class CallMapperImpl( VideoState.UNKNOWN -> VideoStateCalling.UNKNOWN } - override fun fromConversationToConversationType(conversation: Conversation): ConversationType = - when (conversation.type) { + override fun fromConversationTypeToConversationTypeForCall( + conversationType: Conversation.Type, + conversationProtocol: Conversation.ProtocolInfo + ): ConversationTypeForCall = + when (conversationType) { Conversation.Type.GROUP -> { - when (conversation.protocol) { - is Conversation.ProtocolInfo.MLS -> ConversationType.ConferenceMls + when (conversationProtocol) { + is Conversation.ProtocolInfo.MLS -> ConversationTypeForCall.ConferenceMls is Conversation.ProtocolInfo.Proteus, - is Conversation.ProtocolInfo.Mixed -> ConversationType.Conference + is Conversation.ProtocolInfo.Mixed -> ConversationTypeForCall.Conference } } - Conversation.Type.ONE_ON_ONE -> ConversationType.OneOnOne - else -> ConversationType.Unknown + Conversation.Type.ONE_ON_ONE -> ConversationTypeForCall.OneOnOne + else -> ConversationTypeForCall.Unknown } override fun toCallEntity( conversationId: ConversationId, id: String, - type: ConversationType, + type: ConversationTypeForCall, status: CallStatus, conversationType: Conversation.Type, callerId: UserId @@ -179,11 +195,11 @@ class CallMapperImpl( else -> ConversationEntity.Type.ONE_ON_ONE } - private fun toCallEntityType(conversationType: ConversationType): CallEntity.Type = when (conversationType) { - ConversationType.OneOnOne -> CallEntity.Type.ONE_ON_ONE - ConversationType.Conference -> CallEntity.Type.CONFERENCE - ConversationType.ConferenceMls -> CallEntity.Type.MLS_CONFERENCE - ConversationType.Unknown -> CallEntity.Type.UNKNOWN + private fun toCallEntityType(conversationTypeForCall: ConversationTypeForCall): CallEntity.Type = when (conversationTypeForCall) { + ConversationTypeForCall.OneOnOne -> CallEntity.Type.ONE_ON_ONE + ConversationTypeForCall.Conference -> CallEntity.Type.CONFERENCE + ConversationTypeForCall.ConferenceMls -> CallEntity.Type.MLS_CONFERENCE + ConversationTypeForCall.Unknown -> CallEntity.Type.UNKNOWN } override fun toConversationType(conversationType: ConversationEntity.Type): Conversation.Type = when (conversationType) { diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt index 19bdd46b4f9..10b74a8ee4a 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt @@ -83,6 +83,8 @@ interface ConversationMapper { fun verificationStatusFromEntity(verificationStatus: ConversationEntity.VerificationStatus): Conversation.VerificationStatus fun legalHoldStatusToEntity(legalHoldStatus: Conversation.LegalHoldStatus): ConversationEntity.LegalHoldStatus fun legalHoldStatusFromEntity(legalHoldStatus: ConversationEntity.LegalHoldStatus): Conversation.LegalHoldStatus + + fun fromConversationEntityType(type: ConversationEntity.Type): Conversation.Type } @Suppress("TooManyFunctions", "LongParameterList") diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt index 5b245f0a1e8..e2a871c06fb 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt @@ -133,6 +133,8 @@ interface ConversationRepository { suspend fun fetchConversationIfUnknown(conversationID: ConversationId): Either suspend fun observeById(conversationId: ConversationId): Flow> suspend fun getConversationById(conversationId: ConversationId): Conversation? + suspend fun getConversationTypeById(conversationId: ConversationId): Either + suspend fun observeCacheDetailsById(conversationId: ConversationId): Either> suspend fun detailsById(conversationId: ConversationId): Either suspend fun baseInfoById(conversationId: ConversationId): Either suspend fun getConversationRecipients(conversationId: ConversationId): Either> diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/featureConfig/FeatureConfigMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/featureConfig/FeatureConfigMapper.kt index 2aceb2a1fb8..25355fc8ce2 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/featureConfig/FeatureConfigMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/featureConfig/FeatureConfigMapper.kt @@ -132,7 +132,7 @@ class FeatureConfigMapperImpl : FeatureConfigMapper { override fun fromDTO(data: FeatureConfigData.ConferenceCalling): ConferenceCallingModel = ConferenceCallingModel( status = fromDTO(data.status), - useSFTForOneOnOneCalls = data.config.useSFTForOneToOneCalls + useSFTForOneOnOneCalls = data.config?.useSFTForOneToOneCalls ?: false ) override fun fromDTO(data: FeatureConfigData.E2EI?): E2EIModel = diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt index b41fb897030..3346c37ee7d 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt @@ -168,6 +168,8 @@ import com.wire.kalium.logic.feature.call.CallsScope import com.wire.kalium.logic.feature.call.GlobalCallManager import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdaterImpl +import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider +import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProviderImpl import com.wire.kalium.logic.feature.call.usecase.UpdateConversationClientsForCurrentCallUseCase import com.wire.kalium.logic.feature.call.usecase.UpdateConversationClientsForCurrentCallUseCaseImpl import com.wire.kalium.logic.feature.client.ClientScope @@ -1195,6 +1197,7 @@ class UserSessionScope internal constructor( videoStateChecker = videoStateChecker, callMapper = callMapper, conversationClientsInCallUpdater = conversationClientsInCallUpdater, + getCallConversationType = getCallConversationType, kaliumConfigs = kaliumConfigs ) } @@ -1214,6 +1217,14 @@ class UserSessionScope internal constructor( federatedIdMapper = federatedIdMapper ) + private val getCallConversationType: GetCallConversationTypeProvider by lazy { + GetCallConversationTypeProviderImpl( + userConfigRepository = userConfigRepository, + conversationRepository = conversationRepository, + callMapper = callMapper + ) + } + private val updateConversationClientsForCurrentCall: Lazy get() = lazy { UpdateConversationClientsForCurrentCallUseCaseImpl(callRepository, conversationClientsInCallUpdater) @@ -1902,18 +1913,19 @@ class UserSessionScope internal constructor( val calls: CallsScope get() = CallsScope( - callManager, - callRepository, - conversationRepository, - userRepository, - flowManagerService, - mediaManagerService, - syncManager, - qualifiedIdMapper, - clientIdProvider, - userConfigRepository, - conversationClientsInCallUpdater, - kaliumConfigs + callManager = callManager, + callRepository = callRepository, + conversationRepository = conversationRepository, + userRepository = userRepository, + flowManagerService = flowManagerService, + mediaManagerService = mediaManagerService, + syncManager = syncManager, + qualifiedIdMapper = qualifiedIdMapper, + currentClientIdProvider = clientIdProvider, + userConfigRepository = userConfigRepository, + getCallConversationType = getCallConversationType, + conversationClientsInCallUpdater = conversationClientsInCallUpdater, + kaliumConfigs = kaliumConfigs ) val connection: ConnectionScope diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallManager.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallManager.kt index cb0199bc603..780d4da06d5 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallManager.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallManager.kt @@ -18,6 +18,7 @@ package com.wire.kalium.logic.feature.call +import com.wire.kalium.calling.ConversationTypeCalling import com.wire.kalium.logic.data.call.CallClientList import com.wire.kalium.logic.data.call.CallType import com.wire.kalium.logic.data.call.EpochInfo @@ -34,6 +35,7 @@ interface CallManager { suspend fun startCall( conversationId: ConversationId, callType: CallType, + conversationTypeCalling: ConversationTypeCalling, isAudioCbr: Boolean ) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallsScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallsScope.kt index ab8a4650e4b..3b99a5cf29c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallsScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallsScope.kt @@ -25,21 +25,23 @@ import com.wire.kalium.logic.data.call.CallingParticipantsOrderImpl import com.wire.kalium.logic.data.call.ParticipantsFilterImpl import com.wire.kalium.logic.data.call.ParticipantsOrderByNameImpl import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.user.UserRepository -import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.feature.call.usecase.AnswerCallUseCase import com.wire.kalium.logic.feature.call.usecase.AnswerCallUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater import com.wire.kalium.logic.feature.call.usecase.EndCallResultListenerImpl import com.wire.kalium.logic.feature.call.usecase.EndCallOnConversationChangeUseCase import com.wire.kalium.logic.feature.call.usecase.EndCallOnConversationChangeUseCaseImpl +import com.wire.kalium.logic.feature.call.usecase.EndCallResultListenerImpl import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.EndCallUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.FlipToBackCameraUseCase import com.wire.kalium.logic.feature.call.usecase.FlipToFrontCameraUseCase import com.wire.kalium.logic.feature.call.usecase.GetAllCallsWithSortedParticipantsUseCase import com.wire.kalium.logic.feature.call.usecase.GetAllCallsWithSortedParticipantsUseCaseImpl +import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider import com.wire.kalium.logic.feature.call.usecase.GetIncomingCallsUseCase import com.wire.kalium.logic.feature.call.usecase.GetIncomingCallsUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.IsCallRunningUseCase @@ -86,6 +88,7 @@ class CallsScope internal constructor( private val currentClientIdProvider: CurrentClientIdProvider, private val userConfigRepository: UserConfigRepository, private val conversationClientsInCallUpdater: ConversationClientsInCallUpdater, + private val getCallConversationType: GetCallConversationTypeProvider, private val kaliumConfigs: KaliumConfigs, internal val dispatcher: KaliumDispatcher = KaliumDispatcherImpl ) { @@ -121,6 +124,7 @@ class CallsScope internal constructor( syncManager = syncManager, callRepository = callRepository, answerCall = answerCall, + getCallConversationType = getCallConversationType, kaliumConfigs = kaliumConfigs ) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt index 6bc1da5e8c7..9fcd81de371 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt @@ -30,6 +30,7 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater +import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.featureFlags.KaliumConfigs @@ -49,6 +50,8 @@ expect class GlobalCallManager { qualifiedIdMapper: QualifiedIdMapper, videoStateChecker: VideoStateChecker, conversationClientsInCallUpdater: ConversationClientsInCallUpdater, + getCallConversationType: GetCallConversationTypeProvider, + networkStateObserver: NetworkStateObserver, kaliumConfigs: KaliumConfigs ): CallManager diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/GetCallConversationTypeProvider.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/GetCallConversationTypeProvider.kt new file mode 100644 index 00000000000..de362ab863d --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/GetCallConversationTypeProvider.kt @@ -0,0 +1,71 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.call.usecase + +import com.wire.kalium.calling.ConversationTypeCalling +import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.data.call.ConversationTypeForCall +import com.wire.kalium.logic.data.call.mapper.CallMapper +import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.logic.functional.map + +/** + * This class is responsible for providing the conversation type for a call. + */ +interface GetCallConversationTypeProvider { + suspend operator fun invoke(conversationId: ConversationId): ConversationTypeCalling +} + +internal class GetCallConversationTypeProviderImpl( + private val userConfigRepository: UserConfigRepository, + private val conversationRepository: ConversationRepository, + private val callMapper: CallMapper, +) : GetCallConversationTypeProvider { + override suspend fun invoke(conversationId: ConversationId): ConversationTypeCalling { + + val type = conversationRepository.getConversationTypeById(conversationId).fold( + { ConversationTypeForCall.Unknown }, + { + conversationRepository.getConversationProtocolInfo(conversationId).fold({ + ConversationTypeForCall.Unknown + }, { protocol -> + callMapper.fromConversationTypeToConversationTypeForCall(it, protocol) + }) + } + ) + + val callConversationType = callMapper.toConversationTypeCalling(type) + + return userConfigRepository.shouldUseSFTForOneOnOneCalls().fold({ + callConversationType + }, { shouldUseSFTForOneOnOneCalls -> + if (shouldUseSFTForOneOnOneCalls) { + userConfigRepository.isMLSEnabled().map { + if (it) { + return@fold ConversationTypeCalling.ConferenceMls + } + } + ConversationTypeCalling.Conference + } else { + callConversationType + } + }) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/StartCallUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/StartCallUseCase.kt index 6a1c1dff931..b7b7e353acf 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/StartCallUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/StartCallUseCase.kt @@ -36,11 +36,13 @@ import kotlinx.coroutines.withContext * Will wait for sync to finish or fail if it is pending, * and return one [Result]. */ +@Suppress("LongParameterList") class StartCallUseCase internal constructor( private val callManager: Lazy, private val syncManager: SyncManager, private val kaliumConfigs: KaliumConfigs, private val callRepository: CallRepository, + private val getCallConversationType: GetCallConversationTypeProvider, private val answerCall: AnswerCallUseCase, private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl ) { @@ -60,9 +62,13 @@ class StartCallUseCase internal constructor( return@withContext Result.Success } } + + val callConversationType = getCallConversationType(conversationId) + callManager.value.startCall( conversationId = conversationId, callType = callType, + conversationTypeCalling = callConversationType, isAudioCbr = kaliumConfigs.forceConstantBitrateCalls ) return@withContext Result.Success diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallMapperTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallMapperTest.kt index e6cc4393a24..d0fbdc0345e 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallMapperTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallMapperTest.kt @@ -31,6 +31,7 @@ import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl import com.wire.kalium.logic.data.message.MessageTarget import com.wire.kalium.logic.framework.TestCall import com.wire.kalium.persistence.dao.call.CallEntity +import com.wire.kalium.persistence.dao.conversation.ConversationEntity import kotlinx.coroutines.test.runTest import kotlin.test.BeforeTest import kotlin.test.Test @@ -59,15 +60,24 @@ class CallMapperTest { @Test fun whenMappingToConversationTypeCalling_withConversationType_thenReturnConversationTypeCalling() = runTest { - val oneOnOneMap = callMapper.toConversationTypeCalling(conversationType = ConversationType.OneOnOne) - val conferenceMap = callMapper.toConversationTypeCalling(conversationType = ConversationType.Conference) - val unknown = callMapper.toConversationTypeCalling(conversationType = ConversationType.Unknown) + val oneOnOneMap = callMapper.toConversationTypeCalling(conversationTypeForCall = ConversationTypeForCall.OneOnOne) + val conferenceMap = callMapper.toConversationTypeCalling(conversationTypeForCall = ConversationTypeForCall.Conference) + val unknown = callMapper.toConversationTypeCalling(conversationTypeForCall = ConversationTypeForCall.Unknown) assertEquals(ConversationTypeCalling.OneOnOne, oneOnOneMap) assertEquals(ConversationTypeCalling.Conference, conferenceMap) assertEquals(ConversationTypeCalling.Unknown, unknown) } + @Test + fun givenCallMapper_whenMappingToConversationType_thenReturnConversationType() = runTest { + val oneOnOneMap = callMapper.toConversationType(conversationType = ConversationEntity.Type.ONE_ON_ONE) + val conferenceMap = callMapper.toConversationType(conversationType = ConversationEntity.Type.GROUP) + + assertEquals(Conversation.Type.ONE_ON_ONE, oneOnOneMap) + assertEquals(Conversation.Type.GROUP, conferenceMap) + } + @Test fun givenVideoStates_whenMappingWithToVideoStateCalling_thenReturnsTheCorrespondentValues() = runTest { val stopped = callMapper.toVideoStateCalling(videoState = VideoState.STOPPED) @@ -87,7 +97,7 @@ class CallMapperTest { @Test fun given0AsAConversationTypeInputValue_whenMappingToConversationType_ThenReturnOneOnOneType() { - val expected = ConversationType.OneOnOne + val expected = ConversationTypeForCall.OneOnOne val actual = callMapper.fromIntToConversationType(0) @@ -96,7 +106,7 @@ class CallMapperTest { @Test fun given2AsAConversationTypeInputValue_whenMappingToConversationType_ThenReturnConferenceType() { - val expected = ConversationType.Conference + val expected = ConversationTypeForCall.Conference val actual = callMapper.fromIntToConversationType(2) @@ -105,7 +115,7 @@ class CallMapperTest { @Test fun givenADifferentAConversationTypeInputValue_whenMappingToConversationType_ThenReturnConferenceType() { - val expected = ConversationType.Unknown + val expected = ConversationTypeForCall.Unknown val actual = callMapper.fromIntToConversationType(4) @@ -124,7 +134,7 @@ class CallMapperTest { status = CallStatus.ESTABLISHED, conversationType = Conversation.Type.ONE_ON_ONE, callerId = TestCall.CALLER_ID, - type = ConversationType.OneOnOne + type = ConversationTypeForCall.OneOnOne ) // then diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallRepositoryTest.kt index 3a913755b4d..d2ad21ee05c 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallRepositoryTest.kt @@ -197,7 +197,7 @@ class CallRepositoryTest { isMuted = true, isCameraOn = false, isCbrEnabled = false, - type = ConversationType.Conference + type = ConversationTypeForCall.Conference ) // then @@ -247,7 +247,7 @@ class CallRepositoryTest { isMuted = true, isCameraOn = false, isCbrEnabled = false, - type = ConversationType.Conference + type = ConversationTypeForCall.Conference ) // then @@ -293,7 +293,7 @@ class CallRepositoryTest { isMuted = true, isCameraOn = false, isCbrEnabled = false, - type = ConversationType.Conference + type = ConversationTypeForCall.Conference ) // then @@ -348,7 +348,7 @@ class CallRepositoryTest { isMuted = true, isCameraOn = false, isCbrEnabled = false, - type = ConversationType.Conference + type = ConversationTypeForCall.Conference ) // then @@ -397,7 +397,7 @@ class CallRepositoryTest { isMuted = true, isCameraOn = false, isCbrEnabled = false, - type = ConversationType.Conference + type = ConversationTypeForCall.Conference ) // then @@ -432,7 +432,7 @@ class CallRepositoryTest { isMuted = true, isCameraOn = false, isCbrEnabled = false, - type = ConversationType.OneOnOne + type = ConversationTypeForCall.OneOnOne ) // then @@ -471,7 +471,7 @@ class CallRepositoryTest { isMuted = true, isCameraOn = false, isCbrEnabled = false, - type = ConversationType.OneOnOne + type = ConversationTypeForCall.OneOnOne ) // then @@ -510,7 +510,7 @@ class CallRepositoryTest { isMuted = true, isCameraOn = false, isCbrEnabled = false, - type = ConversationType.OneOnOne + type = ConversationTypeForCall.OneOnOne ) verify(arrangement.persistMessage) @@ -557,7 +557,7 @@ class CallRepositoryTest { isMuted = true, isCameraOn = false, isCbrEnabled = false, - type = ConversationType.OneOnOne + type = ConversationTypeForCall.OneOnOne ) // then @@ -595,7 +595,7 @@ class CallRepositoryTest { isMuted = true, isCameraOn = false, isCbrEnabled = false, - type = ConversationType.OneOnOne + type = ConversationTypeForCall.OneOnOne ) // then diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/GetCallConversationTypeProviderTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/GetCallConversationTypeProviderTest.kt new file mode 100644 index 00000000000..068f0a21787 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/GetCallConversationTypeProviderTest.kt @@ -0,0 +1,259 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.call.usecase + +import com.wire.kalium.calling.ConversationTypeCalling +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.data.call.ConversationTypeForCall +import com.wire.kalium.logic.data.call.mapper.CallMapper +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.GroupID +import com.wire.kalium.logic.data.mls.CipherSuite +import com.wire.kalium.logic.framework.TestConversation +import com.wire.kalium.logic.functional.Either +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.eq +import io.mockative.every +import io.mockative.mock +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Clock +import kotlin.test.Test +import kotlin.test.assertEquals + +class GetCallConversationTypeProviderTest { + + @Test + fun givenShouldUseSFTForOneOnOneCallsAndMLSEnabled_whenRunningUseCase_thenReturnConferenceMls() = + runTest { + val conversationId = TestConversation.ID + val groupId = GroupID("groupid") + + val (arrangement, getCallConversationType) = Arrangement() + .withGetConversationTypeByIdSuccess(conversationId, Conversation.Type.ONE_ON_ONE) + .withGetConversationProtocolInfoSuccess( + conversationId, + Conversation.ProtocolInfo.MLS( + groupId, + Conversation.ProtocolInfo.MLSCapable.GroupState.ESTABLISHED, + 1UL, + Clock.System.now(), + CipherSuite.MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + ) + ) + .withMlsConferenceCallMapping() + .withShouldUseSFTForOneOnOneCalls() + .withMLSEnabled() + .arrange() + + val result = getCallConversationType.invoke(conversationId) + + assertEquals(ConversationTypeCalling.ConferenceMls, result) + } + + @Test + fun givenShouldUseSFTForOneOnOneCallsAndMLSDisabled_whenRunningUseCase_thenReturnConference() = + runTest { + val conversationId = TestConversation.ID + + val (_, getCallConversationType) = Arrangement() + .withGetConversationTypeByIdSuccess(conversationId, Conversation.Type.GROUP) + .withGetConversationProtocolInfoSuccess( + conversationId, + Conversation.ProtocolInfo.Proteus + ) + .withConferenceCallMapping() + .withShouldUseSFTForOneOnOneCalls() + .withMLSDisabled() + .arrange() + + val result = getCallConversationType(conversationId) + + assertEquals(ConversationTypeCalling.Conference, result) + } + + @Test + fun givenShouldNotUseSFTForOneOnOneCallsAndOneOnOneConversation_whenRunningUseCase_thenReturnOneOnOneType() = + runTest { + val conversationId = TestConversation.ID + + val (_, getCallConversationType) = Arrangement() + .withShouldNotUseSFTForOneOnOneCalls() + .withGetConversationTypeByIdSuccess(conversationId, Conversation.Type.ONE_ON_ONE) + .withGetConversationProtocolInfoSuccess( + conversationId, + Conversation.ProtocolInfo.Proteus + ) + .withOneOnOneCallMapping() + .arrange() + + val result = getCallConversationType.invoke(conversationId) + + assertEquals(ConversationTypeCalling.OneOnOne, result) + } + + @Test + fun givenUserConfigRepositoryReturnsFailure_whenRunningUseCase_thenReturnConversationType() = + runTest { + val conversationId = TestConversation.ID + + val (_, getCallConversationType) = Arrangement() + .withShouldUseSFTForOneOnOneCallsFailure() + .withGetConversationTypeByIdSuccess(conversationId, Conversation.Type.GROUP) + .withGetConversationProtocolInfoSuccess( + conversationId, + Conversation.ProtocolInfo.Proteus + ) + .withConferenceCallMapping() + .arrange() + + val result = getCallConversationType.invoke(conversationId) + + assertEquals(ConversationTypeCalling.Conference, result) + } + + @Test + fun givenShouldNotUseSFTAndConversationRepositoryFailure_whenRunningUseCase_thenReturnUnknown() = + runTest { + val conversationId = TestConversation.ID + + val (_, getCallConversationType) = Arrangement() + .withShouldNotUseSFTForOneOnOneCalls() + .withGetConversationTypeByIdFailure(conversationId) + .withUnknownCallMapping() + .arrange() + + val result = getCallConversationType.invoke(conversationId) + + assertEquals(ConversationTypeCalling.Unknown, result) + } + + private class Arrangement { + + @Mock + val userConfigRepository = mock(UserConfigRepository::class) + + @Mock + val conversationRepository = mock(ConversationRepository::class) + + @Mock + val callMapper = mock(CallMapper::class) + + private val getCallConversationType = GetCallConversationTypeProviderImpl( + userConfigRepository = userConfigRepository, + conversationRepository = conversationRepository, + callMapper = callMapper + ) + + fun arrange() = this to getCallConversationType + + fun withMLSEnabled() = apply { + every { + userConfigRepository.isMLSEnabled() + }.returns(Either.Right(true)) + } + + fun withMLSDisabled() = apply { + every { + userConfigRepository.isMLSEnabled() + }.returns(Either.Right(false)) + } + + fun withShouldUseSFTForOneOnOneCalls() = apply { + every { + userConfigRepository.shouldUseSFTForOneOnOneCalls() + }.returns(Either.Right(true)) + } + + fun withShouldNotUseSFTForOneOnOneCalls() = apply { + every { + userConfigRepository.shouldUseSFTForOneOnOneCalls() + }.returns(Either.Right(false)) + } + + fun withShouldUseSFTForOneOnOneCallsFailure() = apply { + every { + userConfigRepository.shouldUseSFTForOneOnOneCalls() + }.returns(Either.Left(StorageFailure.DataNotFound)) + } + + suspend fun withGetConversationTypeByIdSuccess( + conversationId: ConversationId, + result: Conversation.Type + ) = apply { + coEvery { + conversationRepository.getConversationTypeById(eq(conversationId)) + }.returns(Either.Right(result)) + } + + suspend fun withGetConversationTypeByIdFailure(conversationId: ConversationId) = apply { + coEvery { + conversationRepository.getConversationTypeById(eq(conversationId)) + }.returns(Either.Left(StorageFailure.DataNotFound)) + } + + suspend fun withGetConversationProtocolInfoSuccess( + conversationId: ConversationId, + protocolResult: Conversation.ProtocolInfo + ) = apply { + coEvery { + conversationRepository.getConversationProtocolInfo(eq(conversationId)) + }.returns(Either.Right(protocolResult)) + } + + fun withOneOnOneCallMapping() = apply { + every { + callMapper.fromConversationTypeToConversationTypeForCall(any(), any()) + }.returns(ConversationTypeForCall.OneOnOne) + + every { + callMapper.toConversationTypeCalling(any()) + }.returns(ConversationTypeCalling.OneOnOne) + } + + fun withConferenceCallMapping() = apply { + every { + callMapper.fromConversationTypeToConversationTypeForCall(any(), any()) + }.returns(ConversationTypeForCall.Conference) + + every { + callMapper.toConversationTypeCalling(any()) + }.returns(ConversationTypeCalling.Conference) + } + + fun withMlsConferenceCallMapping() = apply { + every { + callMapper.fromConversationTypeToConversationTypeForCall(any(), any()) + }.returns(ConversationTypeForCall.ConferenceMls) + + every { + callMapper.toConversationTypeCalling(any()) + }.returns(ConversationTypeCalling.ConferenceMls) + } + + fun withUnknownCallMapping() = apply { + every { + callMapper.toConversationTypeCalling(eq(ConversationTypeForCall.Unknown)) + }.returns(ConversationTypeCalling.Unknown) + } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/StartCallUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/StartCallUseCaseTest.kt index 1bc5bc3c2e3..c341b82417f 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/StartCallUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/StartCallUseCaseTest.kt @@ -18,6 +18,7 @@ package com.wire.kalium.logic.feature.call.usecase +import com.wire.kalium.calling.ConversationTypeCalling import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.NetworkFailure import com.wire.kalium.logic.data.call.CallRepository @@ -52,6 +53,7 @@ class StartCallUseCaseTest { val (arrangement, startCall) = Arrangement() .withWaitingForSyncSucceeding() .withAnIncomingCall() + .withCallConversationTypeUseCaseReturning(ConversationTypeCalling.Conference) .arrange() startCall.invoke(conversationId) @@ -74,6 +76,7 @@ class StartCallUseCaseTest { val (arrangement, startCall) = Arrangement() .withWaitingForSyncSucceeding() .withNoIncomingCall() + .withCallConversationTypeUseCaseReturning(ConversationTypeCalling.Conference) .arrange() startCall.invoke(conversationId, CallType.AUDIO) @@ -96,6 +99,7 @@ class StartCallUseCaseTest { val (arrangement, startCall) = Arrangement() .withWaitingForSyncSucceeding() .withNoIncomingCall() + .withCallConversationTypeUseCaseReturning(ConversationTypeCalling.Conference) .arrange() val result = startCall.invoke(conversationId, CallType.AUDIO) @@ -113,6 +117,7 @@ class StartCallUseCaseTest { val (arrangement, startCall) = Arrangement() .withWaitingForSyncFailing() + .withCallConversationTypeUseCaseReturning(ConversationTypeCalling.OneOnOne) .arrange() startCall.invoke(conversationId, CallType.AUDIO) @@ -143,6 +148,7 @@ class StartCallUseCaseTest { val (arrangement, startCall) = Arrangement() .withWaitingForSyncSucceeding() .withNoIncomingCall() + .withCallConversationTypeUseCaseReturning(ConversationTypeCalling.Conference) .arrangeWithCBR() startCall.invoke(conversationId, CallType.AUDIO) @@ -168,13 +174,16 @@ class StartCallUseCaseTest { @Mock val answerCall = mock(classOf()) + @Mock + val getCallConversationType = mock(GetCallConversationTypeProvider::class) + @Mock val callRepository = mock(classOf()) private val kaliumConfigs = KaliumConfigs() private val startCallUseCase = StartCallUseCase( - lazy { callManager }, syncManager, kaliumConfigs, callRepository, answerCall + lazy { callManager }, syncManager, kaliumConfigs, callRepository, getCallConversationType, answerCall ) private val startCallUseCaseWithCBR = StartCallUseCase( @@ -182,7 +191,8 @@ class StartCallUseCaseTest { syncManager, KaliumConfigs(forceConstantBitrateCalls = true), callRepository, - answerCall + answerCall, + getCallConversationType ) fun withWaitingForSyncSucceeding() = withSyncReturning(Either.Right(Unit)) @@ -195,6 +205,16 @@ class StartCallUseCaseTest { } } + suspend fun withCallConversationTypeUseCaseReturning(result: ConversationTypeCalling) = apply { + coEvery { + getCallConversationType.invoke(any()) + }.returns(result) + given(getCallConversationType) + .suspendFunction(getCallConversationType::invoke) + .whenInvoked() + .then { result } + } + fun withNoIncomingCall() = apply { given(callRepository) .suspendFunction(callRepository::incomingCallsFlow) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/featureConfigs/FeatureConfigResponse.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/featureConfigs/FeatureConfigResponse.kt index 0f211ab1d21..5a7864fd622 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/featureConfigs/FeatureConfigResponse.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/featureConfigs/FeatureConfigResponse.kt @@ -163,7 +163,7 @@ sealed class FeatureConfigData { @SerialName("status") val status: FeatureFlagStatusDTO, @SerialName("config") - val config: ConferenceCallingConfigDTO + val config: ConferenceCallingConfigDTO? // TODO make it non optional when the minimum supported API is v6 ) : FeatureConfigData() @SerialName("conversationGuestLinks") diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Conversations.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Conversations.sq index 368b2ab68a0..13f52b4ddab 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Conversations.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Conversations.sq @@ -331,6 +331,9 @@ SELECT qualified_id FROM Conversation WHERE mls_group_id = ?; selectConversationIds: SELECT qualified_id FROM Conversation WHERE protocol = :protocol AND type = :type AND (:teamId IS NULL OR team_id = :teamId); +getConversationTypeById: +SELECT type FROM Conversation WHERE qualified_id = ?; + updateConversationMutingStatus: UPDATE Conversation SET muted_status = ?, muted_time = ? diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt index 70d0a989b6a..6496b54ab87 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt @@ -53,6 +53,7 @@ interface ConversationDAO { protocol: ConversationEntity.Protocol, teamId: String? = null ): List + suspend fun getConversationTypeById(conversationId: QualifiedIDEntity): ConversationEntity.Type? suspend fun getTeamConversationIdsReadyToCompleteMigration(teamId: String): List suspend fun observeGetConversationByQualifiedID(qualifiedID: QualifiedIDEntity): Flow diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt index 7dd972f6c4c..f711e90a8b0 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt @@ -190,6 +190,11 @@ internal class ConversationDAOImpl internal constructor( } } + override suspend fun getConversationTypeById(conversationId: QualifiedIDEntity): ConversationEntity.Type? = + withContext(coroutineContext) { + conversationQueries.getConversationTypeById(conversationId).executeAsOneOrNull() + } + override suspend fun getTeamConversationIdsReadyToCompleteMigration(teamId: String): List { return withContext(coroutineContext) { conversationQueries.selectAllTeamProteusConversationsReadyForMigration(teamId) From e5a09380de95ee3cf2da27d31ae37e0395f4f2c9 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Fri, 26 Jul 2024 12:37:44 +0200 Subject: [PATCH 2/9] chore: unit test --- .../kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt index 507397c67db..ada0a5a6b68 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt @@ -103,7 +103,6 @@ class CallManagerImpl internal constructor( private val qualifiedIdMapper: QualifiedIdMapper, private val videoStateChecker: VideoStateChecker, private val conversationClientsInCallUpdater: ConversationClientsInCallUpdater, - private val networkStateObserver: NetworkStateObserver, private val getCallConversationType: GetCallConversationTypeProvider, private val kaliumConfigs: KaliumConfigs, private val json: Json = Json { ignoreUnknownKeys = true }, From 3579907a0ece33e0b0f4555382cdb6344dcd5b1c Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Fri, 26 Jul 2024 12:47:58 +0200 Subject: [PATCH 3/9] chore: resolve conflicts --- .../kalium/logic/data/conversation/ConversationMapper.kt | 2 ++ .../logic/data/conversation/ConversationRepository.kt | 8 +++++++- .../com/wire/kalium/logic/feature/call/CallsScope.kt | 1 - .../wire/kalium/logic/feature/call/GlobalCallManager.kt | 1 - 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt index 10b74a8ee4a..d77339e5b2e 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt @@ -437,6 +437,8 @@ internal class ConversationMapperImpl( } } + override fun fromConversationEntityType(type: ConversationEntity.Type): Conversation.Type = type.fromDaoModelToType() + override fun verificationStatusFromEntity(verificationStatus: ConversationEntity.VerificationStatus) = Conversation.VerificationStatus.valueOf(verificationStatus.name) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt index e2a871c06fb..16389449749 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt @@ -134,7 +134,6 @@ interface ConversationRepository { suspend fun observeById(conversationId: ConversationId): Flow> suspend fun getConversationById(conversationId: ConversationId): Conversation? suspend fun getConversationTypeById(conversationId: ConversationId): Either - suspend fun observeCacheDetailsById(conversationId: ConversationId): Either> suspend fun detailsById(conversationId: ConversationId): Either suspend fun baseInfoById(conversationId: ConversationId): Either suspend fun getConversationRecipients(conversationId: ConversationId): Either> @@ -1102,6 +1101,13 @@ internal class ConversationDataSource internal constructor( conversationDAO.selectGroupStatusMembersNamesAndHandles(groupID.value) }.map { EpochChangesData.fromEntity(it) } + override suspend fun getConversationTypeById(conversationId: ConversationId): Either = + wrapStorageRequest { + conversationDAO.getConversationTypeById(conversationId.toDao())?.let { + conversationMapper.fromConversationEntityType(it) + } + } + companion object { const val DEFAULT_MEMBER_ROLE = "wire_member" } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallsScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallsScope.kt index 3b99a5cf29c..17e333eccc3 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallsScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallsScope.kt @@ -34,7 +34,6 @@ import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdat import com.wire.kalium.logic.feature.call.usecase.EndCallResultListenerImpl import com.wire.kalium.logic.feature.call.usecase.EndCallOnConversationChangeUseCase import com.wire.kalium.logic.feature.call.usecase.EndCallOnConversationChangeUseCaseImpl -import com.wire.kalium.logic.feature.call.usecase.EndCallResultListenerImpl import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.EndCallUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.FlipToBackCameraUseCase diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt index 9fcd81de371..5bc154fd0f7 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt @@ -51,7 +51,6 @@ expect class GlobalCallManager { videoStateChecker: VideoStateChecker, conversationClientsInCallUpdater: ConversationClientsInCallUpdater, getCallConversationType: GetCallConversationTypeProvider, - networkStateObserver: NetworkStateObserver, kaliumConfigs: KaliumConfigs ): CallManager From cae76ff0f635e5b79fee61597d400f74e141c3ee Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Fri, 26 Jul 2024 14:52:41 +0200 Subject: [PATCH 4/9] chore: unit test --- .../call/scenario/OnIncomingCallTest.kt | 1 + .../GetCallConversationTypeProviderTest.kt | 115 ++++++++++-------- .../call/usecase/StartCallUseCaseTest.kt | 7 +- 3 files changed, 68 insertions(+), 55 deletions(-) diff --git a/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCallTest.kt b/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCallTest.kt index 964694f3a20..81af7a19d20 100644 --- a/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCallTest.kt +++ b/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCallTest.kt @@ -31,6 +31,7 @@ import com.wire.kalium.logic.framework.TestUser import io.mockative.Mock import io.mockative.configure import io.mockative.eq +import io.mockative.verify import io.mockative.mock import io.mockative.once import kotlinx.coroutines.test.TestScope diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/GetCallConversationTypeProviderTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/GetCallConversationTypeProviderTest.kt index 068f0a21787..22e782d4b30 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/GetCallConversationTypeProviderTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/GetCallConversationTypeProviderTest.kt @@ -31,9 +31,8 @@ import com.wire.kalium.logic.framework.TestConversation import com.wire.kalium.logic.functional.Either import io.mockative.Mock import io.mockative.any -import io.mockative.coEvery import io.mockative.eq -import io.mockative.every +import io.mockative.given import io.mockative.mock import kotlinx.coroutines.test.runTest import kotlinx.datetime.Clock @@ -167,93 +166,109 @@ class GetCallConversationTypeProviderTest { fun arrange() = this to getCallConversationType fun withMLSEnabled() = apply { - every { - userConfigRepository.isMLSEnabled() - }.returns(Either.Right(true)) + given(userConfigRepository) + .function(userConfigRepository::isMLSEnabled) + .whenInvoked() + .thenReturn(Either.Right(true)) } fun withMLSDisabled() = apply { - every { - userConfigRepository.isMLSEnabled() - }.returns(Either.Right(false)) + given(userConfigRepository) + .function(userConfigRepository::isMLSEnabled) + .whenInvoked() + .thenReturn(Either.Right(false)) } fun withShouldUseSFTForOneOnOneCalls() = apply { - every { - userConfigRepository.shouldUseSFTForOneOnOneCalls() - }.returns(Either.Right(true)) + given(userConfigRepository) + .function(userConfigRepository::shouldUseSFTForOneOnOneCalls) + .whenInvoked() + .thenReturn(Either.Right(true)) } fun withShouldNotUseSFTForOneOnOneCalls() = apply { - every { - userConfigRepository.shouldUseSFTForOneOnOneCalls() - }.returns(Either.Right(false)) + given(userConfigRepository) + .function(userConfigRepository::shouldUseSFTForOneOnOneCalls) + .whenInvoked() + .thenReturn(Either.Right(false)) } fun withShouldUseSFTForOneOnOneCallsFailure() = apply { - every { - userConfigRepository.shouldUseSFTForOneOnOneCalls() - }.returns(Either.Left(StorageFailure.DataNotFound)) + given(userConfigRepository) + .function(userConfigRepository::shouldUseSFTForOneOnOneCalls) + .whenInvoked() + .thenReturn(Either.Left(StorageFailure.DataNotFound)) } suspend fun withGetConversationTypeByIdSuccess( conversationId: ConversationId, result: Conversation.Type ) = apply { - coEvery { - conversationRepository.getConversationTypeById(eq(conversationId)) - }.returns(Either.Right(result)) + given(conversationRepository) + .suspendFunction(conversationRepository::getConversationTypeById) + .whenInvokedWith(any()) + .thenReturn(Either.Right(result)) } suspend fun withGetConversationTypeByIdFailure(conversationId: ConversationId) = apply { - coEvery { - conversationRepository.getConversationTypeById(eq(conversationId)) - }.returns(Either.Left(StorageFailure.DataNotFound)) + given(conversationRepository) + .suspendFunction(conversationRepository::getConversationTypeById) + .whenInvokedWith(any()) + .thenReturn(Either.Left(StorageFailure.DataNotFound)) } suspend fun withGetConversationProtocolInfoSuccess( conversationId: ConversationId, protocolResult: Conversation.ProtocolInfo ) = apply { - coEvery { - conversationRepository.getConversationProtocolInfo(eq(conversationId)) - }.returns(Either.Right(protocolResult)) + given(conversationRepository) + .suspendFunction(conversationRepository::getConversationProtocolInfo) + .whenInvokedWith(any()) + .thenReturn(Either.Right(protocolResult)) } fun withOneOnOneCallMapping() = apply { - every { - callMapper.fromConversationTypeToConversationTypeForCall(any(), any()) - }.returns(ConversationTypeForCall.OneOnOne) - - every { - callMapper.toConversationTypeCalling(any()) - }.returns(ConversationTypeCalling.OneOnOne) + given(callMapper) + .function(callMapper::fromConversationTypeToConversationTypeForCall) + .whenInvokedWith(any(), any()) + .thenReturn(ConversationTypeForCall.OneOnOne) + given(callMapper) + .function(callMapper::toConversationTypeCalling) + .whenInvokedWith(any()) + .thenReturn(ConversationTypeCalling.OneOnOne) } fun withConferenceCallMapping() = apply { - every { - callMapper.fromConversationTypeToConversationTypeForCall(any(), any()) - }.returns(ConversationTypeForCall.Conference) - - every { - callMapper.toConversationTypeCalling(any()) - }.returns(ConversationTypeCalling.Conference) + given(callMapper) + .function(callMapper::fromConversationTypeToConversationTypeForCall) + .whenInvokedWith(any(), any()) + .thenReturn(ConversationTypeForCall.Conference) + + given(callMapper) + .function(callMapper::toConversationTypeCalling) + .whenInvokedWith(any()) + .thenReturn(ConversationTypeCalling.Conference) } fun withMlsConferenceCallMapping() = apply { - every { - callMapper.fromConversationTypeToConversationTypeForCall(any(), any()) - }.returns(ConversationTypeForCall.ConferenceMls) - - every { - callMapper.toConversationTypeCalling(any()) - }.returns(ConversationTypeCalling.ConferenceMls) + given(callMapper) + .function(callMapper::fromConversationTypeToConversationTypeForCall) + .whenInvokedWith(any(), any()) + .thenReturn(ConversationTypeForCall.ConferenceMls) + + given(callMapper) + .function(callMapper::toConversationTypeCalling) + .whenInvokedWith(any()) + .thenReturn(ConversationTypeCalling.ConferenceMls) } fun withUnknownCallMapping() = apply { - every { - callMapper.toConversationTypeCalling(eq(ConversationTypeForCall.Unknown)) - }.returns(ConversationTypeCalling.Unknown) + given(callMapper) + .function(callMapper::toConversationTypeCalling) + .whenInvokedWith(eq(ConversationTypeForCall.Unknown)) + .then { + ConversationTypeCalling.Unknown + } } } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/StartCallUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/StartCallUseCaseTest.kt index c341b82417f..fb253944213 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/StartCallUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/StartCallUseCaseTest.kt @@ -191,8 +191,8 @@ class StartCallUseCaseTest { syncManager, KaliumConfigs(forceConstantBitrateCalls = true), callRepository, + getCallConversationType, answerCall, - getCallConversationType ) fun withWaitingForSyncSucceeding() = withSyncReturning(Either.Right(Unit)) @@ -206,12 +206,9 @@ class StartCallUseCaseTest { } suspend fun withCallConversationTypeUseCaseReturning(result: ConversationTypeCalling) = apply { - coEvery { - getCallConversationType.invoke(any()) - }.returns(result) given(getCallConversationType) .suspendFunction(getCallConversationType::invoke) - .whenInvoked() + .whenInvokedWith(any()) .then { result } } From 50959d7e9f2b6db47aeebf1d60ac8e80dcc2b0d7 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Fri, 26 Jul 2024 14:59:38 +0200 Subject: [PATCH 5/9] chore: unit test --- .../kalium/logic/feature/call/usecase/StartCallUseCaseTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/StartCallUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/StartCallUseCaseTest.kt index fb253944213..1d39d96fff8 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/StartCallUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/StartCallUseCaseTest.kt @@ -83,7 +83,7 @@ class StartCallUseCaseTest { verify(arrangement.callManager) .suspendFunction(arrangement.callManager::startCall) - .with(eq(conversationId), eq(CallType.AUDIO), eq(false)) + .with(eq(conversationId), eq(CallType.AUDIO), eq(ConversationTypeCalling.Conference), eq(false)) .wasInvoked(once) verify(arrangement.answerCall) @@ -155,7 +155,7 @@ class StartCallUseCaseTest { verify(arrangement.callManager) .suspendFunction(arrangement.callManager::startCall) - .with(eq(conversationId), eq(CallType.AUDIO), eq(true)) + .with(eq(conversationId), eq(CallType.AUDIO), eq(ConversationTypeCalling.Conference), eq(true)) .wasInvoked(once) verify(arrangement.answerCall) .suspendFunction(arrangement.answerCall::invoke) From fab9f7ec05472cf093bce65f654f4b182230a2e3 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 1 Aug 2024 10:14:09 +0200 Subject: [PATCH 6/9] feat: terminate the SFT OneOnOneCall once the other person hangup the call --- gradle/libs.versions.toml | 2 +- .../logic/feature/call/GlobalCallManager.kt | 4 + .../logic/feature/call/CallManagerImpl.kt | 23 +- .../logic/feature/call/GlobalCallManager.kt | 6 + .../feature/call/scenario/OnCloseCall.kt | 15 +- .../call/scenario/OnParticipantListChanged.kt | 23 ++ .../kalium/logic/data/call/CallRepository.kt | 18 +- .../kalium/logic/data/call/MLSCallHelper.kt | 123 ++++++++ .../JoinSubconversationUseCase.kt | 1 + .../conversation/SubconversationRepository.kt | 101 ++++++- .../data/featureConfig/FeatureConfigMapper.kt | 2 +- .../kalium/logic/feature/UserSessionScope.kt | 5 +- .../logic/feature/call/GlobalCallManager.kt | 4 + .../logic/data/call/MLSCallHelperTest.kt | 268 ++++++++++++++++++ .../logic/feature/scenario/OnCloseCallTest.kt | 12 +- 15 files changed, 582 insertions(+), 25 deletions(-) create mode 100644 logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/MLSCallHelper.kt create mode 100644 logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/MLSCallHelperTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 25b0685c396..dee83e7d406 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,7 +35,7 @@ sqldelight = "2.0.1" sqlcipher-android = "4.5.5" pbandk = "0.14.2" turbine = "1.0.0" -avs = "9.6.13" +avs = "9.6.15" jna = "5.14.0" core-crypto = "1.0.0-rc.56-hotfix.2" core-crypto-multiplatform = "0.6.0-rc.3-multiplatform-pre1" diff --git a/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt b/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt index ac3af705113..9726be6fe9c 100644 --- a/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt +++ b/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt @@ -19,10 +19,12 @@ package com.wire.kalium.logic.feature.call import com.wire.kalium.logic.cache.SelfConversationIdProvider +import com.wire.kalium.logic.configuration.UserConfigRepository import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.call.VideoStateChecker import com.wire.kalium.logic.data.call.mapper.CallMapper import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.conversation.SubconversationRepository import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.id.FederatedIdMapper import com.wire.kalium.logic.data.id.QualifiedID @@ -43,6 +45,8 @@ actual class GlobalCallManager { currentClientIdProvider: CurrentClientIdProvider, selfConversationIdProvider: SelfConversationIdProvider, conversationRepository: ConversationRepository, + subonversationRepository: SubconversationRepository, + userConfigRepository: UserConfigRepository, messageSender: MessageSender, callMapper: CallMapper, federatedIdMapper: FederatedIdMapper, diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt index ada0a5a6b68..b2c771f4050 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt @@ -30,12 +30,14 @@ import com.wire.kalium.calling.types.Uint32_t import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.cache.SelfConversationIdProvider import com.wire.kalium.logic.callingLogger +import com.wire.kalium.logic.configuration.UserConfigRepository import com.wire.kalium.logic.data.call.CallClient import com.wire.kalium.logic.data.call.CallClientList import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.call.CallStatus import com.wire.kalium.logic.data.call.CallType import com.wire.kalium.logic.data.call.EpochInfo +import com.wire.kalium.logic.data.call.MLSCallHelperImpl import com.wire.kalium.logic.data.call.VideoState import com.wire.kalium.logic.data.call.VideoStateChecker import com.wire.kalium.logic.data.call.mapper.CallMapper @@ -43,6 +45,7 @@ import com.wire.kalium.logic.data.call.mapper.ParticipantMapperImpl import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.conversation.SubconversationRepository import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.data.id.FederatedIdMapper @@ -104,6 +107,8 @@ class CallManagerImpl internal constructor( private val videoStateChecker: VideoStateChecker, private val conversationClientsInCallUpdater: ConversationClientsInCallUpdater, private val getCallConversationType: GetCallConversationTypeProvider, + private val subconversationRepository: SubconversationRepository, + private val userConfigRepository: UserConfigRepository, private val kaliumConfigs: KaliumConfigs, private val json: Json = Json { ignoreUnknownKeys = true }, private val shouldRemoteMuteChecker: ShouldRemoteMuteChecker = ShouldRemoteMuteCheckerImpl(), @@ -186,8 +191,16 @@ class CallManagerImpl internal constructor( .keepingStrongReference(), establishedCallHandler = OnEstablishedCall(callRepository, scope, qualifiedIdMapper) .keepingStrongReference(), - closeCallHandler = OnCloseCall(callRepository, scope, qualifiedIdMapper) - .keepingStrongReference(), + closeCallHandler = OnCloseCall( + callRepository = callRepository, + mlsCallHelper = MLSCallHelperImpl( + callRepository = callRepository, + subconversationRepository = subconversationRepository, + userConfigRepository = userConfigRepository + ), + scope = scope, + qualifiedIdMapper = qualifiedIdMapper + ).keepingStrongReference(), metricsHandler = metricsHandler, callConfigRequestHandler = OnConfigRequest(calling, callRepository, scope) .keepingStrongReference(), @@ -452,6 +465,12 @@ class CallManagerImpl internal constructor( qualifiedIdMapper = qualifiedIdMapper, participantMapper = ParticipantMapperImpl(videoStateChecker, callMapper), userRepository = userRepository, + mlsCallHelper = MLSCallHelperImpl( + callRepository = callRepository, + subconversationRepository = subconversationRepository, + userConfigRepository = userConfigRepository + ), + endCall = { endCall(it) }, callingScope = scope ).keepingStrongReference() diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt index 090de749688..94370888b30 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt @@ -24,10 +24,12 @@ import com.wire.kalium.calling.ENVIRONMENT_DEFAULT import com.wire.kalium.calling.callbacks.LogHandler import com.wire.kalium.logic.cache.SelfConversationIdProvider import com.wire.kalium.logic.callingLogger +import com.wire.kalium.logic.configuration.UserConfigRepository import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.call.VideoStateChecker import com.wire.kalium.logic.data.call.mapper.CallMapper import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.conversation.SubconversationRepository import com.wire.kalium.logic.data.id.FederatedIdMapper import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.QualifiedIdMapper @@ -79,6 +81,8 @@ actual class GlobalCallManager( currentClientIdProvider: CurrentClientIdProvider, selfConversationIdProvider: SelfConversationIdProvider, conversationRepository: ConversationRepository, + subconversationRepository: SubconversationRepository, + userConfigRepository: UserConfigRepository, messageSender: MessageSender, callMapper: CallMapper, federatedIdMapper: FederatedIdMapper, @@ -103,6 +107,8 @@ actual class GlobalCallManager( videoStateChecker = videoStateChecker, conversationClientsInCallUpdater = conversationClientsInCallUpdater, getCallConversationType = getCallConversationType, + subconversationRepository = subconversationRepository, + userConfigRepository = userConfigRepository, kaliumConfigs = kaliumConfigs ) } diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCall.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCall.kt index c656d43cd3d..504890d00ff 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCall.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCall.kt @@ -25,16 +25,18 @@ import com.wire.kalium.calling.types.Uint32_t import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.callingLogger import com.wire.kalium.logic.data.call.CallRepository +import com.wire.kalium.logic.data.call.CallStatus +import com.wire.kalium.logic.data.call.MLSCallHelper import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedIdMapper -import com.wire.kalium.logic.data.call.CallStatus import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @Suppress("LongParameterList") class OnCloseCall( private val callRepository: CallRepository, + private val mlsCallHelper: MLSCallHelper, private val scope: CoroutineScope, private val qualifiedIdMapper: QualifiedIdMapper ) : CloseCallHandler { @@ -67,15 +69,20 @@ class OnCloseCall( status = callStatus ) + val conversationType = + callRepository.getCallMetadataProfile()[conversationIdWithDomain]?.conversationType + if (callRepository.getCallMetadataProfile()[conversationIdWithDomain]?.protocol is Conversation.ProtocolInfo.MLS) { - callRepository.leaveMlsConference(conversationIdWithDomain) + mlsCallHelper.handleCallTermination(conversationIdWithDomain, conversationType) } - callingLogger.i("[OnCloseCall] -> ConversationId: ${conversationId.obfuscateId()} | callStatus: $callStatus") } } - private fun shouldPersistMissedCall(conversationId: ConversationId, callStatus: CallStatus): Boolean { + private fun shouldPersistMissedCall( + conversationId: ConversationId, + callStatus: CallStatus + ): Boolean { if (callStatus == CallStatus.MISSED) return true return callRepository.getCallMetadataProfile().data[conversationId]?.let { diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnParticipantListChanged.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnParticipantListChanged.kt index bf48b115217..98933839485 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnParticipantListChanged.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnParticipantListChanged.kt @@ -24,22 +24,29 @@ import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.callingLogger 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.Participant 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.data.user.UserRepository import com.wire.kalium.logic.functional.onFailure import com.wire.kalium.logic.functional.onSuccess +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 userRepository: UserRepository, + private val mlsCallHelper: MLSCallHelper, + private val endCall: suspend (conversationId: ConversationId) -> Unit, private val callingScope: CoroutineScope ) : ParticipantChangedHandler { @@ -65,6 +72,22 @@ class OnParticipantListChanged internal constructor( participants.add(participant) } } + 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) + } + } callRepository.updateCallParticipants( conversationId = conversationIdWithDomain, diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt index d8f02129023..3bfcbfa8e43 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt @@ -33,6 +33,9 @@ import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.conversation.EpochChangesObserver +import com.wire.kalium.logic.data.conversation.JoinSubconversationUseCase +import com.wire.kalium.logic.data.conversation.LeaveSubconversationUseCase import com.wire.kalium.logic.data.conversation.MLSConversationRepository import com.wire.kalium.logic.data.conversation.SubconversationRepository import com.wire.kalium.logic.data.id.ConversationId @@ -50,9 +53,6 @@ import com.wire.kalium.logic.data.team.TeamRepository import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.di.MapperProvider -import com.wire.kalium.logic.data.conversation.JoinSubconversationUseCase -import com.wire.kalium.logic.data.conversation.LeaveSubconversationUseCase -import com.wire.kalium.logic.data.conversation.EpochChangesObserver import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.getOrNull @@ -121,6 +121,7 @@ interface CallRepository { fun updateParticipantsActiveSpeaker(conversationId: ConversationId, activeSpeakers: CallActiveSpeakers) suspend fun getLastClosedCallCreatedByConversationId(conversationId: ConversationId): Flow suspend fun updateOpenCallsToClosedStatus() + suspend fun leavePreviouslyJoinedMlsConferences() suspend fun persistMissedCall(conversationId: ConversationId) suspend fun joinMlsConference( conversationId: ConversationId, @@ -129,6 +130,7 @@ interface CallRepository { suspend fun leaveMlsConference(conversationId: ConversationId) suspend fun observeEpochInfo(conversationId: ConversationId): Either> suspend fun advanceEpoch(conversationId: ConversationId) + fun currentCallProtocol(conversationId: ConversationId): Conversation.ProtocolInfo? } @Suppress("LongParameterList", "TooManyFunctions") @@ -426,7 +428,9 @@ internal class CallDataSource( } } - if (_callMetadataProfile.value[conversationId]?.protocol is Conversation.ProtocolInfo.MLS) { + if (_callMetadataProfile.value[conversationId]?.protocol is Conversation.ProtocolInfo.MLS && + _callMetadataProfile.value[conversationId]?.conversationType == Conversation.Type.GROUP + ) { participants.forEach { participant -> if (participant.hasEstablishedAudio) { clearStaleParticipantTimeout(participant) @@ -502,7 +506,6 @@ internal class CallDataSource( ) override suspend fun updateOpenCallsToClosedStatus() { - leavePreviouslyJoinedMlsConferences() callDAO.updateOpenCallsToClosedStatus() } @@ -542,7 +545,7 @@ internal class CallDataSource( } } - private suspend fun leavePreviouslyJoinedMlsConferences() { + override suspend fun leavePreviouslyJoinedMlsConferences() { callingLogger.i("Leaving previously joined MLS conferences") callDAO.observeEstablishedCalls() @@ -667,6 +670,9 @@ internal class CallDataSource( } ?: callingLogger.w("[CallRepository] -> Requested new epoch but there's no conference subconversation") } + override fun currentCallProtocol(conversationId: ConversationId): Conversation.ProtocolInfo? = + _callMetadataProfile.value.data[conversationId]?.protocol + companion object { val STALE_PARTICIPANT_TIMEOUT = 190.toDuration(kotlin.time.DurationUnit.SECONDS) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/MLSCallHelper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/MLSCallHelper.kt new file mode 100644 index 00000000000..f44ba3f080c --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/MLSCallHelper.kt @@ -0,0 +1,123 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.data.call + +import com.wire.kalium.logic.callingLogger +import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.conversation.SubconversationRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.SubconversationId +import com.wire.kalium.logic.data.id.toModel +import com.wire.kalium.logic.functional.getOrElse +import com.wire.kalium.logic.functional.onFailure +import com.wire.kalium.logic.functional.onSuccess +import com.wire.kalium.network.api.base.authenticated.conversation.SubconversationDeleteRequest + +/** + * Helper class to handle MLS call related operations. + */ +interface MLSCallHelper { + + /** + * Check if the OneOnOne MLS call that uses SFT should be ended. + * The call should be ended if the call has two participants and the second participant has lost audio. + * + * @param conversationId the conversation id. + * @param callProtocol the call protocol. + * @param conversationType the conversation type. + * @param newCallParticipants the new call participants. + * @param previousCallParticipants the previous call participants. + * @return true if the call should be ended, false otherwise. + */ + fun shouldEndSFTOneOnOneCall( + conversationId: ConversationId, + callProtocol: Conversation.ProtocolInfo?, + conversationType: Conversation.Type, + newCallParticipants: List, + previousCallParticipants: List + ): Boolean + + /** + * Handle the call termination. + * If the call is a one on one call is oneOneOne on SFT, then delete MLS sub conversation + * otherwise leave the MLS conference. + * + * @param conversationId the conversation id. + * @param callProtocol the call protocol. + * @param conversationType the conversation type. + */ + suspend fun handleCallTermination( + conversationId: ConversationId, + conversationType: Conversation.Type? + ) +} + +class MLSCallHelperImpl( + private val callRepository: CallRepository, + private val subconversationRepository: SubconversationRepository, + private val userConfigRepository: UserConfigRepository +) : MLSCallHelper { + + override fun shouldEndSFTOneOnOneCall( + conversationId: ConversationId, + callProtocol: Conversation.ProtocolInfo?, + conversationType: Conversation.Type, + newCallParticipants: List, + previousCallParticipants: List + ) = callProtocol is Conversation.ProtocolInfo.MLS && + userConfigRepository.shouldUseSFTForOneOnOneCalls().getOrElse(false) && + 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, + conversationType: Conversation.Type? + ) { + if (userConfigRepository.shouldUseSFTForOneOnOneCalls().getOrElse(false) && + conversationType == Conversation.Type.ONE_ON_ONE + ) { + callingLogger.i("[MLSCallHelper] -> fetching remote MLS sub conversation details") + subconversationRepository.fetchRemoteSubConversationDetails( + conversationId, + CALL_SUBCONVERSATION_ID + ).onSuccess { subconversationDetails -> + callingLogger.i("[MLSCallHelper] -> Deleting remote MLS sub conversation") + subconversationRepository.deleteRemoteSubConversation( + subconversationDetails.parentId.toModel(), + SubconversationId(subconversationDetails.id), + SubconversationDeleteRequest( + subconversationDetails.epoch, + subconversationDetails.groupId + ) + ) + }.onFailure { + callingLogger.e("[MLSCallHelper] -> Error fetching remote MLS sub conversation details") + } + } else { + callingLogger.i("[MLSCallHelper] -> Leaving MLS conference") + callRepository.leaveMlsConference(conversationId) + } + } + + companion object { + const val TWO_PARTICIPANTS = 2 + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/JoinSubconversationUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/JoinSubconversationUseCase.kt index d65e70e00c4..20697352eb0 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/JoinSubconversationUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/JoinSubconversationUseCase.kt @@ -50,6 +50,7 @@ internal interface JoinSubconversationUseCase { suspend operator fun invoke(conversationId: ConversationId, subconversationId: SubconversationId): Either } +// TODO(refactor): usecase should not access API class directly, use SubconversationRepository instead internal class JoinSubconversationUseCaseImpl( private val conversationApi: ConversationApi, private val mlsConversationRepository: MLSConversationRepository, diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubconversationRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubconversationRepository.kt index f2a1a6e8a9b..0f5fac3a304 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubconversationRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubconversationRepository.kt @@ -17,34 +17,81 @@ */ package com.wire.kalium.logic.data.conversation +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.NetworkFailure import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.GroupID import com.wire.kalium.logic.data.id.SubconversationId +import com.wire.kalium.logic.data.id.toApi +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.onFailure +import com.wire.kalium.logic.functional.onSuccess +import com.wire.kalium.logic.kaliumLogger +import com.wire.kalium.logic.wrapApiRequest +import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi +import com.wire.kalium.network.api.base.authenticated.conversation.SubconversationDeleteRequest +import com.wire.kalium.network.api.base.authenticated.conversation.SubconversationResponse import io.ktor.util.collections.ConcurrentMap import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock interface SubconversationRepository { - suspend fun insertSubconversation(conversationId: ConversationId, subconversationId: SubconversationId, groupId: GroupID) - suspend fun getSubconversationInfo(conversationId: ConversationId, subconversationId: SubconversationId): GroupID? - suspend fun deleteSubconversation(conversationId: ConversationId, subconversationId: SubconversationId) + suspend fun insertSubconversation( + conversationId: ConversationId, + subconversationId: SubconversationId, + groupId: GroupID + ) + + suspend fun getSubconversationInfo( + conversationId: ConversationId, + subconversationId: SubconversationId + ): GroupID? + + suspend fun deleteSubconversation( + conversationId: ConversationId, + subconversationId: SubconversationId + ) + suspend fun containsSubconversation(groupId: GroupID): Boolean + suspend fun deleteRemoteSubConversation( + conversationId: ConversationId, + subConversationId: SubconversationId, + subConversationDeleteRequest: SubconversationDeleteRequest + ): Either + suspend fun fetchRemoteSubConversationGroupInfo( + conversationId: ConversationId, + subConversationId: SubconversationId + ): Either + + suspend fun fetchRemoteSubConversationDetails( + conversationId: ConversationId, + subConversationId: SubconversationId + ): Either } -class SubconversationRepositoryImpl : SubconversationRepository { +class SubconversationRepositoryImpl( + private val conversationApi: ConversationApi +) : SubconversationRepository { private val mutex = Mutex() private val subconversations = ConcurrentMap, GroupID>() - override suspend fun insertSubconversation(conversationId: ConversationId, subconversationId: SubconversationId, groupId: GroupID) { + override suspend fun insertSubconversation( + conversationId: ConversationId, + subconversationId: SubconversationId, + groupId: GroupID + ) { mutex.withLock { subconversations[Pair(conversationId, subconversationId)] = groupId } } - override suspend fun getSubconversationInfo(conversationId: ConversationId, subconversationId: SubconversationId): GroupID? { + override suspend fun getSubconversationInfo( + conversationId: ConversationId, + subconversationId: SubconversationId + ): GroupID? { mutex.withLock { return subconversations[Pair(conversationId, subconversationId)] } @@ -56,10 +103,50 @@ class SubconversationRepositoryImpl : SubconversationRepository { } } - override suspend fun deleteSubconversation(conversationId: ConversationId, subconversationId: SubconversationId) { + override suspend fun deleteSubconversation( + conversationId: ConversationId, + subconversationId: SubconversationId + ) { mutex.withLock { subconversations.remove(Pair(conversationId, subconversationId)) } } + override suspend fun deleteRemoteSubConversation( + conversationId: ConversationId, + subConversationId: SubconversationId, + subConversationDeleteRequest: SubconversationDeleteRequest + ): Either = + wrapApiRequest { + conversationApi.deleteSubconversation( + conversationId.toApi(), + subConversationId.toApi(), + subConversationDeleteRequest + ) + }.onSuccess { + kaliumLogger.i("Subconversation deleted successfully") + }.onFailure { + kaliumLogger.i("Failed to delete subconversation") + } + + override suspend fun fetchRemoteSubConversationGroupInfo( + conversationId: ConversationId, + subConversationId: SubconversationId + ): Either = wrapApiRequest { + conversationApi.fetchSubconversationGroupInfo( + conversationId.toApi(), + subConversationId.toApi() + ) + } + + // TODO: Replace SubconversationResponse with a domain model + override suspend fun fetchRemoteSubConversationDetails( + conversationId: ConversationId, + subConversationId: SubconversationId + ): Either = wrapApiRequest { + conversationApi.fetchSubconversationDetails( + conversationId.toApi(), + subConversationId.toApi() + ) + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/featureConfig/FeatureConfigMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/featureConfig/FeatureConfigMapper.kt index 25355fc8ce2..e07f2d21087 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/featureConfig/FeatureConfigMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/featureConfig/FeatureConfigMapper.kt @@ -132,7 +132,7 @@ class FeatureConfigMapperImpl : FeatureConfigMapper { override fun fromDTO(data: FeatureConfigData.ConferenceCalling): ConferenceCallingModel = ConferenceCallingModel( status = fromDTO(data.status), - useSFTForOneOnOneCalls = data.config?.useSFTForOneToOneCalls ?: false + useSFTForOneOnOneCalls = data.config?.useSFTForOneToOneCalls ?: true ) override fun fromDTO(data: FeatureConfigData.E2EI?): E2EIModel = diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt index 3346c37ee7d..a54496a91e4 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt @@ -689,7 +689,7 @@ class UserSessionScope internal constructor( private val notificationTokenRepository get() = NotificationTokenDataSource(globalPreferences.tokenStorage) - private val subconversationRepository = SubconversationRepositoryImpl() + private val subconversationRepository = SubconversationRepositoryImpl(conversationApi = authenticatedNetworkContainer.conversationApi) private val conversationRepository: ConversationRepository get() = ConversationDataSource( @@ -1190,6 +1190,8 @@ class UserSessionScope internal constructor( userRepository = userRepository, currentClientIdProvider = clientIdProvider, conversationRepository = conversationRepository, + subconversationRepository = subconversationRepository, + userConfigRepository = userConfigRepository, selfConversationIdProvider = selfConversationIdProvider, messageSender = messages.messageSender, federatedIdMapper = federatedIdMapper, @@ -2014,6 +2016,7 @@ class UserSessionScope internal constructor( incrementalSyncManager slowSyncManager + callRepository.leavePreviouslyJoinedMlsConferences() callRepository.updateOpenCallsToClosedStatus() messageRepository.resetAssetProgressStatus() } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt index 5bc154fd0f7..0e6a86d1a8f 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt @@ -19,10 +19,12 @@ package com.wire.kalium.logic.feature.call import com.wire.kalium.logic.cache.SelfConversationIdProvider +import com.wire.kalium.logic.configuration.UserConfigRepository import com.wire.kalium.logic.data.call.mapper.CallMapper import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.call.VideoStateChecker import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.conversation.SubconversationRepository import com.wire.kalium.logic.data.id.FederatedIdMapper import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.QualifiedIdMapper @@ -44,6 +46,8 @@ expect class GlobalCallManager { currentClientIdProvider: CurrentClientIdProvider, selfConversationIdProvider: SelfConversationIdProvider, conversationRepository: ConversationRepository, + subconversationRepository: SubconversationRepository, + userConfigRepository: UserConfigRepository, messageSender: MessageSender, callMapper: CallMapper, federatedIdMapper: FederatedIdMapper, diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/MLSCallHelperTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/MLSCallHelperTest.kt new file mode 100644 index 00000000000..378abeb0524 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/MLSCallHelperTest.kt @@ -0,0 +1,268 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.data.call + +import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.conversation.SubconversationRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.GroupID +import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.mls.CipherSuite +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.test_util.TestNetworkException +import com.wire.kalium.network.api.base.authenticated.conversation.SubconversationResponse +import io.mockative.Mock +import io.mockative.any +import io.mockative.classOf +import io.mockative.eq +import io.mockative.given +import io.mockative.mock +import io.mockative.once +import io.mockative.verify +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class MLSCallHelperTest { + + @Test + fun givenMlsConferenceCall_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(participant1, participant2), + previousCallParticipants = listOf(participant1, participant2) + ) + assertFalse { shouldEndSFTOneOnOneCall1 } + + // one participant in the call + val shouldEndSFTOneOnOneCall2 = mLSCallHelper.shouldEndSFTOneOnOneCall( + conversationId = conversationId, + callProtocol = CONVERSATION_MLS_PROTOCOL_INFO, + conversationType = Conversation.Type.ONE_ON_ONE, + newCallParticipants = listOf(participant1), + previousCallParticipants = listOf(participant1) + ) + assertFalse { shouldEndSFTOneOnOneCall2 } + + // Audi not lost for the second participant + val shouldEndSFTOneOnOneCall3 = mLSCallHelper.shouldEndSFTOneOnOneCall( + conversationId = conversationId, + callProtocol = CONVERSATION_MLS_PROTOCOL_INFO, + conversationType = Conversation.Type.GROUP, + newCallParticipants = listOf(participant1, participant2), + previousCallParticipants = listOf(participant1, participant2) + ) + assertFalse { shouldEndSFTOneOnOneCall3 } + + // Audio lost for the second participant + val shouldEndSFTOneOnOneCall4 = mLSCallHelper.shouldEndSFTOneOnOneCall( + conversationId = conversationId, + callProtocol = CONVERSATION_MLS_PROTOCOL_INFO, + conversationType = Conversation.Type.ONE_ON_ONE, + previousCallParticipants = listOf(participant1, participant2), + newCallParticipants = listOf( + participant1, + participant2.copy(hasEstablishedAudio = false) + ) + ) + assertTrue { shouldEndSFTOneOnOneCall4 } + + val (_, mLSCallHelper2) = Arrangement() + .withShouldUseSFTForOneOnOneCallsReturning(Either.Left(StorageFailure.DataNotFound)) + .arrange() + + // ShouldUseSFTForOneOnOneCalls user config is not found + val shouldEndSFTOneOnOneCall5 = mLSCallHelper2.shouldEndSFTOneOnOneCall( + conversationId = conversationId, + callProtocol = CONVERSATION_MLS_PROTOCOL_INFO, + conversationType = Conversation.Type.ONE_ON_ONE, + previousCallParticipants = listOf(participant1, participant2), + newCallParticipants = listOf( + participant1, + participant2.copy(hasEstablishedAudio = false) + ) + ) + assertFalse { shouldEndSFTOneOnOneCall5 } + } + + @Test + fun givenMLSOneOnOneCallAndShouldUseSFTForOneOnOneCall_whenHandleCallTerminationIsCalled_thenDeleteRemoteSubConversation() = + runTest { + val (arrangement, mLSCallHelper) = Arrangement() + .withShouldUseSFTForOneOnOneCallsReturning(Either.Right(true)) + .withFetchRemoteSubConversationDetailsReturning( + Either.Right(subconversationResponse) + ) + .withDeleteRemoteSubConversationSuccess() + .arrange() + + mLSCallHelper.handleCallTermination(conversationId, Conversation.Type.ONE_ON_ONE) + + verify(arrangement.subconversationRepository) + .suspendFunction(arrangement.subconversationRepository::deleteRemoteSubConversation) + .with(any(), any(), any()) + .wasInvoked(exactly = once) + } + + @Test + fun givenSubconversationRepositoryReturnFailure_whenHandleCallTerminationIsCalled_thenDoNotDeleteRemoteSubConversation() = + runTest { + val (arrangement, mLSCallHelper) = Arrangement() + .withShouldUseSFTForOneOnOneCallsReturning(Either.Right(true)) + .withFetchRemoteSubConversationDetailsReturning( + Either.Left( + NetworkFailure.ServerMiscommunication( + TestNetworkException.badRequest + ) + ) + ) + .arrange() + + mLSCallHelper.handleCallTermination(conversationId, Conversation.Type.ONE_ON_ONE) + + verify(arrangement.subconversationRepository) + .suspendFunction(arrangement.subconversationRepository::deleteRemoteSubConversation) + .with(eq(conversationId)) + .wasNotInvoked() + } + + @Test + fun givenShouldNotUseSFTForOneOnOneCall_whenHandleCallTerminationIsCalled_thenLeaveMlsConference() = + runTest { + val (arrangement, mLSCallHelper) = Arrangement() + .withShouldUseSFTForOneOnOneCallsReturning(Either.Right(false)) + .arrange() + + mLSCallHelper.handleCallTermination(conversationId, Conversation.Type.GROUP) + + verify(arrangement.callRepository) + .suspendFunction(arrangement.callRepository::leaveMlsConference) + .with(eq(conversationId)) + .wasInvoked(exactly = once) + } + + @Test + fun givenMLSGroupCall_whenHandleCallTerminationIsCalled_thenLeaveMlsConference() = + runTest { + val (arrangement, mLSCallHelper) = Arrangement() + .withShouldUseSFTForOneOnOneCallsReturning(Either.Right(true)) + .arrange() + + mLSCallHelper.handleCallTermination(conversationId, Conversation.Type.GROUP) + + verify(arrangement.callRepository) + .suspendFunction(arrangement.callRepository::leaveMlsConference) + .with(eq(conversationId)) + .wasInvoked(exactly = once) + } + + private class Arrangement { + + @Mock + val callRepository = mock(classOf()) + + @Mock + val subconversationRepository = mock(classOf()) + + @Mock + val userConfigRepository = mock(classOf()) + + private val mLSCallHelper: MLSCallHelper = MLSCallHelperImpl( + callRepository = callRepository, + subconversationRepository = subconversationRepository, + userConfigRepository = userConfigRepository + ) + + fun arrange() = this to mLSCallHelper + + fun withShouldUseSFTForOneOnOneCallsReturning(result: Either) = + apply { + given(userConfigRepository) + .function(userConfigRepository::shouldUseSFTForOneOnOneCalls) + .whenInvoked() + .thenReturn(result) + } + + fun withFetchRemoteSubConversationDetailsReturning(result: Either) = + apply { + given(subconversationRepository) + .suspendFunction(subconversationRepository::fetchRemoteSubConversationDetails) + .whenInvokedWith(eq(conversationId), eq(CALL_SUBCONVERSATION_ID)) + .thenReturn(result) + } + + fun withDeleteRemoteSubConversationSuccess() = + apply { + given(subconversationRepository) + .suspendFunction(subconversationRepository::deleteRemoteSubConversation) + .whenInvokedWith(any(), any(), any()) + .thenReturn(Either.Right(Unit)) + } + } + + companion object { + val conversationId = ConversationId(value = "convId", domain = "domainId") + val CONVERSATION_MLS_PROTOCOL_INFO = Conversation.ProtocolInfo.MLS( + GroupID("GROUP_ID"), + Conversation.ProtocolInfo.MLSCapable.GroupState.ESTABLISHED, + 5UL, + Instant.parse("2021-03-30T15:36:00.000Z"), + cipherSuite = CipherSuite.MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + ) + val participant1 = Participant( + id = QualifiedID("participantId", "participantDomain"), + clientId = "abcd", + name = "name", + isMuted = true, + isSpeaking = false, + isCameraOn = false, + avatarAssetId = null, + isSharingScreen = false, + hasEstablishedAudio = true + ) + val participant2 = participant1.copy( + id = QualifiedID("participantId2", "participantDomain2"), + clientId = "efgh" + ) + val subconversationResponse = SubconversationResponse( + id = "subconversationId", + parentId = com.wire.kalium.network.api.base.model.ConversationId( + "conversationId", + "domainId" + ), + groupId = "groupId", + epoch = 1UL, + epochTimestamp = "2021-03-30T15:36:00.000Z", + mlsCipherSuiteTag = 5, + members = listOf() + ) + } +} diff --git a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/scenario/OnCloseCallTest.kt b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/scenario/OnCloseCallTest.kt index 9904e2b1a23..e486b56420e 100644 --- a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/scenario/OnCloseCallTest.kt +++ b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/scenario/OnCloseCallTest.kt @@ -27,10 +27,12 @@ import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.GroupID import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl import com.wire.kalium.logic.data.call.CallStatus +import com.wire.kalium.logic.data.call.MLSCallHelper import com.wire.kalium.logic.data.mls.CipherSuite import com.wire.kalium.logic.feature.call.scenario.OnCloseCall import com.wire.kalium.logic.framework.TestUser import io.mockative.Mock +import io.mockative.any import io.mockative.classOf import io.mockative.eq import io.mockative.given @@ -49,6 +51,9 @@ class OnCloseCallTest { @Mock val callRepository = mock(classOf()) + @Mock + val mlsCallHelper = mock(classOf()) + val qualifiedIdMapper = QualifiedIdMapperImpl(TestUser.SELF.id) private lateinit var onCloseCall: OnCloseCall @@ -61,6 +66,7 @@ class OnCloseCallTest { fun setUp() { onCloseCall = OnCloseCall( callRepository, + mlsCallHelper, testScope, qualifiedIdMapper ) @@ -276,9 +282,9 @@ class OnCloseCallTest { .with(eq(conversationId), eq(CallStatus.CLOSED)) .wasInvoked(once) - verify(callRepository) - .suspendFunction(callRepository::leaveMlsConference) - .with(eq(conversationId)) + verify(mlsCallHelper) + .suspendFunction(mlsCallHelper::handleCallTermination) + .with(eq(conversationId), any()) .wasInvoked(once) } From 4c462e19135d7f35d869212dca4dcc524f990423 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 1 Aug 2024 10:54:08 +0200 Subject: [PATCH 7/9] chore: unit test --- .../kotlin/com/wire/kalium/logic/data/call/CallRepository.kt | 1 + .../kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt index 3bfcbfa8e43..3d326cfa447 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt @@ -506,6 +506,7 @@ internal class CallDataSource( ) override suspend fun updateOpenCallsToClosedStatus() { + leavePreviouslyJoinedMlsConferences() callDAO.updateOpenCallsToClosedStatus() } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt index a54496a91e4..f8bc9c72721 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt @@ -2016,7 +2016,6 @@ class UserSessionScope internal constructor( incrementalSyncManager slowSyncManager - callRepository.leavePreviouslyJoinedMlsConferences() callRepository.updateOpenCallsToClosedStatus() messageRepository.resetAssetProgressStatus() } From 867bd899167e42161dc5733f1cf19404fbc49ea8 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 1 Aug 2024 11:21:30 +0200 Subject: [PATCH 8/9] chore: unit test --- .../logic/feature/call/CallManagerTest.kt | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt b/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt index cd126782a03..7aadb684798 100644 --- a/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt +++ b/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt @@ -21,19 +21,23 @@ package com.wire.kalium.logic.feature.call import com.wire.kalium.calling.Calling import com.wire.kalium.calling.types.Handle import com.wire.kalium.logic.cache.SelfConversationIdProvider +import com.wire.kalium.logic.configuration.UserConfigRepository import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.call.VideoStateChecker import com.wire.kalium.logic.data.call.mapper.CallMapperImpl import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.conversation.SubconversationRepository import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.data.id.FederatedIdMapper import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.message.MessageContent import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository -import com.wire.kalium.logic.data.id.CurrentClientIdProvider +import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater +import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.featureFlags.KaliumConfigs import com.wire.kalium.logic.test_util.TestKaliumDispatcher @@ -48,11 +52,6 @@ import kotlinx.coroutines.test.runTest import kotlin.test.BeforeTest import kotlin.test.Ignore import kotlin.test.Test -import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater -import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider -import com.wire.kalium.network.NetworkStateObserver -import com.wire.kalium.util.DateTimeUtil.toIsoDateTimeString -import kotlinx.datetime.Instant class CallManagerTest { @@ -77,6 +76,12 @@ class CallManagerTest { @Mock private val conversationRepository = mock(classOf()) + @Mock + private val subconversationRepository = mock(classOf()) + + @Mock + private val userConfigRepository = mock(classOf()) + @Mock private val federatedIdMapper = mock(classOf()) @@ -109,6 +114,8 @@ class CallManagerTest { currentClientIdProvider = currentClientIdProvider, selfConversationIdProvider = selfConversationIdProvider, conversationRepository = conversationRepository, + subconversationRepository = subconversationRepository, + userConfigRepository = userConfigRepository, messageSender = messageSender, kaliumDispatchers = dispatcher, federatedIdMapper = federatedIdMapper, From ba75b309e586f0eaa7e4a78b1b9713dbf461fc42 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 1 Aug 2024 11:49:07 +0200 Subject: [PATCH 9/9] chore: typo --- .../com/wire/kalium/logic/feature/call/GlobalCallManager.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt b/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt index 9726be6fe9c..8b0c3a91429 100644 --- a/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt +++ b/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt @@ -25,12 +25,12 @@ import com.wire.kalium.logic.data.call.VideoStateChecker import com.wire.kalium.logic.data.call.mapper.CallMapper import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.conversation.SubconversationRepository -import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.data.id.FederatedIdMapper import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.QualifiedIdMapper +import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository -import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider import com.wire.kalium.logic.feature.message.MessageSender @@ -45,7 +45,7 @@ actual class GlobalCallManager { currentClientIdProvider: CurrentClientIdProvider, selfConversationIdProvider: SelfConversationIdProvider, conversationRepository: ConversationRepository, - subonversationRepository: SubconversationRepository, + subconversationRepository: SubconversationRepository, userConfigRepository: UserConfigRepository, messageSender: MessageSender, callMapper: CallMapper,