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.localVideo.release();
649            binding.remoteVideo.setVisibility(View.GONE);
650            binding.remoteVideo.release();
651            binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
652            if (isPictureInPicture()) {
653                binding.appBarLayout.setVisibility(View.GONE);
654                binding.pipPlaceholder.setVisibility(View.VISIBLE);
655                if (state == RtpEndUserState.APPLICATION_ERROR || state == RtpEndUserState.CONNECTIVITY_ERROR) {
656                    binding.pipWarning.setVisibility(View.VISIBLE);
657                    binding.pipWaiting.setVisibility(View.GONE);
658                } else {
659                    binding.pipWarning.setVisibility(View.GONE);
660                    binding.pipWaiting.setVisibility(View.GONE);
661                }
662            } else {
663                binding.appBarLayout.setVisibility(View.VISIBLE);
664                binding.pipPlaceholder.setVisibility(View.GONE);
665            }
666            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
667            return;
668        }
669        if (isPictureInPicture() && (state == RtpEndUserState.CONNECTING || state == RtpEndUserState.ACCEPTING_CALL)) {
670            binding.localVideo.setVisibility(View.GONE);
671            binding.remoteVideo.setVisibility(View.GONE);
672            binding.appBarLayout.setVisibility(View.GONE);
673            binding.pipPlaceholder.setVisibility(View.VISIBLE);
674            binding.pipWarning.setVisibility(View.GONE);
675            binding.pipWaiting.setVisibility(View.VISIBLE);
676            binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
677            return;
678        }
679        final Optional<VideoTrack> localVideoTrack = getLocalVideoTrack();
680        if (localVideoTrack.isPresent() && !isPictureInPicture()) {
681            ensureSurfaceViewRendererIsSetup(binding.localVideo);
682            //paint local view over remote view
683            binding.localVideo.setZOrderMediaOverlay(true);
684            binding.localVideo.setMirror(true);
685            localVideoTrack.get().addSink(binding.localVideo);
686        } else {
687            binding.localVideo.setVisibility(View.GONE);
688        }
689        final Optional<VideoTrack> remoteVideoTrack = getRemoteVideoTrack();
690        if (remoteVideoTrack.isPresent()) {
691            ensureSurfaceViewRendererIsSetup(binding.remoteVideo);
692            remoteVideoTrack.get().addSink(binding.remoteVideo);
693            if (state == RtpEndUserState.CONNECTED) {
694                binding.appBarLayout.setVisibility(View.GONE);
695                getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
696            } else {
697                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
698                binding.remoteVideo.setVisibility(View.GONE);
699            }
700            if (isPictureInPicture() && !requireRtpConnection().isMicrophoneEnabled()) {
701                binding.pipLocalMicOffIndicator.setVisibility(View.VISIBLE);
702            } else {
703                binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
704            }
705        } else {
706            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
707            binding.remoteVideo.setVisibility(View.GONE);
708            binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
709        }
710    }
711
712    private Optional<VideoTrack> getLocalVideoTrack() {
713        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
714        if (connection == null) {
715            return Optional.absent();
716        }
717        return connection.getLocalVideoTrack();
718    }
719
720    private Optional<VideoTrack> getRemoteVideoTrack() {
721        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
722        if (connection == null) {
723            return Optional.absent();
724        }
725        return connection.getRemoteVideoTrack();
726    }
727
728    private void disableMicrophone(View view) {
729        JingleRtpConnection rtpConnection = requireRtpConnection();
730        rtpConnection.setMicrophoneEnabled(false);
731        updateInCallButtonConfiguration();
732    }
733
734    private void enableMicrophone(View view) {
735        JingleRtpConnection rtpConnection = requireRtpConnection();
736        rtpConnection.setMicrophoneEnabled(true);
737        updateInCallButtonConfiguration();
738    }
739
740    private void switchToEarpiece(View view) {
741        requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
742        acquireProximityWakeLock();
743    }
744
745    private void switchToSpeaker(View view) {
746        requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
747        releaseProximityWakeLock();
748    }
749
750    private void retry(View view) {
751        Log.d(Config.LOGTAG, "attempting retry");
752        final Intent intent = getIntent();
753        final Account account = extractAccount(intent);
754        final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
755        final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION);
756        final String action = intent.getAction();
757        final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
758        this.rtpConnectionReference = null;
759        proposeJingleRtpSession(account, with, media);
760    }
761
762    private void exit(View view) {
763        finish();
764    }
765
766    private Contact getWith() {
767        final AbstractJingleConnection.Id id = requireRtpConnection().getId();
768        final Account account = id.account;
769        return account.getRoster().getContact(id.with);
770    }
771
772    private JingleRtpConnection requireRtpConnection() {
773        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
774        if (connection == null) {
775            throw new IllegalStateException("No RTP connection found");
776        }
777        return connection;
778    }
779
780    @Override
781    public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) {
782        Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")");
783        if (END_CARD.contains(state)) {
784            Log.d(Config.LOGTAG, "end card reached");
785            releaseProximityWakeLock();
786            runOnUiThread(() -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON));
787        }
788        if (with.isBareJid()) {
789            updateRtpSessionProposalState(account, with, state);
790            return;
791        }
792        if (this.rtpConnectionReference == null) {
793            if (END_CARD.contains(state)) {
794                Log.d(Config.LOGTAG, "not reinitializing session");
795                return;
796            }
797            //this happens when going from proposed session to actual session
798            reInitializeActivityWithRunningRapSession(account, with, sessionId);
799            return;
800        }
801        final AbstractJingleConnection.Id id = requireRtpConnection().getId();
802        final Set<Media> media = getMedia();
803        final Contact contact = getWith();
804        if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
805            if (state == RtpEndUserState.ENDED) {
806                finish();
807                return;
808            }
809            runOnUiThread(() -> {
810                updateStateDisplay(state, media);
811                updateButtonConfiguration(state, media);
812                updateVideoViews(state);
813                updateProfilePicture(state, contact);
814            });
815            if (END_CARD.contains(state)) {
816                final JingleRtpConnection rtpConnection = requireRtpConnection();
817                resetIntent(account, with, state, rtpConnection.getMedia());
818                releaseVideoTracks(rtpConnection);
819                this.rtpConnectionReference = null;
820            }
821        } else {
822            Log.d(Config.LOGTAG, "received update for other rtp session");
823        }
824    }
825
826    @Override
827    public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
828        Log.d(Config.LOGTAG, "onAudioDeviceChanged in activity: selected:" + selectedAudioDevice + ", available:" + availableAudioDevices);
829        try {
830            if (getMedia().contains(Media.VIDEO)) {
831                Log.d(Config.LOGTAG, "nothing to do; in video mode");
832                return;
833            }
834            final RtpEndUserState endUserState = requireRtpConnection().getEndUserState();
835            if (endUserState == RtpEndUserState.CONNECTED) {
836                final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
837                updateInCallButtonConfigurationSpeaker(
838                        audioManager.getSelectedAudioDevice(),
839                        audioManager.getAudioDevices().size()
840                );
841            } else if (END_CARD.contains(endUserState)) {
842                Log.d(Config.LOGTAG, "onAudioDeviceChanged() nothing to do because end card has been reached");
843            } else {
844                putProximityWakeLockInProperState();
845            }
846        } catch (IllegalStateException e) {
847            Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed");
848        }
849    }
850
851    private void updateRtpSessionProposalState(final Account account, final Jid with, final RtpEndUserState state) {
852        final Intent currentIntent = getIntent();
853        final String withExtra = currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH);
854        if (withExtra == null) {
855            return;
856        }
857        if (Jid.ofEscaped(withExtra).asBareJid().equals(with)) {
858            runOnUiThread(() -> {
859                updateStateDisplay(state);
860                updateButtonConfiguration(state);
861                updateProfilePicture(state);
862            });
863            resetIntent(account, with, state, actionToMedia(currentIntent.getAction()));
864        }
865    }
866
867    private void resetIntent(final Bundle extras) {
868        final Intent intent = new Intent(Intent.ACTION_VIEW);
869        intent.putExtras(extras);
870        setIntent(intent);
871    }
872
873    private void resetIntent(final Account account, Jid with, final RtpEndUserState state, final Set<Media> media) {
874        final Intent intent = new Intent(Intent.ACTION_VIEW);
875        intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString());
876        intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
877        intent.putExtra(EXTRA_LAST_REPORTED_STATE, state.toString());
878        intent.putExtra(EXTRA_LAST_ACTION, media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL);
879        setIntent(intent);
880    }
881}