Browse Source

Merge pull request #2367 from nextcloud/feature/1783/implement-participant-permissions

Respect participant permissions
Tim Krüger 2 years ago
parent
commit
e8c9c6a200

+ 107 - 51
app/src/main/java/com/nextcloud/talk/activities/CallActivity.java

@@ -2,6 +2,8 @@
  * Nextcloud Talk application
  *
  * @author Mario Danic
+ * @author Tim Krüger
+ * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
  * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -90,7 +92,6 @@ import com.nextcloud.talk.utils.ApiUtils;
 import com.nextcloud.talk.utils.DisplayUtils;
 import com.nextcloud.talk.utils.NotificationUtils;
 import com.nextcloud.talk.utils.animations.PulseAnimation;
-import com.nextcloud.talk.utils.bundle.BundleKeys;
 import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil;
 import com.nextcloud.talk.utils.power.PowerManagerUtils;
 import com.nextcloud.talk.utils.preferences.AppPreferences;
@@ -163,6 +164,17 @@ import okhttp3.Cache;
 import pub.devrel.easypermissions.AfterPermissionGranted;
 
 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_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_ROOM_ID;
+import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN;
+import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY;
 import static com.nextcloud.talk.webrtc.Globals.JOB_ID;
 import static com.nextcloud.talk.webrtc.Globals.PARTICIPANTS_UPDATE;
 import static com.nextcloud.talk.webrtc.Globals.ROOM_TOKEN;
@@ -246,7 +258,7 @@ public class CallActivity extends CallBaseActivity {
     private Handler cameraSwitchHandler = new Handler();
 
     // push to talk
-    private boolean isPTTActive = false;
+    private boolean isPushToTalkActive = false;
     private PulseAnimation pulseAnimation;
 
     private String baseUrl;
@@ -283,6 +295,9 @@ public class CallActivity extends CallBaseActivity {
             }
         });
 
+    private boolean canPublishAudioStream;
+    private boolean canPublishVideoStream;
+
     @SuppressLint("ClickableViewAccessibility")
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -297,21 +312,23 @@ public class CallActivity extends CallBaseActivity {
         hideNavigationIfNoPipAvailable();
 
         Bundle extras = getIntent().getExtras();
-        roomId = extras.getString(BundleKeys.INSTANCE.getKEY_ROOM_ID(), "");
-        roomToken = extras.getString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), "");
-        conversationUser = extras.getParcelable(BundleKeys.INSTANCE.getKEY_USER_ENTITY());
-        conversationPassword = extras.getString(BundleKeys.INSTANCE.getKEY_CONVERSATION_PASSWORD(), "");
-        conversationName = extras.getString(BundleKeys.INSTANCE.getKEY_CONVERSATION_NAME(), "");
-        isVoiceOnlyCall = extras.getBoolean(BundleKeys.INSTANCE.getKEY_CALL_VOICE_ONLY(), false);
-        isCallWithoutNotification = extras.getBoolean(BundleKeys.INSTANCE.getKEY_CALL_WITHOUT_NOTIFICATION(), false);
+        roomId = extras.getString(KEY_ROOM_ID, "");
+        roomToken = extras.getString(KEY_ROOM_TOKEN, "");
+        conversationUser = extras.getParcelable(KEY_USER_ENTITY);
+        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);
 
-        if (extras.containsKey(BundleKeys.INSTANCE.getKEY_FROM_NOTIFICATION_START_CALL())) {
-            isIncomingCallFromNotification = extras.getBoolean(BundleKeys.INSTANCE.getKEY_FROM_NOTIFICATION_START_CALL());
+        if (extras.containsKey(KEY_FROM_NOTIFICATION_START_CALL)) {
+            isIncomingCallFromNotification = extras.getBoolean(KEY_FROM_NOTIFICATION_START_CALL);
         }
 
         credentials = ApiUtils.getCredentials(conversationUser.getUsername(), conversationUser.getToken());
 
-        baseUrl = extras.getString(BundleKeys.INSTANCE.getKEY_MODIFIED_BASE_URL(), "");
+        baseUrl = extras.getString(KEY_MODIFIED_BASE_URL, "");
         if (TextUtils.isEmpty(baseUrl)) {
             baseUrl = conversationUser.getBaseUrl();
         }
@@ -377,23 +394,41 @@ public class CallActivity extends CallBaseActivity {
             audioOutputDialog.show();
         });
 
-        binding.microphoneButton.setOnClickListener(l -> onMicrophoneClick());
-        binding.microphoneButton.setOnLongClickListener(l -> {
-            if (!microphoneOn) {
-                callControlHandler.removeCallbacksAndMessages(null);
-                callInfosHandler.removeCallbacksAndMessages(null);
-                cameraSwitchHandler.removeCallbacksAndMessages(null);
-                isPTTActive = true;
-                binding.callControls.setVisibility(View.VISIBLE);
-                if (!isVoiceOnlyCall) {
-                    binding.switchSelfVideoButton.setVisibility(View.VISIBLE);
+        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;
-        });
+                onMicrophoneClick();
+                return true;
+            });
+        } else {
+            binding.microphoneButton.setOnClickListener(
+                l -> Toast.makeText(context,
+                                    R.string.nc_not_allowed_to_activate_audio,
+                                    Toast.LENGTH_SHORT
+                                   ).show()
+                                                       );
+        }
 
-        binding.cameraButton.setOnClickListener(l -> onCameraClick());
+        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);
@@ -548,7 +583,7 @@ public class CallActivity extends CallBaseActivity {
                         }
                     }
 
-                    checkPermissions();
+                    checkDevicePermissions();
                 }
 
                 @Override
@@ -695,7 +730,7 @@ public class CallActivity extends CallBaseActivity {
     }
 
 
-    private void checkPermissions() {
+    private void checkDevicePermissions() {
         if (isVoiceOnlyCall) {
             onMicrophoneClick();
         } else {
@@ -754,7 +789,7 @@ public class CallActivity extends CallBaseActivity {
                 binding.switchSelfVideoButton.setVisibility(View.VISIBLE);
             }
 
-            if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_CAMERA)) {
+            if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_CAMERA) && canPublishVideoStream) {
                 if (!videoOn) {
                     onCameraClick();
                 }
@@ -765,7 +800,7 @@ public class CallActivity extends CallBaseActivity {
             }
         }
 
