/* * Copyright (C) 2016 Open Whisper Systems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.thoughtcrime.securesms.components.webrtc; import android.content.Context; import android.text.SpannableString; import android.text.Spanned; import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.google.android.material.floatingactionbutton.FloatingActionButton; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.VerifySpan; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.webrtc.CameraState; import org.webrtc.SurfaceViewRenderer; import org.whispersystems.libsignal.IdentityKey; /** * A UI widget that encapsulates the entire in-call screen * for both initiators and responders. * * @author Moxie Marlinspike * */ public class WebRtcCallScreen extends FrameLayout implements RecipientModifiedListener { @SuppressWarnings("unused") private static final String TAG = WebRtcCallScreen.class.getSimpleName(); private ImageView photo; private SurfaceViewRenderer localRenderer; private PercentFrameLayout localRenderLayout; private PercentFrameLayout remoteRenderLayout; private TextView name; private TextView phoneNumber; private TextView label; private TextView elapsedTime; private View untrustedIdentityContainer; private TextView untrustedIdentityExplanation; private Button acceptIdentityButton; private Button cancelIdentityButton; private TextView status; private FloatingActionButton endCallButton; private WebRtcCallControls controls; private RelativeLayout expandedInfo; private ViewGroup callHeader; private WebRtcAnswerDeclineButton incomingCallButton; private Recipient recipient; private boolean minimized; public WebRtcCallScreen(Context context) { super(context); initialize(); } public WebRtcCallScreen(Context context, AttributeSet attrs) { super(context, attrs); initialize(); } public WebRtcCallScreen(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initialize(); } public void setActiveCall(@NonNull Recipient personInfo, @NonNull String message, @Nullable String sas, SurfaceViewRenderer localRenderer, SurfaceViewRenderer remoteRenderer) { setCard(personInfo, message); setConnected(localRenderer, remoteRenderer); incomingCallButton.stopRingingAnimation(); incomingCallButton.setVisibility(View.GONE); endCallButton.show(); } public void setActiveCall(@NonNull Recipient personInfo, @NonNull String message) { setCard(personInfo, message); incomingCallButton.stopRingingAnimation(); incomingCallButton.setVisibility(View.GONE); endCallButton.show(); } public void setIncomingCall(Recipient personInfo) { setCard(personInfo, getContext().getString(R.string.CallScreen_Incoming_call)); endCallButton.hide(); incomingCallButton.setVisibility(View.VISIBLE); incomingCallButton.startRingingAnimation(); } public void setUntrustedIdentity(Recipient personInfo, IdentityKey untrustedIdentity) { String name = recipient.toShortString(); String introduction = String.format(getContext().getString(R.string.WebRtcCallScreen_new_safety_numbers), name, name); SpannableString spannableString = new SpannableString(introduction + " " + getContext().getString(R.string.WebRtcCallScreen_you_may_wish_to_verify_this_contact)); spannableString.setSpan(new VerifySpan(getContext(), personInfo.getAddress(), untrustedIdentity), introduction.length()+1, spannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); setPersonInfo(personInfo); incomingCallButton.stopRingingAnimation(); incomingCallButton.setVisibility(View.GONE); this.status.setText(R.string.WebRtcCallScreen_new_safety_number_title); this.untrustedIdentityContainer.setVisibility(View.VISIBLE); this.untrustedIdentityExplanation.setText(spannableString); this.untrustedIdentityExplanation.setMovementMethod(LinkMovementMethod.getInstance()); this.endCallButton.hide(); } public void setIncomingCallActionListener(WebRtcAnswerDeclineButton.AnswerDeclineListener listener) { incomingCallButton.setAnswerDeclineListener(listener); } public void setAudioMuteButtonListener(WebRtcCallControls.MuteButtonListener listener) { this.controls.setAudioMuteButtonListener(listener); } public void setVideoMuteButtonListener(WebRtcCallControls.MuteButtonListener listener) { this.controls.setVideoMuteButtonListener(listener); } public void setCameraFlipButtonListener(WebRtcCallControls.CameraFlipButtonListener listener) { this.controls.setCameraFlipButtonListener(listener); } public void setSpeakerButtonListener(WebRtcCallControls.SpeakerButtonListener listener) { this.controls.setSpeakerButtonListener(listener); } public void setBluetoothButtonListener(WebRtcCallControls.BluetoothButtonListener listener) { this.controls.setBluetoothButtonListener(listener); } public void setHangupButtonListener(final HangupButtonListener listener) { endCallButton.setOnClickListener(v -> listener.onClick()); } public void setAcceptIdentityListener(OnClickListener listener) { this.acceptIdentityButton.setOnClickListener(listener); } public void setCancelIdentityButton(OnClickListener listener) { this.cancelIdentityButton.setOnClickListener(listener); } public void updateAudioState(boolean isBluetoothAvailable, boolean isMicrophoneEnabled) { this.controls.updateAudioState(isBluetoothAvailable); this.controls.setMicrophoneEnabled(isMicrophoneEnabled); } public void setControlsEnabled(boolean enabled) { this.controls.setControlsEnabled(enabled); } public void setLocalVideoState(@NonNull CameraState cameraState) { this.controls.setVideoAvailable(cameraState.getCameraCount() > 0); this.controls.setVideoEnabled(cameraState.isEnabled()); this.controls.setCameraFlipAvailable(cameraState.getCameraCount() > 1); this.controls.setCameraFlipClickable(cameraState.getActiveDirection() != CameraState.Direction.PENDING); this.controls.setCameraFlipButtonEnabled(cameraState.getActiveDirection() == CameraState.Direction.BACK); if (this.localRenderer != null) { this.localRenderer.setMirror(cameraState.getActiveDirection() == CameraState.Direction.FRONT); } if (this.localRenderLayout.isHidden() == cameraState.isEnabled()) { this.localRenderLayout.setHidden(!cameraState.isEnabled()); this.localRenderLayout.requestLayout(); this.localRenderer.setVisibility(cameraState.isEnabled() ? VISIBLE : INVISIBLE); } } public void setRemoteVideoEnabled(boolean enabled) { if (enabled && this.remoteRenderLayout.isHidden()) { this.photo.setVisibility(View.INVISIBLE); setMinimized(true); this.remoteRenderLayout.setHidden(false); this.remoteRenderLayout.requestLayout(); if (localRenderLayout.isHidden()) this.controls.displayVideoTooltip(callHeader); } else if (!enabled && !this.remoteRenderLayout.isHidden()){ setMinimized(false); this.photo.setVisibility(View.VISIBLE); this.remoteRenderLayout.setHidden(true); this.remoteRenderLayout.requestLayout(); } } public boolean isVideoEnabled() { return controls.isVideoEnabled(); } private void initialize() { LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(R.layout.webrtc_call_screen, this, true); this.elapsedTime = findViewById(R.id.elapsedTime); this.photo = findViewById(R.id.photo); this.localRenderLayout = findViewById(R.id.local_render_layout); this.remoteRenderLayout = findViewById(R.id.remote_render_layout); this.phoneNumber = findViewById(R.id.phoneNumber); this.name = findViewById(R.id.name); this.label = findViewById(R.id.label); this.status = findViewById(R.id.callStateLabel); this.controls = findViewById(R.id.inCallControls); this.endCallButton = findViewById(R.id.hangup_fab); this.incomingCallButton = findViewById(R.id.answer_decline_button); this.untrustedIdentityContainer = findViewById(R.id.untrusted_layout); this.untrustedIdentityExplanation = findViewById(R.id.untrusted_explanation); this.acceptIdentityButton = findViewById(R.id.accept_safety_numbers); this.cancelIdentityButton = findViewById(R.id.cancel_safety_numbers); this.expandedInfo = findViewById(R.id.expanded_info); this.callHeader = findViewById(R.id.call_info_1); this.localRenderLayout.setHidden(true); this.remoteRenderLayout.setHidden(true); this.minimized = false; this.remoteRenderLayout.setOnClickListener(v -> { if (!this.remoteRenderLayout.isHidden()) { setMinimized(!minimized); } }); } private void setConnected(SurfaceViewRenderer localRenderer, SurfaceViewRenderer remoteRenderer) { if (localRenderLayout.getChildCount() == 0 && remoteRenderLayout.getChildCount() == 0) { if (localRenderer.getParent() != null) { ((ViewGroup)localRenderer.getParent()).removeView(localRenderer); } if (remoteRenderer.getParent() != null) { ((ViewGroup)remoteRenderer.getParent()).removeView(remoteRenderer); } localRenderLayout.setPosition(7, 70, 25, 25); localRenderLayout.setSquare(true); remoteRenderLayout.setPosition(0, 0, 100, 100); localRenderer.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); remoteRenderer.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); localRenderer.setMirror(true); localRenderer.setZOrderMediaOverlay(true); localRenderLayout.addView(localRenderer); remoteRenderLayout.addView(remoteRenderer); this.localRenderer = localRenderer; } } private void setPersonInfo(final @NonNull Recipient recipient) { this.recipient = recipient; this.recipient.addListener(this); GlideApp.with(getContext().getApplicationContext()) .load(recipient.getContactPhoto()) .fallback(recipient.getFallbackContactPhoto().asCallCard(getContext())) .error(recipient.getFallbackContactPhoto().asCallCard(getContext())) .diskCacheStrategy(DiskCacheStrategy.ALL) .into(this.photo); this.name.setText(recipient.getName()); if (recipient.getName() == null && !TextUtils.isEmpty(recipient.getProfileName())) { this.phoneNumber.setText(recipient.getAddress().serialize() + " (~" + recipient.getProfileName() + ")"); } else { this.phoneNumber.setText(recipient.getAddress().serialize()); } } private void setCard(Recipient recipient, String status) { setPersonInfo(recipient); this.status.setText(status); this.untrustedIdentityContainer.setVisibility(View.GONE); } private void setMinimized(boolean minimized) { if (minimized) { ViewCompat.animate(callHeader).translationY(-1 * expandedInfo.getHeight()); ViewCompat.animate(status).alpha(0); ViewCompat.animate(endCallButton).translationY(endCallButton.getHeight() + ViewUtil.dpToPx(getContext(), 40)); ViewCompat.animate(endCallButton).alpha(0); this.minimized = true; } else { ViewCompat.animate(callHeader).translationY(0); ViewCompat.animate(status).alpha(1); ViewCompat.animate(endCallButton).translationY(0); ViewCompat.animate(endCallButton).alpha(1).withEndAction(() -> { // Note: This is to work around an Android bug, see #6225 endCallButton.requestLayout(); }); this.minimized = false; } } @Override public void onModified(Recipient recipient) { Util.runOnMain(() -> { if (recipient == WebRtcCallScreen.this.recipient) { setPersonInfo(recipient); } }); } public interface HangupButtonListener { void onClick(); } }