Add pre-join vanity view for 1:1 video calls.

master
Cody Henthorne 2020-09-18 15:51:17 -04:00 committed by Greyson Parrelli
parent cd2467085e
commit a8415a3484
15 changed files with 353 additions and 167 deletions

View File

@ -19,8 +19,6 @@ package org.thoughtcrime.securesms;
import android.Manifest;
import android.app.PictureInPictureParams;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
@ -42,10 +40,10 @@ import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
@ -57,7 +55,6 @@ import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
@ -89,6 +86,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.webrtc_call_activity);
//noinspection ConstantConditions
getSupportActionBar().hide();
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
@ -136,11 +134,13 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
super.onStop();
EventBus.getDefault().unregister(this);
}
@Override
public void onConfigurationChanged(Configuration newConfiguration) {
super.onConfigurationChanged(newConfiguration);
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_CANCEL_PRE_JOIN_CALL);
startService(intent);
}
}
@Override
@ -200,7 +200,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
}
private void initializeResources() {
callScreen = ViewUtil.findById(this, R.id.callScreen);
callScreen = findViewById(R.id.callScreen);
callScreen.setControlsListener(new ControlsListener());
}
@ -376,7 +376,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
startService(intent);
}
private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
private void handleOutgoingCall() {
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling));
}
@ -393,27 +393,27 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
delayedFinish();
}
private void handleCallRinging(@NonNull WebRtcViewModel event) {
private void handleCallRinging() {
callScreen.setStatus(getString(R.string.RedPhone_ringing));
}
private void handleCallBusy(@NonNull WebRtcViewModel event) {
private void handleCallBusy() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setStatus(getString(R.string.RedPhone_busy));
delayedFinish(WebRtcCallService.BUSY_TONE_LENGTH);
}
private void handleCallConnected(@NonNull WebRtcViewModel event) {
private void handleCallConnected() {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
}
private void handleRecipientUnavailable(@NonNull WebRtcViewModel event) {
private void handleRecipientUnavailable() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable));
delayedFinish();
}
private void handleServerFailure(@NonNull WebRtcViewModel event) {
private void handleServerFailure() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setStatus(getString(R.string.RedPhone_network_failed));
delayedFinish();
@ -421,24 +421,14 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
private void handleNoSuchUser(final @NonNull WebRtcViewModel event) {
if (isFinishing()) return; // XXX Stuart added this check above, not sure why, so I'm repeating in ignorance. - moxie
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setTitle(R.string.RedPhone_number_not_registered);
dialog.setIconAttribute(R.attr.dialog_alert_icon);
dialog.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice);
dialog.setCancelable(true);
dialog.setPositiveButton(R.string.RedPhone_got_it, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
WebRtcCallActivity.this.handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL);
}
});
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
WebRtcCallActivity.this.handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL);
}
});
dialog.show();
new AlertDialog.Builder(this)
.setTitle(R.string.RedPhone_number_not_registered)
.setIconAttribute(R.attr.dialog_alert_icon)
.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
.setCancelable(true)
.setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
.setOnCancelListener(d -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
.show();
}
private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
@ -490,18 +480,18 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
callScreen.setRecipient(event.getRecipient());
switch (event.getState()) {
case CALL_CONNECTED: handleCallConnected(event); break;
case NETWORK_FAILURE: handleServerFailure(event); break;
case CALL_RINGING: handleCallRinging(event); break;
case CALL_CONNECTED: handleCallConnected(); break;
case NETWORK_FAILURE: handleServerFailure(); break;
case CALL_RINGING: handleCallRinging(); break;
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
case CALL_ACCEPTED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
case CALL_DECLINED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break;
case CALL_ONGOING_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break;
case CALL_NEEDS_PERMISSION: handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break;
case NO_SUCH_USER: handleNoSuchUser(event); break;
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(event); break;
case CALL_OUTGOING: handleOutgoingCall(event); break;
case CALL_BUSY: handleCallBusy(event); break;
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(); break;
case CALL_OUTGOING: handleOutgoingCall(); break;
case CALL_BUSY: handleCallBusy(); break;
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
}
@ -518,12 +508,14 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
private final class ControlsListener implements WebRtcCallView.ControlsListener {
@Override
public void onStartCall() {
public void onStartCall(boolean isVideoCall) {
enableVideoIfAvailable = isVideoCall;
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);
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, (isVideoCall ? OfferMessage.Type.VIDEO_CALL : OfferMessage.Type.AUDIO_CALL).getCode());
startService(intent);
MessageSender.onMessageSent();
}