-        if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_MICROPHONE)) {
+        if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_MICROPHONE) && canPublishAudioStream) {
             if (!microphoneOn) {
                 onMicrophoneClick();
             }
@@ -878,6 +913,22 @@ public class CallActivity extends CallBaseActivity {
     }
 
     public void onMicrophoneClick() {
+
+        if (!canPublishAudioStream) {
+            microphoneOn = false;
+            binding.microphoneButton.getHierarchy().setPlaceholderImage(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 (EffortlessPermissions.hasPermissions(this, PERMISSIONS_MICROPHONE)) {
 
             if (!appPreferences.getPushToTalkIntroShown()) {
@@ -905,7 +956,7 @@ public class CallActivity extends CallBaseActivity {
                 appPreferences.setPushToTalkIntroShown(true);
             }
 
-            if (!isPTTActive) {
+            if (!isPushToTalkActive) {
                 microphoneOn = !microphoneOn;
 
                 if (microphoneOn) {
@@ -926,11 +977,6 @@ public class CallActivity extends CallBaseActivity {
                 pulseAnimation.start();
                 toggleMedia(true, false);
             }
-
-            if (isVoiceOnlyCall && !isConnectionEstablished()) {
-                fetchSignalingSettings();
-            }
-
         } else if (EffortlessPermissions.somePermissionPermanentlyDenied(this, PERMISSIONS_MICROPHONE)) {
             // Microphone permission is permanently denied so we cannot request it normally.
 
@@ -947,6 +993,14 @@ public class CallActivity extends CallBaseActivity {
     }
 
     public void onCameraClick() {
+
+        if (!canPublishVideoStream) {
+            videoOn = false;
+            binding.cameraButton.getHierarchy().setPlaceholderImage(R.drawable.ic_videocam_off_white_24px);
+            binding.switchSelfVideoButton.setVisibility(View.GONE);
+            return;
+        }
+
         if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_CAMERA)) {
             videoOn = !videoOn;
 
@@ -1057,7 +1111,7 @@ public class CallActivity extends CallBaseActivity {
             if (spotlightView != null && spotlightView.getVisibility() != View.GONE) {
                 spotlightView.setVisibility(View.GONE);
             }
-        } else if (!isPTTActive) {
+        } else if (!isPushToTalkActive) {
             float alpha;
             long duration;
 
@@ -1106,7 +1160,7 @@ public class CallActivity extends CallBaseActivity {
                             callControlHandler.postDelayed(new Runnable() {
                                 @Override
                                 public void run() {
-                                    if (!isPTTActive) {
+                                    if (!isPushToTalkActive) {
                                         animateCallControls(false, 0);
                                     }
                                 }
@@ -1133,7 +1187,7 @@ public class CallActivity extends CallBaseActivity {
                             callInfosHandler.postDelayed(new Runnable() {
                                 @Override
                                 public void run() {
-                                    if (!isPTTActive) {
+                                    if (!isPushToTalkActive) {
                                         animateCallControls(false, 0);
                                     }
                                 }
@@ -1374,12 +1428,14 @@ public class CallActivity extends CallBaseActivity {
     }
 
     private void performCall() {
-        int inCallFlag;
-        if (isVoiceOnlyCall) {
-            inCallFlag = Participant.InCallFlags.IN_CALL + Participant.InCallFlags.WITH_AUDIO;
-        } else {
-            inCallFlag =
-                Participant.InCallFlags.IN_CALL + Participant.InCallFlags.WITH_AUDIO + Participant.InCallFlags.WITH_VIDEO;
+        int inCallFlag = Participant.InCallFlags.IN_CALL;
+
+        if (canPublishAudioStream) {
+            inCallFlag += Participant.InCallFlags.WITH_AUDIO;
+        }
+
+        if (!isVoiceOnlyCall && canPublishVideoStream) {
+            inCallFlag += Participant.InCallFlags.WITH_VIDEO;
         }
 
         int apiVersion = ApiUtils.getCallApiVersion(conversationUser, new int[]{ApiUtils.APIv4, 1});
@@ -1485,7 +1541,7 @@ public class CallActivity extends CallBaseActivity {
 
     private void initiateCall() {
         if (!TextUtils.isEmpty(roomToken)) {
-            checkPermissions();
+            checkDevicePermissions();
         } else {
             handleFromNotification();
         }
@@ -2292,7 +2348,7 @@ public class CallActivity extends CallBaseActivity {
         if (peerConnectionWrapper != null) {
             PeerConnection.IceConnectionState iceConnectionState = peerConnectionWrapper.getPeerConnection().iceConnectionState();
             connected = iceConnectionState == PeerConnection.IceConnectionState.CONNECTED ||
-                        iceConnectionState == PeerConnection.IceConnectionState.COMPLETED;
+                iceConnectionState == PeerConnection.IceConnectionState.COMPLETED;
         }
 
         String nick;
@@ -2471,7 +2527,7 @@ public class CallActivity extends CallBaseActivity {
                             binding.callInfosLinearLayout.setVisibility(View.GONE);
                         }
 
-                        if (!isPTTActive) {
+                        if (!isPushToTalkActive) {
                             animateCallControls(false, 5000);
                         }
 
@@ -2590,8 +2646,8 @@ public class CallActivity extends CallBaseActivity {
         @Override
         public boolean onTouch(View v, MotionEvent event) {
             v.onTouchEvent(event);
-            if (event.getAction() == MotionEvent.ACTION_UP && isPTTActive) {
-                isPTTActive = false;
+            if (event.getAction() == MotionEvent.ACTION_UP && isPushToTalkActive) {
+                isPushToTalkActive = false;
                 binding.microphoneButton.getHierarchy().setPlaceholderImage(R.drawable.ic_mic_off_white_24px);
                 pulseAnimation.stop();
                 toggleMedia(false, false);

+ 7 - 7
app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.java

@@ -129,9 +129,9 @@ public class CallNotificationActivity extends CallBaseActivity {
         eventBus.post(new CallNotificationClick());
 
         Bundle extras = getIntent().getExtras();
-        this.roomId = extras.getString(BundleKeys.INSTANCE.getKEY_ROOM_ID(), "");
-        this.currentConversation = Parcels.unwrap(extras.getParcelable(BundleKeys.INSTANCE.getKEY_ROOM()));
-        this.userBeingCalled = extras.getParcelable(BundleKeys.INSTANCE.getKEY_USER_ENTITY());
+        this.roomId = extras.getString(BundleKeys.KEY_ROOM_ID, "");
+        this.currentConversation = Parcels.unwrap(extras.getParcelable(BundleKeys.KEY_ROOM));
+        this.userBeingCalled = extras.getParcelable(BundleKeys.KEY_USER_ENTITY);
 
         this.originalBundle = extras;
         credentials = ApiUtils.getCredentials(userBeingCalled.getUsername(), userBeingCalled.getToken());
@@ -169,13 +169,13 @@ public class CallNotificationActivity extends CallBaseActivity {
     private void initClickListeners() {
         binding.callAnswerVoiceOnlyView.setOnClickListener(l -> {
             Log.d(TAG, "accept call (voice only)");
-            originalBundle.putBoolean(BundleKeys.INSTANCE.getKEY_CALL_VOICE_ONLY(), true);
+            originalBundle.putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, true);
             proceedToCall();
         });
 
         binding.callAnswerCameraView.setOnClickListener(l -> {
             Log.d(TAG, "accept call (with video)");
-            originalBundle.putBoolean(BundleKeys.INSTANCE.getKEY_CALL_VOICE_ONLY(), false);
+            originalBundle.putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, false);
             proceedToCall();
         });
 
@@ -202,8 +202,8 @@ public class CallNotificationActivity extends CallBaseActivity {
     }
 
     private void proceedToCall() {
-        originalBundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), currentConversation.getToken());
-        originalBundle.putString(BundleKeys.INSTANCE.getKEY_CONVERSATION_NAME(), currentConversation.getDisplayName());
+        originalBundle.putString(BundleKeys.KEY_ROOM_TOKEN, currentConversation.getToken());
+        originalBundle.putString(BundleKeys.KEY_CONVERSATION_NAME, currentConversation.getDisplayName());
 
         Intent intent = new Intent(this, CallActivity.class);
         intent.putExtras(originalBundle);

+ 31 - 23
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

@@ -156,7 +156,6 @@ import com.nextcloud.talk.ui.dialog.ShowReactionsDialog
 import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions
 import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
 import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.AttendeePermissionsUtil
 import com.nextcloud.talk.utils.ConductorRemapping
 import com.nextcloud.talk.utils.ConductorRemapping.remapChatController
 import com.nextcloud.talk.utils.ContactUtils
@@ -166,6 +165,7 @@ import com.nextcloud.talk.utils.FileUtils
 import com.nextcloud.talk.utils.ImageEmojiEditText
 import com.nextcloud.talk.utils.MagicCharPolicy
 import com.nextcloud.talk.utils.NotificationUtils
+import com.nextcloud.talk.utils.ParticipantPermissions
 import com.nextcloud.talk.utils.bundle.BundleKeys
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACTIVE_CONVERSATION
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME
@@ -284,7 +284,7 @@ class ChatController(args: Bundle) :
     lateinit var mediaPlayerHandler: Handler
     private var currentlyPlayedVoiceMessage: ChatMessage? = null
 
-    var hasChatPermission: Boolean = false
+    private lateinit var participantPermissions: ParticipantPermissions
 
     private var videoURI: Uri? = null
 
@@ -306,6 +306,7 @@ class ChatController(args: Bundle) :
 
         if (args.containsKey(KEY_ACTIVE_CONVERSATION)) {
             this.currentConversation = Parcels.unwrap<Conversation>(args.getParcelable(KEY_ACTIVE_CONVERSATION))
+            this.participantPermissions = ParticipantPermissions(conversationUser!!, currentConversation!!)
         }
 
         this.roomPassword = args.getString(BundleKeys.KEY_CONVERSATION_PASSWORD, "")
@@ -353,11 +354,7 @@ class ChatController(args: Bundle) :
                         )
                         loadAvatarForStatusBar()
                         setTitle()
-
-                        hasChatPermission =
-                            AttendeePermissionsUtil(currentConversation!!.permissions).hasChatPermission(
-                                conversationUser
-                            )
+                        participantPermissions = ParticipantPermissions(conversationUser, currentConversation!!)
 
                         try {
                             setupSwipeToReply()
@@ -395,7 +392,10 @@ class ChatController(args: Bundle) :
     }
 
     private fun setupSwipeToReply() {
-        if (hasChatPermission && !isReadOnlyConversation()) {
+        if (this::participantPermissions.isInitialized &&
+            participantPermissions.hasChatPermission() &&
+            !isReadOnlyConversation()
+        ) {
             val messageSwipeController = MessageSwipeCallback(
                 activity!!,
                 object : MessageSwipeActions {
@@ -434,6 +434,7 @@ class ChatController(args: Bundle) :
                         if (roomId == conversation.roomId) {
                             roomToken = conversation.token
                             currentConversation = conversation
+                            participantPermissions = ParticipantPermissions(conversationUser!!, currentConversation!!)
                             setTitle()
                             getRoomInfo()
                             break
@@ -1261,7 +1262,7 @@ class ChatController(args: Bundle) :
         if (isAlive()) {
             if (isReadOnlyConversation() ||
                 shouldShowLobby() ||
-                !hasChatPermission
+                !participantPermissions.hasChatPermission()
             ) {
                 binding.messageInputView.visibility = View.GONE
             } else {
@@ -1272,7 +1273,9 @@ class ChatController(args: Bundle) :
 
     private fun shouldShowLobby(): Boolean {
         if (currentConversation != null) {
-            return currentConversation?.shouldShowLobby(conversationUser!!) == true
+            return currentConversation?.lobbyState == Conversation.LobbyState.LOBBY_STATE_MODERATORS_ONLY &&
+                currentConversation?.canModerate(conversationUser!!) == false &&
+                !participantPermissions.canIgnoreLobby()
         }
         return false
     }
@@ -1311,14 +1314,14 @@ class ChatController(args: Bundle) :
 
     private fun checkLobbyState() {
         if (currentConversation != null &&
-            currentConversation?.isLobbyViewApplicable(conversationUser!!) ?: false &&
+            currentConversation?.isLobbyViewApplicable(conversationUser!!) == true &&
             isAlive()
         ) {
             if (!checkingLobbyStatus) {
                 getRoomInfo()
             }
 
-            if (currentConversation?.shouldShowLobby(conversationUser!!) ?: false) {
+            if (shouldShowLobby()) {
                 binding.lobby.lobbyView.visibility = View.VISIBLE
                 binding.messagesListView.visibility = View.GONE
                 binding.messageInputView.visibility = View.GONE
@@ -1610,8 +1613,8 @@ class ChatController(args: Bundle) :
     private fun uploadFile(fileUri: String, isVoiceMessage: Boolean) {
         var metaData = ""
 
-        if (!hasChatPermission) {
-            Log.w(TAG, "uploading file is forbidden because of missing attendee permissions")
+        if (!participantPermissions.hasChatPermission()) {
+            Log.w(TAG, "uploading file(s) is forbidden because of missing attendee permissions")
             return
         }
 
@@ -2146,10 +2149,6 @@ class ChatController(args: Bundle) :
         }
         pullChatMessagesPending = true
 
-        if (currentConversation != null && currentConversation!!.shouldShowLobby(conversationUser!!)) {
-            // return
-        }
-
         val fieldMap = HashMap<String, Int>()
         fieldMap["includeLastKnown"] = 0
 
@@ -2727,7 +2726,8 @@ class ChatController(args: Bundle) :
     }
 
     private fun startACall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean) {
-        if (currentConversation?.canStartCall == false && currentConversation?.hasCall == false) {
+        val pp = ParticipantPermissions(conversationUser!!, currentConversation!!)
+        if (!pp.canStartCall() && currentConversation?.hasCall == false) {
             Toast.makeText(context, R.string.startCallForbidden, Toast.LENGTH_LONG).show()
         } else {
             ApplicationWideCurrentRoomHolder.getInstance().isDialing = true
@@ -2747,6 +2747,14 @@ class ChatController(args: Bundle) :
             bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, roomPassword)
             bundle.putString(BundleKeys.KEY_MODIFIED_BASE_URL, conversationUser?.baseUrl)
             bundle.putString(KEY_CONVERSATION_NAME, it.displayName)
+            bundle.putBoolean(
+                BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO,
+                participantPermissions.canPublishAudio()
+            )
+            bundle.putBoolean(
+                BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO,
+                participantPermissions.canPublishVideo()
+            )
 
             if (isVoiceOnlyCall) {
                 bundle.putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, true)
@@ -2774,7 +2782,7 @@ class ChatController(args: Bundle) :
                 currentConversation,
                 chatMessage,
                 conversationUser,
-                hasChatPermission,
+                participantPermissions.hasChatPermission(),
                 ncApi
             ).show()
         }
@@ -2802,7 +2810,7 @@ class ChatController(args: Bundle) :
                     conversationUser,
                     currentConversation,
                     isShowMessageDeletionButton(message),
-                    hasChatPermission,
+                    participantPermissions.hasChatPermission(),
                     ncApi
                 ).show()
             }
@@ -2814,7 +2822,7 @@ class ChatController(args: Bundle) :
     }
 
     fun deleteMessage(message: IMessage?) {
-        if (!hasChatPermission) {
+        if (!participantPermissions.hasChatPermission()) {
             Log.w(
                 TAG,
                 "Deletion of message is skipped because of restrictions by permissions. " +
@@ -3144,7 +3152,7 @@ class ChatController(args: Bundle) :
             message.hasFileAttachment() -> false
             OBJECT_MESSAGE == message.message -> false
             !CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "delete-messages") -> false
-            !hasChatPermission -> false
+            !participantPermissions.hasChatPermission() -> false
             else -> true
         }
     }

+ 10 - 5
app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.kt

@@ -104,7 +104,7 @@ import com.nextcloud.talk.ui.dialog.ChooseAccountShareToDialogFragment
 import com.nextcloud.talk.ui.dialog.ConversationsListBottomDialog
 import com.nextcloud.talk.users.UserManager
 import com.nextcloud.talk.utils.ApiUtils
-import com.nextcloud.talk.utils.AttendeePermissionsUtil
+import com.nextcloud.talk.utils.ParticipantPermissions
 import com.nextcloud.talk.utils.ClosedInterfaceImpl
 import com.nextcloud.talk.utils.ConductorRemapping.remapChatController
 import com.nextcloud.talk.utils.DisplayUtils
@@ -938,13 +938,11 @@ class ConversationsListController(bundle: Bundle) :
     private fun handleConversation(conversation: Conversation?) {
         selectedConversation = conversation
         if (selectedConversation != null && activity != null) {
-            val hasChatPermission = AttendeePermissionsUtil(selectedConversation!!.permissions).hasChatPermission(
-                currentUser!!
-            )
+            val hasChatPermission = ParticipantPermissions(currentUser!!, selectedConversation!!).hasChatPermission()
             if (showShareToScreen) {
                 if (hasChatPermission &&
                     !isReadOnlyConversation(selectedConversation!!) &&
-                    !selectedConversation!!.shouldShowLobby(currentUser!!)
+                    !shouldShowLobby(selectedConversation!!)
                 ) {
                     handleSharedData()
                 } else {
@@ -963,6 +961,13 @@ class ConversationsListController(bundle: Bundle) :
         }
     }
 
+    private fun shouldShowLobby(conversation: Conversation): Boolean {
+        val participantPermissions = ParticipantPermissions(currentUser!!, conversation)
+        return conversation.lobbyState == Conversation.LobbyState.LOBBY_STATE_MODERATORS_ONLY &&
+            !conversation.canModerate(currentUser!!) &&
+            !participantPermissions.canIgnoreLobby()
+    }
+
     private fun isReadOnlyConversation(conversation: Conversation): Boolean {
         return conversation.conversationReadOnlyState ===
             Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY

+ 6 - 6
app/src/main/java/com/nextcloud/talk/jobs/AddParticipantsToConversation.java

@@ -63,18 +63,18 @@ public class AddParticipantsToConversation extends Worker {
     @Override
     public Result doWork() {
         Data data = getInputData();
-        String[] selectedUserIds = data.getStringArray(BundleKeys.INSTANCE.getKEY_SELECTED_USERS());
-        String[] selectedGroupIds = data.getStringArray(BundleKeys.INSTANCE.getKEY_SELECTED_GROUPS());
-        String[] selectedCircleIds = data.getStringArray(BundleKeys.INSTANCE.getKEY_SELECTED_CIRCLES());
-        String[] selectedEmails = data.getStringArray(BundleKeys.INSTANCE.getKEY_SELECTED_EMAILS());
+        String[] selectedUserIds = data.getStringArray(BundleKeys.KEY_SELECTED_USERS);
+        String[] selectedGroupIds = data.getStringArray(BundleKeys.KEY_SELECTED_GROUPS);
+        String[] selectedCircleIds = data.getStringArray(BundleKeys.KEY_SELECTED_CIRCLES);
+        String[] selectedEmails = data.getStringArray(BundleKeys.KEY_SELECTED_EMAILS);
         User user =
             userManager.getUserWithInternalId(
-                data.getLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), -1))
+                data.getLong(BundleKeys.KEY_INTERNAL_USER_ID, -1))
                 .blockingGet();
 
         int apiVersion = ApiUtils.getConversationApiVersion(user, new int[] {ApiUtils.APIv4, 1});
 
-        String conversationToken = data.getString(BundleKeys.INSTANCE.getKEY_TOKEN());
+        String conversationToken = data.getString(BundleKeys.KEY_TOKEN);
         String credentials = ApiUtils.getCredentials(user.getUsername(), user.getToken());
 
         RetrofitBucket retrofitBucket;

+ 1 - 1
app/src/main/java/com/nextcloud/talk/jobs/CapabilitiesWorker.java

@@ -109,7 +109,7 @@ public class CapabilitiesWorker extends Worker {
 
         Data data = getInputData();
 
-        long internalUserId = data.getLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), -1);
+        long internalUserId = data.getLong(BundleKeys.KEY_INTERNAL_USER_ID, -1);
 
         List<User> userEntityObjectList = new ArrayList<>();
         boolean userExists = userManager.getUserWithInternalId(internalUserId).isEmpty().blockingGet();

+ 2 - 2
app/src/main/java/com/nextcloud/talk/jobs/DeleteConversationWorker.java

@@ -75,8 +75,8 @@ public class DeleteConversationWorker extends Worker {
     @Override
     public Result doWork() {
         Data data = getInputData();
-        long operationUserId = data.getLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), -1);
-        String conversationToken = data.getString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN());
+        long operationUserId = data.getLong(BundleKeys.KEY_INTERNAL_USER_ID, -1);
+        String conversationToken = data.getString(BundleKeys.KEY_ROOM_TOKEN);
         User operationUser = userManager.getUserWithId(operationUserId).blockingGet();
 
         if (operationUser != null) {

+ 2 - 2
app/src/main/java/com/nextcloud/talk/jobs/LeaveConversationWorker.java

@@ -79,8 +79,8 @@ public class LeaveConversationWorker extends Worker {
     @Override
     public Result doWork() {
         Data data = getInputData();
-        long operationUserId = data.getLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), -1);
-        String conversationToken = data.getString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN());
+        long operationUserId = data.getLong(BundleKeys.KEY_INTERNAL_USER_ID, -1);
+        String conversationToken = data.getString(BundleKeys.KEY_ROOM_TOKEN);
         User operationUser = userManager.getUserWithId(operationUserId).blockingGet();
 
         if (operationUser != null) {

+ 17 - 17
app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.java

@@ -138,7 +138,7 @@ public class NotificationWorker extends Worker {
         importantConversation = arbitraryStorageManager.getStorageSetting(
                 UserIdUtils.INSTANCE.getIdForUser(user),
                 "important_conversation",
-                intent.getExtras().getString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN()))
+                intent.getExtras().getString(BundleKeys.KEY_ROOM_TOKEN))
             .map(arbitraryStorage -> {
                 if (arbitraryStorage != null && arbitraryStorage.getValue() != null) {
                     return Boolean.parseBoolean(arbitraryStorage.getValue());
@@ -154,7 +154,7 @@ public class NotificationWorker extends Worker {
         int apiVersion = ApiUtils.getConversationApiVersion(user, new int[] {ApiUtils.APIv4, 1});
 
         ncApi.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, user.getBaseUrl(),
-                intent.getExtras().getString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN())))
+                intent.getExtras().getString(BundleKeys.KEY_ROOM_TOKEN)))
                 .blockingSubscribe(new Observer<RoomOverall>() {
                     @Override
                     public void onSubscribe(Disposable d) {
@@ -165,7 +165,7 @@ public class NotificationWorker extends Worker {
                     public void onNext(RoomOverall roomOverall) {
                         Conversation conversation = roomOverall.getOcs().getData();
 
-                        intent.putExtra(BundleKeys.INSTANCE.getKEY_ROOM(), Parcels.wrap(conversation));
+                        intent.putExtra(BundleKeys.KEY_ROOM, Parcels.wrap(conversation));
                         if (conversation.getType().equals(Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) ||
                                 (!TextUtils.isEmpty(conversation.getObjectType()) && "share:password".equals
                                         (conversation.getObjectType()))) {
@@ -351,12 +351,12 @@ public class NotificationWorker extends Worker {
         }
 
         Bundle notificationInfo = new Bundle();
-        notificationInfo.putLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(),
+        notificationInfo.putLong(BundleKeys.KEY_INTERNAL_USER_ID,
                                  signatureVerification.getUser().getId());
         // could be an ID or a TOKEN
-        notificationInfo.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(),
+        notificationInfo.putString(BundleKeys.KEY_ROOM_TOKEN,
                                    decryptedPushMessage.getId());
-        notificationInfo.putLong(BundleKeys.INSTANCE.getKEY_NOTIFICATION_ID(),
+        notificationInfo.putLong(BundleKeys.KEY_NOTIFICATION_ID,
                                  decryptedPushMessage.getNotificationId());
         notificationBuilder.setExtras(notificationInfo);
 
@@ -442,10 +442,10 @@ public class NotificationWorker extends Worker {
 
         // NOTE - systemNotificationId is an internal ID used on the device only.
         // It is NOT the same as the notification ID used in communication with the server.
-        actualIntent.putExtra(BundleKeys.INSTANCE.getKEY_SYSTEM_NOTIFICATION_ID(), systemNotificationId);
-        actualIntent.putExtra(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(),
+        actualIntent.putExtra(BundleKeys.KEY_SYSTEM_NOTIFICATION_ID, systemNotificationId);
+        actualIntent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID,
                               Objects.requireNonNull(signatureVerification.getUser()).getId());
-        actualIntent.putExtra(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), decryptedPushMessage.getId());
+        actualIntent.putExtra(BundleKeys.KEY_ROOM_TOKEN, decryptedPushMessage.getId());
         actualIntent.putExtra(BundleKeys.KEY_MESSAGE_ID, messageId);
 
         int intentFlag;
@@ -583,8 +583,8 @@ public class NotificationWorker extends Worker {
 
         context = getApplicationContext();
         Data data = getInputData();
-        String subject = data.getString(BundleKeys.INSTANCE.getKEY_NOTIFICATION_SUBJECT());
-        String signature = data.getString(BundleKeys.INSTANCE.getKEY_NOTIFICATION_SIGNATURE());
+        String subject = data.getString(BundleKeys.KEY_NOTIFICATION_SUBJECT);
+        String signature = data.getString(BundleKeys.KEY_NOTIFICATION_SIGNATURE);
 
         try {
             byte[] base64DecodedSubject = Base64.decode(subject, Base64.DEFAULT);
@@ -643,13 +643,13 @@ public class NotificationWorker extends Worker {
 
                             intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
 
-                            bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), decryptedPushMessage.getId());
+                            bundle.putString(BundleKeys.KEY_ROOM_TOKEN, decryptedPushMessage.getId());
 
-                            bundle.putParcelable(BundleKeys.INSTANCE.getKEY_USER_ENTITY(),
+                            bundle.putParcelable(BundleKeys.KEY_USER_ENTITY,
                                                  signatureVerification.getUser());
 
-                            bundle.putBoolean(BundleKeys.INSTANCE.getKEY_FROM_NOTIFICATION_START_CALL(),
-                                    startACall);
+                            bundle.putBoolean(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL,
+                                              startACall);
 
                             intent.putExtras(bundle);
 
@@ -657,12 +657,12 @@ public class NotificationWorker extends Worker {
 
                             switch (decryptedPushMessage.getType()) {
                                 case "call":
-                                    if (bundle.containsKey(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN())) {
+                                    if (bundle.containsKey(BundleKeys.KEY_ROOM_TOKEN)) {
                                         showNotificationForCallWithNoPing(intent);
                                     }
                                     break;
                                 case "room":
-                                    if (bundle.containsKey(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN())) {
+                                    if (bundle.containsKey(BundleKeys.KEY_ROOM_TOKEN)) {
                                         showNotificationWithObjectData(intent);
                                     }
                                     break;

+ 1 - 1
app/src/main/java/com/nextcloud/talk/jobs/SignalingSettingsWorker.java

@@ -74,7 +74,7 @@ public class SignalingSettingsWorker extends Worker {
 
         Data data = getInputData();
 
-        long internalUserId = data.getLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), -1);
+        long internalUserId = data.getLong(BundleKeys.KEY_INTERNAL_USER_ID, -1);
 
         List<User> userEntityObjectList = new ArrayList<>();
         boolean userExists = userManager.getUserWithInternalId(internalUserId).isEmpty().blockingGet();

+ 0 - 4
app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt

@@ -155,10 +155,6 @@ data class Conversation(
         return isParticipantOwnerOrModerator && !isLockedOneToOne(conversationUser)
     }
 
-    fun shouldShowLobby(conversationUser: User): Boolean {
-        return LobbyState.LOBBY_STATE_MODERATORS_ONLY == lobbyState && !canModerate(conversationUser)
-    }
-
     fun isLobbyViewApplicable(conversationUser: User): Boolean {
         return !canModerate(conversationUser) &&
             (type == ConversationType.ROOM_GROUP_CALL || type == ConversationType.ROOM_PUBLIC_CALL)

+ 0 - 72
app/src/main/java/com/nextcloud/talk/utils/AttendeePermissionsUtil.kt

@@ -1,72 +0,0 @@
-/*
- * Nextcloud Talk application
- *
- * @author Marcel Hibbe
- * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.nextcloud.talk.utils
-
-import com.nextcloud.talk.data.user.model.User
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
-
-/**
- * see https://nextcloud-talk.readthedocs.io/en/latest/constants/#attendee-permissions
- */
-class AttendeePermissionsUtil(flag: Int) {
-    var isDefault: Boolean = false
-    var isCustom: Boolean = false
-    var canStartCall: Boolean = false
-    var canJoinCall: Boolean = false
-    var canIgnoreLobby: Boolean = false
-    var canPublishAudio: Boolean = false
-    var canPublishVideo: Boolean = false
-    var canPublishScreen: Boolean = false
-    private var hasChatPermission: Boolean = false
-
-    init {
-        isDefault = (flag and DEFAULT) == DEFAULT
-        isCustom = (flag and CUSTOM) == CUSTOM
-        canStartCall = (flag and START_CALL) == START_CALL
-        canJoinCall = (flag and JOIN_CALL) == JOIN_CALL
-        canIgnoreLobby = (flag and CAN_IGNORE_LOBBY) == CAN_IGNORE_LOBBY
-        canPublishAudio = (flag and PUBLISH_AUDIO) == PUBLISH_AUDIO
-        canPublishVideo = (flag and PUBLISH_VIDEO) == PUBLISH_VIDEO
-        canPublishScreen = (flag and PUBLISH_SCREEN) == PUBLISH_SCREEN
-        hasChatPermission = (flag and CHAT) == CHAT
-    }
-
-    fun hasChatPermission(user: User): Boolean {
-        if (CapabilitiesUtilNew.hasSpreedFeatureCapability(user, "chat-permission")) {
-            return hasChatPermission
-        }
-        // if capability is not available then the spreed version doesn't support to restrict this
-        return true
-    }
-
-    companion object {
-        val TAG = AttendeePermissionsUtil::class.simpleName
-        const val DEFAULT = 0
-        const val CUSTOM = 1
-        const val START_CALL = 2
-        const val JOIN_CALL = 4
-        const val CAN_IGNORE_LOBBY = 8
-        const val PUBLISH_AUDIO = 16
-        const val PUBLISH_VIDEO = 32
-        const val PUBLISH_SCREEN = 64
-        const val CHAT = 128
-    }
-}

+ 107 - 0
app/src/main/java/com/nextcloud/talk/utils/ParticipantPermissions.kt

@@ -0,0 +1,107 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * @author Tim Krüger
+ * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.utils
+
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.models.json.conversations.Conversation
+import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
+
+/**
+ * see https://nextcloud-talk.readthedocs.io/en/latest/constants/#attendee-permissions
+ */
+class ParticipantPermissions(
+    private val user: User,
+    private val conversation: Conversation
+) {
+
+    val isDefault = (conversation.permissions and DEFAULT) == DEFAULT
+    val isCustom = (conversation.permissions and CUSTOM) == CUSTOM
+    private val canStartCall = (conversation.permissions and START_CALL) == START_CALL
+    val canJoinCall = (conversation.permissions and JOIN_CALL) == JOIN_CALL
+    private val canIgnoreLobby = (conversation.permissions and CAN_IGNORE_LOBBY) == CAN_IGNORE_LOBBY
+    private val canPublishAudio = (conversation.permissions and PUBLISH_AUDIO) == PUBLISH_AUDIO
+    private val canPublishVideo = (conversation.permissions and PUBLISH_VIDEO) == PUBLISH_VIDEO
+    val canPublishScreen = (conversation.permissions and PUBLISH_SCREEN) == PUBLISH_SCREEN
+    private val hasChatPermission = (conversation.permissions and CHAT) == CHAT
+
+    private fun hasConversationPermissions(): Boolean {
+        return CapabilitiesUtilNew.hasSpreedFeatureCapability(
+            user,
+            "conversation-permissions"
+        )
+    }
+
+    fun canIgnoreLobby(): Boolean {
+        if (hasConversationPermissions()) {
+            return canIgnoreLobby
+        }
+
+        return false
+    }
+
+    fun canStartCall(): Boolean {
+        return if (hasConversationPermissions()) {
+            canStartCall
+        } else {
+            conversation.canStartCall
+        }
+    }
+
+    fun canPublishAudio(): Boolean {
+        return if (hasConversationPermissions()) {
+            canPublishAudio
+        } else {
+            true
+        }
+    }
+
+    fun canPublishVideo(): Boolean {
+        return if (hasConversationPermissions()) {
+            canPublishVideo
+        } else {
+            true
+        }
+    }
+
+    fun hasChatPermission(): Boolean {
+        if (CapabilitiesUtilNew.hasSpreedFeatureCapability(user, "chat-permission")) {
+            return hasChatPermission
+        }
+        // if capability is not available then the spreed version doesn't support to restrict this
+        return true
+    }
+
+    companion object {
+
+        val TAG = ParticipantPermissions::class.simpleName
+        const val DEFAULT = 0
+        const val CUSTOM = 1
+        const val START_CALL = 2
+        const val JOIN_CALL = 4
+        const val CAN_IGNORE_LOBBY = 8
+        const val PUBLISH_AUDIO = 16
+        const val PUBLISH_VIDEO = 32
+        const val PUBLISH_SCREEN = 64
+        const val CHAT = 128
+    }
+}

+ 54 - 52
app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt

@@ -2,6 +2,8 @@
  * Nextcloud Talk application
  *
  * @author Mario Danic
+ * @author Tim Krüger
+ * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
  * Copyright (C) 2017 Mario Danic
  *
  * This program is free software: you can redistribute it and/or modify
@@ -21,58 +23,58 @@
 package com.nextcloud.talk.utils.bundle
 
 object BundleKeys {
-    val KEY_SELECTED_USERS = "KEY_SELECTED_USERS"
-    val KEY_SELECTED_GROUPS = "KEY_SELECTED_GROUPS"
-    val KEY_SELECTED_CIRCLES = "KEY_SELECTED_CIRCLES"
-    val KEY_SELECTED_EMAILS = "KEY_SELECTED_EMAILS"
-    val KEY_USERNAME = "KEY_USERNAME"
-    val KEY_TOKEN = "KEY_TOKEN"
-    val KEY_BASE_URL = "KEY_BASE_URL"
-    val KEY_IS_ACCOUNT_IMPORT = "KEY_IS_ACCOUNT_IMPORT"
-    val KEY_ORIGINAL_PROTOCOL = "KEY_ORIGINAL_PROTOCOL"
-    val KEY_ROOM = "KEY_CONVERSATION"
-    val KEY_OPERATION_CODE = "KEY_OPERATION_CODE"
-    val KEY_MENU_TYPE = "KEY_MENU_TYPE"
-    val KEY_SHARE_INTENT = "KEY_SHARE_INTENT"
-    val KEY_APP_ITEM_PACKAGE_NAME = "KEY_APP_ITEM_PACKAGE_NAME"
-    val KEY_APP_ITEM_NAME = "KEY_APP_ITEM_NAME"
-    val KEY_CONVERSATION_PASSWORD = "KEY_CONVERSATION_PASSWORD"
-    val KEY_ROOM_TOKEN = "KEY_ROOM_TOKEN"
-    val KEY_ROOM_ONE_TO_ONE = "KEY_ROOM_ONE_TO_ONE"
-    val KEY_USER_ENTITY = "KEY_USER_ENTITY"
-    val KEY_NEW_CONVERSATION = "KEY_NEW_CONVERSATION"
-    val KEY_ADD_PARTICIPANTS = "KEY_ADD_PARTICIPANTS"
-    val KEY_EXISTING_PARTICIPANTS = "KEY_EXISTING_PARTICIPANTS"
-    val KEY_CALL_URL = "KEY_CALL_URL"
-    val KEY_MODIFIED_BASE_URL = "KEY_MODIFIED_BASE_URL"
-    val KEY_NOTIFICATION_SUBJECT = "KEY_NOTIFICATION_SUBJECT"
-    val KEY_NOTIFICATION_SIGNATURE = "KEY_NOTIFICATION_SIGNATURE"
-    val KEY_INTERNAL_USER_ID = "KEY_INTERNAL_USER_ID"
-    val KEY_CONVERSATION_TYPE = "KEY_CONVERSATION_TYPE"
-    val KEY_INVITED_PARTICIPANTS = "KEY_INVITED_PARTICIPANTS"
-    val KEY_INVITED_CIRCLE = "KEY_INVITED_CIRCLE"
-    val KEY_INVITED_GROUP = "KEY_INVITED_GROUP"
-    val KEY_INVITED_EMAIL = "KEY_INVITED_EMAIL"
-    val KEY_CONVERSATION_NAME = "KEY_CONVERSATION_NAME"
-    val KEY_CALL_VOICE_ONLY = "KEY_CALL_VOICE_ONLY"
-    val KEY_CALL_WITHOUT_NOTIFICATION = "KEY_CALL_WITHOUT_NOTIFICATION"
-    val KEY_ACTIVE_CONVERSATION = "KEY_ACTIVE_CONVERSATION"
-    val KEY_SERVER_CAPABILITIES = "KEY_SERVER_CAPABILITIES"
-    val KEY_FROM_NOTIFICATION_START_CALL = "KEY_FROM_NOTIFICATION_START_CALL"
-    val KEY_ROOM_ID = "KEY_ROOM_ID"
-    val KEY_ARE_CALL_SOUNDS = "KEY_ARE_CALL_SOUNDS"
-    val KEY_BROWSER_TYPE = "KEY_BROWSER_TYPE"
-    val KEY_FILE_PATHS = "KEY_FILE_PATHS"
-    val KEY_ACCOUNT = "KEY_ACCOUNT"
-    val KEY_FILE_ID = "KEY_FILE_ID"
-    val KEY_NOTIFICATION_ID = "KEY_NOTIFICATION_ID"
-    val KEY_SHARED_TEXT = "KEY_SHARED_TEXT"
-    val KEY_GEOCODING_QUERY = "KEY_GEOCODING_QUERY"
-    val KEY_META_DATA = "KEY_META_DATA"
-    val KEY_FORWARD_MSG_FLAG = "KEY_FORWARD_MSG_FLAG"
-    val KEY_FORWARD_MSG_TEXT = "KEY_FORWARD_MSG_TEXT"
-    val KEY_FORWARD_HIDE_SOURCE_ROOM = "KEY_FORWARD_HIDE_SOURCE_ROOM"
-    val KEY_SYSTEM_NOTIFICATION_ID = "KEY_SYSTEM_NOTIFICATION_ID"
+    const val KEY_SELECTED_USERS = "KEY_SELECTED_USERS"
+    const val KEY_SELECTED_GROUPS = "KEY_SELECTED_GROUPS"
+    const val KEY_SELECTED_CIRCLES = "KEY_SELECTED_CIRCLES"
+    const val KEY_SELECTED_EMAILS = "KEY_SELECTED_EMAILS"
+    const val KEY_USERNAME = "KEY_USERNAME"
+    const val KEY_TOKEN = "KEY_TOKEN"
+    const val KEY_BASE_URL = "KEY_BASE_URL"
+    const val KEY_IS_ACCOUNT_IMPORT = "KEY_IS_ACCOUNT_IMPORT"
+    const val KEY_ORIGINAL_PROTOCOL = "KEY_ORIGINAL_PROTOCOL"
+    const val KEY_ROOM = "KEY_CONVERSATION"
+    const val KEY_OPERATION_CODE = "KEY_OPERATION_CODE"
+    const val KEY_SHARE_INTENT = "KEY_SHARE_INTENT"
+    const val KEY_APP_ITEM_PACKAGE_NAME = "KEY_APP_ITEM_PACKAGE_NAME"
+    const val KEY_APP_ITEM_NAME = "KEY_APP_ITEM_NAME"
+    const val KEY_CONVERSATION_PASSWORD = "KEY_CONVERSATION_PASSWORD"
+    const val KEY_ROOM_TOKEN = "KEY_ROOM_TOKEN"
+    const val KEY_ROOM_ONE_TO_ONE = "KEY_ROOM_ONE_TO_ONE"
+    const val KEY_USER_ENTITY = "KEY_USER_ENTITY"
+    const val KEY_NEW_CONVERSATION = "KEY_NEW_CONVERSATION"
+    const val KEY_ADD_PARTICIPANTS = "KEY_ADD_PARTICIPANTS"
+    const val KEY_EXISTING_PARTICIPANTS = "KEY_EXISTING_PARTICIPANTS"
+    const val KEY_CALL_URL = "KEY_CALL_URL"
+    const val KEY_MODIFIED_BASE_URL = "KEY_MODIFIED_BASE_URL"
+    const val KEY_NOTIFICATION_SUBJECT = "KEY_NOTIFICATION_SUBJECT"
+    const val KEY_NOTIFICATION_SIGNATURE = "KEY_NOTIFICATION_SIGNATURE"
+    const val KEY_INTERNAL_USER_ID = "KEY_INTERNAL_USER_ID"
+    const val KEY_CONVERSATION_TYPE = "KEY_CONVERSATION_TYPE"
+    const val KEY_INVITED_PARTICIPANTS = "KEY_INVITED_PARTICIPANTS"
+    const val KEY_INVITED_CIRCLE = "KEY_INVITED_CIRCLE"
+    const val KEY_INVITED_GROUP = "KEY_INVITED_GROUP"
+    const val KEY_INVITED_EMAIL = "KEY_INVITED_EMAIL"
+    const val KEY_CONVERSATION_NAME = "KEY_CONVERSATION_NAME"
+    const val KEY_CALL_VOICE_ONLY = "KEY_CALL_VOICE_ONLY"
+    const val KEY_CALL_WITHOUT_NOTIFICATION = "KEY_CALL_WITHOUT_NOTIFICATION"
+    const val KEY_ACTIVE_CONVERSATION = "KEY_ACTIVE_CONVERSATION"
+    const val KEY_SERVER_CAPABILITIES = "KEY_SERVER_CAPABILITIES"
+    const val KEY_FROM_NOTIFICATION_START_CALL = "KEY_FROM_NOTIFICATION_START_CALL"
+    const val KEY_ROOM_ID = "KEY_ROOM_ID"
+    const val KEY_ARE_CALL_SOUNDS = "KEY_ARE_CALL_SOUNDS"
+    const val KEY_FILE_PATHS = "KEY_FILE_PATHS"
+    const val KEY_ACCOUNT = "KEY_ACCOUNT"
+    const val KEY_FILE_ID = "KEY_FILE_ID"
+    const val KEY_NOTIFICATION_ID = "KEY_NOTIFICATION_ID"
+    const val KEY_SHARED_TEXT = "KEY_SHARED_TEXT"
+    const val KEY_GEOCODING_QUERY = "KEY_GEOCODING_QUERY"
+    const val KEY_META_DATA = "KEY_META_DATA"
+    const val KEY_FORWARD_MSG_FLAG = "KEY_FORWARD_MSG_FLAG"
+    const val KEY_FORWARD_MSG_TEXT = "KEY_FORWARD_MSG_TEXT"
+    const val KEY_FORWARD_HIDE_SOURCE_ROOM = "KEY_FORWARD_HIDE_SOURCE_ROOM"
+    const val KEY_SYSTEM_NOTIFICATION_ID = "KEY_SYSTEM_NOTIFICATION_ID"
     const val KEY_MESSAGE_ID = "KEY_MESSAGE_ID"
     const val KEY_MIME_TYPE_FILTER = "KEY_MIME_TYPE_FILTER"
+    const val KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO = "KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO"
+    const val KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO = "KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO"
 }

+ 2 - 2
app/src/main/java/com/nextcloud/talk/webrtc/MagicWebSocketInstance.java

@@ -244,8 +244,8 @@ public class MagicWebSocketInstance extends WebSocketListener {
                                                     shouldRefreshChat = (boolean) chatMap.get("refresh");
                                                     if (shouldRefreshChat) {
                                                         HashMap<String, String> refreshChatHashMap = new HashMap<>();
-                                                        refreshChatHashMap.put(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), (String) messageHashMap.get("roomid"));
-                                                        refreshChatHashMap.put(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), Long.toString(conversationUser.getId()));
+                                                        refreshChatHashMap.put(BundleKeys.KEY_ROOM_TOKEN, (String) messageHashMap.get("roomid"));
+                                                        refreshChatHashMap.put(BundleKeys.KEY_INTERNAL_USER_ID, Long.toString(conversationUser.getId()));
                                                         eventBus.post(new WebSocketCommunicationEvent("refreshChat", refreshChatHashMap));
                                                     }
                                                 }

+ 3 - 0
app/src/main/res/values/strings.xml

@@ -602,4 +602,7 @@
     <string name="nc_expire_message_one_hour">1 hour</string>
     <string name="nc_expire_messages_explanation">Chat messages can be expired after a certain time. Note: Files shared in chat will not be deleted for the owner, but will no longer be shared in the conversation.</string>
 
+    <string name="nc_not_allowed_to_activate_audio">You\'re not allowed to activate audio!</string>
+    <string name="nc_not_allowed_to_activate_video">You\'re not allowed to activate video!</string>
+
 </resources>

+ 0 - 47
app/src/test/java/com/nextcloud/talk/utils/AttendeePermissionsUtilTest.kt

@@ -1,47 +0,0 @@
-/*
- * Nextcloud Talk application
- *
- * @author Marcel Hibbe
- * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.nextcloud.talk.utils
-
-import junit.framework.TestCase
-import org.junit.Test
-
-class AttendeePermissionsUtilTest : TestCase() {
-
-    @Test
-    fun test_areFlagsSet() {
-        val attendeePermissionsUtil =
-            AttendeePermissionsUtil(
-                AttendeePermissionsUtil.PUBLISH_SCREEN or
-                    AttendeePermissionsUtil.JOIN_CALL or
-                    AttendeePermissionsUtil.DEFAULT
-            )
-
-        assert(attendeePermissionsUtil.canPublishScreen)
-        assert(attendeePermissionsUtil.canJoinCall)
-        assert(attendeePermissionsUtil.isDefault)
-
-        assertFalse(attendeePermissionsUtil.isCustom)
-        assertFalse(attendeePermissionsUtil.canStartCall)
-        assertFalse(attendeePermissionsUtil.canIgnoreLobby)
-        assertFalse(attendeePermissionsUtil.canPublishAudio)
-        assertFalse(attendeePermissionsUtil.canPublishVideo)
-    }
-}

+ 57 - 0
app/src/test/java/com/nextcloud/talk/utils/ParticipantPermissionsTest.kt

@@ -0,0 +1,57 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * @author Tim Krüger
+ * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.utils
+
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.models.json.conversations.Conversation
+import junit.framework.TestCase
+import org.junit.Test
+
+class ParticipantPermissionsTest : TestCase() {
+
+    @Test
+    fun test_areFlagsSet() {
+
+        val user = User()
+        val conversation = Conversation()
+        conversation.permissions = ParticipantPermissions.PUBLISH_SCREEN or
+            ParticipantPermissions.JOIN_CALL or
+            ParticipantPermissions.DEFAULT
+
+        val attendeePermissions =
+            ParticipantPermissions(
+                user,
+                conversation
+            )
+
+        assert(attendeePermissions.canPublishScreen)
+        assert(attendeePermissions.canJoinCall)
+        assert(attendeePermissions.isDefault)
+
+        assertFalse(attendeePermissions.isCustom)
+        assertFalse(attendeePermissions.canStartCall())
+        assertFalse(attendeePermissions.canIgnoreLobby())
+        assertTrue(attendeePermissions.canPublishAudio())
+        assertTrue(attendeePermissions.canPublishVideo())
+    }
+}