mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 18:30:20 +01:00
Add initial support for Group Calling.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.components.webrtc;
|
||||
public enum WebRtcLocalRenderState {
|
||||
GONE,
|
||||
SMALL_RECTANGLE,
|
||||
SMALL_SQUARE,
|
||||
SMALLER_RECTANGLE,
|
||||
LARGE,
|
||||
LARGE_NO_VIDEO
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user