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.Manifest;
import android.app.PictureInPictureParams; import android.app.PictureInPictureParams;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.Configuration; import android.content.res.Configuration;
@ -42,10 +40,10 @@ 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.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;
import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog; import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log; 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.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.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; import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
@ -89,6 +86,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.webrtc_call_activity); setContentView(R.layout.webrtc_call_activity);
//noinspection ConstantConditions
getSupportActionBar().hide(); getSupportActionBar().hide();
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL); setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
@ -136,11 +134,13 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
super.onStop(); super.onStop();
EventBus.getDefault().unregister(this); EventBus.getDefault().unregister(this);
}
@Override CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
public void onConfigurationChanged(Configuration newConfiguration) { if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
super.onConfigurationChanged(newConfiguration); Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_CANCEL_PRE_JOIN_CALL);
startService(intent);
}
} }
@Override @Override
@ -200,7 +200,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
} }
private void initializeResources() { private void initializeResources() {
callScreen = ViewUtil.findById(this, R.id.callScreen); callScreen = findViewById(R.id.callScreen);
callScreen.setControlsListener(new ControlsListener()); callScreen.setControlsListener(new ControlsListener());
} }
@ -376,7 +376,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
startService(intent); startService(intent);
} }
private void handleOutgoingCall(@NonNull WebRtcViewModel event) { private void handleOutgoingCall() {
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling)); callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling));
} }
@ -393,27 +393,27 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
delayedFinish(); delayedFinish();
} }
private void handleCallRinging(@NonNull WebRtcViewModel event) { private void handleCallRinging() {
callScreen.setStatus(getString(R.string.RedPhone_ringing)); callScreen.setStatus(getString(R.string.RedPhone_ringing));
} }
private void handleCallBusy(@NonNull WebRtcViewModel event) { private void handleCallBusy() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
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() {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES); getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
} }
private void handleRecipientUnavailable(@NonNull WebRtcViewModel event) { private void handleRecipientUnavailable() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
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() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setStatus(getString(R.string.RedPhone_network_failed)); callScreen.setStatus(getString(R.string.RedPhone_network_failed));
delayedFinish(); delayedFinish();
@ -421,24 +421,14 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
private void handleNoSuchUser(final @NonNull WebRtcViewModel event) { 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 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); new AlertDialog.Builder(this)
dialog.setTitle(R.string.RedPhone_number_not_registered); .setTitle(R.string.RedPhone_number_not_registered)
dialog.setIconAttribute(R.attr.dialog_alert_icon); .setIconAttribute(R.attr.dialog_alert_icon)
dialog.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice); .setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
dialog.setCancelable(true); .setCancelable(true)
dialog.setPositiveButton(R.string.RedPhone_got_it, new OnClickListener() { .setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
@Override .setOnCancelListener(d -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
public void onClick(DialogInterface dialog, int which) { .show();
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();
} }
private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) { private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
@ -490,18 +480,18 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
callScreen.setRecipient(event.getRecipient()); callScreen.setRecipient(event.getRecipient());
switch (event.getState()) { switch (event.getState()) {
case CALL_CONNECTED: handleCallConnected(event); break; case CALL_CONNECTED: handleCallConnected(); break;
case NETWORK_FAILURE: handleServerFailure(event); break; case NETWORK_FAILURE: handleServerFailure(); break;
case CALL_RINGING: handleCallRinging(event); break; case CALL_RINGING: handleCallRinging(); break;
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break; case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
case CALL_ACCEPTED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); 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_DECLINED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break;
case CALL_ONGOING_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); 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 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(); break;
case CALL_OUTGOING: handleOutgoingCall(event); break; case CALL_OUTGOING: handleOutgoingCall(); break;
case CALL_BUSY: handleCallBusy(event); break; case CALL_BUSY: handleCallBusy(); break;
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); 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 { private final class ControlsListener implements WebRtcCallView.ControlsListener {
@Override @Override
public void onStartCall() { public void onStartCall(boolean isVideoCall) {
enableVideoIfAvailable = isVideoCall;
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class); Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL) intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(viewModel.getRecipient().getId())) .putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(viewModel.getRecipient().getId()))
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, OfferMessage.Type.VIDEO_CALL.getCode()); .putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, (isVideoCall ? OfferMessage.Type.VIDEO_CALL : OfferMessage.Type.AUDIO_CALL).getCode());
WebRtcCallActivity.this.startService(intent); startService(intent);
MessageSender.onMessageSent(); MessageSender.onMessageSent();
} }

