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