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.ofEscaped(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.ofEscaped(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.ofEscaped(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            Log.e(Config.LOGTAG,"failed to initialize activity with running rtp session. session not found");
416            finish();
417            return true;
418        }
419        this.rtpConnectionReference = reference;
420        final RtpEndUserState currentState = requireRtpConnection().getEndUserState();
421        if (currentState == RtpEndUserState.ENDED) {
422            Log.e(Config.LOGTAG,"failed to initialize activity with running rtp session. session had ended");
423            finish();
424            return true;
425        }
426        final Set<Media> media = getMedia();
427        if (currentState == RtpEndUserState.INCOMING_CALL) {
428            getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
429        }
430        if (JingleRtpConnection.STATES_SHOWING_ONGOING_CALL.contains(requireRtpConnection().getState())) {
431            putScreenInCallMode();
432        }
433        binding.with.setText(getWith().getDisplayName());
434        updateVideoViews(currentState);
435        updateStateDisplay(currentState, media);
436        updateButtonConfiguration(currentState, media);
437        updateProfilePicture(currentState);
438        return false;
439    }
440
441    private void reInitializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) {
442        runOnUiThread(() -> initializeActivityWithRunningRtpSession(account, with, sessionId));
443        final Intent intent = new Intent(Intent.ACTION_VIEW);
444        intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
445        intent.putExtra(EXTRA_WITH, with.toEscapedString());
446        intent.putExtra(EXTRA_SESSION_ID, sessionId);
447        setIntent(intent);
448    }
449
450    private void ensureSurfaceViewRendererIsSetup(final SurfaceViewRenderer surfaceViewRenderer) {
451        surfaceViewRenderer.setVisibility(View.VISIBLE);
452        try {
453            surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null);
454        } catch (IllegalStateException e) {
455            Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized");
456        }
457        surfaceViewRenderer.setEnableHardwareScaler(true);
458    }
459
460    private void updateStateDisplay(final RtpEndUserState state) {
461        updateStateDisplay(state, Collections.emptySet());
462    }
463
464    private void updateStateDisplay(final RtpEndUserState state, final Set<Media> media) {
465        switch (state) {
466            case INCOMING_CALL:
467                Preconditions.checkArgument(media.size() > 0, "Media must not be empty");
468                if (media.contains(Media.VIDEO)) {
469                    setTitle(R.string.rtp_state_incoming_video_call);
470                } else {
471                    setTitle(R.string.rtp_state_incoming_call);
472                }
473                break;
474            case CONNECTING:
475                setTitle(R.string.rtp_state_connecting);
476                break;
477            case CONNECTED:
478                setTitle(R.string.rtp_state_connected);
479                break;
480            case ACCEPTING_CALL:
481                setTitle(R.string.rtp_state_accepting_call);
482                break;
483            case ENDING_CALL:
484                setTitle(R.string.rtp_state_ending_call);
485                break;
486            case FINDING_DEVICE:
487                setTitle(R.string.rtp_state_finding_device);
488                break;
489            case RINGING:
490                setTitle(R.string.rtp_state_ringing);
491                break;
492            case DECLINED_OR_BUSY:
493                setTitle(R.string.rtp_state_declined_or_busy);
494                break;
495            case CONNECTIVITY_ERROR:
496                setTitle(R.string.rtp_state_connectivity_error);
497                break;
498            case RETRACTED:
499                setTitle(R.string.rtp_state_retracted);
500                break;
501            case APPLICATION_ERROR:
502                setTitle(R.string.rtp_state_application_failure);
503                break;
504            case ENDED:
505                throw new IllegalStateException("Activity should have called finishAndReleaseWakeLock();");
506            default:
507                throw new IllegalStateException(String.format("State %s has not been handled in UI", state));
508        }
509    }
510
511    private void updateProfilePicture(final RtpEndUserState state) {
512        updateProfilePicture(state, null);
513    }
514
515    private void updateProfilePicture(final RtpEndUserState state, final Contact contact) {
516        if (state == RtpEndUserState.INCOMING_CALL || state == RtpEndUserState.ACCEPTING_CALL) {
517            final boolean show = getResources().getBoolean(R.bool.show_avatar_incoming_call);
518            if (show) {
519                binding.contactPhoto.setVisibility(View.VISIBLE);
520                if (contact == null) {
521                    AvatarWorkerTask.loadAvatar(getWith(), binding.contactPhoto, R.dimen.publish_avatar_size);
522                } else {
523                    AvatarWorkerTask.loadAvatar(contact, binding.contactPhoto, R.dimen.publish_avatar_size);
524                }
525            } else {
526                binding.contactPhoto.setVisibility(View.GONE);
527            }
528        } else {
529            binding.contactPhoto.setVisibility(View.GONE);
530        }
531    }
532
533    private Set<Media> getMedia() {
534        return requireRtpConnection().getMedia();
535    }
536
537    private void updateButtonConfiguration(final RtpEndUserState state) {
538        updateButtonConfiguration(state, Collections.emptySet());
539    }
540
541    @SuppressLint("RestrictedApi")
542    private void updateButtonConfiguration(final RtpEndUserState state, final Set<Media> media) {
543        if (state == RtpEndUserState.ENDING_CALL || isPictureInPicture()) {
544            this.binding.rejectCall.setVisibility(View.INVISIBLE);
545            this.binding.endCall.setVisibility(View.INVISIBLE);
546            this.binding.acceptCall.setVisibility(View.INVISIBLE);
547        } else if (state == RtpEndUserState.INCOMING_CALL) {
548            this.binding.rejectCall.setOnClickListener(this::rejectCall);
549            this.binding.rejectCall.setImageResource(R.drawable.ic_call_end_white_48dp);
550            this.binding.rejectCall.setVisibility(View.VISIBLE);
551            this.binding.endCall.setVisibility(View.INVISIBLE);
552            this.binding.acceptCall.setOnClickListener(this::acceptCall);
553            this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp);
554            this.binding.acceptCall.setVisibility(View.VISIBLE);
555        } else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
556            this.binding.rejectCall.setVisibility(View.INVISIBLE);
557            this.binding.endCall.setOnClickListener(this::exit);
558            this.binding.endCall.setImageResource(R.drawable.ic_clear_white_48dp);
559            this.binding.endCall.setVisibility(View.VISIBLE);
560            this.binding.acceptCall.setVisibility(View.INVISIBLE);
561        } else if (asList(RtpEndUserState.CONNECTIVITY_ERROR, RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.RETRACTED).contains(state)) {
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::retry);
567            this.binding.acceptCall.setImageResource(R.drawable.ic_replay_white_48dp);
568            this.binding.acceptCall.setVisibility(View.VISIBLE);
569        } else {
570            this.binding.rejectCall.setVisibility(View.INVISIBLE);
571            this.binding.endCall.setOnClickListener(this::endCall);
572            this.binding.endCall.setImageResource(R.drawable.ic_call_end_white_48dp);
573            this.binding.endCall.setVisibility(View.VISIBLE);
574            this.binding.acceptCall.setVisibility(View.INVISIBLE);
575        }
576        updateInCallButtonConfiguration(state, media);
577    }
578
579    private boolean isPictureInPicture() {
580        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
581            return isInPictureInPictureMode();
582        } else {
583            return false;
584        }
585    }
586
587    private void updateInCallButtonConfiguration() {
588        updateInCallButtonConfiguration(requireRtpConnection().getEndUserState(), requireRtpConnection().getMedia());
589    }
590
591    @SuppressLint("RestrictedApi")
592    private void updateInCallButtonConfiguration(final RtpEndUserState state, final Set<Media> media) {
593        if (state == RtpEndUserState.CONNECTED && !isPictureInPicture()) {
594            Preconditions.checkArgument(media.size() > 0, "Media must not be empty");
595            if (media.contains(Media.VIDEO)) {
596                final JingleRtpConnection rtpConnection = requireRtpConnection();
597                updateInCallButtonConfigurationVideo(rtpConnection.isVideoEnabled(), rtpConnection.isCameraSwitchable());
598            } else {
599                final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
600                updateInCallButtonConfigurationSpeaker(
601                        audioManager.getSelectedAudioDevice(),
602                        audioManager.getAudioDevices().size()
603                );
604                this.binding.inCallActionFarRight.setVisibility(View.GONE);
605            }
606            if (media.contains(Media.AUDIO)) {
607                updateInCallButtonConfigurationMicrophone(requireRtpConnection().isMicrophoneEnabled());
608            } else {
609                this.binding.inCallActionLeft.setVisibility(View.GONE);
610            }
611        } else {
612            this.binding.inCallActionLeft.setVisibility(View.GONE);
613            this.binding.inCallActionRight.setVisibility(View.GONE);
614            this.binding.inCallActionFarRight.setVisibility(View.GONE);
615        }
616    }
617
618    @SuppressLint("RestrictedApi")
619    private void updateInCallButtonConfigurationSpeaker(final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices) {
620        switch (selectedAudioDevice) {
621            case EARPIECE:
622                this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_off_black_24dp);
623                if (numberOfChoices >= 2) {
624                    this.binding.inCallActionRight.setOnClickListener(this::switchToSpeaker);
625                } else {
626                    this.binding.inCallActionRight.setOnClickListener(null);
627                    this.binding.inCallActionRight.setClickable(false);
628                }
629                break;
630            case WIRED_HEADSET:
631                this.binding.inCallActionRight.setImageResource(R.drawable.ic_headset_black_24dp);
632                this.binding.inCallActionRight.setOnClickListener(null);
633                this.binding.inCallActionRight.setClickable(false);
634                break;
635            case SPEAKER_PHONE:
636                this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_up_black_24dp);
637                if (numberOfChoices >= 2) {
638                    this.binding.inCallActionRight.setOnClickListener(this::switchToEarpiece);
639                } else {
640                    this.binding.inCallActionRight.setOnClickListener(null);
641                    this.binding.inCallActionRight.setClickable(false);
642                }
643                break;
644            case BLUETOOTH:
645                this.binding.inCallActionRight.setImageResource(R.drawable.ic_bluetooth_audio_black_24dp);
646                this.binding.inCallActionRight.setOnClickListener(null);
647                this.binding.inCallActionRight.setClickable(false);
648                break;
649        }
650        this.binding.inCallActionRight.setVisibility(View.VISIBLE);
651    }
652
653    @SuppressLint("RestrictedApi")
654    private void updateInCallButtonConfigurationVideo(final boolean videoEnabled, final boolean isCameraSwitchable) {
655        this.binding.inCallActionRight.setVisibility(View.VISIBLE);
656        if (isCameraSwitchable) {
657            this.binding.inCallActionFarRight.setImageResource(R.drawable.ic_flip_camera_android_black_24dp);
658            this.binding.inCallActionFarRight.setVisibility(View.VISIBLE);
659            this.binding.inCallActionFarRight.setOnClickListener(this::switchCamera);
660        } else {
661            this.binding.inCallActionFarRight.setVisibility(View.GONE);
662        }
663        if (videoEnabled) {
664            this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_black_24dp);
665            this.binding.inCallActionRight.setOnClickListener(this::disableVideo);
666        } else {
667            this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_off_black_24dp);
668            this.binding.inCallActionRight.setOnClickListener(this::enableVideo);
669        }
670    }
671
672    private void switchCamera(final View view) {
673        Futures.addCallback(requireRtpConnection().switchCamera(), new FutureCallback<Boolean>() {
674            @Override
675            public void onSuccess(@NullableDecl Boolean isFrontCamera) {
676                binding.localVideo.setMirror(isFrontCamera);
677            }
678
679            @Override
680            public void onFailure(@NonNull final Throwable throwable) {
681                Log.d(Config.LOGTAG, "could not switch camera", Throwables.getRootCause(throwable));
682                Toast.makeText(RtpSessionActivity.this, R.string.could_not_switch_camera, Toast.LENGTH_LONG).show();
683            }
684        }, MainThreadExecutor.getInstance());
685    }
686
687    private void enableVideo(View view) {
688        requireRtpConnection().setVideoEnabled(true);
689        updateInCallButtonConfigurationVideo(true, requireRtpConnection().isCameraSwitchable());
690    }
691
692    private void disableVideo(View view) {
693        requireRtpConnection().setVideoEnabled(false);
694        updateInCallButtonConfigurationVideo(false, requireRtpConnection().isCameraSwitchable());
695
696    }
697
698    @SuppressLint("RestrictedApi")
699    private void updateInCallButtonConfigurationMicrophone(final boolean microphoneEnabled) {
700        if (microphoneEnabled) {
701            this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_black_24dp);
702            this.binding.inCallActionLeft.setOnClickListener(this::disableMicrophone);
703        } else {
704            this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_off_black_24dp);
705            this.binding.inCallActionLeft.setOnClickListener(this::enableMicrophone);
706        }
707        this.binding.inCallActionLeft.setVisibility(View.VISIBLE);
708    }
709
710    private void updateCallDuration() {
711        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
712        if (connection == null || connection.getMedia().contains(Media.VIDEO)) {
713            this.binding.duration.setVisibility(View.GONE);
714            return;
715        }
716        final long rtpConnectionStarted = connection.getRtpConnectionStarted();
717        final long rtpConnectionEnded = connection.getRtpConnectionEnded();
718        if (rtpConnectionStarted != 0) {
719            final long ended = rtpConnectionEnded == 0 ? SystemClock.elapsedRealtime() : rtpConnectionEnded;
720            this.binding.duration.setText(TimeFrameUtils.formatTimePassed(rtpConnectionStarted, ended, false));
721            this.binding.duration.setVisibility(View.VISIBLE);
722        } else {
723            this.binding.duration.setVisibility(View.GONE);
724        }
725    }
726
727    private void updateVideoViews(final RtpEndUserState state) {
728        if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) {
729            binding.localVideo.setVisibility(View.GONE);
730            binding.localVideo.release();
731            binding.remoteVideo.setVisibility(View.GONE);
732            binding.remoteVideo.release();
733            binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
734            if (isPictureInPicture()) {
735                binding.appBarLayout.setVisibility(View.GONE);
736                binding.pipPlaceholder.setVisibility(View.VISIBLE);
737                if (state == RtpEndUserState.APPLICATION_ERROR || state == RtpEndUserState.CONNECTIVITY_ERROR) {
738                    binding.pipWarning.setVisibility(View.VISIBLE);
739                    binding.pipWaiting.setVisibility(View.GONE);
740                } else {
741                    binding.pipWarning.setVisibility(View.GONE);
742                    binding.pipWaiting.setVisibility(View.GONE);
743                }
744            } else {
745                binding.appBarLayout.setVisibility(View.VISIBLE);
746                binding.pipPlaceholder.setVisibility(View.GONE);
747            }
748            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
749            return;
750        }
751        if (isPictureInPicture() && (state == RtpEndUserState.CONNECTING || state == RtpEndUserState.ACCEPTING_CALL)) {
752            binding.localVideo.setVisibility(View.GONE);
753            binding.remoteVideo.setVisibility(View.GONE);
754            binding.appBarLayout.setVisibility(View.GONE);
755            binding.pipPlaceholder.setVisibility(View.VISIBLE);
756            binding.pipWarning.setVisibility(View.GONE);
757            binding.pipWaiting.setVisibility(View.VISIBLE);
758            binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
759            return;
760        }
761        final Optional<VideoTrack> localVideoTrack = getLocalVideoTrack();
762        if (localVideoTrack.isPresent() && !isPictureInPicture()) {
763            ensureSurfaceViewRendererIsSetup(binding.localVideo);
764            //paint local view over remote view
765            binding.localVideo.setZOrderMediaOverlay(true);
766            binding.localVideo.setMirror(requireRtpConnection().isFrontCamera());
767            localVideoTrack.get().addSink(binding.localVideo);
768        } else {
769            binding.localVideo.setVisibility(View.GONE);
770        }
771        final Optional<VideoTrack> remoteVideoTrack = getRemoteVideoTrack();
772        if (remoteVideoTrack.isPresent()) {
773            ensureSurfaceViewRendererIsSetup(binding.remoteVideo);
774            remoteVideoTrack.get().addSink(binding.remoteVideo);
775            if (state == RtpEndUserState.CONNECTED) {
776                binding.appBarLayout.setVisibility(View.GONE);
777                getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
778            } else {
779                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
780                binding.remoteVideo.setVisibility(View.GONE);
781            }
782            if (isPictureInPicture() && !requireRtpConnection().isMicrophoneEnabled()) {
783                binding.pipLocalMicOffIndicator.setVisibility(View.VISIBLE);
784            } else {
785                binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
786            }
787        } else {
788            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
789            binding.remoteVideo.setVisibility(View.GONE);
790            binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
791        }
792    }
793
794    private Optional<VideoTrack> getLocalVideoTrack() {
795        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
796        if (connection == null) {
797            return Optional.absent();
798        }
799        return connection.getLocalVideoTrack();
800    }
801
802    private Optional<VideoTrack> getRemoteVideoTrack() {
803        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
804        if (connection == null) {
805            return Optional.absent();
806        }
807        return connection.getRemoteVideoTrack();
808    }
809
810    private void disableMicrophone(View view) {
811        JingleRtpConnection rtpConnection = requireRtpConnection();
812        rtpConnection.setMicrophoneEnabled(false);
813        updateInCallButtonConfiguration();
814    }
815
816    private void enableMicrophone(View view) {
817        JingleRtpConnection rtpConnection = requireRtpConnection();
818        rtpConnection.setMicrophoneEnabled(true);
819        updateInCallButtonConfiguration();
820    }
821
822    private void switchToEarpiece(View view) {
823        requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
824        acquireProximityWakeLock();
825    }
826
827    private void switchToSpeaker(View view) {
828        requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
829        releaseProximityWakeLock();
830    }
831
832    private void retry(View view) {
833        Log.d(Config.LOGTAG, "attempting retry");
834        final Intent intent = getIntent();
835        final Account account = extractAccount(intent);
836        final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
837        final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION);
838        final String action = intent.getAction();
839        final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
840        this.rtpConnectionReference = null;
841        proposeJingleRtpSession(account, with, media);
842    }
843
844    private void exit(View view) {
845        finish();
846    }
847
848    private Contact getWith() {
849        final AbstractJingleConnection.Id id = requireRtpConnection().getId();
850        final Account account = id.account;
851        return account.getRoster().getContact(id.with);
852    }
853
854    private JingleRtpConnection requireRtpConnection() {
855        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
856        if (connection == null) {
857            throw new IllegalStateException("No RTP connection found");
858        }
859        return connection;
860    }
861
862    @Override
863    public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) {
864        Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")");
865        if (END_CARD.contains(state)) {
866            Log.d(Config.LOGTAG, "end card reached");
867            releaseProximityWakeLock();
868            runOnUiThread(() -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON));
869        }
870        if (with.isBareJid()) {
871            updateRtpSessionProposalState(account, with, state);
872            return;
873        }
874        if (this.rtpConnectionReference == null) {
875            if (END_CARD.contains(state)) {
876                Log.d(Config.LOGTAG, "not reinitializing session");
877                return;
878            }
879            //this happens when going from proposed session to actual session
880            reInitializeActivityWithRunningRapSession(account, with, sessionId);
881            return;
882        }
883        final AbstractJingleConnection.Id id = requireRtpConnection().getId();
884        final Set<Media> media = getMedia();
885        final Contact contact = getWith();
886        if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
887            if (state == RtpEndUserState.ENDED) {
888                finish();
889                return;
890            }
891            runOnUiThread(() -> {
892                updateStateDisplay(state, media);
893                updateButtonConfiguration(state, media);
894                updateVideoViews(state);
895                updateProfilePicture(state, contact);
896            });
897            if (END_CARD.contains(state)) {
898                final JingleRtpConnection rtpConnection = requireRtpConnection();
899                resetIntent(account, with, state, rtpConnection.getMedia());
900                releaseVideoTracks(rtpConnection);
901                this.rtpConnectionReference = null;
902            }
903        } else {
904            Log.d(Config.LOGTAG, "received update for other rtp session");
905        }
906    }
907
908    @Override
909    public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
910        Log.d(Config.LOGTAG, "onAudioDeviceChanged in activity: selected:" + selectedAudioDevice + ", available:" + availableAudioDevices);
911        try {
912            if (getMedia().contains(Media.VIDEO)) {
913                Log.d(Config.LOGTAG, "nothing to do; in video mode");
914                return;
915            }
916            final RtpEndUserState endUserState = requireRtpConnection().getEndUserState();
917            if (endUserState == RtpEndUserState.CONNECTED) {
918                final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
919                updateInCallButtonConfigurationSpeaker(
920                        audioManager.getSelectedAudioDevice(),
921                        audioManager.getAudioDevices().size()
922                );
923            } else if (END_CARD.contains(endUserState)) {
924                Log.d(Config.LOGTAG, "onAudioDeviceChanged() nothing to do because end card has been reached");
925            } else {
926                putProximityWakeLockInProperState();
927            }
928        } catch (IllegalStateException e) {
929            Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed");
930        }
931    }
932
933    private void updateRtpSessionProposalState(final Account account, final Jid with, final RtpEndUserState state) {
934        final Intent currentIntent = getIntent();
935        final String withExtra = currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH);
936        if (withExtra == null) {
937            return;
938        }
939        if (Jid.ofEscaped(withExtra).asBareJid().equals(with)) {
940            runOnUiThread(() -> {
941                updateStateDisplay(state);
942                updateButtonConfiguration(state);
943                updateProfilePicture(state);
944            });
945            resetIntent(account, with, state, actionToMedia(currentIntent.getAction()));
946        }
947    }
948
949    private void resetIntent(final Bundle extras) {
950        final Intent intent = new Intent(Intent.ACTION_VIEW);
951        intent.putExtras(extras);
952        setIntent(intent);
953    }
954
955    private void resetIntent(final Account account, Jid with, final RtpEndUserState state, final Set<Media> media) {
956        final Intent intent = new Intent(Intent.ACTION_VIEW);
957        intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString());
958        intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
959        intent.putExtra(EXTRA_LAST_REPORTED_STATE, state.toString());
960        intent.putExtra(EXTRA_LAST_ACTION, media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL);
961        setIntent(intent);
962    }
963}