Переглянути джерело

Merge pull request #1268 from nextcloud/viewBindingController

View binding controller implementation
Andy Scherzinger 3 роки тому
батько
коміт
9173bfbdaf
37 змінених файлів з 1516 додано та 1055 видалено
  1. 1 1
      app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt
  2. 5 1
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt
  3. 2 1
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java
  4. 10 1
      app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt
  5. 4 6
      app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserController.java
  6. 4 1
      app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java
  7. 254 224
      app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
  8. 13 14
      app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java
  9. 165 207
      app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt
  10. 9 5
      app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java
  11. 0 183
      app/src/main/java/com/nextcloud/talk/controllers/LockedController.java
  12. 173 0
      app/src/main/java/com/nextcloud/talk/controllers/LockedController.kt
  13. 50 52
      app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java
  14. 3 5
      app/src/main/java/com/nextcloud/talk/controllers/RingtoneSelectionController.java
  15. 6 5
      app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java
  16. 4 6
      app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.java
  17. 308 0
      app/src/main/java/com/nextcloud/talk/controllers/base/NewBaseController.kt
  18. 2 1
      app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/CallMenuController.java
  19. 117 79
      app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.java
  20. 49 0
      app/src/main/java/com/nextcloud/talk/controllers/util/ControllerViewBindingDelegate.kt
  21. 16 4
      app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt
  22. 3 3
      app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt
  23. 241 0
      app/src/main/java/com/nextcloud/talk/models/database/CapabilitiesUtil.java
  24. 3 209
      app/src/main/java/com/nextcloud/talk/models/database/User.java
  25. 3 1
      app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.java
  26. 1 2
      app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationRichObject.java
  27. 4 2
      app/src/main/java/com/nextcloud/talk/receivers/PackageReplacedReceiver.kt
  28. 2 1
      app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt
  29. 8 2
      app/src/main/java/com/nextcloud/talk/utils/AccountUtils.kt
  30. 13 11
      app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
  31. 14 11
      app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt
  32. 2 1
      app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.java
  33. 3 1
      app/src/main/res/layout/controller_chat.xml
  34. 21 12
      app/src/main/res/layout/controller_conversation_info.xml
  35. 1 1
      detekt.yml
  36. 1 1
      scripts/analysis/findbugs-results.txt
  37. 1 1
      scripts/analysis/lint-results.txt

+ 1 - 1
app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt

@@ -318,7 +318,7 @@ class MainActivity : BaseActivity(), ActionBarProvider {
             } else {
                 ConductorRemapping.remapChatController(
                     router!!, intent.getLongExtra(BundleKeys.KEY_INTERNAL_USER_ID, -1),
-                    intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN), intent.extras!!, false
+                    intent.getStringExtra(KEY_ROOM_TOKEN)!!, intent.extras!!, false
                 )
             }
         }

+ 5 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt

