RtpSessionActivity.java

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