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