Add foundational UX and state support for Group Calling.

This commit is contained in:
Cody Henthorne 2020-09-10 16:59:47 -04:00 committed by Greyson Parrelli
parent 7baf8052a2
commit dc4faf57cb
44 changed files with 1929 additions and 591 deletions

View File

@ -41,6 +41,8 @@ import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.components.TooltipPopup; 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.WebRtcAudioOutput;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView; import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel; 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.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService; import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter; import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage; import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumberChangeDialog.Callback { public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumberChangeDialog.Callback {
@ -162,7 +166,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
} }
private boolean enterPipModeIfPossible() { private boolean enterPipModeIfPossible() {
if (isSystemPipEnabledAndAvailable()) { if (viewModel.canEnterPipMode() && isSystemPipEnabledAndAvailable()) {
PictureInPictureParams params = new PictureInPictureParams.Builder() PictureInPictureParams params = new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(9, 16)) .setAspectRatio(new Rational(9, 16))
.build(); .build();
@ -203,14 +207,11 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
private void initializeViewModel() { private void initializeViewModel() {
viewModel = ViewModelProviders.of(this).get(WebRtcCallViewModel.class); viewModel = ViewModelProviders.of(this).get(WebRtcCallViewModel.class);
viewModel.setIsInPipMode(isInPipMode()); viewModel.setIsInPipMode(isInPipMode());
viewModel.getRemoteVideoEnabled().observe(this,callScreen::setRemoteVideoEnabled);
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled); 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.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
viewModel.getEvents().observe(this, this::handleViewModelEvent); viewModel.getEvents().observe(this, this::handleViewModelEvent);
viewModel.getCallTime().observe(this, this::handleCallTime); 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) { private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
@ -375,19 +376,13 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
startService(intent); startService(intent);
} }
private void handleIncomingCall(@NonNull WebRtcViewModel event) {
callScreen.setRecipient(event.getRecipient());
}
private void handleOutgoingCall(@NonNull WebRtcViewModel event) { private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling)); callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling));
} }
private void handleTerminate(@NonNull Recipient recipient, @NonNull HangupMessage.Type hangupType) { private void handleTerminate(@NonNull Recipient recipient, @NonNull HangupMessage.Type hangupType) {
Log.i(TAG, "handleTerminate called: " + hangupType.name()); Log.i(TAG, "handleTerminate called: " + hangupType.name());
callScreen.setRecipient(recipient);
callScreen.setStatusFromHangupType(hangupType); callScreen.setStatusFromHangupType(hangupType);
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
@ -399,32 +394,27 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
} }
private void handleCallRinging(@NonNull WebRtcViewModel event) { private void handleCallRinging(@NonNull WebRtcViewModel event) {
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_ringing)); callScreen.setStatus(getString(R.string.RedPhone_ringing));
} }
private void handleCallBusy(@NonNull WebRtcViewModel event) { private void handleCallBusy(@NonNull WebRtcViewModel event) {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_busy)); callScreen.setStatus(getString(R.string.RedPhone_busy));
delayedFinish(WebRtcCallService.BUSY_TONE_LENGTH); delayedFinish(WebRtcCallService.BUSY_TONE_LENGTH);
} }
private void handleCallConnected(@NonNull WebRtcViewModel event) { private void handleCallConnected(@NonNull WebRtcViewModel event) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES); getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
callScreen.setRecipient(event.getRecipient());
} }
private void handleRecipientUnavailable(@NonNull WebRtcViewModel event) { private void handleRecipientUnavailable(@NonNull WebRtcViewModel event) {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable)); callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable));
delayedFinish(); delayedFinish();
} }
private void handleServerFailure(@NonNull WebRtcViewModel event) { private void handleServerFailure(@NonNull WebRtcViewModel event) {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_network_failed)); callScreen.setStatus(getString(R.string.RedPhone_network_failed));
delayedFinish(); delayedFinish();
} }
@ -452,8 +442,8 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
} }
private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) { private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
final IdentityKey theirKey = event.getIdentityKey(); final IdentityKey theirKey = event.getRemoteParticipants().get(0).getIdentityKey();
final Recipient recipient = event.getRecipient(); final Recipient recipient = event.getRemoteParticipants().get(0).getRecipient();
if (theirKey == null) { if (theirKey == null) {
handleTerminate(recipient, HangupMessage.Type.NORMAL); handleTerminate(recipient, HangupMessage.Type.NORMAL);
@ -493,10 +483,11 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
} }
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN) @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); Log.i(TAG, "Got message from service: " + event);
viewModel.setRecipient(event.getRecipient()); viewModel.setRecipient(event.getRecipient());
callScreen.setRecipient(event.getRecipient());
switch (event.getState()) { switch (event.getState()) {
case CALL_CONNECTED: handleCallConnected(event); break; 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 CALL_NEEDS_PERMISSION: handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break;
case NO_SUCH_USER: handleNoSuchUser(event); break; case NO_SUCH_USER: handleNoSuchUser(event); break;
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(event); break; case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(event); break;
case CALL_INCOMING: handleIncomingCall(event); break;
case CALL_OUTGOING: handleOutgoingCall(event); break; case CALL_OUTGOING: handleOutgoingCall(event); break;
case CALL_BUSY: handleCallBusy(event); break; case CALL_BUSY: handleCallBusy(event); break;
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break; case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
} }
callScreen.setLocalRenderer(event.getLocalRenderer()); boolean enableVideo = event.getLocalParticipant().getCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
callScreen.setRemoteRenderer(event.getRemoteRenderer());
boolean enableVideo = event.getLocalCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
viewModel.updateFromWebRtcViewModel(event, enableVideo); viewModel.updateFromWebRtcViewModel(event, enableVideo);
@ -530,6 +517,22 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
private final class ControlsListener implements WebRtcCallView.ControlsListener { 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 @Override
public void onControlsFadeOut() { public void onControlsFadeOut() {
if (videoTooltip != null) { if (videoTooltip != null) {
@ -594,8 +597,13 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
} }
@Override @Override
public void onDownCaretPressed() { public void onShowParticipantsList() {
CallParticipantsListDialog.show(getSupportFragmentManager());
}
@Override
public void onPageChanged(@NonNull CallParticipantsState.SelectedPage page) {
viewModel.setIsViewingFocusedParticipant(page);
} }
} }

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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
}
}

View File

