Skip to content

Commit

Permalink
feat: add Calling.call_quality segmentations (WPB-15086) (#3857)
Browse files Browse the repository at this point in the history
  • Loading branch information
ohassine authored Feb 18, 2025
1 parent f74b244 commit a42e7f5
Show file tree
Hide file tree
Showing 7 changed files with 448 additions and 129 deletions.
163 changes: 163 additions & 0 deletions app/src/main/kotlin/com/wire/android/ui/CallFeedbackViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
* 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 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.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
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<Unit>()

private var currentUserId by mutableStateOf<UserId?>(null)

init {
viewModelScope.launch {
observeAskCallFeedback()
}
}

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)
}

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.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
)
}
)
}
}
}
}
}

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
)
}
)
}
}
}
}
9 changes: 5 additions & 4 deletions app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -383,7 +384,7 @@ class WireActivity : AppCompatActivity() {
val context = LocalContext.current
val callFeedbackSheetState =
rememberWireModalSheetState<Unit>(onDismissAction = {
featureFlagNotificationViewModel.skipCallFeedback(false)
callFeedbackViewModel.skipCallFeedback(false)
})
with(featureFlagNotificationViewModel.featureFlagState) {
if (shouldShowTeamAppLockDialog) {
Expand Down Expand Up @@ -557,8 +558,8 @@ class WireActivity : AppCompatActivity() {

CallFeedbackDialog(
sheetState = callFeedbackSheetState,
onRated = featureFlagNotificationViewModel::rateCall,
onSkipClicked = featureFlagNotificationViewModel::skipCallFeedback
onRated = callFeedbackViewModel::rateCall,
onSkipClicked = callFeedbackViewModel::skipCallFeedback
)

if (startGettingE2EICertificate) {
Expand All @@ -572,7 +573,7 @@ class WireActivity : AppCompatActivity() {
}

LaunchedEffect(Unit) {
featureFlagNotificationViewModel.showCallFeedbackFlow.collectLatest {
callFeedbackViewModel.showCallFeedbackFlow.collectLatest {
callFeedbackSheetState.show()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -48,7 +45,6 @@ import com.wire.kalium.common.functional.Either
import com.wire.kalium.common.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
Expand All @@ -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<Unit>()

private var currentUserId by mutableStateOf<UserId?>(null)

init {
Expand Down Expand Up @@ -128,7 +120,6 @@ class FeatureFlagNotificationViewModel @Inject constructor(
launch { setE2EIRequiredState(userId) }
launch { setTeamAppLockFeatureFlag(userId) }
launch { observeCallEndedBecauseOfConversationDegraded(userId) }
launch { observeAskCallFeedback(userId) }
launch { observeShouldNotifyForRevokedCertificate(userId) }
}
}
Expand Down Expand Up @@ -228,17 +219,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 {
Expand Down Expand Up @@ -354,24 +334,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"
}
Expand Down
Loading

0 comments on commit a42e7f5

Please sign in to comment.