RtpSessionActivity.java

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