diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java deleted file mode 100644 index f1e902656..000000000 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java +++ /dev/null @@ -1,3229 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * @author Tim Krüger - * @author Marcel Hibbe - * Copyright (C) 2022 Marcel Hibbe - * Copyright (C) 2022 Tim Krüger - * Copyright (C) 2017-2018 Mario Danic - * - * 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 . - */ - -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 iceServers; - private CameraEnumerator cameraEnumerator; - private String roomToken; - public User conversationUser; - private String conversationName; - private String callSession; - private MediaStream localStream; - private String credentials; - private List 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 offerAnswerNickProviders = new HashMap<>(); - - private Map callParticipantMessageListeners = - new HashMap<>(); - - private PeerConnectionWrapper.PeerConnectionObserver selfPeerConnectionObserver = new CallActivitySelfPeerConnectionObserver(); - - private Map callParticipants = new HashMap<>(); - - private Map screenParticipantDisplayItemManagers = new HashMap<>(); - - private Handler screenParticipantDisplayItemManagersHandler = new Handler(Looper.getMainLooper()); - - private Map callParticipantEventDisplayers = new HashMap<>(); - - private Handler callParticipantEventDisplayersHandler = new Handler(Looper.getMainLooper()); - - private CallParticipantList.Observer callParticipantListObserver = new CallParticipantList.Observer() { - @Override - public void onCallParticipantsChanged(Collection joined, Collection updated, - Collection left, Collection 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 participantDisplayItems; - private ParticipantsAdapter participantsAdapter; - - private CallActivityBinding binding; - - private AudioOutputDialog audioOutputDialog; - private MoreCallActionsDialog moreCallActionsDialog; - - ActivityResultLauncher requestPermissionLauncher = - registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), permissionMap -> { - - List 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() { - @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 permissionsToRequest = new ArrayList<>(); - List 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 rationaleList = new ArrayList(); - List permissionsToRequest = new ArrayList(); - - rationaleList.add(rationale); - permissionsToRequest.add(permissionToRequest); - - showRationaleDialog(permissionsToRequest, rationaleList); - } - - private void showRationaleDialog(List permissionsToRequest, List 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 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 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() { - @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 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 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() { - @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() { - @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() { - @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() { - @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 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>) 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 peerConnectionIdsToEnd = new ArrayList(peerConnectionWrapperList.size()); - for (PeerConnectionWrapper wrapper : peerConnectionWrapperList) { - peerConnectionIdsToEnd.add(wrapper.getSessionId()); - } - for (String sessionId : peerConnectionIdsToEnd) { - endPeerConnection(sessionId, "video"); - endPeerConnection(sessionId, "screen"); - } - - List callParticipantIdsToEnd = new ArrayList(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() { - @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 joined, Collection updated, - Collection left, Collection 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 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 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() { - @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. - *

- * All listeners are called in the main thread. - */ - private static class InternalSignalingMessageReceiver extends SignalingMessageReceiver { - public void process(List> 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 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() { - @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. - *

- * 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 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; - } - } -} diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt new file mode 100644 index 000000000..d059f2633 --- /dev/null +++ b/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 + * Copyright (C) 2022 Tim Krüger + * Copyright (C) 2017-2018 Mario Danic + * + * 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 . + */ +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? = 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 = 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 = HashMap() + private val callParticipantMessageListeners: MutableMap = HashMap() + private val selfPeerConnectionObserver: PeerConnectionObserver = CallActivitySelfPeerConnectionObserver() + private var callParticipants: MutableMap = HashMap() + private val screenParticipantDisplayItemManagers: MutableMap = + HashMap() + private val screenParticipantDisplayItemManagersHandler = Handler(Looper.getMainLooper()) + private val callParticipantEventDisplayers: MutableMap = HashMap() + private val callParticipantEventDisplayersHandler = Handler(Looper.getMainLooper()) + private val callParticipantListObserver: CallParticipantList.Observer = object : CallParticipantList.Observer { + override fun onCallParticipantsChanged( + joined: Collection, + updated: Collection, + left: Collection, + unchanged: Collection + ) { + 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? = 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 -> + val rationaleList: MutableList = 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 -> + 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 { + 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 = ArrayList() + val rationaleList: MutableList = 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 = ArrayList() + val permissionsToRequest: MutableList = ArrayList() + rationaleList.add(rationale) + permissionsToRequest.add(permissionToRequest) + showRationaleDialog(permissionsToRequest, rationaleList) + } + + private fun showRationaleDialog(permissionsToRequest: List, rationaleList: List) { + 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) { + 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 + ) { + 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 { + 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 { + 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 { + 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 { + 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? -> observable } + .takeWhile { isConnectionEstablished } + .doOnNext { delayOnError.set(0) } + .retryWhen { errors: Observable -> + errors + .flatMap { error: Throwable? -> + if (!isConnectionEstablished) { + return@flatMap Observable.error(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 { + 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?) { + 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?>?) + } 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 = ArrayList( + peerConnectionWrapperList!!.size + ) + for (wrapper in peerConnectionWrapperList) { + peerConnectionIdsToEnd.add(wrapper.sessionId) + } + for (sessionId in peerConnectionIdsToEnd) { + endPeerConnection(sessionId, "video") + endPeerConnection(sessionId, "screen") + } + val callParticipantIdsToEnd: MutableList = 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 { + 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, + updated: Collection, + left: Collection, + unchanged: Collection + ) { + 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 = 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 = 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 { + 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?>?) { + 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 = 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 { + 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() + 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 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt index 8e81d3c7e..73f4a6cbb 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt +++ b/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)