Add support for Picture in Picture for ElementCallActivity
This commit is contained in:
parent
2fb7220ec4
commit
e5ad655242
10 changed files with 395 additions and 11 deletions
|
|
@ -38,10 +38,11 @@
|
||||||
<application>
|
<application>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.ElementCallActivity"
|
android:name=".ui.ElementCallActivity"
|
||||||
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
|
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/element_call"
|
android:label="@string/element_call"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
|
android:supportsPictureInPicture="true"
|
||||||
android:taskAffinity="io.element.android.features.call">
|
android:taskAffinity="io.element.android.features.call">
|
||||||
|
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
sealed interface PictureInPictureEvents {
|
||||||
|
data object EnterPictureInPicture : PictureInPictureEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
* 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 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.mutableStateOf
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("PiP")
|
||||||
|
|
||||||
|
class PictureInPicturePresenter @Inject constructor(
|
||||||
|
pipSupportProvider: PipSupportProvider,
|
||||||
|
) : Presenter<PictureInPictureState> {
|
||||||
|
private val isPipSupported = pipSupportProvider.isPipSupported()
|
||||||
|
private var isInPictureInPicture = mutableStateOf(false)
|
||||||
|
private var hostActivity: WeakReference<Activity>? = null
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): PictureInPictureState {
|
||||||
|
fun handleEvent(event: PictureInPictureEvents) {
|
||||||
|
when (event) {
|
||||||
|
PictureInPictureEvents.EnterPictureInPicture -> switchToPip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PictureInPictureState(
|
||||||
|
supportPip = isPipSupported,
|
||||||
|
isInPictureInPicture = isInPictureInPicture.value,
|
||||||
|
eventSink = ::handleEvent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCreate(activity: Activity) {
|
||||||
|
if (isPipSupported) {
|
||||||
|
Timber.tag(loggerTag.value).d("onCreate: Setting PiP params")
|
||||||
|
hostActivity = WeakReference(activity)
|
||||||
|
hostActivity?.get()?.setPictureInPictureParams(getPictureInPictureParams())
|
||||||
|
} 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.
|
||||||
|
*/
|
||||||
|
private fun switchToPip() {
|
||||||
|
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") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
|
||||||
|
Timber.tag(loggerTag.value).d("onPictureInPictureModeChanged: $isInPictureInPictureMode")
|
||||||
|
isInPictureInPicture.value = isInPictureInPictureMode
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onUserLeaveHint() {
|
||||||
|
Timber.tag(loggerTag.value).d("onUserLeaveHint")
|
||||||
|
switchToPip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
data class PictureInPictureState(
|
||||||
|
val supportPip: Boolean,
|
||||||
|
val isInPictureInPicture: Boolean,
|
||||||
|
val eventSink: (PictureInPictureEvents) -> Unit,
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
fun aPictureInPictureState(
|
||||||
|
supportPip: Boolean = false,
|
||||||
|
isInPictureInPicture: Boolean = false,
|
||||||
|
eventSink: (PictureInPictureEvents) -> Unit = {},
|
||||||
|
): PictureInPictureState {
|
||||||
|
return PictureInPictureState(
|
||||||
|
supportPip = supportPip,
|
||||||
|
isInPictureInPicture = isInPictureInPicture,
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
*
|
||||||
|
* 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 android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.ChecksSdkIntAtLeast
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
interface PipSupportProvider {
|
||||||
|
@ChecksSdkIntAtLeast(Build.VERSION_CODES.O)
|
||||||
|
fun isPipSupported(): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultPipSupportProvider @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
) : PipSupportProvider {
|
||||||
|
override fun isPipSupported(): Boolean {
|
||||||
|
val hasSystemFeaturePip = context.packageManager?.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE).orFalse()
|
||||||
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && hasSystemFeaturePip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.call.impl.R
|
import io.element.android.features.call.impl.R
|
||||||
|
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.aPictureInPictureState
|
||||||
import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor
|
import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
|
|
@ -58,25 +61,36 @@ interface CallScreenNavigator {
|
||||||
@Composable
|
@Composable
|
||||||
internal fun CallScreenView(
|
internal fun CallScreenView(
|
||||||
state: CallScreenState,
|
state: CallScreenState,
|
||||||
|
pipState: PictureInPictureState,
|
||||||
requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit,
|
requestPermissions: (Array<String>, RequestPermissionCallback) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
fun handleBack() {
|
||||||
|
if (pipState.supportPip) {
|
||||||
|
pipState.eventSink.invoke(PictureInPictureEvents.EnterPictureInPicture)
|
||||||
|
} else {
|
||||||
|
state.eventSink(CallScreenEvents.Hangup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
if (!pipState.isInPictureInPicture) {
|
||||||
title = { Text(stringResource(R.string.element_call)) },
|
TopAppBar(
|
||||||
navigationIcon = {
|
title = { Text(stringResource(R.string.element_call)) },
|
||||||
BackButton(
|
navigationIcon = {
|
||||||
imageVector = CompoundIcons.Close(),
|
BackButton(
|
||||||
onClick = { state.eventSink(CallScreenEvents.Hangup) }
|
imageVector = CompoundIcons.Close(),
|
||||||
)
|
onClick = ::handleBack,
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
BackHandler {
|
BackHandler {
|
||||||
state.eventSink(CallScreenEvents.Hangup)
|
handleBack()
|
||||||
}
|
}
|
||||||
CallWebView(
|
CallWebView(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -177,6 +191,7 @@ internal fun CallScreenViewPreview(
|
||||||
) = ElementPreview {
|
) = ElementPreview {
|
||||||
CallScreenView(
|
CallScreenView(
|
||||||
state = state,
|
state = state,
|
||||||
|
pipState = aPictureInPictureState(),
|
||||||
requestPermissions = { _, _ -> },
|
requestPermissions = { _, _ -> },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ import io.element.android.compound.theme.mapToTheme
|
||||||
import io.element.android.features.call.api.CallType
|
import io.element.android.features.call.api.CallType
|
||||||
import io.element.android.features.call.impl.DefaultElementCallEntryPoint
|
import io.element.android.features.call.impl.DefaultElementCallEntryPoint
|
||||||
import io.element.android.features.call.impl.di.CallBindings
|
import io.element.android.features.call.impl.di.CallBindings
|
||||||
|
import io.element.android.features.call.impl.pip.PictureInPicturePresenter
|
||||||
import io.element.android.features.call.impl.services.CallForegroundService
|
import io.element.android.features.call.impl.services.CallForegroundService
|
||||||
import io.element.android.features.call.impl.utils.CallIntentDataParser
|
import io.element.android.features.call.impl.utils.CallIntentDataParser
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
|
|
@ -52,6 +53,7 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
||||||
@Inject lateinit var callIntentDataParser: CallIntentDataParser
|
@Inject lateinit var callIntentDataParser: CallIntentDataParser
|
||||||
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
|
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
|
||||||
@Inject lateinit var appPreferencesStore: AppPreferencesStore
|
@Inject lateinit var appPreferencesStore: AppPreferencesStore
|
||||||
|
@Inject lateinit var pictureInPicturePresenter: PictureInPicturePresenter
|
||||||
|
|
||||||
private lateinit var presenter: CallScreenPresenter
|
private lateinit var presenter: CallScreenPresenter
|
||||||
|
|
||||||
|
|
@ -86,6 +88,8 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
||||||
updateUiMode(resources.configuration)
|
updateUiMode(resources.configuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pictureInPicturePresenter.onCreate(this)
|
||||||
|
|
||||||
audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
|
audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
|
||||||
requestAudioFocus()
|
requestAudioFocus()
|
||||||
|
|
||||||
|
|
@ -95,11 +99,13 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
||||||
}
|
}
|
||||||
.collectAsState(initial = Theme.System)
|
.collectAsState(initial = Theme.System)
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
val pipState = pictureInPicturePresenter.present()
|
||||||
ElementTheme(
|
ElementTheme(
|
||||||
darkTheme = theme.isDark()
|
darkTheme = theme.isDark()
|
||||||
) {
|
) {
|
||||||
CallScreenView(
|
CallScreenView(
|
||||||
state = state,
|
state = state,
|
||||||
|
pipState = pipState,
|
||||||
requestPermissions = { permissions, callback ->
|
requestPermissions = { permissions, callback ->
|
||||||
requestPermissionCallback = callback
|
requestPermissionCallback = callback
|
||||||
requestPermissionsLauncher.launch(permissions)
|
requestPermissionsLauncher.launch(permissions)
|
||||||
|
|
@ -114,6 +120,11 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
||||||
updateUiMode(newConfig)
|
updateUiMode(newConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
|
||||||
|
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||||
|
pictureInPicturePresenter.onPictureInPictureModeChanged(isInPictureInPictureMode)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
setCallType(intent)
|
setCallType(intent)
|
||||||
|
|
@ -131,10 +142,16 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onUserLeaveHint() {
|
||||||
|
super.onUserLeaveHint()
|
||||||
|
pictureInPicturePresenter.onUserLeaveHint()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
releaseAudioFocus()
|
releaseAudioFocus()
|
||||||
CallForegroundService.stop(this)
|
CallForegroundService.stop(this)
|
||||||
|
pictureInPicturePresenter.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
class FakePipSupportProvider(
|
||||||
|
private val isPipSupported: Boolean
|
||||||
|
) : PipSupportProvider {
|
||||||
|
override fun isPipSupported() = isPipSupported
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
* 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 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 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.N, 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) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.supportPip).isFalse()
|
||||||
|
}
|
||||||
|
presenter.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@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)
|
||||||
|
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)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.isInPictureInPicture).isFalse()
|
||||||
|
initialState.eventSink(PictureInPictureEvents.EnterPictureInPicture)
|
||||||
|
presenter.onPictureInPictureModeChanged(true)
|
||||||
|
val pipState = awaitItem()
|
||||||
|
assertThat(pipState.isInPictureInPicture).isTrue()
|
||||||
|
// User stops pip
|
||||||
|
presenter.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)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.isInPictureInPicture).isFalse()
|
||||||
|
presenter.onUserLeaveHint()
|
||||||
|
presenter.onPictureInPictureModeChanged(true)
|
||||||
|
val pipState = awaitItem()
|
||||||
|
assertThat(pipState.isInPictureInPicture).isTrue()
|
||||||
|
}
|
||||||
|
presenter.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPictureInPicturePresenter(
|
||||||
|
supportPip: Boolean = true,
|
||||||
|
): PictureInPicturePresenter {
|
||||||
|
val activity = Robolectric.buildActivity(ElementCallActivity::class.java)
|
||||||
|
return PictureInPicturePresenter(
|
||||||
|
pipSupportProvider = FakePipSupportProvider(supportPip),
|
||||||
|
).apply {
|
||||||
|
onCreate(activity.get())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue