RtpSessionActivity.java

   1package eu.siacs.conversations.ui;
   2
   3import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
   4import static java.util.Arrays.asList;
   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.ActivityInfo;
  13import android.content.pm.PackageManager;
  14import android.opengl.GLException;
  15import android.os.Build;
  16import android.os.Bundle;
  17import android.os.Handler;
  18import android.os.PowerManager;
  19import android.util.Log;
  20import android.util.Rational;
  21import android.view.KeyEvent;
  22import android.view.Menu;
  23import android.view.MenuItem;
  24import android.view.View;
  25import android.view.WindowManager;
  26import android.widget.Toast;
  27import androidx.annotation.NonNull;
  28import androidx.annotation.Nullable;
  29import androidx.annotation.RequiresApi;
  30import androidx.annotation.StringRes;
  31import androidx.databinding.DataBindingUtil;
  32import com.google.android.material.floatingactionbutton.FloatingActionButton;
  33import com.google.common.base.Optional;
  34import com.google.common.base.Preconditions;
  35import com.google.common.base.Strings;
  36import com.google.common.base.Throwables;
  37import com.google.common.collect.ImmutableList;
  38import com.google.common.collect.ImmutableSet;
  39import com.google.common.util.concurrent.FutureCallback;
  40import com.google.common.util.concurrent.Futures;
  41
  42import org.jetbrains.annotations.NotNull;
  43import org.webrtc.RendererCommon;
  44import org.webrtc.SurfaceViewRenderer;
  45import org.webrtc.VideoTrack;
  46
  47import java.lang.ref.WeakReference;
  48import java.util.Arrays;
  49import java.util.Collections;
  50import java.util.List;
  51import java.util.Set;
  52
  53import eu.siacs.conversations.Config;
  54import eu.siacs.conversations.R;
  55import eu.siacs.conversations.databinding.ActivityRtpSessionBinding;
  56import eu.siacs.conversations.entities.Account;
  57import eu.siacs.conversations.entities.Contact;
  58import eu.siacs.conversations.entities.Conversation;
  59import eu.siacs.conversations.services.CallIntegration;
  60import eu.siacs.conversations.services.CallIntegrationConnectionService;
  61import eu.siacs.conversations.services.XmppConnectionService;
  62import eu.siacs.conversations.ui.widget.DialpadView;
  63import eu.siacs.conversations.ui.util.AvatarWorkerTask;
  64import eu.siacs.conversations.ui.util.MainThreadExecutor;
  65import eu.siacs.conversations.ui.util.Rationals;
  66import eu.siacs.conversations.utils.PermissionUtils;
  67import eu.siacs.conversations.utils.TimeFrameUtils;
  68import eu.siacs.conversations.xmpp.Jid;
  69import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
  70import eu.siacs.conversations.xmpp.jingle.ContentAddition;
  71import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
  72import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
  73import eu.siacs.conversations.xmpp.jingle.Media;
  74import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
  75import eu.siacs.conversations.xmpp.jingle.RtpCapability;
  76import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
  77
  78public class RtpSessionActivity extends XmppActivity
  79        implements XmppConnectionService.OnJingleRtpConnectionUpdate,
  80                eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged {
  81
  82    public static final String EXTRA_WITH = "with";
  83    public static final String EXTRA_SESSION_ID = "session_id";
  84    public static final String EXTRA_PROPOSED_SESSION_ID = "proposed_session_id";
  85    public static final String EXTRA_LAST_REPORTED_STATE = "last_reported_state";
  86    public static final String EXTRA_LAST_ACTION = "last_action";
  87    public static final String ACTION_ACCEPT_CALL = "action_accept_call";
  88    public static final String ACTION_MAKE_VOICE_CALL = "action_make_voice_call";
  89    public static final String ACTION_MAKE_VIDEO_CALL = "action_make_video_call";
  90
  91    private static final int CALL_DURATION_UPDATE_INTERVAL = 250;
  92    private static final int BUTTON_VISIBILITY_TIMEOUT = 10_000;
  93
  94    public static final List<RtpEndUserState> END_CARD =
  95            Arrays.asList(
  96                    RtpEndUserState.APPLICATION_ERROR,
  97                    RtpEndUserState.SECURITY_ERROR,
  98                    RtpEndUserState.DECLINED_OR_BUSY,
  99                    RtpEndUserState.CONTACT_OFFLINE,
 100                    RtpEndUserState.CONNECTIVITY_ERROR,
 101                    RtpEndUserState.CONNECTIVITY_LOST_ERROR,
 102                    RtpEndUserState.RETRACTED);
 103    private static final List<RtpEndUserState> STATES_SHOWING_HELP_BUTTON =
 104            Arrays.asList(
 105                    RtpEndUserState.APPLICATION_ERROR,
 106                    RtpEndUserState.CONNECTIVITY_ERROR,
 107                    RtpEndUserState.SECURITY_ERROR);
 108    private static final List<RtpEndUserState> STATES_SHOWING_SWITCH_TO_CHAT =
 109            Arrays.asList(
 110                    RtpEndUserState.CONNECTING,
 111                    RtpEndUserState.CONNECTED,
 112                    RtpEndUserState.RECONNECTING,
 113                    RtpEndUserState.INCOMING_CONTENT_ADD);
 114    private static final List<RtpEndUserState> STATES_CONSIDERED_CONNECTED =
 115            Arrays.asList(RtpEndUserState.CONNECTED, RtpEndUserState.RECONNECTING);
 116    private static final List<RtpEndUserState> STATES_SHOWING_PIP_PLACEHOLDER =
 117            Arrays.asList(
 118                    RtpEndUserState.ACCEPTING_CALL,
 119                    RtpEndUserState.CONNECTING,
 120                    RtpEndUserState.RECONNECTING);
 121    private static final List<RtpEndUserState> STATES_SHOWING_SPEAKER_CONFIGURATION =
 122            new ImmutableList.Builder<RtpEndUserState>()
 123                    .add(RtpEndUserState.FINDING_DEVICE)
 124                    .add(RtpEndUserState.RINGING)
 125                    .add(RtpEndUserState.ACCEPTING_CALL)
 126                    .add(RtpEndUserState.CONNECTING)
 127                    .addAll(STATES_CONSIDERED_CONNECTED)
 128                    .build();
 129    private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session";
 130    private static final int REQUEST_ACCEPT_CALL = 0x1111;
 131    private static final int REQUEST_ACCEPT_CONTENT = 0x1112;
 132    private static final int REQUEST_ADD_CONTENT = 0x1113;
 133    private WeakReference<JingleRtpConnection> rtpConnectionReference;
 134
 135    private ActivityRtpSessionBinding binding;
 136    private PowerManager.WakeLock mProximityWakeLock;
 137
 138    private final Handler mHandler = new Handler();
 139    private final Runnable mTickExecutor =
 140            new Runnable() {
 141                @Override
 142                public void run() {
 143                    updateCallDuration();
 144                    mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL);
 145                }
 146            };
 147    private boolean buttonsHiddenAfterTimeout = false;
 148    private final Runnable mVisibilityToggleExecutor = this::updateButtonInVideoCallVisibility;
 149
 150    public static Set<Media> actionToMedia(final String action) {
 151        if (ACTION_MAKE_VIDEO_CALL.equals(action)) {
 152            return ImmutableSet.of(Media.AUDIO, Media.VIDEO);
 153        } else if (ACTION_MAKE_VOICE_CALL.equals(action)) {
 154            return ImmutableSet.of(Media.AUDIO);
 155        } else {
 156            Log.w(
 157                    Config.LOGTAG,
 158                    "actionToMedia can not get media set from unknown action " + action);
 159            return Collections.emptySet();
 160        }
 161    }
 162
 163    private static void addSink(
 164            final VideoTrack videoTrack, final SurfaceViewRenderer surfaceViewRenderer) {
 165        try {
 166            videoTrack.addSink(surfaceViewRenderer);
 167        } catch (final IllegalStateException e) {
 168            Log.e(
 169                    Config.LOGTAG,
 170                    "possible race condition on trying to display video track. ignoring",
 171                    e);
 172        }
 173    }
 174
 175    @Override
 176    public void onCreate(Bundle savedInstanceState) {
 177        super.onCreate(savedInstanceState);
 178        getWindow()
 179                .addFlags(
 180                        WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
 181                                | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
 182                                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
 183                                | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
 184        this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session);
 185        this.binding.remoteVideo.setOnClickListener(this::onVideoScreenClick);
 186        this.binding.localVideo.setOnClickListener(this::onVideoScreenClick);
 187        setSupportActionBar(binding.toolbar);
 188
 189        binding.dialpad.setClickConsumer(tag -> {
 190            final JingleRtpConnection connection =
 191                this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
 192            if (connection != null) connection.applyDtmfTone(tag);
 193        });
 194
 195        if (savedInstanceState != null) {
 196            boolean dialpadVisible = savedInstanceState.getBoolean("dialpad_visible");
 197            binding.dialpad.setVisibility(dialpadVisible ? View.VISIBLE : View.GONE);
 198        }
 199
 200        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
 201    }
 202
 203    private void onVideoScreenClick(final View view) {
 204        resetVisibilityExecutorShowButtons();
 205    }
 206
 207    @Override
 208    public boolean onCreateOptionsMenu(final Menu menu) {
 209        getMenuInflater().inflate(R.menu.activity_rtp_session, menu);
 210        final MenuItem help = menu.findItem(R.id.action_help);
 211        final MenuItem gotoChat = menu.findItem(R.id.action_goto_chat);
 212        final MenuItem dialpad = menu.findItem(R.id.action_dialpad);
 213        final MenuItem switchToVideo = menu.findItem(R.id.action_switch_to_video);
 214        help.setVisible(Config.HELP != null && isHelpButtonVisible());
 215        gotoChat.setVisible(isSwitchToConversationVisible());
 216        switchToVideo.setVisible(isSwitchToVideoVisible());
 217        dialpad.setVisible(isAudioOnlyConversation());
 218        return super.onCreateOptionsMenu(menu);
 219    }
 220
 221    @Override
 222    public boolean onKeyDown(final int keyCode, final KeyEvent event) {
 223        if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
 224            if (xmppConnectionService != null) {
 225                if (xmppConnectionService.getNotificationService().stopSoundAndVibration()) {
 226                    return true;
 227                }
 228            }
 229        }
 230        return super.onKeyDown(keyCode, event);
 231    }
 232
 233    private boolean isHelpButtonVisible() {
 234        try {
 235            return STATES_SHOWING_HELP_BUTTON.contains(requireRtpConnection().getEndUserState());
 236        } catch (IllegalStateException e) {
 237            final Intent intent = getIntent();
 238            final String state =
 239                    intent != null ? intent.getStringExtra(EXTRA_LAST_REPORTED_STATE) : null;
 240            if (state != null) {
 241                return STATES_SHOWING_HELP_BUTTON.contains(RtpEndUserState.valueOf(state));
 242            } else {
 243                return false;
 244            }
 245        }
 246    }
 247
 248    private boolean isSwitchToConversationVisible() {
 249        final JingleRtpConnection connection =
 250                this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
 251        return connection != null
 252                && STATES_SHOWING_SWITCH_TO_CHAT.contains(connection.getEndUserState());
 253    }
 254
 255    private boolean isAudioOnlyConversation() {
 256        final JingleRtpConnection connection =
 257                this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
 258
 259        return connection != null && !connection.getMedia().contains(Media.VIDEO);
 260    }
 261
 262    private boolean isSwitchToVideoVisible() {
 263        final JingleRtpConnection connection =
 264                this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
 265        if (connection == null) {
 266            return false;
 267        }
 268        return connection.isSwitchToVideoAvailable();
 269    }
 270
 271    private void switchToConversation() {
 272        final Contact contact = getWith();
 273        final Conversation conversation =
 274                xmppConnectionService.findOrCreateConversation(
 275                        contact.getAccount(), contact.getJid(), false, true);
 276        switchToConversation(conversation);
 277    }
 278
 279    private void toggleDialpadVisibility() {
 280        if (binding.dialpad.getVisibility() == View.VISIBLE) {
 281            binding.dialpad.setVisibility(View.GONE);
 282        }
 283        else {
 284            binding.dialpad.setVisibility(View.VISIBLE);
 285        }
 286    }
 287
 288    public boolean onOptionsItemSelected(final MenuItem item) {
 289        final var itemItem = item.getItemId();
 290        if (itemItem == R.id.action_help) {
 291            launchHelpInBrowser();
 292            return true;
 293        } else if (itemItem == R.id.action_goto_chat) {
 294            switchToConversation();
 295            return true;
 296        } else if (itemItem == R.id.action_dialpad) {
 297            toggleDialpadVisibility();
 298            return true;
 299        } else if (itemItem == R.id.action_switch_to_video) {
 300            requestPermissionAndSwitchToVideo();
 301            return true;
 302        } else {
 303            return super.onOptionsItemSelected(item);
 304        }
 305    }
 306
 307    private void launchHelpInBrowser() {
 308        final Intent intent = new Intent(Intent.ACTION_VIEW, Config.HELP);
 309        try {
 310            startActivity(intent);
 311        } catch (final ActivityNotFoundException e) {
 312            Toast.makeText(this, R.string.no_application_found_to_open_link, Toast.LENGTH_LONG)
 313                    .show();
 314        }
 315    }
 316
 317    private void endCall(View view) {
 318        endCall();
 319    }
 320
 321    private void endCall() {
 322        if (this.rtpConnectionReference == null) {
 323            retractSessionProposal();
 324            finish();
 325        } else {
 326            try {
 327                requireRtpConnection().endCall();
 328            } catch (final IllegalStateException e) {
 329                // No call, already done
 330                finish();
 331            }
 332        }
 333    }
 334
 335    private void retractSessionProposal() {
 336        final Intent intent = getIntent();
 337        final String action = intent.getAction();
 338        final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION);
 339        final Account account = extractAccount(intent);
 340        final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
 341        final String state = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE);
 342        if (!Intent.ACTION_VIEW.equals(action)
 343                || state == null
 344                || !END_CARD.contains(RtpEndUserState.valueOf(state))) {
 345            final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
 346            resetIntent(account, with, RtpEndUserState.RETRACTED, media);
 347        }
 348        xmppConnectionService
 349                .getJingleConnectionManager()
 350                .retractSessionProposal(account, with.asBareJid());
 351    }
 352
 353    private void rejectCall(View view) {
 354        requireRtpConnection().rejectCall();
 355        finish();
 356    }
 357
 358    private void acceptCall(View view) {
 359        requestPermissionsAndAcceptCall();
 360    }
 361
 362    private void acceptContentAdd() {
 363        try {
 364            final ContentAddition pendingContentAddition =
 365                    requireRtpConnection().getPendingContentAddition();
 366            if (pendingContentAddition == null) {
 367                Log.d(Config.LOGTAG, "content offer was gone after granting permission");
 368                return;
 369            }
 370            requireRtpConnection().acceptContentAdd(pendingContentAddition.summary);
 371        } catch (final IllegalStateException e) {
 372            Toast.makeText(this, Strings.nullToEmpty(e.getMessage()), Toast.LENGTH_SHORT).show();
 373        }
 374    }
 375
 376    private void requestPermissionAndSwitchToVideo() {
 377        final List<String> permissions = permissions(ImmutableSet.of(Media.VIDEO, Media.AUDIO));
 378        if (PermissionUtils.hasPermission(this, permissions, REQUEST_ADD_CONTENT)) {
 379            switchToVideo();
 380        }
 381    }
 382
 383    private void switchToVideo() {
 384        try {
 385            requireRtpConnection().addMedia(Media.VIDEO);
 386        } catch (final IllegalStateException e) {
 387            Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
 388        }
 389    }
 390
 391    private void acceptContentAdd(final ContentAddition contentAddition) {
 392        if (contentAddition == null
 393                || contentAddition.direction != ContentAddition.Direction.INCOMING) {
 394            Log.d(Config.LOGTAG, "ignore press on content-accept button");
 395            return;
 396        }
 397        requestPermissionAndAcceptContentAdd(contentAddition);
 398    }
 399
 400    private void requestPermissionAndAcceptContentAdd(final ContentAddition contentAddition) {
 401        final List<String> permissions = permissions(contentAddition.media());
 402        if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CONTENT)) {
 403            try {
 404                requireRtpConnection().acceptContentAdd(contentAddition.summary);
 405            } catch (final IllegalStateException e) {
 406                Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
 407            }
 408        }
 409    }
 410
 411    private void rejectContentAdd(final View view) {
 412        requireRtpConnection().rejectContentAdd();
 413    }
 414
 415    private void requestPermissionsAndAcceptCall() {
 416        final List<String> permissions = permissions(getMedia());
 417        if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CALL)) {
 418            putScreenInCallMode();
 419            acceptCall();
 420        }
 421    }
 422
 423    private List<String> permissions(final Set<Media> media) {
 424        final ImmutableList.Builder<String> permissions = ImmutableList.builder();
 425        if (media.contains(Media.VIDEO)) {
 426            permissions.add(Manifest.permission.CAMERA).add(Manifest.permission.RECORD_AUDIO);
 427        } else {
 428            permissions.add(Manifest.permission.RECORD_AUDIO);
 429        }
 430        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
 431            permissions.add(Manifest.permission.BLUETOOTH_CONNECT);
 432        }
 433        return permissions.build();
 434    }
 435
 436    private void acceptCall() {
 437        try {
 438            requireRtpConnection().acceptCall();
 439        } catch (final IllegalStateException e) {
 440            Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
 441        }
 442    }
 443
 444    private void putScreenInCallMode() {
 445        putScreenInCallMode(requireRtpConnection().getMedia());
 446    }
 447
 448    private void putScreenInCallMode(final Set<Media> media) {
 449        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
 450        if (Media.audioOnly(media)) {
 451            final JingleRtpConnection rtpConnection =
 452                    rtpConnectionReference != null ? rtpConnectionReference.get() : null;
 453            final CallIntegration callIntegration =
 454                    rtpConnection == null ? null : rtpConnection.getCallIntegration();
 455            if (callIntegration == null
 456                    || callIntegration.getSelectedAudioDevice()
 457                            == CallIntegration.AudioDevice.EARPIECE) {
 458                acquireProximityWakeLock();
 459            }
 460        }
 461        lockOrientation(media);
 462    }
 463
 464    private void lockOrientation(final Set<Media> media) {
 465        if (Media.audioOnly(media)) {
 466            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
 467        } else {
 468            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
 469        }
 470    }
 471
 472    @SuppressLint("WakelockTimeout")
 473    private void acquireProximityWakeLock() {
 474        final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
 475        if (powerManager == null) {
 476            Log.e(Config.LOGTAG, "power manager not available");
 477            return;
 478        }
 479        if (isFinishing()) {
 480            Log.e(Config.LOGTAG, "do not acquire wakelock. activity is finishing");
 481            return;
 482        }
 483        if (this.mProximityWakeLock == null) {
 484            this.mProximityWakeLock =
 485                    powerManager.newWakeLock(
 486                            PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, PROXIMITY_WAKE_LOCK_TAG);
 487        }
 488        if (!this.mProximityWakeLock.isHeld()) {
 489            Log.d(Config.LOGTAG, "acquiring proximity wake lock");
 490            this.mProximityWakeLock.acquire();
 491        }
 492    }
 493
 494    private void releaseProximityWakeLock() {
 495        if (this.mProximityWakeLock != null && mProximityWakeLock.isHeld()) {
 496            Log.d(Config.LOGTAG, "releasing proximity wake lock");
 497            this.mProximityWakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
 498            this.mProximityWakeLock = null;
 499        }
 500    }
 501
 502    private void putProximityWakeLockInProperState(final CallIntegration.AudioDevice audioDevice) {
 503        if (audioDevice == CallIntegration.AudioDevice.EARPIECE) {
 504            acquireProximityWakeLock();
 505        } else {
 506            releaseProximityWakeLock();
 507        }
 508    }
 509
 510    @Override
 511    protected void refreshUiReal() {}
 512
 513    @Override
 514    public void onNewIntent(final Intent intent) {
 515        Log.d(Config.LOGTAG, this.getClass().getName() + ".onNewIntent()");
 516        super.onNewIntent(intent);
 517        if (intent == null) {
 518            return;
 519        }
 520        setIntent(intent);
 521        if (xmppConnectionService == null) {
 522            Log.d(
 523                    Config.LOGTAG,
 524                    "RtpSessionActivity: background service wasn't bound in onNewIntent()");
 525            return;
 526        }
 527        initializeWithIntent(Event.ON_NEW_INTENT, intent);
 528    }
 529
 530    @Override
 531    protected void onBackendConnected() {
 532        final var intent = getIntent();
 533        if (intent == null) {
 534            return;
 535        }
 536        initializeWithIntent(Event.ON_BACKEND_CONNECTED, intent);
 537    }
 538
 539    private void initializeWithIntent(final Event event, @NonNull final Intent intent) {
 540        final String action = intent.getAction();
 541        Log.d(Config.LOGTAG, "initializeWithIntent(" + event + "," + action + ")");
 542        final Account account = extractAccount(intent);
 543        final var extraWith = intent.getStringExtra(EXTRA_WITH);
 544        final Jid with = Strings.isNullOrEmpty(extraWith) ? null : Jid.of(extraWith);
 545        if (with == null || account == null) {
 546            Log.e(Config.LOGTAG, "intent is missing extras (account or with)");
 547            return;
 548        }
 549        final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID);
 550        if (sessionId != null) {
 551            if (initializeActivityWithRunningRtpSession(account, with, sessionId)) {
 552                return;
 553            }
 554            if (ACTION_ACCEPT_CALL.equals(intent.getAction())) {
 555                Log.d(Config.LOGTAG, "intent action was accept");
 556                requestPermissionsAndAcceptCall();
 557                resetIntent(intent.getExtras());
 558            }
 559        } else if (Intent.ACTION_VIEW.equals(action)) {
 560            final String proposedSessionId = intent.getStringExtra(EXTRA_PROPOSED_SESSION_ID);
 561            final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession =
 562                    xmppConnectionService
 563                            .getJingleConnectionManager()
 564                            .getTerminalSessionState(with, proposedSessionId);
 565            if (terminatedRtpSession != null) {
 566                // termination (due to message error or 'busy' was faster than opening the activity
 567                initializeWithTerminatedSessionState(account, with, terminatedRtpSession);
 568                return;
 569            }
 570            final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE);
 571            final RtpEndUserState state =
 572                    extraLastState == null ? null : RtpEndUserState.valueOf(extraLastState);
 573            final var contact = account.getRoster().getContact(with);
 574            if (state != null) {
 575                Log.d(Config.LOGTAG, "restored last state from intent extra");
 576                updateButtonConfiguration(state);
 577                updateVerifiedShield(false);
 578                updateStateDisplay(state);
 579                updateIncomingCallScreen(state);
 580                updateSupportWarning(state, contact);
 581                invalidateOptionsMenu();
 582            }
 583            setWith(state, contact);
 584            if (xmppConnectionService
 585                    .getJingleConnectionManager()
 586                    .fireJingleRtpConnectionStateUpdates()) {
 587                return;
 588            }
 589            if (END_CARD.contains(state)) {
 590                return;
 591            }
 592            final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION);
 593            final Set<Media> media = actionToMedia(lastAction);
 594            if (xmppConnectionService
 595                    .getJingleConnectionManager()
 596                    .hasMatchingProposal(account, with)) {
 597                putScreenInCallMode(media);
 598                return;
 599            }
 600            Log.d(Config.LOGTAG, "restored state (" + state + ") was not an end card. finishing");
 601            finish();
 602        }
 603    }
 604
 605    private void setWith(final RtpEndUserState state) {
 606        setWith(state, getWith());
 607    }
 608
 609    private void setWith(final RtpEndUserState state, final Contact contact) {
 610        binding.with.setText(contact.getDisplayName());
 611        if (Arrays.asList(RtpEndUserState.INCOMING_CALL, RtpEndUserState.ACCEPTING_CALL)
 612                .contains(state)) {
 613            binding.withJid.setText(contact.getJid().asBareJid().toString());
 614            binding.withJid.setVisibility(View.VISIBLE);
 615        } else {
 616            binding.withJid.setVisibility(View.GONE);
 617        }
 618    }
 619
 620    @Override
 621    public void onRequestPermissionsResult(
 622            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
 623        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
 624        final PermissionUtils.PermissionResult permissionResult =
 625                PermissionUtils.removeBluetoothConnect(permissions, grantResults);
 626        if (PermissionUtils.allGranted(permissionResult.grantResults)) {
 627            if (requestCode == REQUEST_ACCEPT_CALL) {
 628                acceptCall();
 629            } else if (requestCode == REQUEST_ACCEPT_CONTENT) {
 630                acceptContentAdd();
 631            } else if (requestCode == REQUEST_ADD_CONTENT) {
 632                switchToVideo();
 633            }
 634        } else {
 635            @StringRes int res;
 636            final String firstDenied =
 637                    getFirstDenied(permissionResult.grantResults, permissionResult.permissions);
 638            if (firstDenied == null) {
 639                return;
 640            }
 641            if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) {
 642                res = R.string.no_microphone_permission;
 643            } else if (Manifest.permission.CAMERA.equals(firstDenied)) {
 644                res = R.string.no_camera_permission;
 645            } else {
 646                throw new IllegalStateException("Invalid permission result request");
 647            }
 648            Toast.makeText(this, getString(res, getString(R.string.app_name)), Toast.LENGTH_SHORT)
 649                    .show();
 650        }
 651    }
 652
 653    @Override
 654    public void onStart() {
 655        super.onStart();
 656        mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL);
 657        mHandler.postDelayed(mVisibilityToggleExecutor, BUTTON_VISIBILITY_TIMEOUT);
 658        this.binding.remoteVideo.setOnAspectRatioChanged(this);
 659    }
 660
 661    @Override
 662    public void onResume() {
 663        super.onResume();
 664        resetVisibilityExecutorShowButtons();
 665    }
 666
 667    @Override
 668    public void onStop() {
 669        mHandler.removeCallbacks(mTickExecutor);
 670        mHandler.removeCallbacks(mVisibilityToggleExecutor);
 671        binding.remoteVideo.release();
 672        binding.remoteVideo.setOnAspectRatioChanged(null);
 673        binding.localVideo.release();
 674        final WeakReference<JingleRtpConnection> weakReference = this.rtpConnectionReference;
 675        final JingleRtpConnection jingleRtpConnection =
 676                weakReference == null ? null : weakReference.get();
 677        if (jingleRtpConnection != null) {
 678            releaseVideoTracks(jingleRtpConnection);
 679        }
 680        releaseProximityWakeLock();
 681        super.onStop();
 682    }
 683
 684    private void releaseVideoTracks(final JingleRtpConnection jingleRtpConnection) {
 685        final Optional<VideoTrack> remoteVideo = jingleRtpConnection.getRemoteVideoTrack();
 686        if (remoteVideo.isPresent()) {
 687            remoteVideo.get().removeSink(binding.remoteVideo);
 688        }
 689        final Optional<VideoTrack> localVideo = jingleRtpConnection.getLocalVideoTrack();
 690        if (localVideo.isPresent()) {
 691            localVideo.get().removeSink(binding.localVideo);
 692        }
 693    }
 694
 695    @Override
 696    public void onBackPressed() {
 697        if (isConnected()) {
 698            if (switchToPictureInPicture()) {
 699                return;
 700            }
 701        } else {
 702            endCall();
 703        }
 704        super.onBackPressed();
 705    }
 706
 707    @Override
 708    public void onUserLeaveHint() {
 709        super.onUserLeaveHint();
 710        if (switchToPictureInPicture()) {
 711            return;
 712        }
 713        // TODO apparently this method is not getting called on Android 10 when using the task
 714        // switcher
 715        if (emptyReference(rtpConnectionReference) && xmppConnectionService != null) {
 716            retractSessionProposal();
 717        }
 718    }
 719
 720    private boolean isConnected() {
 721        final JingleRtpConnection connection =
 722                this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
 723        final RtpEndUserState endUserState =
 724                connection == null ? null : connection.getEndUserState();
 725        return STATES_CONSIDERED_CONNECTED.contains(endUserState)
 726                || endUserState == RtpEndUserState.INCOMING_CONTENT_ADD;
 727    }
 728
 729    private boolean switchToPictureInPicture() {
 730        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && deviceSupportsPictureInPicture()) {
 731            if (shouldBePictureInPicture()) {
 732                startPictureInPicture();
 733                return true;
 734            }
 735        }
 736        return false;
 737    }
 738
 739    @RequiresApi(api = Build.VERSION_CODES.O)
 740    private void startPictureInPicture() {
 741        try {
 742            final Rational rational = this.binding.remoteVideo.getAspectRatio();
 743            final Rational clippedRational = Rationals.clip(rational);
 744            Log.d(
 745                    Config.LOGTAG,
 746                    "suggested rational " + rational + ". clipped to " + clippedRational);
 747            enterPictureInPictureMode(
 748                    new PictureInPictureParams.Builder().setAspectRatio(clippedRational).build());
 749        } catch (final IllegalStateException e) {
 750            // this sometimes happens on Samsung phones (possibly when Knox is enabled)
 751            Log.w(Config.LOGTAG, "unable to enter picture in picture mode", e);
 752        }
 753    }
 754
 755    @Override
 756    public void onAspectRatioChanged(final Rational rational) {
 757        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isPictureInPicture()) {
 758            final Rational clippedRational = Rationals.clip(rational);
 759            Log.d(
 760                    Config.LOGTAG,
 761                    "suggested rational after aspect ratio change "
 762                            + rational
 763                            + ". clipped to "
 764                            + clippedRational);
 765            setPictureInPictureParams(
 766                    new PictureInPictureParams.Builder().setAspectRatio(clippedRational).build());
 767        }
 768    }
 769
 770    private boolean deviceSupportsPictureInPicture() {
 771        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 772            return getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
 773        } else {
 774            return false;
 775        }
 776    }
 777
 778    private boolean shouldBePictureInPicture() {
 779        try {
 780            final JingleRtpConnection rtpConnection = requireRtpConnection();
 781            return rtpConnection.getMedia().contains(Media.VIDEO)
 782                    && Arrays.asList(
 783                                    RtpEndUserState.ACCEPTING_CALL,
 784                                    RtpEndUserState.CONNECTING,
 785                                    RtpEndUserState.CONNECTED)
 786                            .contains(rtpConnection.getEndUserState());
 787        } catch (final IllegalStateException e) {
 788            return false;
 789        }
 790    }
 791
 792    private boolean isInConnectedVideoCall() {
 793        final JingleRtpConnection rtpConnection;
 794        try {
 795            rtpConnection = requireRtpConnection();
 796        } catch (final IllegalStateException e) {
 797            return false;
 798        }
 799        return rtpConnection.getMedia().contains(Media.VIDEO)
 800                && rtpConnection.getEndUserState() == RtpEndUserState.CONNECTED;
 801    }
 802
 803    private boolean initializeActivityWithRunningRtpSession(
 804            final Account account, Jid with, String sessionId) {
 805        final WeakReference<JingleRtpConnection> reference =
 806                xmppConnectionService
 807                        .getJingleConnectionManager()
 808                        .findJingleRtpConnection(account, with, sessionId);
 809        if (reference == null || reference.get() == null) {
 810            final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession =
 811                    xmppConnectionService
 812                            .getJingleConnectionManager()
 813                            .getTerminalSessionState(with, sessionId);
 814            if (terminatedRtpSession == null) {
 815                throw new IllegalStateException(
 816                        "failed to initialize activity with running rtp session. session not"
 817                                + " found");
 818            }
 819            initializeWithTerminatedSessionState(account, with, terminatedRtpSession);
 820            return true;
 821        }
 822        this.rtpConnectionReference = reference;
 823        final RtpEndUserState currentState = requireRtpConnection().getEndUserState();
 824        final boolean verified = requireRtpConnection().isVerified();
 825        if (currentState == RtpEndUserState.ENDED) {
 826            finish();
 827            return true;
 828        }
 829        final Set<Media> media = getMedia();
 830        final ContentAddition contentAddition = getPendingContentAddition();
 831        if (currentState == RtpEndUserState.INCOMING_CALL) {
 832            getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
 833        }
 834        if (JingleRtpConnection.STATES_SHOWING_ONGOING_CALL.contains(
 835                requireRtpConnection().getState())) {
 836            putScreenInCallMode();
 837        }
 838        setWith(currentState);
 839        updateVideoViews(currentState);
 840        updateStateDisplay(currentState, media, contentAddition);
 841        updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(currentState));
 842        updateButtonConfiguration(currentState, media, contentAddition);
 843        updateIncomingCallScreen(currentState);
 844        invalidateOptionsMenu();
 845        return false;
 846    }
 847
 848    private void initializeWithTerminatedSessionState(
 849            final Account account,
 850            final Jid with,
 851            final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession) {
 852        Log.d(Config.LOGTAG, "initializeWithTerminatedSessionState()");
 853        if (terminatedRtpSession.state == RtpEndUserState.ENDED) {
 854            finish();
 855            return;
 856        }
 857        final RtpEndUserState state = terminatedRtpSession.state;
 858        resetIntent(account, with, terminatedRtpSession.state, terminatedRtpSession.media);
 859        updateButtonConfiguration(state);
 860        updateStateDisplay(state);
 861        updateIncomingCallScreen(state);
 862        updateCallDuration();
 863        updateVerifiedShield(false);
 864        invalidateOptionsMenu();
 865        final var contact = account.getRoster().getContact(with);
 866        setWith(state, contact);
 867        updateSupportWarning(state, contact);
 868    }
 869
 870    private void reInitializeActivityWithRunningRtpSession(
 871            final Account account, Jid with, String sessionId) {
 872        runOnUiThread(() -> initializeActivityWithRunningRtpSession(account, with, sessionId));
 873        resetIntent(account, with, sessionId);
 874    }
 875
 876    private void resetIntent(final Account account, final Jid with, final String sessionId) {
 877        final Intent intent = new Intent(Intent.ACTION_VIEW);
 878        intent.putExtra(EXTRA_ACCOUNT, account.getJid().toString());
 879        intent.putExtra(EXTRA_WITH, with.toString());
 880        intent.putExtra(EXTRA_SESSION_ID, sessionId);
 881        setIntent(intent);
 882    }
 883
 884    private void ensureSurfaceViewRendererIsSetup(final SurfaceViewRenderer surfaceViewRenderer) {
 885        surfaceViewRenderer.setVisibility(View.VISIBLE);
 886        try {
 887            surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null);
 888        } catch (final IllegalStateException ignored) {
 889            // SurfaceViewRenderer was already initialized
 890        } catch (final RuntimeException e) {
 891            if (Throwables.getRootCause(e) instanceof GLException glException) {
 892                Log.w(Config.LOGTAG, "could not set up hardware renderer", glException);
 893            }
 894        }
 895        surfaceViewRenderer.setEnableHardwareScaler(true);
 896    }
 897
 898    private void updateStateDisplay(final RtpEndUserState state) {
 899        updateStateDisplay(state, Collections.emptySet(), null);
 900    }
 901
 902    private void updateStateDisplay(
 903            final RtpEndUserState state,
 904            final Set<Media> media,
 905            final ContentAddition contentAddition) {
 906        switch (state) {
 907            case INCOMING_CALL -> {
 908                Preconditions.checkArgument(!media.isEmpty(), "Media must not be empty");
 909                if (media.contains(Media.VIDEO)) {
 910                    setTitle(R.string.rtp_state_incoming_video_call);
 911                } else {
 912                    setTitle(R.string.rtp_state_incoming_call);
 913                }
 914            }
 915            case INCOMING_CONTENT_ADD -> {
 916                if (contentAddition != null && contentAddition.media().contains(Media.VIDEO)) {
 917                    setTitle(R.string.rtp_state_content_add_video);
 918                } else {
 919                    setTitle(R.string.rtp_state_content_add);
 920                }
 921            }
 922            case CONNECTING -> setTitle(R.string.rtp_state_connecting);
 923            case CONNECTED -> setTitle(R.string.rtp_state_connected);
 924            case RECONNECTING -> setTitle(R.string.rtp_state_reconnecting);
 925            case ACCEPTING_CALL -> setTitle(R.string.rtp_state_accepting_call);
 926            case ENDING_CALL -> setTitle(R.string.rtp_state_ending_call);
 927            case FINDING_DEVICE -> setTitle(R.string.rtp_state_finding_device);
 928            case RINGING -> setTitle(R.string.rtp_state_ringing);
 929            case DECLINED_OR_BUSY -> setTitle(R.string.rtp_state_declined_or_busy);
 930            case CONTACT_OFFLINE -> setTitle(R.string.rtp_state_contact_offline);
 931            case CONNECTIVITY_ERROR -> setTitle(R.string.rtp_state_connectivity_error);
 932            case CONNECTIVITY_LOST_ERROR -> setTitle(R.string.rtp_state_connectivity_lost_error);
 933            case RETRACTED -> setTitle(R.string.rtp_state_retracted);
 934            case APPLICATION_ERROR -> setTitle(R.string.rtp_state_application_failure);
 935            case SECURITY_ERROR -> setTitle(R.string.rtp_state_security_error);
 936            case ENDED ->
 937                    throw new IllegalStateException(
 938                            "Activity should have called finishAndReleaseWakeLock();");
 939            default ->
 940                    throw new IllegalStateException(
 941                            String.format("State %s has not been handled in UI", state));
 942        }
 943    }
 944
 945    private void updateVerifiedShield(final boolean verified) {
 946        if (isPictureInPicture()) {
 947            this.binding.verified.setVisibility(View.GONE);
 948            return;
 949        }
 950        this.binding.verified.setVisibility(verified ? View.VISIBLE : View.GONE);
 951    }
 952
 953    private void updateIncomingCallScreen(final RtpEndUserState state) {
 954        updateIncomingCallScreen(state, null);
 955    }
 956
 957    private void updateIncomingCallScreen(final RtpEndUserState state, final Contact contact) {
 958        if (state == RtpEndUserState.INCOMING_CALL || state == RtpEndUserState.ACCEPTING_CALL) {
 959            final boolean show = getResources().getBoolean(R.bool.is_portrait_mode);
 960            if (show) {
 961                binding.contactPhoto.setVisibility(View.VISIBLE);
 962                if (contact == null) {
 963                    AvatarWorkerTask.loadAvatar(
 964                            getWith(), binding.contactPhoto, R.dimen.publish_avatar_size);
 965                } else {
 966                    AvatarWorkerTask.loadAvatar(
 967                            contact, binding.contactPhoto, R.dimen.publish_avatar_size);
 968                }
 969            } else {
 970                binding.contactPhoto.setVisibility(View.GONE);
 971            }
 972            final Account account = contact == null ? getWith().getAccount() : contact.getAccount();
 973            binding.usingAccount.setVisibility(View.VISIBLE);
 974            binding.usingAccount.setText(
 975                    getString(R.string.using_account, account.getJid().asBareJid().toString()));
 976        } else {
 977            binding.usingAccount.setVisibility(View.GONE);
 978            binding.contactPhoto.setVisibility(View.GONE);
 979        }
 980    }
 981
 982    private void updateSupportWarning(final RtpEndUserState state, final Contact contact) {
 983        if (state == RtpEndUserState.CONNECTIVITY_ERROR
 984                && getResources().getBoolean(R.bool.is_portrait_mode)) {
 985            binding.supportWarning.setVisibility(
 986                    RtpCapability.check(contact) == RtpCapability.Capability.NONE
 987                            ? View.VISIBLE
 988                            : View.GONE);
 989        } else {
 990            binding.supportWarning.setVisibility(View.GONE);
 991        }
 992    }
 993
 994    private Set<Media> getMedia() {
 995        return requireRtpConnection().getMedia();
 996    }
 997
 998    public ContentAddition getPendingContentAddition() {
 999        return requireRtpConnection().getPendingContentAddition();
1000    }
1001
1002    private void updateButtonConfiguration(final RtpEndUserState state) {
1003        updateButtonConfiguration(state, Collections.emptySet(), null);
1004    }
1005
1006    @SuppressLint("RestrictedApi")
1007    private void updateButtonConfiguration(
1008            final RtpEndUserState state,
1009            final Set<Media> media,
1010            final ContentAddition contentAddition) {
1011        if (state == RtpEndUserState.ENDING_CALL
1012                || isPictureInPicture()
1013                || this.buttonsHiddenAfterTimeout) {
1014            this.binding.rejectCall.setVisibility(View.INVISIBLE);
1015            this.binding.endCall.setVisibility(View.INVISIBLE);
1016            this.binding.acceptCall.setVisibility(View.INVISIBLE);
1017        } else if (state == RtpEndUserState.INCOMING_CALL) {
1018            this.binding.rejectCall.setContentDescription(getString(R.string.dismiss_call));
1019            this.binding.rejectCall.setOnClickListener(this::rejectCall);
1020            this.binding.rejectCall.setImageResource(R.drawable.ic_call_end_24dp);
1021            this.binding.rejectCall.setVisibility(View.VISIBLE);
1022            this.binding.endCall.setVisibility(View.INVISIBLE);
1023            this.binding.acceptCall.setContentDescription(getString(R.string.answer_call));
1024            this.binding.acceptCall.setOnClickListener(this::acceptCall);
1025            this.binding.acceptCall.setImageResource(R.drawable.ic_call_24dp);
1026            this.binding.acceptCall.setVisibility(View.VISIBLE);
1027        } else if (state == RtpEndUserState.INCOMING_CONTENT_ADD) {
1028            this.binding.rejectCall.setContentDescription(
1029                    getString(R.string.reject_switch_to_video));
1030            this.binding.rejectCall.setOnClickListener(this::rejectContentAdd);
1031            this.binding.rejectCall.setImageResource(R.drawable.ic_clear_24dp);
1032            this.binding.rejectCall.setVisibility(View.VISIBLE);
1033            this.binding.endCall.setVisibility(View.INVISIBLE);
1034            this.binding.acceptCall.setContentDescription(getString(R.string.accept));
1035            this.binding.acceptCall.setOnClickListener((v -> acceptContentAdd(contentAddition)));
1036            this.binding.acceptCall.setImageResource(R.drawable.ic_check_24dp);
1037            this.binding.acceptCall.setVisibility(View.VISIBLE);
1038        } else if (asList(RtpEndUserState.DECLINED_OR_BUSY, RtpEndUserState.CONTACT_OFFLINE)
1039                .contains(state)) {
1040            this.binding.rejectCall.setContentDescription(getString(R.string.exit));
1041            this.binding.rejectCall.setOnClickListener(this::exit);
1042            this.binding.rejectCall.setImageResource(R.drawable.ic_clear_24dp);
1043            this.binding.rejectCall.setVisibility(View.VISIBLE);
1044            this.binding.endCall.setVisibility(View.INVISIBLE);
1045            this.binding.acceptCall.setContentDescription(getString(R.string.record_voice_mail));
1046            this.binding.acceptCall.setOnClickListener(this::recordVoiceMail);
1047            this.binding.acceptCall.setImageResource(R.drawable.ic_voicemail_24dp);
1048            this.binding.acceptCall.setVisibility(View.VISIBLE);
1049        } else if (asList(
1050                        RtpEndUserState.CONNECTIVITY_ERROR,
1051                        RtpEndUserState.CONNECTIVITY_LOST_ERROR,
1052                        RtpEndUserState.APPLICATION_ERROR,
1053                        RtpEndUserState.RETRACTED,
1054                        RtpEndUserState.SECURITY_ERROR)
1055                .contains(state)) {
1056            this.binding.rejectCall.setContentDescription(getString(R.string.exit));
1057            this.binding.rejectCall.setOnClickListener(this::exit);
1058            this.binding.rejectCall.setImageResource(R.drawable.ic_clear_24dp);
1059            this.binding.rejectCall.setVisibility(View.VISIBLE);
1060            this.binding.endCall.setVisibility(View.INVISIBLE);
1061            this.binding.acceptCall.setContentDescription(getString(R.string.try_again));
1062            this.binding.acceptCall.setOnClickListener(this::retry);
1063            this.binding.acceptCall.setImageResource(R.drawable.ic_replay_24dp);
1064            this.binding.acceptCall.setVisibility(View.VISIBLE);
1065        } else {
1066            this.binding.rejectCall.setVisibility(View.INVISIBLE);
1067            this.binding.endCall.setContentDescription(getString(R.string.hang_up));
1068            this.binding.endCall.setOnClickListener(this::endCall);
1069            this.binding.endCall.setImageResource(R.drawable.ic_call_end_24dp);
1070            setVisibleAndShow(this.binding.endCall);
1071            this.binding.acceptCall.setVisibility(View.INVISIBLE);
1072        }
1073        updateInCallButtonConfiguration(state, media);
1074    }
1075
1076    private static void setVisibleAndShow(final FloatingActionButton button) {
1077        button.show();
1078        button.setVisibility(View.VISIBLE);
1079    }
1080
1081    private boolean isPictureInPicture() {
1082        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1083            return isInPictureInPictureMode();
1084        } else {
1085            return false;
1086        }
1087    }
1088
1089    private void updateInCallButtonConfiguration() {
1090        updateInCallButtonConfiguration(
1091                requireRtpConnection().getEndUserState(), requireRtpConnection().getMedia());
1092    }
1093
1094    @SuppressLint("RestrictedApi")
1095    private void updateInCallButtonConfiguration(
1096            final RtpEndUserState state, final Set<Media> media) {
1097        final var showButtons = !isPictureInPicture() && !buttonsHiddenAfterTimeout;
1098        if (STATES_CONSIDERED_CONNECTED.contains(state) && showButtons) {
1099            Preconditions.checkArgument(!media.isEmpty(), "Media must not be empty");
1100            if (media.contains(Media.VIDEO)) {
1101                final JingleRtpConnection rtpConnection = requireRtpConnection();
1102                updateInCallButtonConfigurationVideo(
1103                        rtpConnection.isVideoEnabled(), rtpConnection.isCameraSwitchable());
1104            } else {
1105                final CallIntegration callIntegration = requireRtpConnection().getCallIntegration();
1106                updateInCallButtonConfigurationSpeaker(
1107                        callIntegration.getSelectedAudioDevice(),
1108                        callIntegration.getAudioDevices().size());
1109                this.binding.inCallActionFarRight.setVisibility(View.GONE);
1110            }
1111            if (media.contains(Media.AUDIO)) {
1112                updateInCallButtonConfigurationMicrophone(
1113                        requireRtpConnection().isMicrophoneEnabled());
1114            } else {
1115                this.binding.inCallActionLeft.setVisibility(View.GONE);
1116            }
1117        } else if (STATES_SHOWING_SPEAKER_CONFIGURATION.contains(state)
1118                && showButtons
1119                && Media.audioOnly(media)) {
1120            final CallIntegration callIntegration;
1121            try {
1122                callIntegration = requireCallIntegration();
1123            } catch (final IllegalStateException e) {
1124                Log.e(Config.LOGTAG, "can not update InCallButtonConfiguration in state " + state);
1125                return;
1126            }
1127            updateInCallButtonConfigurationSpeaker(
1128                    callIntegration.getSelectedAudioDevice(),
1129                    callIntegration.getAudioDevices().size());
1130            this.binding.inCallActionFarRight.setVisibility(View.GONE);
1131        } else {
1132            this.binding.inCallActionLeft.setVisibility(View.GONE);
1133            this.binding.inCallActionRight.setVisibility(View.GONE);
1134            this.binding.inCallActionFarRight.setVisibility(View.GONE);
1135        }
1136    }
1137
1138    @SuppressLint("RestrictedApi")
1139    private void updateInCallButtonConfigurationSpeaker(
1140            final CallIntegration.AudioDevice selectedAudioDevice, final int numberOfChoices) {
1141        switch (selectedAudioDevice) {
1142            case EARPIECE -> {
1143                this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_off_24dp);
1144                if (numberOfChoices >= 2) {
1145                    this.binding.inCallActionRight.setContentDescription(
1146                            getString(R.string.call_is_using_earpiece_tap_to_switch_to_speaker));
1147                    this.binding.inCallActionRight.setOnClickListener(this::switchToSpeaker);
1148                } else {
1149                    this.binding.inCallActionRight.setContentDescription(
1150                            getString(R.string.call_is_using_earpiece));
1151                    this.binding.inCallActionRight.setOnClickListener(null);
1152                    this.binding.inCallActionRight.setClickable(false);
1153                }
1154            }
1155            case WIRED_HEADSET -> {
1156                this.binding.inCallActionRight.setContentDescription(
1157                        getString(R.string.call_is_using_wired_headset));
1158                this.binding.inCallActionRight.setImageResource(R.drawable.ic_headset_mic_24dp);
1159                this.binding.inCallActionRight.setOnClickListener(null);
1160                this.binding.inCallActionRight.setClickable(false);
1161            }
1162            case SPEAKER_PHONE -> {
1163                this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_up_24dp);
1164                if (numberOfChoices >= 2) {
1165                    this.binding.inCallActionRight.setContentDescription(
1166                            getString(R.string.call_is_using_speaker_tap_to_switch_to_earpiece));
1167                    this.binding.inCallActionRight.setOnClickListener(this::switchToEarpiece);
1168                } else {
1169                    this.binding.inCallActionRight.setContentDescription(
1170                            getString(R.string.call_is_using_speaker));
1171                    this.binding.inCallActionRight.setOnClickListener(null);
1172                    this.binding.inCallActionRight.setClickable(false);
1173                }
1174            }
1175            case BLUETOOTH -> {
1176                this.binding.inCallActionRight.setContentDescription(
1177                        getString(R.string.call_is_using_bluetooth));
1178                this.binding.inCallActionRight.setImageResource(R.drawable.ic_bluetooth_audio_24dp);
1179                this.binding.inCallActionRight.setOnClickListener(null);
1180                this.binding.inCallActionRight.setClickable(false);
1181            }
1182        }
1183        setVisibleAndShow(this.binding.inCallActionRight);
1184    }
1185
1186    @SuppressLint("RestrictedApi")
1187    private void updateInCallButtonConfigurationVideo(
1188            final boolean videoEnabled, final boolean isCameraSwitchable) {
1189        setVisibleAndShow(this.binding.inCallActionRight);
1190        if (isCameraSwitchable) {
1191            this.binding.inCallActionFarRight.setImageResource(
1192                    R.drawable.ic_flip_camera_android_24dp);
1193            setVisibleAndShow(this.binding.inCallActionFarRight);
1194            this.binding.inCallActionFarRight.setOnClickListener(this::switchCamera);
1195            this.binding.inCallActionFarRight.setContentDescription(
1196                    getString(R.string.flip_camera));
1197        } else {
1198            this.binding.inCallActionFarRight.setVisibility(View.GONE);
1199        }
1200        if (videoEnabled) {
1201            this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_24dp);
1202            this.binding.inCallActionRight.setOnClickListener(this::disableVideo);
1203            this.binding.inCallActionRight.setContentDescription(
1204                    getString(R.string.video_is_enabled_tap_to_disable));
1205        } else {
1206            this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_off_24dp);
1207            this.binding.inCallActionRight.setOnClickListener(this::enableVideo);
1208            this.binding.inCallActionRight.setContentDescription(
1209                    getString(R.string.video_is_disabled_tap_to_enable));
1210        }
1211    }
1212
1213    private void switchCamera(final View view) {
1214        resetVisibilityToggleExecutor();
1215        Futures.addCallback(
1216                requireRtpConnection().switchCamera(),
1217                new FutureCallback<>() {
1218                    @Override
1219                    public void onSuccess(@Nullable Boolean isFrontCamera) {
1220                        binding.localVideo.setMirror(Boolean.TRUE.equals(isFrontCamera));
1221                    }
1222
1223                    @Override
1224                    public void onFailure(@NonNull final Throwable throwable) {
1225                        Log.d(
1226                                Config.LOGTAG,
1227                                "could not switch camera",
1228                                Throwables.getRootCause(throwable));
1229                        Toast.makeText(
1230                                        RtpSessionActivity.this,
1231                                        R.string.could_not_switch_camera,
1232                                        Toast.LENGTH_LONG)
1233                                .show();
1234                    }
1235                },
1236                MainThreadExecutor.getInstance());
1237        // TODO ^ replace with ContextCompat.getMainExecutor(getApplication())
1238    }
1239
1240    private void enableVideo(final View view) {
1241        resetVisibilityToggleExecutor();
1242        try {
1243            requireRtpConnection().setVideoEnabled(true);
1244        } catch (final IllegalStateException e) {
1245            Toast.makeText(this, R.string.unable_to_enable_video, Toast.LENGTH_SHORT).show();
1246            return;
1247        }
1248        updateInCallButtonConfigurationVideo(true, requireRtpConnection().isCameraSwitchable());
1249    }
1250
1251    private void disableVideo(final View view) {
1252        resetVisibilityToggleExecutor();
1253        final JingleRtpConnection rtpConnection = requireRtpConnection();
1254        final ContentAddition pending = rtpConnection.getPendingContentAddition();
1255        if (pending != null && pending.direction == ContentAddition.Direction.OUTGOING) {
1256            rtpConnection.retractContentAdd();
1257            return;
1258        }
1259        try {
1260            requireRtpConnection().setVideoEnabled(false);
1261        } catch (final IllegalStateException e) {
1262            Toast.makeText(this, R.string.could_not_disable_video, Toast.LENGTH_SHORT).show();
1263            return;
1264        }
1265        updateInCallButtonConfigurationVideo(false, requireRtpConnection().isCameraSwitchable());
1266    }
1267
1268    @SuppressLint("RestrictedApi")
1269    private void updateInCallButtonConfigurationMicrophone(final boolean microphoneEnabled) {
1270        if (microphoneEnabled) {
1271            this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_24dp);
1272            this.binding.inCallActionLeft.setOnClickListener(this::disableMicrophone);
1273        } else {
1274            this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_off_24dp);
1275            this.binding.inCallActionLeft.setOnClickListener(this::enableMicrophone);
1276        }
1277        setVisibleAndShow(this.binding.inCallActionLeft);
1278    }
1279
1280    private void updateCallDuration() {
1281        final JingleRtpConnection connection =
1282                this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
1283        if (connection == null || connection.getMedia().contains(Media.VIDEO)) {
1284            this.binding.duration.setVisibility(View.GONE);
1285            return;
1286        }
1287        if (connection.zeroDuration()) {
1288            this.binding.duration.setVisibility(View.GONE);
1289        } else {
1290            this.binding.duration.setText(
1291                    TimeFrameUtils.formatElapsedTime(connection.getCallDuration(), false));
1292            this.binding.duration.setVisibility(View.VISIBLE);
1293        }
1294    }
1295
1296    private void resetVisibilityToggleExecutor() {
1297        mHandler.removeCallbacks(this.mVisibilityToggleExecutor);
1298        mHandler.postDelayed(this.mVisibilityToggleExecutor, BUTTON_VISIBILITY_TIMEOUT);
1299    }
1300
1301    private void updateButtonInVideoCallVisibility() {
1302        if (isInConnectedVideoCall()) {
1303            if (isPictureInPicture()) {
1304                return;
1305            }
1306            Log.d(Config.LOGTAG, "hiding in-call buttons after timeout was reached");
1307            hideInCallButtons();
1308        }
1309    }
1310
1311    private void hideInCallButtons() {
1312        binding.inCallActionLeft.hide();
1313        binding.endCall.hide();
1314        binding.inCallActionRight.hide();
1315        binding.inCallActionFarRight.hide();
1316    }
1317
1318    private void showInCallButtons() {
1319        this.buttonsHiddenAfterTimeout = false;
1320        final JingleRtpConnection rtpConnection;
1321        try {
1322            rtpConnection = requireRtpConnection();
1323        } catch (final IllegalStateException e) {
1324            return;
1325        }
1326        updateButtonConfiguration(
1327                rtpConnection.getEndUserState(),
1328                rtpConnection.getMedia(),
1329                rtpConnection.getPendingContentAddition());
1330    }
1331
1332    private void resetVisibilityExecutorShowButtons() {
1333        resetVisibilityToggleExecutor();
1334        showInCallButtons();
1335    }
1336
1337    private void updateVideoViews(final RtpEndUserState state) {
1338        if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) {
1339            binding.localVideo.setVisibility(View.GONE);
1340            binding.localVideo.release();
1341            binding.remoteVideoWrapper.setVisibility(View.GONE);
1342            binding.remoteVideo.release();
1343            binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
1344            if (isPictureInPicture()) {
1345                binding.appBarLayout.setVisibility(View.GONE);
1346                binding.pipPlaceholder.setVisibility(View.VISIBLE);
1347                if (Arrays.asList(
1348                                RtpEndUserState.APPLICATION_ERROR,
1349                                RtpEndUserState.CONNECTIVITY_ERROR,
1350                                RtpEndUserState.SECURITY_ERROR)
1351                        .contains(state)) {
1352                    binding.pipWarning.setVisibility(View.VISIBLE);
1353                    binding.pipWaiting.setVisibility(View.GONE);
1354                } else {
1355                    binding.pipWarning.setVisibility(View.GONE);
1356                    binding.pipWaiting.setVisibility(View.GONE);
1357                }
1358            } else {
1359                binding.appBarLayout.setVisibility(View.VISIBLE);
1360                binding.pipPlaceholder.setVisibility(View.GONE);
1361            }
1362            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
1363            return;
1364        }
1365        if (isPictureInPicture() && STATES_SHOWING_PIP_PLACEHOLDER.contains(state)) {
1366            binding.localVideo.setVisibility(View.GONE);
1367            binding.remoteVideoWrapper.setVisibility(View.GONE);
1368            binding.appBarLayout.setVisibility(View.GONE);
1369            binding.pipPlaceholder.setVisibility(View.VISIBLE);
1370            binding.pipWarning.setVisibility(View.GONE);
1371            binding.pipWaiting.setVisibility(View.VISIBLE);
1372            binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
1373            return;
1374        }
1375        final Optional<VideoTrack> localVideoTrack = getLocalVideoTrack();
1376        if (localVideoTrack.isPresent() && !isPictureInPicture()) {
1377            ensureSurfaceViewRendererIsSetup(binding.localVideo);
1378            // paint local view over remote view
1379            binding.localVideo.setZOrderMediaOverlay(true);
1380            binding.localVideo.setMirror(requireRtpConnection().isFrontCamera());
1381            addSink(localVideoTrack.get(), binding.localVideo);
1382        } else {
1383            binding.localVideo.setVisibility(View.GONE);
1384        }
1385        final Optional<VideoTrack> remoteVideoTrack = getRemoteVideoTrack();
1386        if (remoteVideoTrack.isPresent()) {
1387            ensureSurfaceViewRendererIsSetup(binding.remoteVideo);
1388            addSink(remoteVideoTrack.get(), binding.remoteVideo);
1389            binding.remoteVideo.setScalingType(
1390                    RendererCommon.ScalingType.SCALE_ASPECT_FILL,
1391                    RendererCommon.ScalingType.SCALE_ASPECT_FIT);
1392            if (state == RtpEndUserState.CONNECTED) {
1393                binding.appBarLayout.setVisibility(View.GONE);
1394                getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
1395                binding.remoteVideoWrapper.setVisibility(View.VISIBLE);
1396            } else {
1397                binding.appBarLayout.setVisibility(View.VISIBLE);
1398                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
1399                binding.remoteVideoWrapper.setVisibility(View.GONE);
1400            }
1401            if (isPictureInPicture() && !requireRtpConnection().isMicrophoneEnabled()) {
1402                binding.pipLocalMicOffIndicator.setVisibility(View.VISIBLE);
1403            } else {
1404                binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
1405            }
1406        } else {
1407            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
1408            binding.remoteVideoWrapper.setVisibility(View.GONE);
1409            binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
1410        }
1411    }
1412
1413    private Optional<VideoTrack> getLocalVideoTrack() {
1414        final JingleRtpConnection connection =
1415                this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
1416        if (connection == null) {
1417            return Optional.absent();
1418        }
1419        return connection.getLocalVideoTrack();
1420    }
1421
1422    private Optional<VideoTrack> getRemoteVideoTrack() {
1423        final JingleRtpConnection connection =
1424                this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
1425        if (connection == null) {
1426            return Optional.absent();
1427        }
1428        return connection.getRemoteVideoTrack();
1429    }
1430
1431    private void disableMicrophone(final View view) {
1432        setMicrophoneEnabled(false);
1433    }
1434
1435    private void enableMicrophone(final View view) {
1436        setMicrophoneEnabled(true);
1437    }
1438
1439    private void setMicrophoneEnabled(final boolean enabled) {
1440        resetVisibilityExecutorShowButtons();
1441        try {
1442            final JingleRtpConnection rtpConnection = requireRtpConnection();
1443            if (rtpConnection.setMicrophoneEnabled(enabled)) {
1444                updateInCallButtonConfiguration();
1445            }
1446        } catch (final IllegalStateException e) {
1447            Toast.makeText(this, R.string.could_not_modify_call, Toast.LENGTH_SHORT).show();
1448        }
1449    }
1450
1451    private void switchToEarpiece(final View view) {
1452        try {
1453            requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.EARPIECE);
1454            acquireProximityWakeLock();
1455        } catch (final IllegalStateException e) {
1456            Toast.makeText(this, R.string.could_not_modify_call, Toast.LENGTH_SHORT).show();
1457        }
1458    }
1459
1460    private void switchToSpeaker(final View view) {
1461        try {
1462            requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.SPEAKER_PHONE);
1463            releaseProximityWakeLock();
1464        } catch (final IllegalStateException e) {
1465            Toast.makeText(this, R.string.could_not_modify_call, Toast.LENGTH_SHORT).show();
1466        }
1467    }
1468
1469    private void retry(final View view) {
1470        final Intent intent = getIntent();
1471        final Account account = extractAccount(intent);
1472        final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
1473        final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION);
1474        final String action = intent.getAction();
1475        final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
1476        this.rtpConnectionReference = null;
1477        Log.d(Config.LOGTAG, "attempting retry with " + with.toString());
1478        CallIntegrationConnectionService.placeCall(xmppConnectionService, account, with, media);
1479    }
1480
1481    private void exit(final View view) {
1482        finish();
1483    }
1484
1485    private void recordVoiceMail(final View view) {
1486        final Intent intent = getIntent();
1487        final Account account = extractAccount(intent);
1488        final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
1489        final Conversation conversation =
1490                xmppConnectionService.findOrCreateConversation(account, with, false, true);
1491        final Intent launchIntent = new Intent(this, ConversationsActivity.class);
1492        launchIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
1493        launchIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
1494        launchIntent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP);
1495        launchIntent.putExtra(
1496                ConversationsActivity.EXTRA_POST_INIT_ACTION,
1497                ConversationsActivity.POST_ACTION_RECORD_VOICE);
1498        startActivity(launchIntent);
1499        finish();
1500    }
1501
1502    private Contact getWith() {
1503        final AbstractJingleConnection.Id id = requireRtpConnection().getId();
1504        final Account account = id.account;
1505        return account.getRoster().getContact(id.with);
1506    }
1507
1508    private JingleRtpConnection requireRtpConnection() {
1509        final JingleRtpConnection connection =
1510                this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
1511        if (connection == null) {
1512            throw new IllegalStateException("No RTP connection found");
1513        }
1514        return connection;
1515    }
1516
1517    private CallIntegration requireCallIntegration() {
1518        return requireOngoingRtpSession().getCallIntegration();
1519    }
1520
1521    private OngoingRtpSession requireOngoingRtpSession() {
1522        final JingleRtpConnection connection =
1523                this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
1524        if (connection != null) {
1525            return connection;
1526        }
1527        final Intent currentIntent = getIntent();
1528        final String withExtra =
1529                currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH);
1530        final var account = extractAccount(currentIntent);
1531        if (withExtra == null) {
1532            throw new IllegalStateException("Current intent has no EXTRA_WITH");
1533        }
1534        final var matching =
1535                xmppConnectionService
1536                        .getJingleConnectionManager()
1537                        .matchingProposal(account, Jid.of(withExtra));
1538        if (matching.isPresent()) {
1539            return matching.get();
1540        }
1541        throw new IllegalStateException("No matching session proposal");
1542    }
1543
1544    @Override
1545    public void onJingleRtpConnectionUpdate(
1546            Account account, Jid with, final String sessionId, RtpEndUserState state) {
1547        Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")");
1548        if (END_CARD.contains(state)) {
1549            Log.d(Config.LOGTAG, "end card reached");
1550            releaseProximityWakeLock();
1551            runOnUiThread(
1552                    () -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON));
1553        }
1554        if (with.isBareJid()) {
1555            // TODO check for ENDED
1556            updateRtpSessionProposalState(account, with, state);
1557            return;
1558        }
1559        if (emptyReference(this.rtpConnectionReference)) {
1560            if (END_CARD.contains(state)) {
1561                Log.d(Config.LOGTAG, "not reinitializing session");
1562                return;
1563            }
1564            // this happens when going from proposed session to actual session
1565            reInitializeActivityWithRunningRtpSession(account, with, sessionId);
1566            return;
1567        }
1568        final AbstractJingleConnection.Id id = requireRtpConnection().getId();
1569        final boolean verified = requireRtpConnection().isVerified();
1570        final Set<Media> media = getMedia();
1571        lockOrientation(media);
1572        final ContentAddition contentAddition = getPendingContentAddition();
1573        final Contact contact = getWith();
1574        if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
1575            if (state == RtpEndUserState.ENDED) {
1576                finish();
1577                return;
1578            }
1579            resetVisibilityToggleExecutor();
1580            runOnUiThread(
1581                    () -> {
1582                        updateStateDisplay(state, media, contentAddition);
1583                        updateVerifiedShield(
1584                                verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(state));
1585                        updateButtonConfiguration(state, media, contentAddition);
1586                        updateVideoViews(state);
1587                        updateIncomingCallScreen(state, contact);
1588                        updateSupportWarning(state, contact);
1589                        invalidateOptionsMenu();
1590                    });
1591            if (END_CARD.contains(state)) {
1592                final JingleRtpConnection rtpConnection = requireRtpConnection();
1593                resetIntent(account, with, state, rtpConnection.getMedia());
1594                releaseVideoTracks(rtpConnection);
1595                this.rtpConnectionReference = null;
1596            }
1597        } else {
1598            Log.d(Config.LOGTAG, "received update for other rtp session");
1599        }
1600    }
1601
1602    @Override
1603    public void onAudioDeviceChanged(
1604            final CallIntegration.AudioDevice selectedAudioDevice,
1605            final Set<CallIntegration.AudioDevice> availableAudioDevices) {
1606        Log.d(
1607                Config.LOGTAG,
1608                "onAudioDeviceChanged in activity: selected:"
1609                        + selectedAudioDevice
1610                        + ", available:"
1611                        + availableAudioDevices);
1612        try {
1613            final OngoingRtpSession ongoingRtpSession = requireOngoingRtpSession();
1614            final RtpEndUserState endUserState;
1615            if (ongoingRtpSession instanceof JingleRtpConnection jingleRtpConnection) {
1616                endUserState = jingleRtpConnection.getEndUserState();
1617            } else {
1618                // for session proposals all end user states are functionally the same
1619                endUserState = RtpEndUserState.RINGING;
1620            }
1621            final Set<Media> media = ongoingRtpSession.getMedia();
1622            if (END_CARD.contains(endUserState)) {
1623                Log.d(
1624                        Config.LOGTAG,
1625                        "onAudioDeviceChanged() nothing to do because end card has been reached");
1626            } else {
1627                if (Media.audioOnly(media)
1628                        && STATES_SHOWING_SPEAKER_CONFIGURATION.contains(endUserState)) {
1629                    final CallIntegration callIntegration = requireCallIntegration();
1630                    updateInCallButtonConfigurationSpeaker(
1631                            callIntegration.getSelectedAudioDevice(),
1632                            callIntegration.getAudioDevices().size());
1633                }
1634                Log.d(
1635                        Config.LOGTAG,
1636                        "put proximity wake lock into proper state after device update");
1637                putProximityWakeLockInProperState(selectedAudioDevice);
1638            }
1639        } catch (final IllegalStateException e) {
1640            Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed");
1641        }
1642    }
1643
1644    @Override
1645    protected void onSaveInstanceState(@NonNull @NotNull Bundle outState) {
1646        super.onSaveInstanceState(outState);
1647        outState.putBoolean("dialpad_visible", binding.dialpad.getVisibility() == View.VISIBLE);
1648    }
1649
1650    private void updateRtpSessionProposalState(
1651            final Account account, final Jid with, final RtpEndUserState state) {
1652        final Intent currentIntent = getIntent();
1653        final String withExtra =
1654                currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH);
1655        if (withExtra == null) {
1656            return;
1657        }
1658        final Set<Media> media = actionToMedia(currentIntent.getStringExtra(EXTRA_LAST_ACTION));
1659        if (Jid.of(withExtra).asBareJid().equals(with)) {
1660            runOnUiThread(
1661                    () -> {
1662                        updateVerifiedShield(false);
1663                        updateStateDisplay(state);
1664                        updateButtonConfiguration(state, media, null);
1665                        updateIncomingCallScreen(state);
1666                        updateSupportWarning(state, account.getRoster().getContact(with));
1667                        invalidateOptionsMenu();
1668                    });
1669            resetIntent(account, with, state, media);
1670        }
1671    }
1672
1673    private void resetIntent(final Bundle extras) {
1674        final Intent intent = new Intent(Intent.ACTION_VIEW);
1675        intent.putExtras(extras);
1676        setIntent(intent);
1677    }
1678
1679    private void resetIntent(
1680            final Account account, Jid with, final RtpEndUserState state, final Set<Media> media) {
1681        final Intent intent = new Intent(Intent.ACTION_VIEW);
1682        intent.putExtra(EXTRA_ACCOUNT, account.getJid().toString());
1683        if (RtpCapability.jmiSupport(account.getRoster().getContact(with))) {
1684            intent.putExtra(EXTRA_WITH, with.asBareJid().toString());
1685        } else {
1686            intent.putExtra(EXTRA_WITH, with.toString());
1687        }
1688        intent.putExtra(EXTRA_LAST_REPORTED_STATE, state.toString());
1689        intent.putExtra(
1690                EXTRA_LAST_ACTION,
1691                media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL);
1692        setIntent(intent);
1693    }
1694
1695    private static boolean emptyReference(final WeakReference<?> weakReference) {
1696        return weakReference == null || weakReference.get() == null;
1697    }
1698
1699    private enum Event {
1700        ON_BACKEND_CONNECTED,
1701        ON_NEW_INTENT
1702    }
1703}