Add initial support for Group Calling.

This commit is contained in:
Cody Henthorne
2020-11-11 15:11:03 -05:00
parent 696fffb603
commit b1f6786392
53 changed files with 1887 additions and 130 deletions

View File

@@ -1,43 +1,91 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.graphics.Point;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.webrtc.EglBase;
import org.webrtc.VideoFrame;
import org.webrtc.VideoSink;
import org.whispersystems.libsignal.util.Pair;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.WeakHashMap;
public class BroadcastVideoSink implements VideoSink {
private final EglBase eglBase;
private final WeakHashMap<VideoSink, Boolean> sinks;
private final WeakHashMap<Object, Point> requestingSizes;
public BroadcastVideoSink(@Nullable EglBase eglBase) {
this.eglBase = eglBase;
this.sinks = new WeakHashMap<>();
this.eglBase = eglBase;
this.sinks = new WeakHashMap<>();
this.requestingSizes = new WeakHashMap<>();
}
public @Nullable EglBase getEglBase() {
return eglBase;
}
public void addSink(@NonNull VideoSink sink) {
public synchronized void addSink(@NonNull VideoSink sink) {
sinks.put(sink, true);
}
public void removeSink(@NonNull VideoSink sink) {
public synchronized void removeSink(@NonNull VideoSink sink) {
sinks.remove(sink);
}
@Override
public void onFrame(@NonNull VideoFrame videoFrame) {
public synchronized void onFrame(@NonNull VideoFrame videoFrame) {
for (VideoSink sink : sinks.keySet()) {
sink.onFrame(videoFrame);
}
}
void putRequestingSize(@NonNull Object object, @NonNull Point size) {
synchronized (requestingSizes) {
requestingSizes.put(object, size);
}
}
void removeRequestingSize(@NonNull Object object) {
synchronized (requestingSizes) {
requestingSizes.remove(object);
}
}
public @NonNull RequestedSize getMaxRequestingSize() {
int width = 0;
int height = 0;
synchronized (requestingSizes) {
for (Point size : requestingSizes.values()) {
if (width < size.x) {
width = size.x;
height = size.y;
}
}
}
return new RequestedSize(width, height);
}
public static class RequestedSize {
private final int width;
private final int height;
private RequestedSize(int width, int height) {
this.width = width;
this.height = height;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
}
}

View File

@@ -3,6 +3,7 @@ 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.ImageView;
import androidx.annotation.NonNull;
@@ -21,6 +22,7 @@ import org.thoughtcrime.securesms.mms.GlideApp;
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 java.util.Objects;
@@ -32,6 +34,9 @@ public class CallParticipantView extends ConstraintLayout {
private static final FallbackPhotoProvider FALLBACK_PHOTO_PROVIDER = new FallbackPhotoProvider();
private static final int SMALL_AVATAR = ViewUtil.dpToPx(96);
private static final int LARGE_AVATAR = ViewUtil.dpToPx(112);
private RecipientId recipientId;
private AvatarImageView avatar;
private TextureViewRenderer renderer;
@@ -59,6 +64,7 @@ public class CallParticipantView extends ConstraintLayout {
renderer = findViewById(R.id.call_participant_renderer);
avatar.setFallbackPhotoProvider(FALLBACK_PHOTO_PROVIDER);
useLargeAvatar();
}
void setCallParticipant(@NonNull CallParticipant participant) {
@@ -89,6 +95,23 @@ public class CallParticipantView extends ConstraintLayout {
pipAvatar.setVisibility(shouldRenderInPip ? View.VISIBLE : View.GONE);
}
void useLargeAvatar() {
changeAvatarParams(LARGE_AVATAR);
}
void useSmallAvatar() {
changeAvatarParams(SMALL_AVATAR);
}
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.getContactPhoto();
FallbackContactPhoto fallbackPhoto = recipient.getFallbackContactPhoto(FALLBACK_PHOTO_PROVIDER);

View File

@@ -7,6 +7,7 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.cardview.widget.CardView;
import com.google.android.flexbox.AlignItems;
import com.google.android.flexbox.FlexboxLayout;
@@ -14,6 +15,7 @@ import com.google.android.flexbox.FlexboxLayout;
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;
@@ -24,6 +26,9 @@ import java.util.List;
*/
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 List<CallParticipant> callParticipants = Collections.emptyList();
private boolean shouldRenderInPip;
@@ -46,17 +51,33 @@ public class CallParticipantsLayout extends FlexboxLayout {
}
private void updateLayout() {
int previousChildCount = getChildCount();
if (shouldRenderInPip && Util.hasItems(callParticipants)) {
updateChildrenCount(1);
update(0, callParticipants.get(0));
update(0, 1, callParticipants.get(0));
} else {
int count = callParticipants.size();
updateChildrenCount(count);
for (int i = 0; i < callParticipants.size(); i++) {
update(i, callParticipants.get(i));
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) {
@@ -72,15 +93,33 @@ public class CallParticipantsLayout extends FlexboxLayout {
}
}
private void update(int index, @NonNull CallParticipant participant) {
CallParticipantView callParticipantView = (CallParticipantView) getChildAt(index);
private void update(int index, int count, @NonNull CallParticipant participant) {
View view = getChildAt(index);
CardView 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);
setChildLayoutParams(callParticipantView, index, getChildCount());
if (count > 1) {
view.setPadding(MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING);
cardView.setRadius(CORNER_RADIUS);
} else {
view.setPadding(0, 0, 0, 0);
cardView.setRadius(0);
}
if (count > 2) {
callParticipantView.useSmallAvatar();
} else {
callParticipantView.useLargeAvatar();
}
setChildLayoutParams(view, index, getChildCount());
}
private void addCallParticipantView() {
View view = LayoutInflater.from(getContext()).inflate(R.layout.call_participant_item, this, false);
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 File

@@ -1,8 +1,11 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.ringrtc.CameraState;
@@ -21,24 +24,27 @@ public final class CallParticipantsState {
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,
WebRtcLocalRenderState.GONE,
false,
false,
false);
WebRtcViewModel.GroupCallState.IDLE,
Collections.emptyList(),
CallParticipant.createLocal(CameraState.UNKNOWN, new BroadcastVideoSink(null), false),
null,
WebRtcLocalRenderState.GONE,
false,
false,
false);
private final WebRtcViewModel.State callState;
private final List<CallParticipant> remoteParticipants;
private final CallParticipant localParticipant;
private final CallParticipant focusedParticipant;
private final WebRtcLocalRenderState localRenderState;
private final boolean isInPipMode;
private final boolean showVideoForOutgoing;
private final boolean isViewingFocusedParticipant;
private final WebRtcViewModel.State callState;
private final WebRtcViewModel.GroupCallState groupCallState;
private final List<CallParticipant> remoteParticipants;
private final CallParticipant localParticipant;
private final CallParticipant focusedParticipant;
private final WebRtcLocalRenderState localRenderState;
private final boolean isInPipMode;
private final boolean showVideoForOutgoing;
private final boolean isViewingFocusedParticipant;
public CallParticipantsState(@NonNull WebRtcViewModel.State callState,
@NonNull WebRtcViewModel.GroupCallState groupCallState,
@NonNull List<CallParticipant> remoteParticipants,
@NonNull CallParticipant localParticipant,
@Nullable CallParticipant focusedParticipant,
@@ -48,6 +54,7 @@ public final class CallParticipantsState {
boolean isViewingFocusedParticipant)
{
this.callState = callState;
this.groupCallState = groupCallState;
this.remoteParticipants = remoteParticipants;
this.localParticipant = localParticipant;
this.localRenderState = localRenderState;
@@ -61,6 +68,10 @@ public final class CallParticipantsState {
return callState;
}
public @NonNull WebRtcViewModel.GroupCallState getGroupCallState() {
return groupCallState;
}
public @NonNull List<CallParticipant> getGridParticipants() {
if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) {
return getAllRemoteParticipants().subList(0, SMALL_GROUP_MAX);
@@ -87,6 +98,30 @@ public final class CallParticipantsState {
return listParticipants;
}
public @NonNull String getRemoteParticipantsDescription(@NonNull Context context) {
switch (remoteParticipants.size()) {
case 0:
return context.getString(R.string.WebRtcCallView__no_one_else_is_here);
case 1:
if (callState == WebRtcViewModel.State.CALL_PRE_JOIN && groupCallState.isNotIdle()) {
return context.getString(R.string.WebRtcCallView__s_is_in_this_call, remoteParticipants.get(0).getRecipient().getShortDisplayName(context));
} else {
return remoteParticipants.get(0).getRecipient().getDisplayName(context);
}
case 2:
return context.getString(R.string.WebRtcCallView__s_and_s_are_in_this_call,
remoteParticipants.get(0).getRecipient().getShortDisplayName(context),
remoteParticipants.get(1).getRecipient().getShortDisplayName(context));
default:
int others = remoteParticipants.size() - 2;
return context.getResources().getQuantityString(R.plurals.WebRtcCallView__s_s_and_d_others_are_in_this_call,
others,
remoteParticipants.get(0).getRecipient().getShortDisplayName(context),
remoteParticipants.get(1).getRecipient().getShortDisplayName(context),
others);
}
}
public @NonNull List<CallParticipant> getAllRemoteParticipants() {
return remoteParticipants;
}
@@ -132,6 +167,7 @@ public final class CallParticipantsState {
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
return new CallParticipantsState(webRtcViewModel.getState(),
webRtcViewModel.getGroupState(),
webRtcViewModel.getRemoteParticipants(),
webRtcViewModel.getLocalParticipant(),
focused,
@@ -152,6 +188,7 @@ public final class CallParticipantsState {
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
return new CallParticipantsState(oldState.callState,
oldState.groupCallState,
oldState.remoteParticipants,
oldState.localParticipant,
focused,
@@ -172,6 +209,7 @@ public final class CallParticipantsState {
selectedPage == SelectedPage.FOCUSED);
return new CallParticipantsState(oldState.callState,
oldState.groupCallState,
oldState.remoteParticipants,
oldState.localParticipant,
focused,
@@ -193,8 +231,8 @@ public final class CallParticipantsState {
if (displayLocal || showVideoForOutgoing) {
if (callState == WebRtcViewModel.State.CALL_CONNECTED) {
if (isViewingFocusedParticipant || numberOfRemoteParticipants > 3) {
localRenderState = WebRtcLocalRenderState.SMALL_SQUARE;
if (isViewingFocusedParticipant || numberOfRemoteParticipants > 1) {
localRenderState = WebRtcLocalRenderState.SMALLER_RECTANGLE;
} else {
localRenderState = WebRtcLocalRenderState.SMALL_RECTANGLE;
}

View File

@@ -10,8 +10,12 @@ import android.view.TextureView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.webrtc.EglBase;
import org.webrtc.EglRenderer;
import org.webrtc.GlRectDrawer;
@@ -38,6 +42,7 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
private int surfaceHeight;
private boolean isInitialized;
private BroadcastVideoSink attachedVideoSink;
private Lifecycle lifecycle;
public TextureViewRenderer(@NonNull Context context) {
super(context);
@@ -59,7 +64,7 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
this.init(eglBase.getEglBaseContext(), null, EglBase.CONFIG_PLAIN, new GlRectDrawer());
}
public void init(@NonNull EglBase.Context sharedContext, @NonNull RendererCommon.RendererEvents rendererEvents, @NonNull int[] configAttributes, @NonNull RendererCommon.GlDrawer drawer) {
public void init(@NonNull EglBase.Context sharedContext, @Nullable RendererCommon.RendererEvents rendererEvents, @NonNull int[] configAttributes, @NonNull RendererCommon.GlDrawer drawer) {
ThreadUtils.checkIsOnMainThread();
this.rendererEvents = rendererEvents;
@@ -67,6 +72,16 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
this.rotatedFrameHeight = 0;
this.eglRenderer.init(sharedContext, this, configAttributes, drawer);
this.lifecycle = ViewUtil.getActivityLifecycle(this);
if (lifecycle != null) {
lifecycle.addObserver(new DefaultLifecycleObserver() {
@Override
public void onDestroy(@NonNull LifecycleOwner owner) {
release();
}
});
}
}
public void attachBroadcastVideoSink(@Nullable BroadcastVideoSink videoSink) {
@@ -76,10 +91,12 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
if (attachedVideoSink != null) {
attachedVideoSink.removeSink(this);
attachedVideoSink.removeRequestingSize(this);
}
if (videoSink != null) {
videoSink.addSink(this);
videoSink.putRequestingSize(this, new Point(getWidth(), getHeight()));
} else {
clearImage();
}
@@ -90,11 +107,17 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
release();
if (lifecycle == null || lifecycle.getCurrentState() == Lifecycle.State.DESTROYED) {
release();
}
}
public void release() {
eglRenderer.release();
if (attachedVideoSink != null) {
attachedVideoSink.removeSink(this);
attachedVideoSink.removeRequestingSize(this);
}
}
public void addFrameListener(@NonNull EglRenderer.FrameListener listener, float scale, @NonNull RendererCommon.GlDrawer drawerParam) {
@@ -163,6 +186,10 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
setMeasuredDimension(size.x, size.y);
Log.d(TAG, "onMeasure(). New size: " + size.x + "x" + size.y);
if (attachedVideoSink != null) {
attachedVideoSink.putRequestingSize(this, size);
}
}
@Override

View File

@@ -5,6 +5,7 @@ import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
@@ -30,12 +31,14 @@ import androidx.viewpager2.widget.ViewPager2;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.google.android.material.button.MaterialButton;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.ResizeAnimation;
import org.thoughtcrime.securesms.components.AccessibleToggleButton;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.mediasend.SimpleAnimationListener;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -88,8 +91,10 @@ public class WebRtcCallView extends FrameLayout {
private ViewPager2 callParticipantsPager;
private RecyclerView callParticipantsRecycler;
private Toolbar toolbar;
private MaterialButton startCall;
private int pagerBottomMarginDp;
private boolean controlsVisible = true;
private EventListener eventListener;
private WebRtcCallParticipantsPagerAdapter pagerAdapter;
private WebRtcCallParticipantsRecyclerAdapter recyclerAdapter;
@@ -142,13 +147,13 @@ public class WebRtcCallView extends FrameLayout {
callParticipantsPager = findViewById(R.id.call_screen_participants_pager);
callParticipantsRecycler = findViewById(R.id.call_screen_participants_recycler);
toolbar = findViewById(R.id.call_screen_toolbar);
startCall = findViewById(R.id.call_screen_start_call_start_call);
View topGradient = findViewById(R.id.call_screen_header_gradient);
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);
Guideline statusBarGuideline = findViewById(R.id.call_screen_status_bar_guideline);
View startCall = findViewById(R.id.call_screen_start_call_start_call);
View cancelStartCall = findViewById(R.id.call_screen_start_call_cancel);
callParticipantsPager.setPageTransformer(new MarginPageTransformer(ViewUtil.dpToPx(4)));
@@ -163,6 +168,7 @@ public class WebRtcCallView extends FrameLayout {
@Override
public void onPageSelected(int position) {
runIfNonNull(controlsListener, listener -> listener.onPageChanged(position == 0 ? CallParticipantsState.SelectedPage.GRID : CallParticipantsState.SelectedPage.FOCUSED));
runIfNonNull(eventListener, EventListener::onPotentialLayoutChange);
}
});
@@ -233,6 +239,10 @@ public class WebRtcCallView extends FrameLayout {
this.controlsListener = controlsListener;
}
public void setEventListener(@Nullable EventListener eventListener) {
this.eventListener = eventListener;
}
public void setMicEnabled(boolean isMicEnabled) {
micToggle.setChecked(isMicEnabled, false);
}
@@ -248,6 +258,10 @@ public class WebRtcCallView extends FrameLayout {
pages.add(WebRtcCallParticipantsPage.forSingleParticipant(state.getFocusedParticipant(), state.isInPipMode()));
}
if (state.getGroupCallState().isConnected()) {
recipientName.setText(state.getRemoteParticipantsDescription(getContext()));
}
pagerAdapter.submitList(pages);
recyclerAdapter.submitList(state.getListParticipants());
updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant());
@@ -257,6 +271,10 @@ public class WebRtcCallView extends FrameLayout {
} else {
layoutParticipantsForSmallCount();
}
if (eventListener != null) {
eventListener.onPotentialLayoutChange();
}
}
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant) {
@@ -283,17 +301,17 @@ public class WebRtcCallView extends FrameLayout {
case SMALL_RECTANGLE:
smallLocalRenderFrame.setVisibility(View.VISIBLE);
smallLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
animatePipToRectangle();
animatePipToLargeRectangle();
largeLocalRender.attachBroadcastVideoSink(null);
largeLocalRenderFrame.setVisibility(View.GONE);
videoToggle.setChecked(true, false);
break;
case SMALL_SQUARE:
case SMALLER_RECTANGLE:
smallLocalRenderFrame.setVisibility(View.VISIBLE);
smallLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
animatePipToSquare();
animatePipToSmallRectangle();
largeLocalRender.attachBroadcastVideoSink(null);
largeLocalRenderFrame.setVisibility(View.GONE);
@@ -341,7 +359,7 @@ public class WebRtcCallView extends FrameLayout {
recipientId = recipient.getId();
if (recipient.isGroup()) {
recipientName.setText(R.string.WebRtcCallView__group_call);
recipientName.setText(getContext().getString(R.string.WebRtcCallView__s_group_call, recipient.getDisplayName(getContext())));
if (toolbar.getMenu().findItem(R.id.menu_group_call_participants_list) == null) {
toolbar.inflateMenu(R.menu.group_call);
toolbar.setOnMenuItemClickListener(unused -> showParticipantsList());
@@ -375,6 +393,27 @@ public class WebRtcCallView extends FrameLayout {
}
}
public void setStatusFromGroupCallState(@NonNull WebRtcViewModel.GroupCallState groupCallState) {
switch (groupCallState) {
case DISCONNECTED:
status.setText(R.string.WebRtcCallView__disconnected);
break;
case CONNECTING:
status.setText(R.string.WebRtcCallView__connecting);
break;
case RECONNECTING:
status.setText(R.string.WebRtcCallView__reconnecting);
break;
case CONNECTED_AND_JOINING:
status.setText(R.string.WebRtcCallView__joining);
break;
case CONNECTED_AND_JOINED:
case CONNECTED:
status.setText("");
break;
}
}
public void setWebRtcControls(@NonNull WebRtcControls webRtcControls) {
Set<View> lastVisibleSet = new HashSet<>(visibleViewSet);
@@ -383,6 +422,14 @@ public class WebRtcCallView extends FrameLayout {
if (webRtcControls.displayStartCallControls()) {
visibleViewSet.add(footerGradient);
visibleViewSet.add(startCallControls);
startCall.setText(webRtcControls.getStartCallButtonText());
}
MenuItem item = toolbar.getMenu().findItem(R.id.menu_group_call_participants_list);
if (item != null) {
item.setVisible(webRtcControls.displayGroupMembersButton());
item.setEnabled(webRtcControls.displayGroupMembersButton());
}
if (webRtcControls.displayTopViews()) {
@@ -462,7 +509,7 @@ public class WebRtcCallView extends FrameLayout {
return videoToggle;
}
private void animatePipToRectangle() {
private void animatePipToLargeRectangle() {
ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(90), ViewUtil.dpToPx(160));
animation.setDuration(PIP_RESIZE_DURATION);
animation.setAnimationListener(new SimpleAnimationListener() {
@@ -476,11 +523,11 @@ public class WebRtcCallView extends FrameLayout {
smallLocalRenderFrame.startAnimation(animation);
}
private void animatePipToSquare() {
private void animatePipToSmallRectangle() {
pictureInPictureGestureHelper.lockToBottomEnd();
pictureInPictureGestureHelper.performAfterFling(() -> {
ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(72), ViewUtil.dpToPx(72));
ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(40), ViewUtil.dpToPx(72));
animation.setDuration(PIP_RESIZE_DURATION);
animation.setAnimationListener(new SimpleAnimationListener() {
@Override
@@ -606,9 +653,9 @@ public class WebRtcCallView extends FrameLayout {
getHandler().removeCallbacks(fadeOutRunnable);
}
private static void runIfNonNull(@Nullable ControlsListener controlsListener, @NonNull Consumer<ControlsListener> controlsListenerConsumer) {
if (controlsListener != null) {
controlsListenerConsumer.accept(controlsListener);
private static <T> void runIfNonNull(@Nullable T listener, @NonNull Consumer<T> listenerConsumer) {
if (listener != null) {
listenerConsumer.accept(listener);
}
}
@@ -648,4 +695,8 @@ public class WebRtcCallView extends FrameLayout {
void onShowParticipantsList();
void onPageChanged(@NonNull CallParticipantsState.SelectedPage page);
}
public interface EventListener {
void onPotentialLayoutChange();
}
}

View File

@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
public class WebRtcCallViewModel extends ViewModel {
@@ -104,11 +105,13 @@ public class WebRtcCallViewModel extends ViewModel {
participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), webRtcViewModel, enableVideo));
updateWebRtcControls(webRtcViewModel.getState(),
webRtcViewModel.getGroupState(),
localParticipant.getCameraState().isEnabled(),
webRtcViewModel.isRemoteVideoEnabled(),
webRtcViewModel.isRemoteVideoOffer(),
localParticipant.isMoreThanOneCameraAvailable(),
webRtcViewModel.isBluetoothAvailable(),
Util.hasItems(webRtcViewModel.getRemoteParticipants()),
repository.getAudioOutput());
if (webRtcViewModel.getState() == WebRtcViewModel.State.CALL_CONNECTED && callConnectedTime == -1) {
@@ -133,11 +136,13 @@ public class WebRtcCallViewModel extends ViewModel {
}
private void updateWebRtcControls(@NonNull WebRtcViewModel.State state,
@NonNull WebRtcViewModel.GroupCallState groupState,
boolean isLocalVideoEnabled,
boolean isRemoteVideoEnabled,
boolean isRemoteVideoOffer,
boolean isMoreThanOneCameraAvailable,
boolean isBluetoothAvailable,
boolean hasAtLeastOneRemote,
@NonNull WebRtcAudioOutput audioOutput)
{
final WebRtcControls.CallState callState;
@@ -166,12 +171,34 @@ public class WebRtcCallViewModel extends ViewModel {
callState = WebRtcControls.CallState.ONGOING;
}
final WebRtcControls.GroupCallState groupCallState;
switch (groupState) {
case DISCONNECTED:
groupCallState = WebRtcControls.GroupCallState.DISCONNECTED;
break;
case CONNECTING:
case RECONNECTING:
groupCallState = WebRtcControls.GroupCallState.CONNECTING;
break;
case CONNECTED:
case CONNECTED_AND_JOINING:
case CONNECTED_AND_JOINED:
groupCallState = WebRtcControls.GroupCallState.CONNECTED;
break;
default:
groupCallState = WebRtcControls.GroupCallState.NONE;
break;
}
webRtcControls.setValue(new WebRtcControls(isLocalVideoEnabled,
isRemoteVideoEnabled || isRemoteVideoOffer,
isMoreThanOneCameraAvailable,
isBluetoothAvailable,
Boolean.TRUE.equals(isInPipMode.getValue()),
hasAtLeastOneRemote,
callState,
groupCallState,
audioOutput));
}

View File

@@ -1,22 +1,27 @@
package org.thoughtcrime.securesms.components.webrtc;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import org.thoughtcrime.securesms.R;
public final class WebRtcControls {
public static final WebRtcControls NONE = new WebRtcControls();
public static final WebRtcControls PIP = new WebRtcControls(false, false, false, false, true, CallState.NONE, WebRtcAudioOutput.HANDSET);
public static final WebRtcControls PIP = new WebRtcControls(false, false, false, false, true, false, CallState.NONE, GroupCallState.NONE, WebRtcAudioOutput.HANDSET);
private final boolean isRemoteVideoEnabled;
private final boolean isLocalVideoEnabled;
private final boolean isMoreThanOneCameraAvailable;
private final boolean isBluetoothAvailable;
private final boolean isInPipMode;
private final boolean hasAtLeastOneRemote;
private final CallState callState;
private final GroupCallState groupCallState;
private final WebRtcAudioOutput audioOutput;
private WebRtcControls() {
this(false, false, false, false, false, CallState.NONE, WebRtcAudioOutput.HANDSET);
this(false, false, false, false, false, false, CallState.NONE, GroupCallState.NONE, WebRtcAudioOutput.HANDSET);
}
WebRtcControls(boolean isLocalVideoEnabled,
@@ -24,7 +29,9 @@ public final class WebRtcControls {
boolean isMoreThanOneCameraAvailable,
boolean isBluetoothAvailable,
boolean isInPipMode,
boolean hasAtLeastOneRemote,
@NonNull CallState callState,
@NonNull GroupCallState groupCallState,
@NonNull WebRtcAudioOutput audioOutput)
{
this.isLocalVideoEnabled = isLocalVideoEnabled;
@@ -32,7 +39,9 @@ public final class WebRtcControls {
this.isBluetoothAvailable = isBluetoothAvailable;
this.isMoreThanOneCameraAvailable = isMoreThanOneCameraAvailable;
this.isInPipMode = isInPipMode;
this.hasAtLeastOneRemote = hasAtLeastOneRemote;
this.callState = callState;
this.groupCallState = groupCallState;
this.audioOutput = audioOutput;
}
@@ -40,6 +49,17 @@ public final class WebRtcControls {
return isPreJoin();
}
@StringRes int getStartCallButtonText() {
if (isGroupCall() && hasAtLeastOneRemote) {
return R.string.WebRtcCallView__join_call;
}
return R.string.WebRtcCallView__start_call;
}
boolean displayGroupMembersButton() {
return groupCallState == GroupCallState.CONNECTED;
}
boolean displayEndCall() {
return isAtLeastOutgoing();
}
@@ -116,6 +136,10 @@ public final class WebRtcControls {
return callState.isAtLeast(CallState.OUTGOING);
}
private boolean isGroupCall() {
return groupCallState != GroupCallState.NONE;
}
public enum CallState {
NONE,
PRE_JOIN,
@@ -124,8 +148,16 @@ public final class WebRtcControls {
ONGOING,
ENDING;
boolean isAtLeast(@NonNull CallState other) {
boolean isAtLeast(@SuppressWarnings("SameParameterValue") @NonNull CallState other) {
return compareTo(other) >= 0;
}
}
public enum GroupCallState {
NONE,
DISCONNECTED,
CONNECTING,
CONNECTED,
RECONNECTING
}
}

View File

@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.components.webrtc;
public enum WebRtcLocalRenderState {
GONE,
SMALL_RECTANGLE,
SMALL_SQUARE,
SMALLER_RECTANGLE,
LARGE,
LARGE_NO_VIDEO
}

View File

@@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
import org.thoughtcrime.securesms.util.MappingModel;
@@ -79,9 +80,14 @@ public class CallParticipantsListDialog extends BottomSheetDialogFragment {
private void updateList(@NonNull CallParticipantsState callParticipantsState) {
List<MappingModel<?>> items = new ArrayList<>();
items.add(new CallParticipantsListHeader(callParticipantsState.getAllRemoteParticipants().size() + 1));
boolean includeSelf = callParticipantsState.getGroupCallState() == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED;
items.add(new CallParticipantsListHeader(callParticipantsState.getAllRemoteParticipants().size() + (includeSelf ? 1 : 0)));
if (includeSelf) {
items.add(new CallParticipantViewState(callParticipantsState.getLocalParticipant()));
}
items.add(new CallParticipantViewState(callParticipantsState.getLocalParticipant()));
for (CallParticipant callParticipant : callParticipantsState.getAllRemoteParticipants()) {
items.add(new CallParticipantViewState(callParticipant));
}