Implement new call screen UI/UX.

This commit is contained in:
Alex Hart
2020-04-23 16:20:59 -03:00
committed by Greyson Parrelli
parent 33e3f78be6
commit d5419ec9fa
73 changed files with 2793 additions and 1142 deletions

View File

@@ -18,35 +18,47 @@
package org.thoughtcrime.securesms;
import android.Manifest;
import android.app.Activity;
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;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import org.thoughtcrime.securesms.logging.Log;
import android.view.View;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.util.Rational;
import android.view.Window;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProviders;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.components.webrtc.WebRtcAnswerDeclineButton;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallControls;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallScreen;
import org.thoughtcrime.securesms.components.TooltipPopup;
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.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.VerifySpan;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.webrtc.SurfaceViewRenderer;
import org.whispersystems.libsignal.IdentityKey;
@@ -54,7 +66,8 @@ import org.whispersystems.libsignal.SignalProtocolAddress;
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
public class WebRtcCallActivity extends Activity {
public class WebRtcCallActivity extends AppCompatActivity {
private static final String TAG = WebRtcCallActivity.class.getSimpleName();
@@ -67,8 +80,10 @@ public class WebRtcCallActivity extends Activity {
public static final String EXTRA_ENABLE_VIDEO_IF_AVAILABLE = WebRtcCallActivity.class.getCanonicalName() + ".ENABLE_VIDEO_IF_AVAILABLE";
private WebRtcCallScreen callScreen;
private boolean enableVideoIfAvailable;
private WebRtcCallView callScreen;
private TooltipPopup videoTooltip;
private WebRtcCallViewModel viewModel;
private boolean enableVideoIfAvailable;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -79,10 +94,12 @@ public class WebRtcCallActivity extends Activity {
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.webrtc_call_activity);
getSupportActionBar().hide();
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
initializeResources();
initializeViewModel();
processIntent(getIntent());
@@ -90,18 +107,21 @@ public class WebRtcCallActivity extends Activity {
getIntent().removeExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE);
}
@Override
public void onResume() {
Log.i(TAG, "onResume()");
super.onResume();
initializeScreenshotSecurity();
EventBus.getDefault().register(this);
if (!EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().register(this);
}
}
@Override
public void onNewIntent(Intent intent){
Log.i(TAG, "onNewIntent");
super.onNewIntent(intent);
processIntent(intent);
}
@@ -109,6 +129,17 @@ public class WebRtcCallActivity extends Activity {
public void onPause() {
Log.i(TAG, "onPause");
super.onPause();
if (!isInPipMode()) {
EventBus.getDefault().unregister(this);
}
}
@Override
protected void onStop() {
Log.i(TAG, "onStop");
super.onStop();
EventBus.getDefault().unregister(this);
}
@@ -122,9 +153,31 @@ public class WebRtcCallActivity extends Activity {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
@Override
protected void onUserLeaveHint() {
if (deviceSupportsPipMode()) {
PictureInPictureParams params = new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(16, 9))
.build();
setPictureInPictureParams(params);
//noinspection deprecation
enterPictureInPictureMode();
}
}
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
viewModel.setIsInPipMode(isInPictureInPictureMode);
}
private boolean isInPipMode() {
return deviceSupportsPipMode() && isInPictureInPictureMode();
}
private void processIntent(@NonNull Intent intent) {
if (ANSWER_ACTION.equals(intent.getAction())) {
handleAnswerCall();
handleAnswerWithAudio();
} else if (DENY_ACTION.equals(intent.getAction())) {
handleDenyCall();
} else if (END_CALL_ACTION.equals(intent.getAction())) {
@@ -142,13 +195,61 @@ public class WebRtcCallActivity extends Activity {
private void initializeResources() {
callScreen = ViewUtil.findById(this, R.id.callScreen);
callScreen.setHangupButtonListener(new HangupButtonListener());
callScreen.setIncomingCallActionListener(new IncomingCallActionListener());
callScreen.setAudioMuteButtonListener(new AudioMuteButtonListener());
callScreen.setVideoMuteButtonListener(new VideoMuteButtonListener());
callScreen.setCameraFlipButtonListener(new CameraFlipButtonListener());
callScreen.setSpeakerButtonListener(new SpeakerButtonListener());
callScreen.setBluetoothButtonListener(new BluetoothButtonListener());
callScreen.setControlsListener(new ControlsListener());
}
private void initializeViewModel() {
viewModel = ViewModelProviders.of(this).get(WebRtcCallViewModel.class);
viewModel.setIsInPipMode(isInPipMode());
viewModel.getRemoteVideoEnabled().observe(this,callScreen::setRemoteVideoEnabled);
viewModel.getBluetoothEnabled().observe(this, callScreen::setBluetoothEnabled);
viewModel.getAudioOutput().observe(this, callScreen::setAudioOutput);
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
viewModel.getCameraDirection().observe(this, callScreen::setCameraDirection);
viewModel.getLocalRenderState().observe(this, callScreen::setLocalRenderState);
viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
viewModel.getEvents().observe(this, this::handleViewModelEvent);
viewModel.getCallTime().observe(this, this::handleCallTime);
viewModel.displaySquareCallCard().observe(this, callScreen::showCallCard);
viewModel.isMoreThanOneCameraAvailable().observe(this, callScreen::showCameraToggleButton);
}
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
if (isInPipMode()) {
return;
}
switch (event) {
case SHOW_VIDEO_TOOLTIP:
if (videoTooltip == null) {
videoTooltip = TooltipPopup.forTarget(callScreen.getVideoTooltipTarget())
.setBackgroundTint(ContextCompat.getColor(this, R.color.core_ultramarine))
.setTextColor(ContextCompat.getColor(this, R.color.core_white))
.setText(R.string.WebRtcCallActivity__tap_here_to_turn_on_your_video)
.setOnDismissListener(() -> viewModel.onDismissedVideoTooltip())
.show(TooltipPopup.POSITION_ABOVE);
return;
}
break;
case DISMISS_VIDEO_TOOLTIP:
if (videoTooltip != null) {
videoTooltip.dismiss();
videoTooltip = null;
}
break;
default:
throw new IllegalArgumentException("Unknown event: " + event);
}
}
private void handleCallTime(long callTime) {
EllapsedTimeFormatter ellapsedTimeFormatter = EllapsedTimeFormatter.fromDurationMillis(callTime);
if (ellapsedTimeFormatter == null) {
return;
}
callScreen.setStatus(getString(R.string.WebRtcCallActivity__signal_s, ellapsedTimeFormatter.toString()));
}
private void handleSetAudioSpeaker(boolean enabled) {
@@ -173,10 +274,24 @@ public class WebRtcCallActivity extends Activity {
}
private void handleSetMuteVideo(boolean muted) {
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_SET_ENABLE_VIDEO);
intent.putExtra(WebRtcCallService.EXTRA_ENABLE, !muted);
startService(intent);
Recipient recipient = viewModel.getRecipient().get();
if (!recipient.equals(Recipient.UNKNOWN)) {
String recipientDisplayName = recipient.getDisplayName(this);
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera, recipientDisplayName), R.drawable.ic_video_solid_24_tinted)
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera, recipientDisplayName))
.onAllGranted(() -> {
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_SET_ENABLE_VIDEO);
intent.putExtra(WebRtcCallService.EXTRA_ENABLE, !muted);
startService(intent);
})
.execute();
}
}
private void handleFlipCamera() {
@@ -185,18 +300,19 @@ public class WebRtcCallActivity extends Activity {
startService(intent);
}
private void handleAnswerCall() {
WebRtcViewModel event = EventBus.getDefault().getStickyEvent(WebRtcViewModel.class);
private void handleAnswerWithAudio() {
Recipient recipient = viewModel.getRecipient().get();
if (event != null) {
if (!recipient.equals(Recipient.UNKNOWN)) {
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
.request(Manifest.permission.RECORD_AUDIO)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, event.getRecipient().toShortString(this)),
R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted)
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, recipient.getDisplayName(this)),
R.drawable.ic_mic_solid_24)
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
.onAllGranted(() -> {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_answering), event.getLocalRenderer());
callScreen.setRecipient(recipient);
callScreen.setStatus(getString(R.string.RedPhone_answering));
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_ACCEPT_CALL);
@@ -207,15 +323,42 @@ public class WebRtcCallActivity extends Activity {
}
}
private void handleDenyCall() {
WebRtcViewModel event = EventBus.getDefault().getStickyEvent(WebRtcViewModel.class);
private void handleAnswerWithVideo() {
Recipient recipient = viewModel.getRecipient().get();
if (event != null) {
if (!recipient.equals(Recipient.UNKNOWN)) {
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, recipient.getDisplayName(this)),
R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted)
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
.onAllGranted(() -> {
callScreen.setRecipient(recipient);
callScreen.setStatus(getString(R.string.RedPhone_answering));
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_ACCEPT_CALL);
intent.putExtra(WebRtcCallService.EXTRA_ANSWER_WITH_VIDEO, true);
startService(intent);
handleSetMuteVideo(false);
})
.onAnyDenied(this::handleDenyCall)
.execute();
}
}
private void handleDenyCall() {
Recipient recipient = viewModel.getRecipient().get();
if (!recipient.equals(Recipient.UNKNOWN)) {
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_DENY_CALL);
startService(intent);
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_ending_call), event.getLocalRenderer());
callScreen.setRecipient(recipient);
callScreen.setStatus(getString(R.string.RedPhone_ending_call));
delayedFinish();
}
}
@@ -228,46 +371,53 @@ public class WebRtcCallActivity extends Activity {
}
private void handleIncomingCall(@NonNull WebRtcViewModel event) {
callScreen.setIncomingCall(event.getRecipient());
callScreen.setRecipient(event.getRecipient());
}
private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_dialing), event.getLocalRenderer());
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling));
}
private void handleTerminate(@NonNull Recipient recipient, @NonNull SurfaceViewRenderer localRenderer /*, int terminationType */) {
Log.i(TAG, "handleTerminate called");
callScreen.setActiveCall(recipient, getString(R.string.RedPhone_ending_call), localRenderer);
callScreen.setRecipient(recipient);
callScreen.setStatus(getString(R.string.RedPhone_ending_call));
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
delayedFinish();
}
private void handleCallRinging(@NonNull WebRtcViewModel event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_ringing), event.getLocalRenderer());
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_ringing));
}
private void handleCallBusy(@NonNull WebRtcViewModel event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_busy), event.getLocalRenderer());
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_busy));
delayedFinish(BUSY_SIGNAL_DELAY_FINISH);
}
private void handleCallConnected(@NonNull WebRtcViewModel event) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_connected), "", event.getLocalRenderer(), event.getRemoteRenderer());
callScreen.setRecipient(event.getRecipient());
}
private void handleRecipientUnavailable(@NonNull WebRtcViewModel event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_recipient_unavailable), event.getLocalRenderer());
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable));
delayedFinish();
}
private void handleServerFailure(@NonNull WebRtcViewModel event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_network_failed), event.getLocalRenderer());
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_network_failed));
delayedFinish();
}
@@ -294,31 +444,50 @@ public class WebRtcCallActivity extends Activity {
}
private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
final IdentityKey theirIdentity = event.getIdentityKey();
final Recipient recipient = event.getRecipient();
final IdentityKey theirKey = event.getIdentityKey();
final Recipient recipient = event.getRecipient();
callScreen.setUntrustedIdentity(recipient, theirIdentity);
callScreen.setAcceptIdentityListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
synchronized (SESSION_LOCK) {
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(WebRtcCallActivity.this);
identityKeyStore.saveIdentity(new SignalProtocolAddress(recipient.requireServiceId(), 1), theirIdentity, true);
}
if (theirKey == null) {
handleTerminate(recipient, event.getLocalRenderer());
}
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(recipient.getId()));
startService(intent);
}
});
String name = recipient.getDisplayName(this);
String introduction = getString(R.string.WebRtcCallScreen_new_safety_numbers, name, name);
SpannableString spannableString = new SpannableString(introduction + " " + getString(R.string.WebRtcCallScreen_you_may_wish_to_verify_this_contact));
callScreen.setCancelIdentityButton(new View.OnClickListener() {
@Override
public void onClick(View v) {
handleTerminate(recipient, event.getLocalRenderer());
}
});
spannableString.setSpan(new VerifySpan(this, recipient.getId(), theirKey), introduction.length() + 1, spannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
AppCompatTextView untrustedIdentityExplanation = new AppCompatTextView(this);
untrustedIdentityExplanation.setText(spannableString);
untrustedIdentityExplanation.setMovementMethod(LinkMovementMethod.getInstance());
new AlertDialog.Builder(this)
.setView(untrustedIdentityExplanation)
.setPositiveButton(R.string.WebRtcCallScreen_accept, (d, w) -> {
synchronized (SESSION_LOCK) {
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(WebRtcCallActivity.this);
identityKeyStore.saveIdentity(new SignalProtocolAddress(recipient.requireServiceId(), 1), theirKey, true);
}
d.dismiss();
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(recipient.getId()));
startService(intent);
})
.setNegativeButton(R.string.WebRtcCallScreen_end_call, (d, w) -> {
d.dismiss();
handleTerminate(recipient, event.getLocalRenderer());
})
.show();
}
private boolean deviceSupportsPipMode() {
return Build.VERSION.SDK_INT >= 26 &&
FeatureFlags.callingPip() &&
getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
}
private void delayedFinish() {
@@ -326,17 +495,15 @@ public class WebRtcCallActivity extends Activity {
}
private void delayedFinish(int delayMillis) {
callScreen.postDelayed(new Runnable() {
public void run() {
WebRtcCallActivity.this.finish();
}
}, delayMillis);
callScreen.postDelayed(WebRtcCallActivity.this::finish, delayMillis);
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(final WebRtcViewModel event) {
Log.i(TAG, "Got message from service: " + event);
viewModel.setRecipient(event.getRecipient());
switch (event.getState()) {
case CALL_CONNECTED: handleCallConnected(event); break;
case NETWORK_FAILURE: handleServerFailure(event); break;
@@ -350,10 +517,10 @@ public class WebRtcCallActivity extends Activity {
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
}
callScreen.setRemoteVideoEnabled(event.isRemoteVideoEnabled());
callScreen.updateAudioState(event.isBluetoothAvailable(), event.isMicrophoneEnabled());
callScreen.setControlsEnabled(event.getState() != WebRtcViewModel.State.CALL_INCOMING);
callScreen.setLocalVideoState(event.getLocalCameraState(), event.getLocalRenderer());
callScreen.setLocalRenderer(event.getLocalRenderer());
callScreen.setRemoteRenderer(event.getRemoteRenderer());
viewModel.updateFromWebRtcViewModel(event);
if (event.getLocalCameraState().getCameraCount() > 0 && enableVideoIfAvailable) {
enableVideoIfAvailable = false;
@@ -361,56 +528,74 @@ public class WebRtcCallActivity extends Activity {
}
}
private class HangupButtonListener implements WebRtcCallScreen.HangupButtonListener {
public void onClick() {
private final class ControlsListener implements WebRtcCallView.ControlsListener {
@Override
public void onControlsFadeOut() {
if (videoTooltip != null) {
videoTooltip.dismiss();
}
}
@Override
public void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput) {
switch (audioOutput) {
case HANDSET:
handleSetAudioSpeaker(false);
break;
case HEADSET:
handleSetAudioBluetooth(true);
break;
case SPEAKER:
handleSetAudioSpeaker(true);
break;
default:
throw new IllegalStateException("Unknown output: " + audioOutput);
}
}
@Override
public void onVideoChanged(boolean isVideoEnabled) {
handleSetMuteVideo(!isVideoEnabled);
}
@Override
public void onMicChanged(boolean isMicEnabled) {
handleSetMuteAudio(!isMicEnabled);
}
@Override
public void onCameraDirectionChanged() {
handleFlipCamera();
}
@Override
public void onEndCallPressed() {
handleEndCall();
}
}
private class AudioMuteButtonListener implements WebRtcCallControls.MuteButtonListener {
@Override
public void onToggle(boolean isMuted) {
WebRtcCallActivity.this.handleSetMuteAudio(isMuted);
}
}
private class VideoMuteButtonListener implements WebRtcCallControls.MuteButtonListener {
@Override
public void onToggle(boolean isMuted) {
WebRtcCallActivity.this.handleSetMuteVideo(isMuted);
}
}
private class CameraFlipButtonListener implements WebRtcCallControls.CameraFlipButtonListener {
@Override
public void onToggle() {
WebRtcCallActivity.this.handleFlipCamera();
}
}
private class SpeakerButtonListener implements WebRtcCallControls.SpeakerButtonListener {
@Override
public void onSpeakerChange(boolean isSpeaker) {
WebRtcCallActivity.this.handleSetAudioSpeaker(isSpeaker);
}
}
private class BluetoothButtonListener implements WebRtcCallControls.BluetoothButtonListener {
@Override
public void onBluetoothChange(boolean isBluetooth) {
WebRtcCallActivity.this.handleSetAudioBluetooth(isBluetooth);
}
}
private class IncomingCallActionListener implements WebRtcAnswerDeclineButton.AnswerDeclineListener {
@Override
public void onAnswered() {
WebRtcCallActivity.this.handleAnswerCall();
public void onDenyCallPressed() {
handleDenyCall();
}
@Override
public void onDeclined() {
WebRtcCallActivity.this.handleDenyCall();
public void onAcceptCallWithVoiceOnlyPressed() {
handleAnswerWithAudio();
}
@Override
public void onAcceptCallPressed() {
if (viewModel.isAnswerWithVideoAvailable()) {
handleAnswerWithVideo();
} else {
handleAnswerWithAudio();
}
}
@Override
public void onDownCaretPressed() {
}
}

View File

@@ -6,7 +6,6 @@ import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.provider.ContactsContract;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
@@ -24,7 +23,6 @@ import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientExporter;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;

View File

@@ -0,0 +1,59 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import java.util.List;
final class AudioOutputAdapter extends RecyclerView.Adapter<AudioOutputAdapter.AudioOutputViewHolder> {
private final Consumer<WebRtcAudioOutput> consumer;
private final List<WebRtcAudioOutput> audioOutputs;
AudioOutputAdapter(@NonNull Consumer<WebRtcAudioOutput> consumer, @NonNull List<WebRtcAudioOutput> audioOutputs) {
this.audioOutputs = audioOutputs;
this.consumer = consumer;
}
@Override
public @NonNull AudioOutputViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new AudioOutputViewHolder((TextView) LayoutInflater.from(parent.getContext()).inflate(R.layout.audio_output_adapter_item, parent, false), consumer);
}
@Override
public void onBindViewHolder(@NonNull AudioOutputViewHolder holder, int position) {
WebRtcAudioOutput audioOutput = audioOutputs.get(position);
holder.view.setText(audioOutput.getLabelRes());
holder.view.setCompoundDrawablesRelativeWithIntrinsicBounds(audioOutput.getIconRes(), 0, 0, 0);
}
@Override
public int getItemCount() {
return audioOutputs.size();
}
final static class AudioOutputViewHolder extends RecyclerView.ViewHolder {
private final TextView view;
AudioOutputViewHolder(@NonNull TextView itemView, @NonNull Consumer<WebRtcAudioOutput> consumer) {
super(itemView);
view = itemView;
itemView.setOnClickListener(v -> {
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
consumer.accept(WebRtcAudioOutput.values()[getAdapterPosition()]);
}
});
}
}
}

View File

@@ -0,0 +1,284 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.animation.Animator;
import android.annotation.SuppressLint;
import android.graphics.Point;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import androidx.annotation.NonNull;
import androidx.core.view.GestureDetectorCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout;
import java.util.Arrays;
public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestureListener {
private static final float DECELERATION_RATE = 0.99f;
private final ViewGroup parent;
private final View child;
private final int framePadding;
private final int pipWidth;
private final int pipHeight;
private int activePointerId = MotionEvent.INVALID_POINTER_ID;
private float lastTouchX;
private float lastTouchY;
private boolean isDragging;
private boolean isAnimating;
private int extraPaddingTop;
private int extraPaddingBottom;
private double projectionX;
private double projectionY;
private VelocityTracker velocityTracker;
private int maximumFlingVelocity;
@SuppressLint("ClickableViewAccessibility")
public static PictureInPictureGestureHelper applyTo(@NonNull View child) {
TouchInterceptingFrameLayout parent = (TouchInterceptingFrameLayout) child.getParent();
PictureInPictureGestureHelper helper = new PictureInPictureGestureHelper(parent, child);
GestureDetectorCompat gestureDetector = new GestureDetectorCompat(child.getContext(), helper);
parent.setOnInterceptTouchEventListener((event) -> {
if (helper.velocityTracker == null) {
helper.velocityTracker = VelocityTracker.obtain();
}
helper.velocityTracker.addMovement(event);
return false;
});
parent.setOnTouchListener((v, event) -> {
if (helper.velocityTracker != null) {
helper.velocityTracker.recycle();
helper.velocityTracker = null;
}
return false;
});
child.setOnTouchListener((v, event) -> {
boolean handled = gestureDetector.onTouchEvent(event);
if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
if (!handled) {
handled = helper.onGestureFinished(event);
}
if (helper.velocityTracker != null) {
helper.velocityTracker.recycle();
helper.velocityTracker = null;
}
}
return handled;
});
return helper;
}
private PictureInPictureGestureHelper(@NonNull ViewGroup parent, @NonNull View child) {
this.parent = parent;
this.child = child;
this.framePadding = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_frame_padding);
this.pipWidth = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_pip_width);
this.pipHeight = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_pip_height);
this.maximumFlingVelocity = ViewConfiguration.get(child.getContext()).getScaledMaximumFlingVelocity();
}
public void clearVerticalBoundaries() {
setVerticalBoundaries(0, parent.getMeasuredHeight());
}
public void setVerticalBoundaries(int topBoundary, int bottomBoundary) {
extraPaddingTop = topBoundary;
extraPaddingBottom = parent.getMeasuredHeight() - bottomBoundary;
if (isAnimating) {
fling();
} else if (!isDragging) {
onFling(null, null, 0, 0);
}
}
private boolean onGestureFinished(MotionEvent e) {
final int pointerIndex = e.findPointerIndex(activePointerId);
if (e.getActionIndex() == pointerIndex) {
onFling(e, e, 0, 0);
return true;
}
return false;
}
@Override
public boolean onDown(MotionEvent e) {
activePointerId = e.getPointerId(0);
lastTouchX = e.getX(activePointerId) + child.getX();
lastTouchY = e.getY(activePointerId) + child.getY();
isDragging = true;
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
int pointerIndex = e2.findPointerIndex(activePointerId);
float x = e2.getX(pointerIndex) + child.getX();
float y = e2.getY(pointerIndex) + child.getY();
float dx = x - lastTouchX;
float dy = y - lastTouchY;
child.setTranslationX(child.getTranslationX() + dx);
child.setTranslationY(child.getTranslationY() + dy);
lastTouchX = x;
lastTouchY = y;
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (velocityTracker != null) {
velocityTracker.computeCurrentVelocity(1000, maximumFlingVelocity);
projectionX = child.getX() + project(velocityTracker.getXVelocity());
projectionY = child.getY() + project(velocityTracker.getYVelocity());
} else {
projectionX = child.getX();
projectionY = child.getY();
}
fling();
return true;
}
private void fling() {
Point projection = new Point((int) projectionX, (int) projectionY);
Point nearestCornerPosition = findNearestCornerPosition(projection);
isAnimating = true;
isDragging = false;
child.animate()
.translationX(getTranslationXForPoint(nearestCornerPosition))
.translationY(getTranslationYForPoint(nearestCornerPosition))
.setDuration(250)
.setInterpolator(new ViscousFluidInterpolator())
.setListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
isAnimating = false;
}
})
.start();
}
private Point findNearestCornerPosition(Point projection) {
Point maxPoint = null;
double maxDistance = Double.MAX_VALUE;
for (Point point : Arrays.asList(calculateTopLeftCoordinates(),
calculateTopRightCoordinates(parent),
calculateBottomLeftCoordinates(parent),
calculateBottomRightCoordinates(parent)))
{
double distance = distance(point, projection);
if (distance < maxDistance) {
maxDistance = distance;
maxPoint = point;
}
}
return maxPoint;
}
private float getTranslationXForPoint(Point destination) {
return destination.x - child.getLeft();
}
private float getTranslationYForPoint(Point destination) {
return destination.y - child.getTop();
}
private Point calculateTopLeftCoordinates() {
return new Point(framePadding,
framePadding + extraPaddingTop);
}
private Point calculateTopRightCoordinates(@NonNull ViewGroup parent) {
return new Point(parent.getMeasuredWidth() - pipWidth - framePadding,
framePadding + extraPaddingTop);
}
private Point calculateBottomLeftCoordinates(@NonNull ViewGroup parent) {
return new Point(framePadding,
parent.getMeasuredHeight() - pipHeight - framePadding - extraPaddingBottom);
}
private Point calculateBottomRightCoordinates(@NonNull ViewGroup parent) {
return new Point(parent.getMeasuredWidth() - pipWidth - framePadding,
parent.getMeasuredHeight() - pipHeight - framePadding - extraPaddingBottom);
}
private static float project(float initialVelocity) {
return (initialVelocity / 1000f) * DECELERATION_RATE / (1f - DECELERATION_RATE);
}
private static double distance(Point a, Point b) {
return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
}
/** Borrowed from ScrollView */
private static class ViscousFluidInterpolator implements Interpolator {
/** Controls the viscous fluid effect (how much of it). */
private static final float VISCOUS_FLUID_SCALE = 8.0f;
private static final float VISCOUS_FLUID_NORMALIZE;
private static final float VISCOUS_FLUID_OFFSET;
static {
// must be set to 1.0 (used in viscousFluid())
VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f);
// account for very small floating-point error
VISCOUS_FLUID_OFFSET = 1.0f - VISCOUS_FLUID_NORMALIZE * viscousFluid(1.0f);
}
private static float viscousFluid(float x) {
x *= VISCOUS_FLUID_SCALE;
if (x < 1.0f) {
x -= (1.0f - (float)Math.exp(-x));
} else {
float start = 0.36787944117f; // 1/e == exp(-1)
x = 1.0f - (float)Math.exp(1.0f - x);
x = start + x * (1.0f - start);
}
return x;
}
@Override
public float getInterpolation(float input) {
final float interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input);
if (interpolated > 0) {
return interpolated + VISCOUS_FLUID_OFFSET;
}
return interpolated;
}
}
}

