mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-08 07:18:33 +00:00
Add foundational UX and state support for Group Calling.
This commit is contained in:
parent
7baf8052a2
commit
dc4faf57cb
@ -41,6 +41,8 @@ import org.greenrobot.eventbus.EventBus;
|
|||||||
import org.greenrobot.eventbus.Subscribe;
|
import org.greenrobot.eventbus.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
package org.thoughtcrime.securesms.animation;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.animation.Animation;
|
||||||
|
import android.view.animation.Transformation;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class ResizeAnimation extends Animation {
|
||||||
|
|
||||||
|
private final View target;
|
||||||
|
private final int targetWidthPx;
|
||||||
|
private final int targetHeightPx;
|
||||||
|
|
||||||
|
private int startWidth;
|
||||||
|
private int startHeight;
|
||||||
|
|
||||||
|
public ResizeAnimation(@NonNull View target, int targetWidthPx, int targetHeightPx) {
|
||||||
|
this.target = target;
|
||||||
|
this.targetWidthPx = targetWidthPx;
|
||||||
|
this.targetHeightPx = targetHeightPx;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void applyTransformation(float interpolatedTime, Transformation t) {
|
||||||
|
int newWidth = (int) (startWidth + (targetWidthPx - startWidth) * interpolatedTime);
|
||||||
|
int newHeight = (int) (startHeight + (targetHeightPx - startHeight) * interpolatedTime);
|
||||||
|
|
||||||
|
ViewGroup.LayoutParams params = target.getLayoutParams();
|
||||||
|
|
||||||
|
params.width = newWidth;
|
||||||
|
params.height = newHeight;
|
||||||
|
|
||||||
|
target.setLayoutParams(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize(int width, int height, int parentWidth, int parentHeight) {
|
||||||
|
super.initialize(width, height, parentWidth, parentHeight);
|
||||||
|
|
||||||
|
this.startWidth = width;
|
||||||
|
this.startHeight = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean willChangeBounds() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.webrtc;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.webrtc.EglBase;
|
||||||
|
import org.webrtc.VideoFrame;
|
||||||
|
import org.webrtc.VideoSink;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.WeakHashMap;
|
||||||
|
|
||||||
|
public class BroadcastVideoSink implements VideoSink {
|
||||||
|
|
||||||
|
private final EglBase eglBase;
|
||||||
|
private final WeakHashMap<VideoSink, Boolean> sinks;
|
||||||
|
|
||||||
|
public BroadcastVideoSink(@Nullable EglBase eglBase) {
|
||||||
|
this.eglBase = eglBase;
|
||||||
|
this.sinks = new WeakHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable EglBase getEglBase() {
|
||||||
|
return eglBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSink(@NonNull VideoSink sink) {
|
||||||
|
sinks.put(sink, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeSink(@NonNull VideoSink sink) {
|
||||||
|
sinks.remove(sink);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFrame(@NonNull VideoFrame videoFrame) {
|
||||||
|
for (VideoSink sink : sinks.keySet()) {
|
||||||
|
sink.onFrame(videoFrame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,115 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.webrtc;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
|
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||||
|
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||||
|
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||||
|
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||||
|
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulates views needed to show a call participant including their
|
||||||
|
* avatar in full screen or pip mode, and their video feed.
|
||||||
|
*/
|
||||||
|
public class CallParticipantView extends ConstraintLayout {
|
||||||
|
|
||||||
|
private static final FallbackPhotoProvider FALLBACK_PHOTO_PROVIDER = new FallbackPhotoProvider();
|
||||||
|
|
||||||
|
private RecipientId recipientId;
|
||||||
|
private AvatarImageView avatar;
|
||||||
|
private TextureViewRenderer renderer;
|
||||||
|
private ImageView pipAvatar;
|
||||||
|
private ContactPhoto contactPhoto;
|
||||||
|
|
||||||
|
public CallParticipantView(@NonNull Context context) {
|
||||||
|
super(context);
|
||||||
|
onFinishInflate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CallParticipantView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CallParticipantView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onFinishInflate() {
|
||||||
|
super.onFinishInflate();
|
||||||
|
avatar = findViewById(R.id.call_participant_item_avatar);
|
||||||
|
pipAvatar = findViewById(R.id.call_participant_item_pip_avatar);
|
||||||
|
renderer = findViewById(R.id.call_participant_renderer);
|
||||||
|
|
||||||
|
avatar.setFallbackPhotoProvider(FALLBACK_PHOTO_PROVIDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCallParticipant(@NonNull CallParticipant participant) {
|
||||||
|
boolean participantChanged = recipientId == null || !recipientId.equals(participant.getRecipient().getId());
|
||||||
|
recipientId = participant.getRecipient().getId();
|
||||||
|
|
||||||
|
renderer.setVisibility(participant.isVideoEnabled() ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
|
if (participant.isVideoEnabled()) {
|
||||||
|
if (participant.getVideoSink().getEglBase() != null) {
|
||||||
|
renderer.init(participant.getVideoSink().getEglBase());
|
||||||
|
}
|
||||||
|
renderer.attachBroadcastVideoSink(participant.getVideoSink());
|
||||||
|
} else {
|
||||||
|
renderer.attachBroadcastVideoSink(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (participantChanged || !Objects.equals(contactPhoto, participant.getRecipient().getContactPhoto())) {
|
||||||
|
avatar.setAvatar(participant.getRecipient());
|
||||||
|
AvatarUtil.loadBlurredIconIntoViewBackground(participant.getRecipient(), this);
|
||||||
|
setPipAvatar(participant.getRecipient());
|
||||||
|
contactPhoto = participant.getRecipient().getContactPhoto();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setRenderInPip(boolean shouldRenderInPip) {
|
||||||
|
avatar.setVisibility(shouldRenderInPip ? View.GONE : View.VISIBLE);
|
||||||
|
pipAvatar.setVisibility(shouldRenderInPip ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPipAvatar(@NonNull Recipient recipient) {
|
||||||
|
ContactPhoto contactPhoto = recipient.getContactPhoto();
|
||||||
|
FallbackContactPhoto fallbackPhoto = recipient.getFallbackContactPhoto(FALLBACK_PHOTO_PROVIDER);
|
||||||
|
|
||||||
|
GlideApp.with(this)
|
||||||
|
.load(contactPhoto)
|
||||||
|
.fallback(fallbackPhoto.asCallCard(getContext()))
|
||||||
|
.error(fallbackPhoto.asCallCard(getContext()))
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||||
|
.into(pipAvatar);
|
||||||
|
|
||||||
|
pipAvatar.setScaleType(contactPhoto == null ? ImageView.ScaleType.CENTER_INSIDE : ImageView.ScaleType.CENTER_CROP);
|
||||||
|
pipAvatar.setBackgroundColor(recipient.getColor().toActionBarColor(getContext()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider {
|
||||||
|
@Override
|
||||||
|
public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
|
||||||
|
ResourceContactPhoto photo = new ResourceContactPhoto(R.drawable.ic_profile_outline_120);
|
||||||
|
photo.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||||
|
return photo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,104 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.webrtc;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.android.flexbox.AlignItems;
|
||||||
|
import com.google.android.flexbox.FlexboxLayout;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||||
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can dynamically render a collection of call participants, adjusting their
|
||||||
|
* sizing and layout depending on the total number of participants.
|
||||||
|
*/
|
||||||
|
public class CallParticipantsLayout extends FlexboxLayout {
|
||||||
|
|
||||||
|
private List<CallParticipant> callParticipants = Collections.emptyList();
|
||||||
|
private boolean shouldRenderInPip;
|
||||||
|
|
||||||
|
public CallParticipantsLayout(@NonNull Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CallParticipantsLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CallParticipantsLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void update(@NonNull List<CallParticipant> callParticipants, boolean shouldRenderInPip) {
|
||||||
|
this.callParticipants = callParticipants;
|
||||||
|
this.shouldRenderInPip = shouldRenderInPip;
|
||||||
|
updateLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateLayout() {
|
||||||
|
if (shouldRenderInPip && Util.hasItems(callParticipants)) {
|
||||||
|
updateChildrenCount(1);
|
||||||
|
update(0, callParticipants.get(0));
|
||||||
|
} else {
|
||||||
|
int count = callParticipants.size();
|
||||||
|
updateChildrenCount(count);
|
||||||
|
|
||||||
|
for (int i = 0; i < callParticipants.size(); i++) {
|
||||||
|
update(i, callParticipants.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateChildrenCount(int count) {
|
||||||
|
int childCount = getChildCount();
|
||||||
|
if (childCount < count) {
|
||||||
|
for (int i = childCount; i < count; i++) {
|
||||||
|
addCallParticipantView();
|
||||||
|
}
|
||||||
|
} else if (childCount > count) {
|
||||||
|
for (int i = count; i < childCount; i++) {
|
||||||
|
removeViewAt(count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update(int index, @NonNull CallParticipant participant) {
|
||||||
|
CallParticipantView callParticipantView = (CallParticipantView) getChildAt(index);
|
||||||
|
callParticipantView.setCallParticipant(participant);
|
||||||
|
callParticipantView.setRenderInPip(shouldRenderInPip);
|
||||||
|
setChildLayoutParams(callParticipantView, index, getChildCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addCallParticipantView() {
|
||||||
|
View view = LayoutInflater.from(getContext()).inflate(R.layout.call_participant_item, this, false);
|
||||||
|
FlexboxLayout.LayoutParams params = (FlexboxLayout.LayoutParams) view.getLayoutParams();
|
||||||
|
|
||||||
|
params.setAlignSelf(AlignItems.STRETCH);
|
||||||
|
view.setLayoutParams(params);
|
||||||
|
addView(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setChildLayoutParams(@NonNull View child, int childPosition, int childCount) {
|
||||||
|
FlexboxLayout.LayoutParams params = (FlexboxLayout.LayoutParams) child.getLayoutParams();
|
||||||
|
if (childCount < 3) {
|
||||||
|
params.setFlexBasisPercent(1f);
|
||||||
|
} else {
|
||||||
|
if ((childCount % 2) != 0 && childPosition == childCount - 1) {
|
||||||
|
params.setFlexBasisPercent(1f);
|
||||||
|
} else {
|
||||||
|
params.setFlexBasisPercent(0.5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
child.setLayoutParams(params);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,194 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.webrtc;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||||
|
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||||
|
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the state of all participants, remote and local, combined with view state
|
||||||
|
* needed to properly render the participants. The view state primarily consists of
|
||||||
|
* if we are in System PIP mode and if we should show our video for an outgoing call.
|
||||||
|
*/
|
||||||
|
public final class CallParticipantsState {
|
||||||
|
|
||||||
|
public static final CallParticipantsState STARTING_STATE = new CallParticipantsState(WebRtcViewModel.State.CALL_DISCONNECTED,
|
||||||
|
Collections.emptyList(),
|
||||||
|
CallParticipant.createLocal(CameraState.UNKNOWN, new BroadcastVideoSink(null), false),
|
||||||
|
null,
|
||||||
|
WebRtcLocalRenderState.GONE,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false);
|
||||||
|
|
||||||
|
private final WebRtcViewModel.State callState;
|
||||||
|
private final List<CallParticipant> remoteParticipants;
|
||||||
|
private final CallParticipant localParticipant;
|
||||||
|
private final CallParticipant focusedParticipant;
|
||||||
|
private final WebRtcLocalRenderState localRenderState;
|
||||||
|
private final boolean isInPipMode;
|
||||||
|
private final boolean showVideoForOutgoing;
|
||||||
|
private final boolean isViewingFocusedParticipant;
|
||||||
|
|
||||||
|
public CallParticipantsState(@NonNull WebRtcViewModel.State callState,
|
||||||
|
@NonNull List<CallParticipant> remoteParticipants,
|
||||||
|
@NonNull CallParticipant localParticipant,
|
||||||
|
@Nullable CallParticipant focusedParticipant,
|
||||||
|
@NonNull WebRtcLocalRenderState localRenderState,
|
||||||
|
boolean isInPipMode,
|
||||||
|
boolean showVideoForOutgoing,
|
||||||
|
boolean isViewingFocusedParticipant)
|
||||||
|
{
|
||||||
|
this.callState = callState;
|
||||||
|
this.remoteParticipants = remoteParticipants;
|
||||||
|
this.localParticipant = localParticipant;
|
||||||
|
this.localRenderState = localRenderState;
|
||||||
|
this.focusedParticipant = focusedParticipant;
|
||||||
|
this.isInPipMode = isInPipMode;
|
||||||
|
this.showVideoForOutgoing = showVideoForOutgoing;
|
||||||
|
this.isViewingFocusedParticipant = isViewingFocusedParticipant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull List<CallParticipant> getGridParticipants() {
|
||||||
|
if (getAllRemoteParticipants().size() > 6) {
|
||||||
|
return getAllRemoteParticipants().subList(0, 6);
|
||||||
|
} else {
|
||||||
|
return getAllRemoteParticipants();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull List<CallParticipant> getListParticipants() {
|
||||||
|
if (isViewingFocusedParticipant && getAllRemoteParticipants().size() > 1) {
|
||||||
|
return getAllRemoteParticipants().subList(1, getAllRemoteParticipants().size());
|
||||||
|
} else if (getAllRemoteParticipants().size() > 6) {
|
||||||
|
return getAllRemoteParticipants().subList(6, getAllRemoteParticipants().size());
|
||||||
|
} else {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull List<CallParticipant> getAllRemoteParticipants() {
|
||||||
|
return remoteParticipants;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull CallParticipant getLocalParticipant() {
|
||||||
|
return localParticipant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable CallParticipant getFocusedParticipant() {
|
||||||
|
return focusedParticipant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull WebRtcLocalRenderState getLocalRenderState() {
|
||||||
|
return localRenderState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInPipMode() {
|
||||||
|
return isInPipMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState,
|
||||||
|
@NonNull WebRtcViewModel webRtcViewModel,
|
||||||
|
boolean enableVideo)
|
||||||
|
{
|
||||||
|
boolean newShowVideoForOutgoing = oldState.showVideoForOutgoing;
|
||||||
|
if (enableVideo) {
|
||||||
|
newShowVideoForOutgoing = webRtcViewModel.getState() == WebRtcViewModel.State.CALL_OUTGOING;
|
||||||
|
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_OUTGOING) {
|
||||||
|
newShowVideoForOutgoing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(webRtcViewModel.getLocalParticipant(),
|
||||||
|
oldState.isInPipMode,
|
||||||
|
newShowVideoForOutgoing,
|
||||||
|
webRtcViewModel.getState(),
|
||||||
|
oldState.getAllRemoteParticipants().size(),
|
||||||
|
oldState.isViewingFocusedParticipant);
|
||||||
|
|
||||||
|
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
|
||||||
|
|
||||||
|
return new CallParticipantsState(webRtcViewModel.getState(),
|
||||||
|
webRtcViewModel.getRemoteParticipants(),
|
||||||
|
webRtcViewModel.getLocalParticipant(),
|
||||||
|
focused,
|
||||||
|
localRenderState,
|
||||||
|
oldState.isInPipMode,
|
||||||
|
newShowVideoForOutgoing,
|
||||||
|
oldState.isViewingFocusedParticipant);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, boolean isInPip) {
|
||||||
|
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
|
||||||
|
isInPip,
|
||||||
|
oldState.showVideoForOutgoing,
|
||||||
|
oldState.callState,
|
||||||
|
oldState.getAllRemoteParticipants().size(),
|
||||||
|
oldState.isViewingFocusedParticipant);
|
||||||
|
|
||||||
|
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
|
||||||
|
|
||||||
|
return new CallParticipantsState(oldState.callState,
|
||||||
|
oldState.remoteParticipants,
|
||||||
|
oldState.localParticipant,
|
||||||
|
focused,
|
||||||
|
localRenderState,
|
||||||
|
isInPip,
|
||||||
|
oldState.showVideoForOutgoing,
|
||||||
|
oldState.isViewingFocusedParticipant);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, @NonNull SelectedPage selectedPage) {
|
||||||
|
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
|
||||||
|
|
||||||
|
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
|
||||||
|
oldState.isInPipMode,
|
||||||
|
oldState.showVideoForOutgoing,
|
||||||
|
oldState.callState,
|
||||||
|
oldState.getAllRemoteParticipants().size(),
|
||||||
|
selectedPage == SelectedPage.FOCUSED);
|
||||||
|
|
||||||
|
return new CallParticipantsState(oldState.callState,
|
||||||
|
oldState.remoteParticipants,
|
||||||
|
oldState.localParticipant,
|
||||||
|
focused,
|
||||||
|
localRenderState,
|
||||||
|
oldState.isInPipMode,
|
||||||
|
oldState.showVideoForOutgoing,
|
||||||
|
selectedPage == SelectedPage.FOCUSED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @NonNull WebRtcLocalRenderState determineLocalRenderMode(@NonNull CallParticipant localParticipant,
|
||||||
|
boolean isInPip,
|
||||||
|
boolean showVideoForOutgoing,
|
||||||
|
@NonNull WebRtcViewModel.State callState,
|
||||||
|
int numberOfRemoteParticipants,
|
||||||
|
boolean isViewingFocusedParticipant)
|
||||||
|
{
|
||||||
|
boolean displayLocal = !isInPip && localParticipant.isVideoEnabled();
|
||||||
|
WebRtcLocalRenderState localRenderState = WebRtcLocalRenderState.GONE;
|
||||||
|
|
||||||
|
if (displayLocal || showVideoForOutgoing) {
|
||||||
|
if (callState == WebRtcViewModel.State.CALL_CONNECTED) {
|
||||||
|
if (isViewingFocusedParticipant || numberOfRemoteParticipants > 3) {
|
||||||
|
localRenderState = WebRtcLocalRenderState.SMALL_SQUARE;
|
||||||
|
} else {
|
||||||
|
localRenderState = WebRtcLocalRenderState.SMALL_RECTANGLE;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
localRenderState = WebRtcLocalRenderState.LARGE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return localRenderState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SelectedPage {
|
||||||
|
GRID,
|
||||||
|
FOCUSED
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,9 @@ import org.thoughtcrime.securesms.logging.Log;
|
|||||||
import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout;
|
import 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;
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -0,0 +1,64 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.webrtc;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
class WebRtcCallParticipantsPage {
|
||||||
|
|
||||||
|
private final List<CallParticipant> callParticipants;
|
||||||
|
private final boolean isSpeaker;
|
||||||
|
private final boolean isRenderInPip;
|
||||||
|
|
||||||
|
static WebRtcCallParticipantsPage forMultipleParticipants(@NonNull List<CallParticipant> callParticipants,
|
||||||
|
boolean isRenderInPip)
|
||||||
|
{
|
||||||
|
return new WebRtcCallParticipantsPage(callParticipants, false, isRenderInPip);
|
||||||
|
}
|
||||||
|
|
||||||
|
static WebRtcCallParticipantsPage forSingleParticipant(@NonNull CallParticipant singleParticipant,
|
||||||
|
boolean isRenderInPip)
|
||||||
|
{
|
||||||
|
return new WebRtcCallParticipantsPage(Collections.singletonList(singleParticipant), true, isRenderInPip);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebRtcCallParticipantsPage(@NonNull List<CallParticipant> callParticipants,
|
||||||
|
boolean isSpeaker,
|
||||||
|
boolean isRenderInPip)
|
||||||
|
{
|
||||||
|
this.callParticipants = callParticipants;
|
||||||
|
this.isSpeaker = isSpeaker;
|
||||||
|
this.isRenderInPip = isRenderInPip;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull List<CallParticipant> getCallParticipants() {
|
||||||
|
return callParticipants;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRenderInPip() {
|
||||||
|
return isRenderInPip;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSpeaker() {
|
||||||
|
return isSpeaker;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
WebRtcCallParticipantsPage that = (WebRtcCallParticipantsPage) o;
|
||||||
|
return isSpeaker == that.isSpeaker &&
|
||||||
|
isRenderInPip == that.isRenderInPip &&
|
||||||
|
callParticipants.equals(that.callParticipants);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(callParticipants, isSpeaker);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,127 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.webrtc;
|
||||||
|
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.DiffUtil;
|
||||||
|
import androidx.recyclerview.widget.ListAdapter;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
|
||||||
|
class WebRtcCallParticipantsPagerAdapter extends ListAdapter<WebRtcCallParticipantsPage, WebRtcCallParticipantsPagerAdapter.ViewHolder> {
|
||||||
|
|
||||||
|
private static final int VIEW_TYPE_MULTI = 0;
|
||||||
|
private static final int VIEW_TYPE_SINGLE = 1;
|
||||||
|
|
||||||
|
private final Runnable onPageClicked;
|
||||||
|
|
||||||
|
WebRtcCallParticipantsPagerAdapter(@NonNull Runnable onPageClicked) {
|
||||||
|
super(new DiffCallback());
|
||||||
|
this.onPageClicked = onPageClicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
|
||||||
|
super.onAttachedToRecyclerView(recyclerView);
|
||||||
|
recyclerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
final ViewHolder viewHolder;
|
||||||
|
|
||||||
|
switch (viewType) {
|
||||||
|
case VIEW_TYPE_SINGLE:
|
||||||
|
viewHolder = new SingleParticipantViewHolder((CallParticipantView) LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.call_participant_item,
|
||||||
|
parent,
|
||||||
|
false));
|
||||||
|
break;
|
||||||
|
case VIEW_TYPE_MULTI:
|
||||||
|
viewHolder = new MultipleParticipantViewHolder((CallParticipantsLayout) LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.webrtc_call_participants_layout,
|
||||||
|
parent,
|
||||||
|
false));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unsupported viewType: " + viewType);
|
||||||
|
}
|
||||||
|
|
||||||
|
viewHolder.itemView.setOnClickListener(unused -> onPageClicked.run());
|
||||||
|
|
||||||
|
return viewHolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||||
|
holder.bind(getItem(position));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemViewType(int position) {
|
||||||
|
return getItem(position).isSpeaker() ? VIEW_TYPE_SINGLE : VIEW_TYPE_MULTI;
|
||||||
|
}
|
||||||
|
|
||||||
|
static abstract class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
public ViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract void bind(WebRtcCallParticipantsPage page);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class MultipleParticipantViewHolder extends ViewHolder {
|
||||||
|
|
||||||
|
private final CallParticipantsLayout callParticipantsLayout;
|
||||||
|
|
||||||
|
private MultipleParticipantViewHolder(@NonNull CallParticipantsLayout callParticipantsLayout) {
|
||||||
|
super(callParticipantsLayout);
|
||||||
|
this.callParticipantsLayout = callParticipantsLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void bind(WebRtcCallParticipantsPage page) {
|
||||||
|
callParticipantsLayout.update(page.getCallParticipants(), page.isRenderInPip());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SingleParticipantViewHolder extends ViewHolder {
|
||||||
|
|
||||||
|
private final CallParticipantView callParticipantView;
|
||||||
|
|
||||||
|
private SingleParticipantViewHolder(CallParticipantView callParticipantView) {
|
||||||
|
super(callParticipantView);
|
||||||
|
this.callParticipantView = callParticipantView;
|
||||||
|
|
||||||
|
ViewGroup.LayoutParams params = callParticipantView.getLayoutParams();
|
||||||
|
|
||||||
|
params.height = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
|
params.width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
|
|
||||||
|
callParticipantView.setLayoutParams(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void bind(WebRtcCallParticipantsPage page) {
|
||||||
|
callParticipantView.setCallParticipant(page.getCallParticipants().get(0));
|
||||||
|
callParticipantView.setRenderInPip(page.isRenderInPip());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class DiffCallback extends DiffUtil.ItemCallback<WebRtcCallParticipantsPage> {
|
||||||
|
@Override
|
||||||
|
public boolean areItemsTheSame(@NonNull WebRtcCallParticipantsPage oldItem, @NonNull WebRtcCallParticipantsPage newItem) {
|
||||||
|
return oldItem.isSpeaker() == newItem.isSpeaker();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(@NonNull WebRtcCallParticipantsPage oldItem, @NonNull WebRtcCallParticipantsPage newItem) {
|
||||||
|
return oldItem.equals(newItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.webrtc;
|
||||||
|
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.DiffUtil;
|
||||||
|
import androidx.recyclerview.widget.ListAdapter;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||||
|
|
||||||
|
class WebRtcCallParticipantsRecyclerAdapter extends ListAdapter<CallParticipant, WebRtcCallParticipantsRecyclerAdapter.ViewHolder> {
|
||||||
|
|
||||||
|
protected WebRtcCallParticipantsRecyclerAdapter() {
|
||||||
|
super(new DiffCallback());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.webrtc_call_participant_recycler_item, parent, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||||
|
holder.bind(getItem(position));
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
|
private final CallParticipantView callParticipantView;
|
||||||
|
|
||||||
|
ViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
callParticipantView = itemView.findViewById(R.id.call_participant);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bind(@NonNull CallParticipant callParticipant) {
|
||||||
|
callParticipantView.setCallParticipant(callParticipant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DiffCallback extends DiffUtil.ItemCallback<CallParticipant> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areItemsTheSame(@NonNull CallParticipant oldItem, @NonNull CallParticipant newItem) {
|
||||||
|
return oldItem.getRecipient().equals(newItem.getRecipient());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(@NonNull CallParticipant oldItem, @NonNull CallParticipant newItem) {
|
||||||
|
return oldItem.equals(newItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -5,7 +5,7 @@ import android.util.AttributeSet;
|
|||||||
import android.view.LayoutInflater;
|
import android.view.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.webrtc.participantslist;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder;
|
||||||
|
|
||||||
|
public class CallParticipantViewHolder extends RecipientViewHolder<CallParticipantViewState> {
|
||||||
|
public CallParticipantViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bind(@NonNull CallParticipantViewState model) {
|
||||||
|
super.bind(model);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.webrtc.participantslist;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.util.viewholders.RecipientMappingModel;
|
||||||
|
|
||||||
|
public final class CallParticipantViewState extends RecipientMappingModel<CallParticipantViewState> {
|
||||||
|
|
||||||
|
private final CallParticipant callParticipant;
|
||||||
|
|
||||||
|
CallParticipantViewState(@NonNull CallParticipant callParticipant) {
|
||||||
|
this.callParticipant = callParticipant;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull Recipient getRecipient() {
|
||||||
|
return callParticipant.getRecipient();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.webrtc.participantslist;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||||
|
|
||||||
|
public class CallParticipantsListAdapter extends MappingAdapter {
|
||||||
|
|
||||||
|
CallParticipantsListAdapter() {
|
||||||
|
registerFactory(CallParticipantsListHeader.class, new LayoutFactory<>(CallParticipantsListHeaderViewHolder::new, R.layout.call_participants_list_header));
|
||||||
|
registerFactory(CallParticipantViewState.class, new LayoutFactory<>(CallParticipantViewHolder::new, R.layout.call_participants_list_item));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.webrtc.participantslist;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.ContextThemeWrapper;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
|
||||||
|
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
|
||||||
|
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||||
|
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.MappingModel;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class CallParticipantsListDialog extends BottomSheetDialogFragment {
|
||||||
|
|
||||||
|
private RecyclerView participantList;
|
||||||
|
private CallParticipantsListAdapter adapter;
|
||||||
|
|
||||||
|
public static void show(@NonNull FragmentManager manager) {
|
||||||
|
CallParticipantsListDialog fragment = new CallParticipantsListDialog();
|
||||||
|
|
||||||
|
fragment.show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
|
||||||
|
BottomSheetUtil.show(manager, tag, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_RoundedBottomSheet);
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
ContextThemeWrapper contextThemeWrapper = new ContextThemeWrapper(inflater.getContext(), R.style.TextSecure_DarkTheme);
|
||||||
|
LayoutInflater themedInflater = LayoutInflater.from(contextThemeWrapper);
|
||||||
|
|
||||||
|
participantList = (RecyclerView) themedInflater.inflate(R.layout.call_participants_list_dialog, container, false);
|
||||||
|
|
||||||
|
return participantList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onActivityCreated(savedInstanceState);
|
||||||
|
|
||||||
|
final WebRtcCallViewModel viewModel = ViewModelProviders.of(requireActivity()).get(WebRtcCallViewModel.class);
|
||||||
|
|
||||||
|
initializeList();
|
||||||
|
|
||||||
|
viewModel.getCallParticipantsState().observe(getViewLifecycleOwner(), this::updateList);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeList() {
|
||||||
|
adapter = new CallParticipantsListAdapter();
|
||||||
|
|
||||||
|
participantList.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||||
|
participantList.setAdapter(adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateList(@NonNull CallParticipantsState callParticipantsState) {
|
||||||
|
List<MappingModel<?>> items = new ArrayList<>();
|
||||||
|
|
||||||
|
items.add(new CallParticipantsListHeader(callParticipantsState.getAllRemoteParticipants().size() + 1));
|
||||||
|
|
||||||
|
items.add(new CallParticipantViewState(callParticipantsState.getLocalParticipant()));
|
||||||
|
for (CallParticipant callParticipant : callParticipantsState.getAllRemoteParticipants()) {
|
||||||
|
items.add(new CallParticipantViewState(callParticipant));
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.submitList(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.webrtc.participantslist;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.util.MappingModel;
|
||||||
|
|
||||||
|
public class CallParticipantsListHeader implements MappingModel<CallParticipantsListHeader> {
|
||||||
|
|
||||||
|
private int participantCount;
|
||||||
|
|
||||||
|
public CallParticipantsListHeader(int participantCount) {
|
||||||
|
this.participantCount = participantCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull String getHeader(@NonNull Context context) {
|
||||||
|
return context.getResources().getQuantityString(R.plurals.CallParticipantsListDialog_in_this_call_d_people, participantCount, participantCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areItemsTheSame(@NonNull CallParticipantsListHeader newItem) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(@NonNull CallParticipantsListHeader newItem) {
|
||||||
|
return participantCount == newItem.participantCount;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.webrtc.participantslist;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||||
|
|
||||||
|
public class CallParticipantsListHeaderViewHolder extends MappingViewHolder<CallParticipantsListHeader> {
|
||||||
|
|
||||||
|
private final TextView headerText;
|
||||||
|
|
||||||
|
public CallParticipantsListHeaderViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
headerText = findViewById(R.id.call_participants_list_header);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bind(@NonNull CallParticipantsListHeader model) {
|
||||||
|
headerText.setText(model.getHeader(getContext()));
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,8 @@ import android.graphics.drawable.LayerDrawable;
|
|||||||
import android.widget.ImageView;
|
import 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.ui.mentions;
|
|
||||||
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
|
||||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
||||||
import org.thoughtcrime.securesms.util.MappingAdapter;
|
|
||||||
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
|
||||||
|
|
||||||
public class MentionViewHolder extends MappingViewHolder<MentionViewState> {
|
|
||||||
|
|
||||||
private final AvatarImageView avatar;
|
|
||||||
private final TextView name;
|
|
||||||
|
|
||||||
@Nullable private final MentionEventsListener mentionEventsListener;
|
|
||||||
|
|
||||||
public MentionViewHolder(@NonNull View itemView, @Nullable MentionEventsListener mentionEventsListener) {
|
|
||||||
super(itemView);
|
|
||||||
this.mentionEventsListener = mentionEventsListener;
|
|
||||||
|
|
||||||
avatar = findViewById(R.id.mention_recipient_avatar);
|
|
||||||
name = findViewById(R.id.mention_recipient_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void bind(@NonNull MentionViewState model) {
|
|
||||||
avatar.setRecipient(model.getRecipient());
|
|
||||||
name.setText(model.getName(context));
|
|
||||||
itemView.setOnClickListener(v -> {
|
|
||||||
if (mentionEventsListener != null) {
|
|
||||||
mentionEventsListener.onMentionClicked(model.getRecipient());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface MentionEventsListener {
|
|
||||||
void onMentionClicked(@NonNull Recipient recipient);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MappingAdapter.Factory<MentionViewState> createFactory(@Nullable MentionEventsListener mentionEventsListener) {
|
|
||||||
return new MappingAdapter.LayoutFactory<>(view -> new MentionViewHolder(view, mentionEventsListener), R.layout.mentions_picker_recipient_list_item);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +1,11 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.ui.mentions;
|
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -0,0 +1,117 @@
|
|||||||
|
package org.thoughtcrime.securesms.events;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||||
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class CallParticipant {
|
||||||
|
|
||||||
|
private final @NonNull CameraState cameraState;
|
||||||
|
private final @NonNull Recipient recipient;
|
||||||
|
private final @Nullable IdentityKey identityKey;
|
||||||
|
private final @NonNull BroadcastVideoSink videoSink;
|
||||||
|
private final boolean videoEnabled;
|
||||||
|
private final boolean microphoneEnabled;
|
||||||
|
|
||||||
|
public static @NonNull CallParticipant createLocal(@NonNull CameraState cameraState,
|
||||||
|
@NonNull BroadcastVideoSink renderer,
|
||||||
|
boolean microphoneEnabled)
|
||||||
|
{
|
||||||
|
return new CallParticipant(Recipient.self(),
|
||||||
|
null,
|
||||||
|
renderer,
|
||||||
|
cameraState,
|
||||||
|
cameraState.isEnabled() && cameraState.getCameraCount() > 0,
|
||||||
|
microphoneEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NonNull CallParticipant createRemote(@NonNull Recipient recipient,
|
||||||
|
@Nullable IdentityKey identityKey,
|
||||||
|
@NonNull BroadcastVideoSink renderer,
|
||||||
|
boolean videoEnabled)
|
||||||
|
{
|
||||||
|
return new CallParticipant(recipient, identityKey, renderer, CameraState.UNKNOWN, videoEnabled, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CallParticipant(@NonNull Recipient recipient,
|
||||||
|
@Nullable IdentityKey identityKey,
|
||||||
|
@NonNull BroadcastVideoSink videoSink,
|
||||||
|
@NonNull CameraState cameraState,
|
||||||
|
boolean videoEnabled,
|
||||||
|
boolean microphoneEnabled)
|
||||||
|
{
|
||||||
|
this.recipient = recipient;
|
||||||
|
this.identityKey = identityKey;
|
||||||
|
this.videoSink = videoSink;
|
||||||
|
this.cameraState = cameraState;
|
||||||
|
this.videoEnabled = videoEnabled;
|
||||||
|
this.microphoneEnabled = microphoneEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull CallParticipant withIdentityKey(@NonNull IdentityKey identityKey) {
|
||||||
|
return new CallParticipant(recipient, identityKey, videoSink, cameraState, videoEnabled, microphoneEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull CallParticipant withVideoEnabled(boolean videoEnabled) {
|
||||||
|
return new CallParticipant(recipient, identityKey, videoSink, cameraState, videoEnabled, microphoneEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull Recipient getRecipient() {
|
||||||
|
return recipient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable IdentityKey getIdentityKey() {
|
||||||
|
return identityKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull BroadcastVideoSink getVideoSink() {
|
||||||
|
return videoSink;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull CameraState getCameraState() {
|
||||||
|
return cameraState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isVideoEnabled() {
|
||||||
|
return videoEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMicrophoneEnabled() {
|
||||||
|
return microphoneEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull CameraState.Direction getCameraDirection() {
|
||||||
|
if (cameraState.getActiveDirection() == CameraState.Direction.BACK) {
|
||||||
|
return cameraState.getActiveDirection();
|
||||||
|
}
|
||||||
|
return CameraState.Direction.FRONT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMoreThanOneCameraAvailable() {
|
||||||
|
return cameraState.getCameraCount() > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
CallParticipant that = (CallParticipant) o;
|
||||||
|
return videoEnabled == that.videoEnabled &&
|
||||||
|
microphoneEnabled == that.microphoneEnabled &&
|
||||||
|
cameraState.equals(that.cameraState) &&
|
||||||
|
recipient.equals(that.recipient) &&
|
||||||
|
Objects.equals(identityKey, that.identityKey) &&
|
||||||
|
Objects.equals(videoSink, that.videoSink);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(cameraState, recipient, identityKey, videoSink, videoEnabled, microphoneEnabled);
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,14 @@
|
|||||||
package org.thoughtcrime.securesms.events;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
package org.thoughtcrime.securesms.util.viewholders;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.util.MappingModel;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public abstract class RecipientMappingModel<T extends RecipientMappingModel<T>> implements MappingModel<T> {
|
||||||
|
|
||||||
|
public abstract @NonNull Recipient getRecipient();
|
||||||
|
|
||||||
|
public @NonNull String getName(@NonNull Context context) {
|
||||||
|
return getRecipient().getDisplayName(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areItemsTheSame(@NonNull T newItem) {
|
||||||
|
return getRecipient().getId().equals(newItem.getRecipient().getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(@NonNull T newItem) {
|
||||||
|
Context context = ApplicationDependencies.getApplication();
|
||||||
|
return getName(context).equals(newItem.getName(context)) && Objects.equals(getRecipient().getContactPhoto(), newItem.getRecipient().getContactPhoto());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
package org.thoughtcrime.securesms.util.viewholders;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.LayoutRes;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||||
|
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||||
|
|
||||||
|
public class RecipientViewHolder<T extends RecipientMappingModel<T>> extends MappingViewHolder<T> {
|
||||||
|
|
||||||
|
protected final @Nullable AvatarImageView avatar;
|
||||||
|
protected final @Nullable TextView name;
|
||||||
|
protected final @Nullable EventListener<T> eventListener;
|
||||||
|
|
||||||
|
public RecipientViewHolder(@NonNull View itemView, @Nullable EventListener<T> eventListener) {
|
||||||
|
super(itemView);
|
||||||
|
this.eventListener = eventListener;
|
||||||
|
|
||||||
|
avatar = findViewById(R.id.recipient_view_avatar);
|
||||||
|
name = findViewById(R.id.recipient_view_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bind(@NonNull T model) {
|
||||||
|
if (avatar != null) {
|
||||||
|
avatar.setRecipient(model.getRecipient());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name != null) {
|
||||||
|
name.setText(model.getName(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventListener != null) {
|
||||||
|
itemView.setOnClickListener(v -> eventListener.onModelClick(model));
|
||||||
|
} else {
|
||||||
|
itemView.setOnClickListener(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NonNull <T extends RecipientMappingModel<T>> MappingAdapter.Factory<T> createFactory(@LayoutRes int layout, @Nullable EventListener<T> listener) {
|
||||||
|
return new MappingAdapter.LayoutFactory<>(view -> new RecipientViewHolder<>(view, listener), layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface EventListener<T extends RecipientMappingModel<T>> {
|
||||||
|
default void onModelClick(@NonNull T model) {
|
||||||
|
onClick(model.getRecipient());
|
||||||
|
}
|
||||||
|
|
||||||
|
void onClick(@NonNull Recipient recipient);
|
||||||
|
}
|
||||||
|
}
|
43
app/src/main/res/layout/call_participant_item.xml
Normal file
43
app/src/main/res/layout/call_participant_item.xml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<org.thoughtcrime.securesms.components.webrtc.CallParticipantView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:layout_height="match_parent"
|
||||||
|
tools:layout_width="match_parent">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||||
|
android:id="@+id/call_participant_item_avatar"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintDimensionRatio="h,1:1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintWidth_percent="0.5"
|
||||||
|
tools:srcCompat="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/call_participant_item_pip_avatar"
|
||||||
|
android:layout_width="200dp"
|
||||||
|
android:layout_height="200dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:srcCompat="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer
|
||||||
|
android:id="@+id/call_participant_renderer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</org.thoughtcrime.securesms.components.webrtc.CallParticipantView>
|
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
12
app/src/main/res/layout/call_participants_list_header.xml
Normal file
12
app/src/main/res/layout/call_participants_list_header.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/call_participants_list_header"
|
||||||
|
style="@style/TextAppearance.Signal.Body2.Bold"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
tools:text="In this call · 16 people" />
|
34
app/src/main/res/layout/call_participants_list_item.xml
Normal file
34
app/src/main/res/layout/call_participants_list_item.xml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?selectableItemBackground"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:paddingBottom="8dp">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||||
|
android:id="@+id/recipient_view_avatar"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
app:fallbackImageSize="small"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/recipient_view_name"
|
||||||
|
style="@style/TextAppearance.Signal.Body1"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:textAppearance="@style/Signal.Text.Preview"
|
||||||
|
tools:text="@tools:sample/full_names" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -12,14 +12,14 @@
|
|||||||
android:paddingBottom="8dp">
|
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"
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:background="@null"
|
||||||
|
android:clipChildren="true"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
tools:background="@color/red"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/call_participant"
|
||||||
|
layout="@layout/call_participant_item"
|
||||||
|
android:layout_width="72dp"
|
||||||
|
android:layout_height="72dp" />
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<org.thoughtcrime.securesms.components.webrtc.CallParticipantsLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/call_screen_call_participants"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:alignContent="stretch"
|
||||||
|
app:flexDirection="row"
|
||||||
|
app:flexWrap="wrap" />
|
@ -10,31 +10,47 @@
|
|||||||
android:layout_height="match_parent"
|
android: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>
|
47
app/src/main/res/layout/webrtc_call_view_toolbar.xml
Normal file
47
app/src/main/res/layout/webrtc_call_view_toolbar.xml
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:minWidth="160dp">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/action_bar_guideline"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintGuide_begin="?actionBarSize" />
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||||
|
android:id="@+id/call_screen_recipient_name"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold"
|
||||||
|
android:textColor="@color/core_white"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/action_bar_guideline"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Kiera Thompson" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/call_screen_status"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Caption"
|
||||||
|
android:textColor="@color/core_white"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/call_screen_recipient_name"
|
||||||
|
tools:text="Signal Calling..." />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.Toolbar>
|
14
app/src/main/res/menu/group_call.xml
Normal file
14
app/src/main/res/menu/group_call.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_group_call_participants_list"
|
||||||
|
android:icon="@drawable/ic_group_solid_24"
|
||||||
|
android:title="@string/WebRtcCallView__view_participants_list"
|
||||||
|
app:iconTint="@color/white"
|
||||||
|
app:showAsAction="always"
|
||||||
|
tools:ignore="AlwaysShowAction" />
|
||||||
|
|
||||||
|
</menu>
|
@ -1199,10 +1199,20 @@
|
|||||||
<string name="WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera">To call %1$s, Signal needs access to your camera</string>
|
<string name="WebRtcCallActivity__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>
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user