From 515f3dd43f2be38f6356ee9b570273cd7c5a4baf Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 14 Aug 2025 09:28:33 -0300 Subject: [PATCH] Add proper picture in picture support to compose CallScreen component. --- .../components/webrtc/v2/CallScreen.kt | 12 +++++ .../webrtc/v2/ComposeCallScreenMediator.kt | 2 + .../webrtc/v2/PictureInPictureCallScreen.kt | 38 ++++++++++++++++ .../core/ui/compose/ActivityComponents.kt | 44 +++++++++++++++++++ 4 files changed, 96 insertions(+) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/PictureInPictureCallScreen.kt create mode 100644 core-ui/src/main/java/org/signal/core/ui/compose/ActivityComponents.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt index fbb88c0008..2d05b13334 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt @@ -80,6 +80,7 @@ fun CallScreen( callRecipient: Recipient, webRtcCallState: WebRtcViewModel.State, isRemoteVideoOffer: Boolean, + isInPipMode: Boolean, callScreenState: CallScreenState, callControlsState: CallControlsState, callScreenController: CallScreenController = CallScreenController.rememberCallScreenController( @@ -110,6 +111,16 @@ fun CallScreen( callStatus = callScreenState.callStatus, callScreenControlsListener = callScreenControlsListener ) + + return + } + + if (isInPipMode) { + PictureInPictureCallScreen( + callParticipantsPagerState = callParticipantsPagerState, + callScreenController = callScreenController + ) + return } @@ -532,6 +543,7 @@ private fun CallScreenPreview() { callRecipient = Recipient(systemContactName = "Test User"), webRtcCallState = WebRtcViewModel.State.CALL_CONNECTED, isRemoteVideoOffer = false, + isInPipMode = false, callScreenState = CallScreenState(), callControlsState = CallControlsState( displayMicToggle = true, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/ComposeCallScreenMediator.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/ComposeCallScreenMediator.kt index 5b3247f89f..c2baaf7646 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/ComposeCallScreenMediator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/ComposeCallScreenMediator.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.signal.core.ui.compose.rememberIsInPipMode import org.signal.core.ui.compose.theme.SignalTheme import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.components.webrtc.CallParticipantListUpdate @@ -122,6 +123,7 @@ class ComposeCallScreenMediator(private val activity: WebRtcCallActivity, viewMo callRecipient = recipient, webRtcCallState = webRtcCallState, isRemoteVideoOffer = viewModel.isAnswerWithVideoAvailable(), + isInPipMode = rememberIsInPipMode(), callScreenState = callScreenState, callControlsState = callControlsState, callScreenController = callScreenController, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/PictureInPictureCallScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/PictureInPictureCallScreen.kt new file mode 100644 index 0000000000..5fa389b5d5 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/PictureInPictureCallScreen.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.webrtc.v2 + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import kotlinx.coroutines.launch + +/** + * Displayed when the user minimizes the call screen while a call is ongoing. + */ +@Composable +fun PictureInPictureCallScreen( + callParticipantsPagerState: CallParticipantsPagerState, + callScreenController: CallScreenController +) { + val scope = rememberCoroutineScope() + + CallParticipantsPager( + callParticipantsPagerState = callParticipantsPagerState, + modifier = Modifier + .fillMaxSize() + .clickable( + onClick = { + scope.launch { + callScreenController.handleEvent(CallScreenController.Event.TOGGLE_CONTROLS) + } + }, + enabled = false + ) + ) +} diff --git a/core-ui/src/main/java/org/signal/core/ui/compose/ActivityComponents.kt b/core-ui/src/main/java/org/signal/core/ui/compose/ActivityComponents.kt new file mode 100644 index 0000000000..5bfd5a3671 --- /dev/null +++ b/core-ui/src/main/java/org/signal/core/ui/compose/ActivityComponents.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.core.ui.compose + +import android.os.Build +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.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.core.app.PictureInPictureModeChangedInfo +import androidx.core.util.Consumer + +/** + * Returns whether the screen is currently in the system picture-in-picture mode. + * + * This requires an AppCompatActivity context, so it cannot be utilized in Composables + * that require a preview. + */ +@Composable +fun rememberIsInPipMode(): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val activity = LocalContext.current as AppCompatActivity + var pipMode: Boolean by remember { mutableStateOf(activity.isInPictureInPictureMode) } + DisposableEffect(activity) { + val observer = Consumer { info -> + pipMode = info.isInPictureInPictureMode + } + activity.addOnPictureInPictureModeChangedListener( + observer + ) + onDispose { activity.removeOnPictureInPictureModeChangedListener(observer) } + } + return pipMode + } else { + return false + } +}