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