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

fix: camera on/off button when in fullscreen - 🍒 v4.5 [WPB-9815] #3128

Merged
Show file tree
Hide file tree
Changes from all 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
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
Loading