diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallActivity.kt index 08c7357322..70cb5dcd63 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallActivity.kt @@ -18,8 +18,10 @@ import androidx.compose.material3.Surface import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rxjava3.subscribeAsState +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.lifecycle.compose.LocalLifecycleOwner @@ -36,11 +38,13 @@ import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel import org.thoughtcrime.securesms.components.webrtc.controls.CallInfoView import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoViewModel +import org.thoughtcrime.securesms.components.webrtc.controls.RaiseHandSnackbar import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.events.WebRtcViewModel import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.util.FullscreenHelper import org.thoughtcrime.securesms.util.VibrateUtil import org.thoughtcrime.securesms.util.viewModel import org.whispersystems.signalservice.api.messages.calls.HangupMessage @@ -77,6 +81,8 @@ class CallActivity : BaseActivity(), CallControlsCallback { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val fullscreenHelper = FullscreenHelper(this) + lifecycleDisposable.bindTo(this) val compositeDisposable = CompositeDisposable() lifecycleDisposable.add(compositeDisposable) @@ -120,10 +126,20 @@ class CallActivity : BaseActivity(), CallControlsCallback { } } + var areControlsVisible by remember { mutableStateOf(true) } + + LaunchedEffect(areControlsVisible) { + if (areControlsVisible) { + fullscreenHelper.showSystemUI() + } else { + fullscreenHelper.hideSystemUI() + } + } + SignalTheme { Surface { CallScreen( - callRecipient = recipient ?: Recipient.UNKNOWN, + callRecipient = recipient, webRtcCallState = callParticipantsState.callState, callScreenState = callScreenState, callControlsState = callControlsState, @@ -146,8 +162,16 @@ class CallActivity : BaseActivity(), CallControlsCallback { .alpha(it) ) }, + raiseHandSnackbar = { + RaiseHandSnackbar.View( + webRtcCallViewModel = webRtcCallViewModel, + showCallInfoListener = { /*TODO*/ }, + modifier = it + ) + }, onNavigationClick = { finish() }, - onLocalPictureInPictureClicked = webRtcCallViewModel::onLocalPictureInPictureClicked + onLocalPictureInPictureClicked = webRtcCallViewModel::onLocalPictureInPictureClicked, + onControlsToggled = { areControlsVisible = it } ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallControls.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallControls.kt index 894fdc13dc..aa69fbfa9c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallControls.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallControls.kt @@ -11,12 +11,19 @@ import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat @@ -41,10 +48,20 @@ fun CallControls( ) { val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT + val density = LocalDensity.current + val padBottom = with(density) { WindowInsets.navigationBars.getBottom(density).toDp() } + var bottom by remember { + mutableStateOf(padBottom) + } + + if (padBottom != 0.dp) { + bottom = padBottom + } + Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = spacedBy(30.dp), - modifier = modifier.navigationBarsPadding() + modifier = modifier.padding(bottom = bottom) ) { Row( horizontalArrangement = spacedBy(20.dp) 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 8e630be6e5..fd9fe93870 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 @@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.components.webrtc.v2 import android.content.res.Configuration import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn @@ -19,12 +20,10 @@ import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width @@ -38,10 +37,12 @@ import androidx.compose.material3.Text import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberStandardBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -50,11 +51,10 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.signal.core.ui.BottomSheets import org.signal.core.ui.Previews @@ -65,6 +65,7 @@ import org.thoughtcrime.securesms.events.WebRtcViewModel import org.thoughtcrime.securesms.recipients.Recipient import kotlin.math.max import kotlin.math.round +import kotlin.time.Duration.Companion.seconds private const val DRAG_HANDLE_HEIGHT = 22 private const val SHEET_TOP_PADDING = 9 @@ -86,19 +87,26 @@ fun CallScreen( localParticipant: CallParticipant, localRenderState: WebRtcLocalRenderState, callInfoView: @Composable (Float) -> Unit, + raiseHandSnackbar: @Composable (Modifier) -> Unit, onNavigationClick: () -> Unit, - onLocalPictureInPictureClicked: () -> Unit + onLocalPictureInPictureClicked: () -> Unit, + onControlsToggled: (Boolean) -> Unit ) { var peekPercentage by remember { mutableFloatStateOf(0f) } + val skipHiddenState by rememberUpdatedState(newValue = callControlsState.skipHiddenState) + val valueChangeOperation: (SheetValue) -> Boolean = remember { + { + !(it == SheetValue.Hidden && skipHiddenState) + } + } + val scope = rememberCoroutineScope() val scaffoldState = rememberBottomSheetScaffoldState( bottomSheetState = rememberStandardBottomSheetState( - confirmValueChange = { - !(it == SheetValue.Hidden && callControlsState.skipHiddenState) - }, + confirmValueChange = valueChangeOperation, skipHiddenState = false ) ) @@ -169,7 +177,8 @@ fun CallScreen( overflowParticipants = overflowParticipants, scaffoldState = scaffoldState, callControlsState = callControlsState, - onPipClick = onLocalPictureInPictureClicked + onPipClick = onLocalPictureInPictureClicked, + onControlsToggled = onControlsToggled ) } @@ -187,7 +196,8 @@ fun CallScreen( overflowParticipants = overflowParticipants, scaffoldState = scaffoldState, callControlsState = callControlsState, - onPipClick = onLocalPictureInPictureClicked + onPipClick = onLocalPictureInPictureClicked, + onControlsToggled = onControlsToggled ) } } @@ -203,13 +213,19 @@ fun CallScreen( } if (webRtcCallState.isPassedPreJoin) { - CallScreenTopBar( - callRecipient = callRecipient, - callStatus = callScreenState.callStatus, - onNavigationClick = onNavigationClick, - onCallInfoClick = onCallInfoClick, - modifier = Modifier.padding(bottom = padding) - ) + AnimatedVisibility( + visible = scaffoldState.bottomSheetState.targetValue != SheetValue.Hidden, + enter = fadeIn(), + exit = fadeOut() + ) { + CallScreenTopBar( + callRecipient = callRecipient, + callStatus = callScreenState.callStatus, + onNavigationClick = onNavigationClick, + onCallInfoClick = onCallInfoClick, + modifier = Modifier.padding(bottom = padding) + ) + } } else { CallScreenPreJoinOverlay( callRecipient = callRecipient, @@ -221,6 +237,8 @@ fun CallScreen( ) } + raiseHandSnackbar(Modifier.fillMaxWidth()) + AnimatedCallStateUpdate( callControlsChange = callScreenState.callControlsChange, modifier = Modifier @@ -246,7 +264,8 @@ private fun BoxScope.Viewport( overflowParticipants: List, scaffoldState: BottomSheetScaffoldState, callControlsState: CallControlsState, - onPipClick: () -> Unit + onPipClick: () -> Unit, + onControlsToggled: (Boolean) -> Unit ) { if (webRtcCallState.isPreJoinOrNetworkUnavailable) { LargeLocalVideoRenderer( @@ -260,6 +279,15 @@ private fun BoxScope.Viewport( val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT val scope = rememberCoroutineScope() + val hideSheet by rememberUpdatedState(newValue = scaffoldState.bottomSheetState.currentValue == SheetValue.PartiallyExpanded && !callControlsState.skipHiddenState) + LaunchedEffect(hideSheet) { + if (hideSheet) { + delay(5.seconds) + scaffoldState.bottomSheetState.hide() + onControlsToggled(false) + } + } + Row { Column( modifier = Modifier.weight(1f) @@ -275,7 +303,9 @@ private fun BoxScope.Viewport( scope.launch { if (scaffoldState.bottomSheetState.isVisible) { scaffoldState.bottomSheetState.hide() + onControlsToggled(false) } else { + onControlsToggled(true) scaffoldState.bottomSheetState.show() } } @@ -317,17 +347,9 @@ private fun BoxScope.Viewport( } if (webRtcCallState.inOngoingCall && localParticipant.isVideoEnabled && !isLargeGroupCall) { - val padBottom: Dp = if (scaffoldState.bottomSheetState.isVisible) { - 0.dp - } else { - val density = LocalDensity.current - with(density) { WindowInsets.navigationBars.getBottom(density).toDp() } - } - SmallMoveableLocalVideoRenderer( localParticipant = localParticipant, localRenderState = localRenderState, - extraPadBottom = padBottom, onClick = onPipClick ) } @@ -365,12 +387,14 @@ private fun TinyLocalVideoRenderer( val smallSize = remember(isPortrait) { if (isPortrait) DpSize(40.dp, 72.dp) else DpSize(72.dp, 40.dp) } - val largeSize = remember(isPortrait) { + val expandedSize = remember(isPortrait) { if (isPortrait) DpSize(180.dp, 320.dp) else DpSize(320.dp, 180.dp) } - val width by animateDpAsState(label = "tiny-width", targetValue = if (localRenderState == WebRtcLocalRenderState.EXPANDED) largeSize.width else smallSize.width) - val height by animateDpAsState(label = "tiny-height", targetValue = if (localRenderState == WebRtcLocalRenderState.EXPANDED) largeSize.height else smallSize.height) + val size = if (localRenderState == WebRtcLocalRenderState.EXPANDED) expandedSize else smallSize + + val width by animateDpAsState(label = "tiny-width", targetValue = size.width) + val height by animateDpAsState(label = "tiny-height", targetValue = size.height) LocalParticipantRenderer( localParticipant = localParticipant, @@ -391,7 +415,6 @@ private fun TinyLocalVideoRenderer( private fun SmallMoveableLocalVideoRenderer( localParticipant: CallParticipant, localRenderState: WebRtcLocalRenderState, - extraPadBottom: Dp, onClick: () -> Unit ) { val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT @@ -399,15 +422,14 @@ private fun SmallMoveableLocalVideoRenderer( val smallSize = remember(isPortrait) { if (isPortrait) DpSize(90.dp, 160.dp) else DpSize(160.dp, 90.dp) } - val largeSize = remember(isPortrait) { + val expandedSize = remember(isPortrait) { if (isPortrait) DpSize(180.dp, 320.dp) else DpSize(320.dp, 180.dp) } - val size = if (localRenderState == WebRtcLocalRenderState.SMALL_RECTANGLE) smallSize else largeSize + val size = if (localRenderState == WebRtcLocalRenderState.EXPANDED) expandedSize else smallSize val targetWidth by animateDpAsState(label = "animate-pip-width", targetValue = size.width, animationSpec = tween()) val targetHeight by animateDpAsState(label = "animate-pip-height", targetValue = size.height, animationSpec = tween()) - val bottomPadding by animateDpAsState(label = "animate-pip-bottom-pad", targetValue = extraPadBottom, animationSpec = tween()) PictureInPicture( contentSize = DpSize(targetWidth, targetHeight), @@ -415,7 +437,6 @@ private fun SmallMoveableLocalVideoRenderer( .fillMaxSize() .padding(16.dp) .statusBarsPadding() - .padding(bottom = bottomPadding) ) { LocalParticipantRenderer( localParticipant = localParticipant, @@ -482,9 +503,11 @@ private fun CallScreenPreview() { callInfoView = { Text(text = "Call Info View Preview", modifier = Modifier.alpha(it)) }, + raiseHandSnackbar = {}, onNavigationClick = {}, onLocalPictureInPictureClicked = {}, - overflowParticipants = (1..5).map { CallParticipant() } + overflowParticipants = emptyList(), + onControlsToggled = {} ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/LocalParticipantRenderer.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/LocalParticipantRenderer.kt index 064babe832..538ddb1fe0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/LocalParticipantRenderer.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/LocalParticipantRenderer.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.unit.DpSize import androidx.compose.ui.viewinterop.AndroidView import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatarDrawable @@ -47,13 +48,19 @@ fun LocalParticipantRenderer( ) } + val localRecipient = if (LocalInspectionMode.current) { + Recipient() + } else { + localParticipant.recipient + } + val model = remember { - ProfileContactPhoto(Recipient.self()) + ProfileContactPhoto(localRecipient) } val context = LocalContext.current val fallback = remember { - FallbackAvatarDrawable(context, Recipient.self().getFallbackAvatar()) + FallbackAvatarDrawable(context, localRecipient.getFallbackAvatar()) } GlideImage( diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java index 57457d94d5..ce7e9d4d6f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java @@ -91,9 +91,9 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor { seen.add(Recipient.self()); for (GroupCall.RemoteDeviceState device : remoteDeviceStates) { - Recipient recipient = Recipient.externalPush(ACI.from(device.getUserId())); - CallParticipantId callParticipantId = new CallParticipantId(device.getDemuxId(), recipient.getId()); - CallParticipant callParticipant = participants.get(callParticipantId); + Recipient recipient = Recipient.externalPush(ACI.from(device.getUserId())); + CallParticipantId callParticipantId = new CallParticipantId(device.getDemuxId(), recipient.getId()); + CallParticipant callParticipant = participants.get(callParticipantId); BroadcastVideoSink videoSink; VideoTrack videoTrack = device.getVideoTrack();