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