diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/RecentlyEndedCallMetadata.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/RecentlyEndedCallMetadata.kt new file mode 100644 index 00000000000..2d6468356f7 --- /dev/null +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/RecentlyEndedCallMetadata.kt @@ -0,0 +1,46 @@ +/* + * 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.data.conversation.Conversation + +data class RecentlyEndedCallMetadata( + val callEndReason: Int, + val callDetails: CallDetails, + val conversationDetails: ConversationDetails, + val isTeamMember: Boolean +) { + data class CallDetails( + val isCallScreenShare: Boolean, + val screenShareDurationInSeconds: Long, + val callScreenShareUniques: Int, + val isOutgoingCall: Boolean, + val callDurationInSeconds: Long, + val callParticipantsCount: Int, + val conversationServices: Int, + val callAVSwitchToggle: Boolean, + val callVideoEnabled: Boolean + ) + + data class ConversationDetails( + val conversationType: Conversation.Type, + val conversationSize: Int, + val conversationGuests: Int, + val conversationGuestsPro: Int + ) +} 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 2e89948aefd..7435be07478 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 @@ -36,6 +36,7 @@ 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.feature.call.usecase.ConversationClientsInCallUpdater +import com.wire.kalium.logic.feature.call.usecase.CreateAndPersistRecentlyEndedCallMetadataUseCase import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.featureFlags.KaliumConfigs @@ -103,6 +104,9 @@ class CallManagerTest { @Mock private val getCallConversationType = mock(GetCallConversationTypeProvider::class) + @Mock + private val createAndPersistRecentlyEndedCallMetadata = mock(CreateAndPersistRecentlyEndedCallMetadataUseCase::class) + private val dispatcher = TestKaliumDispatcher private lateinit var callManagerImpl: CallManagerImpl @@ -132,7 +136,8 @@ class CallManagerTest { networkStateObserver = networkStateObserver, kaliumConfigs = kaliumConfigs, mediaManagerService = mediaManagerService, - flowManagerService = flowManagerService + flowManagerService = flowManagerService, + createAndPersistRecentlyEndedCallMetadata = createAndPersistRecentlyEndedCallMetadata ) } 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 1382845bf18..648c3d755ee 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 @@ -34,6 +34,7 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider +import com.wire.kalium.logic.feature.call.usecase.CreateAndPersistRecentlyEndedCallMetadataUseCase import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.featureFlags.KaliumConfigs import com.wire.kalium.network.NetworkStateObserver @@ -56,7 +57,8 @@ actual class GlobalCallManager { conversationClientsInCallUpdater: ConversationClientsInCallUpdater, getCallConversationType: GetCallConversationTypeProvider, networkStateObserver: NetworkStateObserver, - kaliumConfigs: KaliumConfigs + kaliumConfigs: KaliumConfigs, + createAndPersistRecentlyEndedCallMetadata: CreateAndPersistRecentlyEndedCallMetadataUseCase ): 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 693c76d98f9..14ce1d51f68 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 @@ -75,6 +75,7 @@ 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.call.usecase.CreateAndPersistRecentlyEndedCallMetadataUseCase import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.featureFlags.KaliumConfigs import com.wire.kalium.logic.functional.fold @@ -119,6 +120,7 @@ class CallManagerImpl internal constructor( private val kaliumConfigs: KaliumConfigs, private val mediaManagerService: MediaManagerService, private val flowManagerService: FlowManagerService, + private val createAndPersistRecentlyEndedCallMetadata: CreateAndPersistRecentlyEndedCallMetadataUseCase, private val json: Json = Json { ignoreUnknownKeys = true }, private val shouldRemoteMuteChecker: ShouldRemoteMuteChecker = ShouldRemoteMuteCheckerImpl(), private val serverTimeHandler: ServerTimeHandler = ServerTimeHandlerImpl(), @@ -219,7 +221,8 @@ class CallManagerImpl internal constructor( callRepository = callRepository, networkStateObserver = networkStateObserver, scope = scope, - qualifiedIdMapper = qualifiedIdMapper + qualifiedIdMapper = qualifiedIdMapper, + createAndPersistRecentlyEndedCallMetadata = createAndPersistRecentlyEndedCallMetadata ).keepingStrongReference(), metricsHandler = metricsHandler, callConfigRequestHandler = OnConfigRequest(calling, callRepository, scope) 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 277eadc1662..1471ec8dd5e 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 @@ -39,6 +39,7 @@ import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider +import com.wire.kalium.logic.feature.call.usecase.CreateAndPersistRecentlyEndedCallMetadataUseCase import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.featureFlags.KaliumConfigs import com.wire.kalium.logic.util.CurrentPlatform @@ -94,7 +95,8 @@ actual class GlobalCallManager( conversationClientsInCallUpdater: ConversationClientsInCallUpdater, getCallConversationType: GetCallConversationTypeProvider, networkStateObserver: NetworkStateObserver, - kaliumConfigs: KaliumConfigs + kaliumConfigs: KaliumConfigs, + createAndPersistRecentlyEndedCallMetadata: CreateAndPersistRecentlyEndedCallMetadataUseCase ): CallManager { if (kaliumConfigs.enableCalling) { return callManagerHolder.computeIfAbsent(userId) { @@ -116,7 +118,8 @@ actual class GlobalCallManager( mediaManagerService = mediaManager, flowManagerService = flowManager, userConfigRepository = userConfigRepository, - kaliumConfigs = kaliumConfigs + kaliumConfigs = kaliumConfigs, + createAndPersistRecentlyEndedCallMetadata = createAndPersistRecentlyEndedCallMetadata ) } } else { 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 26d8c0b8194..cafbc99d38b 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 @@ -31,6 +31,7 @@ import com.wire.kalium.logic.data.call.CallStatus 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.feature.call.usecase.CreateAndPersistRecentlyEndedCallMetadataUseCase import com.wire.kalium.network.NetworkState import com.wire.kalium.network.NetworkStateObserver import kotlinx.coroutines.CoroutineScope @@ -41,7 +42,8 @@ class OnCloseCall( private val callRepository: CallRepository, private val scope: CoroutineScope, private val qualifiedIdMapper: QualifiedIdMapper, - private val networkStateObserver: NetworkStateObserver + private val networkStateObserver: NetworkStateObserver, + private val createAndPersistRecentlyEndedCallMetadata: CreateAndPersistRecentlyEndedCallMetadataUseCase ) : CloseCallHandler { override fun onClosedCall( reason: Int, @@ -62,6 +64,7 @@ class OnCloseCall( val conversationIdWithDomain = qualifiedIdMapper.fromStringToQualifiedID(conversationId) scope.launch { + val callMetadata = callRepository.getCallMetadataProfile()[conversationIdWithDomain] val isConnectedToInternet = networkStateObserver.observeNetworkState().value == NetworkState.ConnectedWithInternet @@ -78,11 +81,15 @@ class OnCloseCall( status = callStatus ) - if (callRepository.getCallMetadataProfile()[conversationIdWithDomain]?.protocol is Conversation.ProtocolInfo.MLS) { + if (callMetadata?.protocol is Conversation.ProtocolInfo.MLS) { callRepository.leaveMlsConference(conversationIdWithDomain) } callingLogger.i("[OnCloseCall] -> ConversationId: ${conversationId.obfuscateId()} | callStatus: $callStatus") } + + scope.launch { + createAndPersistRecentlyEndedCallMetadata(conversationIdWithDomain, reason) + } } private fun shouldPersistMissedCall( 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 f45ce704881..4cc7ea71551 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 @@ -75,6 +75,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine @@ -136,6 +137,9 @@ interface CallRepository { suspend fun advanceEpoch(conversationId: ConversationId) fun currentCallProtocol(conversationId: ConversationId): Conversation.ProtocolInfo? suspend fun observeCurrentCall(conversationId: ConversationId): Flow + + suspend fun updateRecentlyEndedCallMetadata(recentlyEndedCallMetadata: RecentlyEndedCallMetadata) + suspend fun observeRecentlyEndedCallMetadata(): Flow } @Suppress("LongParameterList", "TooManyFunctions") @@ -164,25 +168,38 @@ internal class CallDataSource( private val scope = CoroutineScope(job + kaliumDispatchers.io) private val callJobs = ConcurrentMutableMap() private val staleParticipantJobs = ConcurrentMutableMap() + private val _recentlyEndedCallFlow = MutableSharedFlow( + extraBufferCapacity = 1 + ) override suspend fun observeCurrentCall(conversationId: ConversationId): Flow = _callMetadataProfile.map { - it[conversationId]?.let { currentCall -> - Call( - conversationId = conversationId, - status = currentCall.callStatus, - isMuted = currentCall.isMuted, - isCameraOn = currentCall.isCameraOn, - isCbrEnabled = currentCall.isCbrEnabled, - callerId = currentCall.callerId, - conversationName = currentCall.conversationName, - conversationType = currentCall.conversationType, - callerName = currentCall.callerName, - callerTeamName = currentCall.callerTeamName, - establishedTime = currentCall.establishedTime, - participants = currentCall.getFullParticipants(), - maxParticipants = currentCall.maxParticipants - ) - } + it[conversationId]?.mapCallMetadataToCall(conversationId) + } + + override suspend fun updateRecentlyEndedCallMetadata(recentlyEndedCallMetadata: RecentlyEndedCallMetadata) { + _recentlyEndedCallFlow.emit(recentlyEndedCallMetadata) + } + + private fun CallMetadata.mapCallMetadataToCall(conversationId: ConversationId): Call { + return Call( + conversationId = conversationId, + status = callStatus, + isMuted = isMuted, + isCameraOn = isCameraOn, + isCbrEnabled = isCbrEnabled, + callerId = callerId, + conversationName = conversationName, + conversationType = conversationType, + callerName = callerName, + callerTeamName = callerTeamName, + establishedTime = establishedTime, + participants = getFullParticipants(), + maxParticipants = maxParticipants + ) + } + + override suspend fun observeRecentlyEndedCallMetadata(): Flow { + return _recentlyEndedCallFlow } override suspend fun getCallConfigResponse(limit: Int?): Either = wrapApiRequest { 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 80a977d714e..c445908052c 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 @@ -177,6 +177,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.CreateAndPersistRecentlyEndedCallMetadataUseCase +import com.wire.kalium.logic.feature.call.usecase.CreateAndPersistRecentlyEndedCallMetadataUseCaseImpl 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 @@ -1273,7 +1275,8 @@ class UserSessionScope internal constructor( conversationClientsInCallUpdater = conversationClientsInCallUpdater, getCallConversationType = getCallConversationType, networkStateObserver = networkStateObserver, - kaliumConfigs = kaliumConfigs + kaliumConfigs = kaliumConfigs, + createAndPersistRecentlyEndedCallMetadata = createAndPersistRecentlyEndedCallMetadata ) } @@ -2107,6 +2110,13 @@ class UserSessionScope internal constructor( userScopedLogger ) + private val createAndPersistRecentlyEndedCallMetadata: CreateAndPersistRecentlyEndedCallMetadataUseCase + get() = CreateAndPersistRecentlyEndedCallMetadataUseCaseImpl( + callRepository = callRepository, + observeConversationMembers = conversations.observeConversationMembers, + selfTeamIdProvider = selfTeamId + ) + val migrateFromPersonalToTeam: MigrateFromPersonalToTeamUseCase get() = MigrateFromPersonalToTeamUseCaseImpl(userId, userRepository, invalidateTeamId) 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 59aead0b38f..a67589af772 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 @@ -63,6 +63,8 @@ import com.wire.kalium.logic.feature.call.usecase.ObserveOngoingCallsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveOngoingCallsUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.ObserveOutgoingCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveOutgoingCallUseCaseImpl +import com.wire.kalium.logic.feature.call.usecase.ObserveRecentlyEndedCallMetadataUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveRecentlyEndedCallMetadataUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.ObserveSpeakerUseCase import com.wire.kalium.logic.feature.call.usecase.RejectCallUseCase import com.wire.kalium.logic.feature.call.usecase.RequestVideoStreamsUseCase @@ -232,4 +234,9 @@ class CallsScope internal constructor( val updateNextTimeCallFeedback: UpdateNextTimeCallFeedbackUseCase by lazy { UpdateNextTimeCallFeedbackUseCase(userConfigRepository) } + + val observeRecentlyEndedCallMetadata: ObserveRecentlyEndedCallMetadataUseCase + get() = ObserveRecentlyEndedCallMetadataUseCaseImpl( + callRepository = callRepository + ) } 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 eb7bca57c8b..12f8ee9dadf 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 @@ -34,6 +34,7 @@ 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.call.usecase.CreateAndPersistRecentlyEndedCallMetadataUseCase import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.featureFlags.KaliumConfigs import com.wire.kalium.network.NetworkStateObserver @@ -57,7 +58,8 @@ expect class GlobalCallManager { conversationClientsInCallUpdater: ConversationClientsInCallUpdater, getCallConversationType: GetCallConversationTypeProvider, networkStateObserver: NetworkStateObserver, - kaliumConfigs: KaliumConfigs + kaliumConfigs: KaliumConfigs, + createAndPersistRecentlyEndedCallMetadata: CreateAndPersistRecentlyEndedCallMetadataUseCase ): CallManager suspend fun removeInMemoryCallingManagerForUser(userId: UserId) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/CreateAndPersistRecentlyEndedCallMetadataUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/CreateAndPersistRecentlyEndedCallMetadataUseCase.kt new file mode 100644 index 00000000000..8c5f88e2691 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/CreateAndPersistRecentlyEndedCallMetadataUseCase.kt @@ -0,0 +1,99 @@ +/* + * 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.logic.data.call.CallMetadata +import com.wire.kalium.logic.data.call.CallRepository +import com.wire.kalium.logic.data.call.CallScreenSharingMetadata +import com.wire.kalium.logic.data.call.RecentlyEndedCallMetadata +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.SelfTeamIdProvider +import com.wire.kalium.logic.data.user.type.UserType +import com.wire.kalium.logic.feature.conversation.ObserveConversationMembersUseCase +import com.wire.kalium.logic.functional.getOrNull +import com.wire.kalium.util.DateTimeUtil +import kotlinx.coroutines.flow.first + +/** + * Given a call and raw call end reason create metadata containing all information regarding + * a call. + */ +interface CreateAndPersistRecentlyEndedCallMetadataUseCase { + suspend operator fun invoke(conversationId: ConversationId, callEndedReason: Int) +} + +class CreateAndPersistRecentlyEndedCallMetadataUseCaseImpl internal constructor( + private val callRepository: CallRepository, + private val observeConversationMembers: ObserveConversationMembersUseCase, + private val selfTeamIdProvider: SelfTeamIdProvider, +) : CreateAndPersistRecentlyEndedCallMetadataUseCase { + override suspend fun invoke(conversationId: ConversationId, callEndedReason: Int) { + callRepository.getCallMetadataProfile()[conversationId]?.createMetadata( + conversationId = conversationId, + callEndedReason = callEndedReason + )?.let { metadata -> + callRepository.updateRecentlyEndedCallMetadata(metadata) + } + } + + private suspend fun CallMetadata.createMetadata(conversationId: ConversationId, callEndedReason: Int): RecentlyEndedCallMetadata { + val selfCallUser = getFullParticipants().firstOrNull { participant -> participant.userType == UserType.OWNER } + val conversationMembers = observeConversationMembers(conversationId).first() + val conversationServicesCount = conversationMembers.count { member -> member.user.userType == UserType.SERVICE } + val guestsCount = conversationMembers.count { member -> member.user.userType == UserType.GUEST } + val guestsProCount = conversationMembers.count { member -> member.user.userType == UserType.GUEST && member.user.teamId != null } + val isOutgoingCall = callerId.value == selfCallUser?.id?.value + val callDurationInSeconds = establishedTime?.let { + DateTimeUtil.calculateMillisDifference(it, DateTimeUtil.currentIsoDateTimeString()) / MILLIS_IN_SECOND + } ?: 0L + + return RecentlyEndedCallMetadata( + callEndReason = callEndedReason, + callDetails = RecentlyEndedCallMetadata.CallDetails( + isCallScreenShare = selfCallUser?.isSharingScreen ?: false, + screenShareDurationInSeconds = screenShareMetadata.totalDurationInSeconds(), + callScreenShareUniques = screenShareMetadata.uniqueSharingUsers.size, + isOutgoingCall = isOutgoingCall, + callDurationInSeconds = callDurationInSeconds, + callParticipantsCount = participants.size, + conversationServices = conversationServicesCount, + callAVSwitchToggle = selfCallUser?.isCameraOn ?: false, + callVideoEnabled = isCameraOn + ), + conversationDetails = RecentlyEndedCallMetadata.ConversationDetails( + conversationType = conversationType, + conversationSize = conversationMembers.size, + conversationGuests = guestsCount, + conversationGuestsPro = guestsProCount + ), + isTeamMember = selfTeamIdProvider().getOrNull() != null + ) + } + + private fun CallScreenSharingMetadata.totalDurationInSeconds(): Long { + val now = DateTimeUtil.currentInstant() + val activeScreenSharesDurationInSeconds = + activeScreenShares.values.sumOf { startTime -> DateTimeUtil.calculateMillisDifference(startTime, now) } + + return (activeScreenSharesDurationInSeconds + completedScreenShareDurationInMillis) / MILLIS_IN_SECOND + } + + private companion object { + const val MILLIS_IN_SECOND = 1_000L + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveRecentlyEndedCallMetadataUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveRecentlyEndedCallMetadataUseCase.kt new file mode 100644 index 00000000000..d1a9de46bcf --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveRecentlyEndedCallMetadataUseCase.kt @@ -0,0 +1,39 @@ +/* + * 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.logic.data.call.CallRepository +import com.wire.kalium.logic.data.call.RecentlyEndedCallMetadata +import kotlinx.coroutines.flow.Flow +import com.wire.kalium.logic.data.id.ConversationId + +/** + * Use case to observe recently ended call metadata. This gives us all metadata assigned to a call. + * Used mainly for analytics. + */ +interface ObserveRecentlyEndedCallMetadataUseCase { + suspend operator fun invoke(conversationId: ConversationId): Flow +} + +class ObserveRecentlyEndedCallMetadataUseCaseImpl internal constructor( + private val callRepository: CallRepository, +) : ObserveRecentlyEndedCallMetadataUseCase { + override suspend fun invoke(conversationId: ConversationId): Flow { + return callRepository.observeRecentlyEndedCallMetadata() + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchScope.kt index bb5c11a9ff9..6581e818f7c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchScope.kt @@ -36,7 +36,7 @@ class SearchScope internal constructor( private val kaliumConfigs: KaliumConfigs ) { val searchUsers: SearchUsersUseCase - get() = SearchUsersUseCase( + get() = SearchUsersUseCaseImpl( searchUserRepository, selfUserId, kaliumConfigs.maxRemoteSearchResultCount diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchUsersUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchUsersUseCase.kt index 74b27af6a65..b9cd963b41b 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchUsersUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchUsersUseCase.kt @@ -30,16 +30,26 @@ import kotlinx.coroutines.coroutineScope /** * Use case for searching users. - * @param searchQuery The search query. - * @param excludingMembersOfConversation The conversation to exclude its members from the search. - * @param customDomain The custom domain to search in if null the search will be on the self user domain. */ -class SearchUsersUseCase internal constructor( +interface SearchUsersUseCase { + /** + * @param searchQuery The search query. + * @param excludingMembersOfConversation The conversation to exclude its members from the search. + * @param customDomain The custom domain to search in if null the search will be on the self user domain. + */ + suspend operator fun invoke( + searchQuery: String, + excludingMembersOfConversation: ConversationId?, + customDomain: String? + ): SearchUserResult +} + +class SearchUsersUseCaseImpl internal constructor( private val searchUserRepository: SearchUserRepository, private val selfUserId: UserId, private val maxRemoteSearchResultCount: Int -) { - suspend operator fun invoke( +) : SearchUsersUseCase { + override suspend operator fun invoke( searchQuery: String, excludingMembersOfConversation: ConversationId?, customDomain: String? diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/CreateAndPersistRecentlyEndedCallMetadataUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/CreateAndPersistRecentlyEndedCallMetadataUseCaseTest.kt new file mode 100644 index 00000000000..8a75f6c8c0c --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/CreateAndPersistRecentlyEndedCallMetadataUseCaseTest.kt @@ -0,0 +1,281 @@ +/* + * 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.logic.data.call.CallMetadata +import com.wire.kalium.logic.data.call.CallMetadataProfile +import com.wire.kalium.logic.data.call.CallRepository +import com.wire.kalium.logic.data.call.CallStatus +import com.wire.kalium.logic.data.call.ParticipantMinimized +import com.wire.kalium.logic.data.call.RecentlyEndedCallMetadata +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.conversation.MemberDetails +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.SelfTeamIdProvider +import com.wire.kalium.logic.data.user.type.UserType +import com.wire.kalium.logic.feature.conversation.ObserveConversationMembersUseCase +import com.wire.kalium.logic.framework.TestCall.CALLER_ID +import com.wire.kalium.logic.framework.TestUser +import com.wire.kalium.logic.framework.TestUser.OTHER_MINIMIZED +import com.wire.kalium.logic.functional.Either +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.every +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class CreateAndPersistRecentlyEndedCallMetadataUseCaseTest { + + @Test + fun givenCallAndEndCallReaction_whenUseCaseInvoked_thenRecentlyCallMetadataIsProperlyUpdated() = runTest { + // given + val (arrangement, useCase) = Arrangement() + .withOutgoingCall() + .withSelfTeamIdPresent() + .withConversationMembers() + .arrange() + + // when + useCase( + conversationId = CONVERSATION_ID, + callEndedReason = 2 + ) + + // then + coVerify { + arrangement.callRepository.updateRecentlyEndedCallMetadata(DEFAULT_ENDED_CALL_METADATA) + }.wasInvoked(exactly = once) + } + + @Test + fun givenCallDetailsWithinConversationWithGuests_whenUseCaseInvoked_thenRecentlyEndedCallMetadataHasProperGuestsCount() = runTest { + // given + val (arrangement, useCase) = Arrangement() + .withOutgoingCall() + .withSelfTeamIdPresent() + .withConversationGuests() + .arrange() + + // when + useCase( + conversationId = CONVERSATION_ID, + callEndedReason = 2 + ) + + // then + coVerify { + arrangement.callRepository.updateRecentlyEndedCallMetadata( + DEFAULT_ENDED_CALL_METADATA.copy( + conversationDetails = DEFAULT_ENDED_CALL_METADATA.conversationDetails.copy( + conversationGuests = 1 + ) + ) + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenCallDetailsWithinConversationWithGuests_whenUseCaseInvoked_thenRecentlyEndedCallMetadataHasProperGuestsProCount() = runTest { + // given + val (arrangement, useCase) = Arrangement() + .withOutgoingCall() + .withSelfTeamIdPresent() + .withConversationGuestsPro() + .arrange() + + // when + useCase( + conversationId = CONVERSATION_ID, + callEndedReason = 2 + ) + + // then + coVerify { + arrangement.callRepository.updateRecentlyEndedCallMetadata( + DEFAULT_ENDED_CALL_METADATA.copy( + conversationDetails = DEFAULT_ENDED_CALL_METADATA.conversationDetails.copy( + conversationGuests = 1, + conversationGuestsPro = 1 + ) + ) + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenIncomingCallDetails_whenUseCaseInvoked_thenReturnCorrectMetadataIncomingCall() = runTest { + // given + val (arrangement, useCase) = Arrangement() + .withIncomingCall() + .withSelfTeamIdPresent() + .withConversationMembers() + .arrange() + + // when + useCase( + conversationId = CONVERSATION_ID, + callEndedReason = 2 + ) + + // then + coVerify { + arrangement.callRepository.updateRecentlyEndedCallMetadata( + DEFAULT_ENDED_CALL_METADATA.copy( + callDetails = DEFAULT_ENDED_CALL_METADATA.callDetails.copy( + isOutgoingCall = false + ) + ) + ) + }.wasInvoked(exactly = once) + } + + private class Arrangement { + @Mock + val observeConversationMembers = mock(ObserveConversationMembersUseCase::class) + + @Mock + val selfTeamIdProvider = mock(SelfTeamIdProvider::class) + + @Mock + val callRepository = mock(CallRepository::class) + + fun withOutgoingCall() = apply { + every { callRepository.getCallMetadataProfile() } + .returns(CallMetadataProfile(mapOf(CONVERSATION_ID to callMetadata()))) + } + + fun withIncomingCall() = apply { + every { callRepository.getCallMetadataProfile() } + .returns(CallMetadataProfile(mapOf(CONVERSATION_ID to callMetadata().copy(callerId = CALLER_ID.copy(value = "external"))))) + } + + suspend fun withConversationMembers() = apply { + coEvery { observeConversationMembers(any()) }.returns( + flowOf( + listOf( + MemberDetails(TestUser.SELF, Conversation.Member.Role.Admin), + MemberDetails(TestUser.OTHER, Conversation.Member.Role.Member) + ) + ) + ) + } + + suspend fun withConversationGuests() = apply { + coEvery { observeConversationMembers(any()) }.returns( + flowOf( + listOf( + MemberDetails(TestUser.SELF, Conversation.Member.Role.Admin), + MemberDetails(TestUser.OTHER.copy(userType = UserType.GUEST, teamId = null), Conversation.Member.Role.Member) + ) + ) + ) + } + + suspend fun withConversationGuestsPro() = apply { + coEvery { observeConversationMembers(any()) }.returns( + flowOf( + listOf( + MemberDetails(TestUser.SELF, Conversation.Member.Role.Admin), + MemberDetails(TestUser.OTHER.copy(userType = UserType.GUEST), Conversation.Member.Role.Member) + ) + ) + ) + } + + suspend fun withSelfTeamIdPresent() = apply { + coEvery { selfTeamIdProvider() }.returns(Either.Right(TestUser.SELF.teamId)) + } + + fun arrange(): Pair = + this to CreateAndPersistRecentlyEndedCallMetadataUseCaseImpl( + callRepository = callRepository, + observeConversationMembers = observeConversationMembers, + selfTeamIdProvider = selfTeamIdProvider + ) + + private fun callMetadata(): CallMetadata { + return CallMetadata( + callerId = CALLER_ID.copy(value = "ownerId"), + isMuted = true, + isCameraOn = false, + isCbrEnabled = false, + conversationName = null, + users = listOf( + OTHER_MINIMIZED.copy(id = CALLER_ID.copy(value = "ownerId"), userType = UserType.OWNER), + OTHER_MINIMIZED + ), + participants = listOf( + ParticipantMinimized( + id = CALLER_ID.copy(value = "ownerId"), + userId = CALLER_ID.copy(value = "ownerId"), + clientId = "abcd", + isMuted = true, + isCameraOn = false, + isSharingScreen = false, + hasEstablishedAudio = true + ), + ParticipantMinimized( + id = CALLER_ID, + userId = CALLER_ID, + clientId = "abcd", + isMuted = true, + isCameraOn = false, + isSharingScreen = false, + hasEstablishedAudio = true + ) + ), + conversationType = Conversation.Type.ONE_ON_ONE, + callerName = "User Name", + callerTeamName = null, + callStatus = CallStatus.ESTABLISHED, + protocol = Conversation.ProtocolInfo.Proteus, + activeSpeakers = mapOf() + ) + } + } + + private companion object { + val CONVERSATION_ID = ConversationId(value = "value", domain = "domain") + val DEFAULT_ENDED_CALL_METADATA = RecentlyEndedCallMetadata( + callEndReason = 2, + isTeamMember = true, + callDetails = RecentlyEndedCallMetadata.CallDetails( + isCallScreenShare = false, + screenShareDurationInSeconds = 0L, + callScreenShareUniques = 0, + isOutgoingCall = true, + callDurationInSeconds = 0L, + callParticipantsCount = 2, + conversationServices = 0, + callAVSwitchToggle = false, + callVideoEnabled = false + ), + conversationDetails = RecentlyEndedCallMetadata.ConversationDetails( + conversationType = Conversation.Type.ONE_ON_ONE, + conversationSize = 2, + conversationGuests = 0, + conversationGuestsPro = 0 + ) + ) + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/search/SearchUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/search/SearchUseCaseTest.kt index 821f32c3a2b..aafedb5a00c 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/search/SearchUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/search/SearchUseCaseTest.kt @@ -243,7 +243,7 @@ class SearchUseCaseTest { private class Arrangement : SearchRepositoryArrangement by SearchRepositoryArrangementImpl() { - private val searchUseCase: SearchUsersUseCase = SearchUsersUseCase( + private val searchUseCase: SearchUsersUseCase = SearchUsersUseCaseImpl( searchUserRepository = searchUserRepository, selfUserId = selfUserID, maxRemoteSearchResultCount = 30 diff --git a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCallTest.kt b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCallTest.kt index 56fb0aec4e2..aa34f1b8a9a 100644 --- a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCallTest.kt +++ b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCallTest.kt @@ -19,7 +19,6 @@ package com.wire.kalium.logic.feature.call.scenario import com.wire.kalium.calling.CallClosedReason import com.wire.kalium.calling.types.Uint32_t -import com.wire.kalium.logic.data.call.CallHelper import com.wire.kalium.logic.data.call.CallMetadata import com.wire.kalium.logic.data.call.CallMetadataProfile import com.wire.kalium.logic.data.call.CallRepository @@ -29,11 +28,13 @@ 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.mls.CipherSuite +import com.wire.kalium.logic.feature.call.usecase.CreateAndPersistRecentlyEndedCallMetadataUseCase import com.wire.kalium.logic.framework.TestCall import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.network.NetworkState import com.wire.kalium.network.NetworkStateObserver import io.mockative.Mock +import io.mockative.any import io.mockative.coVerify import io.mockative.eq import io.mockative.every @@ -55,6 +56,9 @@ class OnCloseCallTest { @Mock val networkStateObserver = mock(NetworkStateObserver::class) + @Mock + val createAndPersistRecentlyEndedCallMetadata = mock(CreateAndPersistRecentlyEndedCallMetadataUseCase::class) + val qualifiedIdMapper = QualifiedIdMapperImpl(TestUser.SELF.id) private lateinit var onCloseCall: OnCloseCall @@ -69,7 +73,8 @@ class OnCloseCallTest { callRepository, testScope, qualifiedIdMapper, - networkStateObserver + networkStateObserver, + createAndPersistRecentlyEndedCallMetadata ) every { @@ -338,6 +343,26 @@ class OnCloseCallTest { }.wasNotInvoked() } + @Test + fun givenClosedCall_whenOnCloseCallInvoked_thenCreateAndPersistRecentlyEndedCallIsInvoked() = + testScope.runTest { + val reason = CallClosedReason.CANCELLED.avsValue + + onCloseCall.onClosedCall( + reason, + conversationIdString, + time, + userIdString, + clientId, + null + ) + yield() + + coVerify { + createAndPersistRecentlyEndedCallMetadata(any(), any()) + }.wasInvoked(once) + } + companion object { private val conversationId = ConversationId("conversationId", "wire.com") private const val conversationIdString = "conversationId@wire.com"