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        Log.d(Config.LOGTAG,"updateCallDuration()");
709        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
710        if (connection == null || connection.getMedia().contains(Media.VIDEO)) {
711            Log.d(Config.LOGTAG,"rtpConnection was null or contained video");
712            this.binding.duration.setVisibility(View.GONE);
713            return;
714        }
715        final long rtpConnectionStarted = connection.getRtpConnectionStarted();
716        final long rtpConnectionEnded = connection.getRtpConnectionEnded();
717        if (rtpConnectionStarted != 0) {
718            final long ended = rtpConnectionEnded == 0 ? SystemClock.elapsedRealtime() : rtpConnectionEnded;
719            this.binding.duration.setText(TimeFrameUtils.formatTimePassed(rtpConnectionStarted, ended, false));
720            this.binding.duration.setVisibility(View.VISIBLE);
721        } else {
722            this.binding.duration.setVisibility(View.GONE);
723        }
724    }
725
726    private void updateVideoViews(final RtpEndUserState state) {
727        if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) {
728            binding.localVideo.setVisibility(View.GONE);
729            binding.localVideo.release();
730            binding.remoteVideo.setVisibility(View.GONE);
731            binding.remoteVideo.release();
732            binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
733            if (isPictureInPicture()) {
734                binding.appBarLayout.setVisibility(View.GONE);
735                binding.pipPlaceholder.setVisibility(View.VISIBLE);
736                if (state == RtpEndUserState.APPLICATION_ERROR || state == RtpEndUserState.CONNECTIVITY_ERROR) {
737                    binding.pipWarning.setVisibility(View.VISIBLE);
738                    binding.pipWaiting.setVisibility(View.GONE);
739                } else {
740                    binding.pipWarning.setVisibility(View.GONE);
741                    binding.pipWaiting.setVisibility(View.GONE);
742                }
743            } else {
744                binding.appBarLayout.setVisibility(View.VISIBLE);
745                binding.pipPlaceholder.setVisibility(View.GONE);
746            }
747            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
748            return;
749        }
750        if (isPictureInPicture() && (state == RtpEndUserState.CONNECTING || state == RtpEndUserState.ACCEPTING_CALL)) {
751            binding.localVideo.setVisibility(View.GONE);
752            binding.remoteVideo.setVisibility(View.GONE);
753            binding.appBarLayout.setVisibility(View.GONE);
754            binding.pipPlaceholder.setVisibility(View.VISIBLE);
755            binding.pipWarning.setVisibility(View.GONE);
756            binding.pipWaiting.setVisibility(View.VISIBLE);
757            binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
758            return;
759        }
760        final Optional<VideoTrack> localVideoTrack = getLocalVideoTrack();
761        if (localVideoTrack.isPresent() && !isPictureInPicture()) {
762            ensureSurfaceViewRendererIsSetup(binding.localVideo);
763            //paint local view over remote view
764            binding.localVideo.setZOrderMediaOverlay(true);
765            binding.localVideo.setMirror(requireRtpConnection().isFrontCamera());
766            localVideoTrack.get().addSink(binding.localVideo);
767        } else {
768            binding.localVideo.setVisibility(View.GONE);
769        }
770        final Optional<VideoTrack> remoteVideoTrack = getRemoteVideoTrack();
771        if (remoteVideoTrack.isPresent()) {
772            ensureSurfaceViewRendererIsSetup(binding.remoteVideo);
773            remoteVideoTrack.get().addSink(binding.remoteVideo);
774            if (state == RtpEndUserState.CONNECTED) {
775                binding.appBarLayout.setVisibility(View.GONE);
776                getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
777            } else {
778                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
779                binding.remoteVideo.setVisibility(View.GONE);
780            }
781            if (isPictureInPicture() && !requireRtpConnection().isMicrophoneEnabled()) {
782                binding.pipLocalMicOffIndicator.setVisibility(View.VISIBLE);
783            } else {
784                binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
785            }
786        } else {
787            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
788            binding.remoteVideo.setVisibility(View.GONE);
789            binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
790        }
791    }
792
793    private Optional<VideoTrack> getLocalVideoTrack() {
794        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
795        if (connection == null) {
796            return Optional.absent();
797        }
798        return connection.getLocalVideoTrack();
799    }
800
801    private Optional<VideoTrack> getRemoteVideoTrack() {
802        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
803        if (connection == null) {
804            return Optional.absent();
805        }
806        return connection.getRemoteVideoTrack();
807    }
808
809    private void disableMicrophone(View view) {
810        JingleRtpConnection rtpConnection = requireRtpConnection();
811        rtpConnection.setMicrophoneEnabled(false);
812        updateInCallButtonConfiguration();
813    }
814
815    private void enableMicrophone(View view) {
816        JingleRtpConnection rtpConnection = requireRtpConnection();
817        rtpConnection.setMicrophoneEnabled(true);
818        updateInCallButtonConfiguration();
819    }
820
821    private void switchToEarpiece(View view) {
822        requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
823        acquireProximityWakeLock();
824    }
825
826    private void switchToSpeaker(View view) {
827        requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
828        releaseProximityWakeLock();
829    }
830
831    private void retry(View view) {
832        Log.d(Config.LOGTAG, "attempting retry");
833        final Intent intent = getIntent();
834        final Account account = extractAccount(intent);
835        final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
836        final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION);
837        final String action = intent.getAction();
838        final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
839        this.rtpConnectionReference = null;
840        proposeJingleRtpSession(account, with, media);
841    }
842
843    private void exit(View view) {
844        finish();
845    }
846
847    private Contact getWith() {
848        final AbstractJingleConnection.Id id = requireRtpConnection().getId();
849        final Account account = id.account;
850        return account.getRoster().getContact(id.with);
851    }
852
853    private JingleRtpConnection requireRtpConnection() {
854        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
855        if (connection == null) {
856            throw new IllegalStateException("No RTP connection found");
857        }
858        return connection;
859    }
860
861    @Override
862    public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) {
863        Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")");
864        if (END_CARD.contains(state)) {
865            Log.d(Config.LOGTAG, "end card reached");
866            releaseProximityWakeLock();
867            runOnUiThread(() -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON));
868        }
869        if (with.isBareJid()) {
870            updateRtpSessionProposalState(account, with, state);
871            return;
872        }
873        if (this.rtpConnectionReference == null) {
874            if (END_CARD.contains(state)) {
875                Log.d(Config.LOGTAG, "not reinitializing session");
876                return;
877            }
878            //this happens when going from proposed session to actual session
879            reInitializeActivityWithRunningRapSession(account, with, sessionId);
880            return;
881        }
882        final AbstractJingleConnection.Id id = requireRtpConnection().getId();
883        final Set<Media> media = getMedia();
884        final Contact contact = getWith();
885        if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
886            if (state == RtpEndUserState.ENDED) {
887                finish();
888                return;
889            }
890            runOnUiThread(() -> {
891                updateStateDisplay(state, media);
892                updateButtonConfiguration(state, media);
893                updateVideoViews(state);
894                updateProfilePicture(state, contact);
895            });
896            if (END_CARD.contains(state)) {
897                final JingleRtpConnection rtpConnection = requireRtpConnection();
898                resetIntent(account, with, state, rtpConnection.getMedia());
899                releaseVideoTracks(rtpConnection);
900                this.rtpConnectionReference = null;
901            }
902        } else {
903            Log.d(Config.LOGTAG, "received update for other rtp session");
904        }
905    }
906
907    @Override
908    public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
909        Log.d(Config.LOGTAG, "onAudioDeviceChanged in activity: selected:" + selectedAudioDevice + ", available:" + availableAudioDevices);
910        try {
911            if (getMedia().contains(Media.VIDEO)) {
912                Log.d(Config.LOGTAG, "nothing to do; in video mode");
913                return;
914            }
915            final RtpEndUserState endUserState = requireRtpConnection().getEndUserState();
916            if (endUserState == RtpEndUserState.CONNECTED) {
917                final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
918                updateInCallButtonConfigurationSpeaker(
919                        audioManager.getSelectedAudioDevice(),
920                        audioManager.getAudioDevices().size()
921                );
922            } else if (END_CARD.contains(endUserState)) {
923                Log.d(Config.LOGTAG, "onAudioDeviceChanged() nothing to do because end card has been reached");
924            } else {
925                putProximityWakeLockInProperState();
926            }
927        } catch (IllegalStateException e) {
928            Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed");
929        }
930    }
931
932    private void updateRtpSessionProposalState(final Account account, final Jid with, final RtpEndUserState state) {
933        final Intent currentIntent = getIntent();
934        final String withExtra = currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH);
935        if (withExtra == null) {
936            return;
937        }
938        if (Jid.ofEscaped(withExtra).asBareJid().equals(with)) {
939            runOnUiThread(() -> {
940                updateStateDisplay(state);
941                updateButtonConfiguration(state);
942                updateProfilePicture(state);
943            });
944            resetIntent(account, with, state, actionToMedia(currentIntent.getAction()));
945        }
946    }
947
948    private void resetIntent(final Bundle extras) {
949        final Intent intent = new Intent(Intent.ACTION_VIEW);
950        intent.putExtras(extras);
951        setIntent(intent);
952    }
953
954    private void resetIntent(final Account account, Jid with, final RtpEndUserState state, final Set<Media> media) {
955        final Intent intent = new Intent(Intent.ACTION_VIEW);
956        intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString());
957        intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
958        intent.putExtra(EXTRA_LAST_REPORTED_STATE, state.toString());
959        intent.putExtra(EXTRA_LAST_ACTION, media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL);
960        setIntent(intent);
961    }
962}