Fix ringing screen when an outgoing call is placed and the camera is off.

This commit is contained in:
Alex Hart
2026-01-09 14:17:32 -04:00
parent 0ac9f5d7c0
commit f8d17b04cb
2 changed files with 241 additions and 60 deletions

View File

@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.components.webrtc.v2
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -16,15 +17,20 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.window.core.layout.WindowSizeClass
import org.signal.core.ui.compose.AllNightPreviews
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.R
@@ -65,6 +71,21 @@ fun CallScreenJoiningOverlay(
onCallInfoClick = onCallInfoClick
)
if (!isLocalVideoEnabled) {
val isCompactWidth = !currentWindowAdaptiveInfo().windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND)
if (isCompactWidth) {
YourCameraIsOff(
spacedBy = 8.dp,
modifier = Modifier.align(Alignment.Center)
)
} else {
YourCameraIsOffLandscape(
modifier = Modifier.align(Alignment.Center)
)
}
}
val showCameraToggle = isLocalVideoEnabled && isMoreThanOneCameraAvailable
BottomControlsWithOptionalBar(
@@ -140,6 +161,51 @@ private fun WaitingToBeLetInBar(
}
}
@Composable
private fun YourCameraIsOff(
spacedBy: Dp = 0.dp,
modifier: Modifier = Modifier
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
) {
Icon(
painter = painterResource(id = R.drawable.symbol_video_slash_24),
contentDescription = null,
tint = Color.White,
modifier = Modifier.padding(bottom = spacedBy)
)
Text(
text = stringResource(id = R.string.CallScreenPreJoinOverlay__your_camera_is_off),
color = Color.White
)
}
}
@Composable
private fun YourCameraIsOffLandscape(
modifier: Modifier = Modifier
) {
Row(
horizontalArrangement = spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
) {
Icon(
painter = painterResource(id = R.drawable.symbol_video_slash_24),
contentDescription = null,
tint = Color.White
)
Text(
text = stringResource(id = R.string.CallScreenPreJoinOverlay__your_camera_is_off),
color = Color.White
)
}
}
@AllNightPreviews
@Composable
private fun CallScreenJoiningOverlayPreview() {
@@ -192,3 +258,18 @@ private fun CallScreenJoiningOverlayWaitingPreview() {
)
}
}
@AllNightPreviews
@Composable
private fun CallScreenJoiningOverlayWaitingCameraOffPreview() {
Previews.Preview {
CallScreenJoiningOverlay(
callRecipient = Recipient(systemContactName = "Test User"),
callStatus = "Waiting to be let in...",
localParticipant = CallParticipant.EMPTY,
isLocalVideoEnabled = false,
isMoreThanOneCameraAvailable = false,
isWaitingToBeLetIn = true
)
}
}

View File