@ -20,6 +20,9 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout; import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout;
import java.util.Arrays; import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestureListener { public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestureListener {
@ -28,9 +31,9 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
private final ViewGroup parent; private final ViewGroup parent;
private final View child; private final View child;
private final int framePadding; 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 int activePointerId = MotionEvent.INVALID_POINTER_ID;
private float lastTouchX; private float lastTouchX;
private float lastTouchY; private float lastTouchY;
@ -42,6 +45,8 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
private double projectionY; private double projectionY;
private VelocityTracker velocityTracker; private VelocityTracker velocityTracker;
private int maximumFlingVelocity; private int maximumFlingVelocity;
private boolean isLockedToBottomEnd;
private Queue<Runnable> runAfterFling;
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
public static PictureInPictureGestureHelper applyTo(@NonNull View child) { 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.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.pipHeight = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_pip_height);
this.maximumFlingVelocity = ViewConfiguration.get(child.getContext()).getScaledMaximumFlingVelocity(); this.maximumFlingVelocity = ViewConfiguration.get(child.getContext()).getScaledMaximumFlingVelocity();
this.runAfterFling = new LinkedList<>();
} }
public void clearVerticalBoundaries() { public void clearVerticalBoundaries() {
@ -105,11 +111,7 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
extraPaddingTop = topBoundary - parent.getTop(); extraPaddingTop = topBoundary - parent.getTop();
extraPaddingBottom = parent.getMeasuredHeight() + parent.getTop() - bottomBoundary; extraPaddingBottom = parent.getMeasuredHeight() + parent.getTop() - bottomBoundary;
if (isAnimating) { adjustPip();
fling();
} else if (!isDragging) {
onFling(null, null, 0, 0);
}
} }
private boolean onGestureFinished(MotionEvent e) { private boolean onGestureFinished(MotionEvent e) {
@ -123,12 +125,41 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
return false; 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 @Override
public boolean onDown(MotionEvent e) { public boolean onDown(MotionEvent e) {
activePointerId = e.getPointerId(0); activePointerId = e.getPointerId(0);
lastTouchX = e.getX(activePointerId) + child.getX(); lastTouchX = e.getX(activePointerId) + child.getX();
lastTouchY = e.getY(activePointerId) + child.getY(); lastTouchY = e.getY(activePointerId) + child.getY();
isDragging = true; isDragging = true;
pipWidth = child.getMeasuredWidth();
pipHeight = child.getMeasuredHeight();
return true; return true;
} }
@ -167,6 +198,13 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
return true; return true;
} }
@Override
public boolean onSingleTapUp(MotionEvent e) {
child.performClick();
return true;
}
private void fling() { private void fling() {
Point projection = new Point((int) projectionX, (int) projectionY); Point projection = new Point((int) projectionX, (int) projectionY);
Point nearestCornerPosition = findNearestCornerPosition(projection); Point nearestCornerPosition = findNearestCornerPosition(projection);
@ -183,12 +221,25 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
@Override @Override
public void onAnimationEnd(Animator animation) { public void onAnimationEnd(Animator animation) {
isAnimating = false; isAnimating = false;
Iterator<Runnable> afterFlingRunnables = runAfterFling.iterator();
while (afterFlingRunnables.hasNext()) {
Runnable runnable = afterFlingRunnables.next();
runnable.run();
afterFlingRunnables.remove();
}
} }
}) })
.start(); .start();
} }
private Point findNearestCornerPosition(Point projection) { private Point findNearestCornerPosition(Point projection) {
if (isLockedToBottomEnd) {
return parent.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR ? calculateBottomRightCoordinates(parent)
: calculateBottomLeftCoordinates(parent);
}
Point maxPoint = null; Point maxPoint = null;
double maxDistance = Double.MAX_VALUE; double maxDistance = Double.MAX_VALUE;

View File

@ -36,6 +36,8 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
private boolean enableFixedSize; private boolean enableFixedSize;
private int surfaceWidth; private int surfaceWidth;
private int surfaceHeight; private int surfaceHeight;
private boolean isInitialized;
private BroadcastVideoSink attachedVideoSink;
public TextureViewRenderer(@NonNull Context context) { public TextureViewRenderer(@NonNull Context context) {
super(context); super(context);
@ -49,8 +51,12 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
this.setSurfaceTextureListener(this); this.setSurfaceTextureListener(this);
} }
public void init(@NonNull EglBase.Context sharedContext, @NonNull RendererCommon.RendererEvents rendererEvents) { public void init(@NonNull EglBase eglBase) {
this.init(sharedContext, rendererEvents, EglBase.CONFIG_PLAIN, new GlRectDrawer()); 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) { 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); 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() { public void release() {
eglRenderer.release(); eglRenderer.release();
} }
@ -125,6 +149,9 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
protected void onMeasure(int widthSpec, int heightSpec) { protected void onMeasure(int widthSpec, int heightSpec) {
ThreadUtils.checkIsOnMainThread(); 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); Point size = videoLayoutMeasure.measure(widthSpec, heightSpec, this.rotatedFrameWidth, this.rotatedFrameHeight);
setMeasuredDimension(size.x, size.y); setMeasuredDimension(size.x, size.y);
@ -205,7 +232,9 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
@Override @Override
public void onFrame(VideoFrame videoFrame) { public void onFrame(VideoFrame videoFrame) {
eglRenderer.onFrame(videoFrame); if (isShown()) {
eglRenderer.onFrame(videoFrame);
}
} }
@Override @Override

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -5,7 +5,7 @@ import android.util.AttributeSet;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewParent; import android.view.animation.Animation;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
@ -13,60 +13,57 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.Toolbar;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet; import androidx.constraintlayout.widget.ConstraintSet;
import androidx.constraintlayout.widget.Guideline; import androidx.constraintlayout.widget.Guideline;
import androidx.core.util.Consumer; import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView;
import androidx.transition.AutoTransition; import androidx.transition.AutoTransition;
import androidx.transition.Transition; import androidx.transition.Transition;
import androidx.transition.TransitionManager; import androidx.transition.TransitionManager;
import androidx.viewpager2.widget.MarginPageTransformer;
import com.bumptech.glide.load.engine.DiskCacheStrategy; import androidx.viewpager2.widget.ViewPager2;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.ResizeAnimation;
import org.thoughtcrime.securesms.components.AccessibleToggleButton; import org.thoughtcrime.securesms.components.AccessibleToggleButton;
import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; import org.thoughtcrime.securesms.mediasend.SimpleAnimationListener;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.ringrtc.CameraState; import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.SetUtil; import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.webrtc.RendererCommon; import org.webrtc.RendererCommon;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage; import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
public class WebRtcCallView extends FrameLayout { public class WebRtcCallView extends FrameLayout {
private static final long TRANSITION_DURATION_MILLIS = 250; private static final long TRANSITION_DURATION_MILLIS = 250;
private static final int SMALL_ONGOING_CALL_BUTTON_MARGIN_DP = 8; 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 int LARGE_ONGOING_CALL_BUTTON_MARGIN_DP = 16;
private static final FallbackPhotoProvider FALLBACK_PHOTO_PROVIDER = new FallbackPhotoProvider();
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 WebRtcAudioOutputToggleButton audioToggle;
private AccessibleToggleButton videoToggle; private AccessibleToggleButton videoToggle;
private AccessibleToggleButton micToggle; private AccessibleToggleButton micToggle;
private ViewGroup largeLocalRenderContainer;
private ViewGroup localRenderPipFrame; private ViewGroup localRenderPipFrame;
private ViewGroup smallLocalRenderContainer; private TextureViewRenderer smallLocalRender;
private ViewGroup remoteRenderContainer; private View largeLocalRenderFrame;
private TextureViewRenderer largeLocalRender;
private TextView recipientName; private TextView recipientName;
private TextView status; private TextView status;
private ConstraintLayout parent; private ConstraintLayout parent;
private AvatarImageView avatar;
private ImageView avatarCard;
private ControlsListener controlsListener; private ControlsListener controlsListener;
private RecipientId recipientId; private RecipientId recipientId;
private CameraState.Direction cameraDirection;
private ImageView answer; private ImageView answer;
private ImageView cameraDirectionToggle; private ImageView cameraDirectionToggle;
private PictureInPictureGestureHelper pictureInPictureGestureHelper; private PictureInPictureGestureHelper pictureInPictureGestureHelper;
@ -74,6 +71,13 @@ public class WebRtcCallView extends FrameLayout {
private View answerWithAudio; private View answerWithAudio;
private View answerWithAudioLabel; private View answerWithAudioLabel;
private View ongoingFooterGradient; 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> incomingCallViews = new HashSet<>();
private final Set<View> topViews = new HashSet<>(); private final Set<View> topViews = new HashSet<>();
@ -82,7 +86,8 @@ public class WebRtcCallView extends FrameLayout {
private WebRtcControls controls = WebRtcControls.NONE; private WebRtcControls controls = WebRtcControls.NONE;
private final Runnable fadeOutRunnable = () -> { private final Runnable fadeOutRunnable = () -> {
if (isAttachedToWindow() && controls.isFadeOutEnabled()) fadeOutControls(); }; if (isAttachedToWindow() && controls.isFadeOutEnabled()) fadeOutControls();
};
public WebRtcCallView(@NonNull Context context) { public WebRtcCallView(@NonNull Context context) {
this(context, null); this(context, null);
@ -99,36 +104,53 @@ public class WebRtcCallView extends FrameLayout {
protected void onFinishInflate() { protected void onFinishInflate() {
super.onFinishInflate(); super.onFinishInflate();
audioToggle = findViewById(R.id.call_screen_speaker_toggle); audioToggle = findViewById(R.id.call_screen_speaker_toggle);
videoToggle = findViewById(R.id.call_screen_video_toggle); videoToggle = findViewById(R.id.call_screen_video_toggle);
micToggle = findViewById(R.id.call_screen_audio_mic_toggle); micToggle = findViewById(R.id.call_screen_audio_mic_toggle);
localRenderPipFrame = findViewById(R.id.call_screen_pip); localRenderPipFrame = findViewById(R.id.call_screen_pip);
largeLocalRenderContainer = findViewById(R.id.call_screen_large_local_renderer_holder); smallLocalRender = findViewById(R.id.call_screen_small_local_renderer);
smallLocalRenderContainer = findViewById(R.id.call_screen_small_local_renderer_holder); largeLocalRenderFrame = findViewById(R.id.call_screen_large_local_renderer_frame);
remoteRenderContainer = findViewById(R.id.call_screen_remote_renderer_holder); largeLocalRender = findViewById(R.id.call_screen_large_local_renderer);
recipientName = findViewById(R.id.call_screen_recipient_name); recipientName = findViewById(R.id.call_screen_recipient_name);
status = findViewById(R.id.call_screen_status); status = findViewById(R.id.call_screen_status);
parent = findViewById(R.id.call_screen); parent = findViewById(R.id.call_screen);
avatar = findViewById(R.id.call_screen_recipient_avatar); answer = findViewById(R.id.call_screen_answer_call);
avatarCard = findViewById(R.id.call_screen_recipient_avatar_call_card); cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle);
answer = findViewById(R.id.call_screen_answer_call); hangup = findViewById(R.id.call_screen_end_call);
cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle); answerWithAudio = findViewById(R.id.call_screen_answer_with_audio);
hangup = findViewById(R.id.call_screen_end_call); answerWithAudioLabel = findViewById(R.id.call_screen_answer_with_audio_label);
answerWithAudio = findViewById(R.id.call_screen_answer_with_audio); ongoingFooterGradient = findViewById(R.id.call_screen_ongoing_footer_gradient);
answerWithAudioLabel = findViewById(R.id.call_screen_answer_with_audio_label); startCallControls = findViewById(R.id.call_screen_start_call_controls);
ongoingFooterGradient = findViewById(R.id.call_screen_ongoing_footer_gradient); 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 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 decline = findViewById(R.id.call_screen_decline_call);
View answerLabel = findViewById(R.id.call_screen_answer_call_label); View answerLabel = findViewById(R.id.call_screen_answer_call_label);
View declineLabel = findViewById(R.id.call_screen_decline_call_label); View declineLabel = findViewById(R.id.call_screen_decline_call_label);
View incomingFooterGradient = findViewById(R.id.call_screen_incoming_footer_gradient); View incomingFooterGradient = findViewById(R.id.call_screen_incoming_footer_gradient);
Guideline statusBarGuideline = findViewById(R.id.call_screen_status_bar_guideline); 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(topGradient);
topViews.add(recipientName);
incomingCallViews.add(answer); incomingCallViews.add(answer);
incomingCallViews.add(answerLabel); incomingCallViews.add(answerLabel);
@ -158,16 +180,14 @@ public class WebRtcCallView extends FrameLayout {
hangup.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onEndCallPressed)); hangup.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onEndCallPressed));
decline.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onDenyCallPressed)); decline.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onDenyCallPressed));
downCaret.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onDownCaretPressed));
answer.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallPressed)); answer.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallPressed));
answerWithAudio.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallWithVoiceOnlyPressed)); answerWithAudio.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallWithVoiceOnlyPressed));
setOnClickListener(v -> toggleControls());
avatar.setOnClickListener(v -> toggleControls());
pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(localRenderPipFrame); pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(localRenderPipFrame);
startCall.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onStartCall));
cancelStartCall.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onCancelStartCall));
int statusBarHeight = ViewUtil.getStatusBarHeight(this); int statusBarHeight = ViewUtil.getStatusBarHeight(this);
statusBarGuideline.setGuidelineBegin(statusBarHeight); statusBarGuideline.setGuidelineBegin(statusBarHeight);
} }
@ -195,67 +215,57 @@ public class WebRtcCallView extends FrameLayout {
micToggle.setChecked(isMicEnabled, false); micToggle.setChecked(isMicEnabled, false);
} }
public void setRemoteVideoEnabled(boolean isRemoteVideoEnabled) { public void updateCallParticipants(@NonNull CallParticipantsState state) {
if (isRemoteVideoEnabled) { List<WebRtcCallParticipantsPage> pages = new ArrayList<>(2);
remoteRenderContainer.setVisibility(View.VISIBLE);
} else {
remoteRenderContainer.setVisibility(View.GONE);
}
}
public void setLocalRenderer(@Nullable TextureViewRenderer surfaceViewRenderer) { if (!state.getGridParticipants().isEmpty()) {
if (localRenderer == surfaceViewRenderer) { pages.add(WebRtcCallParticipantsPage.forMultipleParticipants(state.getGridParticipants(), state.isInPipMode()));
return;
} }
localRenderer = surfaceViewRenderer; if (state.getFocusedParticipant() != null && state.getAllRemoteParticipants().size() > 1) {
pages.add(WebRtcCallParticipantsPage.forSingleParticipant(state.getFocusedParticipant(), state.isInPipMode()));
if (surfaceViewRenderer == null) {
setRenderer(largeLocalRenderContainer, null);
setRenderer(smallLocalRenderContainer, null);
} else {
localRenderer.setMirror(cameraDirection == CameraState.Direction.FRONT);
localRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
} }
pagerAdapter.submitList(pages);
recyclerAdapter.submitList(state.getListParticipants());
updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant());
} }
public void setRemoteRenderer(@Nullable TextureViewRenderer remoteRenderer) { public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant) {
setRenderer(remoteRenderContainer, remoteRenderer); 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) { if (localCallParticipant.getVideoSink().getEglBase() != null) {
case GONE: smallLocalRender.init(localCallParticipant.getVideoSink().getEglBase());
localRenderPipFrame.setVisibility(View.GONE); largeLocalRender.init(localCallParticipant.getVideoSink().getEglBase());
largeLocalRenderContainer.setVisibility(View.GONE); }
setRenderer(largeLocalRenderContainer, null);
setRenderer(smallLocalRenderContainer, null); smallLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
break; largeLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
switch (state) {
case LARGE: case LARGE:
largeLocalRenderFrame.setVisibility(View.VISIBLE);
localRenderPipFrame.setVisibility(View.GONE); localRenderPipFrame.setVisibility(View.GONE);
largeLocalRenderContainer.setVisibility(View.VISIBLE);
if (largeLocalRenderContainer.getChildCount() == 0) {
setRenderer(largeLocalRenderContainer, localRenderer);
}
break; 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); localRenderPipFrame.setVisibility(View.VISIBLE);
largeLocalRenderContainer.setVisibility(View.GONE); animatePipToRectangle();
break;
if (smallLocalRenderContainer.getChildCount() == 0) { case SMALL_SQUARE:
setRenderer(smallLocalRenderContainer, localRenderer); largeLocalRenderFrame.setVisibility(View.GONE);
} localRenderPipFrame.setVisibility(View.VISIBLE);
} animatePipToSquare();
}
public void setCameraDirection(@NonNull CameraState.Direction cameraDirection) {
this.cameraDirection = cameraDirection;
if (localRenderer != null) {
localRenderer.setMirror(cameraDirection == CameraState.Direction.FRONT);
} }
} }
@ -265,17 +275,16 @@ public class WebRtcCallView extends FrameLayout {
} }
recipientId = recipient.getId(); 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); if (recipient.isGroup()) {
} recipientName.setText(R.string.WebRtcCallView__group_call);
if (toolbar.getMenu().findItem(R.id.menu_group_call_participants_list) == null) {
public void showCallCard(boolean showCallCard) { toolbar.inflateMenu(R.menu.group_call);
avatarCard.setVisibility(showCallCard ? VISIBLE : GONE); toolbar.setOnMenuItemClickListener(unused -> showParticipantsList());
avatar.setVisibility(showCallCard ? GONE : VISIBLE); }
} else {
recipientName.setText(recipient.getDisplayName(getContext()));
}
} }
public void setStatus(@NonNull String status) { 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); Set<View> lastVisibleSet = new HashSet<>(visibleViewSet);
visibleViewSet.clear(); visibleViewSet.clear();
if (webRtcControls.displayStartCallControls()) {
visibleViewSet.add(startCallControls);
}
if (webRtcControls.displayTopViews()) { if (webRtcControls.displayTopViews()) {
visibleViewSet.addAll(topViews); visibleViewSet.addAll(topViews);
} }
@ -378,8 +391,39 @@ public class WebRtcCallView extends FrameLayout {
return videoToggle; 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() { private void toggleControls() {
if (controls.isFadeOutEnabled() && status.getVisibility() == VISIBLE) { if (controls.isFadeOutEnabled() && toolbar.getVisibility() == VISIBLE) {
fadeOutControls(); fadeOutControls();
} else { } else {
fadeInControls(); 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() { private void updateButtonStateForLargeButtons() {
cameraDirectionToggle.setImageResource(R.drawable.webrtc_call_screen_camera_toggle); cameraDirectionToggle.setImageResource(R.drawable.webrtc_call_screen_camera_toggle);
hangup.setImageResource(R.drawable.webrtc_call_screen_hangup); 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); audioToggle.setImageResource(R.drawable.webrtc_call_screen_speaker_toggle_small);
} }
private static final class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider { private boolean showParticipantsList() {
@Override controlsListener.onShowParticipantsList();
public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() { return true;
return new ResourceContactPhoto(R.drawable.ic_profile_outline_120);
}
} }
public interface ControlsListener { public interface ControlsListener {
void onStartCall();
void onCancelStartCall();
void onControlsFadeOut(); void onControlsFadeOut();
void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput); void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput);
void onVideoChanged(boolean isVideoEnabled); void onVideoChanged(boolean isVideoEnabled);
@ -525,6 +535,7 @@ public class WebRtcCallView extends FrameLayout {
void onDenyCallPressed(); void onDenyCallPressed();
void onAcceptCallWithVoiceOnlyPressed(); void onAcceptCallWithVoiceOnlyPressed();
void onAcceptCallPressed(); void onAcceptCallPressed();
void onDownCaretPressed(); void onShowParticipantsList();
void onPageChanged(@NonNull CallParticipantsState.SelectedPage page);
} }
} }

View File

@ -10,60 +10,38 @@ import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations; import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModel;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.util.SingleLiveEvent; import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
public class WebRtcCallViewModel extends ViewModel { public class WebRtcCallViewModel extends ViewModel {
private final MutableLiveData<Boolean> remoteVideoEnabled = new MutableLiveData<>(false); private final MutableLiveData<Boolean> microphoneEnabled = new MutableLiveData<>(true);
private final MutableLiveData<Boolean> microphoneEnabled = new MutableLiveData<>(true); private final MutableLiveData<Boolean> isInPipMode = new MutableLiveData<>(false);
private final MutableLiveData<WebRtcLocalRenderState> localRenderState = new MutableLiveData<>(WebRtcLocalRenderState.GONE); private final MutableLiveData<WebRtcControls> webRtcControls = new MutableLiveData<>(WebRtcControls.NONE);
private final MutableLiveData<Boolean> isInPipMode = new MutableLiveData<>(false); private final LiveData<WebRtcControls> realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, webRtcControls, this::getRealWebRtcControls);
private final MutableLiveData<Boolean> localVideoEnabled = new MutableLiveData<>(false); private final SingleLiveEvent<Event> events = new SingleLiveEvent<Event>();
private final MutableLiveData<CameraState.Direction> cameraDirection = new MutableLiveData<>(CameraState.Direction.FRONT); private final MutableLiveData<Long> elapsed = new MutableLiveData<>(-1L);
private final LiveData<Boolean> shouldDisplayLocal = LiveDataUtil.combineLatest(isInPipMode, localVideoEnabled, (a, b) -> !a && b); private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
private final LiveData<WebRtcLocalRenderState> realLocalRenderState = LiveDataUtil.combineLatest(shouldDisplayLocal, localRenderState, this::getRealLocalRenderState); private final MutableLiveData<CallParticipantsState> participantsState = new MutableLiveData<>(CallParticipantsState.STARTING_STATE);
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 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(); private final WebRtcCallRepository repository = new WebRtcCallRepository();
public LiveData<Boolean> getRemoteVideoEnabled() {
return Transformations.distinctUntilChanged(remoteVideoEnabled);
}
public LiveData<Boolean> getMicrophoneEnabled() { public LiveData<Boolean> getMicrophoneEnabled() {
return Transformations.distinctUntilChanged(microphoneEnabled); 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() { public LiveData<WebRtcControls> getWebRtcControls() {
return realWebRtcControls; return realWebRtcControls;
} }
@ -81,7 +59,15 @@ public class WebRtcCallViewModel extends ViewModel {
} }
public LiveData<Long> getCallTime() { 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() { public boolean isAnswerWithVideoAvailable() {
@ -91,6 +77,15 @@ public class WebRtcCallViewModel extends ViewModel {
@MainThread @MainThread
public void setIsInPipMode(boolean isInPipMode) { public void setIsInPipMode(boolean isInPipMode) {
this.isInPipMode.setValue(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() { public void onDismissedVideoTooltip() {
@ -99,27 +94,20 @@ public class WebRtcCallViewModel extends ViewModel {
@MainThread @MainThread
public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel, boolean enableVideo) { public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel, boolean enableVideo) {
remoteVideoEnabled.setValue(webRtcViewModel.isRemoteVideoEnabled()); canEnterPipMode = true;
microphoneEnabled.setValue(webRtcViewModel.isMicrophoneEnabled());
if (isValidCameraDirectionForUi(webRtcViewModel.getLocalCameraState().getActiveDirection())) { CallParticipant localParticipant = webRtcViewModel.getLocalParticipant();
cameraDirection.setValue(webRtcViewModel.getLocalCameraState().getActiveDirection());
}
localVideoEnabled.setValue(webRtcViewModel.getLocalCameraState().isEnabled()); microphoneEnabled.setValue(localParticipant.isMicrophoneEnabled());
if (enableVideo) { //noinspection ConstantConditions
showVideoForOutgoing = webRtcViewModel.getState() == WebRtcViewModel.State.CALL_OUTGOING; participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), webRtcViewModel, enableVideo));
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_OUTGOING) {
showVideoForOutgoing = false;
}
updateLocalRenderState(webRtcViewModel.getState());
updateWebRtcControls(webRtcViewModel.getState(), updateWebRtcControls(webRtcViewModel.getState(),
webRtcViewModel.getLocalCameraState().isEnabled(), localParticipant.getCameraState().isEnabled(),
webRtcViewModel.isRemoteVideoEnabled(), webRtcViewModel.isRemoteVideoEnabled(),
webRtcViewModel.isRemoteVideoOffer(), webRtcViewModel.isRemoteVideoOffer(),
webRtcViewModel.getLocalCameraState().getCameraCount() > 1, localParticipant.isMoreThanOneCameraAvailable(),
webRtcViewModel.isBluetoothAvailable(), webRtcViewModel.isBluetoothAvailable(),
repository.getAudioOutput()); repository.getAudioOutput());
@ -131,9 +119,9 @@ public class WebRtcCallViewModel extends ViewModel {
callConnectedTime = -1; callConnectedTime = -1;
} }
if (webRtcViewModel.getLocalCameraState().isEnabled()) { if (localParticipant.getCameraState().isEnabled()) {
canDisplayTooltipIfNeeded = false; canDisplayTooltipIfNeeded = false;
hasEnabledLocalVideo = true; hasEnabledLocalVideo = true;
events.setValue(Event.DISMISS_VIDEO_TOOLTIP); events.setValue(Event.DISMISS_VIDEO_TOOLTIP);
} }
@ -144,27 +132,14 @@ public class WebRtcCallViewModel extends ViewModel {
} }
} }
private boolean isValidCameraDirectionForUi(CameraState.Direction direction) { private void updateWebRtcControls(@NonNull WebRtcViewModel.State state,
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,
boolean isLocalVideoEnabled, boolean isLocalVideoEnabled,
boolean isRemoteVideoEnabled, boolean isRemoteVideoEnabled,
boolean isRemoteVideoOffer, boolean isRemoteVideoOffer,
boolean isMoreThanOneCameraAvailable, boolean isMoreThanOneCameraAvailable,
boolean isBluetoothAvailable, boolean isBluetoothAvailable,
WebRtcAudioOutput audioOutput) @NonNull WebRtcAudioOutput audioOutput)
{ {
final WebRtcControls.CallState callState; final WebRtcControls.CallState callState;
switch (state) { switch (state) {
@ -185,20 +160,14 @@ public class WebRtcCallViewModel extends ViewModel {
audioOutput)); 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) { private @NonNull WebRtcControls getRealWebRtcControls(boolean isInPipMode, @NonNull WebRtcControls controls) {
if (isInPipMode) return WebRtcControls.PIP; return isInPipMode ? WebRtcControls.PIP : controls;
else return controls;
} }
private void startTimer() { private void startTimer() {
cancelTimer(); cancelTimer();
ellapsedTimeHandler.post(ellapsedTimeRunnable); elapsedTimeHandler.post(elapsedTimeRunnable);
} }
private void handleTick() { private void handleTick() {
@ -208,13 +177,13 @@ public class WebRtcCallViewModel extends ViewModel {
long newValue = (System.currentTimeMillis() - callConnectedTime) / 1000; long newValue = (System.currentTimeMillis() - callConnectedTime) / 1000;
ellapsed.postValue(newValue); elapsed.postValue(newValue);
ellapsedTimeHandler.postDelayed(ellapsedTimeRunnable, 1000); elapsedTimeHandler.postDelayed(elapsedTimeRunnable, 1000);
} }
private void cancelTimer() { private void cancelTimer() {
ellapsedTimeHandler.removeCallbacks(ellapsedTimeRunnable); elapsedTimeHandler.removeCallbacks(elapsedTimeRunnable);
} }
@Override @Override

View File

@ -36,6 +36,10 @@ public final class WebRtcControls {
this.audioOutput = audioOutput; this.audioOutput = audioOutput;
} }
boolean displayStartCallControls() {
return false;
}
boolean displayEndCall() { boolean displayEndCall() {
return isOngoing(); return isOngoing();
} }
@ -88,7 +92,7 @@ public final class WebRtcControls {
return !isInPipMode; return !isInPipMode;
} }
WebRtcAudioOutput getAudioOutput() { @NonNull WebRtcAudioOutput getAudioOutput() {
return audioOutput; return audioOutput;
} }

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.webrtc;
public enum WebRtcLocalRenderState { public enum WebRtcLocalRenderState {
GONE, GONE,
SMALL, SMALL_RECTANGLE,
SMALL_SQUARE,
LARGE LARGE
} }

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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()));
}
}

View File

@ -8,6 +8,8 @@ import android.graphics.drawable.LayerDrawable;
import android.widget.ImageView; import android.widget.ImageView;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.content.res.AppCompatResources;
import com.amulyakhare.textdrawable.TextDrawable; import com.amulyakhare.textdrawable.TextDrawable;
@ -22,6 +24,8 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
private final int smallResourceId; private final int smallResourceId;
private final int callCardResourceId; private final int callCardResourceId;
private ImageView.ScaleType scaleType = ImageView.ScaleType.CENTER;
public ResourceContactPhoto(@DrawableRes int resourceId) { public ResourceContactPhoto(@DrawableRes int resourceId) {
this(resourceId, resourceId, resourceId); this(resourceId, resourceId, resourceId);
} }
@ -36,26 +40,31 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
this.smallResourceId = smallResourceId; this.smallResourceId = smallResourceId;
} }
public void setScaleType(@NonNull ImageView.ScaleType scaleType) {
this.scaleType = scaleType;
}
@Override @Override
public Drawable asDrawable(Context context, int color) { public @NonNull Drawable asDrawable(@NonNull Context context, int color) {
return asDrawable(context, color, false); return asDrawable(context, color, false);
} }
@Override @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); return buildDrawable(context, resourceId, color, inverted);
} }
@Override @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); 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); Drawable background = TextDrawable.builder().buildRound(" ", inverted ? Color.WHITE : color);
RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(AppCompatResources.getDrawable(context, resourceId)); RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(AppCompatResources.getDrawable(context, resourceId));
foreground.setScaleType(ImageView.ScaleType.CENTER); //noinspection ConstantConditions
foreground.setScaleType(scaleType);
if (inverted) { if (inverted) {
foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP); foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
@ -68,12 +77,12 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
} }
@Override @Override
public Drawable asCallCard(Context context) { public @Nullable Drawable asCallCard(@NonNull Context context) {
return AppCompatResources.getDrawable(context, callCardResourceId); return AppCompatResources.getDrawable(context, callCardResourceId);
} }
private static class ExpandingLayerDrawable extends LayerDrawable { private static class ExpandingLayerDrawable extends LayerDrawable {
public ExpandingLayerDrawable(Drawable[] layers) { public ExpandingLayerDrawable(@NonNull Drawable[] layers) {
super(layers); super(layers);
} }

View File

@ -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);
}
}

