mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-09 06:18:34 +00:00
Implement new call screen UI/UX.
This commit is contained in:
parent
33e3f78be6
commit
d5419ec9fa
@ -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"
|
||||
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
public enum WebRtcControls {
|
||||
NONE,
|
||||
ONGOING,
|
||||
RINGING,
|
||||
CONNECTED,
|
||||
INCOMING_AUDIO,
|
||||
INCOMING_VIDEO
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
public enum WebRtcLocalRenderState {
|
||||
GONE,
|
||||
SMALL,
|
||||
LARGE
|
||||
}
|
@ -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 + "]";
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
9
app/src/main/res/drawable/ic_mic_off_solid_28.xml
Normal file
9
app/src/main/res/drawable/ic_mic_off_solid_28.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_mic_solid_28.xml
Normal file
9
app/src/main/res/drawable/ic_mic_solid_28.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_phone_down_28.xml
Normal file
9
app/src/main/res/drawable/ic_phone_down_28.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_phone_right_black_28.xml
Normal file
9
app/src/main/res/drawable/ic_phone_right_black_28.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_profile_outline_120.xml
Normal file
9
app/src/main/res/drawable/ic_profile_outline_120.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="120dp"
|
||||
android:height="120dp"
|
||||
android:viewportWidth="120"
|
||||
android:viewportHeight="120">
|
||||
<path
|
||||
android:pathData="M75.027,60.21C78.416,57.169 80.803,53.171 81.872,48.744C82.94,44.318 82.641,39.671 81.012,35.418C79.384,31.166 76.504,27.507 72.752,24.926C69,22.345 64.554,20.964 60,20.964C55.446,20.964 51,22.345 47.248,24.926C43.496,27.507 40.616,31.166 38.988,35.418C37.359,39.671 37.06,44.318 38.128,48.744C39.197,53.171 41.584,57.169 44.973,60.21C39.182,60.946 33.858,63.768 29.997,68.147C26.136,72.526 24.004,78.162 24,84V87H28.5V84C28.506,78.83 30.563,73.874 34.218,70.218C37.874,66.563 42.83,64.506 48,64.5H72C77.17,64.506 82.126,66.563 85.782,70.218C89.437,73.874 91.494,78.83 91.5,84V87H96V84C95.995,78.162 93.864,72.526 90.003,68.147C86.142,63.768 80.818,60.946 75.027,60.21ZM60,61.5C56.44,61.5 52.96,60.444 50,58.466C47.04,56.488 44.732,53.677 43.37,50.388C42.008,47.099 41.651,43.48 42.346,39.988C43.04,36.497 44.755,33.289 47.272,30.772C49.789,28.255 52.997,26.54 56.488,25.846C59.98,25.151 63.599,25.508 66.888,26.87C70.177,28.233 72.989,30.54 74.966,33.5C76.944,36.46 78,39.94 78,43.5C78,48.274 76.104,52.852 72.728,56.228C69.352,59.604 64.774,61.5 60,61.5Z"
|
||||
android:fillColor="@color/core_white"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_speaker_bt_solid_28.xml
Normal file
9
app/src/main/res/drawable/ic_speaker_bt_solid_28.xml
Normal 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>
|
@ -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>
|
9
app/src/main/res/drawable/ic_speaker_solid_28.xml
Normal file
9
app/src/main/res/drawable/ic_speaker_solid_28.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_speaker_solid_black_28.xml
Normal file
9
app/src/main/res/drawable/ic_speaker_solid_black_28.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_speaker_solid_white_28.xml
Normal file
9
app/src/main/res/drawable/ic_speaker_solid_white_28.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_video_off_solid_28.xml
Normal file
9
app/src/main/res/drawable/ic_video_off_solid_28.xml
Normal 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>
|
@ -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>
|
9
app/src/main/res/drawable/ic_video_solid_28.xml
Normal file
9
app/src/main/res/drawable/ic_video_solid_28.xml
Normal 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>
|
10
app/src/main/res/drawable/webrtc_call_screen_answer.xml
Normal file
10
app/src/main/res/drawable/webrtc_call_screen_answer.xml
Normal 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>
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
13
app/src/main/res/drawable/webrtc_call_screen_circle_red.xml
Normal file
13
app/src/main/res/drawable/webrtc_call_screen_circle_red.xml
Normal 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>
|
10
app/src/main/res/drawable/webrtc_call_screen_hangup.xml
Normal file
10
app/src/main/res/drawable/webrtc_call_screen_hangup.xml
Normal 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>
|
@ -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>
|
15
app/src/main/res/drawable/webrtc_call_screen_mic_toggle.xml
Normal file
15
app/src/main/res/drawable/webrtc_call_screen_mic_toggle.xml
Normal 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>
|
@ -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>
|
@ -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>
|
13
app/src/main/res/layout/audio_output_adapter_item.xml
Normal file
13
app/src/main/res/layout/audio_output_adapter_item.xml
Normal 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" />
|
@ -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>
|
@ -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" />
|
||||
|
@ -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>
|
@ -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>
|
280
app/src/main/res/layout/webrtc_call_view.xml
Normal file
280
app/src/main/res/layout/webrtc_call_view.xml
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 that’s stored with Signal encrypted.</string>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
|
@ -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()));
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user