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            throw new IllegalStateException("failed to initialize activity with running rtp session. session not found");
416        }
417        this.rtpConnectionReference = reference;
418        final RtpEndUserState currentState = requireRtpConnection().getEndUserState();
419        if (currentState == RtpEndUserState.ENDED) {
420            reference.get().throwStateTransitionException();
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            addSink(localVideoTrack.get(), 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            addSink(remoteVideoTrack.get(), 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 static void addSink(final VideoTrack videoTrack, final SurfaceViewRenderer surfaceViewRenderer) {
793        try {
794            videoTrack.addSink(surfaceViewRenderer);
795        } catch (final IllegalStateException e) {
796            Log.e(Config.LOGTAG, "possible race condition on trying to display video track. ignoring", e);
797        }
798    }
799
800    private Optional<VideoTrack> getLocalVideoTrack() {
801        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
802        if (connection == null) {
803            return Optional.absent();
804        }
805        return connection.getLocalVideoTrack();
806    }
807
808    private Optional<VideoTrack> getRemoteVideoTrack() {
809        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
810        if (connection == null) {
811            return Optional.absent();
812        }
813        return connection.getRemoteVideoTrack();
814    }
815
816    private void disableMicrophone(View view) {
817        JingleRtpConnection rtpConnection = requireRtpConnection();
818        rtpConnection.setMicrophoneEnabled(false);
819        updateInCallButtonConfiguration();
820    }
821
822    private void enableMicrophone(View view) {
823        JingleRtpConnection rtpConnection = requireRtpConnection();
824        rtpConnection.setMicrophoneEnabled(true);
825        updateInCallButtonConfiguration();
826    }
827
828    private void switchToEarpiece(View view) {
829        requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
830        acquireProximityWakeLock();
831    }
832
833    private void switchToSpeaker(View view) {
834        requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
835        releaseProximityWakeLock();
836    }
837
838    private void retry(View view) {
839        Log.d(Config.LOGTAG, "attempting retry");
840        final Intent intent = getIntent();
841        final Account account = extractAccount(intent);
842        final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
843        final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION);
844        final String action = intent.getAction();
845        final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
846        this.rtpConnectionReference = null;
847        proposeJingleRtpSession(account, with, media);
848    }
849
850    private void exit(View view) {
851        finish();
852    }
853
854    private Contact getWith() {
855        final AbstractJingleConnection.Id id = requireRtpConnection().getId();
856        final Account account = id.account;
857        return account.getRoster().getContact(id.with);
858    }
859
860    private JingleRtpConnection requireRtpConnection() {
861        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
862        if (connection == null) {
863            throw new IllegalStateException("No RTP connection found");
864        }
865        return connection;
866    }
867
868    @Override
869    public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) {
870        Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")");
871        if (END_CARD.contains(state)) {
872            Log.d(Config.LOGTAG, "end card reached");
873            releaseProximityWakeLock();
874            runOnUiThread(() -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON));
875        }
876        if (with.isBareJid()) {
877            updateRtpSessionProposalState(account, with, state);
878            return;
879        }
880        if (this.rtpConnectionReference == null) {
881            if (END_CARD.contains(state)) {
882                Log.d(Config.LOGTAG, "not reinitializing session");
883                return;
884            }
885            //this happens when going from proposed session to actual session
886            reInitializeActivityWithRunningRapSession(account, with, sessionId);
887            return;
888        }
889        final AbstractJingleConnection.Id id = requireRtpConnection().getId();
890        final Set<Media> media = getMedia();
891        final Contact contact = getWith();
892        if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
893            if (state == RtpEndUserState.ENDED) {
894                finish();
895                return;
896            }
897            runOnUiThread(() -> {
898                updateStateDisplay(state, media);
899                updateButtonConfiguration(state, media);
900                updateVideoViews(state);
901                updateProfilePicture(state, contact);
902            });
903            if (END_CARD.contains(state)) {
904                final JingleRtpConnection rtpConnection = requireRtpConnection();
905                resetIntent(account, with, state, rtpConnection.getMedia());
906                releaseVideoTracks(rtpConnection);
907                this.rtpConnectionReference = null;
908            }
909        } else {
910            Log.d(Config.LOGTAG, "received update for other rtp session");
911        }
912    }
913
914    @Override
915    public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
916        Log.d(Config.LOGTAG, "onAudioDeviceChanged in activity: selected:" + selectedAudioDevice + ", available:" + availableAudioDevices);
917        try {
918            if (getMedia().contains(Media.VIDEO)) {
919                Log.d(Config.LOGTAG, "nothing to do; in video mode");
920                return;
921            }
922            final RtpEndUserState endUserState = requireRtpConnection().getEndUserState();
923            if (endUserState == RtpEndUserState.CONNECTED) {
924                final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
925                updateInCallButtonConfigurationSpeaker(
926                        audioManager.getSelectedAudioDevice(),
927                        audioManager.getAudioDevices().size()
928                );
929            } else if (END_CARD.contains(endUserState)) {
930                Log.d(Config.LOGTAG, "onAudioDeviceChanged() nothing to do because end card has been reached");
931            } else {
932                putProximityWakeLockInProperState();
933            }
934        } catch (IllegalStateException e) {
935            Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed");
936        }
937    }
938
939    private void updateRtpSessionProposalState(final Account account, final Jid with, final RtpEndUserState state) {
940        final Intent currentIntent = getIntent();
941        final String withExtra = currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH);
942        if (withExtra == null) {
943            return;
944        }
945        if (Jid.ofEscaped(withExtra).asBareJid().equals(with)) {
946            runOnUiThread(() -> {
947                updateStateDisplay(state);
948                updateButtonConfiguration(state);
949                updateProfilePicture(state);
950            });
951            resetIntent(account, with, state, actionToMedia(currentIntent.getAction()));
952        }
953    }
954
955    private void resetIntent(final Bundle extras) {
956        final Intent intent = new Intent(Intent.ACTION_VIEW);
957        intent.putExtras(extras);
958        setIntent(intent);
959    }
960
961    private void resetIntent(final Account account, Jid with, final RtpEndUserState state, final Set<Media> media) {
962        final Intent intent = new Intent(Intent.ACTION_VIEW);
963        intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString());
964        intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
965        intent.putExtra(EXTRA_LAST_REPORTED_STATE, state.toString());
966        intent.putExtra(EXTRA_LAST_ACTION, media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL);
967        setIntent(intent);
968    }
969}