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