Implement new call screen UI/UX.

master
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

@ -117,7 +117,9 @@
android:theme="@style/TextSecure.LightTheme.WebRTCCall"
android:excludeFromRecents="true"
android:screenOrientation="portrait"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|screenSize|fontScale"
android:supportsPictureInPicture="true"
android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:launchMode="singleTask"/>
<activity android:name=".InviteActivity"

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);

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple android:color="@color/green_700" xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape android:shape="oval">
<solid android:color="@color/green" />
</shape>
</item>
</ripple>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple android:color="@color/transparent_white_30" xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape android:shape="oval">
<solid android:color="@color/transparent_white_40" />
</shape>
</item>
</ripple>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple android:color="@color/red_700" xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape android:shape="oval">
<solid android:color="@color/red" />
</shape>
</item>
</ripple>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<solid android:color="@color/core_grey_05" />
</shape>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:fillColor="@color/core_grey_75"
android:pathData="M25.56,3.56l-22,22L2.5,24.5l4.67,-4.67A9.22,9.22 0,0 1,5.5 14.5L7,14.5a7.74,7.74 0,0 0,1.25 4.25l1.37,-1.37A4.9,4.9 0,0 1,9 15L9,7A5,5 0,0 1,19 7L19,8l5.5,-5.5ZM19,15L19,12.24l-7.22,7.22A5,5 0,0 0,19 15ZM14,22a6.62,6.62 0,0 1,-3.65 -1.11L9.28,22a8.09,8.09 0,0 0,4 1.5L13.28,28h1.5L14.78,23.46a8.82,8.82 0,0 0,7.75 -9L21,14.46A7.27,7.27 0,0 1,14 22Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:fillColor="@color/core_white"
android:pathData="M14,20h0a5,5 0,0 1,-5 -5L9,7a5,5 0,0 1,5 -5h0a5,5 0,0 1,5 5v8A5,5 0,0 1,14 20ZM22.5,14.5L21,14.5A7.27,7.27 0,0 1,14 22a7.27,7.27 0,0 1,-7 -7.5L5.5,14.5a8.82,8.82 0,0 0,7.75 9L13.25,28h1.5L14.75,23.46A8.82,8.82 0,0 0,22.5 14.5Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:fillColor="@color/core_white"
android:pathData="M26.92,14c-0.17,-1 -0.73,-2.64 -3.62,-4A22.94,22.94 0,0 0,4.7 10c-2.89,1.39 -3.45,3 -3.62,4a4.92,4.92 0,0 0,0.23 2.61A2.2,2.2 0,0 0,3.79 18.1l4.12,-0.73a2.18,2.18 0,0 0,1.82 -2.22c0,-0.56 0,-0.93 -0.06,-1.4a1.12,1.12 0,0 1,0.9 -1.21,23.65 23.65,0 0,1 6.86,0 1.12,1.12 0,0 1,0.9 1.21c0,0.47 0,0.84 -0.06,1.4a2.18,2.18 0,0 0,1.82 2.22l4.12,0.73a2.2,2.2 0,0 0,2.48 -1.49A4.92,4.92 0,0 0,26.92 14Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:fillColor="@color/core_black"
android:pathData="M4.31,5.42C3.72,6.25 3,7.8 4,10.83A22.9,22.9 0,0 0,17.17 24c3,1.07 4.58,0.3 5.41,-0.29a4.83,4.83 0,0 0,1.67 -2,2.19 2.19,0 0,0 -0.69,-2.81l-3.43,-2.4a2.18,2.18 0,0 0,-2.85 0.28c-0.39,0.41 -0.64,0.69 -1,1a1.12,1.12 0,0 1,-1.5 0.21,23.7 23.7,0 0,1 -2.6,-2.24A23.7,23.7 0,0 1,10 13.17a1.12,1.12 0,0 1,0.21 -1.5c0.35,-0.31 0.63,-0.56 1,-0.95a2.18,2.18 0,0 0,0.28 -2.85L9.12,4.44a2.19,2.19 0,0 0,-2.81 -0.69A4.83,4.83 0,0 0,4.31 5.42Z"/>
</vector>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:fillColor="@color/core_white"
android:pathData="M16,4.79L16,23.21a1,1 0,0 1,-1.68 0.73L9,19L5,19a3,3 0,0 1,-3 -3L2,12A3,3 0,0 1,5 9L9,9l5.32,-4.94A1,1 0,0 1,16 4.79ZM22.53,22.53 L26.53,18.53a0.75,0.75 0,0 0,0 -1.06L23.06,14l3.47,-3.47a0.75,0.75 0,0 0,0 -1.06l-4,-4A0.75,0.75 0,0 0,21.25 6v6.19L18.53,9.47l-1.06,1.06L20.94,14l-3.47,3.47 1.06,1.06 2.72,-2.72L21.25,22a0.74,0.74 0,0 0,0.46 0.69,0.74 0.74,0 0,0 0.82,-0.16ZM24.94,18l-2.19,2.19L22.75,15.81ZM24.94,10 L22.75,12.19L22.75,7.81Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:fillColor="@color/core_black"
android:pathData="M16,4.79L16,23.21a1,1 0,0 1,-1.68 0.73L9,19L5,19a3,3 0,0 1,-3 -3L2,12A3,3 0,0 1,5 9L9,9l5.32,-4.94A1,1 0,0 1,16 4.79ZM22.53,22.53 L26.53,18.53a0.75,0.75 0,0 0,0 -1.06L23.06,14l3.47,-3.47a0.75,0.75 0,0 0,0 -1.06l-4,-4A0.75,0.75 0,0 0,21.25 6v6.19L18.53,9.47l-1.06,1.06L20.94,14l-3.47,3.47 1.06,1.06 2.72,-2.72L21.25,22a0.74,0.74 0,0 0,0.46 0.69,0.74 0.74,0 0,0 0.82,-0.16ZM24.94,18l-2.19,2.19L22.75,15.81ZM24.94,10 L22.75,12.19L22.75,7.81Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:fillColor="@color/core_grey_75"
android:pathData="M16,4.79L16,23.21a1,1 0,0 1,-1.68 0.73L9,19L5,19a3,3 0,0 1,-3 -3L2,12A3,3 0,0 1,5 9L9,9l5.32,-4.94A1,1 0,0 1,16 4.79ZM27,14A12.91,12.91 0,0 0,21.92 3.69L21,4.88a11.5,11.5 0,0 1,0 18.27l0.91,1.19A12.92,12.92 0,0 0,27 14ZM23,14a9.06,9.06 0,0 0,-3.7 -7.28l-0.89,1.22a7.5,7.5 0,0 1,0.12 12l0.91,1.19A8.94,8.94 0,0 0,23 14Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:fillColor="@color/core_black"
android:pathData="M16,4.79L16,23.21a1,1 0,0 1,-1.68 0.73L9,19L5,19a3,3 0,0 1,-3 -3L2,12A3,3 0,0 1,5 9L9,9l5.32,-4.94A1,1 0,0 1,16 4.79ZM27,14A12.91,12.91 0,0 0,21.92 3.69L21,4.88a11.5,11.5 0,0 1,0 18.27l0.91,1.19A12.92,12.92 0,0 0,27 14ZM23,14a9.06,9.06 0,0 0,-3.7 -7.28l-0.89,1.22a7.5,7.5 0,0 1,0.12 12l0.91,1.19A8.94,8.94 0,0 0,23 14Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:fillColor="@color/core_white"
android:pathData="M16,4.79L16,23.21a1,1 0,0 1,-1.68 0.73L9,19L5,19a3,3 0,0 1,-3 -3L2,12A3,3 0,0 1,5 9L9,9l5.32,-4.94A1,1 0,0 1,16 4.79ZM27,14A12.91,12.91 0,0 0,21.92 3.69L21,4.88a11.5,11.5 0,0 1,0 18.27l0.91,1.19A12.92,12.92 0,0 0,27 14ZM23,14a9.06,9.06 0,0 0,-3.7 -7.28l-0.89,1.22a7.5,7.5 0,0 1,0.12 12l0.91,1.19A8.94,8.94 0,0 0,23 14Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:fillColor="@color/core_grey_75"
android:pathData="M22,12l3.29,-3.29a1,1 0,0 1,1.71 0.7v9.18a1,1 0,0 1,-1.71 0.7L22,16ZM24.5,2.5L19.85,7.15A3,3 0,0 0,17.5 6L6,6A3,3 0,0 0,3 9L3,19a3,3 0,0 0,2.14 2.86L2.5,24.5l1.06,1.06 22,-22ZM9.24,22L17.5,22a3,3 0,0 0,3 -3L20.5,10.74Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:fillColor="@color/core_white"
android:pathData="M22,12l3.29,-3.29a1,1 0,0 1,1.71 0.7v9.18a1,1 0,0 1,-1.71 0.7L22,16ZM24.5,2.5L19.85,7.15A3,3 0,0 0,17.5 6L6,6A3,3 0,0 0,3 9L3,19a3,3 0,0 0,2.14 2.86L2.5,24.5l1.06,1.06 22,-22ZM9.24,22L17.5,22a3,3 0,0 0,3 -3L20.5,10.74Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:fillColor="@color/core_white"
android:pathData="M22,12l3.29,-3.29a1,1 0,0 1,1.71 0.7v9.18a1,1 0,0 1,-1.71 0.7L22,16ZM20.5,19L20.5,9a3,3 0,0 0,-3 -3L6,6A3,3 0,0 0,3 9L3,19a3,3 0,0 0,3 3L17.5,22A3,3 0,0 0,20.5 19Z"/>
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/webrtc_call_screen_circle_green" />
<item
android:bottom="14dp"
android:drawable="@drawable/phone_24dp"
android:left="14dp"
android:right="14dp"
android:top="14dp" />
</layer-list>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/webrtc_call_screen_circle_green" />
<item
android:bottom="14dp"
android:drawable="@drawable/ic_video_solid_28"
android:left="14dp"
android:right="14dp"
android:top="14dp" />
</layer-list>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/webrtc_call_screen_circle_grey_selector" />
<item
android:bottom="14dp"
android:drawable="@drawable/ic_video_off_solid_white_28"
android:left="14dp"
android:right="14dp"
android:top="14dp" />
</layer-list>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="oval">
<solid android:color="@color/green_700" />
</shape>
</item>
<item android:state_pressed="false">
<shape android:shape="oval">
<solid android:color="@color/green" />
</shape>
</item>
</selector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/transparent_white_40" />
</shape>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="oval">
<solid android:color="@color/transparent_white_30" />
</shape>
</item>
<item android:state_pressed="false">
<shape android:shape="oval">
<solid android:color="@color/transparent_white_40" />
</shape>
</item>
</selector>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="oval">
<solid android:color="@color/red_700" />
</shape>
</item>
<item android:state_pressed="false">
<shape android:shape="oval">
<solid android:color="@color/red" />
</shape>
</item>
</selector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/webrtc_call_screen_circle_red" />
<item
android:bottom="14dp"
android:drawable="@drawable/ic_phone_down_28"
android:left="14dp"
android:right="14dp"
android:top="14dp" />
</layer-list>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient android:type="linear"
android:angle="270"
android:startColor="@color/transparent_black_60" />
</shape>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true">
<layer-list>
<item android:drawable="@drawable/webrtc_call_screen_circle_grey" />
<item android:bottom="14dp" android:drawable="@drawable/ic_mic_solid_28" android:left="14dp" android:right="14dp" android:top="14dp" />
</layer-list>
</item>
<item>
<layer-list>
<item android:drawable="@drawable/circle_tintable" />
<item android:bottom="14dp" android:drawable="@drawable/ic_mic_off_solid_28" android:left="14dp" android:right="14dp" android:top="14dp" />
</layer-list>
</item>
</selector>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<item app:state_handset="true">
<layer-list>
<item android:drawable="@drawable/webrtc_call_screen_circle_grey" />
<item android:bottom="14dp" android:drawable="@drawable/ic_speaker_solid_white_28" android:left="14dp" android:right="14dp" android:top="14dp" />
</layer-list>
</item>
<item app:state_speaker="true">
<layer-list>
<item android:drawable="@drawable/circle_tintable" />
<item android:bottom="14dp" android:drawable="@drawable/ic_speaker_solid_28" android:left="14dp" android:right="14dp" android:top="14dp" />
</layer-list>
</item>
<item app:state_headset="true">
<layer-list>
<item android:drawable="@drawable/webrtc_call_screen_circle_grey" />
<item android:bottom="14dp" android:drawable="@drawable/ic_speaker_bt_solid_28" android:left="14dp" android:right="14dp" android:top="14dp" />
</layer-list>
</item>
</selector>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true">
<layer-list>
<item android:drawable="@drawable/webrtc_call_screen_circle_grey" />
<item android:bottom="14dp" android:drawable="@drawable/ic_video_solid_28" android:left="14dp" android:right="14dp" android:top="14dp" />
</layer-list>
</item>
<item>
<layer-list>
<item android:drawable="@drawable/circle_tintable" />
<item android:bottom="14dp" android:drawable="@drawable/ic_video_off_solid_28" android:left="14dp" android:right="14dp" android:top="14dp" />
</layer-list>
</item>
</selector>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeight"
android:drawablePadding="12dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:textColor="@color/black"
android:textSize="16sp"
tools:drawableStart="@drawable/ic_photo_solid_24"
tools:text="@string/WebRtcAudioOutputToggle__phone" />

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<org.thoughtcrime.securesms.components.AvatarImageView
android:id="@+id/avatar"
android:layout_width="112dp"
android:layout_height="112dp" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/description"
style="@style/TextAppearance.AppCompat.Body1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:textColor="@color/core_grey_05"
android:textSize="16sp"
app:lineHeight="22sp"
tools:text="+1234567890 will get a message request from you. You can call once your message request is accepted." />
<Button
android:id="@+id/okay"
style="@style/Widget.Signal.Button.CalleeDialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@android:string/ok" />
</LinearLayout>

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.webrtc.WebRtcCallScreen xmlns:android="http://schemas.android.com/apk/res/android"
<org.thoughtcrime.securesms.components.webrtc.WebRtcCallView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/callScreen"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:background="@color/black"
android:clickable="true"
android:fitsSystemWindows="false" />

