diff --git a/features/call/impl/build.gradle.kts b/features/call/impl/build.gradle.kts index b2d845a003f..50c76b3b559 100644 --- a/features/call/impl/build.gradle.kts +++ b/features/call/impl/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.core) implementation(projects.libraries.designsystem) + implementation(projects.libraries.featureflag.api) implementation(projects.libraries.matrix.impl) implementation(projects.libraries.matrixui) implementation(projects.libraries.network) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureStateProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureStateProvider.kt index 360ee54d3fd..8270e27e7d4 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureStateProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureStateProvider.kt @@ -16,6 +16,16 @@ package io.element.android.features.call.impl.pip +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class PictureInPictureStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aPictureInPictureState(supportPip = true), + aPictureInPictureState(supportPip = true, isInPictureInPicture = true), + ) +} + fun aPictureInPictureState( supportPip: Boolean = false, isInPictureInPicture: Boolean = false, diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipSupportProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipSupportProvider.kt index 16dcb8d66c3..6cf3f080add 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipSupportProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipSupportProvider.kt @@ -24,6 +24,9 @@ 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 io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import kotlinx.coroutines.runBlocking import javax.inject.Inject interface PipSupportProvider { @@ -34,9 +37,15 @@ interface PipSupportProvider { @ContributesBinding(AppScope::class) class DefaultPipSupportProvider @Inject constructor( @ApplicationContext private val context: Context, + private val featureFlagService: FeatureFlagService, ) : 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 + val isSupportedByTheOs = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + context.packageManager?.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE).orFalse() + return if (isSupportedByTheOs) { + runBlocking { featureFlagService.isFeatureEnabled(FeatureFlags.PictureInPicture) } + } else { + false + } } } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt index c8d202f8cc9..70ab2e30c2d 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt @@ -38,6 +38,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons 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.PictureInPictureStateProvider import io.element.android.features.call.impl.pip.aPictureInPictureState import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor import io.element.android.libraries.architecture.AsyncData @@ -81,7 +82,7 @@ internal fun CallScreenView( title = { Text(stringResource(R.string.element_call)) }, navigationIcon = { BackButton( - imageVector = CompoundIcons.Close(), + imageVector = if (pipState.supportPip) CompoundIcons.ArrowLeft() else CompoundIcons.Close(), onClick = ::handleBack, ) } @@ -195,3 +196,15 @@ internal fun CallScreenViewPreview( requestPermissions = { _, _ -> }, ) } + +@PreviewsDayNight +@Composable +internal fun CallScreenPipViewPreview( + @PreviewParameter(PictureInPictureStateProvider::class) state: PictureInPictureState, +) = ElementPreview { + CallScreenView( + state = aCallScreenState(), + pipState = state, + requestPermissions = { _, _ -> }, + ) +} diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt index b0d41efe9e4..3af6c7cf95f 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt @@ -32,6 +32,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.mutableStateOf import androidx.core.content.IntentCompat +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 @@ -41,6 +42,7 @@ import io.element.android.features.call.impl.utils.CallIntentDataParser import io.element.android.libraries.architecture.bindings import io.element.android.libraries.designsystem.theme.ElementThemeApp import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import timber.log.Timber import javax.inject.Inject class ElementCallActivity : AppCompatActivity(), CallScreenNavigator { @@ -63,6 +65,8 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator { private var isDarkMode = false private val webViewTarget = mutableStateOf(null) + private var eventSink: ((CallScreenEvents) -> Unit)? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -91,6 +95,7 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator { val pipState = pictureInPicturePresenter.present() ElementThemeApp(appPreferencesStore) { val state = presenter.present() + eventSink = state.eventSink CallScreenView( state = state, pipState = pipState, @@ -111,6 +116,11 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator { 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) { diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index 17ea03f7a9b..616a549e638 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -113,4 +113,11 @@ enum class FeatureFlags( defaultValue = { true }, isFinished = false, ), + PictureInPicture( + key = "feature.pictureInPicture", + title = "Picture in Picture for Calls", + description = "Allow the Call to be rendered in PiP mode", + defaultValue = { it.buildType != BuildType.RELEASE }, + isFinished = false, + ), } diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt index 52ae3660c6c..ccf6754fc51 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt @@ -72,6 +72,7 @@ class KonsistPreviewTest { "AsyncIndicatorLoadingPreview", "BloomInitialsPreview", "BloomPreview", + "CallScreenPipViewPreview", "ColorAliasesPreview", "DefaultRoomListTopBarWithIndicatorPreview", "GradientFloatingActionButtonCircleShapePreview", diff --git a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Day_0_en.png new file mode 100644 index 00000000000..42d7e86fd0c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b61b391f6380189603939badc2ae5912f5cb072200a001ff946af2d52e81b95a +size 12734 diff --git a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Day_1_en.png new file mode 100644 index 00000000000..8d0995fd9fb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7696eb0f5bf8698d001ea44be6eb2005f414057f9a65703e6d987e8eb75f7f94 +size 9441 diff --git a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Night_0_en.png new file mode 100644 index 00000000000..ba938a8d881 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3d14805d85b33ce1f0fdd26eec433db1973924542cd85a3bb0e4514cdcd857d +size 12392 diff --git a/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Night_1_en.png new file mode 100644 index 00000000000..f8879f9ecb8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.call.impl.ui_CallScreenPipView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:146fc2ce4d344c0ea947fe0370e6b8d94c2e724c69c01c2cc3476a756e1f09e4 +size 9315