View File

@@ -0,0 +1,28 @@
package org.thoughtcrime.securesms.components.webrtc;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import org.thoughtcrime.securesms.R;
public enum WebRtcAudioOutput {
HANDSET(R.string.WebRtcAudioOutputToggle__phone, R.drawable.ic_phone_right_black_28),
SPEAKER(R.string.WebRtcAudioOutputToggle__speaker, R.drawable.ic_speaker_solid_black_28),
HEADSET(R.string.WebRtcAudioOutputToggle__bluetooth, R.drawable.ic_speaker_bt_solid_black_28);
private final @StringRes int labelRes;
private final @DrawableRes int iconRes;
WebRtcAudioOutput(@StringRes int labelRes, @DrawableRes int iconRes) {
this.labelRes = labelRes;
this.iconRes = iconRes;
}
public int getIconRes() {
return iconRes;
}
public int getLabelRes() {
return labelRes;
}
}

View File

@@ -0,0 +1,163 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import java.util.Arrays;
import java.util.List;
public class WebRtcAudioOutputToggleButton extends AppCompatImageView {
private static final String STATE_OUTPUT_INDEX = "audio.output.toggle.state.output.index";
private static final String STATE_HEADSET_ENABLED = "audio.output.toggle.state.headset.enabled";
private static final String STATE_PARENT = "audio.output.toggle.state.parent";
private static final int[] OUTPUT_HANDSET = { R.attr.state_handset };
private static final int[] OUTPUT_SPEAKER = { R.attr.state_speaker };
private static final int[] OUTPUT_HEADSET = { R.attr.state_headset };
private static final int[][] OUTPUT_ENUM = { OUTPUT_HANDSET, OUTPUT_SPEAKER, OUTPUT_HEADSET };
private static final List<WebRtcAudioOutput> OUTPUT_MODES = Arrays.asList(WebRtcAudioOutput.HANDSET, WebRtcAudioOutput.SPEAKER, WebRtcAudioOutput.HEADSET);
private static final WebRtcAudioOutput OUTPUT_FALLBACK = WebRtcAudioOutput.HANDSET;
private boolean isHeadsetAvailable;
private int outputIndex;
private OnAudioOutputChangedListener audioOutputChangedListener;
private AlertDialog picker;
public WebRtcAudioOutputToggleButton(Context context) {
this(context, null);
}
public WebRtcAudioOutputToggleButton(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public WebRtcAudioOutputToggleButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
super.setOnClickListener((v) -> {
if (isHeadsetAvailable) showPicker();
else setAudioOutput(OUTPUT_MODES.get((outputIndex + 1) % OUTPUT_ENUM.length));
});
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] extra = OUTPUT_ENUM[outputIndex];
final int[] drawableState = super.onCreateDrawableState(extraSpace + extra.length);
mergeDrawableStates(drawableState, extra);
return drawableState;
}
@Override
public void setOnClickListener(@Nullable OnClickListener l) {
throw new UnsupportedOperationException("This View does not support custom click listeners.");
}
public void setIsHeadsetAvailable(boolean isHeadsetAvailable) {
this.isHeadsetAvailable = isHeadsetAvailable;
setAudioOutput(OUTPUT_MODES.get(outputIndex));
}
public void setAudioOutput(@NonNull WebRtcAudioOutput audioOutput) {
int oldIndex = outputIndex;
outputIndex = resolveAudioOutputIndex(OUTPUT_MODES.indexOf(audioOutput), isHeadsetAvailable);
if (oldIndex != outputIndex) {
refreshDrawableState();
notifyListener();
}
}
public void setOnAudioOutputChangedListener(@Nullable OnAudioOutputChangedListener listener) {
this.audioOutputChangedListener = listener;
}
private void showPicker() {
RecyclerView rv = new RecyclerView(getContext());
rv.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false));
rv.setAdapter(new AudioOutputAdapter(this::setAudioOutputViaDialog, OUTPUT_MODES));
picker = new AlertDialog.Builder(getContext())
.setView(rv)
.show();
}
private void hidePicker() {
if (picker != null) {
picker.dismiss();
picker = null;
}
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable parentState = super.onSaveInstanceState();
Bundle bundle = new Bundle();
bundle.putParcelable(STATE_PARENT, parentState);
bundle.putInt(STATE_OUTPUT_INDEX, outputIndex);
bundle.putBoolean(STATE_HEADSET_ENABLED, isHeadsetAvailable);
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle savedState = (Bundle) state;
isHeadsetAvailable = savedState.getBoolean(STATE_HEADSET_ENABLED);
setAudioOutput(OUTPUT_MODES.get(
resolveAudioOutputIndex(savedState.getInt(STATE_OUTPUT_INDEX), isHeadsetAvailable))
);
super.onRestoreInstanceState(savedState.getParcelable(STATE_PARENT));
} else {
super.onRestoreInstanceState(state);
}
}
private void notifyListener() {
if (audioOutputChangedListener == null) return;
audioOutputChangedListener.audioOutputChanged(OUTPUT_MODES.get(outputIndex));
}
private void setAudioOutputViaDialog(@NonNull WebRtcAudioOutput audioOutput) {
setAudioOutput(audioOutput);
hidePicker();
}
private static int resolveAudioOutputIndex(int desiredAudioOutputIndex, boolean isHeadsetAvailable) {
if (isIllegalAudioOutputIndex(desiredAudioOutputIndex)) {
throw new IllegalArgumentException("Unsupported index: " + desiredAudioOutputIndex);
}
if (isUnsupportedAudioOutput(desiredAudioOutputIndex, isHeadsetAvailable)) {
return OUTPUT_MODES.indexOf(OUTPUT_FALLBACK);
}
return desiredAudioOutputIndex;
}
private static boolean isIllegalAudioOutputIndex(int desiredFlashIndex) {
return desiredFlashIndex < 0 || desiredFlashIndex > OUTPUT_ENUM.length;
}
private static boolean isUnsupportedAudioOutput(int desiredAudioOutputIndex, boolean isHeadsetAvailable) {
return OUTPUT_MODES.get(desiredAudioOutputIndex) == WebRtcAudioOutput.HEADSET && !isHeadsetAvailable;
}
public interface OnAudioOutputChangedListener {
void audioOutputChanged(WebRtcAudioOutput audioOutput);
}
}

