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            if (xmppConnectionService != null) {
300                retractSessionProposal();
301            }
302        }
303        releaseProximityWakeLock();
304        super.onStop();
305    }
306
307    private void releaseVideoTracks(final JingleRtpConnection jingleRtpConnection) {
308        final Optional<VideoTrack> remoteVideo = jingleRtpConnection.getRemoteVideoTrack();
309        if (remoteVideo.isPresent()) {
310            remoteVideo.get().removeSink(binding.remoteVideo);
311        }
312        final Optional<VideoTrack> localVideo = jingleRtpConnection.geLocalVideoTrack();
313        if (localVideo.isPresent()) {
314            localVideo.get().removeSink(binding.localVideo);
315        }
316    }
317
318    @Override
319    public void onBackPressed() {
320        endCall();
321        super.onBackPressed();
322    }
323
324    @Override
325    public void onUserLeaveHint() {
326        Log.d(Config.LOGTAG, "user leave hint");
327        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
328            if (shouldBePictureInPicture()) {
329                enterPictureInPictureMode(
330                        new PictureInPictureParams.Builder()
331                                .setAspectRatio(new Rational(10, 16))
332                                .build()
333                );
334            }
335        }
336
337    }
338
339    private boolean shouldBePictureInPicture() {
340        try {
341            final JingleRtpConnection rtpConnection = requireRtpConnection();
342            return rtpConnection.getMedia().contains(Media.VIDEO) && Arrays.asList(
343                    RtpEndUserState.ACCEPTING_CALL,
344                    RtpEndUserState.CONNECTING,
345                    RtpEndUserState.CONNECTED
346            ).contains(rtpConnection.getEndUserState());
347        } catch (IllegalStateException e) {
348            return false;
349        }
350    }
351
352    private void initializeActivityWithRunningRtpSession(final Account account, Jid with, String sessionId) {
353        final WeakReference<JingleRtpConnection> reference = xmppConnectionService.getJingleConnectionManager()
354                .findJingleRtpConnection(account, with, sessionId);
355        if (reference == null || reference.get() == null) {
356            finish();
357            return;
358        }
359        this.rtpConnectionReference = reference;
360        final RtpEndUserState currentState = requireRtpConnection().getEndUserState();
361        if (currentState == RtpEndUserState.ENDED) {
362            finish();
363            return;
364        }
365        if (currentState == RtpEndUserState.INCOMING_CALL) {
366            getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
367        }
368        if (JingleRtpConnection.STATES_SHOWING_ONGOING_CALL.contains(requireRtpConnection().getState())) {
369            putScreenInCallMode();
370        }
371        binding.with.setText(getWith().getDisplayName());
372        updateVideoViews(currentState);
373        updateStateDisplay(currentState);
374        updateButtonConfiguration(currentState);
375        updateProfilePicture(currentState);
376    }
377
378    private void reInitializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) {
379        runOnUiThread(() -> {
380            initializeActivityWithRunningRtpSession(account, with, sessionId);
381        });
382        final Intent intent = new Intent(Intent.ACTION_VIEW);
383        intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
384        intent.putExtra(EXTRA_WITH, with.toEscapedString());
385        intent.putExtra(EXTRA_SESSION_ID, sessionId);
386        setIntent(intent);
387    }
388
389    private void ensureSurfaceViewRendererIsSetup(final SurfaceViewRenderer surfaceViewRenderer) {
390        surfaceViewRenderer.setVisibility(View.VISIBLE);
391        try {
392            surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null);
393        } catch (IllegalStateException e) {
394            Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized");
395        }
396        surfaceViewRenderer.setEnableHardwareScaler(true);
397    }
398
399    private void updateStateDisplay(final RtpEndUserState state) {
400        switch (state) {
401            case INCOMING_CALL:
402                if (getMedia().contains(Media.VIDEO)) {
403                    setTitle(R.string.rtp_state_incoming_video_call);
404                } else {
405                    setTitle(R.string.rtp_state_incoming_call);
406                }
407                break;
408            case CONNECTING:
409                setTitle(R.string.rtp_state_connecting);
410                break;
411            case CONNECTED:
412                setTitle(R.string.rtp_state_connected);
413                break;
414            case ACCEPTING_CALL:
415                setTitle(R.string.rtp_state_accepting_call);
416                break;
417            case ENDING_CALL:
418                setTitle(R.string.rtp_state_ending_call);
419                break;
420            case FINDING_DEVICE:
421                setTitle(R.string.rtp_state_finding_device);
422                break;
423            case RINGING:
424                setTitle(R.string.rtp_state_ringing);
425                break;
426            case DECLINED_OR_BUSY:
427                setTitle(R.string.rtp_state_declined_or_busy);
428                break;
429            case CONNECTIVITY_ERROR:
430                setTitle(R.string.rtp_state_connectivity_error);
431                break;
432            case RETRACTED:
433                setTitle(R.string.rtp_state_retracted);
434                break;
435            case APPLICATION_ERROR:
436                setTitle(R.string.rtp_state_application_failure);
437                break;
438            case ENDED:
439                throw new IllegalStateException("Activity should have called finishAndReleaseWakeLock();");
440            default:
441                throw new IllegalStateException(String.format("State %s has not been handled in UI", state));
442        }
443    }
444
445    private void updateProfilePicture(final RtpEndUserState state) {
446        if (state == RtpEndUserState.INCOMING_CALL || state == RtpEndUserState.ACCEPTING_CALL) {
447            final boolean show = getResources().getBoolean(R.bool.show_avatar_incoming_call);
448            if (show) {
449                binding.contactPhoto.setVisibility(View.VISIBLE);
450                AvatarWorkerTask.loadAvatar(getWith(), binding.contactPhoto, R.dimen.publish_avatar_size);
451            } else {
452                binding.contactPhoto.setVisibility(View.GONE);
453            }
454        } else {
455            binding.contactPhoto.setVisibility(View.GONE);
456        }
457    }
458
459    private Set<Media> getMedia() {
460        return requireRtpConnection().getMedia();
461    }
462
463    @SuppressLint("RestrictedApi")
464    private void updateButtonConfiguration(final RtpEndUserState state) {
465        if (state == RtpEndUserState.ENDING_CALL || isPictureInPicture()) {
466            this.binding.rejectCall.setVisibility(View.INVISIBLE);
467            this.binding.endCall.setVisibility(View.INVISIBLE);
468            this.binding.acceptCall.setVisibility(View.INVISIBLE);
469        } else if (state == RtpEndUserState.INCOMING_CALL) {
470            this.binding.rejectCall.setOnClickListener(this::rejectCall);
471            this.binding.rejectCall.setImageResource(R.drawable.ic_call_end_white_48dp);
472            this.binding.rejectCall.setVisibility(View.VISIBLE);
473            this.binding.endCall.setVisibility(View.INVISIBLE);
474            this.binding.acceptCall.setOnClickListener(this::acceptCall);
475            this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp);
476            this.binding.acceptCall.setVisibility(View.VISIBLE);
477        } else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
478            this.binding.rejectCall.setVisibility(View.INVISIBLE);
479            this.binding.endCall.setOnClickListener(this::exit);
480            this.binding.endCall.setImageResource(R.drawable.ic_clear_white_48dp);
481            this.binding.endCall.setVisibility(View.VISIBLE);
482            this.binding.acceptCall.setVisibility(View.INVISIBLE);
483        } else if (asList(RtpEndUserState.CONNECTIVITY_ERROR, RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.RETRACTED).contains(state)) {
484            this.binding.rejectCall.setOnClickListener(this::exit);
485            this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
486            this.binding.rejectCall.setVisibility(View.VISIBLE);
487            this.binding.endCall.setVisibility(View.INVISIBLE);
488            this.binding.acceptCall.setOnClickListener(this::retry);
489            this.binding.acceptCall.setImageResource(R.drawable.ic_replay_white_48dp);
490            this.binding.acceptCall.setVisibility(View.VISIBLE);
491        } else {
492            this.binding.rejectCall.setVisibility(View.INVISIBLE);
493            this.binding.endCall.setOnClickListener(this::endCall);
494            this.binding.endCall.setImageResource(R.drawable.ic_call_end_white_48dp);
495            this.binding.endCall.setVisibility(View.VISIBLE);
496            this.binding.acceptCall.setVisibility(View.INVISIBLE);
497        }
498        updateInCallButtonConfiguration(state);
499    }
500
501    private boolean isPictureInPicture() {
502        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
503            return isInPictureInPictureMode();
504        } else {
505            return false;
506        }
507    }
508
509    private void updateInCallButtonConfiguration() {
510        updateInCallButtonConfiguration(requireRtpConnection().getEndUserState());
511    }
512
513    @SuppressLint("RestrictedApi")
514    private void updateInCallButtonConfiguration(final RtpEndUserState state) {
515        if (state == RtpEndUserState.CONNECTED && !isPictureInPicture()) {
516            if (getMedia().contains(Media.VIDEO)) {
517                updateInCallButtonConfigurationVideo(requireRtpConnection().isVideoEnabled());
518            } else {
519                final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
520                updateInCallButtonConfigurationSpeaker(
521                        audioManager.getSelectedAudioDevice(),
522                        audioManager.getAudioDevices().size()
523                );
524            }
525            updateInCallButtonConfigurationMicrophone(requireRtpConnection().isMicrophoneEnabled());
526        } else {
527            this.binding.inCallActionLeft.setVisibility(View.GONE);
528            this.binding.inCallActionRight.setVisibility(View.GONE);
529        }
530    }
531
532    @SuppressLint("RestrictedApi")
533    private void updateInCallButtonConfigurationSpeaker(final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices) {
534        switch (selectedAudioDevice) {
535            case EARPIECE:
536                this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_off_black_24dp);
537                if (numberOfChoices >= 2) {
538                    this.binding.inCallActionRight.setOnClickListener(this::switchToSpeaker);
539                } else {
540                    this.binding.inCallActionRight.setOnClickListener(null);
541                    this.binding.inCallActionRight.setClickable(false);
542                }
543                break;
544            case WIRED_HEADSET:
545                this.binding.inCallActionRight.setImageResource(R.drawable.ic_headset_black_24dp);
546                this.binding.inCallActionRight.setOnClickListener(null);
547                this.binding.inCallActionRight.setClickable(false);
548                break;
549            case SPEAKER_PHONE:
550                this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_up_black_24dp);
551                if (numberOfChoices >= 2) {
552                    this.binding.inCallActionRight.setOnClickListener(this::switchToEarpiece);
553                } else {
554                    this.binding.inCallActionRight.setOnClickListener(null);
555                    this.binding.inCallActionRight.setClickable(false);
556                }
557                break;
558            case BLUETOOTH:
559                this.binding.inCallActionRight.setImageResource(R.drawable.ic_bluetooth_audio_black_24dp);
560                this.binding.inCallActionRight.setOnClickListener(null);
561                this.binding.inCallActionRight.setClickable(false);
562                break;
563        }
564        this.binding.inCallActionRight.setVisibility(View.VISIBLE);
565    }
566
567    @SuppressLint("RestrictedApi")
568    private void updateInCallButtonConfigurationVideo(final boolean videoEnabled) {
569        this.binding.inCallActionRight.setVisibility(View.VISIBLE);
570        if (videoEnabled) {
571            this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_black_24dp);
572            this.binding.inCallActionRight.setOnClickListener(this::disableVideo);
573        } else {
574            this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_off_black_24dp);
575            this.binding.inCallActionRight.setOnClickListener(this::enableVideo);
576        }
577    }
578
579    private void enableVideo(View view) {
580        requireRtpConnection().setVideoEnabled(true);
581        updateInCallButtonConfigurationVideo(true);
582    }
583
584    private void disableVideo(View view) {
585        requireRtpConnection().setVideoEnabled(false);
586        updateInCallButtonConfigurationVideo(false);
587
588    }
589
590    @SuppressLint("RestrictedApi")
591    private void updateInCallButtonConfigurationMicrophone(final boolean microphoneEnabled) {
592        if (microphoneEnabled) {
593            this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_black_24dp);
594            this.binding.inCallActionLeft.setOnClickListener(this::disableMicrophone);
595        } else {
596            this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_off_black_24dp);
597            this.binding.inCallActionLeft.setOnClickListener(this::enableMicrophone);
598        }
599        this.binding.inCallActionLeft.setVisibility(View.VISIBLE);
600    }
601
602    private void updateVideoViews(final RtpEndUserState state) {
603        if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) {
604            binding.localVideo.setVisibility(View.GONE);
605            binding.remoteVideo.setVisibility(View.GONE);
606            binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
607            if (isPictureInPicture()) {
608                binding.appBarLayout.setVisibility(View.GONE);
609                binding.pipPlaceholder.setVisibility(View.VISIBLE);
610                if (state == RtpEndUserState.APPLICATION_ERROR || state == RtpEndUserState.CONNECTIVITY_ERROR) {
611                    binding.pipWarning.setVisibility(View.VISIBLE);
612                    binding.pipWaiting.setVisibility(View.GONE);
613                } else {
614                    binding.pipWarning.setVisibility(View.GONE);
615                    binding.pipWaiting.setVisibility(View.GONE);
616                }
617            } else {
618                binding.appBarLayout.setVisibility(View.VISIBLE);
619                binding.pipPlaceholder.setVisibility(View.GONE);
620            }
621            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
622            return;
623        }
624        if (isPictureInPicture() && (state == RtpEndUserState.CONNECTING || state == RtpEndUserState.ACCEPTING_CALL)) {
625            binding.localVideo.setVisibility(View.GONE);
626            binding.remoteVideo.setVisibility(View.GONE);
627            binding.appBarLayout.setVisibility(View.GONE);
628            binding.pipPlaceholder.setVisibility(View.VISIBLE);
629            binding.pipWarning.setVisibility(View.GONE);
630            binding.pipWaiting.setVisibility(View.VISIBLE);
631            binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
632            return;
633        }
634        final Optional<VideoTrack> localVideoTrack = requireRtpConnection().geLocalVideoTrack();
635        if (localVideoTrack.isPresent() && !isPictureInPicture()) {
636            ensureSurfaceViewRendererIsSetup(binding.localVideo);
637            //paint local view over remote view
638            binding.localVideo.setZOrderMediaOverlay(true);
639            binding.localVideo.setMirror(true);
640            localVideoTrack.get().addSink(binding.localVideo);
641        } else {
642            binding.localVideo.setVisibility(View.GONE);
643        }
644        final Optional<VideoTrack> remoteVideoTrack = requireRtpConnection().getRemoteVideoTrack();
645        if (remoteVideoTrack.isPresent()) {
646            ensureSurfaceViewRendererIsSetup(binding.remoteVideo);
647            remoteVideoTrack.get().addSink(binding.remoteVideo);
648            if (state == RtpEndUserState.CONNECTED) {
649                binding.appBarLayout.setVisibility(View.GONE);
650                getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
651            } else {
652                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
653                binding.remoteVideo.setVisibility(View.GONE);
654            }
655            if (isPictureInPicture() && !requireRtpConnection().isMicrophoneEnabled()) {
656                binding.pipLocalMicOffIndicator.setVisibility(View.VISIBLE);
657            } else {
658                binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
659            }
660        } else {
661            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
662            binding.remoteVideo.setVisibility(View.GONE);
663            binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
664        }
665    }
666
667    private void disableMicrophone(View view) {
668        JingleRtpConnection rtpConnection = requireRtpConnection();
669        rtpConnection.setMicrophoneEnabled(false);
670        updateInCallButtonConfiguration();
671    }
672
673    private void enableMicrophone(View view) {
674        JingleRtpConnection rtpConnection = requireRtpConnection();
675        rtpConnection.setMicrophoneEnabled(true);
676        updateInCallButtonConfiguration();
677    }
678
679    private void switchToEarpiece(View view) {
680        requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
681        acquireProximityWakeLock();
682    }
683
684    private void switchToSpeaker(View view) {
685        requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
686        releaseProximityWakeLock();
687    }
688
689    private void retry(View view) {
690        Log.d(Config.LOGTAG, "attempting retry");
691        final Intent intent = getIntent();
692        final Account account = extractAccount(intent);
693        final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
694        final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION);
695        final String action = intent.getAction();
696        final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
697        this.rtpConnectionReference = null;
698        proposeJingleRtpSession(account, with, media);
699    }
700
701    private void exit(View view) {
702        finish();
703    }
704
705    private Contact getWith() {
706        final AbstractJingleConnection.Id id = requireRtpConnection().getId();
707        final Account account = id.account;
708        return account.getRoster().getContact(id.with);
709    }
710
711    private JingleRtpConnection requireRtpConnection() {
712        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
713        if (connection == null) {
714            throw new IllegalStateException("No RTP connection found");
715        }
716        return connection;
717    }
718
719    @Override
720    public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) {
721        Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")");
722        if (END_CARD.contains(state)) {
723            Log.d(Config.LOGTAG, "end card reached");
724            releaseProximityWakeLock();
725            runOnUiThread(() -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON));
726        }
727        if (with.isBareJid()) {
728            updateRtpSessionProposalState(account, with, state);
729            return;
730        }
731        if (this.rtpConnectionReference == null) {
732            if (END_CARD.contains(state)) {
733                Log.d(Config.LOGTAG, "not reinitializing session");
734                return;
735            }
736            //this happens when going from proposed session to actual session
737            reInitializeActivityWithRunningRapSession(account, with, sessionId);
738            return;
739        }
740        final AbstractJingleConnection.Id id = requireRtpConnection().getId();
741        if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
742            if (state == RtpEndUserState.ENDED) {
743                finish();
744                return;
745            } else if (END_CARD.contains(state)) {
746                resetIntent(account, with, state, requireRtpConnection().getMedia());
747            }
748            runOnUiThread(() -> {
749                updateStateDisplay(state);
750                updateButtonConfiguration(state);
751                updateVideoViews(state);
752                updateProfilePicture(state);
753            });
754        } else {
755            Log.d(Config.LOGTAG, "received update for other rtp session");
756            //TODO if we only ever have one; we might just switch over? Maybe!
757        }
758    }
759
760    @Override
761    public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
762        Log.d(Config.LOGTAG, "onAudioDeviceChanged in activity: selected:" + selectedAudioDevice + ", available:" + availableAudioDevices);
763        try {
764            if (getMedia().contains(Media.VIDEO)) {
765                Log.d(Config.LOGTAG, "nothing to do; in video mode");
766                return;
767            }
768            final RtpEndUserState endUserState = requireRtpConnection().getEndUserState();
769            if (endUserState == RtpEndUserState.CONNECTED) {
770                final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
771                updateInCallButtonConfigurationSpeaker(
772                        audioManager.getSelectedAudioDevice(),
773                        audioManager.getAudioDevices().size()
774                );
775            } else if (END_CARD.contains(endUserState)) {
776                Log.d(Config.LOGTAG, "onAudioDeviceChanged() nothing to do because end card has been reached");
777            } else {
778                putProximityWakeLockInProperState();
779            }
780        } catch (IllegalStateException e) {
781            Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed");
782        }
783    }
784
785    private void updateRtpSessionProposalState(final Account account, final Jid with, final RtpEndUserState state) {
786        final Intent currentIntent = getIntent();
787        final String withExtra = currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH);
788        if (withExtra == null) {
789            return;
790        }
791        if (Jid.ofEscaped(withExtra).asBareJid().equals(with)) {
792            runOnUiThread(() -> {
793                updateStateDisplay(state);
794                updateButtonConfiguration(state);
795                updateProfilePicture(state);
796            });
797            resetIntent(account, with, state, actionToMedia(currentIntent.getAction()));
798        }
799    }
800
801    private void resetIntent(final Bundle extras) {
802        final Intent intent = new Intent(Intent.ACTION_VIEW);
803        intent.putExtras(extras);
804        setIntent(intent);
805    }
806
807    private void resetIntent(final Account account, Jid with, final RtpEndUserState state, final Set<Media> media) {
808        final Intent intent = new Intent(Intent.ACTION_VIEW);
809        intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString());
810        intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
811        intent.putExtra(EXTRA_LAST_REPORTED_STATE, state.toString());
812        intent.putExtra(EXTRA_LAST_ACTION, media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL);
813        setIntent(intent);
814    }
815}