Explorar o código

convert CallActivity to kotlin

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Marcel Hibbe hai 1 ano
pai
achega
b5529f869a

+ 0 - 3229
app/src/main/java/com/nextcloud/talk/activities/CallActivity.java

@@ -1,3229 +0,0 @@
-/*
- * Nextcloud Talk application
- *
- * @author Mario Danic
- * @author Tim Krüger
- * @author Marcel Hibbe
- * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
- * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
- * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.nextcloud.talk.activities;
-
-import android.Manifest;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.annotation.SuppressLint;
-import android.app.PendingIntent;
-import android.app.RemoteAction;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Configuration;
-import android.graphics.Color;
-import android.graphics.drawable.Icon;
-import android.media.AudioAttributes;
-import android.media.MediaPlayer;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.text.TextUtils;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.widget.FrameLayout;
-import android.widget.RelativeLayout;
-import android.widget.Toast;
-
-import com.bluelinelabs.logansquare.LoganSquare;
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.nextcloud.talk.R;
-import com.nextcloud.talk.adapters.ParticipantDisplayItem;
-import com.nextcloud.talk.adapters.ParticipantsAdapter;
-import com.nextcloud.talk.api.NcApi;
-import com.nextcloud.talk.application.NextcloudTalkApplication;
-import com.nextcloud.talk.call.CallParticipant;
-import com.nextcloud.talk.call.CallParticipantList;
-import com.nextcloud.talk.call.CallParticipantModel;
-import com.nextcloud.talk.call.ReactionAnimator;
-import com.nextcloud.talk.chat.ChatActivity;
-import com.nextcloud.talk.data.user.model.User;
-import com.nextcloud.talk.databinding.CallActivityBinding;
-import com.nextcloud.talk.events.ConfigurationChangeEvent;
-import com.nextcloud.talk.events.NetworkEvent;
-import com.nextcloud.talk.events.ProximitySensorEvent;
-import com.nextcloud.talk.events.WebSocketCommunicationEvent;
-import com.nextcloud.talk.models.ExternalSignalingServer;
-import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall;
-import com.nextcloud.talk.models.json.conversations.Conversation;
-import com.nextcloud.talk.models.json.conversations.RoomOverall;
-import com.nextcloud.talk.models.json.conversations.RoomsOverall;
-import com.nextcloud.talk.models.json.generic.GenericOverall;
-import com.nextcloud.talk.models.json.participants.Participant;
-import com.nextcloud.talk.models.json.signaling.DataChannelMessage;
-import com.nextcloud.talk.models.json.signaling.NCMessagePayload;
-import com.nextcloud.talk.models.json.signaling.NCSignalingMessage;
-import com.nextcloud.talk.models.json.signaling.Signaling;
-import com.nextcloud.talk.models.json.signaling.SignalingOverall;
-import com.nextcloud.talk.models.json.signaling.settings.IceServer;
-import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall;
-import com.nextcloud.talk.raisehand.viewmodel.RaiseHandViewModel;
-import com.nextcloud.talk.signaling.SignalingMessageReceiver;
-import com.nextcloud.talk.signaling.SignalingMessageSender;
-import com.nextcloud.talk.ui.dialog.AudioOutputDialog;
-import com.nextcloud.talk.ui.dialog.MoreCallActionsDialog;
-import com.nextcloud.talk.users.UserManager;
-import com.nextcloud.talk.utils.ApiUtils;
-import com.nextcloud.talk.utils.DisplayUtils;
-import com.nextcloud.talk.utils.NotificationUtils;
-import com.nextcloud.talk.utils.VibrationUtils;
-import com.nextcloud.talk.utils.animations.PulseAnimation;
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew;
-import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew;
-import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil;
-import com.nextcloud.talk.utils.power.PowerManagerUtils;
-import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder;
-import com.nextcloud.talk.viewmodels.CallRecordingViewModel;
-import com.nextcloud.talk.webrtc.MagicWebRTCUtils;
-import com.nextcloud.talk.webrtc.PeerConnectionWrapper;
-import com.nextcloud.talk.webrtc.WebRtcAudioManager;
-import com.nextcloud.talk.webrtc.WebSocketConnectionHelper;
-import com.nextcloud.talk.webrtc.WebSocketInstance;
-import com.wooplr.spotlight.SpotlightView;
-
-import org.apache.commons.lang3.StringEscapeUtils;
-import org.greenrobot.eventbus.Subscribe;
-import org.greenrobot.eventbus.ThreadMode;
-import org.webrtc.AudioSource;
-import org.webrtc.AudioTrack;
-import org.webrtc.Camera1Enumerator;
-import org.webrtc.Camera2Enumerator;
-import org.webrtc.CameraEnumerator;
-import org.webrtc.CameraVideoCapturer;
-import org.webrtc.DefaultVideoDecoderFactory;
-import org.webrtc.DefaultVideoEncoderFactory;
-import org.webrtc.EglBase;
-import org.webrtc.Logging;
-import org.webrtc.MediaConstraints;
-import org.webrtc.MediaStream;
-import org.webrtc.PeerConnection;
-import org.webrtc.PeerConnectionFactory;
-import org.webrtc.RendererCommon;
-import org.webrtc.SurfaceTextureHelper;
-import org.webrtc.VideoCapturer;
-import org.webrtc.VideoSource;
-import org.webrtc.VideoTrack;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import javax.inject.Inject;
-
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
-import androidx.annotation.DrawableRes;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.appcompat.app.AlertDialog;
-import androidx.core.graphics.drawable.DrawableCompat;
-import androidx.lifecycle.ViewModelProvider;
-import autodagger.AutoInjector;
-import io.reactivex.Observable;
-import io.reactivex.Observer;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.disposables.Disposable;
-import io.reactivex.schedulers.Schedulers;
-import okhttp3.Cache;
-
-import static android.app.PendingIntent.FLAG_IMMUTABLE;
-import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_VOICE_ONLY;
-import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_WITHOUT_NOTIFICATION;
-import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME;
-import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_PASSWORD;
-import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FROM_NOTIFICATION_START_CALL;
-import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_BREAKOUT_ROOM;
-import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_MODERATOR;
-import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MODIFIED_BASE_URL;
-import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO;
-import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO;
-import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_RECORDING_STATE;
-import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID;
-import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN;
-import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_START_CALL_AFTER_ROOM_SWITCH;
-import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SWITCH_TO_ROOM;
-
-@AutoInjector(NextcloudTalkApplication.class)
-public class CallActivity extends CallBaseActivity {
-
-    public static boolean active = false;
-
-    public static final String VIDEO_STREAM_TYPE_SCREEN = "screen";
-    public static final String VIDEO_STREAM_TYPE_VIDEO = "video";
-
-    @Inject
-    NcApi ncApi;
-
-    @Inject
-    CurrentUserProviderNew currentUserProvider;
-
-    @Inject
-    UserManager userManager;
-
-    @Inject
-    Cache cache;
-
-    @Inject
-    PlatformPermissionUtil permissionUtil;
-    @Inject
-    ViewModelProvider.Factory viewModelFactory;
-
-    public static final String TAG = "CallActivity";
-
-    public WebRtcAudioManager audioManager;
-
-    public CallRecordingViewModel callRecordingViewModel;
-    public RaiseHandViewModel raiseHandViewModel;
-
-    private static final String[] PERMISSIONS_CAMERA = {
-        Manifest.permission.CAMERA
-    };
-
-    private static final String[] PERMISSIONS_MICROPHONE = {
-        Manifest.permission.RECORD_AUDIO
-    };
-
-    private static final String MICROPHONE_PIP_INTENT_NAME = "microphone_pip_intent";
-    private static final String MICROPHONE_PIP_INTENT_EXTRA_ACTION = "microphone_pip_action";
-    private static final int MICROPHONE_PIP_REQUEST_MUTE = 1;
-    private static final int MICROPHONE_PIP_REQUEST_UNMUTE = 2;
-
-    private BroadcastReceiver mReceiver;
-
-    private PeerConnectionFactory peerConnectionFactory;
-    private MediaConstraints audioConstraints;
-    private MediaConstraints videoConstraints;
-    private MediaConstraints sdpConstraints;
-    private MediaConstraints sdpConstraintsForMCU;
-    private VideoSource videoSource;
-    private VideoTrack localVideoTrack;
-    private AudioSource audioSource;
-    private AudioTrack localAudioTrack;
-    private VideoCapturer videoCapturer;
-    private EglBase rootEglBase;
-    private Disposable signalingDisposable;
-    private List<PeerConnection.IceServer> iceServers;
-    private CameraEnumerator cameraEnumerator;
-    private String roomToken;
-    public User conversationUser;
-    private String conversationName;
-    private String callSession;
-    private MediaStream localStream;
-    private String credentials;
-    private List<PeerConnectionWrapper> peerConnectionWrapperList = new ArrayList<>();
-
-    private boolean videoOn = false;
-    private boolean microphoneOn = false;
-
-    private boolean isVoiceOnlyCall;
-    private boolean isCallWithoutNotification;
-    private boolean isIncomingCallFromNotification;
-    private Handler callControlHandler = new Handler();
-    private Handler callInfosHandler = new Handler();
-    private Handler cameraSwitchHandler = new Handler();
-
-    // push to talk
-    private boolean isPushToTalkActive = false;
-    private PulseAnimation pulseAnimation;
-
-    private String baseUrl;
-    private String roomId;
-
-    private SpotlightView spotlightView;
-
-    private InternalSignalingMessageReceiver internalSignalingMessageReceiver = new InternalSignalingMessageReceiver();
-    private SignalingMessageReceiver signalingMessageReceiver;
-
-    private InternalSignalingMessageSender internalSignalingMessageSender = new InternalSignalingMessageSender();
-    private SignalingMessageSender signalingMessageSender;
-
-    private Map<String, OfferAnswerNickProvider> offerAnswerNickProviders = new HashMap<>();
-
-    private Map<String, SignalingMessageReceiver.CallParticipantMessageListener> callParticipantMessageListeners =
-        new HashMap<>();
-
-    private PeerConnectionWrapper.PeerConnectionObserver selfPeerConnectionObserver = new CallActivitySelfPeerConnectionObserver();
-
-    private Map<String, CallParticipant> callParticipants = new HashMap<>();
-
-    private Map<String, ScreenParticipantDisplayItemManager> screenParticipantDisplayItemManagers = new HashMap<>();
-
-    private Handler screenParticipantDisplayItemManagersHandler = new Handler(Looper.getMainLooper());
-
-    private Map<String, CallParticipantEventDisplayer> callParticipantEventDisplayers = new HashMap<>();
-
-    private Handler callParticipantEventDisplayersHandler = new Handler(Looper.getMainLooper());
-
-    private CallParticipantList.Observer callParticipantListObserver = new CallParticipantList.Observer() {
-        @Override
-        public void onCallParticipantsChanged(Collection<Participant> joined, Collection<Participant> updated,
-                                              Collection<Participant> left, Collection<Participant> unchanged) {
-            handleCallParticipantsChanged(joined, updated, left, unchanged);
-        }
-
-        @Override
-        public void onCallEndedForAll() {
-            Log.d(TAG, "A moderator ended the call for all.");
-            hangup(true);
-        }
-    };
-
-    private CallParticipantList callParticipantList;
-
-    private String switchToRoomToken = "";
-    private boolean isBreakoutRoom = false;
-
-    private SignalingMessageReceiver.LocalParticipantMessageListener localParticipantMessageListener =
-        new SignalingMessageReceiver.LocalParticipantMessageListener() {
-            @Override
-            public void onSwitchTo(String token) {
-                switchToRoomToken = token;
-                hangup(true);
-            }
-        };
-
-    private SignalingMessageReceiver.OfferMessageListener offerMessageListener = new SignalingMessageReceiver.OfferMessageListener() {
-        @Override
-        public void onOffer(String sessionId, String roomType, String sdp, String nick) {
-            getOrCreatePeerConnectionWrapperForSessionIdAndType(sessionId, roomType, false);
-        }
-    };
-
-    private ExternalSignalingServer externalSignalingServer;
-    private WebSocketInstance webSocketClient;
-    private WebSocketConnectionHelper webSocketConnectionHelper;
-    private boolean hasMCU;
-    private boolean hasExternalSignalingServer;
-    private String conversationPassword;
-
-    private PowerManagerUtils powerManagerUtils;
-
-    private Handler handler;
-
-    private CallStatus currentCallStatus;
-
-    private MediaPlayer mediaPlayer;
-
-    private Map<String, ParticipantDisplayItem> participantDisplayItems;
-    private ParticipantsAdapter participantsAdapter;
-
-    private CallActivityBinding binding;
-
-    private AudioOutputDialog audioOutputDialog;
-    private MoreCallActionsDialog moreCallActionsDialog;
-
-    ActivityResultLauncher<String[]> requestPermissionLauncher =
-        registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), permissionMap -> {
-
-            List<String> rationaleList = new ArrayList<>();
-
-            Boolean audioPermission = permissionMap.get(Manifest.permission.RECORD_AUDIO);
-            if (audioPermission != null) {
-                if (Boolean.TRUE.equals(audioPermission)) {
-                    if (!microphoneOn) {
-                        onMicrophoneClick();
-                    }
-                } else {
-                    rationaleList.add((getResources().getString(R.string.nc_microphone_permission_hint)));
-                }
-            }
-
-            Boolean cameraPermission = permissionMap.get(Manifest.permission.CAMERA);
-            if (cameraPermission != null) {
-                if (Boolean.TRUE.equals(cameraPermission)) {
-                    if (!videoOn) {
-                        onCameraClick();
-                    }
-
-                    if (cameraEnumerator.getDeviceNames().length == 0) {
-                        binding.cameraButton.setVisibility(View.GONE);
-                    }
-
-                    if (cameraEnumerator.getDeviceNames().length > 1) {
-                        binding.switchSelfVideoButton.setVisibility(View.VISIBLE);
-                    }
-                } else {
-                    rationaleList.add((getResources().getString(R.string.nc_camera_permission_hint)));
-                }
-            }
-
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
-                Boolean bluetoothPermission = permissionMap.get(Manifest.permission.BLUETOOTH_CONNECT);
-                if (bluetoothPermission != null) {
-                    if (Boolean.TRUE.equals(bluetoothPermission)) {
-                        enableBluetoothManager();
-                    } else {
-                        // Only ask for bluetooth when already asking to grant microphone or camera access. Asking
-                        // for bluetooth solely is not important enough here and would most likely annoy the user.
-                        if (!rationaleList.isEmpty()) {
-                            rationaleList.add((getResources().getString(R.string.nc_bluetooth_permission_hint)));
-                        }
-                    }
-                }
-            }
-
-            if (!rationaleList.isEmpty()) {
-                showRationaleDialogForSettings(rationaleList);
-            }
-        });
-
-
-    private boolean canPublishAudioStream;
-    private boolean canPublishVideoStream;
-
-    private boolean isModerator;
-
-    private ReactionAnimator reactionAnimator;
-
-    @SuppressLint("ClickableViewAccessibility")
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        Log.d(TAG, "onCreate");
-        super.onCreate(savedInstanceState);
-
-        NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
-
-        binding = CallActivityBinding.inflate(getLayoutInflater());
-        setContentView(binding.getRoot());
-
-        hideNavigationIfNoPipAvailable();
-
-        conversationUser = currentUserProvider.getCurrentUser().blockingGet();
-
-        Bundle extras = getIntent().getExtras();
-        roomId = extras.getString(KEY_ROOM_ID, "");
-        roomToken = extras.getString(KEY_ROOM_TOKEN, "");
-        conversationPassword = extras.getString(KEY_CONVERSATION_PASSWORD, "");
-        conversationName = extras.getString(KEY_CONVERSATION_NAME, "");
-        isVoiceOnlyCall = extras.getBoolean(KEY_CALL_VOICE_ONLY, false);
-        isCallWithoutNotification = extras.getBoolean(KEY_CALL_WITHOUT_NOTIFICATION, false);
-        canPublishAudioStream = extras.getBoolean(KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO);
-        canPublishVideoStream = extras.getBoolean(KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO);
-        isModerator = extras.getBoolean(KEY_IS_MODERATOR, false);
-
-        if (extras.containsKey(KEY_FROM_NOTIFICATION_START_CALL)) {
-            isIncomingCallFromNotification = extras.getBoolean(KEY_FROM_NOTIFICATION_START_CALL);
-        }
-
-        if (extras.containsKey(KEY_IS_BREAKOUT_ROOM)) {
-            isBreakoutRoom = extras.getBoolean(KEY_IS_BREAKOUT_ROOM);
-        }
-
-        credentials = ApiUtils.getCredentials(conversationUser.getUsername(), conversationUser.getToken());
-
-        baseUrl = extras.getString(KEY_MODIFIED_BASE_URL, "");
-        if (TextUtils.isEmpty(baseUrl)) {
-            baseUrl = conversationUser.getBaseUrl();
-        }
-
-        powerManagerUtils = new PowerManagerUtils();
-
-        if ("resume".equalsIgnoreCase(extras.getString("state", ""))) {
-            setCallState(CallStatus.IN_CONVERSATION);
-        } else {
-            setCallState(CallStatus.CONNECTING);
-        }
-
-        raiseHandViewModel = new ViewModelProvider(this, viewModelFactory).get((RaiseHandViewModel.class));
-        raiseHandViewModel.setData(roomToken, isBreakoutRoom);
-
-        raiseHandViewModel.getViewState().observe(this, viewState -> {
-            boolean raised = false;
-            if (viewState instanceof RaiseHandViewModel.RaisedHandState) {
-                binding.lowerHandButton.setVisibility(View.VISIBLE);
-                raised = true;
-            } else if (viewState instanceof RaiseHandViewModel.LoweredHandState) {
-                binding.lowerHandButton.setVisibility(View.GONE);
-                raised = false;
-            }
-
-            if (isConnectionEstablished() && peerConnectionWrapperList != null) {
-                for (PeerConnectionWrapper peerConnectionWrapper : peerConnectionWrapperList) {
-                    peerConnectionWrapper.raiseHand(raised);
-                }
-            }
-        });
-
-        callRecordingViewModel = new ViewModelProvider(this, viewModelFactory).get((CallRecordingViewModel.class));
-        callRecordingViewModel.setData(roomToken);
-        callRecordingViewModel.setRecordingState(extras.getInt(KEY_RECORDING_STATE));
-
-        callRecordingViewModel.getViewState().observe(this, viewState -> {
-            if (viewState instanceof CallRecordingViewModel.RecordingStartedState) {
-                binding.callRecordingIndicator.setImageResource(R.drawable.record_stop);
-                binding.callRecordingIndicator.setVisibility(View.VISIBLE);
-                if (((CallRecordingViewModel.RecordingStartedState) viewState).getShowStartedInfo()) {
-                    VibrationUtils.INSTANCE.vibrateShort(context);
-                    Toast.makeText(context, context.getResources().getString(R.string.record_active_info), Toast.LENGTH_LONG).show();
-                }
-            } else if (viewState instanceof CallRecordingViewModel.RecordingStartingState) {
-                if (isAllowedToStartOrStopRecording()) {
-                    binding.callRecordingIndicator.setImageResource(R.drawable.record_starting);
-                    binding.callRecordingIndicator.setVisibility(View.VISIBLE);
-                } else {
-                    binding.callRecordingIndicator.setVisibility(View.GONE);
-                }
-            } else if (viewState instanceof CallRecordingViewModel.RecordingConfirmStopState) {
-                if (isAllowedToStartOrStopRecording()) {
-                    MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(this)
-                        .setTitle(R.string.record_stop_confirm_title)
-                        .setMessage(R.string.record_stop_confirm_message)
-                        .setPositiveButton(R.string.record_stop_description,
-                                           (dialog, which) -> callRecordingViewModel.stopRecording())
-                        .setNegativeButton(R.string.nc_common_dismiss,
-                                           (dialog, which) -> callRecordingViewModel.dismissStopRecording());
-                    viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder);
-                    AlertDialog dialog = dialogBuilder.show();
-
-                    viewThemeUtils.platform.colorTextButtons(
-                        dialog.getButton(AlertDialog.BUTTON_POSITIVE),
-                        dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
-                                                            );
-                } else {
-                    Log.e(TAG, "Being in RecordingConfirmStopState as non moderator. This should not happen!");
-                }
-            } else if (viewState instanceof CallRecordingViewModel.RecordingErrorState) {
-                if (isAllowedToStartOrStopRecording()) {
-                    Toast.makeText(context, context.getResources().getString(R.string.record_failed_info),
-                                   Toast.LENGTH_LONG).show();
-                }
-                binding.callRecordingIndicator.setVisibility(View.GONE);
-            } else {
-                binding.callRecordingIndicator.setVisibility(View.GONE);
-            }
-        });
-
-        initClickListeners();
-        binding.microphoneButton.setOnTouchListener(new MicrophoneButtonTouchListener());
-
-        pulseAnimation = PulseAnimation.create().with(binding.microphoneButton)
-            .setDuration(310)
-            .setRepeatCount(PulseAnimation.INFINITE)
-            .setRepeatMode(PulseAnimation.REVERSE);
-
-        basicInitialization();
-        callParticipants = new HashMap<>();
-        participantDisplayItems = new HashMap<>();
-        initViews();
-
-        if (!isConnectionEstablished()) {
-            initiateCall();
-        }
-
-        updateSelfVideoViewPosition();
-
-        reactionAnimator = new ReactionAnimator(context, binding.reactionAnimationWrapper, viewThemeUtils);
-    }
-
-    public void sendReaction(String emoji) {
-        addReactionForAnimation(emoji, conversationUser.getDisplayName());
-
-        if (isConnectionEstablished() && peerConnectionWrapperList != null) {
-            for (PeerConnectionWrapper peerConnectionWrapper : peerConnectionWrapperList) {
-                peerConnectionWrapper.sendReaction(emoji);
-            }
-        }
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-        active = true;
-        initFeaturesVisibility();
-
-        try {
-            cache.evictAll();
-        } catch (IOException e) {
-            Log.e(TAG, "Failed to evict cache");
-        }
-    }
-
-    @Override
-    public void onStop() {
-        super.onStop();
-        active = false;
-    }
-
-    private void enableBluetoothManager() {
-        if (audioManager != null) {
-            audioManager.startBluetoothManager();
-        }
-    }
-
-    private void initFeaturesVisibility() {
-        if (isAllowedToStartOrStopRecording() || isAllowedToRaiseHand()) {
-            binding.moreCallActions.setVisibility(View.VISIBLE);
-        } else {
-            binding.moreCallActions.setVisibility(View.GONE);
-        }
-    }
-
-    private void initClickListeners() {
-        binding.pictureInPictureButton.setOnClickListener(l -> enterPipMode());
-
-        binding.audioOutputButton.setOnClickListener(v -> {
-            audioOutputDialog = new AudioOutputDialog(this);
-            audioOutputDialog.show();
-        });
-
-        binding.moreCallActions.setOnClickListener(v -> {
-            moreCallActionsDialog = new MoreCallActionsDialog(this);
-            moreCallActionsDialog.show();
-        });
-
-        if (canPublishAudioStream) {
-            binding.microphoneButton.setOnClickListener(l -> onMicrophoneClick());
-            binding.microphoneButton.setOnLongClickListener(l -> {
-                if (!microphoneOn) {
-                    callControlHandler.removeCallbacksAndMessages(null);
-                    callInfosHandler.removeCallbacksAndMessages(null);
-                    cameraSwitchHandler.removeCallbacksAndMessages(null);
-                    isPushToTalkActive = true;
-                    binding.callControls.setVisibility(View.VISIBLE);
-                    if (!isVoiceOnlyCall) {
-                        binding.switchSelfVideoButton.setVisibility(View.VISIBLE);
-                    }
-                }
-                onMicrophoneClick();
-                return true;
-            });
-        } else {
-            binding.microphoneButton.setOnClickListener(
-                l -> Toast.makeText(context,
-                                    R.string.nc_not_allowed_to_activate_audio,
-                                    Toast.LENGTH_SHORT
-                                   ).show()
-                                                       );
-        }
-
-        if (canPublishVideoStream) {
-            binding.cameraButton.setOnClickListener(l -> onCameraClick());
-        } else {
-            binding.cameraButton.setOnClickListener(
-                l -> Toast.makeText(context,
-                                    R.string.nc_not_allowed_to_activate_video,
-                                    Toast.LENGTH_SHORT
-                                   ).show()
-                                                   );
-        }
-
-        binding.hangupButton.setOnClickListener(l -> {
-            hangup(true);
-        });
-
-        binding.switchSelfVideoButton.setOnClickListener(l -> switchCamera());
-
-        binding.gridview.setOnItemClickListener((parent, view, position, id) -> animateCallControls(true, 0));
-
-        binding.callStates.callStateRelativeLayout.setOnClickListener(l -> {
-            if (currentCallStatus == CallStatus.CALLING_TIMEOUT) {
-                setCallState(CallStatus.RECONNECTING);
-                hangupNetworkCalls(false);
-            }
-        });
-
-        binding.callRecordingIndicator.setOnClickListener(l -> {
-            if (isAllowedToStartOrStopRecording()) {
-                if (callRecordingViewModel.getViewState().getValue() instanceof CallRecordingViewModel.RecordingStartingState) {
-                    if (moreCallActionsDialog == null) {
-                        moreCallActionsDialog = new MoreCallActionsDialog(this);
-                    }
-                    moreCallActionsDialog.show();
-                } else {
-                    callRecordingViewModel.clickRecordButton();
-                }
-            } else {
-                Toast.makeText(context, context.getResources().getString(R.string.record_active_info), Toast.LENGTH_LONG).show();
-            }
-        });
-
-        binding.lowerHandButton.setOnClickListener(l -> {
-            raiseHandViewModel.lowerHand();
-        });
-    }
-
-    private void createCameraEnumerator() {
-        boolean camera2EnumeratorIsSupported = false;
-        try {
-            camera2EnumeratorIsSupported = Camera2Enumerator.isSupported(this);
-        } catch (final Throwable t) {
-            Log.w(TAG, "Camera2Enumerator threw an error", t);
-        }
-
-        if (camera2EnumeratorIsSupported) {
-            cameraEnumerator = new Camera2Enumerator(this);
-        } else {
-            cameraEnumerator = new Camera1Enumerator(MagicWebRTCUtils.shouldEnableVideoHardwareAcceleration());
-        }
-    }
-
-    private void basicInitialization() {
-        rootEglBase = EglBase.create();
-        createCameraEnumerator();
-
-        //Create a new PeerConnectionFactory instance.
-        PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
-        DefaultVideoEncoderFactory defaultVideoEncoderFactory = new DefaultVideoEncoderFactory(
-            rootEglBase.getEglBaseContext(), true, true);
-        DefaultVideoDecoderFactory defaultVideoDecoderFactory = new DefaultVideoDecoderFactory(
-            rootEglBase.getEglBaseContext());
-
-        peerConnectionFactory = PeerConnectionFactory.builder()
-            .setOptions(options)
-            .setVideoEncoderFactory(defaultVideoEncoderFactory)
-            .setVideoDecoderFactory(defaultVideoDecoderFactory)
-            .createPeerConnectionFactory();
-
-        //Create MediaConstraints - Will be useful for specifying video and audio constraints.
-        audioConstraints = new MediaConstraints();
-        videoConstraints = new MediaConstraints();
-
-        localStream = peerConnectionFactory.createLocalMediaStream("NCMS");
-
-        // Create and audio manager that will take care of audio routing,
-        // audio modes, audio device enumeration etc.
-        audioManager = WebRtcAudioManager.create(getApplicationContext(), isVoiceOnlyCall);
-        // Store existing audio settings and change audio mode to
-        // MODE_IN_COMMUNICATION for best possible VoIP performance.
-        Log.d(TAG, "Starting the audio manager...");
-        audioManager.start(this::onAudioManagerDevicesChanged);
-
-        if (isVoiceOnlyCall) {
-            setAudioOutputChannel(WebRtcAudioManager.AudioDevice.EARPIECE);
-        } else {
-            setAudioOutputChannel(WebRtcAudioManager.AudioDevice.SPEAKER_PHONE);
-        }
-
-        iceServers = new ArrayList<>();
-
-        //create sdpConstraints
-        sdpConstraints = new MediaConstraints();
-        sdpConstraintsForMCU = new MediaConstraints();
-        sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
-        String offerToReceiveVideoString = "true";
-
-        if (isVoiceOnlyCall) {
-            offerToReceiveVideoString = "false";
-        }
-
-        sdpConstraints.mandatory.add(
-            new MediaConstraints.KeyValuePair("OfferToReceiveVideo", offerToReceiveVideoString));
-
-        sdpConstraintsForMCU.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false"));
-        sdpConstraintsForMCU.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false"));
-
-        sdpConstraintsForMCU.optional.add(new MediaConstraints.KeyValuePair("internalSctpDataChannels", "true"));
-        sdpConstraintsForMCU.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
-
-        sdpConstraints.optional.add(new MediaConstraints.KeyValuePair("internalSctpDataChannels", "true"));
-        sdpConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
-
-        if (!isVoiceOnlyCall) {
-            cameraInitialization();
-        }
-
-        microphoneInitialization();
-    }
-
-    public void setAudioOutputChannel(WebRtcAudioManager.AudioDevice selectedAudioDevice) {
-        if (audioManager != null) {
-            audioManager.selectAudioDevice(selectedAudioDevice);
-            updateAudioOutputButton(audioManager.getCurrentAudioDevice());
-        }
-    }
-
-    private void updateAudioOutputButton(WebRtcAudioManager.AudioDevice activeAudioDevice) {
-        switch (activeAudioDevice) {
-            case BLUETOOTH:
-                binding.audioOutputButton.setImageResource(R.drawable.ic_baseline_bluetooth_audio_24);
-                break;
-            case SPEAKER_PHONE:
-                binding.audioOutputButton.setImageResource(R.drawable.ic_volume_up_white_24dp);
-                break;
-            case EARPIECE:
-                binding.audioOutputButton.setImageResource(R.drawable.ic_baseline_phone_in_talk_24);
-                break;
-            case WIRED_HEADSET:
-                binding.audioOutputButton.setImageResource(R.drawable.ic_baseline_headset_mic_24);
-                break;
-            default:
-                Log.e(TAG, "Icon for audio output not available");
-                break;
-        }
-        DrawableCompat.setTint(binding.audioOutputButton.getDrawable(), Color.WHITE);
-    }
-
-    private void handleFromNotification() {
-        int apiVersion = ApiUtils.getConversationApiVersion(conversationUser, new int[]{ApiUtils.APIv4, 1});
-
-        ncApi.getRooms(credentials, ApiUtils.getUrlForRooms(apiVersion, baseUrl), Boolean.FALSE)
-            .retry(3)
-            .subscribeOn(Schedulers.io())
-            .observeOn(AndroidSchedulers.mainThread())
-            .subscribe(new Observer<RoomsOverall>() {
-                @Override
-                public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
-                    // unused atm
-                }
-
-                @Override
-                public void onNext(@io.reactivex.annotations.NonNull RoomsOverall roomsOverall) {
-                    for (Conversation conversation : roomsOverall.getOcs().getData()) {
-                        if (roomId.equals(conversation.getRoomId())) {
-                            roomToken = conversation.getToken();
-                            break;
-                        }
-                    }
-
-                    checkDevicePermissions();
-                }
-
-                @Override
-                public void onError(@io.reactivex.annotations.NonNull Throwable e) {
-                    // unused atm
-                }
-
-                @Override
-                public void onComplete() {
-                    // unused atm
-                }
-            });
-    }
-
-    @SuppressLint("ClickableViewAccessibility")
-    private void initViews() {
-        Log.d(TAG, "initViews");
-        binding.callInfosLinearLayout.setVisibility(View.VISIBLE);
-        binding.selfVideoViewWrapper.setVisibility(View.VISIBLE);
-
-        if (!isPipModePossible()) {
-            binding.pictureInPictureButton.setVisibility(View.GONE);
-        }
-
-        if (isVoiceOnlyCall) {
-            binding.switchSelfVideoButton.setVisibility(View.GONE);
-            binding.cameraButton.setVisibility(View.GONE);
-            binding.selfVideoRenderer.setVisibility(View.GONE);
-
-            RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
-                                                                                 ViewGroup.LayoutParams.WRAP_CONTENT);
-            params.addRule(RelativeLayout.BELOW, R.id.callInfosLinearLayout);
-            int callControlsHeight = Math.round(getApplicationContext().getResources().getDimension(R.dimen.call_controls_height));
-            params.setMargins(0, 0, 0, callControlsHeight);
-            binding.gridview.setLayoutParams(params);
-        } else {
-            RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
-                                                                                 ViewGroup.LayoutParams.WRAP_CONTENT);
-            params.setMargins(0, 0, 0, 0);
-            binding.gridview.setLayoutParams(params);
-
-            if (cameraEnumerator.getDeviceNames().length < 2) {
-                binding.switchSelfVideoButton.setVisibility(View.GONE);
-            }
-            initSelfVideoView();
-        }
-
-        binding.gridview.setOnTouchListener(new View.OnTouchListener() {
-            public boolean onTouch(View v, MotionEvent me) {
-                int action = me.getActionMasked();
-                if (action == MotionEvent.ACTION_DOWN) {
-                    animateCallControls(true, 0);
-                }
-                return false;
-            }
-        });
-
-        binding.conversationRelativeLayout.setOnTouchListener(new View.OnTouchListener() {
-            public boolean onTouch(View v, MotionEvent me) {
-                int action = me.getActionMasked();
-                if (action == MotionEvent.ACTION_DOWN) {
-                    animateCallControls(true, 0);
-                }
-                return false;
-            }
-        });
-
-        animateCallControls(true, 0);
-
-        initGridAdapter();
-    }
-
-    @SuppressLint("ClickableViewAccessibility")
-    private void initSelfVideoView() {
-        try {
-            binding.selfVideoRenderer.init(rootEglBase.getEglBaseContext(), null);
-        } catch (IllegalStateException e) {
-            Log.d(TAG, "selfVideoRenderer already initialized", e);
-        }
-
-        binding.selfVideoRenderer.setZOrderMediaOverlay(true);
-        // disabled because it causes some devices to crash
-        binding.selfVideoRenderer.setEnableHardwareScaler(false);
-        binding.selfVideoRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
-        binding.selfVideoRenderer.setOnTouchListener(new SelfVideoTouchListener());
-    }
-
-    private void initGridAdapter() {
-        Log.d(TAG, "initGridAdapter");
-        int columns;
-        int participantsInGrid = participantDisplayItems.size();
-        if (getResources() != null
-            && getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
-            if (participantsInGrid > 2) {
-                columns = 2;
-            } else {
-                columns = 1;
-            }
-        } else {
-            if (participantsInGrid > 2) {
-                columns = 3;
-            } else if (participantsInGrid > 1) {
-                columns = 2;
-            } else {
-                columns = 1;
-            }
-        }
-
-        binding.gridview.setNumColumns(columns);
-
-        binding.conversationRelativeLayout
-            .getViewTreeObserver()
-            .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
-                @Override
-                public void onGlobalLayout() {
-                    binding.conversationRelativeLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
-                    int height = binding.conversationRelativeLayout.getMeasuredHeight();
-                    binding.gridview.setMinimumHeight(height);
-                }
-            });
-
-        binding
-            .callInfosLinearLayout
-            .getViewTreeObserver()
-            .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
-                @Override
-                public void onGlobalLayout() {
-                    binding.callInfosLinearLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
-                }
-            });
-
-        if (participantsAdapter != null) {
-            participantsAdapter.destroy();
-        }
-
-        participantsAdapter = new ParticipantsAdapter(
-            this,
-            participantDisplayItems,
-            binding.conversationRelativeLayout,
-            binding.callInfosLinearLayout,
-            columns,
-            isVoiceOnlyCall);
-        binding.gridview.setAdapter(participantsAdapter);
-
-        if (isInPipMode) {
-            updateUiForPipMode();
-        }
-    }
-
-    private void checkDevicePermissions() {
-        List<String> permissionsToRequest = new ArrayList<>();
-        List<String> rationaleList = new ArrayList<>();
-
-        if (permissionUtil.isMicrophonePermissionGranted()) {
-            if (!microphoneOn) {
-                onMicrophoneClick();
-            }
-
-        } else if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) {
-            permissionsToRequest.add(Manifest.permission.RECORD_AUDIO);
-            rationaleList.add((getResources().getString(R.string.nc_microphone_permission_hint)));
-        } else {
-            permissionsToRequest.add(Manifest.permission.RECORD_AUDIO);
-        }
-
-        if (!isVoiceOnlyCall) {
-            if (permissionUtil.isCameraPermissionGranted()) {
-                if (!videoOn) {
-                    onCameraClick();
-                }
-
-                if (cameraEnumerator.getDeviceNames().length == 0) {
-                    binding.cameraButton.setVisibility(View.GONE);
-                }
-
-                if (cameraEnumerator.getDeviceNames().length > 1) {
-                    binding.switchSelfVideoButton.setVisibility(View.VISIBLE);
-                }
-            } else if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
-                permissionsToRequest.add(Manifest.permission.CAMERA);
-                rationaleList.add((getResources().getString(R.string.nc_camera_permission_hint)));
-            } else {
-                permissionsToRequest.add(Manifest.permission.CAMERA);
-            }
-        }
-
-
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
-            if (permissionUtil.isBluetoothPermissionGranted()) {
-                enableBluetoothManager();
-            } else if (shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_CONNECT)) {
-                permissionsToRequest.add(Manifest.permission.BLUETOOTH_CONNECT);
-                rationaleList.add((getResources().getString(R.string.nc_bluetooth_permission_hint)));
-            } else {
-                permissionsToRequest.add(Manifest.permission.BLUETOOTH_CONNECT);
-            }
-        }
-
-        if (!permissionsToRequest.isEmpty()) {
-            if (!rationaleList.isEmpty()) {
-                showRationaleDialog(permissionsToRequest, rationaleList);
-            } else {
-                requestPermissionLauncher.launch(permissionsToRequest.toArray(new String[permissionsToRequest.size()]));
-            }
-        }
-
-        if (!isConnectionEstablished()) {
-            fetchSignalingSettings();
-        }
-    }
-
-    private void showRationaleDialog(String permissionToRequest, String rationale) {
-        List<String> rationaleList = new ArrayList<String>();
-        List<String> permissionsToRequest = new ArrayList<String>();
-
-        rationaleList.add(rationale);
-        permissionsToRequest.add(permissionToRequest);
-
-        showRationaleDialog(permissionsToRequest, rationaleList);
-    }
-
-    private void showRationaleDialog(List<String> permissionsToRequest, List<String> rationaleList) {
-        StringBuilder rationalesWithLineBreaks = new StringBuilder();
-
-        for (String rationale : rationaleList) {
-            rationalesWithLineBreaks.append(rationale).append("\n\n");
-        }
-
-        MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(this)
-            .setTitle(R.string.nc_permissions_rationale_dialog_title)
-            .setMessage(rationalesWithLineBreaks)
-            .setPositiveButton(R.string.nc_permissions_ask, (dialog, which) ->
-                                   requestPermissionLauncher.launch(
-                                       permissionsToRequest.toArray(new String[permissionsToRequest.size()])
-                                                                   )
-                              )
-            .setNegativeButton(R.string.nc_common_dismiss, null);
-        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder);
-        dialogBuilder.show();
-    }
-
-    private void showRationaleDialogForSettings(List<String> rationaleList) {
-        StringBuilder rationalesWithLineBreaks = new StringBuilder();
-        rationalesWithLineBreaks.append(getResources().getString(R.string.nc_permissions_denied));
-        rationalesWithLineBreaks.append('\n');
-        rationalesWithLineBreaks.append(getResources().getString(R.string.nc_permissions_settings_hint));
-        rationalesWithLineBreaks.append("\n\n");
-
-        for (String rationale : rationaleList) {
-            rationalesWithLineBreaks.append(rationale).append("\n\n");
-        }
-
-        MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(this)
-            .setTitle(R.string.nc_permissions_rationale_dialog_title)
-            .setMessage(rationalesWithLineBreaks)
-            .setPositiveButton(R.string.nc_permissions_settings, (dialog, which) -> {
-                Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
-                intent.setData(Uri.fromParts("package", getPackageName(), null));
-                startActivity(intent);
-            })
-            .setNegativeButton(R.string.nc_common_dismiss, null);
-        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder);
-        dialogBuilder.show();
-    }
-
-
-    private boolean isConnectionEstablished() {
-        return (currentCallStatus == CallStatus.JOINED || currentCallStatus == CallStatus.IN_CONVERSATION);
-    }
-
-    private void onAudioManagerDevicesChanged(
-        final WebRtcAudioManager.AudioDevice currentDevice,
-        final Set<WebRtcAudioManager.AudioDevice> availableDevices) {
-        Log.d(TAG, "onAudioManagerDevicesChanged: " + availableDevices + ", "
-            + "currentDevice: " + currentDevice);
-
-        final boolean shouldDisableProximityLock = (currentDevice == WebRtcAudioManager.AudioDevice.WIRED_HEADSET
-            || currentDevice == WebRtcAudioManager.AudioDevice.SPEAKER_PHONE
-            || currentDevice == WebRtcAudioManager.AudioDevice.BLUETOOTH);
-
-        if (shouldDisableProximityLock) {
-            powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.WITHOUT_PROXIMITY_SENSOR_LOCK);
-        } else {
-            powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.WITH_PROXIMITY_SENSOR_LOCK);
-        }
-
-        if (audioOutputDialog != null) {
-            audioOutputDialog.updateOutputDeviceList();
-        }
-        updateAudioOutputButton(currentDevice);
-    }
-
-
-    private void cameraInitialization() {
-        videoCapturer = createCameraCapturer(cameraEnumerator);
-
-        //Create a VideoSource instance
-        if (videoCapturer != null) {
-            SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread",
-                                                                                    rootEglBase.getEglBaseContext());
-            videoSource = peerConnectionFactory.createVideoSource(false);
-            videoCapturer.initialize(surfaceTextureHelper, getApplicationContext(), videoSource.getCapturerObserver());
-        }
-        localVideoTrack = peerConnectionFactory.createVideoTrack("NCv0", videoSource);
-        localStream.addTrack(localVideoTrack);
-        localVideoTrack.setEnabled(false);
-        localVideoTrack.addSink(binding.selfVideoRenderer);
-    }
-
-    private void microphoneInitialization() {
-        //create an AudioSource instance
-        audioSource = peerConnectionFactory.createAudioSource(audioConstraints);
-        localAudioTrack = peerConnectionFactory.createAudioTrack("NCa0", audioSource);
-        localAudioTrack.setEnabled(false);
-        localStream.addTrack(localAudioTrack);
-    }
-
-    private VideoCapturer createCameraCapturer(CameraEnumerator enumerator) {
-        final String[] deviceNames = enumerator.getDeviceNames();
-
-        // First, try to find front facing camera
-        Logging.d(TAG, "Looking for front facing cameras.");
-        for (String deviceName : deviceNames) {
-            if (enumerator.isFrontFacing(deviceName)) {
-                Logging.d(TAG, "Creating front facing camera capturer.");
-                VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
-                if (videoCapturer != null) {
-                    binding.selfVideoRenderer.setMirror(true);
-                    return videoCapturer;
-                }
-            }
-        }
-
-
-        // Front facing camera not found, try something else
-        Logging.d(TAG, "Looking for other cameras.");
-        for (String deviceName : deviceNames) {
-            if (!enumerator.isFrontFacing(deviceName)) {
-                Logging.d(TAG, "Creating other camera capturer.");
-                VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
-
-                if (videoCapturer != null) {
-                    binding.selfVideoRenderer.setMirror(false);
-                    return videoCapturer;
-                }
-            }
-        }
-
-        return null;
-    }
-
-    public void onMicrophoneClick() {
-        if (!canPublishAudioStream) {
-            microphoneOn = false;
-            binding.microphoneButton.setImageResource(R.drawable.ic_mic_off_white_24px);
-            toggleMedia(false, false);
-        }
-
-        if (isVoiceOnlyCall && !isConnectionEstablished()) {
-            fetchSignalingSettings();
-        }
-
-        if (!canPublishAudioStream) {
-            // In the case no audio stream will be published it's not needed to check microphone permissions
-            return;
-        }
-
-        if (permissionUtil.isMicrophonePermissionGranted()) {
-
-            if (!appPreferences.getPushToTalkIntroShown()) {
-                int primary = viewThemeUtils.getScheme(binding.audioOutputButton.getContext()).getPrimary();
-                spotlightView = new SpotlightView.Builder(this)
-                    .introAnimationDuration(300)
-                    .enableRevealAnimation(true)
-                    .performClick(false)
-                    .fadeinTextDuration(400)
-                    .headingTvColor(primary)
-                    .headingTvSize(20)
-                    .headingTvText(getResources().getString(R.string.nc_push_to_talk))
-                    .subHeadingTvColor(getResources().getColor(R.color.bg_default))
-                    .subHeadingTvSize(16)
-                    .subHeadingTvText(getResources().getString(R.string.nc_push_to_talk_desc))
-                    .maskColor(Color.parseColor("#dc000000"))
-                    .target(binding.microphoneButton)
-                    .lineAnimDuration(400)
-                    .lineAndArcColor(primary)
-                    .enableDismissAfterShown(true)
-                    .dismissOnBackPress(true)
-                    .usageId("pushToTalk")
-                    .show();
-
-                appPreferences.setPushToTalkIntroShown(true);
-            }
-
-            if (!isPushToTalkActive) {
-                microphoneOn = !microphoneOn;
-
-                if (microphoneOn) {
-                    binding.microphoneButton.setImageResource(R.drawable.ic_mic_white_24px);
-                    updatePictureInPictureActions(R.drawable.ic_mic_white_24px,
-                                                  getResources().getString(R.string.nc_pip_microphone_mute),
-                                                  MICROPHONE_PIP_REQUEST_MUTE);
-                } else {
-                    binding.microphoneButton.setImageResource(R.drawable.ic_mic_off_white_24px);
-                    updatePictureInPictureActions(R.drawable.ic_mic_off_white_24px,
-                                                  getResources().getString(R.string.nc_pip_microphone_unmute),
-                                                  MICROPHONE_PIP_REQUEST_UNMUTE);
-                }
-
-                toggleMedia(microphoneOn, false);
-            } else {
-                binding.microphoneButton.setImageResource(R.drawable.ic_mic_white_24px);
-                pulseAnimation.start();
-                toggleMedia(true, false);
-            }
-        } else if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) {
-            showRationaleDialog(
-                Manifest.permission.RECORD_AUDIO,
-                getResources().getString(R.string.nc_microphone_permission_hint)
-                               );
-        } else {
-            requestPermissionLauncher.launch(PERMISSIONS_MICROPHONE);
-        }
-    }
-
-    public void onCameraClick() {
-        if (!canPublishVideoStream) {
-            videoOn = false;
-            binding.cameraButton.setImageResource(R.drawable.ic_videocam_off_white_24px);
-            binding.switchSelfVideoButton.setVisibility(View.GONE);
-            return;
-        }
-
-        if (permissionUtil.isCameraPermissionGranted()) {
-            videoOn = !videoOn;
-
-            if (videoOn) {
-                binding.cameraButton.setImageResource(R.drawable.ic_videocam_white_24px);
-                if (cameraEnumerator.getDeviceNames().length > 1) {
-                    binding.switchSelfVideoButton.setVisibility(View.VISIBLE);
-                }
-            } else {
-                binding.cameraButton.setImageResource(R.drawable.ic_videocam_off_white_24px);
-                binding.switchSelfVideoButton.setVisibility(View.GONE);
-            }
-
-            toggleMedia(videoOn, true);
-        } else if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
-            showRationaleDialog(
-                Manifest.permission.CAMERA,
-                getResources().getString(R.string.nc_camera_permission_hint)
-                               );
-        } else {
-            requestPermissionLauncher.launch(PERMISSIONS_CAMERA);
-        }
-    }
-
-    public void switchCamera() {
-        CameraVideoCapturer cameraVideoCapturer = (CameraVideoCapturer) videoCapturer;
-        if (cameraVideoCapturer != null) {
-            cameraVideoCapturer.switchCamera(new CameraVideoCapturer.CameraSwitchHandler() {
-                @Override
-                public void onCameraSwitchDone(boolean currentCameraIsFront) {
-                    binding.selfVideoRenderer.setMirror(currentCameraIsFront);
-                }
-
-                @Override
-                public void onCameraSwitchError(String s) {
-
-                }
-            });
-        }
-    }
-
-    private void toggleMedia(boolean enable, boolean video) {
-        String message;
-        if (video) {
-            message = "videoOff";
-            if (enable) {
-                binding.cameraButton.setAlpha(1.0f);
-                message = "videoOn";
-                startVideoCapture();
-            } else {
-                binding.cameraButton.setAlpha(0.7f);
-                if (videoCapturer != null) {
-                    try {
-                        videoCapturer.stopCapture();
-                    } catch (InterruptedException e) {
-                        Log.d(TAG, "Failed to stop capturing video while sensor is near the ear");
-                    }
-                }
-            }
-
-            if (localStream != null && localStream.videoTracks.size() > 0) {
-                localStream.videoTracks.get(0).setEnabled(enable);
-            }
-            if (enable) {
-                binding.selfVideoRenderer.setVisibility(View.VISIBLE);
-            } else {
-                binding.selfVideoRenderer.setVisibility(View.INVISIBLE);
-            }
-        } else {
-            message = "audioOff";
-            if (enable) {
-                message = "audioOn";
-                binding.microphoneButton.setAlpha(1.0f);
-            } else {
-                binding.microphoneButton.setAlpha(0.7f);
-            }
-
-            if (localStream != null && localStream.audioTracks.size() > 0) {
-                localStream.audioTracks.get(0).setEnabled(enable);
-            }
-        }
-
-        if (isConnectionEstablished() && peerConnectionWrapperList != null) {
-            if (!hasMCU) {
-                for (PeerConnectionWrapper peerConnectionWrapper : peerConnectionWrapperList) {
-                    peerConnectionWrapper.sendChannelData(new DataChannelMessage(message));
-                }
-            } else {
-                for (PeerConnectionWrapper peerConnectionWrapper : peerConnectionWrapperList) {
-                    if (peerConnectionWrapper.getSessionId().equals(webSocketClient.getSessionId())) {
-                        peerConnectionWrapper.sendChannelData(new DataChannelMessage(message));
-                        break;
-                    }
-                }
-            }
-        }
-    }
-
-    public void clickRaiseOrLowerHandButton() {
-        raiseHandViewModel.clickHandButton();
-    }
-
-
-    private void animateCallControls(boolean show, long startDelay) {
-        if (isVoiceOnlyCall) {
-            if (spotlightView != null && spotlightView.getVisibility() != View.GONE) {
-                spotlightView.setVisibility(View.GONE);
-            }
-        } else if (!isPushToTalkActive) {
-            float alpha;
-            long duration;
-
-            if (show) {
-                callControlHandler.removeCallbacksAndMessages(null);
-                callInfosHandler.removeCallbacksAndMessages(null);
-                cameraSwitchHandler.removeCallbacksAndMessages(null);
-                alpha = 1.0f;
-                duration = 1000;
-                if (binding.callControls.getVisibility() != View.VISIBLE) {
-                    binding.callControls.setAlpha(0.0f);
-                    binding.callControls.setVisibility(View.VISIBLE);
-
-                    binding.callInfosLinearLayout.setAlpha(0.0f);
-                    binding.callInfosLinearLayout.setVisibility(View.VISIBLE);
-
-                    binding.switchSelfVideoButton.setAlpha(0.0f);
-                    if (videoOn) {
-                        binding.switchSelfVideoButton.setVisibility(View.VISIBLE);
-                    }
-                } else {
-                    callControlHandler.postDelayed(() -> animateCallControls(false, 0), 5000);
-                    return;
-                }
-            } else {
-                alpha = 0.0f;
-                duration = 1000;
-            }
-
-            binding.callControls.setEnabled(false);
-            binding.callControls.animate()
-                .translationY(0)
-                .alpha(alpha)
-                .setDuration(duration)
-                .setStartDelay(startDelay)
-                .setListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        super.onAnimationEnd(animation);
-                        if (!show) {
-                            binding.callControls.setVisibility(View.GONE);
-                            if (spotlightView != null && spotlightView.getVisibility() != View.GONE) {
-                                spotlightView.setVisibility(View.GONE);
-                            }
-                        } else {
-                            callControlHandler.postDelayed(new Runnable() {
-                                @Override
-                                public void run() {
-                                    if (!isPushToTalkActive) {
-                                        animateCallControls(false, 0);
-                                    }
-                                }
-                            }, 7500);
-                        }
-
-                        binding.callControls.setEnabled(true);
-                    }
-                });
-
-            binding.callInfosLinearLayout.setEnabled(false);
-            binding.callInfosLinearLayout.animate()
-                .translationY(0)
-                .alpha(alpha)
-                .setDuration(duration)
-                .setStartDelay(startDelay)
-                .setListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        super.onAnimationEnd(animation);
-                        if (!show) {
-                            binding.callInfosLinearLayout.setVisibility(View.GONE);
-                        } else {
-                            callInfosHandler.postDelayed(new Runnable() {
-                                @Override
-                                public void run() {
-                                    if (!isPushToTalkActive) {
-                                        animateCallControls(false, 0);
-                                    }
-                                }
-                            }, 7500);
-                        }
-
-                        binding.callInfosLinearLayout.setEnabled(true);
-                    }
-                });
-
-            binding.switchSelfVideoButton.setEnabled(false);
-            binding.switchSelfVideoButton.animate()
-                .translationY(0)
-                .alpha(alpha)
-                .setDuration(duration)
-                .setStartDelay(startDelay)
-                .setListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        super.onAnimationEnd(animation);
-                        if (!show) {
-                            binding.switchSelfVideoButton.setVisibility(View.GONE);
-                        }
-
-                        binding.switchSelfVideoButton.setEnabled(true);
-                    }
-                });
-
-        }
-    }
-
-    @Override
-    public void onDestroy() {
-        if (signalingMessageReceiver != null) {
-            signalingMessageReceiver.removeListener(localParticipantMessageListener);
-            signalingMessageReceiver.removeListener(offerMessageListener);
-        }
-
-        if (localStream != null) {
-            localStream.dispose();
-            localStream = null;
-            Log.d(TAG, "Disposed localStream");
-        } else {
-            Log.d(TAG, "localStream is null");
-        }
-
-        if (currentCallStatus != CallStatus.LEAVING) {
-            hangup(true);
-        }
-        powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.IDLE);
-        super.onDestroy();
-    }
-
-    private void fetchSignalingSettings() {
-        Log.d(TAG, "fetchSignalingSettings");
-        int apiVersion = ApiUtils.getSignalingApiVersion(conversationUser, new int[]{ApiUtils.APIv3, 2, 1});
-
-        ncApi.getSignalingSettings(credentials, ApiUtils.getUrlForSignalingSettings(apiVersion, baseUrl))
-            .subscribeOn(Schedulers.io())
-            .retry(3)
-            .observeOn(AndroidSchedulers.mainThread())
-            .subscribe(new Observer<SignalingSettingsOverall>() {
-                @Override
-                public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
-                    // unused atm
-                }
-
-                @Override
-                public void onNext(@io.reactivex.annotations.NonNull SignalingSettingsOverall signalingSettingsOverall) {
-                    if (signalingSettingsOverall.getOcs() != null
-                        && signalingSettingsOverall.getOcs().getSettings() != null) {
-                        externalSignalingServer = new ExternalSignalingServer();
-
-                        if (!TextUtils.isEmpty(
-                            signalingSettingsOverall.getOcs().getSettings().getExternalSignalingServer()) &&
-                            !TextUtils.isEmpty(
-                                signalingSettingsOverall.getOcs().getSettings().getExternalSignalingTicket())) {
-                            externalSignalingServer = new ExternalSignalingServer();
-                            externalSignalingServer.setExternalSignalingServer(
-                                signalingSettingsOverall.getOcs().getSettings().getExternalSignalingServer());
-                            externalSignalingServer.setExternalSignalingTicket(
-                                signalingSettingsOverall.getOcs().getSettings().getExternalSignalingTicket());
-                            hasExternalSignalingServer = true;
-                        } else {
-                            hasExternalSignalingServer = false;
-                        }
-                        Log.d(TAG, "   hasExternalSignalingServer: " + hasExternalSignalingServer);
-
-                        if (!"?".equals(conversationUser.getUserId()) && conversationUser.getId() != null) {
-                            Log.d(TAG, "Update externalSignalingServer for: " + conversationUser.getId() +
-                                " / " + conversationUser.getUserId());
-                            userManager.updateExternalSignalingServer(conversationUser.getId(), externalSignalingServer)
-                                .subscribeOn(Schedulers.io())
-                                .subscribe();
-                        } else {
-                            conversationUser.setExternalSignalingServer(externalSignalingServer);
-                        }
-
-                        if (signalingSettingsOverall.getOcs().getSettings().getStunServers() != null) {
-                            List<IceServer> stunServers =
-                                signalingSettingsOverall.getOcs().getSettings().getStunServers();
-                            if (apiVersion == ApiUtils.APIv3) {
-                                for (IceServer stunServer : stunServers) {
-                                    if (stunServer.getUrls() != null) {
-                                        for (String url : stunServer.getUrls()) {
-                                            Log.d(TAG, "   STUN server url: " + url);
-                                            iceServers.add(new PeerConnection.IceServer(url));
-                                        }
-                                    }
-                                }
-                            } else {
-                                if (signalingSettingsOverall.getOcs().getSettings().getStunServers() != null) {
-                                    for (IceServer stunServer : stunServers) {
-                                        Log.d(TAG, "   STUN server url: " + stunServer.getUrl());
-                                        iceServers.add(new PeerConnection.IceServer(stunServer.getUrl()));
-                                    }
-                                }
-                            }
-                        }
-
-                        if (signalingSettingsOverall.getOcs().getSettings().getTurnServers() != null) {
-                            List<IceServer> turnServers =
-                                signalingSettingsOverall.getOcs().getSettings().getTurnServers();
-                            for (IceServer turnServer : turnServers) {
-                                if (turnServer.getUrls() != null) {
-                                    for (String url : turnServer.getUrls()) {
-                                        Log.d(TAG, "   TURN server url: " + url);
-                                        iceServers.add(new PeerConnection.IceServer(
-                                            url, turnServer.getUsername(), turnServer.getCredential()
-                                        ));
-                                    }
-                                }
-                            }
-                        }
-                    }
-
-                    checkCapabilities();
-                }
-
-                @Override
-                public void onError(@io.reactivex.annotations.NonNull Throwable e) {
-                    Log.e(TAG, e.getMessage(), e);
-                }
-
-                @Override
-                public void onComplete() {
-                    // unused atm
-                }
-            });
-    }
-
-    private void checkCapabilities() {
-        ncApi.getCapabilities(credentials, ApiUtils.getUrlForCapabilities(baseUrl))
-            .retry(3)
-            .subscribeOn(Schedulers.io())
-            .observeOn(AndroidSchedulers.mainThread())
-            .subscribe(new Observer<CapabilitiesOverall>() {
-                @Override
-                public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
-                    // unused atm
-                }
-
-                @Override
-                public void onNext(@io.reactivex.annotations.NonNull CapabilitiesOverall capabilitiesOverall) {
-                    // FIXME check for compatible Call API version
-                    if (hasExternalSignalingServer) {
-                        setupAndInitiateWebSocketsConnection();
-                    } else {
-                        signalingMessageReceiver = internalSignalingMessageReceiver;
-                        signalingMessageReceiver.addListener(localParticipantMessageListener);
-                        signalingMessageReceiver.addListener(offerMessageListener);
-                        signalingMessageSender = internalSignalingMessageSender;
-                        joinRoomAndCall();
-                    }
-                }
-
-                @Override
-                public void onError(@io.reactivex.annotations.NonNull Throwable e) {
-                    // unused atm
-                }
-
-                @Override
-                public void onComplete() {
-                    // unused atm
-                }
-            });
-    }
-
-    private void joinRoomAndCall() {
-        callSession = ApplicationWideCurrentRoomHolder.getInstance().getSession();
-
-        int apiVersion = ApiUtils.getConversationApiVersion(conversationUser, new int[]{ApiUtils.APIv4, 1});
-
-        Log.d(TAG, "joinRoomAndCall");
-        Log.d(TAG, "   baseUrl= " + baseUrl);
-        Log.d(TAG, "   roomToken= " + roomToken);
-        Log.d(TAG, "   callSession= " + callSession);
-
-        String url = ApiUtils.getUrlForParticipantsActive(apiVersion, baseUrl, roomToken);
-        Log.d(TAG, "   url= " + url);
-
-        if (TextUtils.isEmpty(callSession)) {
-            ncApi.joinRoom(credentials, url, conversationPassword)
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .retry(3)
-                .subscribe(new Observer<RoomOverall>() {
-                    @Override
-                    public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
-                        // unused atm
-                    }
-
-                    @Override
-                    public void onNext(@io.reactivex.annotations.NonNull RoomOverall roomOverall) {
-                        Conversation conversation = roomOverall.getOcs().getData();
-                        callRecordingViewModel.setRecordingState(conversation.getCallRecording());
-
-                        callSession = conversation.getSessionId();
-                        Log.d(TAG, " new callSession by joinRoom= " + callSession);
-
-                        ApplicationWideCurrentRoomHolder.getInstance().setSession(callSession);
-                        ApplicationWideCurrentRoomHolder.getInstance().setCurrentRoomId(conversation.getRoomId());
-                        ApplicationWideCurrentRoomHolder.getInstance().setCurrentRoomToken(roomToken);
-                        ApplicationWideCurrentRoomHolder.getInstance().setUserInRoom(conversationUser);
-                        callOrJoinRoomViaWebSocket();
-                    }
-
-                    @Override
-                    public void onError(@io.reactivex.annotations.NonNull Throwable e) {
-                        Log.e(TAG, "joinRoom onError", e);
-                    }
-
-                    @Override
-                    public void onComplete() {
-                        Log.d(TAG, "joinRoom onComplete");
-                    }
-                });
-        } else {
-            // we are in a room and start a call -> same session needs to be used
-            callOrJoinRoomViaWebSocket();
-        }
-    }
-
-    private void callOrJoinRoomViaWebSocket() {
-        if (hasExternalSignalingServer) {
-            webSocketClient.joinRoomWithRoomTokenAndSession(roomToken, callSession);
-        } else {
-            performCall();
-        }
-    }
-
-    private void performCall() {
-        int inCallFlag = Participant.InCallFlags.IN_CALL;
-
-        if (canPublishAudioStream) {
-            inCallFlag += Participant.InCallFlags.WITH_AUDIO;
-        }
-
-        if (!isVoiceOnlyCall && canPublishVideoStream) {
-            inCallFlag += Participant.InCallFlags.WITH_VIDEO;
-        }
-
-        callParticipantList = new CallParticipantList(signalingMessageReceiver);
-        callParticipantList.addObserver(callParticipantListObserver);
-
-        int apiVersion = ApiUtils.getCallApiVersion(conversationUser, new int[]{ApiUtils.APIv4, 1});
-
-        ncApi.joinCall(
-                credentials,
-                ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken),
-                inCallFlag,
-                isCallWithoutNotification)
-            .subscribeOn(Schedulers.io())
-            .retry(3)
-            .observeOn(AndroidSchedulers.mainThread())
-            .subscribe(new Observer<GenericOverall>() {
-                @Override
-                public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
-                    // unused atm
-                }
-
-                @Override
-                public void onNext(@io.reactivex.annotations.NonNull GenericOverall genericOverall) {
-                    if (currentCallStatus != CallStatus.LEAVING) {
-                        if (currentCallStatus != CallStatus.IN_CONVERSATION) {
-                            setCallState(CallStatus.JOINED);
-                        }
-
-                        ApplicationWideCurrentRoomHolder.getInstance().setInCall(true);
-                        ApplicationWideCurrentRoomHolder.getInstance().setDialing(false);
-
-                        if (!TextUtils.isEmpty(roomToken)) {
-                            NotificationUtils.INSTANCE.cancelExistingNotificationsForRoom(getApplicationContext(),
-                                                                                          conversationUser,
-                                                                                          roomToken);
-                        }
-
-                        if (!hasExternalSignalingServer) {
-                            int apiVersion = ApiUtils.getSignalingApiVersion(conversationUser,
-                                                                             new int[]{ApiUtils.APIv3, 2, 1});
-
-                            AtomicInteger delayOnError = new AtomicInteger(0);
-
-                            ncApi.pullSignalingMessages(credentials,
-                                                        ApiUtils.getUrlForSignaling(apiVersion,
-                                                                                    baseUrl,
-                                                                                    roomToken))
-                                .subscribeOn(Schedulers.io())
-                                .observeOn(AndroidSchedulers.mainThread())
-                                .repeatWhen(observable -> observable)
-                                .takeWhile(observable -> isConnectionEstablished())
-                                .doOnNext(value -> delayOnError.set(0))
-                                .retryWhen(errors -> errors
-                                               .flatMap(error -> {
-                                                   if (!isConnectionEstablished()) {
-                                                       return Observable.error(error);
-                                                   }
-
-                                                   if (delayOnError.get() == 0) {
-                                                       delayOnError.set(1);
-                                                   } else if (delayOnError.get() < 16) {
-                                                       delayOnError.set(delayOnError.get() * 2);
-                                                   }
-
-                                                   return Observable.timer(delayOnError.get(), TimeUnit.SECONDS);
-                                               })
-                                          )
-                                .subscribe(new Observer<SignalingOverall>() {
-                                    @Override
-                                    public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
-                                        signalingDisposable = d;
-                                    }
-
-                                    @Override
-                                    public void onNext(
-                                        @io.reactivex.annotations.NonNull
-                                        SignalingOverall signalingOverall) {
-                                        receivedSignalingMessages(signalingOverall.getOcs().getSignalings());
-                                    }
-
-                                    @Override
-                                    public void onError(@io.reactivex.annotations.NonNull Throwable e) {
-                                        dispose(signalingDisposable);
-                                    }
-
-                                    @Override
-                                    public void onComplete() {
-                                        dispose(signalingDisposable);
-                                    }
-                                });
-                        }
-                    }
-                }
-
-                @Override
-                public void onError(@io.reactivex.annotations.NonNull Throwable e) {
-                    // unused atm
-                }
-
-                @Override
-                public void onComplete() {
-                    // unused atm
-                }
-            });
-    }
-
-    private void setupAndInitiateWebSocketsConnection() {
-        if (webSocketConnectionHelper == null) {
-            webSocketConnectionHelper = new WebSocketConnectionHelper();
-        }
-
-        if (webSocketClient == null) {
-            webSocketClient = WebSocketConnectionHelper.getExternalSignalingInstanceForServer(
-                externalSignalingServer.getExternalSignalingServer(),
-                conversationUser, externalSignalingServer.getExternalSignalingTicket(),
-                TextUtils.isEmpty(credentials));
-            // Although setupAndInitiateWebSocketsConnection could be called several times the web socket is
-            // initialized just once, so the message receiver is also initialized just once.
-            signalingMessageReceiver = webSocketClient.getSignalingMessageReceiver();
-            signalingMessageReceiver.addListener(localParticipantMessageListener);
-            signalingMessageReceiver.addListener(offerMessageListener);
-            signalingMessageSender = webSocketClient.getSignalingMessageSender();
-        } else {
-            if (webSocketClient.isConnected() && currentCallStatus == CallStatus.PUBLISHER_FAILED) {
-                webSocketClient.restartWebSocket();
-            }
-        }
-
-        joinRoomAndCall();
-    }
-
-    private void initiateCall() {
-        if (!TextUtils.isEmpty(roomToken)) {
-            checkDevicePermissions();
-        } else {
-            handleFromNotification();
-        }
-    }
-
-    @Subscribe(threadMode = ThreadMode.BACKGROUND)
-    public void onMessageEvent(WebSocketCommunicationEvent webSocketCommunicationEvent) {
-        if (currentCallStatus == CallStatus.LEAVING) {
-            return;
-        }
-
-        if (webSocketCommunicationEvent.getHashMap() != null) {
-            switch (webSocketCommunicationEvent.getType()) {
-                case "hello":
-                    Log.d(TAG, "onMessageEvent 'hello'");
-                    if (!webSocketCommunicationEvent.getHashMap().containsKey("oldResumeId")) {
-                        if (currentCallStatus == CallStatus.RECONNECTING) {
-                            hangup(false);
-                        } else {
-                            setCallState(CallStatus.RECONNECTING);
-                            runOnUiThread(this::initiateCall);
-                        }
-                    }
-                    break;
-                case "roomJoined":
-                    Log.d(TAG, "onMessageEvent 'roomJoined'");
-                    startSendingNick();
-
-                    if (webSocketCommunicationEvent.getHashMap().get("roomToken").equals(roomToken)) {
-                        performCall();
-                    }
-                    break;
-                case "recordingStatus":
-                    Log.d(TAG, "onMessageEvent 'recordingStatus'");
-
-                    if (webSocketCommunicationEvent.getHashMap().containsKey(KEY_RECORDING_STATE)) {
-                        String recordingStateString =
-                            webSocketCommunicationEvent.getHashMap().get(KEY_RECORDING_STATE);
-
-                        if (recordingStateString != null) {
-                            runOnUiThread(() -> {
-                                callRecordingViewModel.setRecordingState(Integer.parseInt(recordingStateString));
-                            });
-                        }
-                    }
-                    break;
-            }
-        }
-    }
-
-    private void dispose(@Nullable Disposable disposable) {
-        if (disposable != null && !disposable.isDisposed()) {
-            disposable.dispose();
-        } else if (disposable == null) {
-            if (signalingDisposable != null && !signalingDisposable.isDisposed()) {
-                signalingDisposable.dispose();
-                signalingDisposable = null;
-            }
-        }
-    }
-
-    private void receivedSignalingMessages(@Nullable List<Signaling> signalingList) {
-        if (signalingList != null) {
-            for (Signaling signaling : signalingList) {
-                try {
-                    receivedSignalingMessage(signaling);
-                } catch (IOException e) {
-                    Log.e(TAG, "Failed to process received signaling message", e);
-                }
-            }
-        }
-    }
-
-    private void receivedSignalingMessage(Signaling signaling) throws IOException {
-        String messageType = signaling.getType();
-
-        if (!isConnectionEstablished() && currentCallStatus != CallStatus.CONNECTING) {
-            return;
-        }
-
-        if ("usersInRoom".equals(messageType)) {
-            internalSignalingMessageReceiver.process((List<Map<String, Object>>) signaling.getMessageWrapper());
-        } else if ("message".equals(messageType)) {
-            NCSignalingMessage ncSignalingMessage = LoganSquare.parse(signaling.getMessageWrapper().toString(),
-                                                                      NCSignalingMessage.class);
-            internalSignalingMessageReceiver.process(ncSignalingMessage);
-        } else {
-            Log.e(TAG, "unexpected message type when receiving signaling message");
-        }
-    }
-
-    private void hangup(boolean shutDownView) {
-        Log.d(TAG, "hangup! shutDownView=" + shutDownView);
-        if (shutDownView) {
-            setCallState(CallStatus.LEAVING);
-        }
-        stopCallingSound();
-        dispose(null);
-
-        if (shutDownView) {
-
-            if (videoCapturer != null) {
-                try {
-                    videoCapturer.stopCapture();
-                } catch (InterruptedException e) {
-                    Log.e(TAG, "Failed to stop capturing while hanging up");
-                }
-                videoCapturer.dispose();
-                videoCapturer = null;
-            }
-
-            binding.selfVideoRenderer.release();
-
-            if (audioSource != null) {
-                audioSource.dispose();
-                audioSource = null;
-            }
-
-            runOnUiThread(() -> {
-                if (audioManager != null) {
-                    audioManager.stop();
-                    audioManager = null;
-                }
-            });
-
-            if (videoSource != null) {
-                videoSource = null;
-            }
-
-            if (peerConnectionFactory != null) {
-                peerConnectionFactory = null;
-            }
-
-
-            localAudioTrack = null;
-            localVideoTrack = null;
-
-
-            if (TextUtils.isEmpty(credentials) && hasExternalSignalingServer) {
-                WebSocketConnectionHelper.deleteExternalSignalingInstanceForUserEntity(-1);
-            }
-        }
-
-        List<String> peerConnectionIdsToEnd = new ArrayList<String>(peerConnectionWrapperList.size());
-        for (PeerConnectionWrapper wrapper : peerConnectionWrapperList) {
-            peerConnectionIdsToEnd.add(wrapper.getSessionId());
-        }
-        for (String sessionId : peerConnectionIdsToEnd) {
-            endPeerConnection(sessionId, "video");
-            endPeerConnection(sessionId, "screen");
-        }
-
-        List<String> callParticipantIdsToEnd = new ArrayList<String>(peerConnectionWrapperList.size());
-        for (CallParticipant callParticipant : callParticipants.values()) {
-            callParticipantIdsToEnd.add(callParticipant.getCallParticipantModel().getSessionId());
-        }
-        for (String sessionId : callParticipantIdsToEnd) {
-            removeCallParticipant(sessionId);
-        }
-
-        ApplicationWideCurrentRoomHolder.getInstance().setInCall(false);
-        ApplicationWideCurrentRoomHolder.getInstance().setDialing(false);
-        hangupNetworkCalls(shutDownView);
-    }
-
-    private void hangupNetworkCalls(boolean shutDownView) {
-        Log.d(TAG, "hangupNetworkCalls. shutDownView=" + shutDownView);
-        int apiVersion = ApiUtils.getCallApiVersion(conversationUser, new int[]{ApiUtils.APIv4, 1});
-
-        if (callParticipantList != null) {
-            callParticipantList.removeObserver(callParticipantListObserver);
-            callParticipantList.destroy();
-        }
-
-        ncApi.leaveCall(credentials, ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken))
-            .subscribeOn(Schedulers.io())
-            .observeOn(AndroidSchedulers.mainThread())
-            .subscribe(new Observer<GenericOverall>() {
-                @Override
-                public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
-                    // unused atm
-                }
-
-                @Override
-                public void onNext(@io.reactivex.annotations.NonNull GenericOverall genericOverall) {
-                    if (!switchToRoomToken.isEmpty()) {
-                        Intent intent = new Intent(context, ChatActivity.class);
-                        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-
-                        Bundle bundle = new Bundle();
-                        bundle.putBoolean(KEY_SWITCH_TO_ROOM, true);
-                        bundle.putBoolean(KEY_START_CALL_AFTER_ROOM_SWITCH, true);
-                        bundle.putString(KEY_ROOM_TOKEN, switchToRoomToken);
-                        bundle.putBoolean(KEY_CALL_VOICE_ONLY, isVoiceOnlyCall);
-                        intent.putExtras(bundle);
-                        startActivity(intent);
-                        finish();
-                    } else if (shutDownView) {
-                        finish();
-                    } else if (currentCallStatus == CallStatus.RECONNECTING
-                        || currentCallStatus == CallStatus.PUBLISHER_FAILED) {
-                        initiateCall();
-                    }
-                }
-
-                @Override
-                public void onError(@io.reactivex.annotations.NonNull Throwable e) {
-                    Log.e(TAG, "Error while leaving the call", e);
-                }
-
-                @Override
-                public void onComplete() {
-                    // unused atm
-                }
-            });
-    }
-
-    private void startVideoCapture() {
-        if (videoCapturer != null) {
-            videoCapturer.startCapture(1280, 720, 30);
-        }
-    }
-
-    private void handleCallParticipantsChanged(Collection<Participant> joined, Collection<Participant> updated,
-                                               Collection<Participant> left, Collection<Participant> unchanged) {
-        Log.d(TAG, "handleCallParticipantsChanged");
-
-        hasMCU = hasExternalSignalingServer && webSocketClient != null && webSocketClient.hasMCU();
-        Log.d(TAG, "   hasMCU is " + hasMCU);
-
-        // The signaling session is the same as the Nextcloud session only when the MCU is not used.
-        String currentSessionId = callSession;
-        if (hasMCU) {
-            currentSessionId = webSocketClient.getSessionId();
-        }
-
-        Log.d(TAG, "   currentSessionId is " + currentSessionId);
-
-        List<Participant> participantsInCall = new ArrayList<>();
-        participantsInCall.addAll(joined);
-        participantsInCall.addAll(updated);
-        participantsInCall.addAll(unchanged);
-
-        boolean isSelfInCall = false;
-        Participant selfParticipant = null;
-
-        for (Participant participant : participantsInCall) {
-            long inCallFlag = participant.getInCall();
-            if (!participant.getSessionId().equals(currentSessionId)) {
-                Log.d(TAG, "   inCallFlag of participant "
-                    + participant.getSessionId().substring(0, 4)
-                    + " : "
-                    + inCallFlag);
-            } else {
-                Log.d(TAG, "   inCallFlag of currentSessionId: " + inCallFlag);
-                isSelfInCall = inCallFlag != 0;
-                selfParticipant = participant;
-            }
-        }
-
-        if (!isSelfInCall && currentCallStatus != CallStatus.LEAVING && ApplicationWideCurrentRoomHolder.getInstance().isInCall()) {
-            Log.d(TAG, "Most probably a moderator ended the call for all.");
-            hangup(true);
-
-            return;
-        }
-
-        if (!isSelfInCall) {
-            Log.d(TAG, "Self not in call, disconnecting from all other sessions");
-
-            for (Participant participant : participantsInCall) {
-                String sessionId = participant.getSessionId();
-                Log.d(TAG, "   session that will be removed is: " + sessionId);
-                endPeerConnection(sessionId, "video");
-                endPeerConnection(sessionId, "screen");
-                removeCallParticipant(sessionId);
-            }
-
-            return;
-        }
-
-        if (currentCallStatus == CallStatus.LEAVING) {
-            return;
-        }
-
-        if (hasMCU) {
-            // Ensure that own publishing peer is set up.
-            getOrCreatePeerConnectionWrapperForSessionIdAndType(webSocketClient.getSessionId(), VIDEO_STREAM_TYPE_VIDEO, true);
-        }
-
-        boolean selfJoined = false;
-        boolean selfParticipantHasAudioOrVideo = participantInCallFlagsHaveAudioOrVideo(selfParticipant);
-
-        for (Participant participant : joined) {
-            String sessionId = participant.getSessionId();
-
-            if (sessionId == null) {
-                Log.w(TAG, "Null sessionId for call participant, this should not happen: " + participant);
-                continue;
-            }
-
-            if (sessionId.equals(currentSessionId)) {
-                selfJoined = true;
-                continue;
-            }
-
-            Log.d(TAG, "   newSession joined: " + sessionId);
-
-            CallParticipant callParticipant = addCallParticipant(sessionId);
-
-            String userId = participant.getUserId();
-            if (userId != null) {
-                callParticipants.get(sessionId).setUserId(userId);
-            }
-
-            if (participant.getInternal() != null) {
-                callParticipants.get(sessionId).setInternal(participant.getInternal());
-            }
-
-            String nick;
-            if (hasExternalSignalingServer) {
-                nick = webSocketClient.getDisplayNameForSession(sessionId);
-            } else {
-                nick = offerAnswerNickProviders.get(sessionId) != null ? offerAnswerNickProviders.get(sessionId).getNick() : "";
-            }
-            callParticipants.get(sessionId).setNick(nick);
-
-            boolean participantHasAudioOrVideo = participantInCallFlagsHaveAudioOrVideo(participant);
-
-            // FIXME Without MCU, PeerConnectionWrapper only sends an offer if the local session ID is higher than the
-            // remote session ID. However, if the other participant does not have audio nor video that participant
-            // will not send an offer, so no connection is actually established when the remote participant has a
-            // higher session ID but is not publishing media.
-            if ((hasMCU && participantHasAudioOrVideo) ||
-                (!hasMCU && selfParticipantHasAudioOrVideo && (!participantHasAudioOrVideo || sessionId.compareTo(currentSessionId) < 0))) {
-                getOrCreatePeerConnectionWrapperForSessionIdAndType(sessionId, VIDEO_STREAM_TYPE_VIDEO, false);
-            }
-        }
-
-        boolean othersInCall = selfJoined ? joined.size() > 1 : joined.size() > 0;
-        if (othersInCall && currentCallStatus != CallStatus.IN_CONVERSATION) {
-            setCallState(CallStatus.IN_CONVERSATION);
-        }
-
-        for (Participant participant : left) {
-            String sessionId = participant.getSessionId();
-            Log.d(TAG, "   oldSession that will be removed is: " + sessionId);
-            endPeerConnection(sessionId, "video");
-            endPeerConnection(sessionId, "screen");
-            removeCallParticipant(sessionId);
-        }
-    }
-
-    private boolean participantInCallFlagsHaveAudioOrVideo(Participant participant) {
-        if (participant == null) {
-            return false;
-        }
-
-        return (participant.getInCall() & Participant.InCallFlags.WITH_AUDIO) > 0 ||
-            (!isVoiceOnlyCall && (participant.getInCall() & Participant.InCallFlags.WITH_VIDEO) > 0);
-    }
-
-    private PeerConnectionWrapper getPeerConnectionWrapperForSessionIdAndType(String sessionId, String type) {
-        for (PeerConnectionWrapper wrapper : peerConnectionWrapperList) {
-            if (wrapper.getSessionId().equals(sessionId)
-                && wrapper.getVideoStreamType().equals(type)) {
-                return wrapper;
-            }
-        }
-
-        return null;
-    }
-
-    private PeerConnectionWrapper getOrCreatePeerConnectionWrapperForSessionIdAndType(String sessionId,
-                                                                                      String type,
-                                                                                      boolean publisher) {
-        PeerConnectionWrapper peerConnectionWrapper;
-        if ((peerConnectionWrapper = getPeerConnectionWrapperForSessionIdAndType(sessionId, type)) != null) {
-            return peerConnectionWrapper;
-        } else {
-            if (peerConnectionFactory == null) {
-                Log.e(TAG, "peerConnectionFactory was null in getOrCreatePeerConnectionWrapperForSessionIdAndType.");
-                Toast.makeText(context, context.getResources().getString(R.string.nc_common_error_sorry),
-                               Toast.LENGTH_LONG).show();
-                hangup(true);
-                return null;
-            }
-
-            if (hasMCU && publisher) {
-                peerConnectionWrapper = new PeerConnectionWrapper(peerConnectionFactory,
-                                                                  iceServers,
-                                                                  sdpConstraintsForMCU,
-                                                                  sessionId,
-                                                                  callSession,
-                                                                  localStream,
-                                                                  true,
-                                                                  true,
-                                                                  type,
-                                                                  signalingMessageReceiver,
-                                                                  signalingMessageSender);
-
-            } else if (hasMCU) {
-                peerConnectionWrapper = new PeerConnectionWrapper(peerConnectionFactory,
-                                                                  iceServers,
-                                                                  sdpConstraints,
-                                                                  sessionId,
-                                                                  callSession,
-                                                                  null,
-                                                                  false,
-                                                                  true,
-                                                                  type,
-                                                                  signalingMessageReceiver,
-                                                                  signalingMessageSender);
-            } else {
-                if (!"screen".equals(type)) {
-                    peerConnectionWrapper = new PeerConnectionWrapper(peerConnectionFactory,
-                                                                      iceServers,
-                                                                      sdpConstraints,
-                                                                      sessionId,
-                                                                      callSession,
-                                                                      localStream,
-                                                                      false,
-                                                                      false,
-                                                                      type,
-                                                                      signalingMessageReceiver,
-                                                                      signalingMessageSender);
-                } else {
-                    peerConnectionWrapper = new PeerConnectionWrapper(peerConnectionFactory,
-                                                                      iceServers,
-                                                                      sdpConstraints,
-                                                                      sessionId,
-                                                                      callSession,
-                                                                      null,
-                                                                      false,
-                                                                      false,
-                                                                      type,
-                                                                      signalingMessageReceiver,
-                                                                      signalingMessageSender);
-                }
-            }
-
-            peerConnectionWrapperList.add(peerConnectionWrapper);
-
-            if (!publisher) {
-                CallParticipant callParticipant = callParticipants.get(sessionId);
-                if (callParticipant == null) {
-                    callParticipant = addCallParticipant(sessionId);
-                }
-
-                if ("screen".equals(type)) {
-                    callParticipant.setScreenPeerConnectionWrapper(peerConnectionWrapper);
-                } else {
-                    callParticipant.setPeerConnectionWrapper(peerConnectionWrapper);
-                }
-            }
-
-            if (publisher) {
-                peerConnectionWrapper.addObserver(selfPeerConnectionObserver);
-
-                startSendingNick();
-            }
-
-            return peerConnectionWrapper;
-        }
-    }
-
-    private CallParticipant addCallParticipant(String sessionId) {
-        CallParticipant callParticipant = new CallParticipant(sessionId, signalingMessageReceiver);
-        callParticipants.put(sessionId, callParticipant);
-
-        SignalingMessageReceiver.CallParticipantMessageListener callParticipantMessageListener =
-            new CallActivityCallParticipantMessageListener(sessionId);
-        callParticipantMessageListeners.put(sessionId, callParticipantMessageListener);
-        signalingMessageReceiver.addListener(callParticipantMessageListener, sessionId);
-
-        if (!hasExternalSignalingServer) {
-            OfferAnswerNickProvider offerAnswerNickProvider = new OfferAnswerNickProvider(sessionId);
-            offerAnswerNickProviders.put(sessionId, offerAnswerNickProvider);
-            signalingMessageReceiver.addListener(offerAnswerNickProvider.getVideoWebRtcMessageListener(), sessionId, "video");
-            signalingMessageReceiver.addListener(offerAnswerNickProvider.getScreenWebRtcMessageListener(), sessionId, "screen");
-        }
-
-        final CallParticipantModel callParticipantModel = callParticipant.getCallParticipantModel();
-
-        ScreenParticipantDisplayItemManager screenParticipantDisplayItemManager =
-            new ScreenParticipantDisplayItemManager(callParticipantModel);
-        screenParticipantDisplayItemManagers.put(sessionId, screenParticipantDisplayItemManager);
-        callParticipantModel.addObserver(screenParticipantDisplayItemManager, screenParticipantDisplayItemManagersHandler);
-
-        CallParticipantEventDisplayer callParticipantEventDisplayer =
-            new CallParticipantEventDisplayer(callParticipantModel);
-        callParticipantEventDisplayers.put(sessionId, callParticipantEventDisplayer);
-        callParticipantModel.addObserver(callParticipantEventDisplayer, callParticipantEventDisplayersHandler);
-
-        runOnUiThread(() -> {
-            addParticipantDisplayItem(callParticipantModel, "video");
-        });
-
-        return callParticipant;
-    }
-
-    private void endPeerConnection(String sessionId, String type) {
-        PeerConnectionWrapper peerConnectionWrapper = getPeerConnectionWrapperForSessionIdAndType(sessionId, type);
-        if (peerConnectionWrapper == null) {
-            return;
-        }
-
-        if (webSocketClient != null && webSocketClient.getSessionId() != null && webSocketClient.getSessionId().equals(sessionId)) {
-            peerConnectionWrapper.removeObserver(selfPeerConnectionObserver);
-        }
-
-        CallParticipant callParticipant = callParticipants.get(sessionId);
-        if (callParticipant != null) {
-            if ("screen".equals(type)) {
-                callParticipant.setScreenPeerConnectionWrapper(null);
-            } else {
-                callParticipant.setPeerConnectionWrapper(null);
-            }
-        }
-
-        peerConnectionWrapper.removePeerConnection();
-        peerConnectionWrapperList.remove(peerConnectionWrapper);
-    }
-
-    private void removeCallParticipant(String sessionId) {
-        CallParticipant callParticipant = callParticipants.remove(sessionId);
-        if (callParticipant == null) {
-            return;
-        }
-
-        ScreenParticipantDisplayItemManager screenParticipantDisplayItemManager =
-            screenParticipantDisplayItemManagers.remove(sessionId);
-        callParticipant.getCallParticipantModel().removeObserver(screenParticipantDisplayItemManager);
-
-        CallParticipantEventDisplayer callParticipantEventDisplayer =
-            callParticipantEventDisplayers.remove(sessionId);
-        callParticipant.getCallParticipantModel().removeObserver(callParticipantEventDisplayer);
-
-        callParticipant.destroy();
-
-        SignalingMessageReceiver.CallParticipantMessageListener listener = callParticipantMessageListeners.remove(sessionId);
-        signalingMessageReceiver.removeListener(listener);
-
-        OfferAnswerNickProvider offerAnswerNickProvider = offerAnswerNickProviders.remove(sessionId);
-        if (offerAnswerNickProvider != null) {
-            signalingMessageReceiver.removeListener(offerAnswerNickProvider.getVideoWebRtcMessageListener());
-            signalingMessageReceiver.removeListener(offerAnswerNickProvider.getScreenWebRtcMessageListener());
-        }
-
-        runOnUiThread(() -> removeParticipantDisplayItem(sessionId, "video"));
-    }
-
-    private void removeParticipantDisplayItem(String sessionId, String videoStreamType) {
-        Log.d(TAG, "removeParticipantDisplayItem");
-        ParticipantDisplayItem participantDisplayItem = participantDisplayItems.remove(sessionId + "-" + videoStreamType);
-        if (participantDisplayItem == null) {
-            return;
-        }
-
-        participantDisplayItem.destroy();
-
-        if (!isDestroyed()) {
-            initGridAdapter();
-        }
-    }
-
-    @Subscribe(threadMode = ThreadMode.MAIN)
-    public void onMessageEvent(ConfigurationChangeEvent configurationChangeEvent) {
-        powerManagerUtils.setOrientation(Objects.requireNonNull(getResources()).getConfiguration().orientation);
-        initGridAdapter();
-        updateSelfVideoViewPosition();
-    }
-
-    private void updateSelfVideoViewIceConnectionState(PeerConnection.IceConnectionState iceConnectionState) {
-        boolean connected = iceConnectionState == PeerConnection.IceConnectionState.CONNECTED ||
-            iceConnectionState == PeerConnection.IceConnectionState.COMPLETED;
-
-        // FIXME In voice only calls there is no video view, so the progress bar would appear floating in the middle of
-        // nowhere. However, a way to signal that the local participant is not connected to the HPB is still need in
-        // that case.
-        if (!connected && !isVoiceOnlyCall) {
-            binding.selfVideoViewProgressBar.setVisibility(View.VISIBLE);
-        } else {
-            binding.selfVideoViewProgressBar.setVisibility(View.GONE);
-        }
-    }
-
-    private void updateSelfVideoViewPosition() {
-        Log.d(TAG, "updateSelfVideoViewPosition");
-        if (!isInPipMode) {
-            FrameLayout.LayoutParams layoutParams =
-                (FrameLayout.LayoutParams) binding.selfVideoRenderer.getLayoutParams();
-
-            DisplayMetrics displayMetrics = getApplicationContext().getResources().getDisplayMetrics();
-            int screenWidthPx = displayMetrics.widthPixels;
-
-            int screenWidthDp = (int) DisplayUtils.convertPixelToDp(screenWidthPx, getApplicationContext());
-
-            float newXafterRotate = 0;
-            float newYafterRotate;
-            if (binding.callInfosLinearLayout.getVisibility() == View.VISIBLE) {
-                newYafterRotate = 250;
-            } else {
-                newYafterRotate = 20;
-            }
-
-            if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
-                layoutParams.height = (int) getResources().getDimension(R.dimen.call_self_video_short_side_length);
-                layoutParams.width = (int) getResources().getDimension(R.dimen.call_self_video_long_side_length);
-                newXafterRotate = (float) (screenWidthDp - getResources().getDimension(R.dimen.call_self_video_short_side_length) * 0.8);
-
-            } else if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
-                layoutParams.height = (int) getResources().getDimension(R.dimen.call_self_video_long_side_length);
-                layoutParams.width = (int) getResources().getDimension(R.dimen.call_self_video_short_side_length);
-                newXafterRotate = (float) (screenWidthDp - getResources().getDimension(R.dimen.call_self_video_short_side_length) * 0.5);
-            }
-            binding.selfVideoRenderer.setLayoutParams(layoutParams);
-
-            int newXafterRotatePx = (int) DisplayUtils.convertDpToPixel(newXafterRotate, getApplicationContext());
-            binding.selfVideoViewWrapper.setY(newYafterRotate);
-            binding.selfVideoViewWrapper.setX(newXafterRotatePx);
-        }
-    }
-
-    @Subscribe(threadMode = ThreadMode.MAIN)
-    public void onMessageEvent(ProximitySensorEvent proximitySensorEvent) {
-        if (!isVoiceOnlyCall) {
-            boolean enableVideo = proximitySensorEvent.getProximitySensorEventType() ==
-                ProximitySensorEvent.ProximitySensorEventType.SENSOR_FAR && videoOn;
-            if (permissionUtil.isCameraPermissionGranted() &&
-                (currentCallStatus == CallStatus.CONNECTING || isConnectionEstablished()) && videoOn
-                && enableVideo != localVideoTrack.enabled()) {
-                toggleMedia(enableVideo, true);
-            }
-        }
-    }
-
-    private void startSendingNick() {
-        DataChannelMessage dataChannelMessage = new DataChannelMessage();
-        dataChannelMessage.setType("nickChanged");
-        Map<String, String> nickChangedPayload = new HashMap<>();
-        nickChangedPayload.put("userid", conversationUser.getUserId());
-        nickChangedPayload.put("name", conversationUser.getDisplayName());
-        dataChannelMessage.setPayloadMap(nickChangedPayload);
-        for (PeerConnectionWrapper peerConnectionWrapper : peerConnectionWrapperList) {
-            if (peerConnectionWrapper.isMCUPublisher()) {
-                Observable
-                    .interval(1, TimeUnit.SECONDS)
-                    .repeatUntil(() -> (!isConnectionEstablished() || isDestroyed()))
-                    .observeOn(Schedulers.io())
-                    .subscribe(new Observer<Long>() {
-                        @Override
-                        public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
-                            // unused atm
-                        }
-
-                        @Override
-                        public void onNext(@io.reactivex.annotations.NonNull Long aLong) {
-                            peerConnectionWrapper.sendChannelData(dataChannelMessage);
-                        }
-
-                        @Override
-                        public void onError(@io.reactivex.annotations.NonNull Throwable e) {
-                            // unused atm
-                        }
-
-                        @Override
-                        public void onComplete() {
-                            // unused atm
-                        }
-                    });
-                break;
-            }
-
-        }
-    }
-
-    private void addParticipantDisplayItem(CallParticipantModel callParticipantModel, String videoStreamType) {
-        if (callParticipantModel.isInternal() != null && callParticipantModel.isInternal()) {
-            return;
-        }
-
-        String defaultGuestNick = getResources().getString(R.string.nc_nick_guest);
-
-        ParticipantDisplayItem participantDisplayItem = new ParticipantDisplayItem(baseUrl,
-                                                                                   defaultGuestNick,
-                                                                                   rootEglBase,
-                                                                                   videoStreamType,
-                                                                                   callParticipantModel);
-        String sessionId = callParticipantModel.getSessionId();
-        participantDisplayItems.put(sessionId + "-" + videoStreamType, participantDisplayItem);
-
-        initGridAdapter();
-    }
-
-    private void setCallState(CallStatus callState) {
-        if (currentCallStatus == null || currentCallStatus != callState) {
-            currentCallStatus = callState;
-            if (handler == null) {
-                handler = new Handler(Looper.getMainLooper());
-            } else {
-                handler.removeCallbacksAndMessages(null);
-            }
-
-            switch (callState) {
-                case CONNECTING:
-                    handler.post(() -> {
-                        playCallingSound();
-                        if (isIncomingCallFromNotification) {
-                            binding.callStates.callStateTextView.setText(R.string.nc_call_incoming);
-                        } else {
-                            binding.callStates.callStateTextView.setText(R.string.nc_call_ringing);
-                        }
-                        binding.callConversationNameTextView.setText(conversationName);
-
-                        binding.callModeTextView.setText(getDescriptionForCallType());
-
-                        if (binding.callStates.callStateRelativeLayout.getVisibility() != View.VISIBLE) {
-                            binding.callStates.callStateRelativeLayout.setVisibility(View.VISIBLE);
-                        }
-
-                        if (binding.gridview.getVisibility() != View.INVISIBLE) {
-                            binding.gridview.setVisibility(View.INVISIBLE);
-                        }
-
-                        if (binding.callStates.callStateProgressBar.getVisibility() != View.VISIBLE) {
-                            binding.callStates.callStateProgressBar.setVisibility(View.VISIBLE);
-                        }
-
-                        if (binding.callStates.errorImageView.getVisibility() != View.GONE) {
-                            binding.callStates.errorImageView.setVisibility(View.GONE);
-                        }
-                    });
-                    break;
-                case CALLING_TIMEOUT:
-                    handler.post(() -> {
-                        hangup(false);
-                        binding.callStates.callStateTextView.setText(R.string.nc_call_timeout);
-                        binding.callModeTextView.setText(getDescriptionForCallType());
-                        if (binding.callStates.callStateRelativeLayout.getVisibility() != View.VISIBLE) {
-                            binding.callStates.callStateRelativeLayout.setVisibility(View.VISIBLE);
-                        }
-
-                        if (binding.callStates.callStateProgressBar.getVisibility() != View.GONE) {
-                            binding.callStates.callStateProgressBar.setVisibility(View.GONE);
-                        }
-
-                        if (binding.gridview.getVisibility() != View.INVISIBLE) {
-                            binding.gridview.setVisibility(View.INVISIBLE);
-                        }
-
-                        binding.callStates.errorImageView.setImageResource(R.drawable.ic_av_timer_timer_24dp);
-
-                        if (binding.callStates.errorImageView.getVisibility() != View.VISIBLE) {
-                            binding.callStates.errorImageView.setVisibility(View.VISIBLE);
-                        }
-                    });
-                    break;
-                case PUBLISHER_FAILED:
-                    handler.post(() -> {
-                        // No calling sound when the publisher failed
-                        binding.callStates.callStateTextView.setText(R.string.nc_call_reconnecting);
-                        binding.callModeTextView.setText(getDescriptionForCallType());
-                        if (binding.callStates.callStateRelativeLayout.getVisibility() != View.VISIBLE) {
-                            binding.callStates.callStateRelativeLayout.setVisibility(View.VISIBLE);
-                        }
-                        if (binding.gridview.getVisibility() != View.INVISIBLE) {
-                            binding.gridview.setVisibility(View.INVISIBLE);
-                        }
-                        if (binding.callStates.callStateProgressBar.getVisibility() != View.VISIBLE) {
-                            binding.callStates.callStateProgressBar.setVisibility(View.VISIBLE);
-                        }
-                        if (binding.callStates.errorImageView.getVisibility() != View.GONE) {
-                            binding.callStates.errorImageView.setVisibility(View.GONE);
-                        }
-                    });
-                    break;
-                case RECONNECTING:
-                    handler.post(() -> {
-                        playCallingSound();
-                        binding.callStates.callStateTextView.setText(R.string.nc_call_reconnecting);
-                        binding.callModeTextView.setText(getDescriptionForCallType());
-                        if (binding.callStates.callStateRelativeLayout.getVisibility() != View.VISIBLE) {
-                            binding.callStates.callStateRelativeLayout.setVisibility(View.VISIBLE);
-                        }
-                        if (binding.gridview.getVisibility() != View.INVISIBLE) {
-                            binding.gridview.setVisibility(View.INVISIBLE);
-                        }
-                        if (binding.callStates.callStateProgressBar.getVisibility() != View.VISIBLE) {
-                            binding.callStates.callStateProgressBar.setVisibility(View.VISIBLE);
-                        }
-
-                        if (binding.callStates.errorImageView.getVisibility() != View.GONE) {
-                            binding.callStates.errorImageView.setVisibility(View.GONE);
-                        }
-                    });
-                    break;
-                case JOINED:
-                    handler.postDelayed(() -> setCallState(CallStatus.CALLING_TIMEOUT), 45000);
-                    handler.post(() -> {
-                        binding.callModeTextView.setText(getDescriptionForCallType());
-                        if (isIncomingCallFromNotification) {
-                            binding.callStates.callStateTextView.setText(R.string.nc_call_incoming);
-                        } else {
-                            binding.callStates.callStateTextView.setText(R.string.nc_call_ringing);
-                        }
-                        if (binding.callStates.callStateRelativeLayout.getVisibility() != View.VISIBLE) {
-                            binding.callStates.callStateRelativeLayout.setVisibility(View.VISIBLE);
-                        }
-
-                        if (binding.callStates.callStateProgressBar.getVisibility() != View.VISIBLE) {
-                            binding.callStates.callStateProgressBar.setVisibility(View.VISIBLE);
-                        }
-
-                        if (binding.gridview.getVisibility() != View.INVISIBLE) {
-                            binding.gridview.setVisibility(View.INVISIBLE);
-                        }
-
-                        if (binding.callStates.errorImageView.getVisibility() != View.GONE) {
-                            binding.callStates.errorImageView.setVisibility(View.GONE);
-                        }
-                    });
-                    break;
-                case IN_CONVERSATION:
-                    handler.post(() -> {
-                        stopCallingSound();
-                        binding.callModeTextView.setText(getDescriptionForCallType());
-
-                        if (!isVoiceOnlyCall) {
-                            binding.callInfosLinearLayout.setVisibility(View.GONE);
-                        }
-
-                        if (!isPushToTalkActive) {
-                            animateCallControls(false, 5000);
-                        }
-
-                        if (binding.callStates.callStateRelativeLayout.getVisibility() != View.INVISIBLE) {
-                            binding.callStates.callStateRelativeLayout.setVisibility(View.INVISIBLE);
-                        }
-
-                        if (binding.callStates.callStateProgressBar.getVisibility() != View.GONE) {
-                            binding.callStates.callStateProgressBar.setVisibility(View.GONE);
-                        }
-
-                        if (binding.gridview.getVisibility() != View.VISIBLE) {
-                            binding.gridview.setVisibility(View.VISIBLE);
-                        }
-
-                        if (binding.callStates.errorImageView.getVisibility() != View.GONE) {
-                            binding.callStates.errorImageView.setVisibility(View.GONE);
-                        }
-                    });
-                    break;
-                case OFFLINE:
-                    handler.post(() -> {
-                        stopCallingSound();
-
-                        binding.callStates.callStateTextView.setText(R.string.nc_offline);
-
-                        if (binding.callStates.callStateRelativeLayout.getVisibility() != View.VISIBLE) {
-                            binding.callStates.callStateRelativeLayout.setVisibility(View.VISIBLE);
-                        }
-
-
-                        if (binding.gridview.getVisibility() != View.INVISIBLE) {
-                            binding.gridview.setVisibility(View.INVISIBLE);
-                        }
-
-                        if (binding.callStates.callStateProgressBar.getVisibility() != View.GONE) {
-                            binding.callStates.callStateProgressBar.setVisibility(View.GONE);
-                        }
-
-                        binding.callStates.errorImageView.setImageResource(R.drawable.ic_signal_wifi_off_white_24dp);
-                        if (binding.callStates.errorImageView.getVisibility() != View.VISIBLE) {
-                            binding.callStates.errorImageView.setVisibility(View.VISIBLE);
-                        }
-                    });
-                    break;
-                case LEAVING:
-                    handler.post(() -> {
-                        if (!isDestroyed()) {
-                            stopCallingSound();
-                            binding.callModeTextView.setText(getDescriptionForCallType());
-                            binding.callStates.callStateTextView.setText(R.string.nc_leaving_call);
-                            binding.callStates.callStateRelativeLayout.setVisibility(View.VISIBLE);
-                            binding.gridview.setVisibility(View.INVISIBLE);
-                            binding.callStates.callStateProgressBar.setVisibility(View.VISIBLE);
-                            binding.callStates.errorImageView.setVisibility(View.GONE);
-                        }
-                    });
-                    break;
-                default:
-            }
-        }
-    }
-
-    private String getDescriptionForCallType() {
-        String appName = getResources().getString(R.string.nc_app_product_name);
-        if (isVoiceOnlyCall) {
-            return String.format(getResources().getString(R.string.nc_call_voice), appName);
-        } else {
-            return String.format(getResources().getString(R.string.nc_call_video), appName);
-        }
-    }
-
-    private void playCallingSound() {
-        stopCallingSound();
-        Uri ringtoneUri;
-        if (isIncomingCallFromNotification) {
-            ringtoneUri = NotificationUtils.INSTANCE.getCallRingtoneUri(getApplicationContext(), appPreferences);
-        } else {
-            ringtoneUri = Uri.parse("android.resource://" + getApplicationContext().getPackageName() + "/raw" +
-                                        "/tr110_1_kap8_3_freiton1");
-        }
-
-        if (ringtoneUri != null) {
-            mediaPlayer = new MediaPlayer();
-            try {
-                mediaPlayer.setDataSource(this, ringtoneUri);
-                mediaPlayer.setLooping(true);
-                AudioAttributes audioAttributes = new AudioAttributes.Builder().setContentType(
-                        AudioAttributes.CONTENT_TYPE_SONIFICATION)
-                    .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
-                    .build();
-                mediaPlayer.setAudioAttributes(audioAttributes);
-
-                mediaPlayer.setOnPreparedListener(mp -> mediaPlayer.start());
-
-                mediaPlayer.prepareAsync();
-
-            } catch (IOException e) {
-                Log.e(TAG, "Failed to play sound");
-            }
-        }
-    }
-
-    private void stopCallingSound() {
-        if (mediaPlayer != null) {
-            try {
-                if (mediaPlayer.isPlaying()) {
-                    mediaPlayer.stop();
-                }
-            } catch (IllegalStateException e) {
-                Log.e(TAG, "mediaPlayer was not initialized", e);
-            } finally {
-                if (mediaPlayer != null) {
-                    mediaPlayer.release();
-                }
-                mediaPlayer = null;
-            }
-        }
-    }
-
-    public void addReactionForAnimation(String emoji, String displayName) {
-        reactionAnimator.addReaction(emoji, displayName);
-    }
-
-    /**
-     * Temporary implementation of SignalingMessageReceiver until signaling related code is extracted from
-     * CallActivity.
-     * <p>
-     * All listeners are called in the main thread.
-     */
-    private static class InternalSignalingMessageReceiver extends SignalingMessageReceiver {
-        public void process(List<Map<String, Object>> users) {
-            processUsersInRoom(users);
-        }
-
-        public void process(NCSignalingMessage message) {
-            processSignalingMessage(message);
-        }
-    }
-
-    private class OfferAnswerNickProvider {
-
-        private final WebRtcMessageListener videoWebRtcMessageListener = new WebRtcMessageListener();
-        private final WebRtcMessageListener screenWebRtcMessageListener = new WebRtcMessageListener();
-
-        private final String sessionId;
-
-        private String nick;
-
-        private class WebRtcMessageListener implements SignalingMessageReceiver.WebRtcMessageListener {
-
-            @Override
-            public void onOffer(String sdp, String nick) {
-                onOfferOrAnswer(nick);
-            }
-
-            @Override
-            public void onAnswer(String sdp, String nick) {
-                onOfferOrAnswer(nick);
-            }
-
-            @Override
-            public void onCandidate(String sdpMid, int sdpMLineIndex, String sdp) {
-            }
-
-            @Override
-            public void onEndOfCandidates() {
-            }
-        }
-
-        private OfferAnswerNickProvider(String sessionId) {
-            this.sessionId = sessionId;
-        }
-
-        private void onOfferOrAnswer(String nick) {
-            this.nick = nick;
-
-            if (callParticipants.get(sessionId) != null) {
-                callParticipants.get(sessionId).setNick(nick);
-            }
-        }
-
-        public WebRtcMessageListener getVideoWebRtcMessageListener() {
-            return videoWebRtcMessageListener;
-        }
-
-        public WebRtcMessageListener getScreenWebRtcMessageListener() {
-            return screenWebRtcMessageListener;
-        }
-
-        public String getNick() {
-            return nick;
-        }
-    }
-
-    private class CallActivityCallParticipantMessageListener implements SignalingMessageReceiver.CallParticipantMessageListener {
-
-        private final String sessionId;
-
-        public CallActivityCallParticipantMessageListener(String sessionId) {
-            this.sessionId = sessionId;
-        }
-
-        @Override
-        public void onRaiseHand(boolean state, long timestamp) {
-        }
-
-        @Override
-        public void onReaction(String reaction) {
-        }
-
-        @Override
-        public void onUnshareScreen() {
-            endPeerConnection(sessionId, "screen");
-        }
-    }
-
-    private class CallActivitySelfPeerConnectionObserver implements PeerConnectionWrapper.PeerConnectionObserver {
-
-        @Override
-        public void onStreamAdded(MediaStream mediaStream) {
-        }
-
-        @Override
-        public void onStreamRemoved(MediaStream mediaStream) {
-        }
-
-        @Override
-        public void onIceConnectionStateChanged(PeerConnection.IceConnectionState iceConnectionState) {
-            runOnUiThread(() -> {
-                updateSelfVideoViewIceConnectionState(iceConnectionState);
-
-                if (iceConnectionState == PeerConnection.IceConnectionState.FAILED) {
-                    setCallState(CallStatus.PUBLISHER_FAILED);
-                    webSocketClient.clearResumeId();
-                    hangup(false);
-                }
-            });
-        }
-    }
-
-    private class ScreenParticipantDisplayItemManager implements CallParticipantModel.Observer {
-
-        private final CallParticipantModel callParticipantModel;
-
-        private ScreenParticipantDisplayItemManager(CallParticipantModel callParticipantModel) {
-            this.callParticipantModel = callParticipantModel;
-        }
-
-        @Override
-        public void onChange() {
-            String sessionId = callParticipantModel.getSessionId();
-            if (callParticipantModel.getScreenIceConnectionState() == null) {
-                removeParticipantDisplayItem(sessionId, "screen");
-
-                return;
-            }
-
-            boolean hasScreenParticipantDisplayItem = participantDisplayItems.get(sessionId + "-screen") != null;
-            if (!hasScreenParticipantDisplayItem) {
-                addParticipantDisplayItem(callParticipantModel, "screen");
-            }
-        }
-
-        @Override
-        public void onReaction(String reaction) {
-        }
-    }
-
-    private class CallParticipantEventDisplayer implements CallParticipantModel.Observer {
-
-        private final CallParticipantModel callParticipantModel;
-
-        private boolean raisedHand;
-
-        private CallParticipantEventDisplayer(CallParticipantModel callParticipantModel) {
-            this.callParticipantModel = callParticipantModel;
-            this.raisedHand = callParticipantModel.getRaisedHand() != null ?
-                callParticipantModel.getRaisedHand().getState() : false;
-        }
-
-        @Override
-        public void onChange() {
-            if (callParticipantModel.getRaisedHand() == null || !callParticipantModel.getRaisedHand().getState()) {
-                raisedHand = false;
-
-                return;
-            }
-
-            if (raisedHand) {
-                return;
-            }
-            raisedHand = true;
-
-            String nick = callParticipantModel.getNick();
-            Toast.makeText(context, String.format(context.getResources().getString(R.string.nc_call_raised_hand), nick), Toast.LENGTH_LONG).show();
-        }
-
-        @Override
-        public void onReaction(String reaction) {
-            addReactionForAnimation(reaction, callParticipantModel.getNick());
-        }
-    }
-
-    private class InternalSignalingMessageSender implements SignalingMessageSender {
-
-        @Override
-        public void send(NCSignalingMessage ncSignalingMessage) {
-            addLocalParticipantNickIfNeeded(ncSignalingMessage);
-
-            String serializedNcSignalingMessage;
-            try {
-                serializedNcSignalingMessage = LoganSquare.serialize(ncSignalingMessage);
-            } catch (IOException e) {
-                Log.e(TAG, "Failed to serialize signaling message", e);
-                return;
-            }
-
-            // The message wrapper can not be defined in a JSON model to be directly serialized, as sent messages
-            // need to be serialized twice; first the signaling message, and then the wrapper as a whole. Received
-            // messages, on the other hand, just need to be deserialized once.
-            StringBuilder stringBuilder = new StringBuilder();
-            stringBuilder.append('{')
-                .append("\"fn\":\"")
-                .append(StringEscapeUtils.escapeJson(serializedNcSignalingMessage))
-                .append('\"')
-                .append(',')
-                .append("\"sessionId\":")
-                .append('\"').append(StringEscapeUtils.escapeJson(callSession)).append('\"')
-                .append(',')
-                .append("\"ev\":\"message\"")
-                .append('}');
-
-            List<String> strings = new ArrayList<>();
-            String stringToSend = stringBuilder.toString();
-            strings.add(stringToSend);
-
-            int apiVersion = ApiUtils.getSignalingApiVersion(conversationUser, new int[]{ApiUtils.APIv3, 2, 1});
-
-            ncApi.sendSignalingMessages(credentials, ApiUtils.getUrlForSignaling(apiVersion, baseUrl, roomToken),
-                                        strings.toString())
-                .retry(3)
-                .subscribeOn(Schedulers.io())
-                .subscribe(new Observer<SignalingOverall>() {
-                    @Override
-                    public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
-                    }
-
-                    @Override
-                    public void onNext(@io.reactivex.annotations.NonNull SignalingOverall signalingOverall) {
-                        // When sending messages to the internal signaling server the response has been empty since
-                        // Talk v2.9.0, so it is not really needed to process it, but there is no harm either in
-                        // doing that, as technically messages could be returned.
-                        receivedSignalingMessages(signalingOverall.getOcs().getSignalings());
-                    }
-
-                    @Override
-                    public void onError(@io.reactivex.annotations.NonNull Throwable e) {
-                        Log.e(TAG, "", e);
-                    }
-
-                    @Override
-                    public void onComplete() {
-                    }
-                });
-        }
-
-        /**
-         * Adds the local participant nick to offers and answers.
-         * <p>
-         * For legacy reasons the offers and answers sent when the internal signaling server is used are expected to
-         * provide the nick of the local participant.
-         *
-         * @param ncSignalingMessage the message to add the nick to
-         */
-        private void addLocalParticipantNickIfNeeded(NCSignalingMessage ncSignalingMessage) {
-            String type = ncSignalingMessage.getType();
-            if (!"offer".equals(type) && !"answer".equals(type)) {
-                return;
-            }
-
-            NCMessagePayload payload = ncSignalingMessage.getPayload();
-            if (payload == null) {
-                // Broken message, this should not happen
-                return;
-            }
-
-            payload.setNick(conversationUser.getDisplayName());
-        }
-    }
-
-    private class MicrophoneButtonTouchListener implements View.OnTouchListener {
-
-        @SuppressLint("ClickableViewAccessibility")
-        @Override
-        public boolean onTouch(View v, MotionEvent event) {
-            v.onTouchEvent(event);
-            if (event.getAction() == MotionEvent.ACTION_UP && isPushToTalkActive) {
-                isPushToTalkActive = false;
-                binding.microphoneButton.setImageResource(R.drawable.ic_mic_off_white_24px);
-                pulseAnimation.stop();
-                toggleMedia(false, false);
-                animateCallControls(false, 5000);
-            }
-            return true;
-        }
-    }
-
-    @Subscribe(threadMode = ThreadMode.BACKGROUND)
-    public void onMessageEvent(NetworkEvent networkEvent) {
-        if (networkEvent.getNetworkConnectionEvent() == NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED) {
-            if (handler != null) {
-                handler.removeCallbacksAndMessages(null);
-            }
-        } else if (networkEvent.getNetworkConnectionEvent() ==
-            NetworkEvent.NetworkConnectionEvent.NETWORK_DISCONNECTED) {
-            if (handler != null) {
-                handler.removeCallbacksAndMessages(null);
-            }
-        }
-    }
-
-    @RequiresApi(api = Build.VERSION_CODES.O)
-    @Override
-    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
-        super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
-        Log.d(TAG, "onPictureInPictureModeChanged");
-        Log.d(TAG, "isInPictureInPictureMode= " + isInPictureInPictureMode);
-        isInPipMode = isInPictureInPictureMode;
-        if (isInPictureInPictureMode) {
-            mReceiver =
-                new BroadcastReceiver() {
-                    @Override
-                    public void onReceive(Context context, Intent intent) {
-                        if (intent == null || !MICROPHONE_PIP_INTENT_NAME.equals(intent.getAction())) {
-                            return;
-                        }
-
-                        final int action = intent.getIntExtra(MICROPHONE_PIP_INTENT_EXTRA_ACTION, 0);
-                        switch (action) {
-                            case MICROPHONE_PIP_REQUEST_MUTE:
-                            case MICROPHONE_PIP_REQUEST_UNMUTE:
-                                onMicrophoneClick();
-                                break;
-                        }
-                    }
-                };
-            registerReceiver(mReceiver,
-                             new IntentFilter(MICROPHONE_PIP_INTENT_NAME),
-                             permissionUtil.getPrivateBroadcastPermission(),
-                             null);
-
-            updateUiForPipMode();
-        } else {
-            unregisterReceiver(mReceiver);
-            mReceiver = null;
-
-            updateUiForNormalMode();
-        }
-    }
-
-    void updatePictureInPictureActions(
-        @DrawableRes int iconId,
-        String title,
-        int requestCode) {
-
-        if (isGreaterEqualOreo() && isPipModePossible()) {
-            final ArrayList<RemoteAction> actions = new ArrayList<>();
-
-            final Icon icon = Icon.createWithResource(this, iconId);
-
-            int intentFlag;
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
-                intentFlag = FLAG_IMMUTABLE;
-            } else {
-                intentFlag = 0;
-            }
-            final PendingIntent intent =
-                PendingIntent.getBroadcast(
-                    this,
-                    requestCode,
-                    new Intent(MICROPHONE_PIP_INTENT_NAME).putExtra(MICROPHONE_PIP_INTENT_EXTRA_ACTION, requestCode),
-                    intentFlag);
-
-            actions.add(new RemoteAction(icon, title, title, intent));
-
-            mPictureInPictureParamsBuilder.setActions(actions);
-            setPictureInPictureParams(mPictureInPictureParamsBuilder.build());
-        }
-    }
-
-    public void updateUiForPipMode() {
-        Log.d(TAG, "updateUiForPipMode");
-        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
-                                                                             ViewGroup.LayoutParams.WRAP_CONTENT);
-        params.setMargins(0, 0, 0, 0);
-        binding.gridview.setLayoutParams(params);
-
-
-        binding.callControls.setVisibility(View.GONE);
-        binding.callInfosLinearLayout.setVisibility(View.GONE);
-        binding.selfVideoViewWrapper.setVisibility(View.GONE);
-        binding.callStates.callStateRelativeLayout.setVisibility(View.GONE);
-
-        if (participantDisplayItems.size() > 1) {
-            binding.pipCallConversationNameTextView.setText(conversationName);
-            binding.pipGroupCallOverlay.setVisibility(View.VISIBLE);
-        } else {
-            binding.pipGroupCallOverlay.setVisibility(View.GONE);
-        }
-
-        binding.selfVideoRenderer.release();
-    }
-
-    public void updateUiForNormalMode() {
-        Log.d(TAG, "updateUiForNormalMode");
-        if (isVoiceOnlyCall) {
-            binding.callControls.setVisibility(View.VISIBLE);
-        } else {
-            // animateCallControls needs this to be invisible for a check.
-            binding.callControls.setVisibility(View.INVISIBLE);
-        }
-        initViews();
-
-        binding.callInfosLinearLayout.setVisibility(View.VISIBLE);
-        binding.selfVideoViewWrapper.setVisibility(View.VISIBLE);
-
-        binding.pipGroupCallOverlay.setVisibility(View.GONE);
-    }
-
-    @Override
-    public void suppressFitsSystemWindows() {
-        binding.controllerCallLayout.setFitsSystemWindows(false);
-    }
-
-    public void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
-        eventBus.post(new ConfigurationChangeEvent());
-    }
-
-    public boolean isAllowedToStartOrStopRecording() {
-        return CapabilitiesUtilNew.isCallRecordingAvailable(conversationUser)
-            && isModerator;
-    }
-
-    public boolean isAllowedToRaiseHand() {
-        return CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "raise-hand") ||
-            isBreakoutRoom;
-    }
-
-    private class SelfVideoTouchListener implements View.OnTouchListener {
-
-        @SuppressLint("ClickableViewAccessibility")
-        @Override
-        public boolean onTouch(View view, MotionEvent event) {
-            long duration = event.getEventTime() - event.getDownTime();
-
-            if (event.getActionMasked() == MotionEvent.ACTION_MOVE) {
-                float newY = event.getRawY() - binding.selfVideoViewWrapper.getHeight() / (float) 2;
-                float newX = event.getRawX() - binding.selfVideoViewWrapper.getWidth() / (float) 2;
-                binding.selfVideoViewWrapper.setY(newY);
-                binding.selfVideoViewWrapper.setX(newX);
-            } else if (event.getActionMasked() == MotionEvent.ACTION_UP && duration < 100) {
-                switchCamera();
-            }
-            return true;
-        }
-    }
-}

