414 lines
15 KiB
Kotlin
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)
|
|
}
|
|
}
|
|
}
|