View File

@ -57,6 +57,10 @@ public final class CallParticipantsState {
this.isViewingFocusedParticipant = isViewingFocusedParticipant;
}
public @NonNull WebRtcViewModel.State getCallState() {
return callState;
}
public @NonNull List<CallParticipant> getGridParticipants() {
if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) {
return getAllRemoteParticipants().subList(0, SMALL_GROUP_MAX);
@ -194,9 +198,11 @@ public final class CallParticipantsState {
} else {
localRenderState = WebRtcLocalRenderState.SMALL_RECTANGLE;
}
} else {
} else if (callState != WebRtcViewModel.State.CALL_DISCONNECTED) {
localRenderState = WebRtcLocalRenderState.LARGE;
}
} else if (callState == WebRtcViewModel.State.CALL_PRE_JOIN) {
localRenderState = WebRtcLocalRenderState.LARGE_NO_VIDEO;
}
return localRenderState;

View File

@ -70,12 +70,18 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
}
public void attachBroadcastVideoSink(@Nullable BroadcastVideoSink videoSink) {
if (attachedVideoSink == videoSink) {
return;
}
if (attachedVideoSink != null) {
attachedVideoSink.removeSink(this);
}
if (videoSink != null) {
videoSink.addSink(this);
} else {
clearImage();
}
attachedVideoSink = videoSink;
@ -232,9 +238,7 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
@Override
public void onFrame(VideoFrame videoFrame) {
if (isShown()) {
eglRenderer.onFrame(videoFrame);
}
eglRenderer.onFrame(videoFrame);
}
@Override

View File

@ -1,6 +1,8 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
@ -27,14 +29,20 @@ import androidx.transition.TransitionSet;
import androidx.viewpager2.widget.MarginPageTransformer;
import androidx.viewpager2.widget.ViewPager2;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.ResizeAnimation;
import org.thoughtcrime.securesms.components.AccessibleToggleButton;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.mediasend.SimpleAnimationListener;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.util.BlurTransformation;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.webrtc.RendererCommon;
@ -58,10 +66,12 @@ public class WebRtcCallView extends FrameLayout {
private WebRtcAudioOutputToggleButton audioToggle;
private AccessibleToggleButton videoToggle;
private AccessibleToggleButton micToggle;
private ViewGroup localRenderPipFrame;
private ViewGroup smallLocalRenderFrame;
private TextureViewRenderer smallLocalRender;
private View largeLocalRenderFrame;
private TextureViewRenderer largeLocalRender;
private View largeLocalRenderNoVideo;
private ImageView largeLocalRenderNoVideoAvatar;
private TextView recipientName;
private TextView status;
private ConstraintLayout parent;
@ -74,7 +84,7 @@ public class WebRtcCallView extends FrameLayout {
private ImageView hangup;
private View answerWithAudio;
private View answerWithAudioLabel;
private View ongoingFooterGradient;
private View footerGradient;
private View startCallControls;
private ViewPager2 callParticipantsPager;
private RecyclerView callParticipantsRecycler;
@ -110,33 +120,34 @@ public class WebRtcCallView extends FrameLayout {
protected void onFinishInflate() {
super.onFinishInflate();
audioToggle = findViewById(R.id.call_screen_speaker_toggle);
videoToggle = findViewById(R.id.call_screen_video_toggle);
micToggle = findViewById(R.id.call_screen_audio_mic_toggle);
localRenderPipFrame = findViewById(R.id.call_screen_pip);
smallLocalRender = findViewById(R.id.call_screen_small_local_renderer);
largeLocalRenderFrame = findViewById(R.id.call_screen_large_local_renderer_frame);
largeLocalRender = findViewById(R.id.call_screen_large_local_renderer);
recipientName = findViewById(R.id.call_screen_recipient_name);
status = findViewById(R.id.call_screen_status);
parent = findViewById(R.id.call_screen);
participantsParent = findViewById(R.id.call_screen_participants_parent);
answer = findViewById(R.id.call_screen_answer_call);
cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle);
hangup = findViewById(R.id.call_screen_end_call);
answerWithAudio = findViewById(R.id.call_screen_answer_with_audio);
answerWithAudioLabel = findViewById(R.id.call_screen_answer_with_audio_label);
ongoingFooterGradient = findViewById(R.id.call_screen_ongoing_footer_gradient);
startCallControls = findViewById(R.id.call_screen_start_call_controls);
callParticipantsPager = findViewById(R.id.call_screen_participants_pager);
callParticipantsRecycler = findViewById(R.id.call_screen_participants_recycler);
toolbar = findViewById(R.id.call_screen_toolbar);
audioToggle = findViewById(R.id.call_screen_speaker_toggle);
videoToggle = findViewById(R.id.call_screen_video_toggle);
micToggle = findViewById(R.id.call_screen_audio_mic_toggle);
smallLocalRenderFrame = findViewById(R.id.call_screen_pip);
smallLocalRender = findViewById(R.id.call_screen_small_local_renderer);
largeLocalRenderFrame = findViewById(R.id.call_screen_large_local_renderer_frame);
largeLocalRender = findViewById(R.id.call_screen_large_local_renderer);
largeLocalRenderNoVideo = findViewById(R.id.call_screen_large_local_video_off);
largeLocalRenderNoVideoAvatar = findViewById(R.id.call_screen_large_local_video_off_avatar);
recipientName = findViewById(R.id.call_screen_recipient_name);
status = findViewById(R.id.call_screen_status);
parent = findViewById(R.id.call_screen);
participantsParent = findViewById(R.id.call_screen_participants_parent);
answer = findViewById(R.id.call_screen_answer_call);
cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle);
hangup = findViewById(R.id.call_screen_end_call);
answerWithAudio = findViewById(R.id.call_screen_answer_with_audio);
answerWithAudioLabel = findViewById(R.id.call_screen_answer_with_audio_label);
footerGradient = findViewById(R.id.call_screen_footer_gradient);
startCallControls = findViewById(R.id.call_screen_start_call_controls);
callParticipantsPager = findViewById(R.id.call_screen_participants_pager);
callParticipantsRecycler = findViewById(R.id.call_screen_participants_recycler);
toolbar = findViewById(R.id.call_screen_toolbar);
View topGradient = findViewById(R.id.call_screen_header_gradient);
View decline = findViewById(R.id.call_screen_decline_call);
View answerLabel = findViewById(R.id.call_screen_answer_call_label);
View declineLabel = findViewById(R.id.call_screen_decline_call_label);
View incomingFooterGradient = findViewById(R.id.call_screen_incoming_footer_gradient);
Guideline statusBarGuideline = findViewById(R.id.call_screen_status_bar_guideline);
View startCall = findViewById(R.id.call_screen_start_call_start_call);
View cancelStartCall = findViewById(R.id.call_screen_start_call_cancel);
@ -163,7 +174,7 @@ public class WebRtcCallView extends FrameLayout {
incomingCallViews.add(answerLabel);
incomingCallViews.add(decline);
incomingCallViews.add(declineLabel);
incomingCallViews.add(incomingFooterGradient);
incomingCallViews.add(footerGradient);
adjustableMarginsSet.add(micToggle);
adjustableMarginsSet.add(cameraDirectionToggle);
@ -190,11 +201,16 @@ public class WebRtcCallView extends FrameLayout {
answer.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallPressed));
answerWithAudio.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallWithVoiceOnlyPressed));
pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(localRenderPipFrame);
pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(smallLocalRenderFrame);
startCall.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onStartCall));
startCall.setOnClickListener(v -> runIfNonNull(controlsListener, listener -> listener.onStartCall(videoToggle.isChecked())));
cancelStartCall.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onCancelStartCall));
ColorMatrix greyScaleMatrix = new ColorMatrix();
greyScaleMatrix.setSaturation(0);
largeLocalRenderNoVideoAvatar.setAlpha(0.6f);
largeLocalRenderNoVideoAvatar.setColorFilter(new ColorMatrixColorFilter(greyScaleMatrix));
int statusBarHeight = ViewUtil.getStatusBarHeight(this);
statusBarGuideline.setGuidelineBegin(statusBarHeight);
}
@ -245,8 +261,6 @@ public class WebRtcCallView extends FrameLayout {
}
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant) {
videoToggle.setChecked(state != WebRtcLocalRenderState.GONE, false);
smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
largeLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
@ -258,27 +272,65 @@ public class WebRtcCallView extends FrameLayout {
largeLocalRender.init(localCallParticipant.getVideoSink().getEglBase());
}
smallLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
largeLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
switch (state) {
case LARGE:
largeLocalRenderFrame.setVisibility(View.VISIBLE);
localRenderPipFrame.setVisibility(View.GONE);
break;
case GONE:
largeLocalRender.attachBroadcastVideoSink(null);
largeLocalRenderFrame.setVisibility(View.GONE);
localRenderPipFrame.setVisibility(View.GONE);
smallLocalRender.attachBroadcastVideoSink(null);
smallLocalRenderFrame.setVisibility(View.GONE);
videoToggle.setChecked(false, false);
break;
case SMALL_RECTANGLE:
largeLocalRenderFrame.setVisibility(View.GONE);
localRenderPipFrame.setVisibility(View.VISIBLE);
smallLocalRenderFrame.setVisibility(View.VISIBLE);
smallLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
animatePipToRectangle();
largeLocalRender.attachBroadcastVideoSink(null);
largeLocalRenderFrame.setVisibility(View.GONE);
videoToggle.setChecked(true, false);
break;
case SMALL_SQUARE:
largeLocalRenderFrame.setVisibility(View.GONE);
localRenderPipFrame.setVisibility(View.VISIBLE);
smallLocalRenderFrame.setVisibility(View.VISIBLE);
smallLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
animatePipToSquare();
largeLocalRender.attachBroadcastVideoSink(null);
largeLocalRenderFrame.setVisibility(View.GONE);
videoToggle.setChecked(true, false);
break;
case LARGE:
largeLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
largeLocalRenderFrame.setVisibility(View.VISIBLE);
largeLocalRenderNoVideo.setVisibility(View.GONE);
largeLocalRenderNoVideoAvatar.setVisibility(View.GONE);
smallLocalRender.attachBroadcastVideoSink(null);
smallLocalRenderFrame.setVisibility(View.GONE);
videoToggle.setChecked(true, false);
break;
case LARGE_NO_VIDEO:
largeLocalRender.attachBroadcastVideoSink(null);
largeLocalRenderFrame.setVisibility(View.VISIBLE);
largeLocalRenderNoVideo.setVisibility(View.VISIBLE);
largeLocalRenderNoVideoAvatar.setVisibility(View.VISIBLE);
GlideApp.with(getContext().getApplicationContext())
.load(new ProfileContactPhoto(localCallParticipant.getRecipient(), localCallParticipant.getRecipient().getProfileAvatar()))
.transform(new CenterCrop(), new BlurTransformation(getContext(), 0.25f, BlurTransformation.MAX_RADIUS))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(largeLocalRenderNoVideoAvatar);
smallLocalRender.attachBroadcastVideoSink(null);
smallLocalRenderFrame.setVisibility(View.GONE);
videoToggle.setChecked(false, false);
break;
}
}
@ -330,6 +382,7 @@ public class WebRtcCallView extends FrameLayout {
visibleViewSet.clear();
if (webRtcControls.displayStartCallControls()) {
visibleViewSet.add(footerGradient);
visibleViewSet.add(startCallControls);
}
@ -367,7 +420,7 @@ public class WebRtcCallView extends FrameLayout {
if (webRtcControls.displayEndCall()) {
visibleViewSet.add(hangup);
visibleViewSet.add(ongoingFooterGradient);
visibleViewSet.add(footerGradient);
}
if (webRtcControls.displayMuteAudio()) {
@ -411,7 +464,7 @@ public class WebRtcCallView extends FrameLayout {
}
private void animatePipToRectangle() {
ResizeAnimation animation = new ResizeAnimation(localRenderPipFrame, ViewUtil.dpToPx(90), ViewUtil.dpToPx(160));
ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(90), ViewUtil.dpToPx(160));
animation.setDuration(PIP_RESIZE_DURATION);
animation.setAnimationListener(new SimpleAnimationListener() {
@Override
@ -421,14 +474,14 @@ public class WebRtcCallView extends FrameLayout {
}
});
localRenderPipFrame.startAnimation(animation);
smallLocalRenderFrame.startAnimation(animation);
}
private void animatePipToSquare() {
pictureInPictureGestureHelper.lockToBottomEnd();
pictureInPictureGestureHelper.performAfterFling(() -> {
ResizeAnimation animation = new ResizeAnimation(localRenderPipFrame, ViewUtil.dpToPx(72), ViewUtil.dpToPx(72));
ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(72), ViewUtil.dpToPx(72));
animation.setDuration(PIP_RESIZE_DURATION);
animation.setAnimationListener(new SimpleAnimationListener() {
@Override
@ -437,7 +490,7 @@ public class WebRtcCallView extends FrameLayout {
}
});
localRenderPipFrame.startAnimation(animation);
smallLocalRenderFrame.startAnimation(animation);
});
}
@ -580,7 +633,7 @@ public class WebRtcCallView extends FrameLayout {
}
public interface ControlsListener {
void onStartCall();
void onStartCall(boolean isVideoCall);
void onCancelStartCall();
void onControlsFadeOut();
void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput);

View File

@ -94,7 +94,7 @@ public class WebRtcCallViewModel extends ViewModel {
@MainThread
public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel, boolean enableVideo) {
canEnterPipMode = true;
canEnterPipMode = webRtcViewModel.getState() != WebRtcViewModel.State.CALL_PRE_JOIN;
CallParticipant localParticipant = webRtcViewModel.getLocalParticipant();
@ -143,6 +143,9 @@ public class WebRtcCallViewModel extends ViewModel {
final WebRtcControls.CallState callState;
switch (state) {
case CALL_PRE_JOIN:
callState = WebRtcControls.CallState.PRE_JOIN;
break;
case CALL_INCOMING:
callState = WebRtcControls.CallState.INCOMING;
answerWithVideoAvailable = isRemoteVideoOffer;
@ -167,7 +170,7 @@ public class WebRtcCallViewModel extends ViewModel {
isRemoteVideoEnabled || isRemoteVideoOffer,
isMoreThanOneCameraAvailable,
isBluetoothAvailable,
isInPipMode.getValue() == Boolean.TRUE,
Boolean.TRUE.equals(isInPipMode.getValue()),
callState,
audioOutput));
}

View File

@ -37,7 +37,7 @@ public final class WebRtcControls {
}
boolean displayStartCallControls() {
return false;
return isPreJoin();
}
boolean displayEndCall() {
@ -45,19 +45,19 @@ public final class WebRtcControls {
}
boolean displayMuteAudio() {
return isAtLeastOutgoing();
return isPreJoin() || isAtLeastOutgoing();
}
boolean displayVideoToggle() {
return isAtLeastOutgoing();
return isPreJoin() || isAtLeastOutgoing();
}
boolean displayAudioToggle() {
return isAtLeastOutgoing() && (!isLocalVideoEnabled || isBluetoothAvailable);
return (isPreJoin() || isAtLeastOutgoing()) && (!isLocalVideoEnabled || isBluetoothAvailable);
}
boolean displayCameraToggle() {
return isAtLeastOutgoing() && isLocalVideoEnabled && isMoreThanOneCameraAvailable;
return (isPreJoin() || isAtLeastOutgoing()) && isLocalVideoEnabled && isMoreThanOneCameraAvailable;
}
boolean displayRemoteVideoRecycler() {
@ -100,6 +100,10 @@ public final class WebRtcControls {
return audioOutput;
}
private boolean isPreJoin() {
return callState == CallState.PRE_JOIN;
}
private boolean isOngoing() {
return callState == CallState.ONGOING;
}
@ -114,6 +118,7 @@ public final class WebRtcControls {
public enum CallState {
NONE,
PRE_JOIN,
INCOMING,
OUTGOING,
ONGOING,

View File

@ -4,5 +4,6 @@ public enum WebRtcLocalRenderState {
GONE,
SMALL_RECTANGLE,
SMALL_SQUARE,
LARGE
LARGE,
LARGE_NO_VIDEO
}

View File

@ -14,6 +14,7 @@ public class WebRtcViewModel {
public enum State {
// Normal states
CALL_PRE_JOIN,
CALL_INCOMING,
CALL_OUTGOING,
CALL_CONNECTED,

View File

@ -6,19 +6,14 @@ import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import java.io.IOException;
import java.util.List;
import java.util.LinkedList;
import org.signal.ringrtc.CameraControl;
import org.thoughtcrime.securesms.logging.Log;
import org.webrtc.Camera1Enumerator;
import org.webrtc.Camera2Capturer;
import org.webrtc.Camera2Enumerator;
@ -28,6 +23,9 @@ import org.webrtc.CapturerObserver;
import org.webrtc.EglBase;
import org.webrtc.SurfaceTextureHelper;
import java.util.LinkedList;
import java.util.List;
import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.BACK;
import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.FRONT;
import static org.thoughtcrime.securesms.ringrtc.CameraState.Direction.NONE;
@ -48,9 +46,10 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
@NonNull private CameraState.Direction activeDirection;
private boolean enabled;
public Camera(@NonNull Context context,
public Camera(@NonNull Context context,
@NonNull CameraEventListener cameraEventListener,
@NonNull EglBase eglBase)
@NonNull EglBase eglBase,
@NonNull CameraState.Direction desiredCameraDirection)
{
this.context = context;
this.cameraEventListener = cameraEventListener;
@ -58,13 +57,16 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
CameraEnumerator enumerator = getCameraEnumerator(context);
cameraCount = enumerator.getDeviceNames().length;
CameraVideoCapturer capturerCandidate = createVideoCapturer(enumerator, FRONT);
CameraState.Direction firstChoice = desiredCameraDirection.isUsable() ? desiredCameraDirection : FRONT;
CameraVideoCapturer capturerCandidate = createVideoCapturer(enumerator, firstChoice);
if (capturerCandidate != null) {
activeDirection = FRONT;
activeDirection = firstChoice;
} else {
capturerCandidate = createVideoCapturer(enumerator, BACK);
CameraState.Direction secondChoice = firstChoice.switchDirection();
capturerCandidate = createVideoCapturer(enumerator, secondChoice);
if (capturerCandidate != null) {
activeDirection = BACK;
activeDirection = secondChoice;
} else {
activeDirection = NONE;
}

View File

@ -32,6 +32,21 @@ public class CameraState {
}
public enum Direction {
FRONT, BACK, NONE, PENDING
FRONT, BACK, NONE, PENDING;
public boolean isUsable() {
return this == FRONT || this == BACK;
}
public Direction switchDirection() {
switch (this) {
case FRONT:
return BACK;
case BACK:
return FRONT;
default:
return this;
}
}
}
}

View File

@ -57,13 +57,15 @@ import org.thoughtcrime.securesms.webrtc.audio.BluetoothStateManager;
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.webrtc.CapturerObserver;
import org.webrtc.EglBase;
import org.webrtc.PeerConnection;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.util.Pair;
import org.webrtc.VideoFrame;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.DjbECPublicKey;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
@ -128,6 +130,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
public static final String EXTRA_BROADCAST = "broadcast";
public static final String EXTRA_ANSWER_WITH_VIDEO = "enable_video";
public static final String ACTION_PRE_JOIN_CALL = "CALL_PRE_JOIN";
public static final String ACTION_CANCEL_PRE_JOIN_CALL = "CANCEL_PRE_JOIN_CALL";
public static final String ACTION_OUTGOING_CALL = "CALL_OUTGOING";
public static final String ACTION_DENY_CALL = "DENY_CALL";
public static final String ACTION_LOCAL_HANGUP = "LOCAL_HANGUP";
@ -196,6 +200,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
@Nullable private CallManager callManager;
@Nullable private RemotePeer activePeer;
@Nullable private RemotePeer busyPeer;
@Nullable private RemotePeer preJoinPeer;
@Nullable private SparseArray<RemotePeer> peerMap;
@Nullable private EglBase eglBase;
@ -232,6 +237,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
serviceExecutor.execute(() -> {
if (intent.getAction().equals(ACTION_RECEIVE_OFFER)) handleReceivedOffer(intent);
else if (intent.getAction().equals(ACTION_RECEIVE_BUSY)) handleReceivedBusy(intent);
else if (intent.getAction().equals(ACTION_PRE_JOIN_CALL)) handlePreJoinCall(intent);
else if (intent.getAction().equals(ACTION_CANCEL_PRE_JOIN_CALL)) handleCancelPreJoinCall();
else if (intent.getAction().equals(ACTION_OUTGOING_CALL) && isIdle()) handleOutgoingCall(intent);
else if (intent.getAction().equals(ACTION_DENY_CALL)) handleDenyCall(intent);
else if (intent.getAction().equals(ACTION_LOCAL_HANGUP)) handleLocalHangup(intent);
@ -334,6 +341,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
localCameraState = newCameraState;
if (activePeer != null) {
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
} else if (preJoinPeer != null) {
sendMessage(WebRtcViewModel.State.CALL_PRE_JOIN, preJoinPeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
}
}
@ -447,6 +456,59 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
}
}
private void handlePreJoinCall(Intent intent) {
Log.i(TAG, "handlePreJoinCall():");
RemotePeer remotePeer = getRemotePeer(intent);
if (remotePeer.getState() != CallState.IDLE) {
throw new IllegalStateException("Dialing from non-idle?");
}
preJoinPeer = remotePeer;
initializeVideo();
localCameraState = initializeVanityCamera();
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
sendMessage(WebRtcViewModel.State.CALL_PRE_JOIN,
remotePeer,
localCameraState,
bluetoothAvailable,
microphoneEnabled,
false);
}
private @NonNull CameraState initializeVanityCamera() {
if (camera == null || localSink == null) {
return CameraState.UNKNOWN;
}
if (camera.hasCapturer()) {
camera.initCapturer(new CapturerObserver() {
@Override
public void onFrameCaptured(VideoFrame videoFrame) {
localSink.onFrame(videoFrame);
}
@Override
public void onCapturerStarted(boolean success) {}
@Override
public void onCapturerStopped() {}
});
camera.setEnabled(true);
}
return camera.getCameraState();
}
private void handleCancelPreJoinCall() {
cleanupVideo();
preJoinPeer = null;
}
private void handleOutgoingCall(Intent intent) {
Log.i(TAG, "handleOutgoingCall():");
@ -456,6 +518,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
throw new IllegalStateException("Dialing from non-idle?");
}
preJoinPeer = null;
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
peerMap.append(remotePeer.hashCode(), remotePeer);
@ -575,6 +639,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
localCameraState = camera.getCameraState();
if (activePeer != null) {
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
} else if (preJoinPeer != null) {
sendMessage(WebRtcViewModel.State.CALL_PRE_JOIN, preJoinPeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
}
}
}
@ -1016,7 +1082,15 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
AudioManager audioManager = ServiceUtil.getAudioManager(this);
if (activePeer == null) {
Log.w(TAG, "handleSetEnableVideo(): Ignoring for inactive call.");
if (preJoinPeer != null) {
Log.w(TAG, "handleSetEnableVideo(): Changing for pre-join call.");
camera.setEnabled(enable);
enableVideoOnCreate = enable;
localCameraState = camera.getCameraState();
sendMessage(WebRtcViewModel.State.CALL_PRE_JOIN, preJoinPeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
} else {
Log.w(TAG, "handleSetEnableVideo(): Ignoring for inactive call.");
}
return;
}
@ -1300,13 +1374,35 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
private void initializeVideo() {
Util.runOnMainSync(() -> {
eglBase = EglBase.create();
localSink = new BroadcastVideoSink(eglBase);
camera = new Camera(WebRtcCallService.this, WebRtcCallService.this, eglBase);
if (eglBase == null) {
eglBase = EglBase.create();
localSink = new BroadcastVideoSink(eglBase);
}
if (camera != null) {
camera.setEnabled(false);
camera.dispose();
}
camera = new Camera(WebRtcCallService.this, WebRtcCallService.this, eglBase, localCameraState.getActiveDirection());
localCameraState = camera.getCameraState();
});
}
private void cleanupVideo() {
if (camera != null) {
camera.dispose();
camera = null;
}
if (eglBase != null) {
eglBase.release();
eglBase = null;
}
localCameraState = CameraState.UNKNOWN;
}
private void setCallInProgressNotification(int type, RemotePeer remotePeer) {
startForeground(CallNotificationBuilder.getNotificationId(getApplicationContext(), type),
CallNotificationBuilder.getCallInProgressNotification(this, type, remotePeer.getRecipient()));
@ -1334,26 +1430,15 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
audioManager.stop(playDisconnectSound);
bluetoothStateManager.setWantsConnection(false);
if (camera != null) {
camera.dispose();
camera = null;
}
cleanupVideo();
if (eglBase != null) {
eglBase.release();
eglBase = null;
}
this.localCameraState = CameraState.UNKNOWN;
this.microphoneEnabled = true;
this.enableVideoOnCreate = false;
Log.i(TAG, "clear activePeer callId: " + activePeer.getCallId() + " key: " + activePeer.hashCode());
this.activePeer = null;
for (CallParticipant participant : remoteParticipantMap.values()) {
remoteParticipantMap.put(participant.getRecipient(), participant.withVideoEnabled(false));
}
remoteParticipantMap.clear();
lockManager.updatePhoneState(LockManager.PhoneState.IDLE);
}

View File

@ -81,16 +81,7 @@ public class CommunicationActions {
WebRtcCallService.isCallActive(activity, new ResultReceiver(new Handler(Looper.getMainLooper())) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
if (resultCode == 1) {
startCallInternal(activity, recipient, false);
} else {
new AlertDialog.Builder(activity)
.setMessage(R.string.CommunicationActions_start_video_call)
.setPositiveButton(R.string.CommunicationActions_call, (d, w) -> startCallInternal(activity, recipient, true))
.setNegativeButton(R.string.CommunicationActions_cancel, (d, w) -> d.dismiss())
.setCancelable(true)
.show();
}
startCallInternal(activity, recipient, resultCode != 1);
}
});
}
@ -268,13 +259,11 @@ public class CommunicationActions {
.withPermanentDenialDialog(activity.getString(R.string.ConversationActivity_signal_needs_the_microphone_and_camera_permissions_in_order_to_call_s, recipient.getDisplayName(activity)))
.onAllGranted(() -> {
Intent intent = new Intent(activity, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
intent.setAction(WebRtcCallService.ACTION_PRE_JOIN_CALL)
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(recipient.getId()))
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, OfferMessage.Type.VIDEO_CALL.getCode());
activity.startService(intent);
MessageSender.onMessageSent();
Intent activityIntent = new Intent(activity, WebRtcCallActivity.class);
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

View File

@ -27,18 +27,10 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout
android:id="@+id/call_screen_large_local_renderer_frame"
<include
layout="@layout/webrtc_call_view_large_local_render"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer
android:id="@+id/call_screen_large_local_renderer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
android:layout_height="match_parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/call_screen"
@ -61,24 +53,22 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/call_screen_ongoing_footer_gradient"
<Space
android:id="@+id/call_screen_footer_gradient_spacer"
android:layout_width="match_parent"
android:layout_height="160dp"
android:background="@drawable/webrtc_call_screen_header_gradient"
android:rotation="180"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
tools:visibility="gone" />
android:layout_height="30dp"
app:layout_constraintBottom_toBottomOf="@id/call_screen_footer_gradient_barrier"/>
<View
android:id="@+id/call_screen_incoming_footer_gradient"
android:id="@+id/call_screen_footer_gradient"
android:layout_width="match_parent"
android:layout_height="240dp"
android:layout_height="0dp"
android:background="@drawable/webrtc_call_screen_header_gradient"
android:rotation="180"
android:visibility="gone"
android:layout_margin="-40dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="@id/call_screen_footer_gradient_spacer"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView
@ -330,5 +320,12 @@
</LinearLayout>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/call_screen_footer_gradient_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
app:constraint_referenced_ids="call_screen_answer_call,call_screen_decline_call,call_screen_audio_mic_toggle,call_screen_camera_direction_toggle,call_screen_video_toggle,,call_screen_answer_with_audio,call_screen_speaker_toggle,call_screen_end_call" />
</androidx.constraintlayout.widget.ConstraintLayout>
</merge>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/call_screen_large_local_renderer_frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer
android:id="@+id/call_screen_large_local_renderer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/call_screen_large_local_video_off_avatar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/call_screen_large_local_video_off"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawableTop="@drawable/ic_video_off_solid_white_28"
android:text="@string/WebRtcCallView__your_video_is_off"
android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/white"
android:visibility="gone"
tools:visibility="visible"/>
</FrameLayout>

View File

@ -1207,6 +1207,7 @@
<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>
<string name="WebRtcCallView__your_video_is_off">Your video is off</string>
<!-- CallParticipantsListDialog -->
<plurals name="CallParticipantsListDialog_in_this_call_d_people">