View File

@ -1,17 +1,11 @@
package org.thoughtcrime.securesms.conversation.ui.mentions; package org.thoughtcrime.securesms.conversation.ui.mentions;
import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.MappingModel; import org.thoughtcrime.securesms.util.viewholders.RecipientMappingModel;
import org.thoughtcrime.securesms.util.Util;
import java.util.Objects; public final class MentionViewState extends RecipientMappingModel<MentionViewState> {
public final class MentionViewState implements MappingModel<MentionViewState> {
private final Recipient recipient; private final Recipient recipient;
@ -19,23 +13,8 @@ public final class MentionViewState implements MappingModel<MentionViewState> {
this.recipient = recipient; this.recipient = recipient;
} }
@NonNull String getName(@NonNull Context context) { @Override
return recipient.getDisplayName(context); public @NonNull Recipient getRecipient() {
}
@NonNull Recipient getRecipient() {
return recipient; 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());
}
} }

View File

@ -3,18 +3,20 @@ package org.thoughtcrime.securesms.conversation.ui.mentions;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; 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.MappingAdapter;
import org.thoughtcrime.securesms.util.MappingModel; 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; import java.util.List;
public class MentionsPickerAdapter extends MappingAdapter { public class MentionsPickerAdapter extends MappingAdapter {
private final Runnable currentListChangedListener; private final Runnable currentListChangedListener;
public MentionsPickerAdapter(@Nullable MentionEventsListener mentionEventsListener, @NonNull Runnable currentListChangedListener) { public MentionsPickerAdapter(@Nullable EventListener<MentionViewState> listener, @NonNull Runnable currentListChangedListener) {
this.currentListChangedListener = currentListChangedListener; this.currentListChangedListener = currentListChangedListener;
registerFactory(MentionViewState.class, MentionViewHolder.createFactory(mentionEventsListener)); registerFactory(MentionViewState.class, RecipientViewHolder.createFactory(R.layout.mentions_picker_recipient_list_item, listener));
} }
@Override @Override

View File

@ -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);
}
}

