Skip to content

Commit

Permalink
fix: camera on/off button when in fullscreen - πŸ’ v4.5 [WPB-9815] (#3128)
Browse files Browse the repository at this point in the history
  • Loading branch information
saleniuk authored Jun 25, 2024
1 parent 033d25b commit d37d51b
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,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 dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
Expand Down Expand Up @@ -167,6 +168,13 @@ class CallsModule {
): UpdateVideoStateUseCase =
callsScope.updateVideoState

@ViewModelScoped
@Provides
fun provideSetVideoSendStateUseCase(
callsScope: CallsScope
): SetVideoSendStateUseCase =
callsScope.setVideoSendState

@ViewModelScoped
@Provides
fun provideIsCallRunningUseCase(callsScope: CallsScope) =
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 @@ -275,14 +276,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 @@ -291,18 +296,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 @@ -81,6 +81,7 @@ import com.wire.android.ui.theme.wireColorScheme
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.call.CallStatus
import com.wire.kalium.logic.data.id.ConversationId
import java.util.Locale

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 @@ -35,11 +35,14 @@ import com.wire.android.ui.calling.model.UICallParticipant
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.Call
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.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 @@ -33,12 +33,14 @@ import com.wire.android.util.CurrentScreenManager
import com.wire.kalium.logic.data.call.Call
import com.wire.kalium.logic.data.call.CallClient
import com.wire.kalium.logic.data.call.CallStatus
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.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 Down Expand Up @@ -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 @@ -263,6 +263,7 @@ class SharedCallingViewModelTest {
advanceUntilIdle()

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

@Test
Expand All @@ -274,6 +275,7 @@ class SharedCallingViewModelTest {
advanceUntilIdle()

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

@Test
Expand Down Expand Up @@ -317,7 +319,7 @@ class SharedCallingViewModelTest {
}

@Test
fun `given an active call, when setVideoPreview is called, then set the video preview and update video state to STARTED`() =
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
Expand All @@ -326,48 +328,16 @@ class SharedCallingViewModelTest {
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 {
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)
fun `given a call, when clearVideoPreview is called, then clear view`() = runTest {
coEvery { setVideoPreview(any(), any()) } returns Unit
coEvery { turnLoudSpeakerOff() } returns Unit

sharedCallingViewModel.stopVideo()
sharedCallingViewModel.clearVideoPreview()
advanceUntilIdle()

coVerify(inverse = true) { setVideoPreview(any(), any()) }
coVerify(inverse = true) { turnLoudSpeakerOff() }
coVerify(exactly = 1) { setVideoPreview(any(), any()) }
}

companion object {
Expand Down

0 comments on commit d37d51b

Please sign in to comment.