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