Implement new call screen UI/UX.
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,7 +80,9 @@ 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 WebRtcCallView callScreen;
|
||||
private TooltipPopup videoTooltip;
|
||||
private WebRtcCallViewModel viewModel;
|
||||
private boolean enableVideoIfAvailable;
|
||||
|
||||
@Override
|
||||
|
@ -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();
|
||||
|
||||
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) {
|
||||
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 IdentityKey theirKey = event.getIdentityKey();
|
||||
final Recipient recipient = event.getRecipient();
|
||||
|
||||
callScreen.setUntrustedIdentity(recipient, theirIdentity);
|
||||
callScreen.setAcceptIdentityListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (theirKey == null) {
|
||||
handleTerminate(recipient, event.getLocalRenderer());
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
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), theirIdentity, true);
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
callScreen.setCancelIdentityButton(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -372,6 +377,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
|||
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) {
|
||||
|
@ -672,10 +690,16 @@ public class WebRtcCallService extends Service implements CallManager.Observer,
|
|||
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();
|
||||
|
||||
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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="28dp"
|
||||
android:height="28dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<path
|
||||
android:fillColor="@color/core_black"
|
||||
android:pathData="M4.31,5.42C3.72,6.25 3,7.8 4,10.83A22.9,22.9 0,0 0,17.17 24c3,1.07 4.58,0.3 5.41,-0.29a4.83,4.83 0,0 0,1.67 -2,2.19 2.19,0 0,0 -0.69,-2.81l-3.43,-2.4a2.18,2.18 0,0 0,-2.85 0.28c-0.39,0.41 -0.64,0.69 -1,1a1.12,1.12 0,0 1,-1.5 0.21,23.7 23.7,0 0,1 -2.6,-2.24A23.7,23.7 0,0 1,10 13.17a1.12,1.12 0,0 1,0.21 -1.5c0.35,-0.31 0.63,-0.56 1,-0.95a2.18,2.18 0,0 0,0.28 -2.85L9.12,4.44a2.19,2.19 0,0 0,-2.81 -0.69A4.83,4.83 0,0 0,4.31 5.42Z"/>
|
||||
</vector>
|
File diff suppressed because one or more lines are too long
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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…
Reference in New Issue