View File

@ -1,63 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/inCallControls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
tools:background="@color/textsecure_primary"
tools:showIn="@layout/webrtc_call_screen">
<org.thoughtcrime.securesms.components.AccessibleToggleButton
android:id="@+id/speakerButton"
style="@style/WebRtcCallCompoundButton"
android:layout_marginEnd="16dp"
android:width="36dp"
android:height="36dp"
android:background="@drawable/webrtc_speaker_button"
android:contentDescription="@string/WebRtcCallControls_speaker_button_description"
tools:checked="true" />
<org.thoughtcrime.securesms.components.AccessibleToggleButton
android:id="@+id/bluetoothButton"
style="@style/WebRtcCallCompoundButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="16dp"
android:background="@drawable/webrtc_bluetooth_button"
android:contentDescription="@string/WebRtcCallControls_bluetooth_button_description"
android:visibility="gone"
tools:checked="true"
tools:visibility="visible" />
<org.thoughtcrime.securesms.components.AccessibleToggleButton
android:id="@+id/muteButton"
style="@style/WebRtcCallCompoundButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="16dp"
android:background="@drawable/webrtc_mute_button"
android:contentDescription="@string/WebRtcCallControls_mute_button_description"
tools:checked="false" />
<org.thoughtcrime.securesms.components.AccessibleToggleButton
android:id="@+id/video_mute_button"
style="@style/WebRtcCallCompoundButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="16dp"
android:background="@drawable/webrtc_video_mute_button"
android:contentDescription="@string/WebRtcCallControls_your_camera_button_description" />
<org.thoughtcrime.securesms.components.AccessibleToggleButton
android:id="@+id/camera_flip_button"
style="@style/WebRtcCallCompoundButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="16dp"
android:background="@drawable/webrtc_camera_flip_button"
android:contentDescription="@string/WebRtcCallControls_switch_to_rear_camera_button_description"
android:visibility="gone"
tools:visibility="visible" />
</merge>