View File

@ -1,13 +1,14 @@
package org.thoughtcrime.securesms.events; package org.thoughtcrime.securesms.events;
import androidx.annotation.NonNull; 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.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.CameraState; import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.webrtc.SurfaceViewRenderer;
import org.whispersystems.libsignal.IdentityKey; import java.util.List;
public class WebRtcViewModel { public class WebRtcViewModel {
@ -33,70 +34,34 @@ public class WebRtcViewModel {
CALL_ONGOING_ELSEWHERE CALL_ONGOING_ELSEWHERE
} }
private final @NonNull State state;
private final @NonNull State state; private final @NonNull Recipient recipient;
private final @NonNull Recipient recipient;
private final @Nullable IdentityKey identityKey;
private final boolean remoteVideoEnabled;
private final boolean isBluetoothAvailable; private final boolean isBluetoothAvailable;
private final boolean isMicrophoneEnabled;
private final boolean isRemoteVideoOffer; private final boolean isRemoteVideoOffer;
private final long callConnectedTime;
private final CameraState localCameraState; private final CallParticipant localParticipant;
private final TextureViewRenderer localRenderer; private final List<CallParticipant> remoteParticipants;
private final TextureViewRenderer remoteRenderer;
private final long callConnectedTime; public WebRtcViewModel(@NonNull State state,
@NonNull Recipient recipient,
public WebRtcViewModel(@NonNull State state, @NonNull CameraState localCameraState,
@NonNull Recipient recipient, @NonNull BroadcastVideoSink localSink,
@NonNull CameraState localCameraState, boolean isBluetoothAvailable,
@NonNull TextureViewRenderer localRenderer, boolean isMicrophoneEnabled,
@NonNull TextureViewRenderer remoteRenderer, boolean isRemoteVideoOffer,
boolean remoteVideoEnabled, long callConnectedTime,
boolean isBluetoothAvailable, @NonNull List<CallParticipant> remoteParticipants)
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)
{ {
this.state = state; this.state = state;
this.recipient = recipient; this.recipient = recipient;
this.localCameraState = localCameraState;
this.localRenderer = localRenderer;
this.remoteRenderer = remoteRenderer;
this.identityKey = identityKey;
this.remoteVideoEnabled = remoteVideoEnabled;
this.isBluetoothAvailable = isBluetoothAvailable; this.isBluetoothAvailable = isBluetoothAvailable;
this.isMicrophoneEnabled = isMicrophoneEnabled;
this.isRemoteVideoOffer = isRemoteVideoOffer; this.isRemoteVideoOffer = isRemoteVideoOffer;
this.callConnectedTime = callConnectedTime; this.callConnectedTime = callConnectedTime;
this.remoteParticipants = remoteParticipants;
localParticipant = CallParticipant.createLocal(localCameraState, localSink, isMicrophoneEnabled);
} }
public @NonNull State getState() { public @NonNull State getState() {
@ -107,50 +72,28 @@ public class WebRtcViewModel {
return recipient; return recipient;
} }
public @NonNull CameraState getLocalCameraState() {
return localCameraState;
}
public @Nullable IdentityKey getIdentityKey() {
return identityKey;
}
public boolean isRemoteVideoEnabled() { public boolean isRemoteVideoEnabled() {
return remoteVideoEnabled; return Stream.of(remoteParticipants).anyMatch(CallParticipant::isVideoEnabled);
} }
public boolean isBluetoothAvailable() { public boolean isBluetoothAvailable() {
return isBluetoothAvailable; return isBluetoothAvailable;
} }
public boolean isMicrophoneEnabled() {
return isMicrophoneEnabled;
}
public boolean isRemoteVideoOffer() { public boolean isRemoteVideoOffer() {
return isRemoteVideoOffer; return isRemoteVideoOffer;
} }
public TextureViewRenderer getLocalRenderer() {
return localRenderer;
}
public TextureViewRenderer getRemoteRenderer() {
return remoteRenderer;
}
public long getCallConnectedTime() { public long getCallConnectedTime() {
return callConnectedTime; return callConnectedTime;
} }
public @NonNull String toString() { public @NonNull CallParticipant getLocalParticipant() {
return "[State: " + state + return localParticipant;
", recipient: " + recipient.getId().serialize() +
", identity: " + identityKey +
", remoteVideo: " + remoteVideoEnabled +
", localVideo: " + localCameraState.isEnabled() +
", isRemoteVideoOffer: " + isRemoteVideoOffer +
", callConnectedTime: " + callConnectedTime +
"]";
} }
public @NonNull List<CallParticipant> getRemoteParticipants() {
return remoteParticipants;
}
} }