@@ -182,7 +182,11 @@ class MagicIncomingTextMessageViewHolder(incomingView: View) : MessageHolders
             for (key in messageParameters.keys) {
                 val individualHashMap = message.messageParameters[key]
                 if (individualHashMap != null) {
-                    if (individualHashMap["type"] == "user" || individualHashMap["type"] == "guest" || individualHashMap["type"] == "call") {
+                    if (
+                        individualHashMap["type"] == "user" ||
+                        individualHashMap["type"] == "guest" ||
+                        individualHashMap["type"] == "call"
+                    ) {
                         if (individualHashMap["id"] == message.activeUser!!.userId) {
                             messageString = DisplayUtils.searchAndReplaceWithMentionSpan(
                                 messageText!!.context,

+ 2 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java

@@ -46,6 +46,7 @@ import com.nextcloud.talk.components.filebrowser.models.BrowserFile;
 import com.nextcloud.talk.components.filebrowser.models.DavResponse;
 import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation;
 import com.nextcloud.talk.jobs.DownloadFileToCacheWorker;
+import com.nextcloud.talk.models.database.CapabilitiesUtil;
 import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.models.json.chat.ChatMessage;
 import com.nextcloud.talk.utils.AccountUtils;
@@ -335,7 +336,7 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM
 
         String baseUrl = message.activeUser.getBaseUrl();
         String userId = message.activeUser.getUserId();
-        String attachmentFolder = message.activeUser.getAttachmentFolder();
+        String attachmentFolder = CapabilitiesUtil.getAttachmentFolder(message.activeUser);
 
         String fileName = message.getSelectedIndividualHashMap().get("name");
         String mimetype = message.getSelectedIndividualHashMap().get("mimetype");

+ 10 - 1
app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt

@@ -77,7 +77,16 @@ import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 import javax.inject.Singleton
 
-@AutoComponent(modules = [BusModule::class, ContextModule::class, DatabaseModule::class, RestModule::class, UserModule::class, ArbitraryStorageModule::class])
+@AutoComponent(
+    modules = [
+        BusModule::class,
+        ContextModule::class,
+        DatabaseModule::class,
+        RestModule::class,
+        UserModule::class,
+        ArbitraryStorageModule::class
+    ]
+)
 @Singleton
 @AutoInjector(NextcloudTalkApplication::class)
 class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {

+ 4 - 6
app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserController.java

@@ -145,13 +145,11 @@ public abstract class BrowserController extends BaseController implements Listin
 
     @Override
     public boolean onOptionsItemSelected(@NonNull MenuItem item) {
-        switch (item.getItemId()) {
-            case R.id.files_selection_done:
-                onFileSelectionDone();
-                return true;
-            default:
-                return super.onOptionsItemSelected(item);
+        if (item.getItemId() == R.id.files_selection_done) {
+            onFileSelectionDone();
+            return true;
         }
+        return super.onOptionsItemSelected(item);
     }
 
     @Override

+ 4 - 1
app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java

@@ -64,6 +64,7 @@ import com.nextcloud.talk.controllers.base.BaseController;
 import com.nextcloud.talk.events.CallNotificationClick;
 import com.nextcloud.talk.events.ConfigurationChangeEvent;
 import com.nextcloud.talk.models.RingtoneSettings;
+import com.nextcloud.talk.models.database.CapabilitiesUtil;
 import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.models.json.conversations.Conversation;
 import com.nextcloud.talk.models.json.conversations.RoomOverall;
@@ -278,7 +279,9 @@ public class CallNotificationController extends BaseController {
                         runAllThings();
 
                         if (apiVersion >= 3) {
-                            boolean hasCallFlags = userBeingCalled.hasSpreedFeatureCapability("conversation-call-flags");
+                            boolean hasCallFlags =
+                                    CapabilitiesUtil.hasSpreedFeatureCapability(userBeingCalled,
+                                                                                "conversation-call-flags");
                             if (hasCallFlags) {
                                 if (isInCallWithVideo(currentConversation.callFlag)) {
                                     incomingCallVoiceOrVideoTextView.setText(String.format(getResources().getString(R.string.nc_call_video),

Різницю між файлами не показано, бо вона завелика
+ 254 - 224
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt


+ 13 - 14
app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java

@@ -56,6 +56,7 @@ import com.nextcloud.talk.controllers.bottomsheet.OperationsMenuController;
 import com.nextcloud.talk.events.BottomSheetLockEvent;
 import com.nextcloud.talk.jobs.AddParticipantsToConversation;
 import com.nextcloud.talk.models.RetrofitBucket;
+import com.nextcloud.talk.models.database.CapabilitiesUtil;
 import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall;
 import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser;
@@ -435,20 +436,18 @@ public class ContactsController extends BaseController implements SearchView.OnQ
 
     @Override
     public boolean onOptionsItemSelected(@NonNull MenuItem item) {
-        switch (item.getItemId()) {
-            case android.R.id.home:
-                getRouter().popCurrentController();
-                return true;
-            case R.id.contacts_selection_done:
-                selectionDone();
-                return true;
-            default:
-                return super.onOptionsItemSelected(item);
+        int itemId = item.getItemId();
+        if (itemId == android.R.id.home) {
+            return getRouter().popCurrentController();
+        } else if (itemId == R.id.contacts_selection_done) {
+            selectionDone();
+            return true;
         }
+        return super.onOptionsItemSelected(item);
     }
 
     @Override
-    public void onCreateOptionsMenu(Menu menu, @NonNull MenuInflater inflater) {
+    public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
         super.onCreateOptionsMenu(menu, inflater);
         inflater.inflate(R.menu.menu_contacts, menu);
         searchItem = menu.findItem(R.id.action_search);
@@ -493,13 +492,13 @@ public class ContactsController extends BaseController implements SearchView.OnQ
         if (!isAddingParticipantsView) {
             // groups
             shareTypesList.add("1");
-        } else if (currentUser.hasSpreedFeatureCapability("invite-groups-and-mails")) {
+        } else if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "invite-groups-and-mails")) {
             // groups
             shareTypesList.add("1");
             // emails
             shareTypesList.add("4");
         }
-        if (currentUser.hasSpreedFeatureCapability("circles-support")) {
+        if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "circles-support")) {
             // circles
             shareTypesList.add("7");
         }
@@ -974,8 +973,8 @@ public class ContactsController extends BaseController implements SearchView.OnQ
                     }
                 }
 
-                if (currentUser.hasSpreedFeatureCapability("last-room-activity")
-                        && !currentUser.hasSpreedFeatureCapability("invite-groups-and-mails") &&
+                if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "last-room-activity")
+                        && !CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "invite-groups-and-mails") &&
                         "groups".equals(((UserItem) adapter.getItem(position)).getModel().getSource()) &&
                         participant.isSelected() &&
                         adapter.getSelectedItemCount() > 1) {

+ 165 - 207
app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt

@@ -2,6 +2,8 @@
  * Nextcloud Talk application
  *
  * @author Mario Danic
+ * @author Andy Scherzinger
+ * Copyright (C) 2021 Andy Scherzinger (info@andy-scherzinger.de)
  * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -21,26 +23,19 @@
 package com.nextcloud.talk.controllers
 
 import android.annotation.SuppressLint
-import android.content.Context
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.LayerDrawable
 import android.os.Bundle
 import android.text.TextUtils
 import android.util.Log
-import android.view.LayoutInflater
 import android.view.MenuItem
 import android.view.View
-import android.view.ViewGroup
-import android.widget.ProgressBar
 import androidx.appcompat.widget.SwitchCompat
-import androidx.emoji.widget.EmojiTextView
-import androidx.recyclerview.widget.RecyclerView
+import androidx.core.content.ContextCompat
 import androidx.work.Data
 import androidx.work.OneTimeWorkRequest
 import androidx.work.WorkManager
 import autodagger.AutoInjector
-import butterknife.BindView
-import butterknife.OnClick
 import com.afollestad.materialdialogs.LayoutMode.WRAP_CONTENT
 import com.afollestad.materialdialogs.MaterialDialog
 import com.afollestad.materialdialogs.bottomsheets.BottomSheet
@@ -48,17 +43,19 @@ import com.afollestad.materialdialogs.datetime.dateTimePicker
 import com.bluelinelabs.conductor.RouterTransaction
 import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
 import com.facebook.drawee.backends.pipeline.Fresco
-import com.facebook.drawee.view.SimpleDraweeView
 import com.nextcloud.talk.R
 import com.nextcloud.talk.adapters.items.UserItem
 import com.nextcloud.talk.api.NcApi
 import com.nextcloud.talk.application.NextcloudTalkApplication
-import com.nextcloud.talk.controllers.base.BaseController
+import com.nextcloud.talk.controllers.base.NewBaseController
 import com.nextcloud.talk.controllers.bottomsheet.items.BasicListItemWithImage
 import com.nextcloud.talk.controllers.bottomsheet.items.listItemsWithImage
+import com.nextcloud.talk.controllers.util.viewBinding
+import com.nextcloud.talk.databinding.ControllerConversationInfoBinding
 import com.nextcloud.talk.events.EventStatus
 import com.nextcloud.talk.jobs.DeleteConversationWorker
 import com.nextcloud.talk.jobs.LeaveConversationWorker
+import com.nextcloud.talk.models.database.CapabilitiesUtil
 import com.nextcloud.talk.models.database.UserEntity
 import com.nextcloud.talk.models.json.conversations.Conversation
 import com.nextcloud.talk.models.json.conversations.RoomOverall
@@ -75,11 +72,6 @@ import com.nextcloud.talk.utils.bundle.BundleKeys
 import com.nextcloud.talk.utils.preferences.preferencestorage.DatabaseStorageModule
 import com.yarolegovich.lovelydialog.LovelySaveStateHandler
 import com.yarolegovich.lovelydialog.LovelyStandardDialog
-import com.yarolegovich.mp.MaterialChoicePreference
-import com.yarolegovich.mp.MaterialPreferenceCategory
-import com.yarolegovich.mp.MaterialPreferenceScreen
-import com.yarolegovich.mp.MaterialStandardPreference
-import com.yarolegovich.mp.MaterialSwitchPreference
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
 import io.reactivex.Observer
@@ -96,70 +88,22 @@ import java.util.Locale
 import javax.inject.Inject
 
 @AutoInjector(NextcloudTalkApplication::class)
-class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleAdapter.OnItemClickListener {
-
-    @BindView(R.id.notification_settings)
-    lateinit var notificationsPreferenceScreen: MaterialPreferenceScreen
-
-    @BindView(R.id.progressBar)
-    lateinit var progressBar: ProgressBar
-
-    @BindView(R.id.conversation_info_message_notifications)
-    lateinit var messageNotificationLevel: MaterialChoicePreference
-
-    @BindView(R.id.webinar_settings)
-    lateinit var conversationInfoWebinar: MaterialPreferenceScreen
-
-    @BindView(R.id.conversation_info_lobby)
-    lateinit var conversationInfoLobby: MaterialSwitchPreference
-
-    @BindView(R.id.conversation_info_name)
-    lateinit var nameCategoryView: MaterialPreferenceCategory
-
-    @BindView(R.id.start_time_preferences)
-    lateinit var startTimeView: MaterialStandardPreference
-
-    @BindView(R.id.avatar_image)
-    lateinit var conversationAvatarImageView: SimpleDraweeView
-
-    @BindView(R.id.display_name_text)
-    lateinit var conversationDisplayName: EmojiTextView
-
-    @BindView(R.id.conversation_description)
-    lateinit var descriptionCategoryView: MaterialPreferenceCategory
-
-    @BindView(R.id.description_text)
-    lateinit var conversationDescription: EmojiTextView
-
-    @BindView(R.id.participants_list_category)
-    lateinit var participantsListCategory: MaterialPreferenceCategory
-
-    @BindView(R.id.addParticipantsAction)
-    lateinit var addParticipantsAction: MaterialStandardPreference
-
-    @BindView(R.id.recycler_view)
-    lateinit var recyclerView: RecyclerView
-
-    @BindView(R.id.deleteConversationAction)
-    lateinit var deleteConversationAction: MaterialStandardPreference
-
-    @BindView(R.id.leaveConversationAction)
-    lateinit var leaveConversationAction: MaterialStandardPreference
-
-    @BindView(R.id.ownOptions)
-    lateinit var ownOptionsCategory: MaterialPreferenceCategory
-
-    @BindView(R.id.muteCalls)
-    lateinit var muteCalls: MaterialSwitchPreference
-
-    @set:Inject
-    lateinit var ncApi: NcApi
-
-    @set:Inject
-    lateinit var context: Context
-
-    @set:Inject
-    lateinit var eventBus: EventBus
+class ConversationInfoController(args: Bundle) :
+    NewBaseController(
+        R.layout.controller_conversation_info,
+        args
+    ),
+    FlexibleAdapter
+    .OnItemClickListener {
+    private val binding: ControllerConversationInfoBinding by viewBinding(ControllerConversationInfoBinding::bind)
+
+    @Inject
+    @JvmField
+    var ncApi: NcApi? = null
+
+    @Inject
+    @JvmField
+    var eventBus: EventBus? = null
 
     private val conversationToken: String?
     private val conversationUser: UserEntity?
@@ -207,20 +151,20 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
         }
     }
 
-    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
-        return inflater.inflate(R.layout.controller_conversation_info, container, false)
-    }
-
     override fun onAttach(view: View) {
         super.onAttach(view)
-        eventBus.register(this)
+        eventBus?.register(this)
 
         if (databaseStorageModule == null) {
             databaseStorageModule = DatabaseStorageModule(conversationUser!!, conversationToken)
         }
 
-        notificationsPreferenceScreen.setStorageModule(databaseStorageModule)
-        conversationInfoWebinar.setStorageModule(databaseStorageModule)
+        binding.notificationSettingsView.notificationSettings.setStorageModule(databaseStorageModule)
+        binding.webinarInfoView.webinarSettings.setStorageModule(databaseStorageModule)
+
+        binding.deleteConversationAction.setOnClickListener { showDeleteConversationDialog(null) }
+        binding.leaveConversationAction.setOnClickListener { leaveConversation() }
+        binding.addParticipantsAction.setOnClickListener { addParticipants() }
 
         fetchRoomInfo()
     }
@@ -232,27 +176,24 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
             saveStateHandler = LovelySaveStateHandler()
         }
 
-        addParticipantsAction.visibility = View.GONE
+        binding.addParticipantsAction.visibility = View.GONE
     }
 
     private fun setupWebinaryView() {
-        if (conversationUser!!.hasSpreedFeatureCapability("webinary-lobby") &&
-            (
-                conversation!!.type == Conversation.ConversationType.ROOM_GROUP_CALL ||
-                    conversation!!.type == Conversation.ConversationType.ROOM_PUBLIC_CALL
-                ) &&
+        if (CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "webinary-lobby") &&
+            webinaryRoomType(conversation!!) &&
             conversation!!.canModerate(conversationUser)
         ) {
-            conversationInfoWebinar.visibility = View.VISIBLE
+            binding.webinarInfoView.webinarSettings.visibility = View.VISIBLE
 
             val isLobbyOpenToModeratorsOnly =
                 conversation!!.lobbyState == Conversation.LobbyState.LOBBY_STATE_MODERATORS_ONLY
-            (conversationInfoLobby.findViewById<View>(R.id.mp_checkable) as SwitchCompat)
+            (binding.webinarInfoView.conversationInfoLobby.findViewById<View>(R.id.mp_checkable) as SwitchCompat)
                 .isChecked = isLobbyOpenToModeratorsOnly
 
             reconfigureLobbyTimerView()
 
-            startTimeView.setOnClickListener {
+            binding.webinarInfoView.startTimePreferences.setOnClickListener {
                 MaterialDialog(activity!!, BottomSheet(WRAP_CONTENT)).show {
                     val currentTimeCalendar = Calendar.getInstance()
                     if (conversation!!.lobbyTimer != null && conversation!!.lobbyTimer != 0L) {
@@ -273,17 +214,25 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
                 }
             }
 
-            (conversationInfoLobby.findViewById<View>(R.id.mp_checkable) as SwitchCompat).setOnCheckedChangeListener { _, _ ->
-                reconfigureLobbyTimerView()
-                submitLobbyChanges()
-            }
+            (binding.webinarInfoView.conversationInfoLobby.findViewById<View>(R.id.mp_checkable) as SwitchCompat)
+                .setOnCheckedChangeListener { _, _ ->
+                    reconfigureLobbyTimerView()
+                    submitLobbyChanges()
+                }
         } else {
-            conversationInfoWebinar.visibility = View.GONE
+            binding.webinarInfoView.webinarSettings.visibility = View.GONE
         }
     }
 
+    private fun webinaryRoomType(conversation: Conversation): Boolean {
+        return conversation.type == Conversation.ConversationType.ROOM_GROUP_CALL ||
+            conversation.type == Conversation.ConversationType.ROOM_PUBLIC_CALL
+    }
+
     fun reconfigureLobbyTimerView(dateTime: Calendar? = null) {
-        val isChecked = (conversationInfoLobby.findViewById<View>(R.id.mp_checkable) as SwitchCompat).isChecked
+        val isChecked =
+            (binding.webinarInfoView.conversationInfoLobby.findViewById<View>(R.id.mp_checkable) as SwitchCompat)
+                .isChecked
 
         if (dateTime != null && isChecked) {
             conversation!!.lobbyTimer = (dateTime.timeInMillis - (dateTime.time.seconds * 1000)) / 1000
@@ -294,35 +243,44 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
         conversation!!.lobbyState = if (isChecked) Conversation.LobbyState
             .LOBBY_STATE_MODERATORS_ONLY else Conversation.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS
 
-        if (conversation!!.lobbyTimer != null && conversation!!.lobbyTimer != java.lang.Long.MIN_VALUE && conversation!!.lobbyTimer != 0L) {
-            startTimeView.setSummary(DateUtils.getLocalDateStringFromTimestampForLobby(conversation!!.lobbyTimer))
+        if (
+            conversation!!.lobbyTimer != null &&
+            conversation!!.lobbyTimer != java.lang.Long.MIN_VALUE &&
+            conversation!!.lobbyTimer != 0L
+        ) {
+            binding.webinarInfoView.startTimePreferences.setSummary(
+                DateUtils.getLocalDateStringFromTimestampForLobby(
+                    conversation!!.lobbyTimer
+                )
+            )
         } else {
-            startTimeView.setSummary(R.string.nc_manual)
+            binding.webinarInfoView.startTimePreferences.setSummary(R.string.nc_manual)
         }
 
         if (isChecked) {
-            startTimeView.visibility = View.VISIBLE
+            binding.webinarInfoView.startTimePreferences.visibility = View.VISIBLE
         } else {
-            startTimeView.visibility = View.GONE
+            binding.webinarInfoView.startTimePreferences.visibility = View.GONE
         }
     }
 
     fun submitLobbyChanges() {
         val state = if (
-            (conversationInfoLobby.findViewById<View>(R.id.mp_checkable) as SwitchCompat).isChecked
+            (binding.webinarInfoView.conversationInfoLobby.findViewById<View>(R.id.mp_checkable) as SwitchCompat)
+                .isChecked
         ) 1 else 0
 
         val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
 
-        ncApi.setLobbyForConversation(
+        ncApi?.setLobbyForConversation(
             ApiUtils.getCredentials(conversationUser!!.username, conversationUser.token),
             ApiUtils.getUrlForRoomWebinaryLobby(apiVersion, conversationUser.baseUrl, conversation!!.token),
             state,
             conversation!!.lobbyTimer
         )
-            .subscribeOn(Schedulers.io())
-            .observeOn(AndroidSchedulers.mainThread())
-            .subscribe(object : Observer<GenericOverall> {
+            ?.subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(object : Observer<GenericOverall> {
                 override fun onComplete() {
                 }
 
@@ -352,7 +310,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
 
     override fun onDetach(view: View) {
         super.onDetach(view)
-        eventBus.unregister(this)
+        eventBus?.unregister(this)
     }
 
     private fun showDeleteConversationDialog(savedInstanceState: Bundle?) {
@@ -397,9 +355,9 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
             }
 
             val layoutManager = SmoothScrollLinearLayoutManager(activity)
-            recyclerView.layoutManager = layoutManager
-            recyclerView.setHasFixedSize(true)
-            recyclerView.adapter = adapter
+            binding.recyclerView.layoutManager = layoutManager
+            binding.recyclerView.setHasFixedSize(true)
+            binding.recyclerView.adapter = adapter
 
             adapter!!.addListener(this)
         }
@@ -438,17 +396,17 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
 
         setupAdapter()
 
-        participantsListCategory.visibility = View.VISIBLE
+        binding.participantsListCategory.visibility = View.VISIBLE
         adapter!!.updateDataSet(recyclerViewItems)
     }
 
-    override fun getTitle(): String? {
-        return if (hasAvatarSpacing) {
-            " " + resources!!.getString(R.string.nc_conversation_menu_conversation_info)
-        } else {
-            resources!!.getString(R.string.nc_conversation_menu_conversation_info)
-        }
-    }
+    override val title: String
+        get() =
+            if (hasAvatarSpacing) {
+                " " + resources!!.getString(R.string.nc_conversation_menu_conversation_info)
+            } else {
+                resources!!.getString(R.string.nc_conversation_menu_conversation_info)
+            }
 
     private fun getListOfParticipants() {
         var apiVersion = 1
@@ -457,13 +415,13 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
             apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
         }
 
-        ncApi.getPeersForCall(
+        ncApi?.getPeersForCall(
             credentials,
             ApiUtils.getUrlForParticipants(apiVersion, conversationUser!!.baseUrl, conversationToken)
         )
-            .subscribeOn(Schedulers.io())
-            .observeOn(AndroidSchedulers.mainThread())
-            .subscribe(object : Observer<ParticipantsOverall> {
+            ?.subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(object : Observer<ParticipantsOverall> {
                 override fun onSubscribe(d: Disposable) {
                     participantsDisposable = d
                 }
@@ -481,7 +439,6 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
             })
     }
 
-    @OnClick(R.id.addParticipantsAction)
     internal fun addParticipants() {
         val bundle = Bundle()
         val existingParticipantsId = arrayListOf<String>()
@@ -511,8 +468,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
         )
     }
 
-    @OnClick(R.id.leaveConversationAction)
-    internal fun leaveConversation() {
+    private fun leaveConversation() {
         workerData?.let {
             WorkManager.getInstance().enqueue(
                 OneTimeWorkRequest.Builder(
@@ -535,11 +491,6 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
         }
     }
 
-    @OnClick(R.id.deleteConversationAction)
-    internal fun deleteConversationClick() {
-        showDeleteConversationDialog(null)
-    }
-
     private fun popTwoLastControllers() {
         var backstack = router.backstack
         backstack = backstack.subList(0, backstack.size - 2)
@@ -553,10 +504,10 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
             apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
         }
 
-        ncApi.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, conversationUser!!.baseUrl, conversationToken))
-            .subscribeOn(Schedulers.io())
-            .observeOn(AndroidSchedulers.mainThread())
-            .subscribe(object : Observer<RoomOverall> {
+        ncApi?.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, conversationUser!!.baseUrl, conversationToken))
+            ?.subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(object : Observer<RoomOverall> {
                 override fun onSubscribe(d: Disposable) {
                     roomDisposable = d
                 }
@@ -567,49 +518,49 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
                     val conversationCopy = conversation
 
                     if (conversationCopy!!.canModerate(conversationUser)) {
-                        addParticipantsAction.visibility = View.VISIBLE
+                        binding.addParticipantsAction.visibility = View.VISIBLE
                     } else {
-                        addParticipantsAction.visibility = View.GONE
+                        binding.addParticipantsAction.visibility = View.GONE
                     }
 
                     if (isAttached && (!isBeingDestroyed || !isDestroyed)) {
-                        ownOptionsCategory.visibility = View.VISIBLE
+                        binding.ownOptions.visibility = View.VISIBLE
 
                         setupWebinaryView()
 
                         if (!conversation!!.canLeave(conversationUser)) {
-                            leaveConversationAction.visibility = View.GONE
+                            binding.leaveConversationAction.visibility = View.GONE
                         } else {
-                            leaveConversationAction.visibility = View.VISIBLE
+                            binding.leaveConversationAction.visibility = View.VISIBLE
                         }
 
                         if (!conversation!!.canDelete(conversationUser)) {
-                            deleteConversationAction.visibility = View.GONE
+                            binding.deleteConversationAction.visibility = View.GONE
                         } else {
-                            deleteConversationAction.visibility = View.VISIBLE
+                            binding.deleteConversationAction.visibility = View.VISIBLE
                         }
 
                         if (Conversation.ConversationType.ROOM_SYSTEM == conversation!!.type) {
-                            muteCalls.visibility = View.GONE
+                            binding.notificationSettingsView.muteCalls.visibility = View.GONE
                         }
 
                         getListOfParticipants()
 
-                        progressBar.visibility = View.GONE
+                        binding.progressBar.visibility = View.GONE
 
-                        nameCategoryView.visibility = View.VISIBLE
+                        binding.conversationInfoName.visibility = View.VISIBLE
 
-                        conversationDisplayName.text = conversation!!.displayName
+                        binding.displayNameText.text = conversation!!.displayName
 
                         if (conversation!!.description != null && !conversation!!.description.isEmpty()) {
-                            conversationDescription.text = conversation!!.description
-                            descriptionCategoryView.visibility = View.VISIBLE
+                            binding.descriptionText.text = conversation!!.description
+                            binding.conversationDescription.visibility = View.VISIBLE
                         }
 
                         loadConversationAvatar()
                         adjustNotificationLevelUI()
 
-                        notificationsPreferenceScreen.visibility = View.VISIBLE
+                        binding.notificationSettingsView.notificationSettings.visibility = View.VISIBLE
                     }
                 }
 
@@ -624,9 +575,12 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
 
     private fun adjustNotificationLevelUI() {
         if (conversation != null) {
-            if (conversationUser != null && conversationUser.hasSpreedFeatureCapability("notification-levels")) {
-                messageNotificationLevel.isEnabled = true
-                messageNotificationLevel.alpha = 1.0f
+            if (
+                conversationUser != null &&
+                CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "notification-levels")
+            ) {
+                binding.notificationSettingsView.conversationInfoMessageNotifications.isEnabled = true
+                binding.notificationSettingsView.conversationInfoMessageNotifications.alpha = 1.0f
 
                 if (conversation!!.notificationLevel != Conversation.NotificationLevel.DEFAULT) {
                     val stringValue: String =
@@ -637,13 +591,13 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
                             else -> "mention"
                         }
 
-                    messageNotificationLevel.value = stringValue
+                    binding.notificationSettingsView.conversationInfoMessageNotifications.value = stringValue
                 } else {
                     setProperNotificationValue(conversation)
                 }
             } else {
-                messageNotificationLevel.isEnabled = false
-                messageNotificationLevel.alpha = 0.38f
+                binding.notificationSettingsView.conversationInfoMessageNotifications.isEnabled = false
+                binding.notificationSettingsView.conversationInfoMessageNotifications.alpha = LOW_EMPHASIS_OPACITY
                 setProperNotificationValue(conversation)
             }
         }
@@ -652,13 +606,13 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
     private fun setProperNotificationValue(conversation: Conversation?) {
         if (conversation!!.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
             // hack to see if we get mentioned always or just on mention
-            if (conversationUser!!.hasSpreedFeatureCapability("mention-flag")) {
-                messageNotificationLevel.value = "always"
+            if (CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "mention-flag")) {
+                binding.notificationSettingsView.conversationInfoMessageNotifications.value = "always"
             } else {
-                messageNotificationLevel.value = "mention"
+                binding.notificationSettingsView.conversationInfoMessageNotifications.value = "mention"
             }
         } else {
-            messageNotificationLevel.value = "mention"
+            binding.notificationSettingsView.conversationInfoMessageNotifications.value = "mention"
         }
     }
 
@@ -666,7 +620,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
         when (conversation!!.type) {
             Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> if (!TextUtils.isEmpty(conversation!!.name)) {
                 val draweeController = Fresco.newDraweeControllerBuilder()
-                    .setOldController(conversationAvatarImageView.controller)
+                    .setOldController(binding.avatarImage.controller)
                     .setAutoPlayAnimations(true)
                     .setImageRequest(
                         DisplayUtils.getImageRequestForUrl(
@@ -678,20 +632,20 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
                         )
                     )
                     .build()
-                conversationAvatarImageView.controller = draweeController
+                binding.avatarImage.controller = draweeController
             }
-            Conversation.ConversationType.ROOM_GROUP_CALL -> conversationAvatarImageView.hierarchy.setPlaceholderImage(
+            Conversation.ConversationType.ROOM_GROUP_CALL -> binding.avatarImage.hierarchy.setPlaceholderImage(
                 R.drawable.ic_circular_group
             )
-            Conversation.ConversationType.ROOM_PUBLIC_CALL -> conversationAvatarImageView.hierarchy.setPlaceholderImage(
+            Conversation.ConversationType.ROOM_PUBLIC_CALL -> binding.avatarImage.hierarchy.setPlaceholderImage(
                 R.drawable.ic_circular_link
             )
             Conversation.ConversationType.ROOM_SYSTEM -> {
                 val layers = arrayOfNulls<Drawable>(2)
-                layers[0] = context.getDrawable(R.drawable.ic_launcher_background)
-                layers[1] = context.getDrawable(R.drawable.ic_launcher_foreground)
+                layers[0] = ContextCompat.getDrawable(context!!, R.drawable.ic_launcher_background)
+                layers[1] = ContextCompat.getDrawable(context!!, R.drawable.ic_launcher_foreground)
                 val layerDrawable = LayerDrawable(layers)
-                conversationAvatarImageView.hierarchy.setPlaceholderImage(DisplayUtils.getRoundedDrawable(layerDrawable))
+                binding.avatarImage.hierarchy.setPlaceholderImage(DisplayUtils.getRoundedDrawable(layerDrawable))
             }
 
             else -> {
@@ -720,7 +674,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
         if (participant.type == Participant.ParticipantType.MODERATOR ||
             participant.type == Participant.ParticipantType.GUEST_MODERATOR
         ) {
-            ncApi.demoteAttendeeFromModerator(
+            ncApi?.demoteAttendeeFromModerator(
                 credentials,
                 ApiUtils.getUrlForRoomModerators(
                     apiVersion,
@@ -729,13 +683,13 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
                 ),
                 participant.attendeeId
             )
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(subscriber)
+                ?.subscribeOn(Schedulers.io())
+                ?.observeOn(AndroidSchedulers.mainThread())
+                ?.subscribe(subscriber)
         } else if (participant.type == Participant.ParticipantType.USER ||
             participant.type == Participant.ParticipantType.GUEST
         ) {
-            ncApi.promoteAttendeeToModerator(
+            ncApi?.promoteAttendeeToModerator(
                 credentials,
                 ApiUtils.getUrlForRoomModerators(
                     apiVersion,
@@ -744,9 +698,9 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
                 ),
                 participant.attendeeId
             )
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(subscriber)
+                ?.subscribeOn(Schedulers.io())
+                ?.observeOn(AndroidSchedulers.mainThread())
+                ?.subscribe(subscriber)
         }
     }
 
@@ -769,7 +723,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
         }
 
         if (participant.type == Participant.ParticipantType.MODERATOR) {
-            ncApi.demoteModeratorToUser(
+            ncApi?.demoteModeratorToUser(
                 credentials,
                 ApiUtils.getUrlForRoomModerators(
                     apiVersion,
@@ -778,11 +732,11 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
                 ),
                 participant.userId
             )
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(subscriber)
+                ?.subscribeOn(Schedulers.io())
+                ?.observeOn(AndroidSchedulers.mainThread())
+                ?.subscribe(subscriber)
         } else if (participant.type == Participant.ParticipantType.USER) {
-            ncApi.promoteUserToModerator(
+            ncApi?.promoteUserToModerator(
                 credentials,
                 ApiUtils.getUrlForRoomModerators(
                     apiVersion,
@@ -791,15 +745,15 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
                 ),
                 participant.userId
             )
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(subscriber)
+                ?.subscribeOn(Schedulers.io())
+                ?.observeOn(AndroidSchedulers.mainThread())
+                ?.subscribe(subscriber)
         }
     }
 
     fun removeAttendeeFromConversation(apiVersion: Int, participant: Participant) {
         if (apiVersion >= ApiUtils.APIv4) {
-            ncApi.removeAttendeeFromConversation(
+            ncApi?.removeAttendeeFromConversation(
                 credentials,
                 ApiUtils.getUrlForAttendees(
                     apiVersion,
@@ -808,9 +762,9 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
                 ),
                 participant.attendeeId
             )
-                .subscribeOn(Schedulers.io())
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(object : Observer<GenericOverall> {
+                ?.subscribeOn(Schedulers.io())
+                ?.observeOn(AndroidSchedulers.mainThread())
+                ?.subscribe(object : Observer<GenericOverall> {
                     override fun onSubscribe(d: Disposable) {
                     }
 
@@ -830,7 +784,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
             if (participant.type == Participant.ParticipantType.GUEST ||
                 participant.type == Participant.ParticipantType.USER_FOLLOWING_LINK
             ) {
-                ncApi.removeParticipantFromConversation(
+                ncApi?.removeParticipantFromConversation(
                     credentials,
                     ApiUtils.getUrlForRemovingParticipantFromConversation(
                         conversationUser!!.baseUrl,
@@ -839,9 +793,9 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
                     ),
                     participant.sessionId
                 )
-                    .subscribeOn(Schedulers.io())
-                    .observeOn(AndroidSchedulers.mainThread())
-                    .subscribe(object : Observer<GenericOverall> {
+                    ?.subscribeOn(Schedulers.io())
+                    ?.observeOn(AndroidSchedulers.mainThread())
+                    ?.subscribe(object : Observer<GenericOverall> {
                         override fun onSubscribe(d: Disposable) {
                         }
 
@@ -858,7 +812,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
                         }
                     })
             } else {
-                ncApi.removeParticipantFromConversation(
+                ncApi?.removeParticipantFromConversation(
                     credentials,
                     ApiUtils.getUrlForRemovingParticipantFromConversation(
                         conversationUser!!.baseUrl,
@@ -867,9 +821,9 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
                     ),
                     participant.userId
                 )
-                    .subscribeOn(Schedulers.io())
-                    .observeOn(AndroidSchedulers.mainThread())
-                    .subscribe(object : Observer<GenericOverall> {
+                    ?.subscribeOn(Schedulers.io())
+                    ?.observeOn(AndroidSchedulers.mainThread())
+                    ?.subscribe(object : Observer<GenericOverall> {
                         override fun onSubscribe(d: Disposable) {
                         }
 
@@ -904,7 +858,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
                 val items = mutableListOf(
                     BasicListItemWithImage(
                         R.drawable.ic_lock_grey600_24px,
-                        context.getString(R.string.nc_attendee_pin, participant.attendeePin)
+                        context!!.getString(R.string.nc_attendee_pin, participant.attendeePin)
                     )
                 )
                 MaterialDialog(activity!!, BottomSheet(WRAP_CONTENT)).show {
@@ -930,7 +884,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
             val items = mutableListOf(
                 BasicListItemWithImage(
                     R.drawable.ic_delete_grey600_24dp,
-                    context.getString(R.string.nc_remove_group_and_members)
+                    context!!.getString(R.string.nc_remove_group_and_members)
                 )
             )
             MaterialDialog(activity!!, BottomSheet(WRAP_CONTENT)).show {
@@ -946,16 +900,16 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
             return true
         }
 
-        var items = mutableListOf(
+        val items = mutableListOf(
             BasicListItemWithImage(
                 R.drawable.ic_lock_grey600_24px,
-                context.getString(R.string.nc_attendee_pin, participant.attendeePin)
+                context!!.getString(R.string.nc_attendee_pin, participant.attendeePin)
             ),
-            BasicListItemWithImage(R.drawable.ic_pencil_grey600_24dp, context.getString(R.string.nc_promote)),
-            BasicListItemWithImage(R.drawable.ic_pencil_grey600_24dp, context.getString(R.string.nc_demote)),
+            BasicListItemWithImage(R.drawable.ic_pencil_grey600_24dp, context!!.getString(R.string.nc_promote)),
+            BasicListItemWithImage(R.drawable.ic_pencil_grey600_24dp, context!!.getString(R.string.nc_demote)),
             BasicListItemWithImage(
                 R.drawable.ic_delete_grey600_24dp,
-                context.getString(R.string.nc_remove_participant)
+                context!!.getString(R.string.nc_remove_participant)
             )
         )
 
@@ -1011,9 +965,9 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
     }
 
     companion object {
-
         private const val TAG = "ConversationInfoController"
         private const val ID_DELETE_CONVERSATION_DIALOG = 0
+        private val LOW_EMPHASIS_OPACITY: Float = 0.38f
     }
 
     /**
@@ -1025,7 +979,11 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
             val rightIsGroup = right.model.actorType == GROUPS
             if (leftIsGroup != rightIsGroup) {
                 // Groups below participants
-                return if (rightIsGroup) { -1 } else { 1 }
+                return if (rightIsGroup) {
+                    -1
+                } else {
+                    1
+                }
             }
 
             if (left.isOnline && !right.isOnline) {

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

@@ -79,6 +79,7 @@ import com.nextcloud.talk.jobs.AccountRemovalWorker;
 import com.nextcloud.talk.jobs.ContactAddressBookWorker;
 import com.nextcloud.talk.jobs.DeleteConversationWorker;
 import com.nextcloud.talk.jobs.UploadAndShareFilesWorker;
+import com.nextcloud.talk.models.database.CapabilitiesUtil;
 import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.models.json.conversations.Conversation;
 import com.nextcloud.talk.models.json.participants.Participant;
@@ -280,13 +281,14 @@ public class ConversationsListController extends BaseController implements Searc
         currentUser = userUtils.getCurrentUser();
 
         if (currentUser != null) {
-            if (currentUser.isServerEOL()) {
+            if (CapabilitiesUtil.isServerEOL(currentUser)) {
                 showServerEOLDialog();
                 return;
             }
 
             credentials = ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken());
-            shouldUseLastMessageLayout = currentUser.hasSpreedFeatureCapability("last-room-activity");
+            shouldUseLastMessageLayout = CapabilitiesUtil.hasSpreedFeatureCapability(currentUser,
+                                                                                     "last-room-activity");
             if (getActivity() != null && getActivity() instanceof MainActivity) {
                 loadUserAvatar(((MainActivity) getActivity()).binding.switchAccountButton);
             }
@@ -489,7 +491,7 @@ public class ConversationsListController extends BaseController implements Searc
                         }
                     }
 
-                    if (currentUser.hasSpreedFeatureCapability("last-room-activity")) {
+                    if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "last-room-activity")) {
                         Collections.sort(callItems, (o1, o2) -> {
                             Conversation conversation1 = ((ConversationItem) o1).getModel();
                             Conversation conversation2 = ((ConversationItem) o2).getModel();
@@ -817,7 +819,7 @@ public class ConversationsListController extends BaseController implements Searc
         if (showShareToScreen) {
             Log.d(TAG, "sharing to multiple rooms not yet implemented. onItemLongClick is ignored.");
 
-        } else if (currentUser.hasSpreedFeatureCapability("last-room-activity")) {
+        } else if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "last-room-activity")) {
             Object clickedItem = adapter.getItem(position);
             if (clickedItem != null) {
                 Conversation conversation;
@@ -883,7 +885,9 @@ public class ConversationsListController extends BaseController implements Searc
 
             Data data = new Data.Builder()
                     .putStringArray(UploadAndShareFilesWorker.DEVICE_SOURCEFILES, filesToShareArray)
-                    .putString(UploadAndShareFilesWorker.NC_TARGETPATH, currentUser.getAttachmentFolder())
+                    .putString(
+                            UploadAndShareFilesWorker.NC_TARGETPATH,
+                            CapabilitiesUtil.getAttachmentFolder(currentUser))
                     .putString(UploadAndShareFilesWorker.ROOM_TOKEN, selectedConversation.getToken())
                     .build();
             OneTimeWorkRequest uploadWorker = new OneTimeWorkRequest.Builder(UploadAndShareFilesWorker.class)

+ 0 - 183
app/src/main/java/com/nextcloud/talk/controllers/LockedController.java

@@ -1,183 +0,0 @@
-/*
- * Nextcloud Talk application
- *
- * @author Mario Danic
- * @author Andy Scherzinger
- * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
- * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.nextcloud.talk.controllers;
-
-import android.app.Activity;
-import android.app.KeyguardManager;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.nextcloud.talk.R;
-import com.nextcloud.talk.application.NextcloudTalkApplication;
-import com.nextcloud.talk.controllers.base.BaseController;
-import com.nextcloud.talk.utils.DisplayUtils;
-import com.nextcloud.talk.utils.SecurityUtils;
-import com.nextcloud.talk.utils.preferences.AppPreferences;
-
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.biometric.BiometricPrompt;
-import androidx.core.content.res.ResourcesCompat;
-import androidx.fragment.app.FragmentActivity;
-import autodagger.AutoInjector;
-import butterknife.OnClick;
-
-@AutoInjector(NextcloudTalkApplication.class)
-public class LockedController extends BaseController {
-    public static final String TAG = "LockedController";
-    private static final int REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS = 112;
-
-    @Inject
-    AppPreferences appPreferences;
-
-    @NonNull
-    @Override
-    protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
-        return inflater.inflate(R.layout.controller_locked, container, false);
-    }
-
-    @Override
-    protected void onViewBound(@NonNull View view) {
-        super.onViewBound(view);
-        NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
-    }
-
-    @RequiresApi(api = Build.VERSION_CODES.M)
-    @Override
-    protected void onAttach(@NonNull View view) {
-        super.onAttach(view);
-        if (getActivity() != null && getResources() != null) {
-            DisplayUtils.applyColorToStatusBar(getActivity(), ResourcesCompat.getColor(getResources(), R.color.colorPrimary, null));
-            DisplayUtils.applyColorToNavigationBar(getActivity().getWindow(), ResourcesCompat.getColor(getResources(), R.color.colorPrimary, null));
-        }
-        checkIfWeAreSecure();
-    }
-
-    @RequiresApi(api = Build.VERSION_CODES.M)
-    @OnClick(R.id.unlockContainer)
-    void unlock() {
-        checkIfWeAreSecure();
-    }
-
-    @RequiresApi(api = Build.VERSION_CODES.M)
-    private void showBiometricDialog() {
-        Context context = getActivity();
-
-        if (context != null) {
-            final BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
-                    .setTitle(String.format(context.getString(R.string.nc_biometric_unlock), context.getString(R.string.nc_app_name)))
-                    .setNegativeButtonText(context.getString(R.string.nc_cancel))
-                    .build();
-
-            Executor executor = Executors.newSingleThreadExecutor();
-
-            final BiometricPrompt biometricPrompt = new BiometricPrompt((FragmentActivity) context, executor,
-                    new BiometricPrompt.AuthenticationCallback() {
-                        @Override
-                        public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
-                            super.onAuthenticationSucceeded(result);
-                            Log.d(TAG, "Fingerprint recognised successfully");
-                            new Handler(Looper.getMainLooper()).post(() -> getRouter().popCurrentController());
-                        }
-
-                        @Override
-                        public void onAuthenticationFailed() {
-                            super.onAuthenticationFailed();
-                            Log.d(TAG, "Fingerprint not recognised");
-                        }
-
-                        @Override
-                        public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
-                            super.onAuthenticationError(errorCode, errString);
-                            showAuthenticationScreen();
-                        }
-                    }
-            );
-
-            BiometricPrompt.CryptoObject cryptoObject = SecurityUtils.getCryptoObject();
-            if (cryptoObject != null) {
-                biometricPrompt.authenticate(promptInfo, cryptoObject);
-            } else {
-                biometricPrompt.authenticate(promptInfo);
-            }
-        }
-    }
-
-    @RequiresApi(api = Build.VERSION_CODES.M)
-    private void checkIfWeAreSecure() {
-        if (getActivity() != null) {
-            KeyguardManager keyguardManager = (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE);
-            if (keyguardManager != null && keyguardManager.isKeyguardSecure() && appPreferences.getIsScreenLocked()) {
-                if (!SecurityUtils.checkIfWeAreAuthenticated(appPreferences.getScreenLockTimeout())) {
-                    showBiometricDialog();
-                } else {
-                    getRouter().popCurrentController();
-                }
-            }
-        }
-    }
-
-    private void showAuthenticationScreen() {
-        if (getActivity() != null) {
-            KeyguardManager keyguardManager = (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE);
-            Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(null, null);
-            if (intent != null) {
-                startActivityForResult(intent, REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS);
-            }
-        }
-    }
-
-    @Override
-    public void onActivityResult(int requestCode, int resultCode, Intent data) {
-        super.onActivityResult(requestCode, resultCode, data);
-
-        if (requestCode == REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS) {
-            if (resultCode == Activity.RESULT_OK) {
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                    if (SecurityUtils.checkIfWeAreAuthenticated(appPreferences.getScreenLockTimeout())) {
-                        Log.d(TAG, "All went well, dismiss locked controller");
-                        getRouter().popCurrentController();
-                    }
-                }
-            } else {
-                Log.d(TAG, "Authorization failed");
-            }
-        }
-    }
-
-    public AppBarLayoutType getAppBarLayoutType() {
-        return AppBarLayoutType.EMPTY;
-    }
-}

+ 173 - 0
app/src/main/java/com/nextcloud/talk/controllers/LockedController.kt

@@ -0,0 +1,173 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Andy Scherzinger
+ * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.controllers
+
+import android.app.Activity
+import android.app.KeyguardManager
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import android.view.View
+import androidx.annotation.RequiresApi
+import androidx.biometric.BiometricPrompt
+import androidx.biometric.BiometricPrompt.PromptInfo
+import androidx.core.content.res.ResourcesCompat
+import androidx.fragment.app.FragmentActivity
+import autodagger.AutoInjector
+import com.nextcloud.talk.R
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.controllers.base.NewBaseController
+import com.nextcloud.talk.controllers.util.viewBinding
+import com.nextcloud.talk.databinding.ControllerLockedBinding
+import com.nextcloud.talk.utils.DisplayUtils
+import com.nextcloud.talk.utils.SecurityUtils
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
+
+@AutoInjector(NextcloudTalkApplication::class)
+class LockedController : NewBaseController(R.layout.controller_locked) {
+    private val binding: ControllerLockedBinding by viewBinding(ControllerLockedBinding::bind)
+
+    override val appBarLayoutType: AppBarLayoutType
+        get() = AppBarLayoutType.EMPTY
+
+    companion object {
+        const val TAG = "LockedController"
+        private const val REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS = 112
+    }
+
+    override fun onViewBound(view: View) {
+        super.onViewBound(view)
+        sharedApplication!!.componentApplication.inject(this)
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            binding.unlockContainer.setOnClickListener {
+                unlock()
+            }
+        }
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.M)
+    override fun onAttach(view: View) {
+        super.onAttach(view)
+        if (activity != null && resources != null) {
+            DisplayUtils.applyColorToStatusBar(
+                activity,
+                ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null)
+            )
+            DisplayUtils.applyColorToNavigationBar(
+                activity!!.window,
+                ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null)
+            )
+        }
+        checkIfWeAreSecure()
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.M)
+    fun unlock() {
+        checkIfWeAreSecure()
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.M)
+    private fun showBiometricDialog() {
+        val context: Context? = activity
+        if (context != null) {
+            val promptInfo = PromptInfo.Builder()
+                .setTitle(
+                    String.format(
+                        context.getString(R.string.nc_biometric_unlock),
+                        context.getString(R.string.nc_app_name)
+                    )
+                )
+                .setNegativeButtonText(context.getString(R.string.nc_cancel))
+                .build()
+            val executor: Executor = Executors.newSingleThreadExecutor()
+            val biometricPrompt = BiometricPrompt(
+                (context as FragmentActivity?)!!, executor,
+                object : BiometricPrompt.AuthenticationCallback() {
+                    override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
+                        super.onAuthenticationSucceeded(result)
+                        Log.d(TAG, "Fingerprint recognised successfully")
+                        Handler(Looper.getMainLooper()).post { router.popCurrentController() }
+                    }
+
+                    override fun onAuthenticationFailed() {
+                        super.onAuthenticationFailed()
+                        Log.d(TAG, "Fingerprint not recognised")
+                    }
+
+                    override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+                        super.onAuthenticationError(errorCode, errString)
+                        showAuthenticationScreen()
+                    }
+                }
+            )
+            val cryptoObject = SecurityUtils.getCryptoObject()
+            if (cryptoObject != null) {
+                biometricPrompt.authenticate(promptInfo, cryptoObject)
+            } else {
+                biometricPrompt.authenticate(promptInfo)
+            }
+        }
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.M)
+    private fun checkIfWeAreSecure() {
+        val keyguardManager = activity?.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
+        if (keyguardManager?.isKeyguardSecure == true && appPreferences!!.isScreenLocked) {
+            if (!SecurityUtils.checkIfWeAreAuthenticated(appPreferences!!.screenLockTimeout)) {
+                showBiometricDialog()
+            } else {
+                router.popCurrentController()
+            }
+        }
+    }
+
+    private fun showAuthenticationScreen() {
+        val keyguardManager = activity?.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
+        val intent = keyguardManager?.createConfirmDeviceCredentialIntent(null, null)
+        if (intent != null) {
+            startActivityForResult(intent, REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS)
+        }
+    }
+
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        super.onActivityResult(requestCode, resultCode, data)
+        if (requestCode == REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS) {
+            if (resultCode == Activity.RESULT_OK) {
+                if (
+                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
+                    SecurityUtils.checkIfWeAreAuthenticated(appPreferences!!.screenLockTimeout)
+                ) {
+                    Log.d(TAG, "All went well, dismiss locked controller")
+                    router.popCurrentController()
+                }
+            } else {
+                Log.d(TAG, "Authorization failed")
+            }
+        }
+    }
+}

+ 50 - 52
app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java

@@ -53,6 +53,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.components.filebrowser.controllers.BrowserController;
 import com.nextcloud.talk.components.filebrowser.controllers.BrowserForAvatarController;
 import com.nextcloud.talk.controllers.base.BaseController;
+import com.nextcloud.talk.models.database.CapabilitiesUtil;
 import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.models.json.generic.GenericOverall;
 import com.nextcloud.talk.models.json.userprofile.Scope;
@@ -156,70 +157,67 @@ public class ProfileController extends BaseController {
 
     @Override
     public boolean onOptionsItemSelected(@NonNull MenuItem item) {
-        switch (item.getItemId()) {
-            case R.id.edit:
-                if (edit) {
-                    save();
-                }
+        if (item.getItemId() == R.id.edit) {
+            if (edit) {
+                save();
+            }
 
-                edit = !edit;
+            edit = !edit;
 
-                if (edit) {
-                    item.setTitle(R.string.save);
+            if (edit) {
+                item.setTitle(R.string.save);
 
-                    getActivity().findViewById(R.id.emptyList).setVisibility(View.GONE);
-                    getActivity().findViewById(R.id.userinfo_list).setVisibility(View.VISIBLE);
+                getActivity().findViewById(R.id.emptyList).setVisibility(View.GONE);
+                getActivity().findViewById(R.id.userinfo_list).setVisibility(View.VISIBLE);
 
-                    if (currentUser.isAvatarEndpointAvailable()) {
-                        // TODO later avatar can also be checked via user fields, for now it is in Talk capability
-                        getActivity().findViewById(R.id.avatar_buttons).setVisibility(View.VISIBLE);
-                    }
+                if (CapabilitiesUtil.isAvatarEndpointAvailable(currentUser)) {
+                    // TODO later avatar can also be checked via user fields, for now it is in Talk capability
+                    getActivity().findViewById(R.id.avatar_buttons).setVisibility(View.VISIBLE);
+                }
 
-                    ncApi.getEditableUserProfileFields(
-                            ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()),
-                            ApiUtils.getUrlForUserFields(currentUser.getBaseUrl()))
-                            .subscribeOn(Schedulers.io())
-                            .observeOn(AndroidSchedulers.mainThread())
-                            .subscribe(new Observer<UserProfileFieldsOverall>() {
-                                @Override
-                                public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
-                                    // unused atm
-                                }
+                ncApi.getEditableUserProfileFields(
+                        ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()),
+                        ApiUtils.getUrlForUserFields(currentUser.getBaseUrl()))
+                        .subscribeOn(Schedulers.io())
+                        .observeOn(AndroidSchedulers.mainThread())
+                        .subscribe(new Observer<UserProfileFieldsOverall>() {
+                            @Override
+                            public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
+                                // unused atm
+                            }
 
-                                @Override
-                                public void onNext(@io.reactivex.annotations.NonNull UserProfileFieldsOverall userProfileFieldsOverall) {
-                                    editableFields = userProfileFieldsOverall.getOcs().getData();
-                                    adapter.notifyDataSetChanged();
-                                }
+                            @Override
+                            public void onNext(@io.reactivex.annotations.NonNull UserProfileFieldsOverall userProfileFieldsOverall) {
+                                editableFields = userProfileFieldsOverall.getOcs().getData();
+                                adapter.notifyDataSetChanged();
+                            }
 
-                                @Override
-                                public void onError(@io.reactivex.annotations.NonNull Throwable e) {
-                                    Log.e(TAG, "Error loading editable user profile from server", e);
-                                    edit = false;
-                                }
+                            @Override
+                            public void onError(@io.reactivex.annotations.NonNull Throwable e) {
+                                Log.e(TAG, "Error loading editable user profile from server", e);
+                                edit = false;
+                            }
 
-                                @Override
-                                public void onComplete() {
-                                    // unused atm
-                                }
-                            });
-                } else {
-                    item.setTitle(R.string.edit);
-                    getActivity().findViewById(R.id.avatar_buttons).setVisibility(View.INVISIBLE);
+                            @Override
+                            public void onComplete() {
+                                // unused atm
+                            }
+                        });
+            } else {
+                item.setTitle(R.string.edit);
+                getActivity().findViewById(R.id.avatar_buttons).setVisibility(View.INVISIBLE);
 
-                    if (adapter.filteredDisplayList.size() == 0) {
-                        getActivity().findViewById(R.id.emptyList).setVisibility(View.VISIBLE);
-                        getActivity().findViewById(R.id.userinfo_list).setVisibility(View.GONE);
-                    }
+                if (adapter.filteredDisplayList.size() == 0) {
+                    getActivity().findViewById(R.id.emptyList).setVisibility(View.VISIBLE);
+                    getActivity().findViewById(R.id.userinfo_list).setVisibility(View.GONE);
                 }
+            }
 
-                adapter.notifyDataSetChanged();
-
-                return true;
+            adapter.notifyDataSetChanged();
 
-            default:
-                return super.onOptionsItemSelected(item);
+            return true;
         }
+        return super.onOptionsItemSelected(item);
     }
 
     @Override
@@ -345,7 +343,7 @@ public class ProfileController extends BaseController {
         }
 
         // show edit button
-        if (currentUser.canEditScopes()) {
+        if (CapabilitiesUtil.canEditScopes(currentUser)) {
             ncApi.getEditableUserProfileFields(ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()),
                     ApiUtils.getUrlForUserFields(currentUser.getBaseUrl()))
                     .subscribeOn(Schedulers.io())

+ 3 - 5
app/src/main/java/com/nextcloud/talk/controllers/RingtoneSelectionController.java

@@ -116,12 +116,10 @@ public class RingtoneSelectionController extends BaseController implements Flexi
 
     @Override
     public boolean onOptionsItemSelected(@NonNull MenuItem item) {
-        switch (item.getItemId()) {
-            case android.R.id.home:
-                return getRouter().popCurrentController();
-            default:
-                return super.onOptionsItemSelected(item);
+        if (item.getItemId() == android.R.id.home) {
+            return getRouter().popCurrentController();
         }
+        return super.onOptionsItemSelected(item);
     }
 
     private void prepareViews() {

+ 6 - 5
app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java

@@ -68,6 +68,7 @@ import com.nextcloud.talk.controllers.base.BaseController;
 import com.nextcloud.talk.jobs.AccountRemovalWorker;
 import com.nextcloud.talk.jobs.ContactAddressBookWorker;
 import com.nextcloud.talk.models.RingtoneSettings;
+import com.nextcloud.talk.models.database.CapabilitiesUtil;
 import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.models.json.generic.GenericOverall;
 import com.nextcloud.talk.models.json.userprofile.UserProfileOverall;
@@ -317,7 +318,7 @@ public class SettingsController extends BaseController {
                     .popChangeHandler(new HorizontalChangeHandler()));
         });
 
-        if (userUtils.getCurrentUser().isPhoneBookIntegrationAvailable()) {
+        if (CapabilitiesUtil.isPhoneBookIntegrationAvailable(userUtils.getCurrentUser())) {
             phoneBookIntegrationPreference.setVisibility(View.VISIBLE);
         } else {
             phoneBookIntegrationPreference.setVisibility(View.GONE);
@@ -456,8 +457,8 @@ public class SettingsController extends BaseController {
             ((Checkable) incognitoKeyboardSwitchPreference.findViewById(R.id.mp_checkable)).setChecked(appPreferences.getIsKeyboardIncognito());
         }
 
-        if (userUtils.getCurrentUser().isReadStatusAvailable()) {
-            ((Checkable) readPrivacyPreference.findViewById(R.id.mp_checkable)).setChecked(!currentUser.isReadStatusPrivate());
+        if (CapabilitiesUtil.isReadStatusAvailable(userUtils.getCurrentUser())) {
+            ((Checkable) readPrivacyPreference.findViewById(R.id.mp_checkable)).setChecked(!CapabilitiesUtil.isReadStatusPrivate(currentUser));
         } else {
             readPrivacyPreference.setVisibility(View.GONE);
         }
@@ -537,12 +538,12 @@ public class SettingsController extends BaseController {
 
             baseUrlTextView.setText(Uri.parse(currentUser.getBaseUrl()).getHost());
 
-            if (currentUser.isServerEOL()) {
+            if (CapabilitiesUtil.isServerEOL(currentUser)) {
                 serverAgeTextView.setTextColor(ContextCompat.getColor(context, R.color.nc_darkRed));
                 serverAgeTextView.setText(R.string.nc_settings_server_eol);
                 serverAgeIcon.setColorFilter(ContextCompat.getColor(context, R.color.nc_darkRed),
                                              PorterDuff.Mode.SRC_IN);
-            } else if (currentUser.isServerAlmostEOL()) {
+            } else if (CapabilitiesUtil.isServerAlmostEOL(currentUser)) {
                 serverAgeTextView.setTextColor(ContextCompat.getColor(context, R.color.nc_darkYellow));
                 serverAgeTextView.setText(R.string.nc_settings_server_almost_eol);
                 serverAgeIcon.setColorFilter(ContextCompat.getColor(context, R.color.nc_darkYellow),

+ 4 - 6
app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.java

@@ -84,13 +84,11 @@ public abstract class BaseController extends ButterKnifeController {
 
     @Override
     public boolean onOptionsItemSelected(@NonNull MenuItem item) {
-        switch (item.getItemId()) {
-            case android.R.id.home:
-                getRouter().popCurrentController();
-                return true;
-            default:
-                return super.onOptionsItemSelected(item);
+        if (item.getItemId() == android.R.id.home) {
+            getRouter().popCurrentController();
+            return true;
         }
+        return super.onOptionsItemSelected(item);
     }
 
     private void cleanTempCertPreference() {

+ 308 - 0
app/src/main/java/com/nextcloud/talk/controllers/base/NewBaseController.kt

@@ -0,0 +1,308 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * @author BlueLine Labs, Inc.
+ * @author Mario Danic
+ * Copyright (C) 2021 Andy Scherzinger (info@andy-scherzinger.de)
+ * Copyright (C) 2021 BlueLine Labs, Inc.
+ * Copyright (C) 2020 Mario Danic (mario@lovelyhq.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.nextcloud.talk.controllers.base
+
+import android.animation.AnimatorInflater
+import android.app.Activity
+import android.content.Context
+import android.content.res.Resources
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.InputMethodManager
+import android.widget.EditText
+import androidx.annotation.LayoutRes
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.ActionBar
+import androidx.core.content.res.ResourcesCompat
+import autodagger.AutoInjector
+import com.bluelinelabs.conductor.Controller
+import com.bluelinelabs.conductor.ControllerChangeHandler
+import com.bluelinelabs.conductor.ControllerChangeType
+import com.google.android.material.appbar.AppBarLayout
+import com.nextcloud.talk.R
+import com.nextcloud.talk.activities.MainActivity
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.controllers.AccountVerificationController
+import com.nextcloud.talk.controllers.ServerSelectionController
+import com.nextcloud.talk.controllers.SwitchAccountController
+import com.nextcloud.talk.controllers.WebViewLoginController
+import com.nextcloud.talk.controllers.base.providers.ActionBarProvider
+import com.nextcloud.talk.databinding.ActivityMainBinding
+import com.nextcloud.talk.utils.DisplayUtils
+import com.nextcloud.talk.utils.preferences.AppPreferences
+import java.util.ArrayList
+import javax.inject.Inject
+import kotlin.jvm.internal.Intrinsics
+
+@AutoInjector(NextcloudTalkApplication::class)
+abstract class NewBaseController(@LayoutRes var layoutRes: Int, args: Bundle? = null) : Controller(args) {
+    enum class AppBarLayoutType {
+        TOOLBAR, SEARCH_BAR, EMPTY
+    }
+
+    @Inject
+    @JvmField
+    var appPreferences: AppPreferences? = null
+
+    @Inject
+    @JvmField
+    var context: Context? = null
+
+    protected open val title: String?
+        get() = null
+
+    @Suppress("Detekt.TooGenericExceptionCaught")
+    protected val actionBar: ActionBar?
+        get() {
+            var actionBarProvider: ActionBarProvider? = null
+            if (this.activity is ActionBarProvider) {
+                try {
+                    actionBarProvider = this.activity as ActionBarProvider?
+                } catch (e: Exception) {
+                    Log.d(TAG, "Failed to fetch the action bar provider", e)
+                }
+            }
+            return actionBarProvider?.supportActionBar
+        }
+
+    init {
+        addLifecycleListener(object : LifecycleListener() {
+            override fun postCreateView(controller: Controller, view: View) {
+                onViewBound(view)
+                actionBar?.let { setTitle() }
+            }
+        })
+        cleanTempCertPreference()
+    }
+
+    fun isAlive(): Boolean {
+        return !isDestroyed && !isBeingDestroyed
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup,
+        savedViewState: Bundle?
+    ): View {
+        return inflater.inflate(layoutRes, container, false)
+    }
+
+    protected open fun onViewBound(view: View) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences!!.isKeyboardIncognito) {
+            disableKeyboardPersonalisedLearning(view as ViewGroup)
+            if (activity != null && activity is MainActivity) {
+                val activity = activity as MainActivity?
+                disableKeyboardPersonalisedLearning(activity!!.binding.appBar)
+            }
+        }
+    }
+
+    override fun onAttach(view: View) {
+        showSearchOrToolbar()
+        setTitle()
+        if (actionBar != null) {
+            actionBar!!.setDisplayHomeAsUpEnabled(parentController != null || router.backstackSize > 1)
+        }
+        super.onAttach(view)
+    }
+
+    protected fun showSearchOrToolbar() {
+        if (isValidActivity(activity)) {
+            val showSearchBar = appBarLayoutType == AppBarLayoutType.SEARCH_BAR
+            val activity = activity as MainActivity
+
+            if (appBarLayoutType == AppBarLayoutType.EMPTY) {
+                hideBars(activity.binding)
+            } else {
+                if (showSearchBar) {
+                    showSearchBar(activity.binding)
+                } else {
+                    showToolbar(activity.binding)
+                }
+                colorizeStatusBar(showSearchBar, activity, resources)
+            }
+
+            colorizeNavigationBar(activity, resources)
+        }
+    }
+
+    private fun isValidActivity(activity: Activity?): Boolean {
+        return activity != null && activity is MainActivity
+    }
+
+    private fun showSearchBar(binding: ActivityMainBinding) {
+        val layoutParams = binding.searchToolbar.layoutParams as AppBarLayout.LayoutParams
+        binding.searchToolbar.visibility = View.VISIBLE
+        binding.searchText.hint = searchHint
+        binding.toolbar.visibility = View.GONE
+        // layoutParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout
+        // .LayoutParams.SCROLL_FLAG_SNAP | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS);
+        layoutParams.scrollFlags = 0
+        binding.appBar.stateListAnimator = AnimatorInflater.loadStateListAnimator(
+            binding.appBar.context,
+            R.animator.appbar_elevation_off
+        )
+        binding.searchToolbar.layoutParams = layoutParams
+    }
+
+    private fun showToolbar(binding: ActivityMainBinding) {
+        val layoutParams = binding.searchToolbar.layoutParams as AppBarLayout.LayoutParams
+        binding.searchToolbar.visibility = View.GONE
+        binding.toolbar.visibility = View.VISIBLE
+        layoutParams.scrollFlags = 0
+        binding.appBar.stateListAnimator = AnimatorInflater.loadStateListAnimator(
+            binding.appBar.context,
+            R.animator.appbar_elevation_on
+        )
+        binding.searchToolbar.layoutParams = layoutParams
+    }
+
+    private fun hideBars(binding: ActivityMainBinding) {
+        binding.toolbar.visibility = View.GONE
+        binding.searchToolbar.visibility = View.GONE
+    }
+
+    private fun colorizeStatusBar(showSearchBar: Boolean, activity: Activity?, resources: Resources?) {
+        if (activity != null && resources != null) {
+            if (showSearchBar) {
+                DisplayUtils.applyColorToStatusBar(
+                    activity,
+                    ResourcesCompat.getColor(
+                        resources, R.color.bg_default, null
+                    )
+                )
+            } else {
+                DisplayUtils.applyColorToStatusBar(
+                    activity,
+                    ResourcesCompat.getColor(
+                        resources, R.color.appbar, null
+                    )
+                )
+            }
+        }
+    }
+
+    private fun colorizeNavigationBar(activity: Activity?, resources: Resources?) {
+        if (activity != null && resources != null) {
+            DisplayUtils.applyColorToNavigationBar(
+                activity.window,
+                ResourcesCompat.getColor(resources, R.color.bg_default, null)
+            )
+        }
+    }
+
+    override fun onDetach(view: View) {
+        super.onDetach(view)
+        val imm = context!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+        imm.hideSoftInputFromWindow(view.windowToken, 0)
+    }
+
+    protected fun setTitle() {
+        if (isTitleSetable()) {
+            run {
+                calculateValidParentController()
+            }
+            actionBar!!.title = title
+        }
+    }
+
+    private fun calculateValidParentController() {
+        var parentController = parentController
+        while (parentController != null) {
+            if (isValidController(parentController)) {
+                return
+            }
+            parentController = parentController.parentController
+        }
+    }
+
+    private fun isValidController(parentController: Controller): Boolean {
+        return parentController is BaseController && parentController.title != null
+    }
+
+    private fun isTitleSetable(): Boolean {
+        return title != null && actionBar != null
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        if (item.itemId == android.R.id.home) {
+            router.popCurrentController()
+            return true
+        }
+        return super.onOptionsItemSelected(item)
+    }
+
+    override fun onChangeStarted(changeHandler: ControllerChangeHandler, changeType: ControllerChangeType) {
+        super.onChangeStarted(changeHandler, changeType)
+        if (changeType.isEnter && actionBar != null) {
+            configureMenu(actionBar!!)
+        }
+    }
+
+    fun configureMenu(toolbar: ActionBar) {
+        Intrinsics.checkNotNullParameter(toolbar, "toolbar")
+    }
+
+    private fun cleanTempCertPreference() {
+        sharedApplication!!.componentApplication.inject(this)
+        val temporaryClassNames: MutableList<String> = ArrayList()
+        temporaryClassNames.add(ServerSelectionController::class.java.name)
+        temporaryClassNames.add(AccountVerificationController::class.java.name)
+        temporaryClassNames.add(WebViewLoginController::class.java.name)
+        temporaryClassNames.add(SwitchAccountController::class.java.name)
+        if (!temporaryClassNames.contains(javaClass.name)) {
+            appPreferences!!.removeTemporaryClientCertAlias()
+        }
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.O)
+    private fun disableKeyboardPersonalisedLearning(viewGroup: ViewGroup) {
+        var view: View?
+        var editText: EditText
+        for (i in 0 until viewGroup.childCount) {
+            view = viewGroup.getChildAt(i)
+            if (view is EditText) {
+                editText = view
+                editText.imeOptions = editText.imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
+            } else if (view is ViewGroup) {
+                disableKeyboardPersonalisedLearning(view)
+            }
+        }
+    }
+
+    open val appBarLayoutType: AppBarLayoutType
+        get() = AppBarLayoutType.TOOLBAR
+    val searchHint: String
+        get() = context!!.getString(R.string.appbar_search_in, context!!.getString(R.string.nc_app_name))
+
+    companion object {
+        private val TAG = BaseController::class.java.simpleName
+    }
+}

+ 2 - 1
app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/CallMenuController.java

@@ -48,6 +48,7 @@ import com.nextcloud.talk.controllers.base.BaseController;
 import com.nextcloud.talk.events.BottomSheetLockEvent;
 import com.nextcloud.talk.interfaces.ConversationMenuInterface;
 import com.nextcloud.talk.jobs.LeaveConversationWorker;
+import com.nextcloud.talk.models.database.CapabilitiesUtil;
 import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.models.json.conversations.Conversation;
 import com.nextcloud.talk.utils.DisplayUtils;
@@ -151,7 +152,7 @@ public class CallMenuController extends BaseController implements FlexibleAdapte
 
             if (conversation.isFavorite()) {
                 menuItems.add(new MenuItem(getResources().getString(R.string.nc_remove_from_favorites), 97, DisplayUtils.getTintedDrawable(getResources(), R.drawable.ic_star_border_black_24dp, R.color.grey_600)));
-            } else if (currentUser.hasSpreedFeatureCapability("favorites")) {
+            } else if (CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "favorites")) {
                 menuItems.add(new MenuItem(getResources().getString(R.string.nc_add_to_favorites)
                         , 98, DisplayUtils.getTintedDrawable(getResources(), R.drawable.ic_star_black_24dp, R.color.grey_600)));
             }

+ 117 - 79
app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.java

@@ -34,8 +34,6 @@ import android.widget.ImageView;
 import android.widget.ProgressBar;
 import android.widget.TextView;
 
-import androidx.annotation.NonNull;
-
 import com.bluelinelabs.conductor.RouterTransaction;
 import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
 import com.bluelinelabs.logansquare.LoganSquare;
@@ -46,6 +44,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.controllers.base.BaseController;
 import com.nextcloud.talk.events.BottomSheetLockEvent;
 import com.nextcloud.talk.models.RetrofitBucket;
+import com.nextcloud.talk.models.database.CapabilitiesUtil;
 import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.models.json.capabilities.Capabilities;
 import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall;
@@ -69,6 +68,7 @@ import java.util.ArrayList;
 
 import javax.inject.Inject;
 
+import androidx.annotation.NonNull;
 import autodagger.AutoInjector;
 import butterknife.BindView;
 import io.reactivex.Observer;
@@ -157,6 +157,7 @@ public class OperationsMenuController extends BaseController {
 
     }
 
+    @NonNull
     @Override
     protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
         return inflater.inflate(R.layout.controller_operations_menu, container, false);
@@ -222,17 +223,23 @@ public class OperationsMenuController extends BaseController {
                 .observeOn(AndroidSchedulers.mainThread())
                 .subscribe(new Observer<CapabilitiesOverall>() {
                     @Override
-                    public void onSubscribe(Disposable d) {
+                    public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
                     }
 
                     @SuppressLint("LongLogTag")
                     @Override
-                    public void onNext(CapabilitiesOverall capabilitiesOverall) {
+                    public void onNext(@io.reactivex.annotations.NonNull CapabilitiesOverall capabilitiesOverall) {
                         currentUser = new UserEntity();
                         currentUser.setBaseUrl(baseUrl);
                         currentUser.setUserId("?");
                         try {
-                            currentUser.setCapabilities(LoganSquare.serialize(capabilitiesOverall.getOcs().getData().getCapabilities()));
+                            currentUser.setCapabilities(
+                                    LoganSquare
+                                            .serialize(
+                                                    capabilitiesOverall
+                                                            .getOcs()
+                                                            .getData()
+                                                            .getCapabilities()));
                         } catch (IOException e) {
                             Log.e("OperationsMenu", "Failed to serialize capabilities");
                         }
@@ -248,7 +255,7 @@ public class OperationsMenuController extends BaseController {
 
                     @SuppressLint("LongLogTag")
                     @Override
-                    public void onError(Throwable e) {
+                    public void onError(@io.reactivex.annotations.NonNull Throwable e) {
                         showResultImage(false, false);
                         Log.e(TAG, "Error fetching capabilities for guest", e);
                     }
@@ -325,12 +332,12 @@ public class OperationsMenuController extends BaseController {
                         .retry(1)
                         .subscribe(new Observer<RoomOverall>() {
                             @Override
-                            public void onSubscribe(Disposable d) {
+                            public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
                                 disposable = d;
                             }
 
                             @Override
-                            public void onNext(RoomOverall roomOverall) {
+                            public void onNext(@io.reactivex.annotations.NonNull RoomOverall roomOverall) {
                                 conversation = roomOverall.getOcs().getData();
                                 if (conversation.isHasPassword() && conversation.isGuest()) {
                                     eventBus.post(new BottomSheetLockEvent(true, 0,
@@ -358,25 +365,27 @@ public class OperationsMenuController extends BaseController {
                                             .observeOn(AndroidSchedulers.mainThread())
                                             .subscribe(new Observer<RoomOverall>() {
                                                 @Override
-                                                public void onSubscribe(Disposable d) {
-
+                                                public void onSubscribe(
+                                                        @io.reactivex.annotations.NonNull Disposable d
+                                                                       ) {
                                                 }
 
                                                 @Override
-                                                public void onNext(RoomOverall roomOverall) {
+                                                public void onNext(
+                                                        @io.reactivex.annotations.NonNull RoomOverall roomOverall
+                                                                  ) {
                                                     conversation = roomOverall.getOcs().getData();
                                                     initiateConversation(false);
                                                 }
 
                                                 @Override
-                                                public void onError(Throwable e) {
+                                                public void onError(@io.reactivex.annotations.NonNull Throwable e) {
                                                     showResultImage(false, false);
                                                     dispose();
                                                 }
 
                                                 @Override
                                                 public void onComplete() {
-
                                                 }
                                             });
                                 } else {
@@ -385,7 +394,7 @@ public class OperationsMenuController extends BaseController {
                             }
 
                             @Override
-                            public void onError(Throwable e) {
+                            public void onError(@io.reactivex.annotations.NonNull Throwable e) {
                                 showResultImage(false, false);
                                 dispose();
                             }
@@ -418,12 +427,12 @@ public class OperationsMenuController extends BaseController {
                         .retry(1)
                         .subscribe(new Observer<RoomOverall>() {
                             @Override
-                            public void onSubscribe(Disposable d) {
+                            public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
 
                             }
 
                             @Override
-                            public void onNext(RoomOverall roomOverall) {
+                            public void onNext(@io.reactivex.annotations.NonNull RoomOverall roomOverall) {
                                 conversation = roomOverall.getOcs().getData();
 
                                 ncApi.getRoom(credentials,
@@ -433,18 +442,20 @@ public class OperationsMenuController extends BaseController {
                                         .observeOn(AndroidSchedulers.mainThread())
                                         .subscribe(new Observer<RoomOverall>() {
                                             @Override
-                                            public void onSubscribe(Disposable d) {
+                                            public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
 
                                             }
 
                                             @Override
-                                            public void onNext(RoomOverall roomOverall) {
+                                            public void onNext(
+                                                    @io.reactivex.annotations.NonNull RoomOverall roomOverall
+                                                              ) {
                                                 conversation = roomOverall.getOcs().getData();
                                                 inviteUsersToAConversation();
                                             }
 
                                             @Override
-                                            public void onError(Throwable e) {
+                                            public void onError(@io.reactivex.annotations.NonNull Throwable e) {
                                                 showResultImage(false, false);
                                                 dispose();
                                             }
@@ -458,7 +469,7 @@ public class OperationsMenuController extends BaseController {
                             }
 
                             @Override
-                            public void onError(Throwable e) {
+                            public void onError(@io.reactivex.annotations.NonNull Throwable e) {
                                 showResultImage(false, false);
                                 dispose();
                             }
@@ -510,12 +521,16 @@ public class OperationsMenuController extends BaseController {
     private void showResultImage(boolean everythingOK, boolean isGuestSupportError) {
         progressBar.setVisibility(View.GONE);
 
-        if (everythingOK) {
-            resultImageView.setImageDrawable(DisplayUtils.getTintedDrawable(getResources(), R.drawable
-                    .ic_check_circle_black_24dp, R.color.nc_darkGreen));
-        } else {
-            resultImageView.setImageDrawable(DisplayUtils.getTintedDrawable(getResources(), R.drawable
-                    .ic_cancel_black_24dp, R.color.nc_darkRed));
+        if (getResources() != null) {
+            if (everythingOK) {
+                resultImageView.setImageDrawable(DisplayUtils.getTintedDrawable(getResources(),
+                                                                                R.drawable.ic_check_circle_black_24dp,
+                                                                                R.color.nc_darkGreen));
+            } else {
+                resultImageView.setImageDrawable(DisplayUtils.getTintedDrawable(getResources(),
+                                                                                R.drawable.ic_cancel_black_24dp,
+                                                                                R.color.nc_darkRed));
+            }
         }
 
         resultImageView.setVisibility(View.VISIBLE);
@@ -581,59 +596,81 @@ public class OperationsMenuController extends BaseController {
 
         int apiVersion = ApiUtils.getConversationApiVersion(currentUser, new int[] {4, 1});
 
-        if (localInvitedUsers.size() > 0 || (localInvitedGroups.size() > 0 && currentUser.hasSpreedFeatureCapability("invite-groups-and-mails"))) {
-            if ((localInvitedGroups.size() > 0 && currentUser.hasSpreedFeatureCapability("invite-groups-and-mails"))) {
-                for (int i = 0; i < localInvitedGroups.size(); i++) {
-                    final String groupId = localInvitedGroups.get(i);
-                    retrofitBucket = ApiUtils.getRetrofitBucketForAddParticipantWithSource(
-                            apiVersion,
-                            currentUser.getBaseUrl(),
-                            conversation.getToken(),
-                            "groups",
-                            groupId
-                                                                                          );
-
-                    ncApi.addParticipant(credentials, retrofitBucket.getUrl(), retrofitBucket.getQueryMap())
-                            .subscribeOn(Schedulers.io())
-                            .observeOn(AndroidSchedulers.mainThread())
-                            .retry(1)
-                            .subscribe(new Observer<AddParticipantOverall>() {
-                                @Override
-                                public void onSubscribe(Disposable d) {
+        if (localInvitedUsers.size() > 0 || (localInvitedGroups.size() > 0 &&
+                CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "invite-groups-and-mails"))) {
+            addGroupsToConversation(localInvitedUsers, localInvitedGroups, apiVersion);
+            addUsersToConversation(localInvitedUsers, localInvitedGroups, apiVersion);
+        } else {
+            initiateConversation(true);
+        }
+    }
 
-                                }
+    private void addUsersToConversation(
+            ArrayList<String> localInvitedUsers,
+            ArrayList<String> localInvitedGroups,
+            int apiVersion)
+    {
+        RetrofitBucket retrofitBucket;
+        for (int i = 0; i < localInvitedUsers.size(); i++) {
+            final String userId = invitedUsers.get(i);
+            retrofitBucket = ApiUtils.getRetrofitBucketForAddParticipant(apiVersion,
+                                                                         currentUser.getBaseUrl(),
+                                                                         conversation.getToken(),
+                                                                         userId);
+
+            ncApi.addParticipant(credentials, retrofitBucket.getUrl(), retrofitBucket.getQueryMap())
+                    .subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .retry(1)
+                    .subscribe(new Observer<AddParticipantOverall>() {
+                        @Override
+                        public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
 
-                                @Override
-                                public void onNext(AddParticipantOverall addParticipantOverall) {
-                                }
+                        }
 
-                                @Override
-                                public void onError(Throwable e) {
-                                    dispose();
-                                }
+                        @Override
+                        public void onNext(
+                                @io.reactivex.annotations.NonNull AddParticipantOverall addParticipantOverall
+                                          ) {
+                        }
 
-                                @Override
-                                public void onComplete() {
-                                    synchronized (localInvitedGroups) {
-                                        localInvitedGroups.remove(groupId);
-                                    }
+                        @Override
+                        public void onError(@io.reactivex.annotations.NonNull Throwable e) {
+                            dispose();
+                        }
 
-                                    if (localInvitedGroups.size() == 0 && localInvitedUsers.size() == 0) {
-                                        initiateConversation(true);
-                                    }
-                                    dispose();
-                                }
-                            });
+                        @Override
+                        public void onComplete() {
+                            synchronized (localInvitedUsers) {
+                                localInvitedUsers.remove(userId);
+                            }
 
-                }
-            }
+                            if (localInvitedGroups.size() == 0 && localInvitedUsers.size() == 0) {
+                                initiateConversation(true);
+                            }
+                            dispose();
+                        }
+                    });
+        }
+    }
 
-            for (int i = 0; i < localInvitedUsers.size(); i++) {
-                final String userId = invitedUsers.get(i);
-                retrofitBucket = ApiUtils.getRetrofitBucketForAddParticipant(apiVersion,
-                                                                             currentUser.getBaseUrl(),
-                                                                             conversation.getToken(),
-                                                                             userId);
+    private void addGroupsToConversation(
+            ArrayList<String> localInvitedUsers,
+            ArrayList<String> localInvitedGroups,
+            int apiVersion)
+    {
+        RetrofitBucket retrofitBucket;
+        if ((localInvitedGroups.size() > 0 &&
+                CapabilitiesUtil.hasSpreedFeatureCapability(currentUser, "invite-groups-and-mails"))) {
+            for (int i = 0; i < localInvitedGroups.size(); i++) {
+                final String groupId = localInvitedGroups.get(i);
+                retrofitBucket = ApiUtils.getRetrofitBucketForAddParticipantWithSource(
+                        apiVersion,
+                        currentUser.getBaseUrl(),
+                        conversation.getToken(),
+                        "groups",
+                        groupId
+                                                                                      );
 
                 ncApi.addParticipant(credentials, retrofitBucket.getUrl(), retrofitBucket.getQueryMap())
                         .subscribeOn(Schedulers.io())
@@ -641,23 +678,25 @@ public class OperationsMenuController extends BaseController {
                         .retry(1)
                         .subscribe(new Observer<AddParticipantOverall>() {
                             @Override
-                            public void onSubscribe(Disposable d) {
+                            public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
 
                             }
 
                             @Override
-                            public void onNext(AddParticipantOverall addParticipantOverall) {
+                            public void onNext(
+                                    @io.reactivex.annotations.NonNull AddParticipantOverall addParticipantOverall
+                                              ) {
                             }
 
                             @Override
-                            public void onError(Throwable e) {
+                            public void onError(@io.reactivex.annotations.NonNull Throwable e) {
                                 dispose();
                             }
 
                             @Override
                             public void onComplete() {
-                                synchronized (localInvitedUsers) {
-                                    localInvitedUsers.remove(userId);
+                                synchronized (localInvitedGroups) {
+                                    localInvitedGroups.remove(groupId);
                                 }
 
                                 if (localInvitedGroups.size() == 0 && localInvitedUsers.size() == 0) {
@@ -666,9 +705,8 @@ public class OperationsMenuController extends BaseController {
                                 dispose();
                             }
                         });
+
             }
-        } else {
-            initiateConversation(true);
         }
     }
 

+ 49 - 0
app/src/main/java/com/nextcloud/talk/controllers/util/ControllerViewBindingDelegate.kt

@@ -0,0 +1,49 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author BlueLine Labs, Inc.
+ * Copyright (C) 2016 BlueLine Labs, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.nextcloud.talk.controllers.util
+
+import android.view.View
+import androidx.lifecycle.LifecycleObserver
+import androidx.viewbinding.ViewBinding
+import com.bluelinelabs.conductor.Controller
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+
+fun <T : ViewBinding> Controller.viewBinding(bindingFactory: (View) -> T) =
+    ControllerViewBindingDelegate(this, bindingFactory)
+
+class ControllerViewBindingDelegate<T : ViewBinding>(
+    controller: Controller,
+    private val viewBinder: (View) -> T
+) : ReadOnlyProperty<Controller, T>, LifecycleObserver {
+
+    private var binding: T? = null
+
+    init {
+        controller.addLifecycleListener(object : Controller.LifecycleListener() {
+            override fun postDestroyView(controller: Controller) {
+                binding = null
+            }
+        })
+    }
+
+    override fun getValue(thisRef: Controller, property: KProperty<*>): T {
+        return binding ?: viewBinder(thisRef.view!!).also { binding = it }
+    }
+}

+ 16 - 4
app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt

@@ -241,7 +241,10 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
         fun hasLinkedAccount(id: String): Boolean {
             var hasLinkedAccount = false
             val where =
-                ContactsContract.Data.MIMETYPE + " = ? AND " + ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID + " = ?"
+                ContactsContract.Data.MIMETYPE +
+                    " = ? AND " +
+                    ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID +
+                    " = ?"
             val params = arrayOf(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, id)
 
             val rawContactUri = ContactsContract.Data.CONTENT_URI
@@ -393,7 +396,10 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
     private fun getDisplayNameFromDeviceContact(id: String?): String? {
         var displayName: String? = null
         val whereName =
-            ContactsContract.Data.MIMETYPE + " = ? AND " + ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID + " = ?"
+            ContactsContract.Data.MIMETYPE +
+                " = ? AND " +
+                ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID +
+                " = ?"
         val whereNameParams = arrayOf(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, id)
         val nameCursor = context.contentResolver.query(
             ContactsContract.Data.CONTENT_URI,
@@ -405,7 +411,9 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
         if (nameCursor != null) {
             while (nameCursor.moveToNext()) {
                 displayName =
-                    nameCursor.getString(nameCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME))
+                    nameCursor.getString(
+                        nameCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME)
+                    )
             }
             nameCursor.close()
         }
@@ -424,7 +432,11 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
 
         if (phonesNumbersCursor != null) {
             while (phonesNumbersCursor.moveToNext()) {
-                numbers.add(phonesNumbersCursor.getString(phonesNumbersCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)))
+                numbers.add(
+                    phonesNumbersCursor.getString(
+                        phonesNumbersCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
+                    )
+                )
             }
             phonesNumbersCursor.close()
         }

+ 3 - 3
app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt

@@ -193,16 +193,16 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
 
         fun isStoragePermissionGranted(context: Context): Boolean {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                if (PermissionChecker.checkSelfPermission(
+                return if (PermissionChecker.checkSelfPermission(
                         context,
                         Manifest.permission.WRITE_EXTERNAL_STORAGE
                     ) == PermissionChecker.PERMISSION_GRANTED
                 ) {
                     Log.d(TAG, "Permission is granted")
-                    return true
+                    true
                 } else {
                     Log.d(TAG, "Permission is revoked")
-                    return false
+                    false
                 }
             } else { // permission is automatically granted on sdk<23 upon installation
                 Log.d(TAG, "Permission is granted")

+ 241 - 0
app/src/main/java/com/nextcloud/talk/models/database/CapabilitiesUtil.java

@@ -0,0 +1,241 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * @author Mario Danic
+ * Copyright (C) 2021 Andy Scherzinger (info@andy-scherzinger.de)
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.models.database;
+
+import android.util.Log;
+
+import com.bluelinelabs.logansquare.LoganSquare;
+import com.nextcloud.talk.models.json.capabilities.Capabilities;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import androidx.annotation.Nullable;
+
+public abstract class CapabilitiesUtil {
+    private static final String TAG = CapabilitiesUtil.class.getSimpleName();
+
+    public static boolean hasNotificationsCapability(@Nullable UserEntity user, String capabilityName) {
+        if (user != null && user.getCapabilities() != null) {
+            try {
+                Capabilities capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
+                if (capabilities.getNotificationsCapability() != null &&
+                        capabilities.getNotificationsCapability().getFeatures() != null) {
+                    return capabilities.getSpreedCapability().getFeatures().contains(capabilityName);
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to get capabilities for the user");
+            }
+        }
+        return false;
+    }
+
+    public static boolean hasExternalCapability(@Nullable UserEntity user, String capabilityName) {
+        if (user != null && user.getCapabilities() != null) {
+            try {
+                Capabilities capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
+                if (capabilities.getExternalCapability() != null &&
+                        capabilities.getExternalCapability().containsKey("v1")) {
+                    return capabilities.getExternalCapability().get("v1").contains("capabilityName");
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to get capabilities for the user");
+            }
+        }
+        return false;
+    }
+
+    public static boolean isServerEOL(@Nullable UserEntity user) {
+        // Capability is available since Talk 4 => Nextcloud 14 => Autmn 2018
+        return !hasSpreedFeatureCapability(user, "no-ping");
+    }
+
+    public static boolean isServerAlmostEOL(@Nullable UserEntity user) {
+        // Capability is available since Talk 8 => Nextcloud 18 => January 2020
+        return !hasSpreedFeatureCapability(user, "chat-replies");
+    }
+
+    public static boolean hasSpreedFeatureCapability(@Nullable UserEntity user, String capabilityName) {
+        if (user != null && user.getCapabilities() != null) {
+            try {
+                Capabilities capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
+                if (capabilities != null && capabilities.getSpreedCapability() != null &&
+                        capabilities.getSpreedCapability().getFeatures() != null) {
+                    return capabilities.getSpreedCapability().getFeatures().contains(capabilityName);
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to get capabilities for the user");
+            }
+        }
+        return false;
+    }
+
+    public static Integer getMessageMaxLength(@Nullable UserEntity user) {
+        if (user != null && user.getCapabilities() != null) {
+            try {
+                Capabilities capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
+                if (capabilities != null &&
+                        capabilities.getSpreedCapability() != null &&
+                        capabilities.getSpreedCapability().getConfig() != null &&
+                        capabilities.getSpreedCapability().getConfig().containsKey("chat")) {
+                    HashMap<String, String> chatConfigHashMap = capabilities
+                            .getSpreedCapability()
+                            .getConfig()
+                            .get("chat");
+                    if (chatConfigHashMap != null && chatConfigHashMap.containsKey("max-length")) {
+                        int chatSize = Integer.parseInt(chatConfigHashMap.get("max-length"));
+                        if (chatSize > 0) {
+                            return chatSize;
+                        } else {
+                            return 1000;
+                        }
+                    }
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to get capabilities for the user");
+            }
+        }
+        return 1000;
+    }
+
+    public static boolean isPhoneBookIntegrationAvailable(@Nullable UserEntity user) {
+        if (user != null && user.getCapabilities() != null) {
+            try {
+                Capabilities capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
+                return capabilities != null &&
+                        capabilities.getSpreedCapability() != null &&
+                        capabilities.getSpreedCapability().getFeatures() != null &&
+                        capabilities.getSpreedCapability().getFeatures().contains("phonebook-search");
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to get capabilities for the user");
+            }
+        }
+        return false;
+    }
+
+    public static boolean isReadStatusAvailable(@Nullable UserEntity user) {
+        if (user != null && user.getCapabilities() != null) {
+            try {
+                Capabilities capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
+                if (capabilities != null &&
+                        capabilities.getSpreedCapability() != null &&
+                        capabilities.getSpreedCapability().getConfig() != null &&
+                        capabilities.getSpreedCapability().getConfig().containsKey("chat")) {
+                    Map<String, String> map = capabilities.getSpreedCapability().getConfig().get("chat");
+                    return map != null && map.containsKey("read-privacy");
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to get capabilities for the user");
+            }
+        }
+        return false;
+    }
+
+    public static boolean isReadStatusPrivate(@Nullable UserEntity user) {
+        if (user != null && user.getCapabilities() != null) {
+            try {
+                Capabilities capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
+                if (capabilities != null &&
+                        capabilities.getSpreedCapability() != null &&
+                        capabilities.getSpreedCapability().getConfig() != null &&
+                        capabilities.getSpreedCapability().getConfig().containsKey("chat")) {
+                    HashMap<String, String> map = capabilities.getSpreedCapability().getConfig().get("chat");
+                    if (map != null && map.containsKey("read-privacy")) {
+                        return Integer.parseInt(map.get("read-privacy")) == 1;
+                    }
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to get capabilities for the user");
+            }
+        }
+        return false;
+    }
+
+    public static String getAttachmentFolder(@Nullable UserEntity user) {
+        if (user != null && user.getCapabilities() != null) {
+            try {
+                Capabilities capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
+                if (capabilities != null &&
+                        capabilities.getSpreedCapability() != null &&
+                        capabilities.getSpreedCapability().getConfig() != null &&
+                        capabilities.getSpreedCapability().getConfig().containsKey("attachments")) {
+                    HashMap<String, String> map = capabilities.getSpreedCapability().getConfig().get("attachments");
+                    if (map != null && map.containsKey("folder")) {
+                        return map.get("folder");
+                    }
+                }
+            } catch (IOException e) {
+                Log.e("User.java", "Failed to get attachment folder", e);
+            }
+        }
+        return "/Talk";
+    }
+
+    public static String getServerName(@Nullable UserEntity user) {
+        if (user != null && user.getCapabilities() != null) {
+            Capabilities capabilities;
+            try {
+                capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
+                if (capabilities != null && capabilities.getThemingCapability() != null) {
+                    return capabilities.getThemingCapability().getName();
+                }
+            } catch (IOException e) {
+                Log.e("User.java", "Failed to get server name", e);
+            }
+        }
+        return "";
+    }
+
+    // TODO later avatar can also be checked via user fields, for now it is in Talk capability
+    public static boolean isAvatarEndpointAvailable(@Nullable UserEntity user) {
+        if (user != null && user.getCapabilities() != null) {
+            Capabilities capabilities;
+            try {
+                capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
+                return (capabilities != null &&
+                        capabilities.getSpreedCapability() != null &&
+                        capabilities.getSpreedCapability().getFeatures() != null &&
+                        capabilities.getSpreedCapability().getFeatures().contains("temp-user-avatar-api"));
+            } catch (IOException e) {
+                Log.e("User.java", "Failed to get server name", e);
+            }
+        }
+        return false;
+    }
+
+    public static boolean canEditScopes(@Nullable UserEntity user) {
+        if (user != null && user.getCapabilities() != null) {
+            Capabilities capabilities;
+            try {
+                capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
+                return (capabilities != null &&
+                        capabilities.getProvisioningCapability() != null &&
+                        capabilities.getProvisioningCapability().getAccountPropertyScopesVersion() != null &&
+                        capabilities.getProvisioningCapability().getAccountPropertyScopesVersion() > 1);
+            } catch (IOException e) {
+                Log.e("User.java", "Failed to get server name", e);
+            }
+        }
+        return false;
+    }
+}

+ 3 - 209
app/src/main/java/com/nextcloud/talk/models/database/User.java

@@ -2,6 +2,8 @@
  * Nextcloud Talk application
  *
  * @author Mario Danic
+ * @author Andy Scherzinger
+ * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
  * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -20,14 +22,8 @@
 package com.nextcloud.talk.models.database;
 
 import android.os.Parcelable;
-import android.util.Log;
 
-import com.bluelinelabs.logansquare.LoganSquare;
-import com.nextcloud.talk.models.json.capabilities.Capabilities;
-
-import java.io.IOException;
 import java.io.Serializable;
-import java.util.HashMap;
 
 import io.requery.Entity;
 import io.requery.Generated;
@@ -36,7 +32,7 @@ import io.requery.Persistable;
 
 @Entity
 public interface User extends Parcelable, Persistable, Serializable {
-    static final String TAG = "UserEntity";
+    String TAG = "UserEntity";
 
     @Key
     @Generated
@@ -63,206 +59,4 @@ public interface User extends Parcelable, Persistable, Serializable {
     boolean getCurrent();
 
     boolean getScheduledForDeletion();
-
-    default boolean hasNotificationsCapability(String capabilityName) {
-        if (getCapabilities() != null) {
-            try {
-                Capabilities capabilities = LoganSquare.parse(getCapabilities(), Capabilities.class);
-                if (capabilities.getNotificationsCapability() != null && capabilities.getNotificationsCapability().getFeatures() != null) {
-                    return capabilities.getSpreedCapability().getFeatures().contains(capabilityName);
-                }
-            } catch (IOException e) {
-                Log.e(TAG, "Failed to get capabilities for the user");
-            }
-        }
-        return false;
-    }
-
-    default boolean hasExternalCapability(String capabilityName) {
-        if (getCapabilities() != null) {
-            try {
-                Capabilities capabilities = LoganSquare.parse(getCapabilities(), Capabilities.class);
-                if (capabilities.getExternalCapability() != null && capabilities.getExternalCapability().containsKey("v1")) {
-                    return capabilities.getExternalCapability().get("v1").contains("capabilityName");
-                }
-            } catch (IOException e) {
-                Log.e(TAG, "Failed to get capabilities for the user");
-            }
-        }
-        return false;
-    }
-
-    default boolean isServerEOL() {
-        // Capability is available since Talk 4 => Nextcloud 14 => Autmn 2018
-        return !hasSpreedFeatureCapability("no-ping");
-    }
-
-    default boolean isServerAlmostEOL() {
-        // Capability is available since Talk 8 => Nextcloud 18 => January 2020
-        return !hasSpreedFeatureCapability("chat-replies");
-    }
-
-    default boolean hasSpreedFeatureCapability(String capabilityName) {
-        if (getCapabilities() != null) {
-            try {
-                Capabilities capabilities = LoganSquare.parse(getCapabilities(), Capabilities.class);
-                if (capabilities != null && capabilities.getSpreedCapability() != null &&
-                        capabilities.getSpreedCapability().getFeatures() != null) {
-                    return capabilities.getSpreedCapability().getFeatures().contains(capabilityName);
-                }
-            } catch (IOException e) {
-                Log.e(TAG, "Failed to get capabilities for the user");
-            }
-        }
-        return false;
-    }
-
-    default int getMessageMaxLength() {
-        if (getCapabilities() != null) {
-            Capabilities capabilities = null;
-            try {
-                capabilities = LoganSquare.parse(getCapabilities(), Capabilities.class);
-                if (capabilities != null && capabilities.getSpreedCapability() != null && capabilities.getSpreedCapability().getConfig() != null
-                        && capabilities.getSpreedCapability().getConfig().containsKey("chat")) {
-                    HashMap<String, String> chatConfigHashMap = capabilities.getSpreedCapability().getConfig().get("chat");
-                    if (chatConfigHashMap != null && chatConfigHashMap.containsKey("max-length")) {
-                        int chatSize = Integer.parseInt(chatConfigHashMap.get("max-length"));
-                        if (chatSize > 0) {
-                            return chatSize;
-                        } else {
-                            return 1000;
-                        }
-                    }
-                }
-            } catch (IOException e) {
-                e.printStackTrace();
-            }
-        }
-        return 1000;
-    }
-
-    default boolean isPhoneBookIntegrationAvailable() {
-        if (getCapabilities() != null) {
-            Capabilities capabilities;
-            try {
-                capabilities = LoganSquare.parse(getCapabilities(), Capabilities.class);
-                return capabilities != null &&
-                        capabilities.getSpreedCapability() != null &&
-                        capabilities.getSpreedCapability().getFeatures() != null &&
-                        capabilities.getSpreedCapability().getFeatures().contains("phonebook-search");
-            } catch (IOException e) {
-                e.printStackTrace();
-            }
-        }
-        return false;
-    }
-
-    default boolean isReadStatusAvailable() {
-        if (getCapabilities() != null) {
-            Capabilities capabilities;
-            try {
-                capabilities = LoganSquare.parse(getCapabilities(), Capabilities.class);
-                if (capabilities != null &&
-                        capabilities.getSpreedCapability() != null &&
-                        capabilities.getSpreedCapability().getConfig() != null &&
-                        capabilities.getSpreedCapability().getConfig().containsKey("chat")) {
-                    HashMap<String, String> map = capabilities.getSpreedCapability().getConfig().get("chat");
-                    return map != null && map.containsKey("read-privacy");
-                }
-            } catch (IOException e) {
-                e.printStackTrace();
-            }
-        }
-        return false;
-    }
-
-    default boolean isReadStatusPrivate() {
-        if (getCapabilities() != null) {
-            Capabilities capabilities;
-            try {
-                capabilities = LoganSquare.parse(getCapabilities(), Capabilities.class);
-                if (capabilities != null &&
-                        capabilities.getSpreedCapability() != null &&
-                        capabilities.getSpreedCapability().getConfig() != null &&
-                        capabilities.getSpreedCapability().getConfig().containsKey("chat")) {
-                    HashMap<String, String> map = capabilities.getSpreedCapability().getConfig().get("chat");
-                    if (map != null && map.containsKey("read-privacy")) {
-                        return Integer.parseInt(map.get("read-privacy")) == 1;
-                    }
-                }
-            } catch (IOException e) {
-                e.printStackTrace();
-            }
-        }
-        return false;
-    }
-
-    default String getAttachmentFolder() {
-        if (getCapabilities() != null) {
-            Capabilities capabilities;
-            try {
-                capabilities = LoganSquare.parse(getCapabilities(), Capabilities.class);
-                if (capabilities != null &&
-                        capabilities.getSpreedCapability() != null &&
-                        capabilities.getSpreedCapability().getConfig() != null &&
-                        capabilities.getSpreedCapability().getConfig().containsKey("attachments")) {
-                    HashMap<String, String> map = capabilities.getSpreedCapability().getConfig().get("attachments");
-                    if (map != null && map.containsKey("folder")) {
-                        return map.get("folder");
-                    }
-                }
-            } catch (IOException e) {
-                Log.e("User.java", "Failed to get attachment folder", e);
-            }
-        }
-        return "/Talk";
-    }
-
-    default String getServerName() {
-        if (getCapabilities() != null) {
-            Capabilities capabilities;
-            try {
-                capabilities = LoganSquare.parse(getCapabilities(), Capabilities.class);
-                if (capabilities != null && capabilities.getThemingCapability() != null) {
-                    return capabilities.getThemingCapability().getName();
-                }
-            } catch (IOException e) {
-                Log.e("User.java", "Failed to get server name", e);
-            }
-        }
-        return "";
-    }
-
-    // TODO later avatar can also be checked via user fields, for now it is in Talk capability
-    default boolean isAvatarEndpointAvailable() {
-        if (getCapabilities() != null) {
-            Capabilities capabilities;
-            try {
-                capabilities = LoganSquare.parse(getCapabilities(), Capabilities.class);
-                return (capabilities != null &&
-                        capabilities.getSpreedCapability() != null &&
-                        capabilities.getSpreedCapability().getFeatures() != null &&
-                        capabilities.getSpreedCapability().getFeatures().contains("temp-user-avatar-api"));
-            } catch (IOException e) {
-                e.printStackTrace();
-            }
-        }
-        return false;
-    }
-
-    default boolean canEditScopes() {
-        if (getCapabilities() != null) {
-            Capabilities capabilities;
-            try {
-                capabilities = LoganSquare.parse(getCapabilities(), Capabilities.class);
-                return (capabilities != null &&
-                        capabilities.getProvisioningCapability() != null &&
-                        capabilities.getProvisioningCapability().getAccountPropertyScopesVersion() != null &&
-                        capabilities.getProvisioningCapability().getAccountPropertyScopesVersion() > 1);
-            } catch (IOException e) {
-                e.printStackTrace();
-            }
-        }
-        return false;
-    }
 }

+ 3 - 1
app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.java

@@ -22,6 +22,7 @@ package com.nextcloud.talk.models.json.conversations;
 
 import com.bluelinelabs.logansquare.annotation.JsonField;
 import com.bluelinelabs.logansquare.annotation.JsonObject;
+import com.nextcloud.talk.models.database.CapabilitiesUtil;
 import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.models.json.chat.ChatMessage;
 import com.nextcloud.talk.models.json.converters.EnumLobbyStateConverter;
@@ -108,7 +109,8 @@ public class Conversation {
     }
 
     private boolean isLockedOneToOne(UserEntity conversationUser) {
-        return (getType() == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL && conversationUser.hasSpreedFeatureCapability("locked-one-to-one-rooms"));
+        return (getType() == ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL &&
+                CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "locked-one-to-one-rooms"));
     }
 
     public boolean canModerate(UserEntity conversationUser) {

+ 1 - 2
app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationRichObject.java

@@ -100,8 +100,7 @@ public class NotificationRichObject {
         final Object $type = this.getType();
         result = result * PRIME + ($type == null ? 43 : $type.hashCode());
         final Object $name = this.getName();
-        result = result * PRIME + ($name == null ? 43 : $name.hashCode());
-        return result;
+        return result * PRIME + ($name == null ? 43 : $name.hashCode());
     }
 
     public String toString() {

+ 4 - 2
app/src/main/java/com/nextcloud/talk/receivers/PackageReplacedReceiver.kt

@@ -71,8 +71,10 @@ class PackageReplacedReceiver : BroadcastReceiver() {
                     }
 
                     if (!appPreferences.isNotificationChannelUpgradedToV3 && packageInfo.versionCode > 51) {
-                        notificationManager.deleteNotificationChannel(NotificationUtils.NOTIFICATION_CHANNEL_MESSAGES_V2)
-                        notificationManager.deleteNotificationChannel(NotificationUtils.NOTIFICATION_CHANNEL_CALLS_V2)
+                        notificationManager
+                            .deleteNotificationChannel(NotificationUtils.NOTIFICATION_CHANNEL_MESSAGES_V2)
+                        notificationManager
+                            .deleteNotificationChannel(NotificationUtils.NOTIFICATION_CHANNEL_CALLS_V2)
                         appPreferences.setNotificationChannelIsUpgradedToV3(true)
                     }
 

+ 2 - 1
app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt

@@ -31,6 +31,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
 import com.nextcloud.talk.R
 import com.nextcloud.talk.components.filebrowser.controllers.BrowserController
 import com.nextcloud.talk.controllers.ChatController
+import com.nextcloud.talk.models.database.CapabilitiesUtil
 
 class AttachmentDialog(val activity: Activity, var chatController: ChatController) : BottomSheetDialog(activity) {
 
@@ -51,7 +52,7 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle
         window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
         unbinder = ButterKnife.bind(this, view)
 
-        var serverName = chatController.conversationUser?.serverName
+        var serverName = CapabilitiesUtil.getServerName(chatController.conversationUser)
         attachFromCloud?.text = chatController.resources?.let {
             if (serverName.isNullOrEmpty()) {
                 serverName = it.getString(R.string.nc_server_product_name)

+ 8 - 2
app/src/main/java/com/nextcloud/talk/utils/AccountUtils.kt

@@ -55,8 +55,14 @@ object AccountUtils {
                 internalUserEntity = userEntitiesList[i]
                 importAccount = getInformationFromAccount(account)
                 if (importAccount.token != null) {
-                    if (importAccount.baseUrl.startsWith("http://") || importAccount.baseUrl.startsWith("https://")) {
-                        if (internalUserEntity.username == importAccount.username && internalUserEntity.baseUrl == importAccount.baseUrl) {
+                    if (
+                        importAccount.baseUrl.startsWith("http://") ||
+                        importAccount.baseUrl.startsWith("https://")
+                    ) {
+                        if (
+                            internalUserEntity.username == importAccount.username &&
+                            internalUserEntity.baseUrl == importAccount.baseUrl
+                        ) {
                             accountFound = true
                             break
                         }

+ 13 - 11
app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java

@@ -27,6 +27,7 @@ import com.nextcloud.talk.BuildConfig;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.models.RetrofitBucket;
+import com.nextcloud.talk.models.database.CapabilitiesUtil;
 import com.nextcloud.talk.models.database.UserEntity;
 
 import java.util.HashMap;
@@ -115,7 +116,7 @@ public class ApiUtils {
         return getConversationApiVersion(capabilities, versions);
     }
 
-    public static int getConversationApiVersion(UserEntity capabilities, int[] versions) throws NoSupportedApiException {
+    public static int getConversationApiVersion(UserEntity user, int[] versions) throws NoSupportedApiException {
         boolean hasApiV4 = false;
         for (int version : versions) {
             hasApiV4 |= version == 4;
@@ -127,16 +128,17 @@ public class ApiUtils {
         }
 
         for (int version : versions) {
-            if (capabilities.hasSpreedFeatureCapability("conversation-v" + version)) {
+            if (CapabilitiesUtil.hasSpreedFeatureCapability(user, "conversation-v" + version)) {
                 return version;
             }
 
             // Fallback for old API versions
             if ((version == 1 || version == 2)) {
-                if (capabilities.hasSpreedFeatureCapability("conversation-v2")) {
+                if (CapabilitiesUtil.hasSpreedFeatureCapability(user, "conversation-v2")) {
                     return version;
                 }
-                if (version == 1  && capabilities.hasSpreedFeatureCapability("conversation")) {
+                if (version == 1  &&
+                        CapabilitiesUtil.hasSpreedFeatureCapability(user, "conversation")) {
                     return version;
                 }
             }
@@ -144,20 +146,20 @@ public class ApiUtils {
         throw new NoSupportedApiException();
     }
 
-    public static int getSignalingApiVersion(UserEntity capabilities, int[] versions) throws NoSupportedApiException {
+    public static int getSignalingApiVersion(UserEntity user, int[] versions) throws NoSupportedApiException {
         for (int version : versions) {
-            if (capabilities.hasSpreedFeatureCapability("signaling-v" + version)) {
+            if (CapabilitiesUtil.hasSpreedFeatureCapability(user, "signaling-v" + version)) {
                 return version;
             }
 
             if (version == 2 &&
-                    capabilities.hasSpreedFeatureCapability("sip-support") &&
-                    !capabilities.hasSpreedFeatureCapability("signaling-v3")) {
+                    CapabilitiesUtil.hasSpreedFeatureCapability(user, "sip-support") &&
+                    !CapabilitiesUtil.hasSpreedFeatureCapability(user, "signaling-v3")) {
                 return version;
             }
 
             if (version == 1 &&
-                    !capabilities.hasSpreedFeatureCapability("signaling-v3")) {
+                    !CapabilitiesUtil.hasSpreedFeatureCapability(user, "signaling-v3")) {
                 // Has no capability, we just assume it is always there when there is no v3 or later
                 return version;
             }
@@ -165,9 +167,9 @@ public class ApiUtils {
         throw new NoSupportedApiException();
     }
 
-    public static int getChatApiVersion(UserEntity capabilities, int[] versions) throws NoSupportedApiException {
+    public static int getChatApiVersion(UserEntity user, int[] versions) throws NoSupportedApiException {
         for (int version : versions) {
-            if (version == 1 && capabilities.hasSpreedFeatureCapability("chat-v2")) {
+            if (version == 1 && CapabilitiesUtil.hasSpreedFeatureCapability(user, "chat-v2")) {
                 // Do not question that chat-v2 capability shows the availability of api/v1/ endpoint *see no evil*
                 return version;
             }

+ 14 - 11
app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt

@@ -85,7 +85,10 @@ object NotificationUtils {
 
         val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
 
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && notificationManager.getNotificationChannel(channelId) == null) {
+        if (
+            Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
+            notificationManager.getNotificationChannel(channelId) == null
+        ) {
 
             val channel = NotificationChannel(
                 channelId, channelName,
@@ -156,9 +159,9 @@ object NotificationUtils {
                 notification = statusBarNotification.notification
 
                 if (notification != null && !notification.extras.isEmpty) {
-                    if (conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) && notificationId == notification.extras.getLong(
-                            BundleKeys.KEY_NOTIFICATION_ID
-                        )
+                    if (
+                        conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) &&
+                        notificationId == notification.extras.getLong(BundleKeys.KEY_NOTIFICATION_ID)
                     ) {
                         notificationManager.cancel(statusBarNotification.id)
                     }
@@ -184,9 +187,9 @@ object NotificationUtils {
                 notification = statusBarNotification.notification
 
                 if (notification != null && !notification.extras.isEmpty) {
-                    if (conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) && roomTokenOrId == statusBarNotification.notification.extras.getString(
-                            BundleKeys.KEY_ROOM_TOKEN
-                        )
+                    if (
+                        conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) &&
+                        roomTokenOrId == statusBarNotification.notification.extras.getString(BundleKeys.KEY_ROOM_TOKEN)
                     ) {
                         return statusBarNotification
                     }
@@ -202,7 +205,9 @@ object NotificationUtils {
         conversationUser: UserEntity,
         roomTokenOrId: String
     ) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && conversationUser.id != -1L &&
+        if (
+            Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
+            conversationUser.id != -1L &&
             context != null
         ) {
 
@@ -215,9 +220,7 @@ object NotificationUtils {
 
                 if (notification != null && !notification.extras.isEmpty) {
                     if (conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) &&
-                        roomTokenOrId == statusBarNotification.notification.extras.getString(
-                                BundleKeys.KEY_ROOM_TOKEN
-                            )
+                        roomTokenOrId == statusBarNotification.notification.extras.getString(BundleKeys.KEY_ROOM_TOKEN)
                     ) {
                         notificationManager.cancel(statusBarNotification.id)
                     }

+ 2 - 1
app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.java

@@ -26,6 +26,7 @@ import autodagger.AutoInjector;
 import com.nextcloud.talk.api.NcApi;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.models.database.ArbitraryStorageEntity;
+import com.nextcloud.talk.models.database.CapabilitiesUtil;
 import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.models.json.generic.GenericOverall;
 import com.nextcloud.talk.utils.ApiUtils;
@@ -76,7 +77,7 @@ public class DatabaseStorageModule implements StorageModule {
         if (!key.equals("message_notification_level")) {
             arbitraryStorageUtils.storeStorageSetting(accountIdentifier, key, value, conversationToken);
         } else {
-            if (conversationUser.hasSpreedFeatureCapability("notification-levels")) {
+            if (CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "notification-levels")) {
                 if (!TextUtils.isEmpty(messageNotificationLevel) && !messageNotificationLevel.equals(value)) {
                     int intValue;
                     switch (value) {

+ 3 - 1
app/src/main/res/layout/controller_chat.xml

@@ -26,7 +26,9 @@
     android:animateLayoutChanges="true"
     android:background="@color/bg_default">
 
-    <include layout="@layout/lobby_view"
+    <include
+        android:id="@+id/lobby"
+        layout="@layout/lobby_view"
         android:visibility="gone"
         tools:visibility="visible"/>
 

+ 21 - 12
app/src/main/res/layout/controller_conversation_info.xml

@@ -128,7 +128,7 @@
                 android:id="@+id/participants_list_category"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:layout_below="@+id/webinar_settings"
+                android:layout_below="@+id/settings"
                 android:visibility="gone"
                 apc:cardBackgroundColor="@color/bg_default"
                 apc:cardElevation="0dp"
@@ -180,21 +180,30 @@
 
             </com.yarolegovich.mp.MaterialPreferenceCategory>
 
-            <include
-                layout="@layout/notification_settings_item"
+            <LinearLayout
+                android:id="@+id/settings"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_below="@id/otherRoomOptions"
-                android:visibility="gone"
-                tools:visibility="gone" />
+                android:orientation="vertical">
 
-            <include
-                layout="@layout/webinar_info_item"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_below="@id/notification_settings"
-                android:visibility="gone"
-                tools:visibility="visible" />
+                <include
+                    android:id="@+id/notification_settings_view"
+                    layout="@layout/notification_settings_item"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:visibility="gone"
+                    tools:visibility="gone" />
+
+                <include
+                    android:id="@+id/webinar_info_view"
+                    layout="@layout/webinar_info_item"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:visibility="gone"
+                    tools:visibility="visible" />
+
+            </LinearLayout>
         </RelativeLayout>
     </ScrollView>
 </RelativeLayout>

+ 1 - 1
detekt.yml

@@ -1,5 +1,5 @@
 build:
-  maxIssues: 346
+  maxIssues: 201
   weights:
     # complexity: 2
     # LongParameterList: 1

+ 1 - 1
scripts/analysis/findbugs-results.txt

@@ -1 +1 @@
-457
+450

+ 1 - 1
scripts/analysis/lint-results.txt

@@ -1,2 +1,2 @@
 DO NOT TOUCH; GENERATED BY DRONE
-      <span class="mdl-layout-title">Lint Report: 3 errors and 329 warnings</span>
+      <span class="mdl-layout-title">Lint Report: 3 errors and 290 warnings</span>

Деякі файли не було показано, через те що забагато файлів було змінено