RtpSessionActivity.java

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