mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-08 07:38:34 +00:00
Add foundational UX and state support for Group Calling.
This commit is contained in:
parent
7baf8052a2
commit
dc4faf57cb
@ -41,6 +41,8 @@ import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
|
||||
import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
|
||||
@ -52,11 +54,13 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.WebRtcCallService;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
||||
|
||||
public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumberChangeDialog.Callback {
|
||||
|
||||
@ -162,7 +166,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
||||
}
|
||||
|
||||
private boolean enterPipModeIfPossible() {
|
||||
if (isSystemPipEnabledAndAvailable()) {
|
||||
if (viewModel.canEnterPipMode() && isSystemPipEnabledAndAvailable()) {
|
||||
PictureInPictureParams params = new PictureInPictureParams.Builder()
|
||||
.setAspectRatio(new Rational(9, 16))
|
||||
.build();
|
||||
@ -203,14 +207,11 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
||||
private void initializeViewModel() {
|
||||
viewModel = ViewModelProviders.of(this).get(WebRtcCallViewModel.class);
|
||||
viewModel.setIsInPipMode(isInPipMode());
|
||||
viewModel.getRemoteVideoEnabled().observe(this,callScreen::setRemoteVideoEnabled);
|
||||
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
|
||||
viewModel.getCameraDirection().observe(this, callScreen::setCameraDirection);
|
||||
viewModel.getLocalRenderState().observe(this, callScreen::setLocalRenderState);
|
||||
viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
|
||||
viewModel.getEvents().observe(this, this::handleViewModelEvent);
|
||||
viewModel.getCallTime().observe(this, this::handleCallTime);
|
||||
viewModel.displaySquareCallCard().observe(this, callScreen::showCallCard);
|
||||
viewModel.getCallParticipantsState().observe(this, callScreen::updateCallParticipants);
|
||||
}
|
||||
|
||||
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
|
||||
@ -375,19 +376,13 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
private void handleIncomingCall(@NonNull WebRtcViewModel event) {
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
}
|
||||
|
||||
private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling));
|
||||
}
|
||||
|
||||
private void handleTerminate(@NonNull Recipient recipient, @NonNull HangupMessage.Type hangupType) {
|
||||
Log.i(TAG, "handleTerminate called: " + hangupType.name());
|
||||
|
||||
callScreen.setRecipient(recipient);
|
||||
callScreen.setStatusFromHangupType(hangupType);
|
||||
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||
@ -399,32 +394,27 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
||||
}
|
||||
|
||||
private void handleCallRinging(@NonNull WebRtcViewModel event) {
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
callScreen.setStatus(getString(R.string.RedPhone_ringing));
|
||||
}
|
||||
|
||||
private void handleCallBusy(@NonNull WebRtcViewModel event) {
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
callScreen.setStatus(getString(R.string.RedPhone_busy));
|
||||
delayedFinish(WebRtcCallService.BUSY_TONE_LENGTH);
|
||||
}
|
||||
|
||||
private void handleCallConnected(@NonNull WebRtcViewModel event) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
}
|
||||
|
||||
private void handleRecipientUnavailable(@NonNull WebRtcViewModel event) {
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable));
|
||||
delayedFinish();
|
||||
}
|
||||
|
||||
private void handleServerFailure(@NonNull WebRtcViewModel event) {
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
callScreen.setStatus(getString(R.string.RedPhone_network_failed));
|
||||
delayedFinish();
|
||||
}
|
||||
@ -452,8 +442,8 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
||||
}
|
||||
|
||||
private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
|
||||
final IdentityKey theirKey = event.getIdentityKey();
|
||||
final Recipient recipient = event.getRecipient();
|
||||
final IdentityKey theirKey = event.getRemoteParticipants().get(0).getIdentityKey();
|
||||
final Recipient recipient = event.getRemoteParticipants().get(0).getRecipient();
|
||||
|
||||
if (theirKey == null) {
|
||||
handleTerminate(recipient, HangupMessage.Type.NORMAL);
|
||||
@ -493,10 +483,11 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
||||
}
|
||||
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
public void onEventMainThread(final WebRtcViewModel event) {
|
||||
public void onEventMainThread(@NonNull WebRtcViewModel event) {
|
||||
Log.i(TAG, "Got message from service: " + event);
|
||||
|
||||
viewModel.setRecipient(event.getRecipient());
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
|
||||
switch (event.getState()) {
|
||||
case CALL_CONNECTED: handleCallConnected(event); break;
|
||||
@ -509,16 +500,12 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
||||
case CALL_NEEDS_PERMISSION: handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break;
|
||||
case NO_SUCH_USER: handleNoSuchUser(event); break;
|
||||
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(event); break;
|
||||
case CALL_INCOMING: handleIncomingCall(event); break;
|
||||
case CALL_OUTGOING: handleOutgoingCall(event); break;
|
||||
case CALL_BUSY: handleCallBusy(event); break;
|
||||
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
|
||||
}
|
||||
|
||||
callScreen.setLocalRenderer(event.getLocalRenderer());
|
||||
callScreen.setRemoteRenderer(event.getRemoteRenderer());
|
||||
|
||||
boolean enableVideo = event.getLocalCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
|
||||
boolean enableVideo = event.getLocalParticipant().getCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
|
||||
|
||||
viewModel.updateFromWebRtcViewModel(event, enableVideo);
|
||||
|
||||
@ -530,6 +517,22 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
||||
|
||||
private final class ControlsListener implements WebRtcCallView.ControlsListener {
|
||||
|
||||
@Override
|
||||
public void onStartCall() {
|
||||
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(viewModel.getRecipient().getId()))
|
||||
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, OfferMessage.Type.VIDEO_CALL.getCode());
|
||||
WebRtcCallActivity.this.startService(intent);
|
||||
|
||||
MessageSender.onMessageSent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelStartCall() {
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onControlsFadeOut() {
|
||||
if (videoTooltip != null) {
|
||||
@ -594,8 +597,13 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownCaretPressed() {
|
||||
public void onShowParticipantsList() {
|
||||
CallParticipantsListDialog.show(getSupportFragmentManager());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageChanged(@NonNull CallParticipantsState.SelectedPage page) {
|
||||
viewModel.setIsViewingFocusedParticipant(page);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,50 @@
|
||||
package org.thoughtcrime.securesms.animation;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.Transformation;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class ResizeAnimation extends Animation {
|
||||
|
||||
private final View target;
|
||||
private final int targetWidthPx;
|
||||
private final int targetHeightPx;
|
||||
|
||||
private int startWidth;
|
||||
private int startHeight;
|
||||
|
||||
public ResizeAnimation(@NonNull View target, int targetWidthPx, int targetHeightPx) {
|
||||
this.target = target;
|
||||
this.targetWidthPx = targetWidthPx;
|
||||
this.targetHeightPx = targetHeightPx;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyTransformation(float interpolatedTime, Transformation t) {
|
||||
int newWidth = (int) (startWidth + (targetWidthPx - startWidth) * interpolatedTime);
|
||||
int newHeight = (int) (startHeight + (targetHeightPx - startHeight) * interpolatedTime);
|
||||
|
||||
ViewGroup.LayoutParams params = target.getLayoutParams();
|
||||
|
||||
params.width = newWidth;
|
||||
params.height = newHeight;
|
||||
|
||||
target.setLayoutParams(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(int width, int height, int parentWidth, int parentHeight) {
|
||||
super.initialize(width, height, parentWidth, parentHeight);
|
||||
|
||||
this.startWidth = width;
|
||||
this.startHeight = height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean willChangeBounds() {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
@ -8,6 +8,8 @@ import android.graphics.drawable.LayerDrawable;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
@ -22,6 +24,8 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
|
||||
private final int smallResourceId;
|
||||
private final int callCardResourceId;
|
||||
|
||||
private ImageView.ScaleType scaleType = ImageView.ScaleType.CENTER;
|
||||
|
||||
public ResourceContactPhoto(@DrawableRes int resourceId) {
|
||||
this(resourceId, resourceId, resourceId);
|
||||
}
|
||||
@ -36,26 +40,31 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
|
||||
this.smallResourceId = smallResourceId;
|
||||
}
|
||||
|
||||
public void setScaleType(@NonNull ImageView.ScaleType scaleType) {
|
||||
this.scaleType = scaleType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asDrawable(Context context, int color) {
|
||||
public @NonNull Drawable asDrawable(@NonNull Context context, int color) {
|
||||
return asDrawable(context, color, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asDrawable(Context context, int color, boolean inverted) {
|
||||
public @NonNull Drawable asDrawable(@NonNull Context context, int color, boolean inverted) {
|
||||
return buildDrawable(context, resourceId, color, inverted);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asSmallDrawable(Context context, int color, boolean inverted) {
|
||||
public @NonNull Drawable asSmallDrawable(@NonNull Context context, int color, boolean inverted) {
|
||||
return buildDrawable(context, smallResourceId, color, inverted);
|
||||
}
|
||||
|
||||
private Drawable buildDrawable(Context context, int resourceId, int color, boolean inverted) {
|
||||
private @NonNull Drawable buildDrawable(@NonNull Context context, int resourceId, int color, boolean inverted) {
|
||||
Drawable background = TextDrawable.builder().buildRound(" ", inverted ? Color.WHITE : color);
|
||||
RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(AppCompatResources.getDrawable(context, resourceId));
|
||||
|
||||
foreground.setScaleType(ImageView.ScaleType.CENTER);
|
||||
//noinspection ConstantConditions
|
||||
foreground.setScaleType(scaleType);
|
||||
|
||||
if (inverted) {
|
||||
foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
|
||||
@ -68,12 +77,12 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asCallCard(Context context) {
|
||||
public @Nullable Drawable asCallCard(@NonNull Context context) {
|
||||
return AppCompatResources.getDrawable(context, callCardResourceId);
|
||||
}
|
||||
|
||||
private static class ExpandingLayerDrawable extends LayerDrawable {
|
||||
public ExpandingLayerDrawable(Drawable[] layers) {
|
||||
public ExpandingLayerDrawable(@NonNull Drawable[] layers) {
|
||||
super(layers);
|
||||
}
|
||||
|
||||
|
@ -1,48 +0,0 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.mentions;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||
|
||||
public class MentionViewHolder extends MappingViewHolder<MentionViewState> {
|
||||
|
||||
private final AvatarImageView avatar;
|
||||
private final TextView name;
|
||||
|
||||
@Nullable private final MentionEventsListener mentionEventsListener;
|
||||
|
||||
public MentionViewHolder(@NonNull View itemView, @Nullable MentionEventsListener mentionEventsListener) {
|
||||
super(itemView);
|
||||
this.mentionEventsListener = mentionEventsListener;
|
||||
|
||||
avatar = findViewById(R.id.mention_recipient_avatar);
|
||||
name = findViewById(R.id.mention_recipient_name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull MentionViewState model) {
|
||||
avatar.setRecipient(model.getRecipient());
|
||||
name.setText(model.getName(context));
|
||||
itemView.setOnClickListener(v -> {
|
||||
if (mentionEventsListener != null) {
|
||||
mentionEventsListener.onMentionClicked(model.getRecipient());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public interface MentionEventsListener {
|
||||
void onMentionClicked(@NonNull Recipient recipient);
|
||||
}
|
||||
|
||||
public static MappingAdapter.Factory<MentionViewState> createFactory(@Nullable MentionEventsListener mentionEventsListener) {
|
||||
return new MappingAdapter.LayoutFactory<>(view -> new MentionViewHolder(view, mentionEventsListener), R.layout.mentions_picker_recipient_list_item);
|
||||
}
|
||||
}
|
@ -1,17 +1,11 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.mentions;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.viewholders.RecipientMappingModel;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class MentionViewState implements MappingModel<MentionViewState> {
|
||||
public final class MentionViewState extends RecipientMappingModel<MentionViewState> {
|
||||
|
||||
private final Recipient recipient;
|
||||
|
||||
@ -19,23 +13,8 @@ public final class MentionViewState implements MappingModel<MentionViewState> {
|
||||
this.recipient = recipient;
|
||||
}
|
||||
|
||||
@NonNull String getName(@NonNull Context context) {
|
||||
return recipient.getDisplayName(context);
|
||||
}
|
||||
|
||||
@NonNull Recipient getRecipient() {
|
||||
@Override
|
||||
public @NonNull Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull MentionViewState newItem) {
|
||||
return recipient.getId().equals(newItem.recipient.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull MentionViewState newItem) {
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
return recipient.getDisplayName(context).equals(newItem.recipient.getDisplayName(context)) &&
|
||||
Objects.equals(recipient.getProfileAvatar(), newItem.recipient.getProfileAvatar());
|
||||
}
|
||||
}
|
||||
|
@ -3,18 +3,20 @@ package org.thoughtcrime.securesms.conversation.ui.mentions;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionViewHolder.MentionEventsListener;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder;
|
||||
import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder.EventListener;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MentionsPickerAdapter extends MappingAdapter {
|
||||
private final Runnable currentListChangedListener;
|
||||
|
||||
public MentionsPickerAdapter(@Nullable MentionEventsListener mentionEventsListener, @NonNull Runnable currentListChangedListener) {
|
||||
public MentionsPickerAdapter(@Nullable EventListener<MentionViewState> listener, @NonNull Runnable currentListChangedListener) {
|
||||
this.currentListChangedListener = currentListChangedListener;
|
||||
registerFactory(MentionViewState.class, MentionViewHolder.createFactory(mentionEventsListener));
|
||||
registerFactory(MentionViewState.class, RecipientViewHolder.createFactory(R.layout.mentions_picker_recipient_list_item, listener));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,117 @@
|
||||
package org.thoughtcrime.securesms.events;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class CallParticipant {
|
||||
|
||||
private final @NonNull CameraState cameraState;
|
||||
private final @NonNull Recipient recipient;
|
||||
private final @Nullable IdentityKey identityKey;
|
||||
private final @NonNull BroadcastVideoSink videoSink;
|
||||
private final boolean videoEnabled;
|
||||
private final boolean microphoneEnabled;
|
||||
|
||||
public static @NonNull CallParticipant createLocal(@NonNull CameraState cameraState,
|
||||
@NonNull BroadcastVideoSink renderer,
|
||||
boolean microphoneEnabled)
|
||||
{
|
||||
return new CallParticipant(Recipient.self(),
|
||||
null,
|
||||
renderer,
|
||||
cameraState,
|
||||
cameraState.isEnabled() && cameraState.getCameraCount() > 0,
|
||||
microphoneEnabled);
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipant createRemote(@NonNull Recipient recipient,
|
||||
@Nullable IdentityKey identityKey,
|
||||
@NonNull BroadcastVideoSink renderer,
|
||||
boolean videoEnabled)
|
||||
{
|
||||
return new CallParticipant(recipient, identityKey, renderer, CameraState.UNKNOWN, videoEnabled, true);
|
||||
}
|
||||
|
||||
private CallParticipant(@NonNull Recipient recipient,
|
||||
@Nullable IdentityKey identityKey,
|
||||
@NonNull BroadcastVideoSink videoSink,
|
||||
@NonNull CameraState cameraState,
|
||||
boolean videoEnabled,
|
||||
boolean microphoneEnabled)
|
||||
{
|
||||
this.recipient = recipient;
|
||||
this.identityKey = identityKey;
|
||||
this.videoSink = videoSink;
|
||||
this.cameraState = cameraState;
|
||||
this.videoEnabled = videoEnabled;
|
||||
this.microphoneEnabled = microphoneEnabled;
|
||||
}
|
||||
|
||||
public @NonNull CallParticipant withIdentityKey(@NonNull IdentityKey identityKey) {
|
||||
return new CallParticipant(recipient, identityKey, videoSink, cameraState, videoEnabled, microphoneEnabled);
|
||||
}
|
||||
|
||||
public @NonNull CallParticipant withVideoEnabled(boolean videoEnabled) {
|
||||
return new CallParticipant(recipient, identityKey, videoSink, cameraState, videoEnabled, microphoneEnabled);
|
||||
}
|
||||
|
||||
public @NonNull Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public @Nullable IdentityKey getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public @NonNull BroadcastVideoSink getVideoSink() {
|
||||
return videoSink;
|
||||
}
|
||||
|
||||
public @NonNull CameraState getCameraState() {
|
||||
return cameraState;
|
||||
}
|
||||
|
||||
public boolean isVideoEnabled() {
|
||||
return videoEnabled;
|
||||
}
|
||||
|
||||
public boolean isMicrophoneEnabled() {
|
||||
return microphoneEnabled;
|
||||
}
|
||||
|
||||
public @NonNull CameraState.Direction getCameraDirection() {
|
||||
if (cameraState.getActiveDirection() == CameraState.Direction.BACK) {
|
||||
return cameraState.getActiveDirection();
|
||||
}
|
||||
return CameraState.Direction.FRONT;
|
||||
}
|
||||
|
||||
public boolean isMoreThanOneCameraAvailable() {
|
||||
return cameraState.getCameraCount() > 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
CallParticipant that = (CallParticipant) o;
|
||||
return videoEnabled == that.videoEnabled &&
|
||||
microphoneEnabled == that.microphoneEnabled &&
|
||||
cameraState.equals(that.cameraState) &&
|
||||
recipient.equals(that.recipient) &&
|
||||
Objects.equals(identityKey, that.identityKey) &&
|
||||
Objects.equals(videoSink, that.videoSink);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(cameraState, recipient, identityKey, videoSink, videoEnabled, microphoneEnabled);
|
||||
}
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
package org.thoughtcrime.securesms.events;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
import org.webrtc.SurfaceViewRenderer;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class WebRtcViewModel {
|
||||
|
||||
@ -33,70 +34,34 @@ public class WebRtcViewModel {
|
||||
CALL_ONGOING_ELSEWHERE
|
||||
}
|
||||
|
||||
|
||||
private final @NonNull State state;
|
||||
private final @NonNull Recipient recipient;
|
||||
private final @Nullable IdentityKey identityKey;
|
||||
|
||||
private final boolean remoteVideoEnabled;
|
||||
private final @NonNull State state;
|
||||
private final @NonNull Recipient recipient;
|
||||
|
||||
private final boolean isBluetoothAvailable;
|
||||
private final boolean isMicrophoneEnabled;
|
||||
private final boolean isRemoteVideoOffer;
|
||||
private final long callConnectedTime;
|
||||
|
||||
private final CameraState localCameraState;
|
||||
private final TextureViewRenderer localRenderer;
|
||||
private final TextureViewRenderer remoteRenderer;
|
||||
private final CallParticipant localParticipant;
|
||||
private final List<CallParticipant> remoteParticipants;
|
||||
|
||||
private final long callConnectedTime;
|
||||
|
||||
public WebRtcViewModel(@NonNull State state,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull CameraState localCameraState,
|
||||
@NonNull TextureViewRenderer localRenderer,
|
||||
@NonNull TextureViewRenderer remoteRenderer,
|
||||
boolean remoteVideoEnabled,
|
||||
boolean isBluetoothAvailable,
|
||||
boolean isMicrophoneEnabled,
|
||||
boolean isRemoteVideoOffer,
|
||||
long callConnectedTime)
|
||||
{
|
||||
this(state,
|
||||
recipient,
|
||||
null,
|
||||
localCameraState,
|
||||
localRenderer,
|
||||
remoteRenderer,
|
||||
remoteVideoEnabled,
|
||||
isBluetoothAvailable,
|
||||
isMicrophoneEnabled,
|
||||
isRemoteVideoOffer,
|
||||
callConnectedTime);
|
||||
}
|
||||
|
||||
public WebRtcViewModel(@NonNull State state,
|
||||
@NonNull Recipient recipient,
|
||||
@Nullable IdentityKey identityKey,
|
||||
@NonNull CameraState localCameraState,
|
||||
@NonNull TextureViewRenderer localRenderer,
|
||||
@NonNull TextureViewRenderer remoteRenderer,
|
||||
boolean remoteVideoEnabled,
|
||||
boolean isBluetoothAvailable,
|
||||
boolean isMicrophoneEnabled,
|
||||
boolean isRemoteVideoOffer,
|
||||
long callConnectedTime)
|
||||
public WebRtcViewModel(@NonNull State state,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull CameraState localCameraState,
|
||||
@NonNull BroadcastVideoSink localSink,
|
||||
boolean isBluetoothAvailable,
|
||||
boolean isMicrophoneEnabled,
|
||||
boolean isRemoteVideoOffer,
|
||||
long callConnectedTime,
|
||||
@NonNull List<CallParticipant> remoteParticipants)
|
||||
{
|
||||
this.state = state;
|
||||
this.recipient = recipient;
|
||||
this.localCameraState = localCameraState;
|
||||
this.localRenderer = localRenderer;
|
||||
this.remoteRenderer = remoteRenderer;
|
||||
this.identityKey = identityKey;
|
||||
this.remoteVideoEnabled = remoteVideoEnabled;
|
||||
this.isBluetoothAvailable = isBluetoothAvailable;
|
||||
this.isMicrophoneEnabled = isMicrophoneEnabled;
|
||||
this.isRemoteVideoOffer = isRemoteVideoOffer;
|
||||
this.callConnectedTime = callConnectedTime;
|
||||
this.remoteParticipants = remoteParticipants;
|
||||
|
||||
localParticipant = CallParticipant.createLocal(localCameraState, localSink, isMicrophoneEnabled);
|
||||
}
|
||||
|
||||
public @NonNull State getState() {
|
||||
@ -107,50 +72,28 @@ public class WebRtcViewModel {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public @NonNull CameraState getLocalCameraState() {
|
||||
return localCameraState;
|
||||
}
|
||||
|
||||
public @Nullable IdentityKey getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public boolean isRemoteVideoEnabled() {
|
||||
return remoteVideoEnabled;
|
||||
return Stream.of(remoteParticipants).anyMatch(CallParticipant::isVideoEnabled);
|
||||
}
|
||||
|
||||
public boolean isBluetoothAvailable() {
|
||||
return isBluetoothAvailable;
|
||||
}
|
||||
|
||||
public boolean isMicrophoneEnabled() {
|
||||
return isMicrophoneEnabled;
|
||||
}
|
||||
|
||||
public boolean isRemoteVideoOffer() {
|
||||
return isRemoteVideoOffer;
|
||||
}
|
||||
|
||||
public TextureViewRenderer getLocalRenderer() {
|
||||
return localRenderer;
|
||||
}
|
||||
|
||||
public TextureViewRenderer getRemoteRenderer() {
|
||||
return remoteRenderer;
|
||||
}
|
||||
|
||||
public long getCallConnectedTime() {
|
||||
return callConnectedTime;
|
||||
}
|
||||
|
||||
public @NonNull String toString() {
|
||||
return "[State: " + state +
|
||||
", recipient: " + recipient.getId().serialize() +
|
||||
", identity: " + identityKey +
|
||||
", remoteVideo: " + remoteVideoEnabled +
|
||||
", localVideo: " + localCameraState.isEnabled() +
|
||||
", isRemoteVideoOffer: " + isRemoteVideoOffer +
|
||||
", callConnectedTime: " + callConnectedTime +
|
||||
"]";
|
||||
public @NonNull CallParticipant getLocalParticipant() {
|
||||
return localParticipant;
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getRemoteParticipants() {
|
||||
return remoteParticipants;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -26,11 +26,12 @@ import org.signal.ringrtc.IceCandidate;
|
||||
import org.signal.ringrtc.Remote;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.WebRtcCallActivity;
|
||||
import org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer;
|
||||
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.notifications.DoNotDisturbUtil;
|
||||
@ -48,7 +49,6 @@ import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TelephonyUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder;
|
||||
import org.thoughtcrime.securesms.webrtc.IncomingPstnCallReceiver;
|
||||
import org.thoughtcrime.securesms.webrtc.UncaughtExceptionHandlerManager;
|
||||
@ -58,7 +58,6 @@ import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
|
||||
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.PeerConnection;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
@ -74,8 +73,11 @@ import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserExce
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@ -167,7 +169,6 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
|
||||
private CameraState localCameraState = CameraState.UNKNOWN;
|
||||
private boolean microphoneEnabled = true;
|
||||
private boolean remoteVideoEnabled = false;
|
||||
private boolean bluetoothAvailable = false;
|
||||
private boolean enableVideoOnCreate = false;
|
||||
private boolean isRemoteVideoOffer = false;
|
||||
@ -189,10 +190,12 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
@Nullable private RemotePeer activePeer;
|
||||
@Nullable private RemotePeer busyPeer;
|
||||
@Nullable private SparseArray<RemotePeer> peerMap;
|
||||
@Nullable private TextureViewRenderer localRenderer;
|
||||
@Nullable private TextureViewRenderer remoteRenderer;
|
||||
@Nullable private EglBase eglBase;
|
||||
@Nullable private Camera camera;
|
||||
|
||||
@Nullable private EglBase eglBase;
|
||||
@Nullable private BroadcastVideoSink localSink;
|
||||
@Nullable private Camera camera;
|
||||
|
||||
private final Map<Recipient, CallParticipant> remoteParticipantMap = new LinkedHashMap<>();
|
||||
|
||||
private final ExecutorService serviceExecutor = Executors.newSingleThreadExecutor();
|
||||
private final ExecutorService networkExecutor = Executors.newSingleThreadExecutor();
|
||||
@ -323,7 +326,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
public void onCameraSwitchCompleted(@NonNull CameraState newCameraState) {
|
||||
localCameraState = newCameraState;
|
||||
if (activePeer != null) {
|
||||
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
}
|
||||
|
||||
@ -448,6 +451,13 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
|
||||
initializeVideo();
|
||||
|
||||
remoteParticipantMap.put(remotePeer.getRecipient(), CallParticipant.createRemote(
|
||||
remotePeer.getRecipient(),
|
||||
null,
|
||||
new BroadcastVideoSink(eglBase),
|
||||
false
|
||||
));
|
||||
|
||||
OfferMessage.Type offerType = OfferMessage.Type.fromCode(intent.getStringExtra(EXTRA_OFFER_TYPE));
|
||||
CallManager.CallMediaType callMediaType = getCallMediaTypeFromOfferType(offerType);
|
||||
|
||||
@ -505,7 +515,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
}
|
||||
|
||||
if (activePeer != null) {
|
||||
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
}
|
||||
|
||||
@ -519,7 +529,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
}
|
||||
|
||||
if (activePeer != null) {
|
||||
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
}
|
||||
|
||||
@ -541,7 +551,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
}
|
||||
|
||||
if (activePeer != null) {
|
||||
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
}
|
||||
|
||||
@ -552,7 +562,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
camera.flip();
|
||||
localCameraState = camera.getCameraState();
|
||||
if (activePeer != null) {
|
||||
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -561,7 +571,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
bluetoothAvailable = intent.getBooleanExtra(EXTRA_AVAILABLE, false);
|
||||
|
||||
if (activePeer != null) {
|
||||
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
}
|
||||
|
||||
@ -584,7 +594,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
audioManager.setSpeakerphoneOn(true);
|
||||
}
|
||||
|
||||
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
}
|
||||
|
||||
@ -613,7 +623,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
AudioManager androidAudioManager = ServiceUtil.getAudioManager(this);
|
||||
androidAudioManager.setSpeakerphoneOn(false);
|
||||
|
||||
sendMessage(WebRtcViewModel.State.CALL_OUTGOING, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(WebRtcViewModel.State.CALL_OUTGOING, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
lockManager.updatePhoneState(getInCallPhoneState());
|
||||
audioManager.initializeAudioForCall();
|
||||
audioManager.startOutgoingRinger(OutgoingRinger.Type.RINGING);
|
||||
@ -633,8 +643,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
callManager.proceed(activePeer.getCallId(),
|
||||
WebRtcCallService.this,
|
||||
eglBase,
|
||||
localRenderer,
|
||||
remoteRenderer,
|
||||
localSink,
|
||||
remoteParticipantMap.get(activePeer.getRecipient()).getVideoSink(),
|
||||
camera,
|
||||
iceServers,
|
||||
isAlwaysTurn,
|
||||
@ -645,7 +655,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
|
||||
localCameraState = camera.getCameraState();
|
||||
if (activePeer != null) {
|
||||
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -668,6 +678,13 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
|
||||
initializeVideo();
|
||||
|
||||
remoteParticipantMap.put(remotePeer.getRecipient(), CallParticipant.createRemote(
|
||||
remotePeer.getRecipient(),
|
||||
null,
|
||||
new BroadcastVideoSink(eglBase),
|
||||
false
|
||||
));
|
||||
|
||||
setCallInProgressNotification(TYPE_INCOMING_CONNECTING, activePeer);
|
||||
|
||||
retrieveTurnServers().addListener(new SuccessOnlyListener<List<PeerConnection.IceServer>>(this.activePeer.getState(), this.activePeer.getCallId()) {
|
||||
@ -680,8 +697,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
callManager.proceed(activePeer.getCallId(),
|
||||
WebRtcCallService.this,
|
||||
eglBase,
|
||||
localRenderer,
|
||||
remoteRenderer,
|
||||
localSink,
|
||||
remoteParticipantMap.get(activePeer.getRecipient()).getVideoSink(),
|
||||
camera,
|
||||
iceServers,
|
||||
hideIp,
|
||||
@ -692,7 +709,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
|
||||
lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING);
|
||||
if (activePeer != null) {
|
||||
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -883,7 +900,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
activePeer.localRinging();
|
||||
lockManager.updatePhoneState(LockManager.PhoneState.INTERACTIVE);
|
||||
|
||||
sendMessage(WebRtcViewModel.State.CALL_INCOMING, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(WebRtcViewModel.State.CALL_INCOMING, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
boolean shouldDisturbUserWithCall = DoNotDisturbUtil.shouldDisturbUserWithCall(getApplicationContext(), recipient);
|
||||
if (shouldDisturbUserWithCall) {
|
||||
startCallCardActivityIfPossible();
|
||||
@ -914,7 +931,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
Log.i(TAG, "handleRemoteRinging(): call_id: " + remotePeer.getCallId());
|
||||
|
||||
activePeer.remoteRinging();
|
||||
sendMessage(WebRtcViewModel.State.CALL_RINGING, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(WebRtcViewModel.State.CALL_RINGING, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
|
||||
private void handleCallConnected(Intent intent) {
|
||||
@ -940,7 +957,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
|
||||
callConnectedTime = System.currentTimeMillis();
|
||||
|
||||
sendMessage(WebRtcViewModel.State.CALL_CONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(WebRtcViewModel.State.CALL_CONNECTED, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
|
||||
unregisterPowerButtonReceiver();
|
||||
|
||||
@ -969,8 +986,11 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
|
||||
Log.i(TAG, "handleRemoteVideoEnable(): call_id: " + activePeer.getCallId());
|
||||
|
||||
remoteVideoEnabled = enable;
|
||||
sendMessage(WebRtcViewModel.State.CALL_CONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
CallParticipant oldParticipant = Objects.requireNonNull(remoteParticipantMap.get(activePeer.getRecipient()));
|
||||
CallParticipant newParticipant = oldParticipant.withVideoEnabled(enable);
|
||||
remoteParticipantMap.put(activePeer.getRecipient(), newParticipant);
|
||||
|
||||
sendMessage(WebRtcViewModel.State.CALL_CONNECTED, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
|
||||
}
|
||||
|
||||
@ -1022,13 +1042,13 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
audioManager.setSpeakerphoneOn(true);
|
||||
}
|
||||
|
||||
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
|
||||
private void handleLocalHangup(Intent intent) {
|
||||
if (activePeer == null) {
|
||||
if (busyPeer != null) {
|
||||
sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, busyPeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, busyPeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
busyPeer = null;
|
||||
}
|
||||
|
||||
@ -1043,7 +1063,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
|
||||
try {
|
||||
callManager.hangup();
|
||||
sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
terminate(activePeer);
|
||||
} catch (CallException e) {
|
||||
callFailure("hangup() failed: ", e);
|
||||
@ -1097,9 +1117,9 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
if (remotePeer.callIdEquals(activePeer)) {
|
||||
boolean outgoingBeforeAccept = remotePeer.getState() == CallState.DIALING || remotePeer.getState() == CallState.REMOTE_RINGING;
|
||||
if (outgoingBeforeAccept) {
|
||||
sendMessage(WebRtcViewModel.State.RECIPIENT_UNAVAILABLE, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(WebRtcViewModel.State.RECIPIENT_UNAVAILABLE, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
} else {
|
||||
sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1117,7 +1137,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
Log.i(TAG, "handleEndedRemoteHangupAccepted(): call_id: " + remotePeer.getCallId());
|
||||
|
||||
if (remotePeer.callIdEquals(activePeer)) {
|
||||
sendMessage(WebRtcViewModel.State.CALL_ACCEPTED_ELSEWHERE, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(WebRtcViewModel.State.CALL_ACCEPTED_ELSEWHERE, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
|
||||
terminate(remotePeer);
|
||||
@ -1129,7 +1149,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
Log.i(TAG, "handleEndedRemoteHangupBusy(): call_id: " + remotePeer.getCallId());
|
||||
|
||||
if (remotePeer.callIdEquals(activePeer)) {
|
||||
sendMessage(WebRtcViewModel.State.CALL_ONGOING_ELSEWHERE, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(WebRtcViewModel.State.CALL_ONGOING_ELSEWHERE, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
|
||||
terminate(remotePeer);
|
||||
@ -1141,7 +1161,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
Log.i(TAG, "handleEndedRemoteHangupDeclined(): call_id: " + remotePeer.getCallId());
|
||||
|
||||
if (remotePeer.callIdEquals(activePeer)) {
|
||||
sendMessage(WebRtcViewModel.State.CALL_DECLINED_ELSEWHERE, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(WebRtcViewModel.State.CALL_DECLINED_ELSEWHERE, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
|
||||
terminate(remotePeer);
|
||||
@ -1163,7 +1183,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
busyPeer = null;
|
||||
}, BUSY_TONE_LENGTH);
|
||||
|
||||
sendMessage(WebRtcViewModel.State.CALL_BUSY, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(WebRtcViewModel.State.CALL_BUSY, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
|
||||
terminate(remotePeer);
|
||||
@ -1175,7 +1195,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
Log.i(TAG, "handleEndedRemoteNeedPermission(): call_id: " + remotePeer.getCallId());
|
||||
|
||||
if (remotePeer.callIdEquals(activePeer)) {
|
||||
sendMessage(WebRtcViewModel.State.CALL_NEEDS_PERMISSION, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(WebRtcViewModel.State.CALL_NEEDS_PERMISSION, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
|
||||
terminate(remotePeer);
|
||||
@ -1187,7 +1207,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
Log.i(TAG, "handleEndedRemoteGlare(): call_id: " + remotePeer.getCallId());
|
||||
|
||||
if (remotePeer.callIdEquals(activePeer)) {
|
||||
sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
|
||||
boolean incomingBeforeAccept = remotePeer.getState() == CallState.ANSWERING || remotePeer.getState() == CallState.LOCAL_RINGING;
|
||||
@ -1204,7 +1224,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
Log.i(TAG, "handleEndedFailure(): call_id: " + remotePeer.getCallId());
|
||||
|
||||
if (remotePeer.callIdEquals(activePeer)) {
|
||||
sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
|
||||
if (remotePeer.getState() == CallState.ANSWERING || remotePeer.getState() == CallState.LOCAL_RINGING) {
|
||||
@ -1254,14 +1274,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
|
||||
private void initializeVideo() {
|
||||
Util.runOnMainSync(() -> {
|
||||
|
||||
eglBase = EglBase.create();
|
||||
localRenderer = new TextureViewRenderer(WebRtcCallService.this);
|
||||
remoteRenderer = new TextureViewRenderer(WebRtcCallService.this);
|
||||
|
||||
localRenderer.init(eglBase.getEglBaseContext(), null);
|
||||
remoteRenderer.init(eglBase.getEglBaseContext(), null);
|
||||
|
||||
eglBase = EglBase.create();
|
||||
localSink = new BroadcastVideoSink(eglBase);
|
||||
camera = new Camera(WebRtcCallService.this, WebRtcCallService.this, eglBase);
|
||||
localCameraState = camera.getCameraState();
|
||||
});
|
||||
@ -1299,31 +1313,28 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
camera = null;
|
||||
}
|
||||
|
||||
if (eglBase != null && localRenderer != null && remoteRenderer != null) {
|
||||
localRenderer.release();
|
||||
remoteRenderer.release();
|
||||
if (eglBase != null) {
|
||||
eglBase.release();
|
||||
|
||||
localRenderer = null;
|
||||
remoteRenderer = null;
|
||||
eglBase = null;
|
||||
eglBase = null;
|
||||
}
|
||||
|
||||
this.localCameraState = CameraState.UNKNOWN;
|
||||
this.microphoneEnabled = true;
|
||||
this.remoteVideoEnabled = false;
|
||||
this.enableVideoOnCreate = false;
|
||||
|
||||
Log.i(TAG, "clear activePeer callId: " + activePeer.getCallId() + " key: " + activePeer.hashCode());
|
||||
this.activePeer = null;
|
||||
|
||||
for (CallParticipant participant : remoteParticipantMap.values()) {
|
||||
remoteParticipantMap.put(participant.getRecipient(), participant.withVideoEnabled(false));
|
||||
}
|
||||
|
||||
lockManager.updatePhoneState(LockManager.PhoneState.IDLE);
|
||||
}
|
||||
|
||||
private void sendMessage(@NonNull WebRtcViewModel.State state,
|
||||
@NonNull RemotePeer remotePeer,
|
||||
@NonNull CameraState localCameraState,
|
||||
boolean remoteVideoEnabled,
|
||||
boolean bluetoothAvailable,
|
||||
boolean microphoneEnabled,
|
||||
boolean isRemoteVideoOffer)
|
||||
@ -1331,35 +1342,12 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
EventBus.getDefault().postSticky(new WebRtcViewModel(state,
|
||||
remotePeer.getRecipient(),
|
||||
localCameraState,
|
||||
localRenderer,
|
||||
remoteRenderer,
|
||||
remoteVideoEnabled,
|
||||
localSink,
|
||||
bluetoothAvailable,
|
||||
microphoneEnabled,
|
||||
isRemoteVideoOffer,
|
||||
callConnectedTime));
|
||||
}
|
||||
|
||||
private void sendMessage(@NonNull WebRtcViewModel.State state,
|
||||
@NonNull RemotePeer remotePeer,
|
||||
@NonNull IdentityKey identityKey,
|
||||
@NonNull CameraState localCameraState,
|
||||
boolean remoteVideoEnabled,
|
||||
boolean bluetoothAvailable,
|
||||
boolean microphoneEnabled,
|
||||
boolean isRemoteVideoOffer)
|
||||
{
|
||||
EventBus.getDefault().postSticky(new WebRtcViewModel(state,
|
||||
remotePeer.getRecipient(),
|
||||
identityKey,
|
||||
localCameraState,
|
||||
localRenderer,
|
||||
remoteRenderer,
|
||||
remoteVideoEnabled,
|
||||
bluetoothAvailable,
|
||||
microphoneEnabled,
|
||||
isRemoteVideoOffer,
|
||||
callConnectedTime));
|
||||
callConnectedTime,
|
||||
new ArrayList<>(remoteParticipantMap.values())));
|
||||
}
|
||||
|
||||
private ListenableFutureTask<Boolean> sendMessage(@NonNull final RemotePeer remotePeer,
|
||||
@ -1429,7 +1417,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
Log.w(TAG, "callFailure(): " + message, error);
|
||||
|
||||
if (activePeer != null) {
|
||||
sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
|
||||
if (callManager != null) {
|
||||
@ -1710,11 +1698,16 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
||||
}
|
||||
|
||||
if (error instanceof UntrustedIdentityException) {
|
||||
sendMessage(WebRtcViewModel.State.UNTRUSTED_IDENTITY, activePeer, ((UntrustedIdentityException)error).getIdentityKey(), localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
CallParticipant participant = Objects.requireNonNull(remoteParticipantMap.get(activePeer.getRecipient()));
|
||||
CallParticipant untrusted = participant.withIdentityKey(((UntrustedIdentityException) error).getIdentityKey());
|
||||
|
||||
remoteParticipantMap.put(activePeer.getRecipient(), untrusted);
|
||||
|
||||
sendMessage(WebRtcViewModel.State.UNTRUSTED_IDENTITY, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
} else if (error instanceof UnregisteredUserException) {
|
||||
sendMessage(WebRtcViewModel.State.NO_SUCH_USER, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(WebRtcViewModel.State.NO_SUCH_USER, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
} else if (error instanceof IOException) {
|
||||
sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -113,20 +113,20 @@ public class MappingAdapter extends ListAdapter<MappingModel<?>, MappingViewHold
|
||||
}
|
||||
|
||||
public interface Factory<T extends MappingModel<T>> {
|
||||
@NonNull MappingViewHolder<T> createViewHolder(ViewGroup parent);
|
||||
@NonNull MappingViewHolder<T> createViewHolder(@NonNull ViewGroup parent);
|
||||
}
|
||||
|
||||
public static class LayoutFactory<T extends MappingModel<T>> implements Factory<T> {
|
||||
private Function<View, MappingViewHolder<T>> creator;
|
||||
private final int layout;
|
||||
|
||||
public LayoutFactory(Function<View, MappingViewHolder<T>> creator, @LayoutRes int layout) {
|
||||
public LayoutFactory(@NonNull Function<View, MappingViewHolder<T>> creator, @LayoutRes int layout) {
|
||||
this.creator = creator;
|
||||
this.layout = layout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MappingViewHolder<T> createViewHolder(ViewGroup parent) {
|
||||
public @NonNull MappingViewHolder<T> createViewHolder(@NonNull ViewGroup parent) {
|
||||
return creator.apply(LayoutInflater.from(parent.getContext()).inflate(layout, parent, false));
|
||||
}
|
||||
}
|
||||
|
@ -23,5 +23,9 @@ public abstract class MappingViewHolder<Model extends MappingModel<Model>> exten
|
||||
return itemView.findViewById(id);
|
||||
}
|
||||
|
||||
public @NonNull Context getContext() {
|
||||
return itemView.getContext();
|
||||
}
|
||||
|
||||
public abstract void bind(@NonNull Model model);
|
||||
}
|
||||
|
@ -0,0 +1,31 @@
|
||||
package org.thoughtcrime.securesms.util.viewholders;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public abstract class RecipientMappingModel<T extends RecipientMappingModel<T>> implements MappingModel<T> {
|
||||
|
||||
public abstract @NonNull Recipient getRecipient();
|
||||
|
||||
public @NonNull String getName(@NonNull Context context) {
|
||||
return getRecipient().getDisplayName(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull T newItem) {
|
||||
return getRecipient().getId().equals(newItem.getRecipient().getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull T newItem) {
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
return getName(context).equals(newItem.getName(context)) && Objects.equals(getRecipient().getContactPhoto(), newItem.getRecipient().getContactPhoto());
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package org.thoughtcrime.securesms.util.viewholders;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||
|
||||
public class RecipientViewHolder<T extends RecipientMappingModel<T>> extends MappingViewHolder<T> {
|
||||
|
||||
protected final @Nullable AvatarImageView avatar;
|
||||
protected final @Nullable TextView name;
|
||||
protected final @Nullable EventListener<T> eventListener;
|
||||
|
||||
public RecipientViewHolder(@NonNull View itemView, @Nullable EventListener<T> eventListener) {
|
||||
super(itemView);
|
||||
this.eventListener = eventListener;
|
||||
|
||||
avatar = findViewById(R.id.recipient_view_avatar);
|
||||
name = findViewById(R.id.recipient_view_name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull T model) {
|
||||
if (avatar != null) {
|
||||
avatar.setRecipient(model.getRecipient());
|
||||
}
|
||||
|
||||
if (name != null) {
|
||||
name.setText(model.getName(context));
|
||||
}
|
||||
|
||||
if (eventListener != null) {
|
||||
itemView.setOnClickListener(v -> eventListener.onModelClick(model));
|
||||
} else {
|
||||
itemView.setOnClickListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull <T extends RecipientMappingModel<T>> MappingAdapter.Factory<T> createFactory(@LayoutRes int layout, @Nullable EventListener<T> listener) {
|
||||
return new MappingAdapter.LayoutFactory<>(view -> new RecipientViewHolder<>(view, listener), layout);
|
||||
}
|
||||
|
||||
public interface EventListener<T extends RecipientMappingModel<T>> {
|
||||
default void onModelClick(@NonNull T model) {
|
||||
onClick(model.getRecipient());
|
||||
}
|
||||
|
||||
void onClick(@NonNull Recipient recipient);
|
||||
}
|
||||
}
|
43
app/src/main/res/layout/call_participant_item.xml
Normal file
43
app/src/main/res/layout/call_participant_item.xml
Normal file
@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.components.webrtc.CallParticipantView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:layout_height="match_parent"
|
||||
tools:layout_width="match_parent">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/call_participant_item_avatar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="h,1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent="0.5"
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/call_participant_item_pip_avatar"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer
|
||||
android:id="@+id/call_participant_renderer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</org.thoughtcrime.securesms.components.webrtc.CallParticipantView>
|
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
12
app/src/main/res/layout/call_participants_list_header.xml
Normal file
12
app/src/main/res/layout/call_participants_list_header.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/call_participants_list_header"
|
||||
style="@style/TextAppearance.Signal.Body2.Bold"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
tools:text="In this call · 16 people" />
|
34
app/src/main/res/layout/call_participants_list_item.xml
Normal file
34
app/src/main/res/layout/call_participants_list_item.xml
Normal file
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/recipient_view_avatar"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
app:fallbackImageSize="small"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/recipient_view_name"
|
||||
style="@style/TextAppearance.Signal.Body1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Preview"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
</LinearLayout>
|
@ -12,14 +12,14 @@
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/mention_recipient_avatar"
|
||||
android:id="@+id/recipient_view_avatar"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
app:fallbackImageSize="small"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mention_recipient_name"
|
||||
android:id="@+id/recipient_view_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
|
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="@null"
|
||||
android:clipChildren="true"
|
||||
app:cardCornerRadius="8dp"
|
||||
tools:background="@color/red"
|
||||
tools:visibility="visible">
|
||||
|
||||
<include
|
||||
android:id="@+id/call_participant"
|
||||
layout="@layout/call_participant_item"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="72dp" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.components.webrtc.CallParticipantsLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/call_screen_call_participants"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:alignContent="stretch"
|
||||
app:flexDirection="row"
|
||||
app:flexWrap="wrap" />
|
@ -10,31 +10,47 @@
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/transparent_black_40" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/call_screen_recipient_avatar"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
android:layout_gravity="center" />
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/call_screen_recipient_avatar_call_card"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/call_screen_participants_pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:orientation="vertical" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/call_screen_participants_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="72dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:orientation="horizontal"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:reverseLayout="true"
|
||||
tools:listitem="@layout/webrtc_call_participant_recycler_item" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/call_screen_remote_renderer_holder"
|
||||
android:id="@+id/call_screen_large_local_renderer_frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
android:background="@color/black">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/call_screen_large_local_renderer_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
android:visibility="gone" />
|
||||
<org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer
|
||||
android:id="@+id/call_screen_large_local_renderer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/call_screen"
|
||||
@ -95,60 +111,28 @@
|
||||
android:background="@null"
|
||||
android:clipChildren="true"
|
||||
android:translationX="100000dp"
|
||||
android:translationY="-100000dp"
|
||||
android:translationY="100000dp"
|
||||
android:visibility="gone"
|
||||
app:cardCornerRadius="8dp"
|
||||
tools:background="@color/red"
|
||||
tools:visibility="visible">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/call_screen_small_local_renderer_holder"
|
||||
<org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer
|
||||
android:id="@+id/call_screen_small_local_renderer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
</org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/call_screen_down_arrow"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="11dp"
|
||||
android:layout_marginStart="13dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/call_screen_recipient_name"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/call_screen_recipient_name" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/call_screen_recipient_name"
|
||||
android:layout_width="wrap_content"
|
||||
<include
|
||||
android:id="@+id/call_screen_toolbar"
|
||||
layout="@layout/webrtc_call_view_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="39dp"
|
||||
android:shadowColor="@color/transparent_black_20"
|
||||
android:shadowDx="0"
|
||||
android:shadowDy="0"
|
||||
android:shadowRadius="4.0"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Title2"
|
||||
android:textColor="@color/core_white"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/call_screen_status_bar_guideline"
|
||||
tools:text="Kiera Thompson" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/call_screen_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:shadowColor="@color/transparent_black_40"
|
||||
android:shadowDx="0"
|
||||
android:shadowDy="0"
|
||||
android:shadowRadius="4.0"
|
||||
android:textColor="@color/core_white"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/call_screen_recipient_name"
|
||||
tools:text="Signal Calling..." />
|
||||
app:layout_constraintTop_toTopOf="@id/call_screen_status_bar_guideline" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutputToggleButton
|
||||
android:id="@+id/call_screen_speaker_toggle"
|
||||
@ -158,7 +142,7 @@
|
||||
android:layout_marginBottom="34dp"
|
||||
android:scaleType="fitXY"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/call_screen_start_call_controls"
|
||||
app:layout_constraintEnd_toStartOf="@id/call_screen_camera_direction_toggle"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@ -174,7 +158,7 @@
|
||||
android:clickable="false"
|
||||
android:scaleType="fitXY"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/call_screen_start_call_controls"
|
||||
app:layout_constraintEnd_toStartOf="@id/call_screen_video_toggle"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toEndOf="@id/call_screen_speaker_toggle"
|
||||
@ -189,7 +173,7 @@
|
||||
android:background="@drawable/webrtc_call_screen_video_toggle"
|
||||
android:stateListAnimator="@null"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/call_screen_start_call_controls"
|
||||
app:layout_constraintEnd_toStartOf="@id/call_screen_audio_mic_toggle"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toEndOf="@id/call_screen_camera_direction_toggle"
|
||||
@ -204,7 +188,7 @@
|
||||
android:background="@drawable/webrtc_call_screen_mic_toggle"
|
||||
android:stateListAnimator="@null"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/call_screen_start_call_controls"
|
||||
app:layout_constraintEnd_toStartOf="@id/call_screen_end_call"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toEndOf="@id/call_screen_video_toggle"
|
||||
@ -218,7 +202,7 @@
|
||||
android:clickable="false"
|
||||
android:scaleType="fitXY"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/call_screen_start_call_controls"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toEndOf="@id/call_screen_audio_mic_toggle"
|
||||
@ -304,5 +288,45 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/call_screen_start_call_controls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="32dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/call_screen_start_call_cancel"
|
||||
style="@style/Widget.Signal.Button.Flat"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@android:string/cancel"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/core_white"
|
||||
app:backgroundTint="@color/transparent_white_40" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/call_screen_start_call_start_call"
|
||||
style="@style/Widget.Signal.Button.Flat"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/WebRtcCallView__start_call"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/core_white"
|
||||
app:backgroundTint="@color/core_green" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</merge>
|
47
app/src/main/res/layout/webrtc_call_view_toolbar.xml
Normal file
47
app/src/main/res/layout/webrtc_call_view_toolbar.xml
Normal file
@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:minWidth="160dp">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/action_bar_guideline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_begin="?actionBarSize" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/call_screen_recipient_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold"
|
||||
android:textColor="@color/core_white"
|
||||
app:layout_constraintBottom_toTopOf="@id/action_bar_guideline"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Kiera Thompson" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/call_screen_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Caption"
|
||||
android:textColor="@color/core_white"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/call_screen_recipient_name"
|
||||
tools:text="Signal Calling..." />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
14
app/src/main/res/menu/group_call.xml
Normal file
14
app/src/main/res/menu/group_call.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_group_call_participants_list"
|
||||
android:icon="@drawable/ic_group_solid_24"
|
||||
android:title="@string/WebRtcCallView__view_participants_list"
|
||||
app:iconTint="@color/white"
|
||||
app:showAsAction="always"
|
||||
tools:ignore="AlwaysShowAction" />
|
||||
|
||||
</menu>
|
@ -1199,10 +1199,20 @@
|
||||
<string name="WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera">To call %1$s, Signal needs access to your camera</string>
|
||||
<string name="WebRtcCallActivity__signal_s">Signal %1$s</string>
|
||||
<string name="WebRtcCallActivity__calling">Calling…</string>
|
||||
<string name="WebRtcCallActivity__group_call">Group Call</string>
|
||||
|
||||
<!-- WebRtcCallView -->
|
||||
<string name="WebRtcCallView__signal_voice_call">Signal voice call…</string>
|
||||
<string name="WebRtcCallView__signal_video_call">Signal video call…</string>
|
||||
<string name="WebRtcCallView__start_call">Start Call</string>
|
||||
<string name="WebRtcCallView__group_call">Group Call</string>
|
||||
<string name="WebRtcCallView__view_participants_list">View participants</string>
|
||||
|
||||
<!-- CallParticipantsListDialog -->
|
||||
<plurals name="CallParticipantsListDialog_in_this_call_d_people">
|
||||
<item quantity="one">In this call · %1$d person</item>
|
||||
<item quantity="other">In this call · %1$d people</item>
|
||||
</plurals>
|
||||
|
||||
<!-- RegistrationActivity -->
|
||||
<string name="RegistrationActivity_select_your_country">Select your country</string>
|
||||
|
@ -513,4 +513,10 @@
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">50%</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Signal.Button.Flat" parent="Widget.MaterialComponents.Button.UnelevatedButton">
|
||||
<item name="android:textAllCaps">false</item>
|
||||
<item name="android:insetTop">0dp</item>
|
||||
<item name="android:insetBottom">0dp</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
Loading…
x
Reference in New Issue
Block a user