RtpSessionActivity.java

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