Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Calling.call_quality segmentations (WPB-15086) #3857

Merged
merged 17 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading