Browse Source

Communicate with Element Call about PiP status.

Also only use eventSink to communicate with the Presenter, instead of having public methods.
Change WeakReference to an Activity to a listener and update tests.
pull/3334/head
Benoit Marty 4 weeks ago committed by Benoit Marty
parent
commit
18dcdc0e64
  1. 4
      features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureEvents.kt
  2. 94
      features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt
  3. 23
      features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipActivity.kt
  4. 3
      features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt
  5. 51
      features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt
  6. 23
      features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebPipApi.kt
  7. 41
      features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWebPipApi.kt
  8. 29
      features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipActivity.kt
  9. 32
      features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakeWebPipApi.kt
  10. 96
      features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenterTest.kt

4
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureEvents.kt

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

94
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt

@ -16,17 +16,17 @@ @@ -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.WebPipApi
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( @@ -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 pipActivity: PipActivity? = null
@Composable
override fun present(): PictureInPictureState {
val coroutineScope = rememberCoroutineScope()
var isInPictureInPicture by remember { mutableStateOf(false) }
var webPipApi by remember { mutableStateOf<WebPipApi?>(null) }
fun handleEvent(event: PictureInPictureEvents) {
when (event) {
PictureInPictureEvents.EnterPictureInPicture -> switchToPip()
is PictureInPictureEvents.SetupWebPipApi -> {
webPipApi = event.webPipApi
}
PictureInPictureEvents.EnterPictureInPicture -> {
coroutineScope.launch {
switchToPip(webPipApi)
}
}
is PictureInPictureEvents.OnPictureInPictureModeChanged -> {
Timber.tag(loggerTag.value).d("onPictureInPictureModeChanged: ${event.isInPip}")
isInPictureInPicture = event.isInPip
if (event.isInPip) {
webPipApi?.enterPip()
} else {
webPipApi?.exitPip()
}
}
}
}
return PictureInPictureState(
supportPip = isPipSupported,
isInPictureInPicture = isInPictureInPicture.value,
isInPictureInPicture = isInPictureInPicture,
eventSink = ::handleEvent,
)
}
fun onCreate(activity: Activity) {
fun setPipActivity(pipActivity: PipActivity?) {
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.pipActivity = pipActivity
pipActivity?.setPipParams()
} else {
Timber.tag(loggerTag.value).d("onCreate: 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(webPipApi: WebPipApi?) {
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 (webPipApi == null) {
Timber.tag(loggerTag.value).w("webPipApi is not available")
}
if (webPipApi == null || webPipApi.canEnterPip()) {
Timber.tag(loggerTag.value).d("Switch to PiP mode")
pipActivity?.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")
pipActivity?.hangUp()
}
}
}
fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
Timber.tag(loggerTag.value).d("onPictureInPictureModeChanged: $isInPictureInPictureMode")
isInPictureInPicture.value = isInPictureInPictureMode
}
fun onUserLeaveHint() {
Timber.tag(loggerTag.value).d("onUserLeaveHint")
switchToPip()
}
}

23
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipActivity.kt

@ -0,0 +1,23 @@ @@ -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 PipActivity {
fun setPipParams()
fun enterPipMode(): Boolean
fun hangUp()
}

3
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt

