sms-translate/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt

414 lines
15 KiB
Kotlin

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<Conversation>(activity, recyclerView, ConversationDiffCallback(), itemClick, onRefresh),
RecyclerViewFastScroller.OnPopupTextUpdate {
private var fontSize = activity.getTextSize()
private var drafts = HashMap<Long, String?>()
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<Conversation>
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<Conversation>)
}
}
}
}
}
private fun markAsRead() {
if (selectedKeys.isEmpty()) {
return
}
val conversationsMarkedAsRead = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
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<Conversation>
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<Conversation>
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<Long, String?>) {
drafts.clear()
for ((threadId, draft) in activity.getAllDrafts()) {
drafts[threadId] = draft
}
}
fun updateFontSize() {
fontSize = activity.getTextSize()
notifyDataSetChanged()
}
fun updateConversations(newConversations: ArrayList<Conversation>, commitCallback: (() -> Unit)? = null) {
saveRecyclerViewState()
submitList(newConversations.toList(), commitCallback)
}
fun updateDrafts() {
ensureBackgroundThread {
val newDrafts = HashMap<Long, String?>()
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<TextView>(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<Conversation>() {
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)
}
}
}