mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 18:30:20 +01:00
Add foundational UX and state support for Group Calling.
This commit is contained in:
committed by
Greyson Parrelli
parent
7baf8052a2
commit
dc4faf57cb
@@ -0,0 +1,43 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.VideoFrame;
|
||||
import org.webrtc.VideoSink;
|
||||
|
||||
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;
|
||||
|
||||
public BroadcastVideoSink(@Nullable EglBase eglBase) {
|
||||
this.eglBase = eglBase;
|
||||
this.sinks = new WeakHashMap<>();
|
||||
}
|
||||
|
||||
public @Nullable EglBase getEglBase() {
|
||||
return eglBase;
|
||||
}
|
||||
|
||||
public void addSink(@NonNull VideoSink sink) {
|
||||
sinks.put(sink, true);
|
||||
}
|
||||
|
||||
public void removeSink(@NonNull VideoSink sink) {
|
||||
sinks.remove(sink);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFrame(@NonNull VideoFrame videoFrame) {
|
||||
for (VideoSink sink : sinks.keySet()) {
|
||||
sink.onFrame(videoFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
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 java.util.Objects;
|
||||
|
||||
/**
|
||||
* 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 FallbackPhotoProvider FALLBACK_PHOTO_PROVIDER = new FallbackPhotoProvider();
|
||||
|
||||
private RecipientId recipientId;
|
||||
private AvatarImageView avatar;
|
||||
private TextureViewRenderer renderer;
|
||||
private ImageView pipAvatar;
|
||||
private ContactPhoto contactPhoto;
|
||||
|
||||
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();
|
||||
avatar = findViewById(R.id.call_participant_item_avatar);
|
||||
pipAvatar = findViewById(R.id.call_participant_item_pip_avatar);
|
||||
renderer = findViewById(R.id.call_participant_renderer);
|
||||
|
||||
avatar.setFallbackPhotoProvider(FALLBACK_PHOTO_PROVIDER);
|
||||
}
|
||||
|
||||
void setCallParticipant(@NonNull CallParticipant participant) {
|
||||
boolean participantChanged = recipientId == null || !recipientId.equals(participant.getRecipient().getId());
|
||||
recipientId = participant.getRecipient().getId();
|
||||
|
||||
renderer.setVisibility(participant.isVideoEnabled() ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (participant.isVideoEnabled()) {
|
||||
if (participant.getVideoSink().getEglBase() != null) {
|
||||
renderer.init(participant.getVideoSink().getEglBase());
|
||||
}
|
||||
renderer.attachBroadcastVideoSink(participant.getVideoSink());
|
||||
} else {
|
||||
renderer.attachBroadcastVideoSink(null);
|
||||
}
|
||||
|
||||
if (participantChanged || !Objects.equals(contactPhoto, participant.getRecipient().getContactPhoto())) {
|
||||
avatar.setAvatar(participant.getRecipient());
|
||||
AvatarUtil.loadBlurredIconIntoViewBackground(participant.getRecipient(), this);
|
||||
setPipAvatar(participant.getRecipient());
|
||||
contactPhoto = participant.getRecipient().getContactPhoto();
|
||||
}
|
||||
}
|
||||
|
||||
void setRenderInPip(boolean shouldRenderInPip) {
|
||||
avatar.setVisibility(shouldRenderInPip ? View.GONE : View.VISIBLE);
|
||||
pipAvatar.setVisibility(shouldRenderInPip ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void setPipAvatar(@NonNull Recipient recipient) {
|
||||
ContactPhoto contactPhoto = recipient.getContactPhoto();
|
||||
FallbackContactPhoto fallbackPhoto = recipient.getFallbackContactPhoto(FALLBACK_PHOTO_PROVIDER);
|
||||
|
||||
GlideApp.with(this)
|
||||
.load(contactPhoto)
|
||||
.fallback(fallbackPhoto.asCallCard(getContext()))
|
||||
.error(fallbackPhoto.asCallCard(getContext()))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.into(pipAvatar);
|
||||
|
||||
pipAvatar.setScaleType(contactPhoto == null ? ImageView.ScaleType.CENTER_INSIDE : ImageView.ScaleType.CENTER_CROP);
|
||||
pipAvatar.setBackgroundColor(recipient.getColor().toActionBarColor(getContext()));
|
||||
}
|
||||
|
||||
private static final class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider {
|
||||
@Override
|
||||
public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
|
||||
ResourceContactPhoto photo = new ResourceContactPhoto(R.drawable.ic_profile_outline_120);
|
||||
photo.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||
return photo;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
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 org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
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 List<CallParticipant> callParticipants = Collections.emptyList();
|
||||
private boolean shouldRenderInPip;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void update(@NonNull List<CallParticipant> callParticipants, boolean shouldRenderInPip) {
|
||||
this.callParticipants = callParticipants;
|
||||
this.shouldRenderInPip = shouldRenderInPip;
|
||||
updateLayout();
|
||||
}
|
||||
|
||||
private void updateLayout() {
|
||||
if (shouldRenderInPip && Util.hasItems(callParticipants)) {
|
||||
updateChildrenCount(1);
|
||||
update(0, callParticipants.get(0));
|
||||
} else {
|
||||
int count = callParticipants.size();
|
||||
updateChildrenCount(count);
|
||||
|
||||
for (int i = 0; i < callParticipants.size(); i++) {
|
||||
update(i, callParticipants.get(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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++) {
|
||||
removeViewAt(count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void update(int index, @NonNull CallParticipant participant) {
|
||||
CallParticipantView callParticipantView = (CallParticipantView) getChildAt(index);
|
||||
callParticipantView.setCallParticipant(participant);
|
||||
callParticipantView.setRenderInPip(shouldRenderInPip);
|
||||
setChildLayoutParams(callParticipantView, index, getChildCount());
|
||||
}
|
||||
|
||||
private void addCallParticipantView() {
|
||||
View view = LayoutInflater.from(getContext()).inflate(R.layout.call_participant_item, this, false);
|
||||
FlexboxLayout.LayoutParams params = (FlexboxLayout.LayoutParams) view.getLayoutParams();
|
||||
|
||||
params.setAlignSelf(AlignItems.STRETCH);
|
||||
view.setLayoutParams(params);
|
||||
addView(view);
|
||||
}
|
||||
|
||||
private void setChildLayoutParams(@NonNull View child, int childPosition, int childCount) {
|
||||
FlexboxLayout.LayoutParams params = (FlexboxLayout.LayoutParams) child.getLayoutParams();
|
||||
if (childCount < 3) {
|
||||
params.setFlexBasisPercent(1f);
|
||||
} else {
|
||||
if ((childCount % 2) != 0 && childPosition == childCount - 1) {
|
||||
params.setFlexBasisPercent(1f);
|
||||
} else {
|
||||
params.setFlexBasisPercent(0.5f);
|
||||
}
|
||||
}
|
||||
child.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents the state of all participants, remote and local, combined with view state
|
||||
* needed to properly render the participants. The view state primarily consists of
|
||||
* if we are in System PIP mode and if we should show our video for an outgoing call.
|
||||
*/
|
||||
public final class CallParticipantsState {
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
|
||||
public CallParticipantsState(@NonNull WebRtcViewModel.State callState,
|
||||
@NonNull List<CallParticipant> remoteParticipants,
|
||||
@NonNull CallParticipant localParticipant,
|
||||
@Nullable CallParticipant focusedParticipant,
|
||||
@NonNull WebRtcLocalRenderState localRenderState,
|
||||
boolean isInPipMode,
|
||||
boolean showVideoForOutgoing,
|
||||
boolean isViewingFocusedParticipant)
|
||||
{
|
||||
this.callState = callState;
|
||||
this.remoteParticipants = remoteParticipants;
|
||||
this.localParticipant = localParticipant;
|
||||
this.localRenderState = localRenderState;
|
||||
this.focusedParticipant = focusedParticipant;
|
||||
this.isInPipMode = isInPipMode;
|
||||
this.showVideoForOutgoing = showVideoForOutgoing;
|
||||
this.isViewingFocusedParticipant = isViewingFocusedParticipant;
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getGridParticipants() {
|
||||
if (getAllRemoteParticipants().size() > 6) {
|
||||
return getAllRemoteParticipants().subList(0, 6);
|
||||
} else {
|
||||
return getAllRemoteParticipants();
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getListParticipants() {
|
||||
if (isViewingFocusedParticipant && getAllRemoteParticipants().size() > 1) {
|
||||
return getAllRemoteParticipants().subList(1, getAllRemoteParticipants().size());
|
||||
} else if (getAllRemoteParticipants().size() > 6) {
|
||||
return getAllRemoteParticipants().subList(6, getAllRemoteParticipants().size());
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getAllRemoteParticipants() {
|
||||
return remoteParticipants;
|
||||
}
|
||||
|
||||
public @NonNull CallParticipant getLocalParticipant() {
|
||||
return localParticipant;
|
||||
}
|
||||
|
||||
public @Nullable CallParticipant getFocusedParticipant() {
|
||||
return focusedParticipant;
|
||||
}
|
||||
|
||||
public @NonNull WebRtcLocalRenderState getLocalRenderState() {
|
||||
return localRenderState;
|
||||
}
|
||||
|
||||
public boolean isInPipMode() {
|
||||
return isInPipMode;
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState,
|
||||
@NonNull WebRtcViewModel webRtcViewModel,
|
||||
boolean enableVideo)
|
||||
{
|
||||
boolean newShowVideoForOutgoing = oldState.showVideoForOutgoing;
|
||||
if (enableVideo) {
|
||||
newShowVideoForOutgoing = webRtcViewModel.getState() == WebRtcViewModel.State.CALL_OUTGOING;
|
||||
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_OUTGOING) {
|
||||
newShowVideoForOutgoing = false;
|
||||
}
|
||||
|
||||
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(webRtcViewModel.getLocalParticipant(),
|
||||
oldState.isInPipMode,
|
||||
newShowVideoForOutgoing,
|
||||
webRtcViewModel.getState(),
|
||||
oldState.getAllRemoteParticipants().size(),
|
||||
oldState.isViewingFocusedParticipant);
|
||||
|
||||
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
|
||||
|
||||
return new CallParticipantsState(webRtcViewModel.getState(),
|
||||
webRtcViewModel.getRemoteParticipants(),
|
||||
webRtcViewModel.getLocalParticipant(),
|
||||
focused,
|
||||
localRenderState,
|
||||
oldState.isInPipMode,
|
||||
newShowVideoForOutgoing,
|
||||
oldState.isViewingFocusedParticipant);
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, boolean isInPip) {
|
||||
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
|
||||
isInPip,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.callState,
|
||||
oldState.getAllRemoteParticipants().size(),
|
||||
oldState.isViewingFocusedParticipant);
|
||||
|
||||
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
|
||||
|
||||
return new CallParticipantsState(oldState.callState,
|
||||
oldState.remoteParticipants,
|
||||
oldState.localParticipant,
|
||||
focused,
|
||||
localRenderState,
|
||||
isInPip,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.isViewingFocusedParticipant);
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, @NonNull SelectedPage selectedPage) {
|
||||
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
|
||||
|
||||
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
|
||||
oldState.isInPipMode,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.callState,
|
||||
oldState.getAllRemoteParticipants().size(),
|
||||
selectedPage == SelectedPage.FOCUSED);
|
||||
|
||||
return new CallParticipantsState(oldState.callState,
|
||||
oldState.remoteParticipants,
|
||||
oldState.localParticipant,
|
||||
focused,
|
||||
localRenderState,
|
||||
oldState.isInPipMode,
|
||||
oldState.showVideoForOutgoing,
|
||||
selectedPage == SelectedPage.FOCUSED);
|
||||
}
|
||||
|
||||
private static @NonNull WebRtcLocalRenderState determineLocalRenderMode(@NonNull CallParticipant localParticipant,
|
||||
boolean isInPip,
|
||||
boolean showVideoForOutgoing,
|
||||
@NonNull WebRtcViewModel.State callState,
|
||||
int numberOfRemoteParticipants,
|
||||
boolean isViewingFocusedParticipant)
|
||||
{
|
||||
boolean displayLocal = !isInPip && localParticipant.isVideoEnabled();
|
||||
WebRtcLocalRenderState localRenderState = WebRtcLocalRenderState.GONE;
|
||||
|
||||
if (displayLocal || showVideoForOutgoing) {
|
||||
if (callState == WebRtcViewModel.State.CALL_CONNECTED) {
|
||||
if (isViewingFocusedParticipant || numberOfRemoteParticipants > 3) {
|
||||
localRenderState = WebRtcLocalRenderState.SMALL_SQUARE;
|
||||
} else {
|
||||
localRenderState = WebRtcLocalRenderState.SMALL_RECTANGLE;
|
||||
}
|
||||
} else {
|
||||
localRenderState = WebRtcLocalRenderState.LARGE;
|
||||
}
|
||||
}
|
||||
|
||||
return localRenderState;
|
||||
}
|
||||
|
||||
public enum SelectedPage {
|
||||
GRID,
|
||||
FOCUSED
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,9 @@ import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
|
||||
public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestureListener {
|
||||
|
||||
@@ -28,9 +31,9 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
||||
private final ViewGroup parent;
|
||||
private final View child;
|
||||
private final int framePadding;
|
||||
private final int pipWidth;
|
||||
private final int pipHeight;
|
||||
|
||||
private int pipWidth;
|
||||
private int pipHeight;
|
||||
private int activePointerId = MotionEvent.INVALID_POINTER_ID;
|
||||
private float lastTouchX;
|
||||
private float lastTouchY;
|
||||
@@ -42,6 +45,8 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
||||
private double projectionY;
|
||||
private VelocityTracker velocityTracker;
|
||||
private int maximumFlingVelocity;
|
||||
private boolean isLockedToBottomEnd;
|
||||
private Queue<Runnable> runAfterFling;
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public static PictureInPictureGestureHelper applyTo(@NonNull View child) {
|
||||
@@ -95,6 +100,7 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
||||
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.runAfterFling = new LinkedList<>();
|
||||
}
|
||||
|
||||
public void clearVerticalBoundaries() {
|
||||
@@ -105,11 +111,7 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
||||
extraPaddingTop = topBoundary - parent.getTop();
|
||||
extraPaddingBottom = parent.getMeasuredHeight() + parent.getTop() - bottomBoundary;
|
||||
|
||||
if (isAnimating) {
|
||||
fling();
|
||||
} else if (!isDragging) {
|
||||
onFling(null, null, 0, 0);
|
||||
}
|
||||
adjustPip();
|
||||
}
|
||||
|
||||
private boolean onGestureFinished(MotionEvent e) {
|
||||
@@ -123,12 +125,41 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
||||
return false;
|
||||
}
|
||||
|
||||
public void adjustPip() {
|
||||
pipWidth = child.getMeasuredWidth();
|
||||
pipHeight = child.getMeasuredHeight();
|
||||
|
||||
if (isAnimating) {
|
||||
fling();
|
||||
} else if (!isDragging) {
|
||||
onFling(null, null, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void lockToBottomEnd() {
|
||||
isLockedToBottomEnd = true;
|
||||
}
|
||||
|
||||
public void enableCorners() {
|
||||
isLockedToBottomEnd = false;
|
||||
}
|
||||
|
||||
public void performAfterFling(@NonNull Runnable runnable) {
|
||||
if (isAnimating) {
|
||||
runAfterFling.add(runnable);
|
||||
} else {
|
||||
runnable.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDown(MotionEvent e) {
|
||||
activePointerId = e.getPointerId(0);
|
||||
lastTouchX = e.getX(activePointerId) + child.getX();
|
||||
lastTouchY = e.getY(activePointerId) + child.getY();
|
||||
isDragging = true;
|
||||
pipWidth = child.getMeasuredWidth();
|
||||
pipHeight = child.getMeasuredHeight();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -167,6 +198,13 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e) {
|
||||
child.performClick();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void fling() {
|
||||
Point projection = new Point((int) projectionX, (int) projectionY);
|
||||
Point nearestCornerPosition = findNearestCornerPosition(projection);
|
||||
@@ -183,12 +221,25 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
isAnimating = false;
|
||||
|
||||
Iterator<Runnable> afterFlingRunnables = runAfterFling.iterator();
|
||||
while (afterFlingRunnables.hasNext()) {
|
||||
Runnable runnable = afterFlingRunnables.next();
|
||||
|
||||
runnable.run();
|
||||
afterFlingRunnables.remove();
|
||||
}
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
private Point findNearestCornerPosition(Point projection) {
|
||||
if (isLockedToBottomEnd) {
|
||||
return parent.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR ? calculateBottomRightCoordinates(parent)
|
||||
: calculateBottomLeftCoordinates(parent);
|
||||
}
|
||||
|
||||
Point maxPoint = null;
|
||||
double maxDistance = Double.MAX_VALUE;
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
|
||||
private boolean enableFixedSize;
|
||||
private int surfaceWidth;
|
||||
private int surfaceHeight;
|
||||
private boolean isInitialized;
|
||||
private BroadcastVideoSink attachedVideoSink;
|
||||
|
||||
public TextureViewRenderer(@NonNull Context context) {
|
||||
super(context);
|
||||
@@ -49,8 +51,12 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
|
||||
this.setSurfaceTextureListener(this);
|
||||
}
|
||||
|
||||
public void init(@NonNull EglBase.Context sharedContext, @NonNull RendererCommon.RendererEvents rendererEvents) {
|
||||
this.init(sharedContext, rendererEvents, EglBase.CONFIG_PLAIN, new GlRectDrawer());
|
||||
public void init(@NonNull EglBase eglBase) {
|
||||
if (isInitialized) return;
|
||||
|
||||
isInitialized = true;
|
||||
|
||||
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) {
|
||||
@@ -63,6 +69,24 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
|
||||
this.eglRenderer.init(sharedContext, this, configAttributes, drawer);
|
||||
}
|
||||
|
||||
public void attachBroadcastVideoSink(@Nullable BroadcastVideoSink videoSink) {
|
||||
if (attachedVideoSink != null) {
|
||||
attachedVideoSink.removeSink(this);
|
||||
}
|
||||
|
||||
if (videoSink != null) {
|
||||
videoSink.addSink(this);
|
||||
}
|
||||
|
||||
attachedVideoSink = videoSink;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
release();
|
||||
}
|
||||
|
||||
public void release() {
|
||||
eglRenderer.release();
|
||||
}
|
||||
@@ -125,6 +149,9 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
|
||||
protected void onMeasure(int widthSpec, int heightSpec) {
|
||||
ThreadUtils.checkIsOnMainThread();
|
||||
|
||||
widthSpec = MeasureSpec.makeMeasureSpec(resolveSizeAndState(0, widthSpec, 0), MeasureSpec.AT_MOST);
|
||||
heightSpec = MeasureSpec.makeMeasureSpec(resolveSizeAndState(0, heightSpec, 0), MeasureSpec.AT_MOST);
|
||||
|
||||
Point size = videoLayoutMeasure.measure(widthSpec, heightSpec, this.rotatedFrameWidth, this.rotatedFrameHeight);
|
||||
|
||||
setMeasuredDimension(size.x, size.y);
|
||||
@@ -205,7 +232,9 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
|
||||
|
||||
@Override
|
||||
public void onFrame(VideoFrame videoFrame) {
|
||||
eglRenderer.onFrame(videoFrame);
|
||||
if (isShown()) {
|
||||
eglRenderer.onFrame(videoFrame);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
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<CallParticipant> callParticipants;
|
||||
private final boolean isSpeaker;
|
||||
private final boolean isRenderInPip;
|
||||
|
||||
static WebRtcCallParticipantsPage forMultipleParticipants(@NonNull List<CallParticipant> callParticipants,
|
||||
boolean isRenderInPip)
|
||||
{
|
||||
return new WebRtcCallParticipantsPage(callParticipants, false, isRenderInPip);
|
||||
}
|
||||
|
||||
static WebRtcCallParticipantsPage forSingleParticipant(@NonNull CallParticipant singleParticipant,
|
||||
boolean isRenderInPip)
|
||||
{
|
||||
return new WebRtcCallParticipantsPage(Collections.singletonList(singleParticipant), true, isRenderInPip);
|
||||
}
|
||||
|
||||
private WebRtcCallParticipantsPage(@NonNull List<CallParticipant> callParticipants,
|
||||
boolean isSpeaker,
|
||||
boolean isRenderInPip)
|
||||
{
|
||||
this.callParticipants = callParticipants;
|
||||
this.isSpeaker = isSpeaker;
|
||||
this.isRenderInPip = isRenderInPip;
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getCallParticipants() {
|
||||
return callParticipants;
|
||||
}
|
||||
|
||||
public boolean isRenderInPip() {
|
||||
return isRenderInPip;
|
||||
}
|
||||
|
||||
public boolean isSpeaker() {
|
||||
return isSpeaker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
WebRtcCallParticipantsPage that = (WebRtcCallParticipantsPage) o;
|
||||
return isSpeaker == that.isSpeaker &&
|
||||
isRenderInPip == that.isRenderInPip &&
|
||||
callParticipants.equals(that.callParticipants);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(callParticipants, isSpeaker);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
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;
|
||||
|
||||
class WebRtcCallParticipantsPagerAdapter extends ListAdapter<WebRtcCallParticipantsPage, WebRtcCallParticipantsPagerAdapter.ViewHolder> {
|
||||
|
||||
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.isRenderInPip());
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
callParticipantView.setCallParticipant(page.getCallParticipants().get(0));
|
||||
callParticipantView.setRenderInPip(page.isRenderInPip());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DiffCallback extends DiffUtil.ItemCallback<WebRtcCallParticipantsPage> {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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;
|
||||
|
||||
class WebRtcCallParticipantsRecyclerAdapter extends ListAdapter<CallParticipant, WebRtcCallParticipantsRecyclerAdapter.ViewHolder> {
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.bind(getItem(position));
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final CallParticipantView callParticipantView;
|
||||
|
||||
ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
callParticipantView = itemView.findViewById(R.id.call_participant);
|
||||
}
|
||||
|
||||
void bind(@NonNull CallParticipant callParticipant) {
|
||||
callParticipantView.setCallParticipant(callParticipant);
|
||||
}
|
||||
}
|
||||
|
||||
private static class DiffCallback extends DiffUtil.ItemCallback<CallParticipant> {
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.view.animation.Animation;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
@@ -13,60 +13,57 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.constraintlayout.widget.Guideline;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.transition.AutoTransition;
|
||||
import androidx.transition.Transition;
|
||||
import androidx.transition.TransitionManager;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import androidx.viewpager2.widget.MarginPageTransformer;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.ResizeAnimation;
|
||||
import org.thoughtcrime.securesms.components.AccessibleToggleButton;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.mediasend.SimpleAnimationListener;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
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.Set;
|
||||
|
||||
public class WebRtcCallView extends FrameLayout {
|
||||
|
||||
private static final long TRANSITION_DURATION_MILLIS = 250;
|
||||
private static final int SMALL_ONGOING_CALL_BUTTON_MARGIN_DP = 8;
|
||||
private static final int LARGE_ONGOING_CALL_BUTTON_MARGIN_DP = 16;
|
||||
private static final FallbackPhotoProvider FALLBACK_PHOTO_PROVIDER = new FallbackPhotoProvider();
|
||||
private static final long TRANSITION_DURATION_MILLIS = 250;
|
||||
private static final int SMALL_ONGOING_CALL_BUTTON_MARGIN_DP = 8;
|
||||
private static final int LARGE_ONGOING_CALL_BUTTON_MARGIN_DP = 16;
|
||||
|
||||
public static final int FADE_OUT_DELAY = 5000;
|
||||
public static final int FADE_OUT_DELAY = 5000;
|
||||
public static final int PIP_RESIZE_DURATION = 300;
|
||||
|
||||
private TextureViewRenderer localRenderer;
|
||||
private WebRtcAudioOutputToggleButton audioToggle;
|
||||
private AccessibleToggleButton videoToggle;
|
||||
private AccessibleToggleButton micToggle;
|
||||
private ViewGroup largeLocalRenderContainer;
|
||||
private ViewGroup localRenderPipFrame;
|
||||
private ViewGroup smallLocalRenderContainer;
|
||||
private ViewGroup remoteRenderContainer;
|
||||
private TextureViewRenderer smallLocalRender;
|
||||
private View largeLocalRenderFrame;
|
||||
private TextureViewRenderer largeLocalRender;
|
||||
private TextView recipientName;
|
||||
private TextView status;
|
||||
private ConstraintLayout parent;
|
||||
private AvatarImageView avatar;
|
||||
private ImageView avatarCard;
|
||||
private ControlsListener controlsListener;
|
||||
private RecipientId recipientId;
|
||||
private CameraState.Direction cameraDirection;
|
||||
private ImageView answer;
|
||||
private ImageView cameraDirectionToggle;
|
||||
private PictureInPictureGestureHelper pictureInPictureGestureHelper;
|
||||
@@ -74,6 +71,13 @@ public class WebRtcCallView extends FrameLayout {
|
||||
private View answerWithAudio;
|
||||
private View answerWithAudioLabel;
|
||||
private View ongoingFooterGradient;
|
||||
private View startCallControls;
|
||||
private ViewPager2 callParticipantsPager;
|
||||
private RecyclerView callParticipantsRecycler;
|
||||
private Toolbar toolbar;
|
||||
|
||||
private WebRtcCallParticipantsPagerAdapter pagerAdapter;
|
||||
private WebRtcCallParticipantsRecyclerAdapter recyclerAdapter;
|
||||
|
||||
private final Set<View> incomingCallViews = new HashSet<>();
|
||||
private final Set<View> topViews = new HashSet<>();
|
||||
@@ -82,7 +86,8 @@ public class WebRtcCallView extends FrameLayout {
|
||||
|
||||
private WebRtcControls controls = WebRtcControls.NONE;
|
||||
private final Runnable fadeOutRunnable = () -> {
|
||||
if (isAttachedToWindow() && controls.isFadeOutEnabled()) fadeOutControls(); };
|
||||
if (isAttachedToWindow() && controls.isFadeOutEnabled()) fadeOutControls();
|
||||
};
|
||||
|
||||
public WebRtcCallView(@NonNull Context context) {
|
||||
this(context, null);
|
||||
@@ -99,36 +104,53 @@ public class WebRtcCallView extends FrameLayout {
|
||||
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);
|
||||
localRenderPipFrame = findViewById(R.id.call_screen_pip);
|
||||
largeLocalRenderContainer = findViewById(R.id.call_screen_large_local_renderer_holder);
|
||||
smallLocalRenderContainer = findViewById(R.id.call_screen_small_local_renderer_holder);
|
||||
remoteRenderContainer = findViewById(R.id.call_screen_remote_renderer_holder);
|
||||
recipientName = findViewById(R.id.call_screen_recipient_name);
|
||||
status = findViewById(R.id.call_screen_status);
|
||||
parent = findViewById(R.id.call_screen);
|
||||
avatar = findViewById(R.id.call_screen_recipient_avatar);
|
||||
avatarCard = findViewById(R.id.call_screen_recipient_avatar_call_card);
|
||||
answer = findViewById(R.id.call_screen_answer_call);
|
||||
cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle);
|
||||
hangup = findViewById(R.id.call_screen_end_call);
|
||||
answerWithAudio = findViewById(R.id.call_screen_answer_with_audio);
|
||||
answerWithAudioLabel = findViewById(R.id.call_screen_answer_with_audio_label);
|
||||
ongoingFooterGradient = findViewById(R.id.call_screen_ongoing_footer_gradient);
|
||||
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);
|
||||
localRenderPipFrame = 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);
|
||||
recipientName = findViewById(R.id.call_screen_recipient_name);
|
||||
status = findViewById(R.id.call_screen_status);
|
||||
parent = findViewById(R.id.call_screen);
|
||||
answer = findViewById(R.id.call_screen_answer_call);
|
||||
cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle);
|
||||
hangup = findViewById(R.id.call_screen_end_call);
|
||||
answerWithAudio = findViewById(R.id.call_screen_answer_with_audio);
|
||||
answerWithAudioLabel = findViewById(R.id.call_screen_answer_with_audio_label);
|
||||
ongoingFooterGradient = findViewById(R.id.call_screen_ongoing_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);
|
||||
toolbar = findViewById(R.id.call_screen_toolbar);
|
||||
|
||||
View topGradient = findViewById(R.id.call_screen_header_gradient);
|
||||
View downCaret = findViewById(R.id.call_screen_down_arrow);
|
||||
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);
|
||||
View incomingFooterGradient = findViewById(R.id.call_screen_incoming_footer_gradient);
|
||||
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);
|
||||
|
||||
topViews.add(status);
|
||||
callParticipantsPager.setPageTransformer(new MarginPageTransformer(ViewUtil.dpToPx(4)));
|
||||
|
||||
pagerAdapter = new WebRtcCallParticipantsPagerAdapter(this::toggleControls);
|
||||
recyclerAdapter = new WebRtcCallParticipantsRecyclerAdapter();
|
||||
|
||||
callParticipantsPager.setAdapter(pagerAdapter);
|
||||
callParticipantsRecycler.setAdapter(recyclerAdapter);
|
||||
|
||||
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(toolbar);
|
||||
topViews.add(topGradient);
|
||||
topViews.add(recipientName);
|
||||
|
||||
incomingCallViews.add(answer);
|
||||
incomingCallViews.add(answerLabel);
|
||||
@@ -158,16 +180,14 @@ public class WebRtcCallView extends FrameLayout {
|
||||
hangup.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onEndCallPressed));
|
||||
decline.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onDenyCallPressed));
|
||||
|
||||
downCaret.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onDownCaretPressed));
|
||||
|
||||
answer.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallPressed));
|
||||
answerWithAudio.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallWithVoiceOnlyPressed));
|
||||
|
||||
setOnClickListener(v -> toggleControls());
|
||||
avatar.setOnClickListener(v -> toggleControls());
|
||||
|
||||
pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(localRenderPipFrame);
|
||||
|
||||
startCall.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onStartCall));
|
||||
cancelStartCall.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onCancelStartCall));
|
||||
|
||||
int statusBarHeight = ViewUtil.getStatusBarHeight(this);
|
||||
statusBarGuideline.setGuidelineBegin(statusBarHeight);
|
||||
}
|
||||
@@ -195,67 +215,57 @@ public class WebRtcCallView extends FrameLayout {
|
||||
micToggle.setChecked(isMicEnabled, false);
|
||||
}
|
||||
|
||||
public void setRemoteVideoEnabled(boolean isRemoteVideoEnabled) {
|
||||
if (isRemoteVideoEnabled) {
|
||||
remoteRenderContainer.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
remoteRenderContainer.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
public void updateCallParticipants(@NonNull CallParticipantsState state) {
|
||||
List<WebRtcCallParticipantsPage> pages = new ArrayList<>(2);
|
||||
|
||||
public void setLocalRenderer(@Nullable TextureViewRenderer surfaceViewRenderer) {
|
||||
if (localRenderer == surfaceViewRenderer) {
|
||||
return;
|
||||
if (!state.getGridParticipants().isEmpty()) {
|
||||
pages.add(WebRtcCallParticipantsPage.forMultipleParticipants(state.getGridParticipants(), state.isInPipMode()));
|
||||
}
|
||||
|
||||
localRenderer = surfaceViewRenderer;
|
||||
|
||||
if (surfaceViewRenderer == null) {
|
||||
setRenderer(largeLocalRenderContainer, null);
|
||||
setRenderer(smallLocalRenderContainer, null);
|
||||
} else {
|
||||
localRenderer.setMirror(cameraDirection == CameraState.Direction.FRONT);
|
||||
localRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
|
||||
if (state.getFocusedParticipant() != null && state.getAllRemoteParticipants().size() > 1) {
|
||||
pages.add(WebRtcCallParticipantsPage.forSingleParticipant(state.getFocusedParticipant(), state.isInPipMode()));
|
||||
}
|
||||
|
||||
pagerAdapter.submitList(pages);
|
||||
recyclerAdapter.submitList(state.getListParticipants());
|
||||
updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant());
|
||||
}
|
||||
|
||||
public void setRemoteRenderer(@Nullable TextureViewRenderer remoteRenderer) {
|
||||
setRenderer(remoteRenderContainer, remoteRenderer);
|
||||
}
|
||||
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant) {
|
||||
videoToggle.setChecked(state != WebRtcLocalRenderState.GONE, false);
|
||||
|
||||
public void setLocalRenderState(WebRtcLocalRenderState localRenderState) {
|
||||
smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
|
||||
largeLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
|
||||
|
||||
videoToggle.setChecked(localRenderState != WebRtcLocalRenderState.GONE, false);
|
||||
smallLocalRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
|
||||
largeLocalRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
|
||||
|
||||
switch (localRenderState) {
|
||||
case GONE:
|
||||
localRenderPipFrame.setVisibility(View.GONE);
|
||||
largeLocalRenderContainer.setVisibility(View.GONE);
|
||||
setRenderer(largeLocalRenderContainer, null);
|
||||
setRenderer(smallLocalRenderContainer, null);
|
||||
break;
|
||||
if (localCallParticipant.getVideoSink().getEglBase() != null) {
|
||||
smallLocalRender.init(localCallParticipant.getVideoSink().getEglBase());
|
||||
largeLocalRender.init(localCallParticipant.getVideoSink().getEglBase());
|
||||
}
|
||||
|
||||
smallLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
|
||||
largeLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
|
||||
|
||||
switch (state) {
|
||||
case LARGE:
|
||||
largeLocalRenderFrame.setVisibility(View.VISIBLE);
|
||||
localRenderPipFrame.setVisibility(View.GONE);
|
||||
largeLocalRenderContainer.setVisibility(View.VISIBLE);
|
||||
if (largeLocalRenderContainer.getChildCount() == 0) {
|
||||
setRenderer(largeLocalRenderContainer, localRenderer);
|
||||
}
|
||||
break;
|
||||
case SMALL:
|
||||
case GONE:
|
||||
largeLocalRenderFrame.setVisibility(View.GONE);
|
||||
localRenderPipFrame.setVisibility(View.GONE);
|
||||
break;
|
||||
case SMALL_RECTANGLE:
|
||||
largeLocalRenderFrame.setVisibility(View.GONE);
|
||||
localRenderPipFrame.setVisibility(View.VISIBLE);
|
||||
largeLocalRenderContainer.setVisibility(View.GONE);
|
||||
|
||||
if (smallLocalRenderContainer.getChildCount() == 0) {
|
||||
setRenderer(smallLocalRenderContainer, localRenderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setCameraDirection(@NonNull CameraState.Direction cameraDirection) {
|
||||
this.cameraDirection = cameraDirection;
|
||||
|
||||
if (localRenderer != null) {
|
||||
localRenderer.setMirror(cameraDirection == CameraState.Direction.FRONT);
|
||||
animatePipToRectangle();
|
||||
break;
|
||||
case SMALL_SQUARE:
|
||||
largeLocalRenderFrame.setVisibility(View.GONE);
|
||||
localRenderPipFrame.setVisibility(View.VISIBLE);
|
||||
animatePipToSquare();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,17 +275,16 @@ public class WebRtcCallView extends FrameLayout {
|
||||
}
|
||||
|
||||
recipientId = recipient.getId();
|
||||
recipientName.setText(recipient.getDisplayName(getContext()));
|
||||
avatar.setFallbackPhotoProvider(FALLBACK_PHOTO_PROVIDER);
|
||||
avatar.setAvatar(GlideApp.with(this), recipient, false);
|
||||
AvatarUtil.loadBlurredIconIntoViewBackground(recipient, this);
|
||||
|
||||
setRecipientCallCard(recipient);
|
||||
}
|
||||
|
||||
public void showCallCard(boolean showCallCard) {
|
||||
avatarCard.setVisibility(showCallCard ? VISIBLE : GONE);
|
||||
avatar.setVisibility(showCallCard ? GONE : VISIBLE);
|
||||
if (recipient.isGroup()) {
|
||||
recipientName.setText(R.string.WebRtcCallView__group_call);
|
||||
if (toolbar.getMenu().findItem(R.id.menu_group_call_participants_list) == null) {
|
||||
toolbar.inflateMenu(R.menu.group_call);
|
||||
toolbar.setOnMenuItemClickListener(unused -> showParticipantsList());
|
||||
}
|
||||
} else {
|
||||
recipientName.setText(recipient.getDisplayName(getContext()));
|
||||
}
|
||||
}
|
||||
|
||||
public void setStatus(@NonNull String status) {
|
||||
@@ -302,11 +311,15 @@ public class WebRtcCallView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
public void setWebRtcControls(WebRtcControls webRtcControls) {
|
||||
public void setWebRtcControls(@NonNull WebRtcControls webRtcControls) {
|
||||
Set<View> lastVisibleSet = new HashSet<>(visibleViewSet);
|
||||
|
||||
visibleViewSet.clear();
|
||||
|
||||
if (webRtcControls.displayStartCallControls()) {
|
||||
visibleViewSet.add(startCallControls);
|
||||
}
|
||||
|
||||
if (webRtcControls.displayTopViews()) {
|
||||
visibleViewSet.addAll(topViews);
|
||||
}
|
||||
@@ -378,8 +391,39 @@ public class WebRtcCallView extends FrameLayout {
|
||||
return videoToggle;
|
||||
}
|
||||
|
||||
private void animatePipToRectangle() {
|
||||
ResizeAnimation animation = new ResizeAnimation(localRenderPipFrame, ViewUtil.dpToPx(90), ViewUtil.dpToPx(160));
|
||||
animation.setDuration(PIP_RESIZE_DURATION);
|
||||
animation.setAnimationListener(new SimpleAnimationListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
pictureInPictureGestureHelper.enableCorners();
|
||||
pictureInPictureGestureHelper.adjustPip();
|
||||
}
|
||||
});
|
||||
|
||||
localRenderPipFrame.startAnimation(animation);
|
||||
}
|
||||
|
||||
private void animatePipToSquare() {
|
||||
pictureInPictureGestureHelper.lockToBottomEnd();
|
||||
|
||||
pictureInPictureGestureHelper.performAfterFling(() -> {
|
||||
ResizeAnimation animation = new ResizeAnimation(localRenderPipFrame, ViewUtil.dpToPx(72), ViewUtil.dpToPx(72));
|
||||
animation.setDuration(PIP_RESIZE_DURATION);
|
||||
animation.setAnimationListener(new SimpleAnimationListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
pictureInPictureGestureHelper.adjustPip();
|
||||
}
|
||||
});
|
||||
|
||||
localRenderPipFrame.startAnimation(animation);
|
||||
});
|
||||
}
|
||||
|
||||
private void toggleControls() {
|
||||
if (controls.isFadeOutEnabled() && status.getVisibility() == VISIBLE) {
|
||||
if (controls.isFadeOutEnabled() && toolbar.getVisibility() == VISIBLE) {
|
||||
fadeOutControls();
|
||||
} else {
|
||||
fadeInControls();
|
||||
@@ -458,40 +502,6 @@ public class WebRtcCallView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private static void setRenderer(@NonNull ViewGroup container, @Nullable View renderer) {
|
||||
if (renderer == null) {
|
||||
container.removeAllViews();
|
||||
return;
|
||||
}
|
||||
|
||||
ViewParent parent = renderer.getParent();
|
||||
if (parent != null && parent != container) {
|
||||
((ViewGroup) parent).removeAllViews();
|
||||
}
|
||||
|
||||
if (parent == container) {
|
||||
return;
|
||||
}
|
||||
|
||||
container.addView(renderer);
|
||||
}
|
||||
|
||||
private void setRecipientCallCard(@NonNull Recipient recipient) {
|
||||
ContactPhoto contactPhoto = recipient.getContactPhoto();
|
||||
FallbackContactPhoto fallbackPhoto = recipient.getFallbackContactPhoto(FALLBACK_PHOTO_PROVIDER);
|
||||
|
||||
GlideApp.with(this).load(contactPhoto)
|
||||
.fallback(fallbackPhoto.asCallCard(getContext()))
|
||||
.error(fallbackPhoto.asCallCard(getContext()))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.into(this.avatarCard);
|
||||
|
||||
if (contactPhoto == null) this.avatarCard.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
|
||||
else this.avatarCard.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||
|
||||
this.avatarCard.setBackgroundColor(recipient.getColor().toActionBarColor(getContext()));
|
||||
}
|
||||
|
||||
private void updateButtonStateForLargeButtons() {
|
||||
cameraDirectionToggle.setImageResource(R.drawable.webrtc_call_screen_camera_toggle);
|
||||
hangup.setImageResource(R.drawable.webrtc_call_screen_hangup);
|
||||
@@ -508,14 +518,14 @@ public class WebRtcCallView extends FrameLayout {
|
||||
audioToggle.setImageResource(R.drawable.webrtc_call_screen_speaker_toggle_small);
|
||||
}
|
||||
|
||||
private static final class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider {
|
||||
@Override
|
||||
public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
|
||||
return new ResourceContactPhoto(R.drawable.ic_profile_outline_120);
|
||||
}
|
||||
private boolean showParticipantsList() {
|
||||
controlsListener.onShowParticipantsList();
|
||||
return true;
|
||||
}
|
||||
|
||||
public interface ControlsListener {
|
||||
void onStartCall();
|
||||
void onCancelStartCall();
|
||||
void onControlsFadeOut();
|
||||
void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput);
|
||||
void onVideoChanged(boolean isVideoEnabled);
|
||||
@@ -525,6 +535,7 @@ public class WebRtcCallView extends FrameLayout {
|
||||
void onDenyCallPressed();
|
||||
void onAcceptCallWithVoiceOnlyPressed();
|
||||
void onAcceptCallPressed();
|
||||
void onDownCaretPressed();
|
||||
void onShowParticipantsList();
|
||||
void onPageChanged(@NonNull CallParticipantsState.SelectedPage page);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,60 +10,38 @@ import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
|
||||
public class WebRtcCallViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<Boolean> remoteVideoEnabled = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<Boolean> microphoneEnabled = new MutableLiveData<>(true);
|
||||
private final MutableLiveData<WebRtcLocalRenderState> localRenderState = new MutableLiveData<>(WebRtcLocalRenderState.GONE);
|
||||
private final MutableLiveData<Boolean> isInPipMode = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<Boolean> localVideoEnabled = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<CameraState.Direction> cameraDirection = new MutableLiveData<>(CameraState.Direction.FRONT);
|
||||
private final LiveData<Boolean> shouldDisplayLocal = LiveDataUtil.combineLatest(isInPipMode, localVideoEnabled, (a, b) -> !a && b);
|
||||
private final LiveData<WebRtcLocalRenderState> realLocalRenderState = LiveDataUtil.combineLatest(shouldDisplayLocal, localRenderState, this::getRealLocalRenderState);
|
||||
private final MutableLiveData<WebRtcControls> webRtcControls = new MutableLiveData<>(WebRtcControls.NONE);
|
||||
private final LiveData<WebRtcControls> realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, webRtcControls, this::getRealWebRtcControls);
|
||||
private final SingleLiveEvent<Event> events = new SingleLiveEvent<Event>();
|
||||
private final MutableLiveData<Long> ellapsed = new MutableLiveData<>(-1L);
|
||||
private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
|
||||
|
||||
private boolean canDisplayTooltipIfNeeded = true;
|
||||
private boolean hasEnabledLocalVideo = false;
|
||||
private boolean showVideoForOutgoing = false;
|
||||
private long callConnectedTime = -1;
|
||||
private Handler ellapsedTimeHandler = new Handler(Looper.getMainLooper());
|
||||
private boolean answerWithVideoAvailable = false;
|
||||
private Runnable ellapsedTimeRunnable = this::handleTick;
|
||||
private final MutableLiveData<Boolean> microphoneEnabled = new MutableLiveData<>(true);
|
||||
private final MutableLiveData<Boolean> isInPipMode = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<WebRtcControls> webRtcControls = new MutableLiveData<>(WebRtcControls.NONE);
|
||||
private final LiveData<WebRtcControls> realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, webRtcControls, this::getRealWebRtcControls);
|
||||
private final SingleLiveEvent<Event> events = new SingleLiveEvent<Event>();
|
||||
private final MutableLiveData<Long> elapsed = new MutableLiveData<>(-1L);
|
||||
private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
|
||||
private final MutableLiveData<CallParticipantsState> participantsState = new MutableLiveData<>(CallParticipantsState.STARTING_STATE);
|
||||
|
||||
private boolean canDisplayTooltipIfNeeded = true;
|
||||
private boolean hasEnabledLocalVideo = false;
|
||||
private long callConnectedTime = -1;
|
||||
private Handler elapsedTimeHandler = new Handler(Looper.getMainLooper());
|
||||
private boolean answerWithVideoAvailable = false;
|
||||
private Runnable elapsedTimeRunnable = this::handleTick;
|
||||
private boolean canEnterPipMode = false;
|
||||
|
||||
private final WebRtcCallRepository repository = new WebRtcCallRepository();
|
||||
|
||||
public LiveData<Boolean> getRemoteVideoEnabled() {
|
||||
return Transformations.distinctUntilChanged(remoteVideoEnabled);
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getMicrophoneEnabled() {
|
||||
return Transformations.distinctUntilChanged(microphoneEnabled);
|
||||
}
|
||||
|
||||
public LiveData<CameraState.Direction> getCameraDirection() {
|
||||
return Transformations.distinctUntilChanged(cameraDirection);
|
||||
}
|
||||
|
||||
public LiveData<Boolean> displaySquareCallCard() {
|
||||
return isInPipMode;
|
||||
}
|
||||
|
||||
public LiveData<WebRtcLocalRenderState> getLocalRenderState() {
|
||||
return realLocalRenderState;
|
||||
}
|
||||
|
||||
public LiveData<WebRtcControls> getWebRtcControls() {
|
||||
return realWebRtcControls;
|
||||
}
|
||||
@@ -81,7 +59,15 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
public LiveData<Long> getCallTime() {
|
||||
return Transformations.map(ellapsed, timeInCall -> callConnectedTime == -1 ? -1 : timeInCall);
|
||||
return Transformations.map(elapsed, timeInCall -> callConnectedTime == -1 ? -1 : timeInCall);
|
||||
}
|
||||
|
||||
public LiveData<CallParticipantsState> getCallParticipantsState() {
|
||||
return participantsState;
|
||||
}
|
||||
|
||||
public boolean canEnterPipMode() {
|
||||
return canEnterPipMode;
|
||||
}
|
||||
|
||||
public boolean isAnswerWithVideoAvailable() {
|
||||
@@ -91,6 +77,15 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
@MainThread
|
||||
public void setIsInPipMode(boolean isInPipMode) {
|
||||
this.isInPipMode.setValue(isInPipMode);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), isInPipMode));
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void setIsViewingFocusedParticipant(@NonNull CallParticipantsState.SelectedPage page) {
|
||||
//noinspection ConstantConditions
|
||||
participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), page));
|
||||
}
|
||||
|
||||
public void onDismissedVideoTooltip() {
|
||||
@@ -99,27 +94,20 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
|
||||
@MainThread
|
||||
public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel, boolean enableVideo) {
|
||||
remoteVideoEnabled.setValue(webRtcViewModel.isRemoteVideoEnabled());
|
||||
microphoneEnabled.setValue(webRtcViewModel.isMicrophoneEnabled());
|
||||
canEnterPipMode = true;
|
||||
|
||||
if (isValidCameraDirectionForUi(webRtcViewModel.getLocalCameraState().getActiveDirection())) {
|
||||
cameraDirection.setValue(webRtcViewModel.getLocalCameraState().getActiveDirection());
|
||||
}
|
||||
CallParticipant localParticipant = webRtcViewModel.getLocalParticipant();
|
||||
|
||||
localVideoEnabled.setValue(webRtcViewModel.getLocalCameraState().isEnabled());
|
||||
microphoneEnabled.setValue(localParticipant.isMicrophoneEnabled());
|
||||
|
||||
if (enableVideo) {
|
||||
showVideoForOutgoing = webRtcViewModel.getState() == WebRtcViewModel.State.CALL_OUTGOING;
|
||||
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_OUTGOING) {
|
||||
showVideoForOutgoing = false;
|
||||
}
|
||||
//noinspection ConstantConditions
|
||||
participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), webRtcViewModel, enableVideo));
|
||||
|
||||
updateLocalRenderState(webRtcViewModel.getState());
|
||||
updateWebRtcControls(webRtcViewModel.getState(),
|
||||
webRtcViewModel.getLocalCameraState().isEnabled(),
|
||||
localParticipant.getCameraState().isEnabled(),
|
||||
webRtcViewModel.isRemoteVideoEnabled(),
|
||||
webRtcViewModel.isRemoteVideoOffer(),
|
||||
webRtcViewModel.getLocalCameraState().getCameraCount() > 1,
|
||||
localParticipant.isMoreThanOneCameraAvailable(),
|
||||
webRtcViewModel.isBluetoothAvailable(),
|
||||
repository.getAudioOutput());
|
||||
|
||||
@@ -131,9 +119,9 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
callConnectedTime = -1;
|
||||
}
|
||||
|
||||
if (webRtcViewModel.getLocalCameraState().isEnabled()) {
|
||||
if (localParticipant.getCameraState().isEnabled()) {
|
||||
canDisplayTooltipIfNeeded = false;
|
||||
hasEnabledLocalVideo = true;
|
||||
hasEnabledLocalVideo = true;
|
||||
events.setValue(Event.DISMISS_VIDEO_TOOLTIP);
|
||||
}
|
||||
|
||||
@@ -144,27 +132,14 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidCameraDirectionForUi(CameraState.Direction direction) {
|
||||
return direction == CameraState.Direction.FRONT || direction == CameraState.Direction.BACK;
|
||||
}
|
||||
|
||||
private void updateLocalRenderState(WebRtcViewModel.State state) {
|
||||
if (state == WebRtcViewModel.State.CALL_CONNECTED) {
|
||||
localRenderState.setValue(WebRtcLocalRenderState.SMALL);
|
||||
} else {
|
||||
localRenderState.setValue(WebRtcLocalRenderState.LARGE);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateWebRtcControls(WebRtcViewModel.State state,
|
||||
private void updateWebRtcControls(@NonNull WebRtcViewModel.State state,
|
||||
boolean isLocalVideoEnabled,
|
||||
boolean isRemoteVideoEnabled,
|
||||
boolean isRemoteVideoOffer,
|
||||
boolean isMoreThanOneCameraAvailable,
|
||||
boolean isBluetoothAvailable,
|
||||
WebRtcAudioOutput audioOutput)
|
||||
@NonNull WebRtcAudioOutput audioOutput)
|
||||
{
|
||||
|
||||
final WebRtcControls.CallState callState;
|
||||
|
||||
switch (state) {
|
||||
@@ -185,20 +160,14 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
audioOutput));
|
||||
}
|
||||
|
||||
private @NonNull WebRtcLocalRenderState getRealLocalRenderState(boolean shouldDisplayLocalVideo, @NonNull WebRtcLocalRenderState state) {
|
||||
if (shouldDisplayLocalVideo || showVideoForOutgoing) return state;
|
||||
else return WebRtcLocalRenderState.GONE;
|
||||
}
|
||||
|
||||
private @NonNull WebRtcControls getRealWebRtcControls(boolean isInPipMode, @NonNull WebRtcControls controls) {
|
||||
if (isInPipMode) return WebRtcControls.PIP;
|
||||
else return controls;
|
||||
return isInPipMode ? WebRtcControls.PIP : controls;
|
||||
}
|
||||
|
||||
private void startTimer() {
|
||||
cancelTimer();
|
||||
|
||||
ellapsedTimeHandler.post(ellapsedTimeRunnable);
|
||||
elapsedTimeHandler.post(elapsedTimeRunnable);
|
||||
}
|
||||
|
||||
private void handleTick() {
|
||||
@@ -208,13 +177,13 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
|
||||
long newValue = (System.currentTimeMillis() - callConnectedTime) / 1000;
|
||||
|
||||
ellapsed.postValue(newValue);
|
||||
elapsed.postValue(newValue);
|
||||
|
||||
ellapsedTimeHandler.postDelayed(ellapsedTimeRunnable, 1000);
|
||||
elapsedTimeHandler.postDelayed(elapsedTimeRunnable, 1000);
|
||||
}
|
||||
|
||||
private void cancelTimer() {
|
||||
ellapsedTimeHandler.removeCallbacks(ellapsedTimeRunnable);
|
||||
elapsedTimeHandler.removeCallbacks(elapsedTimeRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -36,6 +36,10 @@ public final class WebRtcControls {
|
||||
this.audioOutput = audioOutput;
|
||||
}
|
||||
|
||||
boolean displayStartCallControls() {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean displayEndCall() {
|
||||
return isOngoing();
|
||||
}
|
||||
@@ -88,7 +92,7 @@ public final class WebRtcControls {
|
||||
return !isInPipMode;
|
||||
}
|
||||
|
||||
WebRtcAudioOutput getAudioOutput() {
|
||||
@NonNull WebRtcAudioOutput getAudioOutput() {
|
||||
return audioOutput;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
public enum WebRtcLocalRenderState {
|
||||
GONE,
|
||||
SMALL,
|
||||
SMALL_RECTANGLE,
|
||||
SMALL_SQUARE,
|
||||
LARGE
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc.participantslist;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder;
|
||||
|
||||
public class CallParticipantViewHolder extends RecipientViewHolder<CallParticipantViewState> {
|
||||
public CallParticipantViewHolder(@NonNull View itemView) {
|
||||
super(itemView, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull CallParticipantViewState model) {
|
||||
super.bind(model);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc.participantslist;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.viewholders.RecipientMappingModel;
|
||||
|
||||
public final class CallParticipantViewState extends RecipientMappingModel<CallParticipantViewState> {
|
||||
|
||||
private final CallParticipant callParticipant;
|
||||
|
||||
CallParticipantViewState(@NonNull CallParticipant callParticipant) {
|
||||
this.callParticipant = callParticipant;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Recipient getRecipient() {
|
||||
return callParticipant.getRecipient();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc.participantslist;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||
|
||||
public class CallParticipantsListAdapter extends MappingAdapter {
|
||||
|
||||
CallParticipantsListAdapter() {
|
||||
registerFactory(CallParticipantsListHeader.class, new LayoutFactory<>(CallParticipantsListHeaderViewHolder::new, R.layout.call_participants_list_header));
|
||||
registerFactory(CallParticipantViewState.class, new LayoutFactory<>(CallParticipantViewHolder::new, R.layout.call_participants_list_item));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc.participantslist;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
|
||||
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.util.BottomSheetUtil;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class CallParticipantsListDialog extends BottomSheetDialogFragment {
|
||||
|
||||
private RecyclerView participantList;
|
||||
private CallParticipantsListAdapter adapter;
|
||||
|
||||
public static void show(@NonNull FragmentManager manager) {
|
||||
CallParticipantsListDialog fragment = new CallParticipantsListDialog();
|
||||
|
||||
fragment.show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
|
||||
BottomSheetUtil.show(manager, tag, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_RoundedBottomSheet);
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
ContextThemeWrapper contextThemeWrapper = new ContextThemeWrapper(inflater.getContext(), R.style.TextSecure_DarkTheme);
|
||||
LayoutInflater themedInflater = LayoutInflater.from(contextThemeWrapper);
|
||||
|
||||
participantList = (RecyclerView) themedInflater.inflate(R.layout.call_participants_list_dialog, container, false);
|
||||
|
||||
return participantList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
final WebRtcCallViewModel viewModel = ViewModelProviders.of(requireActivity()).get(WebRtcCallViewModel.class);
|
||||
|
||||
initializeList();
|
||||
|
||||
viewModel.getCallParticipantsState().observe(getViewLifecycleOwner(), this::updateList);
|
||||
}
|
||||
|
||||
private void initializeList() {
|
||||
adapter = new CallParticipantsListAdapter();
|
||||
|
||||
participantList.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
participantList.setAdapter(adapter);
|
||||
}
|
||||
|
||||
private void updateList(@NonNull CallParticipantsState callParticipantsState) {
|
||||
List<MappingModel<?>> items = new ArrayList<>();
|
||||
|
||||
items.add(new CallParticipantsListHeader(callParticipantsState.getAllRemoteParticipants().size() + 1));
|
||||
|
||||
items.add(new CallParticipantViewState(callParticipantsState.getLocalParticipant()));
|
||||
for (CallParticipant callParticipant : callParticipantsState.getAllRemoteParticipants()) {
|
||||
items.add(new CallParticipantViewState(callParticipant));
|
||||
}
|
||||
|
||||
adapter.submitList(items);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc.participantslist;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
|
||||
public class CallParticipantsListHeader implements MappingModel<CallParticipantsListHeader> {
|
||||
|
||||
private int participantCount;
|
||||
|
||||
public CallParticipantsListHeader(int participantCount) {
|
||||
this.participantCount = participantCount;
|
||||
}
|
||||
|
||||
@NonNull String getHeader(@NonNull Context context) {
|
||||
return context.getResources().getQuantityString(R.plurals.CallParticipantsListDialog_in_this_call_d_people, participantCount, participantCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull CallParticipantsListHeader newItem) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull CallParticipantsListHeader newItem) {
|
||||
return participantCount == newItem.participantCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc.participantslist;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||
|
||||
public class CallParticipantsListHeaderViewHolder extends MappingViewHolder<CallParticipantsListHeader> {
|
||||
|
||||
private final TextView headerText;
|
||||
|
||||
public CallParticipantsListHeaderViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
headerText = findViewById(R.id.call_participants_list_header);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull CallParticipantsListHeader model) {
|
||||
headerText.setText(model.getHeader(getContext()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user