View File

@ -26,11 +26,12 @@ import org.signal.ringrtc.IceCandidate;
import org.signal.ringrtc.Remote; import org.signal.ringrtc.Remote;
import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.WebRtcCallActivity; 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.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState; import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.DoNotDisturbUtil; 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.TelephonyUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder; import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder;
import org.thoughtcrime.securesms.webrtc.IncomingPstnCallReceiver; import org.thoughtcrime.securesms.webrtc.IncomingPstnCallReceiver;
import org.thoughtcrime.securesms.webrtc.UncaughtExceptionHandlerManager; 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.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.webrtc.EglBase; import org.webrtc.EglBase;
import org.webrtc.PeerConnection; import org.webrtc.PeerConnection;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
@ -74,8 +73,11 @@ import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserExce
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@ -167,7 +169,6 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
private CameraState localCameraState = CameraState.UNKNOWN; private CameraState localCameraState = CameraState.UNKNOWN;
private boolean microphoneEnabled = true; private boolean microphoneEnabled = true;
private boolean remoteVideoEnabled = false;
private boolean bluetoothAvailable = false; private boolean bluetoothAvailable = false;
private boolean enableVideoOnCreate = false; private boolean enableVideoOnCreate = false;
private boolean isRemoteVideoOffer = false; private boolean isRemoteVideoOffer = false;
@ -189,10 +190,12 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
@Nullable private RemotePeer activePeer; @Nullable private RemotePeer activePeer;
@Nullable private RemotePeer busyPeer; @Nullable private RemotePeer busyPeer;
@Nullable private SparseArray<RemotePeer> peerMap; @Nullable private SparseArray<RemotePeer> peerMap;
@Nullable private TextureViewRenderer localRenderer;
@Nullable private TextureViewRenderer remoteRenderer; @Nullable private EglBase eglBase;
@Nullable private EglBase eglBase; @Nullable private BroadcastVideoSink localSink;
@Nullable private Camera camera; @Nullable private Camera camera;
private final Map<Recipient, CallParticipant> remoteParticipantMap = new LinkedHashMap<>();
private final ExecutorService serviceExecutor = Executors.newSingleThreadExecutor(); private final ExecutorService serviceExecutor = Executors.newSingleThreadExecutor();
private final ExecutorService networkExecutor = 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) { public void onCameraSwitchCompleted(@NonNull CameraState newCameraState) {
localCameraState = newCameraState; localCameraState = newCameraState;
if (activePeer != null) { 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(); 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)); OfferMessage.Type offerType = OfferMessage.Type.fromCode(intent.getStringExtra(EXTRA_OFFER_TYPE));
CallManager.CallMediaType callMediaType = getCallMediaTypeFromOfferType(offerType); CallManager.CallMediaType callMediaType = getCallMediaTypeFromOfferType(offerType);
@ -505,7 +515,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
} }
if (activePeer != null) { 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) { 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) { 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(); camera.flip();
localCameraState = camera.getCameraState(); localCameraState = camera.getCameraState();
if (activePeer != null) { 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); bluetoothAvailable = intent.getBooleanExtra(EXTRA_AVAILABLE, false);
if (activePeer != null) { 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); 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); AudioManager androidAudioManager = ServiceUtil.getAudioManager(this);
androidAudioManager.setSpeakerphoneOn(false); 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()); lockManager.updatePhoneState(getInCallPhoneState());
audioManager.initializeAudioForCall(); audioManager.initializeAudioForCall();
audioManager.startOutgoingRinger(OutgoingRinger.Type.RINGING); audioManager.startOutgoingRinger(OutgoingRinger.Type.RINGING);
@ -633,8 +643,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
callManager.proceed(activePeer.getCallId(), callManager.proceed(activePeer.getCallId(),
WebRtcCallService.this, WebRtcCallService.this,
eglBase, eglBase,
localRenderer, localSink,
remoteRenderer, remoteParticipantMap.get(activePeer.getRecipient()).getVideoSink(),
camera, camera,
iceServers, iceServers,
isAlwaysTurn, isAlwaysTurn,
@ -645,7 +655,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
localCameraState = camera.getCameraState(); localCameraState = camera.getCameraState();
if (activePeer != null) { 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(); initializeVideo();
remoteParticipantMap.put(remotePeer.getRecipient(), CallParticipant.createRemote(
remotePeer.getRecipient(),
null,
new BroadcastVideoSink(eglBase),
false
));
setCallInProgressNotification(TYPE_INCOMING_CONNECTING, activePeer); setCallInProgressNotification(TYPE_INCOMING_CONNECTING, activePeer);
retrieveTurnServers().addListener(new SuccessOnlyListener<List<PeerConnection.IceServer>>(this.activePeer.getState(), this.activePeer.getCallId()) { 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(), callManager.proceed(activePeer.getCallId(),
WebRtcCallService.this, WebRtcCallService.this,
eglBase, eglBase,
localRenderer, localSink,
remoteRenderer, remoteParticipantMap.get(activePeer.getRecipient()).getVideoSink(),
camera, camera,
iceServers, iceServers,
hideIp, hideIp,
@ -692,7 +709,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING); lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING);
if (activePeer != null) { 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(); activePeer.localRinging();
lockManager.updatePhoneState(LockManager.PhoneState.INTERACTIVE); 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); boolean shouldDisturbUserWithCall = DoNotDisturbUtil.shouldDisturbUserWithCall(getApplicationContext(), recipient);
if (shouldDisturbUserWithCall) { if (shouldDisturbUserWithCall) {
startCallCardActivityIfPossible(); startCallCardActivityIfPossible();
@ -914,7 +931,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
Log.i(TAG, "handleRemoteRinging(): call_id: " + remotePeer.getCallId()); Log.i(TAG, "handleRemoteRinging(): call_id: " + remotePeer.getCallId());
activePeer.remoteRinging(); 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) { private void handleCallConnected(Intent intent) {
@ -940,7 +957,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
callConnectedTime = System.currentTimeMillis(); 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(); unregisterPowerButtonReceiver();
@ -969,8 +986,11 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
Log.i(TAG, "handleRemoteVideoEnable(): call_id: " + activePeer.getCallId()); Log.i(TAG, "handleRemoteVideoEnable(): call_id: " + activePeer.getCallId());
remoteVideoEnabled = enable; CallParticipant oldParticipant = Objects.requireNonNull(remoteParticipantMap.get(activePeer.getRecipient()));
sendMessage(WebRtcViewModel.State.CALL_CONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); 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); 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) { private void handleLocalHangup(Intent intent) {
if (activePeer == null) { if (activePeer == null) {
if (busyPeer != 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; busyPeer = null;
} }
@ -1043,7 +1063,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
try { try {
callManager.hangup(); 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); terminate(activePeer);
} catch (CallException e) { } catch (CallException e) {
callFailure("hangup() failed: ", e); callFailure("hangup() failed: ", e);
@ -1097,9 +1117,9 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
if (remotePeer.callIdEquals(activePeer)) { if (remotePeer.callIdEquals(activePeer)) {
boolean outgoingBeforeAccept = remotePeer.getState() == CallState.DIALING || remotePeer.getState() == CallState.REMOTE_RINGING; boolean outgoingBeforeAccept = remotePeer.getState() == CallState.DIALING || remotePeer.getState() == CallState.REMOTE_RINGING;
if (outgoingBeforeAccept) { if (outgoingBeforeAccept) {
sendMessage(WebRtcViewModel.State.RECIPIENT_UNAVAILABLE, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); sendMessage(WebRtcViewModel.State.RECIPIENT_UNAVAILABLE, remotePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
} else { } 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()); Log.i(TAG, "handleEndedRemoteHangupAccepted(): call_id: " + remotePeer.getCallId());
if (remotePeer.callIdEquals(activePeer)) { 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); terminate(remotePeer);
@ -1129,7 +1149,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
Log.i(TAG, "handleEndedRemoteHangupBusy(): call_id: " + remotePeer.getCallId()); Log.i(TAG, "handleEndedRemoteHangupBusy(): call_id: " + remotePeer.getCallId());
if (remotePeer.callIdEquals(activePeer)) { 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); terminate(remotePeer);
@ -1141,7 +1161,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
Log.i(TAG, "handleEndedRemoteHangupDeclined(): call_id: " + remotePeer.getCallId()); Log.i(TAG, "handleEndedRemoteHangupDeclined(): call_id: " + remotePeer.getCallId());
if (remotePeer.callIdEquals(activePeer)) { 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); terminate(remotePeer);
@ -1163,7 +1183,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
busyPeer = null; busyPeer = null;
}, BUSY_TONE_LENGTH); }, 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); terminate(remotePeer);
@ -1175,7 +1195,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
Log.i(TAG, "handleEndedRemoteNeedPermission(): call_id: " + remotePeer.getCallId()); Log.i(TAG, "handleEndedRemoteNeedPermission(): call_id: " + remotePeer.getCallId());
if (remotePeer.callIdEquals(activePeer)) { 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); terminate(remotePeer);
@ -1187,7 +1207,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
Log.i(TAG, "handleEndedRemoteGlare(): call_id: " + remotePeer.getCallId()); Log.i(TAG, "handleEndedRemoteGlare(): call_id: " + remotePeer.getCallId());
if (remotePeer.callIdEquals(activePeer)) { 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; 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()); Log.i(TAG, "handleEndedFailure(): call_id: " + remotePeer.getCallId());
if (remotePeer.callIdEquals(activePeer)) { 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) { 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() { private void initializeVideo() {
Util.runOnMainSync(() -> { Util.runOnMainSync(() -> {
eglBase = EglBase.create();
eglBase = EglBase.create(); localSink = new BroadcastVideoSink(eglBase);
localRenderer = new TextureViewRenderer(WebRtcCallService.this);
remoteRenderer = new TextureViewRenderer(WebRtcCallService.this);
localRenderer.init(eglBase.getEglBaseContext(), null);
remoteRenderer.init(eglBase.getEglBaseContext(), null);
camera = new Camera(WebRtcCallService.this, WebRtcCallService.this, eglBase); camera = new Camera(WebRtcCallService.this, WebRtcCallService.this, eglBase);
localCameraState = camera.getCameraState(); localCameraState = camera.getCameraState();
}); });
@ -1299,31 +1313,28 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
camera = null; camera = null;
} }
if (eglBase != null && localRenderer != null && remoteRenderer != null) { if (eglBase != null) {
localRenderer.release();
remoteRenderer.release();
eglBase.release(); eglBase.release();
eglBase = null;
localRenderer = null;
remoteRenderer = null;
eglBase = null;
} }
this.localCameraState = CameraState.UNKNOWN; this.localCameraState = CameraState.UNKNOWN;
this.microphoneEnabled = true; this.microphoneEnabled = true;
this.remoteVideoEnabled = false;
this.enableVideoOnCreate = false; this.enableVideoOnCreate = false;
Log.i(TAG, "clear activePeer callId: " + activePeer.getCallId() + " key: " + activePeer.hashCode()); Log.i(TAG, "clear activePeer callId: " + activePeer.getCallId() + " key: " + activePeer.hashCode());
this.activePeer = null; this.activePeer = null;
for (CallParticipant participant : remoteParticipantMap.values()) {
remoteParticipantMap.put(participant.getRecipient(), participant.withVideoEnabled(false));
}
lockManager.updatePhoneState(LockManager.PhoneState.IDLE); lockManager.updatePhoneState(LockManager.PhoneState.IDLE);
} }
private void sendMessage(@NonNull WebRtcViewModel.State state, private void sendMessage(@NonNull WebRtcViewModel.State state,
@NonNull RemotePeer remotePeer, @NonNull RemotePeer remotePeer,
@NonNull CameraState localCameraState, @NonNull CameraState localCameraState,
boolean remoteVideoEnabled,
boolean bluetoothAvailable, boolean bluetoothAvailable,
boolean microphoneEnabled, boolean microphoneEnabled,
boolean isRemoteVideoOffer) boolean isRemoteVideoOffer)
@ -1331,35 +1342,12 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
EventBus.getDefault().postSticky(new WebRtcViewModel(state, EventBus.getDefault().postSticky(new WebRtcViewModel(state,
remotePeer.getRecipient(), remotePeer.getRecipient(),
localCameraState, localCameraState,
localRenderer, localSink,
remoteRenderer,
remoteVideoEnabled,
bluetoothAvailable, bluetoothAvailable,
microphoneEnabled, microphoneEnabled,
isRemoteVideoOffer, isRemoteVideoOffer,
callConnectedTime)); callConnectedTime,
} new ArrayList<>(remoteParticipantMap.values())));
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));
} }
private ListenableFutureTask<Boolean> sendMessage(@NonNull final RemotePeer remotePeer, 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); Log.w(TAG, "callFailure(): " + message, error);
if (activePeer != null) { 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) { if (callManager != null) {
@ -1710,11 +1698,16 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
} }
if (error instanceof UntrustedIdentityException) { 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) { } 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) { } 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);
} }
} }
} }

