RtpSessionActivity.java

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