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