View File

@ -113,20 +113,20 @@ public class MappingAdapter extends ListAdapter<MappingModel<?>, MappingViewHold
} }
public interface Factory<T extends MappingModel<T>> { 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> { public static class LayoutFactory<T extends MappingModel<T>> implements Factory<T> {
private Function<View, MappingViewHolder<T>> creator; private Function<View, MappingViewHolder<T>> creator;
private final int layout; 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.creator = creator;
this.layout = layout; this.layout = layout;
} }
@Override @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)); return creator.apply(LayoutInflater.from(parent.getContext()).inflate(layout, parent, false));
} }
} }

View File

@ -23,5 +23,9 @@ public abstract class MappingViewHolder<Model extends MappingModel<Model>> exten
return itemView.findViewById(id); return itemView.findViewById(id);
} }
public @NonNull Context getContext() {
return itemView.getContext();
}
public abstract void bind(@NonNull Model model); public abstract void bind(@NonNull Model model);
} }

View File

@ -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());
}
}

View File

@ -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);
}
}

View 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>

View File

@ -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" />

View 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" />

View 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>

View File

@ -12,14 +12,14 @@
android:paddingBottom="8dp"> android:paddingBottom="8dp">
<org.thoughtcrime.securesms.components.AvatarImageView <org.thoughtcrime.securesms.components.AvatarImageView
android:id="@+id/mention_recipient_avatar" android:id="@+id/recipient_view_avatar"
android:layout_width="36dp" android:layout_width="36dp"
android:layout_height="36dp" android:layout_height="36dp"
app:fallbackImageSize="small" app:fallbackImageSize="small"
tools:src="@tools:sample/avatars" /> tools:src="@tools:sample/avatars" />
<TextView <TextView
android:id="@+id/mention_recipient_name" android:id="@+id/recipient_view_name"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="12dp" android:layout_marginStart="12dp"

