package com.simplemobiletools.smsmessenger.adapters import android.content.Intent import android.graphics.Typeface import android.os.Parcelable import android.text.TextUtils import android.util.TypedValue import android.view.Menu import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller import com.simplemobiletools.commons.adapters.MyRecyclerViewListAdapter import com.simplemobiletools.commons.dialogs.ConfirmationDialog import com.simplemobiletools.commons.dialogs.FeatureLockedDialog import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.KEY_PHONE import com.simplemobiletools.commons.helpers.SimpleContactsHelper import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.commons.helpers.isNougatPlus import com.simplemobiletools.commons.views.MyRecyclerView import com.simplemobiletools.smsmessenger.R import com.simplemobiletools.smsmessenger.activities.SimpleActivity import com.simplemobiletools.smsmessenger.dialogs.RenameConversationDialog import com.simplemobiletools.smsmessenger.extensions.* import com.simplemobiletools.smsmessenger.helpers.refreshMessages import com.simplemobiletools.smsmessenger.messaging.isShortCodeWithLetters import com.simplemobiletools.smsmessenger.models.Conversation import kotlinx.android.synthetic.main.item_conversation.view.* class ConversationsAdapter( activity: SimpleActivity, recyclerView: MyRecyclerView, onRefresh: () -> Unit, itemClick: (Any) -> Unit ) : MyRecyclerViewListAdapter(activity, recyclerView, ConversationDiffCallback(), itemClick, onRefresh), RecyclerViewFastScroller.OnPopupTextUpdate { private var fontSize = activity.getTextSize() private var drafts = HashMap() private var recyclerViewState: Parcelable? = null init { setupDragListener(true) ensureBackgroundThread { fetchDrafts(drafts) } setHasStableIds(true) registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onChanged() = restoreRecyclerViewState() override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) = restoreRecyclerViewState() override fun onItemRangeInserted(positionStart: Int, itemCount: Int) = restoreRecyclerViewState() }) } override fun getActionMenuId() = R.menu.cab_conversations override fun prepareActionMode(menu: Menu) { val selectedItems = getSelectedItems() val isSingleSelection = isOneItemSelected() val selectedConversation = selectedItems.firstOrNull() val isGroupConversation = selectedConversation?.isGroupConversation == true menu.apply { findItem(R.id.cab_block_number).title = activity.addLockedLabelIfNeeded(R.string.block_number) findItem(R.id.cab_block_number).isVisible = isNougatPlus() findItem(R.id.cab_add_number_to_contact).isVisible = isSingleSelection && !isGroupConversation findItem(R.id.cab_dial_number).isVisible = isSingleSelection && !isGroupConversation && !isShortCodeWithLetters(selectedConversation!!.phoneNumber) findItem(R.id.cab_copy_number).isVisible = isSingleSelection && !isGroupConversation findItem(R.id.rename_conversation).isVisible = isSingleSelection && isGroupConversation findItem(R.id.cab_mark_as_read).isVisible = selectedItems.any { !it.read } findItem(R.id.cab_mark_as_unread).isVisible = selectedItems.any { it.read } checkPinBtnVisibility(this) } } override fun actionItemPressed(id: Int) { if (selectedKeys.isEmpty()) { return } when (id) { R.id.cab_add_number_to_contact -> addNumberToContact() R.id.cab_block_number -> tryBlocking() R.id.cab_dial_number -> dialNumber() R.id.cab_copy_number -> copyNumberToClipboard() R.id.cab_delete -> askConfirmDelete() R.id.rename_conversation -> renameConversation(getSelectedItems().first()) R.id.cab_mark_as_read -> markAsRead() R.id.cab_mark_as_unread -> markAsUnread() R.id.cab_pin_conversation -> pinConversation(true) R.id.cab_unpin_conversation -> pinConversation(false) R.id.cab_select_all -> selectAll() } } override fun getSelectableItemCount() = itemCount override fun getIsItemSelectable(position: Int) = true override fun getItemSelectionKey(position: Int) = currentList.getOrNull(position)?.hashCode() override fun getItemKeyPosition(key: Int) = currentList.indexOfFirst { it.hashCode() == key } override fun onActionModeCreated() {} override fun onActionModeDestroyed() {} override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = createViewHolder(R.layout.item_conversation, parent) override fun onBindViewHolder(holder: ViewHolder, position: Int) { val conversation = getItem(position) holder.bindView(conversation, allowSingleClick = true, allowLongClick = true) { itemView, _ -> setupView(itemView, conversation) } bindViewHolder(holder) } override fun getItemId(position: Int) = getItem(position).threadId override fun onViewRecycled(holder: ViewHolder) { super.onViewRecycled(holder) if (!activity.isDestroyed && !activity.isFinishing) { Glide.with(activity).clear(holder.itemView.conversation_image) } } private fun tryBlocking() { if (activity.isOrWasThankYouInstalled()) { askConfirmBlock() } else { FeatureLockedDialog(activity) { } } } private fun askConfirmBlock() { val numbers = getSelectedItems().distinctBy { it.phoneNumber }.map { it.phoneNumber } val numbersString = TextUtils.join(", ", numbers) val question = String.format(resources.getString(R.string.block_confirmation), numbersString) ConfirmationDialog(activity, question) { blockNumbers() } } private fun blockNumbers() { if (selectedKeys.isEmpty()) { return } val numbersToBlock = getSelectedItems() val newList = currentList.toMutableList().apply { removeAll(numbersToBlock) } ensureBackgroundThread { numbersToBlock.map { it.phoneNumber }.forEach { number -> activity.addBlockedNumber(number) } activity.runOnUiThread { submitList(newList) finishActMode() } } } private fun dialNumber() { val conversation = getSelectedItems().firstOrNull() ?: return activity.dialNumber(conversation.phoneNumber) { finishActMode() } } private fun copyNumberToClipboard() { val conversation = getSelectedItems().firstOrNull() ?: return activity.copyToClipboard(conversation.phoneNumber) finishActMode() } private fun askConfirmDelete() { val itemsCnt = selectedKeys.size val items = resources.getQuantityString(R.plurals.delete_conversations, itemsCnt, itemsCnt) val baseString = R.string.deletion_confirmation val question = String.format(resources.getString(baseString), items) ConfirmationDialog(activity, question) { ensureBackgroundThread { deleteConversations() } } } private fun deleteConversations() { if (selectedKeys.isEmpty()) { return } val conversationsToRemove = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList conversationsToRemove.forEach { activity.deleteConversation(it.threadId) activity.notificationManager.cancel(it.hashCode()) } val newList = try { currentList.toMutableList().apply { removeAll(conversationsToRemove) } } catch (ignored: Exception) { currentList.toMutableList() } activity.runOnUiThread { if (newList.none { selectedKeys.contains(it.hashCode()) }) { refreshMessages() finishActMode() } else { submitList(newList) if (newList.isEmpty()) { refreshMessages() } } } } private fun renameConversation(conversation: Conversation) { RenameConversationDialog(activity, conversation) { ensureBackgroundThread { val updatedConv = activity.renameConversation(conversation, newTitle = it) activity.runOnUiThread { finishActMode() currentList.toMutableList().apply { set(indexOf(conversation), updatedConv) updateConversations(this as ArrayList) } } } } } private fun markAsRead() { if (selectedKeys.isEmpty()) { return } val conversationsMarkedAsRead = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList ensureBackgroundThread { conversationsMarkedAsRead.filter { conversation -> !conversation.read }.forEach { activity.markThreadMessagesRead(it.threadId) } refreshConversations() } } private fun markAsUnread() { if (selectedKeys.isEmpty()) { return } val conversationsMarkedAsUnread = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList ensureBackgroundThread { conversationsMarkedAsUnread.filter { conversation -> conversation.read }.forEach { activity.markThreadMessagesUnread(it.threadId) } refreshConversations() } } private fun addNumberToContact() { val conversation = getSelectedItems().firstOrNull() ?: return Intent().apply { action = Intent.ACTION_INSERT_OR_EDIT type = "vnd.android.cursor.item/contact" putExtra(KEY_PHONE, conversation.phoneNumber) activity.launchActivityIntent(this) } } private fun getSelectedItems() = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList private fun pinConversation(pin: Boolean) { val conversations = getSelectedItems() if (conversations.isEmpty()) { return } if (pin) { activity.config.addPinnedConversations(conversations) } else { activity.config.removePinnedConversations(conversations) } getSelectedItemPositions().forEach { notifyItemChanged(it) } refreshConversations() } private fun checkPinBtnVisibility(menu: Menu) { val pinnedConversations = activity.config.pinnedConversations val selectedConversations = getSelectedItems() menu.findItem(R.id.cab_pin_conversation).isVisible = selectedConversations.any { !pinnedConversations.contains(it.threadId.toString()) } menu.findItem(R.id.cab_unpin_conversation).isVisible = selectedConversations.any { pinnedConversations.contains(it.threadId.toString()) } } private fun fetchDrafts(drafts: HashMap) { drafts.clear() for ((threadId, draft) in activity.getAllDrafts()) { drafts[threadId] = draft } } fun updateFontSize() { fontSize = activity.getTextSize() notifyDataSetChanged() } fun updateConversations(newConversations: ArrayList, commitCallback: (() -> Unit)? = null) { saveRecyclerViewState() submitList(newConversations.toList(), commitCallback) } fun updateDrafts() { ensureBackgroundThread { val newDrafts = HashMap() fetchDrafts(newDrafts) if (drafts.hashCode() != newDrafts.hashCode()) { drafts = newDrafts activity.runOnUiThread { notifyDataSetChanged() } } } } private fun setupView(view: View, conversation: Conversation) { view.apply { val smsDraft = drafts[conversation.threadId] draft_indicator.beVisibleIf(smsDraft != null) draft_indicator.setTextColor(properPrimaryColor) pin_indicator.beVisibleIf(activity.config.pinnedConversations.contains(conversation.threadId.toString())) pin_indicator.applyColorFilter(textColor) conversation_frame.isSelected = selectedKeys.contains(conversation.hashCode()) conversation_address.apply { text = conversation.title setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 1.2f) } conversation_body_short.apply { text = smsDraft ?: conversation.snippet setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 0.9f) } conversation_date.apply { text = conversation.date.formatDateOrTime(context, true, false) setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 0.8f) } val style = if (conversation.read) { conversation_body_short.alpha = 0.7f if (conversation.isScheduled) Typeface.ITALIC else Typeface.NORMAL } else { conversation_body_short.alpha = 1f if (conversation.isScheduled) Typeface.BOLD_ITALIC else Typeface.BOLD } conversation_address.setTypeface(null, style) conversation_body_short.setTypeface(null, style) arrayListOf(conversation_address, conversation_body_short, conversation_date).forEach { it.setTextColor(textColor) } // at group conversations we use an icon as the placeholder, not any letter val placeholder = if (conversation.isGroupConversation) { SimpleContactsHelper(context).getColoredGroupIcon(conversation.title) } else { null } SimpleContactsHelper(context).loadContactImage(conversation.photoUri, conversation_image, conversation.title, placeholder) } } override fun onChange(position: Int) = currentList.getOrNull(position)?.title ?: "" private fun refreshConversations() { activity.runOnUiThread { refreshMessages() finishActMode() } } private fun saveRecyclerViewState() { recyclerViewState = recyclerView.layoutManager?.onSaveInstanceState() } private fun restoreRecyclerViewState() { recyclerView.layoutManager?.onRestoreInstanceState(recyclerViewState) } private class ConversationDiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: Conversation, newItem: Conversation): Boolean { return Conversation.areItemsTheSame(oldItem, newItem) } override fun areContentsTheSame(oldItem: Conversation, newItem: Conversation): Boolean { return Conversation.areContentsTheSame(oldItem, newItem) } } }