View File

@@ -1,217 +0,0 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.AudioManager;
import android.os.Build;
import androidx.annotation.NonNull;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.LinearLayout;
import com.tomergoldst.tooltips.ToolTip;
import com.tomergoldst.tooltips.ToolTipsManager;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AccessibleToggleButton;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
public class WebRtcCallControls extends LinearLayout {
private static final String TAG = WebRtcCallControls.class.getSimpleName();
private AccessibleToggleButton audioMuteButton;
private AccessibleToggleButton videoMuteButton;
private AccessibleToggleButton speakerButton;
private AccessibleToggleButton bluetoothButton;
private AccessibleToggleButton cameraFlipButton;
private boolean cameraFlipAvailable;
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public WebRtcCallControls(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initialize();
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public WebRtcCallControls(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
public WebRtcCallControls(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public WebRtcCallControls(Context context) {
super(context);
initialize();
}
private void initialize() {
LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.webrtc_call_controls, this, true);
this.speakerButton = ViewUtil.findById(this, R.id.speakerButton);
this.bluetoothButton = ViewUtil.findById(this, R.id.bluetoothButton);
this.audioMuteButton = ViewUtil.findById(this, R.id.muteButton);
this.videoMuteButton = ViewUtil.findById(this, R.id.video_mute_button);
this.cameraFlipButton = ViewUtil.findById(this, R.id.camera_flip_button);
}
public void setAudioMuteButtonListener(final MuteButtonListener listener) {
audioMuteButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
listener.onToggle(b);
}
});
}
public void setVideoMuteButtonListener(final MuteButtonListener listener) {
videoMuteButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
boolean videoMuted = !isChecked;
listener.onToggle(videoMuted);
cameraFlipButton.setVisibility(!videoMuted && cameraFlipAvailable ? View.VISIBLE : View.GONE);
}
});
}
public void setCameraFlipButtonListener(final CameraFlipButtonListener listener) {
cameraFlipButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
listener.onToggle();
cameraFlipButton.setEnabled(false);
}
});
}
public void setSpeakerButtonListener(final SpeakerButtonListener listener) {
speakerButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
listener.onSpeakerChange(isChecked);
}
});
}
public void setBluetoothButtonListener(final BluetoothButtonListener listener) {
bluetoothButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
listener.onBluetoothChange(isChecked);
}
});
}
public void updateAudioState(boolean isBluetoothAvailable) {
AudioManager audioManager = ServiceUtil.getAudioManager(getContext());
if (!isBluetoothAvailable) {
bluetoothButton.setVisibility(View.GONE);
} else {
bluetoothButton.setVisibility(View.VISIBLE);
}
if (audioManager.isBluetoothScoOn()) {
bluetoothButton.setChecked(true, false);
speakerButton.setChecked(false, false);
} else if (audioManager.isSpeakerphoneOn()) {
speakerButton.setChecked(true, false);
bluetoothButton.setChecked(false, false);
} else {
speakerButton.setChecked(false, false);
bluetoothButton.setChecked(false, false);
}
}
public boolean isVideoEnabled() {
return videoMuteButton.isChecked();
}
public void setVideoEnabled(boolean enabled) {
videoMuteButton.setChecked(enabled, false);
}
public void setVideoAvailable(boolean available) {
videoMuteButton.setVisibility(available ? VISIBLE : GONE);
}
public void setCameraFlipButtonEnabled(boolean enabled) {
cameraFlipButton.setChecked(enabled, false);
}
public void setCameraFlipAvailable(boolean available) {
cameraFlipAvailable = available;
cameraFlipButton.setVisibility(cameraFlipAvailable && isVideoEnabled() ? View.VISIBLE : View.GONE);
}
public void setCameraFlipClickable(boolean clickable) {
setControlEnabled(cameraFlipButton, clickable);
}
public void setMicrophoneEnabled(boolean enabled) {
audioMuteButton.setChecked(!enabled, false);
}
public void setControlsEnabled(boolean enabled) {
setControlEnabled(speakerButton, enabled);
setControlEnabled(bluetoothButton, enabled);
setControlEnabled(videoMuteButton, enabled);
setControlEnabled(cameraFlipButton, enabled);
setControlEnabled(audioMuteButton, enabled);
}
private void setControlEnabled(@NonNull View view, boolean enabled) {
if (enabled) {
view.setAlpha(1.0f);
view.setEnabled(true);
} else {
view.setAlpha(0.3f);
view.setEnabled(false);
}
}
public void displayVideoTooltip(ViewGroup viewGroup) {
if (videoMuteButton.getVisibility() == VISIBLE) {
final ToolTipsManager toolTipsManager = new ToolTipsManager();
ToolTip toolTip = new ToolTip.Builder(getContext(), videoMuteButton, viewGroup,
getContext().getString(R.string.WebRtcCallControls_tap_to_enable_your_video),
ToolTip.POSITION_BELOW).build();
toolTipsManager.show(toolTip);
videoMuteButton.postDelayed(() -> toolTipsManager.findAndDismiss(videoMuteButton), 4000);
}
}
public static interface MuteButtonListener {
public void onToggle(boolean isMuted);
}
public static interface CameraFlipButtonListener {
public void onToggle();
}
public static interface SpeakerButtonListener {
public void onSpeakerChange(boolean isSpeaker);
}
public static interface BluetoothButtonListener {
public void onBluetoothChange(boolean isBluetooth);
}
}