View File

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

View File

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

View File

@ -1,6 +1,8 @@
package org.thoughtcrime.securesms.components.webrtc; package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context; import android.content.Context;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@ -27,14 +29,20 @@ import androidx.transition.TransitionSet;
import androidx.viewpager2.widget.MarginPageTransformer; import androidx.viewpager2.widget.MarginPageTransformer;
import androidx.viewpager2.widget.ViewPager2; 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.R;
import org.thoughtcrime.securesms.animation.ResizeAnimation; import org.thoughtcrime.securesms.animation.ResizeAnimation;
import org.thoughtcrime.securesms.components.AccessibleToggleButton; import org.thoughtcrime.securesms.components.AccessibleToggleButton;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.events.CallParticipant; import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.mediasend.SimpleAnimationListener; import org.thoughtcrime.securesms.mediasend.SimpleAnimationListener;
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.BlurTransformation;
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;
@ -58,10 +66,12 @@ public class WebRtcCallView extends FrameLayout {
private WebRtcAudioOutputToggleButton audioToggle; private WebRtcAudioOutputToggleButton audioToggle;
private AccessibleToggleButton videoToggle; private AccessibleToggleButton videoToggle;
private AccessibleToggleButton micToggle; private AccessibleToggleButton micToggle;
private ViewGroup localRenderPipFrame; private ViewGroup smallLocalRenderFrame;
private TextureViewRenderer smallLocalRender; private TextureViewRenderer smallLocalRender;
private View largeLocalRenderFrame; private View largeLocalRenderFrame;
private TextureViewRenderer largeLocalRender; private TextureViewRenderer largeLocalRender;
private View largeLocalRenderNoVideo;
private ImageView largeLocalRenderNoVideoAvatar;
private TextView recipientName; private TextView recipientName;
private TextView status; private TextView status;
private ConstraintLayout parent; private ConstraintLayout parent;
@ -74,7 +84,7 @@ public class WebRtcCallView extends FrameLayout {
private ImageView hangup; private ImageView hangup;
private View answerWithAudio; private View answerWithAudio;
private View answerWithAudioLabel; private View answerWithAudioLabel;
private View ongoingFooterGradient; private View footerGradient;
private View startCallControls; private View startCallControls;
private ViewPager2 callParticipantsPager; private ViewPager2 callParticipantsPager;
private RecyclerView callParticipantsRecycler; private RecyclerView callParticipantsRecycler;
@ -110,33 +120,34 @@ 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); smallLocalRenderFrame = findViewById(R.id.call_screen_pip);
smallLocalRender = findViewById(R.id.call_screen_small_local_renderer); smallLocalRender = findViewById(R.id.call_screen_small_local_renderer);
largeLocalRenderFrame = findViewById(R.id.call_screen_large_local_renderer_frame); largeLocalRenderFrame = findViewById(R.id.call_screen_large_local_renderer_frame);
largeLocalRender = findViewById(R.id.call_screen_large_local_renderer); largeLocalRender = findViewById(R.id.call_screen_large_local_renderer);
recipientName = findViewById(R.id.call_screen_recipient_name); largeLocalRenderNoVideo = findViewById(R.id.call_screen_large_local_video_off);
status = findViewById(R.id.call_screen_status); largeLocalRenderNoVideoAvatar = findViewById(R.id.call_screen_large_local_video_off_avatar);
parent = findViewById(R.id.call_screen); recipientName = findViewById(R.id.call_screen_recipient_name);
participantsParent = findViewById(R.id.call_screen_participants_parent); status = findViewById(R.id.call_screen_status);
answer = findViewById(R.id.call_screen_answer_call); parent = findViewById(R.id.call_screen);
cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle); participantsParent = findViewById(R.id.call_screen_participants_parent);
hangup = findViewById(R.id.call_screen_end_call); answer = findViewById(R.id.call_screen_answer_call);
answerWithAudio = findViewById(R.id.call_screen_answer_with_audio); cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle);
answerWithAudioLabel = findViewById(R.id.call_screen_answer_with_audio_label); hangup = findViewById(R.id.call_screen_end_call);
ongoingFooterGradient = findViewById(R.id.call_screen_ongoing_footer_gradient); answerWithAudio = findViewById(R.id.call_screen_answer_with_audio);
startCallControls = findViewById(R.id.call_screen_start_call_controls); answerWithAudioLabel = findViewById(R.id.call_screen_answer_with_audio_label);
callParticipantsPager = findViewById(R.id.call_screen_participants_pager); footerGradient = findViewById(R.id.call_screen_footer_gradient);
callParticipantsRecycler = findViewById(R.id.call_screen_participants_recycler); startCallControls = findViewById(R.id.call_screen_start_call_controls);
toolbar = findViewById(R.id.call_screen_toolbar); 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 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);
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 startCall = findViewById(R.id.call_screen_start_call_start_call);
View cancelStartCall = findViewById(R.id.call_screen_start_call_cancel); View cancelStartCall = findViewById(R.id.call_screen_start_call_cancel);
@ -163,7 +174,7 @@ public class WebRtcCallView extends FrameLayout {
incomingCallViews.add(answerLabel); incomingCallViews.add(answerLabel);
incomingCallViews.add(decline); incomingCallViews.add(decline);
incomingCallViews.add(declineLabel); incomingCallViews.add(declineLabel);
incomingCallViews.add(incomingFooterGradient); incomingCallViews.add(footerGradient);
adjustableMarginsSet.add(micToggle); adjustableMarginsSet.add(micToggle);
adjustableMarginsSet.add(cameraDirectionToggle); adjustableMarginsSet.add(cameraDirectionToggle);
@ -190,11 +201,16 @@ public class WebRtcCallView extends FrameLayout {
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));
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)); 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); int statusBarHeight = ViewUtil.getStatusBarHeight(this);
statusBarGuideline.setGuidelineBegin(statusBarHeight); statusBarGuideline.setGuidelineBegin(statusBarHeight);
} }
@ -245,8 +261,6 @@ public class WebRtcCallView extends FrameLayout {
} }
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant) { public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant) {
videoToggle.setChecked(state != WebRtcLocalRenderState.GONE, false);
smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT); smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
largeLocalRender.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()); largeLocalRender.init(localCallParticipant.getVideoSink().getEglBase());
} }
smallLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
largeLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
switch (state) { switch (state) {
case LARGE:
largeLocalRenderFrame.setVisibility(View.VISIBLE);
localRenderPipFrame.setVisibility(View.GONE);
break;
case GONE: case GONE:
largeLocalRender.attachBroadcastVideoSink(null);
largeLocalRenderFrame.setVisibility(View.GONE); largeLocalRenderFrame.setVisibility(View.GONE);
localRenderPipFrame.setVisibility(View.GONE); smallLocalRender.attachBroadcastVideoSink(null);
smallLocalRenderFrame.setVisibility(View.GONE);
videoToggle.setChecked(false, false);
break; break;
case SMALL_RECTANGLE: case SMALL_RECTANGLE:
largeLocalRenderFrame.setVisibility(View.GONE); smallLocalRenderFrame.setVisibility(View.VISIBLE);
localRenderPipFrame.setVisibility(View.VISIBLE); smallLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
animatePipToRectangle(); animatePipToRectangle();
largeLocalRender.attachBroadcastVideoSink(null);
largeLocalRenderFrame.setVisibility(View.GONE);
videoToggle.setChecked(true, false);
break; break;
case SMALL_SQUARE: case SMALL_SQUARE:
largeLocalRenderFrame.setVisibility(View.GONE); smallLocalRenderFrame.setVisibility(View.VISIBLE);
localRenderPipFrame.setVisibility(View.VISIBLE); smallLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
animatePipToSquare(); 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(); visibleViewSet.clear();
if (webRtcControls.displayStartCallControls()) { if (webRtcControls.displayStartCallControls()) {
visibleViewSet.add(footerGradient);
visibleViewSet.add(startCallControls); visibleViewSet.add(startCallControls);
} }
@ -367,7 +420,7 @@ public class WebRtcCallView extends FrameLayout {
if (webRtcControls.displayEndCall()) { if (webRtcControls.displayEndCall()) {
visibleViewSet.add(hangup); visibleViewSet.add(hangup);
visibleViewSet.add(ongoingFooterGradient); visibleViewSet.add(footerGradient);
} }
if (webRtcControls.displayMuteAudio()) { if (webRtcControls.displayMuteAudio()) {
@ -411,7 +464,7 @@ public class WebRtcCallView extends FrameLayout {
} }
private void animatePipToRectangle() { 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.setDuration(PIP_RESIZE_DURATION);
animation.setAnimationListener(new SimpleAnimationListener() { animation.setAnimationListener(new SimpleAnimationListener() {
@Override @Override
@ -421,14 +474,14 @@ public class WebRtcCallView extends FrameLayout {
} }
}); });
localRenderPipFrame.startAnimation(animation); smallLocalRenderFrame.startAnimation(animation);
} }
private void animatePipToSquare() { private void animatePipToSquare() {
pictureInPictureGestureHelper.lockToBottomEnd(); pictureInPictureGestureHelper.lockToBottomEnd();
pictureInPictureGestureHelper.performAfterFling(() -> { 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.setDuration(PIP_RESIZE_DURATION);
animation.setAnimationListener(new SimpleAnimationListener() { animation.setAnimationListener(new SimpleAnimationListener() {
@Override @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 { public interface ControlsListener {
void onStartCall(); void onStartCall(boolean isVideoCall);
void onCancelStartCall(); void onCancelStartCall();
void onControlsFadeOut(); void onControlsFadeOut();
void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput); void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,6 +32,21 @@ public class CameraState {
} }
public enum Direction { 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.OutgoingRinger;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager; import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.thoughtcrime.securesms.webrtc.locks.LockManager; import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.webrtc.CapturerObserver;
import org.webrtc.EglBase; import org.webrtc.EglBase;
import org.webrtc.PeerConnection; import org.webrtc.PeerConnection;
import org.whispersystems.libsignal.ecc.ECPublicKey; import org.webrtc.VideoFrame;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.DjbECPublicKey; 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.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; 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_BROADCAST = "broadcast";
public static final String EXTRA_ANSWER_WITH_VIDEO = "enable_video"; 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_OUTGOING_CALL = "CALL_OUTGOING";
public static final String ACTION_DENY_CALL = "DENY_CALL"; public static final String ACTION_DENY_CALL = "DENY_CALL";
public static final String ACTION_LOCAL_HANGUP = "LOCAL_HANGUP"; 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 CallManager callManager;
@Nullable private RemotePeer activePeer; @Nullable private RemotePeer activePeer;
@Nullable private RemotePeer busyPeer; @Nullable private RemotePeer busyPeer;
@Nullable private RemotePeer preJoinPeer;
@Nullable private SparseArray<RemotePeer> peerMap; @Nullable private SparseArray<RemotePeer> peerMap;
@Nullable private EglBase eglBase; @Nullable private EglBase eglBase;
@ -232,6 +237,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
serviceExecutor.execute(() -> { serviceExecutor.execute(() -> {
if (intent.getAction().equals(ACTION_RECEIVE_OFFER)) handleReceivedOffer(intent); 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_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_OUTGOING_CALL) && isIdle()) handleOutgoingCall(intent);
else if (intent.getAction().equals(ACTION_DENY_CALL)) handleDenyCall(intent); else if (intent.getAction().equals(ACTION_DENY_CALL)) handleDenyCall(intent);
else if (intent.getAction().equals(ACTION_LOCAL_HANGUP)) handleLocalHangup(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; localCameraState = newCameraState;
if (activePeer != null) { if (activePeer != null) {
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); 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) { private void handleOutgoingCall(Intent intent) {
Log.i(TAG, "handleOutgoingCall():"); Log.i(TAG, "handleOutgoingCall():");
@ -456,6 +518,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
throw new IllegalStateException("Dialing from non-idle?"); throw new IllegalStateException("Dialing from non-idle?");
} }
preJoinPeer = null;
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
peerMap.append(remotePeer.hashCode(), remotePeer); peerMap.append(remotePeer.hashCode(), remotePeer);
@ -575,6 +639,8 @@ 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, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); 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); AudioManager audioManager = ServiceUtil.getAudioManager(this);
if (activePeer == null) { 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; return;
} }
@ -1300,13 +1374,35 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
private void initializeVideo() { private void initializeVideo() {
Util.runOnMainSync(() -> { Util.runOnMainSync(() -> {
eglBase = EglBase.create(); if (eglBase == null) {
localSink = new BroadcastVideoSink(eglBase); eglBase = EglBase.create();
camera = new Camera(WebRtcCallService.this, WebRtcCallService.this, eglBase); 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(); 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) { private void setCallInProgressNotification(int type, RemotePeer remotePeer) {
startForeground(CallNotificationBuilder.getNotificationId(getApplicationContext(), type), startForeground(CallNotificationBuilder.getNotificationId(getApplicationContext(), type),
CallNotificationBuilder.getCallInProgressNotification(this, type, remotePeer.getRecipient())); CallNotificationBuilder.getCallInProgressNotification(this, type, remotePeer.getRecipient()));
@ -1334,26 +1430,15 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
audioManager.stop(playDisconnectSound); audioManager.stop(playDisconnectSound);
bluetoothStateManager.setWantsConnection(false); bluetoothStateManager.setWantsConnection(false);
if (camera != null) { cleanupVideo();
camera.dispose();
camera = null;
}
if (eglBase != null) {
eglBase.release();
eglBase = null;
}
this.localCameraState = CameraState.UNKNOWN;
this.microphoneEnabled = true; this.microphoneEnabled = true;
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.clear();
remoteParticipantMap.put(participant.getRecipient(), participant.withVideoEnabled(false));
}
lockManager.updatePhoneState(LockManager.PhoneState.IDLE); lockManager.updatePhoneState(LockManager.PhoneState.IDLE);
} }

View File

@ -81,16 +81,7 @@ public class CommunicationActions {
WebRtcCallService.isCallActive(activity, new ResultReceiver(new Handler(Looper.getMainLooper())) { WebRtcCallService.isCallActive(activity, new ResultReceiver(new Handler(Looper.getMainLooper())) {
@Override @Override
protected void onReceiveResult(int resultCode, Bundle resultData) { protected void onReceiveResult(int resultCode, Bundle resultData) {
if (resultCode == 1) { startCallInternal(activity, recipient, 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();
}
} }
}); });
} }
@ -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))) .withPermanentDenialDialog(activity.getString(R.string.ConversationActivity_signal_needs_the_microphone_and_camera_permissions_in_order_to_call_s, recipient.getDisplayName(activity)))
.onAllGranted(() -> { .onAllGranted(() -> {
Intent intent = new Intent(activity, WebRtcCallService.class); 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_REMOTE_PEER, new RemotePeer(recipient.getId()))
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, OfferMessage.Type.VIDEO_CALL.getCode()); .putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, OfferMessage.Type.VIDEO_CALL.getCode());
activity.startService(intent); activity.startService(intent);
MessageSender.onMessageSent();
Intent activityIntent = new Intent(activity, WebRtcCallActivity.class); Intent activityIntent = new Intent(activity, WebRtcCallActivity.class);
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

View File

@ -27,18 +27,10 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout <include
android:id="@+id/call_screen_large_local_renderer_frame" layout="@layout/webrtc_call_view_large_local_render"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/call_screen" android:id="@+id/call_screen"
@ -61,24 +53,22 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<View <Space
android:id="@+id/call_screen_ongoing_footer_gradient" android:id="@+id/call_screen_footer_gradient_spacer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="160dp" android:layout_height="30dp"
android:background="@drawable/webrtc_call_screen_header_gradient" app:layout_constraintBottom_toBottomOf="@id/call_screen_footer_gradient_barrier"/>
android:rotation="180"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
tools:visibility="gone" />
<View <View
android:id="@+id/call_screen_incoming_footer_gradient" android:id="@+id/call_screen_footer_gradient"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="240dp" android:layout_height="0dp"
android:background="@drawable/webrtc_call_screen_header_gradient" android:background="@drawable/webrtc_call_screen_header_gradient"
android:rotation="180" android:rotation="180"
android:visibility="gone" android:visibility="gone"
android:layout_margin="-40dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="@id/call_screen_footer_gradient_spacer"
tools:visibility="visible" /> tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
@ -330,5 +320,12 @@
</LinearLayout> </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> </androidx.constraintlayout.widget.ConstraintLayout>
</merge> </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__start_call">Start Call</string>
<string name="WebRtcCallView__group_call">Group Call</string> <string name="WebRtcCallView__group_call">Group Call</string>
<string name="WebRtcCallView__view_participants_list">View participants</string> <string name="WebRtcCallView__view_participants_list">View participants</string>
<string name="WebRtcCallView__your_video_is_off">Your video is off</string>
<!-- CallParticipantsListDialog --> <!-- CallParticipantsListDialog -->
<plurals name="CallParticipantsListDialog_in_this_call_d_people"> <plurals name="CallParticipantsListDialog_in_this_call_d_people">