diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/AudioStateUpdater.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/AudioStateUpdater.kt index 1e1879a23a..1690547e29 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/AudioStateUpdater.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/AudioStateUpdater.kt @@ -1,7 +1,7 @@ package org.thoughtcrime.securesms.components.webrtc /** - * This is an interface for [WebRtcAudioPicker31] and [WebRtcAudioPickerLegacy] to reference methods in [WebRtcAudioOutputToggleButton] without actually depending on it. + * This is an interface for [WebRtcAudioPicker31] and [WebRtcAudioPickerLegacy] as a callback for [org.thoughtcrime.securesms.components.webrtc.v2.CallAudioToggleButton] */ interface AudioStateUpdater { fun updateAudioOutputState(audioOutput: WebRtcAudioOutput) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallOverflowPopupWindow.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallOverflowPopupWindow.kt deleted file mode 100644 index 9fa67b1de1..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallOverflowPopupWindow.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2023 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.webrtc - -import android.graphics.Rect -import android.view.Gravity -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.LinearLayout -import android.widget.PopupWindow -import android.widget.TextView -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.widget.PopupWindowCompat -import androidx.fragment.app.FragmentActivity -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.dependencies.AppDependencies -import org.thoughtcrime.securesms.util.visible - -/** - * A popup window for calls that holds extra actions, such as reactions, raise hand, and screen sharing. - * - */ -class CallOverflowPopupWindow(private val activity: FragmentActivity, parentViewGroup: ViewGroup, private val raisedHandDelegate: RaisedHandDelegate) : PopupWindow( - LayoutInflater.from(activity).inflate(R.layout.call_overflow_holder, parentViewGroup, false), - activity.resources.getDimension(R.dimen.calling_reaction_popup_menu_width).toInt(), - activity.resources.getDimension(R.dimen.calling_reaction_popup_menu_height).toInt() -) { - private val raiseHandLabel: TextView = (contentView as LinearLayout).findViewById(R.id.raise_hand_label) - - init { - val root = (contentView as LinearLayout) - val reactionScrubber = root.findViewById(R.id.reaction_scrubber) - reactionScrubber.initialize(activity.supportFragmentManager) { - AppDependencies.signalCallManager.react(it) - dismiss() - } - val raiseHand = root.findViewById(R.id.raise_hand_layout_parent) - raiseHand.visible = true - raiseHand.setOnClickListener { - AppDependencies.signalCallManager.raiseHand(!raisedHandDelegate.isSelfHandRaised()) - dismiss() - } - } - - fun show(anchor: View) { - isFocusable = true - - val resources = activity.resources - - val margin = resources.getDimension(R.dimen.calling_reaction_scrubber_margin).toInt() - - val windowRect = Rect() - contentView.getWindowVisibleDisplayFrame(windowRect) - val windowWidth = windowRect.width() - val popupWidth = resources.getDimension(R.dimen.reaction_scrubber_width).toInt() - - val popupHeight = resources.getDimension(R.dimen.calling_reaction_popup_menu_height).toInt() - - val xOffset = windowWidth - popupWidth - margin - val yOffset = -popupHeight - margin - - raiseHandLabel.setText(if (raisedHandDelegate.isSelfHandRaised()) R.string.CallOverflowPopupWindow__lower_hand else R.string.CallOverflowPopupWindow__raise_hand) - - PopupWindowCompat.showAsDropDown(this, anchor, xOffset, yOffset, Gravity.NO_GRAVITY) - } - - fun interface RaisedHandDelegate { - fun isSelfHandRaised(): Boolean - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantListUpdate.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantListUpdate.java index b9517e48ab..52c14faf80 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantListUpdate.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantListUpdate.java @@ -6,9 +6,9 @@ import androidx.annotation.VisibleForTesting; import com.annimon.stream.Collectors; import com.annimon.stream.Stream; +import org.signal.core.util.SetUtil; import org.thoughtcrime.securesms.events.CallParticipant; import org.thoughtcrime.securesms.events.CallParticipantId; -import org.signal.core.util.SetUtil; import java.util.List; import java.util.Objects; @@ -16,7 +16,7 @@ import java.util.Set; /** * Represents the delta between two lists of CallParticipant objects. This is used along with - * {@link CallParticipantsListUpdatePopupWindow} to display in-call notifications to the user + * {@link org.thoughtcrime.securesms.components.webrtc.v2.CallParticipantUpdatePopupKt} to display in-call notifications to the user * whenever remote participants leave or reconnect to the call. */ public final class CallParticipantListUpdate { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java deleted file mode 100644 index c4fed216a5..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java +++ /dev/null @@ -1,465 +0,0 @@ -package org.thoughtcrime.securesms.components.webrtc; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.AppCompatImageView; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.constraintlayout.widget.ConstraintSet; -import androidx.core.content.ContextCompat; -import androidx.core.view.ViewKt; -import androidx.core.widget.ImageViewCompat; -import androidx.transition.Transition; -import androidx.transition.TransitionManager; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import org.signal.core.util.ThreadUtil; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatarDrawable; -import org.thoughtcrime.securesms.badges.BadgeImageView; -import org.thoughtcrime.securesms.components.AvatarImageView; -import org.thoughtcrime.securesms.components.emoji.EmojiTextView; -import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; -import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto; -import org.thoughtcrime.securesms.conversation.colors.ChatColors; -import org.thoughtcrime.securesms.events.CallParticipant; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.util.AvatarUtil; -import org.thoughtcrime.securesms.util.ViewUtil; -import org.webrtc.RendererCommon; -import org.whispersystems.signalservice.api.util.Preconditions; - -import java.util.Objects; -import java.util.concurrent.TimeUnit; - -/** - * Encapsulates views needed to show a call participant including their - * avatar in full screen or pip mode, and their video feed. - */ -public class CallParticipantView extends ConstraintLayout { - - private static final long DELAY_SHOWING_MISSING_MEDIA_KEYS = TimeUnit.SECONDS.toMillis(5); - private static final int SMALL_AVATAR = ViewUtil.dpToPx(96); - private static final int LARGE_AVATAR = ViewUtil.dpToPx(112); - - private RecipientId recipientId; - private boolean infoMode; - private boolean raiseHandAllowed; - private Runnable missingMediaKeysUpdater; - private boolean shouldRenderInPip; - - private SelfPipMode selfPipMode = SelfPipMode.NOT_SELF_PIP; - - private AppCompatImageView backgroundAvatar; - private AvatarImageView avatar; - private BadgeImageView badge; - private View rendererFrame; - private TextureViewRenderer renderer; - private ImageView pipAvatar; - private BadgeImageView pipBadge; - private ContactPhoto contactPhoto; - private AudioIndicatorView audioIndicator; - private View infoOverlay; - private EmojiTextView infoMessage; - private Button infoMoreInfo; - private AppCompatImageView infoIcon; - private View switchCameraIconFrame; - private View switchCameraIcon; - private ImageView raiseHandIcon; - private TextView nameLabel; - - public CallParticipantView(@NonNull Context context) { - super(context); - onFinishInflate(); - } - - public CallParticipantView(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - public CallParticipantView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - backgroundAvatar = findViewById(R.id.call_participant_background_avatar); - avatar = findViewById(R.id.call_participant_item_avatar); - pipAvatar = findViewById(R.id.call_participant_item_pip_avatar); - rendererFrame = findViewById(R.id.call_participant_renderer_frame); - renderer = findViewById(R.id.call_participant_renderer); - audioIndicator = findViewById(R.id.call_participant_audio_indicator); - infoOverlay = findViewById(R.id.call_participant_info_overlay); - infoIcon = findViewById(R.id.call_participant_info_icon); - infoMessage = findViewById(R.id.call_participant_info_message); - infoMoreInfo = findViewById(R.id.call_participant_info_more_info); - badge = findViewById(R.id.call_participant_item_badge); - pipBadge = findViewById(R.id.call_participant_item_pip_badge); - switchCameraIconFrame = findViewById(R.id.call_participant_switch_camera); - switchCameraIcon = findViewById(R.id.call_participant_switch_camera_icon); - raiseHandIcon = findViewById(R.id.call_participant_raise_hand_icon); - nameLabel = findViewById(R.id.call_participant_name_label); - - useLargeAvatar(); - } - - public void setMirror(boolean mirror) { - renderer.setMirror(mirror); - } - - public void setScalingType(@NonNull RendererCommon.ScalingType scalingType) { - renderer.setScalingType(scalingType); - } - - void setScalingType(@NonNull RendererCommon.ScalingType scalingTypeMatchOrientation, @NonNull RendererCommon.ScalingType scalingTypeMismatchOrientation) { - renderer.setScalingType(scalingTypeMatchOrientation, scalingTypeMismatchOrientation); - } - - public void setCallParticipant(@NonNull CallParticipant participant) { - boolean participantChanged = recipientId == null || !recipientId.equals(participant.getRecipient().getId()); - recipientId = participant.getRecipient().getId(); - infoMode = participant.getRecipient().isBlocked() || isMissingMediaKeys(participant); - - if (infoMode) { - rendererFrame.setVisibility(View.GONE); - renderer.setVisibility(View.GONE); - renderer.attachBroadcastVideoSink(null); - audioIndicator.setVisibility(View.GONE); - avatar.setVisibility(View.GONE); - badge.setVisibility(View.GONE); - pipAvatar.setVisibility(View.GONE); - pipBadge.setVisibility(View.GONE); - - infoOverlay.setVisibility(View.VISIBLE); - - ImageViewCompat.setImageTintList(infoIcon, ContextCompat.getColorStateList(getContext(), R.color.core_white)); - - if (participant.getRecipient().isBlocked()) { - infoIcon.setImageResource(R.drawable.ic_block_tinted_24); - infoMessage.setText(getContext().getString(R.string.CallParticipantView__s_is_blocked, participant.getRecipient().getShortDisplayName(getContext()))); - infoMoreInfo.setOnClickListener(v -> showBlockedDialog(participant.getRecipient())); - } else { - infoIcon.setImageResource(R.drawable.ic_error_solid_24); - infoMessage.setText(getContext().getString(R.string.CallParticipantView__cant_receive_audio_video_from_s, participant.getRecipient().getShortDisplayName(getContext()))); - infoMoreInfo.setOnClickListener(v -> showNoMediaKeysDialog(participant.getRecipient())); - } - } else { - infoOverlay.setVisibility(View.GONE); - - //TODO: [calling] SFU instability causes the forwarding video flag to alternate quickly, should restore after calling server update - boolean hasContentToRender = (participant.isVideoEnabled() || participant.isScreenSharing()); // && participant.isForwardingVideo(); - - rendererFrame.setVisibility(hasContentToRender ? View.VISIBLE : View.INVISIBLE); - renderer.setVisibility(hasContentToRender ? View.VISIBLE : View.INVISIBLE); - - if (participant.isVideoEnabled()) { - participant.getVideoSink().getLockableEglBase().performWithValidEglBase(eglBase -> { - renderer.init(eglBase); - }); - renderer.attachBroadcastVideoSink(participant.getVideoSink()); - } else { - renderer.attachBroadcastVideoSink(null); - } - - audioIndicator.setVisibility(View.VISIBLE); - audioIndicator.bind(participant.isMicrophoneEnabled(), participant.getAudioLevel()); - final String shortRecipientDisplayName = participant.getShortRecipientDisplayName(getContext()); - if (raiseHandAllowed && participant.isHandRaised()) { - raiseHandIcon.setVisibility(View.VISIBLE); - nameLabel.setVisibility(View.VISIBLE); - nameLabel.setText(shortRecipientDisplayName); - } else { - raiseHandIcon.setVisibility(View.GONE); - nameLabel.setVisibility(View.GONE); - } - } - - if (participantChanged || !Objects.equals(contactPhoto, participant.getRecipient().getContactPhoto())) { - avatar.setAvatarUsingProfile(participant.getRecipient()); - badge.setBadgeFromRecipient(participant.getRecipient()); - AvatarUtil.loadBlurredIconIntoImageView(participant.getRecipient(), backgroundAvatar); - setPipAvatar(participant.getRecipient()); - pipBadge.setBadgeFromRecipient(participant.getRecipient()); - contactPhoto = participant.getRecipient().getContactPhoto(); - } - - setRenderInPip(shouldRenderInPip); - } - - private boolean isMissingMediaKeys(@NonNull CallParticipant participant) { - if (missingMediaKeysUpdater != null) { - ThreadUtil.cancelRunnableOnMain(missingMediaKeysUpdater); - missingMediaKeysUpdater = null; - } - - if (!participant.isMediaKeysReceived()) { - long time = System.currentTimeMillis() - participant.getAddedToCallTime(); - if (time > DELAY_SHOWING_MISSING_MEDIA_KEYS) { - return true; - } else { - missingMediaKeysUpdater = () -> { - if (recipientId.equals(participant.getRecipient().getId())) { - setCallParticipant(participant); - } - }; - ThreadUtil.runOnMainDelayed(missingMediaKeysUpdater, DELAY_SHOWING_MISSING_MEDIA_KEYS - time); - } - } - return false; - } - - public void setRenderInPip(boolean shouldRenderInPip) { - this.shouldRenderInPip = shouldRenderInPip; - - if (infoMode) { - infoMessage.setVisibility(shouldRenderInPip ? View.GONE : View.VISIBLE); - infoMoreInfo.setVisibility(shouldRenderInPip ? View.GONE : View.VISIBLE); - infoOverlay.setOnClickListener(shouldRenderInPip ? v -> infoMoreInfo.performClick() : null); - return; - } else { - infoOverlay.setOnClickListener(null); - } - - avatar.setVisibility(shouldRenderInPip ? View.GONE : View.VISIBLE); - badge.setVisibility(shouldRenderInPip ? View.GONE : View.VISIBLE); - pipAvatar.setVisibility(shouldRenderInPip ? View.VISIBLE : View.GONE); - pipBadge.setVisibility(shouldRenderInPip ? View.VISIBLE : View.GONE); - } - - public void setRaiseHandAllowed(boolean raiseHandAllowed) { - this.raiseHandAllowed = raiseHandAllowed; - } - - public void setCameraToggleOnClickListener(@Nullable View.OnClickListener onClickListener) { - switchCameraIconFrame.setOnClickListener(onClickListener); - } - - /** - * Adjust UI elements for the various self PIP positions. If called after a {@link TransitionManager#beginDelayedTransition(ViewGroup, Transition)}, - * the changes to the UI elements will animate. - */ - public void setSelfPipMode(@NonNull SelfPipMode selfPipMode, boolean isMoreThanOneCameraAvailable) { - Preconditions.checkArgument(selfPipMode != SelfPipMode.NOT_SELF_PIP); - - if (this.selfPipMode == selfPipMode) { - return; - } - - this.selfPipMode = selfPipMode; - - ConstraintSet constraints = new ConstraintSet(); - constraints.clone(this); - - switch (selfPipMode) { - case NORMAL_SELF_PIP -> { - constraints.connect( - R.id.call_participant_audio_indicator, - ConstraintSet.START, - ConstraintSet.PARENT_ID, - ConstraintSet.START, - ViewUtil.dpToPx(6) - ); - constraints.clear( - R.id.call_participant_audio_indicator, - ConstraintSet.END - ); - constraints.setMargin( - R.id.call_participant_audio_indicator, - ConstraintSet.BOTTOM, - ViewUtil.dpToPx(6) - ); - - if (isMoreThanOneCameraAvailable) { - constraints.setVisibility(R.id.call_participant_switch_camera, View.VISIBLE); - constraints.setMargin( - R.id.call_participant_switch_camera, - ConstraintSet.END, - ViewUtil.dpToPx(6) - ); - constraints.setMargin( - R.id.call_participant_switch_camera, - ConstraintSet.BOTTOM, - ViewUtil.dpToPx(6) - ); - constraints.constrainWidth(R.id.call_participant_switch_camera, ViewUtil.dpToPx(28)); - constraints.constrainHeight(R.id.call_participant_switch_camera, ViewUtil.dpToPx(28)); - - ViewGroup.LayoutParams params = switchCameraIcon.getLayoutParams(); - params.width = params.height = ViewUtil.dpToPx(16); - switchCameraIcon.setLayoutParams(params); - - switchCameraIconFrame.setClickable(false); - switchCameraIconFrame.setEnabled(false); - } else { - constraints.setVisibility(R.id.call_participant_switch_camera, View.GONE); - } - } - case EXPANDED_SELF_PIP -> { - constraints.connect( - R.id.call_participant_audio_indicator, - ConstraintSet.START, - ConstraintSet.PARENT_ID, - ConstraintSet.START, - ViewUtil.dpToPx(8) - ); - constraints.clear( - R.id.call_participant_audio_indicator, - ConstraintSet.END - ); - constraints.setMargin( - R.id.call_participant_audio_indicator, - ConstraintSet.BOTTOM, - ViewUtil.dpToPx(8) - ); - - if (isMoreThanOneCameraAvailable) { - constraints.setVisibility(R.id.call_participant_switch_camera, View.VISIBLE); - constraints.setMargin( - R.id.call_participant_switch_camera, - ConstraintSet.END, - ViewUtil.dpToPx(8) - ); - constraints.setMargin( - R.id.call_participant_switch_camera, - ConstraintSet.BOTTOM, - ViewUtil.dpToPx(8) - ); - constraints.constrainWidth(R.id.call_participant_switch_camera, ViewUtil.dpToPx(48)); - constraints.constrainHeight(R.id.call_participant_switch_camera, ViewUtil.dpToPx(48)); - - ViewGroup.LayoutParams params = switchCameraIcon.getLayoutParams(); - params.width = params.height = ViewUtil.dpToPx(24); - switchCameraIcon.setLayoutParams(params); - - switchCameraIconFrame.setClickable(true); - switchCameraIconFrame.setEnabled(true); - } else { - constraints.setVisibility(R.id.call_participant_switch_camera, View.GONE); - } - } - case MINI_SELF_PIP -> { - constraints.connect( - R.id.call_participant_audio_indicator, - ConstraintSet.START, - ConstraintSet.PARENT_ID, - ConstraintSet.START, - 0 - ); - constraints.connect( - R.id.call_participant_audio_indicator, - ConstraintSet.END, - ConstraintSet.PARENT_ID, - ConstraintSet.END, - 0 - ); - constraints.setMargin( - R.id.call_participant_audio_indicator, - ConstraintSet.BOTTOM, - ViewUtil.dpToPx(6) - ); - constraints.setVisibility(R.id.call_participant_switch_camera, View.GONE); - } - } - - constraints.applyTo(this); - } - - void hideAvatar() { - avatar.setAlpha(0f); - badge.setAlpha(0f); - } - - void showAvatar() { - avatar.setAlpha(1f); - badge.setAlpha(1f); - } - - void useLargeAvatar() { - changeAvatarParams(LARGE_AVATAR); - } - - void useSmallAvatar() { - changeAvatarParams(SMALL_AVATAR); - } - - void setBottomInset(int bottomInset) { - int desiredMargin = getResources().getDimensionPixelSize(R.dimen.webrtc_audio_indicator_margin) + bottomInset; - if (ViewKt.getMarginBottom(audioIndicator) == desiredMargin) { - return; - } - - TransitionManager.beginDelayedTransition(this); - - ViewUtil.setBottomMargin(audioIndicator, desiredMargin); - } - - public void releaseRenderer() { - renderer.release(); - } - - private void changeAvatarParams(int dimension) { - ViewGroup.LayoutParams params = avatar.getLayoutParams(); - if (params.height != dimension) { - params.height = dimension; - params.width = dimension; - avatar.setLayoutParams(params); - } - } - - private void setPipAvatar(@NonNull Recipient recipient) { - ContactPhoto contactPhoto = recipient.isSelf() ? new ProfileContactPhoto(Recipient.self()) - : recipient.getContactPhoto(); - - FallbackAvatarDrawable fallbackAvatarDrawable = new FallbackAvatarDrawable(getContext(), recipient.getFallbackAvatar()); - - Glide.with(this) - .load(contactPhoto) - .fallback(fallbackAvatarDrawable) - .error(fallbackAvatarDrawable) - .diskCacheStrategy(DiskCacheStrategy.ALL) - .fitCenter() - .into(pipAvatar); - - pipAvatar.setScaleType(contactPhoto == null ? ImageView.ScaleType.CENTER_INSIDE : ImageView.ScaleType.CENTER_CROP); - - ChatColors chatColors = recipient.getChatColors(); - - pipAvatar.setBackground(chatColors.getChatBubbleMask()); - } - - private void showBlockedDialog(@NonNull Recipient recipient) { - new MaterialAlertDialogBuilder(getContext()) - .setTitle(getContext().getString(R.string.CallParticipantView__s_is_blocked, recipient.getShortDisplayName(getContext()))) - .setMessage(R.string.CallParticipantView__you_wont_receive_their_audio_or_video) - .setPositiveButton(android.R.string.ok, null) - .show(); - } - - private void showNoMediaKeysDialog(@NonNull Recipient recipient) { - new MaterialAlertDialogBuilder(getContext()) - .setTitle(getContext().getString(R.string.CallParticipantView__cant_receive_audio_and_video_from_s, recipient.getShortDisplayName(getContext()))) - .setMessage(R.string.CallParticipantView__this_may_be_Because_they_have_not_verified_your_safety_number_change) - .setPositiveButton(android.R.string.ok, null) - .show(); - } - - public enum SelfPipMode { - NOT_SELF_PIP, - NORMAL_SELF_PIP, - EXPANDED_SELF_PIP, - MINI_SELF_PIP - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsLayout.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsLayout.java deleted file mode 100644 index de99eeb57a..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsLayout.java +++ /dev/null @@ -1,173 +0,0 @@ -package org.thoughtcrime.securesms.components.webrtc; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.google.android.flexbox.AlignItems; -import com.google.android.flexbox.FlexboxLayout; -import com.google.android.material.card.MaterialCardView; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.events.CallParticipant; -import org.thoughtcrime.securesms.util.Util; -import org.thoughtcrime.securesms.util.ViewUtil; - -import java.util.Collections; -import java.util.List; - -/** - * Can dynamically render a collection of call participants, adjusting their - * sizing and layout depending on the total number of participants. - */ -public class CallParticipantsLayout extends FlexboxLayout { - - private static final int MULTIPLE_PARTICIPANT_SPACING = ViewUtil.dpToPx(3); - private static final int CORNER_RADIUS = ViewUtil.dpToPx(10); - private static final int RAISE_HAND_MINIMUM_COUNT = 2; - - private List callParticipants = Collections.emptyList(); - private CallParticipant focusedParticipant = null; - private boolean shouldRenderInPip; - private boolean isPortrait; - private boolean hideAvatar; - private int navBarBottomInset; - private LayoutStrategy layoutStrategy; - - public CallParticipantsLayout(@NonNull Context context) { - super(context); - } - - public CallParticipantsLayout(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - public CallParticipantsLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public void update(@NonNull List callParticipants, - @NonNull CallParticipant focusedParticipant, - boolean shouldRenderInPip, - boolean isPortrait, - boolean hideAvatar, - int navBarBottomInset, - @NonNull LayoutStrategy layoutStrategy) - { - this.callParticipants = callParticipants; - this.focusedParticipant = focusedParticipant; - this.shouldRenderInPip = shouldRenderInPip; - this.isPortrait = isPortrait; - this.hideAvatar = hideAvatar; - this.navBarBottomInset = navBarBottomInset; - this.layoutStrategy = layoutStrategy; - - setFlexDirection(layoutStrategy.getFlexDirection()); - updateLayout(); - } - - private void updateLayout() { - int previousChildCount = getChildCount(); - - if (shouldRenderInPip && Util.hasItems(callParticipants)) { - updateChildrenCount(1); - update(0, 1, focusedParticipant); - } else { - int count = callParticipants.size(); - updateChildrenCount(count); - - for (int i = 0; i < count; i++) { - update(i, count, callParticipants.get(i)); - } - } - - if (previousChildCount != getChildCount()) { - updateMarginsForLayout(); - } - } - - private void updateMarginsForLayout() { - MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams(); - if (callParticipants.size() > 1 && !shouldRenderInPip) { - layoutParams.setMargins(MULTIPLE_PARTICIPANT_SPACING, ViewUtil.getStatusBarHeight(this), MULTIPLE_PARTICIPANT_SPACING, 0); - } else { - layoutParams.setMargins(0, 0, 0, 0); - } - setLayoutParams(layoutParams); - } - - private void updateChildrenCount(int count) { - int childCount = getChildCount(); - if (childCount < count) { - for (int i = childCount; i < count; i++) { - addCallParticipantView(); - } - } else if (childCount > count) { - for (int i = count; i < childCount; i++) { - CallParticipantView callParticipantView = getChildAt(count).findViewById(R.id.group_call_participant); - callParticipantView.releaseRenderer(); - removeViewAt(count); - } - } - } - - private void update(int index, int count, @NonNull CallParticipant participant) { - View view = getChildAt(index); - MaterialCardView cardView = view.findViewById(R.id.group_call_participant_card_wrapper); - CallParticipantView callParticipantView = view.findViewById(R.id.group_call_participant); - - callParticipantView.setCallParticipant(participant); - callParticipantView.setRenderInPip(shouldRenderInPip); - layoutStrategy.setChildScaling(participant, callParticipantView, isPortrait, count); - - if (count > 1) { - view.setPadding(MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING); - cardView.setRadius(CORNER_RADIUS); - callParticipantView.setBottomInset(0); - } else { - view.setPadding(0, 0, 0, 0); - cardView.setRadius(0); - callParticipantView.setBottomInset(navBarBottomInset); - } - - if (hideAvatar) { - callParticipantView.hideAvatar(); - } else { - callParticipantView.showAvatar(); - } - - if (count > 2) { - callParticipantView.useSmallAvatar(); - } else { - callParticipantView.useLargeAvatar(); - } - - callParticipantView.setRaiseHandAllowed(count >= RAISE_HAND_MINIMUM_COUNT); - - layoutStrategy.setChildLayoutParams(view, index, getChildCount()); - } - - private void addCallParticipantView() { - View view = LayoutInflater.from(getContext()).inflate(R.layout.group_call_participant_item, this, false); - FlexboxLayout.LayoutParams params = (FlexboxLayout.LayoutParams) view.getLayoutParams(); - - params.setAlignSelf(AlignItems.STRETCH); - view.setLayoutParams(params); - addView(view); - } - - public interface LayoutStrategy { - int getFlexDirection(); - - void setChildScaling(@NonNull CallParticipant callParticipant, - @NonNull CallParticipantView callParticipantView, - boolean isPortrait, - int childCount); - - void setChildLayoutParams(@NonNull View child, int childPosition, int childCount); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsLayoutStrategies.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsLayoutStrategies.kt deleted file mode 100644 index 9770881b73..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsLayoutStrategies.kt +++ /dev/null @@ -1,73 +0,0 @@ -package org.thoughtcrime.securesms.components.webrtc - -import android.view.View -import com.google.android.flexbox.FlexDirection -import com.google.android.flexbox.FlexboxLayout -import org.thoughtcrime.securesms.events.CallParticipant -import org.webrtc.RendererCommon - -object CallParticipantsLayoutStrategies { - - private object Portrait : CallParticipantsLayout.LayoutStrategy { - override fun getFlexDirection(): Int = FlexDirection.ROW - - override fun setChildScaling(callParticipant: CallParticipant, callParticipantView: CallParticipantView, isPortrait: Boolean, childCount: Int) { - if (callParticipant.isScreenSharing) { - callParticipantView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) - } else { - val matchOrientationScaling = if (isPortrait || childCount < 3) RendererCommon.ScalingType.SCALE_ASPECT_FILL else RendererCommon.ScalingType.SCALE_ASPECT_BALANCED - val mismatchOrientationScaling = if (childCount == 1) RendererCommon.ScalingType.SCALE_ASPECT_FIT else matchOrientationScaling - callParticipantView.setScalingType(matchOrientationScaling, mismatchOrientationScaling) - } - } - - override fun setChildLayoutParams(child: View, childPosition: Int, childCount: Int) { - val params = child.layoutParams as FlexboxLayout.LayoutParams - if (childCount < 3) { - params.flexBasisPercent = 1f - } else { - if ((childCount % 2) != 0 && childPosition == childCount - 1) { - params.flexBasisPercent = 1f - } else { - params.flexBasisPercent = 0.5f - } - } - child.layoutParams = params - } - } - - private object Landscape : CallParticipantsLayout.LayoutStrategy { - override fun getFlexDirection() = FlexDirection.COLUMN - - override fun setChildScaling(callParticipant: CallParticipant, callParticipantView: CallParticipantView, isPortrait: Boolean, childCount: Int) { - if (callParticipant.isScreenSharing) { - callParticipantView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) - } else { - callParticipantView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL, RendererCommon.ScalingType.SCALE_ASPECT_BALANCED) - } - } - - override fun setChildLayoutParams(child: View, childPosition: Int, childCount: Int) { - val params = child.layoutParams as FlexboxLayout.LayoutParams - if (childCount < 4) { - params.flexBasisPercent = 1f - } else { - if ((childCount % 2) != 0 && childPosition == childCount - 1) { - params.flexBasisPercent = 1f - } else { - params.flexBasisPercent = 0.5f - } - } - child.layoutParams = params - } - } - - @JvmStatic - fun getStrategy(isPortrait: Boolean, isLandscapeEnabled: Boolean): CallParticipantsLayout.LayoutStrategy { - return if (isPortrait || !isLandscapeEnabled) { - Portrait - } else { - Landscape - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsListUpdatePopupWindow.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsListUpdatePopupWindow.java deleted file mode 100644 index d9070d08a3..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsListUpdatePopupWindow.java +++ /dev/null @@ -1,209 +0,0 @@ -package org.thoughtcrime.securesms.components.webrtc; - - -import android.content.Context; -import android.os.Handler; -import android.os.Looper; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.PopupWindow; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.PluralsRes; -import androidx.annotation.StringRes; -import androidx.lifecycle.DefaultLifecycleObserver; -import androidx.lifecycle.LifecycleOwner; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.badges.BadgeImageView; -import org.thoughtcrime.securesms.components.AvatarImageView; -import org.thoughtcrime.securesms.recipients.Recipient; - -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -public class CallParticipantsListUpdatePopupWindow extends PopupWindow implements DefaultLifecycleObserver { - - private static final long DURATION = TimeUnit.SECONDS.toMillis(5); - - private final ViewGroup parent; - private final AvatarImageView avatarImageView; - private final BadgeImageView badgeImageView; - private final TextView descriptionTextView; - private final Handler handler; - - private final Set pendingAdditions = new HashSet<>(); - private final Set pendingRemovals = new HashSet<>(); - - private boolean isEnabled = true; - - public CallParticipantsListUpdatePopupWindow(@NonNull ViewGroup parent) { - super(LayoutInflater.from(parent.getContext()).inflate(R.layout.call_participant_list_update, parent, false), - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT); - - this.parent = parent; - this.avatarImageView = getContentView().findViewById(R.id.avatar); - this.badgeImageView = getContentView().findViewById(R.id.badge); - this.descriptionTextView = getContentView().findViewById(R.id.description); - this.handler = new Handler(Looper.getMainLooper()); - - setOnDismissListener(this::showPending); - getContentView().setOnClickListener(v -> dismiss()); - setAnimationStyle(R.style.PopupAnimation); - } - - public void addCallParticipantListUpdate(@NonNull CallParticipantListUpdate update) { - pendingAdditions.addAll(update.getAdded()); - pendingAdditions.removeAll(update.getRemoved()); - - pendingRemovals.addAll(update.getRemoved()); - pendingRemovals.removeAll(update.getAdded()); - - if (!isShowing()) { - showPending(); - } - } - - public void setEnabled(boolean isEnabled) { - this.isEnabled = isEnabled; - - if (!isEnabled) { - dismiss(); - } - } - - @Override - public void onDestroy(@NonNull LifecycleOwner owner) { - handler.removeCallbacksAndMessages(null); - setOnDismissListener(null); - dismiss(); - } - - private void showPending() { - if (!pendingAdditions.isEmpty()) { - showAdditions(); - } else if (!pendingRemovals.isEmpty()) { - showRemovals(); - } - } - - private void showAdditions() { - setAvatar(getNextRecipient(pendingAdditions.iterator())); - setDescription(pendingAdditions, true); - pendingAdditions.clear(); - show(); - } - - private void showRemovals() { - setAvatar(getNextRecipient(pendingRemovals.iterator())); - setDescription(pendingRemovals, false); - pendingRemovals.clear(); - show(); - } - - private void show() { - if (!isEnabled || !parent.isAttachedToWindow()) { - return; - } - - showAtLocation(parent, Gravity.TOP | Gravity.START, 0, 0); - measureChild(); - update(); - handler.postDelayed(this::dismiss, DURATION); - } - - private void measureChild() { - getContentView().measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); - } - - private void setAvatar(@Nullable Recipient recipient) { - avatarImageView.setAvatarUsingProfile(recipient); - badgeImageView.setBadgeFromRecipient(recipient); - avatarImageView.setVisibility(recipient == null ? View.GONE : View.VISIBLE); - } - - private void setDescription(@NonNull Set wrappers, boolean isAdded) { - if (wrappers.isEmpty()) { - descriptionTextView.setText(""); - } else { - setDescriptionForRecipients(wrappers, isAdded); - } - } - - private void setDescriptionForRecipients(@NonNull Set recipients, boolean isAdded) { - descriptionTextView.setText(getDescriptionForRecipients(getContentView().getContext(), recipients, isAdded)); - } - - public static @NonNull String getDescriptionForRecipients(@NonNull Context context, @NonNull Set recipients, boolean isAdded) { - Iterator iterator = recipients.iterator(); - String description; - - switch (recipients.size()) { - case 0: - throw new IllegalArgumentException("Recipients must contain 1 or more entries"); - case 1: - description = context.getString(getOneMemberDescriptionResourceId(isAdded), getNextDisplayName(context, iterator)); - break; - case 2: - description = context.getString(getTwoMemberDescriptionResourceId(isAdded), getNextDisplayName(context, iterator), getNextDisplayName(context, iterator)); - break; - case 3: - description = context.getString(getThreeMemberDescriptionResourceId(isAdded), getNextDisplayName(context, iterator), getNextDisplayName(context, iterator), getNextDisplayName(context, iterator)); - break; - default: - description = context.getResources().getQuantityString(getManyMemberDescriptionResourceId(isAdded), recipients.size() - 2, getNextDisplayName(context, iterator), getNextDisplayName(context, iterator), recipients.size() - 2); - } - - return description; - } - - private @NonNull Recipient getNextRecipient(@NonNull Iterator wrapperIterator) { - return wrapperIterator.next().getCallParticipant().getRecipient(); - } - - private static @NonNull String getNextDisplayName(@NonNull Context context, @NonNull Iterator wrapperIterator) { - CallParticipantListUpdate.Wrapper wrapper = wrapperIterator.next(); - - return wrapper.getCallParticipant().getRecipientDisplayName(context); - } - - private static @StringRes int getOneMemberDescriptionResourceId(boolean isAdded) { - if (isAdded) { - return R.string.CallParticipantsListUpdatePopupWindow__s_joined; - } else { - return R.string.CallParticipantsListUpdatePopupWindow__s_left; - } - } - - private static @StringRes int getTwoMemberDescriptionResourceId(boolean isAdded) { - if (isAdded) { - return R.string.CallParticipantsListUpdatePopupWindow__s_and_s_joined; - } else { - return R.string.CallParticipantsListUpdatePopupWindow__s_and_s_left; - } - } - - private static @StringRes int getThreeMemberDescriptionResourceId(boolean isAdded) { - if (isAdded) { - return R.string.CallParticipantsListUpdatePopupWindow__s_s_and_s_joined; - } else { - return R.string.CallParticipantsListUpdatePopupWindow__s_s_and_s_left; - } - } - - private static @PluralsRes int getManyMemberDescriptionResourceId(boolean isAdded) { - if (isAdded) { - return R.plurals.CallParticipantsListUpdatePopupWindow__s_s_and_d_others_joined; - } else { - return R.plurals.CallParticipantsListUpdatePopupWindow__s_s_and_d_others_left; - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.kt index e47bd97313..9132a22542 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.kt @@ -19,7 +19,6 @@ import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.ringrtc.CameraState import org.thoughtcrime.securesms.service.webrtc.collections.ParticipantCollection import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState -import org.thoughtcrime.securesms.util.RemoteConfig import java.util.concurrent.TimeUnit /** @@ -83,9 +82,7 @@ data class CallParticipantsState( } else { listParticipants.addAll(remoteParticipants.listParticipants) } - if (foldableState.isFlat && !RemoteConfig.newCallUi) { - listParticipants.add(CallParticipant.EMPTY) - } + listParticipants.reverse() return listParticipants } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallReactionScrubber.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallReactionScrubber.kt deleted file mode 100644 index 4c456f6b26..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallReactionScrubber.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2023 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.webrtc - -import android.content.Context -import android.util.AttributeSet -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.content.ContextCompat -import androidx.fragment.app.FragmentManager -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.components.emoji.EmojiImageView -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment - -class CallReactionScrubber @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : ConstraintLayout(context, attrs, defStyleAttr) { - companion object { - const val CUSTOM_REACTION_BOTTOM_SHEET_TAG = "CallReaction" - - @JvmStatic - fun dismissCustomEmojiBottomSheet(fm: FragmentManager) { - val bottomSheet = fm.findFragmentByTag(CUSTOM_REACTION_BOTTOM_SHEET_TAG) as? ReactWithAnyEmojiBottomSheetDialogFragment - - bottomSheet?.dismissNow() - } - } - - private val emojiViews: Array - private var customEmojiIndex = 0 - - init { - inflate(context, R.layout.call_overflow_popup, this) - - emojiViews = arrayOf( - findViewById(R.id.reaction_1), - findViewById(R.id.reaction_2), - findViewById(R.id.reaction_3), - findViewById(R.id.reaction_4), - findViewById(R.id.reaction_5), - findViewById(R.id.reaction_6), - findViewById(R.id.reaction_7) - ) - customEmojiIndex = emojiViews.size - 1 - } - - fun initialize(fragmentManager: FragmentManager, listener: (String) -> Unit) { - val emojis = SignalStore.emoji.reactions - for (i in emojiViews.indices) { - val view = emojiViews[i] - val isAtCustomIndex = i == customEmojiIndex - if (isAtCustomIndex) { - view.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_any_emoji_32)) - view.setOnClickListener { - val bottomSheet = ReactWithAnyEmojiBottomSheetDialogFragment.createForCallingReactions() - bottomSheet.show(fragmentManager, CUSTOM_REACTION_BOTTOM_SHEET_TAG) - } - } else { - val preferredVariation = SignalStore.emoji.getPreferredVariation(emojis[i]) - view.setImageEmoji(preferredVariation) - view.setOnClickListener { listener(preferredVariation) } - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallStateUpdatePopupWindow.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallStateUpdatePopupWindow.kt deleted file mode 100644 index 8c1ae3cce9..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallStateUpdatePopupWindow.kt +++ /dev/null @@ -1,107 +0,0 @@ -package org.thoughtcrime.securesms.components.webrtc - -import android.view.Gravity -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.PopupWindow -import android.widget.TextView -import androidx.core.view.ViewCompat -import org.signal.core.util.dp -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.components.webrtc.v2.CallControlsChange -import org.thoughtcrime.securesms.util.Debouncer -import org.thoughtcrime.securesms.util.visible -import java.util.concurrent.TimeUnit - -/** - * Popup window which is displayed whenever the call state changes from user input. - */ -class CallStateUpdatePopupWindow(private val parent: ViewGroup) : PopupWindow( - LayoutInflater.from(parent.context).inflate(R.layout.call_state_update, parent, false), - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT -) { - - private var enabled: Boolean = true - private var pendingUpdate: CallControlsChange? = null - private var lastUpdate: CallControlsChange? = null - private val dismissDebouncer = Debouncer(2, TimeUnit.SECONDS) - private val iconView = contentView.findViewById(R.id.icon) - private val descriptionView = contentView.findViewById(R.id.description) - - init { - setOnDismissListener { - val pending = pendingUpdate - if (pending != null) { - onCallStateUpdate(pending) - } - } - - animationStyle = R.style.CallStateToastAnimation - } - - fun setEnabled(enabled: Boolean) { - this.enabled = enabled - if (!enabled) { - dismissDebouncer.clear() - dismiss() - } - } - - fun onCallStateUpdate(callControlsChange: CallControlsChange) { - if (isShowing && lastUpdate == callControlsChange) { - dismissDebouncer.publish { dismiss() } - } else if (isShowing) { - dismissDebouncer.clear() - pendingUpdate = callControlsChange - dismiss() - } else { - pendingUpdate = null - lastUpdate = callControlsChange - presentCallState(callControlsChange) - show() - } - } - - private fun presentCallState(callControlsChange: CallControlsChange) { - if (callControlsChange.iconRes == null) { - iconView.setImageDrawable(null) - } else { - iconView.setImageResource(callControlsChange.iconRes) - } - - iconView.visible = callControlsChange.iconRes != null - descriptionView.setText(callControlsChange.stringRes) - } - - private fun show() { - if (!enabled || parent.windowToken == null) { - return - } - - measureChild() - - val anchor: View = ViewCompat.requireViewById(parent, R.id.call_screen_above_controls_guideline) - val pill: View = ViewCompat.requireViewById(contentView, R.id.call_state_pill) - - // 54 is the top margin of the contentView (30) plus the desired padding (24) - showAtLocation( - parent, - Gravity.TOP or Gravity.START, - 0, - anchor.top - 54.dp - pill.measuredHeight - ) - - update() - dismissDebouncer.publish { dismiss() } - } - - private fun measureChild() { - contentView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) - ) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallToastPopupWindow.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallToastPopupWindow.java deleted file mode 100644 index 289893af1b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallToastPopupWindow.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.thoughtcrime.securesms.components.webrtc; - -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.PopupWindow; -import android.widget.TextView; - -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.util.ViewUtil; - -import java.util.concurrent.TimeUnit; - -/** - * Top screen toast to be shown to the user for 3 seconds. - * - * Currently hard coded to show specific text, but could be easily expanded to be customizable - * if desired. Based on {@link CallParticipantsListUpdatePopupWindow}. - */ -public class CallToastPopupWindow extends PopupWindow { - - private static final long DURATION = TimeUnit.SECONDS.toMillis(3); - - private final ViewGroup parent; - - public static void show(@NonNull ViewGroup viewGroup) { - CallToastPopupWindow toast = new CallToastPopupWindow(viewGroup); - toast.show(); - } - - public static void show(@NonNull ViewGroup viewGroup, @DrawableRes int iconId, @NonNull String description) { - CallToastPopupWindow toast = new CallToastPopupWindow(viewGroup); - - TextView text = toast.getContentView().findViewById(R.id.description); - text.setText(description); - text.setCompoundDrawablesRelativeWithIntrinsicBounds(iconId, 0, 0, 0); - toast.show(); - } - - private CallToastPopupWindow(@NonNull ViewGroup parent) { - super(LayoutInflater.from(parent.getContext()).inflate(R.layout.call_toast_popup_window, parent, false), - ViewGroup.LayoutParams.MATCH_PARENT, - ViewUtil.dpToPx(94)); - - this.parent = parent; - - setAnimationStyle(R.style.PopupAnimation); - } - - public void show() { - showAtLocation(parent, Gravity.TOP | Gravity.START, 0, 0); - measureChild(); - update(); - getContentView().postDelayed(this::dismiss, DURATION); - } - - private void measureChild() { - getContentView().measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/EglBaseWrapper.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/EglBaseWrapper.kt index 1bb98d98f3..7a3f72e14e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/EglBaseWrapper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/EglBaseWrapper.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components.webrtc import android.opengl.EGL14 import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper.Companion.acquireEglBase +import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper.Companion.releaseEglBase import org.webrtc.EglBase import org.webrtc.EglBase10 import org.webrtc.EglBase14 diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/LayoutPositions.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/LayoutPositions.kt deleted file mode 100644 index 289ca65a3d..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/LayoutPositions.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2023 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ -package org.thoughtcrime.securesms.components.webrtc - -import androidx.annotation.Dimension -import androidx.annotation.IdRes -import androidx.constraintlayout.widget.ConstraintSet -import org.thoughtcrime.securesms.R - -/** Constraints to apply for different call sizes */ -enum class LayoutPositions( - @JvmField @IdRes val participantBottomViewId: Int, - @JvmField @Dimension val participantBottomMargin: Int, - @JvmField @IdRes val reactionBottomViewId: Int, - @JvmField @Dimension val reactionBottomMargin: Int -) { - /** 1:1 or small calls anchor full screen or controls */ - SMALL_GROUP( - participantBottomViewId = ConstraintSet.PARENT_ID, - participantBottomMargin = 0, - reactionBottomViewId = R.id.call_screen_pending_recipients, - reactionBottomMargin = 8 - ), - - /** Large calls have a participant rail to anchor to */ - LARGE_GROUP( - participantBottomViewId = R.id.call_screen_participants_recycler, - participantBottomMargin = 16, - reactionBottomViewId = R.id.call_screen_pending_recipients, - reactionBottomMargin = 20 - ); - - @JvmField - val participantBottomViewEndSide: Int = if (participantBottomViewId == ConstraintSet.PARENT_ID) ConstraintSet.BOTTOM else ConstraintSet.TOP -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PercentFrameLayout.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PercentFrameLayout.java deleted file mode 100644 index 2a4cca8589..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PercentFrameLayout.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2015 The WebRTC Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ - -package org.thoughtcrime.securesms.components.webrtc; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; - -/** - * Simple container that confines the children to a subrectangle specified as percentage values of - * the container size. The children are centered horizontally and vertically inside the confined - * space. - */ -public class PercentFrameLayout extends ViewGroup { - private int xPercent = 0; - private int yPercent = 0; - private int widthPercent = 100; - private int heightPercent = 100; - - private boolean square = false; - private boolean hidden = false; - - public PercentFrameLayout(Context context) { - super(context); - } - - public PercentFrameLayout(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public PercentFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public void setSquare(boolean square) { - this.square = square; - } - - public void setHidden(boolean hidden) { - this.hidden = hidden; - } - - public boolean isHidden() { - return this.hidden; - } - - public void setPosition(int xPercent, int yPercent, int widthPercent, int heightPercent) { - this.xPercent = xPercent; - this.yPercent = yPercent; - this.widthPercent = widthPercent; - this.heightPercent = heightPercent; - } - - @Override - public boolean shouldDelayChildPressedState() { - return false; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - final int width = getDefaultSize(Integer.MAX_VALUE, widthMeasureSpec); - final int height = getDefaultSize(Integer.MAX_VALUE, heightMeasureSpec); - - setMeasuredDimension(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); - - int childWidth = width * widthPercent / 100; - int childHeight = height * heightPercent / 100; - - if (square) { - if (width > height) childWidth = childHeight; - else childHeight = childWidth; - } - - if (hidden) { - childWidth = 1; - childHeight = 1; - } - - int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST); - int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST); - - for (int i = 0; i < getChildCount(); ++i) { - final View child = getChildAt(i); - if (child.getVisibility() != GONE) { - child.measure(childWidthMeasureSpec, childHeightMeasureSpec); - } - } - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - final int width = right - left; - final int height = bottom - top; - // Sub-rectangle specified by percentage values. - final int subWidth = width * widthPercent / 100; - final int subHeight = height * heightPercent / 100; - final int subLeft = left + width * xPercent / 100; - final int subTop = top + height * yPercent / 100; - - - for (int i = 0; i < getChildCount(); ++i) { - final View child = getChildAt(i); - if (child.getVisibility() != GONE) { - final int childWidth = child.getMeasuredWidth(); - final int childHeight = child.getMeasuredHeight(); - // Center child both vertically and horizontally. - int childLeft = subLeft + (subWidth - childWidth) / 2; - int childTop = subTop + (subHeight - childHeight) / 2; - - if (hidden) { - childLeft = 0; - childTop = 0; - } - - child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureExpansionHelper.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureExpansionHelper.java deleted file mode 100644 index 94fb3ccd65..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureExpansionHelper.java +++ /dev/null @@ -1,205 +0,0 @@ -package org.thoughtcrime.securesms.components.webrtc; - -import android.graphics.Point; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.MainThread; -import androidx.annotation.NonNull; -import androidx.transition.AutoTransition; -import androidx.transition.Transition; -import androidx.transition.TransitionListenerAdapter; -import androidx.transition.TransitionManager; - -import org.thoughtcrime.securesms.util.ViewUtil; - -/** - * Helps manage the expansion and shrinking of the in-app pip. - */ -@MainThread -final class PictureInPictureExpansionHelper { - - private static final int PIP_RESIZE_DURATION_MS = 300; - private static final int EXPANDED_PIP_WIDTH_DP = 170; - private static final int EXPANDED_PIP_HEIGHT_DP = 300; - - public static final int NORMAL_PIP_WIDTH_DP = 90; - public static final int NORMAL_PIP_HEIGHT_DP = 160; - - public static final int MINI_PIP_WIDTH_DP = 40; - public static final int MINI_PIP_HEIGHT_DP = 72; - - private final View selfPip; - private final ViewGroup parent; - - private State state = State.IS_SHRUNKEN; - private Point defaultDimensions; - private Point expandedDimensions; - - private final OnStateChangedListener onStateChangedListener; - - public PictureInPictureExpansionHelper(@NonNull View selfPip, @NonNull OnStateChangedListener onStateChangedListener) { - this.selfPip = selfPip; - this.parent = (ViewGroup) selfPip.getParent(); - this.defaultDimensions = new Point(selfPip.getLayoutParams().width, selfPip.getLayoutParams().height); - this.expandedDimensions = new Point(ViewUtil.dpToPx(EXPANDED_PIP_WIDTH_DP), ViewUtil.dpToPx(EXPANDED_PIP_HEIGHT_DP)); - this.onStateChangedListener = onStateChangedListener; - } - - public boolean isExpandedOrExpanding() { - return state == State.IS_EXPANDED || state == State.IS_EXPANDING; - } - - public boolean isShrunkenOrShrinking() { - return state == State.IS_SHRUNKEN || state == State.IS_SHRINKING; - } - - public boolean isMiniSize() { - return defaultDimensions.x < ViewUtil.dpToPx(NORMAL_PIP_WIDTH_DP); - } - - public void startExpandedSizeTransition(@NonNull Point dimensions, @NonNull Callback callback) { - if (defaultDimensions.equals(dimensions)) { - return; - } - - defaultDimensions = dimensions; - - int x = (dimensions.x > dimensions.y) ? EXPANDED_PIP_HEIGHT_DP : EXPANDED_PIP_WIDTH_DP; - int y = (dimensions.x > dimensions.y) ? EXPANDED_PIP_WIDTH_DP : EXPANDED_PIP_HEIGHT_DP; - - expandedDimensions = new Point(ViewUtil.dpToPx(x), ViewUtil.dpToPx(y)); - - if (isExpandedOrExpanding()) { - return; - } - - beginResizeSelfPipTransition(expandedDimensions, new Callback() { - @Override - public void onAnimationWillStart() { - setState(State.IS_EXPANDING); - callback.onAnimationWillStart(); - } - - @Override - public void onAnimationHasFinished() { - setState(State.IS_EXPANDED); - callback.onAnimationHasFinished(); - } - }); - } - - public void startDefaultSizeTransition(@NonNull Point dimensions, @NonNull Callback callback) { - if (defaultDimensions.equals(dimensions)) { - return; - } - - defaultDimensions = dimensions; - - int x = (dimensions.x > dimensions.y) ? EXPANDED_PIP_HEIGHT_DP : EXPANDED_PIP_WIDTH_DP; - int y = (dimensions.x > dimensions.y) ? EXPANDED_PIP_WIDTH_DP : EXPANDED_PIP_HEIGHT_DP; - - expandedDimensions = new Point(ViewUtil.dpToPx(x), ViewUtil.dpToPx(y)); - - if (isExpandedOrExpanding()) { - return; - } - - beginResizeSelfPipTransition(defaultDimensions, callback); - } - - public void beginExpandTransition() { - if (isExpandedOrExpanding()) { - return; - } - - beginResizeSelfPipTransition(expandedDimensions, new Callback() { - @Override - public void onAnimationWillStart() { - setState(State.IS_EXPANDING); - } - - @Override - public void onAnimationHasFinished() { - setState(State.IS_EXPANDED); - } - }); - } - - public void beginShrinkTransition() { - if (isShrunkenOrShrinking()) { - return; - } - - beginResizeSelfPipTransition(defaultDimensions, new Callback() { - @Override - public void onAnimationWillStart() { - setState(State.IS_SHRINKING); - } - - @Override - public void onAnimationHasFinished() { - setState(State.IS_SHRUNKEN); - } - }); - } - - private void beginResizeSelfPipTransition(@NonNull Point dimension, @NonNull Callback callback) { - TransitionManager.endTransitions(parent); - - Transition transition = new AutoTransition().setDuration(PIP_RESIZE_DURATION_MS); - transition.addListener(new TransitionListenerAdapter() { - @Override - public void onTransitionStart(@NonNull Transition transition) { - callback.onAnimationWillStart(); - } - - @Override - public void onTransitionEnd(@NonNull Transition transition) { - callback.onAnimationHasFinished(); - } - }); - - TransitionManager.beginDelayedTransition(parent, transition); - - ViewGroup.LayoutParams params = selfPip.getLayoutParams(); - - params.width = dimension.x; - params.height = dimension.y; - - selfPip.setLayoutParams(params); - } - - private void setState(@NonNull State state) { - this.state = state; - - if (onStateChangedListener != null) { - onStateChangedListener.onStateChanged(state); - } - } - - enum State { - IS_EXPANDING, - IS_EXPANDED, - IS_SHRINKING, - IS_SHRUNKEN - } - - interface OnStateChangedListener { - void onStateChanged(@NonNull State state); - } - - public interface Callback { - /** - * Called when an animation (shrink or expand) will begin. This happens before any animation - * is executed. - */ - default void onAnimationWillStart() {} - - /** - * Called when the animation is complete. Useful for e.g. adjusting the pip's final location to - * make sure it is respecting the screen space available. - */ - default void onAnimationHasFinished() {} - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureGestureHelper.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureGestureHelper.java deleted file mode 100644 index 5b28fd5243..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureGestureHelper.java +++ /dev/null @@ -1,449 +0,0 @@ -package org.thoughtcrime.securesms.components.webrtc; - -import android.annotation.SuppressLint; -import android.graphics.Point; -import android.view.GestureDetector; -import android.view.Gravity; -import android.view.MotionEvent; -import android.view.VelocityTracker; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.view.animation.AccelerateDecelerateInterpolator; -import android.view.animation.Interpolator; -import android.widget.FrameLayout; - -import androidx.annotation.NonNull; -import androidx.core.view.GestureDetectorCompat; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.util.ViewUtil; -import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout; - -import java.util.Arrays; - -public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestureListener { - - private static final float DECELERATION_RATE = 0.99f; - private static final Interpolator FLING_INTERPOLATOR = new ViscousFluidInterpolator(); - private static final Interpolator ADJUST_INTERPOLATOR = new AccelerateDecelerateInterpolator(); - - private final ViewGroup parent; - private final View child; - private final int framePadding; - - private int pipWidth; - private int pipHeight; - private int activePointerId = MotionEvent.INVALID_POINTER_ID; - private float lastTouchX; - private float lastTouchY; - private int extraPaddingTop; - private int extraPaddingBottom; - private double projectionX; - private double projectionY; - private VelocityTracker velocityTracker; - private int maximumFlingVelocity; - private boolean isLockedToBottomEnd; - private Interpolator interpolator; - private Corner currentCornerPosition = Corner.BOTTOM_RIGHT; - private int previousTopBoundary = -1; - private int expandedVerticalBoundary = -1; - private int collapsedVerticalBoundary = -1; - private BoundaryState boundaryState = BoundaryState.EXPANDED; - private boolean isCollapsedStateAllowed = false; - - @SuppressLint("ClickableViewAccessibility") - public static PictureInPictureGestureHelper applyTo(@NonNull View child) { - TouchInterceptingFrameLayout parent = (TouchInterceptingFrameLayout) child.getParent(); - PictureInPictureGestureHelper helper = new PictureInPictureGestureHelper(parent, child); - GestureDetectorCompat gestureDetector = new GestureDetectorCompat(child.getContext(), helper); - - parent.setOnInterceptTouchEventListener((event) -> { - final int action = event.getAction(); - final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; - - if (pointerIndex > 0) { - return false; - } - - if (helper.velocityTracker == null) { - helper.velocityTracker = VelocityTracker.obtain(); - } - - helper.velocityTracker.addMovement(event); - - return false; - }); - - parent.setOnTouchListener((v, event) -> { - if (helper.velocityTracker != null) { - helper.velocityTracker.recycle(); - helper.velocityTracker = null; - } - - return false; - }); - - child.setOnTouchListener((v, event) -> { - boolean handled = gestureDetector.onTouchEvent(event); - - if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMasked() == MotionEvent.ACTION_CANCEL) { - if (!handled) { - handled = helper.onGestureFinished(event); - } - - if (helper.velocityTracker != null) { - helper.velocityTracker.recycle(); - helper.velocityTracker = null; - } - } - - return handled; - }); - - return helper; - } - - private PictureInPictureGestureHelper(@NonNull ViewGroup parent, @NonNull View child) { - this.parent = parent; - this.child = child; - this.framePadding = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_frame_padding); - this.pipWidth = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_pip_width); - this.pipHeight = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_pip_height); - this.maximumFlingVelocity = ViewConfiguration.get(child.getContext()).getScaledMaximumFlingVelocity(); - this.interpolator = ADJUST_INTERPOLATOR; - } - - public void setTopVerticalBoundary(int topBoundary) { - if (topBoundary == previousTopBoundary) { - return; - } - previousTopBoundary = topBoundary; - - extraPaddingTop = topBoundary - parent.getTop(); - - ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) child.getLayoutParams(); - layoutParams.setMargins(layoutParams.leftMargin, extraPaddingTop + framePadding, layoutParams.rightMargin, layoutParams.bottomMargin); - child.setLayoutParams(layoutParams); - } - - public void setCollapsedVerticalBoundary(int bottomBoundary) { - final int oldBoundary = collapsedVerticalBoundary; - collapsedVerticalBoundary = bottomBoundary; - - if (oldBoundary != bottomBoundary && boundaryState == BoundaryState.COLLAPSED) { - applyBottomVerticalBoundary(bottomBoundary); - } - } - - public void setExpandedVerticalBoundary(int bottomBoundary) { - final int oldBoundary = expandedVerticalBoundary; - expandedVerticalBoundary = bottomBoundary; - - if (oldBoundary != bottomBoundary && boundaryState == BoundaryState.EXPANDED) { - applyBottomVerticalBoundary(bottomBoundary); - } - } - - public void setBoundaryState(@NonNull BoundaryState boundaryState) { - if (!isCollapsedStateAllowed && boundaryState == BoundaryState.COLLAPSED) { - return; - } - - final BoundaryState old = this.boundaryState; - this.boundaryState = boundaryState; - if (old != boundaryState) { - applyBottomVerticalBoundary(boundaryState == BoundaryState.EXPANDED ? expandedVerticalBoundary : collapsedVerticalBoundary); - } - } - - public void allowCollapsedState() { - if (isCollapsedStateAllowed) { - return; - } - - isCollapsedStateAllowed = true; - setBoundaryState(BoundaryState.COLLAPSED); - } - - private void applyBottomVerticalBoundary(int bottomBoundary) { - extraPaddingBottom = parent.getMeasuredHeight() + parent.getTop() - bottomBoundary; - ViewUtil.setBottomMargin(child, extraPaddingBottom + framePadding); - } - - private boolean onGestureFinished(MotionEvent e) { - final int pointerIndex = e.findPointerIndex(activePointerId); - - if (e.getActionIndex() == pointerIndex) { - onFling(e, e, 0, 0); - return true; - } - - return false; - } - - public void lockToBottomEnd() { - isLockedToBottomEnd = true; - fling(); - } - - public void enableCorners() { - isLockedToBottomEnd = false; - } - - @Override - public boolean onDown(MotionEvent e) { - activePointerId = e.getPointerId(0); - lastTouchX = e.getX(0) + child.getX(); - lastTouchY = e.getY(0) + child.getY(); - pipWidth = child.getMeasuredWidth(); - pipHeight = child.getMeasuredHeight(); - interpolator = FLING_INTERPOLATOR; - - return true; - } - - @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { - if (isLockedToBottomEnd) { - return false; - } - - int pointerIndex = e2.findPointerIndex(activePointerId); - - if (pointerIndex == -1) { - fling(); - return false; - } - - float x = e2.getX(pointerIndex) + child.getX(); - float y = e2.getY(pointerIndex) + child.getY(); - float dx = x - lastTouchX; - float dy = y - lastTouchY; - - child.setTranslationX(child.getTranslationX() + dx); - child.setTranslationY(child.getTranslationY() + dy); - - lastTouchX = x; - lastTouchY = y; - - return true; - } - - @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { - if (isLockedToBottomEnd) { - return false; - } - - if (velocityTracker != null) { - velocityTracker.computeCurrentVelocity(1000, maximumFlingVelocity); - - projectionX = child.getX() + project(velocityTracker.getXVelocity()); - projectionY = child.getY() + project(velocityTracker.getYVelocity()); - } else { - projectionX = child.getX(); - projectionY = child.getY(); - } - - fling(); - - return true; - } - - @Override - public boolean onSingleTapUp(MotionEvent e) { - child.performClick(); - return true; - } - - private void fling() { - Point projection = new Point((int) projectionX, (int) projectionY); - Corner nearestCornerPosition = findNearestCornerPosition(projection); - - FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) child.getLayoutParams(); - layoutParams.gravity = nearestCornerPosition.gravity; - - if (currentCornerPosition != null && currentCornerPosition != nearestCornerPosition) { - adjustTranslationFrameOfReference(child, currentCornerPosition, nearestCornerPosition); - } - currentCornerPosition = nearestCornerPosition; - - child.setLayoutParams(layoutParams); - - child.animate() - .translationX(0) - .translationY(0) - .setDuration(250) - .setInterpolator(interpolator) - .start(); - } - - private Corner findNearestCornerPosition(Point projection) { - if (isLockedToBottomEnd) { - return ViewUtil.isLtr(parent) ? Corner.BOTTOM_RIGHT - : Corner.BOTTOM_LEFT; - } - - CornerPoint maxPoint = null; - double maxDistance = Double.MAX_VALUE; - - for (CornerPoint cornerPoint : Arrays.asList(calculateTopLeftCoordinates(), - calculateTopRightCoordinates(parent), - calculateBottomLeftCoordinates(parent), - calculateBottomRightCoordinates(parent))) { - double distance = distance(cornerPoint.point, projection); - - if (distance < maxDistance) { - maxDistance = distance; - maxPoint = cornerPoint; - } - } - - //noinspection DataFlowIssue - return maxPoint.corner; - } - - private CornerPoint calculateTopLeftCoordinates() { - return new CornerPoint(new Point(framePadding, framePadding + extraPaddingTop), - Corner.TOP_LEFT); - } - - private CornerPoint calculateTopRightCoordinates(@NonNull ViewGroup parent) { - return new CornerPoint(new Point(parent.getMeasuredWidth() - pipWidth - framePadding, framePadding + extraPaddingTop), - Corner.TOP_RIGHT); - } - - private CornerPoint calculateBottomLeftCoordinates(@NonNull ViewGroup parent) { - return new CornerPoint(new Point(framePadding, parent.getMeasuredHeight() - pipHeight - framePadding - extraPaddingBottom), - Corner.BOTTOM_LEFT); - } - - private CornerPoint calculateBottomRightCoordinates(@NonNull ViewGroup parent) { - return new CornerPoint(new Point(parent.getMeasuredWidth() - pipWidth - framePadding, parent.getMeasuredHeight() - pipHeight - framePadding - extraPaddingBottom), - Corner.BOTTOM_RIGHT); - } - - private static float project(float initialVelocity) { - return (initialVelocity / 1000f) * DECELERATION_RATE / (1f - DECELERATION_RATE); - } - - private static double distance(Point a, Point b) { - return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)); - } - - - /** - * User drag is implemented by translating the view from the current gravity anchor (corner). When the user drags - * to a new corner, we need to adjust the translations for the new corner so the animation of translation X/Y to 0 - * works correctly. - * - * For example, if in bottom right and need to move to top right, we need to calculate a new translation Y since instead - * of being translated up from bottom it's translated down from the top. - */ - private void adjustTranslationFrameOfReference(@NonNull View child, @NonNull Corner previous, @NonNull Corner next) { - TouchInterceptingFrameLayout parent = (TouchInterceptingFrameLayout) child.getParent(); - FrameLayout.LayoutParams childLayoutParams = (FrameLayout.LayoutParams) child.getLayoutParams(); - int parentWidth = parent.getWidth(); - int parentHeight = parent.getHeight(); - - if (previous.topHalf != next.topHalf) { - int childHeight = childLayoutParams.height + childLayoutParams.topMargin + childLayoutParams.bottomMargin; - - float adjustedTranslationY; - if (previous.topHalf) { - adjustedTranslationY = -(parentHeight - child.getTranslationY() - childHeight); - } else { - adjustedTranslationY = parentHeight + child.getTranslationY() - childHeight; - } - child.setTranslationY(adjustedTranslationY); - } - - if (previous.leftSide != next.leftSide) { - int childWidth = childLayoutParams.width + childLayoutParams.leftMargin + childLayoutParams.rightMargin; - - float adjustedTranslationX; - if (previous.leftSide) { - adjustedTranslationX = -(parentWidth - child.getTranslationX() - childWidth); - } else { - adjustedTranslationX = parentWidth + child.getTranslationX() - childWidth; - } - child.setTranslationX(adjustedTranslationX); - } - } - - private static class CornerPoint { - final Point point; - final Corner corner; - - public CornerPoint(@NonNull Point point, @NonNull Corner corner) { - this.point = point; - this.corner = corner; - } - } - - @SuppressLint("RtlHardcoded") - private enum Corner { - TOP_LEFT(Gravity.TOP | Gravity.LEFT, true, true), - TOP_RIGHT(Gravity.TOP | Gravity.RIGHT, false, true), - BOTTOM_LEFT(Gravity.BOTTOM | Gravity.LEFT, true, false), - BOTTOM_RIGHT(Gravity.BOTTOM | Gravity.RIGHT, false, false); - - final int gravity; - final boolean leftSide; - final boolean topHalf; - - Corner(int gravity, boolean leftSide, boolean topHalf) { - this.gravity = gravity; - this.leftSide = leftSide; - this.topHalf = topHalf; - } - } - - /** - * Borrowed from ScrollView - */ - private static class ViscousFluidInterpolator implements Interpolator { - /** - * Controls the viscous fluid effect (how much of it). - */ - private static final float VISCOUS_FLUID_SCALE = 8.0f; - - private static final float VISCOUS_FLUID_NORMALIZE; - private static final float VISCOUS_FLUID_OFFSET; - - static { - - // must be set to 1.0 (used in viscousFluid()) - VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f); - // account for very small floating-point error - VISCOUS_FLUID_OFFSET = 1.0f - VISCOUS_FLUID_NORMALIZE * viscousFluid(1.0f); - } - - private static float viscousFluid(float x) { - x *= VISCOUS_FLUID_SCALE; - if (x < 1.0f) { - x -= (1.0f - (float) Math.exp(-x)); - } else { - float start = 0.36787944117f; // 1/e == exp(-1) - x = 1.0f - (float) Math.exp(1.0f - x); - x = start + x * (1.0f - start); - } - return x; - } - - @Override - public float getInterpolation(float input) { - final float interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input); - if (interpolated > 0) { - return interpolated + VISCOUS_FLUID_OFFSET; - } - return interpolated; - } - } - - public enum BoundaryState { - EXPANDED, - COLLAPSED - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/SlideUpWithCallControlsBehavior.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/SlideUpWithCallControlsBehavior.kt deleted file mode 100644 index 369599c1ab..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/SlideUpWithCallControlsBehavior.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.webrtc - -import android.content.Context -import android.util.AttributeSet -import android.view.View -import androidx.constraintlayout.widget.Barrier -import androidx.coordinatorlayout.widget.CoordinatorLayout -import com.google.android.material.bottomsheet.BottomSheetBehavior -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.util.views.SlideUpWithDependencyBehavior -import kotlin.math.max - -/** - * Coordinator Layout Behavior which allows us to "pin" UI Elements to the top of the controls sheet. - */ -class SlideUpWithCallControlsBehavior( - context: Context, - attributeSet: AttributeSet? -) : SlideUpWithDependencyBehavior(context, attributeSet, offsetY = 0f) { - - private var minTranslationY: Float = 0f - - var onTopOfControlsChangedListener: OnTopOfControlsChangedListener? = null - - override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean { - super.onDependentViewChanged(parent, child, dependency) - - val bottomSheetBehavior = (dependency.layoutParams as CoordinatorLayout.LayoutParams).behavior as BottomSheetBehavior<*> - val slideOffset = bottomSheetBehavior.calculateSlideOffset() - if (slideOffset == 0f) { - minTranslationY = child.translationY - } else { - child.translationY = max(child.translationY, minTranslationY) - } - - emitViewChanged(child) - return true - } - - override fun onLayoutChild(parent: CoordinatorLayout, child: View, layoutDirection: Int): Boolean { - emitViewChanged(child) - return false - } - - override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean { - return dependency.id == R.id.call_controls_info_parent - } - - private fun emitViewChanged(child: View) { - val barrier = child.findViewById(R.id.call_screen_above_controls_barrier) - onTopOfControlsChangedListener?.onTopOfControlsChanged(barrier.bottom + child.translationY.toInt()) - } - - interface OnTopOfControlsChangedListener { - fun onTopOfControlsChanged(topOfControls: Int) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/ToggleButtonOutputState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/ToggleButtonOutputState.kt index 5f3d028949..45129be498 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/ToggleButtonOutputState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/ToggleButtonOutputState.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.setValue import kotlin.math.min /** - * This holds UI state for [WebRtcAudioOutputToggleButton] + * This holds UI state for [org.thoughtcrime.securesms.components.webrtc.v2.CallAudioToggleButton] */ class ToggleButtonOutputState { private val availableOutputs: LinkedHashSet = linkedSetOf(WebRtcAudioOutput.SPEAKER) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioOutputToggleButton.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioOutputToggleButton.kt deleted file mode 100644 index 005251cf47..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcAudioOutputToggleButton.kt +++ /dev/null @@ -1,172 +0,0 @@ -package org.thoughtcrime.securesms.components.webrtc - -import android.content.Context -import android.content.ContextWrapper -import android.content.DialogInterface -import android.os.Build -import android.os.Bundle -import android.os.Parcelable -import android.util.AttributeSet -import android.view.View.OnClickListener -import android.widget.Toast -import androidx.annotation.RequiresApi -import androidx.appcompat.widget.AppCompatImageView -import androidx.fragment.app.FragmentActivity -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.R - -/** - * A UI button that triggers a picker dialog/bottom sheet allowing the user to select the audio output for the ongoing call. - */ -class WebRtcAudioOutputToggleButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : AppCompatImageView(context, attrs, defStyleAttr), AudioStateUpdater { - private val TAG = Log.tag(WebRtcAudioOutputToggleButton::class.java) - - private var outputState: ToggleButtonOutputState = ToggleButtonOutputState() - - private var audioOutputChangedListener: OnAudioOutputChangedListener = OnAudioOutputChangedListener { Log.e(TAG, "Attempted to call audioOutputChangedListenerLegacy without initializing!") } - private var picker: DialogInterface? = null - - private val clickListenerLegacy: OnClickListener = OnClickListener { - if (picker != null) { - Log.d(TAG, "Tried to launch new audio device picker but one is already present.") - return@OnClickListener - } - - val outputs = outputState.getOutputs() - if (outputs.size >= SHOW_PICKER_THRESHOLD || !outputState.isEarpieceAvailable) { - picker = WebRtcAudioPickerLegacy(audioOutputChangedListener, outputState, this).showPicker(context, outputs) { picker = null } - } else { - val audioOutput = outputState.peekNext() - audioOutputChangedListener.audioOutputChanged(WebRtcAudioDevice(audioOutput, null)) - updateAudioOutputState(audioOutput) - } - } - - @RequiresApi(31) - private val clickListener31 = OnClickListener { - if (picker != null) { - Log.d(TAG, "Tried to launch new audio device picker but one is already present.") - return@OnClickListener - } - - val fragmentActivity = context.fragmentActivity() - if (fragmentActivity != null) { - picker = WebRtcAudioPicker31(audioOutputChangedListener, outputState, this).showPicker(fragmentActivity, SHOW_PICKER_THRESHOLD) { picker = null } - } else { - Log.e(TAG, "WebRtcAudioOutputToggleButton instantiated from a context that does not inherit from FragmentActivity.") - Toast.makeText(context, R.string.WebRtcAudioOutputToggleButton_fragment_activity_error, Toast.LENGTH_LONG).show() - } - } - - init { - super.setOnClickListener( - if (Build.VERSION.SDK_INT >= 31) { - clickListener31 - } else { - clickListenerLegacy - } - ) - } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - hidePicker() - } - - /** - * DO NOT REMOVE senseless comparison suppression. - * Somehow, through XML inflation (reflection?), [outputState] can actually be null, - * even though the compiler disagrees. - * */ - override fun onCreateDrawableState(extraSpace: Int): IntArray { - @Suppress("SENSELESS_COMPARISON") - if (outputState == null) { - return super.onCreateDrawableState(extraSpace) - } - - val currentOutput = outputState.getCurrentOutput() - - val shouldShowDropdownForSpeaker = outputState.getOutputs().size >= SHOW_PICKER_THRESHOLD || !outputState.getOutputs().contains(WebRtcAudioOutput.HANDSET) - val extra = when (currentOutput) { - WebRtcAudioOutput.HANDSET -> intArrayOf(R.attr.state_speaker_off) - WebRtcAudioOutput.SPEAKER -> if (shouldShowDropdownForSpeaker) intArrayOf(R.attr.state_speaker_selected) else intArrayOf(R.attr.state_speaker_on) - WebRtcAudioOutput.BLUETOOTH_HEADSET -> intArrayOf(R.attr.state_bt_headset_selected) - WebRtcAudioOutput.WIRED_HEADSET -> intArrayOf(R.attr.state_wired_headset_selected) - } - - Log.d(TAG, "Switching button drawable to $currentOutput") - - val drawableState = super.onCreateDrawableState(extraSpace + extra.size) - mergeDrawableStates(drawableState, extra) - return drawableState - } - - override fun setOnClickListener(l: OnClickListener?) { - throw UnsupportedOperationException("This View does not support custom click listeners.") - } - - fun setControlAvailability(isEarpieceAvailable: Boolean, isBluetoothHeadsetAvailable: Boolean, isHeadsetAvailable: Boolean) { - outputState.isEarpieceAvailable = isEarpieceAvailable - outputState.isBluetoothHeadsetAvailable = isBluetoothHeadsetAvailable - outputState.isWiredHeadsetAvailable = isHeadsetAvailable - refreshDrawableState() - } - - override fun updateAudioOutputState(audioOutput: WebRtcAudioOutput) { - val oldOutput = outputState.getCurrentOutput() - if (oldOutput != audioOutput) { - outputState.setCurrentOutput(audioOutput) - refreshDrawableState() - } - } - - fun setOnAudioOutputChangedListener(listener: OnAudioOutputChangedListener) { - audioOutputChangedListener = listener - } - - override fun onSaveInstanceState(): Parcelable { - val parentState = super.onSaveInstanceState() - val bundle = Bundle() - bundle.putParcelable(STATE_PARENT, parentState) - bundle.putInt(STATE_OUTPUT_INDEX, outputState.getBackingIndexForBackup()) - bundle.putBoolean(STATE_HEADSET_ENABLED, outputState.isBluetoothHeadsetAvailable) - bundle.putBoolean(STATE_HANDSET_ENABLED, outputState.isEarpieceAvailable) - return bundle - } - - override fun onRestoreInstanceState(state: Parcelable) { - if (state is Bundle) { - outputState.isBluetoothHeadsetAvailable = state.getBoolean(STATE_HEADSET_ENABLED) - outputState.isEarpieceAvailable = state.getBoolean(STATE_HANDSET_ENABLED) - outputState.setBackingIndexForRestore(state.getInt(STATE_OUTPUT_INDEX)) - refreshDrawableState() - super.onRestoreInstanceState(state.getParcelable(STATE_PARENT)) - } else { - super.onRestoreInstanceState(state) - } - } - - override fun hidePicker() { - try { - picker?.dismiss() - } catch (e: IllegalStateException) { - Log.w(TAG, "Picker is not attached to a window.") - } - - picker = null - } - - companion object { - const val SHOW_PICKER_THRESHOLD = 3 - - private const val STATE_OUTPUT_INDEX = "audio.output.toggle.state.output.index" - private const val STATE_HEADSET_ENABLED = "audio.output.toggle.state.headset.enabled" - private const val STATE_HANDSET_ENABLED = "audio.output.toggle.state.handset.enabled" - private const val STATE_PARENT = "audio.output.toggle.state.parent" - - private tailrec fun Context.fragmentActivity(): FragmentActivity? = when (this) { - is FragmentActivity -> this - else -> (this as? ContextWrapper)?.baseContext?.fragmentActivity() - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPage.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPage.java deleted file mode 100644 index af82bfd15d..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPage.java +++ /dev/null @@ -1,111 +0,0 @@ -package org.thoughtcrime.securesms.components.webrtc; - -import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.events.CallParticipant; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -class WebRtcCallParticipantsPage { - - private final List callParticipants; - private final CallParticipant focusedParticipant; - private final boolean isSpeaker; - private final boolean isRenderInPip; - private final boolean isPortrait; - private final boolean isLandscapeEnabled; - private final boolean hideAvatar; - private final int navBarBottomInset; - - static WebRtcCallParticipantsPage forMultipleParticipants(@NonNull List callParticipants, - @NonNull CallParticipant focusedParticipant, - boolean isRenderInPip, - boolean isPortrait, - boolean isLandscapeEnabled, - boolean hideAvatar, - int navBarBottomInset) - { - return new WebRtcCallParticipantsPage(callParticipants, focusedParticipant, false, isRenderInPip, isPortrait, isLandscapeEnabled, hideAvatar, navBarBottomInset); - } - - static WebRtcCallParticipantsPage forSingleParticipant(@NonNull CallParticipant singleParticipant, - boolean isRenderInPip, - boolean isPortrait, - boolean isLandscapeEnabled) - { - return new WebRtcCallParticipantsPage(Collections.singletonList(singleParticipant), singleParticipant, true, isRenderInPip, isPortrait, isLandscapeEnabled, false, 0); - } - - private WebRtcCallParticipantsPage(@NonNull List callParticipants, - @NonNull CallParticipant focusedParticipant, - boolean isSpeaker, - boolean isRenderInPip, - boolean isPortrait, - boolean isLandscapeEnabled, - boolean hideAvatar, - int navBarBottomInset) - { - this.callParticipants = callParticipants; - this.focusedParticipant = focusedParticipant; - this.isSpeaker = isSpeaker; - this.isRenderInPip = isRenderInPip; - this.isPortrait = isPortrait; - this.isLandscapeEnabled = isLandscapeEnabled; - this.hideAvatar = hideAvatar; - this.navBarBottomInset = navBarBottomInset; - } - - public @NonNull List getCallParticipants() { - return callParticipants; - } - - public @NonNull CallParticipant getFocusedParticipant() { - return focusedParticipant; - } - - public boolean isRenderInPip() { - return isRenderInPip; - } - - public boolean isSpeaker() { - return isSpeaker; - } - - public boolean isPortrait() { - return isPortrait; - } - - public boolean shouldHideAvatar() { - return hideAvatar; - } - - public int getNavBarBottomInset() { - return navBarBottomInset; - } - - public @NonNull CallParticipantsLayout.LayoutStrategy getLayoutStrategy() { - return CallParticipantsLayoutStrategies.getStrategy(isPortrait, isLandscapeEnabled); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final WebRtcCallParticipantsPage that = (WebRtcCallParticipantsPage) o; - return isSpeaker == that.isSpeaker && - isRenderInPip == that.isRenderInPip && - isPortrait == that.isPortrait && - isLandscapeEnabled == that.isLandscapeEnabled && - hideAvatar == that.hideAvatar && - callParticipants.equals(that.callParticipants) && - focusedParticipant.equals(that.focusedParticipant) && - navBarBottomInset == that.navBarBottomInset; - } - - @Override - public int hashCode() { - return Objects.hash(callParticipants, focusedParticipant, isSpeaker, isRenderInPip, isPortrait, isLandscapeEnabled, hideAvatar, navBarBottomInset); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPagerAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPagerAdapter.java deleted file mode 100644 index 05bf93bdfc..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsPagerAdapter.java +++ /dev/null @@ -1,136 +0,0 @@ -package org.thoughtcrime.securesms.components.webrtc; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.ListAdapter; -import androidx.recyclerview.widget.RecyclerView; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.events.CallParticipant; -import org.webrtc.RendererCommon; - -class WebRtcCallParticipantsPagerAdapter extends ListAdapter { - - private static final int VIEW_TYPE_MULTI = 0; - private static final int VIEW_TYPE_SINGLE = 1; - - private final Runnable onPageClicked; - - WebRtcCallParticipantsPagerAdapter(@NonNull Runnable onPageClicked) { - super(new DiffCallback()); - this.onPageClicked = onPageClicked; - } - - @Override - public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { - super.onAttachedToRecyclerView(recyclerView); - recyclerView.setOverScrollMode(View.OVER_SCROLL_NEVER); - } - - @Override - public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - final ViewHolder viewHolder; - - switch (viewType) { - case VIEW_TYPE_SINGLE: - viewHolder = new SingleParticipantViewHolder((CallParticipantView) LayoutInflater.from(parent.getContext()) - .inflate(R.layout.call_participant_item, - parent, - false)); - break; - case VIEW_TYPE_MULTI: - viewHolder = new MultipleParticipantViewHolder((CallParticipantsLayout) LayoutInflater.from(parent.getContext()) - .inflate(R.layout.webrtc_call_participants_layout, - parent, - false)); - break; - default: - throw new IllegalArgumentException("Unsupported viewType: " + viewType); - } - - viewHolder.itemView.setOnClickListener(unused -> onPageClicked.run()); - - return viewHolder; - } - - @Override - public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - holder.bind(getItem(position)); - } - - @Override - public int getItemViewType(int position) { - return getItem(position).isSpeaker() ? VIEW_TYPE_SINGLE : VIEW_TYPE_MULTI; - } - - static abstract class ViewHolder extends RecyclerView.ViewHolder { - public ViewHolder(@NonNull View itemView) { - super(itemView); - } - - abstract void bind(WebRtcCallParticipantsPage page); - } - - private static class MultipleParticipantViewHolder extends ViewHolder { - - private final CallParticipantsLayout callParticipantsLayout; - - private MultipleParticipantViewHolder(@NonNull CallParticipantsLayout callParticipantsLayout) { - super(callParticipantsLayout); - this.callParticipantsLayout = callParticipantsLayout; - } - - @Override - void bind(WebRtcCallParticipantsPage page) { - callParticipantsLayout.update(page.getCallParticipants(), page.getFocusedParticipant(), page.isRenderInPip(), page.isPortrait(), page.shouldHideAvatar(), page.getNavBarBottomInset(), page.getLayoutStrategy()); - } - } - - private static class SingleParticipantViewHolder extends ViewHolder { - - private final CallParticipantView callParticipantView; - - private SingleParticipantViewHolder(CallParticipantView callParticipantView) { - super(callParticipantView); - this.callParticipantView = callParticipantView; - - ViewGroup.LayoutParams params = callParticipantView.getLayoutParams(); - - params.height = ViewGroup.LayoutParams.MATCH_PARENT; - params.width = ViewGroup.LayoutParams.MATCH_PARENT; - - callParticipantView.setLayoutParams(params); - } - - - @Override - void bind(WebRtcCallParticipantsPage page) { - CallParticipant participant = page.getCallParticipants().get(0); - callParticipantView.setCallParticipant(participant); - callParticipantView.setRenderInPip(page.isRenderInPip()); - callParticipantView.setRaiseHandAllowed(false); - if (participant.isScreenSharing()) { - callParticipantView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT); - } else { - callParticipantView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL); - } - } - } - - private static final class DiffCallback extends DiffUtil.ItemCallback { - @Override - public boolean areItemsTheSame(@NonNull WebRtcCallParticipantsPage oldItem, @NonNull WebRtcCallParticipantsPage newItem) { - return oldItem.isSpeaker() == newItem.isSpeaker(); - } - - @Override - public boolean areContentsTheSame(@NonNull WebRtcCallParticipantsPage oldItem, @NonNull WebRtcCallParticipantsPage newItem) { - return oldItem.equals(newItem); - } - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsRecyclerAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsRecyclerAdapter.java deleted file mode 100644 index fe03fb2c2b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsRecyclerAdapter.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.thoughtcrime.securesms.components.webrtc; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.ListAdapter; -import androidx.recyclerview.widget.RecyclerView; - -import org.signal.core.util.DimensionUnit; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.events.CallParticipant; -import org.thoughtcrime.securesms.util.ViewUtil; -import org.webrtc.RendererCommon; - -public class WebRtcCallParticipantsRecyclerAdapter extends ListAdapter { - - private static final int PARTICIPANT = 0; - private static final int EMPTY = 1; - - public WebRtcCallParticipantsRecyclerAdapter() { - super(new DiffCallback()); - } - - @Override - public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - if (viewType == PARTICIPANT) { - return new ParticipantViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.webrtc_call_participant_recycler_item, parent, false)); - } else { - return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.webrtc_call_participant_recycler_empty_item, parent, false)); - } - } - - @Override - public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - holder.bind(getItem(position)); - } - - @Override - public int getItemViewType(int position) { - return getItem(position) == CallParticipant.EMPTY ? EMPTY : PARTICIPANT; - } - - public static class ViewHolder extends RecyclerView.ViewHolder { - ViewHolder(@NonNull View itemView) { - super(itemView); - } - - void bind(@NonNull CallParticipant callParticipant) {} - } - - private static class ParticipantViewHolder extends ViewHolder { - - private final CallParticipantView callParticipantView; - - ParticipantViewHolder(@NonNull View itemView) { - super(itemView); - callParticipantView = itemView.findViewById(R.id.call_participant); - - View audioIndicator = callParticipantView.findViewById(R.id.call_participant_audio_indicator); - int audioIndicatorMargin = (int) DimensionUnit.DP.toPixels(8f); - ViewUtil.setLeftMargin(audioIndicator, audioIndicatorMargin); - ViewUtil.setBottomMargin(audioIndicator, audioIndicatorMargin); - } - - @Override - void bind(@NonNull CallParticipant callParticipant) { - callParticipantView.setCallParticipant(callParticipant); - callParticipantView.setRenderInPip(true); - callParticipantView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL); - } - } - - private static class DiffCallback extends DiffUtil.ItemCallback { - - @Override - public boolean areItemsTheSame(@NonNull CallParticipant oldItem, @NonNull CallParticipant newItem) { - return oldItem.getRecipient().equals(newItem.getRecipient()); - } - - @Override - public boolean areContentsTheSame(@NonNull CallParticipant oldItem, @NonNull CallParticipant newItem) { - return oldItem.equals(newItem); - } - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallRepository.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallRepository.java index 4b9df18c22..95b01ef42a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallRepository.java @@ -1,7 +1,5 @@ package org.thoughtcrime.securesms.components.webrtc; -import android.content.Context; - import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import androidx.core.util.Consumer; diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java deleted file mode 100644 index 0f5ad952f5..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java +++ /dev/null @@ -1,965 +0,0 @@ -/* - * Copyright 2023 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.webrtc; - -import android.Manifest; -import android.content.Context; -import android.content.res.Configuration; -import android.graphics.ColorMatrix; -import android.graphics.ColorMatrixColorFilter; -import android.graphics.Point; -import android.os.Build; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowInsets; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.appcompat.widget.Toolbar; -import androidx.compose.ui.platform.ComposeView; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.constraintlayout.widget.ConstraintSet; -import androidx.constraintlayout.widget.Guideline; -import androidx.coordinatorlayout.widget.CoordinatorLayout; -import androidx.core.util.Consumer; -import androidx.core.view.WindowInsetsCompat; -import androidx.recyclerview.widget.DefaultItemAnimator; -import androidx.recyclerview.widget.RecyclerView; -import androidx.viewpager2.widget.MarginPageTransformer; -import androidx.viewpager2.widget.ViewPager2; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.load.resource.bitmap.CenterCrop; -import com.google.android.material.button.MaterialButton; - -import org.signal.core.util.DimensionUnit; -import org.signal.core.util.SetUtil; -import org.signal.core.util.ThreadUtil; -import org.signal.core.util.logging.Log; -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; -import org.thoughtcrime.securesms.events.WebRtcViewModel; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -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.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; -import org.thoughtcrime.securesms.util.ViewUtil; -import org.thoughtcrime.securesms.util.views.Stub; -import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState; -import org.webrtc.RendererCommon; -import org.whispersystems.signalservice.api.messages.calls.HangupMessage; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; - -public class WebRtcCallView extends InsetAwareConstraintLayout { - - private static final String TAG = Log.tag(WebRtcCallView.class); - - private static final long TRANSITION_DURATION_MILLIS = 250; - - private WebRtcAudioOutputToggleButton audioToggle; - private AccessibleToggleButton videoToggle; - private AccessibleToggleButton micToggle; - private ViewGroup smallLocalRenderFrame; - private CallParticipantView smallLocalRender; - private View largeLocalRenderFrame; - private TextureViewRenderer largeLocalRender; - private View largeLocalRenderNoVideo; - private ImageView largeLocalRenderNoVideoAvatar; - private TextView recipientName; - private TextView status; - private TextView incomingRingStatus; - private CallScreenControlsListener controlsListener; - private RecipientId recipientId; - private ImageView answer; - private TextView answerWithoutVideoLabel; - private ImageView cameraDirectionToggle; - private AccessibleToggleButton ringToggle; - private PictureInPictureGestureHelper pictureInPictureGestureHelper; - private ImageView overflow; - private ImageView hangup; - private View answerWithoutVideo; - private View topGradient; - private View footerGradient; - private View startCallControls; - private ViewPager2 callParticipantsPager; - private RecyclerView callParticipantsRecycler; - private ConstraintLayout largeHeader; - private MaterialButton startCall; - private Stub groupCallSpeakerHint; - private Stub groupCallFullStub; - private View errorButton; - private Guideline showParticipantsGuideline; - private Guideline aboveControlsGuideline; - private Guideline topFoldGuideline; - private Guideline callScreenTopFoldGuideline; - private AvatarImageView largeHeaderAvatar; - private int navBarBottomInset; - private View fullScreenShade; - private Toolbar collapsedToolbar; - private Toolbar headerToolbar; - private Stub pendingParticipantsViewStub; - private Stub callLinkWarningCard; - private RecyclerView groupReactionsFeed; - private MultiReactionBurstLayout reactionViews; - private ComposeView raiseHandSnackbar; - private View missingPermissionContainer; - private MaterialButton allowAccessButton; - private Guideline callParticipantsOverflowGuideline; - private View callControlsSheet; - - private WebRtcCallParticipantsPagerAdapter pagerAdapter; - private WebRtcCallParticipantsRecyclerAdapter recyclerAdapter; - private WebRtcReactionsRecyclerAdapter reactionsAdapter; - private PictureInPictureExpansionHelper pictureInPictureExpansionHelper; - private PendingParticipantsListener pendingParticipantsViewListener; - - private final Set incomingCallViews = new HashSet<>(); - private final Set topViews = new HashSet<>(); - private final Set visibleViewSet = new HashSet<>(); - private final Set allTimeVisibleViews = new HashSet<>(); - - private final ThrottledDebouncer throttledDebouncer = new ThrottledDebouncer(TRANSITION_DURATION_MILLIS); - private WebRtcControls controls = WebRtcControls.NONE; - - private CallParticipantsViewState lastState; - private ContactPhoto previousLocalAvatar; - private LayoutPositions previousLayoutPositions = null; - - public WebRtcCallView(@NonNull Context context) { - this(context, null); - } - - public WebRtcCallView(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - - inflate(context, R.layout.webrtc_call_view, this); - } - - @SuppressWarnings("CodeBlock2Expr") - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - audioToggle = findViewById(R.id.call_screen_speaker_toggle); - videoToggle = findViewById(R.id.call_screen_video_toggle); - micToggle = findViewById(R.id.call_screen_audio_mic_toggle); - smallLocalRenderFrame = findViewById(R.id.call_screen_pip); - smallLocalRender = findViewById(R.id.call_screen_small_local_renderer); - largeLocalRenderFrame = findViewById(R.id.call_screen_large_local_renderer_frame); - largeLocalRender = findViewById(R.id.call_screen_large_local_renderer); - largeLocalRenderNoVideo = findViewById(R.id.call_screen_large_local_video_off); - largeLocalRenderNoVideoAvatar = findViewById(R.id.call_screen_large_local_video_off_avatar); - recipientName = findViewById(R.id.call_screen_recipient_name); - status = findViewById(R.id.call_screen_status); - incomingRingStatus = findViewById(R.id.call_screen_incoming_ring_status); - answer = findViewById(R.id.call_screen_answer_call); - answerWithoutVideoLabel = findViewById(R.id.call_screen_answer_without_video_label); - cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle); - ringToggle = findViewById(R.id.call_screen_audio_ring_toggle); - overflow = findViewById(R.id.call_screen_overflow_button); - hangup = findViewById(R.id.call_screen_end_call); - answerWithoutVideo = findViewById(R.id.call_screen_answer_without_video); - topGradient = findViewById(R.id.call_screen_header_gradient); - footerGradient = findViewById(R.id.call_screen_footer_gradient); - startCallControls = findViewById(R.id.call_screen_start_call_controls); - callParticipantsPager = findViewById(R.id.call_screen_participants_pager); - callParticipantsRecycler = findViewById(R.id.call_screen_participants_recycler); - largeHeader = findViewById(R.id.call_screen_header); - startCall = findViewById(R.id.call_screen_start_call_start_call); - errorButton = findViewById(R.id.call_screen_error_cancel); - groupCallSpeakerHint = new Stub<>(findViewById(R.id.call_screen_group_call_speaker_hint)); - groupCallFullStub = new Stub<>(findViewById(R.id.group_call_call_full_view)); - showParticipantsGuideline = findViewById(R.id.call_screen_show_participants_guideline); - aboveControlsGuideline = findViewById(R.id.call_screen_above_controls_guideline); - topFoldGuideline = findViewById(R.id.fold_top_guideline); - callScreenTopFoldGuideline = findViewById(R.id.fold_top_call_screen_guideline); - largeHeaderAvatar = findViewById(R.id.call_screen_header_avatar); - fullScreenShade = findViewById(R.id.call_screen_full_shade); - collapsedToolbar = findViewById(R.id.webrtc_call_view_toolbar_text); - headerToolbar = findViewById(R.id.webrtc_call_view_toolbar_no_text); - pendingParticipantsViewStub = new Stub<>(findViewById(R.id.call_screen_pending_recipients)); - callLinkWarningCard = new Stub<>(findViewById(R.id.call_screen_call_link_warning)); - groupReactionsFeed = findViewById(R.id.call_screen_reactions_feed); - reactionViews = findViewById(R.id.call_screen_reactions_container); - raiseHandSnackbar = findViewById(R.id.call_screen_raise_hand_view); - missingPermissionContainer = findViewById(R.id.missing_permissions_container); - allowAccessButton = findViewById(R.id.allow_access_button); - callParticipantsOverflowGuideline = findViewById(R.id.call_screen_participants_overflow_guideline); - callControlsSheet = findViewById(R.id.call_controls_info_parent); - - View decline = findViewById(R.id.call_screen_decline_call); - View answerLabel = findViewById(R.id.call_screen_answer_call_label); - View declineLabel = findViewById(R.id.call_screen_decline_call_label); - - callParticipantsPager.setPageTransformer(new MarginPageTransformer(ViewUtil.dpToPx(4))); - - pagerAdapter = new WebRtcCallParticipantsPagerAdapter(this::toggleControls); - recyclerAdapter = new WebRtcCallParticipantsRecyclerAdapter(); - reactionsAdapter = new WebRtcReactionsRecyclerAdapter(); - - callParticipantsPager.setAdapter(pagerAdapter); - callParticipantsRecycler.setAdapter(recyclerAdapter); - groupReactionsFeed.setAdapter(reactionsAdapter); - - DefaultItemAnimator animator = new DefaultItemAnimator(); - animator.setSupportsChangeAnimations(false); - callParticipantsRecycler.setItemAnimator(animator); - - groupReactionsFeed.addItemDecoration(new WebRtcReactionsAlphaItemDecoration()); - groupReactionsFeed.setItemAnimator(new WebRtcReactionsItemAnimator()); - - callParticipantsPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { - @Override - public void onPageSelected(int position) { - runIfNonNull(controlsListener, listener -> listener.onPageChanged(position == 0 ? CallParticipantsState.SelectedPage.GRID : CallParticipantsState.SelectedPage.FOCUSED)); - } - }); - - topViews.add(largeHeader); - topViews.add(topGradient); - - incomingCallViews.add(answer); - incomingCallViews.add(answerLabel); - incomingCallViews.add(decline); - incomingCallViews.add(declineLabel); - incomingCallViews.add(footerGradient); - incomingCallViews.add(incomingRingStatus); - - audioToggle.setOnAudioOutputChangedListener(webRtcAudioDevice -> { - runIfNonNull(controlsListener, listener -> - { - if (Build.VERSION.SDK_INT >= 31) { - if (webRtcAudioDevice.getDeviceId() != null) { - listener.onAudioOutputChanged31(webRtcAudioDevice); - } else { - Log.e(TAG, "Attempted to change audio output to null device ID."); - } - } else { - listener.onAudioOutputChanged(webRtcAudioDevice.getWebRtcAudioOutput()); - } - }); - }); - - videoToggle.setOnCheckedChangeListener((v, isOn) -> { - if (!hasCameraPermission()) { - videoToggle.setChecked(false); - } - runIfNonNull(controlsListener, listener -> listener.onVideoChanged(isOn)); - }); - - micToggle.setOnCheckedChangeListener((v, isOn) -> { - if (!hasAudioPermission()) { - micToggle.setChecked(false); - } - runIfNonNull(controlsListener, listener -> listener.onMicChanged(isOn)); - }); - - ringToggle.setOnCheckedChangeListener((v, isOn) -> { - runIfNonNull(controlsListener, listener -> listener.onRingGroupChanged(isOn, ringToggle.isActivated())); - }); - - 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, CallScreenControlsListener::onOverflowClicked); - }); - - hangup.setOnClickListener(v -> runIfNonNull(controlsListener, CallScreenControlsListener::onEndCallPressed)); - decline.setOnClickListener(v -> runIfNonNull(controlsListener, CallScreenControlsListener::onDenyCallPressed)); - - answer.setOnClickListener(v -> runIfNonNull(controlsListener, CallScreenControlsListener::onAcceptCallPressed)); - answerWithoutVideo.setOnClickListener(v -> runIfNonNull(controlsListener, CallScreenControlsListener::onAcceptCallWithVoiceOnlyPressed)); - - pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(smallLocalRenderFrame); - pictureInPictureExpansionHelper = new PictureInPictureExpansionHelper(smallLocalRenderFrame, state -> { - if (state == PictureInPictureExpansionHelper.State.IS_SHRUNKEN) { - pictureInPictureGestureHelper.setBoundaryState(PictureInPictureGestureHelper.BoundaryState.COLLAPSED); - } else { - pictureInPictureGestureHelper.setBoundaryState(PictureInPictureGestureHelper.BoundaryState.EXPANDED); - } - }); - - smallLocalRenderFrame.setOnClickListener(v -> { - if (controlsListener != null) { - controlsListener.onLocalPictureInPictureClicked(); - } - }); - - View smallLocalAudioIndicator = smallLocalRender.findViewById(R.id.call_participant_audio_indicator); - int audioIndicatorMargin = (int) DimensionUnit.DP.toPixels(8f); - ViewUtil.setLeftMargin(smallLocalAudioIndicator, audioIndicatorMargin); - ViewUtil.setBottomMargin(smallLocalAudioIndicator, audioIndicatorMargin); - - startCall.setOnClickListener(v -> { - Runnable onGranted = () -> { - if (controlsListener != null) { - startCall.setEnabled(false); - controlsListener.onStartCall(videoToggle.isChecked()); - } - }; - runIfNonNull(controlsListener, listener -> listener.onAudioPermissionsRequested(onGranted)); - }); - - ColorMatrix greyScaleMatrix = new ColorMatrix(); - greyScaleMatrix.setSaturation(0); - largeLocalRenderNoVideoAvatar.setAlpha(0.6f); - largeLocalRenderNoVideoAvatar.setColorFilter(new ColorMatrixColorFilter(greyScaleMatrix)); - - errorButton.setOnClickListener(v -> { - if (controlsListener != null) { - controlsListener.onCancelStartCall(); - } - }); - - collapsedToolbar.setNavigationOnClickListener(unused -> { - if (controlsListener != null) { - controlsListener.onNavigateUpClicked(); - } - }); - - collapsedToolbar.setOnMenuItemClickListener(item -> { - if (item.getItemId() == R.id.action_info && controlsListener != null) { - controlsListener.onCallInfoClicked(); - return true; - } - - return false; - }); - - headerToolbar.setNavigationOnClickListener(unused -> { - if (controlsListener != null) { - controlsListener.onNavigateUpClicked(); - } - }); - - headerToolbar.setOnMenuItemClickListener(item -> { - if (item.getItemId() == R.id.action_info && controlsListener != null) { - controlsListener.onCallInfoClicked(); - return true; - } - - return false; - }); - - missingPermissionContainer.setVisibility(hasCameraPermission() ? View.GONE : View.VISIBLE); - - allowAccessButton.setOnClickListener(v -> { - runIfNonNull(controlsListener, listener -> listener.onVideoChanged(videoToggle.isEnabled())); - }); - - ConstraintLayout aboveControls = findViewById(R.id.call_controls_floating_parent); - - if (getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { - aboveControls.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { - pictureInPictureGestureHelper.setCollapsedVerticalBoundary(bottom + ViewUtil.getStatusBarHeight(v)); - }); - } - - SlideUpWithCallControlsBehavior behavior = (SlideUpWithCallControlsBehavior) ((CoordinatorLayout.LayoutParams) aboveControls.getLayoutParams()).getBehavior(); - Objects.requireNonNull(behavior).setOnTopOfControlsChangedListener(topOfControls -> { - pictureInPictureGestureHelper.setExpandedVerticalBoundary(topOfControls); - }); - - if (callParticipantsOverflowGuideline != null) { - callParticipantsRecycler.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { - callParticipantsOverflowGuideline.setGuidelineEnd(bottom - top); - }); - } - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - - final int pipWidth = smallLocalRenderFrame.getMeasuredWidth(); - final int controlsWidth = callControlsSheet.getMeasuredWidth(); - final float protection = DimensionUnit.DP.toPixels(16 * 4); - final float requiredWidth = pipWidth + controlsWidth + protection; - - if (w > h && w >= requiredWidth) { - pictureInPictureGestureHelper.allowCollapsedState(); - } - } - - @Override - public WindowInsets onApplyWindowInsets(WindowInsets insets) { - navBarBottomInset = WindowInsetsCompat.toWindowInsetsCompat(insets).getInsets(WindowInsetsCompat.Type.navigationBars()).bottom; - - if (lastState != null) { - updateCallParticipants(lastState); - } - - return super.onApplyWindowInsets(insets); - } - - @Override - public void onWindowSystemUiVisibilityChanged(int visible) { - final Guideline statusBarGuideline = getStatusBarGuideline(); - if ((visible & SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) { - pictureInPictureGestureHelper.setTopVerticalBoundary(collapsedToolbar.getBottom()); - } else if (statusBarGuideline != null) { - pictureInPictureGestureHelper.setTopVerticalBoundary(statusBarGuideline.getBottom()); - } else { - Log.d(TAG, "Could not update PiP gesture helper."); - } - } - - public void setControlsListener(@Nullable CallScreenControlsListener controlsListener) { - this.controlsListener = controlsListener; - } - - public void maybeDismissAudioPicker() { - audioToggle.hidePicker(); - } - - public void setMicEnabled(boolean isMicEnabled) { - micToggle.setChecked(hasAudioPermission() && isMicEnabled, false); - } - - public void setPendingParticipantsViewListener(@Nullable PendingParticipantsListener listener) { - pendingParticipantsViewListener = listener; - } - - public void updatePendingParticipantsList(@NonNull PendingParticipantsState state) { - if (state.isInPipMode()) { - pendingParticipantsViewStub.setVisibility(View.GONE); - return; - } - - if (state.getPendingParticipantCollection().getUnresolvedPendingParticipants().isEmpty()) { - if (pendingParticipantsViewStub.resolved()) { - pendingParticipantsViewStub.get().setListener(pendingParticipantsViewListener); - pendingParticipantsViewStub.get().applyState(state.getPendingParticipantCollection()); - } - } else { - pendingParticipantsViewStub.get().setListener(pendingParticipantsViewListener); - pendingParticipantsViewStub.get().applyState(state.getPendingParticipantCollection()); - } - } - - private boolean hasCameraPermission() { - return Permissions.hasAll(getContext(), Manifest.permission.CAMERA); - } - - private boolean hasAudioPermission() { - return Permissions.hasAll(getContext(), Manifest.permission.RECORD_AUDIO); - } - - public void updateCallParticipants(@NonNull CallParticipantsViewState callParticipantsViewState) { - lastState = callParticipantsViewState; - - CallParticipantsState state = callParticipantsViewState.getCallParticipantsState(); - boolean isPortrait = callParticipantsViewState.isPortrait(); - boolean isLandscapeEnabled = callParticipantsViewState.isLandscapeEnabled(); - List pages = new ArrayList<>(2); - - if (!state.getCallState().isErrorState()) { - if (!state.getGridParticipants().isEmpty()) { - pages.add(WebRtcCallParticipantsPage.forMultipleParticipants(state.getGridParticipants(), state.getFocusedParticipant(), state.isInPipMode(), isPortrait, isLandscapeEnabled, state.getHideAvatar(), navBarBottomInset)); - } - - if (state.getFocusedParticipant() != CallParticipant.EMPTY && state.getAllRemoteParticipants().size() > 1) { - pages.add(WebRtcCallParticipantsPage.forSingleParticipant(state.getFocusedParticipant(), state.isInPipMode(), isPortrait, isLandscapeEnabled)); - } - } - - if (state.getGroupCallState().isNotIdle()) { - if (state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) { - if (state.isCallLink()) { - TextView warningTextView = callLinkWarningCard.get().findViewById(R.id.call_screen_call_link_warning_textview); - warningTextView.setText(SignalStore.phoneNumberPrivacy().isPhoneNumberSharingEnabled() ? R.string.WebRtcCallView__anyone_who_joins_pnp_enabled : R.string.WebRtcCallView__anyone_who_joins_pnp_disabled); - callLinkWarningCard.setVisibility(View.VISIBLE); - } else { - callLinkWarningCard.setVisibility(View.GONE); - } - setStatus(state.getPreJoinGroupDescription(getContext())); - } else if (state.getCallState() == WebRtcViewModel.State.CALL_CONNECTED && state.isInOutgoingRingingMode()) { - callLinkWarningCard.setVisibility(View.GONE); - setStatus(state.getOutgoingRingingGroupDescription(getContext())); - } else if (state.getGroupCallState().isRinging()) { - callLinkWarningCard.setVisibility(View.GONE); - setStatus(state.getIncomingRingingGroupDescription(getContext())); - } else { - callLinkWarningCard.setVisibility(View.GONE); - } - } - - if (state.getGroupCallState().isNotIdle()) { - boolean enabled = state.getParticipantCount().isPresent(); - collapsedToolbar.getMenu().getItem(0).setVisible(enabled); - headerToolbar.getMenu().getItem(0).setVisible(enabled); - } else { - collapsedToolbar.getMenu().getItem(0).setVisible(false); - headerToolbar.getMenu().getItem(0).setVisible(false); - } - - pagerAdapter.submitList(pages); - recyclerAdapter.submitList(state.getListParticipants()); - reactionsAdapter.submitList(state.getReactions()); - - reactionViews.displayReactions(state.getReactions()); - - boolean displaySmallSelfPipInLandscape = !isPortrait && isLandscapeEnabled; - - updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant(), displaySmallSelfPipInLandscape); - - if (state.isLargeGroup()) { - adjustLayoutForLargeCount(); - } else { - adjustLayoutForSmallCount(); - } - } - - public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, - @NonNull CallParticipant localCallParticipant, - boolean displaySmallSelfPipInLandscape) - { - largeLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT); - - smallLocalRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL); - largeLocalRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL); - - localCallParticipant.getVideoSink().getLockableEglBase().performWithValidEglBase(eglBase -> { - largeLocalRender.init(eglBase); - }); - - - videoToggle.setChecked(hasCameraPermission() && localCallParticipant.isVideoEnabled(), false); - smallLocalRender.setRenderInPip(true); - smallLocalRender.setCallParticipant(localCallParticipant); - smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT); - - if (state == WebRtcLocalRenderState.EXPANDED) { - if (largeLocalRenderFrame.getVisibility() == View.VISIBLE) { - smallLocalRenderFrame.setVisibility(View.VISIBLE); - animatePipToExpandedRectangle(displaySmallSelfPipInLandscape, localCallParticipant.isMoreThanOneCameraAvailable()); - - largeLocalRender.attachBroadcastVideoSink(null); - largeLocalRenderFrame.setVisibility(View.GONE); - } else { - pictureInPictureExpansionHelper.beginExpandTransition(); - smallLocalRender.setSelfPipMode(CallParticipantView.SelfPipMode.EXPANDED_SELF_PIP, localCallParticipant.isMoreThanOneCameraAvailable()); - } - - return; - } else if ((state.isAnySmall() || state == WebRtcLocalRenderState.GONE) && pictureInPictureExpansionHelper.isExpandedOrExpanding()) { - pictureInPictureExpansionHelper.beginShrinkTransition(); - smallLocalRender.setSelfPipMode(pictureInPictureExpansionHelper.isMiniSize() ? CallParticipantView.SelfPipMode.MINI_SELF_PIP : CallParticipantView.SelfPipMode.NORMAL_SELF_PIP, localCallParticipant.isMoreThanOneCameraAvailable()); - - if (state != WebRtcLocalRenderState.GONE) { - return; - } - } - - switch (state) { - case GONE: - largeLocalRender.attachBroadcastVideoSink(null); - largeLocalRenderFrame.setVisibility(View.GONE); - smallLocalRenderFrame.setVisibility(View.GONE); - - break; - case SMALL_RECTANGLE: - smallLocalRenderFrame.setVisibility(View.VISIBLE); - animatePipToLargeRectangle(displaySmallSelfPipInLandscape, localCallParticipant.isMoreThanOneCameraAvailable()); - - largeLocalRender.attachBroadcastVideoSink(null); - largeLocalRenderFrame.setVisibility(View.GONE); - break; - case SMALLER_RECTANGLE: - smallLocalRenderFrame.setVisibility(View.VISIBLE); - animatePipToSmallRectangle(displaySmallSelfPipInLandscape, localCallParticipant.isMoreThanOneCameraAvailable()); - - largeLocalRender.attachBroadcastVideoSink(null); - largeLocalRenderFrame.setVisibility(View.GONE); - break; - case LARGE: - largeLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink()); - largeLocalRenderFrame.setVisibility(View.VISIBLE); - - largeLocalRenderNoVideo.setVisibility(View.GONE); - largeLocalRenderNoVideoAvatar.setVisibility(View.GONE); - - smallLocalRenderFrame.setVisibility(View.GONE); - break; - case LARGE_NO_VIDEO: - largeLocalRender.attachBroadcastVideoSink(null); - largeLocalRenderFrame.setVisibility(View.VISIBLE); - - largeLocalRenderNoVideo.setVisibility(View.VISIBLE); - largeLocalRenderNoVideoAvatar.setVisibility(View.VISIBLE); - - ContactPhoto localAvatar = new ProfileContactPhoto(localCallParticipant.getRecipient()); - - if (!localAvatar.equals(previousLocalAvatar)) { - previousLocalAvatar = localAvatar; - Glide.with(getContext().getApplicationContext()) - .load(localAvatar) - .transform(new CenterCrop(), new BlurTransformation(getContext(), 0.25f, BlurTransformation.MAX_RADIUS)) - .diskCacheStrategy(DiskCacheStrategy.ALL) - .into(largeLocalRenderNoVideoAvatar); - } - - smallLocalRenderFrame.setVisibility(View.GONE); - break; - } - } - - public void setRecipient(@NonNull Recipient recipient) { - collapsedToolbar.setTitle(recipient.getDisplayName(getContext())); - recipientName.setText(recipient.getDisplayName(getContext())); - - if (recipient.getId() == recipientId) { - return; - } - - recipientId = recipient.getId(); - largeHeaderAvatar.setRecipient(recipient, false); - } - - public void setStatus(@Nullable String status) { - ThreadUtil.assertMainThread(); - this.status.setText(status); - try { - // Toolbar's subtitle view sometimes already has a parent somehow, - // so we clear it out first so that it removes the view from its parent. - // In addition, we catch the ISE to prevent a crash. - collapsedToolbar.setSubtitle(null); - collapsedToolbar.setSubtitle(status); - } catch (IllegalStateException e) { - Log.w(TAG, "IllegalStateException trying to set status on collapsed Toolbar."); - } - } - - public void setWebRtcControls(@NonNull WebRtcControls webRtcControls) { - Set lastVisibleSet = new HashSet<>(visibleViewSet); - - visibleViewSet.clear(); - - if (webRtcControls.adjustForFold()) { - showParticipantsGuideline.setGuidelineBegin(-1); - showParticipantsGuideline.setGuidelineEnd(webRtcControls.getFold()); - topFoldGuideline.setGuidelineEnd(webRtcControls.getFold()); - callScreenTopFoldGuideline.setGuidelineEnd(webRtcControls.getFold()); - } else { - showParticipantsGuideline.setGuidelineBegin(((LayoutParams) getStatusBarGuideline().getLayoutParams()).guideBegin); - showParticipantsGuideline.setGuidelineEnd(-1); - topFoldGuideline.setGuidelineEnd(0); - callScreenTopFoldGuideline.setGuidelineEnd(0); - } - - if (webRtcControls.displayStartCallControls()) { - visibleViewSet.add(footerGradient); - visibleViewSet.add(startCallControls); - - startCall.setText(webRtcControls.getStartCallButtonText()); - startCall.setEnabled(webRtcControls.isStartCallEnabled()); - } - - if (webRtcControls.displayErrorControls()) { - visibleViewSet.add(footerGradient); - visibleViewSet.add(errorButton); - } - - if (webRtcControls.displayGroupCallFull()) { - groupCallFullStub.get().setVisibility(View.VISIBLE); - ((TextView) groupCallFullStub.get().findViewById(R.id.group_call_call_full_message)).setText(webRtcControls.getGroupCallFullMessage(getContext())); - } else if (groupCallFullStub.resolved()) { - groupCallFullStub.get().setVisibility(View.GONE); - } - - if (webRtcControls.displayTopViews()) { - visibleViewSet.addAll(topViews); - } - - if (webRtcControls.displayIncomingCallButtons()) { - visibleViewSet.addAll(incomingCallViews); - - incomingRingStatus.setText(webRtcControls.displayAnswerWithoutVideo() ? R.string.WebRtcCallView__signal_video_call: R.string.WebRtcCallView__signal_call); - - answer.setImageDrawable(AppCompatResources.getDrawable(getContext(), R.drawable.webrtc_call_screen_answer)); - } - - if (webRtcControls.displayAnswerWithoutVideo()) { - visibleViewSet.add(answerWithoutVideo); - visibleViewSet.add(answerWithoutVideoLabel); - - answer.setImageDrawable(AppCompatResources.getDrawable(getContext(), R.drawable.webrtc_call_screen_answer_with_video)); - } - - if (!webRtcControls.displayIncomingCallButtons()){ - incomingRingStatus.setVisibility(GONE); - } - - if (webRtcControls.displayAudioToggle()) { - audioToggle.setControlAvailability(webRtcControls.isEarpieceAvailableForAudioToggle(), - webRtcControls.isBluetoothHeadsetAvailableForAudioToggle(), - webRtcControls.isWiredHeadsetAvailableForAudioToggle()); - - audioToggle.updateAudioOutputState(webRtcControls.getAudioOutput()); - } - - if (webRtcControls.displaySmallCallButtons()) { - updateButtonStateForSmallButtons(); - } else { - updateButtonStateForLargeButtons(); - } - - if (webRtcControls.displayRemoteVideoRecycler()) { - callParticipantsRecycler.setVisibility(View.VISIBLE); - } else { - callParticipantsRecycler.setVisibility(View.GONE); - } - - if (webRtcControls.showFullScreenShade()) { - fullScreenShade.setVisibility(VISIBLE); - visibleViewSet.remove(topGradient); - visibleViewSet.remove(footerGradient); - } else { - fullScreenShade.setVisibility(GONE); - } - - if (webRtcControls.displayReactions()) { - visibleViewSet.add(reactionViews); - visibleViewSet.add(groupReactionsFeed); - } - - if (webRtcControls.displayRaiseHand()) { - visibleViewSet.add(raiseHandSnackbar); - } - - boolean forceUpdate = webRtcControls.adjustForFold() && !controls.adjustForFold(); - controls = webRtcControls; - - if (!controls.isFadeOutEnabled()) { - boolean controlsVisible = true; - } - - allTimeVisibleViews.addAll(visibleViewSet); - - if (!visibleViewSet.equals(lastVisibleSet) || - !controls.isFadeOutEnabled() || - (webRtcControls.showSmallHeader() && largeHeaderAvatar.getVisibility() == View.VISIBLE) || - (!webRtcControls.showSmallHeader() && largeHeaderAvatar.getVisibility() == View.GONE) || - forceUpdate) - { - throttledDebouncer.publish(() -> fadeInNewUiState(webRtcControls.showSmallHeader())); - } - - onWindowSystemUiVisibilityChanged(getWindowSystemUiVisibility()); - } - - public @NonNull View getVideoTooltipTarget() { - return videoToggle; - } - - public @NonNull View getSwitchCameraTooltipTarget() { - return smallLocalRenderFrame; - } - - public void showSpeakerViewHint() { - groupCallSpeakerHint.get().setVisibility(View.VISIBLE); - } - - public void hideSpeakerViewHint() { - if (groupCallSpeakerHint.resolved()) { - groupCallSpeakerHint.get().setVisibility(View.GONE); - } - } - - private void animatePipToExpandedRectangle(boolean isLandscape, boolean moreThanOneCameraAvailable) { - final Point dimens; - if (isLandscape) { - dimens = new Point(ViewUtil.dpToPx(PictureInPictureExpansionHelper.NORMAL_PIP_HEIGHT_DP), - ViewUtil.dpToPx(PictureInPictureExpansionHelper.NORMAL_PIP_WIDTH_DP)); - } else { - dimens = new Point(ViewUtil.dpToPx(PictureInPictureExpansionHelper.NORMAL_PIP_WIDTH_DP), - ViewUtil.dpToPx(PictureInPictureExpansionHelper.NORMAL_PIP_HEIGHT_DP)); - } - - pictureInPictureExpansionHelper.startExpandedSizeTransition(dimens, new PictureInPictureExpansionHelper.Callback() { - @Override - public void onAnimationHasFinished() { - pictureInPictureGestureHelper.enableCorners(); - } - }); - - smallLocalRender.setSelfPipMode(CallParticipantView.SelfPipMode.EXPANDED_SELF_PIP, moreThanOneCameraAvailable); - } - - private void animatePipToLargeRectangle(boolean isLandscape, boolean moreThanOneCameraAvailable) { - final Point dimens; - if (isLandscape) { - dimens = new Point(ViewUtil.dpToPx(PictureInPictureExpansionHelper.NORMAL_PIP_HEIGHT_DP), - ViewUtil.dpToPx(PictureInPictureExpansionHelper.NORMAL_PIP_WIDTH_DP)); - } else { - dimens = new Point(ViewUtil.dpToPx(PictureInPictureExpansionHelper.NORMAL_PIP_WIDTH_DP), - ViewUtil.dpToPx(PictureInPictureExpansionHelper.NORMAL_PIP_HEIGHT_DP)); - } - - pictureInPictureExpansionHelper.startDefaultSizeTransition(dimens, new PictureInPictureExpansionHelper.Callback() { - @Override - public void onAnimationHasFinished() { - pictureInPictureGestureHelper.enableCorners(); - } - }); - - smallLocalRender.setSelfPipMode(CallParticipantView.SelfPipMode.NORMAL_SELF_PIP, moreThanOneCameraAvailable); - } - - private void animatePipToSmallRectangle(boolean isLandscape, boolean moreThanOneCameraAvailable) { - final Point dimens; - if (isLandscape) { - dimens = new Point(ViewUtil.dpToPx(PictureInPictureExpansionHelper.MINI_PIP_HEIGHT_DP), - ViewUtil.dpToPx(PictureInPictureExpansionHelper.MINI_PIP_WIDTH_DP)); - } else { - dimens = new Point(ViewUtil.dpToPx(PictureInPictureExpansionHelper.MINI_PIP_WIDTH_DP), - ViewUtil.dpToPx(PictureInPictureExpansionHelper.MINI_PIP_HEIGHT_DP)); - } - - pictureInPictureExpansionHelper.startDefaultSizeTransition(dimens, - new PictureInPictureExpansionHelper.Callback() { - @Override - public void onAnimationHasFinished() { - pictureInPictureGestureHelper.lockToBottomEnd(); - } - }); - - smallLocalRender.setSelfPipMode(CallParticipantView.SelfPipMode.MINI_SELF_PIP, moreThanOneCameraAvailable); - } - - private void toggleControls() { - controlsListener.toggleControls(); - } - - private void adjustLayoutForSmallCount() { - adjustLayoutPositions(LayoutPositions.SMALL_GROUP); - } - - private void adjustLayoutForLargeCount() { - adjustLayoutPositions(LayoutPositions.LARGE_GROUP); - } - - private void adjustLayoutPositions(@NonNull LayoutPositions layoutPositions) { - if (previousLayoutPositions == layoutPositions) { - return; - } - - previousLayoutPositions = layoutPositions; - - ConstraintSet constraintSet = new ConstraintSet(); - constraintSet.setForceId(false); - constraintSet.clone(this); - - constraintSet.connect(R.id.call_screen_reactions_feed, - ConstraintSet.BOTTOM, - layoutPositions.reactionBottomViewId, - ConstraintSet.TOP, - ViewUtil.dpToPx(layoutPositions.reactionBottomMargin)); - - constraintSet.connect(pendingParticipantsViewStub.getId(), - ConstraintSet.BOTTOM, - layoutPositions.reactionBottomViewId, - ConstraintSet.TOP, - ViewUtil.dpToPx(layoutPositions.reactionBottomMargin)); - - constraintSet.applyTo(this); - } - - private void fadeInNewUiState(boolean showSmallHeader) { - for (View view : SetUtil.difference(allTimeVisibleViews, visibleViewSet)) { - view.setVisibility(GONE); - } - - for (View view : visibleViewSet) { - view.setVisibility(VISIBLE); - } - - if (showSmallHeader) { - collapsedToolbar.setEnabled(true); - collapsedToolbar.setAlpha(1); - headerToolbar.setEnabled(false); - headerToolbar.setAlpha(0); - largeHeader.setEnabled(false); - largeHeader.setAlpha(0); - } else { - collapsedToolbar.setEnabled(false); - collapsedToolbar.setAlpha(0); - headerToolbar.setEnabled(true); - headerToolbar.setAlpha(1); - largeHeader.setEnabled(true); - largeHeader.setAlpha(1); - } - } - - private static void runIfNonNull(@Nullable T listener, @NonNull Consumer listenerConsumer) { - if (listener != null) { - listenerConsumer.accept(listener); - } - } - - private void updateButtonStateForLargeButtons() { - cameraDirectionToggle.setImageResource(R.drawable.webrtc_call_screen_camera_toggle); - hangup.setImageResource(R.drawable.webrtc_call_screen_hangup); - overflow.setImageResource(R.drawable.webrtc_call_screen_overflow_menu); - micToggle.setBackgroundResource(R.drawable.webrtc_call_screen_mic_toggle); - videoToggle.setBackgroundResource(R.drawable.webrtc_call_screen_video_toggle); - audioToggle.setImageResource(R.drawable.webrtc_call_screen_speaker_toggle); - ringToggle.setBackgroundResource(R.drawable.webrtc_call_screen_ring_toggle); - overflow.setBackgroundResource(R.drawable.webrtc_call_screen_overflow_menu); - } - - private void updateButtonStateForSmallButtons() { - cameraDirectionToggle.setImageResource(R.drawable.webrtc_call_screen_camera_toggle_small); - hangup.setImageResource(R.drawable.webrtc_call_screen_hangup_small); - overflow.setImageResource(R.drawable.webrtc_call_screen_overflow_menu_small); - micToggle.setBackgroundResource(R.drawable.webrtc_call_screen_mic_toggle_small); - videoToggle.setBackgroundResource(R.drawable.webrtc_call_screen_video_toggle_small); - audioToggle.setImageResource(R.drawable.webrtc_call_screen_speaker_toggle_small); - ringToggle.setBackgroundResource(R.drawable.webrtc_call_screen_ring_toggle_small); - overflow.setBackgroundResource(R.drawable.webrtc_call_screen_overflow_menu_small); - } - - public void switchToSpeakerView() { - if (pagerAdapter.getItemCount() > 0) { - callParticipantsPager.setCurrentItem(pagerAdapter.getItemCount() - 1, false); - } - } - - public void setRingGroup(boolean shouldRingGroup) { - ringToggle.setChecked(shouldRingGroup, false); - } - - public void enableRingGroup(boolean enabled) { - ringToggle.setActivated(enabled); - } - - public void onControlTopChanged() { - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java index a021e00bc1..5342bcbf47 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java @@ -6,9 +6,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.Px; import androidx.annotation.StringRes; -import androidx.annotation.VisibleForTesting; -import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager; diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WifiToCellularPopupWindow.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WifiToCellularPopupWindow.kt deleted file mode 100644 index 6a1a9ada3c..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WifiToCellularPopupWindow.kt +++ /dev/null @@ -1,43 +0,0 @@ -package org.thoughtcrime.securesms.components.webrtc - -import android.view.Gravity -import android.view.LayoutInflater -import android.view.ViewGroup -import android.view.WindowManager -import android.widget.PopupWindow -import androidx.core.view.postDelayed -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.util.VibrateUtil -import java.util.concurrent.TimeUnit - -/** - * Popup shown when the device is connected to a WiFi and cellular network, and WiFi is unusable for - * RingRTC. - */ -class WifiToCellularPopupWindow(private val parent: ViewGroup) : PopupWindow( - LayoutInflater.from(parent.context).inflate(R.layout.wifi_to_cellular_popup, parent, false), - WindowManager.LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.WRAP_CONTENT -) { - - init { - animationStyle = R.style.PopupAnimation - } - - fun show() { - if (parent.windowToken == null) { - return - } - - showAtLocation(parent, Gravity.TOP or Gravity.START, 0, 0) - VibrateUtil.vibrate(parent.context, VIBRATE_DURATION_MS) - contentView.postDelayed(DISPLAY_DURATION_MS) { - dismiss() - } - } - - companion object { - private val DISPLAY_DURATION_MS = TimeUnit.SECONDS.toMillis(4) - private const val VIBRATE_DURATION_MS = 50 - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/ControlsAndInfoController.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/ControlsAndInfoController.kt deleted file mode 100644 index c072492945..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/controls/ControlsAndInfoController.kt +++ /dev/null @@ -1,509 +0,0 @@ -/* - * Copyright 2023 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.webrtc.controls - -import android.annotation.SuppressLint -import android.content.res.ColorStateList -import android.content.res.Configuration -import android.graphics.Color -import android.os.Handler -import android.os.Parcelable -import android.view.View -import android.widget.FrameLayout -import android.widget.TextView -import android.widget.Toast -import androidx.annotation.IdRes -import androidx.annotation.Px -import androidx.compose.material3.Surface -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.platform.rememberNestedScrollInteropConnection -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.constraintlayout.widget.ConstraintSet -import androidx.constraintlayout.widget.Guideline -import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.core.content.ContextCompat -import androidx.core.view.isVisible -import androidx.transition.AutoTransition -import androidx.transition.TransitionManager -import androidx.transition.TransitionSet -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback -import com.google.android.material.bottomsheet.BottomSheetBehaviorHack -import com.google.android.material.progressindicator.CircularProgressIndicatorSpec -import com.google.android.material.progressindicator.IndeterminateDrawable -import com.google.android.material.shape.CornerFamily -import com.google.android.material.shape.MaterialShapeDrawable -import com.google.android.material.shape.ShapeAppearanceModel -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.disposables.Disposable -import io.reactivex.rxjava3.kotlin.addTo -import io.reactivex.rxjava3.kotlin.subscribeBy -import kotlinx.parcelize.Parcelize -import org.signal.core.util.dp -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.BaseActivity -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment -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.compose.SignalTheme -import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult -import org.thoughtcrime.securesms.util.padding -import org.thoughtcrime.securesms.util.visible -import kotlin.math.max -import kotlin.time.Duration.Companion.seconds - -/** - * Brain for rendering the call controls and info within a bottom sheet when we display the activity in portrait mode. - */ -class ControlsAndInfoController private constructor( - private val activity: BaseActivity, - private val webRtcCallView: WebRtcCallView, - private val overflowPopupWindow: CallOverflowPopupWindow, - private val viewModel: WebRtcCallViewModel, - private val controlsAndInfoViewModel: ControlsAndInfoViewModel, - private val disposables: CompositeDisposable -) : Disposable by disposables { - - constructor( - activity: BaseActivity, - webRtcCallView: WebRtcCallView, - overflowPopupWindow: CallOverflowPopupWindow, - viewModel: WebRtcCallViewModel, - controlsAndInfoViewModel: ControlsAndInfoViewModel - ) : this( - activity, - webRtcCallView, - overflowPopupWindow, - viewModel, - controlsAndInfoViewModel, - CompositeDisposable() - ) - - companion object { - private val TAG = Log.tag(ControlsAndInfoController::class.java) - - private const val CONTROL_TRANSITION_DURATION = 250L - private const val CONTROL_FADE_OUT_START = 0f - private const val CONTROL_FADE_OUT_DONE = 0.23f - private const val INFO_FADE_IN_START = CONTROL_FADE_OUT_DONE - private const val INFO_FADE_IN_DONE = 0.8f - private val INFO_TRANSLATION_DISTANCE = 24f.dp - private val HIDE_CONTROL_DELAY = 5.seconds.inWholeMilliseconds - } - - private val coordinator: CoordinatorLayout = webRtcCallView.findViewById(R.id.call_controls_info_coordinator) - private val callInfoComposeView: ComposeView = webRtcCallView.findViewById(R.id.call_info_compose) - private val frame: FrameLayout = webRtcCallView.findViewById(R.id.call_controls_info_parent) - private val behavior = BottomSheetBehavior.from(frame) - private val raiseHandComposeView: ComposeView = webRtcCallView.findViewById(R.id.call_screen_raise_hand_view) - private val aboveControlsGuideline: Guideline = webRtcCallView.findViewById(R.id.call_screen_above_controls_guideline) - private val toggleCameraDirectionView: View = webRtcCallView.findViewById(R.id.call_screen_camera_direction_toggle) - private val startCallControls: View = webRtcCallView.findViewById(R.id.call_screen_start_call_controls) - private val callControls: ConstraintLayout = webRtcCallView.findViewById(R.id.call_controls_constraint_layout) - private val isLandscape = activity.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - private val waitingToBeLetInProgressDrawable = IndeterminateDrawable.createCircularDrawable( - activity, - CircularProgressIndicatorSpec(activity, null).apply { - indicatorSize = 20.dp - indicatorInset = 0.dp - trackThickness = 2.dp - trackCornerRadius = 1.dp - indicatorColors = intArrayOf(ContextCompat.getColor(activity, R.color.signal_colorOnBackground)) - trackColor = Color.TRANSPARENT - } - ) - private val waitingToBeLetIn: TextView = webRtcCallView.findViewById(R.id.call_controls_waiting_to_be_let_in).apply { - setCompoundDrawablesRelativeWithIntrinsicBounds(waitingToBeLetInProgressDrawable, null, null, null) - } - - private val scheduleHideControlsRunnable: Runnable = Runnable { onScheduledHide() } - private val callControlsVisibilityListeners = mutableSetOf() - - private val handler: Handler? - get() = webRtcCallView.handler - - private var previousCallControlHeightData = HeightData() - private var controlState: WebRtcControls = WebRtcControls.NONE - - private val callInfoCallbacks = CallInfoCallbacks(activity, controlsAndInfoViewModel) - - init { - raiseHandComposeView.apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - RaiseHandSnackbar.View(viewModel, showCallInfoListener = ::showCallInfo) - } - } - - coordinator.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> - webRtcCallView.post { onControlTopChanged() } - } - - raiseHandComposeView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> - onControlTopChanged() - } - - val maxBehaviorHeightPercentage = if (isLandscape) 1f else 0.66f - val minFrameHeightDenominator = if (isLandscape) 1 else 2 - - callControls.viewTreeObserver.addOnGlobalLayoutListener { - if (callControls.height > 0 && previousCallControlHeightData.hasChanged(callControls.height, coordinator.height)) { - previousCallControlHeightData = HeightData(callControls.height, coordinator.height) - - val controlPeakHeight = callControls.height + callControls.y.toInt() + 16.dp - if (startCallControls.isVisible) { - behavior.peekHeight = max(behavior.peekHeight, controlPeakHeight) - } else { - behavior.peekHeight = controlPeakHeight - } - frame.minimumHeight = coordinator.height / minFrameHeightDenominator - behavior.maxHeight = (coordinator.height.toFloat() * maxBehaviorHeightPercentage).toInt() - - webRtcCallView.post { onControlTopChanged() } - } - } - - webRtcCallView.addWindowInsetsListener(object : InsetAwareConstraintLayout.WindowInsetsListener { - override fun onApplyWindowInsets(statusBar: Int, navigationBar: Int, parentStart: Int, parentEnd: Int) { - if (navigationBar > 0) { - callControls.padding(bottom = navigationBar) - } - } - }) - - overflowPopupWindow.setOnDismissListener { - hide(delay = HIDE_CONTROL_DELAY) - } - - activity - .supportFragmentManager - .setFragmentResultListener(EditCallLinkNameDialogFragment.RESULT_KEY, activity) { resultKey, bundle -> - if (bundle.containsKey(resultKey)) { - setName(bundle.getString(resultKey)!!) - } - } - - frame.background = MaterialShapeDrawable( - ShapeAppearanceModel.builder() - .setTopLeftCorner(CornerFamily.ROUNDED, 18.dp.toFloat()) - .setTopRightCorner(CornerFamily.ROUNDED, 18.dp.toFloat()) - .build() - ).apply { - fillColor = ColorStateList.valueOf(ContextCompat.getColor(activity, R.color.signal_colorSurface)) - } - - behavior.isHideable = true - behavior.peekHeight = 0 - BottomSheetBehaviorHack.setNestedScrollingChild(behavior, callInfoComposeView) - - behavior.addBottomSheetCallback(object : BottomSheetCallback() { - @SuppressLint("SwitchIntDef") - override fun onStateChanged(bottomSheet: View, newState: Int) { - overflowPopupWindow.dismiss() - when (newState) { - BottomSheetBehavior.STATE_COLLAPSED -> { - controlsAndInfoViewModel.resetScrollState() - if (controlState.isFadeOutEnabled) { - hide(delay = HIDE_CONTROL_DELAY) - } - updateCallSheetVisibilities(0f) - } - BottomSheetBehavior.STATE_EXPANDED -> { - cancelScheduledHide() - updateCallSheetVisibilities(1f) - } - BottomSheetBehavior.STATE_DRAGGING -> { - cancelScheduledHide() - } - BottomSheetBehavior.STATE_HIDDEN -> { - controlsAndInfoViewModel.resetScrollState() - updateCallSheetVisibilities(-1f) - } - } - } - - override fun onSlide(bottomSheet: View, slideOffset: Float) { - if (slideOffset <= 1 || slideOffset >= -1) { - updateCallSheetVisibilities(slideOffset) - } - } - }) - - callInfoComposeView.apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - val nestedScrollInterop = rememberNestedScrollInteropConnection() - - SignalTheme( - isDarkMode = true - ) { - Surface { - CallInfoView.View(viewModel, controlsAndInfoViewModel, callInfoCallbacks, Modifier.nestedScroll(nestedScrollInterop)) - } - } - } - } - - callInfoComposeView.alpha = 0f - callInfoComposeView.translationY = INFO_TRANSLATION_DISTANCE - } - - fun addVisibilityListener(listener: CallControlsVisibilityListener): Boolean { - return callControlsVisibilityListeners.add(listener) - } - - fun onStateRestored() { - when (behavior.state) { - BottomSheetBehavior.STATE_EXPANDED -> { - showCallInfo() - updateCallSheetVisibilities(1f) - } - BottomSheetBehavior.STATE_HIDDEN -> { - hide() - updateCallSheetVisibilities(-1f) - } - else -> { - showControls() - updateCallSheetVisibilities(0f) - } - } - } - - fun showCallInfo() { - cancelScheduledHide() - - behavior.isHideable = false - behavior.state = BottomSheetBehavior.STATE_EXPANDED - } - - private fun showControls() { - cancelScheduledHide() - behavior.isHideable = false - behavior.state = BottomSheetBehavior.STATE_COLLAPSED - - callControlsVisibilityListeners.forEach { it.onShown() } - } - - private fun hide(delay: Long = 0L) { - if (delay == 0L) { - if (controlState.isFadeOutEnabled || controlState == WebRtcControls.PIP || controlState.displayErrorControls() || controlState.displayIncomingCallButtons()) { - behavior.isHideable = true - behavior.state = BottomSheetBehavior.STATE_HIDDEN - - callControlsVisibilityListeners.forEach { it.onHidden() } - } - } else { - cancelScheduledHide() - handler?.postDelayed(scheduleHideControlsRunnable, delay) - } - } - - fun toggleControls() { - if (behavior.state == BottomSheetBehavior.STATE_EXPANDED || behavior.state == BottomSheetBehavior.STATE_HIDDEN) { - showControls() - } else { - hide() - } - } - - fun toggleOverflowPopup() { - if (overflowPopupWindow.isShowing) { - overflowPopupWindow.dismiss() - } else { - cancelScheduledHide() - overflowPopupWindow.show(aboveControlsGuideline) - } - } - - fun updateControls(newControlState: WebRtcControls) { - val previousState = controlState - controlState = newControlState - - showOrHideControlsOnUpdate(previousState) - - if (controlState == WebRtcControls.PIP) { - waitingToBeLetIn.visible = false - toggleCameraDirectionView.visible = false - } - - if (controlState != WebRtcControls.PIP && controlState.controlVisibilitiesChanged(previousState)) { - updateControlVisibilities() - } - } - - fun restartHideControlsTimer() { - hide(delay = HIDE_CONTROL_DELAY) - } - - private fun updateCallSheetVisibilities(slideOffset: Float) { - callControls.alpha = alphaControls(slideOffset) - callControls.visible = callControls.alpha > 0f - - callInfoComposeView.alpha = alphaCallInfo(slideOffset) - callInfoComposeView.translationY = INFO_TRANSLATION_DISTANCE - (INFO_TRANSLATION_DISTANCE * callInfoComposeView.alpha) - } - - private fun onControlTopChanged() { - val guidelineTop = max(frame.top, coordinator.height - behavior.peekHeight) - aboveControlsGuideline.setGuidelineBegin(guidelineTop) - webRtcCallView.onControlTopChanged() - } - - private fun showOrHideControlsOnUpdate(previousState: WebRtcControls) { - if (controlState == WebRtcControls.PIP || controlState.displayErrorControls() || controlState.displayIncomingCallButtons()) { - hide() - return - } - - if (controlState.hideControlsSheetInitially()) { - return - } - - if (previousState.hideControlsSheetInitially() && (previousState != WebRtcControls.PIP)) { - showControls() - return - } - - if (controlState.isFadeOutEnabled) { - if (!previousState.isFadeOutEnabled) { - hide(delay = HIDE_CONTROL_DELAY) - } - } else { - cancelScheduledHide() - if (behavior.state != BottomSheetBehavior.STATE_EXPANDED) { - showControls() - } - } - } - - private fun updateControlVisibilities() { - TransitionManager.endTransitions(callControls) - TransitionManager.beginDelayedTransition( - callControls, - AutoTransition().apply { - ordering = TransitionSet.ORDERING_TOGETHER - duration = CONTROL_TRANSITION_DURATION - } - ) - - val constraints = ConstraintSet().apply { - clone(callControls) - val margin = if (controlState.displaySmallCallButtons()) 4.dp else 8.dp - - setControlConstraints(R.id.call_screen_speaker_toggle, controlState.displayAudioToggle(), margin) - setControlConstraints(R.id.call_screen_video_toggle, controlState.displayVideoToggle(), margin) - setControlConstraints(R.id.call_screen_audio_mic_toggle, controlState.displayMuteAudio(), margin) - setControlConstraints(R.id.call_screen_audio_ring_toggle, controlState.displayRingToggle(), margin) - setControlConstraints(R.id.call_screen_overflow_button, controlState.displayOverflow(), margin) - setControlConstraints(R.id.call_screen_end_call, controlState.displayEndCall(), margin) - } - - constraints.applyTo(callControls) - - toggleCameraDirectionView.visible = controlState.displayCameraToggle() - waitingToBeLetIn.visible = controlState.displayWaitingToBeLetIn() - - if (controlState.displayWaitingToBeLetIn()) { - waitingToBeLetInProgressDrawable.setVisible(true, false) - } else { - waitingToBeLetInProgressDrawable.stop() - } - } - - private fun onScheduledHide() { - if (behavior.state != BottomSheetBehavior.STATE_EXPANDED && !isDisposed) { - hide() - } - } - - private fun cancelScheduledHide() { - handler?.removeCallbacks(scheduleHideControlsRunnable) - } - - private fun setName(name: String) { - controlsAndInfoViewModel.setName(name) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeBy( - onSuccess = { - if (it !is UpdateCallLinkResult.Update) { - Log.w(TAG, "Failed to set name. $it") - toastFailure() - } - }, - onError = handleError("setName") - ) - .addTo(disposables) - } - - private fun handleError(method: String): (throwable: Throwable) -> Unit { - return { - Log.w(TAG, "Failure during $method", it) - toastFailure() - } - } - - private fun toastFailure() { - Toast.makeText(activity, R.string.CallLinkDetailsFragment__couldnt_save_changes, Toast.LENGTH_LONG).show() - } - - private fun ConstraintSet.setControlConstraints(@IdRes viewId: Int, visible: Boolean, @Px horizontalMargins: Int) { - setVisibility(viewId, if (visible) View.VISIBLE else View.GONE) - setMargin(viewId, ConstraintSet.START, horizontalMargins) - setMargin(viewId, ConstraintSet.END, horizontalMargins) - } - - private fun WebRtcControls.controlVisibilitiesChanged(previousState: WebRtcControls): Boolean { - return displayAudioToggle() != previousState.displayAudioToggle() || - displayCameraToggle() != previousState.displayCameraToggle() || - displayVideoToggle() != previousState.displayVideoToggle() || - displayMuteAudio() != previousState.displayMuteAudio() || - displayRingToggle() != previousState.displayRingToggle() || - displayOverflow() != previousState.displayOverflow() || - displayEndCall() != previousState.displayEndCall() || - displayWaitingToBeLetIn() != previousState.displayWaitingToBeLetIn() || - (previousState == WebRtcControls.PIP && this != WebRtcControls.PIP) - } - - private fun alphaControls(slideOffset: Float): Float { - return if (slideOffset <= CONTROL_FADE_OUT_START) { - 1f - } else if (slideOffset >= CONTROL_FADE_OUT_DONE) { - 0f - } else { - 1f - (1f * (slideOffset - CONTROL_FADE_OUT_START) / (CONTROL_FADE_OUT_DONE - CONTROL_FADE_OUT_START)) - } - } - - private fun alphaCallInfo(slideOffset: Float): Float { - return if (slideOffset >= INFO_FADE_IN_DONE) { - 1f - } else if (slideOffset <= INFO_FADE_IN_START) { - 0f - } else { - (1f * (slideOffset - INFO_FADE_IN_START) / (INFO_FADE_IN_DONE - INFO_FADE_IN_START)) - } - } - - @Parcelize - private data class HeightData( - val controlHeight: Int = 0, - val coordinatorHeight: Int = 0 - ) : Parcelable { - fun hasChanged(controlHeight: Int, coordinatorHeight: Int): Boolean { - return controlHeight != this.controlHeight || coordinatorHeight != this.coordinatorHeight - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantUpdatePopup.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantUpdatePopup.kt index 70eaf91b3e..aa78921d4b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantUpdatePopup.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantUpdatePopup.kt @@ -5,27 +5,16 @@ package org.thoughtcrime.securesms.components.webrtc.v2 -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.MutableTransitionState -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import android.content.Context import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -37,10 +26,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import kotlinx.coroutines.delay import org.signal.core.ui.compose.Buttons import org.signal.core.ui.compose.NightPreview import org.signal.core.ui.compose.Previews @@ -48,7 +34,6 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.avatar.AvatarImage import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImageSmall import org.thoughtcrime.securesms.components.webrtc.CallParticipantListUpdate -import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow import org.thoughtcrime.securesms.components.webrtc.v2.CallParticipantUpdatePopupController.DisplayState import org.thoughtcrime.securesms.events.CallParticipant import org.thoughtcrime.securesms.events.CallParticipantId @@ -65,36 +50,16 @@ fun CallParticipantUpdatePopup( controller: CallParticipantUpdatePopupController, modifier: Modifier = Modifier ) { - val transitionState = remember { MutableTransitionState(controller.displayState != DisplayState.NONE) } - transitionState.targetState = controller.displayState != DisplayState.NONE - - LaunchedEffect(transitionState.isIdle) { - if (transitionState.isIdle && !transitionState.targetState) { - controller.updateDisplay() - } - } - - AnimatedVisibility( - visibleState = transitionState, - enter = slideInVertically { fullHeight -> -fullHeight } + fadeIn(), - exit = slideOutVertically { fullHeight -> -fullHeight } + fadeOut(), + CallScreenPopup( + visible = controller.displayState != DisplayState.NONE, + onDismiss = { controller.hide() }, + displayDuration = controller.displayDuration, + onTransitionComplete = { controller.updateDisplay() }, modifier = modifier - .heightIn(min = 96.dp) - .fillMaxWidth() ) { - LaunchedEffect(controller.displayState, controller.participants) { - if (controller.displayState != DisplayState.NONE) { - delay(controller.displayDuration) - controller.hide() - } - } - PopupContent( displayState = controller.displayState, - participants = controller.participants, - onClick = { - controller.hide() - } + participants = controller.participants ) } } @@ -105,8 +70,7 @@ fun CallParticipantUpdatePopup( @Composable private fun PopupContent( displayState: DisplayState, - participants: Set, - onClick: () -> Unit + participants: Set ) { val context = LocalContext.current @@ -135,7 +99,7 @@ private fun PopupContent( previousDisplayState = displayStateForDescription if (participants.isNotEmpty()) { - previousDescription = CallParticipantsListUpdatePopupWindow.getDescriptionForRecipients( + previousDescription = getDescriptionForRecipients( context, participants, displayStateForDescription == DisplayState.ADD @@ -147,18 +111,7 @@ private fun PopupContent( } Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .wrapContentSize() - .padding(start = 12.dp, top = 30.dp, end = 12.dp) - .background( - color = colorResource(R.color.signal_light_colorSecondaryContainer), - shape = RoundedCornerShape(percent = 50) - ) - .clickable( - onClick = onClick, - role = Role.Button - ) + verticalAlignment = Alignment.CenterVertically ) { Box( modifier = Modifier.size(48.dp) @@ -173,7 +126,8 @@ private fun PopupContent( BadgeImageSmall( badge = avatarRecipient.featuredBadge, - modifier = Modifier.padding(top = 28.dp, start = 28.dp) + modifier = Modifier + .padding(top = 28.dp, start = 28.dp) .size(16.dp) ) } @@ -181,11 +135,48 @@ private fun PopupContent( Text( text = description, color = colorResource(R.color.signal_light_colorOnSecondaryContainer), - modifier = Modifier.padding(vertical = 14.dp).padding(start = 10.dp, end = 24.dp) + modifier = Modifier + .padding(vertical = 14.dp) + .padding(start = 10.dp, end = 24.dp) ) } } +private fun getDescriptionForRecipients( + context: Context, + recipients: Set, + isAdded: Boolean +): String { + val iterator = recipients.iterator() + return when (recipients.size) { + 0 -> throw IllegalArgumentException("Recipients must contain 1 or more entries") + 1 -> context.getString(getOneMemberDescriptionResourceId(isAdded), getNextDisplayName(context, iterator)) + 2 -> context.getString(getTwoMemberDescriptionResourceId(isAdded), getNextDisplayName(context, iterator), getNextDisplayName(context, iterator)) + 3 -> context.getString(getThreeMemberDescriptionResourceId(isAdded), getNextDisplayName(context, iterator), getNextDisplayName(context, iterator), getNextDisplayName(context, iterator)) + else -> context.resources.getQuantityString(getManyMemberDescriptionResourceId(isAdded), recipients.size - 2, getNextDisplayName(context, iterator), getNextDisplayName(context, iterator), recipients.size - 2) + } +} + +private fun getNextDisplayName(context: Context, iterator: Iterator): String { + return iterator.next().callParticipant.getRecipientDisplayName(context) +} + +private fun getOneMemberDescriptionResourceId(isAdded: Boolean): Int { + return if (isAdded) R.string.CallParticipantsListUpdatePopupWindow__s_joined else R.string.CallParticipantsListUpdatePopupWindow__s_left +} + +private fun getTwoMemberDescriptionResourceId(isAdded: Boolean): Int { + return if (isAdded) R.string.CallParticipantsListUpdatePopupWindow__s_and_s_joined else R.string.CallParticipantsListUpdatePopupWindow__s_and_s_left +} + +private fun getThreeMemberDescriptionResourceId(isAdded: Boolean): Int { + return if (isAdded) R.string.CallParticipantsListUpdatePopupWindow__s_s_and_s_joined else R.string.CallParticipantsListUpdatePopupWindow__s_s_and_s_left +} + +private fun getManyMemberDescriptionResourceId(isAdded: Boolean): Int { + return if (isAdded) R.plurals.CallParticipantsListUpdatePopupWindow__s_s_and_d_others_joined else R.plurals.CallParticipantsListUpdatePopupWindow__s_s_and_d_others_left +} + /** * Controller owned by the [CallScreenMediator] which allows its callbacks to control this popup. */ @@ -265,8 +256,7 @@ private fun PopupContentPreview() { Previews.Preview { PopupContent( displayState = DisplayState.ADD, - participants = participants.take(1).map { CallParticipantListUpdate.createWrapper(it) }.toSet(), - onClick = {} + participants = participants.take(1).map { CallParticipantListUpdate.createWrapper(it) }.toSet() ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantViewer.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantViewer.kt index bb63448fa7..123582989a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantViewer.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallParticipantViewer.kt @@ -70,8 +70,6 @@ import org.webrtc.RendererCommon * Displays a remote participant (or local participant in pre-join screen). * Handles both full-size grid view and system PIP mode. * - * This is a Compose reimplementation of [org.thoughtcrime.securesms.components.webrtc.CallParticipantView]. - * * @param participant The call participant to display * @param renderInPip Whether rendering in system PIP mode (smaller, simplified UI) * @param raiseHandAllowed Whether to show raise hand indicator diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt index 3e14e5561d..c5a73ca409 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt @@ -114,7 +114,10 @@ fun CallScreen( onLocalPictureInPictureClicked: () -> Unit, onLocalPictureInPictureFocusClicked: () -> Unit, onControlsToggled: (Boolean) -> Unit, - onCallScreenDialogDismissed: () -> Unit = {} + onCallScreenDialogDismissed: () -> Unit = {}, + onWifiToCellularPopupDismissed: () -> Unit = {}, + onSwipeToSpeakerHintDismissed: () -> Unit = {}, + onRemoteMuteToastDismissed: () -> Unit = {} ) { if (webRtcCallState == WebRtcViewModel.State.CALL_INCOMING) { IncomingCallScreen( @@ -421,6 +424,30 @@ fun CallScreen( ) } + WifiToCellularPopup( + visible = callScreenState.displayWifiToCellularPopup, + onDismiss = onWifiToCellularPopupDismissed, + modifier = Modifier + .statusBarsPadding() + .fillMaxWidth() + ) + + SwipeToSpeakerHintPopup( + visible = callScreenState.displaySwipeToSpeakerHint, + onDismiss = onSwipeToSpeakerHintDismissed, + modifier = Modifier + .statusBarsPadding() + .fillMaxWidth() + ) + + RemoteMuteToastPopup( + message = callScreenState.remoteMuteToastMessage, + onDismiss = onRemoteMuteToastDismissed, + modifier = Modifier + .statusBarsPadding() + .fillMaxWidth() + ) + CallScreenDialog(callScreenDialogType, onCallScreenDialogDismissed) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenMediator.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenMediator.kt index 6988fb4705..0cac639b50 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenMediator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenMediator.kt @@ -11,7 +11,6 @@ import org.thoughtcrime.securesms.components.webrtc.CallParticipantListUpdate import org.thoughtcrime.securesms.components.webrtc.WebRtcControls import org.thoughtcrime.securesms.events.WebRtcViewModel 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 @@ -52,6 +51,7 @@ interface CallScreenMediator { fun enableParticipantUpdatePopup(enabled: Boolean) fun enableCallStateUpdatePopup(enabled: Boolean) fun showWifiToCellularPopupWindow() + fun showRemoteMuteToast(message: String) fun hideMissingPermissionsNotice() fun setStatusFromGroupCallState(context: Context, groupCallState: WebRtcViewModel.GroupCallState) { @@ -91,11 +91,7 @@ interface CallScreenMediator { companion object { fun create(activity: WebRtcCallActivity, viewModel: WebRtcCallViewModel): CallScreenMediator { - return if (RemoteConfig.newCallUi) { - ComposeCallScreenMediator(activity, viewModel) - } else { - ViewCallScreenMediator(activity, viewModel) - } + return ComposeCallScreenMediator(activity, viewModel) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenPopup.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenPopup.kt new file mode 100644 index 0000000000..1247a38bf5 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenPopup.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.webrtc.v2 + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay +import org.thoughtcrime.securesms.R +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +/** + * Common popup container for call screen notifications that slide in from the top. + * Used for participant updates, wifi-to-cellular notifications, etc. + * + * @param visible Whether the popup should be visible + * @param onDismiss Called when the popup is dismissed (either by timeout or user interaction) + * @param displayDuration How long to display the popup before auto-dismissing + * @param onTransitionComplete Called when the exit transition completes, useful for queuing next popups + * @param modifier Modifier for the outer container + * @param content The content to display inside the pill-shaped popup + */ +@Composable +fun CallScreenPopup( + visible: Boolean, + onDismiss: () -> Unit, + displayDuration: Duration = 4.seconds, + onTransitionComplete: (() -> Unit)? = null, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit +) { + val transitionState = remember { MutableTransitionState(visible) } + transitionState.targetState = visible + + LaunchedEffect(transitionState.isIdle) { + if (transitionState.isIdle && !transitionState.targetState) { + onTransitionComplete?.invoke() + } + } + + AnimatedVisibility( + visibleState = transitionState, + enter = slideInVertically { fullHeight -> -fullHeight } + fadeIn(), + exit = slideOutVertically { fullHeight -> -fullHeight } + fadeOut(), + modifier = modifier + .heightIn(min = 96.dp) + .fillMaxWidth() + ) { + LaunchedEffect(visible) { + if (visible) { + delay(displayDuration) + onDismiss() + } + } + + Box( + contentAlignment = Alignment.TopCenter, + modifier = Modifier + .wrapContentSize() + .padding(start = 12.dp, top = 30.dp, end = 12.dp) + .background( + color = colorResource(R.color.signal_light_colorSecondaryContainer), + shape = RoundedCornerShape(percent = 50) + ) + .clickable( + onClick = onDismiss, + role = Role.Button + ), + content = content + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenState.kt index 1aff92d8c0..86019576fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenState.kt @@ -27,6 +27,7 @@ data class CallScreenState( val displayVideoTooltip: Boolean = false, val displaySwipeToSpeakerHint: Boolean = false, val displayWifiToCellularPopup: Boolean = false, + val remoteMuteToastMessage: String? = null, val displayAdditionalActionsDialog: Boolean = false, val displayMissingPermissionsNotice: Boolean = false, val pendingParticipantsState: PendingParticipantsState? = null, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/ComposeCallScreenMediator.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/ComposeCallScreenMediator.kt index a5e757e2b4..9a3ac0dee8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/ComposeCallScreenMediator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/ComposeCallScreenMediator.kt @@ -37,7 +37,6 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment import org.thoughtcrime.securesms.components.webrtc.CallParticipantListUpdate import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState -import org.thoughtcrime.securesms.components.webrtc.CallReactionScrubber.Companion.CUSTOM_REACTION_BOTTOM_SHEET_TAG import org.thoughtcrime.securesms.components.webrtc.WebRtcControls import org.thoughtcrime.securesms.components.webrtc.controls.CallInfoView import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoViewModel @@ -61,6 +60,7 @@ class ComposeCallScreenMediator(private val activity: WebRtcCallActivity, viewMo companion object { private val TAG = Log.tag(ComposeCallScreenMediator::class) + private const val CUSTOM_REACTION_BOTTOM_SHEET_TAG = "CallReaction" } private val callScreenViewModel = ViewModelProvider(activity)[CallScreenViewModel::class] @@ -212,6 +212,9 @@ class ComposeCallScreenMediator(private val activity: WebRtcCallActivity, viewMo onLocalPictureInPictureFocusClicked = viewModel::onLocalPictureInPictureFocusClicked, onControlsToggled = onControlsToggled, onCallScreenDialogDismissed = { callScreenViewModel.dialog.update { CallScreenDialogType.NONE } }, + onWifiToCellularPopupDismissed = { callScreenViewModel.callScreenState.update { it.copy(displayWifiToCellularPopup = false) } }, + onSwipeToSpeakerHintDismissed = { callScreenViewModel.callScreenState.update { it.copy(displaySwipeToSpeakerHint = false) } }, + onRemoteMuteToastDismissed = { callScreenViewModel.callScreenState.update { it.copy(remoteMuteToastMessage = null) } }, callParticipantUpdatePopupController = callParticipantUpdatePopupController ) } @@ -355,6 +358,10 @@ class ComposeCallScreenMediator(private val activity: WebRtcCallActivity, viewMo callScreenViewModel.callScreenState.update { it.copy(displayWifiToCellularPopup = true) } } + override fun showRemoteMuteToast(message: String) { + callScreenViewModel.callScreenState.update { it.copy(remoteMuteToastMessage = message) } + } + override fun hideMissingPermissionsNotice() { callScreenViewModel.callScreenState.update { it.copy(displayMissingPermissionsNotice = false) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/MoveableLocalVideoRenderer.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/MoveableLocalVideoRenderer.kt index b1beaca10d..d01925e5aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/MoveableLocalVideoRenderer.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/MoveableLocalVideoRenderer.kt @@ -33,7 +33,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.dropShadow -import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.shadow.Shadow import androidx.compose.ui.graphics.vector.ImageVector @@ -41,7 +40,6 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpSize diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/RemoteMuteToastPopup.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/RemoteMuteToastPopup.kt new file mode 100644 index 0000000000..ea08b930d7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/RemoteMuteToastPopup.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.webrtc.v2 + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import org.signal.core.ui.compose.NightPreview +import org.signal.core.ui.compose.Previews +import org.thoughtcrime.securesms.R +import kotlin.time.Duration.Companion.seconds + +/** + * Popup shown when a user is remotely muted during a call. + */ +@Composable +fun RemoteMuteToastPopup( + message: String?, + onDismiss: () -> Unit, + modifier: Modifier = Modifier +) { + CallScreenPopup( + visible = message != null, + onDismiss = onDismiss, + displayDuration = 3.seconds, + modifier = modifier + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 12.dp) + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_mic_off_solid_18), + contentDescription = null, + tint = colorResource(R.color.signal_light_colorOnSecondaryContainer), + modifier = Modifier.size(18.dp) + ) + + Text( + text = message ?: "", + color = colorResource(R.color.signal_light_colorOnSecondaryContainer), + modifier = Modifier.padding(start = 8.dp) + ) + } + } +} + +@NightPreview +@Composable +private fun RemoteMuteToastPopupPreview() { + Previews.Preview { + RemoteMuteToastPopup( + message = "Alex muted you", + onDismiss = {} + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/SwipeToSpeakerHintPopup.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/SwipeToSpeakerHintPopup.kt new file mode 100644 index 0000000000..00a155edff --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/SwipeToSpeakerHintPopup.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.webrtc.v2 + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import org.signal.core.ui.compose.NightPreview +import org.signal.core.ui.compose.Previews +import org.thoughtcrime.securesms.R +import kotlin.time.Duration.Companion.seconds + +/** + * Popup shown to hint the user that they can swipe to view screen share. + */ +@Composable +fun SwipeToSpeakerHintPopup( + visible: Boolean, + onDismiss: () -> Unit, + modifier: Modifier = Modifier +) { + CallScreenPopup( + visible = visible, + onDismiss = onDismiss, + displayDuration = 3.seconds, + modifier = modifier + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 12.dp) + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.symbol_arrow_down_24), + contentDescription = null, + tint = colorResource(R.color.signal_light_colorOnSecondaryContainer), + modifier = Modifier.size(24.dp) + ) + + Text( + text = stringResource(R.string.CallToastPopupWindow__swipe_to_view_screen_share), + color = colorResource(R.color.signal_light_colorOnSecondaryContainer), + modifier = Modifier.padding(start = 8.dp) + ) + } + } +} + +@NightPreview +@Composable +private fun SwipeToSpeakerHintPopupPreview() { + Previews.Preview { + SwipeToSpeakerHintPopup( + visible = true, + onDismiss = {} + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/ViewCallScreenMediator.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/ViewCallScreenMediator.kt deleted file mode 100644 index 22185b870d..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/ViewCallScreenMediator.kt +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright 2025 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.webrtc.v2 - -import android.view.View -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.util.visible -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() - } - - override fun hideMissingPermissionsNotice() { - callScreen.findViewById(R.id.missing_permissions_container).visible = false - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallActivity.kt index ecbcb50a26..5ddbd93461 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallActivity.kt @@ -56,8 +56,6 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.sensors.Orientation import org.thoughtcrime.securesms.components.webrtc.CallLinkProfileKeySender import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState -import org.thoughtcrime.securesms.components.webrtc.CallReactionScrubber -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 @@ -87,7 +85,6 @@ import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.ThrottledDebouncer import org.thoughtcrime.securesms.util.VibrateUtil -import org.thoughtcrime.securesms.util.WindowUtil import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.ChosenAudioDeviceIdentifier @@ -102,6 +99,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re private const val STANDARD_DELAY_FINISH = 1000L private const val VIBRATE_DURATION = 50 + private const val CUSTOM_REACTION_BOTTOM_SHEET_TAG = "CallReaction" } private lateinit var callScreen: CallScreenMediator @@ -182,10 +180,6 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re initializePendingParticipantFragmentListener() - if (!RemoteConfig.newCallUi) { - WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_dark_colorSurface)) - } - if (!hasCameraPermission() && !hasAudioPermission()) { askCameraAudioPermissions { callScreen.setMicEnabled(viewModel.microphoneEnabled.value) @@ -460,10 +454,6 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re private fun initializeViewModel() { val orientation: Orientation = resolveOrientationFromContext() - if (orientation == Orientation.PORTRAIT_BOTTOM_EDGE && !RemoteConfig.newCallUi) { - WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_dark_colorSurface2)) - WindowUtil.clearTranslucentNavigationBar(window) - } AppDependencies.signalCallManager.orientationChanged(true, orientation.degrees) @@ -624,7 +614,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re private fun registerSystemPipChangeListeners() { addOnPictureInPictureModeChangedListener { - CallReactionScrubber.dismissCustomEmojiBottomSheet(supportFragmentManager) + (supportFragmentManager.findFragmentByTag(CUSTOM_REACTION_BOTTOM_SHEET_TAG) as? ReactWithAnyEmojiBottomSheetDialogFragment)?.dismissNow() } } @@ -879,8 +869,8 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re 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(rootView()) - is CallEvent.ShowRemoteMuteToast -> CallToastPopupWindow.show(rootView(), R.drawable.ic_mic_off_solid_18, event.getDescription(this)) + is CallEvent.ShowSwipeToSpeakerHint -> callScreen.showSpeakerViewHint() + is CallEvent.ShowRemoteMuteToast -> callScreen.showRemoteMuteToast(event.getDescription(this)) is CallEvent.ShowVideoTooltip -> { if (isInPipMode()) return @@ -1104,12 +1094,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re private val fullScreenHelper: FullscreenHelper = FullscreenHelper(this@WebRtcCallActivity) init { - fullScreenHelper.showAndHideWithSystemUI( - window, - findViewById(R.id.call_screen_header_gradient), - findViewById(R.id.webrtc_call_view_toolbar_text), - findViewById(R.id.webrtc_call_view_toolbar_no_text) - ) + fullScreenHelper.showAndHideWithSystemUI(window) } override fun onShown() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WifiToCellularPopup.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WifiToCellularPopup.kt new file mode 100644 index 0000000000..078e2c5d8a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WifiToCellularPopup.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.webrtc.v2 + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.signal.core.ui.compose.NightPreview +import org.signal.core.ui.compose.Previews +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.util.VibrateUtil +import kotlin.time.Duration.Companion.seconds + +private const val VIBRATE_DURATION_MS = 50 + +/** + * Popup shown when the device is connected to a WiFi and cellular network, and WiFi is unusable for + * RingRTC, causing a switch to cellular. + */ +@Composable +fun WifiToCellularPopup( + visible: Boolean, + onDismiss: () -> Unit, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + + LaunchedEffect(visible) { + if (visible) { + VibrateUtil.vibrate(context, VIBRATE_DURATION_MS) + } + } + + CallScreenPopup( + visible = visible, + onDismiss = onDismiss, + displayDuration = 4.seconds, + modifier = modifier + ) { + Text( + text = stringResource(R.string.WifiToCellularPopupWindow__weak_wifi_switched_to_cellular), + color = colorResource(R.color.signal_light_colorOnSecondaryContainer), + modifier = Modifier.padding(horizontal = 24.dp, vertical = 14.dp) + ) + } +} + +@NightPreview +@Composable +private fun WifiToCellularPopupPreview() { + Previews.Preview { + WifiToCellularPopup( + visible = true, + onDismiss = {} + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt index 89a49387f5..b2f9fb9d1e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt @@ -1114,14 +1114,6 @@ object RemoteConfig { hotSwappable = true ) - @JvmStatic - @get:JvmName("newCallUi") - val newCallUi: Boolean by remoteBoolean( - key = "android.newCallUi.2", - defaultValue = false, - hotSwappable = false - ) - @JvmStatic @get:JvmName("useHevcEncoder") val useHevcEncoder: Boolean by remoteBoolean( diff --git a/app/src/main/res/layout-land/webrtc_call_controls.xml b/app/src/main/res/layout-land/webrtc_call_controls.xml deleted file mode 100644 index 382ac50d32..0000000000 --- a/app/src/main/res/layout-land/webrtc_call_controls.xml +++ /dev/null @@ -1,265 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/webrtc_call_participant_overflow_recycler.xml b/app/src/main/res/layout-land/webrtc_call_participant_overflow_recycler.xml deleted file mode 100644 index 0970b424ee..0000000000 --- a/app/src/main/res/layout-land/webrtc_call_participant_overflow_recycler.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/webrtc_call_participant_pager.xml b/app/src/main/res/layout-land/webrtc_call_participant_pager.xml deleted file mode 100644 index c9f69d400b..0000000000 --- a/app/src/main/res/layout-land/webrtc_call_participant_pager.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/layout-land/webrtc_call_view_incoming_call_buttons.xml b/app/src/main/res/layout-land/webrtc_call_view_incoming_call_buttons.xml deleted file mode 100644 index 155cceaf4e..0000000000 --- a/app/src/main/res/layout-land/webrtc_call_view_incoming_call_buttons.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_overflow_holder.xml b/app/src/main/res/layout/call_overflow_holder.xml deleted file mode 100644 index 71d26510d5..0000000000 --- a/app/src/main/res/layout/call_overflow_holder.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_overflow_popup.xml b/app/src/main/res/layout/call_overflow_popup.xml deleted file mode 100644 index 8fc37bcd3d..0000000000 --- a/app/src/main/res/layout/call_overflow_popup.xml +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_participant_item.xml b/app/src/main/res/layout/call_participant_item.xml deleted file mode 100644 index 0d744ba636..0000000000 --- a/app/src/main/res/layout/call_participant_item.xml +++ /dev/null @@ -1,194 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/call_participant_list_update.xml b/app/src/main/res/layout/call_participant_list_update.xml deleted file mode 100644 index ca449f1710..0000000000 --- a/app/src/main/res/layout/call_participant_list_update.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_participants_list_dialog.xml b/app/src/main/res/layout/call_participants_list_dialog.xml deleted file mode 100644 index 9dac0a7c60..0000000000 --- a/app/src/main/res/layout/call_participants_list_dialog.xml +++ /dev/null @@ -1,6 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_participants_list_header.xml b/app/src/main/res/layout/call_participants_list_header.xml deleted file mode 100644 index fb993d49f4..0000000000 --- a/app/src/main/res/layout/call_participants_list_header.xml +++ /dev/null @@ -1,13 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_participants_list_item.xml b/app/src/main/res/layout/call_participants_list_item.xml deleted file mode 100644 index 5f5a4fb335..0000000000 --- a/app/src/main/res/layout/call_participants_list_item.xml +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/call_screen_call_link_warning_card.xml b/app/src/main/res/layout/call_screen_call_link_warning_card.xml deleted file mode 100644 index 1bff8c7dc3..0000000000 --- a/app/src/main/res/layout/call_screen_call_link_warning_card.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_screen_pending_participants_view.xml b/app/src/main/res/layout/call_screen_pending_participants_view.xml deleted file mode 100644 index 021b58d7db..0000000000 --- a/app/src/main/res/layout/call_screen_pending_participants_view.xml +++ /dev/null @@ -1,15 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_state_update.xml b/app/src/main/res/layout/call_state_update.xml deleted file mode 100644 index c7b411c6ce..0000000000 --- a/app/src/main/res/layout/call_state_update.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_toast_popup_window.xml b/app/src/main/res/layout/call_toast_popup_window.xml deleted file mode 100644 index bd6e58c163..0000000000 --- a/app/src/main/res/layout/call_toast_popup_window.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/group_call_call_full.xml b/app/src/main/res/layout/group_call_call_full.xml deleted file mode 100644 index d6e9360262..0000000000 --- a/app/src/main/res/layout/group_call_call_full.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/group_call_participant_item.xml b/app/src/main/res/layout/group_call_participant_item.xml deleted file mode 100644 index b836458c10..0000000000 --- a/app/src/main/res/layout/group_call_participant_item.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/group_call_speaker_hint.xml b/app/src/main/res/layout/group_call_speaker_hint.xml deleted file mode 100644 index 0285227235..0000000000 --- a/app/src/main/res/layout/group_call_speaker_hint.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/webrtc_call_activity.xml b/app/src/main/res/layout/webrtc_call_activity.xml deleted file mode 100644 index f0425dc2ff..0000000000 --- a/app/src/main/res/layout/webrtc_call_activity.xml +++ /dev/null @@ -1,10 +0,0 @@ - - diff --git a/app/src/main/res/layout/webrtc_call_controls.xml b/app/src/main/res/layout/webrtc_call_controls.xml deleted file mode 100644 index 7516b1ede6..0000000000 --- a/app/src/main/res/layout/webrtc_call_controls.xml +++ /dev/null @@ -1,262 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/webrtc_call_participant_overflow_recycler.xml b/app/src/main/res/layout/webrtc_call_participant_overflow_recycler.xml deleted file mode 100644 index 6b553ec026..0000000000 --- a/app/src/main/res/layout/webrtc_call_participant_overflow_recycler.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/layout/webrtc_call_participant_pager.xml b/app/src/main/res/layout/webrtc_call_participant_pager.xml deleted file mode 100644 index fe90aa437f..0000000000 --- a/app/src/main/res/layout/webrtc_call_participant_pager.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/layout/webrtc_call_participant_recycler_empty_item.xml b/app/src/main/res/layout/webrtc_call_participant_recycler_empty_item.xml deleted file mode 100644 index 0f1ae19712..0000000000 --- a/app/src/main/res/layout/webrtc_call_participant_recycler_empty_item.xml +++ /dev/null @@ -1,8 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/webrtc_call_participant_recycler_item.xml b/app/src/main/res/layout/webrtc_call_participant_recycler_item.xml deleted file mode 100644 index 6ebef7756d..0000000000 --- a/app/src/main/res/layout/webrtc_call_participant_recycler_item.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/webrtc_call_participants_layout.xml b/app/src/main/res/layout/webrtc_call_participants_layout.xml deleted file mode 100644 index 8c179d59c8..0000000000 --- a/app/src/main/res/layout/webrtc_call_participants_layout.xml +++ /dev/null @@ -1,14 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/webrtc_call_view.xml b/app/src/main/res/layout/webrtc_call_view.xml deleted file mode 100644 index 5f2eeab7f8..0000000000 --- a/app/src/main/res/layout/webrtc_call_view.xml +++ /dev/null @@ -1,238 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/webrtc_call_view_header_large.xml b/app/src/main/res/layout/webrtc_call_view_header_large.xml deleted file mode 100644 index b18168d16f..0000000000 --- a/app/src/main/res/layout/webrtc_call_view_header_large.xml +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/webrtc_call_view_incoming_call_buttons.xml b/app/src/main/res/layout/webrtc_call_view_incoming_call_buttons.xml deleted file mode 100644 index 6bacf2661e..0000000000 --- a/app/src/main/res/layout/webrtc_call_view_incoming_call_buttons.xml +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/wifi_to_cellular_popup.xml b/app/src/main/res/layout/wifi_to_cellular_popup.xml deleted file mode 100644 index b39ef6b0bb..0000000000 --- a/app/src/main/res/layout/wifi_to_cellular_popup.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - -