From a9c9da86d5409bc1dbb3463223e9ca5573c1f831 Mon Sep 17 00:00:00 2001 From: ohassine Date: Fri, 31 Jan 2025 18:55:44 +0100 Subject: [PATCH 01/12] feat: add calling.call_quality_review segmentations --- .../wire/android/ui/CallFeedbackViewModel.kt | 186 ++++++++++++++++++ .../com/wire/android/ui/WireActivity.kt | 9 +- .../feature/analytics/model/AnalyticsEvent.kt | 91 ++++++--- 3 files changed, 256 insertions(+), 30 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt diff --git a/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt new file mode 100644 index 00000000000..374aa8acf28 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt @@ -0,0 +1,186 @@ +/* + * Wire + * Copyright (C) 2025 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.android.ui + +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.feature.analytics.AnonymousAnalyticsManager +import com.wire.android.feature.analytics.model.AnalyticsEvent +import com.wire.android.ui.analytics.IsAnalyticsAvailableUseCase +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.data.sync.SyncState +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase +import com.wire.kalium.logic.feature.session.CurrentSessionResult +import com.wire.kalium.logic.feature.user.ShouldAskCallFeedbackUseCaseResult +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class CallFeedbackViewModel @Inject constructor( + @KaliumCoreLogic private val coreLogic: CoreLogic, + private val currentSessionFlow: CurrentSessionFlowUseCase, + private val isAnalyticsAvailable: IsAnalyticsAvailableUseCase, + private val analyticsManager: AnonymousAnalyticsManager +) : ViewModel() { + + val showCallFeedbackFlow = MutableSharedFlow() + + private var currentUserId by mutableStateOf(null) + + init { + viewModelScope.launch { + currentSessionFlow() + .distinctUntilChanged() + .collectLatest { session -> + if (session is CurrentSessionResult.Success && session.accountInfo.isValid()) { + currentUserId = session.accountInfo.userId + coreLogic.getSessionScope(currentUserId!!).observeSyncState().firstOrNull { it == SyncState.Live }?.let { + observeAskCallFeedback(currentUserId!!) + } + } else { + currentUserId = null + } + } + } + } + + private suspend fun observeAskCallFeedback(userId: UserId) = + coreLogic.getSessionScope(userId).calls.observeAskCallFeedbackUseCase().collect { shouldAskFeedback -> + if (!isAnalyticsAvailable(userId)) { + return@collect + } + + when (shouldAskFeedback) { + is ShouldAskCallFeedbackUseCaseResult.ShouldAskCallFeedback -> { + Log.d("callFeedbackViewModel", "observeAskCallFeedback: ShouldAskCallFeedback") + showCallFeedbackFlow.emit(Unit) + } + + is ShouldAskCallFeedbackUseCaseResult.ShouldNotAskCallFeedback.CallDurationIsLessThanOneMinute -> { + Log.d( + "callFeedbackViewModel", + "observeAskCallFeedback: shortCall = duration is ${shouldAskFeedback.callDurationInSeconds.toInt()}" + ) + currentUserId?.let { + val recentlyEndedCallMetadata = coreLogic.getSessionScope(it).calls.observeRecentlyEndedCallMetadata().first() + analyticsManager.sendEvent( + with(recentlyEndedCallMetadata) { + AnalyticsEvent.CallQualityFeedback.TooShort( + callDuration = shouldAskFeedback.callDurationInSeconds.toInt(), + isTeamMember = isTeamMember, + participantsCount = callDetails.callParticipantsCount, + isScreenSharedDuringCall = callDetails.isCallScreenShare, + isCameraEnabledDuringCall = callDetails.callVideoEnabled + ) + } + ) + } + } + + is ShouldAskCallFeedbackUseCaseResult.ShouldNotAskCallFeedback.NextTimeForCallFeedbackIsNotReached -> { + Log.d( + "callFeedbackViewModel", + "observeAskCallFeedback: NextTimeForCallFeedbackIsNotReached = duration is ${shouldAskFeedback.callDurationInSeconds.toInt()}" + ) + currentUserId?.let { + val recentlyEndedCallMetadata = coreLogic.getSessionScope(it).calls.observeRecentlyEndedCallMetadata().first() + + // call not established + if (shouldAskFeedback.callDurationInSeconds.toInt() == 0) { + analyticsManager.sendEvent( + AnalyticsEvent.CallQualityFeedback.Muted( + callDuration = shouldAskFeedback.callDurationInSeconds.toInt(), + isTeamMember = recentlyEndedCallMetadata.isTeamMember, + participantsCount = 0, + isScreenSharedDuringCall = false, + isCameraEnabledDuringCall = false + ) + ) + } else { + analyticsManager.sendEvent( + with(recentlyEndedCallMetadata) { + AnalyticsEvent.CallQualityFeedback.TooShort( + callDuration = shouldAskFeedback.callDurationInSeconds.toInt(), + isTeamMember = isTeamMember, + participantsCount = callDetails.callParticipantsCount, + isScreenSharedDuringCall = callDetails.isCallScreenShare, + isCameraEnabledDuringCall = callDetails.callVideoEnabled + ) + } + ) + } + + } + + } + } + } + + fun rateCall(rate: Int, doNotAsk: Boolean) { + currentUserId?.let { + viewModelScope.launch { + val recentlyEndedCallMetadata = coreLogic.getSessionScope(it).calls.observeRecentlyEndedCallMetadata().first() + analyticsManager.sendEvent( + with(recentlyEndedCallMetadata) { + AnalyticsEvent.CallQualityFeedback.Answered( + score = rate, + callDuration = callDetails.callDurationInSeconds.toInt(), + isTeamMember = isTeamMember, + participantsCount = callDetails.callParticipantsCount, + isScreenSharedDuringCall = callDetails.isCallScreenShare, + isCameraEnabledDuringCall = callDetails.callVideoEnabled + ) + } + ) + coreLogic.getSessionScope(it).calls.updateNextTimeCallFeedback(doNotAsk) + } + } + } + + fun skipCallFeedback(doNotAsk: Boolean) { + currentUserId?.let { + viewModelScope.launch { + val recentlyEndedCallMetadata = coreLogic.getSessionScope(it).calls.observeRecentlyEndedCallMetadata().first() + coreLogic.getSessionScope(it).calls.updateNextTimeCallFeedback(doNotAsk) + analyticsManager.sendEvent( + with(recentlyEndedCallMetadata) { + AnalyticsEvent.CallQualityFeedback.Dismissed( + callDuration = callDetails.callDurationInSeconds.toInt(), + isTeamMember = isTeamMember, + participantsCount = callDetails.callParticipantsCount, + isScreenSharedDuringCall = callDetails.isCallScreenShare, + isCameraEnabledDuringCall = callDetails.callVideoEnabled + ) + } + ) + } + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index 3dc15193a48..323acf65e8f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -144,6 +144,7 @@ class WireActivity : AppCompatActivity() { private val viewModel: WireActivityViewModel by viewModels() private val featureFlagNotificationViewModel: FeatureFlagNotificationViewModel by viewModels() + private val callFeedbackViewModel: CallFeedbackViewModel by viewModels() private val commonTopAppBarViewModel: CommonTopAppBarViewModel by viewModels() private val legalHoldRequestedViewModel: LegalHoldRequestedViewModel by viewModels() @@ -383,7 +384,7 @@ class WireActivity : AppCompatActivity() { val context = LocalContext.current val callFeedbackSheetState = rememberWireModalSheetState(onDismissAction = { - featureFlagNotificationViewModel.skipCallFeedback(false) + callFeedbackViewModel.skipCallFeedback(false) }) with(featureFlagNotificationViewModel.featureFlagState) { if (shouldShowTeamAppLockDialog) { @@ -557,8 +558,8 @@ class WireActivity : AppCompatActivity() { CallFeedbackDialog( sheetState = callFeedbackSheetState, - onRated = featureFlagNotificationViewModel::rateCall, - onSkipClicked = featureFlagNotificationViewModel::skipCallFeedback + onRated = callFeedbackViewModel::rateCall, + onSkipClicked = callFeedbackViewModel::skipCallFeedback ) if (startGettingE2EICertificate) { @@ -572,7 +573,7 @@ class WireActivity : AppCompatActivity() { } LaunchedEffect(Unit) { - featureFlagNotificationViewModel.showCallFeedbackFlow.collectLatest { + callFeedbackViewModel.showCallFeedbackFlow.collectLatest { callFeedbackSheetState.show() } } diff --git a/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsEvent.kt b/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsEvent.kt index af601cad31c..b4b73273d7f 100644 --- a/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsEvent.kt +++ b/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsEvent.kt @@ -20,29 +20,29 @@ package com.wire.android.feature.analytics.model import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_ENDED import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_ENDED_AV_SWITCH_TOGGLE import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_ENDED_CALL_DIRECTION -import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_ENDED_CALL_DURATION -import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_ENDED_CALL_PARTICIPANTS import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_ENDED_CALL_SCREEN_SHARE -import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_ENDED_CALL_VIDEO import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_ENDED_CONVERSATION_GUESTS import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_ENDED_CONVERSATION_GUESTS_PRO import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_ENDED_CONVERSATION_SERVICES import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_ENDED_CONVERSATION_SIZE import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_ENDED_CONVERSATION_TYPE import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_ENDED_END_REASON -import com.wire.android.feature.analytics.model.AnalyticsEventConstants.IS_TEAM_MEMBER import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_ENDED_UNIQUE_SCREEN_SHARE +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_QUALITY_REVIEW_CALL_SCREEN_SHARE +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_QUALITY_REVIEW_CALL_TOO_SHORT import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_QUALITY_REVIEW_IGNORE_REASON -import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_QUALITY_REVIEW_IGNORE_REASON_KEY import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_QUALITY_REVIEW_LABEL_ANSWERED import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_QUALITY_REVIEW_LABEL_DISMISSED import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_QUALITY_REVIEW_LABEL_KEY -import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_QUALITY_REVIEW_LABEL_NOT_DISPLAYED import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_QUALITY_REVIEW_SCORE_KEY +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALL_DURATION +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALL_PARTICIPANTS +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALL_VIDEO import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CLICKED_CREATE_TEAM import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CLICKED_DISMISS_CTA import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CLICKED_PERSONAL_MIGRATION_CTA_EVENT import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CONTRIBUTED_LOCATION +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.IS_TEAM_MEMBER import com.wire.android.feature.analytics.model.AnalyticsEventConstants.MESSAGE_ACTION_KEY import com.wire.android.feature.analytics.model.AnalyticsEventConstants.MIGRATION_DOT_ACTIVE import com.wire.android.feature.analytics.model.AnalyticsEventConstants.MODAL_BACK_TO_WIRE_CLICKED @@ -114,38 +114,76 @@ interface AnalyticsEvent { override val key: String get() = AnalyticsEventConstants.CALLING_QUALITY_REVIEW val label: String + val callDuration: Int + val isTeamMember: Boolean + val participantsCount: Int + val isScreenSharedDuringCall: Boolean + val isCameraEnabledDuringCall: Boolean override fun toSegmentation(): Map { return mapOf( - CALLING_QUALITY_REVIEW_LABEL_KEY to label + CALLING_QUALITY_REVIEW_LABEL_KEY to label, + CALL_DURATION to callDuration, + IS_TEAM_MEMBER to isTeamMember, + CALL_PARTICIPANTS to participantsCount, + CALLING_QUALITY_REVIEW_CALL_SCREEN_SHARE to isScreenSharedDuringCall, + CALL_VIDEO to isCameraEnabledDuringCall ) } - data class Answered(val score: Int) : CallQualityFeedback { + data class Answered( + val score: Int, + override val callDuration: Int, + override val isTeamMember: Boolean, + override val participantsCount: Int, + override val isScreenSharedDuringCall: Boolean, + override val isCameraEnabledDuringCall: Boolean + ) : CallQualityFeedback { override val label: String get() = CALLING_QUALITY_REVIEW_LABEL_ANSWERED override fun toSegmentation(): Map { return mapOf( + CALLING_QUALITY_REVIEW_SCORE_KEY to score, CALLING_QUALITY_REVIEW_LABEL_KEY to label, - CALLING_QUALITY_REVIEW_SCORE_KEY to score + CALL_DURATION to callDuration, + IS_TEAM_MEMBER to isTeamMember, + CALL_PARTICIPANTS to participantsCount, + CALLING_QUALITY_REVIEW_CALL_SCREEN_SHARE to isScreenSharedDuringCall, + CALL_VIDEO to isCameraEnabledDuringCall ) } } - data object NotDisplayed : CallQualityFeedback { + data class TooShort( + override val callDuration: Int, + override val isTeamMember: Boolean, + override val participantsCount: Int, + override val isScreenSharedDuringCall: Boolean, + override val isCameraEnabledDuringCall: Boolean + ) : CallQualityFeedback { override val label: String - get() = CALLING_QUALITY_REVIEW_LABEL_NOT_DISPLAYED + get() = CALLING_QUALITY_REVIEW_CALL_TOO_SHORT + } - override fun toSegmentation(): Map { - return mapOf( - CALLING_QUALITY_REVIEW_LABEL_KEY to label, - CALLING_QUALITY_REVIEW_IGNORE_REASON_KEY to CALLING_QUALITY_REVIEW_IGNORE_REASON - ) - } + data class Muted( + override val callDuration: Int, + override val isTeamMember: Boolean, + override val participantsCount: Int, + override val isScreenSharedDuringCall: Boolean, + override val isCameraEnabledDuringCall: Boolean + ) : CallQualityFeedback { + override val label: String + get() = CALLING_QUALITY_REVIEW_IGNORE_REASON } - data object Dismissed : CallQualityFeedback { + data class Dismissed( + override val callDuration: Int, + override val isTeamMember: Boolean, + override val participantsCount: Int, + override val isScreenSharedDuringCall: Boolean, + override val isCameraEnabledDuringCall: Boolean + ) : CallQualityFeedback { override val label: String get() = CALLING_QUALITY_REVIEW_LABEL_DISMISSED } @@ -160,16 +198,16 @@ interface AnalyticsEvent { CALLING_ENDED_CALL_SCREEN_SHARE to metadata.callDetails.screenShareDurationInSeconds, CALLING_ENDED_UNIQUE_SCREEN_SHARE to metadata.callDetails.callScreenShareUniques, CALLING_ENDED_CALL_DIRECTION to metadata.toCallDirection(), - CALLING_ENDED_CALL_DURATION to metadata.callDetails.callDurationInSeconds, + CALL_DURATION to metadata.callDetails.callDurationInSeconds, CALLING_ENDED_CONVERSATION_TYPE to metadata.toConversationType(), CALLING_ENDED_CONVERSATION_SIZE to metadata.conversationDetails.conversationSize, CALLING_ENDED_CONVERSATION_GUESTS to metadata.conversationDetails.conversationGuests, CALLING_ENDED_CONVERSATION_GUESTS_PRO to metadata.conversationDetails.conversationGuestsPro, - CALLING_ENDED_CALL_PARTICIPANTS to metadata.callDetails.callParticipantsCount, + CALL_PARTICIPANTS to metadata.callDetails.callParticipantsCount, CALLING_ENDED_END_REASON to metadata.callEndReason, CALLING_ENDED_CONVERSATION_SERVICES to metadata.callDetails.conversationServices, CALLING_ENDED_AV_SWITCH_TOGGLE to metadata.callDetails.callAVSwitchToggle, - CALLING_ENDED_CALL_VIDEO to metadata.callDetails.callVideoEnabled, + CALL_VIDEO to metadata.callDetails.callVideoEnabled, ) } @@ -419,11 +457,15 @@ object AnalyticsEventConstants { const val CALLING_QUALITY_REVIEW = "calling.call_quality_review" const val CALLING_QUALITY_REVIEW_LABEL_KEY = "label" const val CALLING_QUALITY_REVIEW_LABEL_ANSWERED = "answered" - const val CALLING_QUALITY_REVIEW_LABEL_NOT_DISPLAYED = "not-displayed" const val CALLING_QUALITY_REVIEW_LABEL_DISMISSED = "dismissed" const val CALLING_QUALITY_REVIEW_SCORE_KEY = "score" - const val CALLING_QUALITY_REVIEW_IGNORE_REASON_KEY = "ignore-reason" const val CALLING_QUALITY_REVIEW_IGNORE_REASON = "muted" + const val CALLING_QUALITY_REVIEW_CALL_TOO_SHORT = "call_too_short" + const val CALLING_QUALITY_REVIEW_CALL_SCREEN_SHARE = "call_screen_share" + + const val CALL_DURATION = "call_duration" + const val CALL_PARTICIPANTS = "call_participants" + const val CALL_VIDEO = "call_video" /** * Call ended @@ -431,16 +473,13 @@ object AnalyticsEventConstants { const val CALLING_ENDED_CALL_SCREEN_SHARE = "call_screen_share_duration" const val CALLING_ENDED_UNIQUE_SCREEN_SHARE = "call_screen_share_unique" const val CALLING_ENDED_CALL_DIRECTION = "call_direction" - const val CALLING_ENDED_CALL_DURATION = "call_duration" const val CALLING_ENDED_CONVERSATION_TYPE = "conversation_type" const val CALLING_ENDED_CONVERSATION_SIZE = "conversation_size" const val CALLING_ENDED_CONVERSATION_GUESTS = "conversation_guests" const val CALLING_ENDED_CONVERSATION_GUESTS_PRO = "conversation_guest_pro" - const val CALLING_ENDED_CALL_PARTICIPANTS = "call_participants" const val CALLING_ENDED_END_REASON = "call_end_reason" const val CALLING_ENDED_CONVERSATION_SERVICES = "conversation_services" const val CALLING_ENDED_AV_SWITCH_TOGGLE = "call_av_switch_toggle" - const val CALLING_ENDED_CALL_VIDEO = "call_video" /** * Backup From 7b3e2accbf69e08751b2f5ed7c96cd6aef9f3a4a Mon Sep 17 00:00:00 2001 From: ohassine Date: Tue, 4 Feb 2025 13:51:51 +0100 Subject: [PATCH 02/12] feat: cover CallFeedbackViewModel with unit test --- .../wire/android/ui/CallFeedbackViewModel.kt | 44 +--- .../sync/FeatureFlagNotificationViewModel.kt | 30 --- .../android/ui/CallFeedbackViewModelTest.kt | 211 ++++++++++++++++++ .../FeatureFlagNotificationViewModelTest.kt | 45 ---- 4 files changed, 220 insertions(+), 110 deletions(-) create mode 100644 app/src/test/kotlin/com/wire/android/ui/CallFeedbackViewModelTest.kt diff --git a/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt index 374aa8acf28..339d2432bb5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt @@ -17,7 +17,6 @@ */ package com.wire.android.ui -import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -47,7 +46,7 @@ class CallFeedbackViewModel @Inject constructor( @KaliumCoreLogic private val coreLogic: CoreLogic, private val currentSessionFlow: CurrentSessionFlowUseCase, private val isAnalyticsAvailable: IsAnalyticsAvailableUseCase, - private val analyticsManager: AnonymousAnalyticsManager + private val analyticsManager: AnonymousAnalyticsManager, ) : ViewModel() { val showCallFeedbackFlow = MutableSharedFlow() @@ -79,15 +78,10 @@ class CallFeedbackViewModel @Inject constructor( when (shouldAskFeedback) { is ShouldAskCallFeedbackUseCaseResult.ShouldAskCallFeedback -> { - Log.d("callFeedbackViewModel", "observeAskCallFeedback: ShouldAskCallFeedback") showCallFeedbackFlow.emit(Unit) } is ShouldAskCallFeedbackUseCaseResult.ShouldNotAskCallFeedback.CallDurationIsLessThanOneMinute -> { - Log.d( - "callFeedbackViewModel", - "observeAskCallFeedback: shortCall = duration is ${shouldAskFeedback.callDurationInSeconds.toInt()}" - ) currentUserId?.let { val recentlyEndedCallMetadata = coreLogic.getSessionScope(it).calls.observeRecentlyEndedCallMetadata().first() analyticsManager.sendEvent( @@ -105,40 +99,20 @@ class CallFeedbackViewModel @Inject constructor( } is ShouldAskCallFeedbackUseCaseResult.ShouldNotAskCallFeedback.NextTimeForCallFeedbackIsNotReached -> { - Log.d( - "callFeedbackViewModel", - "observeAskCallFeedback: NextTimeForCallFeedbackIsNotReached = duration is ${shouldAskFeedback.callDurationInSeconds.toInt()}" - ) currentUserId?.let { val recentlyEndedCallMetadata = coreLogic.getSessionScope(it).calls.observeRecentlyEndedCallMetadata().first() - - // call not established - if (shouldAskFeedback.callDurationInSeconds.toInt() == 0) { - analyticsManager.sendEvent( + analyticsManager.sendEvent( + with(recentlyEndedCallMetadata) { AnalyticsEvent.CallQualityFeedback.Muted( callDuration = shouldAskFeedback.callDurationInSeconds.toInt(), - isTeamMember = recentlyEndedCallMetadata.isTeamMember, - participantsCount = 0, - isScreenSharedDuringCall = false, - isCameraEnabledDuringCall = false + isTeamMember = isTeamMember, + participantsCount = callDetails.callParticipantsCount, + isScreenSharedDuringCall = callDetails.isCallScreenShare, + isCameraEnabledDuringCall = callDetails.callVideoEnabled ) - ) - } else { - analyticsManager.sendEvent( - with(recentlyEndedCallMetadata) { - AnalyticsEvent.CallQualityFeedback.TooShort( - callDuration = shouldAskFeedback.callDurationInSeconds.toInt(), - isTeamMember = isTeamMember, - participantsCount = callDetails.callParticipantsCount, - isScreenSharedDuringCall = callDetails.isCallScreenShare, - isCameraEnabledDuringCall = callDetails.callVideoEnabled - ) - } - ) - } - + } + ) } - } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt index 0c98b27c9b8..980a420fecb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt @@ -128,7 +128,6 @@ class FeatureFlagNotificationViewModel @Inject constructor( launch { setE2EIRequiredState(userId) } launch { setTeamAppLockFeatureFlag(userId) } launch { observeCallEndedBecauseOfConversationDegraded(userId) } - launch { observeAskCallFeedback(userId) } launch { observeShouldNotifyForRevokedCertificate(userId) } } } @@ -228,17 +227,6 @@ class FeatureFlagNotificationViewModel @Inject constructor( featureFlagState = featureFlagState.copy(showCallEndedBecauseOfConversationDegraded = true) } - private suspend fun observeAskCallFeedback(userId: UserId) = - coreLogic.getSessionScope(userId).calls.observeAskCallFeedbackUseCase().collect { shouldAskFeedback -> - if (!isAnalyticsAvailable(userId)) { - // Analytics is disabled. Do nothing. - } else if (shouldAskFeedback) { - showCallFeedbackFlow.emit(Unit) - } else { - analyticsManager.sendEvent(AnalyticsEvent.CallQualityFeedback.NotDisplayed) - } - } - fun dismissSelfDeletingMessagesDialog() { featureFlagState = featureFlagState.copy(shouldShowSelfDeletingMessagesDialog = false) viewModelScope.launch { @@ -354,24 +342,6 @@ class FeatureFlagNotificationViewModel @Inject constructor( featureFlagState = featureFlagState.copy(e2EIResult = null) } - fun rateCall(rate: Int, doNotAsk: Boolean) { - currentUserId?.let { - viewModelScope.launch { - analyticsManager.sendEvent(AnalyticsEvent.CallQualityFeedback.Answered(rate)) - coreLogic.getSessionScope(it).calls.updateNextTimeCallFeedback(doNotAsk) - } - } - } - - fun skipCallFeedback(doNotAsk: Boolean) { - currentUserId?.let { - viewModelScope.launch { - coreLogic.getSessionScope(it).calls.updateNextTimeCallFeedback(doNotAsk) - analyticsManager.sendEvent(AnalyticsEvent.CallQualityFeedback.Dismissed) - } - } - } - companion object { private const val TAG = "FeatureFlagNotificationViewModel" } diff --git a/app/src/test/kotlin/com/wire/android/ui/CallFeedbackViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/CallFeedbackViewModelTest.kt new file mode 100644 index 00000000000..4efabf18ee2 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/CallFeedbackViewModelTest.kt @@ -0,0 +1,211 @@ +/* + * Wire + * Copyright (C) 2025 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.android.ui + +import app.cash.turbine.test +import com.wire.android.config.CoroutineTestExtension +import com.wire.android.feature.analytics.AnonymousAnalyticsManager +import com.wire.android.feature.analytics.model.AnalyticsEvent +import com.wire.android.framework.TestUser +import com.wire.android.ui.analytics.IsAnalyticsAvailableUseCase +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.data.auth.AccountInfo +import com.wire.kalium.logic.data.call.RecentlyEndedCallMetadata +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.sync.SyncState +import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase +import com.wire.kalium.logic.feature.session.CurrentSessionResult +import com.wire.kalium.logic.feature.user.ShouldAskCallFeedbackUseCaseResult +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(CoroutineTestExtension::class) +class CallFeedbackViewModelTest { + + @Test + fun `given analytics is not available when use case is observed then it should skip sending feedback event`() = runTest { + // Given + val (arrangement, viewModel) = Arrangement() + .arrange() + + coEvery { arrangement.isAnalyticsAvailable(any()) } returns false + + viewModel.showCallFeedbackFlow.test { + expectNoEvents() + } + } + + @Test + fun `given short call when use case is observed then send TooShort event`() = runTest { + val shortDuration = 2L + val (arrangement, _) = Arrangement() + .withObserveAskCallFeedbackUseCaseReturning( + ShouldAskCallFeedbackUseCaseResult.ShouldNotAskCallFeedback.CallDurationIsLessThanOneMinute( + shortDuration + ) + ) + .arrange() + + coVerify(exactly = 1) { + arrangement.analyticsManager.sendEvent( + AnalyticsEvent.CallQualityFeedback.TooShort( + callDuration = shortDuration.toInt(), + isTeamMember = recentlyEndedCallMetadata.isTeamMember, + participantsCount = recentlyEndedCallMetadata.callDetails.callParticipantsCount, + isScreenSharedDuringCall = recentlyEndedCallMetadata.callDetails.isCallScreenShare, + isCameraEnabledDuringCall = recentlyEndedCallMetadata.callDetails.callVideoEnabled + ) + ) + } + } + @Test + fun `given NextTimeForCallFeedbackIsNotReached when use case is observed then send Muted event`() = runTest { + val (arrangement, _) = Arrangement() + .withObserveAskCallFeedbackUseCaseReturning( + ShouldAskCallFeedbackUseCaseResult.ShouldNotAskCallFeedback.NextTimeForCallFeedbackIsNotReached( + CALL_DURATION + ) + ) + .arrange() + + coVerify(exactly = 1) { + arrangement.analyticsManager.sendEvent( + AnalyticsEvent.CallQualityFeedback.Muted( + callDuration = CALL_DURATION.toInt(), + isTeamMember = recentlyEndedCallMetadata.isTeamMember, + participantsCount = recentlyEndedCallMetadata.callDetails.callParticipantsCount, + isScreenSharedDuringCall = recentlyEndedCallMetadata.callDetails.isCallScreenShare, + isCameraEnabledDuringCall = recentlyEndedCallMetadata.callDetails.callVideoEnabled + ) + ) + } + } + + @Test + fun `given A rate Call is displayed when sending score then invoke event for score with value`() = runTest { + val (arrangement, viewModel) = Arrangement() + .arrange() + + viewModel.rateCall(5, false) + + coVerify(exactly = 1) { + arrangement.analyticsManager.sendEvent( + AnalyticsEvent.CallQualityFeedback.Answered( + score = 5, + callDuration = recentlyEndedCallMetadata.callDetails.callDurationInSeconds.toInt(), + isTeamMember = recentlyEndedCallMetadata.isTeamMember, + participantsCount = recentlyEndedCallMetadata.callDetails.callParticipantsCount, + isScreenSharedDuringCall = recentlyEndedCallMetadata.callDetails.isCallScreenShare, + isCameraEnabledDuringCall = recentlyEndedCallMetadata.callDetails.callVideoEnabled + ) + ) + } + } + + @Test + fun `given a rate call is displayed when dismissing it then invoke event for dismiss`() = runTest { + val (arrangement, viewModel) = Arrangement() + .arrange() + + viewModel.skipCallFeedback(false) + + coVerify(exactly = 1) { + arrangement.analyticsManager.sendEvent( + AnalyticsEvent.CallQualityFeedback.Dismissed( + callDuration = recentlyEndedCallMetadata.callDetails.callDurationInSeconds.toInt(), + isTeamMember = recentlyEndedCallMetadata.isTeamMember, + participantsCount = recentlyEndedCallMetadata.callDetails.callParticipantsCount, + isScreenSharedDuringCall = recentlyEndedCallMetadata.callDetails.isCallScreenShare, + isCameraEnabledDuringCall = recentlyEndedCallMetadata.callDetails.callVideoEnabled + ) + ) + } + } + + private inner class Arrangement { + + @MockK + lateinit var coreLogic: CoreLogic + + @MockK + lateinit var analyticsManager: AnonymousAnalyticsManager + + @MockK + lateinit var currentSessionFlow: CurrentSessionFlowUseCase + + @MockK + lateinit var isAnalyticsAvailable: IsAnalyticsAvailableUseCase + + val viewModel: CallFeedbackViewModel by lazy { + CallFeedbackViewModel( + coreLogic = coreLogic, + currentSessionFlow = currentSessionFlow, + isAnalyticsAvailable = isAnalyticsAvailable, + analyticsManager = analyticsManager + ) + } + + fun withObserveAskCallFeedbackUseCaseReturning(result: ShouldAskCallFeedbackUseCaseResult) = apply { + coEvery { coreLogic.getSessionScope(any()).calls.observeAskCallFeedbackUseCase() } returns flowOf(result) + } + + init { + MockKAnnotations.init(this, relaxUnitFun = true) + coEvery { isAnalyticsAvailable(any()) } returns true + coEvery { currentSessionFlow() } returns flowOf(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.USER_ID))) + coEvery { coreLogic.getSessionScope(any()).observeSyncState() } returns flowOf(SyncState.Live) + coEvery { coreLogic.getSessionScope(any()).calls.observeAskCallFeedbackUseCase() } returns flowOf() + coEvery { coreLogic.getSessionScope(any()).calls.updateNextTimeCallFeedback(any()) } returns Unit + coEvery { coreLogic.getSessionScope(any()).calls.observeRecentlyEndedCallMetadata() } returns flowOf(recentlyEndedCallMetadata) + } + + fun arrange() = this to viewModel + } + + companion object { + const val CALL_DURATION = 100L + val recentlyEndedCallMetadata = RecentlyEndedCallMetadata( + callEndReason = 1, + callDetails = RecentlyEndedCallMetadata.CallDetails( + isCallScreenShare = false, + screenShareDurationInSeconds = 20L, + callScreenShareUniques = 5, + isOutgoingCall = true, + callDurationInSeconds = CALL_DURATION, + callParticipantsCount = 5, + conversationServices = 1, + callAVSwitchToggle = false, + callVideoEnabled = false + ), + conversationDetails = RecentlyEndedCallMetadata.ConversationDetails( + conversationType = Conversation.Type.ONE_ON_ONE, + conversationSize = 5, + conversationGuests = 2, + conversationGuestsPro = 1 + ), + isTeamMember = true + ) + } +} diff --git a/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt index e3f708ed23d..41d16a57c49 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt @@ -22,8 +22,6 @@ import com.wire.android.datastore.GlobalDataStore import com.wire.android.feature.AppLockSource import com.wire.android.feature.DisableAppLockUseCase import com.wire.android.feature.analytics.AnonymousAnalyticsManager -import com.wire.android.feature.analytics.model.AnalyticsEvent -import com.wire.android.feature.analytics.model.AnalyticsEventConstants import com.wire.android.framework.TestUser import com.wire.android.ui.analytics.IsAnalyticsAvailableUseCase import com.wire.android.ui.home.FeatureFlagState @@ -301,49 +299,6 @@ class FeatureFlagNotificationViewModelTest { coVerify(exactly = 1) { arrangement.markNotifyForRevokedCertificateAsNotified() } } - @Test - fun givenARateCallIsDisplayed_whenSendingScore_thenInvokeEventForScoreWithValue() = runTest { - val (arrangement, viewModel) = Arrangement() - .withCurrentSessionsFlow(flowOf(CurrentSessionResult.Success(AccountInfo.Valid(UserId("value", "domain"))))) - .arrange() - - viewModel.rateCall(5, false) - - coVerify(exactly = 1) { - arrangement.analyticsManager.sendEvent( - match { - it is AnalyticsEvent.CallQualityFeedback.Answered && it.score == 5 - } - ) - } - coVerify(exactly = 1) { - arrangement.analyticsManager.sendEvent( - match { - it is AnalyticsEvent.CallQualityFeedback && it.label == - AnalyticsEventConstants.CALLING_QUALITY_REVIEW_LABEL_ANSWERED - } - ) - } - } - - @Test - fun givenARateCallIsDisplayed_whenDismissingIt_thenInvokeEventForDismiss() = runTest { - val (arrangement, viewModel) = Arrangement() - .withCurrentSessionsFlow(flowOf(CurrentSessionResult.Success(AccountInfo.Valid(UserId("value", "domain"))))) - .arrange() - - viewModel.skipCallFeedback(false) - - coVerify(exactly = 1) { - arrangement.analyticsManager.sendEvent( - match { - it is AnalyticsEvent.CallQualityFeedback && it.label == - AnalyticsEventConstants.CALLING_QUALITY_REVIEW_LABEL_DISMISSED - } - ) - } - } - private inner class Arrangement { @MockK From 9dba53a539b0069f6e90ed1c5d0f2312243dce32 Mon Sep 17 00:00:00 2001 From: ohassine Date: Tue, 4 Feb 2025 13:56:57 +0100 Subject: [PATCH 03/12] feat: kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index e21b0a87e27..06b6018565f 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit e21b0a87e2763187d2773652b0cb3992b45e7bc2 +Subproject commit 06b6018565f761ce7c3de056698fcecc45ba5dec From 849590d2063b629a76a3df2cc0eb25509a788af1 Mon Sep 17 00:00:00 2001 From: ohassine Date: Tue, 4 Feb 2025 14:28:45 +0100 Subject: [PATCH 04/12] feat: kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 06b6018565f..0e11108f705 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 06b6018565f761ce7c3de056698fcecc45ba5dec +Subproject commit 0e11108f70505ba80e58a60ce13b969a901635a6 From 76230ada900c98406b5008bbcbb84abcb3d7bcaf Mon Sep 17 00:00:00 2001 From: ohassine Date: Tue, 4 Feb 2025 16:05:30 +0100 Subject: [PATCH 05/12] feat: kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 0e11108f705..2493acd9b00 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 0e11108f70505ba80e58a60ce13b969a901635a6 +Subproject commit 2493acd9b00f845cd0054cf6ae9712ecee478b51 From 2f7bf85de4095afe5a155a4538f1a9d1b6fa956c Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 5 Feb 2025 08:33:39 +0100 Subject: [PATCH 06/12] feat: detekt --- .../sync/FeatureFlagNotificationViewModel.kt | 10 +--------- .../wire/android/ui/CallFeedbackViewModelTest.kt | 6 ++++-- .../sync/FeatureFlagNotificationViewModelTest.kt | 16 +--------------- 3 files changed, 6 insertions(+), 26 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt index 980a420fecb..c7dfa75a4a4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt @@ -28,9 +28,6 @@ import com.wire.android.datastore.GlobalDataStore import com.wire.android.di.KaliumCoreLogic import com.wire.android.feature.AppLockSource import com.wire.android.feature.DisableAppLockUseCase -import com.wire.android.feature.analytics.AnonymousAnalyticsManager -import com.wire.android.feature.analytics.model.AnalyticsEvent -import com.wire.android.ui.analytics.IsAnalyticsAvailableUseCase import com.wire.android.ui.home.FeatureFlagState import com.wire.android.ui.home.conversations.selfdeletion.SelfDeletionMapper.toSelfDeletionDuration import com.wire.android.ui.home.messagecomposer.SelfDeletionDuration @@ -48,7 +45,6 @@ import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.fold import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.firstOrNull @@ -61,16 +57,12 @@ class FeatureFlagNotificationViewModel @Inject constructor( @KaliumCoreLogic private val coreLogic: CoreLogic, private val currentSessionFlow: CurrentSessionFlowUseCase, private val globalDataStore: GlobalDataStore, - private val disableAppLockUseCase: DisableAppLockUseCase, - private val isAnalyticsAvailable: IsAnalyticsAvailableUseCase, - private val analyticsManager: AnonymousAnalyticsManager + private val disableAppLockUseCase: DisableAppLockUseCase ) : ViewModel() { var featureFlagState by mutableStateOf(FeatureFlagState()) private set - val showCallFeedbackFlow = MutableSharedFlow() - private var currentUserId by mutableStateOf(null) init { diff --git a/app/src/test/kotlin/com/wire/android/ui/CallFeedbackViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/CallFeedbackViewModelTest.kt index 4efabf18ee2..a71580de8ee 100644 --- a/app/src/test/kotlin/com/wire/android/ui/CallFeedbackViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/CallFeedbackViewModelTest.kt @@ -35,7 +35,6 @@ import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify import io.mockk.impl.annotations.MockK -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test @@ -80,6 +79,7 @@ class CallFeedbackViewModelTest { ) } } + @Test fun `given NextTimeForCallFeedbackIsNotReached when use case is observed then send Muted event`() = runTest { val (arrangement, _) = Arrangement() @@ -178,7 +178,9 @@ class CallFeedbackViewModelTest { coEvery { coreLogic.getSessionScope(any()).observeSyncState() } returns flowOf(SyncState.Live) coEvery { coreLogic.getSessionScope(any()).calls.observeAskCallFeedbackUseCase() } returns flowOf() coEvery { coreLogic.getSessionScope(any()).calls.updateNextTimeCallFeedback(any()) } returns Unit - coEvery { coreLogic.getSessionScope(any()).calls.observeRecentlyEndedCallMetadata() } returns flowOf(recentlyEndedCallMetadata) + coEvery { + coreLogic.getSessionScope(any()).calls.observeRecentlyEndedCallMetadata() + } returns flowOf(recentlyEndedCallMetadata) } fun arrange() = this to viewModel diff --git a/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt index 41d16a57c49..17e690850db 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt @@ -21,9 +21,7 @@ import com.wire.android.config.CoroutineTestExtension import com.wire.android.datastore.GlobalDataStore import com.wire.android.feature.AppLockSource import com.wire.android.feature.DisableAppLockUseCase -import com.wire.android.feature.analytics.AnonymousAnalyticsManager import com.wire.android.framework.TestUser -import com.wire.android.ui.analytics.IsAnalyticsAvailableUseCase import com.wire.android.ui.home.FeatureFlagState import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.configuration.AppLockTeamConfig @@ -325,12 +323,6 @@ class FeatureFlagNotificationViewModelTest { @MockK lateinit var globalDataStore: GlobalDataStore - @MockK - lateinit var isAnalyticsAvailable: IsAnalyticsAvailableUseCase - - @MockK - lateinit var analyticsManager: AnonymousAnalyticsManager - @MockK lateinit var markNotifyForRevokedCertificateAsNotified: MarkNotifyForRevokedCertificateAsNotifiedUseCase @@ -339,9 +331,7 @@ class FeatureFlagNotificationViewModelTest { coreLogic = coreLogic, currentSessionFlow = currentSessionFlow, globalDataStore = globalDataStore, - disableAppLockUseCase = disableAppLockUseCase, - isAnalyticsAvailable = isAnalyticsAvailable, - analyticsManager = analyticsManager + disableAppLockUseCase = disableAppLockUseCase ) } @@ -374,10 +364,6 @@ class FeatureFlagNotificationViewModelTest { coEvery { globalDataStore.isAppLockPasscodeSet() } returns result } - fun withSyncState(stateFlow: Flow) = apply { - coEvery { coreLogic.getSessionScope(any()).observeSyncState() } returns stateFlow - } - fun withAppLockSource(source: AppLockSource) = apply { coEvery { globalDataStore.getAppLockSource() } returns source } From 1ed3bd17d55f9b55fffe1e5fda0c1bf1e98fde09 Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 12 Feb 2025 16:49:45 +0100 Subject: [PATCH 07/12] feat: address comment --- .../wire/android/ui/CallFeedbackViewModel.kt | 108 +++++++++--------- 1 file changed, 56 insertions(+), 52 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt index 339d2432bb5..1336b562665 100644 --- a/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt @@ -36,8 +36,10 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch import javax.inject.Inject @@ -55,67 +57,69 @@ class CallFeedbackViewModel @Inject constructor( init { viewModelScope.launch { - currentSessionFlow() - .distinctUntilChanged() - .collectLatest { session -> - if (session is CurrentSessionResult.Success && session.accountInfo.isValid()) { - currentUserId = session.accountInfo.userId - coreLogic.getSessionScope(currentUserId!!).observeSyncState().firstOrNull { it == SyncState.Live }?.let { - observeAskCallFeedback(currentUserId!!) - } - } else { - currentUserId = null - } - } + observeAskCallFeedback() } } - private suspend fun observeAskCallFeedback(userId: UserId) = - coreLogic.getSessionScope(userId).calls.observeAskCallFeedbackUseCase().collect { shouldAskFeedback -> - if (!isAnalyticsAvailable(userId)) { - return@collect + private suspend fun observeAskCallFeedback() { + currentSessionFlow() + .distinctUntilChanged() + .flatMapLatest { session -> + if (session is CurrentSessionResult.Success && session.accountInfo.isValid()) { + currentUserId = session.accountInfo.userId + coreLogic.getSessionScope(currentUserId!!).observeSyncState() + } else { + currentUserId = null + emptyFlow() + } } + .filter { it == SyncState.Live } + .flatMapLatest { + coreLogic.getSessionScope(currentUserId!!).calls.observeAskCallFeedbackUseCase() + } + .collectLatest { shouldAskFeedback -> + if (isAnalyticsAvailable(currentUserId!!)) { + when (shouldAskFeedback) { + is ShouldAskCallFeedbackUseCaseResult.ShouldAskCallFeedback -> { + showCallFeedbackFlow.emit(Unit) + } - when (shouldAskFeedback) { - is ShouldAskCallFeedbackUseCaseResult.ShouldAskCallFeedback -> { - showCallFeedbackFlow.emit(Unit) - } + is ShouldAskCallFeedbackUseCaseResult.ShouldNotAskCallFeedback.CallDurationIsLessThanOneMinute -> { + val recentlyEndedCallMetadata = + coreLogic.getSessionScope(currentUserId!!).calls.observeRecentlyEndedCallMetadata().first() + analyticsManager.sendEvent( + with(recentlyEndedCallMetadata) { + AnalyticsEvent.CallQualityFeedback.TooShort( + callDuration = shouldAskFeedback.callDurationInSeconds.toInt(), + isTeamMember = isTeamMember, + participantsCount = callDetails.callParticipantsCount, + isScreenSharedDuringCall = callDetails.isCallScreenShare, + isCameraEnabledDuringCall = callDetails.callVideoEnabled + ) + } + ) + } - is ShouldAskCallFeedbackUseCaseResult.ShouldNotAskCallFeedback.CallDurationIsLessThanOneMinute -> { - currentUserId?.let { - val recentlyEndedCallMetadata = coreLogic.getSessionScope(it).calls.observeRecentlyEndedCallMetadata().first() - analyticsManager.sendEvent( - with(recentlyEndedCallMetadata) { - AnalyticsEvent.CallQualityFeedback.TooShort( - callDuration = shouldAskFeedback.callDurationInSeconds.toInt(), - isTeamMember = isTeamMember, - participantsCount = callDetails.callParticipantsCount, - isScreenSharedDuringCall = callDetails.isCallScreenShare, - isCameraEnabledDuringCall = callDetails.callVideoEnabled - ) - } - ) + is ShouldAskCallFeedbackUseCaseResult.ShouldNotAskCallFeedback.NextTimeForCallFeedbackIsNotReached -> { + val recentlyEndedCallMetadata = + coreLogic.getSessionScope(currentUserId!!).calls.observeRecentlyEndedCallMetadata().first() + analyticsManager.sendEvent( + with(recentlyEndedCallMetadata) { + AnalyticsEvent.CallQualityFeedback.Muted( + callDuration = shouldAskFeedback.callDurationInSeconds.toInt(), + isTeamMember = isTeamMember, + participantsCount = callDetails.callParticipantsCount, + isScreenSharedDuringCall = callDetails.isCallScreenShare, + isCameraEnabledDuringCall = callDetails.callVideoEnabled + ) + } + ) + } } - } - is ShouldAskCallFeedbackUseCaseResult.ShouldNotAskCallFeedback.NextTimeForCallFeedbackIsNotReached -> { - currentUserId?.let { - val recentlyEndedCallMetadata = coreLogic.getSessionScope(it).calls.observeRecentlyEndedCallMetadata().first() - analyticsManager.sendEvent( - with(recentlyEndedCallMetadata) { - AnalyticsEvent.CallQualityFeedback.Muted( - callDuration = shouldAskFeedback.callDurationInSeconds.toInt(), - isTeamMember = isTeamMember, - participantsCount = callDetails.callParticipantsCount, - isScreenSharedDuringCall = callDetails.isCallScreenShare, - isCameraEnabledDuringCall = callDetails.callVideoEnabled - ) - } - ) - } } } - } + } fun rateCall(rate: Int, doNotAsk: Boolean) { currentUserId?.let { From 6e3d58917a920384c9b1a9b730da23aa6d472677 Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 12 Feb 2025 17:00:41 +0100 Subject: [PATCH 08/12] feat: kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 2493acd9b00..4572ce69c0c 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 2493acd9b00f845cd0054cf6ae9712ecee478b51 +Subproject commit 4572ce69c0ccecd8ecddb66f540935770371400f From 2ea6f1adef6e7f13feebfa11e98a56ac348712ef Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 12 Feb 2025 17:01:34 +0100 Subject: [PATCH 09/12] feat: kalium --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 348929bb245..3332efe9d3d 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 348929bb2455b0cfee6a0fe71a598aef8560811a +Subproject commit 3332efe9d3d03fbbe356637aa7050a8264b46472 From efc6ed24e18d81ea5f140719d73085556b8351d4 Mon Sep 17 00:00:00 2001 From: ohassine Date: Wed, 12 Feb 2025 17:26:44 +0100 Subject: [PATCH 10/12] feat: clean up --- app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt index 1336b562665..4ce8541c4b9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt @@ -116,7 +116,6 @@ class CallFeedbackViewModel @Inject constructor( ) } } - } } } From 6668d445578a6ee50fb3475f48d05982b20c3990 Mon Sep 17 00:00:00 2001 From: ohassine Date: Tue, 18 Feb 2025 10:08:12 +0100 Subject: [PATCH 11/12] feat: kaliun reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 3332efe9d3d..4abd5751161 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 3332efe9d3d03fbbe356637aa7050a8264b46472 +Subproject commit 4abd575116102fb83b8f0331d490e124ccec773b From ddba653005d45945aa48067a0808dd778ce1a0aa Mon Sep 17 00:00:00 2001 From: ohassine Date: Tue, 18 Feb 2025 18:14:51 +0100 Subject: [PATCH 12/12] feat: kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 4abd5751161..7c462ea8667 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 4abd575116102fb83b8f0331d490e124ccec773b +Subproject commit 7c462ea8667fbb0ff791b005e68dc7c6fba1ee09