459 lines
17 KiB
Java
459 lines
17 KiB
Java
package org.thoughtcrime.securesms.components.webrtc;
|
|
|
|
import android.content.Context;
|
|
import android.util.AttributeSet;
|
|
import android.view.LayoutInflater;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.ViewParent;
|
|
import android.widget.FrameLayout;
|
|
import android.widget.ImageView;
|
|
import android.widget.TextView;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.appcompat.content.res.AppCompatResources;
|
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
|
import androidx.constraintlayout.widget.ConstraintSet;
|
|
import androidx.constraintlayout.widget.Group;
|
|
import androidx.core.util.Consumer;
|
|
import androidx.transition.AutoTransition;
|
|
import androidx.transition.Transition;
|
|
import androidx.transition.TransitionManager;
|
|
|
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
|
|
|
import org.thoughtcrime.securesms.R;
|
|
import org.thoughtcrime.securesms.components.AccessibleToggleButton;
|
|
import org.thoughtcrime.securesms.components.AvatarImageView;
|
|
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
|
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
|
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
|
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
|
import org.thoughtcrime.securesms.util.AvatarUtil;
|
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
|
import org.webrtc.RendererCommon;
|
|
import org.webrtc.SurfaceViewRenderer;
|
|
|
|
public class WebRtcCallView extends FrameLayout {
|
|
|
|
private static final long TRANSITION_DURATION_MILLIS = 250;
|
|
private static final FallbackPhotoProvider FALLBACK_PHOTO_PROVIDER = new FallbackPhotoProvider();
|
|
|
|
public static final int FADE_OUT_DELAY = 5000;
|
|
|
|
private SurfaceViewRenderer localRenderer;
|
|
private Group ongoingCallButtons;
|
|
private Group incomingCallButtons;
|
|
private Group answerWithVoiceGroup;
|
|
private Group topViews;
|
|
private View topGradient;
|
|
private WebRtcAudioOutputToggleButton speakerToggle;
|
|
private AccessibleToggleButton videoToggle;
|
|
private AccessibleToggleButton micToggle;
|
|
private ViewGroup largeLocalRenderContainer;
|
|
private ViewGroup localRenderPipFrame;
|
|
private ViewGroup smallLocalRenderContainer;
|
|
private ViewGroup remoteRenderContainer;
|
|
private TextView recipientName;
|
|
private TextView status;
|
|
private ConstraintLayout parent;
|
|
private AvatarImageView avatar;
|
|
private ImageView avatarCard;
|
|
private ControlsListener controlsListener;
|
|
private RecipientId recipientId;
|
|
private CameraState.Direction cameraDirection;
|
|
private boolean shouldFadeControls;
|
|
private ImageView accept;
|
|
private View cameraDirectionToggle;
|
|
private PictureInPictureGestureHelper pictureInPictureGestureHelper;
|
|
|
|
private final Runnable fadeOutRunnable = () -> { if (isAttachedToWindow()) fadeOutControls(); };
|
|
|
|
public WebRtcCallView(@NonNull Context context) {
|
|
this(context, null);
|
|
}
|
|
|
|
public WebRtcCallView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
|
super(context, attrs);
|
|
|
|
LayoutInflater.from(context).inflate(R.layout.webrtc_call_view, this, true);
|
|
}
|
|
|
|
@SuppressWarnings("CodeBlock2Expr")
|
|
@Override
|
|
protected void onFinishInflate() {
|
|
super.onFinishInflate();
|
|
|
|
ongoingCallButtons = findViewById(R.id.call_screen_in_call_buttons);
|
|
incomingCallButtons = findViewById(R.id.call_screen_incoming_call_buttons);
|
|
answerWithVoiceGroup = findViewById(R.id.call_screen_answer_with_audio_button);
|
|
topViews = findViewById(R.id.call_screen_top_views);
|
|
topGradient = findViewById(R.id.call_screen_header_gradient);
|
|
speakerToggle = findViewById(R.id.call_screen_speaker_toggle);
|
|
videoToggle = findViewById(R.id.call_screen_video_toggle);
|
|
micToggle = findViewById(R.id.call_screen_mic_toggle);
|
|
localRenderPipFrame = findViewById(R.id.call_screen_pip);
|
|
largeLocalRenderContainer = findViewById(R.id.call_screen_large_local_renderer_holder);
|
|
smallLocalRenderContainer = findViewById(R.id.call_screen_small_local_renderer_holder);
|
|
remoteRenderContainer = findViewById(R.id.call_screen_remote_renderer_holder);
|
|
recipientName = findViewById(R.id.call_screen_recipient_name);
|
|
status = findViewById(R.id.call_screen_status);
|
|
parent = findViewById(R.id.call_screen);
|
|
avatar = findViewById(R.id.call_screen_recipient_avatar);
|
|
avatarCard = findViewById(R.id.call_screen_recipient_avatar_call_card);
|
|
accept = findViewById(R.id.call_screen_answer_call);
|
|
cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle);
|
|
|
|
View hangup = findViewById(R.id.call_screen_end_call);
|
|
View downCaret = findViewById(R.id.call_screen_down_arrow);
|
|
View decline = findViewById(R.id.call_screen_decline_call);
|
|
View answerWithAudio = findViewById(R.id.call_screen_answer_with_audio);
|
|
|
|
speakerToggle.setOnAudioOutputChangedListener(outputMode -> {
|
|
runIfNonNull(controlsListener, listener -> listener.onAudioOutputChanged(outputMode));
|
|
});
|
|
|
|
videoToggle.setOnCheckedChangeListener((v, isOn) -> {
|
|
runIfNonNull(controlsListener, listener -> listener.onVideoChanged(isOn));
|
|
});
|
|
|
|
micToggle.setOnCheckedChangeListener((v, isOn) -> {
|
|
runIfNonNull(controlsListener, listener -> listener.onMicChanged(isOn));
|
|
});
|
|
|
|
cameraDirectionToggle.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onCameraDirectionChanged));
|
|
|
|
hangup.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onEndCallPressed));
|
|
decline.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onDenyCallPressed));
|
|
|
|
downCaret.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onDownCaretPressed));
|
|
|
|
accept.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallPressed));
|
|
answerWithAudio.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallWithVoiceOnlyPressed));
|
|
|
|
setOnClickListener(v -> toggleControls());
|
|
avatar.setOnClickListener(v -> toggleControls());
|
|
|
|
pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(localRenderPipFrame);
|
|
|
|
int statusBarHeight = ViewUtil.getStatusBarHeight(this);
|
|
MarginLayoutParams params = (MarginLayoutParams) parent.getLayoutParams();
|
|
|
|
params.topMargin = statusBarHeight;
|
|
parent.setLayoutParams(params);
|
|
}
|
|
|
|
@Override
|
|
protected void onAttachedToWindow() {
|
|
super.onAttachedToWindow();
|
|
|
|
if (shouldFadeControls) {
|
|
scheduleFadeOut();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onDetachedFromWindow() {
|
|
super.onDetachedFromWindow();
|
|
cancelFadeOut();
|
|
}
|
|
|
|
public void showCameraToggleButton(boolean shouldShowCameraToggleButton) {
|
|
cameraDirectionToggle.setVisibility(shouldShowCameraToggleButton ? VISIBLE : GONE);
|
|
}
|
|
|
|
public void setControlsListener(@Nullable ControlsListener controlsListener) {
|
|
this.controlsListener = controlsListener;
|
|
}
|
|
|
|
public void setMicEnabled(boolean isMicEnabled) {
|
|
micToggle.setChecked(isMicEnabled, false);
|
|
}
|
|
|
|
public void setBluetoothEnabled(boolean isBluetoothEnabled) {
|
|
speakerToggle.setIsHeadsetAvailable(isBluetoothEnabled);
|
|
}
|
|
|
|
public void setAudioOutput(WebRtcAudioOutput output) {
|
|
speakerToggle.setAudioOutput(output);
|
|
}
|
|
|
|
public void setRemoteVideoEnabled(boolean isRemoteVideoEnabled) {
|
|
boolean wasRemoteVideoEnabled = remoteRenderContainer.getVisibility() == View.VISIBLE;
|
|
|
|
shouldFadeControls = isRemoteVideoEnabled;
|
|
|
|
if (isRemoteVideoEnabled) {
|
|
remoteRenderContainer.setVisibility(View.VISIBLE);
|
|
} else {
|
|
remoteRenderContainer.setVisibility(View.GONE);
|
|
}
|
|
|
|
if (shouldFadeControls && !wasRemoteVideoEnabled) {
|
|
fadeInControls();
|
|
} else if (!shouldFadeControls && wasRemoteVideoEnabled) {
|
|
fadeOutControls();
|
|
cancelFadeOut();
|
|
}
|
|
}
|
|
|
|
public void setLocalRenderer(@Nullable SurfaceViewRenderer surfaceViewRenderer) {
|
|
if (localRenderer == surfaceViewRenderer) {
|
|
return;
|
|
}
|
|
|
|
localRenderer = surfaceViewRenderer;
|
|
|
|
if (surfaceViewRenderer == null) {
|
|
setRenderer(largeLocalRenderContainer, null);
|
|
setRenderer(smallLocalRenderContainer, null);
|
|
} else {
|
|
localRenderer.setMirror(cameraDirection == CameraState.Direction.FRONT);
|
|
localRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
|
|
}
|
|
}
|
|
|
|
public void setRemoteRenderer(@Nullable SurfaceViewRenderer surfaceViewRenderer) {
|
|
setRenderer(remoteRenderContainer, surfaceViewRenderer);
|
|
}
|
|
|
|
public void setLocalRenderState(WebRtcLocalRenderState localRenderState) {
|
|
boolean enableZOverlay = localRenderState == WebRtcLocalRenderState.SMALL;
|
|
|
|
videoToggle.setChecked(localRenderState != WebRtcLocalRenderState.GONE, false);
|
|
|
|
switch (localRenderState) {
|
|
case GONE:
|
|
localRenderPipFrame.setVisibility(View.GONE);
|
|
largeLocalRenderContainer.setVisibility(View.GONE);
|
|
cameraDirectionToggle.animate().setDuration(0).alpha(0f);
|
|
setRenderer(largeLocalRenderContainer, null);
|
|
setRenderer(smallLocalRenderContainer, null);
|
|
break;
|
|
case LARGE:
|
|
localRenderPipFrame.setVisibility(View.GONE);
|
|
largeLocalRenderContainer.setVisibility(View.VISIBLE);
|
|
cameraDirectionToggle.animate().setDuration(0).alpha(0f);
|
|
if (largeLocalRenderContainer.getChildCount() == 0) {
|
|
setRenderer(largeLocalRenderContainer, localRenderer);
|
|
}
|
|
break;
|
|
case SMALL:
|
|
localRenderPipFrame.setVisibility(View.VISIBLE);
|
|
largeLocalRenderContainer.setVisibility(View.GONE);
|
|
cameraDirectionToggle.animate()
|
|
.setDuration(450)
|
|
.alpha(1f);
|
|
|
|
if (smallLocalRenderContainer.getChildCount() == 0) {
|
|
setRenderer(smallLocalRenderContainer, localRenderer);
|
|
}
|
|
}
|
|
|
|
if (localRenderer != null) {
|
|
localRenderer.setZOrderMediaOverlay(enableZOverlay);
|
|
}
|
|
}
|
|
|
|
public void setCameraDirection(@NonNull CameraState.Direction cameraDirection) {
|
|
this.cameraDirection = cameraDirection;
|
|
|
|
if (localRenderer != null) {
|
|
localRenderer.setMirror(cameraDirection == CameraState.Direction.FRONT);
|
|
}
|
|
}
|
|
|
|
public void setRecipient(@NonNull Recipient recipient) {
|
|
if (recipient.getId() == recipientId) {
|
|
return;
|
|
}
|
|
|
|
recipientId = recipient.getId();
|
|
recipientName.setText(recipient.getDisplayName(getContext()));
|
|
avatar.setFallbackPhotoProvider(FALLBACK_PHOTO_PROVIDER);
|
|
avatar.setAvatar(GlideApp.with(this), recipient, false);
|
|
AvatarUtil.loadBlurredIconIntoViewBackground(recipient, this);
|
|
|
|
setRecipientCallCard(recipient);
|
|
}
|
|
|
|
public void showCallCard(boolean showCallCard) {
|
|
avatarCard.setVisibility(showCallCard ? VISIBLE : GONE);
|
|
avatar.setVisibility(showCallCard ? GONE : VISIBLE);
|
|
}
|
|
|
|
public void setStatus(@NonNull String status) {
|
|
this.status.setText(status);
|
|
}
|
|
|
|
public void setWebRtcControls(WebRtcControls webRtcControls) {
|
|
answerWithVoiceGroup.setVisibility(View.GONE);
|
|
|
|
switch (webRtcControls) {
|
|
case NONE:
|
|
ongoingCallButtons.setVisibility(View.GONE);
|
|
incomingCallButtons.setVisibility(View.GONE);
|
|
setTopViewsVisibility(View.GONE);
|
|
break;
|
|
case INCOMING_VIDEO:
|
|
answerWithVoiceGroup.setVisibility(View.VISIBLE);
|
|
setTopViewsVisibility(View.VISIBLE);
|
|
ongoingCallButtons.setVisibility(View.GONE);
|
|
incomingCallButtons.setVisibility(View.VISIBLE);
|
|
status.setText(R.string.WebRtcCallView__signal_video_call);
|
|
accept.setImageDrawable(AppCompatResources.getDrawable(getContext(), R.drawable.webrtc_call_screen_answer_with_video));
|
|
break;
|
|
case INCOMING_AUDIO:
|
|
setTopViewsVisibility(View.VISIBLE);
|
|
ongoingCallButtons.setVisibility(View.GONE);
|
|
incomingCallButtons.setVisibility(View.VISIBLE);
|
|
status.setText(R.string.WebRtcCallView__signal_voice_call);
|
|
accept.setImageDrawable(AppCompatResources.getDrawable(getContext(), R.drawable.webrtc_call_screen_answer));
|
|
break;
|
|
case RINGING:
|
|
setTopViewsVisibility(View.VISIBLE);
|
|
incomingCallButtons.setVisibility(View.GONE);
|
|
ongoingCallButtons.setVisibility(View.VISIBLE);
|
|
break;
|
|
case CONNECTED:
|
|
setTopViewsVisibility(View.VISIBLE);
|
|
incomingCallButtons.setVisibility(View.GONE);
|
|
ongoingCallButtons.setVisibility(View.VISIBLE);
|
|
|
|
post(() -> {
|
|
pictureInPictureGestureHelper.setVerticalBoundaries(status.getBottom(), speakerToggle.getTop());
|
|
});
|
|
}
|
|
}
|
|
|
|
private void setTopViewsVisibility(int visibility) {
|
|
topViews.setVisibility(visibility);
|
|
topGradient.setVisibility(visibility);
|
|
}
|
|
|
|
public @NonNull View getVideoTooltipTarget() {
|
|
return videoToggle;
|
|
}
|
|
|
|
private void toggleControls() {
|
|
if (shouldFadeControls) {
|
|
if (status.getVisibility() == VISIBLE) {
|
|
fadeOutControls();
|
|
} else {
|
|
fadeInControls();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void fadeOutControls() {
|
|
fadeControls(ConstraintSet.GONE);
|
|
controlsListener.onControlsFadeOut();
|
|
pictureInPictureGestureHelper.clearVerticalBoundaries();
|
|
}
|
|
|
|
private void fadeInControls() {
|
|
fadeControls(ConstraintSet.VISIBLE);
|
|
pictureInPictureGestureHelper.setVerticalBoundaries(status.getBottom(), speakerToggle.getTop());
|
|
|
|
scheduleFadeOut();
|
|
}
|
|
|
|
private void fadeControls(int visibility) {
|
|
Transition transition = new AutoTransition().setDuration(TRANSITION_DURATION_MILLIS);
|
|
|
|
TransitionManager.beginDelayedTransition(parent, transition);
|
|
|
|
ConstraintSet constraintSet = new ConstraintSet();
|
|
constraintSet.clone(parent);
|
|
|
|
constraintSet.setVisibility(R.id.call_screen_in_call_buttons, visibility);
|
|
constraintSet.setVisibility(R.id.call_screen_top_views, visibility);
|
|
|
|
constraintSet.applyTo(parent);
|
|
|
|
topGradient.animate()
|
|
.alpha(visibility == ConstraintSet.VISIBLE ? 1f : 0f)
|
|
.setDuration(TRANSITION_DURATION_MILLIS)
|
|
.start();
|
|
}
|
|
|
|
private void scheduleFadeOut() {
|
|
cancelFadeOut();
|
|
shouldFadeControls = true;
|
|
|
|
if (getHandler() == null) return;
|
|
getHandler().postDelayed(fadeOutRunnable, FADE_OUT_DELAY);
|
|
}
|
|
|
|
private void cancelFadeOut() {
|
|
shouldFadeControls = false;
|
|
|
|
if (getHandler() == null) return;
|
|
getHandler().removeCallbacks(fadeOutRunnable);
|
|
}
|
|
|
|
private static void runIfNonNull(@Nullable ControlsListener controlsListener, @NonNull Consumer<ControlsListener> controlsListenerConsumer) {
|
|
if (controlsListener != null) {
|
|
controlsListenerConsumer.accept(controlsListener);
|
|
}
|
|
}
|
|
|
|
private static void setRenderer(@NonNull ViewGroup container, @Nullable View renderer) {
|
|
if (renderer == null) {
|
|
container.removeAllViews();
|
|
return;
|
|
}
|
|
|
|
ViewParent parent = renderer.getParent();
|
|
if (parent != null && parent != container) {
|
|
((ViewGroup) parent).removeAllViews();
|
|
}
|
|
|
|
if (parent == container) {
|
|
return;
|
|
}
|
|
|
|
container.addView(renderer);
|
|
}
|
|
|
|
private void setRecipientCallCard(@NonNull Recipient recipient) {
|
|
ContactPhoto contactPhoto = recipient.getContactPhoto();
|
|
FallbackContactPhoto fallbackPhoto = recipient.getFallbackContactPhoto(FALLBACK_PHOTO_PROVIDER);
|
|
|
|
GlideApp.with(this).load(contactPhoto)
|
|
.fallback(fallbackPhoto.asCallCard(getContext()))
|
|
.error(fallbackPhoto.asCallCard(getContext()))
|
|
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
|
.into(this.avatarCard);
|
|
|
|
if (contactPhoto == null) this.avatarCard.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
|
|
else this.avatarCard.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
|
|
|
this.avatarCard.setBackgroundColor(recipient.getColor().toActionBarColor(getContext()));
|
|
}
|
|
|
|
private static final class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider {
|
|
@Override
|
|
public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
|
|
return new ResourceContactPhoto(R.drawable.ic_profile_outline_120);
|
|
}
|
|
}
|
|
|
|
public interface ControlsListener {
|
|
void onControlsFadeOut();
|
|
void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput);
|
|
void onVideoChanged(boolean isVideoEnabled);
|
|
void onMicChanged(boolean isMicEnabled);
|
|
void onCameraDirectionChanged();
|
|
void onEndCallPressed();
|
|
void onDenyCallPressed();
|
|
void onAcceptCallWithVoiceOnlyPressed();
|
|
void onAcceptCallPressed();
|
|
void onDownCaretPressed();
|
|
}
|
|
}
|