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