Merge pull request #3334 from element-hq/feature/bma/pipCallApi

Use new functions exposed by Element Call about PiP
This commit is contained in:
Benoit Marty 2024-08-26 18:00:17 +02:00 committed by GitHub
commit 1a43aa38fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 355 additions and 98 deletions

View file

@ -16,6 +16,10 @@
package io.element.android.features.call.impl.pip
import io.element.android.features.call.impl.utils.PipController
sealed interface PictureInPictureEvents {
data class SetPipController(val pipController: PipController) : PictureInPictureEvents
data object EnterPictureInPicture : PictureInPictureEvents
data class OnPictureInPictureModeChanged(val isInPip: Boolean) : PictureInPictureEvents
}

View file

@ -16,17 +16,17 @@
package io.element.android.features.call.impl.pip
import android.app.Activity
import android.app.PictureInPictureParams
import android.os.Build
import android.util.Rational
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import io.element.android.features.call.impl.utils.PipController
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.log.logger.LoggerTag
import kotlinx.coroutines.launch
import timber.log.Timber
import java.lang.ref.WeakReference
import javax.inject.Inject
private val loggerTag = LoggerTag("PiP")
@ -35,71 +35,69 @@ class PictureInPicturePresenter @Inject constructor(
pipSupportProvider: PipSupportProvider,
) : Presenter<PictureInPictureState> {
private val isPipSupported = pipSupportProvider.isPipSupported()
private var isInPictureInPicture = mutableStateOf(false)
private var hostActivity: WeakReference<Activity>? = null
private var pipView: PipView? = null
@Composable
override fun present(): PictureInPictureState {
val coroutineScope = rememberCoroutineScope()
var isInPictureInPicture by remember { mutableStateOf(false) }
var pipController by remember { mutableStateOf<PipController?>(null) }
fun handleEvent(event: PictureInPictureEvents) {
when (event) {
PictureInPictureEvents.EnterPictureInPicture -> switchToPip()
is PictureInPictureEvents.SetPipController -> {
pipController = event.pipController
}
PictureInPictureEvents.EnterPictureInPicture -> {
coroutineScope.launch {
switchToPip(pipController)
}
}
is PictureInPictureEvents.OnPictureInPictureModeChanged -> {
Timber.tag(loggerTag.value).d("onPictureInPictureModeChanged: ${event.isInPip}")
isInPictureInPicture = event.isInPip
if (event.isInPip) {
pipController?.enterPip()
} else {
pipController?.exitPip()
}
}
}
}
return PictureInPictureState(
supportPip = isPipSupported,
isInPictureInPicture = isInPictureInPicture.value,
isInPictureInPicture = isInPictureInPicture,
eventSink = ::handleEvent,
)
}
fun onCreate(activity: Activity) {
fun setPipView(pipView: PipView?) {
if (isPipSupported) {
Timber.tag(loggerTag.value).d("onCreate: Setting PiP params")
hostActivity = WeakReference(activity)
hostActivity?.get()?.setPictureInPictureParams(getPictureInPictureParams())
Timber.tag(loggerTag.value).d("Setting PiP params")
this.pipView = pipView
pipView?.setPipParams()
} else {
Timber.tag(loggerTag.value).d("onCreate: PiP is not supported")
Timber.tag(loggerTag.value).d("setPipView: PiP is not supported")
}
}
fun onDestroy() {
Timber.tag(loggerTag.value).d("onDestroy")
hostActivity?.clear()
hostActivity = null
}
@RequiresApi(Build.VERSION_CODES.O)
private fun getPictureInPictureParams(): PictureInPictureParams {
return PictureInPictureParams.Builder()
// Portrait for calls seems more appropriate
.setAspectRatio(Rational(3, 5))
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
setAutoEnterEnabled(true)
}
}
.build()
}
/**
* Enters Picture-in-Picture mode.
* Enters Picture-in-Picture mode, if allowed by Element Call.
*/
private fun switchToPip() {
private suspend fun switchToPip(pipController: PipController?) {
if (isPipSupported) {
Timber.tag(loggerTag.value).d("Switch to PiP mode")
hostActivity?.get()?.enterPictureInPictureMode(getPictureInPictureParams())
?.also { Timber.tag(loggerTag.value).d("Switch to PiP mode result: $it") }
if (pipController == null) {
Timber.tag(loggerTag.value).w("webPipApi is not available")
}
if (pipController == null || pipController.canEnterPip()) {
Timber.tag(loggerTag.value).d("Switch to PiP mode")
pipView?.enterPipMode()
?.also { Timber.tag(loggerTag.value).d("Switch to PiP mode result: $it") }
} else {
Timber.tag(loggerTag.value).w("Cannot enter PiP mode, hangup the call")
pipView?.hangUp()
}
}
}
fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
Timber.tag(loggerTag.value).d("onPictureInPictureModeChanged: $isInPictureInPictureMode")
isInPictureInPicture.value = isInPictureInPictureMode
}
fun onUserLeaveHint() {
Timber.tag(loggerTag.value).d("onUserLeaveHint")
switchToPip()
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.call.impl.pip
interface PipView {
fun setPipParams()
fun enterPipMode(): Boolean
fun hangUp()
}

View file

@ -40,6 +40,7 @@ import io.element.android.features.call.impl.pip.PictureInPictureEvents
import io.element.android.features.call.impl.pip.PictureInPictureState
import io.element.android.features.call.impl.pip.PictureInPictureStateProvider
import io.element.android.features.call.impl.pip.aPictureInPictureState
import io.element.android.features.call.impl.utils.WebViewPipController
import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.ProgressDialog
@ -95,9 +96,9 @@ internal fun CallScreenView(
}
CallWebView(
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)
.fillMaxSize(),
.padding(padding)
.consumeWindowInsets(padding)
.fillMaxSize(),
url = state.urlState,
userAgent = state.userAgent,
onPermissionsRequest = { request ->
@ -108,6 +109,8 @@ internal fun CallScreenView(
onWebViewCreate = { webView ->
val interceptor = WebViewWidgetMessageInterceptor(webView)
state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor))
val pipController = WebViewPipController(webView)
pipState.eventSink(PictureInPictureEvents.SetPipController(pipController))
}
)
when (state.urlState) {

View file

@ -17,6 +17,7 @@
package io.element.android.features.call.impl.ui
import android.Manifest
import android.app.PictureInPictureParams
import android.content.Intent
import android.content.res.Configuration
import android.media.AudioAttributes
@ -24,19 +25,30 @@ import android.media.AudioFocusRequest
import android.media.AudioManager
import android.os.Build
import android.os.Bundle
import android.util.Rational
import android.view.WindowManager
import android.webkit.PermissionRequest
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberUpdatedState
import androidx.core.app.PictureInPictureModeChangedInfo
import androidx.core.content.IntentCompat
import androidx.core.util.Consumer
import androidx.lifecycle.Lifecycle
import io.element.android.features.call.api.CallType
import io.element.android.features.call.impl.DefaultElementCallEntryPoint
import io.element.android.features.call.impl.di.CallBindings
import io.element.android.features.call.impl.pip.PictureInPictureEvents
import io.element.android.features.call.impl.pip.PictureInPicturePresenter
import io.element.android.features.call.impl.pip.PictureInPictureState
import io.element.android.features.call.impl.pip.PipView
import io.element.android.features.call.impl.services.CallForegroundService
import io.element.android.features.call.impl.utils.CallIntentDataParser
import io.element.android.libraries.architecture.bindings
@ -45,7 +57,10 @@ import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import timber.log.Timber
import javax.inject.Inject
class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
class ElementCallActivity :
AppCompatActivity(),
CallScreenNavigator,
PipView {
@Inject lateinit var callIntentDataParser: CallIntentDataParser
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
@Inject lateinit var appPreferencesStore: AppPreferencesStore
@ -86,13 +101,14 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
updateUiMode(resources.configuration)
}
pictureInPicturePresenter.onCreate(this)
pictureInPicturePresenter.setPipView(this)
audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
requestAudioFocus()
setContent {
val pipState = pictureInPicturePresenter.present()
ListenToAndroidEvents(pipState)
ElementThemeApp(appPreferencesStore) {
val state = presenter.present()
eventSink = state.eventSink
@ -108,21 +124,38 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
}
}
@Composable
private fun ListenToAndroidEvents(pipState: PictureInPictureState) {
val pipEventSink by rememberUpdatedState(pipState.eventSink)
DisposableEffect(Unit) {
val onUserLeaveHintListener = Runnable {
pipEventSink(PictureInPictureEvents.EnterPictureInPicture)
}
addOnUserLeaveHintListener(onUserLeaveHintListener)
onDispose {
removeOnUserLeaveHintListener(onUserLeaveHintListener)
}
}
DisposableEffect(Unit) {
val onPictureInPictureModeChangedListener = Consumer { _: PictureInPictureModeChangedInfo ->
pipEventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(isInPictureInPictureMode))
if (!isInPictureInPictureMode && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
Timber.d("Exiting PiP mode: Hangup the call")
eventSink?.invoke(CallScreenEvents.Hangup)
}
}
addOnPictureInPictureModeChangedListener(onPictureInPictureModeChangedListener)
onDispose {
removeOnPictureInPictureModeChangedListener(onPictureInPictureModeChangedListener)
}
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
updateUiMode(newConfig)
}
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
pictureInPicturePresenter.onPictureInPictureModeChanged(isInPictureInPictureMode)
if (!isInPictureInPictureMode && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
Timber.d("Exiting PiP mode: Hangup the call")
eventSink?.invoke(CallScreenEvents.Hangup)
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setCallType(intent)
@ -140,16 +173,11 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
}
}
override fun onUserLeaveHint() {
super.onUserLeaveHint()
pictureInPicturePresenter.onUserLeaveHint()
}
override fun onDestroy() {
super.onDestroy()
releaseAudioFocus()
CallForegroundService.stop(this)
pictureInPicturePresenter.onDestroy()
pictureInPicturePresenter.setPipView(null)
}
override fun finish() {
@ -249,6 +277,33 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
}
}
}
@RequiresApi(Build.VERSION_CODES.O)
override fun setPipParams() {
setPictureInPictureParams(getPictureInPictureParams())
}
@RequiresApi(Build.VERSION_CODES.O)
override fun enterPipMode(): Boolean {
return enterPictureInPictureMode(getPictureInPictureParams())
}
@RequiresApi(Build.VERSION_CODES.O)
private fun getPictureInPictureParams(): PictureInPictureParams {
return PictureInPictureParams.Builder()
// Portrait for calls seems more appropriate
.setAspectRatio(Rational(3, 5))
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
setAutoEnterEnabled(true)
}
}
.build()
}
override fun hangUp() {
eventSink?.invoke(CallScreenEvents.Hangup)
}
}
internal fun mapWebkitPermissions(permissions: Array<String>): List<String> {

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.call.impl.utils
interface PipController {
suspend fun canEnterPip(): Boolean
fun enterPip()
fun exitPip()
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.call.impl.utils
import android.webkit.WebView
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class WebViewPipController(
private val webView: WebView,
) : PipController {
override suspend fun canEnterPip(): Boolean {
return suspendCoroutine { continuation ->
webView.evaluateJavascript("controls.canEnterPip()") { result ->
// Note if the method is not available, it will return "null"
continuation.resume(result == "true" || result == "null")
}
}
}
override fun enterPip() {
webView.evaluateJavascript("controls.enablePip()", null)
}
override fun exitPip() {
webView.evaluateJavascript("controls.disablePip()", null)
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.call.impl.pip
import io.element.android.features.call.impl.utils.PipController
import io.element.android.tests.testutils.lambda.lambdaError
class FakePipController(
private val canEnterPipResult: () -> Boolean = { lambdaError() },
private val enterPipResult: () -> Unit = { lambdaError() },
private val exitPipResult: () -> Unit = { lambdaError() },
) : PipController {
override suspend fun canEnterPip(): Boolean = canEnterPipResult()
override fun enterPip() = enterPipResult()
override fun exitPip() = exitPipResult()
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.call.impl.pip
import io.element.android.tests.testutils.lambda.lambdaError
class FakePipView(
private val setPipParamsResult: () -> Unit = { lambdaError() },
private val enterPipModeResult: () -> Boolean = { lambdaError() },
private val handUpResult: () -> Unit = { lambdaError() }
) : PipView {
override fun setPipParams() = setPipParamsResult()
override fun enterPipMode(): Boolean = enterPipModeResult()
override fun hangUp() = handUpResult()
}

View file

@ -16,23 +16,16 @@
package io.element.android.features.call.impl.pip
import android.os.Build.VERSION_CODES
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.call.impl.ui.ElementCallActivity
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
class PictureInPicturePresenterTest {
@Test
@Config(sdk = [VERSION_CODES.O, VERSION_CODES.S])
fun `when pip is not supported, the state value supportPip is false`() = runTest {
val presenter = createPictureInPicturePresenter(supportPip = false)
moleculeFlow(RecompositionMode.Immediate) {
@ -41,68 +34,119 @@ class PictureInPicturePresenterTest {
val initialState = awaitItem()
assertThat(initialState.supportPip).isFalse()
}
presenter.onDestroy()
presenter.setPipView(null)
}
@Test
@Config(sdk = [VERSION_CODES.O, VERSION_CODES.S])
fun `when pip is supported, the state value supportPip is true`() = runTest {
val presenter = createPictureInPicturePresenter(supportPip = true)
val presenter = createPictureInPicturePresenter(
supportPip = true,
pipView = FakePipView(setPipParamsResult = { }),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.supportPip).isTrue()
}
presenter.onDestroy()
}
@Test
@Config(sdk = [VERSION_CODES.S])
fun `when entering pip is supported, the state value isInPictureInPicture is true`() = runTest {
val presenter = createPictureInPicturePresenter(supportPip = true)
val enterPipModeResult = lambdaRecorder<Boolean> { true }
val presenter = createPictureInPicturePresenter(
supportPip = true,
pipView = FakePipView(
setPipParamsResult = { },
enterPipModeResult = enterPipModeResult,
),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.isInPictureInPicture).isFalse()
initialState.eventSink(PictureInPictureEvents.EnterPictureInPicture)
presenter.onPictureInPictureModeChanged(true)
enterPipModeResult.assertions().isCalledOnce()
initialState.eventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(true))
val pipState = awaitItem()
assertThat(pipState.isInPictureInPicture).isTrue()
// User stops pip
presenter.onPictureInPictureModeChanged(false)
initialState.eventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(false))
val finalState = awaitItem()
assertThat(finalState.isInPictureInPicture).isFalse()
}
presenter.onDestroy()
}
@Test
@Config(sdk = [VERSION_CODES.S])
fun `when onUserLeaveHint is called, the state value isInPictureInPicture becomes true`() = runTest {
val presenter = createPictureInPicturePresenter(supportPip = true)
fun `with webPipApi, when entering pip is supported, but web deny it, the call is finished`() = runTest {
val handUpResult = lambdaRecorder<Unit> { }
val presenter = createPictureInPicturePresenter(
supportPip = true,
pipView = FakePipView(
setPipParamsResult = { },
handUpResult = handUpResult
),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.isInPictureInPicture).isFalse()
presenter.onUserLeaveHint()
presenter.onPictureInPictureModeChanged(true)
initialState.eventSink(PictureInPictureEvents.SetPipController(FakePipController(canEnterPipResult = { false })))
initialState.eventSink(PictureInPictureEvents.EnterPictureInPicture)
handUpResult.assertions().isCalledOnce()
}
}
@Test
fun `with webPipApi, when entering pip is supported, and web allows it, the state value isInPictureInPicture is true`() = runTest {
val enterPipModeResult = lambdaRecorder<Boolean> { true }
val enterPipResult = lambdaRecorder<Unit> { }
val exitPipResult = lambdaRecorder<Unit> { }
val presenter = createPictureInPicturePresenter(
supportPip = true,
pipView = FakePipView(
setPipParamsResult = { },
enterPipModeResult = enterPipModeResult
),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(
PictureInPictureEvents.SetPipController(
FakePipController(
canEnterPipResult = { true },
enterPipResult = enterPipResult,
exitPipResult = exitPipResult,
)
)
)
initialState.eventSink(PictureInPictureEvents.EnterPictureInPicture)
enterPipModeResult.assertions().isCalledOnce()
enterPipResult.assertions().isNeverCalled()
initialState.eventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(true))
val pipState = awaitItem()
assertThat(pipState.isInPictureInPicture).isTrue()
enterPipResult.assertions().isCalledOnce()
// User stops pip
exitPipResult.assertions().isNeverCalled()
initialState.eventSink(PictureInPictureEvents.OnPictureInPictureModeChanged(false))
val finalState = awaitItem()
assertThat(finalState.isInPictureInPicture).isFalse()
exitPipResult.assertions().isCalledOnce()
}
presenter.onDestroy()
}
private fun createPictureInPicturePresenter(
supportPip: Boolean = true,
pipView: PipView? = FakePipView()
): PictureInPicturePresenter {
val activity = Robolectric.buildActivity(ElementCallActivity::class.java)
return PictureInPicturePresenter(
pipSupportProvider = FakePipSupportProvider(supportPip),
).apply {
onCreate(activity.get())
setPipView(pipView)
}
}
}

View file

@ -37,7 +37,7 @@ class CallScreenViewTest {
@Test
fun `clicking on back when pip is not supported hangs up`() {
val eventsRecorder = EventsRecorder<CallScreenEvents>()
val pipEventsRecorder = EventsRecorder<PictureInPictureEvents>(expectEvents = false)
val pipEventsRecorder = EventsRecorder<PictureInPictureEvents>()
rule.setCallScreenView(
aCallScreenState(
eventSink = eventsRecorder
@ -51,6 +51,8 @@ class CallScreenViewTest {
eventsRecorder.assertSize(2)
eventsRecorder.assertTrue(0) { it is CallScreenEvents.SetupMessageChannels }
eventsRecorder.assertTrue(1) { it == CallScreenEvents.Hangup }
pipEventsRecorder.assertSize(1)
pipEventsRecorder.assertTrue(0) { it is PictureInPictureEvents.SetPipController }
}
@Test
@ -69,7 +71,9 @@ class CallScreenViewTest {
rule.pressBack()
eventsRecorder.assertSize(1)
eventsRecorder.assertTrue(0) { it is CallScreenEvents.SetupMessageChannels }
pipEventsRecorder.assertSingle(PictureInPictureEvents.EnterPictureInPicture)
pipEventsRecorder.assertSize(2)
pipEventsRecorder.assertTrue(0) { it is PictureInPictureEvents.SetPipController }
pipEventsRecorder.assertTrue(1) { it == PictureInPictureEvents.EnterPictureInPicture }
}
}