Skip to content

Commit

Permalink
fix: camera on/off button when in fullscreen - πŸ’ v4.4 [WPB-9815] (#3125)
Browse files Browse the repository at this point in the history
  • Loading branch information
saleniuk authored Jun 25, 2024
1 parent 253e323 commit 6023583
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 55 deletions.
11 changes: 10 additions & 1 deletion app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
}
}
Expand Down Expand Up @@ -261,14 +262,18 @@ class SharedCallingViewModel @Inject constructor(
callState = callState.copy(
isCameraOn = !callState.isCameraOn
)
if (callState.isCameraOn) {
updateVideoState(conversationId, VideoState.STARTED)
} else {
updateVideoState(conversationId, VideoState.STOPPED)
}
}
}

fun clearVideoPreview() {
viewModelScope.launch {
appLogger.i("SharedCallingViewModel: clearing video preview..")
setVideoPreview(conversationId, PlatformView(null))
updateVideoState(conversationId, VideoState.STOPPED)
}
}

Expand All @@ -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()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -72,13 +76,36 @@ class OngoingCallViewModel @Inject constructor(
init {
viewModelScope.launch {
establishedCalls().first { it.isNotEmpty() }.run {
initCameraState(this)
// We start observing once we have an ongoing call
observeCurrentCall()
}
}
showDoubleTapToast()
}

private fun initCameraState(calls: List<Call>) {
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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,25 @@ 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
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
Expand All @@ -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)
Expand All @@ -70,6 +72,9 @@ class OngoingCallViewModelTest {
@MockK
private lateinit var currentScreenManager: CurrentScreenManager

@MockK
private lateinit var setVideoSendState: SetVideoSendStateUseCase

@MockK
private lateinit var globalDataStore: GlobalDataStore

Expand All @@ -82,17 +87,33 @@ 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,
establishedCalls = establishedCall,
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -250,6 +250,7 @@ class SharedCallingViewModelTest {
advanceUntilIdle()

sharedCallingViewModel.callState.isCameraOn shouldBeEqualTo false
coVerify(exactly = 1) { updateVideoState(any(), VideoState.STOPPED) }
}

@Test
Expand All @@ -261,6 +262,7 @@ class SharedCallingViewModelTest {
advanceUntilIdle()

sharedCallingViewModel.callState.isCameraOn shouldBeEqualTo true
coVerify(exactly = 1) { updateVideoState(any(), VideoState.STARTED) }
}

@Test
Expand All @@ -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 {
Expand Down

0 comments on commit 6023583

Please sign in to comment.