View File

@ -1,238 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2007 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/incall_screen"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.thoughtcrime.securesms.components.webrtc.PercentFrameLayout
android:id="@+id/remote_render_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:visibility="invisible" />
<org.thoughtcrime.securesms.components.webrtc.PercentFrameLayout
android:id="@+id/local_large_render_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:visibility="invisible" />
<!-- "Call info" block #1, for the foreground call. -->
<RelativeLayout
android:id="@+id/call_info_1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true">
<!-- Contact photo for call_info_1 -->
<FrameLayout
android:id="@+id/image_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/call_banner_1"
android:gravity="top|center_horizontal">
<ImageView
android:id="@+id/photo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/WebRtcCallControls_contact_photo_description"
android:scaleType="centerCrop"
android:visibility="visible"
tools:src="@drawable/ic_person_large" />
<LinearLayout
android:id="@+id/untrusted_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/grey_400"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/untrusted_explanation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:lineSpacingExtra="2sp"
android:maxWidth="270dp"
android:textSize="16sp"
tools:text="The safety numbers for your conversation with Masha have changed. This could either mean that someone is trying to intercept your communication, or that Masha simply re-installed Signal. You may wish to verify safety numbers for this contact." />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:maxWidth="250dp">
<Button
android:id="@+id/cancel_safety_numbers"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="@android:string/cancel" />
<Button
android:id="@+id/accept_safety_numbers"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/WebRtcCallScreen_accept" />
</LinearLayout>
</LinearLayout>
</FrameLayout>
<!-- "Call Banner" for call #1, the foregound or ringing call.
The "call banner" is a block of info about a single call,
including the contact name, phone number, call time counter,
and other status info. This info is shown as a "banner"
overlaid across the top of contact photo. -->
<LinearLayout
android:id="@+id/call_banner_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:minHeight="80dp"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/expanded_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/core_ultramarine"
android:paddingStart="24dp"
android:paddingTop="16dp"
android:paddingEnd="24dp">
<!-- Name (or the phone number, if we don't have a name to display). -->
<TextView
android:id="@+id/name"
style="@style/WebRtcCallScreenTextWhite.ExtraLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:ellipsize="end"
android:maxLines="1"
android:singleLine="true"
android:textAlignment="viewStart"
tools:text="Ali Connors" />
<!-- Label (like "Mobile" or "Work", if present) and phone number, side by side -->
<LinearLayout
android:id="@+id/labelAndNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/name"
android:orientation="horizontal">
<TextView
android:id="@+id/label"
style="@style/WebRtcCallScreenTextWhite.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:singleLine="true"
android:text="@string/redphone_call_card__signal_call" />
<TextView
android:id="@+id/phoneNumber"
style="@style/WebRtcCallScreenTextWhite.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
tools:text="+14152222222" />
</LinearLayout>
<!-- Elapsed time indication for a call in progress. -->
<TextView
android:id="@+id/elapsedTime"
style="@style/WebRtcCallScreenTextWhite.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:singleLine="true" />
</RelativeLayout>
<org.thoughtcrime.securesms.components.webrtc.WebRtcCallControls
android:id="@+id/inCallControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/core_ultramarine"
android:paddingStart="24dp"
android:paddingTop="16dp"
android:paddingEnd="24dp"
android:paddingBottom="20dp" />
<TextView
android:id="@+id/callStateLabel"
style="@style/WebRtcCallScreenTextWhite.Small"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#8033b5e5"
android:gravity="end"
android:paddingStart="24dp"
android:paddingTop="8dp"
android:paddingEnd="24dp"
android:paddingBottom="8dp"
android:textAllCaps="true"
tools:text="connected" />
</LinearLayout> <!-- End of call_banner for call_info #1. -->
<!-- The "call state label": In some states, this shows a special
indication like "Dialing" or "Incoming call" or "Call ended".
It's unused for the normal case of an active ongoing call. -->
<!-- This is visually part of the call banner, but it's not actually
part of the "call_banner_1" RelativeLayout since it needs a
different background color. -->
</RelativeLayout>
<org.thoughtcrime.securesms.components.webrtc.PercentFrameLayout
android:id="@+id/local_render_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:visibility="invisible" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/hangup_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="40dp"
android:contentDescription="@string/WebRtcCallScreen_end_call"
android:focusable="true"
android:src="@drawable/ic_call_end_white_48dp"
app:backgroundTint="@color/red_500" />
<org.thoughtcrime.securesms.components.webrtc.WebRtcAnswerDeclineButton
android:id="@+id/answer_decline_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:layout_marginBottom="16dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,280 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="org.thoughtcrime.securesms.components.webrtc.WebRtcCallView">
<View
android:id="@+id/call_screen_blur_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/transparent_black_40" />
<org.thoughtcrime.securesms.components.AvatarImageView
android:id="@+id/call_screen_recipient_avatar"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center" />
<ImageView
android:id="@+id/call_screen_recipient_avatar_call_card"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center"
android:visibility="gone" />
<FrameLayout
android:id="@+id/call_screen_remote_renderer_holder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<FrameLayout
android:id="@+id/call_screen_large_local_renderer_holder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/call_screen_header_gradient"
android:layout_width="match_parent"
android:layout_height="160dp"
android:background="@drawable/webrtc_call_screen_header_gradient" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/call_screen"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout
android:id="@+id/call_screen_pip_area"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0">
<FrameLayout
android:id="@+id/call_screen_pip"
android:layout_width="@dimen/picture_in_picture_gesture_helper_pip_width"
android:layout_height="@dimen/picture_in_picture_gesture_helper_pip_height"
android:translationX="100000dp"
android:translationY="-100000dp"
android:visibility="gone"
tools:background="@color/red"
tools:visibility="visible">
<FrameLayout
android:id="@+id/call_screen_small_local_renderer_holder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/call_screen_camera_direction_toggle"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="bottom|center_horizontal"
android:paddingStart="9dp"
android:paddingEnd="9dp"
android:paddingBottom="10dp"
android:src="@drawable/ic_switch_camera_32" />
</FrameLayout>
</org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout>
<ImageView
android:id="@+id/call_screen_down_arrow"
android:layout_width="20dp"
android:layout_height="11dp"
android:layout_marginStart="13dp"
app:layout_constraintBottom_toBottomOf="@id/call_screen_recipient_name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/call_screen_recipient_name" />
<TextView
android:id="@+id/call_screen_recipient_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="39dp"
android:shadowColor="@color/transparent_black_20"
android:shadowDx="0"
android:shadowDy="0"
android:shadowRadius="4.0"
android:textAppearance="@style/TextAppearance.Signal.Title2"
android:textColor="@color/core_white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Kiera Thompson" />
<TextView
android:id="@+id/call_screen_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:shadowColor="@color/transparent_black_40"
android:shadowDx="0"
android:shadowDy="0"
android:shadowRadius="4.0"
android:textColor="@color/core_white"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/call_screen_recipient_name"
tools:text="Signal Calling..." />
<org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutputToggleButton
android:id="@+id/call_screen_speaker_toggle"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="34dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/call_screen_video_toggle"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@drawable/webrtc_call_screen_speaker_toggle" />
<org.thoughtcrime.securesms.components.AccessibleToggleButton
android:id="@+id/call_screen_video_toggle"
style="@style/WebRtcCallV2CompoundButton"
android:layout_marginEnd="16dp"
android:layout_marginBottom="34dp"
android:background="@drawable/webrtc_call_screen_video_toggle"
android:stateListAnimator="@null"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/call_screen_mic_toggle"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/call_screen_speaker_toggle" />
<org.thoughtcrime.securesms.components.AccessibleToggleButton
android:id="@+id/call_screen_mic_toggle"
style="@style/WebRtcCallV2CompoundButton"
android:layout_marginEnd="16dp"
android:layout_marginBottom="34dp"
android:background="@drawable/webrtc_call_screen_mic_toggle"
android:stateListAnimator="@null"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/call_screen_end_call"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/call_screen_video_toggle" />
<ImageView
android:id="@+id/call_screen_end_call"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginBottom="34dp"
android:clickable="false"
android:src="@drawable/webrtc_call_screen_hangup"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/call_screen_mic_toggle" />
<ImageView
android:id="@+id/call_screen_decline_call"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginStart="66dp"
android:layout_marginBottom="65dp"
android:src="@drawable/webrtc_call_screen_hangup"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/call_screen_answer_call"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/call_screen_decline_call_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="@string/WebRtcCallScreen__decline"
android:textColor="@color/core_white"
app:layout_constraintEnd_toEndOf="@id/call_screen_decline_call"
app:layout_constraintStart_toStartOf="@id/call_screen_decline_call"
app:layout_constraintTop_toBottomOf="@id/call_screen_decline_call" />
<ImageView
android:id="@+id/call_screen_answer_call"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginEnd="66dp"
android:layout_marginBottom="65dp"
android:src="@drawable/webrtc_call_screen_answer"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toEndOf="@id/call_screen_decline_call" />
<TextView
android:id="@+id/call_screen_answer_call_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="@string/WebRtcCallScreen__answer"
android:textColor="@color/core_white"
app:layout_constraintEnd_toEndOf="@id/call_screen_answer_call"
app:layout_constraintStart_toStartOf="@id/call_screen_answer_call"
app:layout_constraintTop_toBottomOf="@id/call_screen_answer_call" />
<ImageView
android:id="@+id/call_screen_answer_with_audio"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginBottom="5dp"
android:src="@drawable/webrtc_call_screen_answer_without_video"
app:layout_constraintBottom_toTopOf="@id/call_screen_answer_with_audio_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/call_screen_answer_with_audio_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="9dp"
android:text="@string/WebRtcCallScreen__answer_without_video"
android:textColor="@color/core_white"
app:layout_constraintBottom_toTopOf="@id/call_screen_answer_call"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.constraintlayout.widget.Group
android:id="@+id/call_screen_in_call_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="call_screen_end_call,call_screen_mic_toggle,call_screen_video_toggle,call_screen_speaker_toggle" />
<androidx.constraintlayout.widget.Group
android:id="@+id/call_screen_incoming_call_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="call_screen_decline_call,call_screen_decline_call_label,call_screen_answer_call,call_screen_answer_call_label" />
<androidx.constraintlayout.widget.Group
android:id="@+id/call_screen_answer_with_audio_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="call_screen_answer_with_audio,call_screen_answer_with_audio_label" />
<androidx.constraintlayout.widget.Group
android:id="@+id/call_screen_top_views"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:constraint_referenced_ids="call_screen_recipient_name,call_screen_status,call_screen_down_arrow" />
</androidx.constraintlayout.widget.ConstraintLayout>
</merge>

View File

@ -507,4 +507,9 @@
<attr name="maxHeight" />
</declare-styleable>
<declare-styleable name="WebRtcAudioOutputToggleButtonState">
<attr name="state_headset" format="boolean" />
<attr name="state_speaker" format="boolean" />
<attr name="state_handset" format="boolean" />
</declare-styleable>
</resources>

View File

@ -12,6 +12,7 @@
<color name="transparent_white_20">#33ffffff</color>
<color name="transparent_white_30">#4Dffffff</color>
<color name="transparent_white_40">#66ffffff</color>
<color name="transparent_white_60">#99ffffff</color>
<color name="transparent_white_80">#ccffffff</color>
<color name="transparent_white_90">#e6ffffff</color>

View File

@ -152,4 +152,8 @@
<dimen name="debug_log_text_size">12sp</dimen>
<dimen name="picture_in_picture_gesture_helper_frame_padding">12dp</dimen>
<dimen name="picture_in_picture_gesture_helper_pip_width">90dp</dimen>
<dimen name="picture_in_picture_gesture_helper_pip_height">160dp</dimen>
</resources>

View File

@ -238,7 +238,6 @@
<string name="ConversationActivity_to_send_audio_messages_allow_signal_access_to_your_microphone">To send audio messages, allow Signal access to your microphone.</string>
<string name="ConversationActivity_signal_requires_the_microphone_permission_in_order_to_send_audio_messages">Signal requires the Microphone permission in order to send audio messages, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Microphone\".</string>
<string name="ConversationActivity_to_call_s_signal_needs_access_to_your_microphone_and_camera">To call %s, Signal needs access to your microphone and camera.</string>
<string name="ConversationActivity_signal_needs_the_microphone_and_camera_permissions_in_order_to_call_s">Signal needs the Microphone and Camera permissions in order to call %s, but they have been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Microphone\" and \"Camera\".</string>
<string name="ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera">To capture photos and video, allow Signal access to the camera.</string>
<string name="ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video">Signal needs the Camera permission to take photos or video, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Camera\".</string>
@ -264,6 +263,8 @@
<string name="ConversationActivity_you_will_leave_this_group_and_it_will_be_deleted_from_all_of_your_devices">You will leave this group, and it will be deleted from all your devices.</string>
<string name="ConversationActivity_delete">Delete</string>
<string name="ConversationActivity_delete_and_leave">Delete and leave</string>
<string name="ConversationActivity__to_call_s_signal_needs_access_to_your_microphone">To call %1$s, Signal needs access to your microphone</string>
<string name="ConversationActivity__to_call_s_signal_needs_access_to_your_microphone_and_camera">To call %1$s, Signal needs access to your microphone and camera.</string>
<!-- ConversationAdapter -->
<plurals name="ConversationAdapter_n_unread_messages">
@ -867,6 +868,16 @@
<string name="RedPhone_the_number_you_dialed_does_not_support_secure_voice">The number you dialed does not support secure voice!</string>
<string name="RedPhone_got_it">Got it</string>
<!-- WebRtcCallActivity -->
<string name="WebRtcCallActivity__tap_here_to_turn_on_your_video">Tap here to turn on your video</string>
<string name="WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera">To call %1$s, Signal needs access to your camera</string>
<string name="WebRtcCallActivity__signal_s">Signal %1$s</string>
<string name="WebRtcCallActivity__calling">Calling…</string>
<!-- WebRtcCallView -->
<string name="WebRtcCallView__signal_voice_call">Signal voice call…</string>
<string name="WebRtcCallView__signal_video_call">Signal video call…</string>
<!-- RegistrationActivity -->
<string name="RegistrationActivity_select_your_country">Select your country</string>
<string name="RegistrationActivity_you_must_specify_your_country_code">You must specify your
@ -1205,6 +1216,16 @@
<string name="WebRtcCallScreen_accept">Accept</string>
<string name="WebRtcCallScreen_end_call">End call</string>
<!-- WebRtcCallScreen V2 -->
<string name="WebRtcCallScreen__decline">Decline</string>
<string name="WebRtcCallScreen__answer">Answer</string>
<string name="WebRtcCallScreen__answer_without_video">Answer without video</string>
<!-- WebRtcAudioOutputToggle -->
<string name="WebRtcAudioOutputToggle__phone">Phone</string>
<string name="WebRtcAudioOutputToggle__speaker">Speaker</string>
<string name="WebRtcAudioOutputToggle__bluetooth">Bluetooth</string>
<!-- WebRtcCallControls -->
<string name="WebRtcCallControls_tap_to_enable_your_video">Tap to enable your video</string>
@ -2004,6 +2025,10 @@
<item quantity="other">%1$d attempts remaining.</item>
</plurals>
<!-- CalleeMustAcceptMessageRequestDialogFragment -->
<string name="CalleeMustAcceptMessageRequestDialogFragment__okay">Okay</string>
<string name="CalleeMustAcceptMessageRequestDialogFragment__s_will_get_a_message_request_from_you">%1$s will get a message request from you. You can call once your message request is accepted.</string>
<!-- KBS Megaphone -->
<string name="KbsMegaphone__create_a_pin">Create a PIN</string>
<string name="KbsMegaphone__pins_keep_information_thats_stored_with_signal_encrytped">PINs keep information thats stored with Signal encrypted.</string>

View File

@ -262,6 +262,13 @@
<item name="android:textOff">@null</item>
</style>
<style name="WebRtcCallV2CompoundButton">
<item name="android:layout_height">56dp</item>
<item name="android:layout_width">56dp</item>
<item name="android:textOn">@null</item>
<item name="android:textOff">@null</item>
</style>
<style name="IdentityKey">
<item name="android:fontFamily">monospace</item>
<item name="android:typeface">monospace</item>
@ -416,4 +423,9 @@
<item name="android:textAlignment">viewStart</item>
<item name="android:drawablePadding">16dp</item>
</style>
<style name="Widget.Signal.Button.CalleeDialog" parent="Widget.AppCompat.Button">
<item name="android:textColor">@color/core_ultramarine</item>
<item name="android:background">@drawable/callee_dialog_button_background</item>
</style>
</resources>

View File

@ -768,6 +768,8 @@
<style name="TextSecure.LightTheme.WebRTCCall">
<item name="android:statusBarColor" tools:ignore="NewApi">@color/core_ultramarine</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowLightStatusBar" tools:ignore="NewApi">false</item>
<item name="android:navigationBarColor" tools:ignore="NewApi">@color/core_black</item>
<item name="android:windowLightNavigationBar" tools:ignore="NewApi">false</item>

View File

@ -666,7 +666,8 @@ public class SignalServiceMessageSender {
OfferMessage offer = callMessage.getOfferMessage().get();
builder.setOffer(CallMessage.Offer.newBuilder()
.setId(offer.getId())
.setDescription(offer.getDescription()));
.setDescription(offer.getDescription())
.setType(offer.getType().getProtoType()));
} else if (callMessage.getAnswerMessage().isPresent()) {
AnswerMessage answer = callMessage.getAnswerMessage().get();
builder.setAnswer(CallMessage.Answer.newBuilder()

View File

@ -532,7 +532,7 @@ public final class SignalServiceContent {
private static SignalServiceCallMessage createCallMessage(SignalServiceProtos.CallMessage content) {
if (content.hasOffer()) {
SignalServiceProtos.CallMessage.Offer offerContent = content.getOffer();
return SignalServiceCallMessage.forOffer(new OfferMessage(offerContent.getId(), offerContent.getDescription()));
return SignalServiceCallMessage.forOffer(new OfferMessage(offerContent.getId(), offerContent.getDescription(), OfferMessage.Type.fromProto(offerContent.getType())));
} else if (content.hasAnswer()) {
SignalServiceProtos.CallMessage.Answer answerContent = content.getAnswer();
return SignalServiceCallMessage.forAnswer(new AnswerMessage(answerContent.getId(), answerContent.getDescription()));

View File

@ -1,14 +1,18 @@
package org.whispersystems.signalservice.api.messages.calls;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
public class OfferMessage {
private final long id;
private final String description;
private final Type type;
public OfferMessage(long id, String description) {
public OfferMessage(long id, String description, Type type) {
this.id = id;
this.description = description;
this.type = type;
}
public String getDescription() {
@ -18,4 +22,50 @@ public class OfferMessage {
public long getId() {
return id;
}
public Type getType() {
return type;
}
public enum Type {
AUDIO_CALL("audio_call", SignalServiceProtos.CallMessage.Offer.Type.OFFER_AUDIO_CALL),
VIDEO_CALL("video_call", SignalServiceProtos.CallMessage.Offer.Type.OFFER_VIDEO_CALL),
NEED_PERMISSION("need_permission", SignalServiceProtos.CallMessage.Offer.Type.OFFER_NEED_PERMISSION);
private final String code;
private final SignalServiceProtos.CallMessage.Offer.Type protoType;
Type(String code, SignalServiceProtos.CallMessage.Offer.Type protoType) {
this.code = code;
this.protoType = protoType;
}
public String getCode() {
return code;
}
public SignalServiceProtos.CallMessage.Offer.Type getProtoType() {
return protoType;
}
public static Type fromProto(SignalServiceProtos.CallMessage.Offer.Type offerType) {
for (Type type : Type.values()) {
if (type.getProtoType().equals(offerType)) {
return type;
}
}
throw new IllegalArgumentException("Unexpected type: " + offerType.name());
}
public static Type fromCode(String code) {
for (Type type : Type.values()) {
if (type.getCode().equals(code)) {
return type;
}
}
throw new IllegalArgumentException("Unexpected code: " + code);
}
}
}

View File

@ -43,8 +43,15 @@ message Content {
message CallMessage {
message Offer {
enum Type {
OFFER_AUDIO_CALL = 0;
OFFER_VIDEO_CALL = 1;
OFFER_NEED_PERMISSION = 2;
}
optional uint64 id = 1;
optional string description = 2;
optional Type type = 3;
}
message Answer {