diff --git a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt index 239a70079f0..d538202236a 100644 --- a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt @@ -51,7 +51,8 @@ import com.wire.kalium.logic.feature.call.usecase.StartCallUseCase import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOffUseCase import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOnUseCase import com.wire.kalium.logic.feature.call.usecase.UnMuteCallUseCase -import com.wire.kalium.logic.feature.call.usecase.UpdateVideoStateUseCase +import com.wire.kalium.logic.feature.call.usecase.video.SetVideoSendStateUseCase +import com.wire.kalium.logic.feature.call.usecase.video.UpdateVideoStateUseCase import com.wire.kalium.logic.feature.client.ClientFingerprintUseCase import com.wire.kalium.logic.feature.client.ObserveClientDetailsUseCase import com.wire.kalium.logic.feature.client.ObserveCurrentClientIdUseCase @@ -694,6 +695,14 @@ class UseCaseModule { ): UpdateVideoStateUseCase = coreLogic.getSessionScope(currentAccount).calls.updateVideoState + @ViewModelScoped + @Provides + fun provideSetVideoSendStateUseCase( + @KaliumCoreLogic coreLogic: CoreLogic, + @CurrentAccount currentAccount: UserId + ): SetVideoSendStateUseCase = + coreLogic.getSessionScope(currentAccount).calls.setVideoSendState + @ViewModelScoped @Provides fun provideCreateGroupConversationUseCase( diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt index e3f717e90ea..60dee4a395e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt @@ -54,7 +54,7 @@ import com.wire.kalium.logic.feature.call.usecase.SetVideoPreviewUseCase import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOffUseCase import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOnUseCase import com.wire.kalium.logic.feature.call.usecase.UnMuteCallUseCase -import com.wire.kalium.logic.feature.call.usecase.UpdateVideoStateUseCase +import com.wire.kalium.logic.feature.call.usecase.video.UpdateVideoStateUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.util.PlatformView import dagger.hilt.android.lifecycle.HiltViewModel @@ -129,8 +129,9 @@ class SharedCallingViewModel @Inject constructor( private suspend fun observeScreenState() { currentScreenManager.observeCurrentScreen(viewModelScope).collect { - if (it == CurrentScreen.InBackground) { - stopVideo() + // clear video preview when the screen is in background to avoid memory leaks + if (it == CurrentScreen.InBackground && callState.isCameraOn) { + clearVideoPreview() } } } @@ -261,6 +262,11 @@ class SharedCallingViewModel @Inject constructor( callState = callState.copy( isCameraOn = !callState.isCameraOn ) + if (callState.isCameraOn) { + updateVideoState(conversationId, VideoState.STARTED) + } else { + updateVideoState(conversationId, VideoState.STOPPED) + } } } @@ -268,7 +274,6 @@ class SharedCallingViewModel @Inject constructor( viewModelScope.launch { appLogger.i("SharedCallingViewModel: clearing video preview..") setVideoPreview(conversationId, PlatformView(null)) - updateVideoState(conversationId, VideoState.STOPPED) } } @@ -277,18 +282,6 @@ class SharedCallingViewModel @Inject constructor( appLogger.i("SharedCallingViewModel: setting video preview..") setVideoPreview(conversationId, PlatformView(null)) setVideoPreview(conversationId, PlatformView(view)) - updateVideoState(conversationId, VideoState.STARTED) - } - } - - fun stopVideo() { - viewModelScope.launch { - if (callState.isCameraOn) { - appLogger.i("SharedCallingViewModel: stopping video..") - callState = callState.copy(isCameraOn = false, isSpeakerOn = false) - clearVideoPreview() - turnLoudSpeakerOff() - } } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt index d741f8f06b1..7a4632144c6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt @@ -82,6 +82,7 @@ import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.feature.call.CallStatus import java.util.Locale @RootNavGraph @@ -126,6 +127,16 @@ fun OngoingCallScreen( ) BackHandler(enabled = isCameraOn, navigator::navigateBack) } + + // Start/stop sending video feed based on the camera state when the call is established. + LaunchedEffect(sharedCallingViewModel.callState.callStatus, sharedCallingViewModel.callState.isCameraOn) { + if (sharedCallingViewModel.callState.callStatus == CallStatus.ESTABLISHED) { + when (sharedCallingViewModel.callState.isCameraOn) { + true -> ongoingCallViewModel.startSendingVideoFeed() + false -> ongoingCallViewModel.stopSendingVideoFeed() + } + } + } } @OptIn(ExperimentalMaterial3Api::class) diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt index 7df9b0662ae..af684da8029 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt @@ -36,10 +36,13 @@ import com.wire.android.ui.navArgs import com.wire.android.util.CurrentScreen import com.wire.android.util.CurrentScreenManager import com.wire.kalium.logic.data.call.CallClient +import com.wire.kalium.logic.data.call.VideoState import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.call.Call import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.call.usecase.RequestVideoStreamsUseCase +import com.wire.kalium.logic.feature.call.usecase.video.SetVideoSendStateUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged @@ -56,7 +59,8 @@ class OngoingCallViewModel @Inject constructor( private val globalDataStore: GlobalDataStore, private val establishedCalls: ObserveEstablishedCallsUseCase, private val requestVideoStreams: RequestVideoStreamsUseCase, - private val currentScreenManager: CurrentScreenManager, + private val setVideoSendState: SetVideoSendStateUseCase, + private val currentScreenManager: CurrentScreenManager ) : ViewModel() { private val ongoingCallNavArgs: CallingNavArgs = savedStateHandle.navArgs() @@ -72,6 +76,7 @@ class OngoingCallViewModel @Inject constructor( init { viewModelScope.launch { establishedCalls().first { it.isNotEmpty() }.run { + initCameraState(this) // We start observing once we have an ongoing call observeCurrentCall() } @@ -79,6 +84,28 @@ class OngoingCallViewModel @Inject constructor( showDoubleTapToast() } + private fun initCameraState(calls: List) { + val currentCall = calls.find { call -> call.conversationId == conversationId } + currentCall?.let { + if (it.isCameraOn) { + startSendingVideoFeed() + } else { + stopSendingVideoFeed() + } + } + } + + fun startSendingVideoFeed() { + viewModelScope.launch { + setVideoSendState(conversationId, VideoState.STARTED) + } + } + fun stopSendingVideoFeed() { + viewModelScope.launch { + setVideoSendState(conversationId, VideoState.STOPPED) + } + } + private suspend fun observeCurrentCall() { establishedCalls() .distinctUntilChanged() diff --git a/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt index 87e12234ae2..45ebc5a9d02 100644 --- a/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt @@ -22,8 +22,8 @@ package com.wire.android.ui.calling import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension -import com.wire.android.datastore.GlobalDataStore import com.wire.android.config.NavigationTestExtension +import com.wire.android.datastore.GlobalDataStore import com.wire.android.ui.calling.model.UICallParticipant import com.wire.android.ui.calling.ongoing.OngoingCallViewModel import com.wire.android.ui.home.conversationslist.model.Membership @@ -31,14 +31,16 @@ import com.wire.android.ui.navArgs import com.wire.android.util.CurrentScreen import com.wire.android.util.CurrentScreenManager import com.wire.kalium.logic.data.call.CallClient +import com.wire.kalium.logic.data.call.VideoState import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.call.Call import com.wire.kalium.logic.feature.call.CallStatus -import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.call.usecase.RequestVideoStreamsUseCase +import com.wire.kalium.logic.feature.call.usecase.video.SetVideoSendStateUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify @@ -50,8 +52,8 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(NavigationTestExtension::class) @@ -70,6 +72,9 @@ class OngoingCallViewModelTest { @MockK private lateinit var currentScreenManager: CurrentScreenManager + @MockK + private lateinit var setVideoSendState: SetVideoSendStateUseCase + @MockK private lateinit var globalDataStore: GlobalDataStore @@ -82,6 +87,7 @@ class OngoingCallViewModelTest { coEvery { establishedCall.invoke() } returns flowOf(listOf(provideCall())) coEvery { currentScreenManager.observeCurrentScreen(any()) } returns MutableStateFlow(CurrentScreen.SomeOther) coEvery { globalDataStore.getShouldShowDoubleTapToast(any()) } returns false + coEvery { setVideoSendState.invoke(any(), any()) } returns Unit ongoingCallViewModel = OngoingCallViewModel( savedStateHandle = savedStateHandle, @@ -89,10 +95,25 @@ class OngoingCallViewModelTest { requestVideoStreams = requestVideoStreams, currentScreenManager = currentScreenManager, currentUserId = currentUserId, + setVideoSendState = setVideoSendState, globalDataStore = globalDataStore, ) } + @Test + fun givenAnOngoingCall_WhenTurningOnCamera_ThenSetVideoSendStateToStarted() = runTest { + ongoingCallViewModel.startSendingVideoFeed() + + coVerify(exactly = 1) { setVideoSendState.invoke(any(), VideoState.STARTED) } + } + + @Test + fun givenAnOngoingCall_WhenTurningOffCamera_ThenSetVideoSendStateToStopped() = runTest { + ongoingCallViewModel.stopSendingVideoFeed() + + coVerify { setVideoSendState.invoke(any(), VideoState.STOPPED) } + } + @Test fun givenParticipantsList_WhenRequestingVideoStream_ThenRequestItForOnlyParticipantsWithVideoEnabled() = runTest { val expectedClients = listOf( diff --git a/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt index c6f030511f1..db609effbad 100644 --- a/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt @@ -44,7 +44,7 @@ import com.wire.kalium.logic.feature.call.usecase.SetVideoPreviewUseCase import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOffUseCase import com.wire.kalium.logic.feature.call.usecase.TurnLoudSpeakerOnUseCase import com.wire.kalium.logic.feature.call.usecase.UnMuteCallUseCase -import com.wire.kalium.logic.feature.call.usecase.UpdateVideoStateUseCase +import com.wire.kalium.logic.feature.call.usecase.video.UpdateVideoStateUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -250,6 +250,7 @@ class SharedCallingViewModelTest { advanceUntilIdle() sharedCallingViewModel.callState.isCameraOn shouldBeEqualTo false + coVerify(exactly = 1) { updateVideoState(any(), VideoState.STOPPED) } } @Test @@ -261,6 +262,7 @@ class SharedCallingViewModelTest { advanceUntilIdle() sharedCallingViewModel.callState.isCameraOn shouldBeEqualTo true + coVerify(exactly = 1) { updateVideoState(any(), VideoState.STARTED) } } @Test @@ -279,53 +281,23 @@ class SharedCallingViewModelTest { } @Test - fun `given an active call, when setVideoPreview is called, then set the video preview and update video state to STARTED`() = runTest { + fun `given a call, when setVideoPreview is called, then set the video preview`() = runTest { coEvery { setVideoPreview(any(), any()) } returns Unit - coEvery { updateVideoState(any(), any()) } returns Unit sharedCallingViewModel.setVideoPreview(view) advanceUntilIdle() coVerify(exactly = 2) { setVideoPreview(any(), any()) } - coVerify(exactly = 1) { updateVideoState(any(), VideoState.STARTED) } } @Test - fun `given an active call, when clearVideoPreview is called, then update video state to STOPPED`() = runTest { + fun `given a call, when clearVideoPreview is called, then clear view`() = runTest { coEvery { setVideoPreview(any(), any()) } returns Unit - coEvery { updateVideoState(any(), any()) } returns Unit sharedCallingViewModel.clearVideoPreview() advanceUntilIdle() - coVerify(exactly = 1) { updateVideoState(any(), VideoState.STOPPED) } - } - - @Test - fun `given a video call, when stopping video, then clear Video Preview and turn off speaker`() = runTest { - sharedCallingViewModel.callState = sharedCallingViewModel.callState.copy(isCameraOn = true) - coEvery { setVideoPreview(any(), any()) } returns Unit - coEvery { updateVideoState(any(), any()) } returns Unit - coEvery { turnLoudSpeakerOff() } returns Unit - - sharedCallingViewModel.stopVideo() - advanceUntilIdle() - coVerify(exactly = 1) { setVideoPreview(any(), any()) } - coVerify(exactly = 1) { turnLoudSpeakerOff() } - } - - @Test - fun `given an audio call, when stopVideo is invoked, then do not do anything`() = runTest { - sharedCallingViewModel.callState = sharedCallingViewModel.callState.copy(isCameraOn = false) - coEvery { setVideoPreview(any(), any()) } returns Unit - coEvery { turnLoudSpeakerOff() } returns Unit - - sharedCallingViewModel.stopVideo() - advanceUntilIdle() - - coVerify(inverse = true) { setVideoPreview(any(), any()) } - coVerify(inverse = true) { turnLoudSpeakerOff() } } companion object { diff --git a/kalium b/kalium index ebcc9f88367..dc607c73cb6 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit ebcc9f88367c29e75fd582d5b1d06d64d3d9ddb7 +Subproject commit dc607c73cb6a9a886638e0562391a6c2e037e2d7