View File

@@ -0,0 +1,25 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.media.AudioManager;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.ServiceUtil;
class WebRtcCallRepository {
private final AudioManager audioManager;
WebRtcCallRepository() {
this.audioManager = ServiceUtil.getAudioManager(ApplicationDependencies.getApplication());
}
WebRtcAudioOutput getAudioOutput() {
if (audioManager.isBluetoothScoOn()) {
return WebRtcAudioOutput.HEADSET;
} else if (audioManager.isSpeakerphoneOn()) {
return WebRtcAudioOutput.SPEAKER;
} else {
return WebRtcAudioOutput.HANDSET;
}
}
}

View File

@@ -1,433 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import android.text.SpannableString;
import android.text.Spanned;
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.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.VerifySpan;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.webrtc.RendererCommon;
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 RecipientForeverObserver {
@SuppressWarnings("unused")
private static final String TAG = WebRtcCallScreen.class.getSimpleName();
private ImageView photo;
private SurfaceViewRenderer localRenderer;
private PercentFrameLayout localRenderLayout;
private PercentFrameLayout remoteRenderLayout;
private PercentFrameLayout localLargeRenderLayout;
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 LiveRecipient 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, @NonNull SurfaceViewRenderer localRenderer) {
setCard(personInfo, message);
setRinging(localRenderer);
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.get().toShortString(getContext());
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.getId(), untrustedIdentity),
introduction.length()+1, spannableString.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (this.recipient != null) this.recipient.removeForeverObserver(this);
this.recipient = personInfo.live();
this.recipient.observeForever(this);
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, @NonNull SurfaceViewRenderer localRenderer) {
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);
localRenderer.setMirror(cameraState.getActiveDirection() == CameraState.Direction.FRONT);
localRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
this.localRenderer = localRenderer;
if (localRenderLayout.getChildCount() != 0) {
displayLocalRendererInSmallLayout(!cameraState.isEnabled());
} else {
displayLocalRendererInLargeLayout(!cameraState.isEnabled());
}
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 displayLocalRendererInLargeLayout(boolean hide) {
if (localLargeRenderLayout.getChildCount() == 0) {
localRenderLayout.removeAllViews();
if (localRenderer != null) {
localLargeRenderLayout.addView(localRenderer);
}
}
localRenderLayout.setHidden(true);
localRenderLayout.requestLayout();
localLargeRenderLayout.setHidden(hide);
localLargeRenderLayout.requestLayout();
if (hide) {
photo.setVisibility(View.VISIBLE);
} else {
photo.setVisibility(View.INVISIBLE);
}
}
private void displayLocalRendererInSmallLayout(boolean hide) {
if (localRenderLayout.getChildCount() == 0) {
localLargeRenderLayout.removeAllViews();
if (localRenderer != null) {
localRenderLayout.addView(localRenderer);
}
}
localLargeRenderLayout.setHidden(true);
localLargeRenderLayout.requestLayout();
localRenderLayout.setHidden(hide);
localRenderLayout.requestLayout();
if (remoteRenderLayout.isHidden()) {
photo.setVisibility(View.VISIBLE);
}
}
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.localLargeRenderLayout = findViewById(R.id.local_large_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 setRinging(SurfaceViewRenderer localRenderer) {
if (localLargeRenderLayout.getChildCount() == 0) {
if (localRenderer.getParent() != null) {
((ViewGroup)localRenderer.getParent()).removeView(localRenderer);
}
localLargeRenderLayout.setPosition(0, 0, 100, 100);
localRenderer.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
localRenderer.setMirror(true);
localRenderer.setZOrderMediaOverlay(true);
localLargeRenderLayout.addView(localRenderer);
this.localRenderer = localRenderer;
}
}
private void setConnected(SurfaceViewRenderer localRenderer, SurfaceViewRenderer remoteRenderer) {
if (localRenderLayout.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);
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) {
GlideApp.with(getContext().getApplicationContext())
.load(recipient.getContactPhoto())
.fallback(recipient.getFallbackContactPhoto().asCallCard(getContext()))
.error(recipient.getFallbackContactPhoto().asCallCard(getContext()))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(this.photo);
if (FeatureFlags.profileDisplay()) {
this.name.setText(recipient.getDisplayName(getContext()));
if (recipient.getE164().isPresent()) {
this.phoneNumber.setText(recipient.requireE164());
this.phoneNumber.setVisibility(View.VISIBLE);
} else {
this.phoneNumber.setVisibility(View.GONE);
}
} else {
this.name.setText(recipient.getName(getContext()));
if (recipient.getName(getContext()) == null && !recipient.getProfileName().isEmpty()) {
this.phoneNumber.setText(recipient.requireE164() + " (~" + recipient.getProfileName().toString() + ")");
} else {
this.phoneNumber.setText(recipient.requireE164());
}
}
}
private void setCard(Recipient recipient, String status) {
if (this.recipient != null) this.recipient.removeForeverObserver(this);
this.recipient = recipient.live();
this.recipient.observeForever(this);
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 onRecipientChanged(@NonNull Recipient recipient) {
setPersonInfo(recipient);
}
public interface HangupButtonListener {
void onClick();
}
}

View File

@@ -0,0 +1,458 @@
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();
}
}

