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