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 622fe0bd56..84c7805e07 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 @@ -451,7 +451,7 @@ internal class CallDataSource( val updatedCallMetadata = callMetadataProfile.data.toMutableMap().apply { this[conversationId] = call.copy( participants = participants, - maxParticipants = max(call.maxParticipants, participants.size + 1), + maxParticipants = max(call.maxParticipants, participants.size), users = updatedUsers, screenShareMetadata = updateScreenSharingMetadata( metadata = call.screenShareMetadata, 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 bf77cbc916..72092d3f3a 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 @@ -53,6 +53,7 @@ import com.wire.kalium.logic.feature.call.usecase.IsLastCallClosedUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.MuteCallUseCase import com.wire.kalium.logic.feature.call.usecase.MuteCallUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.ObserveAskCallFeedbackUseCase +import com.wire.kalium.logic.feature.call.usecase.observeAskCallFeedbackUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveConferenceCallingEnabledUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveConferenceCallingEnabledUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.ObserveEndCallDueToConversationDegradationUseCase @@ -229,7 +230,7 @@ class CallsScope internal constructor( val observeEndCallDueToDegradationDialog: ObserveEndCallDueToConversationDegradationUseCase get() = ObserveEndCallDueToConversationDegradationUseCaseImpl(EndCallResultListenerImpl) val observeAskCallFeedbackUseCase: ObserveAskCallFeedbackUseCase - get() = ObserveAskCallFeedbackUseCase(EndCallResultListenerImpl) + get() = observeAskCallFeedbackUseCase(EndCallResultListenerImpl) private val shouldAskCallFeedback: ShouldAskCallFeedbackUseCase by lazy { ShouldAskCallFeedbackUseCase(userConfigRepository) 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 index bd76b9c419..649ce32bfc 100644 --- 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 @@ -20,6 +20,7 @@ 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.CallStatus import com.wire.kalium.logic.data.call.RecentlyEndedCallMetadata import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.SelfTeamIdProvider @@ -57,7 +58,7 @@ class CreateAndPersistRecentlyEndedCallMetadataUseCaseImpl internal constructor( 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 isOutgoingCall = callStatus == CallStatus.STARTED val callDurationInSeconds = establishedTime?.let { DateTimeUtil.calculateMillisDifference(it, DateTimeUtil.currentIsoDateTimeString()) / MILLIS_IN_SECOND } ?: 0L diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallResultListener.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallResultListener.kt index a376b3fc4b..313e77e74b 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallResultListener.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallResultListener.kt @@ -17,13 +17,14 @@ */ package com.wire.kalium.logic.feature.call.usecase +import com.wire.kalium.logic.feature.user.ShouldAskCallFeedbackUseCaseResult import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow interface EndCallResultListener { suspend fun observeCallEndedResult(): Flow suspend fun onCallEndedBecauseOfVerificationDegraded() - suspend fun onCallEndedAskForFeedback(shouldAsk: Boolean) + suspend fun onCallEndedAskForFeedback(shouldAskCallFeedback: ShouldAskCallFeedbackUseCaseResult) } /** @@ -39,12 +40,12 @@ object EndCallResultListenerImpl : EndCallResultListener { conversationCallEnded.emit(EndCallResult.VerificationDegraded) } - override suspend fun onCallEndedAskForFeedback(shouldAsk: Boolean) { - conversationCallEnded.emit(EndCallResult.AskForFeedback(shouldAsk)) + override suspend fun onCallEndedAskForFeedback(shouldAskCallFeedback: ShouldAskCallFeedbackUseCaseResult) { + conversationCallEnded.emit(EndCallResult.AskForFeedback(shouldAskCallFeedback)) } } sealed class EndCallResult { data object VerificationDegraded : EndCallResult() - data class AskForFeedback(val shouldAsk: Boolean) : EndCallResult() + data class AskForFeedback(val shouldAskCallFeedback: ShouldAskCallFeedbackUseCaseResult) : EndCallResult() } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallUseCase.kt index 97d61e4ab5..74e3b7d606 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallUseCase.kt @@ -29,6 +29,7 @@ import com.wire.kalium.util.KaliumDispatcher import com.wire.kalium.util.KaliumDispatcherImpl import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext +import kotlinx.datetime.toInstant /** * This use case is responsible for ending a call. @@ -56,7 +57,7 @@ internal class EndCallUseCaseImpl( * @param conversationId the id of the conversation for the call should be ended. */ override suspend operator fun invoke(conversationId: ConversationId) = withContext(dispatchers.default) { - callRepository.callsFlow().first().find { + val endedCall = callRepository.callsFlow().first().find { // This use case can be invoked while joining the call or when the call is established. it.conversationId == conversationId && it.status in listOf( CallStatus.STARTED, @@ -72,10 +73,11 @@ internal class EndCallUseCaseImpl( callingLogger.d("[EndCallUseCase] -> Updating call status to CLOSED") callRepository.updateCallStatusById(conversationId, CallStatus.CLOSED) } + it } callManager.value.endCall(conversationId) callRepository.updateIsCameraOnById(conversationId, false) - endCallListener.onCallEndedAskForFeedback(shouldAskCallFeedback()) + endCallListener.onCallEndedAskForFeedback(shouldAskCallFeedback(endedCall?.establishedTime?.toInstant())) } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveAskCallFeedbackUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveAskCallFeedbackUseCase.kt index 5c3e691504..daaa4badf3 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveAskCallFeedbackUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveAskCallFeedbackUseCase.kt @@ -17,26 +17,26 @@ */ package com.wire.kalium.logic.feature.call.usecase +import com.wire.kalium.logic.feature.user.ShouldAskCallFeedbackUseCaseResult import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.map /** - * The useCase for observing when the ongoing call was ended because of degradation of conversation verification status (Proteus or MLS) + * Use case to observe if we should ask for feedback after the call has ended. */ interface ObserveAskCallFeedbackUseCase { /** - * @return [Flow] that emits only when the call was ended because of degradation of conversation verification status (Proteus or MLS) + * @return [Flow] that emits [ShouldAskCallFeedbackUseCaseResult] when the call has ended and we should ask for feedback. */ - suspend operator fun invoke(): Flow + suspend operator fun invoke(): Flow } -@Suppress("FunctionNaming") -internal fun ObserveAskCallFeedbackUseCase( +internal fun observeAskCallFeedbackUseCase( endCallListener: EndCallResultListener ) = object : ObserveAskCallFeedbackUseCase { - override suspend fun invoke(): Flow = + override suspend fun invoke(): Flow = endCallListener.observeCallEndedResult() .filterIsInstance(EndCallResult.AskForFeedback::class) - .map { it.shouldAsk } + .map { it.shouldAskCallFeedback } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/ShouldAskCallFeedbackUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/ShouldAskCallFeedbackUseCase.kt index 56ef7e77c0..d1fd8bfd59 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/ShouldAskCallFeedbackUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/ShouldAskCallFeedbackUseCase.kt @@ -14,6 +14,7 @@ * * 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.user @@ -24,13 +25,13 @@ import com.wire.kalium.util.DateTimeUtil import kotlinx.datetime.Instant /** - * Use case that returns [Boolean] if user should be asked for a feedback about call quality or not. + * Use case to determine if the call feedback should be asked. */ interface ShouldAskCallFeedbackUseCase { - /** - * @return [Boolean] if user should be asked for a feedback about call quality or not. - */ - suspend operator fun invoke(): Boolean + suspend operator fun invoke( + establishedTime: Instant?, + currentTime: Instant = DateTimeUtil.currentInstant() + ): ShouldAskCallFeedbackUseCaseResult } @Suppress("FunctionNaming") @@ -38,9 +39,46 @@ internal fun ShouldAskCallFeedbackUseCase( userConfigRepository: UserConfigRepository ) = object : ShouldAskCallFeedbackUseCase { - override suspend fun invoke(): Boolean = - userConfigRepository.getNextTimeForCallFeedback().map { + override suspend fun invoke( + establishedTime: Instant?, + currentTime: Instant + ): ShouldAskCallFeedbackUseCaseResult { + val callDurationInSeconds = establishedTime?.let { + DateTimeUtil.calculateMillisDifference(it, currentTime) / MILLIS_IN_SECOND + } ?: 0L + + return when { + callDurationInSeconds in 1.. { + ShouldAskCallFeedbackUseCaseResult.ShouldNotAskCallFeedback.CallDurationIsLessThanOneMinute(callDurationInSeconds) + } + + !isNextTimeForCallFeedbackReached() -> { + ShouldAskCallFeedbackUseCaseResult.ShouldNotAskCallFeedback.NextTimeForCallFeedbackIsNotReached(callDurationInSeconds) + } + + else -> { + ShouldAskCallFeedbackUseCaseResult.ShouldAskCallFeedback(callDurationInSeconds) + } + } + } + + private suspend fun isNextTimeForCallFeedbackReached(): Boolean { + return userConfigRepository.getNextTimeForCallFeedback().map { it > 0L && DateTimeUtil.currentInstant() > Instant.fromEpochMilliseconds(it) }.getOrElse(true) + } +} +sealed class ShouldAskCallFeedbackUseCaseResult { + data class ShouldAskCallFeedback(val callDurationInSeconds: Long) : ShouldAskCallFeedbackUseCaseResult() + sealed class ShouldNotAskCallFeedback(val reason: String) : ShouldAskCallFeedbackUseCaseResult() { + data class CallDurationIsLessThanOneMinute(val callDurationInSeconds: Long) : + ShouldNotAskCallFeedback("Call duration is less than 1 minute") + + data class NextTimeForCallFeedbackIsNotReached(val callDurationInSeconds: Long) : + ShouldNotAskCallFeedback("Next time for call feedback is not reached") + } } + +private const val MILLIS_IN_SECOND = 1_000L +private const val ONE_MINUTE = 60 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 index 9d96e58acf..4683b8a43e 100644 --- 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 @@ -161,12 +161,29 @@ class CreateAndPersistRecentlyEndedCallMetadataUseCaseTest { fun withOutgoingCall() = apply { every { callRepository.getCallMetadataProfile() } - .returns(CallMetadataProfile(mapOf(CONVERSATION_ID to callMetadata()))) + .returns( + CallMetadataProfile( + mapOf( + CONVERSATION_ID to callMetadata().copy( + callStatus = CallStatus.STARTED + ) + ) + ) + ) } fun withIncomingCall() = apply { every { callRepository.getCallMetadataProfile() } - .returns(CallMetadataProfile(mapOf(CONVERSATION_ID to callMetadata().copy(callerId = CALLER_ID.copy(value = "external"))))) + .returns( + CallMetadataProfile( + mapOf( + CONVERSATION_ID to callMetadata().copy( + callerId = CALLER_ID.copy(value = "external"), + callStatus = CallStatus.INCOMING + ) + ) + ) + ) } suspend fun withConversationMembers() = apply { diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallUseCaseTest.kt index 76cd652160..096b93bc54 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallUseCaseTest.kt @@ -24,6 +24,7 @@ import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.user.ShouldAskCallFeedbackUseCase +import com.wire.kalium.logic.feature.user.ShouldAskCallFeedbackUseCaseResult import com.wire.kalium.logic.test_util.TestKaliumDispatcher import com.wire.kalium.logic.util.arrangement.repository.CallManagerArrangement import com.wire.kalium.logic.util.arrangement.repository.CallManagerArrangementImpl @@ -69,7 +70,9 @@ class EndCallUseCaseTest { }.wasInvoked(once) coVerify { - arrangement.endCallResultListener.onCallEndedAskForFeedback(eq(false)) + arrangement.endCallResultListener.onCallEndedAskForFeedback( + eq(ShouldAskCallFeedbackUseCaseResult.ShouldAskCallFeedback(100)) + ) }.wasInvoked(once) } @@ -100,7 +103,9 @@ class EndCallUseCaseTest { }.wasInvoked(once) coVerify { - arrangement.endCallResultListener.onCallEndedAskForFeedback(eq(false)) + arrangement.endCallResultListener.onCallEndedAskForFeedback( + eq(ShouldAskCallFeedbackUseCaseResult.ShouldAskCallFeedback(100)) + ) }.wasInvoked(once) } @@ -131,7 +136,9 @@ class EndCallUseCaseTest { }.wasInvoked(once) coVerify { - arrangement.endCallResultListener.onCallEndedAskForFeedback(eq(false)) + arrangement.endCallResultListener.onCallEndedAskForFeedback( + eq(ShouldAskCallFeedbackUseCaseResult.ShouldAskCallFeedback(100)) + ) }.wasInvoked(once) } @@ -162,7 +169,9 @@ class EndCallUseCaseTest { }.wasInvoked(once) coVerify { - arrangement.endCallResultListener.onCallEndedAskForFeedback(eq(false)) + arrangement.endCallResultListener.onCallEndedAskForFeedback( + eq(ShouldAskCallFeedbackUseCaseResult.ShouldAskCallFeedback(100)) + ) }.wasInvoked(once) } @@ -192,7 +201,9 @@ class EndCallUseCaseTest { }.wasNotInvoked() coVerify { - arrangement.endCallResultListener.onCallEndedAskForFeedback(eq(false)) + arrangement.endCallResultListener.onCallEndedAskForFeedback( + eq(ShouldAskCallFeedbackUseCaseResult.ShouldAskCallFeedback(100)) + ) }.wasInvoked(once) } @@ -220,8 +231,10 @@ class EndCallUseCaseTest { ) } - suspend fun withShouldAskCallFeedback(should: Boolean = false) { - coEvery { shouldAskCallFeedback.invoke() }.returns(should) + suspend fun withShouldAskCallFeedback( + result: ShouldAskCallFeedbackUseCaseResult = ShouldAskCallFeedbackUseCaseResult.ShouldAskCallFeedback(100) + ) { + coEvery { shouldAskCallFeedback.invoke(any(), any()) }.returns(result) } suspend fun withOnCallEndedAskForFeedback() { diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/ShouldAskCallFeedbackUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/ShouldAskCallFeedbackUseCaseTest.kt index 5b66a16c5d..e323d2023a 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/ShouldAskCallFeedbackUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/ShouldAskCallFeedbackUseCaseTest.kt @@ -25,58 +25,70 @@ import com.wire.kalium.logic.util.arrangement.repository.UserConfigRepositoryArr import com.wire.kalium.util.DateTimeUtil import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant import kotlin.test.Test -import kotlin.test.assertFalse import kotlin.test.assertTrue import kotlin.time.Duration.Companion.days class ShouldAskCallFeedbackUseCaseTest { @Test - fun givenNoNextTimeForCallFeedbackSaved_whenInvoked_thenTrueIsReturned() = runTest { + fun givenNoNextTimeForCallFeedbackSaved_whenInvoked_thenShouldAskCallFeedbackIsReturned() = runTest { val (_, useCase) = Arrangement().arrange { withGetNextTimeForCallFeedback(StorageFailure.DataNotFound.left()) } - val result = useCase() + val result = useCase(ESTABLISHED_TIME) - assertTrue(result) + assertTrue(result is ShouldAskCallFeedbackUseCaseResult.ShouldAskCallFeedback) } @Test - fun givenNextTimeForCallFeedbackInPast_whenInvoked_thenTrueIsReturned() = runTest { + fun givenNextTimeForCallFeedbackInPast_whenInvoked_thenShouldAskCallFeedbackIsReturned() = runTest { val nextTimeToAsk = DateTimeUtil.currentInstant().minus(1.days).toEpochMilliseconds() val (_, useCase) = Arrangement().arrange { withGetNextTimeForCallFeedback(nextTimeToAsk.right()) } - val result = useCase() + val result = useCase(ESTABLISHED_TIME) - assertTrue(result) + assertTrue(result is ShouldAskCallFeedbackUseCaseResult.ShouldAskCallFeedback) } @Test - fun givenNextTimeForCallFeedbackInFuture_whenInvoked_thenFalseIsReturned() = runTest { + fun givenNextTimeForCallFeedbackInFuture_whenInvoked_thenNextTimeForCallFeedbackIsNotReachedIsReturned() = runTest { val nextTimeToAsk = DateTimeUtil.currentInstant().plus(1.days).toEpochMilliseconds() val (_, useCase) = Arrangement().arrange { withGetNextTimeForCallFeedback(nextTimeToAsk.right()) } - val result = useCase() + val result = useCase(ESTABLISHED_TIME) - assertFalse(result) + assertTrue(result is ShouldAskCallFeedbackUseCaseResult.ShouldNotAskCallFeedback.NextTimeForCallFeedbackIsNotReached) } @Test - fun givenNextTimeForCallFeedbackIsNegative_whenInvoked_thenFalseIsReturned() = runTest { + fun givenNextTimeForCallFeedbackIsNegative_whenInvoked_thenNextTimeForCallFeedbackIsNotReachedReturned() = runTest { val nextTimeToAsk = -1L val (_, useCase) = Arrangement().arrange { withGetNextTimeForCallFeedback(nextTimeToAsk.right()) } - val result = useCase() + val result = useCase(ESTABLISHED_TIME) - assertFalse(result) + assertTrue(result is ShouldAskCallFeedbackUseCaseResult.ShouldNotAskCallFeedback.NextTimeForCallFeedbackIsNotReached) + } + + @Test + fun givenCallDurationLessThanOneMinute_whenInvoked_thenCallDurationIsLessThanOneMinuteIsReturned() = runTest { + val nextTimeToAsk = -1L + val (_, useCase) = Arrangement().arrange { + withGetNextTimeForCallFeedback(nextTimeToAsk.right()) + } + + val result = useCase(ESTABLISHED_TIME, CURRENT_TIME) + + assertTrue(result is ShouldAskCallFeedbackUseCaseResult.ShouldNotAskCallFeedback.CallDurationIsLessThanOneMinute) } private class Arrangement : UserConfigRepositoryArrangement by UserConfigRepositoryArrangementImpl() { @@ -86,4 +98,9 @@ class ShouldAskCallFeedbackUseCaseTest { return this to ShouldAskCallFeedbackUseCase(userConfigRepository) } } + + companion object { + val ESTABLISHED_TIME = Instant.parse("2024-02-03T15:36:00.000Z") + val CURRENT_TIME = Instant.parse("2024-02-03T15:36:09.000Z") + } }