View File

@ -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>

View File

@ -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" />

View File

@ -10,31 +10,47 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/transparent_black_40" /> android:background="@color/transparent_black_40" />
<org.thoughtcrime.securesms.components.AvatarImageView <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/call_screen_recipient_avatar" android:layout_width="match_parent"
android:layout_width="200dp" android:layout_height="match_parent">
android:layout_height="200dp"
android:layout_gravity="center" />
<ImageView <androidx.viewpager2.widget.ViewPager2
android:id="@+id/call_screen_recipient_avatar_call_card" android:id="@+id/call_screen_participants_pager"
android:layout_width="200dp" android:layout_width="match_parent"
android:layout_height="200dp" android:layout_height="match_parent"
android:layout_gravity="center" app:layout_constraintStart_toStartOf="parent"
android:visibility="gone" /> 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 <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_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone" /> android:background="@color/black">
<FrameLayout <org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer
android:id="@+id/call_screen_large_local_renderer_holder" android:id="@+id/call_screen_large_local_renderer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent" />
android:background="@color/black"
android:visibility="gone" /> </FrameLayout>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/call_screen" android:id="@+id/call_screen"
@ -95,60 +111,28 @@
android:background="@null" android:background="@null"
android:clipChildren="true" android:clipChildren="true"
android:translationX="100000dp" android:translationX="100000dp"
android:translationY="-100000dp" android:translationY="100000dp"
android:visibility="gone" android:visibility="gone"
app:cardCornerRadius="8dp" app:cardCornerRadius="8dp"
tools:background="@color/red" tools:background="@color/red"
tools:visibility="visible"> tools:visibility="visible">
<FrameLayout <org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer
android:id="@+id/call_screen_small_local_renderer_holder" android:id="@+id/call_screen_small_local_renderer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
</org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout> </org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout>
<ImageView <include
android:id="@+id/call_screen_down_arrow" android:id="@+id/call_screen_toolbar"
android:layout_width="20dp" layout="@layout/webrtc_call_view_toolbar"
android:layout_height="11dp" android:layout_width="match_parent"
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"
android:layout_height="wrap_content" 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_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/call_screen_status_bar_guideline" 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..." />
<org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutputToggleButton <org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutputToggleButton
android:id="@+id/call_screen_speaker_toggle" android:id="@+id/call_screen_speaker_toggle"
@ -158,7 +142,7 @@
android:layout_marginBottom="34dp" android:layout_marginBottom="34dp"
android:scaleType="fitXY" android:scaleType="fitXY"
android:visibility="gone" 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_constraintEnd_toStartOf="@id/call_screen_camera_direction_toggle"
app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -174,7 +158,7 @@
android:clickable="false" android:clickable="false"
android:scaleType="fitXY" android:scaleType="fitXY"
android:visibility="gone" 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_constraintEnd_toStartOf="@id/call_screen_video_toggle"
app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/call_screen_speaker_toggle" app:layout_constraintStart_toEndOf="@id/call_screen_speaker_toggle"
@ -189,7 +173,7 @@
android:background="@drawable/webrtc_call_screen_video_toggle" android:background="@drawable/webrtc_call_screen_video_toggle"
android:stateListAnimator="@null" android:stateListAnimator="@null"
android:visibility="gone" 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_constraintEnd_toStartOf="@id/call_screen_audio_mic_toggle"
app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/call_screen_camera_direction_toggle" app:layout_constraintStart_toEndOf="@id/call_screen_camera_direction_toggle"
@ -204,7 +188,7 @@
android:background="@drawable/webrtc_call_screen_mic_toggle" android:background="@drawable/webrtc_call_screen_mic_toggle"
android:stateListAnimator="@null" android:stateListAnimator="@null"
android:visibility="gone" 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_constraintEnd_toStartOf="@id/call_screen_end_call"
app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/call_screen_video_toggle" app:layout_constraintStart_toEndOf="@id/call_screen_video_toggle"
@ -218,7 +202,7 @@
android:clickable="false" android:clickable="false"
android:scaleType="fitXY" android:scaleType="fitXY"
android:visibility="gone" 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_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/call_screen_audio_mic_toggle" app:layout_constraintStart_toEndOf="@id/call_screen_audio_mic_toggle"
@ -304,5 +288,45 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" /> 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> </androidx.constraintlayout.widget.ConstraintLayout>
</merge> </merge>

View 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>

View 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>

View File

@ -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__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__signal_s">Signal %1$s</string>
<string name="WebRtcCallActivity__calling">Calling…</string> <string name="WebRtcCallActivity__calling">Calling…</string>
<string name="WebRtcCallActivity__group_call">Group Call</string>
<!-- WebRtcCallView --> <!-- WebRtcCallView -->
<string name="WebRtcCallView__signal_voice_call">Signal voice call…</string> <string name="WebRtcCallView__signal_voice_call">Signal voice call…</string>
<string name="WebRtcCallView__signal_video_call">Signal video 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 --> <!-- RegistrationActivity -->
<string name="RegistrationActivity_select_your_country">Select your country</string> <string name="RegistrationActivity_select_your_country">Select your country</string>

View File

@ -513,4 +513,10 @@
<item name="cornerFamily">rounded</item> <item name="cornerFamily">rounded</item>
<item name="cornerSize">50%</item> <item name="cornerSize">50%</item>
</style> </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> </resources>