+ 2886 - 0
app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt

@@ -0,0 +1,2886 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Tim Krüger
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.activities
+
+import android.Manifest
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.app.RemoteAction
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.DialogInterface
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.res.Configuration
+import android.graphics.Color
+import android.graphics.drawable.Icon
+import android.media.AudioAttributes
+import android.media.MediaPlayer
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+import android.text.TextUtils
+import android.util.Log
+import android.view.MotionEvent
+import android.view.View
+import android.view.View.OnTouchListener
+import android.view.ViewGroup
+import android.view.ViewTreeObserver.OnGlobalLayoutListener
+import android.widget.AdapterView
+import android.widget.FrameLayout
+import android.widget.RelativeLayout
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.DrawableRes
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AlertDialog
+import androidx.core.graphics.drawable.DrawableCompat
+import androidx.lifecycle.ViewModelProvider
+import autodagger.AutoInjector
+import com.bluelinelabs.logansquare.LoganSquare
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.nextcloud.talk.R
+import com.nextcloud.talk.adapters.ParticipantDisplayItem
+import com.nextcloud.talk.adapters.ParticipantsAdapter
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.call.CallParticipant
+import com.nextcloud.talk.call.CallParticipantList
+import com.nextcloud.talk.call.CallParticipantModel
+import com.nextcloud.talk.call.ReactionAnimator
+import com.nextcloud.talk.chat.ChatActivity
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.databinding.CallActivityBinding
+import com.nextcloud.talk.events.ConfigurationChangeEvent
+import com.nextcloud.talk.events.NetworkEvent
+import com.nextcloud.talk.events.ProximitySensorEvent
+import com.nextcloud.talk.events.WebSocketCommunicationEvent
+import com.nextcloud.talk.models.ExternalSignalingServer
+import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall
+import com.nextcloud.talk.models.json.conversations.RoomOverall
+import com.nextcloud.talk.models.json.conversations.RoomsOverall
+import com.nextcloud.talk.models.json.generic.GenericOverall
+import com.nextcloud.talk.models.json.participants.Participant
+import com.nextcloud.talk.models.json.signaling.DataChannelMessage
+import com.nextcloud.talk.models.json.signaling.NCSignalingMessage
+import com.nextcloud.talk.models.json.signaling.Signaling
+import com.nextcloud.talk.models.json.signaling.SignalingOverall
+import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall
+import com.nextcloud.talk.raisehand.viewmodel.RaiseHandViewModel
+import com.nextcloud.talk.raisehand.viewmodel.RaiseHandViewModel.LoweredHandState
+import com.nextcloud.talk.raisehand.viewmodel.RaiseHandViewModel.RaisedHandState
+import com.nextcloud.talk.signaling.SignalingMessageReceiver
+import com.nextcloud.talk.signaling.SignalingMessageReceiver.CallParticipantMessageListener
+import com.nextcloud.talk.signaling.SignalingMessageReceiver.LocalParticipantMessageListener
+import com.nextcloud.talk.signaling.SignalingMessageReceiver.OfferMessageListener
+import com.nextcloud.talk.signaling.SignalingMessageSender
+import com.nextcloud.talk.ui.dialog.AudioOutputDialog
+import com.nextcloud.talk.ui.dialog.MoreCallActionsDialog
+import com.nextcloud.talk.users.UserManager
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.DisplayUtils
+import com.nextcloud.talk.utils.NotificationUtils.cancelExistingNotificationsForRoom
+import com.nextcloud.talk.utils.NotificationUtils.getCallRingtoneUri
+import com.nextcloud.talk.utils.VibrationUtils.vibrateShort
+import com.nextcloud.talk.utils.animations.PulseAnimation
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_VOICE_ONLY
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_WITHOUT_NOTIFICATION
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_PASSWORD
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FROM_NOTIFICATION_START_CALL
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_BREAKOUT_ROOM
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_MODERATOR
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MODIFIED_BASE_URL
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_RECORDING_STATE
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_START_CALL_AFTER_ROOM_SWITCH
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SWITCH_TO_ROOM
+import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.hasSpreedFeatureCapability
+import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.isCallRecordingAvailable
+import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
+import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
+import com.nextcloud.talk.utils.power.PowerManagerUtils
+import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
+import com.nextcloud.talk.viewmodels.CallRecordingViewModel
+import com.nextcloud.talk.viewmodels.CallRecordingViewModel.RecordingConfirmStopState
+import com.nextcloud.talk.viewmodels.CallRecordingViewModel.RecordingErrorState
+import com.nextcloud.talk.viewmodels.CallRecordingViewModel.RecordingStartedState
+import com.nextcloud.talk.viewmodels.CallRecordingViewModel.RecordingStartingState
+import com.nextcloud.talk.webrtc.MagicWebRTCUtils
+import com.nextcloud.talk.webrtc.PeerConnectionWrapper
+import com.nextcloud.talk.webrtc.PeerConnectionWrapper.PeerConnectionObserver
+import com.nextcloud.talk.webrtc.WebRtcAudioManager
+import com.nextcloud.talk.webrtc.WebRtcAudioManager.AudioDevice
+import com.nextcloud.talk.webrtc.WebRtcAudioManager.AudioManagerListener
+import com.nextcloud.talk.webrtc.WebSocketConnectionHelper
+import com.nextcloud.talk.webrtc.WebSocketInstance
+import com.wooplr.spotlight.SpotlightView
+import io.reactivex.Observable
+import io.reactivex.Observer
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import okhttp3.Cache
+import org.apache.commons.lang3.StringEscapeUtils
+import org.greenrobot.eventbus.Subscribe
+import org.greenrobot.eventbus.ThreadMode
+import org.webrtc.AudioSource
+import org.webrtc.AudioTrack
+import org.webrtc.Camera1Enumerator
+import org.webrtc.Camera2Enumerator
+import org.webrtc.CameraEnumerator
+import org.webrtc.CameraVideoCapturer
+import org.webrtc.CameraVideoCapturer.CameraSwitchHandler
+import org.webrtc.DefaultVideoDecoderFactory
+import org.webrtc.DefaultVideoEncoderFactory
+import org.webrtc.EglBase
+import org.webrtc.Logging
+import org.webrtc.MediaConstraints
+import org.webrtc.MediaStream
+import org.webrtc.PeerConnection
+import org.webrtc.PeerConnection.IceConnectionState
+import org.webrtc.PeerConnectionFactory
+import org.webrtc.RendererCommon
+import org.webrtc.SurfaceTextureHelper
+import org.webrtc.VideoCapturer
+import org.webrtc.VideoSource
+import org.webrtc.VideoTrack
+import java.io.IOException
+import java.util.Objects
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+import javax.inject.Inject
+import kotlin.math.roundToInt
+
+@AutoInjector(NextcloudTalkApplication::class)
+class CallActivity : CallBaseActivity() {
+    @JvmField
+    @Inject
+    var ncApi: NcApi? = null
+
+    @JvmField
+    @Inject
+    var currentUserProvider: CurrentUserProviderNew? = null
+
+    @JvmField
+    @Inject
+    var userManager: UserManager? = null
+
+    @JvmField
+    @Inject
+    var cache: Cache? = null
+
+    @JvmField
+    @Inject
+    var permissionUtil: PlatformPermissionUtil? = null
+
+    @Inject
+    lateinit var viewModelFactory: ViewModelProvider.Factory
+    var audioManager: WebRtcAudioManager? = null
+    var callRecordingViewModel: CallRecordingViewModel? = null
+    var raiseHandViewModel: RaiseHandViewModel? = null
+    private var mReceiver: BroadcastReceiver? = null
+    private var peerConnectionFactory: PeerConnectionFactory? = null
+    private var audioConstraints: MediaConstraints? = null
+    private var videoConstraints: MediaConstraints? = null
+    private var sdpConstraints: MediaConstraints? = null
+    private var sdpConstraintsForMCU: MediaConstraints? = null
+    private var videoSource: VideoSource? = null
+    private var localVideoTrack: VideoTrack? = null
+    private var audioSource: AudioSource? = null
+    private var localAudioTrack: AudioTrack? = null
+    private var videoCapturer: VideoCapturer? = null
+    private var rootEglBase: EglBase? = null
+    private var signalingDisposable: Disposable? = null
+    private var iceServers: MutableList<PeerConnection.IceServer>? = null
+    private var cameraEnumerator: CameraEnumerator? = null
+    private var roomToken: String? = null
+    var conversationUser: User? = null
+    private var conversationName: String? = null
+    private var callSession: String? = null
+    private var localStream: MediaStream? = null
+    private var credentials: String? = null
+    private val peerConnectionWrapperList: MutableList<PeerConnectionWrapper> = ArrayList()
+    private var videoOn = false
+    private var microphoneOn = false
+    private var isVoiceOnlyCall = false
+    private var isCallWithoutNotification = false
+    private var isIncomingCallFromNotification = false
+    private val callControlHandler = Handler()
+    private val callInfosHandler = Handler()
+    private val cameraSwitchHandler = Handler()
+
+    // push to talk
+    private var isPushToTalkActive = false
+    private var pulseAnimation: PulseAnimation? = null
+    private var baseUrl: String? = null
+    private var roomId: String? = null
+    private var spotlightView: SpotlightView? = null
+    private val internalSignalingMessageReceiver = InternalSignalingMessageReceiver()
+    private var signalingMessageReceiver: SignalingMessageReceiver? = null
+    private val internalSignalingMessageSender = InternalSignalingMessageSender()
+    private var signalingMessageSender: SignalingMessageSender? = null
+    private val offerAnswerNickProviders: MutableMap<String?, OfferAnswerNickProvider?> = HashMap()
+    private val callParticipantMessageListeners: MutableMap<String?, CallParticipantMessageListener> = HashMap()
+    private val selfPeerConnectionObserver: PeerConnectionObserver = CallActivitySelfPeerConnectionObserver()
+    private var callParticipants: MutableMap<String?, CallParticipant?> = HashMap()
+    private val screenParticipantDisplayItemManagers: MutableMap<String?, ScreenParticipantDisplayItemManager> =
+        HashMap()
+    private val screenParticipantDisplayItemManagersHandler = Handler(Looper.getMainLooper())
+    private val callParticipantEventDisplayers: MutableMap<String?, CallParticipantEventDisplayer> = HashMap()
+    private val callParticipantEventDisplayersHandler = Handler(Looper.getMainLooper())
+    private val callParticipantListObserver: CallParticipantList.Observer = object : CallParticipantList.Observer {
+        override fun onCallParticipantsChanged(
+            joined: Collection<Participant>,
+            updated: Collection<Participant>,
+            left: Collection<Participant>,
+            unchanged: Collection<Participant>
+        ) {
+            handleCallParticipantsChanged(joined, updated, left, unchanged)
+        }
+
+        override fun onCallEndedForAll() {
+            Log.d(TAG, "A moderator ended the call for all.")
+            hangup(true)
+        }
+    }
+    private var callParticipantList: CallParticipantList? = null
+    private var switchToRoomToken = ""
+    private var isBreakoutRoom = false
+    private val localParticipantMessageListener = LocalParticipantMessageListener { token ->
+        switchToRoomToken = token
+        hangup(true)
+    }
+    private val offerMessageListener = OfferMessageListener { sessionId, roomType, sdp, nick ->
+        getOrCreatePeerConnectionWrapperForSessionIdAndType(
+            sessionId,
+            roomType,
+            false
+        )
+    }
+    private var externalSignalingServer: ExternalSignalingServer? = null
+    private var webSocketClient: WebSocketInstance? = null
+    private var webSocketConnectionHelper: WebSocketConnectionHelper? = null
+    private var hasMCU = false
+    private var hasExternalSignalingServer = false
+    private var conversationPassword: String? = null
+    private var powerManagerUtils: PowerManagerUtils? = null
+    private var handler: Handler? = null
+    private var currentCallStatus: CallStatus? = null
+    private var mediaPlayer: MediaPlayer? = null
+    private var participantDisplayItems: MutableMap<String, ParticipantDisplayItem?>? = null
+    private var participantsAdapter: ParticipantsAdapter? = null
+    private var binding: CallActivityBinding? = null
+    private var audioOutputDialog: AudioOutputDialog? = null
+    private var moreCallActionsDialog: MoreCallActionsDialog? = null
+
+    private var requestPermissionLauncher = registerForActivityResult(
+        ActivityResultContracts.RequestMultiplePermissions()
+    ) { permissionMap: Map<String, Boolean> ->
+        val rationaleList: MutableList<String> = ArrayList()
+        val audioPermission = permissionMap[Manifest.permission.RECORD_AUDIO]
+        if (audioPermission != null) {
+            if (java.lang.Boolean.TRUE == audioPermission) {
+                if (!microphoneOn) {
+                    onMicrophoneClick()
+                }
+            } else {
+                rationaleList.add(resources.getString(R.string.nc_microphone_permission_hint))
+            }
+        }
+        val cameraPermission = permissionMap[Manifest.permission.CAMERA]
+        if (cameraPermission != null) {
+            if (java.lang.Boolean.TRUE == cameraPermission) {
+                if (!videoOn) {
+                    onCameraClick()
+                }
+                if (cameraEnumerator!!.deviceNames.isEmpty()) {
+                    binding!!.cameraButton.visibility = View.GONE
+                }
+                if (cameraEnumerator!!.deviceNames.size > 1) {
+                    binding!!.switchSelfVideoButton.visibility = View.VISIBLE
+                }
+            } else {
+                rationaleList.add(resources.getString(R.string.nc_camera_permission_hint))
+            }
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            val bluetoothPermission = permissionMap[Manifest.permission.BLUETOOTH_CONNECT]
+            if (bluetoothPermission != null) {
+                if (java.lang.Boolean.TRUE == bluetoothPermission) {
+                    enableBluetoothManager()
+                } else {
+                    // Only ask for bluetooth when already asking to grant microphone or camera access. Asking
+                    // for bluetooth solely is not important enough here and would most likely annoy the user.
+                    if (rationaleList.isNotEmpty()) {
+                        rationaleList.add(resources.getString(R.string.nc_bluetooth_permission_hint))
+                    }
+                }
+            }
+        }
+        if (!rationaleList.isEmpty()) {
+            showRationaleDialogForSettings(rationaleList)
+        }
+    }
+    private var canPublishAudioStream = false
+    private var canPublishVideoStream = false
+    private var isModerator = false
+    private var reactionAnimator: ReactionAnimator? = null
+
+    @SuppressLint("ClickableViewAccessibility")
+    override fun onCreate(savedInstanceState: Bundle?) {
+        Log.d(TAG, "onCreate")
+        super.onCreate(savedInstanceState)
+        sharedApplication!!.componentApplication.inject(this)
+        binding = CallActivityBinding.inflate(layoutInflater)
+        setContentView(binding!!.root)
+        hideNavigationIfNoPipAvailable()
+        conversationUser = currentUserProvider!!.currentUser.blockingGet()
+        val extras = intent.extras
+        roomId = extras!!.getString(KEY_ROOM_ID, "")
+        roomToken = extras.getString(KEY_ROOM_TOKEN, "")
+        conversationPassword = extras.getString(KEY_CONVERSATION_PASSWORD, "")
+        conversationName = extras.getString(KEY_CONVERSATION_NAME, "")
+        isVoiceOnlyCall = extras.getBoolean(KEY_CALL_VOICE_ONLY, false)
+        isCallWithoutNotification = extras.getBoolean(KEY_CALL_WITHOUT_NOTIFICATION, false)
+        canPublishAudioStream = extras.getBoolean(KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO)
+        canPublishVideoStream = extras.getBoolean(KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO)
+        isModerator = extras.getBoolean(KEY_IS_MODERATOR, false)
+        if (extras.containsKey(KEY_FROM_NOTIFICATION_START_CALL)) {
+            isIncomingCallFromNotification = extras.getBoolean(KEY_FROM_NOTIFICATION_START_CALL)
+        }
+        if (extras.containsKey(KEY_IS_BREAKOUT_ROOM)) {
+            isBreakoutRoom = extras.getBoolean(KEY_IS_BREAKOUT_ROOM)
+        }
+        credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token)
+        baseUrl = extras.getString(KEY_MODIFIED_BASE_URL, "")
+        if (TextUtils.isEmpty(baseUrl)) {
+            baseUrl = conversationUser!!.baseUrl
+        }
+        powerManagerUtils = PowerManagerUtils()
+        if ("resume".equals(extras.getString("state", ""), ignoreCase = true)) {
+            setCallState(CallStatus.IN_CONVERSATION)
+        } else {
+            setCallState(CallStatus.CONNECTING)
+        }
+        raiseHandViewModel = ViewModelProvider(this, viewModelFactory).get(RaiseHandViewModel::class.java)
+        raiseHandViewModel!!.setData(roomToken!!, isBreakoutRoom)
+        raiseHandViewModel!!.viewState.observe(this) { viewState: RaiseHandViewModel.ViewState? ->
+            var raised = false
+            if (viewState is RaisedHandState) {
+                binding!!.lowerHandButton.visibility = View.VISIBLE
+                raised = true
+            } else if (viewState is LoweredHandState) {
+                binding!!.lowerHandButton.visibility = View.GONE
+                raised = false
+            }
+            if (isConnectionEstablished && peerConnectionWrapperList != null) {
+                for (peerConnectionWrapper in peerConnectionWrapperList) {
+                    peerConnectionWrapper.raiseHand(raised)
+                }
+            }
+        }
+        callRecordingViewModel = ViewModelProvider(this, viewModelFactory).get(
+            CallRecordingViewModel::class.java
+        )
+        callRecordingViewModel!!.setData(roomToken!!)
+        callRecordingViewModel!!.setRecordingState(extras.getInt(KEY_RECORDING_STATE))
+        callRecordingViewModel!!.viewState.observe(this) { viewState: CallRecordingViewModel.ViewState? ->
+            if (viewState is RecordingStartedState) {
+                binding!!.callRecordingIndicator.setImageResource(R.drawable.record_stop)
+                binding!!.callRecordingIndicator.visibility = View.VISIBLE
+                if (viewState.showStartedInfo) {
+                    vibrateShort(context)
+                    Toast.makeText(context, context.resources.getString(R.string.record_active_info), Toast.LENGTH_LONG)
+                        .show()
+                }
+            } else if (viewState is RecordingStartingState) {
+                if (isAllowedToStartOrStopRecording) {
+                    binding!!.callRecordingIndicator.setImageResource(R.drawable.record_starting)
+                    binding!!.callRecordingIndicator.visibility = View.VISIBLE
+                } else {
+                    binding!!.callRecordingIndicator.visibility = View.GONE
+                }
+            } else if (viewState is RecordingConfirmStopState) {
+                if (isAllowedToStartOrStopRecording) {
+                    val dialogBuilder = MaterialAlertDialogBuilder(this)
+                        .setTitle(R.string.record_stop_confirm_title)
+                        .setMessage(R.string.record_stop_confirm_message)
+                        .setPositiveButton(
+                            R.string.record_stop_description
+                        ) { dialog: DialogInterface?, which: Int -> callRecordingViewModel!!.stopRecording() }
+                        .setNegativeButton(
+                            R.string.nc_common_dismiss
+                        ) { dialog: DialogInterface?, which: Int -> callRecordingViewModel!!.dismissStopRecording() }
+                    viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder)
+                    val dialog = dialogBuilder.show()
+                    viewThemeUtils.platform.colorTextButtons(
+                        dialog.getButton(AlertDialog.BUTTON_POSITIVE),
+                        dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
+                    )
+                } else {
+                    Log.e(TAG, "Being in RecordingConfirmStopState as non moderator. This should not happen!")
+                }
+            } else if (viewState is RecordingErrorState) {
+                if (isAllowedToStartOrStopRecording) {
+                    Toast.makeText(
+                        context,
+                        context.resources.getString(R.string.record_failed_info),
+                        Toast.LENGTH_LONG
+                    ).show()
+                }
+                binding!!.callRecordingIndicator.visibility = View.GONE
+            } else {
+                binding!!.callRecordingIndicator.visibility = View.GONE
+            }
+        }
+        initClickListeners()
+        binding!!.microphoneButton.setOnTouchListener(MicrophoneButtonTouchListener())
+        pulseAnimation = PulseAnimation.create().with(binding!!.microphoneButton)
+            .setDuration(310)
+            .setRepeatCount(PulseAnimation.INFINITE)
+            .setRepeatMode(PulseAnimation.REVERSE)
+        basicInitialization()
+        callParticipants = HashMap()
+        participantDisplayItems = HashMap()
+        initViews()
+        if (!isConnectionEstablished) {
+            initiateCall()
+        }
+        updateSelfVideoViewPosition()
+        reactionAnimator = ReactionAnimator(context, binding!!.reactionAnimationWrapper, viewThemeUtils)
+    }
+
+    fun sendReaction(emoji: String?) {
+        addReactionForAnimation(emoji, conversationUser!!.displayName)
+        if (isConnectionEstablished && peerConnectionWrapperList != null) {
+            for (peerConnectionWrapper in peerConnectionWrapperList) {
+                peerConnectionWrapper.sendReaction(emoji)
+            }
+        }
+    }
+
+    override fun onStart() {
+        super.onStart()
+        active = true
+        initFeaturesVisibility()
+        try {
+            cache!!.evictAll()
+        } catch (e: IOException) {
+            Log.e(TAG, "Failed to evict cache")
+        }
+    }
+
+    override fun onStop() {
+        super.onStop()
+        active = false
+    }
+
+    private fun enableBluetoothManager() {
+        if (audioManager != null) {
+            audioManager!!.startBluetoothManager()
+        }
+    }
+
+    private fun initFeaturesVisibility() {
+        if (isAllowedToStartOrStopRecording || isAllowedToRaiseHand) {
+            binding!!.moreCallActions.visibility = View.VISIBLE
+        } else {
+            binding!!.moreCallActions.visibility = View.GONE
+        }
+    }
+
+    private fun initClickListeners() {
+        binding!!.pictureInPictureButton.setOnClickListener { l: View? -> enterPipMode() }
+        binding!!.audioOutputButton.setOnClickListener { v: View? ->
+            audioOutputDialog = AudioOutputDialog(this)
+            audioOutputDialog!!.show()
+        }
+        binding!!.moreCallActions.setOnClickListener { v: View? ->
+            moreCallActionsDialog = MoreCallActionsDialog(this)
+            moreCallActionsDialog!!.show()
+        }
+        if (canPublishAudioStream) {
+            binding!!.microphoneButton.setOnClickListener { l: View? -> onMicrophoneClick() }
+            binding!!.microphoneButton.setOnLongClickListener { l: View? ->
+                if (!microphoneOn) {
+                    callControlHandler.removeCallbacksAndMessages(null)
+                    callInfosHandler.removeCallbacksAndMessages(null)
+                    cameraSwitchHandler.removeCallbacksAndMessages(null)
+                    isPushToTalkActive = true
+                    binding!!.callControls.visibility = View.VISIBLE
+                    if (!isVoiceOnlyCall) {
+                        binding!!.switchSelfVideoButton.visibility = View.VISIBLE
+                    }
+                }
+                onMicrophoneClick()
+                true
+            }
+        } else {
+            binding!!.microphoneButton.setOnClickListener { l: View? ->
+                Toast.makeText(
+                    context,
+                    R.string.nc_not_allowed_to_activate_audio,
+                    Toast.LENGTH_SHORT
+                ).show()
+            }
+        }
+        if (canPublishVideoStream) {
+            binding!!.cameraButton.setOnClickListener { l: View? -> onCameraClick() }
+        } else {
+            binding!!.cameraButton.setOnClickListener { l: View? ->
+                Toast.makeText(
+                    context,
+                    R.string.nc_not_allowed_to_activate_video,
+                    Toast.LENGTH_SHORT
+                ).show()
+            }
+        }
+        binding!!.hangupButton.setOnClickListener { l: View? -> hangup(true) }
+        binding!!.switchSelfVideoButton.setOnClickListener { l: View? -> switchCamera() }
+        binding!!.gridview.onItemClickListener =
+            AdapterView.OnItemClickListener { parent: AdapterView<*>?, view: View?, position: Int, id: Long ->
+                animateCallControls(
+                    true,
+                    0
+                )
+            }
+        binding!!.callStates.callStateRelativeLayout.setOnClickListener { l: View? ->
+            if (currentCallStatus === CallStatus.CALLING_TIMEOUT) {
+                setCallState(CallStatus.RECONNECTING)
+                hangupNetworkCalls(false)
+            }
+        }
+        binding!!.callRecordingIndicator.setOnClickListener { l: View? ->
+            if (isAllowedToStartOrStopRecording) {
+                if (callRecordingViewModel!!.viewState.value is RecordingStartingState) {
+                    if (moreCallActionsDialog == null) {
+                        moreCallActionsDialog = MoreCallActionsDialog(this)
+                    }
+                    moreCallActionsDialog!!.show()
+                } else {
+                    callRecordingViewModel!!.clickRecordButton()
+                }
+            } else {
+                Toast.makeText(context, context.resources.getString(R.string.record_active_info), Toast.LENGTH_LONG)
+                    .show()
+            }
+        }
+        binding!!.lowerHandButton.setOnClickListener { l: View? -> raiseHandViewModel!!.lowerHand() }
+    }
+
+    private fun createCameraEnumerator() {
+        var camera2EnumeratorIsSupported = false
+        try {
+            camera2EnumeratorIsSupported = Camera2Enumerator.isSupported(this)
+        } catch (t: Throwable) {
+            Log.w(TAG, "Camera2Enumerator threw an error", t)
+        }
+        cameraEnumerator = if (camera2EnumeratorIsSupported) {
+            Camera2Enumerator(this)
+        } else {
+            Camera1Enumerator(MagicWebRTCUtils.shouldEnableVideoHardwareAcceleration())
+        }
+    }
+
+    private fun basicInitialization() {
+        rootEglBase = EglBase.create()
+        createCameraEnumerator()
+
+        // Create a new PeerConnectionFactory instance.
+        val options = PeerConnectionFactory.Options()
+        val defaultVideoEncoderFactory = DefaultVideoEncoderFactory(
+            rootEglBase!!.eglBaseContext,
+            true,
+            true
+        )
+        val defaultVideoDecoderFactory = DefaultVideoDecoderFactory(
+            rootEglBase!!.eglBaseContext
+        )
+        peerConnectionFactory = PeerConnectionFactory.builder()
+            .setOptions(options)
+            .setVideoEncoderFactory(defaultVideoEncoderFactory)
+            .setVideoDecoderFactory(defaultVideoDecoderFactory)
+            .createPeerConnectionFactory()
+
+        // Create MediaConstraints - Will be useful for specifying video and audio constraints.
+        audioConstraints = MediaConstraints()
+        videoConstraints = MediaConstraints()
+        localStream = peerConnectionFactory!!.createLocalMediaStream("NCMS")
+
+        // Create and audio manager that will take care of audio routing,
+        // audio modes, audio device enumeration etc.
+        audioManager = WebRtcAudioManager.create(applicationContext, isVoiceOnlyCall)
+        // Store existing audio settings and change audio mode to
+        // MODE_IN_COMMUNICATION for best possible VoIP performance.
+        Log.d(TAG, "Starting the audio manager...")
+        audioManager!!.start(
+            AudioManagerListener { currentDevice: AudioDevice, availableDevices: Set<AudioDevice> ->
+                onAudioManagerDevicesChanged(
+                    currentDevice,
+                    availableDevices
+                )
+            }
+        )
+        if (isVoiceOnlyCall) {
+            setAudioOutputChannel(AudioDevice.EARPIECE)
+        } else {
+            setAudioOutputChannel(AudioDevice.SPEAKER_PHONE)
+        }
+        iceServers = ArrayList()
+
+        // create sdpConstraints
+        sdpConstraints = MediaConstraints()
+        sdpConstraintsForMCU = MediaConstraints()
+        sdpConstraints!!.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
+        var offerToReceiveVideoString = "true"
+        if (isVoiceOnlyCall) {
+            offerToReceiveVideoString = "false"
+        }
+        sdpConstraints!!.mandatory.add(
+            MediaConstraints.KeyValuePair("OfferToReceiveVideo", offerToReceiveVideoString)
+        )
+        sdpConstraintsForMCU!!.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false"))
+        sdpConstraintsForMCU!!.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false"))
+        sdpConstraintsForMCU!!.optional.add(MediaConstraints.KeyValuePair("internalSctpDataChannels", "true"))
+        sdpConstraintsForMCU!!.optional.add(MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"))
+        sdpConstraints!!.optional.add(MediaConstraints.KeyValuePair("internalSctpDataChannels", "true"))
+        sdpConstraints!!.optional.add(MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"))
+        if (!isVoiceOnlyCall) {
+            cameraInitialization()
+        }
+        microphoneInitialization()
+    }
+
+    fun setAudioOutputChannel(selectedAudioDevice: AudioDevice?) {
+        if (audioManager != null) {
+            audioManager!!.selectAudioDevice(selectedAudioDevice)
+            updateAudioOutputButton(audioManager!!.currentAudioDevice)
+        }
+    }
+
+    private fun updateAudioOutputButton(activeAudioDevice: AudioDevice) {
+        when (activeAudioDevice) {
+            AudioDevice.BLUETOOTH -> binding!!.audioOutputButton.setImageResource(
+                R.drawable.ic_baseline_bluetooth_audio_24
+            )
+
+            AudioDevice.SPEAKER_PHONE -> binding!!.audioOutputButton.setImageResource(
+                R.drawable.ic_volume_up_white_24dp
+            )
+
+            AudioDevice.EARPIECE -> binding!!.audioOutputButton.setImageResource(
+                R.drawable.ic_baseline_phone_in_talk_24
+            )
+
+            AudioDevice.WIRED_HEADSET -> binding!!.audioOutputButton.setImageResource(
+                R.drawable.ic_baseline_headset_mic_24
+            )
+
+            else -> Log.e(TAG, "Icon for audio output not available")
+        }
+        DrawableCompat.setTint(binding!!.audioOutputButton.drawable, Color.WHITE)
+    }
+
+    private fun handleFromNotification() {
+        val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
+        ncApi!!.getRooms(credentials, ApiUtils.getUrlForRooms(apiVersion, baseUrl), java.lang.Boolean.FALSE)
+            .retry(3)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+            .subscribe(object : Observer<RoomsOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+
+                override fun onNext(roomsOverall: RoomsOverall) {
+                    for ((roomId1, token) in roomsOverall.ocs!!.data!!) {
+                        if (roomId == roomId1) {
+                            roomToken = token
+                            break
+                        }
+                    }
+                    checkDevicePermissions()
+                }
+
+                override fun onError(e: Throwable) {
+                    // unused atm
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+    }
+
+    @SuppressLint("ClickableViewAccessibility")
+    private fun initViews() {
+        Log.d(TAG, "initViews")
+        binding!!.callInfosLinearLayout.visibility = View.VISIBLE
+        binding!!.selfVideoViewWrapper.visibility = View.VISIBLE
+        if (!isPipModePossible) {
+            binding!!.pictureInPictureButton.visibility = View.GONE
+        }
+        if (isVoiceOnlyCall) {
+            binding!!.switchSelfVideoButton.visibility = View.GONE
+            binding!!.cameraButton.visibility = View.GONE
+            binding!!.selfVideoRenderer.visibility = View.GONE
+            val params = RelativeLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT
+            )
+            params.addRule(RelativeLayout.BELOW, R.id.callInfosLinearLayout)
+            val callControlsHeight =
+                applicationContext.resources.getDimension(R.dimen.call_controls_height).roundToInt()
+            params.setMargins(0, 0, 0, callControlsHeight)
+            binding!!.gridview.layoutParams = params
+        } else {
+            val params = RelativeLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT
+            )
+            params.setMargins(0, 0, 0, 0)
+            binding!!.gridview.layoutParams = params
+            if (cameraEnumerator!!.deviceNames.size < 2) {
+                binding!!.switchSelfVideoButton.visibility = View.GONE
+            }
+            initSelfVideoView()
+        }
+        binding!!.gridview.setOnTouchListener { v, me ->
+            val action = me.actionMasked
+            if (action == MotionEvent.ACTION_DOWN) {
+                animateCallControls(true, 0)
+            }
+            false
+        }
+        binding!!.conversationRelativeLayout.setOnTouchListener { v, me ->
+            val action = me.actionMasked
+            if (action == MotionEvent.ACTION_DOWN) {
+                animateCallControls(true, 0)
+            }
+            false
+        }
+        animateCallControls(true, 0)
+        initGridAdapter()
+    }
+
+    @SuppressLint("ClickableViewAccessibility")
+    private fun initSelfVideoView() {
+        try {
+            binding!!.selfVideoRenderer.init(rootEglBase!!.eglBaseContext, null)
+        } catch (e: IllegalStateException) {
+            Log.d(TAG, "selfVideoRenderer already initialized", e)
+        }
+        binding!!.selfVideoRenderer.setZOrderMediaOverlay(true)
+        // disabled because it causes some devices to crash
+        binding!!.selfVideoRenderer.setEnableHardwareScaler(false)
+        binding!!.selfVideoRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
+        binding!!.selfVideoRenderer.setOnTouchListener(SelfVideoTouchListener())
+    }
+
+    private fun initGridAdapter() {
+        Log.d(TAG, "initGridAdapter")
+        val columns: Int
+        val participantsInGrid = participantDisplayItems!!.size
+        columns = if (resources != null &&
+            resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
+        ) {
+            if (participantsInGrid > 2) {
+                2
+            } else {
+                1
+            }
+        } else {
+            if (participantsInGrid > 2) {
+                3
+            } else if (participantsInGrid > 1) {
+                2
+            } else {
+                1
+            }
+        }
+        binding!!.gridview.numColumns = columns
+        binding!!.conversationRelativeLayout
+            .viewTreeObserver
+            .addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
+                override fun onGlobalLayout() {
+                    binding!!.conversationRelativeLayout.viewTreeObserver.removeOnGlobalLayoutListener(this)
+                    val height = binding!!.conversationRelativeLayout.measuredHeight
+                    binding!!.gridview.minimumHeight = height
+                }
+            })
+        binding!!.callInfosLinearLayout
+            .viewTreeObserver
+            .addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
+                override fun onGlobalLayout() {
+                    binding!!.callInfosLinearLayout.viewTreeObserver.removeOnGlobalLayoutListener(this)
+                }
+            })
+        if (participantsAdapter != null) {
+            participantsAdapter!!.destroy()
+        }
+        participantsAdapter = ParticipantsAdapter(
+            this,
+            participantDisplayItems,
+            binding!!.conversationRelativeLayout,
+            binding!!.callInfosLinearLayout,
+            columns,
+            isVoiceOnlyCall
+        )
+        binding!!.gridview.adapter = participantsAdapter
+        if (isInPipMode) {
+            updateUiForPipMode()
+        }
+    }
+
+    private fun checkDevicePermissions() {
+        val permissionsToRequest: MutableList<String> = ArrayList()
+        val rationaleList: MutableList<String> = ArrayList()
+        if (permissionUtil!!.isMicrophonePermissionGranted()) {
+            if (!microphoneOn) {
+                onMicrophoneClick()
+            }
+        } else if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) {
+            permissionsToRequest.add(Manifest.permission.RECORD_AUDIO)
+            rationaleList.add(resources.getString(R.string.nc_microphone_permission_hint))
+        } else {
+            permissionsToRequest.add(Manifest.permission.RECORD_AUDIO)
+        }
+        if (!isVoiceOnlyCall) {
+            if (permissionUtil!!.isCameraPermissionGranted()) {
+                if (!videoOn) {
+                    onCameraClick()
+                }
+                if (cameraEnumerator!!.deviceNames.size == 0) {
+                    binding!!.cameraButton.visibility = View.GONE
+                }
+                if (cameraEnumerator!!.deviceNames.size > 1) {
+                    binding!!.switchSelfVideoButton.visibility = View.VISIBLE
+                }
+            } else if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
+                permissionsToRequest.add(Manifest.permission.CAMERA)
+                rationaleList.add(resources.getString(R.string.nc_camera_permission_hint))
+            } else {
+                permissionsToRequest.add(Manifest.permission.CAMERA)
+            }
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            if (permissionUtil!!.isBluetoothPermissionGranted()) {
+                enableBluetoothManager()
+            } else if (shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_CONNECT)) {
+                permissionsToRequest.add(Manifest.permission.BLUETOOTH_CONNECT)
+                rationaleList.add(resources.getString(R.string.nc_bluetooth_permission_hint))
+            } else {
+                permissionsToRequest.add(Manifest.permission.BLUETOOTH_CONNECT)
+            }
+        }
+        if (!permissionsToRequest.isEmpty()) {
+            if (!rationaleList.isEmpty()) {
+                showRationaleDialog(permissionsToRequest, rationaleList)
+            } else {
+                requestPermissionLauncher.launch(permissionsToRequest.toTypedArray())
+            }
+        }
+        if (!isConnectionEstablished) {
+            fetchSignalingSettings()
+        }
+    }
+
+    private fun showRationaleDialog(permissionToRequest: String, rationale: String) {
+        val rationaleList: MutableList<String> = ArrayList()
+        val permissionsToRequest: MutableList<String> = ArrayList()
+        rationaleList.add(rationale)
+        permissionsToRequest.add(permissionToRequest)
+        showRationaleDialog(permissionsToRequest, rationaleList)
+    }
+
+    private fun showRationaleDialog(permissionsToRequest: List<String>, rationaleList: List<String>) {
+        val rationalesWithLineBreaks = StringBuilder()
+        for (rationale in rationaleList) {
+            rationalesWithLineBreaks.append(rationale).append("\n\n")
+        }
+        val dialogBuilder = MaterialAlertDialogBuilder(this)
+            .setTitle(R.string.nc_permissions_rationale_dialog_title)
+            .setMessage(rationalesWithLineBreaks)
+            .setPositiveButton(
+                R.string.nc_permissions_ask
+            ) { dialog: DialogInterface?, which: Int ->
+                requestPermissionLauncher.launch(
+                    permissionsToRequest.toTypedArray()
+                )
+            }
+            .setNegativeButton(R.string.nc_common_dismiss, null)
+        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder)
+        dialogBuilder.show()
+    }
+
+    private fun showRationaleDialogForSettings(rationaleList: List<String>) {
+        val rationalesWithLineBreaks = StringBuilder()
+        rationalesWithLineBreaks.append(resources.getString(R.string.nc_permissions_denied))
+        rationalesWithLineBreaks.append('\n')
+        rationalesWithLineBreaks.append(resources.getString(R.string.nc_permissions_settings_hint))
+        rationalesWithLineBreaks.append("\n\n")
+        for (rationale in rationaleList) {
+            rationalesWithLineBreaks.append(rationale).append("\n\n")
+        }
+        val dialogBuilder = MaterialAlertDialogBuilder(this)
+            .setTitle(R.string.nc_permissions_rationale_dialog_title)
+            .setMessage(rationalesWithLineBreaks)
+            .setPositiveButton(R.string.nc_permissions_settings) { dialog: DialogInterface?, which: Int ->
+                val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+                intent.data = Uri.fromParts("package", packageName, null)
+                startActivity(intent)
+            }
+            .setNegativeButton(R.string.nc_common_dismiss, null)
+        viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder)
+        dialogBuilder.show()
+    }
+
+    private val isConnectionEstablished: Boolean
+        get() = currentCallStatus === CallStatus.JOINED || currentCallStatus === CallStatus.IN_CONVERSATION
+
+    private fun onAudioManagerDevicesChanged(
+        currentDevice: AudioDevice,
+        availableDevices: Set<AudioDevice>
+    ) {
+        Log.d(
+            TAG,
+            "onAudioManagerDevicesChanged: " + availableDevices + ", " +
+                "currentDevice: " + currentDevice
+        )
+        val shouldDisableProximityLock =
+            currentDevice == AudioDevice.WIRED_HEADSET ||
+                currentDevice == AudioDevice.SPEAKER_PHONE ||
+                currentDevice == AudioDevice.BLUETOOTH
+        if (shouldDisableProximityLock) {
+            powerManagerUtils!!.updatePhoneState(PowerManagerUtils.PhoneState.WITHOUT_PROXIMITY_SENSOR_LOCK)
+        } else {
+            powerManagerUtils!!.updatePhoneState(PowerManagerUtils.PhoneState.WITH_PROXIMITY_SENSOR_LOCK)
+        }
+        if (audioOutputDialog != null) {
+            audioOutputDialog!!.updateOutputDeviceList()
+        }
+        updateAudioOutputButton(currentDevice)
+    }
+
+    private fun cameraInitialization() {
+        videoCapturer = createCameraCapturer(cameraEnumerator)
+
+        // Create a VideoSource instance
+        if (videoCapturer != null) {
+            val surfaceTextureHelper = SurfaceTextureHelper.create(
+                "CaptureThread",
+                rootEglBase!!.eglBaseContext
+            )
+            videoSource = peerConnectionFactory!!.createVideoSource(false)
+            videoCapturer!!.initialize(surfaceTextureHelper, applicationContext, videoSource!!.capturerObserver)
+        }
+        localVideoTrack = peerConnectionFactory!!.createVideoTrack("NCv0", videoSource)
+        localStream!!.addTrack(localVideoTrack)
+        localVideoTrack!!.setEnabled(false)
+        localVideoTrack!!.addSink(binding!!.selfVideoRenderer)
+    }
+
+    private fun microphoneInitialization() {
+        // create an AudioSource instance
+        audioSource = peerConnectionFactory!!.createAudioSource(audioConstraints)
+        localAudioTrack = peerConnectionFactory!!.createAudioTrack("NCa0", audioSource)
+        localAudioTrack!!.setEnabled(false)
+        localStream!!.addTrack(localAudioTrack)
+    }
+
+    private fun createCameraCapturer(enumerator: CameraEnumerator?): VideoCapturer? {
+        val deviceNames = enumerator!!.deviceNames
+
+        // First, try to find front facing camera
+        Logging.d(TAG, "Looking for front facing cameras.")
+        for (deviceName in deviceNames) {
+            if (enumerator.isFrontFacing(deviceName)) {
+                Logging.d(TAG, "Creating front facing camera capturer.")
+                val videoCapturer: VideoCapturer? = enumerator.createCapturer(deviceName, null)
+                if (videoCapturer != null) {
+                    binding!!.selfVideoRenderer.setMirror(true)
+                    return videoCapturer
+                }
+            }
+        }
+
+        // Front facing camera not found, try something else
+        Logging.d(TAG, "Looking for other cameras.")
+        for (deviceName in deviceNames) {
+            if (!enumerator.isFrontFacing(deviceName)) {
+                Logging.d(TAG, "Creating other camera capturer.")
+                val videoCapturer: VideoCapturer? = enumerator.createCapturer(deviceName, null)
+                if (videoCapturer != null) {
+                    binding!!.selfVideoRenderer.setMirror(false)
+                    return videoCapturer
+                }
+            }
+        }
+        return null
+    }
+
+    fun onMicrophoneClick() {
+        if (!canPublishAudioStream) {
+            microphoneOn = false
+            binding!!.microphoneButton.setImageResource(R.drawable.ic_mic_off_white_24px)
+            toggleMedia(false, false)
+        }
+        if (isVoiceOnlyCall && !isConnectionEstablished) {
+            fetchSignalingSettings()
+        }
+        if (!canPublishAudioStream) {
+            // In the case no audio stream will be published it's not needed to check microphone permissions
+            return
+        }
+        if (permissionUtil!!.isMicrophonePermissionGranted()) {
+            if (!appPreferences.pushToTalkIntroShown) {
+                val primary = viewThemeUtils.getScheme(binding!!.audioOutputButton.context).primary
+                spotlightView = SpotlightView.Builder(this)
+                    .introAnimationDuration(300)
+                    .enableRevealAnimation(true)
+                    .performClick(false)
+                    .fadeinTextDuration(400)
+                    .headingTvColor(primary)
+                    .headingTvSize(20)
+                    .headingTvText(resources.getString(R.string.nc_push_to_talk))
+                    .subHeadingTvColor(resources.getColor(R.color.bg_default, null))
+                    .subHeadingTvSize(16)
+                    .subHeadingTvText(resources.getString(R.string.nc_push_to_talk_desc))
+                    .maskColor(Color.parseColor("#dc000000"))
+                    .target(binding!!.microphoneButton)
+                    .lineAnimDuration(400)
+                    .lineAndArcColor(primary)
+                    .enableDismissAfterShown(true)
+                    .dismissOnBackPress(true)
+                    .usageId("pushToTalk")
+                    .show()
+                appPreferences.pushToTalkIntroShown = true
+            }
+            if (!isPushToTalkActive) {
+                microphoneOn = !microphoneOn
+                if (microphoneOn) {
+                    binding!!.microphoneButton.setImageResource(R.drawable.ic_mic_white_24px)
+                    updatePictureInPictureActions(
+                        R.drawable.ic_mic_white_24px,
+                        resources.getString(R.string.nc_pip_microphone_mute),
+                        MICROPHONE_PIP_REQUEST_MUTE
+                    )
+                } else {
+                    binding!!.microphoneButton.setImageResource(R.drawable.ic_mic_off_white_24px)
+                    updatePictureInPictureActions(
+                        R.drawable.ic_mic_off_white_24px,
+                        resources.getString(R.string.nc_pip_microphone_unmute),
+                        MICROPHONE_PIP_REQUEST_UNMUTE
+                    )
+                }
+                toggleMedia(microphoneOn, false)
+            } else {
+                binding!!.microphoneButton.setImageResource(R.drawable.ic_mic_white_24px)
+                pulseAnimation!!.start()
+                toggleMedia(true, false)
+            }
+        } else if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) {
+            showRationaleDialog(
+                Manifest.permission.RECORD_AUDIO,
+                resources.getString(R.string.nc_microphone_permission_hint)
+            )
+        } else {
+            requestPermissionLauncher.launch(PERMISSIONS_MICROPHONE)
+        }
+    }
+
+    fun onCameraClick() {
+        if (!canPublishVideoStream) {
+            videoOn = false
+            binding!!.cameraButton.setImageResource(R.drawable.ic_videocam_off_white_24px)
+            binding!!.switchSelfVideoButton.visibility = View.GONE
+            return
+        }
+        if (permissionUtil!!.isCameraPermissionGranted()) {
+            videoOn = !videoOn
+            if (videoOn) {
+                binding!!.cameraButton.setImageResource(R.drawable.ic_videocam_white_24px)
+                if (cameraEnumerator!!.deviceNames.size > 1) {
+                    binding!!.switchSelfVideoButton.visibility = View.VISIBLE
+                }
+            } else {
+                binding!!.cameraButton.setImageResource(R.drawable.ic_videocam_off_white_24px)
+                binding!!.switchSelfVideoButton.visibility = View.GONE
+            }
+            toggleMedia(videoOn, true)
+        } else if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
+            showRationaleDialog(
+                Manifest.permission.CAMERA,
+                resources.getString(R.string.nc_camera_permission_hint)
+            )
+        } else {
+            requestPermissionLauncher.launch(PERMISSIONS_CAMERA)
+        }
+    }
+
+    fun switchCamera() {
+        val cameraVideoCapturer = videoCapturer as CameraVideoCapturer?
+        cameraVideoCapturer?.switchCamera(object : CameraSwitchHandler {
+            override fun onCameraSwitchDone(currentCameraIsFront: Boolean) {
+                binding!!.selfVideoRenderer.setMirror(currentCameraIsFront)
+            }
+
+            override fun onCameraSwitchError(s: String) {}
+        })
+    }
+
+    private fun toggleMedia(enable: Boolean, video: Boolean) {
+        var message: String
+        if (video) {
+            message = "videoOff"
+            if (enable) {
+                binding!!.cameraButton.alpha = 1.0f
+                message = "videoOn"
+                startVideoCapture()
+            } else {
+                binding!!.cameraButton.alpha = 0.7f
+                if (videoCapturer != null) {
+                    try {
+                        videoCapturer!!.stopCapture()
+                    } catch (e: InterruptedException) {
+                        Log.d(TAG, "Failed to stop capturing video while sensor is near the ear")
+                    }
+                }
+            }
+            if (localStream != null && localStream!!.videoTracks.size > 0) {
+                localStream!!.videoTracks[0].setEnabled(enable)
+            }
+            if (enable) {
+                binding!!.selfVideoRenderer.visibility = View.VISIBLE
+            } else {
+                binding!!.selfVideoRenderer.visibility = View.INVISIBLE
+            }
+        } else {
+            message = "audioOff"
+            if (enable) {
+                message = "audioOn"
+                binding!!.microphoneButton.alpha = 1.0f
+            } else {
+                binding!!.microphoneButton.alpha = 0.7f
+            }
+            if (localStream != null && localStream!!.audioTracks.size > 0) {
+                localStream!!.audioTracks[0].setEnabled(enable)
+            }
+        }
+        if (isConnectionEstablished && peerConnectionWrapperList != null) {
+            if (!hasMCU) {
+                for (peerConnectionWrapper in peerConnectionWrapperList) {
+                    peerConnectionWrapper.sendChannelData(DataChannelMessage(message))
+                }
+            } else {
+                for (peerConnectionWrapper in peerConnectionWrapperList) {
+                    if (peerConnectionWrapper.sessionId == webSocketClient!!.sessionId) {
+                        peerConnectionWrapper.sendChannelData(DataChannelMessage(message))
+                        break
+                    }
+                }
+            }
+        }
+    }
+
+    fun clickRaiseOrLowerHandButton() {
+        raiseHandViewModel!!.clickHandButton()
+    }
+
+    private fun animateCallControls(show: Boolean, startDelay: Long) {
+        if (isVoiceOnlyCall) {
+            if (spotlightView != null && spotlightView!!.visibility != View.GONE) {
+                spotlightView!!.visibility = View.GONE
+            }
+        } else if (!isPushToTalkActive) {
+            val alpha: Float
+            val duration: Long
+            if (show) {
+                callControlHandler.removeCallbacksAndMessages(null)
+                callInfosHandler.removeCallbacksAndMessages(null)
+                cameraSwitchHandler.removeCallbacksAndMessages(null)
+                alpha = 1.0f
+                duration = 1000
+                if (binding!!.callControls.visibility != View.VISIBLE) {
+                    binding!!.callControls.alpha = 0.0f
+                    binding!!.callControls.visibility = View.VISIBLE
+                    binding!!.callInfosLinearLayout.alpha = 0.0f
+                    binding!!.callInfosLinearLayout.visibility = View.VISIBLE
+                    binding!!.switchSelfVideoButton.alpha = 0.0f
+                    if (videoOn) {
+                        binding!!.switchSelfVideoButton.visibility = View.VISIBLE
+                    }
+                } else {
+                    callControlHandler.postDelayed({ animateCallControls(false, 0) }, 5000)
+                    return
+                }
+            } else {
+                alpha = 0.0f
+                duration = 1000
+            }
+            binding!!.callControls.isEnabled = false
+            binding!!.callControls.animate()
+                .translationY(0f)
+                .alpha(alpha)
+                .setDuration(duration)
+                .setStartDelay(startDelay)
+                .setListener(object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animation: Animator) {
+                        super.onAnimationEnd(animation)
+                        if (!show) {
+                            binding!!.callControls.visibility = View.GONE
+                            if (spotlightView != null && spotlightView!!.visibility != View.GONE) {
+                                spotlightView!!.visibility = View.GONE
+                            }
+                        } else {
+                            callControlHandler.postDelayed({
+                                if (!isPushToTalkActive) {
+                                    animateCallControls(false, 0)
+                                }
+                            }, 7500)
+                        }
+                        binding!!.callControls.isEnabled = true
+                    }
+                })
+            binding!!.callInfosLinearLayout.isEnabled = false
+            binding!!.callInfosLinearLayout.animate()
+                .translationY(0f)
+                .alpha(alpha)
+                .setDuration(duration)
+                .setStartDelay(startDelay)
+                .setListener(object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animation: Animator) {
+                        super.onAnimationEnd(animation)
+                        if (!show) {
+                            binding!!.callInfosLinearLayout.visibility = View.GONE
+                        } else {
+                            callInfosHandler.postDelayed({
+                                if (!isPushToTalkActive) {
+                                    animateCallControls(false, 0)
+                                }
+                            }, 7500)
+                        }
+                        binding!!.callInfosLinearLayout.isEnabled = true
+                    }
+                })
+            binding!!.switchSelfVideoButton.isEnabled = false
+            binding!!.switchSelfVideoButton.animate()
+                .translationY(0f)
+                .alpha(alpha)
+                .setDuration(duration)
+                .setStartDelay(startDelay)
+                .setListener(object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animation: Animator) {
+                        super.onAnimationEnd(animation)
+                        if (!show) {
+                            binding!!.switchSelfVideoButton.visibility = View.GONE
+                        }
+                        binding!!.switchSelfVideoButton.isEnabled = true
+                    }
+                })
+        }
+    }
+
+    public override fun onDestroy() {
+        if (signalingMessageReceiver != null) {
+            signalingMessageReceiver!!.removeListener(localParticipantMessageListener)
+            signalingMessageReceiver!!.removeListener(offerMessageListener)
+        }
+        if (localStream != null) {
+            localStream!!.dispose()
+            localStream = null
+            Log.d(TAG, "Disposed localStream")
+        } else {
+            Log.d(TAG, "localStream is null")
+        }
+        if (currentCallStatus !== CallStatus.LEAVING) {
+            hangup(true)
+        }
+        powerManagerUtils!!.updatePhoneState(PowerManagerUtils.PhoneState.IDLE)
+        super.onDestroy()
+    }
+
+    private fun fetchSignalingSettings() {
+        Log.d(TAG, "fetchSignalingSettings")
+        val apiVersion = ApiUtils.getSignalingApiVersion(conversationUser, intArrayOf(ApiUtils.APIv3, 2, 1))
+        ncApi!!.getSignalingSettings(credentials, ApiUtils.getUrlForSignalingSettings(apiVersion, baseUrl))
+            .subscribeOn(Schedulers.io())
+            .retry(3)
+            .observeOn(AndroidSchedulers.mainThread())
+            .subscribe(object : Observer<SignalingSettingsOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+
+                override fun onNext(signalingSettingsOverall: SignalingSettingsOverall) {
+                    if (signalingSettingsOverall.ocs != null &&
+                        signalingSettingsOverall.ocs!!.settings != null
+                    ) {
+                        externalSignalingServer = ExternalSignalingServer()
+                        if (!TextUtils.isEmpty(
+                                signalingSettingsOverall.ocs!!.settings!!.externalSignalingServer
+                            ) &&
+                            !TextUtils.isEmpty(
+                                    signalingSettingsOverall.ocs!!.settings!!.externalSignalingTicket
+                                )
+                        ) {
+                            externalSignalingServer = ExternalSignalingServer()
+                            externalSignalingServer!!.externalSignalingServer =
+                                signalingSettingsOverall.ocs!!.settings!!.externalSignalingServer
+                            externalSignalingServer!!.externalSignalingTicket =
+                                signalingSettingsOverall.ocs!!.settings!!.externalSignalingTicket
+                            hasExternalSignalingServer = true
+                        } else {
+                            hasExternalSignalingServer = false
+                        }
+                        Log.d(TAG, "   hasExternalSignalingServer: $hasExternalSignalingServer")
+                        if ("?" != conversationUser!!.userId && conversationUser!!.id != null) {
+                            Log.d(
+                                TAG,
+                                "Update externalSignalingServer for: " + conversationUser!!.id +
+                                    " / " + conversationUser!!.userId
+                            )
+                            userManager!!.updateExternalSignalingServer(
+                                conversationUser!!.id!!,
+                                externalSignalingServer!!
+                            )
+                                .subscribeOn(Schedulers.io())
+                                .subscribe()
+                        } else {
+                            conversationUser!!.externalSignalingServer = externalSignalingServer
+                        }
+                        if (signalingSettingsOverall.ocs!!.settings!!.stunServers != null) {
+                            val stunServers = signalingSettingsOverall.ocs!!.settings!!.stunServers
+                            if (apiVersion == ApiUtils.APIv3) {
+                                for ((_, urls) in stunServers!!) {
+                                    if (urls != null) {
+                                        for (url in urls) {
+                                            Log.d(TAG, "   STUN server url: $url")
+                                            iceServers!!.add(PeerConnection.IceServer(url))
+                                        }
+                                    }
+                                }
+                            } else {
+                                if (signalingSettingsOverall.ocs!!.settings!!.stunServers != null) {
+                                    for ((url) in stunServers!!) {
+                                        Log.d(TAG, "   STUN server url: $url")
+                                        iceServers!!.add(PeerConnection.IceServer(url))
+                                    }
+                                }
+                            }
+                        }
+                        if (signalingSettingsOverall.ocs!!.settings!!.turnServers != null) {
+                            val turnServers = signalingSettingsOverall.ocs!!.settings!!.turnServers
+                            for ((_, urls, username, credential) in turnServers!!) {
+                                if (urls != null) {
+                                    for (url in urls) {
+                                        Log.d(TAG, "   TURN server url: $url")
+                                        iceServers!!.add(
+                                            PeerConnection.IceServer(
+                                                url,
+                                                username,
+                                                credential
+                                            )
+                                        )
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    checkCapabilities()
+                }
+
+                override fun onError(e: Throwable) {
+                    Log.e(TAG, e.message, e)
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+    }
+
+    private fun checkCapabilities() {
+        ncApi!!.getCapabilities(credentials, ApiUtils.getUrlForCapabilities(baseUrl))
+            .retry(3)
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+            .subscribe(object : Observer<CapabilitiesOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+
+                override fun onNext(capabilitiesOverall: CapabilitiesOverall) {
+                    // FIXME check for compatible Call API version
+                    if (hasExternalSignalingServer) {
+                        setupAndInitiateWebSocketsConnection()
+                    } else {
+                        signalingMessageReceiver = internalSignalingMessageReceiver
+                        signalingMessageReceiver!!.addListener(localParticipantMessageListener)
+                        signalingMessageReceiver!!.addListener(offerMessageListener)
+                        signalingMessageSender = internalSignalingMessageSender
+                        joinRoomAndCall()
+                    }
+                }
+
+                override fun onError(e: Throwable) {
+                    // unused atm
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+    }
+
+    private fun joinRoomAndCall() {
+        callSession = ApplicationWideCurrentRoomHolder.getInstance().session
+        val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
+        Log.d(TAG, "joinRoomAndCall")
+        Log.d(TAG, "   baseUrl= $baseUrl")
+        Log.d(TAG, "   roomToken= $roomToken")
+        Log.d(TAG, "   callSession= $callSession")
+        val url = ApiUtils.getUrlForParticipantsActive(apiVersion, baseUrl, roomToken)
+        Log.d(TAG, "   url= $url")
+        if (TextUtils.isEmpty(callSession)) {
+            ncApi!!.joinRoom(credentials, url, conversationPassword)
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .retry(3)
+                .subscribe(object : Observer<RoomOverall> {
+                    override fun onSubscribe(d: Disposable) {
+                        // unused atm
+                    }
+
+                    override fun onNext(roomOverall: RoomOverall) {
+                        val conversation = roomOverall.ocs!!.data
+                        callRecordingViewModel!!.setRecordingState(conversation!!.callRecording)
+                        callSession = conversation.sessionId
+                        Log.d(TAG, " new callSession by joinRoom= $callSession")
+                        ApplicationWideCurrentRoomHolder.getInstance().session = callSession
+                        ApplicationWideCurrentRoomHolder.getInstance().currentRoomId = conversation.roomId
+                        ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken = roomToken
+                        ApplicationWideCurrentRoomHolder.getInstance().userInRoom = conversationUser
+                        callOrJoinRoomViaWebSocket()
+                    }
+
+                    override fun onError(e: Throwable) {
+                        Log.e(TAG, "joinRoom onError", e)
+                    }
+
+                    override fun onComplete() {
+                        Log.d(TAG, "joinRoom onComplete")
+                    }
+                })
+        } else {
+            // we are in a room and start a call -> same session needs to be used
+            callOrJoinRoomViaWebSocket()
+        }
+    }
+
+    private fun callOrJoinRoomViaWebSocket() {
+        if (hasExternalSignalingServer) {
+            webSocketClient!!.joinRoomWithRoomTokenAndSession(roomToken!!, callSession)
+        } else {
+            performCall()
+        }
+    }
+
+    private fun performCall() {
+        var inCallFlag = Participant.InCallFlags.IN_CALL
+        if (canPublishAudioStream) {
+            inCallFlag += Participant.InCallFlags.WITH_AUDIO
+        }
+        if (!isVoiceOnlyCall && canPublishVideoStream) {
+            inCallFlag += Participant.InCallFlags.WITH_VIDEO
+        }
+        callParticipantList = CallParticipantList(signalingMessageReceiver)
+        callParticipantList!!.addObserver(callParticipantListObserver)
+        val apiVersion = ApiUtils.getCallApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
+        ncApi!!.joinCall(
+            credentials,
+            ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken),
+            inCallFlag,
+            isCallWithoutNotification
+        )
+            .subscribeOn(Schedulers.io())
+            .retry(3)
+            .observeOn(AndroidSchedulers.mainThread())
+            .subscribe(object : Observer<GenericOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+
+                override fun onNext(genericOverall: GenericOverall) {
+                    if (currentCallStatus !== CallStatus.LEAVING) {
+                        if (currentCallStatus !== CallStatus.IN_CONVERSATION) {
+                            setCallState(CallStatus.JOINED)
+                        }
+                        ApplicationWideCurrentRoomHolder.getInstance().isInCall = true
+                        ApplicationWideCurrentRoomHolder.getInstance().isDialing = false
+                        if (!TextUtils.isEmpty(roomToken)) {
+                            cancelExistingNotificationsForRoom(
+                                applicationContext,
+                                conversationUser!!,
+                                roomToken!!
+                            )
+                        }
+                        if (!hasExternalSignalingServer) {
+                            val signalingApiVersion =
+                                ApiUtils.getSignalingApiVersion(conversationUser, intArrayOf(ApiUtils.APIv3, 2, 1))
+                            val delayOnError = AtomicInteger(0)
+                            ncApi!!.pullSignalingMessages(
+                                credentials,
+                                ApiUtils.getUrlForSignaling(
+                                    signalingApiVersion,
+                                    baseUrl,
+                                    roomToken
+                                )
+                            )
+                                .subscribeOn(Schedulers.io())
+                                .observeOn(AndroidSchedulers.mainThread())
+                                .repeatWhen { observable: Observable<Any?>? -> observable }
+                                .takeWhile { isConnectionEstablished }
+                                .doOnNext { delayOnError.set(0) }
+                                .retryWhen { errors: Observable<Throwable?> ->
+                                    errors
+                                        .flatMap { error: Throwable? ->
+                                            if (!isConnectionEstablished) {
+                                                return@flatMap Observable.error<Long>(error)
+                                            }
+                                            if (delayOnError.get() == 0) {
+                                                delayOnError.set(1)
+                                            } else if (delayOnError.get() < 16) {
+                                                delayOnError.set(delayOnError.get() * 2)
+                                            }
+                                            Observable.timer(delayOnError.get().toLong(), TimeUnit.SECONDS)
+                                        }
+                                }
+                                .subscribe(object : Observer<SignalingOverall> {
+                                    override fun onSubscribe(d: Disposable) {
+                                        signalingDisposable = d
+                                    }
+
+                                    override fun onNext(
+                                        signalingOverall: SignalingOverall
+                                    ) {
+                                        receivedSignalingMessages(signalingOverall.ocs!!.signalings)
+                                    }
+
+                                    override fun onError(e: Throwable) {
+                                        dispose(signalingDisposable)
+                                    }
+
+                                    override fun onComplete() {
+                                        dispose(signalingDisposable)
+                                    }
+                                })
+                        }
+                    }
+                }
+
+                override fun onError(e: Throwable) {
+                    // unused atm
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+    }
+
+    private fun setupAndInitiateWebSocketsConnection() {
+        if (webSocketConnectionHelper == null) {
+            webSocketConnectionHelper = WebSocketConnectionHelper()
+        }
+        if (webSocketClient == null) {
+            webSocketClient = WebSocketConnectionHelper.getExternalSignalingInstanceForServer(
+                externalSignalingServer!!.externalSignalingServer,
+                conversationUser,
+                externalSignalingServer!!.externalSignalingTicket,
+                TextUtils.isEmpty(credentials)
+            )
+            // Although setupAndInitiateWebSocketsConnection could be called several times the web socket is
+            // initialized just once, so the message receiver is also initialized just once.
+            signalingMessageReceiver = webSocketClient!!.getSignalingMessageReceiver()
+            signalingMessageReceiver!!.addListener(localParticipantMessageListener)
+            signalingMessageReceiver!!.addListener(offerMessageListener)
+            signalingMessageSender = webSocketClient!!.signalingMessageSender
+        } else {
+            if (webSocketClient!!.isConnected && currentCallStatus === CallStatus.PUBLISHER_FAILED) {
+                webSocketClient!!.restartWebSocket()
+            }
+        }
+        joinRoomAndCall()
+    }
+
+    private fun initiateCall() {
+        if (!TextUtils.isEmpty(roomToken)) {
+            checkDevicePermissions()
+        } else {
+            handleFromNotification()
+        }
+    }
+
+    @Subscribe(threadMode = ThreadMode.BACKGROUND)
+    fun onMessageEvent(webSocketCommunicationEvent: WebSocketCommunicationEvent) {
+        if (currentCallStatus === CallStatus.LEAVING) {
+            return
+        }
+        if (webSocketCommunicationEvent.getHashMap() != null) {
+            when (webSocketCommunicationEvent.getType()) {
+                "hello" -> {
+                    Log.d(TAG, "onMessageEvent 'hello'")
+                    if (!webSocketCommunicationEvent.getHashMap()!!.containsKey("oldResumeId")) {
+                        if (currentCallStatus === CallStatus.RECONNECTING) {
+                            hangup(false)
+                        } else {
+                            setCallState(CallStatus.RECONNECTING)
+                            runOnUiThread { initiateCall() }
+                        }
+                    }
+                }
+
+                "roomJoined" -> {
+                    Log.d(TAG, "onMessageEvent 'roomJoined'")
+                    startSendingNick()
+                    if (webSocketCommunicationEvent.getHashMap()!!["roomToken"] == roomToken) {
+                        performCall()
+                    }
+                }
+
+                "recordingStatus" -> {
+                    Log.d(TAG, "onMessageEvent 'recordingStatus'")
+                    if (webSocketCommunicationEvent.getHashMap()!!.containsKey(KEY_RECORDING_STATE)) {
+                        val recordingStateString = webSocketCommunicationEvent.getHashMap()!![KEY_RECORDING_STATE]
+                        if (recordingStateString != null) {
+                            runOnUiThread { callRecordingViewModel!!.setRecordingState(recordingStateString.toInt()) }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private fun dispose(disposable: Disposable?) {
+        if (disposable != null && !disposable.isDisposed) {
+            disposable.dispose()
+        } else if (disposable == null) {
+            if (signalingDisposable != null && !signalingDisposable!!.isDisposed) {
+                signalingDisposable!!.dispose()
+                signalingDisposable = null
+            }
+        }
+    }
+
+    private fun receivedSignalingMessages(signalingList: List<Signaling>?) {
+        if (signalingList != null) {
+            for (signaling in signalingList) {
+                try {
+                    receivedSignalingMessage(signaling)
+                } catch (e: IOException) {
+                    Log.e(TAG, "Failed to process received signaling message", e)
+                }
+            }
+        }
+    }
+
+    @Throws(IOException::class)
+    private fun receivedSignalingMessage(signaling: Signaling) {
+        val messageType = signaling.type
+        if (!isConnectionEstablished && currentCallStatus !== CallStatus.CONNECTING) {
+            return
+        }
+        if ("usersInRoom" == messageType) {
+            internalSignalingMessageReceiver.process(signaling.messageWrapper as List<Map<String?, Any?>?>?)
+        } else if ("message" == messageType) {
+            val ncSignalingMessage = LoganSquare.parse(
+                signaling.messageWrapper.toString(),
+                NCSignalingMessage::class.java
+            )
+            internalSignalingMessageReceiver.process(ncSignalingMessage)
+        } else {
+            Log.e(TAG, "unexpected message type when receiving signaling message")
+        }
+    }
+
+    private fun hangup(shutDownView: Boolean) {
+        Log.d(TAG, "hangup! shutDownView=$shutDownView")
+        if (shutDownView) {
+            setCallState(CallStatus.LEAVING)
+        }
+        stopCallingSound()
+        dispose(null)
+        if (shutDownView) {
+            if (videoCapturer != null) {
+                try {
+                    videoCapturer!!.stopCapture()
+                } catch (e: InterruptedException) {
+                    Log.e(TAG, "Failed to stop capturing while hanging up")
+                }
+                videoCapturer!!.dispose()
+                videoCapturer = null
+            }
+            binding!!.selfVideoRenderer.release()
+            if (audioSource != null) {
+                audioSource!!.dispose()
+                audioSource = null
+            }
+            runOnUiThread {
+                if (audioManager != null) {
+                    audioManager!!.stop()
+                    audioManager = null
+                }
+            }
+            if (videoSource != null) {
+                videoSource = null
+            }
+            if (peerConnectionFactory != null) {
+                peerConnectionFactory = null
+            }
+            localAudioTrack = null
+            localVideoTrack = null
+            if (TextUtils.isEmpty(credentials) && hasExternalSignalingServer) {
+                WebSocketConnectionHelper.deleteExternalSignalingInstanceForUserEntity(-1)
+            }
+        }
+        val peerConnectionIdsToEnd: MutableList<String> = ArrayList(
+            peerConnectionWrapperList!!.size
+        )
+        for (wrapper in peerConnectionWrapperList) {
+            peerConnectionIdsToEnd.add(wrapper.sessionId)
+        }
+        for (sessionId in peerConnectionIdsToEnd) {
+            endPeerConnection(sessionId, "video")
+            endPeerConnection(sessionId, "screen")
+        }
+        val callParticipantIdsToEnd: MutableList<String> = ArrayList(
+            peerConnectionWrapperList.size
+        )
+        for (callParticipant in callParticipants.values) {
+            callParticipantIdsToEnd.add(callParticipant!!.callParticipantModel.sessionId)
+        }
+        for (sessionId in callParticipantIdsToEnd) {
+            removeCallParticipant(sessionId)
+        }
+        ApplicationWideCurrentRoomHolder.getInstance().isInCall = false
+        ApplicationWideCurrentRoomHolder.getInstance().isDialing = false
+        hangupNetworkCalls(shutDownView)
+    }
+
+    private fun hangupNetworkCalls(shutDownView: Boolean) {
+        Log.d(TAG, "hangupNetworkCalls. shutDownView=$shutDownView")
+        val apiVersion = ApiUtils.getCallApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
+        if (callParticipantList != null) {
+            callParticipantList!!.removeObserver(callParticipantListObserver)
+            callParticipantList!!.destroy()
+        }
+        ncApi!!.leaveCall(credentials, ApiUtils.getUrlForCall(apiVersion, baseUrl, roomToken))
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+            .subscribe(object : Observer<GenericOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+
+                override fun onNext(genericOverall: GenericOverall) {
+                    if (!switchToRoomToken.isEmpty()) {
+                        val intent = Intent(context, ChatActivity::class.java)
+                        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+                        val bundle = Bundle()
+                        bundle.putBoolean(KEY_SWITCH_TO_ROOM, true)
+                        bundle.putBoolean(KEY_START_CALL_AFTER_ROOM_SWITCH, true)
+                        bundle.putString(KEY_ROOM_TOKEN, switchToRoomToken)
+                        bundle.putBoolean(KEY_CALL_VOICE_ONLY, isVoiceOnlyCall)
+                        intent.putExtras(bundle)
+                        startActivity(intent)
+                        finish()
+                    } else if (shutDownView) {
+                        finish()
+                    } else if (currentCallStatus === CallStatus.RECONNECTING ||
+                        currentCallStatus === CallStatus.PUBLISHER_FAILED
+                    ) {
+                        initiateCall()
+                    }
+                }
+
+                override fun onError(e: Throwable) {
+                    Log.e(TAG, "Error while leaving the call", e)
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+    }
+
+    private fun startVideoCapture() {
+        if (videoCapturer != null) {
+            videoCapturer!!.startCapture(1280, 720, 30)
+        }
+    }
+
+    private fun handleCallParticipantsChanged(
+        joined: Collection<Participant>,
+        updated: Collection<Participant>,
+        left: Collection<Participant>,
+        unchanged: Collection<Participant>
+    ) {
+        Log.d(TAG, "handleCallParticipantsChanged")
+        hasMCU = hasExternalSignalingServer && webSocketClient != null && webSocketClient!!.hasMCU()
+        Log.d(TAG, "   hasMCU is $hasMCU")
+
+        // The signaling session is the same as the Nextcloud session only when the MCU is not used.
+        var currentSessionId = callSession
+        if (hasMCU) {
+            currentSessionId = webSocketClient!!.sessionId
+        }
+        Log.d(TAG, "   currentSessionId is $currentSessionId")
+        val participantsInCall: MutableList<Participant> = ArrayList()
+        participantsInCall.addAll(joined)
+        participantsInCall.addAll(updated)
+        participantsInCall.addAll(unchanged)
+        var isSelfInCall = false
+        var selfParticipant: Participant? = null
+        for (participant in participantsInCall) {
+            val inCallFlag = participant.inCall
+            if (participant.sessionId != currentSessionId) {
+                Log.d(
+                    TAG,
+                    "   inCallFlag of participant " +
+                        participant.sessionId!!.substring(0, 4) +
+                        " : " +
+                        inCallFlag
+                )
+            } else {
+                Log.d(TAG, "   inCallFlag of currentSessionId: $inCallFlag")
+                isSelfInCall = inCallFlag != 0L
+                selfParticipant = participant
+            }
+        }
+        if (!isSelfInCall &&
+            currentCallStatus !== CallStatus.LEAVING &&
+            ApplicationWideCurrentRoomHolder.getInstance().isInCall
+        ) {
+            Log.d(TAG, "Most probably a moderator ended the call for all.")
+            hangup(true)
+            return
+        }
+        if (!isSelfInCall) {
+            Log.d(TAG, "Self not in call, disconnecting from all other sessions")
+            for ((_, _, _, _, _, _, _, _, _, _, sessionId) in participantsInCall) {
+                Log.d(TAG, "   session that will be removed is: $sessionId")
+                endPeerConnection(sessionId, "video")
+                endPeerConnection(sessionId, "screen")
+                removeCallParticipant(sessionId)
+            }
+            return
+        }
+        if (currentCallStatus === CallStatus.LEAVING) {
+            return
+        }
+        if (hasMCU) {
+            // Ensure that own publishing peer is set up.
+            getOrCreatePeerConnectionWrapperForSessionIdAndType(
+                webSocketClient!!.sessionId,
+                VIDEO_STREAM_TYPE_VIDEO,
+                true
+            )
+        }
+        var selfJoined = false
+        val selfParticipantHasAudioOrVideo = participantInCallFlagsHaveAudioOrVideo(selfParticipant)
+        for (participant in joined) {
+            val sessionId = participant.sessionId
+            if (sessionId == null) {
+                Log.w(TAG, "Null sessionId for call participant, this should not happen: $participant")
+                continue
+            }
+            if (sessionId == currentSessionId) {
+                selfJoined = true
+                continue
+            }
+            Log.d(TAG, "   newSession joined: $sessionId")
+            addCallParticipant(sessionId)
+            val userId = participant.userId
+            if (userId != null) {
+                callParticipants[sessionId]!!.setUserId(userId)
+            }
+            if (participant.internal != null) {
+                callParticipants[sessionId]!!.setInternal(participant.internal)
+            }
+            var nick: String?
+            nick = if (hasExternalSignalingServer) {
+                webSocketClient!!.getDisplayNameForSession(sessionId)
+            } else {
+                if (offerAnswerNickProviders[sessionId] != null) offerAnswerNickProviders[sessionId]?.nick else ""
+            }
+            callParticipants[sessionId]!!.setNick(nick)
+            val participantHasAudioOrVideo = participantInCallFlagsHaveAudioOrVideo(participant)
+
+            // FIXME Without MCU, PeerConnectionWrapper only sends an offer if the local session ID is higher than the
+            // remote session ID. However, if the other participant does not have audio nor video that participant
+            // will not send an offer, so no connection is actually established when the remote participant has a
+            // higher session ID but is not publishing media.
+            if (hasMCU && participantHasAudioOrVideo || !hasMCU && selfParticipantHasAudioOrVideo && (
+                !participantHasAudioOrVideo || sessionId.compareTo(
+                        currentSessionId!!
+                    ) < 0
+                )
+            ) {
+                getOrCreatePeerConnectionWrapperForSessionIdAndType(sessionId, VIDEO_STREAM_TYPE_VIDEO, false)
+            }
+        }
+        val othersInCall = if (selfJoined) joined.size > 1 else joined.size > 0
+        if (othersInCall && currentCallStatus !== CallStatus.IN_CONVERSATION) {
+            setCallState(CallStatus.IN_CONVERSATION)
+        }
+        for ((_, _, _, _, _, _, _, _, _, _, sessionId) in left) {
+            Log.d(TAG, "   oldSession that will be removed is: $sessionId")
+            endPeerConnection(sessionId, "video")
+            endPeerConnection(sessionId, "screen")
+            removeCallParticipant(sessionId)
+        }
+    }
+
+    private fun participantInCallFlagsHaveAudioOrVideo(participant: Participant?): Boolean {
+        return if (participant == null) {
+            false
+        } else {
+            participant.inCall and Participant.InCallFlags.WITH_AUDIO.toLong() > 0 ||
+                !isVoiceOnlyCall &&
+                participant.inCall and Participant.InCallFlags.WITH_VIDEO.toLong() > 0
+        }
+    }
+
+    private fun getPeerConnectionWrapperForSessionIdAndType(sessionId: String?, type: String): PeerConnectionWrapper? {
+        for (wrapper in peerConnectionWrapperList!!) {
+            if (wrapper.sessionId == sessionId && wrapper.videoStreamType == type) {
+                return wrapper
+            }
+        }
+        return null
+    }
+
+    private fun getOrCreatePeerConnectionWrapperForSessionIdAndType(
+        sessionId: String?,
+        type: String,
+        publisher: Boolean
+    ): PeerConnectionWrapper? {
+        var peerConnectionWrapper: PeerConnectionWrapper?
+        peerConnectionWrapper = getPeerConnectionWrapperForSessionIdAndType(sessionId, type)
+
+        return if (peerConnectionWrapper != null) {
+            peerConnectionWrapper
+        } else {
+            if (peerConnectionFactory == null) {
+                Log.e(TAG, "peerConnectionFactory was null in getOrCreatePeerConnectionWrapperForSessionIdAndType")
+                Toast.makeText(
+                    context,
+                    context.resources.getString(R.string.nc_common_error_sorry),
+                    Toast.LENGTH_LONG
+                ).show()
+                hangup(true)
+                return null
+            }
+            peerConnectionWrapper = if (hasMCU && publisher) {
+                PeerConnectionWrapper(
+                    peerConnectionFactory,
+                    iceServers,
+                    sdpConstraintsForMCU,
+                    sessionId,
+                    callSession,
+                    localStream,
+                    true,
+                    true,
+                    type,
+                    signalingMessageReceiver,
+                    signalingMessageSender
+                )
+            } else if (hasMCU) {
+                PeerConnectionWrapper(
+                    peerConnectionFactory,
+                    iceServers,
+                    sdpConstraints,
+                    sessionId,
+                    callSession,
+                    null,
+                    false,
+                    true,
+                    type,
+                    signalingMessageReceiver,
+                    signalingMessageSender
+                )
+            } else {
+                if ("screen" != type) {
+                    PeerConnectionWrapper(
+                        peerConnectionFactory,
+                        iceServers,
+                        sdpConstraints,
+                        sessionId,
+                        callSession,
+                        localStream,
+                        false,
+                        false,
+                        type,
+                        signalingMessageReceiver,
+                        signalingMessageSender
+                    )
+                } else {
+                    PeerConnectionWrapper(
+                        peerConnectionFactory,
+                        iceServers,
+                        sdpConstraints,
+                        sessionId,
+                        callSession,
+                        null,
+                        false,
+                        false,
+                        type,
+                        signalingMessageReceiver,
+                        signalingMessageSender
+                    )
+                }
+            }
+            peerConnectionWrapperList!!.add(peerConnectionWrapper)
+            if (!publisher) {
+                var callParticipant = callParticipants[sessionId]
+                if (callParticipant == null) {
+                    callParticipant = addCallParticipant(sessionId)
+                }
+                if ("screen" == type) {
+                    callParticipant.setScreenPeerConnectionWrapper(peerConnectionWrapper)
+                } else {
+                    callParticipant.setPeerConnectionWrapper(peerConnectionWrapper)
+                }
+            }
+            if (publisher) {
+                peerConnectionWrapper.addObserver(selfPeerConnectionObserver)
+                startSendingNick()
+            }
+            peerConnectionWrapper
+        }
+    }
+
+    private fun addCallParticipant(sessionId: String?): CallParticipant {
+        val callParticipant = CallParticipant(sessionId, signalingMessageReceiver)
+        callParticipants[sessionId] = callParticipant
+        val callParticipantMessageListener: CallParticipantMessageListener =
+            CallActivityCallParticipantMessageListener(sessionId)
+        callParticipantMessageListeners[sessionId] = callParticipantMessageListener
+        signalingMessageReceiver!!.addListener(callParticipantMessageListener, sessionId)
+        if (!hasExternalSignalingServer) {
+            val offerAnswerNickProvider = OfferAnswerNickProvider(sessionId)
+            offerAnswerNickProviders[sessionId] = offerAnswerNickProvider
+            signalingMessageReceiver!!.addListener(
+                offerAnswerNickProvider.videoWebRtcMessageListener,
+                sessionId,
+                "video"
+            )
+            signalingMessageReceiver!!.addListener(
+                offerAnswerNickProvider.screenWebRtcMessageListener,
+                sessionId,
+                "screen"
+            )
+        }
+        val callParticipantModel = callParticipant.callParticipantModel
+        val screenParticipantDisplayItemManager = ScreenParticipantDisplayItemManager(callParticipantModel)
+        screenParticipantDisplayItemManagers[sessionId] = screenParticipantDisplayItemManager
+        callParticipantModel.addObserver(
+            screenParticipantDisplayItemManager,
+            screenParticipantDisplayItemManagersHandler
+        )
+        val callParticipantEventDisplayer = CallParticipantEventDisplayer(callParticipantModel)
+        callParticipantEventDisplayers[sessionId] = callParticipantEventDisplayer
+        callParticipantModel.addObserver(callParticipantEventDisplayer, callParticipantEventDisplayersHandler)
+        runOnUiThread { addParticipantDisplayItem(callParticipantModel, "video") }
+        return callParticipant
+    }
+
+    private fun endPeerConnection(sessionId: String?, type: String) {
+        val peerConnectionWrapper = getPeerConnectionWrapperForSessionIdAndType(sessionId, type) ?: return
+        if (webSocketClient != null &&
+            webSocketClient!!.sessionId != null &&
+            webSocketClient!!.sessionId == sessionId
+        ) {
+            peerConnectionWrapper.removeObserver(selfPeerConnectionObserver)
+        }
+        val callParticipant = callParticipants[sessionId]
+        if (callParticipant != null) {
+            if ("screen" == type) {
+                callParticipant.setScreenPeerConnectionWrapper(null)
+            } else {
+                callParticipant.setPeerConnectionWrapper(null)
+            }
+        }
+        peerConnectionWrapper.removePeerConnection()
+        peerConnectionWrapperList!!.remove(peerConnectionWrapper)
+    }
+
+    private fun removeCallParticipant(sessionId: String?) {
+        val callParticipant = callParticipants.remove(sessionId) ?: return
+        val screenParticipantDisplayItemManager = screenParticipantDisplayItemManagers.remove(sessionId)
+        callParticipant.callParticipantModel.removeObserver(screenParticipantDisplayItemManager)
+        val callParticipantEventDisplayer = callParticipantEventDisplayers.remove(sessionId)
+        callParticipant.callParticipantModel.removeObserver(callParticipantEventDisplayer)
+        callParticipant.destroy()
+        val listener = callParticipantMessageListeners.remove(sessionId)
+        signalingMessageReceiver!!.removeListener(listener)
+        val offerAnswerNickProvider = offerAnswerNickProviders.remove(sessionId)
+        if (offerAnswerNickProvider != null) {
+            signalingMessageReceiver!!.removeListener(offerAnswerNickProvider.videoWebRtcMessageListener)
+            signalingMessageReceiver!!.removeListener(offerAnswerNickProvider.screenWebRtcMessageListener)
+        }
+        runOnUiThread { removeParticipantDisplayItem(sessionId, "video") }
+    }
+
+    private fun removeParticipantDisplayItem(sessionId: String?, videoStreamType: String) {
+        Log.d(TAG, "removeParticipantDisplayItem")
+        val participantDisplayItem = participantDisplayItems!!.remove("$sessionId-$videoStreamType") ?: return
+        participantDisplayItem.destroy()
+        if (!isDestroyed) {
+            initGridAdapter()
+        }
+    }
+
+    @Subscribe(threadMode = ThreadMode.MAIN)
+    fun onMessageEvent(configurationChangeEvent: ConfigurationChangeEvent?) {
+        powerManagerUtils!!.setOrientation(Objects.requireNonNull(resources).configuration.orientation)
+        initGridAdapter()
+        updateSelfVideoViewPosition()
+    }
+
+    private fun updateSelfVideoViewIceConnectionState(iceConnectionState: IceConnectionState) {
+        val connected = iceConnectionState == IceConnectionState.CONNECTED ||
+            iceConnectionState == IceConnectionState.COMPLETED
+
+        // FIXME In voice only calls there is no video view, so the progress bar would appear floating in the middle of
+        // nowhere. However, a way to signal that the local participant is not connected to the HPB is still need in
+        // that case.
+        if (!connected && !isVoiceOnlyCall) {
+            binding!!.selfVideoViewProgressBar.visibility = View.VISIBLE
+        } else {
+            binding!!.selfVideoViewProgressBar.visibility = View.GONE
+        }
+    }
+
+    private fun updateSelfVideoViewPosition() {
+        Log.d(TAG, "updateSelfVideoViewPosition")
+        if (!isInPipMode) {
+            val layoutParams = binding!!.selfVideoRenderer.layoutParams as FrameLayout.LayoutParams
+            val displayMetrics = applicationContext.resources.displayMetrics
+            val screenWidthPx = displayMetrics.widthPixels
+            val screenWidthDp = DisplayUtils.convertPixelToDp(screenWidthPx.toFloat(), applicationContext).toInt()
+            var newXafterRotate = 0f
+            val newYafterRotate: Float
+            newYafterRotate = if (binding!!.callInfosLinearLayout.visibility == View.VISIBLE) {
+                250f
+            } else {
+                20f
+            }
+            if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
+                layoutParams.height = resources.getDimension(R.dimen.call_self_video_short_side_length).toInt()
+                layoutParams.width = resources.getDimension(R.dimen.call_self_video_long_side_length).toInt()
+                newXafterRotate =
+                    (screenWidthDp - resources.getDimension(R.dimen.call_self_video_short_side_length) * 0.8).toFloat()
+            } else if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
+                layoutParams.height = resources.getDimension(R.dimen.call_self_video_long_side_length).toInt()
+                layoutParams.width = resources.getDimension(R.dimen.call_self_video_short_side_length).toInt()
+                newXafterRotate =
+                    (screenWidthDp - resources.getDimension(R.dimen.call_self_video_short_side_length) * 0.5).toFloat()
+            }
+            binding!!.selfVideoRenderer.layoutParams = layoutParams
+            val newXafterRotatePx = DisplayUtils.convertDpToPixel(newXafterRotate, applicationContext).toInt()
+            binding!!.selfVideoViewWrapper.y = newYafterRotate
+            binding!!.selfVideoViewWrapper.x = newXafterRotatePx.toFloat()
+        }
+    }
+
+    @Subscribe(threadMode = ThreadMode.MAIN)
+    fun onMessageEvent(proximitySensorEvent: ProximitySensorEvent) {
+        if (!isVoiceOnlyCall) {
+            val enableVideo = proximitySensorEvent.proximitySensorEventType ==
+                ProximitySensorEvent.ProximitySensorEventType.SENSOR_FAR && videoOn
+            if (permissionUtil!!.isCameraPermissionGranted() &&
+                (currentCallStatus === CallStatus.CONNECTING || isConnectionEstablished) &&
+                videoOn && enableVideo != localVideoTrack!!.enabled()
+            ) {
+                toggleMedia(enableVideo, true)
+            }
+        }
+    }
+
+    private fun startSendingNick() {
+        val dataChannelMessage = DataChannelMessage()
+        dataChannelMessage.type = "nickChanged"
+        val nickChangedPayload: MutableMap<String, String> = HashMap()
+        nickChangedPayload["userid"] = conversationUser!!.userId!!
+        nickChangedPayload["name"] = conversationUser!!.displayName!!
+        dataChannelMessage.payloadMap = nickChangedPayload.toMap()
+        for (peerConnectionWrapper in peerConnectionWrapperList!!) {
+            if (peerConnectionWrapper.isMCUPublisher) {
+                Observable
+                    .interval(1, TimeUnit.SECONDS)
+                    .repeatUntil { !isConnectionEstablished || isDestroyed }
+                    .observeOn(Schedulers.io())
+                    .subscribe(object : Observer<Long> {
+                        override fun onSubscribe(d: Disposable) {
+                            // unused atm
+                        }
+
+                        override fun onNext(aLong: Long) {
+                            peerConnectionWrapper.sendChannelData(dataChannelMessage)
+                        }
+
+                        override fun onError(e: Throwable) {
+                            // unused atm
+                        }
+
+                        override fun onComplete() {
+                            // unused atm
+                        }
+                    })
+                break
+            }
+        }
+    }
+
+    private fun addParticipantDisplayItem(callParticipantModel: CallParticipantModel, videoStreamType: String) {
+        if (callParticipantModel.isInternal != null && callParticipantModel.isInternal) {
+            return
+        }
+        val defaultGuestNick = resources.getString(R.string.nc_nick_guest)
+        val participantDisplayItem = ParticipantDisplayItem(
+            baseUrl,
+            defaultGuestNick,
+            rootEglBase,
+            videoStreamType,
+            callParticipantModel
+        )
+        val sessionId = callParticipantModel.sessionId
+        participantDisplayItems!!["$sessionId-$videoStreamType"] = participantDisplayItem
+        initGridAdapter()
+    }
+
+    private fun setCallState(callState: CallStatus) {
+        if (currentCallStatus == null || currentCallStatus !== callState) {
+            currentCallStatus = callState
+            if (handler == null) {
+                handler = Handler(Looper.getMainLooper())
+            } else {
+                handler!!.removeCallbacksAndMessages(null)
+            }
+            when (callState) {
+                CallStatus.CONNECTING -> handler!!.post {
+                    playCallingSound()
+                    if (isIncomingCallFromNotification) {
+                        binding!!.callStates.callStateTextView.setText(R.string.nc_call_incoming)
+                    } else {
+                        binding!!.callStates.callStateTextView.setText(R.string.nc_call_ringing)
+                    }
+                    binding!!.callConversationNameTextView.text = conversationName
+                    binding!!.callModeTextView.text = descriptionForCallType
+                    if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) {
+                        binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
+                    }
+                    if (binding!!.gridview.visibility != View.INVISIBLE) {
+                        binding!!.gridview.visibility = View.INVISIBLE
+                    }
+                    if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) {
+                        binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE
+                    }
+                    if (binding!!.callStates.errorImageView.visibility != View.GONE) {
+                        binding!!.callStates.errorImageView.visibility = View.GONE
+                    }
+                }
+
+                CallStatus.CALLING_TIMEOUT -> handler!!.post {
+                    hangup(false)
+                    binding!!.callStates.callStateTextView.setText(R.string.nc_call_timeout)
+                    binding!!.callModeTextView.text = descriptionForCallType
+                    if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) {
+                        binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
+                    }
+                    if (binding!!.callStates.callStateProgressBar.visibility != View.GONE) {
+                        binding!!.callStates.callStateProgressBar.visibility = View.GONE
+                    }
+                    if (binding!!.gridview.visibility != View.INVISIBLE) {
+                        binding!!.gridview.visibility = View.INVISIBLE
+                    }
+                    binding!!.callStates.errorImageView.setImageResource(R.drawable.ic_av_timer_timer_24dp)
+                    if (binding!!.callStates.errorImageView.visibility != View.VISIBLE) {
+                        binding!!.callStates.errorImageView.visibility = View.VISIBLE
+                    }
+                }
+
+                CallStatus.PUBLISHER_FAILED -> handler!!.post {
+                    // No calling sound when the publisher failed
+                    binding!!.callStates.callStateTextView.setText(R.string.nc_call_reconnecting)
+                    binding!!.callModeTextView.text = descriptionForCallType
+                    if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) {
+                        binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
+                    }
+                    if (binding!!.gridview.visibility != View.INVISIBLE) {
+                        binding!!.gridview.visibility = View.INVISIBLE
+                    }
+                    if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) {
+                        binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE
+                    }
+                    if (binding!!.callStates.errorImageView.visibility != View.GONE) {
+                        binding!!.callStates.errorImageView.visibility = View.GONE
+                    }
+                }
+
+                CallStatus.RECONNECTING -> handler!!.post {
+                    playCallingSound()
+                    binding!!.callStates.callStateTextView.setText(R.string.nc_call_reconnecting)
+                    binding!!.callModeTextView.text = descriptionForCallType
+                    if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) {
+                        binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
+                    }
+                    if (binding!!.gridview.visibility != View.INVISIBLE) {
+                        binding!!.gridview.visibility = View.INVISIBLE
+                    }
+                    if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) {
+                        binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE
+                    }
+                    if (binding!!.callStates.errorImageView.visibility != View.GONE) {
+                        binding!!.callStates.errorImageView.visibility = View.GONE
+                    }
+                }
+
+                CallStatus.JOINED -> {
+                    handler!!.postDelayed({ setCallState(CallStatus.CALLING_TIMEOUT) }, 45000)
+                    handler!!.post {
+                        binding!!.callModeTextView.text = descriptionForCallType
+                        if (isIncomingCallFromNotification) {
+                            binding!!.callStates.callStateTextView.setText(R.string.nc_call_incoming)
+                        } else {
+                            binding!!.callStates.callStateTextView.setText(R.string.nc_call_ringing)
+                        }
+                        if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) {
+                            binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
+                        }
+                        if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) {
+                            binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE
+                        }
+                        if (binding!!.gridview.visibility != View.INVISIBLE) {
+                            binding!!.gridview.visibility = View.INVISIBLE
+                        }
+                        if (binding!!.callStates.errorImageView.visibility != View.GONE) {
+                            binding!!.callStates.errorImageView.visibility = View.GONE
+                        }
+                    }
+                }
+
+                CallStatus.IN_CONVERSATION -> handler!!.post {
+                    stopCallingSound()
+                    binding!!.callModeTextView.text = descriptionForCallType
+                    if (!isVoiceOnlyCall) {
+                        binding!!.callInfosLinearLayout.visibility = View.GONE
+                    }
+                    if (!isPushToTalkActive) {
+                        animateCallControls(false, 5000)
+                    }
+                    if (binding!!.callStates.callStateRelativeLayout.visibility != View.INVISIBLE) {
+                        binding!!.callStates.callStateRelativeLayout.visibility = View.INVISIBLE
+                    }
+                    if (binding!!.callStates.callStateProgressBar.visibility != View.GONE) {
+                        binding!!.callStates.callStateProgressBar.visibility = View.GONE
+                    }
+                    if (binding!!.gridview.visibility != View.VISIBLE) {
+                        binding!!.gridview.visibility = View.VISIBLE
+                    }
+                    if (binding!!.callStates.errorImageView.visibility != View.GONE) {
+                        binding!!.callStates.errorImageView.visibility = View.GONE
+                    }
+                }
+
+                CallStatus.OFFLINE -> handler!!.post {
+                    stopCallingSound()
+                    binding!!.callStates.callStateTextView.setText(R.string.nc_offline)
+                    if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) {
+                        binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
+                    }
+                    if (binding!!.gridview.visibility != View.INVISIBLE) {
+                        binding!!.gridview.visibility = View.INVISIBLE
+                    }
+                    if (binding!!.callStates.callStateProgressBar.visibility != View.GONE) {
+                        binding!!.callStates.callStateProgressBar.visibility = View.GONE
+                    }
+                    binding!!.callStates.errorImageView.setImageResource(R.drawable.ic_signal_wifi_off_white_24dp)
+                    if (binding!!.callStates.errorImageView.visibility != View.VISIBLE) {
+                        binding!!.callStates.errorImageView.visibility = View.VISIBLE
+                    }
+                }
+
+                CallStatus.LEAVING -> handler!!.post {
+                    if (!isDestroyed) {
+                        stopCallingSound()
+                        binding!!.callModeTextView.text = descriptionForCallType
+                        binding!!.callStates.callStateTextView.setText(R.string.nc_leaving_call)
+                        binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
+                        binding!!.gridview.visibility = View.INVISIBLE
+                        binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE
+                        binding!!.callStates.errorImageView.visibility = View.GONE
+                    }
+                }
+
+                else -> {}
+            }
+        }
+    }
+
+    private val descriptionForCallType: String
+        private get() {
+            val appName = resources.getString(R.string.nc_app_product_name)
+            return if (isVoiceOnlyCall) {
+                String.format(resources.getString(R.string.nc_call_voice), appName)
+            } else {
+                String.format(resources.getString(R.string.nc_call_video), appName)
+            }
+        }
+
+    private fun playCallingSound() {
+        stopCallingSound()
+        val ringtoneUri: Uri?
+        ringtoneUri = if (isIncomingCallFromNotification) {
+            getCallRingtoneUri(applicationContext, appPreferences)
+        } else {
+            Uri.parse(
+                "android.resource://" + applicationContext.packageName + "/raw" +
+                    "/tr110_1_kap8_3_freiton1"
+            )
+        }
+        if (ringtoneUri != null) {
+            mediaPlayer = MediaPlayer()
+            try {
+                mediaPlayer!!.setDataSource(this, ringtoneUri)
+                mediaPlayer!!.isLooping = true
+                val audioAttributes = AudioAttributes.Builder().setContentType(
+                    AudioAttributes.CONTENT_TYPE_SONIFICATION
+                )
+                    .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
+                    .build()
+                mediaPlayer!!.setAudioAttributes(audioAttributes)
+                mediaPlayer!!.setOnPreparedListener { mp: MediaPlayer? -> mediaPlayer!!.start() }
+                mediaPlayer!!.prepareAsync()
+            } catch (e: IOException) {
+                Log.e(TAG, "Failed to play sound")
+            }
+        }
+    }
+
+    private fun stopCallingSound() {
+        if (mediaPlayer != null) {
+            try {
+                if (mediaPlayer!!.isPlaying) {
+                    mediaPlayer!!.stop()
+                }
+            } catch (e: IllegalStateException) {
+                Log.e(TAG, "mediaPlayer was not initialized", e)
+            } finally {
+                if (mediaPlayer != null) {
+                    mediaPlayer!!.release()
+                }
+                mediaPlayer = null
+            }
+        }
+    }
+
+    fun addReactionForAnimation(emoji: String?, displayName: String?) {
+        reactionAnimator!!.addReaction(emoji!!, displayName!!)
+    }
+
+    /**
+     * Temporary implementation of SignalingMessageReceiver until signaling related code is extracted from
+     * CallActivity.
+     *
+     *
+     * All listeners are called in the main thread.
+     */
+    private class InternalSignalingMessageReceiver : SignalingMessageReceiver() {
+        fun process(users: List<Map<String?, Any?>?>?) {
+            processUsersInRoom(users)
+        }
+
+        fun process(message: NCSignalingMessage?) {
+            processSignalingMessage(message)
+        }
+    }
+
+    private inner class OfferAnswerNickProvider(private val sessionId: String?) {
+        val videoWebRtcMessageListener: WebRtcMessageListener = WebRtcMessageListener()
+        val screenWebRtcMessageListener: WebRtcMessageListener = WebRtcMessageListener()
+        var nick: String? = null
+            private set
+
+        private inner class WebRtcMessageListener : SignalingMessageReceiver.WebRtcMessageListener {
+            override fun onOffer(sdp: String, nick: String) {
+                onOfferOrAnswer(nick)
+            }
+
+            override fun onAnswer(sdp: String, nick: String) {
+                onOfferOrAnswer(nick)
+            }
+
+            override fun onCandidate(sdpMid: String, sdpMLineIndex: Int, sdp: String) {}
+            override fun onEndOfCandidates() {}
+        }
+
+        private fun onOfferOrAnswer(nick: String) {
+            this.nick = nick
+            if (callParticipants[sessionId] != null) {
+                callParticipants[sessionId]!!.setNick(nick)
+            }
+        }
+    }
+
+    private inner class CallActivityCallParticipantMessageListener(private val sessionId: String?) :
+        CallParticipantMessageListener {
+        override fun onRaiseHand(state: Boolean, timestamp: Long) {}
+        override fun onReaction(reaction: String) {}
+        override fun onUnshareScreen() {
+            endPeerConnection(sessionId, "screen")
+        }
+    }
+
+    private inner class CallActivitySelfPeerConnectionObserver : PeerConnectionObserver {
+        override fun onStreamAdded(mediaStream: MediaStream) {}
+        override fun onStreamRemoved(mediaStream: MediaStream) {}
+        override fun onIceConnectionStateChanged(iceConnectionState: IceConnectionState) {
+            runOnUiThread {
+                updateSelfVideoViewIceConnectionState(iceConnectionState)
+                if (iceConnectionState == IceConnectionState.FAILED) {
+                    setCallState(CallStatus.PUBLISHER_FAILED)
+                    webSocketClient!!.clearResumeId()
+                    hangup(false)
+                }
+            }
+        }
+    }
+
+    private inner class ScreenParticipantDisplayItemManager(private val callParticipantModel: CallParticipantModel) :
+        CallParticipantModel.Observer {
+        override fun onChange() {
+            val sessionId = callParticipantModel.sessionId
+            if (callParticipantModel.screenIceConnectionState == null) {
+                removeParticipantDisplayItem(sessionId, "screen")
+                return
+            }
+            val hasScreenParticipantDisplayItem = participantDisplayItems!!["$sessionId-screen"] != null
+            if (!hasScreenParticipantDisplayItem) {
+                addParticipantDisplayItem(callParticipantModel, "screen")
+            }
+        }
+
+        override fun onReaction(reaction: String) {}
+    }
+
+    private inner class CallParticipantEventDisplayer(private val callParticipantModel: CallParticipantModel) :
+        CallParticipantModel.Observer {
+        private var raisedHand: Boolean
+
+        init {
+            raisedHand = if (callParticipantModel.raisedHand != null) callParticipantModel.raisedHand.state else false
+        }
+
+        override fun onChange() {
+            if (callParticipantModel.raisedHand == null || !callParticipantModel.raisedHand.state) {
+                raisedHand = false
+                return
+            }
+            if (raisedHand) {
+                return
+            }
+            raisedHand = true
+            val nick = callParticipantModel.nick
+            Toast.makeText(
+                context,
+                String.format(context.resources.getString(R.string.nc_call_raised_hand), nick),
+                Toast.LENGTH_LONG
+            ).show()
+        }
+
+        override fun onReaction(reaction: String) {
+            addReactionForAnimation(reaction, callParticipantModel.nick)
+        }
+    }
+
+    private inner class InternalSignalingMessageSender : SignalingMessageSender {
+        override fun send(ncSignalingMessage: NCSignalingMessage) {
+            addLocalParticipantNickIfNeeded(ncSignalingMessage)
+            val serializedNcSignalingMessage: String
+            serializedNcSignalingMessage = try {
+                LoganSquare.serialize(ncSignalingMessage)
+            } catch (e: IOException) {
+                Log.e(TAG, "Failed to serialize signaling message", e)
+                return
+            }
+
+            // The message wrapper can not be defined in a JSON model to be directly serialized, as sent messages
+            // need to be serialized twice; first the signaling message, and then the wrapper as a whole. Received
+            // messages, on the other hand, just need to be deserialized once.
+            val stringBuilder = StringBuilder()
+            stringBuilder.append('{')
+                .append("\"fn\":\"")
+                .append(StringEscapeUtils.escapeJson(serializedNcSignalingMessage))
+                .append('\"')
+                .append(',')
+                .append("\"sessionId\":")
+                .append('\"').append(StringEscapeUtils.escapeJson(callSession)).append('\"')
+                .append(',')
+                .append("\"ev\":\"message\"")
+                .append('}')
+            val strings: MutableList<String> = ArrayList()
+            val stringToSend = stringBuilder.toString()
+            strings.add(stringToSend)
+            val apiVersion = ApiUtils.getSignalingApiVersion(conversationUser, intArrayOf(ApiUtils.APIv3, 2, 1))
+            ncApi!!.sendSignalingMessages(
+                credentials,
+                ApiUtils.getUrlForSignaling(apiVersion, baseUrl, roomToken),
+                strings.toString()
+            )
+                .retry(3)
+                .subscribeOn(Schedulers.io())
+                .subscribe(object : Observer<SignalingOverall> {
+                    override fun onSubscribe(d: Disposable) {}
+                    override fun onNext(signalingOverall: SignalingOverall) {
+                        // When sending messages to the internal signaling server the response has been empty since
+                        // Talk v2.9.0, so it is not really needed to process it, but there is no harm either in
+                        // doing that, as technically messages could be returned.
+                        receivedSignalingMessages(signalingOverall.ocs!!.signalings)
+                    }
+
+                    override fun onError(e: Throwable) {
+                        Log.e(TAG, "", e)
+                    }
+
+                    override fun onComplete() {}
+                })
+        }
+
+        /**
+         * Adds the local participant nick to offers and answers.
+         *
+         *
+         * For legacy reasons the offers and answers sent when the internal signaling server is used are expected to
+         * provide the nick of the local participant.
+         *
+         * @param ncSignalingMessage the message to add the nick to
+         */
+        private fun addLocalParticipantNickIfNeeded(ncSignalingMessage: NCSignalingMessage) {
+            val type = ncSignalingMessage.type
+            if ("offer" != type && "answer" != type) {
+                return
+            }
+            val payload = ncSignalingMessage.payload
+                ?: // Broken message, this should not happen
+                return
+            payload.nick = conversationUser!!.displayName
+        }
+    }
+
+    private inner class MicrophoneButtonTouchListener : OnTouchListener {
+        @SuppressLint("ClickableViewAccessibility")
+        override fun onTouch(v: View, event: MotionEvent): Boolean {
+            v.onTouchEvent(event)
+            if (event.action == MotionEvent.ACTION_UP && isPushToTalkActive) {
+                isPushToTalkActive = false
+                binding!!.microphoneButton.setImageResource(R.drawable.ic_mic_off_white_24px)
+                pulseAnimation!!.stop()
+                toggleMedia(false, false)
+                animateCallControls(false, 5000)
+            }
+            return true
+        }
+    }
+
+    @Subscribe(threadMode = ThreadMode.BACKGROUND)
+    fun onMessageEvent(networkEvent: NetworkEvent) {
+        if (networkEvent.networkConnectionEvent == NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED) {
+            if (handler != null) {
+                handler!!.removeCallbacksAndMessages(null)
+            }
+        } else if (networkEvent.networkConnectionEvent ==
+            NetworkEvent.NetworkConnectionEvent.NETWORK_DISCONNECTED
+        ) {
+            if (handler != null) {
+                handler!!.removeCallbacksAndMessages(null)
+            }
+        }
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.O)
+    override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
+        super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
+        Log.d(TAG, "onPictureInPictureModeChanged")
+        Log.d(TAG, "isInPictureInPictureMode= $isInPictureInPictureMode")
+        isInPipMode = isInPictureInPictureMode
+        if (isInPictureInPictureMode) {
+            mReceiver = object : BroadcastReceiver() {
+                override fun onReceive(context: Context, intent: Intent) {
+                    if (MICROPHONE_PIP_INTENT_NAME != intent.action) {
+                        return
+                    }
+                    when (intent.getIntExtra(MICROPHONE_PIP_INTENT_EXTRA_ACTION, 0)) {
+                        MICROPHONE_PIP_REQUEST_MUTE, MICROPHONE_PIP_REQUEST_UNMUTE -> onMicrophoneClick()
+                    }
+                }
+            }
+            registerReceiver(
+                mReceiver,
+                IntentFilter(MICROPHONE_PIP_INTENT_NAME),
+                permissionUtil!!.privateBroadcastPermission,
+                null
+            )
+            updateUiForPipMode()
+        } else {
+            unregisterReceiver(mReceiver)
+            mReceiver = null
+            updateUiForNormalMode()
+        }
+    }
+
+    fun updatePictureInPictureActions(
+        @DrawableRes iconId: Int,
+        title: String?,
+        requestCode: Int
+    ) {
+        if (isGreaterEqualOreo && isPipModePossible) {
+            val actions = ArrayList<RemoteAction>()
+            val icon = Icon.createWithResource(this, iconId)
+            val intentFlag: Int
+            intentFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                PendingIntent.FLAG_IMMUTABLE
+            } else {
+                0
+            }
+            val intent = PendingIntent.getBroadcast(
+                this,
+                requestCode,
+                Intent(MICROPHONE_PIP_INTENT_NAME).putExtra(MICROPHONE_PIP_INTENT_EXTRA_ACTION, requestCode),
+                intentFlag
+            )
+            actions.add(RemoteAction(icon, title!!, title, intent))
+            mPictureInPictureParamsBuilder.setActions(actions)
+            setPictureInPictureParams(mPictureInPictureParamsBuilder.build())
+        }
+    }
+
+    override fun updateUiForPipMode() {
+        Log.d(TAG, "updateUiForPipMode")
+        val params = RelativeLayout.LayoutParams(
+            ViewGroup.LayoutParams.MATCH_PARENT,
+            ViewGroup.LayoutParams.WRAP_CONTENT
+        )
+        params.setMargins(0, 0, 0, 0)
+        binding!!.gridview.layoutParams = params
+        binding!!.callControls.visibility = View.GONE
+        binding!!.callInfosLinearLayout.visibility = View.GONE
+        binding!!.selfVideoViewWrapper.visibility = View.GONE
+        binding!!.callStates.callStateRelativeLayout.visibility = View.GONE
+        if (participantDisplayItems!!.size > 1) {
+            binding!!.pipCallConversationNameTextView.text = conversationName
+            binding!!.pipGroupCallOverlay.visibility = View.VISIBLE
+        } else {
+            binding!!.pipGroupCallOverlay.visibility = View.GONE
+        }
+        binding!!.selfVideoRenderer.release()
+    }
+
+    override fun updateUiForNormalMode() {
+        Log.d(TAG, "updateUiForNormalMode")
+        if (isVoiceOnlyCall) {
+            binding!!.callControls.visibility = View.VISIBLE
+        } else {
+            // animateCallControls needs this to be invisible for a check.
+            binding!!.callControls.visibility = View.INVISIBLE
+        }
+        initViews()
+        binding!!.callInfosLinearLayout.visibility = View.VISIBLE
+        binding!!.selfVideoViewWrapper.visibility = View.VISIBLE
+        binding!!.pipGroupCallOverlay.visibility = View.GONE
+    }
+
+    override fun suppressFitsSystemWindows() {
+        binding!!.controllerCallLayout.fitsSystemWindows = false
+    }
+
+    override fun onConfigurationChanged(newConfig: Configuration) {
+        super.onConfigurationChanged(newConfig)
+        eventBus.post(ConfigurationChangeEvent())
+    }
+
+    val isAllowedToStartOrStopRecording: Boolean
+        get() = (
+            isCallRecordingAvailable(conversationUser!!) &&
+                isModerator
+            )
+    val isAllowedToRaiseHand: Boolean
+        get() = hasSpreedFeatureCapability(conversationUser, "raise-hand") ||
+            isBreakoutRoom
+
+    private inner class SelfVideoTouchListener : OnTouchListener {
+        @SuppressLint("ClickableViewAccessibility")
+        override fun onTouch(view: View, event: MotionEvent): Boolean {
+            val duration = event.eventTime - event.downTime
+            if (event.actionMasked == MotionEvent.ACTION_MOVE) {
+                val newY = event.rawY - binding!!.selfVideoViewWrapper.height / 2f
+                val newX = event.rawX - binding!!.selfVideoViewWrapper.width / 2f
+                binding!!.selfVideoViewWrapper.y = newY
+                binding!!.selfVideoViewWrapper.x = newX
+            } else if (event.actionMasked == MotionEvent.ACTION_UP && duration < 100) {
+                switchCamera()
+            }
+            return true
+        }
+    }
+
+    companion object {
+        var active = false
+        const val VIDEO_STREAM_TYPE_SCREEN = "screen"
+        const val VIDEO_STREAM_TYPE_VIDEO = "video"
+        const val TAG = "CallActivity"
+        private val PERMISSIONS_CAMERA = arrayOf(
+            Manifest.permission.CAMERA
+        )
+        private val PERMISSIONS_MICROPHONE = arrayOf(
+            Manifest.permission.RECORD_AUDIO
+        )
+        private const val MICROPHONE_PIP_INTENT_NAME = "microphone_pip_intent"
+        private const val MICROPHONE_PIP_INTENT_EXTRA_ACTION = "microphone_pip_action"
+        private const val MICROPHONE_PIP_REQUEST_MUTE = 1
+        private const val MICROPHONE_PIP_REQUEST_UNMUTE = 2
+    }
+}

+ 4 - 4
app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt

@@ -93,7 +93,7 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee
 
     private fun initClickListeners() {
         binding.recordCall.setOnClickListener {
-            callActivity.callRecordingViewModel.clickRecordButton()
+            callActivity.callRecordingViewModel?.clickRecordButton()
         }
 
         binding.raiseHand.setOnClickListener {
@@ -105,7 +105,7 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee
         if (CapabilitiesUtilNew.isCallReactionsSupported(callActivity.conversationUser)) {
             binding.advancedCallOptionsTitle.visibility = View.GONE
 
-            val capabilities = callActivity.conversationUser.capabilities
+            val capabilities = callActivity.conversationUser?.capabilities
             val availableReactions: ArrayList<*> =
                 capabilities?.spreedCapability?.config!!["call"]!!["supported-reactions"] as ArrayList<*>
 
@@ -133,7 +133,7 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee
     }
 
     private fun initObservers() {
-        callActivity.callRecordingViewModel.viewState.observe(this) { state ->
+        callActivity.callRecordingViewModel?.viewState?.observe(this) { state ->
             when (state) {
                 is CallRecordingViewModel.RecordingStoppedState,
                 is CallRecordingViewModel.RecordingErrorState -> {
@@ -173,7 +173,7 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee
             }
         }
 
-        callActivity.raiseHandViewModel.viewState.observe(this) { state ->
+        callActivity.raiseHandViewModel?.viewState?.observe(this) { state ->
             when (state) {
                 is RaiseHandViewModel.RaisedHandState -> {
                     binding.raiseHandText.text = context.getText(R.string.lower_hand)