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