diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.java index 68306dac9..297999946 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsState.java @@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.events.CallParticipant; import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.ringrtc.CameraState; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -17,7 +18,9 @@ import java.util.List; */ public final class CallParticipantsState { - public static final CallParticipantsState STARTING_STATE = new CallParticipantsState(WebRtcViewModel.State.CALL_DISCONNECTED, + private static final int SMALL_GROUP_MAX = 6; + + public static final CallParticipantsState STARTING_STATE = new CallParticipantsState(WebRtcViewModel.State.CALL_DISCONNECTED, Collections.emptyList(), CallParticipant.createLocal(CameraState.UNKNOWN, new BroadcastVideoSink(null), false), null, @@ -55,21 +58,29 @@ public final class CallParticipantsState { } public @NonNull List getGridParticipants() { - if (getAllRemoteParticipants().size() > 6) { - return getAllRemoteParticipants().subList(0, 6); + if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) { + return getAllRemoteParticipants().subList(0, SMALL_GROUP_MAX); } else { return getAllRemoteParticipants(); } } public @NonNull List getListParticipants() { + List listParticipants = new ArrayList<>(); + if (isViewingFocusedParticipant && getAllRemoteParticipants().size() > 1) { - return getAllRemoteParticipants().subList(1, getAllRemoteParticipants().size()); - } else if (getAllRemoteParticipants().size() > 6) { - return getAllRemoteParticipants().subList(6, getAllRemoteParticipants().size()); + listParticipants.addAll(getAllRemoteParticipants().subList(1, getAllRemoteParticipants().size())); + } else if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) { + listParticipants.addAll(getAllRemoteParticipants().subList(SMALL_GROUP_MAX, getAllRemoteParticipants().size())); } else { return Collections.emptyList(); } + + listParticipants.add(CallParticipant.EMPTY); + + Collections.reverse(listParticipants); + + return listParticipants; } public @NonNull List getAllRemoteParticipants() { @@ -88,6 +99,10 @@ public final class CallParticipantsState { return localRenderState; } + public boolean isLargeVideoGroup() { + return getAllRemoteParticipants().size() > SMALL_GROUP_MAX; + } + public boolean isInPipMode() { return isInPipMode; } 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 index 600ac7ce3..cc03f42ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureGestureHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureGestureHelper.java @@ -9,6 +9,7 @@ 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 androidx.annotation.NonNull; @@ -16,7 +17,6 @@ import androidx.core.view.GestureDetectorCompat; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.animation.AnimationCompleteListener; -import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout; import java.util.Arrays; @@ -26,11 +26,14 @@ import java.util.Queue; public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestureListener { - private static final float DECELERATION_RATE = 0.99f; + 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 final ViewGroup parent; + private final View child; + private final int framePadding; + private final Queue runAfterFling; private int pipWidth; private int pipHeight; @@ -46,7 +49,7 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu private VelocityTracker velocityTracker; private int maximumFlingVelocity; private boolean isLockedToBottomEnd; - private Queue runAfterFling; + private Interpolator interpolator; @SuppressLint("ClickableViewAccessibility") public static PictureInPictureGestureHelper applyTo(@NonNull View child) { @@ -101,6 +104,7 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu this.pipHeight = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_pip_height); this.maximumFlingVelocity = ViewConfiguration.get(child.getContext()).getScaledMaximumFlingVelocity(); this.runAfterFling = new LinkedList<>(); + this.interpolator = ADJUST_INTERPOLATOR; } public void clearVerticalBoundaries() { @@ -130,8 +134,12 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu pipHeight = child.getMeasuredHeight(); if (isAnimating) { + interpolator = ADJUST_INTERPOLATOR; + fling(); } else if (!isDragging) { + interpolator = ADJUST_INTERPOLATOR; + onFling(null, null, 0, 0); } } @@ -160,6 +168,7 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu isDragging = true; pipWidth = child.getMeasuredWidth(); pipHeight = child.getMeasuredHeight(); + interpolator = FLING_INTERPOLATOR; return true; } @@ -216,7 +225,7 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu .translationX(getTranslationXForPoint(nearestCornerPosition)) .translationY(getTranslationYForPoint(nearestCornerPosition)) .setDuration(250) - .setInterpolator(new ViscousFluidInterpolator()) + .setInterpolator(interpolator) .setListener(new AnimationCompleteListener() { @Override public void onAnimationEnd(Animator animation) { 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 index 381ae18ed..8ba176d0e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsRecyclerAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallParticipantsRecyclerAdapter.java @@ -14,13 +14,20 @@ import org.thoughtcrime.securesms.events.CallParticipant; class WebRtcCallParticipantsRecyclerAdapter extends ListAdapter { + private static final int PARTICIPANT = 0; + private static final int EMPTY = 1; + protected WebRtcCallParticipantsRecyclerAdapter() { super(new DiffCallback()); } @Override public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.webrtc_call_participant_recycler_item, parent, false)); + 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 @@ -28,15 +35,29 @@ class WebRtcCallParticipantsRecyclerAdapter extends ListAdapter previouslyVisibleViewSet, boolean useSmallMargins) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java index aa2e42fbe..b428ed7cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java @@ -147,6 +147,18 @@ public class WebRtcCallViewModel extends ViewModel { callState = WebRtcControls.CallState.INCOMING; answerWithVideoAvailable = isRemoteVideoOffer; break; + case CALL_OUTGOING: + case CALL_RINGING: + callState = WebRtcControls.CallState.OUTGOING; + break; + case CALL_ACCEPTED_ELSEWHERE: + case CALL_DECLINED_ELSEWHERE: + case CALL_ONGOING_ELSEWHERE: + case CALL_NEEDS_PERMISSION: + case CALL_BUSY: + case CALL_DISCONNECTED: + callState = WebRtcControls.CallState.ENDING; + break; default: callState = WebRtcControls.CallState.ONGOING; } 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 ea72165b6..2c1732f2c 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 @@ -41,23 +41,27 @@ public final class WebRtcControls { } boolean displayEndCall() { - return isOngoing(); + return isAtLeastOutgoing(); } boolean displayMuteAudio() { - return isOngoing(); + return isAtLeastOutgoing(); } boolean displayVideoToggle() { - return isOngoing(); + return isAtLeastOutgoing(); } boolean displayAudioToggle() { - return isOngoing() && (!isLocalVideoEnabled || isBluetoothAvailable); + return isAtLeastOutgoing() && (!isLocalVideoEnabled || isBluetoothAvailable); } boolean displayCameraToggle() { - return isOngoing() && isLocalVideoEnabled && isMoreThanOneCameraAvailable; + return isAtLeastOutgoing() && isLocalVideoEnabled && isMoreThanOneCameraAvailable; + } + + boolean displayRemoteVideoRecycler() { + return isOngoing(); } boolean displayAnswerWithAudio() { @@ -77,15 +81,15 @@ public final class WebRtcControls { } boolean isFadeOutEnabled() { - return isOngoing() && isRemoteVideoEnabled; + return isAtLeastOutgoing() && isRemoteVideoEnabled; } boolean displaySmallOngoingCallButtons() { - return isOngoing() && displayAudioToggle() && displayCameraToggle(); + return isAtLeastOutgoing() && displayAudioToggle() && displayCameraToggle(); } boolean displayLargeOngoingCallButtons() { - return isOngoing() && !(displayAudioToggle() && displayCameraToggle()); + return isAtLeastOutgoing() && !(displayAudioToggle() && displayCameraToggle()); } boolean displayTopViews() { @@ -104,9 +108,19 @@ public final class WebRtcControls { return callState == CallState.INCOMING; } + private boolean isAtLeastOutgoing() { + return callState.isAtLeast(CallState.OUTGOING); + } + public enum CallState { NONE, INCOMING, - ONGOING + OUTGOING, + ONGOING, + ENDING; + + boolean isAtLeast(@NonNull CallState other) { + return compareTo(other) >= 0; + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.java b/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.java index aa5ad1a34..863bf82c0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.java +++ b/app/src/main/java/org/thoughtcrime/securesms/events/CallParticipant.java @@ -12,6 +12,8 @@ import java.util.Objects; public class CallParticipant { + public static final CallParticipant EMPTY = createRemote(Recipient.UNKNOWN, null, new BroadcastVideoSink(null), false); + private final @NonNull CameraState cameraState; private final @NonNull Recipient recipient; private final @Nullable IdentityKey identityKey; diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java index 89c6bd51a..5232c0713 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java @@ -78,6 +78,7 @@ import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserExce import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; 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 new file mode 100644 index 000000000..298568785 --- /dev/null +++ b/app/src/main/res/layout/webrtc_call_participant_recycler_empty_item.xml @@ -0,0 +1,7 @@ + + \ 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 index 7308c41b7..3569ebb5e 100644 --- a/app/src/main/res/layout/webrtc_call_view.xml +++ b/app/src/main/res/layout/webrtc_call_view.xml @@ -11,6 +11,7 @@ android:background="@color/transparent_black_40" /> @@ -24,19 +25,6 @@ app:layout_constraintBottom_toBottomOf="parent" android:orientation="vertical" /> - - + + 12sp - 12dp + 16dp 90dp 160dp