Skip to content

Commit a42e7f5

Browse files
authored
feat: add Calling.call_quality segmentations (WPB-15086) (#3857)
1 parent f74b244 commit a42e7f5

File tree

7 files changed

+448
-129
lines changed

7 files changed

+448
-129
lines changed
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* Wire
3+
* Copyright (C) 2025 Wire Swiss GmbH
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see http://www.gnu.org/licenses/.
17+
*/
18+
package com.wire.android.ui
19+
20+
import androidx.compose.runtime.getValue
21+
import androidx.compose.runtime.mutableStateOf
22+
import androidx.compose.runtime.setValue
23+
import androidx.lifecycle.ViewModel
24+
import androidx.lifecycle.viewModelScope
25+
import com.wire.android.di.KaliumCoreLogic
26+
import com.wire.android.feature.analytics.AnonymousAnalyticsManager
27+
import com.wire.android.feature.analytics.model.AnalyticsEvent
28+
import com.wire.android.ui.analytics.IsAnalyticsAvailableUseCase
29+
import com.wire.kalium.logic.CoreLogic
30+
import com.wire.kalium.logic.data.sync.SyncState
31+
import com.wire.kalium.logic.data.user.UserId
32+
import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase
33+
import com.wire.kalium.logic.feature.session.CurrentSessionResult
34+
import com.wire.kalium.logic.feature.user.ShouldAskCallFeedbackUseCaseResult
35+
import dagger.hilt.android.lifecycle.HiltViewModel
36+
import kotlinx.coroutines.flow.MutableSharedFlow
37+
import kotlinx.coroutines.flow.collectLatest
38+
import kotlinx.coroutines.flow.distinctUntilChanged
39+
import kotlinx.coroutines.flow.emptyFlow
40+
import kotlinx.coroutines.flow.filter
41+
import kotlinx.coroutines.flow.first
42+
import kotlinx.coroutines.flow.flatMapLatest
43+
import kotlinx.coroutines.launch
44+
import javax.inject.Inject
45+
46+
@HiltViewModel
47+
class CallFeedbackViewModel @Inject constructor(
48+
@KaliumCoreLogic private val coreLogic: CoreLogic,
49+
private val currentSessionFlow: CurrentSessionFlowUseCase,
50+
private val isAnalyticsAvailable: IsAnalyticsAvailableUseCase,
51+
private val analyticsManager: AnonymousAnalyticsManager,
52+
) : ViewModel() {
53+
54+
val showCallFeedbackFlow = MutableSharedFlow<Unit>()
55+
56+
private var currentUserId by mutableStateOf<UserId?>(null)
57+
58+
init {
59+
viewModelScope.launch {
60+
observeAskCallFeedback()
61+
}
62+
}
63+
64+
private suspend fun observeAskCallFeedback() {
65+
currentSessionFlow()
66+
.distinctUntilChanged()
67+
.flatMapLatest { session ->
68+
if (session is CurrentSessionResult.Success && session.accountInfo.isValid()) {
69+
currentUserId = session.accountInfo.userId
70+
coreLogic.getSessionScope(currentUserId!!).observeSyncState()
71+
} else {
72+
currentUserId = null
73+
emptyFlow()
74+
}
75+
}
76+
.filter { it == SyncState.Live }
77+
.flatMapLatest {
78+
coreLogic.getSessionScope(currentUserId!!).calls.observeAskCallFeedbackUseCase()
79+
}
80+
.collectLatest { shouldAskFeedback ->
81+
if (isAnalyticsAvailable(currentUserId!!)) {
82+
when (shouldAskFeedback) {
83+
is ShouldAskCallFeedbackUseCaseResult.ShouldAskCallFeedback -> {
84+
showCallFeedbackFlow.emit(Unit)
85+
}
86+
87+
is ShouldAskCallFeedbackUseCaseResult.ShouldNotAskCallFeedback.CallDurationIsLessThanOneMinute -> {
88+
val recentlyEndedCallMetadata =
89+
coreLogic.getSessionScope(currentUserId!!).calls.observeRecentlyEndedCallMetadata().first()
90+
analyticsManager.sendEvent(
91+
with(recentlyEndedCallMetadata) {
92+
AnalyticsEvent.CallQualityFeedback.TooShort(
93+
callDuration = shouldAskFeedback.callDurationInSeconds.toInt(),
94+
isTeamMember = isTeamMember,
95+
participantsCount = callDetails.callParticipantsCount,
96+
isScreenSharedDuringCall = callDetails.isCallScreenShare,
97+
isCameraEnabledDuringCall = callDetails.callVideoEnabled
98+
)
99+
}
100+
)
101+
}
102+
103+
is ShouldAskCallFeedbackUseCaseResult.ShouldNotAskCallFeedback.NextTimeForCallFeedbackIsNotReached -> {
104+
val recentlyEndedCallMetadata =
105+
coreLogic.getSessionScope(currentUserId!!).calls.observeRecentlyEndedCallMetadata().first()
106+
analyticsManager.sendEvent(
107+
with(recentlyEndedCallMetadata) {
108+
AnalyticsEvent.CallQualityFeedback.Muted(
109+
callDuration = shouldAskFeedback.callDurationInSeconds.toInt(),
110+
isTeamMember = isTeamMember,
111+
participantsCount = callDetails.callParticipantsCount,
112+
isScreenSharedDuringCall = callDetails.isCallScreenShare,
113+
isCameraEnabledDuringCall = callDetails.callVideoEnabled
114+
)
115+
}
116+
)
117+
}
118+
}
119+
}
120+
}
121+
}
122+
123+
fun rateCall(rate: Int, doNotAsk: Boolean) {
124+
currentUserId?.let {
125+
viewModelScope.launch {
126+
val recentlyEndedCallMetadata = coreLogic.getSessionScope(it).calls.observeRecentlyEndedCallMetadata().first()
127+
analyticsManager.sendEvent(
128+
with(recentlyEndedCallMetadata) {
129+
AnalyticsEvent.CallQualityFeedback.Answered(
130+
score = rate,
131+
callDuration = callDetails.callDurationInSeconds.toInt(),
132+
isTeamMember = isTeamMember,
133+
participantsCount = callDetails.callParticipantsCount,
134+
isScreenSharedDuringCall = callDetails.isCallScreenShare,
135+
isCameraEnabledDuringCall = callDetails.callVideoEnabled
136+
)
137+
}
138+
)
139+
coreLogic.getSessionScope(it).calls.updateNextTimeCallFeedback(doNotAsk)
140+
}
141+
}
142+
}
143+
144+
fun skipCallFeedback(doNotAsk: Boolean) {
145+
currentUserId?.let {
146+
viewModelScope.launch {
147+
val recentlyEndedCallMetadata = coreLogic.getSessionScope(it).calls.observeRecentlyEndedCallMetadata().first()
148+
coreLogic.getSessionScope(it).calls.updateNextTimeCallFeedback(doNotAsk)
149+
analyticsManager.sendEvent(
150+
with(recentlyEndedCallMetadata) {
151+
AnalyticsEvent.CallQualityFeedback.Dismissed(
152+
callDuration = callDetails.callDurationInSeconds.toInt(),
153+
isTeamMember = isTeamMember,
154+
participantsCount = callDetails.callParticipantsCount,
155+
isScreenSharedDuringCall = callDetails.isCallScreenShare,
156+
isCameraEnabledDuringCall = callDetails.callVideoEnabled
157+
)
158+
}
159+
)
160+
}
161+
}
162+
}
163+
}

app/src/main/kotlin/com/wire/android/ui/WireActivity.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ class WireActivity : AppCompatActivity() {
144144
private val viewModel: WireActivityViewModel by viewModels()
145145

146146
private val featureFlagNotificationViewModel: FeatureFlagNotificationViewModel by viewModels()
147+
private val callFeedbackViewModel: CallFeedbackViewModel by viewModels()
147148

148149
private val commonTopAppBarViewModel: CommonTopAppBarViewModel by viewModels()
149150
private val legalHoldRequestedViewModel: LegalHoldRequestedViewModel by viewModels()
@@ -383,7 +384,7 @@ class WireActivity : AppCompatActivity() {
383384
val context = LocalContext.current
384385
val callFeedbackSheetState =
385386
rememberWireModalSheetState<Unit>(onDismissAction = {
386-
featureFlagNotificationViewModel.skipCallFeedback(false)
387+
callFeedbackViewModel.skipCallFeedback(false)
387388
})
388389
with(featureFlagNotificationViewModel.featureFlagState) {
389390
if (shouldShowTeamAppLockDialog) {
@@ -557,8 +558,8 @@ class WireActivity : AppCompatActivity() {
557558

558559
CallFeedbackDialog(
559560
sheetState = callFeedbackSheetState,
560-
onRated = featureFlagNotificationViewModel::rateCall,
561-
onSkipClicked = featureFlagNotificationViewModel::skipCallFeedback
561+
onRated = callFeedbackViewModel::rateCall,
562+
onSkipClicked = callFeedbackViewModel::skipCallFeedback
562563
)
563564

564565
if (startGettingE2EICertificate) {
@@ -572,7 +573,7 @@ class WireActivity : AppCompatActivity() {
572573
}
573574

574575
LaunchedEffect(Unit) {
575-
featureFlagNotificationViewModel.showCallFeedbackFlow.collectLatest {
576+
callFeedbackViewModel.showCallFeedbackFlow.collectLatest {
576577
callFeedbackSheetState.show()
577578
}
578579
}

app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@ import com.wire.android.datastore.GlobalDataStore
2828
import com.wire.android.di.KaliumCoreLogic
2929
import com.wire.android.feature.AppLockSource
3030
import com.wire.android.feature.DisableAppLockUseCase
31-
import com.wire.android.feature.analytics.AnonymousAnalyticsManager
32-
import com.wire.android.feature.analytics.model.AnalyticsEvent
33-
import com.wire.android.ui.analytics.IsAnalyticsAvailableUseCase
3431
import com.wire.android.ui.home.FeatureFlagState
3532
import com.wire.android.ui.home.conversations.selfdeletion.SelfDeletionMapper.toSelfDeletionDuration
3633
import com.wire.android.ui.home.messagecomposer.SelfDeletionDuration
@@ -48,7 +45,6 @@ import com.wire.kalium.common.functional.Either
4845
import com.wire.kalium.common.functional.fold
4946
import dagger.hilt.android.lifecycle.HiltViewModel
5047
import kotlinx.coroutines.coroutineScope
51-
import kotlinx.coroutines.flow.MutableSharedFlow
5248
import kotlinx.coroutines.flow.collectLatest
5349
import kotlinx.coroutines.flow.distinctUntilChanged
5450
import kotlinx.coroutines.flow.firstOrNull
@@ -61,16 +57,12 @@ class FeatureFlagNotificationViewModel @Inject constructor(
6157
@KaliumCoreLogic private val coreLogic: CoreLogic,
6258
private val currentSessionFlow: CurrentSessionFlowUseCase,
6359
private val globalDataStore: GlobalDataStore,
64-
private val disableAppLockUseCase: DisableAppLockUseCase,
65-
private val isAnalyticsAvailable: IsAnalyticsAvailableUseCase,
66-
private val analyticsManager: AnonymousAnalyticsManager
60+
private val disableAppLockUseCase: DisableAppLockUseCase
6761
) : ViewModel() {
6862

6963
var featureFlagState by mutableStateOf(FeatureFlagState())
7064
private set
7165

72-
val showCallFeedbackFlow = MutableSharedFlow<Unit>()
73-
7466
private var currentUserId by mutableStateOf<UserId?>(null)
7567

7668
init {
@@ -128,7 +120,6 @@ class FeatureFlagNotificationViewModel @Inject constructor(
128120
launch { setE2EIRequiredState(userId) }
129121
launch { setTeamAppLockFeatureFlag(userId) }
130122
launch { observeCallEndedBecauseOfConversationDegraded(userId) }
131-
launch { observeAskCallFeedback(userId) }
132123
launch { observeShouldNotifyForRevokedCertificate(userId) }
133124
}
134125
}
@@ -228,17 +219,6 @@ class FeatureFlagNotificationViewModel @Inject constructor(
228219
featureFlagState = featureFlagState.copy(showCallEndedBecauseOfConversationDegraded = true)
229220
}
230221

231-
private suspend fun observeAskCallFeedback(userId: UserId) =
232-
coreLogic.getSessionScope(userId).calls.observeAskCallFeedbackUseCase().collect { shouldAskFeedback ->
233-
if (!isAnalyticsAvailable(userId)) {
234-
// Analytics is disabled. Do nothing.
235-
} else if (shouldAskFeedback) {
236-
showCallFeedbackFlow.emit(Unit)
237-
} else {
238-
analyticsManager.sendEvent(AnalyticsEvent.CallQualityFeedback.NotDisplayed)
239-
}
240-
}
241-
242222
fun dismissSelfDeletingMessagesDialog() {
243223
featureFlagState = featureFlagState.copy(shouldShowSelfDeletingMessagesDialog = false)
244224
viewModelScope.launch {
@@ -354,24 +334,6 @@ class FeatureFlagNotificationViewModel @Inject constructor(
354334
featureFlagState = featureFlagState.copy(e2EIResult = null)
355335
}
356336

357-
fun rateCall(rate: Int, doNotAsk: Boolean) {
358-
currentUserId?.let {
359-
viewModelScope.launch {
360-
analyticsManager.sendEvent(AnalyticsEvent.CallQualityFeedback.Answered(rate))
361-
coreLogic.getSessionScope(it).calls.updateNextTimeCallFeedback(doNotAsk)
362-
}
363-
}
364-
}
365-
366-
fun skipCallFeedback(doNotAsk: Boolean) {
367-
currentUserId?.let {
368-
viewModelScope.launch {
369-
coreLogic.getSessionScope(it).calls.updateNextTimeCallFeedback(doNotAsk)
370-
analyticsManager.sendEvent(AnalyticsEvent.CallQualityFeedback.Dismissed)
371-
}
372-
}
373-
}
374-
375337
companion object {
376338
private const val TAG = "FeatureFlagNotificationViewModel"
377339
}

0 commit comments

Comments
 (0)