View File

@@ -0,0 +1,228 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.livedata.LiveDataPair;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
public class WebRtcCallViewModel extends ViewModel {
private final MutableLiveData<Boolean> remoteVideoEnabled = new MutableLiveData<>(false);
private final MutableLiveData<WebRtcAudioOutput> audioOutput = new MutableLiveData<>();
private final MutableLiveData<Boolean> bluetoothEnabled = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> microphoneEnabled = new MutableLiveData<>(true);
private final MutableLiveData<WebRtcLocalRenderState> localRenderState = new MutableLiveData<>(WebRtcLocalRenderState.GONE);
private final MutableLiveData<Boolean> isInPipMode = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> localVideoEnabled = new MutableLiveData<>(false);
private final MutableLiveData<CameraState.Direction> cameraDirection = new MutableLiveData<>(CameraState.Direction.FRONT);
private final MutableLiveData<Boolean> hasMultipleCameras = new MutableLiveData<>(false);
private final LiveData<Boolean> shouldDisplayLocal = LiveDataUtil.combineLatest(isInPipMode, localVideoEnabled, (a, b) -> !a && b);
private final LiveData<WebRtcLocalRenderState> realLocalRenderState = LiveDataUtil.combineLatest(shouldDisplayLocal, localRenderState, this::getRealLocalRenderState);
private final MutableLiveData<WebRtcControls> webRtcControls = new MutableLiveData<>(WebRtcControls.NONE);
private final LiveData<WebRtcControls> realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, webRtcControls, this::getRealWebRtcControls);
private final SingleLiveEvent<Event> events = new SingleLiveEvent<Event>();
private final MutableLiveData<Long> ellapsed = new MutableLiveData<>(-1L);
private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
private boolean canDisplayTooltipIfNeeded = true;
private boolean hasEnabledLocalVideo = false;
private long callConnectedTime = -1;
private Handler ellapsedTimeHandler = new Handler(Looper.getMainLooper());
private boolean answerWithVideoAvailable = false;
private Runnable ellapsedTimeRunnable = this::handleTick;
private final WebRtcCallRepository repository = new WebRtcCallRepository();
public WebRtcCallViewModel() {
audioOutput.setValue(repository.getAudioOutput());
}
public LiveData<Boolean> getRemoteVideoEnabled() {
return Transformations.distinctUntilChanged(remoteVideoEnabled);
}
public LiveData<WebRtcAudioOutput> getAudioOutput() {
return Transformations.distinctUntilChanged(audioOutput);
}
public LiveData<Boolean> getBluetoothEnabled() {
return Transformations.distinctUntilChanged(bluetoothEnabled);
}
public LiveData<Boolean> getMicrophoneEnabled() {
return Transformations.distinctUntilChanged(microphoneEnabled);
}
public LiveData<CameraState.Direction> getCameraDirection() {
return Transformations.distinctUntilChanged(cameraDirection);
}
public LiveData<Boolean> displaySquareCallCard() {
return isInPipMode;
}
public LiveData<WebRtcLocalRenderState> getLocalRenderState() {
return realLocalRenderState;
}
public LiveData<WebRtcControls> getWebRtcControls() {
return realWebRtcControls;
}
public LiveRecipient getRecipient() {
return liveRecipient.getValue();
}
public void setRecipient(@NonNull Recipient recipient) {
liveRecipient.setValue(recipient.live());
}
public LiveData<Event> getEvents() {
return events;
}
public LiveData<Long> getCallTime() {
return Transformations.map(ellapsed, timeInCall -> callConnectedTime == -1 ? -1 : timeInCall);
}
public LiveData<Boolean> isMoreThanOneCameraAvailable() {
return hasMultipleCameras;
}
public boolean isAnswerWithVideoAvailable() {
return answerWithVideoAvailable;
}
@MainThread
public void setIsInPipMode(boolean isInPipMode) {
this.isInPipMode.setValue(isInPipMode);
}
public void onDismissedVideoTooltip() {
canDisplayTooltipIfNeeded = false;
}
@MainThread
public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel) {
remoteVideoEnabled.setValue(webRtcViewModel.isRemoteVideoEnabled());
bluetoothEnabled.setValue(webRtcViewModel.isBluetoothAvailable());
audioOutput.setValue(repository.getAudioOutput());
microphoneEnabled.setValue(webRtcViewModel.isMicrophoneEnabled());
if (isValidCameraDirectionForUi(webRtcViewModel.getLocalCameraState().getActiveDirection())) {
cameraDirection.setValue(webRtcViewModel.getLocalCameraState().getActiveDirection());
}
hasMultipleCameras.setValue(webRtcViewModel.getLocalCameraState().getCameraCount() > 0);
localVideoEnabled.setValue(webRtcViewModel.getLocalCameraState().isEnabled());
updateLocalRenderState(webRtcViewModel.getState());
updateWebRtcControls(webRtcViewModel.getState(), webRtcViewModel.isRemoteVideoOffer());
if (webRtcViewModel.getState() == WebRtcViewModel.State.CALL_CONNECTED && callConnectedTime == -1) {
callConnectedTime = System.currentTimeMillis();
startTimer();
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_CONNECTED) {
callConnectedTime = -1;
cancelTimer();
}
if (webRtcViewModel.getLocalCameraState().isEnabled()) {
canDisplayTooltipIfNeeded = false;
hasEnabledLocalVideo = true;
events.setValue(Event.DISMISS_VIDEO_TOOLTIP);
}
// If remote video is enabled and we a) haven't shown our video and b) have not dismissed the popup
if (canDisplayTooltipIfNeeded && webRtcViewModel.isRemoteVideoEnabled() && !hasEnabledLocalVideo) {
canDisplayTooltipIfNeeded = false;
events.setValue(Event.SHOW_VIDEO_TOOLTIP);
}
}
private boolean isValidCameraDirectionForUi(CameraState.Direction direction) {
return direction == CameraState.Direction.FRONT || direction == CameraState.Direction.BACK;
}
private void updateLocalRenderState(WebRtcViewModel.State state) {
if (state == WebRtcViewModel.State.CALL_CONNECTED) {
localRenderState.setValue(WebRtcLocalRenderState.SMALL);
} else {
localRenderState.setValue(WebRtcLocalRenderState.LARGE);
}
}
private void updateWebRtcControls(WebRtcViewModel.State state, boolean isRemoteVideoOffer) {
switch (state) {
case CALL_INCOMING:
webRtcControls.setValue(isRemoteVideoOffer ? WebRtcControls.INCOMING_VIDEO : WebRtcControls.INCOMING_AUDIO);
answerWithVideoAvailable = isRemoteVideoOffer;
break;
case CALL_CONNECTED:
webRtcControls.setValue(WebRtcControls.CONNECTED);
break;
case CALL_OUTGOING:
webRtcControls.setValue(WebRtcControls.RINGING);
break;
default:
webRtcControls.setValue(WebRtcControls.ONGOING);
}
}
private @NonNull WebRtcLocalRenderState getRealLocalRenderState(boolean shouldDisplayLocalVideo, @NonNull WebRtcLocalRenderState state) {
if (shouldDisplayLocalVideo) return state;
else return WebRtcLocalRenderState.GONE;
}
private @NonNull WebRtcControls getRealWebRtcControls(boolean neverDisplayControls, @NonNull WebRtcControls controls) {
if (neverDisplayControls) return WebRtcControls.NONE;
else return controls;
}
private void startTimer() {
cancelTimer();
ellapsedTimeHandler.post(ellapsedTimeRunnable);
}
private void handleTick() {
if (callConnectedTime == -1) {
return;
}
long newValue = (System.currentTimeMillis() - callConnectedTime) / 1000;
ellapsed.postValue(newValue);
ellapsedTimeHandler.postDelayed(ellapsedTimeRunnable, 1000);
}
private void cancelTimer() {
ellapsedTimeHandler.removeCallbacks(ellapsedTimeRunnable);
}
@Override
protected void onCleared() {
super.onCleared();
cancelTimer();
}
public enum Event {
SHOW_VIDEO_TOOLTIP,
DISMISS_VIDEO_TOOLTIP
}
}

