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