@@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -35,6 +34,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
@@ -55,6 +55,7 @@ import org.thoughtcrime.securesms.events.CallParticipant
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.rememberRecipientField
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import kotlin.math.max
/**
* Pre-join call screen overlay.
@@ -84,55 +85,35 @@ fun CallScreenPreJoinOverlay(
.background(color = Color(0f, 0f, 0f, 0.4f))
.then(modifier)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
CallScreenTopAppBar(
val isCompactWidth = currentWindowAdaptiveInfo().windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT
if (!isLocalVideoEnabled) {
TopWithCenteredContentLayout(
topSlot = {
PreJoinHeader(
callRecipient = callRecipient,
callStatus = callStatus,
onNavigationClick = onNavigationClick,
onCallInfoClick = onCallInfoClick
)
},
centerSlot = {
if (isCompactWidth) {
YourCameraIsOff(spacedBy = 8.dp)
} else {
YourCameraIsOffLandscape()
}
},
modifier = Modifier
.fillMaxSize()
)
} else {
PreJoinHeader(
callRecipient = callRecipient,
callStatus = callStatus,
onNavigationClick = onNavigationClick,
onCallInfoClick = onCallInfoClick
)
AvatarImage(
recipient = callRecipient,
modifier = Modifier
.padding(top = 8.dp)
.size(96.dp)
)
Text(
text = callRecipient.getDisplayName(LocalContext.current),
style = MaterialTheme.typography.headlineMedium,
color = Color.White,
modifier = Modifier.padding(top = 16.dp)
)
if (callStatus != null) {
Text(
text = callStatus,
style = MaterialTheme.typography.bodyMedium,
color = Color.White,
modifier = Modifier.padding(top = 8.dp)
)
}
if (!isLocalVideoEnabled) {
Spacer(modifier = Modifier.weight(1f))
val isCompactWidth = currentWindowAdaptiveInfo().windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT
if (isCompactWidth) {
YourCameraIsOff(spacedBy = 8.dp)
} else {
Row(
horizontalArrangement = spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
YourCameraIsOff()
}
}
Spacer(modifier = Modifier.weight(1f))
}
}
// Bottom controls in a separate layer for proper screen-edge positioning
@@ -177,6 +158,95 @@ fun CallScreenPreJoinOverlay(
}
}
@Composable
private fun PreJoinHeader(
callRecipient: Recipient,
callStatus: String?,
onNavigationClick: () -> Unit,
onCallInfoClick: () -> Unit
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
CallScreenTopAppBar(
onNavigationClick = onNavigationClick,
onCallInfoClick = onCallInfoClick
)
AvatarImage(
recipient = callRecipient,
modifier = Modifier
.padding(top = 8.dp)
.size(96.dp)
)
Text(
text = callRecipient.getDisplayName(LocalContext.current),
style = MaterialTheme.typography.headlineMedium,
color = Color.White,
modifier = Modifier.padding(top = 16.dp)
)
if (callStatus != null) {
Text(
text = callStatus,
style = MaterialTheme.typography.bodyMedium,
color = Color.White,
modifier = Modifier.padding(top = 8.dp)
)
}
}
}
private object TopWithCenteredContentLayoutId {
const val TOP = "top"
const val CENTER = "center"
}
/**
* A layout that places content at the top and centers other content in the viewport.
* If the centered content would overlap with the top content, it is pushed down to stay below.
*/
@Composable
private fun TopWithCenteredContentLayout(
topSlot: @Composable () -> Unit,
centerSlot: @Composable () -> Unit,
modifier: Modifier = Modifier
) {
Layout(
content = {
Box(modifier = Modifier.layoutId(TopWithCenteredContentLayoutId.TOP)) { topSlot() }
Box(modifier = Modifier.layoutId(TopWithCenteredContentLayoutId.CENTER)) { centerSlot() }
},
modifier = modifier
) { measurables, constraints ->
val looseConstraints = constraints.copy(minHeight = 0, minWidth = 0)
val topPlaceable = measurables.first { it.layoutId == TopWithCenteredContentLayoutId.TOP }.measure(looseConstraints)
val centerPlaceable = measurables.first { it.layoutId == TopWithCenteredContentLayoutId.CENTER }.measure(looseConstraints)
val layoutHeight = if (constraints.hasBoundedHeight) {
constraints.maxHeight
} else {
topPlaceable.height + centerPlaceable.height
}
layout(constraints.maxWidth, layoutHeight) {
topPlaceable.placeRelative(
x = (constraints.maxWidth - topPlaceable.width) / 2,
y = 0
)
val viewportCenterY = (layoutHeight - centerPlaceable.height) / 2
val minY = topPlaceable.height
centerPlaceable.placeRelative(
x = (constraints.maxWidth - centerPlaceable.width) / 2,
y = max(viewportCenterY, minY)
)
}
}
}
@Composable
private fun CallLinkInfoCard(
modifier: Modifier = Modifier
@@ -212,21 +282,51 @@ private fun CallLinkInfoCard(
@Composable
private fun YourCameraIsOff(
spacedBy: Dp = 0.dp
spacedBy: Dp = 0.dp,
modifier: Modifier = Modifier
) {
Icon(
painter = painterResource(
id = R.drawable.symbol_video_slash_24
),
contentDescription = null,
tint = Color.White,
modifier = Modifier.padding(bottom = spacedBy)
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
) {
Icon(
painter = painterResource(
id = R.drawable.symbol_video_slash_24
),
contentDescription = null,
tint = Color.White,
modifier = Modifier.padding(bottom = spacedBy)
)
Text(
text = stringResource(id = R.string.CallScreenPreJoinOverlay__your_camera_is_off),
color = Color.White
)
Text(
text = stringResource(id = R.string.CallScreenPreJoinOverlay__your_camera_is_off),
color = Color.White
)
}
}
@Composable
private fun YourCameraIsOffLandscape(
modifier: Modifier = Modifier
) {
Row(
horizontalArrangement = spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
) {
Icon(
painter = painterResource(
id = R.drawable.symbol_video_slash_24
),
contentDescription = null,
tint = Color.White
)
Text(
text = stringResource(id = R.string.CallScreenPreJoinOverlay__your_camera_is_off),
color = Color.White
)
}
}
@Composable