diff --git a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt index ab78ce354..20d3298cb 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt +++ b/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 ) } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt index 1de435c3a..3b2a8cf33 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt +++ b/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, diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java index a77c2e6c9..d0ee7abe9 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java +++ b/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"); diff --git a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt index 3ecad94cf..cb9001cf6 100644 --- a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt +++ b/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 { diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserController.java b/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserController.java index 16e581041..cec4f5e2a 100644 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserController.java +++ b/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 diff --git a/app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java b/app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java index e6e64f84e..3f3530af2 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java +++ b/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), diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt index 29ac0f3a7..71f099c26 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt @@ -3,8 +3,10 @@ * * @author Mario Danic * @author Marcel Hibbe - * Copyright (C) 2017-2019 Mario Danic + * @author Andy Scherzinger + * Copyright (C) 2021 Andy Scherzinger * Copyright (C) 2021 Marcel Hibbe + * Copyright (C) 2017-2019 Mario Danic * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -34,7 +36,6 @@ import android.graphics.drawable.ColorDrawable import android.net.Uri import android.os.Bundle import android.os.Handler -import android.os.Parcelable import android.text.Editable import android.text.InputFilter import android.text.TextUtils @@ -42,25 +43,20 @@ import android.text.TextWatcher import android.util.Log import android.util.TypedValue import android.view.Gravity -import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View -import android.view.ViewGroup import android.widget.AbsListView import android.widget.ImageButton import android.widget.ImageView import android.widget.PopupMenu -import android.widget.ProgressBar import android.widget.RelativeLayout import android.widget.Space -import android.widget.TextView import android.widget.Toast import androidx.appcompat.view.ContextThemeWrapper import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.emoji.text.EmojiCompat -import androidx.emoji.widget.EmojiEditText import androidx.emoji.widget.EmojiTextView import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -68,8 +64,6 @@ import androidx.work.Data import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager import autodagger.AutoInjector -import butterknife.BindView -import butterknife.OnClick import coil.load import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler @@ -78,7 +72,6 @@ import com.facebook.common.executors.UiThreadImmediateExecutorService import com.facebook.common.references.CloseableReference import com.facebook.datasource.DataSource import com.facebook.drawee.backends.pipeline.Fresco -import com.facebook.drawee.view.SimpleDraweeView import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber import com.facebook.imagepipeline.image.CloseableImage import com.google.android.flexbox.FlexboxLayout @@ -95,10 +88,13 @@ import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.callbacks.MentionAutocompleteCallback import com.nextcloud.talk.components.filebrowser.controllers.BrowserController import com.nextcloud.talk.components.filebrowser.controllers.BrowserForSharingController -import com.nextcloud.talk.controllers.base.BaseController +import com.nextcloud.talk.controllers.base.NewBaseController +import com.nextcloud.talk.controllers.util.viewBinding +import com.nextcloud.talk.databinding.ControllerChatBinding import com.nextcloud.talk.events.UserMentionClickEvent import com.nextcloud.talk.events.WebSocketCommunicationEvent 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.chat.ChatMessage import com.nextcloud.talk.models.json.chat.ChatOverall @@ -126,7 +122,6 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY import com.nextcloud.talk.utils.database.user.UserUtils -import com.nextcloud.talk.utils.preferences.AppPreferences import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder import com.nextcloud.talk.utils.text.Spans import com.nextcloud.talk.webrtc.MagicWebSocketInstance @@ -135,12 +130,9 @@ import com.otaliastudios.autocomplete.Autocomplete import com.stfalcon.chatkit.commons.ImageLoader import com.stfalcon.chatkit.commons.models.IMessage import com.stfalcon.chatkit.messages.MessageHolders -import com.stfalcon.chatkit.messages.MessageInput -import com.stfalcon.chatkit.messages.MessagesList import com.stfalcon.chatkit.messages.MessagesListAdapter import com.stfalcon.chatkit.utils.DateFormatter import com.vanniktech.emoji.EmojiPopup -import com.webianks.library.PopupBubble import com.yarolegovich.lovelydialog.LovelyStandardDialog import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers @@ -161,11 +153,15 @@ import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) class ChatController(args: Bundle) : - BaseController(args), + NewBaseController( + R.layout.controller_chat, + args + ), MessagesListAdapter.OnLoadMoreListener, MessagesListAdapter.Formatter, MessagesListAdapter.OnMessageViewLongClickListener, MessageHolders.ContentChecker { + private val binding: ControllerChatBinding by viewBinding(ControllerChatBinding::bind) @Inject @JvmField @@ -175,58 +171,12 @@ class ChatController(args: Bundle) : @JvmField var userUtils: UserUtils? = null - @Inject - @JvmField - var appPreferences: AppPreferences? = null - - @Inject - @JvmField - var context: Context? = null - @Inject @JvmField var eventBus: EventBus? = null - @BindView(R.id.messagesListView) - @JvmField - var messagesListView: MessagesList? = null - - @BindView(R.id.messageInputView) - @JvmField - var messageInputView: MessageInput? = null - - @BindView(R.id.messageInput) - @JvmField - var messageInput: EmojiEditText? = null - - @BindView(R.id.popupBubbleView) - @JvmField - var popupBubble: PopupBubble? = null - - @BindView(R.id.progressBar) - @JvmField - var loadingProgressBar: ProgressBar? = null - - @BindView(R.id.smileyButton) - @JvmField - var smileyButton: ImageButton? = null - - @BindView(R.id.lobby_view) - @JvmField - var lobbyView: RelativeLayout? = null - - @BindView(R.id.lobby_text_view) - @JvmField - var conversationLobbyText: TextView? = null val disposableList = ArrayList() - @JvmField - @BindView(R.id.quotedChatMessageView) - var quotedChatMessageView: RelativeLayout? = null - - @BindView(R.id.callControlToggleChat) - @JvmField - var toggleChat: SimpleDraweeView? = null var roomToken: String? = null val conversationUser: UserEntity? val roomPassword: String @@ -271,14 +221,13 @@ class ChatController(args: Bundle) : setHasOptionsMenu(true) NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) - this.conversationUser = args.getParcelable(BundleKeys.KEY_USER_ENTITY) - this.roomId = args.getString(BundleKeys.KEY_ROOM_ID, "") - this.roomToken = args.getString(BundleKeys.KEY_ROOM_TOKEN, "") + this.conversationUser = args.getParcelable(KEY_USER_ENTITY) + this.roomId = args.getString(KEY_ROOM_ID, "") + this.roomToken = args.getString(KEY_ROOM_TOKEN, "") this.sharedText = args.getString(BundleKeys.KEY_SHARED_TEXT, "") - if (args.containsKey(BundleKeys.KEY_ACTIVE_CONVERSATION)) { - this.currentConversation = - Parcels.unwrap(args.getParcelable(BundleKeys.KEY_ACTIVE_CONVERSATION)) + if (args.containsKey(KEY_ACTIVE_CONVERSATION)) { + this.currentConversation = Parcels.unwrap(args.getParcelable(KEY_ACTIVE_CONVERSATION)) } this.roomPassword = args.getString(BundleKeys.KEY_CONVERSATION_PASSWORD, "") @@ -286,7 +235,7 @@ class ChatController(args: Bundle) : if (conversationUser?.userId == "?") { credentials = null } else { - credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token) + credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser.token) } if (args.containsKey(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) { @@ -297,7 +246,7 @@ class ChatController(args: Bundle) : } private fun getRoomInfo() { - val shouldRepeat = conversationUser?.hasSpreedFeatureCapability("webinary-lobby") ?: false + val shouldRepeat = CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "webinary-lobby") if (shouldRepeat) { checkingLobbyStatus = true } @@ -313,17 +262,24 @@ class ChatController(args: Bundle) : disposableList.add(d) } + @Suppress("Detekt.TooGenericExceptionCaught") override fun onNext(roomOverall: RoomOverall) { currentConversation = roomOverall.ocs.data loadAvatarForStatusBar() setTitle() - setupMentionAutocomplete() - checkReadOnlyState() - checkLobbyState() + try { + setupMentionAutocomplete() + checkReadOnlyState() + checkLobbyState() - if (!inConversation) { - joinRoomWithPassword() + if (!inConversation) { + joinRoomWithPassword() + } + } catch (npe: NullPointerException) { + // view binding can be null + // since this is called asynchrously and UI might have been destroyed in the meantime + Log.i(TAG, "UI destroyed - view binding already gone") } } @@ -336,7 +292,7 @@ class ChatController(args: Bundle) : lobbyTimerHandler = Handler() } - lobbyTimerHandler?.postDelayed({ getRoomInfo() }, 5000) + lobbyTimerHandler?.postDelayed({ getRoomInfo() }, LOBBY_TIMER_DELAY) } } }) @@ -377,10 +333,6 @@ class ChatController(args: Bundle) : }) } - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.controller_chat, container, false) - } - private fun loadAvatarForStatusBar() { if (inOneToOneCall() && activity != null && conversationVoiceCallMenuItem != null) { val avatarSize = DisplayUtils.convertDpToPixel( @@ -427,7 +379,7 @@ class ChatController(args: Bundle) : var adapterWasNull = false if (adapter == null) { - loadingProgressBar?.visibility = View.VISIBLE + binding.progressBar.visibility = View.VISIBLE adapterWasNull = true @@ -488,19 +440,19 @@ class ChatController(args: Bundle) : } ) } else { - messagesListView?.visibility = View.VISIBLE + binding.messagesListView.visibility = View.VISIBLE } - messagesListView?.setAdapter(adapter) + binding.messagesListView.setAdapter(adapter) adapter?.setLoadMoreListener(this) adapter?.setDateHeadersFormatter { format(it) } adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message) } - layoutManager = messagesListView?.layoutManager as LinearLayoutManager? + layoutManager = binding.messagesListView.layoutManager as LinearLayoutManager? - popupBubble?.setRecyclerView(messagesListView) + binding.popupBubbleView.setRecyclerView(binding.messagesListView) - popupBubble?.setPopupBubbleListener { context -> + binding.popupBubbleView.setPopupBubbleListener { context -> if (newMessagesCount != 0) { val scrollPosition: Int if (newMessagesCount - 1 < 0) { @@ -508,20 +460,25 @@ class ChatController(args: Bundle) : } else { scrollPosition = newMessagesCount - 1 } - Handler().postDelayed({ messagesListView?.smoothScrollToPosition(scrollPosition) }, 200) + Handler().postDelayed( + { + binding.messagesListView.smoothScrollToPosition(scrollPosition) + }, + NEW_MESSAGES_POPUP_BUBBLE_DELAY + ) } } if (args.containsKey("showToggleChat") && args.getBoolean("showToggleChat")) { - toggleChat?.visibility = View.VISIBLE + binding.callControlToggleChat.visibility = View.VISIBLE wasDetached = true } - toggleChat?.setOnClickListener { + binding.callControlToggleChat.setOnClickListener { (activity as MagicCallActivity).showCall() } - messagesListView?.addOnScrollListener(object : RecyclerView.OnScrollListener() { + binding.messagesListView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) @@ -530,8 +487,8 @@ class ChatController(args: Bundle) : if (layoutManager!!.findFirstCompletelyVisibleItemPosition() < newMessagesCount) { newMessagesCount = 0 - if (popupBubble != null && popupBubble!!.isShown) { - popupBubble?.hide() + if (binding.popupBubbleView.isShown == true) { + binding.popupBubbleView.hide() } } } @@ -540,29 +497,29 @@ class ChatController(args: Bundle) : }) val filters = arrayOfNulls(1) - val lengthFilter = conversationUser?.messageMaxLength ?: 1000 + val lengthFilter = CapabilitiesUtil.getMessageMaxLength(conversationUser) ?: MESSAGE_MAX_LENGTH filters[0] = InputFilter.LengthFilter(lengthFilter) - messageInput?.filters = filters + binding.messageInputView.inputEditText?.filters = filters - messageInput?.addTextChangedListener(object : TextWatcher { + binding.messageInputView.inputEditText?.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { } override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { if (s.length >= lengthFilter) { - messageInput?.error = String.format( + binding.messageInputView.inputEditText?.error = String.format( Objects.requireNonNull(resources).getString(R.string.nc_limit_hit), Integer.toString(lengthFilter) ) } else { - messageInput?.error = null + binding.messageInputView.inputEditText?.error = null } - val editable = messageInput?.editableText - if (editable != null && messageInput != null) { + val editable = binding.messageInputView.inputEditText?.editableText + if (editable != null && binding.messageInputView.inputEditText != null) { val mentionSpans = editable.getSpans( - 0, messageInput!!.length(), + 0, binding.messageInputView.inputEditText!!.length(), Spans.MentionChipSpan::class.java ) var mentionSpan: Spans.MentionChipSpan @@ -585,14 +542,14 @@ class ChatController(args: Bundle) : } }) - messageInput?.setText(sharedText) - messageInputView?.setAttachmentsListener { + binding.messageInputView.inputEditText?.setText(sharedText) + binding.messageInputView.setAttachmentsListener { activity?.let { AttachmentDialog(it, this).show() } } - messageInputView?.button?.setOnClickListener { v -> submitMessage() } + binding.messageInputView.button.setOnClickListener { v -> submitMessage() } - messageInputView?.button?.contentDescription = resources?.getString( + binding.messageInputView.button.contentDescription = resources?.getString( R.string .nc_description_send_message_button ) @@ -614,7 +571,7 @@ class ChatController(args: Bundle) : } private fun checkReadOnlyState() { - if (currentConversation != null) { + if (currentConversation != null && isAlive()) { if (currentConversation?.shouldShowLobby(conversationUser) ?: false || currentConversation?.conversationReadOnlyState != null && currentConversation?.conversationReadOnlyState == @@ -623,7 +580,7 @@ class ChatController(args: Bundle) : conversationVoiceCallMenuItem?.icon?.alpha = 99 conversationVideoMenuItem?.icon?.alpha = 99 - messageInputView?.visibility = View.GONE + binding.messageInputView.visibility = View.GONE } else { if (conversationVoiceCallMenuItem != null) { conversationVoiceCallMenuItem?.icon?.alpha = 255 @@ -635,31 +592,34 @@ class ChatController(args: Bundle) : if (currentConversation != null && currentConversation!!.shouldShowLobby(conversationUser) ) { - messageInputView?.visibility = View.GONE + binding.messageInputView.visibility = View.GONE } else { - messageInputView?.visibility = View.VISIBLE + binding.messageInputView.visibility = View.VISIBLE } } } } private fun checkLobbyState() { - if (currentConversation != null && currentConversation?.isLobbyViewApplicable(conversationUser) ?: false) { + if (currentConversation != null && + currentConversation?.isLobbyViewApplicable(conversationUser) ?: false && + isAlive() + ) { if (!checkingLobbyStatus) { getRoomInfo() } if (currentConversation?.shouldShowLobby(conversationUser) ?: false) { - lobbyView?.visibility = View.VISIBLE - messagesListView?.visibility = View.GONE - messageInputView?.visibility = View.GONE - loadingProgressBar?.visibility = View.GONE + binding.lobby.lobbyView.visibility = View.VISIBLE + binding.messagesListView.visibility = View.GONE + binding.messageInputView.visibility = View.GONE + binding.progressBar.visibility = View.GONE if (currentConversation?.lobbyTimer != null && currentConversation?.lobbyTimer != 0L ) { - conversationLobbyText?.text = String.format( + binding.lobby.lobbyTextView.text = String.format( resources!!.getString(R.string.nc_lobby_waiting_with_date), DateUtils.getLocalDateStringFromTimestampForLobby( currentConversation?.lobbyTimer @@ -667,12 +627,12 @@ class ChatController(args: Bundle) : ) ) } else { - conversationLobbyText?.setText(R.string.nc_lobby_waiting) + binding.lobby.lobbyTextView.setText(R.string.nc_lobby_waiting) } } else { - lobbyView?.visibility = View.GONE - messagesListView?.visibility = View.VISIBLE - messageInput?.visibility = View.VISIBLE + binding.lobby.lobbyView.visibility = View.GONE + binding.messagesListView.visibility = View.VISIBLE + binding.messageInputView.inputEditText?.visibility = View.VISIBLE if (isFirstMessagesProcessing && pastPreconditionFailed) { pastPreconditionFailed = false pullChatMessages(0) @@ -682,9 +642,9 @@ class ChatController(args: Bundle) : } } } else { - lobbyView?.visibility = View.GONE - messagesListView?.visibility = View.VISIBLE - messageInput?.visibility = View.VISIBLE + binding.lobby.lobbyView.visibility = View.GONE + binding.messagesListView.visibility = View.VISIBLE + binding.messageInputView.inputEditText?.visibility = View.VISIBLE } } @@ -765,7 +725,10 @@ class ChatController(args: Bundle) : require(files.isNotEmpty()) val data: Data = Data.Builder() .putStringArray(UploadAndShareFilesWorker.DEVICE_SOURCEFILES, files.toTypedArray()) - .putString(UploadAndShareFilesWorker.NC_TARGETPATH, conversationUser?.getAttachmentFolder()) + .putString( + UploadAndShareFilesWorker.NC_TARGETPATH, + CapabilitiesUtil.getAttachmentFolder(conversationUser) + ) .putString(UploadAndShareFilesWorker.ROOM_TOKEN, roomToken) .build() val uploadWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(UploadAndShareFilesWorker::class.java) @@ -825,23 +788,26 @@ class ChatController(args: Bundle) : } private fun setupMentionAutocomplete() { - val elevation = 6f - resources?.let { - val backgroundDrawable = ColorDrawable(it.getColor(R.color.bg_default)) - val presenter = MentionAutocompletePresenter(activity, roomToken) - val callback = MentionAutocompleteCallback( - activity, - conversationUser, messageInput - ) + if (isAlive()) { + val elevation = 6f + resources?.let { + val backgroundDrawable = ColorDrawable(it.getColor(R.color.bg_default)) + val presenter = MentionAutocompletePresenter(activity, roomToken) + val callback = MentionAutocompleteCallback( + activity, + conversationUser, + binding.messageInputView.inputEditText + ) - if (mentionAutocomplete == null && messageInput != null) { - mentionAutocomplete = Autocomplete.on(messageInput) - .with(elevation) - .with(backgroundDrawable) - .with(MagicCharPolicy('@')) - .with(presenter) - .with(callback) - .build() + if (mentionAutocomplete == null && binding.messageInputView.inputEditText != null) { + mentionAutocomplete = Autocomplete.on(binding.messageInputView.inputEditText) + .with(elevation) + .with(backgroundDrawable) + .with(MagicCharPolicy('@')) + .with(presenter) + .with(callback) + .build() + } } } } @@ -851,7 +817,7 @@ class ChatController(args: Bundle) : eventBus?.register(this) if (conversationUser?.userId != "?" && - conversationUser?.hasSpreedFeatureCapability("mention-flag") ?: false && + CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "mention-flag") ?: false && activity != null ) { activity?.findViewById(R.id.toolbar)?.setOnClickListener { v -> showConversationInfoScreen() } @@ -865,17 +831,33 @@ class ChatController(args: Bundle) : isLinkPreviewAllowed = appPreferences?.areLinkPreviewsAllowed ?: false - emojiPopup = messageInput?.let { + val smileyButton = binding.messageInputView.findViewById(R.id.smileyButton) + + emojiPopup = binding.messageInputView.inputEditText?.let { EmojiPopup.Builder.fromRootView(view).setOnEmojiPopupShownListener { if (resources != null) { - smileyButton?.setColorFilter(resources!!.getColor(R.color.colorPrimary), PorterDuff.Mode.SRC_IN) + smileyButton?.setColorFilter( + resources!!.getColor(R.color.colorPrimary), + PorterDuff.Mode.SRC_IN + ) } }.setOnEmojiPopupDismissListener { smileyButton?.setColorFilter( resources!!.getColor(R.color.emoji_icons), PorterDuff.Mode.SRC_IN ) - }.setOnEmojiClickListener { emoji, imageView -> messageInput?.editableText?.append(" ") }.build(it) + }.setOnEmojiClickListener { emoji, + imageView -> + binding.messageInputView.inputEditText?.editableText?.append(" ") + }.build(it) + } + + smileyButton?.setOnClickListener { + emojiPopup?.toggle() + } + + binding.messageInputView.findViewById(R.id.cancelReplyButton)?.setOnClickListener { + cancelReply() } if (activity != null) { @@ -893,12 +875,19 @@ class ChatController(args: Bundle) : } } + private fun cancelReply() { + binding.messageInputView.findViewById(R.id.quotedChatMessageView)?.visibility = View.GONE + binding.messageInputView.findViewById(R.id.attachmentButton)?.visibility = View.VISIBLE + binding.messageInputView.findViewById(R.id.attachmentButtonSpace)?.visibility = View.VISIBLE + } + private fun cancelNotificationsForCurrentConversation() { if (conversationUser != null) { if (!TextUtils.isEmpty(roomToken)) { NotificationUtils.cancelExistingNotificationsForRoom( applicationContext, - conversationUser, roomToken!! + conversationUser, + roomToken!! ) } } @@ -931,13 +920,13 @@ class ChatController(args: Bundle) : } } - override fun getTitle(): String { - currentConversation?.displayName?.let { - return " " + EmojiCompat.get().process(it as CharSequence).toString() - } - - return "" - } + override val title: String + get() = + if (currentConversation?.displayName != null) { + " " + EmojiCompat.get().process(currentConversation?.displayName as CharSequence).toString() + } else { + "" + } public override fun onDestroy() { super.onDestroy() @@ -962,11 +951,6 @@ class ChatController(args: Bundle) : } } - @OnClick(R.id.smileyButton) - internal fun onSmileyClick() { - emojiPopup?.toggle() - } - private fun joinRoomWithPassword() { if (currentConversation == null || TextUtils.isEmpty(currentConversation?.sessionId) || @@ -991,6 +975,7 @@ class ChatController(args: Bundle) : disposableList.add(d) } + @Suppress("Detekt.TooGenericExceptionCaught") override fun onNext(roomOverall: RoomOverall) { inConversation = true currentConversation?.sessionId = roomOverall.ocs.data.sessionId @@ -999,7 +984,14 @@ class ChatController(args: Bundle) : currentConversation?.sessionId setupWebsocket() - checkLobbyState() + + try { + checkLobbyState() + } catch (npe: NullPointerException) { + // view binding can be null + // since this is called asynchrously and UI might have been destroyed in the meantime + Log.i(TAG, "UI destroyed - view binding already gone") + } if (isFirstMessagesProcessing) { pullChatMessages(0) @@ -1092,8 +1084,8 @@ class ChatController(args: Bundle) : } private fun submitMessage() { - if (messageInput != null) { - val editable = messageInput!!.editableText + if (binding.messageInputView.inputEditText != null) { + val editable = binding.messageInputView.inputEditText!!.editableText val mentionSpans = editable.getSpans( 0, editable.length, Spans.MentionChipSpan::class.java @@ -1108,11 +1100,15 @@ class ChatController(args: Bundle) : editable.replace(editable.getSpanStart(mentionSpan), editable.getSpanEnd(mentionSpan), "@$mentionId") } - messageInput?.setText("") + binding.messageInputView.inputEditText?.setText("") val replyMessageId: Int? = view?.findViewById(R.id.quotedChatMessageView)?.tag as Int? sendMessage( editable, - if (view?.findViewById(R.id.quotedChatMessageView)?.visibility == View.VISIBLE) replyMessageId else null + if ( + view + ?.findViewById(R.id.quotedChatMessageView) + ?.visibility == View.VISIBLE + ) replyMessageId else null ) cancelReply() } @@ -1134,16 +1130,24 @@ class ChatController(args: Bundle) : ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(object : Observer { override fun onSubscribe(d: Disposable) { + // unused atm } + @Suppress("Detekt.TooGenericExceptionCaught") override fun onNext(genericOverall: GenericOverall) { myFirstMessage = message - if (popupBubble?.isShown ?: false) { - popupBubble?.hide() - } + try { + if (binding.popupBubbleView.isShown == true) { + binding.popupBubbleView.hide() + } - messagesListView?.smoothScrollToPosition(0) + binding.messagesListView.smoothScrollToPosition(0) + } catch (npe: NullPointerException) { + // view binding can be null + // since this is called asynchrously and UI might have been destroyed in the meantime + Log.i(TAG, "UI destroyed - view binding already gone") + } } override fun onError(e: Throwable) { @@ -1152,16 +1156,17 @@ class ChatController(args: Bundle) : if (Integer.toString(code).startsWith("2")) { myFirstMessage = message - if (popupBubble?.isShown ?: false) { - popupBubble?.hide() + if (binding.popupBubbleView.isShown == true) { + binding.popupBubbleView.hide() } - messagesListView?.smoothScrollToPosition(0) + binding.messagesListView.smoothScrollToPosition(0) } } } override fun onComplete() { + // unused atm } }) } @@ -1245,20 +1250,29 @@ class ChatController(args: Bundle) : disposableList.add(d) } + @Suppress("Detekt.TooGenericExceptionCaught") override fun onNext(response: Response<*>) { - if (response.code() == 304) { - pullChatMessages(1, setReadMarker, xChatLastCommonRead) - } else if (response.code() == 412) { - futurePreconditionFailed = true - } else { - processMessages(response, true, finalTimeout) + try { + if (response.code() == 304) { + pullChatMessages(1, setReadMarker, xChatLastCommonRead) + } else if (response.code() == 412) { + futurePreconditionFailed = true + } else { + processMessages(response, true, finalTimeout) + } + } catch (npe: NullPointerException) { + // view binding can be null + // since this is called asynchrously and UI might have been destroyed in the meantime + Log.i(TAG, "UI destroyed - view binding already gone") } } override fun onError(e: Throwable) { + // unused atm } override fun onComplete() { + // unused atm } }) } else { @@ -1274,18 +1288,27 @@ class ChatController(args: Bundle) : disposableList.add(d) } + @Suppress("Detekt.TooGenericExceptionCaught") override fun onNext(response: Response<*>) { - if (response.code() == 412) { - pastPreconditionFailed = true - } else { - processMessages(response, false, 0) + try { + if (response.code() == 412) { + pastPreconditionFailed = true + } else { + processMessages(response, false, 0) + } + } catch (npe: NullPointerException) { + // view binding can be null + // since this is called asynchrously and UI might have been destroyed in the meantime + Log.i(TAG, "UI destroyed - view binding already gone") } } override fun onError(e: Throwable) { + // unused atm } override fun onComplete() { + // unused atm } }) } @@ -1312,7 +1335,7 @@ class ChatController(args: Bundle) : } } - if (response.code() == 200) { + if (response.code() == HTTP_CODE_OK) { val chatOverall = response.body() as ChatOverall? val chatMessageList = setDeletionFlagsAndRemoveInfomessages(chatOverall?.ocs!!.data) @@ -1321,9 +1344,9 @@ class ChatController(args: Bundle) : cancelNotificationsForCurrentConversation() isFirstMessagesProcessing = false - loadingProgressBar?.visibility = View.GONE + binding.progressBar.visibility = View.GONE - messagesListView?.visibility = View.VISIBLE + binding.messagesListView.visibility = View.VISIBLE } var countGroupedMessages = 0 @@ -1387,11 +1410,11 @@ class ChatController(args: Bundle) : adapter != null && adapter?.itemCount == 0 - if (!shouldAddNewMessagesNotice && !shouldScroll && popupBubble != null) { - if (!popupBubble!!.isShown) { + if (!shouldAddNewMessagesNotice && !shouldScroll) { + if (!binding.popupBubbleView.isShown) { newMessagesCount = 1 - popupBubble?.show() - } else if (popupBubble!!.isShown) { + binding.popupBubbleView.show() + } else if (binding.popupBubbleView.isShown == true) { newMessagesCount++ } } else { @@ -1411,10 +1434,10 @@ class ChatController(args: Bundle) : } } - if (shouldAddNewMessagesNotice && adapter != null && messagesListView != null) { + if (shouldAddNewMessagesNotice && adapter != null) { layoutManager?.scrollToPositionWithOffset( adapter!!.getMessagePositionByIdInReverse("-1"), - messagesListView!!.height / 2 + binding.messagesListView.height / 2 ) } } @@ -1443,7 +1466,7 @@ class ChatController(args: Bundle) : cancelNotificationsForCurrentConversation() isFirstMessagesProcessing = false - loadingProgressBar?.visibility = View.GONE + binding.progressBar.visibility = View.GONE } historyRead = true @@ -1489,7 +1512,7 @@ class ChatController(args: Bundle) : override fun onPrepareOptionsMenu(menu: Menu) { super.onPrepareOptionsMenu(menu) conversationUser?.let { - if (it.hasSpreedFeatureCapability("read-only-rooms")) { + if (CapabilitiesUtil.hasSpreedFeatureCapability(it, "read-only-rooms")) { checkReadOnlyState() } } @@ -1558,9 +1581,9 @@ class ChatController(args: Bundle) : private fun getIntentForCall(isVoiceOnlyCall: Boolean): Intent? { currentConversation?.let { val bundle = Bundle() - bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) - bundle.putString(BundleKeys.KEY_ROOM_ID, roomId) - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser) + bundle.putString(KEY_ROOM_TOKEN, roomToken) + bundle.putString(KEY_ROOM_ID, roomId) + bundle.putParcelable(KEY_USER_ENTITY, conversationUser) bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, roomPassword) bundle.putString(BundleKeys.KEY_MODIFIED_BASE_URL, conversationUser?.baseUrl) bundle.putString(BundleKeys.KEY_CONVERSATION_NAME, it.displayName) @@ -1581,18 +1604,13 @@ class ChatController(args: Bundle) : } } - @OnClick(R.id.cancelReplyButton) - fun cancelReply() { - quotedChatMessageView?.visibility = View.GONE - messageInputView!!.findViewById(R.id.attachmentButton)?.visibility = View.VISIBLE - messageInputView!!.findViewById(R.id.attachmentButtonSpace)?.visibility = View.VISIBLE - } - override fun onMessageViewLongClick(view: View?, message: IMessage?) { PopupMenu( ContextThemeWrapper(view?.context, R.style.appActionBarPopupMenu), view, - if (message?.user?.id == currentConversation?.actorType + "/" + currentConversation?.actorId) Gravity.END else Gravity.START + if ( + message?.user?.id == currentConversation?.actorType + "/" + currentConversation?.actorId + ) Gravity.END else Gravity.START ).apply { setOnMenuItemClickListener { item -> when (item?.itemId) { @@ -1607,45 +1625,54 @@ class ChatController(args: Bundle) : R.id.action_reply_to_message -> { val chatMessage = message as ChatMessage? chatMessage?.let { - messageInputView?.findViewById(R.id.attachmentButton)?.visibility = View.GONE - messageInputView?.findViewById(R.id.attachmentButtonSpace)?.visibility = View.GONE - messageInputView?.findViewById(R.id.cancelReplyButton)?.visibility = + binding.messageInputView.findViewById(R.id.attachmentButton)?.visibility = + View.GONE + binding.messageInputView.findViewById(R.id.attachmentButtonSpace)?.visibility = + View.GONE + binding.messageInputView.findViewById(R.id.cancelReplyButton)?.visibility = View.VISIBLE - messageInputView?.findViewById(R.id.quotedMessage)?.maxLines = 2 - messageInputView?.findViewById(R.id.quotedMessage)?.ellipsize = - TextUtils.TruncateAt.END - messageInputView?.findViewById(R.id.quotedMessage)?.text = it.text - messageInputView?.findViewById(R.id.quotedMessageAuthor)?.text = + + val quotedMessage = binding + .messageInputView + .findViewById(R.id.quotedMessage) + + quotedMessage?.maxLines = 2 + quotedMessage?.ellipsize = TextUtils.TruncateAt.END + quotedMessage?.text = it.text + binding.messageInputView.findViewById(R.id.quotedMessageAuthor)?.text = it.actorDisplayName ?: context!!.getText(R.string.nc_nick_guest) conversationUser?.let { currentUser -> - + val quotedMessageImage = binding + .messageInputView + .findViewById(R.id.quotedMessageImage) chatMessage.imageUrl?.let { previewImageUrl -> - messageInputView?.findViewById(R.id.quotedMessageImage)?.visibility = - View.VISIBLE + quotedMessageImage?.visibility = View.VISIBLE val px = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 96f, resources?.displayMetrics ) - messageInputView?.findViewById(R.id.quotedMessageImage)?.maxHeight = - px.toInt() - val layoutParams = - messageInputView?.findViewById(R.id.quotedMessageImage)?.layoutParams as FlexboxLayout.LayoutParams + + quotedMessageImage?.maxHeight = px.toInt() + val layoutParams = quotedMessageImage?.layoutParams as FlexboxLayout.LayoutParams layoutParams.flexGrow = 0f - messageInputView?.findViewById(R.id.quotedMessageImage)?.layoutParams = - layoutParams - messageInputView?.findViewById(R.id.quotedMessageImage) - ?.load(previewImageUrl) { - addHeader("Authorization", credentials!!) - } + quotedMessageImage.layoutParams = layoutParams + quotedMessageImage.load(previewImageUrl) { + addHeader("Authorization", credentials!!) + } } ?: run { - messageInputView?.findViewById(R.id.quotedMessageImage)?.visibility = - View.GONE + binding + .messageInputView + .findViewById(R.id.quotedMessageImage) + ?.visibility = View.GONE } } + val quotedChatMessageView = binding + .messageInputView + .findViewById(R.id.quotedChatMessageView) quotedChatMessageView?.tag = message?.jsonMessageId quotedChatMessageView?.visibility = View.VISIBLE } @@ -1669,14 +1696,17 @@ class ChatController(args: Bundle) : .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(object : Observer { - override fun onSubscribe(d: Disposable) {} + override fun onSubscribe(d: Disposable) { + // unused atm + } + override fun onNext(roomOverall: RoomOverall) { val bundle = Bundle() bundle.putParcelable(KEY_USER_ENTITY, conversationUser) bundle.putString(KEY_ROOM_TOKEN, roomOverall.getOcs().getData().getToken()) bundle.putString(KEY_ROOM_ID, roomOverall.getOcs().getData().getRoomId()) - // FIXME once APIv2 or later is used only, the createRoom already returns all the data + // FIXME once APIv2+ is used only, the createRoom already returns all the data ncApi!!.getRoom( credentials, ApiUtils.getUrlForRoom( @@ -1687,7 +1717,10 @@ class ChatController(args: Bundle) : .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(object : Observer { - override fun onSubscribe(d: Disposable) {} + override fun onSubscribe(d: Disposable) { + // unused atm + } + override fun onNext(roomOverall: RoomOverall) { bundle.putParcelable( KEY_ACTIVE_CONVERSATION, @@ -1703,7 +1736,9 @@ class ChatController(args: Bundle) : Log.e(TAG, e.message, e) } - override fun onComplete() {} + override fun onComplete() { + // unused atm + } }) } @@ -1711,7 +1746,9 @@ class ChatController(args: Bundle) : Log.e(TAG, e.message, e) } - override fun onComplete() {} + override fun onComplete() { + // unused atm + } }) true } @@ -1734,6 +1771,7 @@ class ChatController(args: Bundle) : ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(object : Observer { override fun onSubscribe(d: Disposable) { + // unused atm } override fun onNext(t: ChatOverallSingleMessage) { @@ -1756,6 +1794,7 @@ class ChatController(args: Bundle) : } override fun onComplete() { + // unused atm } }) true @@ -1802,8 +1841,9 @@ class ChatController(args: Bundle) : if (message.hasFileAttachment()) return false - val sixHoursInMillis = 6 * 3600 * 1000 - val isOlderThanSixHours = message.createdAt?.before(Date(System.currentTimeMillis() - sixHoursInMillis)) == true + val isOlderThanSixHours = message + .createdAt + ?.before(Date(System.currentTimeMillis() - AGE_THREHOLD_FOR_DELETE_MESSAGE)) == true if (isOlderThanSixHours) return false val isUserAllowedByPrivileges = if (message.actorId == conversationUser.userId) { @@ -1813,18 +1853,17 @@ class ChatController(args: Bundle) : } if (!isUserAllowedByPrivileges) return false - if (!conversationUser.hasSpreedFeatureCapability("delete-messages")) return false + if (!CapabilitiesUtil.hasSpreedFeatureCapability(conversationUser, "delete-messages")) return false return true } override fun hasContentFor(message: IMessage, type: Byte): Boolean { - when (type) { - CONTENT_TYPE_SYSTEM_MESSAGE -> return !TextUtils.isEmpty(message.systemMessage) - CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> return message.id == "-1" + return when (type) { + CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage) + CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == "-1" + else -> false } - - return false } @Subscribe(threadMode = ThreadMode.BACKGROUND) @@ -1833,7 +1872,11 @@ class ChatController(args: Bundle) : switch (webSocketCommunicationEvent.getType()) { case "refreshChat": - if (webSocketCommunicationEvent.getHashMap().get(BundleKeys.KEY_INTERNAL_USER_ID).equals(Long.toString(conversationUser.getId()))) { + if ( + webSocketCommunicationEvent + .getHashMap().get(BundleKeys.KEY_INTERNAL_USER_ID) + .equals(Long.toString(conversationUser.getId())) + ) { if (roomToken.equals(webSocketCommunicationEvent.getHashMap().get(BundleKeys.KEY_ROOM_TOKEN))) { pullChatMessages(2); } @@ -1872,18 +1915,19 @@ class ChatController(args: Bundle) : ?.observeOn(AndroidSchedulers.mainThread()) ?.subscribe(object : Observer { override fun onSubscribe(d: Disposable) { + // unused atm } override fun onNext(roomOverall: RoomOverall) { val conversationIntent = Intent(activity, MagicCallActivity::class.java) val bundle = Bundle() - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser) - bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomOverall.ocs.data.token) - bundle.putString(BundleKeys.KEY_ROOM_ID, roomOverall.ocs.data.roomId) + bundle.putParcelable(KEY_USER_ENTITY, conversationUser) + bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs.data.token) + bundle.putString(KEY_ROOM_ID, roomOverall.ocs.data.roomId) if (conversationUser != null) { bundle.putParcelable( - BundleKeys.KEY_ACTIVE_CONVERSATION, + KEY_ACTIVE_CONVERSATION, Parcels.wrap(roomOverall.ocs.data) ) conversationIntent.putExtras(bundle) @@ -1901,23 +1945,32 @@ class ChatController(args: Bundle) : router.popCurrentController() } }, - 100 + POP_CURRENT_CONTROLLER_DELAY ) } } override fun onError(e: Throwable) { + // unused atm } - override fun onComplete() {} + override fun onComplete() { + // unused atm + } }) } } companion object { - private val TAG = "ChatController" - private val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 1 - private val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 2 - val REQUEST_CODE_CHOOSE_FILE: Int = 555 + private const val TAG = "ChatController" + private const val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 1 + private const val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 2 + private const val NEW_MESSAGES_POPUP_BUBBLE_DELAY: Long = 200 + private const val POP_CURRENT_CONTROLLER_DELAY: Long = 100 + private const val LOBBY_TIMER_DELAY: Long = 5000 + private const val HTTP_CODE_OK: Int = 200 + private const val MESSAGE_MAX_LENGTH: Int = 1000 + private const val AGE_THREHOLD_FOR_DELETE_MESSAGE: Int = 21600000 // (6 hours in millis = 6 * 3600 * 1000) + private const val REQUEST_CODE_CHOOSE_FILE: Int = 555 } } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java b/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java index fd9d5243d..83fc55359 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java +++ b/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) { diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt index ddfb96798..f3069485b 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt +++ b/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 * * 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 { +class ConversationInfoController(args: Bundle) : + NewBaseController( + R.layout.controller_conversation_info, + args + ), + FlexibleAdapter + .OnItemClickListener { + private val binding: ControllerConversationInfoBinding by viewBinding(ControllerConversationInfoBinding::bind) - @BindView(R.id.notification_settings) - lateinit var notificationsPreferenceScreen: MaterialPreferenceScreen + @Inject + @JvmField + var ncApi: NcApi? = null - @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 + @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(R.id.mp_checkable) as SwitchCompat) + (binding.webinarInfoView.conversationInfoLobby.findViewById(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(R.id.mp_checkable) as SwitchCompat).setOnCheckedChangeListener { _, _ -> - reconfigureLobbyTimerView() - submitLobbyChanges() - } + (binding.webinarInfoView.conversationInfoLobby.findViewById(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(R.id.mp_checkable) as SwitchCompat).isChecked + val isChecked = + (binding.webinarInfoView.conversationInfoLobby.findViewById(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(R.id.mp_checkable) as SwitchCompat).isChecked + (binding.webinarInfoView.conversationInfoLobby.findViewById(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 { + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { 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 { + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { 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() @@ -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 { + ncApi?.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, conversationUser!!.baseUrl, conversationToken)) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { 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(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 { + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { 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 { + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { 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 { + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { 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) { diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java b/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java index 744af7aff..d428ab62a 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java +++ b/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) diff --git a/app/src/main/java/com/nextcloud/talk/controllers/LockedController.java b/app/src/main/java/com/nextcloud/talk/controllers/LockedController.java deleted file mode 100644 index c34c2cbe3..000000000 --- a/app/src/main/java/com/nextcloud/talk/controllers/LockedController.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * @author Andy Scherzinger - * Copyright (C) 2021 Andy Scherzinger - * Copyright (C) 2017-2018 Mario Danic - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.nextcloud.talk.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; - } -} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/LockedController.kt b/app/src/main/java/com/nextcloud/talk/controllers/LockedController.kt new file mode 100644 index 000000000..c4a7e0356 --- /dev/null +++ b/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 + * Copyright (C) 2017-2018 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.nextcloud.talk.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") + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java b/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java index f4070d2b5..27c57dd93 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ProfileController.java +++ b/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; + + if (edit) { + item.setTitle(R.string.save); + + getActivity().findViewById(R.id.emptyList).setVisibility(View.GONE); + getActivity().findViewById(R.id.userinfo_list).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); } - edit = !edit; + ncApi.getEditableUserProfileFields( + ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()), + ApiUtils.getUrlForUserFields(currentUser.getBaseUrl())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) { + // unused atm + } - if (edit) { - item.setTitle(R.string.save); + @Override + public void onNext(@io.reactivex.annotations.NonNull UserProfileFieldsOverall userProfileFieldsOverall) { + editableFields = userProfileFieldsOverall.getOcs().getData(); + adapter.notifyDataSetChanged(); + } - getActivity().findViewById(R.id.emptyList).setVisibility(View.GONE); - getActivity().findViewById(R.id.userinfo_list).setVisibility(View.VISIBLE); + @Override + public void onError(@io.reactivex.annotations.NonNull Throwable e) { + Log.e(TAG, "Error loading editable user profile from server", e); + edit = false; + } - 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); - } + @Override + public void onComplete() { + // unused atm + } + }); + } else { + item.setTitle(R.string.edit); + getActivity().findViewById(R.id.avatar_buttons).setVisibility(View.INVISIBLE); - ncApi.getEditableUserProfileFields( - ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()), - ApiUtils.getUrlForUserFields(currentUser.getBaseUrl())) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer() { - @Override - public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) { - // unused atm - } - - @Override - public void onNext(@io.reactivex.annotations.NonNull 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 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(); + adapter.notifyDataSetChanged(); - return true; - - 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()) diff --git a/app/src/main/java/com/nextcloud/talk/controllers/RingtoneSelectionController.java b/app/src/main/java/com/nextcloud/talk/controllers/RingtoneSelectionController.java index f5edc8cf9..6c8e06b92 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/RingtoneSelectionController.java +++ b/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() { diff --git a/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java b/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java index 3355b9549..3dfa341ea 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java +++ b/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), diff --git a/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.java b/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.java index 8353afbfe..7b9ed699e 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.java +++ b/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() { diff --git a/app/src/main/java/com/nextcloud/talk/controllers/base/NewBaseController.kt b/app/src/main/java/com/nextcloud/talk/controllers/base/NewBaseController.kt new file mode 100644 index 000000000..7771cb4cb --- /dev/null +++ b/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 = 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 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/CallMenuController.java b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/CallMenuController.java index 1925c4d89..3dcba6705 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/CallMenuController.java +++ b/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))); } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.java b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.java index d14cd85e2..d1226ba3a 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/OperationsMenuController.java +++ b/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() { @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() { @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() { @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() { @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() { @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 - ); + 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); + } + } - ncApi.addParticipant(credentials, retrofitBucket.getUrl(), retrofitBucket.getQueryMap()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .retry(1) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { + private void addUsersToConversation( + ArrayList localInvitedUsers, + ArrayList 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() { + @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 localInvitedUsers, + ArrayList 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() { @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); } } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/util/ControllerViewBindingDelegate.kt b/app/src/main/java/com/nextcloud/talk/controllers/util/ControllerViewBindingDelegate.kt new file mode 100644 index 000000000..67b6244ab --- /dev/null +++ b/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 Controller.viewBinding(bindingFactory: (View) -> T) = + ControllerViewBindingDelegate(this, bindingFactory) + +class ControllerViewBindingDelegate( + controller: Controller, + private val viewBinder: (View) -> T +) : ReadOnlyProperty, 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 } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt index 26f995a15..d28020b89 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/ContactAddressBookWorker.kt +++ b/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() } diff --git a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt index f2d91f747..e9099c2a3 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt +++ b/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") diff --git a/app/src/main/java/com/nextcloud/talk/models/database/CapabilitiesUtil.java b/app/src/main/java/com/nextcloud/talk/models/database/CapabilitiesUtil.java new file mode 100644 index 000000000..bd4fe3952 --- /dev/null +++ b/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 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.nextcloud.talk.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 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 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 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 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; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/database/User.java b/app/src/main/java/com/nextcloud/talk/models/database/User.java index 7e599f782..47e9fac7e 100644 --- a/app/src/main/java/com/nextcloud/talk/models/database/User.java +++ b/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 * Copyright (C) 2017-2018 Mario Danic * * 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 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 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 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 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; - } } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.java b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.java index d068c2750..bf4f63c2f 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.java +++ b/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) { diff --git a/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationRichObject.java b/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationRichObject.java index 8a5d37b61..5ae1ab55d 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationRichObject.java +++ b/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() { diff --git a/app/src/main/java/com/nextcloud/talk/receivers/PackageReplacedReceiver.kt b/app/src/main/java/com/nextcloud/talk/receivers/PackageReplacedReceiver.kt index 714eab4f5..81a53026f 100644 --- a/app/src/main/java/com/nextcloud/talk/receivers/PackageReplacedReceiver.kt +++ b/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) } diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt index 921f2f2ab..29e47b131 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt +++ b/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) diff --git a/app/src/main/java/com/nextcloud/talk/utils/AccountUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/AccountUtils.kt index 9fc105c81..8f3a3c5c2 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/AccountUtils.kt +++ b/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 } diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java index a95717788..ebb129776 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java +++ b/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; } diff --git a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt index a4896c6e0..204111cf3 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt +++ b/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) } diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.java b/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.java index 9e6c29721..6712db869 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.java +++ b/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) { diff --git a/app/src/main/res/layout/controller_chat.xml b/app/src/main/res/layout/controller_chat.xml index 206bd6b19..4f7d8e159 100644 --- a/app/src/main/res/layout/controller_chat.xml +++ b/app/src/main/res/layout/controller_chat.xml @@ -26,7 +26,9 @@ android:animateLayoutChanges="true" android:background="@color/bg_default"> - diff --git a/app/src/main/res/layout/controller_conversation_info.xml b/app/src/main/res/layout/controller_conversation_info.xml index ddd597588..09a791561 100644 --- a/app/src/main/res/layout/controller_conversation_info.xml +++ b/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 @@ - + android:orientation="vertical"> - + + + + + diff --git a/detekt.yml b/detekt.yml index 7b254e8ec..d93d0475f 100644 --- a/detekt.yml +++ b/detekt.yml @@ -1,5 +1,5 @@ build: - maxIssues: 346 + maxIssues: 201 weights: # complexity: 2 # LongParameterList: 1 diff --git a/scripts/analysis/findbugs-results.txt b/scripts/analysis/findbugs-results.txt index 2da432533..e966f9075 100644 --- a/scripts/analysis/findbugs-results.txt +++ b/scripts/analysis/findbugs-results.txt @@ -1 +1 @@ -457 \ No newline at end of file +450 \ No newline at end of file diff --git a/scripts/analysis/lint-results.txt b/scripts/analysis/lint-results.txt index 2d5d3a92e..b0fd55a3e 100644 --- a/scripts/analysis/lint-results.txt +++ b/scripts/analysis/lint-results.txt @@ -1,2 +1,2 @@ DO NOT TOUCH; GENERATED BY DRONE - Lint Report: 3 errors and 329 warnings + Lint Report: 3 errors and 290 warnings