mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 13:08:46 +00:00
Add CallScreenMediator pattern to facilitate moving from views to compose.
This commit is contained in:
committed by
Greyson Parrelli
parent
f80ab7402a
commit
216c29c206
@@ -15,6 +15,7 @@ import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.webrtc.v2.PendingParticipantsListener
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.service.webrtc.PendingParticipantCollection
|
||||
@@ -32,7 +33,7 @@ class PendingParticipantsView @JvmOverloads constructor(
|
||||
inflate(context, R.layout.pending_participant_view, this)
|
||||
}
|
||||
|
||||
var listener: Listener? = null
|
||||
var listener: PendingParticipantsListener? = null
|
||||
|
||||
private val avatar: AvatarImageView = findViewById(R.id.pending_participants_avatar)
|
||||
private val name: TextView = findViewById(R.id.pending_participants_name)
|
||||
@@ -81,26 +82,4 @@ class PendingParticipantsView @JvmOverloads constructor(
|
||||
|
||||
visible = true
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
/**
|
||||
* Display the sheet containing the request for the top level participant
|
||||
*/
|
||||
fun onLaunchRecipientSheet(pendingRecipient: Recipient)
|
||||
|
||||
/**
|
||||
* Given recipient should be admitted to the call
|
||||
*/
|
||||
fun onAllowPendingRecipient(pendingRecipient: Recipient)
|
||||
|
||||
/**
|
||||
* Given recipient should be rejected from the call
|
||||
*/
|
||||
fun onRejectPendingRecipient(pendingRecipient: Recipient)
|
||||
|
||||
/**
|
||||
* Display the sheet containing all of the requests for the given call
|
||||
*/
|
||||
fun onLaunchPendingRequestsSheet()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
@@ -51,6 +50,8 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AccessibleToggleButton;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.InsetAwareConstraintLayout;
|
||||
import org.thoughtcrime.securesms.components.webrtc.v2.CallScreenControlsListener;
|
||||
import org.thoughtcrime.securesms.components.webrtc.v2.PendingParticipantsListener;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
@@ -60,7 +61,7 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.PendingParticipantsState;
|
||||
import org.thoughtcrime.securesms.components.webrtc.v2.PendingParticipantsState;
|
||||
import org.thoughtcrime.securesms.stories.viewer.reply.reaction.MultiReactionBurstLayout;
|
||||
import org.thoughtcrime.securesms.util.BlurTransformation;
|
||||
import org.thoughtcrime.securesms.util.ThrottledDebouncer;
|
||||
@@ -94,7 +95,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
|
||||
private TextView recipientName;
|
||||
private TextView status;
|
||||
private TextView incomingRingStatus;
|
||||
private ControlsListener controlsListener;
|
||||
private CallScreenControlsListener controlsListener;
|
||||
private RecipientId recipientId;
|
||||
private ImageView answer;
|
||||
private TextView answerWithoutVideoLabel;
|
||||
@@ -137,7 +138,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
|
||||
private WebRtcCallParticipantsRecyclerAdapter recyclerAdapter;
|
||||
private WebRtcReactionsRecyclerAdapter reactionsAdapter;
|
||||
private PictureInPictureExpansionHelper pictureInPictureExpansionHelper;
|
||||
private PendingParticipantsView.Listener pendingParticipantsViewListener;
|
||||
private PendingParticipantsListener pendingParticipantsViewListener;
|
||||
|
||||
private final Set<View> incomingCallViews = new HashSet<>();
|
||||
private final Set<View> topViews = new HashSet<>();
|
||||
@@ -284,18 +285,18 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
|
||||
runIfNonNull(controlsListener, listener -> listener.onRingGroupChanged(isOn, ringToggle.isActivated()));
|
||||
});
|
||||
|
||||
cameraDirectionToggle.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onCameraDirectionChanged));
|
||||
smallLocalRender.findViewById(R.id.call_participant_switch_camera).setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onCameraDirectionChanged));
|
||||
cameraDirectionToggle.setOnClickListener(v -> runIfNonNull(controlsListener, CallScreenControlsListener::onCameraDirectionChanged));
|
||||
smallLocalRender.findViewById(R.id.call_participant_switch_camera).setOnClickListener(v -> runIfNonNull(controlsListener, CallScreenControlsListener::onCameraDirectionChanged));
|
||||
|
||||
overflow.setOnClickListener(v -> {
|
||||
runIfNonNull(controlsListener, ControlsListener::onOverflowClicked);
|
||||
runIfNonNull(controlsListener, CallScreenControlsListener::onOverflowClicked);
|
||||
});
|
||||
|
||||
hangup.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onEndCallPressed));
|
||||
decline.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onDenyCallPressed));
|
||||
hangup.setOnClickListener(v -> runIfNonNull(controlsListener, CallScreenControlsListener::onEndCallPressed));
|
||||
decline.setOnClickListener(v -> runIfNonNull(controlsListener, CallScreenControlsListener::onDenyCallPressed));
|
||||
|
||||
answer.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallPressed));
|
||||
answerWithoutVideo.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallWithVoiceOnlyPressed));
|
||||
answer.setOnClickListener(v -> runIfNonNull(controlsListener, CallScreenControlsListener::onAcceptCallPressed));
|
||||
answerWithoutVideo.setOnClickListener(v -> runIfNonNull(controlsListener, CallScreenControlsListener::onAcceptCallWithVoiceOnlyPressed));
|
||||
|
||||
pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(smallLocalRenderFrame);
|
||||
pictureInPictureExpansionHelper = new PictureInPictureExpansionHelper(smallLocalRenderFrame, state -> {
|
||||
@@ -430,7 +431,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
public void setControlsListener(@Nullable ControlsListener controlsListener) {
|
||||
public void setControlsListener(@Nullable CallScreenControlsListener controlsListener) {
|
||||
this.controlsListener = controlsListener;
|
||||
}
|
||||
|
||||
@@ -442,7 +443,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
|
||||
micToggle.setChecked(hasAudioPermission() && isMicEnabled, false);
|
||||
}
|
||||
|
||||
public void setPendingParticipantsViewListener(@Nullable PendingParticipantsView.Listener listener) {
|
||||
public void setPendingParticipantsViewListener(@Nullable PendingParticipantsListener listener) {
|
||||
pendingParticipantsViewListener = listener;
|
||||
}
|
||||
|
||||
@@ -647,52 +648,6 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private void setStatus(@StringRes int statusRes) {
|
||||
setStatus(getContext().getString(statusRes));
|
||||
}
|
||||
|
||||
public void setStatusFromHangupType(@NonNull HangupMessage.Type hangupType) {
|
||||
switch (hangupType) {
|
||||
case NORMAL:
|
||||
case NEED_PERMISSION:
|
||||
setStatus(R.string.RedPhone_ending_call);
|
||||
break;
|
||||
case ACCEPTED:
|
||||
setStatus(R.string.WebRtcCallActivity__answered_on_a_linked_device);
|
||||
break;
|
||||
case DECLINED:
|
||||
setStatus(R.string.WebRtcCallActivity__declined_on_a_linked_device);
|
||||
break;
|
||||
case BUSY:
|
||||
setStatus(R.string.WebRtcCallActivity__busy_on_a_linked_device);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unknown hangup type: " + hangupType);
|
||||
}
|
||||
}
|
||||
|
||||
public void setStatusFromGroupCallState(@NonNull WebRtcViewModel.GroupCallState groupCallState) {
|
||||
switch (groupCallState) {
|
||||
case DISCONNECTED:
|
||||
setStatus(R.string.WebRtcCallView__disconnected);
|
||||
break;
|
||||
case RECONNECTING:
|
||||
setStatus(R.string.WebRtcCallView__reconnecting);
|
||||
break;
|
||||
case CONNECTED_AND_JOINING:
|
||||
setStatus(R.string.WebRtcCallView__joining);
|
||||
break;
|
||||
case CONNECTED_AND_PENDING:
|
||||
setStatus(R.string.WebRtcCallView__waiting_to_be_let_in);
|
||||
break;
|
||||
case CONNECTING:
|
||||
case CONNECTED_AND_JOINED:
|
||||
case CONNECTED:
|
||||
setStatus("");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void setWebRtcControls(@NonNull WebRtcControls webRtcControls) {
|
||||
Set<View> lastVisibleSet = new HashSet<>(visibleViewSet);
|
||||
|
||||
@@ -978,27 +933,4 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
|
||||
|
||||
public void onControlTopChanged() {
|
||||
}
|
||||
|
||||
public interface ControlsListener {
|
||||
void onStartCall(boolean isVideoCall);
|
||||
void onCancelStartCall();
|
||||
void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput);
|
||||
@RequiresApi(31)
|
||||
void onAudioOutputChanged31(@NonNull WebRtcAudioDevice audioOutput);
|
||||
void onVideoChanged(boolean isVideoEnabled);
|
||||
void onMicChanged(boolean isMicEnabled);
|
||||
void onOverflowClicked();
|
||||
void onCameraDirectionChanged();
|
||||
void onEndCallPressed();
|
||||
void onDenyCallPressed();
|
||||
void onAcceptCallWithVoiceOnlyPressed();
|
||||
void onAcceptCallPressed();
|
||||
void onPageChanged(@NonNull CallParticipantsState.SelectedPage page);
|
||||
void onLocalPictureInPictureClicked();
|
||||
void onRingGroupChanged(boolean ringGroup, boolean ringingAllowed);
|
||||
void onCallInfoClicked();
|
||||
void onNavigateUpClicked();
|
||||
void toggleControls();
|
||||
void onAudioPermissionsRequested(Runnable onGranted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ public final class WebRtcControls {
|
||||
public @Px int getFold() {
|
||||
return foldableState.getFoldPoint();
|
||||
}
|
||||
|
||||
|
||||
public @StringRes int getStartCallButtonText() {
|
||||
if (isGroupCall()) {
|
||||
if (groupCallState == GroupCallState.FULL) {
|
||||
|
||||
@@ -53,6 +53,7 @@ import org.thoughtcrime.securesms.components.InsetAwareConstraintLayout
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallOverflowPopupWindow
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls
|
||||
import org.thoughtcrime.securesms.components.webrtc.v2.CallControlsVisibilityListener
|
||||
import org.thoughtcrime.securesms.components.webrtc.v2.CallInfoCallbacks
|
||||
import org.thoughtcrime.securesms.components.webrtc.v2.WebRtcCallViewModel
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
|
||||
@@ -125,7 +126,7 @@ class ControlsAndInfoController private constructor(
|
||||
}
|
||||
|
||||
private val scheduleHideControlsRunnable: Runnable = Runnable { onScheduledHide() }
|
||||
private val bottomSheetVisibilityListeners = mutableSetOf<BottomSheetVisibilityListener>()
|
||||
private val callControlsVisibilityListeners = mutableSetOf<CallControlsVisibilityListener>()
|
||||
|
||||
private val handler: Handler?
|
||||
get() = webRtcCallView.handler
|
||||
@@ -246,8 +247,8 @@ class ControlsAndInfoController private constructor(
|
||||
callInfoComposeView.translationY = INFO_TRANSLATION_DISTANCE
|
||||
}
|
||||
|
||||
fun addVisibilityListener(listener: BottomSheetVisibilityListener): Boolean {
|
||||
return bottomSheetVisibilityListeners.add(listener)
|
||||
fun addVisibilityListener(listener: CallControlsVisibilityListener): Boolean {
|
||||
return callControlsVisibilityListeners.add(listener)
|
||||
}
|
||||
|
||||
fun onStateRestored() {
|
||||
@@ -279,7 +280,7 @@ class ControlsAndInfoController private constructor(
|
||||
behavior.isHideable = false
|
||||
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
|
||||
bottomSheetVisibilityListeners.forEach { it.onShown() }
|
||||
callControlsVisibilityListeners.forEach { it.onShown() }
|
||||
}
|
||||
|
||||
private fun hide(delay: Long = 0L) {
|
||||
@@ -288,7 +289,7 @@ class ControlsAndInfoController private constructor(
|
||||
behavior.isHideable = true
|
||||
behavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
|
||||
bottomSheetVisibilityListeners.forEach { it.onHidden() }
|
||||
callControlsVisibilityListeners.forEach { it.onHidden() }
|
||||
}
|
||||
} else {
|
||||
cancelScheduledHide()
|
||||
@@ -490,9 +491,4 @@ class ControlsAndInfoController private constructor(
|
||||
return controlHeight != this.controlHeight || coordinatorHeight != this.coordinatorHeight
|
||||
}
|
||||
}
|
||||
|
||||
interface BottomSheetVisibilityListener {
|
||||
fun onShown()
|
||||
fun onHidden()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -47,7 +48,8 @@ import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
fun CallControls(
|
||||
displayVideoTooltip: Boolean,
|
||||
callControlsState: CallControlsState,
|
||||
callControlsCallback: CallControlsCallback,
|
||||
callScreenControlsListener: CallScreenControlsListener,
|
||||
callScreenSheetDisplayListener: CallScreenSheetDisplayListener,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
@@ -87,11 +89,21 @@ fun CallControls(
|
||||
}
|
||||
}
|
||||
|
||||
val onSelectedAudioDeviceChanged: (WebRtcAudioDevice) -> Unit = remember {
|
||||
{
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
callScreenControlsListener.onAudioOutputChanged31(it)
|
||||
} else {
|
||||
callScreenControlsListener.onAudioOutputChanged(it.webRtcAudioOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CallAudioToggleButton(
|
||||
outputState = outputState,
|
||||
contentDescription = stringResource(id = R.string.WebRtcAudioOutputToggle__audio_output),
|
||||
onSelectedDeviceChanged = callControlsCallback::onSelectedAudioDeviceChanged,
|
||||
onSheetDisplayChanged = callControlsCallback::onAudioDeviceSheetDisplayChanged
|
||||
onSelectedDeviceChanged = onSelectedAudioDeviceChanged,
|
||||
onSheetDisplayChanged = callScreenSheetDisplayListener::onAudioDeviceSheetDisplayChanged
|
||||
)
|
||||
}
|
||||
|
||||
@@ -100,11 +112,11 @@ fun CallControls(
|
||||
CallScreenTooltipBox(
|
||||
text = stringResource(R.string.WebRtcCallActivity__tap_here_to_turn_on_your_video),
|
||||
displayTooltip = displayVideoTooltip,
|
||||
onTooltipDismissed = callControlsCallback::onVideoTooltipDismissed
|
||||
onTooltipDismissed = callScreenSheetDisplayListener::onVideoTooltipDismissed
|
||||
) {
|
||||
ToggleVideoButton(
|
||||
isVideoEnabled = callControlsState.isVideoEnabled && hasCameraPermission,
|
||||
onChange = callControlsCallback::onVideoToggleClick
|
||||
onChange = callScreenControlsListener::onVideoChanged
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -113,7 +125,7 @@ fun CallControls(
|
||||
if (callControlsState.displayMicToggle) {
|
||||
ToggleMicButton(
|
||||
isMicEnabled = callControlsState.isMicEnabled && hasRecordAudioPermission,
|
||||
onChange = callControlsCallback::onMicToggleClick
|
||||
onChange = callScreenControlsListener::onMicChanged
|
||||
)
|
||||
}
|
||||
|
||||
@@ -121,22 +133,24 @@ fun CallControls(
|
||||
ToggleRingButton(
|
||||
isRingEnabled = callControlsState.isGroupRingingEnabled,
|
||||
isRingAllowed = callControlsState.isGroupRingingAllowed,
|
||||
onChange = callControlsCallback::onGroupRingingToggleClick
|
||||
onChange = callScreenControlsListener::onRingGroupChanged
|
||||
)
|
||||
}
|
||||
|
||||
if (callControlsState.displayAdditionalActions) {
|
||||
AdditionalActionsButton(onClick = callControlsCallback::onAdditionalActionsClick)
|
||||
AdditionalActionsButton(onClick = callScreenControlsListener::onOverflowClicked)
|
||||
}
|
||||
|
||||
if (callControlsState.displayEndCallButton) {
|
||||
HangupButton(onClick = callControlsCallback::onEndCallClick)
|
||||
HangupButton(onClick = callScreenControlsListener::onEndCallPressed)
|
||||
}
|
||||
|
||||
if (callControlsState.displayStartCallButton && !isPortrait) {
|
||||
StartCallButton(
|
||||
text = stringResource(callControlsState.startCallButtonText),
|
||||
onClick = { callControlsCallback.onStartCallClick(callControlsState.isVideoEnabled) }
|
||||
onClick = {
|
||||
callScreenControlsListener.onStartCall(callControlsState.isVideoEnabled)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -144,7 +158,9 @@ fun CallControls(
|
||||
if (callControlsState.displayStartCallButton && isPortrait) {
|
||||
StartCallButton(
|
||||
text = stringResource(callControlsState.startCallButtonText),
|
||||
onClick = { callControlsCallback.onStartCallClick(callControlsState.isVideoEnabled) }
|
||||
onClick = {
|
||||
callScreenControlsListener.onStartCall(callControlsState.isVideoEnabled)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -170,7 +186,8 @@ fun CallControlsPreview() {
|
||||
displayEndCallButton = true
|
||||
),
|
||||
displayVideoTooltip = false,
|
||||
callControlsCallback = CallControlsCallback.Empty
|
||||
callScreenControlsListener = CallScreenControlsListener.Empty,
|
||||
callScreenSheetDisplayListener = CallScreenSheetDisplayListener.Empty
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -178,26 +195,12 @@ fun CallControlsPreview() {
|
||||
/**
|
||||
* Callbacks for call controls actions.
|
||||
*/
|
||||
interface CallControlsCallback {
|
||||
interface CallScreenSheetDisplayListener {
|
||||
fun onAudioDeviceSheetDisplayChanged(displayed: Boolean)
|
||||
fun onSelectedAudioDeviceChanged(audioDevice: WebRtcAudioDevice)
|
||||
fun onVideoToggleClick(enabled: Boolean)
|
||||
fun onMicToggleClick(enabled: Boolean)
|
||||
fun onGroupRingingToggleClick(enabled: Boolean, allowed: Boolean)
|
||||
fun onAdditionalActionsClick()
|
||||
fun onStartCallClick(isVideoCall: Boolean)
|
||||
fun onEndCallClick()
|
||||
fun onVideoTooltipDismissed()
|
||||
|
||||
object Empty : CallControlsCallback {
|
||||
object Empty : CallScreenSheetDisplayListener {
|
||||
override fun onAudioDeviceSheetDisplayChanged(displayed: Boolean) = Unit
|
||||
override fun onSelectedAudioDeviceChanged(audioDevice: WebRtcAudioDevice) = Unit
|
||||
override fun onVideoToggleClick(enabled: Boolean) = Unit
|
||||
override fun onMicToggleClick(enabled: Boolean) = Unit
|
||||
override fun onGroupRingingToggleClick(enabled: Boolean, allowed: Boolean) = Unit
|
||||
override fun onAdditionalActionsClick() = Unit
|
||||
override fun onStartCallClick(isVideoCall: Boolean) = Unit
|
||||
override fun onEndCallClick() = Unit
|
||||
override fun onVideoTooltipDismissed() = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
interface CallControlsVisibilityListener {
|
||||
fun onShown()
|
||||
fun onHidden()
|
||||
|
||||
companion object Empty : CallControlsVisibilityListener {
|
||||
override fun onShown() = Unit
|
||||
override fun onHidden() = Unit
|
||||
}
|
||||
}
|
||||
@@ -34,8 +34,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SheetValue
|
||||
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
|
||||
@@ -81,8 +79,14 @@ fun CallScreen(
|
||||
webRtcCallState: WebRtcViewModel.State,
|
||||
callScreenState: CallScreenState,
|
||||
callControlsState: CallControlsState,
|
||||
callControlsCallback: CallControlsCallback = CallControlsCallback.Empty,
|
||||
callScreenController: CallScreenController = CallScreenController.rememberCallScreenController(
|
||||
skipHiddenState = callControlsState.skipHiddenState,
|
||||
onControlsToggled = {}
|
||||
),
|
||||
callScreenControlsListener: CallScreenControlsListener = CallScreenControlsListener.Empty,
|
||||
callScreenSheetDisplayListener: CallScreenSheetDisplayListener = CallScreenSheetDisplayListener.Empty,
|
||||
callParticipantsPagerState: CallParticipantsPagerState,
|
||||
pendingParticipantsListener: PendingParticipantsListener = PendingParticipantsListener.Empty,
|
||||
overflowParticipants: List<CallParticipant>,
|
||||
localParticipant: CallParticipant,
|
||||
localRenderState: WebRtcLocalRenderState,
|
||||
@@ -98,20 +102,8 @@ fun CallScreen(
|
||||
mutableFloatStateOf(0f)
|
||||
}
|
||||
|
||||
val skipHiddenState by rememberUpdatedState(newValue = callControlsState.skipHiddenState)
|
||||
val valueChangeOperation: (SheetValue) -> Boolean = remember {
|
||||
{
|
||||
!(it == SheetValue.Hidden && skipHiddenState)
|
||||
}
|
||||
}
|
||||
|
||||
val scaffoldState = remember(callScreenController) { callScreenController.scaffoldState }
|
||||
val scope = rememberCoroutineScope()
|
||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = rememberStandardBottomSheetState(
|
||||
confirmValueChange = valueChangeOperation,
|
||||
skipHiddenState = false
|
||||
)
|
||||
)
|
||||
val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
|
||||
BoxWithConstraints {
|
||||
@@ -123,7 +115,7 @@ fun CallScreen(
|
||||
var peekHeight by remember { mutableFloatStateOf(88f) }
|
||||
|
||||
BottomSheetScaffold(
|
||||
scaffoldState = scaffoldState,
|
||||
scaffoldState = callScreenController.scaffoldState,
|
||||
sheetDragHandle = null,
|
||||
sheetPeekHeight = peekHeight.dp,
|
||||
sheetMaxWidth = 540.dp,
|
||||
@@ -153,7 +145,8 @@ fun CallScreen(
|
||||
if (callControlsAlpha > 0f) {
|
||||
CallControls(
|
||||
callControlsState = callControlsState,
|
||||
callControlsCallback = callControlsCallback,
|
||||
callScreenControlsListener = callScreenControlsListener,
|
||||
callScreenSheetDisplayListener = callScreenSheetDisplayListener,
|
||||
displayVideoTooltip = callScreenState.displayVideoTooltip,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -182,7 +175,8 @@ fun CallScreen(
|
||||
callControlsState = callControlsState,
|
||||
callScreenState = callScreenState,
|
||||
onPipClick = onLocalPictureInPictureClicked,
|
||||
onControlsToggled = onControlsToggled
|
||||
onControlsToggled = onControlsToggled,
|
||||
callScreenController = callScreenController
|
||||
)
|
||||
}
|
||||
|
||||
@@ -202,7 +196,8 @@ fun CallScreen(
|
||||
callControlsState = callControlsState,
|
||||
callScreenState = callScreenState,
|
||||
onPipClick = onLocalPictureInPictureClicked,
|
||||
onControlsToggled = onControlsToggled
|
||||
onControlsToggled = onControlsToggled,
|
||||
callScreenController = callScreenController
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -251,6 +246,17 @@ fun CallScreen(
|
||||
.padding(bottom = padding)
|
||||
.padding(bottom = 20.dp)
|
||||
)
|
||||
|
||||
val state = remember(callScreenState.pendingParticipantsState) {
|
||||
callScreenState.pendingParticipantsState
|
||||
}
|
||||
|
||||
if (state != null) {
|
||||
PendingParticipants(
|
||||
pendingParticipantsState = state,
|
||||
pendingParticipantsListener = pendingParticipantsListener
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,6 +278,7 @@ private fun BoxScope.Viewport(
|
||||
scaffoldState: BottomSheetScaffoldState,
|
||||
callControlsState: CallControlsState,
|
||||
callScreenState: CallScreenState,
|
||||
callScreenController: CallScreenController,
|
||||
onPipClick: () -> Unit,
|
||||
onControlsToggled: (Boolean) -> Unit
|
||||
) {
|
||||
@@ -288,7 +295,7 @@ private fun BoxScope.Viewport(
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val hideSheet by rememberUpdatedState(newValue = scaffoldState.bottomSheetState.currentValue == SheetValue.PartiallyExpanded && !callControlsState.skipHiddenState && !callScreenState.isDisplayingAudioToggleSheet)
|
||||
LaunchedEffect(hideSheet) {
|
||||
LaunchedEffect(callScreenController.restartTimerRequests, hideSheet) {
|
||||
if (hideSheet) {
|
||||
delay(5.seconds)
|
||||
scaffoldState.bottomSheetState.hide()
|
||||
@@ -309,13 +316,7 @@ private fun BoxScope.Viewport(
|
||||
.clickable(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
if (scaffoldState.bottomSheetState.isVisible) {
|
||||
scaffoldState.bottomSheetState.hide()
|
||||
onControlsToggled(false)
|
||||
} else {
|
||||
onControlsToggled(true)
|
||||
scaffoldState.bottomSheetState.show()
|
||||
}
|
||||
callScreenController.handleEvent(CallScreenController.Event.TOGGLE_CONTROLS)
|
||||
}
|
||||
},
|
||||
enabled = !callControlsState.skipHiddenState
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
import androidx.compose.material3.BottomSheetScaffoldState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||
import androidx.compose.material3.rememberStandardBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
|
||||
/**
|
||||
* Collects and manages state objects for manipulating the call screen UI programatically.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
class CallScreenController private constructor(
|
||||
val scaffoldState: BottomSheetScaffoldState,
|
||||
val onControlsToggled: (Boolean) -> Unit
|
||||
) {
|
||||
|
||||
var restartTimerRequests by mutableLongStateOf(0L)
|
||||
|
||||
suspend fun handleEvent(event: Event) {
|
||||
when (event) {
|
||||
Event.SWITCH_TO_SPEAKER_VIEW -> {} // TODO [calling-v2]
|
||||
Event.DISMISS_AUDIO_PICKER -> {} // TODO [calling-v2]
|
||||
Event.TOGGLE_CONTROLS -> {
|
||||
if (scaffoldState.bottomSheetState.isVisible) {
|
||||
scaffoldState.bottomSheetState.hide()
|
||||
onControlsToggled(false)
|
||||
} else {
|
||||
onControlsToggled(true)
|
||||
scaffoldState.bottomSheetState.show()
|
||||
}
|
||||
}
|
||||
Event.SHOW_CALL_INFO -> {
|
||||
scaffoldState.bottomSheetState.expand()
|
||||
}
|
||||
Event.RESTART_HIDE_CONTROLS_TIMER -> {
|
||||
restartTimerRequests += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Composable
|
||||
fun rememberCallScreenController(skipHiddenState: Boolean, onControlsToggled: (Boolean) -> Unit): CallScreenController {
|
||||
val skip by rememberUpdatedState(skipHiddenState)
|
||||
val valueChangeOperation: (SheetValue) -> Boolean = remember {
|
||||
{
|
||||
!(it == SheetValue.Hidden && skip)
|
||||
}
|
||||
}
|
||||
|
||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = rememberStandardBottomSheetState(
|
||||
confirmValueChange = valueChangeOperation,
|
||||
skipHiddenState = skip
|
||||
)
|
||||
)
|
||||
|
||||
return remember(scaffoldState) {
|
||||
CallScreenController(
|
||||
scaffoldState = scaffoldState,
|
||||
onControlsToggled = onControlsToggled
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class Event {
|
||||
SWITCH_TO_SPEAKER_VIEW,
|
||||
DISMISS_AUDIO_PICKER,
|
||||
TOGGLE_CONTROLS,
|
||||
SHOW_CALL_INFO,
|
||||
RESTART_HIDE_CONTROLS_TIMER
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioDevice
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput
|
||||
|
||||
/**
|
||||
* Mediator callbacks for call screen signals.
|
||||
*/
|
||||
interface CallScreenControlsListener {
|
||||
fun onStartCall(isVideoCall: Boolean)
|
||||
fun onCancelStartCall()
|
||||
fun onAudioOutputChanged(audioOutput: WebRtcAudioOutput)
|
||||
|
||||
@RequiresApi(31)
|
||||
fun onAudioOutputChanged31(audioOutput: WebRtcAudioDevice)
|
||||
fun onVideoChanged(isVideoEnabled: Boolean)
|
||||
fun onMicChanged(isMicEnabled: Boolean)
|
||||
fun onOverflowClicked()
|
||||
fun onCameraDirectionChanged()
|
||||
fun onEndCallPressed()
|
||||
fun onDenyCallPressed()
|
||||
fun onAcceptCallWithVoiceOnlyPressed()
|
||||
fun onAcceptCallPressed()
|
||||
fun onPageChanged(page: CallParticipantsState.SelectedPage)
|
||||
fun onLocalPictureInPictureClicked()
|
||||
fun onRingGroupChanged(ringGroup: Boolean, ringingAllowed: Boolean)
|
||||
fun onCallInfoClicked()
|
||||
fun onNavigateUpClicked()
|
||||
fun toggleControls()
|
||||
fun onAudioPermissionsRequested(onGranted: Runnable?)
|
||||
|
||||
object Empty : CallScreenControlsListener {
|
||||
override fun onStartCall(isVideoCall: Boolean) = Unit
|
||||
override fun onCancelStartCall() = Unit
|
||||
override fun onAudioOutputChanged(audioOutput: WebRtcAudioOutput) = Unit
|
||||
override fun onAudioOutputChanged31(audioOutput: WebRtcAudioDevice) = Unit
|
||||
override fun onVideoChanged(isVideoEnabled: Boolean) = Unit
|
||||
override fun onMicChanged(isMicEnabled: Boolean) = Unit
|
||||
override fun onOverflowClicked() = Unit
|
||||
override fun onCameraDirectionChanged() = Unit
|
||||
override fun onEndCallPressed() = Unit
|
||||
override fun onDenyCallPressed() = Unit
|
||||
override fun onAcceptCallWithVoiceOnlyPressed() = Unit
|
||||
override fun onAcceptCallPressed() = Unit
|
||||
override fun onPageChanged(page: CallParticipantsState.SelectedPage) = Unit
|
||||
override fun onLocalPictureInPictureClicked() = Unit
|
||||
override fun onRingGroupChanged(ringGroup: Boolean, ringingAllowed: Boolean) = Unit
|
||||
override fun onCallInfoClicked() = Unit
|
||||
override fun onNavigateUpClicked() = Unit
|
||||
override fun toggleControls() = Unit
|
||||
override fun onAudioPermissionsRequested(onGranted: Runnable?) = Unit
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantListUpdate
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage
|
||||
|
||||
/**
|
||||
* Mediates between the activity and the call screen to allow for a consistent API
|
||||
* regardless of View or Compose implementation.
|
||||
*/
|
||||
interface CallScreenMediator {
|
||||
|
||||
fun setWebRtcCallState(callState: WebRtcViewModel.State)
|
||||
|
||||
fun setControlsAndInfoVisibilityListener(listener: CallControlsVisibilityListener)
|
||||
fun onStateRestored()
|
||||
fun toggleOverflowPopup()
|
||||
fun restartHideControlsTimer()
|
||||
fun showCallInfo()
|
||||
fun toggleControls()
|
||||
|
||||
fun setControlsListener(controlsListener: CallScreenControlsListener)
|
||||
fun setMicEnabled(enabled: Boolean)
|
||||
fun setStatus(status: String)
|
||||
fun setRecipient(recipient: Recipient)
|
||||
fun setWebRtcControls(webRtcControls: WebRtcControls)
|
||||
fun updateCallParticipants(callParticipantsViewState: CallParticipantsViewState)
|
||||
fun maybeDismissAudioPicker()
|
||||
fun setPendingParticipantsViewListener(pendingParticipantsViewListener: PendingParticipantsListener)
|
||||
fun updatePendingParticipantsList(pendingParticipantsList: PendingParticipantsState)
|
||||
fun setRingGroup(ringGroup: Boolean)
|
||||
fun switchToSpeakerView()
|
||||
fun enableRingGroup(canRing: Boolean)
|
||||
fun showSpeakerViewHint()
|
||||
fun hideSpeakerViewHint()
|
||||
fun showVideoTooltip(): Dismissible
|
||||
fun showCameraTooltip(): Dismissible
|
||||
fun onCallStateUpdate(callControlsChange: CallControlsChange)
|
||||
fun dismissCallOverflowPopup()
|
||||
fun onParticipantListUpdate(callParticipantListUpdate: CallParticipantListUpdate)
|
||||
fun enableParticipantUpdatePopup(enabled: Boolean)
|
||||
fun enableCallStateUpdatePopup(enabled: Boolean)
|
||||
fun showWifiToCellularPopupWindow()
|
||||
|
||||
fun setStatusFromGroupCallState(context: Context, groupCallState: WebRtcViewModel.GroupCallState) {
|
||||
when (groupCallState) {
|
||||
WebRtcViewModel.GroupCallState.DISCONNECTED -> setStatus(context.getString(R.string.WebRtcCallView__disconnected))
|
||||
WebRtcViewModel.GroupCallState.CONNECTING, WebRtcViewModel.GroupCallState.CONNECTED, WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED -> {
|
||||
setStatus("")
|
||||
}
|
||||
WebRtcViewModel.GroupCallState.RECONNECTING -> setStatus(context.getString(R.string.WebRtcCallView__reconnecting))
|
||||
WebRtcViewModel.GroupCallState.CONNECTED_AND_PENDING -> setStatus(context.getString(R.string.WebRtcCallView__waiting_to_be_let_in))
|
||||
WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINING -> setStatus(context.getString(R.string.WebRtcCallView__joining))
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
fun setStatusFromHangupType(context: Context, hangupType: HangupMessage.Type) {
|
||||
when (hangupType) {
|
||||
HangupMessage.Type.NORMAL, HangupMessage.Type.NEED_PERMISSION -> setStatus(context.getString(R.string.RedPhone_ending_call))
|
||||
HangupMessage.Type.ACCEPTED -> setStatus(context.getString(R.string.WebRtcCallActivity__answered_on_a_linked_device))
|
||||
HangupMessage.Type.DECLINED -> setStatus(context.getString(R.string.WebRtcCallActivity__declined_on_a_linked_device))
|
||||
HangupMessage.Type.BUSY -> setStatus(context.getString(R.string.WebRtcCallActivity__busy_on_a_linked_device))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(activity: WebRtcCallActivity, viewModel: WebRtcCallViewModel): CallScreenMediator {
|
||||
return if (RemoteConfig.newCallUi || (RemoteConfig.internalUser && SignalStore.internal.newCallingUi)) {
|
||||
ComposeCallScreenMediator(activity, viewModel)
|
||||
} else {
|
||||
ViewCallScreenMediator(activity, viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun interface Dismissible {
|
||||
fun dismiss()
|
||||
}
|
||||
@@ -6,33 +6,27 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* This contains higher level information that would have traditionally been directly
|
||||
* set on views. (Statuses, popups, etc.), allowing us to manage this from CallViewModel
|
||||
*
|
||||
* @param callRecipientId The recipient ID of the call target (1:1 recipient, call link, or group)
|
||||
* @param hangup Set on call termination.
|
||||
* @param callControlsChange Update to display in a CallStateUpdate component.
|
||||
* @param callStatus Status text resource to display as call status.
|
||||
* @param isDisplayingAudioToggleSheet Whether the audio toggle sheet is currently displayed. Displaying this sheet should suppress hiding the controls.
|
||||
*/
|
||||
data class CallScreenState(
|
||||
val callRecipientId: RecipientId = RecipientId.UNKNOWN,
|
||||
val hangup: Hangup? = null,
|
||||
val callControlsChange: CallControlsChange? = null,
|
||||
val callStatus: CallString? = null,
|
||||
val callStatus: String? = null,
|
||||
val isDisplayingAudioToggleSheet: Boolean = false,
|
||||
val displaySwitchCameraTooltip: Boolean = false,
|
||||
val displayVideoTooltip: Boolean = false,
|
||||
val displaySwipeToSpeakerHint: Boolean = false,
|
||||
val displayWifiToCellularPopup: Boolean = false
|
||||
) {
|
||||
data class Hangup(
|
||||
val hangupMessageType: HangupMessage.Type,
|
||||
val delay: Duration = 1.seconds
|
||||
)
|
||||
}
|
||||
val displayWifiToCellularPopup: Boolean = false,
|
||||
val displayAdditionalActionsPopup: Boolean = false,
|
||||
val pendingParticipantsState: PendingParticipantsState? = null,
|
||||
val isParticipantUpdatePopupEnabled: Boolean = false,
|
||||
val isCallStateUpdatePopupEnabled: Boolean = false
|
||||
)
|
||||
|
||||
@@ -43,7 +43,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
@Composable
|
||||
fun CallScreenTopBar(
|
||||
callRecipient: Recipient,
|
||||
callStatus: CallString?,
|
||||
callStatus: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
onNavigationClick: () -> Unit = {},
|
||||
onCallInfoClick: () -> Unit = {}
|
||||
@@ -70,7 +70,7 @@ fun CallScreenTopBar(
|
||||
@Composable
|
||||
fun CallScreenPreJoinOverlay(
|
||||
callRecipient: Recipient,
|
||||
callStatus: CallString?,
|
||||
callStatus: String?,
|
||||
isLocalVideoEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onNavigationClick: () -> Unit = {},
|
||||
@@ -103,7 +103,7 @@ fun CallScreenPreJoinOverlay(
|
||||
|
||||
if (callStatus != null) {
|
||||
Text(
|
||||
text = callStatus.renderToString(),
|
||||
text = callStatus,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.White,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
@@ -136,7 +136,7 @@ fun CallScreenPreJoinOverlay(
|
||||
@Composable
|
||||
private fun CallScreenTopAppBar(
|
||||
callRecipient: Recipient? = null,
|
||||
callStatus: CallString? = null,
|
||||
callStatus: String? = null,
|
||||
onNavigationClick: () -> Unit = {},
|
||||
onCallInfoClick: () -> Unit = {}
|
||||
) {
|
||||
@@ -162,7 +162,7 @@ private fun CallScreenTopAppBar(
|
||||
|
||||
if (callStatus != null) {
|
||||
Text(
|
||||
text = callStatus.renderToString(),
|
||||
text = callStatus,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(shadow = textShadow),
|
||||
modifier = Modifier.padding(top = 2.dp)
|
||||
)
|
||||
@@ -212,7 +212,7 @@ fun CallScreenPreJoinOverlayPreview() {
|
||||
Previews.Preview {
|
||||
CallScreenPreJoinOverlay(
|
||||
callRecipient = Recipient(systemContactName = "Test User"),
|
||||
callStatus = CallString.ResourceString(R.string.Recipient_unknown),
|
||||
callStatus = stringResource(R.string.Recipient_unknown),
|
||||
isLocalVideoEnabled = false
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
/**
|
||||
* Objects that can be rendered into a string, including Recipient display names
|
||||
* and group info. This allows us to pass these objects through the view model without
|
||||
* having to pass around context.
|
||||
*/
|
||||
sealed interface CallString {
|
||||
@Composable
|
||||
fun renderToString(): String
|
||||
|
||||
data class RecipientDisplayName(
|
||||
val recipient: Recipient
|
||||
) : CallString {
|
||||
@Composable
|
||||
override fun renderToString(): String {
|
||||
return recipient.getDisplayName(LocalContext.current)
|
||||
}
|
||||
}
|
||||
|
||||
data class ResourceString(
|
||||
@StringRes val resource: Int
|
||||
) : CallString {
|
||||
@Composable
|
||||
override fun renderToString(): String {
|
||||
return stringResource(id = resource)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantListUpdate
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls
|
||||
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.events.WebRtcViewModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState
|
||||
import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState
|
||||
|
||||
/**
|
||||
* Compose call screen wrapper
|
||||
*/
|
||||
class ComposeCallScreenMediator(activity: WebRtcCallActivity, viewModel: WebRtcCallViewModel) : CallScreenMediator {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ComposeCallScreenMediator::class)
|
||||
}
|
||||
|
||||
private val callScreenViewModel = ViewModelProvider(activity)[CallScreenViewModel::class]
|
||||
private val controlsAndInfoViewModel = ViewModelProvider(activity)[ControlsAndInfoViewModel::class]
|
||||
private val callInfoCallbacks = CallInfoCallbacks(activity, controlsAndInfoViewModel)
|
||||
|
||||
private val controlsListener = MutableStateFlow<CallScreenControlsListener>(CallScreenControlsListener.Empty)
|
||||
private val controlsVisibilityListener = MutableStateFlow<CallControlsVisibilityListener>(CallControlsVisibilityListener.Empty)
|
||||
private val pendingParticipantsViewListener = MutableStateFlow<PendingParticipantsListener>(PendingParticipantsListener.Empty)
|
||||
|
||||
init {
|
||||
activity.setContent {
|
||||
val recipient by viewModel.getRecipientFlow().collectAsStateWithLifecycle(Recipient.UNKNOWN)
|
||||
val webRtcCallState by callScreenViewModel.callState.collectAsStateWithLifecycle()
|
||||
val callScreenState by callScreenViewModel.callScreenState.collectAsStateWithLifecycle()
|
||||
val callControlsState by viewModel.getCallControlsState().collectAsStateWithLifecycle(CallControlsState())
|
||||
val callParticipantsViewState by callScreenViewModel.callParticipantsViewState.collectAsStateWithLifecycle()
|
||||
val callParticipantsState = remember(callParticipantsViewState) { callParticipantsViewState.callParticipantsState }
|
||||
val callParticipantsPagerState = remember(callParticipantsState) {
|
||||
CallParticipantsPagerState(
|
||||
callParticipants = callParticipantsState.gridParticipants,
|
||||
focusedParticipant = callParticipantsState.focusedParticipant,
|
||||
isRenderInPip = callParticipantsState.isInPipMode,
|
||||
hideAvatar = callParticipantsState.hideAvatar
|
||||
)
|
||||
}
|
||||
val dialog by callScreenViewModel.dialog.collectAsStateWithLifecycle(CallScreenDialogType.NONE)
|
||||
val callScreenControlsListener by controlsListener.collectAsStateWithLifecycle()
|
||||
val callScreenSheetDisplayListener = remember {
|
||||
object : CallScreenSheetDisplayListener {
|
||||
override fun onAudioDeviceSheetDisplayChanged(displayed: Boolean) {
|
||||
callScreenViewModel.callScreenState.update { it.copy(isDisplayingAudioToggleSheet = displayed) }
|
||||
}
|
||||
|
||||
override fun onVideoTooltipDismissed() {
|
||||
callScreenViewModel.callScreenState.update { it.copy(displayVideoTooltip = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val callControlsVisibilityListener by controlsVisibilityListener.collectAsStateWithLifecycle()
|
||||
val onControlsToggled: (Boolean) -> Unit = remember(controlsVisibilityListener) {
|
||||
{
|
||||
if (it) {
|
||||
callControlsVisibilityListener.onShown()
|
||||
} else {
|
||||
callControlsVisibilityListener.onHidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val pendingParticipantsListener by this.pendingParticipantsViewListener.collectAsStateWithLifecycle()
|
||||
|
||||
val callScreenController = CallScreenController.rememberCallScreenController(
|
||||
skipHiddenState = callControlsState.skipHiddenState,
|
||||
onControlsToggled = onControlsToggled
|
||||
)
|
||||
|
||||
LaunchedEffect(callScreenController) {
|
||||
callScreenViewModel.callScreenControllerEvents.collectLatest {
|
||||
callScreenController.handleEvent(it)
|
||||
}
|
||||
}
|
||||
|
||||
SignalTheme(isDarkMode = true) {
|
||||
CallScreen(
|
||||
callRecipient = recipient,
|
||||
webRtcCallState = webRtcCallState,
|
||||
callScreenState = callScreenState,
|
||||
callControlsState = callControlsState,
|
||||
callScreenController = callScreenController,
|
||||
callScreenControlsListener = callScreenControlsListener,
|
||||
callScreenSheetDisplayListener = callScreenSheetDisplayListener,
|
||||
callParticipantsPagerState = callParticipantsPagerState,
|
||||
pendingParticipantsListener = pendingParticipantsListener,
|
||||
overflowParticipants = callParticipantsState.listParticipants,
|
||||
localParticipant = callParticipantsState.localParticipant,
|
||||
localRenderState = callParticipantsState.localRenderState,
|
||||
callScreenDialogType = dialog,
|
||||
callInfoView = {
|
||||
CallInfoView.View(
|
||||
webRtcCallViewModel = viewModel,
|
||||
controlsAndInfoViewModel = controlsAndInfoViewModel,
|
||||
callbacks = callInfoCallbacks,
|
||||
modifier = Modifier
|
||||
.alpha(it)
|
||||
)
|
||||
},
|
||||
raiseHandSnackbar = {
|
||||
RaiseHandSnackbar.View(
|
||||
webRtcCallViewModel = viewModel,
|
||||
showCallInfoListener = { showCallInfo() },
|
||||
modifier = it
|
||||
)
|
||||
},
|
||||
onNavigationClick = { activity.onBackPressedDispatcher.onBackPressed() },
|
||||
onLocalPictureInPictureClicked = viewModel::onLocalPictureInPictureClicked,
|
||||
onControlsToggled = onControlsToggled,
|
||||
onCallScreenDialogDismissed = { callScreenViewModel.dialog.update { CallScreenDialogType.NONE } }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setWebRtcCallState(callState: WebRtcViewModel.State) {
|
||||
callScreenViewModel.callState.update { callState }
|
||||
}
|
||||
|
||||
override fun setControlsAndInfoVisibilityListener(listener: CallControlsVisibilityListener) {
|
||||
controlsVisibilityListener.update { listener }
|
||||
}
|
||||
|
||||
override fun onStateRestored() {
|
||||
Log.d(TAG, "Ignoring call to onStateRestored.")
|
||||
}
|
||||
|
||||
override fun toggleOverflowPopup() {
|
||||
callScreenViewModel.callScreenState.update {
|
||||
it.copy(displayAdditionalActionsPopup = !it.displayAdditionalActionsPopup)
|
||||
}
|
||||
}
|
||||
|
||||
override fun restartHideControlsTimer() {
|
||||
callScreenViewModel.emitControllerEvent(CallScreenController.Event.RESTART_HIDE_CONTROLS_TIMER)
|
||||
}
|
||||
|
||||
override fun showCallInfo() {
|
||||
callScreenViewModel.emitControllerEvent(CallScreenController.Event.SHOW_CALL_INFO)
|
||||
}
|
||||
|
||||
override fun toggleControls() {
|
||||
callScreenViewModel.emitControllerEvent(CallScreenController.Event.TOGGLE_CONTROLS)
|
||||
}
|
||||
|
||||
override fun setControlsListener(controlsListener: CallScreenControlsListener) {
|
||||
this.controlsListener.update { controlsListener }
|
||||
}
|
||||
|
||||
override fun setMicEnabled(enabled: Boolean) {
|
||||
Log.d(TAG, "Ignoring call to setMicEnabled.")
|
||||
}
|
||||
|
||||
override fun setStatus(status: String) {
|
||||
callScreenViewModel.callScreenState.update { it.copy(callStatus = status) }
|
||||
}
|
||||
|
||||
override fun setRecipient(recipient: Recipient) {
|
||||
callScreenViewModel.callScreenState.update { it.copy(callRecipientId = recipient.id) }
|
||||
}
|
||||
|
||||
override fun setWebRtcControls(webRtcControls: WebRtcControls) {
|
||||
Log.d(TAG, "Ignoring call to setWebRtcControls.")
|
||||
}
|
||||
|
||||
override fun updateCallParticipants(callParticipantsViewState: CallParticipantsViewState) {
|
||||
callScreenViewModel.callParticipantsViewState.update { callParticipantsViewState }
|
||||
}
|
||||
|
||||
override fun maybeDismissAudioPicker() {
|
||||
callScreenViewModel.emitControllerEvent(CallScreenController.Event.DISMISS_AUDIO_PICKER)
|
||||
}
|
||||
|
||||
override fun setPendingParticipantsViewListener(pendingParticipantsViewListener: PendingParticipantsListener) {
|
||||
this.pendingParticipantsViewListener.update { pendingParticipantsViewListener }
|
||||
}
|
||||
|
||||
override fun updatePendingParticipantsList(pendingParticipantsList: PendingParticipantsState) {
|
||||
callScreenViewModel.callScreenState.update { it.copy(pendingParticipantsState = pendingParticipantsList) }
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a no-op since this state is controlled by [CallControlsState]
|
||||
*/
|
||||
override fun setRingGroup(ringGroup: Boolean) {
|
||||
Log.d(TAG, "Ignoring call to setRingGroup.")
|
||||
}
|
||||
|
||||
override fun switchToSpeakerView() {
|
||||
callScreenViewModel.emitControllerEvent(CallScreenController.Event.SWITCH_TO_SPEAKER_VIEW)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a no-op since this state is controlled by [CallControlsState]
|
||||
*/
|
||||
override fun enableRingGroup(canRing: Boolean) {
|
||||
Log.d(TAG, "Ignoring call to enableRingGroup.")
|
||||
}
|
||||
|
||||
override fun showSpeakerViewHint() {
|
||||
callScreenViewModel.callScreenState.update { it.copy(displaySwipeToSpeakerHint = true) }
|
||||
}
|
||||
|
||||
override fun hideSpeakerViewHint() {
|
||||
callScreenViewModel.callScreenState.update { it.copy(displaySwipeToSpeakerHint = false) }
|
||||
}
|
||||
|
||||
override fun showVideoTooltip(): Dismissible {
|
||||
callScreenViewModel.callScreenState.update { it.copy(displayVideoTooltip = true) }
|
||||
|
||||
return Dismissible {
|
||||
callScreenViewModel.callScreenState.update { it.copy(displayVideoTooltip = false) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun showCameraTooltip(): Dismissible {
|
||||
callScreenViewModel.callScreenState.update { it.copy(displaySwitchCameraTooltip = true) }
|
||||
|
||||
return Dismissible {
|
||||
callScreenViewModel.callScreenState.update { it.copy(displaySwitchCameraTooltip = false) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCallStateUpdate(callControlsChange: CallControlsChange) {
|
||||
callScreenViewModel.callScreenState.update { it.copy(callControlsChange = callControlsChange) }
|
||||
}
|
||||
|
||||
override fun dismissCallOverflowPopup() {
|
||||
callScreenViewModel.callScreenState.update { it.copy(displayAdditionalActionsPopup = false) }
|
||||
}
|
||||
|
||||
override fun onParticipantListUpdate(callParticipantListUpdate: CallParticipantListUpdate) {
|
||||
callScreenViewModel.callParticipantListUpdate.update { callParticipantListUpdate }
|
||||
}
|
||||
|
||||
override fun enableParticipantUpdatePopup(enabled: Boolean) {
|
||||
callScreenViewModel.callScreenState.update { it.copy(isParticipantUpdatePopupEnabled = enabled) }
|
||||
}
|
||||
|
||||
override fun enableCallStateUpdatePopup(enabled: Boolean) {
|
||||
callScreenViewModel.callScreenState.update { it.copy(isCallStateUpdatePopupEnabled = enabled) }
|
||||
}
|
||||
|
||||
override fun showWifiToCellularPopupWindow() {
|
||||
callScreenViewModel.callScreenState.update { it.copy(displayWifiToCellularPopup = true) }
|
||||
}
|
||||
|
||||
/**
|
||||
* State holder for compose call screen
|
||||
*/
|
||||
class CallScreenViewModel : ViewModel() {
|
||||
val callScreenControllerEvents = MutableSharedFlow<CallScreenController.Event>()
|
||||
val callState = MutableStateFlow(WebRtcViewModel.State.IDLE)
|
||||
val callScreenState = MutableStateFlow(CallScreenState())
|
||||
val dialog = MutableStateFlow(CallScreenDialogType.NONE)
|
||||
val callParticipantsViewState = MutableStateFlow(
|
||||
CallParticipantsViewState(
|
||||
callParticipantsState = CallParticipantsState(),
|
||||
ephemeralState = WebRtcEphemeralState(),
|
||||
isPortrait = true,
|
||||
isLandscapeEnabled = true
|
||||
)
|
||||
)
|
||||
|
||||
val callParticipantListUpdate = MutableStateFlow(CallParticipantListUpdate.computeDeltaUpdate(emptyList(), emptyList()))
|
||||
|
||||
fun emitControllerEvent(controllerEvent: CallScreenController.Event) {
|
||||
viewModelScope.launch { callScreenControllerEvents.emit(controllerEvent) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
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.viewinterop.AndroidView
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.thoughtcrime.securesms.components.webrtc.PendingParticipantsView
|
||||
import org.thoughtcrime.securesms.service.webrtc.PendingParticipantCollection
|
||||
|
||||
/**
|
||||
* Re-implementation of PendingParticipantsView in compose.
|
||||
*/
|
||||
@Composable
|
||||
fun PendingParticipants(
|
||||
pendingParticipantsState: PendingParticipantsState,
|
||||
pendingParticipantsListener: PendingParticipantsListener
|
||||
) {
|
||||
if (pendingParticipantsState.isInPipMode) {
|
||||
return
|
||||
}
|
||||
|
||||
var hasDisplayedContent by remember { mutableStateOf(false) }
|
||||
|
||||
if (hasDisplayedContent || pendingParticipantsState.pendingParticipantCollection.getUnresolvedPendingParticipants().isNotEmpty()) {
|
||||
hasDisplayedContent = true
|
||||
|
||||
AndroidView(
|
||||
::PendingParticipantsView
|
||||
) { view ->
|
||||
view.listener = pendingParticipantsListener
|
||||
view.applyState(pendingParticipantsState.pendingParticipantCollection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun PendingParticipantsPreview() {
|
||||
Previews.Preview {
|
||||
PendingParticipants(
|
||||
pendingParticipantsState = PendingParticipantsState(
|
||||
pendingParticipantCollection = PendingParticipantCollection(),
|
||||
isInPipMode = false
|
||||
),
|
||||
pendingParticipantsListener = PendingParticipantsListener.Empty
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
interface PendingParticipantsListener {
|
||||
/**
|
||||
* Display the sheet containing the request for the top level participant
|
||||
*/
|
||||
fun onLaunchRecipientSheet(pendingRecipient: Recipient)
|
||||
|
||||
/**
|
||||
* Given recipient should be admitted to the call
|
||||
*/
|
||||
fun onAllowPendingRecipient(pendingRecipient: Recipient)
|
||||
|
||||
/**
|
||||
* Given recipient should be rejected from the call
|
||||
*/
|
||||
fun onRejectPendingRecipient(pendingRecipient: Recipient)
|
||||
|
||||
/**
|
||||
* Display the sheet containing all of the requests for the given call
|
||||
*/
|
||||
fun onLaunchPendingRequestsSheet()
|
||||
|
||||
object Empty : PendingParticipantsListener {
|
||||
override fun onLaunchRecipientSheet(pendingRecipient: Recipient) = Unit
|
||||
override fun onAllowPendingRecipient(pendingRecipient: Recipient) = Unit
|
||||
override fun onRejectPendingRecipient(pendingRecipient: Recipient) = Unit
|
||||
override fun onLaunchPendingRequestsSheet() = Unit
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.service.webrtc.state
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
import org.thoughtcrime.securesms.service.webrtc.PendingParticipantCollection
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.webrtc.v2
|
||||
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.TooltipPopup
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallOverflowPopupWindow
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantListUpdate
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallStateUpdatePopupWindow
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls
|
||||
import org.thoughtcrime.securesms.components.webrtc.WifiToCellularPopupWindow
|
||||
import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoController
|
||||
import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoViewModel
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState
|
||||
|
||||
/**
|
||||
* Wraps WebRtcCallView and supporting code into a mediator subclass
|
||||
*/
|
||||
class ViewCallScreenMediator(
|
||||
private val activity: WebRtcCallActivity,
|
||||
private val viewModel: WebRtcCallViewModel
|
||||
) : CallScreenMediator {
|
||||
private val callScreen: WebRtcCallView
|
||||
private val participantUpdateWindow: CallParticipantsListUpdatePopupWindow
|
||||
private val callStateUpdatePopupWindow: CallStateUpdatePopupWindow
|
||||
private val callOverflowPopupWindow: CallOverflowPopupWindow
|
||||
private val wifiToCellularPopupWindow: WifiToCellularPopupWindow
|
||||
private val controlsAndInfo: ControlsAndInfoController
|
||||
private val controlsAndInfoViewModel: ControlsAndInfoViewModel
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
init {
|
||||
activity.setContentView(R.layout.webrtc_call_activity)
|
||||
callScreen = activity.findViewById(R.id.callScreen)
|
||||
|
||||
participantUpdateWindow = CallParticipantsListUpdatePopupWindow(callScreen)
|
||||
callStateUpdatePopupWindow = CallStateUpdatePopupWindow(callScreen)
|
||||
wifiToCellularPopupWindow = WifiToCellularPopupWindow(callScreen)
|
||||
callOverflowPopupWindow = CallOverflowPopupWindow(activity, callScreen) {
|
||||
val state: CallParticipantsState = viewModel.callParticipantsStateSnapshot
|
||||
state.localParticipant.isHandRaised
|
||||
}
|
||||
|
||||
activity.lifecycle.addObserver(participantUpdateWindow)
|
||||
|
||||
controlsAndInfoViewModel = ViewModelProvider(activity)[ControlsAndInfoViewModel::class]
|
||||
controlsAndInfo = ControlsAndInfoController(activity, callScreen, callOverflowPopupWindow, viewModel, controlsAndInfoViewModel)
|
||||
|
||||
lifecycleDisposable.bindTo(activity.lifecycle)
|
||||
lifecycleDisposable.add(controlsAndInfo)
|
||||
}
|
||||
|
||||
override fun setWebRtcCallState(callState: WebRtcViewModel.State) = Unit
|
||||
|
||||
override fun setControlsAndInfoVisibilityListener(listener: CallControlsVisibilityListener) {
|
||||
controlsAndInfo.addVisibilityListener(listener)
|
||||
}
|
||||
|
||||
override fun onStateRestored() {
|
||||
controlsAndInfo.onStateRestored()
|
||||
}
|
||||
|
||||
override fun toggleOverflowPopup() {
|
||||
controlsAndInfo.toggleOverflowPopup()
|
||||
}
|
||||
|
||||
override fun restartHideControlsTimer() {
|
||||
controlsAndInfo.restartHideControlsTimer()
|
||||
}
|
||||
|
||||
override fun showCallInfo() {
|
||||
controlsAndInfo.showCallInfo()
|
||||
}
|
||||
|
||||
override fun toggleControls() {
|
||||
controlsAndInfo.toggleControls()
|
||||
}
|
||||
|
||||
override fun setControlsListener(controlsListener: CallScreenControlsListener) {
|
||||
callScreen.setControlsListener(controlsListener)
|
||||
}
|
||||
|
||||
override fun setMicEnabled(enabled: Boolean) {
|
||||
callScreen.setMicEnabled(enabled)
|
||||
}
|
||||
|
||||
override fun setRecipient(recipient: Recipient) {
|
||||
controlsAndInfoViewModel.setRecipient(recipient)
|
||||
callScreen.setRecipient(recipient)
|
||||
}
|
||||
|
||||
override fun setStatus(status: String) {
|
||||
callScreen.setStatus(status)
|
||||
}
|
||||
|
||||
override fun setWebRtcControls(webRtcControls: WebRtcControls) {
|
||||
callScreen.setWebRtcControls(webRtcControls)
|
||||
controlsAndInfo.updateControls(webRtcControls)
|
||||
}
|
||||
|
||||
override fun updateCallParticipants(callParticipantsViewState: CallParticipantsViewState) {
|
||||
callScreen.updateCallParticipants(callParticipantsViewState)
|
||||
}
|
||||
|
||||
override fun maybeDismissAudioPicker() {
|
||||
callScreen.maybeDismissAudioPicker()
|
||||
}
|
||||
|
||||
override fun setPendingParticipantsViewListener(pendingParticipantsViewListener: PendingParticipantsListener) {
|
||||
callScreen.setPendingParticipantsViewListener(pendingParticipantsViewListener)
|
||||
}
|
||||
|
||||
override fun updatePendingParticipantsList(pendingParticipantsList: PendingParticipantsState) {
|
||||
callScreen.updatePendingParticipantsList(pendingParticipantsList)
|
||||
}
|
||||
|
||||
override fun setRingGroup(ringGroup: Boolean) {
|
||||
callScreen.setRingGroup(ringGroup)
|
||||
}
|
||||
|
||||
override fun switchToSpeakerView() {
|
||||
callScreen.switchToSpeakerView()
|
||||
}
|
||||
|
||||
override fun enableRingGroup(canRing: Boolean) {
|
||||
callScreen.enableRingGroup(canRing)
|
||||
}
|
||||
|
||||
override fun showSpeakerViewHint() {
|
||||
callScreen.showSpeakerViewHint()
|
||||
}
|
||||
|
||||
override fun hideSpeakerViewHint() {
|
||||
callScreen.hideSpeakerViewHint()
|
||||
}
|
||||
|
||||
override fun showVideoTooltip(): Dismissible {
|
||||
val tooltip = TooltipPopup.forTarget(callScreen.videoTooltipTarget)
|
||||
.setBackgroundTint(ContextCompat.getColor(activity, R.color.core_ultramarine))
|
||||
.setTextColor(ContextCompat.getColor(activity, R.color.core_white))
|
||||
.setText(R.string.WebRtcCallActivity__tap_here_to_turn_on_your_video)
|
||||
.setOnDismissListener { viewModel.onDismissedVideoTooltip() }
|
||||
.show(TooltipPopup.POSITION_ABOVE)
|
||||
|
||||
return Dismissible {
|
||||
tooltip.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
override fun showCameraTooltip(): Dismissible {
|
||||
val tooltip = TooltipPopup.forTarget(callScreen.switchCameraTooltipTarget)
|
||||
.setBackgroundTint(ContextCompat.getColor(activity, R.color.core_ultramarine))
|
||||
.setTextColor(ContextCompat.getColor(activity, R.color.core_white))
|
||||
.setText(R.string.WebRtcCallActivity__flip_camera_tooltip)
|
||||
.setOnDismissListener {
|
||||
viewModel.onDismissedSwitchCameraTooltip()
|
||||
}
|
||||
.show(TooltipPopup.POSITION_ABOVE)
|
||||
|
||||
return Dismissible { tooltip.dismiss() }
|
||||
}
|
||||
|
||||
override fun onCallStateUpdate(callControlsChange: CallControlsChange) {
|
||||
callStateUpdatePopupWindow.onCallStateUpdate(callControlsChange)
|
||||
}
|
||||
|
||||
override fun dismissCallOverflowPopup() {
|
||||
callOverflowPopupWindow.dismiss()
|
||||
}
|
||||
|
||||
override fun onParticipantListUpdate(callParticipantListUpdate: CallParticipantListUpdate) {
|
||||
participantUpdateWindow.addCallParticipantListUpdate(callParticipantListUpdate)
|
||||
}
|
||||
|
||||
override fun enableParticipantUpdatePopup(enabled: Boolean) {
|
||||
participantUpdateWindow.setEnabled(enabled)
|
||||
}
|
||||
|
||||
override fun enableCallStateUpdatePopup(enabled: Boolean) {
|
||||
callStateUpdatePopupWindow.setEnabled(enabled)
|
||||
}
|
||||
|
||||
override fun showWifiToCellularPopupWindow() {
|
||||
wifiToCellularPopupWindow.show()
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import android.os.Bundle
|
||||
import android.util.Rational
|
||||
import android.view.Surface
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.viewModels
|
||||
@@ -49,26 +50,17 @@ import org.signal.core.util.logging.Log
|
||||
import org.signal.ringrtc.GroupCall
|
||||
import org.thoughtcrime.securesms.BaseActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.TooltipPopup
|
||||
import org.thoughtcrime.securesms.components.sensors.Orientation
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallLinkProfileKeySender
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallOverflowPopupWindow
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallReactionScrubber
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallStateUpdatePopupWindow
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallToastPopupWindow
|
||||
import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil
|
||||
import org.thoughtcrime.securesms.components.webrtc.InCallStatus
|
||||
import org.thoughtcrime.securesms.components.webrtc.PendingParticipantsBottomSheet
|
||||
import org.thoughtcrime.securesms.components.webrtc.PendingParticipantsView
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioDevice
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls
|
||||
import org.thoughtcrime.securesms.components.webrtc.WifiToCellularPopupWindow
|
||||
import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoController
|
||||
import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoViewModel
|
||||
import org.thoughtcrime.securesms.components.webrtc.requests.CallLinkIncomingRequestSheet
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
@@ -110,17 +102,11 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
private const val VIBRATE_DURATION = 50
|
||||
}
|
||||
|
||||
private lateinit var participantUpdateWindow: CallParticipantsListUpdatePopupWindow
|
||||
private lateinit var callStateUpdatePopupWindow: CallStateUpdatePopupWindow
|
||||
private lateinit var callOverflowPopupWindow: CallOverflowPopupWindow
|
||||
private lateinit var wifiToCellularPopupWindow: WifiToCellularPopupWindow
|
||||
|
||||
private lateinit var fullscreenHelper: FullscreenHelper
|
||||
private lateinit var callScreen: WebRtcCallView
|
||||
private var videoTooltip: TooltipPopup? = null
|
||||
private var switchCameraTooltip: TooltipPopup? = null
|
||||
private lateinit var callScreen: CallScreenMediator
|
||||
private var videoTooltip: Dismissible? = null
|
||||
private var switchCameraTooltip: Dismissible? = null
|
||||
private val viewModel: WebRtcCallViewModel by viewModels()
|
||||
private val controlsAndInfoViewModel: ControlsAndInfoViewModel by viewModels()
|
||||
private var enableVideoIfAvailable: Boolean = false
|
||||
private var hasWarnedAboutBluetooth: Boolean = false
|
||||
private lateinit var windowLayoutInfoConsumer: WindowLayoutInfoConsumer
|
||||
@@ -129,7 +115,6 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
private lateinit var pipBuilderParams: PictureInPictureParams.Builder
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
private var lastCallLinkDisconnectDialogShowTime: Long = 0L
|
||||
private lateinit var controlsAndInfo: ControlsAndInfoController
|
||||
private var enterPipOnResume: Boolean = false
|
||||
private var lastProcessedIntentTimestamp = 0L
|
||||
private var previousEvent: WebRtcViewModel? = null
|
||||
@@ -161,7 +146,6 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
setContentView(R.layout.webrtc_call_activity)
|
||||
|
||||
fullscreenHelper = FullscreenHelper(this)
|
||||
|
||||
@@ -171,8 +155,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
initializeViewModel()
|
||||
initializePictureInPictureParams()
|
||||
|
||||
controlsAndInfo = ControlsAndInfoController(this, callScreen, callOverflowPopupWindow, viewModel, controlsAndInfoViewModel)
|
||||
controlsAndInfo.addVisibilityListener(FadeCallback())
|
||||
callScreen.setControlsAndInfoVisibilityListener(FadeCallback())
|
||||
|
||||
fullscreenHelper.showAndHideWithSystemUI(
|
||||
window,
|
||||
@@ -181,8 +164,6 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
findViewById(R.id.webrtc_call_view_toolbar_no_text)
|
||||
)
|
||||
|
||||
lifecycleDisposable.add(controlsAndInfo)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
logIntent(callIntent)
|
||||
|
||||
@@ -227,7 +208,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
controlsAndInfo.onStateRestored()
|
||||
callScreen.onStateRestored()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
@@ -385,7 +366,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
|
||||
override fun onReactWithAnyEmojiSelected(emoji: String) {
|
||||
AppDependencies.signalCallManager.react(emoji)
|
||||
callOverflowPopupWindow.dismiss()
|
||||
callScreen.dismissCallOverflowPopup()
|
||||
}
|
||||
|
||||
override fun onProofCompleted() {
|
||||
@@ -406,7 +387,8 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
previousEvent = event
|
||||
|
||||
viewModel.setRecipient(event.recipient)
|
||||
controlsAndInfoViewModel.setRecipient(event.recipient)
|
||||
callScreen.setRecipient(event.recipient)
|
||||
callScreen.setWebRtcCallState(event.state)
|
||||
|
||||
when (event.state) {
|
||||
WebRtcViewModel.State.IDLE -> Unit
|
||||
@@ -475,18 +457,10 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
}
|
||||
|
||||
private fun initializeResources() {
|
||||
callScreen = findViewById(R.id.callScreen)
|
||||
callScreen = CallScreenMediator.create(this, viewModel)
|
||||
callScreen.setControlsListener(ControlsListener())
|
||||
|
||||
participantUpdateWindow = CallParticipantsListUpdatePopupWindow(callScreen)
|
||||
callStateUpdatePopupWindow = CallStateUpdatePopupWindow(callScreen)
|
||||
wifiToCellularPopupWindow = WifiToCellularPopupWindow(callScreen)
|
||||
callOverflowPopupWindow = CallOverflowPopupWindow(this, callScreen) {
|
||||
val state: CallParticipantsState = viewModel.callParticipantsStateSnapshot
|
||||
state.localParticipant.isHandRaised
|
||||
}
|
||||
|
||||
lifecycle.addObserver(participantUpdateWindow)
|
||||
val viewRoot = rootView()
|
||||
}
|
||||
|
||||
private fun initializeViewModel() {
|
||||
@@ -512,7 +486,6 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
launch {
|
||||
viewModel.getWebRtcControls().collectLatest {
|
||||
callScreen.setWebRtcControls(it)
|
||||
controlsAndInfo.updateControls(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,12 +511,12 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
viewModel.callParticipantsState,
|
||||
viewModel.getEphemeralState().filterNotNull()
|
||||
) { state, ephemeralState ->
|
||||
CallParticipantsViewState(state, ephemeralState, orientation == Orientation.PORTRAIT_BOTTOM_EDGE, true, isStartedFromCallLink)
|
||||
CallParticipantsViewState(state, ephemeralState, orientation == Orientation.PORTRAIT_BOTTOM_EDGE, true)
|
||||
}.collectLatest(callScreen::updateCallParticipants)
|
||||
}
|
||||
|
||||
launch {
|
||||
viewModel.getCallParticipantListUpdate().collectLatest(participantUpdateWindow::addCallParticipantListUpdate)
|
||||
viewModel.getCallParticipantListUpdate().collectLatest(callScreen::onParticipantListUpdate)
|
||||
}
|
||||
|
||||
launch {
|
||||
@@ -564,7 +537,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
}
|
||||
}
|
||||
|
||||
callScreen.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
rootView().viewTreeObserver.addOnGlobalLayoutListener {
|
||||
val state = viewModel.callParticipantsStateSnapshot
|
||||
if (state.needsNewRequestSizes()) {
|
||||
requestNewSizesThrottle.publish { AppDependencies.signalCallManager.updateRenderedResolutions() }
|
||||
@@ -573,8 +546,8 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
|
||||
addOnPictureInPictureModeChangedListener { info ->
|
||||
viewModel.setIsInPipMode(info.isInPictureInPictureMode)
|
||||
participantUpdateWindow.setEnabled(!info.isInPictureInPictureMode)
|
||||
callStateUpdatePopupWindow.setEnabled(!info.isInPictureInPictureMode)
|
||||
callScreen.enableParticipantUpdatePopup(!info.isInPictureInPictureMode)
|
||||
callScreen.enableCallStateUpdatePopup(!info.isInPictureInPictureMode)
|
||||
if (info.isInPictureInPictureMode) {
|
||||
callScreen.maybeDismissAudioPicker()
|
||||
}
|
||||
@@ -793,7 +766,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
|
||||
private fun handleOutgoingCall(event: WebRtcViewModel) {
|
||||
if (event.groupState.isNotIdle) {
|
||||
callScreen.setStatusFromGroupCallState(event.groupState)
|
||||
callScreen.setStatusFromGroupCallState(this, event.groupState)
|
||||
} else {
|
||||
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling))
|
||||
}
|
||||
@@ -809,7 +782,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
private fun handleTerminate(recipient: Recipient, hangupType: HangupMessage.Type) {
|
||||
Log.i(TAG, "handleTerminate called: $hangupType")
|
||||
|
||||
callScreen.setStatusFromHangupType(hangupType)
|
||||
callScreen.setStatusFromHangupType(this, hangupType)
|
||||
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel::class.java)
|
||||
|
||||
@@ -848,7 +821,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
private fun handleCallConnected(event: WebRtcViewModel) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES)
|
||||
if (event.groupState.isNotIdleOrConnected) {
|
||||
callScreen.setStatusFromGroupCallState(event.groupState)
|
||||
callScreen.setStatusFromGroupCallState(this, event.groupState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -891,22 +864,19 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
SafetyNumberBottomSheet.forCall(recipient.id).show(supportFragmentManager)
|
||||
}
|
||||
|
||||
private fun rootView(): ViewGroup = findViewById(android.R.id.content)
|
||||
|
||||
private fun handleViewModelEvent(event: CallEvent) {
|
||||
when (event) {
|
||||
is CallEvent.StartCall -> startCall(event.isVideoCall)
|
||||
is CallEvent.ShowGroupCallSafetyNumberChange -> SafetyNumberBottomSheet.forGroupCall(event.identityRecords).show(supportFragmentManager)
|
||||
is CallEvent.SwitchToSpeaker -> callScreen.switchToSpeakerView()
|
||||
is CallEvent.ShowSwipeToSpeakerHint -> CallToastPopupWindow.show(callScreen)
|
||||
is CallEvent.ShowSwipeToSpeakerHint -> CallToastPopupWindow.show(rootView())
|
||||
is CallEvent.ShowVideoTooltip -> {
|
||||
if (isInPipMode()) return
|
||||
|
||||
if (videoTooltip == null) {
|
||||
videoTooltip = TooltipPopup.forTarget(callScreen.videoTooltipTarget)
|
||||
.setBackgroundTint(ContextCompat.getColor(this, R.color.core_ultramarine))
|
||||
.setTextColor(ContextCompat.getColor(this, R.color.core_white))
|
||||
.setText(R.string.WebRtcCallActivity__tap_here_to_turn_on_your_video)
|
||||
.setOnDismissListener { viewModel.onDismissedVideoTooltip() }
|
||||
.show(TooltipPopup.POSITION_ABOVE)
|
||||
videoTooltip = callScreen.showVideoTooltip()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -919,19 +889,14 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
|
||||
is CallEvent.ShowWifiToCellularPopup -> {
|
||||
if (isInPipMode()) return
|
||||
wifiToCellularPopupWindow.show()
|
||||
callScreen.showWifiToCellularPopupWindow()
|
||||
}
|
||||
|
||||
is CallEvent.ShowSwitchCameraTooltip -> {
|
||||
if (isInPipMode()) return
|
||||
|
||||
if (switchCameraTooltip == null) {
|
||||
switchCameraTooltip = TooltipPopup.forTarget(callScreen.switchCameraTooltipTarget)
|
||||
.setBackgroundTint(ContextCompat.getColor(this, R.color.core_ultramarine))
|
||||
.setTextColor(ContextCompat.getColor(this, R.color.core_white))
|
||||
.setText(R.string.WebRtcCallActivity__flip_camera_tooltip)
|
||||
.setOnDismissListener { viewModel.onDismissedSwitchCameraTooltip() }
|
||||
.show(TooltipPopup.POSITION_ABOVE)
|
||||
switchCameraTooltip = callScreen.showCameraTooltip()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1077,7 +1042,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
}
|
||||
|
||||
private fun delayedFinish(delayMillis: Long = STANDARD_DELAY_FINISH) {
|
||||
callScreen.postDelayed(this::finish, delayMillis)
|
||||
rootView().postDelayed(this::finish, delayMillis)
|
||||
}
|
||||
|
||||
private fun displayRemovedFromCallLinkDialog() {
|
||||
@@ -1099,9 +1064,9 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
private fun maybeDisplaySpeakerphonePopup(nextOutput: WebRtcAudioOutput) {
|
||||
val currentOutput = viewModel.getCurrentAudioOutput()
|
||||
if (currentOutput == WebRtcAudioOutput.SPEAKER && nextOutput != WebRtcAudioOutput.SPEAKER) {
|
||||
callStateUpdatePopupWindow.onCallStateUpdate(CallControlsChange.SPEAKER_OFF)
|
||||
callScreen.onCallStateUpdate(CallControlsChange.SPEAKER_OFF)
|
||||
} else if (currentOutput != WebRtcAudioOutput.SPEAKER && nextOutput == WebRtcAudioOutput.SPEAKER) {
|
||||
callStateUpdatePopupWindow.onCallStateUpdate(CallControlsChange.SPEAKER_ON)
|
||||
callScreen.onCallStateUpdate(CallControlsChange.SPEAKER_ON)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1123,7 +1088,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
}
|
||||
}
|
||||
|
||||
private inner class FadeCallback : ControlsAndInfoController.BottomSheetVisibilityListener {
|
||||
private inner class FadeCallback : CallControlsVisibilityListener {
|
||||
override fun onShown() {
|
||||
fullscreenHelper.showSystemUI()
|
||||
}
|
||||
@@ -1137,7 +1102,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ControlsListener : WebRtcCallView.ControlsListener {
|
||||
private inner class ControlsListener : CallScreenControlsListener {
|
||||
override fun onStartCall(isVideoCall: Boolean) {
|
||||
viewModel.startCall(isVideoCall)
|
||||
}
|
||||
@@ -1168,13 +1133,13 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
|
||||
override fun onMicChanged(isMicEnabled: Boolean) {
|
||||
askAudioPermissions {
|
||||
callStateUpdatePopupWindow.onCallStateUpdate(if (isMicEnabled) CallControlsChange.MIC_ON else CallControlsChange.MIC_OFF)
|
||||
callScreen.onCallStateUpdate(if (isMicEnabled) CallControlsChange.MIC_ON else CallControlsChange.MIC_OFF)
|
||||
handleSetMuteAudio(!isMicEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOverflowClicked() {
|
||||
controlsAndInfo.toggleOverflowPopup()
|
||||
callScreen.toggleOverflowPopup()
|
||||
}
|
||||
|
||||
override fun onCameraDirectionChanged() {
|
||||
@@ -1207,21 +1172,21 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
|
||||
override fun onLocalPictureInPictureClicked() {
|
||||
viewModel.onLocalPictureInPictureClicked()
|
||||
controlsAndInfo.restartHideControlsTimer()
|
||||
callScreen.restartHideControlsTimer()
|
||||
}
|
||||
|
||||
override fun onRingGroupChanged(ringGroup: Boolean, ringingAllowed: Boolean) {
|
||||
if (ringingAllowed) {
|
||||
AppDependencies.signalCallManager.setRingGroup(ringGroup)
|
||||
callStateUpdatePopupWindow.onCallStateUpdate(if (ringGroup) CallControlsChange.RINGING_ON else CallControlsChange.RINGING_OFF)
|
||||
callScreen.onCallStateUpdate(if (ringGroup) CallControlsChange.RINGING_ON else CallControlsChange.RINGING_OFF)
|
||||
} else {
|
||||
AppDependencies.signalCallManager.setRingGroup(false)
|
||||
callStateUpdatePopupWindow.onCallStateUpdate(CallControlsChange.RINGING_DISABLED)
|
||||
callScreen.onCallStateUpdate(CallControlsChange.RINGING_DISABLED)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCallInfoClicked() {
|
||||
controlsAndInfo.showCallInfo()
|
||||
callScreen.showCallInfo()
|
||||
}
|
||||
|
||||
override fun onNavigateUpClicked() {
|
||||
@@ -1231,7 +1196,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
override fun toggleControls() {
|
||||
val controlState = viewModel.getWebRtcControls().value
|
||||
if (!controlState.displayIncomingCallButtons() && !controlState.displayErrorControls()) {
|
||||
controlsAndInfo.toggleControls()
|
||||
callScreen.toggleControls()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1240,7 +1205,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re
|
||||
}
|
||||
}
|
||||
|
||||
private inner class PendingParticipantsViewListener : PendingParticipantsView.Listener {
|
||||
private inner class PendingParticipantsViewListener : PendingParticipantsListener {
|
||||
override fun onLaunchRecipientSheet(pendingRecipient: Recipient) {
|
||||
CallLinkIncomingRequestSheet.show(supportFragmentManager, pendingRecipient.id)
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ import org.thoughtcrime.securesms.recipients.LiveRecipient
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.PendingParticipantCollection
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.PendingParticipantsState
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState
|
||||
import org.thoughtcrime.securesms.util.NetworkUtil
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
|
||||
|
||||
@@ -94,6 +94,10 @@ public final class FullscreenHelper {
|
||||
boolean hide = (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0;
|
||||
|
||||
for (View view : views) {
|
||||
if (view == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
view.animate()
|
||||
.alpha(hide ? 0 : 1)
|
||||
.withStartAction(() -> {
|
||||
|
||||
@@ -7,8 +7,7 @@ class CallParticipantsViewState(
|
||||
callParticipantsState: CallParticipantsState,
|
||||
ephemeralState: WebRtcEphemeralState,
|
||||
val isPortrait: Boolean,
|
||||
val isLandscapeEnabled: Boolean,
|
||||
val isStartedFromCallLink: Boolean
|
||||
val isLandscapeEnabled: Boolean
|
||||
) {
|
||||
|
||||
val callParticipantsState = CallParticipantsState.update(callParticipantsState, ephemeralState)
|
||||
|
||||
@@ -15,7 +15,7 @@ import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
class BackListenerTest {
|
||||
class BackPendingParticipantsListenerTest {
|
||||
|
||||
@Test
|
||||
fun testBackupJitterExactlyWithinJitterWindow() {
|
||||
Reference in New Issue
Block a user