View File

@@ -0,0 +1,10 @@
package org.thoughtcrime.securesms.components.webrtc;
public enum WebRtcControls {
NONE,
ONGOING,
RINGING,
CONNECTED,
INCOMING_AUDIO,
INCOMING_VIDEO
}

View File

@@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.components.webrtc;
public enum WebRtcLocalRenderState {
GONE,
SMALL,
LARGE
}

View File

@@ -35,6 +35,7 @@ public class WebRtcViewModel {
private final boolean isBluetoothAvailable;
private final boolean isMicrophoneEnabled;
private final boolean isRemoteVideoOffer;
private final CameraState localCameraState;
private final SurfaceViewRenderer localRenderer;
@@ -47,7 +48,8 @@ public class WebRtcViewModel {
@NonNull SurfaceViewRenderer remoteRenderer,
boolean remoteVideoEnabled,
boolean isBluetoothAvailable,
boolean isMicrophoneEnabled)
boolean isMicrophoneEnabled,
boolean isRemoteVideoOffer)
{
this(state,
recipient,
@@ -57,7 +59,8 @@ public class WebRtcViewModel {
remoteRenderer,
remoteVideoEnabled,
isBluetoothAvailable,
isMicrophoneEnabled);
isMicrophoneEnabled,
isRemoteVideoOffer);
}
public WebRtcViewModel(@NonNull State state,
@@ -68,7 +71,8 @@ public class WebRtcViewModel {
@NonNull SurfaceViewRenderer remoteRenderer,
boolean remoteVideoEnabled,
boolean isBluetoothAvailable,
boolean isMicrophoneEnabled)
boolean isMicrophoneEnabled,
boolean isRemoteVideoOffer)
{
this.state = state;
this.recipient = recipient;
@@ -79,6 +83,7 @@ public class WebRtcViewModel {
this.remoteVideoEnabled = remoteVideoEnabled;
this.isBluetoothAvailable = isBluetoothAvailable;
this.isMicrophoneEnabled = isMicrophoneEnabled;
this.isRemoteVideoOffer = isRemoteVideoOffer;
}
public @NonNull State getState() {
@@ -109,6 +114,10 @@ public class WebRtcViewModel {
return isMicrophoneEnabled;
}
public boolean isRemoteVideoOffer() {
return isRemoteVideoOffer;
}
public SurfaceViewRenderer getLocalRenderer() {
return localRenderer;
}
@@ -118,6 +127,6 @@ public class WebRtcViewModel {
}
public @NonNull String toString() {
return "[State: " + state + ", recipient: " + recipient.getId().serialize() + ", identity: " + identityKey + ", remoteVideo: " + remoteVideoEnabled + ", localVideo: " + localCameraState.isEnabled() + "]";
return "[State: " + state + ", recipient: " + recipient.getId().serialize() + ", identity: " + identityKey + ", remoteVideo: " + remoteVideoEnabled + ", localVideo: " + localCameraState.isEnabled() + ", isRemoteVideoOffer: " + isRemoteVideoOffer + "]";
}
}

View File

@@ -407,7 +407,8 @@ public final class PushProcessMessageJob extends BaseJob {
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer)
.putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice())
.putExtra(WebRtcCallService.EXTRA_OFFER_DESCRIPTION, message.getDescription())
.putExtra(WebRtcCallService.EXTRA_TIMESTAMP, content.getTimestamp());
.putExtra(WebRtcCallService.EXTRA_TIMESTAMP, content.getTimestamp())
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, message.getType().getCode());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) context.startForegroundService(intent);
else context.startService(intent);

View File

@@ -0,0 +1,102 @@
package org.thoughtcrime.securesms.messagerequests;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProviders;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
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 java.util.concurrent.TimeUnit;
public class CalleeMustAcceptMessageRequestDialogFragment extends DialogFragment {
private static final long TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10);
private static final String ARG_RECIPIENT_ID = "arg.recipient.id";
private TextView description;
private AvatarImageView avatar;
private View okay;
private final Handler handler = new Handler(Looper.getMainLooper());
private final Runnable dismisser = this::dismiss;
public static DialogFragment create(@NonNull RecipientId recipientId) {
DialogFragment fragment = new CalleeMustAcceptMessageRequestDialogFragment();
Bundle args = new Bundle();
args.putParcelable(ARG_RECIPIENT_ID, recipientId);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(DialogFragment.STYLE_NO_FRAME, R.style.TextSecure_DarkNoActionBar);
}
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.callee_must_accept_message_request_dialog_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
description = view.findViewById(R.id.description);
avatar = view.findViewById(R.id.avatar);
okay = view.findViewById(R.id.okay);
avatar.setFallbackPhotoProvider(new FallbackPhotoProvider());
okay.setOnClickListener(v -> dismiss());
RecipientId recipientId = requireArguments().getParcelable(ARG_RECIPIENT_ID);
CalleeMustAcceptMessageRequestViewModel.Factory factory = new CalleeMustAcceptMessageRequestViewModel.Factory(recipientId);
CalleeMustAcceptMessageRequestViewModel viewModel = ViewModelProviders.of(this, factory).get(CalleeMustAcceptMessageRequestViewModel.class);
viewModel.getRecipient().observe(getViewLifecycleOwner(), recipient -> {
description.setText(getString(R.string.CalleeMustAcceptMessageRequestDialogFragment__s_will_get_a_message_request_from_you, recipient.getDisplayName(requireContext())));
avatar.setAvatar(GlideApp.with(this), recipient, false);
});
}
@Override
public void onResume() {
super.onResume();
handler.postDelayed(dismisser, TIMEOUT_MS);
}
@Override
public void onPause() {
super.onPause();
handler.removeCallbacks(dismisser);
}
private static class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider {
@Override
public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
return new ResourceContactPhoto(R.drawable.ic_profile_80);
}
}
}

View File

@@ -0,0 +1,37 @@
package org.thoughtcrime.securesms.messagerequests;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
public class CalleeMustAcceptMessageRequestViewModel extends ViewModel {
private final LiveData<Recipient> recipient;
private CalleeMustAcceptMessageRequestViewModel(@NonNull RecipientId recipientId) {
recipient = Recipient.live(recipientId).getLiveData();
}
public LiveData<Recipient> getRecipient() {
return recipient;
}
public static class Factory implements ViewModelProvider.Factory {
private final RecipientId recipientId;
public Factory(@NonNull RecipientId recipientId) {
this.recipientId = recipientId;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new CalleeMustAcceptMessageRequestViewModel(recipientId));
}
}
}

View File