@ -40,6 +40,7 @@ import io.element.android.features.call.impl.pip.PictureInPictureEvents @@ -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.WebViewWebPipApi
import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.ProgressDialog
@ -108,6 +109,8 @@ internal fun CallScreenView( @@ -108,6 +109,8 @@ internal fun CallScreenView(
onWebViewCreate = { webView ->
val interceptor = WebViewWidgetMessageInterceptor(webView)
state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor))
val webPipApi = WebViewWebPipApi(webView)
pipState.eventSink(PictureInPictureEvents.SetupWebPipApi(webPipApi))
}
)
when (state.urlState) {

51
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt

@ -17,6 +17,7 @@ @@ -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,11 +25,13 @@ import android.media.AudioFocusRequest @@ -24,11 +25,13 @@ 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.mutableStateOf
import androidx.core.content.IntentCompat
@ -36,7 +39,9 @@ import androidx.lifecycle.Lifecycle @@ -36,7 +39,9 @@ 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.PipActivity
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 +50,10 @@ import io.element.android.libraries.preferences.api.store.AppPreferencesStore @@ -45,7 +50,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,
PipActivity {
@Inject lateinit var callIntentDataParser: CallIntentDataParser
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
@Inject lateinit var appPreferencesStore: AppPreferencesStore
@ -66,6 +74,7 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator { @@ -66,6 +74,7 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
private val webViewTarget = mutableStateOf<CallType?>(null)
private var eventSink: ((CallScreenEvents) -> Unit)? = null
private var pipEventSink: ((PictureInPictureEvents) -> Unit)? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -86,13 +95,14 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator { @@ -86,13 +95,14 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
updateUiMode(resources.configuration)
}
pictureInPicturePresenter.onCreate(this)
pictureInPicturePresenter.setPipActivity(this)
audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
requestAudioFocus()
setContent {
val pipState = pictureInPicturePresenter.present()
pipEventSink = pipState.eventSink
ElementThemeApp(appPreferencesStore) {
val state = presenter.present()
eventSink = state.eventSink
@ -115,7 +125,7 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator { @@ -115,7 +125,7 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
pictureInPicturePresenter.onPictureInPictureModeChanged(isInPictureInPictureMode)
pipEventSink?.invoke(PictureInPictureEvents.OnPictureInPictureModeChanged(isInPictureInPictureMode))
if (!isInPictureInPictureMode && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
Timber.d("Exiting PiP mode: Hangup the call")
@ -142,14 +152,14 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator { @@ -142,14 +152,14 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
override fun onUserLeaveHint() {
super.onUserLeaveHint()
pictureInPicturePresenter.onUserLeaveHint()
pipEventSink?.invoke(PictureInPictureEvents.EnterPictureInPicture)
}
override fun onDestroy() {
super.onDestroy()
releaseAudioFocus()
CallForegroundService.stop(this)
pictureInPicturePresenter.onDestroy()
pictureInPicturePresenter.setPipActivity(null)
}
override fun finish() {
@ -249,6 +259,37 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator { @@ -249,6 +259,37 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
}
}
}
override fun setPipParams() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setPictureInPictureParams(getPictureInPictureParams())
}
}
override fun enterPipMode(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
enterPictureInPictureMode(getPictureInPictureParams())
} else {
false
}
}
@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> {

23
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebPipApi.kt

@ -0,0 +1,23 @@ @@ -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 WebPipApi {
suspend fun canEnterPip(): Boolean
fun enterPip()
fun exitPip()
}

41
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWebPipApi.kt

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
/*
* 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 WebViewWebPipApi(
private val webView: WebView,
) : WebPipApi {
override suspend fun canEnterPip(): Boolean {
return suspendCoroutine { continuation ->
webView.evaluateJavascript("controls.canEnterPip()") { result ->
continuation.resume(result == "true")
}
}
}
override fun enterPip() {
webView.evaluateJavascript("controls.enablePip()", null)
}
override fun exitPip() {
webView.evaluateJavascript("controls.disablePip()", null)
}
}

29
features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipActivity.kt

@ -0,0 +1,29 @@ @@ -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 FakePipActivity(
private val setPipParamsResult: () -> Unit = { lambdaError() },
private val enterPipModeResult: () -> Boolean = { lambdaError() },
private val handUpResult: () -> Unit = { lambdaError() }
) : PipActivity {
override fun setPipParams() = setPipParamsResult()
override fun enterPipMode(): Boolean = enterPipModeResult()
override fun hangUp() = handUpResult()
}

32
features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakeWebPipApi.kt

@ -0,0 +1,32 @@ @@ -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.WebPipApi
import io.element.android.tests.testutils.lambda.lambdaError
class FakeWebPipApi(
private val canEnterPipResult: () -> Boolean = { lambdaError() },
private val enterPipResult: () -> Unit = { lambdaError() },
private val exitPipResult: () -> Unit = { lambdaError() },
) : WebPipApi {
override suspend fun canEnterPip(): Boolean = canEnterPipResult()
override fun enterPip() = enterPipResult()
override fun exitPip() = exitPipResult()
}

96
features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenterTest.kt

@ -16,23 +16,16 @@ @@ -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 { @@ -41,68 +34,119 @@ class PictureInPicturePresenterTest {
val initialState = awaitItem()
assertThat(initialState.supportPip).isFalse()
}
presenter.onDestroy()
presenter.setPipActivity(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,
pipActivity = FakePipActivity(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,
pipActivity = FakePipActivity(
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,
pipActivity = FakePipActivity(
setPipParamsResult = { },
handUpResult = handUpResult
),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.isInPictureInPicture).isFalse()
presenter.onUserLeaveHint()
presenter.onPictureInPictureModeChanged(true)
initialState.eventSink(PictureInPictureEvents.SetupWebPipApi(FakeWebPipApi(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,
pipActivity = FakePipActivity(
setPipParamsResult = { },
enterPipModeResult = enterPipModeResult
),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(
PictureInPictureEvents.SetupWebPipApi(
FakeWebPipApi(
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,
pipActivity: PipActivity? = FakePipActivity()
): PictureInPicturePresenter {
val activity = Robolectric.buildActivity(ElementCallActivity::class.java)
return PictureInPicturePresenter(
pipSupportProvider = FakePipSupportProvider(supportPip),
).apply {
onCreate(activity.get())
setPipActivity(pipActivity)
}
}
}

Loading…
Cancel
Save