RtpSessionActivity.java

   1package eu.siacs.conversations.ui;
   2
   3import static java.util.Arrays.asList;
   4import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
   5
   6import android.Manifest;
   7import android.annotation.SuppressLint;
   8import android.app.PictureInPictureParams;
   9import android.content.ActivityNotFoundException;
  10import android.content.Context;
  11import android.content.Intent;
  12import android.content.pm.PackageManager;
  13import android.os.Build;
  14import android.os.Bundle;
  15import android.os.Handler;
  16import android.os.PowerManager;
  17import android.os.SystemClock;
  18import android.util.Log;
  19import android.util.Rational;
  20import android.view.KeyEvent;
  21import android.view.Menu;
  22import android.view.MenuItem;
  23import android.view.View;
  24import android.view.WindowManager;
  25import android.widget.Toast;
  26
  27import androidx.annotation.NonNull;
  28import androidx.annotation.RequiresApi;
  29import androidx.annotation.StringRes;
  30import androidx.databinding.DataBindingUtil;
  31
  32import com.google.common.base.Optional;
  33import com.google.common.base.Preconditions;
  34import com.google.common.base.Throwables;
  35import com.google.common.collect.ImmutableList;
  36import com.google.common.collect.ImmutableSet;
  37import com.google.common.util.concurrent.FutureCallback;
  38import com.google.common.util.concurrent.Futures;
  39
  40import org.checkerframework.checker.nullness.compatqual.NullableDecl;
  41import org.jetbrains.annotations.NotNull;
  42import org.webrtc.RendererCommon;
  43import org.webrtc.SurfaceViewRenderer;
  44import org.webrtc.VideoTrack;
  45
  46import java.lang.ref.WeakReference;
  47import java.util.Arrays;
  48import java.util.Collections;
  49import java.util.List;
  50import java.util.Set;
  51
  52import eu.siacs.conversations.Config;
  53import eu.siacs.conversations.R;
  54import eu.siacs.conversations.databinding.ActivityRtpSessionBinding;
  55import eu.siacs.conversations.entities.Account;
  56import eu.siacs.conversations.entities.Contact;
  57import eu.siacs.conversations.entities.Conversation;
  58import eu.siacs.conversations.services.AppRTCAudioManager;
  59import eu.siacs.conversations.services.XmppConnectionService;
  60import eu.siacs.conversations.ui.widget.DialpadView;
  61import eu.siacs.conversations.ui.util.AvatarWorkerTask;
  62import eu.siacs.conversations.ui.util.MainThreadExecutor;
  63import eu.siacs.conversations.ui.util.Rationals;
  64import eu.siacs.conversations.utils.PermissionUtils;
  65import eu.siacs.conversations.utils.TimeFrameUtils;
  66import eu.siacs.conversations.xml.Namespace;
  67import eu.siacs.conversations.xmpp.Jid;
  68import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
  69import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
  70import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
  71import eu.siacs.conversations.xmpp.jingle.Media;
  72import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
  73
  74public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate, eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged {
  75
  76    public static final String EXTRA_WITH = "with";
  77    public static final String EXTRA_SESSION_ID = "session_id";
  78    public static final String EXTRA_LAST_REPORTED_STATE = "last_reported_state";
  79    public static final String EXTRA_LAST_ACTION = "last_action";
  80    public static final String ACTION_ACCEPT_CALL = "action_accept_call";
  81    public static final String ACTION_MAKE_VOICE_CALL = "action_make_voice_call";
  82    public static final String ACTION_MAKE_VIDEO_CALL = "action_make_video_call";
  83
  84    private static final int CALL_DURATION_UPDATE_INTERVAL = 333;
  85
  86    private static final List<RtpEndUserState> END_CARD = Arrays.asList(
  87            RtpEndUserState.APPLICATION_ERROR,
  88            RtpEndUserState.SECURITY_ERROR,
  89            RtpEndUserState.DECLINED_OR_BUSY,
  90            RtpEndUserState.CONNECTIVITY_ERROR,
  91            RtpEndUserState.CONNECTIVITY_LOST_ERROR,
  92            RtpEndUserState.RETRACTED
  93    );
  94    private static final List<RtpEndUserState> STATES_SHOWING_HELP_BUTTON = Arrays.asList(
  95            RtpEndUserState.APPLICATION_ERROR,
  96            RtpEndUserState.CONNECTIVITY_ERROR,
  97            RtpEndUserState.SECURITY_ERROR
  98    );
  99    private static final List<RtpEndUserState> STATES_SHOWING_SWITCH_TO_CHAT = Arrays.asList(
 100            RtpEndUserState.CONNECTING,
 101            RtpEndUserState.CONNECTED,
 102            RtpEndUserState.RECONNECTING
 103    );
 104    private static final List<RtpEndUserState> STATES_CONSIDERED_CONNECTED = Arrays.asList(
 105            RtpEndUserState.CONNECTED,
 106            RtpEndUserState.RECONNECTING
 107    );
 108    private static final List<RtpEndUserState> STATES_SHOWING_PIP_PLACEHOLDER = Arrays.asList(
 109            RtpEndUserState.ACCEPTING_CALL,
 110            RtpEndUserState.CONNECTING,
 111            RtpEndUserState.RECONNECTING
 112    );
 113    private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session";
 114    private static final int REQUEST_ACCEPT_CALL = 0x1111;
 115    private WeakReference<JingleRtpConnection> rtpConnectionReference;
 116
 117    private ActivityRtpSessionBinding binding;
 118    private PowerManager.WakeLock mProximityWakeLock;
 119
 120    private final Handler mHandler = new Handler();
 121    private final Runnable mTickExecutor = new Runnable() {
 122        @Override
 123        public void run() {
 124            updateCallDuration();
 125            mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL);
 126        }
 127    };
 128
 129    private static Set<Media> actionToMedia(final String action) {
 130        if (ACTION_MAKE_VIDEO_CALL.equals(action)) {
 131            return ImmutableSet.of(Media.AUDIO, Media.VIDEO);
 132        } else {
 133            return ImmutableSet.of(Media.AUDIO);
 134        }
 135    }
 136
 137    private static void addSink(final VideoTrack videoTrack, final SurfaceViewRenderer surfaceViewRenderer) {
 138        try {
 139            videoTrack.addSink(surfaceViewRenderer);
 140        } catch (final IllegalStateException e) {
 141            Log.e(Config.LOGTAG, "possible race condition on trying to display video track. ignoring", e);
 142        }
 143    }
 144
 145    @Override
 146    public void onCreate(Bundle savedInstanceState) {
 147        super.onCreate(savedInstanceState);
 148        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
 149                | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
 150                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
 151                | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
 152        this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session);
 153        setSupportActionBar(binding.toolbar);
 154
 155        binding.dialpad.setClickConsumer(tag -> {
 156            requireRtpConnection().applyDtmfTone(tag);
 157        });
 158
 159        if (savedInstanceState != null) {
 160            boolean dialpadVisible = savedInstanceState.getBoolean("dialpad_visible");
 161            binding.dialpad.setVisibility(dialpadVisible ? View.VISIBLE : View.GONE);
 162        }
 163    }
 164
 165    @Override
 166    public boolean onCreateOptionsMenu(final Menu menu) {
 167        getMenuInflater().inflate(R.menu.activity_rtp_session, menu);
 168        final MenuItem help = menu.findItem(R.id.action_help);
 169        final MenuItem gotoChat = menu.findItem(R.id.action_goto_chat);
 170        final MenuItem dialpad = menu.findItem(R.id.action_dialpad);
 171        help.setVisible(isHelpButtonVisible());
 172        gotoChat.setVisible(isSwitchToConversationVisible());
 173        dialpad.setVisible(isAudioOnlyConversation());
 174        return super.onCreateOptionsMenu(menu);
 175    }
 176
 177    @Override
 178    public boolean onKeyDown(final int keyCode, final KeyEvent event) {
 179        if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
 180            if (xmppConnectionService != null) {
 181                if (xmppConnectionService.getNotificationService().stopSoundAndVibration()) {
 182                    return true;
 183                }
 184            }
 185        }
 186        return super.onKeyDown(keyCode, event);
 187    }
 188
 189    private boolean isHelpButtonVisible() {
 190        try {
 191            return STATES_SHOWING_HELP_BUTTON.contains(requireRtpConnection().getEndUserState());
 192        } catch (IllegalStateException e) {
 193            final Intent intent = getIntent();
 194            final String state = intent != null ? intent.getStringExtra(EXTRA_LAST_REPORTED_STATE) : null;
 195            if (state != null) {
 196                return STATES_SHOWING_HELP_BUTTON.contains(RtpEndUserState.valueOf(state));
 197            } else {
 198                return false;
 199            }
 200        }
 201    }
 202
 203    private boolean isSwitchToConversationVisible() {
 204        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
 205        return connection != null && STATES_SHOWING_SWITCH_TO_CHAT.contains(connection.getEndUserState());
 206    }
 207
 208    private boolean isAudioOnlyConversation() {
 209        final JingleRtpConnection connection =
 210                this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
 211
 212        return connection != null && !connection.getMedia().contains(Media.VIDEO);
 213    }
 214
 215    private void switchToConversation() {
 216        final Contact contact = getWith();
 217        final Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true);
 218        switchToConversation(conversation);
 219    }
 220
 221    private void toggleDialpadVisibility() {
 222        if (binding.dialpad.getVisibility() == View.VISIBLE) {
 223            binding.dialpad.setVisibility(View.GONE);
 224        }
 225        else {
 226            binding.dialpad.setVisibility(View.VISIBLE);
 227        }
 228    }
 229
 230    public boolean onOptionsItemSelected(final MenuItem item) {
 231        switch (item.getItemId()) {
 232            case R.id.action_help:
 233                launchHelpInBrowser();
 234                break;
 235            case R.id.action_goto_chat:
 236                switchToConversation();
 237                break;
 238            case R.id.action_dialpad:
 239                toggleDialpadVisibility();
 240                break;
 241        }
 242        return super.onOptionsItemSelected(item);
 243    }
 244
 245    private void launchHelpInBrowser() {
 246        final Intent intent = new Intent(Intent.ACTION_VIEW, Config.HELP);
 247        try {
 248            startActivity(intent);
 249        } catch (final ActivityNotFoundException e) {
 250            Toast.makeText(this, R.string.no_application_found_to_open_link, Toast.LENGTH_LONG).show();
 251        }
 252    }
 253
 254    private void endCall(View view) {
 255        endCall();
 256    }
 257
 258    private void endCall() {
 259        if (this.rtpConnectionReference == null) {
 260            retractSessionProposal();
 261            finish();
 262        } else {
 263            requireRtpConnection().endCall();
 264        }
 265    }
 266
 267    private void retractSessionProposal() {
 268        final Intent intent = getIntent();
 269        final String action = intent.getAction();
 270        final Account account = extractAccount(intent);
 271        final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
 272        final String state = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE);
 273        if (!Intent.ACTION_VIEW.equals(action) || state == null || !END_CARD.contains(RtpEndUserState.valueOf(state))) {
 274            resetIntent(account, with, RtpEndUserState.RETRACTED, actionToMedia(intent.getAction()));
 275        }
 276        xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid());
 277    }
 278
 279    private void rejectCall(View view) {
 280        requireRtpConnection().rejectCall();
 281        finish();
 282    }
 283
 284    private void acceptCall(View view) {
 285        requestPermissionsAndAcceptCall();
 286    }
 287
 288    private void requestPermissionsAndAcceptCall() {
 289        final List<String> permissions;
 290        if (getMedia().contains(Media.VIDEO)) {
 291            permissions = ImmutableList.of(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO);
 292        } else {
 293            permissions = ImmutableList.of(Manifest.permission.RECORD_AUDIO);
 294        }
 295        if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CALL)) {
 296            putScreenInCallMode();
 297            checkRecorderAndAcceptCall();
 298        }
 299    }
 300
 301    private void checkRecorderAndAcceptCall() {
 302        checkMicrophoneAvailability();
 303        try {
 304            requireRtpConnection().acceptCall();
 305        } catch (final IllegalStateException e) {
 306            Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
 307        }
 308    }
 309
 310    private void checkMicrophoneAvailability() {
 311        new Thread(() -> {
 312            final long start = SystemClock.elapsedRealtime();
 313            final boolean isMicrophoneAvailable = AppRTCAudioManager.isMicrophoneAvailable();
 314            final long stop = SystemClock.elapsedRealtime();
 315            Log.d(Config.LOGTAG, "checking microphone availability took " + (stop - start) + "ms");
 316            if (isMicrophoneAvailable) {
 317                return;
 318            }
 319            runOnUiThread(() -> Toast.makeText(this, R.string.microphone_unavailable, Toast.LENGTH_LONG).show());
 320        }
 321        ).start();
 322    }
 323
 324    private void putScreenInCallMode() {
 325        putScreenInCallMode(requireRtpConnection().getMedia());
 326    }
 327
 328    private void putScreenInCallMode(final Set<Media> media) {
 329        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
 330        if (!media.contains(Media.VIDEO)) {
 331            final JingleRtpConnection rtpConnection = rtpConnectionReference != null ? rtpConnectionReference.get() : null;
 332            final AppRTCAudioManager audioManager = rtpConnection == null ? null : rtpConnection.getAudioManager();
 333            if (audioManager == null || audioManager.getSelectedAudioDevice() == AppRTCAudioManager.AudioDevice.EARPIECE) {
 334                acquireProximityWakeLock();
 335            }
 336        }
 337    }
 338
 339    @SuppressLint("WakelockTimeout")
 340    private void acquireProximityWakeLock() {
 341        final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
 342        if (powerManager == null) {
 343            Log.e(Config.LOGTAG, "power manager not available");
 344            return;
 345        }
 346        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
 347            if (this.mProximityWakeLock == null) {
 348                this.mProximityWakeLock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, PROXIMITY_WAKE_LOCK_TAG);
 349            }
 350            if (!this.mProximityWakeLock.isHeld()) {
 351                Log.d(Config.LOGTAG, "acquiring proximity wake lock");
 352                this.mProximityWakeLock.acquire();
 353            }
 354        }
 355    }
 356
 357    private void releaseProximityWakeLock() {
 358        if (this.mProximityWakeLock != null && mProximityWakeLock.isHeld()) {
 359            Log.d(Config.LOGTAG, "releasing proximity wake lock");
 360            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
 361                this.mProximityWakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
 362            } else {
 363                this.mProximityWakeLock.release();
 364            }
 365            this.mProximityWakeLock = null;
 366        }
 367    }
 368
 369    private void putProximityWakeLockInProperState(final AppRTCAudioManager.AudioDevice audioDevice) {
 370        if (audioDevice == AppRTCAudioManager.AudioDevice.EARPIECE) {
 371            acquireProximityWakeLock();
 372        } else {
 373            releaseProximityWakeLock();
 374        }
 375    }
 376
 377    @Override
 378    protected void refreshUiReal() {
 379
 380    }
 381
 382    @Override
 383    public void onNewIntent(final Intent intent) {
 384        Log.d(Config.LOGTAG, this.getClass().getName() + ".onNewIntent()");
 385        super.onNewIntent(intent);
 386        setIntent(intent);
 387        if (xmppConnectionService == null) {
 388            Log.d(Config.LOGTAG, "RtpSessionActivity: background service wasn't bound in onNewIntent()");
 389            return;
 390        }
 391        final Account account = extractAccount(intent);
 392        final String action = intent.getAction();
 393        final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
 394        final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID);
 395        if (sessionId != null) {
 396            Log.d(Config.LOGTAG, "reinitializing from onNewIntent()");
 397            if (initializeActivityWithRunningRtpSession(account, with, sessionId)) {
 398                return;
 399            }
 400            if (ACTION_ACCEPT_CALL.equals(intent.getAction())) {
 401                Log.d(Config.LOGTAG, "accepting call from onNewIntent()");
 402                requestPermissionsAndAcceptCall();
 403                resetIntent(intent.getExtras());
 404            }
 405        } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) {
 406            proposeJingleRtpSession(account, with, actionToMedia(action));
 407            binding.with.setText(account.getRoster().getContact(with).getDisplayName());
 408        } else {
 409            throw new IllegalStateException("received onNewIntent without sessionId");
 410        }
 411    }
 412
 413    @Override
 414    void onBackendConnected() {
 415        final Intent intent = getIntent();
 416        final String action = intent.getAction();
 417        final Account account = extractAccount(intent);
 418        final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
 419        final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID);
 420        if (sessionId != null) {
 421            if (initializeActivityWithRunningRtpSession(account, with, sessionId)) {
 422                return;
 423            }
 424            if (ACTION_ACCEPT_CALL.equals(intent.getAction())) {
 425                Log.d(Config.LOGTAG, "intent action was accept");
 426                requestPermissionsAndAcceptCall();
 427                resetIntent(intent.getExtras());
 428            }
 429        } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) {
 430            proposeJingleRtpSession(account, with, actionToMedia(action));
 431            binding.with.setText(account.getRoster().getContact(with).getDisplayName());
 432        } else if (Intent.ACTION_VIEW.equals(action)) {
 433            final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE);
 434            final RtpEndUserState state = extraLastState == null ? null : RtpEndUserState.valueOf(extraLastState);
 435            if (state != null) {
 436                Log.d(Config.LOGTAG, "restored last state from intent extra");
 437                updateButtonConfiguration(state);
 438                updateVerifiedShield(false);
 439                updateStateDisplay(state);
 440                updateProfilePicture(state);
 441                invalidateOptionsMenu();
 442            }
 443            binding.with.setText(account.getRoster().getContact(with).getDisplayName());
 444            if (xmppConnectionService.getJingleConnectionManager().fireJingleRtpConnectionStateUpdates()) {
 445                return;
 446            }
 447            if (END_CARD.contains(state) || xmppConnectionService.getJingleConnectionManager().hasMatchingProposal(account, with)) {
 448                return;
 449            }
 450            Log.d(Config.LOGTAG, "restored state (" + state + ") was not an end card. finishing");
 451            finish();
 452        }
 453    }
 454
 455    private void proposeJingleRtpSession(final Account account, final Jid with, final Set<Media> media) {
 456        checkMicrophoneAvailability();
 457        if (with.isBareJid()) {
 458            xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with, media);
 459        } else {
 460            final String sessionId = xmppConnectionService.getJingleConnectionManager().initializeRtpSession(account, with, media);
 461            initializeActivityWithRunningRtpSession(account, with, sessionId);
 462            resetIntent(account, with, sessionId);
 463        }
 464        putScreenInCallMode(media);
 465    }
 466
 467    @Override
 468    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
 469        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
 470        if (PermissionUtils.allGranted(grantResults)) {
 471            if (requestCode == REQUEST_ACCEPT_CALL) {
 472                checkRecorderAndAcceptCall();
 473            }
 474        } else {
 475            @StringRes int res;
 476            final String firstDenied = getFirstDenied(grantResults, permissions);
 477            if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) {
 478                res = R.string.no_microphone_permission;
 479            } else if (Manifest.permission.CAMERA.equals(firstDenied)) {
 480                res = R.string.no_camera_permission;
 481            } else {
 482                throw new IllegalStateException("Invalid permission result request");
 483            }
 484            Toast.makeText(this, getString(res, getString(R.string.app_name)), Toast.LENGTH_SHORT).show();
 485        }
 486    }
 487
 488    @Override
 489    public void onStart() {
 490        super.onStart();
 491        mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL);
 492        this.binding.remoteVideo.setOnAspectRatioChanged(this);
 493    }
 494
 495    @Override
 496    public void onStop() {
 497        mHandler.removeCallbacks(mTickExecutor);
 498        binding.remoteVideo.release();
 499        binding.remoteVideo.setOnAspectRatioChanged(null);
 500        binding.localVideo.release();
 501        final WeakReference<JingleRtpConnection> weakReference = this.rtpConnectionReference;
 502        final JingleRtpConnection jingleRtpConnection = weakReference == null ? null : weakReference.get();
 503        if (jingleRtpConnection != null) {
 504            releaseVideoTracks(jingleRtpConnection);
 505        }
 506        releaseProximityWakeLock();
 507        super.onStop();
 508    }
 509
 510    private void releaseVideoTracks(final JingleRtpConnection jingleRtpConnection) {
 511        final Optional<VideoTrack> remoteVideo = jingleRtpConnection.getRemoteVideoTrack();
 512        if (remoteVideo.isPresent()) {
 513            remoteVideo.get().removeSink(binding.remoteVideo);
 514        }
 515        final Optional<VideoTrack> localVideo = jingleRtpConnection.getLocalVideoTrack();
 516        if (localVideo.isPresent()) {
 517            localVideo.get().removeSink(binding.localVideo);
 518        }
 519    }
 520
 521    @Override
 522    public void onBackPressed() {
 523        if (isConnected()) {
 524            if (switchToPictureInPicture()) {
 525                return;
 526            }
 527        } else {
 528            endCall();
 529        }
 530        super.onBackPressed();
 531    }
 532
 533    @Override
 534    public void onUserLeaveHint() {
 535        super.onUserLeaveHint();
 536        if (switchToPictureInPicture()) {
 537            return;
 538        }
 539        //TODO apparently this method is not getting called on Android 10 when using the task switcher
 540        if (emptyReference(rtpConnectionReference) && xmppConnectionService != null) {
 541            retractSessionProposal();
 542        }
 543    }
 544
 545    private boolean isConnected() {
 546        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
 547        return connection != null && STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState());
 548    }
 549
 550    private boolean switchToPictureInPicture() {
 551        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && deviceSupportsPictureInPicture()) {
 552            if (shouldBePictureInPicture()) {
 553                startPictureInPicture();
 554                return true;
 555            }
 556        }
 557        return false;
 558    }
 559
 560    @RequiresApi(api = Build.VERSION_CODES.O)
 561    private void startPictureInPicture() {
 562        try {
 563            final Rational rational = this.binding.remoteVideo.getAspectRatio();
 564            final Rational clippedRational = Rationals.clip(rational);
 565            Log.d(Config.LOGTAG, "suggested rational " + rational + ". clipped to " + clippedRational);
 566            enterPictureInPictureMode(
 567                    new PictureInPictureParams.Builder()
 568                            .setAspectRatio(clippedRational)
 569                            .build()
 570            );
 571        } catch (final IllegalStateException e) {
 572            //this sometimes happens on Samsung phones (possibly when Knox is enabled)
 573            Log.w(Config.LOGTAG, "unable to enter picture in picture mode", e);
 574        }
 575    }
 576
 577    @Override
 578    public void onAspectRatioChanged(final Rational rational) {
 579        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isPictureInPicture()) {
 580            final Rational clippedRational = Rationals.clip(rational);
 581            Log.d(Config.LOGTAG, "suggested rational after aspect ratio change " + rational + ". clipped to " + clippedRational);
 582            setPictureInPictureParams(new PictureInPictureParams.Builder()
 583                    .setAspectRatio(clippedRational)
 584                    .build());
 585        }
 586    }
 587
 588    private boolean deviceSupportsPictureInPicture() {
 589        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 590            return getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
 591        } else {
 592            return false;
 593        }
 594    }
 595
 596    private boolean shouldBePictureInPicture() {
 597        try {
 598            final JingleRtpConnection rtpConnection = requireRtpConnection();
 599            return rtpConnection.getMedia().contains(Media.VIDEO) && Arrays.asList(
 600                    RtpEndUserState.ACCEPTING_CALL,
 601                    RtpEndUserState.CONNECTING,
 602                    RtpEndUserState.CONNECTED
 603            ).contains(rtpConnection.getEndUserState());
 604        } catch (final IllegalStateException e) {
 605            return false;
 606        }
 607    }
 608
 609    private boolean initializeActivityWithRunningRtpSession(final Account account, Jid with, String sessionId) {
 610        final WeakReference<JingleRtpConnection> reference = xmppConnectionService.getJingleConnectionManager()
 611                .findJingleRtpConnection(account, with, sessionId);
 612        if (reference == null || reference.get() == null) {
 613            final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession = xmppConnectionService
 614                    .getJingleConnectionManager().getTerminalSessionState(with, sessionId);
 615            if (terminatedRtpSession == null) {
 616                throw new IllegalStateException("failed to initialize activity with running rtp session. session not found");
 617            }
 618            initializeWithTerminatedSessionState(account, with, terminatedRtpSession);
 619            return true;
 620        }
 621        this.rtpConnectionReference = reference;
 622        final RtpEndUserState currentState = requireRtpConnection().getEndUserState();
 623        final boolean verified = requireRtpConnection().isVerified();
 624        if (currentState == RtpEndUserState.ENDED) {
 625            reference.get().throwStateTransitionException();
 626            finish();
 627            return true;
 628        }
 629        final Set<Media> media = getMedia();
 630        if (currentState == RtpEndUserState.INCOMING_CALL) {
 631            getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
 632        }
 633        if (JingleRtpConnection.STATES_SHOWING_ONGOING_CALL.contains(requireRtpConnection().getState())) {
 634            putScreenInCallMode();
 635        }
 636        binding.with.setText(getWith().getDisplayName());
 637        updateVideoViews(currentState);
 638        updateStateDisplay(currentState, media);
 639        updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(currentState));
 640        updateButtonConfiguration(currentState, media);
 641        updateProfilePicture(currentState);
 642        invalidateOptionsMenu();
 643        return false;
 644    }
 645
 646    private void initializeWithTerminatedSessionState(final Account account, final Jid with, final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession) {
 647        Log.d(Config.LOGTAG, "initializeWithTerminatedSessionState()");
 648        if (terminatedRtpSession.state == RtpEndUserState.ENDED) {
 649            finish();
 650            return;
 651        }
 652        RtpEndUserState state = terminatedRtpSession.state;
 653        resetIntent(account, with, terminatedRtpSession.state, terminatedRtpSession.media);
 654        updateButtonConfiguration(state);
 655        updateStateDisplay(state);
 656        updateProfilePicture(state);
 657        updateCallDuration();
 658        updateVerifiedShield(false);
 659        invalidateOptionsMenu();
 660        binding.with.setText(account.getRoster().getContact(with).getDisplayName());
 661    }
 662
 663    private void reInitializeActivityWithRunningRtpSession(final Account account, Jid with, String sessionId) {
 664        runOnUiThread(() -> initializeActivityWithRunningRtpSession(account, with, sessionId));
 665        resetIntent(account, with, sessionId);
 666    }
 667
 668    private void resetIntent(final Account account, final Jid with, final String sessionId) {
 669        final Intent intent = new Intent(Intent.ACTION_VIEW);
 670        intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
 671        intent.putExtra(EXTRA_WITH, with.toEscapedString());
 672        intent.putExtra(EXTRA_SESSION_ID, sessionId);
 673        setIntent(intent);
 674    }
 675
 676    private void ensureSurfaceViewRendererIsSetup(final SurfaceViewRenderer surfaceViewRenderer) {
 677        surfaceViewRenderer.setVisibility(View.VISIBLE);
 678        try {
 679            surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null);
 680        } catch (final IllegalStateException e) {
 681            //Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized");
 682        }
 683        surfaceViewRenderer.setEnableHardwareScaler(true);
 684    }
 685
 686    private void updateStateDisplay(final RtpEndUserState state) {
 687        updateStateDisplay(state, Collections.emptySet());
 688    }
 689
 690    private void updateStateDisplay(final RtpEndUserState state, final Set<Media> media) {
 691        switch (state) {
 692            case INCOMING_CALL:
 693                Preconditions.checkArgument(media.size() > 0, "Media must not be empty");
 694                if (media.contains(Media.VIDEO)) {
 695                    setTitle(R.string.rtp_state_incoming_video_call);
 696                } else {
 697                    setTitle(R.string.rtp_state_incoming_call);
 698                }
 699                break;
 700            case CONNECTING:
 701                setTitle(R.string.rtp_state_connecting);
 702                break;
 703            case CONNECTED:
 704                setTitle(R.string.rtp_state_connected);
 705                break;
 706            case RECONNECTING:
 707                setTitle(R.string.rtp_state_reconnecting);
 708                break;
 709            case ACCEPTING_CALL:
 710                setTitle(R.string.rtp_state_accepting_call);
 711                break;
 712            case ENDING_CALL:
 713                setTitle(R.string.rtp_state_ending_call);
 714                break;
 715            case FINDING_DEVICE:
 716                setTitle(R.string.rtp_state_finding_device);
 717                break;
 718            case RINGING:
 719                setTitle(R.string.rtp_state_ringing);
 720                break;
 721            case DECLINED_OR_BUSY:
 722                setTitle(R.string.rtp_state_declined_or_busy);
 723                break;
 724            case CONNECTIVITY_ERROR:
 725                setTitle(R.string.rtp_state_connectivity_error);
 726                break;
 727            case CONNECTIVITY_LOST_ERROR:
 728                setTitle(R.string.rtp_state_connectivity_lost_error);
 729                break;
 730            case RETRACTED:
 731                setTitle(R.string.rtp_state_retracted);
 732                break;
 733            case APPLICATION_ERROR:
 734                setTitle(R.string.rtp_state_application_failure);
 735                break;
 736            case SECURITY_ERROR:
 737                setTitle(R.string.rtp_state_security_error);
 738                break;
 739            case ENDED:
 740                throw new IllegalStateException("Activity should have called finishAndReleaseWakeLock();");
 741            default:
 742                throw new IllegalStateException(String.format("State %s has not been handled in UI", state));
 743        }
 744    }
 745
 746    private void updateVerifiedShield(final boolean verified) {
 747        if (isPictureInPicture()) {
 748            this.binding.verified.setVisibility(View.GONE);
 749            return;
 750        }
 751        this.binding.verified.setVisibility(verified ? View.VISIBLE : View.GONE);
 752    }
 753
 754    private void updateProfilePicture(final RtpEndUserState state) {
 755        updateProfilePicture(state, null);
 756    }
 757
 758    private void updateProfilePicture(final RtpEndUserState state, final Contact contact) {
 759        if (state == RtpEndUserState.INCOMING_CALL || state == RtpEndUserState.ACCEPTING_CALL) {
 760            final boolean show = getResources().getBoolean(R.bool.show_avatar_incoming_call);
 761            if (show) {
 762                binding.contactPhoto.setVisibility(View.VISIBLE);
 763                if (contact == null) {
 764                    AvatarWorkerTask.loadAvatar(getWith(), binding.contactPhoto, R.dimen.publish_avatar_size);
 765                } else {
 766                    AvatarWorkerTask.loadAvatar(contact, binding.contactPhoto, R.dimen.publish_avatar_size);
 767                }
 768            } else {
 769                binding.contactPhoto.setVisibility(View.GONE);
 770            }
 771        } else {
 772            binding.contactPhoto.setVisibility(View.GONE);
 773        }
 774    }
 775
 776    private Set<Media> getMedia() {
 777        return requireRtpConnection().getMedia();
 778    }
 779
 780    private void updateButtonConfiguration(final RtpEndUserState state) {
 781        updateButtonConfiguration(state, Collections.emptySet());
 782    }
 783
 784    @SuppressLint("RestrictedApi")
 785    private void updateButtonConfiguration(final RtpEndUserState state, final Set<Media> media) {
 786        if (state == RtpEndUserState.ENDING_CALL || isPictureInPicture()) {
 787            this.binding.rejectCall.setVisibility(View.INVISIBLE);
 788            this.binding.endCall.setVisibility(View.INVISIBLE);
 789            this.binding.acceptCall.setVisibility(View.INVISIBLE);
 790        } else if (state == RtpEndUserState.INCOMING_CALL) {
 791            this.binding.rejectCall.setContentDescription(getString(R.string.dismiss_call));
 792            this.binding.rejectCall.setOnClickListener(this::rejectCall);
 793            this.binding.rejectCall.setImageResource(R.drawable.ic_call_end_white_48dp);
 794            this.binding.rejectCall.setVisibility(View.VISIBLE);
 795            this.binding.endCall.setVisibility(View.INVISIBLE);
 796            this.binding.acceptCall.setContentDescription(getString(R.string.answer_call));
 797            this.binding.acceptCall.setOnClickListener(this::acceptCall);
 798            this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp);
 799            this.binding.acceptCall.setVisibility(View.VISIBLE);
 800        } else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
 801            this.binding.rejectCall.setContentDescription(getString(R.string.exit));
 802            this.binding.rejectCall.setOnClickListener(this::exit);
 803            this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
 804            this.binding.rejectCall.setVisibility(View.VISIBLE);
 805            this.binding.endCall.setVisibility(View.INVISIBLE);
 806            this.binding.acceptCall.setContentDescription(getString(R.string.record_voice_mail));
 807            this.binding.acceptCall.setOnClickListener(this::recordVoiceMail);
 808            this.binding.acceptCall.setImageResource(R.drawable.ic_voicemail_white_24dp);
 809            this.binding.acceptCall.setVisibility(View.VISIBLE);
 810        } else if (asList(
 811                RtpEndUserState.CONNECTIVITY_ERROR,
 812                RtpEndUserState.CONNECTIVITY_LOST_ERROR,
 813                RtpEndUserState.APPLICATION_ERROR,
 814                RtpEndUserState.RETRACTED,
 815                RtpEndUserState.SECURITY_ERROR
 816        ).contains(state)) {
 817            this.binding.rejectCall.setContentDescription(getString(R.string.exit));
 818            this.binding.rejectCall.setOnClickListener(this::exit);
 819            this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
 820            this.binding.rejectCall.setVisibility(View.VISIBLE);
 821            this.binding.endCall.setVisibility(View.INVISIBLE);
 822            this.binding.acceptCall.setContentDescription(getString(R.string.try_again));
 823            this.binding.acceptCall.setOnClickListener(this::retry);
 824            this.binding.acceptCall.setImageResource(R.drawable.ic_replay_white_48dp);
 825            this.binding.acceptCall.setVisibility(View.VISIBLE);
 826        } else {
 827            this.binding.rejectCall.setVisibility(View.INVISIBLE);
 828            this.binding.endCall.setContentDescription(getString(R.string.hang_up));
 829            this.binding.endCall.setOnClickListener(this::endCall);
 830            this.binding.endCall.setImageResource(R.drawable.ic_call_end_white_48dp);
 831            this.binding.endCall.setVisibility(View.VISIBLE);
 832            this.binding.acceptCall.setVisibility(View.INVISIBLE);
 833        }
 834        updateInCallButtonConfiguration(state, media);
 835    }
 836
 837    private boolean isPictureInPicture() {
 838        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
 839            return isInPictureInPictureMode();
 840        } else {
 841            return false;
 842        }
 843    }
 844
 845    private void updateInCallButtonConfiguration() {
 846        updateInCallButtonConfiguration(requireRtpConnection().getEndUserState(), requireRtpConnection().getMedia());
 847    }
 848
 849    @SuppressLint("RestrictedApi")
 850    private void updateInCallButtonConfiguration(final RtpEndUserState state, final Set<Media> media) {
 851        if (STATES_CONSIDERED_CONNECTED.contains(state) && !isPictureInPicture()) {
 852            Preconditions.checkArgument(media.size() > 0, "Media must not be empty");
 853            if (media.contains(Media.VIDEO)) {
 854                final JingleRtpConnection rtpConnection = requireRtpConnection();
 855                updateInCallButtonConfigurationVideo(rtpConnection.isVideoEnabled(), rtpConnection.isCameraSwitchable());
 856            } else {
 857                final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
 858                updateInCallButtonConfigurationSpeaker(
 859                        audioManager.getSelectedAudioDevice(),
 860                        audioManager.getAudioDevices().size()
 861                );
 862                this.binding.inCallActionFarRight.setVisibility(View.GONE);
 863            }
 864            if (media.contains(Media.AUDIO)) {
 865                updateInCallButtonConfigurationMicrophone(requireRtpConnection().isMicrophoneEnabled());
 866            } else {
 867                this.binding.inCallActionLeft.setVisibility(View.GONE);
 868            }
 869        } else {
 870            this.binding.inCallActionLeft.setVisibility(View.GONE);
 871            this.binding.inCallActionRight.setVisibility(View.GONE);
 872            this.binding.inCallActionFarRight.setVisibility(View.GONE);
 873        }
 874    }
 875
 876    @SuppressLint("RestrictedApi")
 877    private void updateInCallButtonConfigurationSpeaker(final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices) {
 878        switch (selectedAudioDevice) {
 879            case EARPIECE:
 880                this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_off_black_24dp);
 881                if (numberOfChoices >= 2) {
 882                    this.binding.inCallActionRight.setOnClickListener(this::switchToSpeaker);
 883                } else {
 884                    this.binding.inCallActionRight.setOnClickListener(null);
 885                    this.binding.inCallActionRight.setClickable(false);
 886                }
 887                break;
 888            case WIRED_HEADSET:
 889                this.binding.inCallActionRight.setImageResource(R.drawable.ic_headset_black_24dp);
 890                this.binding.inCallActionRight.setOnClickListener(null);
 891                this.binding.inCallActionRight.setClickable(false);
 892                break;
 893            case SPEAKER_PHONE:
 894                this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_up_black_24dp);
 895                if (numberOfChoices >= 2) {
 896                    this.binding.inCallActionRight.setOnClickListener(this::switchToEarpiece);
 897                } else {
 898                    this.binding.inCallActionRight.setOnClickListener(null);
 899                    this.binding.inCallActionRight.setClickable(false);
 900                }
 901                break;
 902            case BLUETOOTH:
 903                this.binding.inCallActionRight.setImageResource(R.drawable.ic_bluetooth_audio_black_24dp);
 904                this.binding.inCallActionRight.setOnClickListener(null);
 905                this.binding.inCallActionRight.setClickable(false);
 906                break;
 907        }
 908        this.binding.inCallActionRight.setVisibility(View.VISIBLE);
 909    }
 910
 911    @SuppressLint("RestrictedApi")
 912    private void updateInCallButtonConfigurationVideo(final boolean videoEnabled, final boolean isCameraSwitchable) {
 913        this.binding.inCallActionRight.setVisibility(View.VISIBLE);
 914        if (isCameraSwitchable) {
 915            this.binding.inCallActionFarRight.setImageResource(R.drawable.ic_flip_camera_android_black_24dp);
 916            this.binding.inCallActionFarRight.setVisibility(View.VISIBLE);
 917            this.binding.inCallActionFarRight.setOnClickListener(this::switchCamera);
 918        } else {
 919            this.binding.inCallActionFarRight.setVisibility(View.GONE);
 920        }
 921        if (videoEnabled) {
 922            this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_black_24dp);
 923            this.binding.inCallActionRight.setOnClickListener(this::disableVideo);
 924        } else {
 925            this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_off_black_24dp);
 926            this.binding.inCallActionRight.setOnClickListener(this::enableVideo);
 927        }
 928    }
 929
 930    private void switchCamera(final View view) {
 931        Futures.addCallback(requireRtpConnection().switchCamera(), new FutureCallback<Boolean>() {
 932            @Override
 933            public void onSuccess(@NullableDecl Boolean isFrontCamera) {
 934                binding.localVideo.setMirror(isFrontCamera);
 935            }
 936
 937            @Override
 938            public void onFailure(@NonNull final Throwable throwable) {
 939                Log.d(Config.LOGTAG, "could not switch camera", Throwables.getRootCause(throwable));
 940                Toast.makeText(RtpSessionActivity.this, R.string.could_not_switch_camera, Toast.LENGTH_LONG).show();
 941            }
 942        }, MainThreadExecutor.getInstance());
 943    }
 944
 945    private void enableVideo(View view) {
 946        try {
 947            requireRtpConnection().setVideoEnabled(true);
 948        } catch (final IllegalStateException e) {
 949            Toast.makeText(this, R.string.unable_to_enable_video, Toast.LENGTH_SHORT).show();
 950            return;
 951        }
 952        updateInCallButtonConfigurationVideo(true, requireRtpConnection().isCameraSwitchable());
 953    }
 954
 955    private void disableVideo(View view) {
 956        requireRtpConnection().setVideoEnabled(false);
 957        updateInCallButtonConfigurationVideo(false, requireRtpConnection().isCameraSwitchable());
 958
 959    }
 960
 961    @SuppressLint("RestrictedApi")
 962    private void updateInCallButtonConfigurationMicrophone(final boolean microphoneEnabled) {
 963        if (microphoneEnabled) {
 964            this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_black_24dp);
 965            this.binding.inCallActionLeft.setOnClickListener(this::disableMicrophone);
 966        } else {
 967            this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_off_black_24dp);
 968            this.binding.inCallActionLeft.setOnClickListener(this::enableMicrophone);
 969        }
 970        this.binding.inCallActionLeft.setVisibility(View.VISIBLE);
 971    }
 972
 973    private void updateCallDuration() {
 974        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
 975        if (connection == null || connection.getMedia().contains(Media.VIDEO)) {
 976            this.binding.duration.setVisibility(View.GONE);
 977            return;
 978        }
 979        if (connection.zeroDuration()) {
 980            this.binding.duration.setVisibility(View.GONE);
 981        } else {
 982            this.binding.duration.setText(TimeFrameUtils.formatElapsedTime(connection.getCallDuration(), false));
 983            this.binding.duration.setVisibility(View.VISIBLE);
 984        }
 985    }
 986
 987    private void updateVideoViews(final RtpEndUserState state) {
 988        if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) {
 989            binding.localVideo.setVisibility(View.GONE);
 990            binding.localVideo.release();
 991            binding.remoteVideoWrapper.setVisibility(View.GONE);
 992            binding.remoteVideo.release();
 993            binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
 994            if (isPictureInPicture()) {
 995                binding.appBarLayout.setVisibility(View.GONE);
 996                binding.pipPlaceholder.setVisibility(View.VISIBLE);
 997                if (Arrays.asList(
 998                        RtpEndUserState.APPLICATION_ERROR,
 999                        RtpEndUserState.CONNECTIVITY_ERROR,
1000                        RtpEndUserState.SECURITY_ERROR)
1001                        .contains(state)) {
1002                    binding.pipWarning.setVisibility(View.VISIBLE);
1003                    binding.pipWaiting.setVisibility(View.GONE);
1004                } else {
1005                    binding.pipWarning.setVisibility(View.GONE);
1006                    binding.pipWaiting.setVisibility(View.GONE);
1007                }
1008            } else {
1009                binding.appBarLayout.setVisibility(View.VISIBLE);
1010                binding.pipPlaceholder.setVisibility(View.GONE);
1011            }
1012            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
1013            return;
1014        }
1015        if (isPictureInPicture() && STATES_SHOWING_PIP_PLACEHOLDER.contains(state)) {
1016            binding.localVideo.setVisibility(View.GONE);
1017            binding.remoteVideoWrapper.setVisibility(View.GONE);
1018            binding.appBarLayout.setVisibility(View.GONE);
1019            binding.pipPlaceholder.setVisibility(View.VISIBLE);
1020            binding.pipWarning.setVisibility(View.GONE);
1021            binding.pipWaiting.setVisibility(View.VISIBLE);
1022            binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
1023            return;
1024        }
1025        final Optional<VideoTrack> localVideoTrack = getLocalVideoTrack();
1026        if (localVideoTrack.isPresent() && !isPictureInPicture()) {
1027            ensureSurfaceViewRendererIsSetup(binding.localVideo);
1028            //paint local view over remote view
1029            binding.localVideo.setZOrderMediaOverlay(true);
1030            binding.localVideo.setMirror(requireRtpConnection().isFrontCamera());
1031            addSink(localVideoTrack.get(), binding.localVideo);
1032        } else {
1033            binding.localVideo.setVisibility(View.GONE);
1034        }
1035        final Optional<VideoTrack> remoteVideoTrack = getRemoteVideoTrack();
1036        if (remoteVideoTrack.isPresent()) {
1037            ensureSurfaceViewRendererIsSetup(binding.remoteVideo);
1038            addSink(remoteVideoTrack.get(), binding.remoteVideo);
1039            binding.remoteVideo.setScalingType(
1040                    RendererCommon.ScalingType.SCALE_ASPECT_FILL,
1041                    RendererCommon.ScalingType.SCALE_ASPECT_FIT
1042            );
1043            if (state == RtpEndUserState.CONNECTED) {
1044                binding.appBarLayout.setVisibility(View.GONE);
1045                getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
1046                binding.remoteVideoWrapper.setVisibility(View.VISIBLE);
1047            } else {
1048                binding.appBarLayout.setVisibility(View.VISIBLE);
1049                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
1050                binding.remoteVideoWrapper.setVisibility(View.GONE);
1051            }
1052            if (isPictureInPicture() && !requireRtpConnection().isMicrophoneEnabled()) {
1053                binding.pipLocalMicOffIndicator.setVisibility(View.VISIBLE);
1054            } else {
1055                binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
1056            }
1057        } else {
1058            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
1059            binding.remoteVideoWrapper.setVisibility(View.GONE);
1060            binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
1061        }
1062    }
1063
1064    private Optional<VideoTrack> getLocalVideoTrack() {
1065        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
1066        if (connection == null) {
1067            return Optional.absent();
1068        }
1069        return connection.getLocalVideoTrack();
1070    }
1071
1072    private Optional<VideoTrack> getRemoteVideoTrack() {
1073        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
1074        if (connection == null) {
1075            return Optional.absent();
1076        }
1077        return connection.getRemoteVideoTrack();
1078    }
1079
1080    private void disableMicrophone(View view) {
1081        final JingleRtpConnection rtpConnection = requireRtpConnection();
1082        if (rtpConnection.setMicrophoneEnabled(false)) {
1083            updateInCallButtonConfiguration();
1084        }
1085    }
1086
1087    private void enableMicrophone(View view) {
1088        final JingleRtpConnection rtpConnection = requireRtpConnection();
1089        if (rtpConnection.setMicrophoneEnabled(true)) {
1090            updateInCallButtonConfiguration();
1091        }
1092    }
1093
1094    private void switchToEarpiece(View view) {
1095        requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
1096        acquireProximityWakeLock();
1097    }
1098
1099    private void switchToSpeaker(View view) {
1100        requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
1101        releaseProximityWakeLock();
1102    }
1103
1104    private void retry(View view) {
1105        final Intent intent = getIntent();
1106        final Account account = extractAccount(intent);
1107        final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
1108        final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION);
1109        final String action = intent.getAction();
1110        final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
1111        this.rtpConnectionReference = null;
1112        Log.d(Config.LOGTAG, "attempting retry with " + with.toEscapedString());
1113        proposeJingleRtpSession(account, with, media);
1114    }
1115
1116    private void exit(final View view) {
1117        finish();
1118    }
1119
1120    private void recordVoiceMail(final View view) {
1121        final Intent intent = getIntent();
1122        final Account account = extractAccount(intent);
1123        final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
1124        final Conversation conversation = xmppConnectionService.findOrCreateConversation(account, with, false, true);
1125        final Intent launchIntent = new Intent(this, ConversationsActivity.class);
1126        launchIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
1127        launchIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
1128        launchIntent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP);
1129        launchIntent.putExtra(ConversationsActivity.EXTRA_POST_INIT_ACTION, ConversationsActivity.POST_ACTION_RECORD_VOICE);
1130        startActivity(launchIntent);
1131        finish();
1132    }
1133
1134    private Contact getWith() {
1135        final AbstractJingleConnection.Id id = requireRtpConnection().getId();
1136        final Account account = id.account;
1137        return account.getRoster().getContact(id.with);
1138    }
1139
1140    private JingleRtpConnection requireRtpConnection() {
1141        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
1142        if (connection == null) {
1143            throw new IllegalStateException("No RTP connection found");
1144        }
1145        return connection;
1146    }
1147
1148    @Override
1149    public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) {
1150        Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")");
1151        if (END_CARD.contains(state)) {
1152            Log.d(Config.LOGTAG, "end card reached");
1153            releaseProximityWakeLock();
1154            runOnUiThread(() -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON));
1155        }
1156        if (with.isBareJid()) {
1157            updateRtpSessionProposalState(account, with, state);
1158            return;
1159        }
1160        if (emptyReference(this.rtpConnectionReference)) {
1161            if (END_CARD.contains(state)) {
1162                Log.d(Config.LOGTAG, "not reinitializing session");
1163                return;
1164            }
1165            //this happens when going from proposed session to actual session
1166            reInitializeActivityWithRunningRtpSession(account, with, sessionId);
1167            return;
1168        }
1169        final AbstractJingleConnection.Id id = requireRtpConnection().getId();
1170        final boolean verified = requireRtpConnection().isVerified();
1171        final Set<Media> media = getMedia();
1172        final Contact contact = getWith();
1173        if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
1174            if (state == RtpEndUserState.ENDED) {
1175                finish();
1176                return;
1177            }
1178            runOnUiThread(() -> {
1179                updateStateDisplay(state, media);
1180                updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(state));
1181                updateButtonConfiguration(state, media);
1182                updateVideoViews(state);
1183                updateProfilePicture(state, contact);
1184                invalidateOptionsMenu();
1185            });
1186            if (END_CARD.contains(state)) {
1187                final JingleRtpConnection rtpConnection = requireRtpConnection();
1188                resetIntent(account, with, state, rtpConnection.getMedia());
1189                releaseVideoTracks(rtpConnection);
1190                this.rtpConnectionReference = null;
1191            }
1192        } else {
1193            Log.d(Config.LOGTAG, "received update for other rtp session");
1194        }
1195    }
1196
1197    @Override
1198    public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
1199        Log.d(Config.LOGTAG, "onAudioDeviceChanged in activity: selected:" + selectedAudioDevice + ", available:" + availableAudioDevices);
1200        try {
1201            if (getMedia().contains(Media.VIDEO)) {
1202                Log.d(Config.LOGTAG, "nothing to do; in video mode");
1203                return;
1204            }
1205            final RtpEndUserState endUserState = requireRtpConnection().getEndUserState();
1206            if (endUserState == RtpEndUserState.CONNECTED) {
1207                final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
1208                updateInCallButtonConfigurationSpeaker(
1209                        audioManager.getSelectedAudioDevice(),
1210                        audioManager.getAudioDevices().size()
1211                );
1212            } else if (END_CARD.contains(endUserState)) {
1213                Log.d(Config.LOGTAG, "onAudioDeviceChanged() nothing to do because end card has been reached");
1214            } else {
1215                putProximityWakeLockInProperState(selectedAudioDevice);
1216            }
1217        } catch (IllegalStateException e) {
1218            Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed");
1219        }
1220    }
1221
1222    @Override
1223    protected void onSaveInstanceState(@NonNull @NotNull Bundle outState) {
1224        super.onSaveInstanceState(outState);
1225        outState.putBoolean("dialpad_visible", binding.dialpad.getVisibility() == View.VISIBLE);
1226    }
1227
1228    private void updateRtpSessionProposalState(final Account account, final Jid with, final RtpEndUserState state) {
1229        final Intent currentIntent = getIntent();
1230        final String withExtra = currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH);
1231        if (withExtra == null) {
1232            return;
1233        }
1234        if (Jid.ofEscaped(withExtra).asBareJid().equals(with)) {
1235            runOnUiThread(() -> {
1236                updateVerifiedShield(false);
1237                updateStateDisplay(state);
1238                updateButtonConfiguration(state);
1239                updateProfilePicture(state);
1240                invalidateOptionsMenu();
1241            });
1242            resetIntent(account, with, state, actionToMedia(currentIntent.getAction()));
1243        }
1244    }
1245
1246    private void resetIntent(final Bundle extras) {
1247        final Intent intent = new Intent(Intent.ACTION_VIEW);
1248        intent.putExtras(extras);
1249        setIntent(intent);
1250    }
1251
1252    private void resetIntent(final Account account, Jid with, final RtpEndUserState state, final Set<Media> media) {
1253        final Intent intent = new Intent(Intent.ACTION_VIEW);
1254        intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
1255        if (account.getRoster().getContact(with).getPresences().anySupport(Namespace.JINGLE_MESSAGE)) {
1256            intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString());
1257        } else {
1258            intent.putExtra(EXTRA_WITH, with.toEscapedString());
1259        }
1260        intent.putExtra(EXTRA_LAST_REPORTED_STATE, state.toString());
1261        intent.putExtra(EXTRA_LAST_ACTION, media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL);
1262        setIntent(intent);
1263    }
1264
1265    private static boolean emptyReference(final WeakReference<?> weakReference) {
1266        return weakReference == null || weakReference.get() == null;
1267    }
1268}