@@ -78,7 +78,7 @@ final class RecipientDialogViewModel extends ViewModel {
recipientDialogRepository.getRecipient(recipient -> CommunicationActions.startConversation(activity, recipient, null));
}
void onSecureCallClicked(@NonNull Activity activity) {
void onSecureCallClicked(@NonNull FragmentActivity activity) {
recipientDialogRepository.getRecipient(recipient -> CommunicationActions.startVoiceCall(activity, recipient));
}

View File

@@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.ringrtc.CameraEventListener;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.ringrtc.IceCandidateParcel;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.FutureTaskListener;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.ServiceUtil;
@@ -105,10 +106,12 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
public static final String EXTRA_REMOTE_PEER = "remote_peer";
public static final String EXTRA_REMOTE_DEVICE = "remote_device";
public static final String EXTRA_OFFER_DESCRIPTION = "offer_description";
public static final String EXTRA_OFFER_TYPE = "offer_type";
public static final String EXTRA_ANSWER_DESCRIPTION = "answer_description";
public static final String EXTRA_ICE_CANDIDATES = "ice_candidates";
public static final String EXTRA_ENABLE = "enable_value";
public static final String EXTRA_BROADCAST = "broadcast";
public static final String EXTRA_ANSWER_WITH_VIDEO = "enable_video";
public static final String ACTION_OUTGOING_CALL = "CALL_OUTGOING";
public static final String ACTION_DENY_CALL = "DENY_CALL";
@@ -155,6 +158,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
private boolean remoteVideoEnabled = false;
private boolean bluetoothAvailable = false;
private boolean enableVideoOnCreate = false;
private boolean isRemoteVideoOffer = false;
private boolean acceptWithVideo = false;
private SignalServiceMessageSender messageSender;
private SignalServiceAccountManager accountManager;
@@ -299,7 +304,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
public void onCameraSwitchCompleted(@NonNull CameraState newCameraState) {
localCameraState = newCameraState;
if (activePeer != null) {
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
}
}
@@ -367,11 +372,12 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
// Handlers
private void handleReceivedOffer(Intent intent) {
CallId callId = getCallId(intent);
RemotePeer remotePeer = getRemotePeer(intent);
Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1);
String offer = intent.getStringExtra(EXTRA_OFFER_DESCRIPTION);
Long timeStamp = intent.getLongExtra(EXTRA_TIMESTAMP, -1);
CallId callId = getCallId(intent);
RemotePeer remotePeer = getRemotePeer(intent);
Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1);
String offer = intent.getStringExtra(EXTRA_OFFER_DESCRIPTION);
Long timeStamp = intent.getLongExtra(EXTRA_TIMESTAMP, -1);
OfferMessage.Type offerType = OfferMessage.Type.fromCode(intent.getStringExtra(EXTRA_OFFER_TYPE));
Log.i(TAG, "handleReceivedOffer(): id: " + callId.format(remoteDevice));
@@ -383,6 +389,16 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
return;
}
if (offerType == OfferMessage.Type.NEED_PERMISSION || FeatureFlags.profileForCalling() && !remotePeer.getRecipient().resolve().isProfileSharing()) {
Log.i(TAG, "handleReceivedOffer(): Caller is untrusted.");
intent.putExtra(EXTRA_BROADCAST, true);
handleSendHangup(intent);
insertMissedCall(remotePeer, true);
return;
}
isRemoteVideoOffer = offerType == OfferMessage.Type.VIDEO_CALL;
try {
callManager.receivedOffer(callId, remotePeer, remoteDevice, offer, timeStamp);
} catch (CallException e) {
@@ -458,7 +474,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
}
if (activePeer != null) {
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
}
}
@@ -479,7 +495,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
}
if (activePeer != null) {
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
}
}
@@ -501,7 +517,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
}
if (activePeer != null) {
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
}
}
@@ -512,7 +528,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
camera.flip();
localCameraState = camera.getCameraState();
if (activePeer != null) {
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
}
}
}
@@ -521,7 +537,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
bluetoothAvailable = intent.getBooleanExtra(EXTRA_AVAILABLE, false);
if (activePeer != null) {
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
}
}
@@ -544,7 +560,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
audioManager.setSpeakerphoneOn(true);
}
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
}
}
@@ -561,7 +577,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
private void handleStartOutgoingCall(Intent intent) {
Log.i(TAG, "handleStartOutgoingCall(): callId: " + activePeer.getCallId());
sendMessage(WebRtcViewModel.State.CALL_OUTGOING, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(WebRtcViewModel.State.CALL_OUTGOING, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
lockManager.updatePhoneState(getInCallPhoneState());
audioManager.initializeAudioForCall();
audioManager.startOutgoingRinger(OutgoingRinger.Type.RINGING);
@@ -598,7 +614,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
localCameraState = camera.getCameraState();
if (activePeer != null) {
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
}
}
});
@@ -642,7 +658,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING);
if (activePeer != null) {
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
}
}
});
@@ -659,6 +675,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
DatabaseFactory.getSmsDatabase(this).insertReceivedCall(activePeer.getId());
acceptWithVideo = intent.getBooleanExtra(EXTRA_ANSWER_WITH_VIDEO, false);
try {
callManager.acceptCall(activePeer.getCallId());
} catch (CallException e) {
@@ -667,15 +685,21 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
}
private void handleSendOffer(Intent intent) {
RemotePeer remotePeer = getRemotePeer(intent);
CallId callId = getCallId(intent);
Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1);
Boolean broadcast = intent.getBooleanExtra(EXTRA_BROADCAST, false);
String offer = intent.getStringExtra(EXTRA_OFFER_DESCRIPTION);
RemotePeer remotePeer = getRemotePeer(intent);
CallId callId = getCallId(intent);
Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1);
Boolean broadcast = intent.getBooleanExtra(EXTRA_BROADCAST, false);
String offer = intent.getStringExtra(EXTRA_OFFER_DESCRIPTION);
OfferMessage.Type offerType = OfferMessage.Type.fromCode(intent.getStringExtra(EXTRA_OFFER_TYPE));
Log.i(TAG, "handleSendOffer: id: " + callId.format(remoteDevice));
OfferMessage offerMessage = new OfferMessage(callId.longValue(), offer);
if (FeatureFlags.profileForCalling() && remotePeer.getRecipient().resolve().getProfileKey() == null) {
offer = "";
offerType = OfferMessage.Type.NEED_PERMISSION;
}
OfferMessage offerMessage = new OfferMessage(callId.longValue(), offer, offerType);
SignalServiceCallMessage callMessage = SignalServiceCallMessage.forOffer(offerMessage);
sendCallMessage(remotePeer, remoteDevice, broadcast, callMessage);
@@ -816,7 +840,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
activePeer.localRinging();
lockManager.updatePhoneState(LockManager.PhoneState.INTERACTIVE);
sendMessage(WebRtcViewModel.State.CALL_INCOMING, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(WebRtcViewModel.State.CALL_INCOMING, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
boolean shouldDisturbUserWithCall = DoNotDisturbUtil.shouldDisturbUserWithCall(getApplicationContext(), recipient);
if (shouldDisturbUserWithCall) {
startCallCardActivityIfPossible();
@@ -850,7 +874,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
}
activePeer.remoteRinging();
sendMessage(WebRtcViewModel.State.CALL_RINGING, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(WebRtcViewModel.State.CALL_RINGING, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
}
private void handleCallConnected(Intent intent) {
@@ -874,7 +898,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
lockManager.updatePhoneState(getInCallPhoneState());
}
sendMessage(WebRtcViewModel.State.CALL_CONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(WebRtcViewModel.State.CALL_CONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
unregisterPowerButtonReceiver();
@@ -889,6 +913,10 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
} catch (CallException e) {
callFailure("Enabling audio/video failed: ", e);
}
if (acceptWithVideo) {
handleSetEnableVideo(new Intent().putExtra(EXTRA_ENABLE, true));
}
}
private void handleRemoteVideoEnable(Intent intent) {
@@ -902,7 +930,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
Log.i(TAG, "handleRemoteVideoEnable: call_id: " + activePeer.getCallId());
remoteVideoEnabled = enable;
sendMessage(WebRtcViewModel.State.CALL_CONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(WebRtcViewModel.State.CALL_CONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
}
@@ -945,7 +973,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
audioManager.setSpeakerphoneOn(true);
}
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(viewModelStateFor(activePeer), activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
}
private void handleLocalHangup(Intent intent) {
@@ -957,13 +985,13 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
Log.i(TAG, "handleLocalHangup(): call_id: " + activePeer.getCallId());
if (activePeer.getState() == CallState.RECEIVED_BUSY) {
sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
terminate();
} else {
accountManager.cancelInFlightRequests();
messageSender.cancelInFlightRequests();
sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
try {
callManager.hangup();
@@ -1011,9 +1039,9 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
if (remotePeer.callIdEquals(activePeer)) {
boolean outgoingBeforeAccept = remotePeer.getState() == CallState.DIALING || remotePeer.getState() == CallState.REMOTE_RINGING;
if (outgoingBeforeAccept) {
sendMessage(WebRtcViewModel.State.RECIPIENT_UNAVAILABLE, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(WebRtcViewModel.State.RECIPIENT_UNAVAILABLE, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
} else {
sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
}
}
@@ -1042,7 +1070,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
}
activePeer.receivedBusy();
sendMessage(WebRtcViewModel.State.CALL_BUSY, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(WebRtcViewModel.State.CALL_BUSY, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
audioManager.startOutgoingRinger(OutgoingRinger.Type.BUSY);
Util.runOnMainDelayed(() -> {
@@ -1062,7 +1090,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
Log.i(TAG, "handleEndedFailure(): call_id: " + remotePeer.getCallId());
if (remotePeer.callIdEquals(activePeer)) {
sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
}
if (remotePeer.getState() == CallState.ANSWERING || remotePeer.getState() == CallState.LOCAL_RINGING) {
@@ -1182,7 +1210,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
@NonNull CameraState localCameraState,
boolean remoteVideoEnabled,
boolean bluetoothAvailable,
boolean microphoneEnabled)
boolean microphoneEnabled,
boolean isRemoteVideoOffer)
{
EventBus.getDefault().postSticky(new WebRtcViewModel(state,
remotePeer.getRecipient(),
@@ -1191,7 +1220,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
remoteRenderer,
remoteVideoEnabled,
bluetoothAvailable,
microphoneEnabled));
microphoneEnabled,
isRemoteVideoOffer));
}
private void sendMessage(@NonNull WebRtcViewModel.State state,
@@ -1200,7 +1230,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
@NonNull CameraState localCameraState,
boolean remoteVideoEnabled,
boolean bluetoothAvailable,
boolean microphoneEnabled)
boolean microphoneEnabled,
boolean isRemoteVideoOffer)
{
EventBus.getDefault().postSticky(new WebRtcViewModel(state,
remotePeer.getRecipient(),
@@ -1210,7 +1241,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
remoteRenderer,
remoteVideoEnabled,
bluetoothAvailable,
microphoneEnabled));
microphoneEnabled,
isRemoteVideoOffer));
}
private ListenableFutureTask<Boolean> sendMessage(@NonNull final RemotePeer remotePeer,
@@ -1259,7 +1291,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
Log.w(TAG, message, error);
if (activePeer != null) {
sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
}
if (callManager != null) {
@@ -1496,11 +1528,11 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
}
if (error instanceof UntrustedIdentityException) {
sendMessage(WebRtcViewModel.State.UNTRUSTED_IDENTITY, activePeer, ((UntrustedIdentityException)error).getIdentityKey(), localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(WebRtcViewModel.State.UNTRUSTED_IDENTITY, activePeer, ((UntrustedIdentityException)error).getIdentityKey(), localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
} else if (error instanceof UnregisteredUserException) {
sendMessage(WebRtcViewModel.State.NO_SUCH_USER, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(WebRtcViewModel.State.NO_SUCH_USER, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
} else if (error instanceof IOException) {
sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled);
sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer);
}
}
@@ -1655,7 +1687,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
.putExtra(EXTRA_REMOTE_PEER, remotePeer)
.putExtra(EXTRA_REMOTE_DEVICE, remoteDevice)
.putExtra(EXTRA_BROADCAST, broadcast)
.putExtra(EXTRA_OFFER_DESCRIPTION, offer);
.putExtra(EXTRA_OFFER_DESCRIPTION, offer)
.putExtra(EXTRA_OFFER_TYPE, (enableVideoOnCreate ? OfferMessage.Type.VIDEO_CALL : OfferMessage.Type.AUDIO_CALL).getCode());
startService(intent);
} else {

View File

@@ -3,14 +3,20 @@ package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.IconCompat;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.request.target.CustomViewTarget;
import com.bumptech.glide.request.transition.Transition;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.color.MaterialColor;
@@ -29,6 +35,35 @@ public final class AvatarUtil {
private AvatarUtil() {
}
public static void loadBlurredIconIntoViewBackground(@NonNull Recipient recipient, @NonNull View target) {
Context context = target.getContext();
if (recipient.getContactPhoto() == null) {
target.setBackgroundColor(ContextCompat.getColor(target.getContext(), R.color.black));
return;
}
GlideApp.with(target)
.load(recipient.getContactPhoto())
.transform(new CenterCrop(), new BlurTransformation(context, 0.25f, BlurTransformation.MAX_RADIUS))
.into(new CustomViewTarget<View, Drawable>(target) {
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
target.setBackgroundColor(ContextCompat.getColor(target.getContext(), R.color.black));
}
@Override
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
target.setBackground(resource);
}
@Override
protected void onResourceCleared(@Nullable Drawable placeholder) {
target.setBackground(placeholder);
}
});
}
public static void loadIconIntoImageView(@NonNull Recipient recipient, @NonNull ImageView target) {
Context context = target.getContext();

View File

@@ -0,0 +1,61 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.renderscript.Allocation;
import android.renderscript.Element;
import android.renderscript.RenderScript;
import android.renderscript.ScriptIntrinsicBlur;
import androidx.annotation.NonNull;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
import org.whispersystems.libsignal.util.guava.Preconditions;
import java.security.MessageDigest;
import java.util.Locale;
public final class BlurTransformation extends BitmapTransformation {
public static final float MAX_RADIUS = 25f;
private final RenderScript rs;
private final float bitmapScaleFactor;
private final float blurRadius;
public BlurTransformation(@NonNull Context context, float bitmapScaleFactor, float blurRadius) {
rs = RenderScript.create(context);
Preconditions.checkArgument(blurRadius >= 0 && blurRadius <= 25, "Blur radius must be a non-negative value less than or equal to 25.");
Preconditions.checkArgument(bitmapScaleFactor > 0, "Bitmap scale factor must be a non-negative value");
this.bitmapScaleFactor = bitmapScaleFactor;
this.blurRadius = blurRadius;
}
@Override
protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
Matrix scaleMatrix = new Matrix();
scaleMatrix.setScale(bitmapScaleFactor, bitmapScaleFactor);
Bitmap blurredBitmap = Bitmap.createBitmap(toTransform, 0, 0, outWidth, outHeight, scaleMatrix, true);
Allocation input = Allocation.createFromBitmap(rs, blurredBitmap, Allocation.MipmapControl.MIPMAP_FULL, Allocation.USAGE_SHARED);
Allocation output = Allocation.createTyped(rs, input.getType());
ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
script.setInput(input);
script.setRadius(blurRadius);
script.forEach(output);
output.copyTo(blurredBitmap);
return blurredBitmap;
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
messageDigest.update(String.format(Locale.US, "blur-%f-%f", bitmapScaleFactor, blurRadius).getBytes());
}
}

View File

@@ -18,23 +18,26 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.TaskStackBuilder;
import androidx.fragment.app.FragmentActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestDialogFragment;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
public class CommunicationActions {
private static final String TAG = Log.tag(CommunicationActions.class);
public static void startVoiceCall(@NonNull Activity activity, @NonNull Recipient recipient) {
public static void startVoiceCall(@NonNull FragmentActivity activity, @NonNull Recipient recipient) {
if (TelephonyUtil.isAnyPstnLineBusy(activity)) {
Toast.makeText(activity,
R.string.CommunicationActions_a_cellular_call_is_already_in_progress,
@@ -60,7 +63,7 @@ public class CommunicationActions {
});
}
public static void startVideoCall(@NonNull Activity activity, @NonNull Recipient recipient) {
public static void startVideoCall(@NonNull FragmentActivity activity, @NonNull Recipient recipient) {
if (TelephonyUtil.isAnyPstnLineBusy(activity)) {
Toast.makeText(activity,
R.string.CommunicationActions_a_cellular_call_is_already_in_progress,
@@ -173,29 +176,69 @@ public class CommunicationActions {
}
}
private static void startCallInternal(@NonNull Activity activity, @NonNull Recipient recipient, boolean isVideo) {
private static void startCallInternal(@NonNull FragmentActivity activity, @NonNull Recipient recipient, boolean isVideo) {
if (isVideo) startVideoCallInternal(activity, recipient);
else startAudioCallInternal(activity, recipient);
}
private static void startAudioCallInternal(@NonNull FragmentActivity activity, @NonNull Recipient recipient) {
Permissions.with(activity)
.request(Manifest.permission.RECORD_AUDIO)
.ifNecessary()
.withRationaleDialog(activity.getString(R.string.ConversationActivity__to_call_s_signal_needs_access_to_your_microphone, recipient.getDisplayName(activity)),
R.drawable.ic_mic_solid_24)
.withPermanentDenialDialog(activity.getString(R.string.ConversationActivity__to_call_s_signal_needs_access_to_your_microphone, recipient.getDisplayName(activity)))
.onAllGranted(() -> {
Intent intent = new Intent(activity, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(recipient.getId()))
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, OfferMessage.Type.AUDIO_CALL.getCode());
activity.startService(intent);
MessageSender.onMessageSent();
if (FeatureFlags.profileForCalling() && recipient.resolve().getProfileKey() == null) {
CalleeMustAcceptMessageRequestDialogFragment.create(recipient.getId())
.show(activity.getSupportFragmentManager(), null);
} else {
Intent activityIntent = new Intent(activity, WebRtcCallActivity.class);
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(activityIntent);
}
})
.execute();
}
private static void startVideoCallInternal(@NonNull FragmentActivity activity, @NonNull Recipient recipient) {
Permissions.with(activity)
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(activity.getString(R.string.ConversationActivity_to_call_s_signal_needs_access_to_your_microphone_and_camera, recipient.getDisplayName(activity)),
.withRationaleDialog(activity.getString(R.string.ConversationActivity_signal_needs_the_microphone_and_camera_permissions_in_order_to_call_s, recipient.getDisplayName(activity)),
R.drawable.ic_mic_solid_24,
R.drawable.ic_video_solid_24_tinted)
.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)
.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());
activity.startService(intent);
Intent activityIntent = new Intent(activity, WebRtcCallActivity.class);
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (isVideo) {
activityIntent.putExtra(WebRtcCallActivity.EXTRA_ENABLE_VIDEO_IF_AVAILABLE, true);
}
MessageSender.onMessageSent();
activity.startActivity(activityIntent);
if (FeatureFlags.profileForCalling() && recipient.resolve().getProfileKey() == null) {
CalleeMustAcceptMessageRequestDialogFragment.create(recipient.getId())
.show(activity.getSupportFragmentManager(), null);
} else {
Intent activityIntent = new Intent(activity, WebRtcCallActivity.class);
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(WebRtcCallActivity.EXTRA_ENABLE_VIDEO_IF_AVAILABLE, true);
activity.startActivity(activityIntent);
}
})
.execute();
}

View File

@@ -0,0 +1,35 @@
package org.thoughtcrime.securesms.util;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Locale;
public class EllapsedTimeFormatter {
private final long hours;
private final long minutes;
private final long seconds;
private EllapsedTimeFormatter(long durationMillis) {
hours = durationMillis / 3600;
minutes = durationMillis % 3600 / 60;
seconds = durationMillis % 3600 % 60;
}
@Override
public @NonNull String toString() {
if (hours > 0) {
return String.format(Locale.US, "%02d:%02d:%02d", hours, minutes, seconds);
} else {
return String.format(Locale.US, "%02d:%02d", minutes, seconds);
}
}
public static @Nullable EllapsedTimeFormatter fromDurationMillis(long durationMillis) {
if (durationMillis == -1) {
return null;
}
return new EllapsedTimeFormatter(durationMillis);
}
}

View File

@@ -56,6 +56,9 @@ public final class FeatureFlags {
private static final String PROFILE_NAMES_MEGAPHONE = "android.profileNamesMegaphone";
private static final String ATTACHMENTS_V3 = "android.attachmentsV3";
private static final String REMOTE_DELETE = "android.remoteDelete";
private static final String PROFILE_FOR_CALLING = "android.profileForCalling";
private static final String CALLING_PIP = "android.callingPip";
private static final String NEW_GROUP_UI = "android.newGroupUI";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@@ -70,7 +73,10 @@ public final class FeatureFlags {
PROFILE_NAMES_MEGAPHONE,
MESSAGE_REQUESTS,
ATTACHMENTS_V3,
REMOTE_DELETE
REMOTE_DELETE,
PROFILE_FOR_CALLING,
CALLING_PIP,
NEW_GROUP_UI
);
/**
@@ -226,6 +232,16 @@ public final class FeatureFlags {
return getValue(REMOTE_DELETE, false);
}
/** Whether or not profile sharing is required for calling */
public static boolean profileForCalling() {
return messageRequests() && getValue(PROFILE_FOR_CALLING, false);
}
/** Whether or not to display Calling PIP */
public static boolean callingPip() {
return getValue(CALLING_PIP, false);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Boolean> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);

View File

@@ -198,6 +198,10 @@ public class ViewUtil {
}
}
public static float pxToDp(float px) {
return px / Resources.getSystem().getDisplayMetrics().density;
}
public static int dpToPx(Context context, int dp) {
return (int)((dp * context.getResources().getDisplayMetrics().density) + 0.5);
}

View File

@@ -0,0 +1,43 @@
package org.thoughtcrime.securesms.util.views;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class TouchInterceptingFrameLayout extends FrameLayout {
private OnInterceptTouchEventListener listener;
public TouchInterceptingFrameLayout(@NonNull Context context) {
super(context);
}
public TouchInterceptingFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public TouchInterceptingFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (listener != null) {
return listener.onInterceptTouchEvent(ev);
} else {
return super.onInterceptTouchEvent(ev);
}
}
public void setOnInterceptTouchEventListener(@Nullable OnInterceptTouchEventListener listener) {
this.listener = listener;
}
public interface OnInterceptTouchEventListener {
boolean onInterceptTouchEvent(MotionEvent ev);
}
}

View File

@@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
public class VoiceCallShare extends Activity {
@@ -34,7 +35,8 @@ public class VoiceCallShare extends Activity {
if (!TextUtils.isEmpty(destination)) {
Intent serviceIntent = new Intent(this, WebRtcCallService.class);
serviceIntent.setAction(WebRtcCallService.ACTION_OUTGOING_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.AUDIO_CALL.getCode());
startService(serviceIntent);
Intent activityIntent = new Intent(this, WebRtcCallActivity.class);