Merge pull request #698 from esensar/feature/177-conversations-archive

Implement archive feature for conversations using system API
This commit is contained in:
Tibor Kaputa 2023-07-19 16:40:16 +02:00 committed by GitHub
commit 0c5242df2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 1241 additions and 196 deletions

View file

@ -0,0 +1,160 @@
package com.simplemobiletools.smsmessenger.activities
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.adapters.ArchivedConversationsAdapter
import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.helpers.*
import com.simplemobiletools.smsmessenger.models.Conversation
import com.simplemobiletools.smsmessenger.models.Events
import kotlinx.android.synthetic.main.activity_archived_conversations.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
class ArchivedConversationsActivity : SimpleActivity() {
private var bus: EventBus? = null
@SuppressLint("InlinedApi")
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_archived_conversations)
setupOptionsMenu()
updateMaterialActivityViews(archive_coordinator, conversations_list, useTransparentNavigation = true, useTopSearchMenu = false)
setupMaterialScrollListener(conversations_list, archive_toolbar)
loadArchivedConversations()
}
override fun onResume() {
super.onResume()
setupToolbar(archive_toolbar, NavigationIcon.Arrow)
updateMenuColors()
loadArchivedConversations()
}
override fun onDestroy() {
super.onDestroy()
bus?.unregister(this)
}
private fun setupOptionsMenu() {
archive_toolbar.inflateMenu(R.menu.archive_menu)
archive_toolbar.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.empty_archive -> removeAll()
else -> return@setOnMenuItemClickListener false
}
return@setOnMenuItemClickListener true
}
}
private fun updateOptionsMenu(conversations: ArrayList<Conversation>) {
archive_toolbar.menu.apply {
findItem(R.id.empty_archive).isVisible = conversations.isNotEmpty()
}
}
private fun updateMenuColors() {
updateStatusbarColor(getProperBackgroundColor())
}
private fun loadArchivedConversations() {
ensureBackgroundThread {
val conversations = try {
conversationsDB.getAllArchived().toMutableList() as ArrayList<Conversation>
} catch (e: Exception) {
ArrayList()
}
runOnUiThread {
setupConversations(conversations)
}
}
bus = EventBus.getDefault()
try {
bus!!.register(this)
} catch (e: Exception) {
}
}
private fun removeAll() {
ConfirmationDialog(this, "", R.string.empty_archive_confirmation, R.string.yes, R.string.no) {
removeAllArchivedConversations {
loadArchivedConversations()
}
}
}
private fun getOrCreateConversationsAdapter(): ArchivedConversationsAdapter {
var currAdapter = conversations_list.adapter
if (currAdapter == null) {
hideKeyboard()
currAdapter = ArchivedConversationsAdapter(
activity = this,
recyclerView = conversations_list,
onRefresh = { notifyDatasetChanged() },
itemClick = { handleConversationClick(it) }
)
conversations_list.adapter = currAdapter
if (areSystemAnimationsEnabled) {
conversations_list.scheduleLayoutAnimation()
}
}
return currAdapter as ArchivedConversationsAdapter
}
private fun setupConversations(conversations: ArrayList<Conversation>) {
val sortedConversations = conversations.sortedWith(
compareByDescending<Conversation> { config.pinnedConversations.contains(it.threadId.toString()) }
.thenByDescending { it.date }
).toMutableList() as ArrayList<Conversation>
showOrHidePlaceholder(conversations.isEmpty())
updateOptionsMenu(conversations)
try {
getOrCreateConversationsAdapter().apply {
updateConversations(sortedConversations)
}
} catch (ignored: Exception) {
}
}
private fun showOrHidePlaceholder(show: Boolean) {
conversations_fastscroller.beGoneIf(show)
no_conversations_placeholder.beVisibleIf(show)
no_conversations_placeholder.text = getString(R.string.no_archived_conversations)
}
@SuppressLint("NotifyDataSetChanged")
private fun notifyDatasetChanged() {
getOrCreateConversationsAdapter().notifyDataSetChanged()
}
private fun handleConversationClick(any: Any) {
Intent(this, ThreadActivity::class.java).apply {
val conversation = any as Conversation
putExtra(THREAD_ID, conversation.threadId)
putExtra(THREAD_TITLE, conversation.title)
putExtra(WAS_PROTECTION_HANDLED, true)
startActivity(this)
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun refreshMessages(event: Events.RefreshMessages) {
loadArchivedConversations()
}
}

View file

@ -177,6 +177,7 @@ class MainActivity : SimpleActivity() {
R.id.import_messages -> tryImportMessages()
R.id.export_messages -> tryToExportMessages()
R.id.more_apps_from_us -> launchMoreAppsFromUsIntent()
R.id.show_archived -> launchArchivedConversations()
R.id.settings -> launchSettings()
R.id.about -> launchAbout()
else -> return@setOnMenuItemClickListener false
@ -292,15 +293,21 @@ class MainActivity : SimpleActivity() {
private fun getCachedConversations() {
ensureBackgroundThread {
val conversations = try {
conversationsDB.getAll().toMutableList() as ArrayList<Conversation>
conversationsDB.getNonArchived().toMutableList() as ArrayList<Conversation>
} catch (e: Exception) {
ArrayList()
}
val archived = try {
conversationsDB.getAllArchived()
} catch (e: Exception) {
listOf()
}
updateUnreadCountBadge(conversations)
runOnUiThread {
setupConversations(conversations, cached = true)
getNewConversations(conversations)
getNewConversations((conversations + archived).toMutableList() as ArrayList<Conversation>)
}
conversations.forEach {
clearExpiredScheduledMessages(it.threadId)
@ -354,7 +361,7 @@ class MainActivity : SimpleActivity() {
}
}
val allConversations = conversationsDB.getAll() as ArrayList<Conversation>
val allConversations = conversationsDB.getNonArchived() as ArrayList<Conversation>
runOnUiThread {
setupConversations(allConversations)
}
@ -559,6 +566,11 @@ class MainActivity : SimpleActivity() {
}
}
private fun launchArchivedConversations() {
hideKeyboard()
startActivity(Intent(applicationContext, ArchivedConversationsActivity::class.java))
}
private fun launchSettings() {
hideKeyboard()
startActivity(Intent(applicationContext, SettingsActivity::class.java))

View file

@ -250,6 +250,8 @@ class ThreadActivity : SimpleActivity() {
val firstPhoneNumber = participants.firstOrNull()?.phoneNumbers?.firstOrNull()?.value
thread_toolbar.menu.apply {
findItem(R.id.delete).isVisible = threadItems.isNotEmpty()
findItem(R.id.archive).isVisible = threadItems.isNotEmpty() && conversation?.isArchived == false
findItem(R.id.unarchive).isVisible = threadItems.isNotEmpty() && conversation?.isArchived == true
findItem(R.id.rename_conversation).isVisible = participants.size > 1 && conversation != null
findItem(R.id.conversation_details).isVisible = conversation != null
findItem(R.id.block_number).title = addLockedLabelIfNeeded(R.string.block_number)
@ -274,6 +276,8 @@ class ThreadActivity : SimpleActivity() {
when (menuItem.itemId) {
R.id.block_number -> tryBlocking()
R.id.delete -> askConfirmDelete()
R.id.archive -> archiveConversation()
R.id.unarchive -> unarchiveConversation()
R.id.rename_conversation -> renameConversation()
R.id.conversation_details -> showConversationDetails()
R.id.add_number_to_contact -> addNumberToContact()
@ -913,7 +917,8 @@ class ThreadActivity : SimpleActivity() {
}
private fun askConfirmDelete() {
ConfirmationDialog(this, getString(R.string.delete_whole_conversation_confirmation)) {
val confirmationMessage = R.string.delete_whole_conversation_confirmation
ConfirmationDialog(this, getString(confirmationMessage)) {
ensureBackgroundThread {
deleteConversation(threadId)
runOnUiThread {
@ -924,6 +929,26 @@ class ThreadActivity : SimpleActivity() {
}
}
private fun archiveConversation() {
ensureBackgroundThread {
updateConversationArchivedStatus(threadId, true)
runOnUiThread {
refreshMessages()
finish()
}
}
}
private fun unarchiveConversation() {
ensureBackgroundThread {
updateConversationArchivedStatus(threadId, false)
runOnUiThread {
refreshMessages()
finish()
}
}
}
private fun dialNumber() {
val phoneNumber = participants.first().phoneNumbers.first().normalizedNumber
dialNumber(phoneNumber)
@ -1352,6 +1377,7 @@ class ThreadActivity : SimpleActivity() {
}
}
messagesDB.insertOrUpdate(message)
updateConversationArchivedStatus(message.threadId, false)
}
// show selected contacts, properly split to new lines when appropriate

View file

@ -0,0 +1,96 @@
package com.simplemobiletools.smsmessenger.adapters
import android.view.Menu
import com.simplemobiletools.commons.dialogs.ConfirmationDialog
import com.simplemobiletools.commons.extensions.notificationManager
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.activities.SimpleActivity
import com.simplemobiletools.smsmessenger.extensions.deleteConversation
import com.simplemobiletools.smsmessenger.extensions.updateConversationArchivedStatus
import com.simplemobiletools.smsmessenger.helpers.refreshMessages
import com.simplemobiletools.smsmessenger.models.Conversation
class ArchivedConversationsAdapter(
activity: SimpleActivity, recyclerView: MyRecyclerView, onRefresh: () -> Unit, itemClick: (Any) -> Unit
) : BaseConversationsAdapter(activity, recyclerView, onRefresh, itemClick) {
override fun getActionMenuId() = R.menu.cab_archived_conversations
override fun prepareActionMode(menu: Menu) {}
override fun actionItemPressed(id: Int) {
if (selectedKeys.isEmpty()) {
return
}
when (id) {
R.id.cab_delete -> askConfirmDelete()
R.id.cab_unarchive -> unarchiveConversation()
R.id.cab_select_all -> selectAll()
}
}
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.threadId.hashCode())
}
removeConversationsFromList(conversationsToRemove)
}
private fun unarchiveConversation() {
if (selectedKeys.isEmpty()) {
return
}
ensureBackgroundThread {
val conversationsToUnarchive = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
conversationsToUnarchive.forEach {
activity.updateConversationArchivedStatus(it.threadId, false)
}
removeConversationsFromList(conversationsToUnarchive)
}
}
private fun removeConversationsFromList(removedConversations: List<Conversation>) {
val newList = try {
currentList.toMutableList().apply { removeAll(removedConversations) }
} catch (ignored: Exception) {
currentList.toMutableList()
}
activity.runOnUiThread {
if (newList.none { selectedKeys.contains(it.hashCode()) }) {
refreshMessages()
finishActMode()
} else {
submitList(newList)
if (newList.isEmpty()) {
refreshMessages()
}
}
}
}
}

View file

@ -0,0 +1,183 @@
package com.simplemobiletools.smsmessenger.adapters
import android.graphics.Typeface
import android.os.Parcelable
import android.util.TypedValue
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.extensions.*
import com.simplemobiletools.commons.helpers.SimpleContactsHelper
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.activities.SimpleActivity
import com.simplemobiletools.smsmessenger.extensions.*
import com.simplemobiletools.smsmessenger.models.Conversation
import kotlinx.android.synthetic.main.item_conversation.view.*
@Suppress("LeakingThis")
abstract class BaseConversationsAdapter(
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()
})
}
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()
}
}
}
}
override fun getSelectableItemCount() = itemCount
protected fun getSelectedItems() = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
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 fetchDrafts(drafts: HashMap<Long, String?>) {
drafts.clear()
for ((threadId, draft) in activity.getAllDrafts()) {
drafts[threadId] = draft
}
}
private fun setupView(view: View, conversation: Conversation) {
view.apply {
setupViewBackground(activity)
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 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)
}
}
}

View file

@ -1,24 +1,12 @@
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
@ -29,31 +17,10 @@ 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()
})
}
) : BaseConversationsAdapter(activity, recyclerView, onRefresh, itemClick) {
override fun getActionMenuId() = R.menu.cab_conversations
override fun prepareActionMode(menu: Menu) {
@ -86,6 +53,7 @@ class ConversationsAdapter(
R.id.cab_dial_number -> dialNumber()
R.id.cab_copy_number -> copyNumberToClipboard()
R.id.cab_delete -> askConfirmDelete()
R.id.cab_archive -> askConfirmArchive()
R.id.cab_rename_conversation -> renameConversation(getSelectedItems().first())
R.id.cab_mark_as_read -> markAsRead()
R.id.cab_mark_as_unread -> markAsUnread()
@ -95,37 +63,6 @@ class ConversationsAdapter(
}
}
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()
@ -191,6 +128,50 @@ class ConversationsAdapter(
}
}
private fun askConfirmArchive() {
val itemsCnt = selectedKeys.size
val items = resources.getQuantityString(R.plurals.delete_conversations, itemsCnt, itemsCnt)
val baseString = R.string.archive_confirmation
val question = String.format(resources.getString(baseString), items)
ConfirmationDialog(activity, question) {
ensureBackgroundThread {
archiveConversations()
}
}
}
private fun archiveConversations() {
if (selectedKeys.isEmpty()) {
return
}
val conversationsToRemove = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
conversationsToRemove.forEach {
activity.updateConversationArchivedStatus(it.threadId, true)
activity.notificationManager.cancel(it.threadId.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 deleteConversations() {
if (selectedKeys.isEmpty()) {
return
@ -276,8 +257,6 @@ class ConversationsAdapter(
}
}
private fun getSelectedItems() = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList<Conversation>
private fun pinConversation(pin: Boolean) {
val conversations = getSelectedItems()
if (conversations.isEmpty()) {
@ -303,113 +282,10 @@ class ConversationsAdapter(
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 {
setupViewBackground(activity)
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)
}
}
}

View file

@ -12,12 +12,9 @@ import com.simplemobiletools.smsmessenger.interfaces.AttachmentsDao
import com.simplemobiletools.smsmessenger.interfaces.ConversationsDao
import com.simplemobiletools.smsmessenger.interfaces.MessageAttachmentsDao
import com.simplemobiletools.smsmessenger.interfaces.MessagesDao
import com.simplemobiletools.smsmessenger.models.Attachment
import com.simplemobiletools.smsmessenger.models.Conversation
import com.simplemobiletools.smsmessenger.models.Message
import com.simplemobiletools.smsmessenger.models.MessageAttachment
import com.simplemobiletools.smsmessenger.models.*
@Database(entities = [Conversation::class, Attachment::class, MessageAttachment::class, Message::class], version = 7)
@Database(entities = [Conversation::class, Attachment::class, MessageAttachment::class, Message::class], version = 8)
@TypeConverters(Converters::class)
abstract class MessagesDatabase : RoomDatabase() {
@ -44,6 +41,7 @@ abstract class MessagesDatabase : RoomDatabase() {
.addMigrations(MIGRATION_4_5)
.addMigrations(MIGRATION_5_6)
.addMigrations(MIGRATION_6_7)
.addMigrations(MIGRATION_7_8)
.build()
}
}
@ -115,5 +113,13 @@ abstract class MessagesDatabase : RoomDatabase() {
}
}
}
private val MIGRATION_7_8 = object : Migration(7, 8) {
override fun migrate(database: SupportSQLiteDatabase) {
database.apply {
execSQL("ALTER TABLE conversations ADD COLUMN archived INTEGER NOT NULL DEFAULT 0")
}
}
}
}
}

View file

@ -268,7 +268,8 @@ fun Context.getConversations(threadId: Long? = null, privateContacts: ArrayList<
Threads.SNIPPET,
Threads.DATE,
Threads.READ,
Threads.RECIPIENT_IDS
Threads.RECIPIENT_IDS,
Threads.ARCHIVED
)
var selection = "${Threads.MESSAGE_COUNT} > ?"
@ -307,7 +308,8 @@ fun Context.getConversations(threadId: Long? = null, privateContacts: ArrayList<
val photoUri = if (phoneNumbers.size == 1) simpleContactHelper.getPhotoUriFromPhoneNumber(phoneNumbers.first()) else ""
val isGroupConversation = phoneNumbers.size > 1
val read = cursor.getIntValue(Threads.READ) == 1
val conversation = Conversation(id, snippet, date.toInt(), read, title, photoUri, isGroupConversation, phoneNumbers.first())
val archived = cursor.getIntValue(Threads.ARCHIVED) == 1
val conversation = Conversation(id, snippet, date.toInt(), read, title, photoUri, isGroupConversation, phoneNumbers.first(), isArchived = archived)
conversations.add(conversation)
}
@ -620,6 +622,20 @@ fun Context.insertNewSMS(
}
}
fun Context.removeAllArchivedConversations(callback: (() -> Unit)? = null) {
ensureBackgroundThread {
try {
for (conversation in conversationsDB.getAllArchived()) {
deleteConversation(conversation.threadId)
}
toast(R.string.archive_emptied_successfully)
callback?.invoke()
} catch (e: Exception) {
toast(R.string.unknown_error_occurred)
}
}
}
fun Context.deleteConversation(threadId: Long) {
var uri = Sms.CONTENT_URI
val selection = "${Sms.THREAD_ID} = ?"
@ -641,6 +657,21 @@ fun Context.deleteConversation(threadId: Long) {
messagesDB.deleteThreadMessages(threadId)
}
fun Context.updateConversationArchivedStatus(threadId: Long, archived: Boolean) {
val uri = Threads.CONTENT_URI
val values = ContentValues().apply {
put(Threads.ARCHIVED, archived)
}
val selection = "${Threads._ID} = ?"
val selectionArgs = arrayOf(threadId.toString())
contentResolver.update(uri, values, selection, selectionArgs)
if (archived) {
conversationsDB.moveToArchive(threadId)
} else {
conversationsDB.unarchive(threadId)
}
}
fun Context.deleteMessage(id: Long, isMMS: Boolean) {
val uri = if (isMMS) Mms.CONTENT_URI else Sms.CONTENT_URI
val selection = "${Sms._ID} = ?"
@ -969,7 +1000,8 @@ fun Context.createTemporaryThread(message: Message, threadId: Long = generateRan
isGroupConversation = addresses.size > 1,
phoneNumber = addresses.first(),
isScheduled = true,
usesCustomTitle = cachedConv?.usesCustomTitle == true
usesCustomTitle = cachedConv?.usesCustomTitle == true,
isArchived = false
)
try {
conversationsDB.insertOrUpdate(conversation)

View file

@ -103,4 +103,12 @@ class Config(context: Context) : BaseConfig(context) {
var keyboardHeight: Int
get() = prefs.getInt(SOFT_KEYBOARD_HEIGHT, context.getDefaultKeyboardHeight())
set(keyboardHeight) = prefs.edit().putInt(SOFT_KEYBOARD_HEIGHT, keyboardHeight).apply()
var useArchive: Boolean
get() = prefs.getBoolean(USE_ARCHIVE, false)
set(useArchive) = prefs.edit().putBoolean(USE_ARCHIVE, useArchive).apply()
var lastArchiveCheck: Long
get() = prefs.getLong(LAST_ARCHIVE_CHECK, 0L)
set(lastArchiveCheck) = prefs.edit().putLong(LAST_ARCHIVE_CHECK, lastArchiveCheck).apply()
}

View file

@ -40,6 +40,8 @@ const val SCHEDULED_MESSAGE_ID = "scheduled_message_id"
const val SOFT_KEYBOARD_HEIGHT = "soft_keyboard_height"
const val IS_MMS = "is_mms"
const val MESSAGE_ID = "message_id"
const val USE_ARCHIVE = "use_archive"
const val LAST_ARCHIVE_CHECK = "last_archive_check"
private const val PATH = "com.simplemobiletools.smsmessenger.action."
const val MARK_AS_READ = PATH + "mark_as_read"

View file

@ -1,9 +1,6 @@
package com.simplemobiletools.smsmessenger.interfaces
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.*
import com.simplemobiletools.smsmessenger.models.Conversation
@Dao
@ -11,8 +8,11 @@ interface ConversationsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrUpdate(conversation: Conversation): Long
@Query("SELECT * FROM conversations")
fun getAll(): List<Conversation>
@Query("SELECT * FROM conversations WHERE archived = 0")
fun getNonArchived(): List<Conversation>
@Query("SELECT * FROM conversations WHERE archived = 1")
fun getAllArchived(): List<Conversation>
@Query("SELECT * FROM conversations WHERE thread_id = :threadId")
fun getConversationWithThreadId(threadId: Long): Conversation?
@ -29,6 +29,12 @@ interface ConversationsDao {
@Query("UPDATE conversations SET read = 0 WHERE thread_id = :threadId")
fun markUnread(threadId: Long)
@Query("UPDATE conversations SET archived = 1 WHERE thread_id = :threadId")
fun moveToArchive(threadId: Long)
@Query("UPDATE conversations SET archived = 0 WHERE thread_id = :threadId")
fun unarchive(threadId: Long)
@Query("DELETE FROM conversations WHERE thread_id = :threadId")
fun deleteThreadId(threadId: Long)
}

View file

@ -0,0 +1,15 @@
package com.simplemobiletools.smsmessenger.models
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
tableName = "archived_conversations",
indices = [(Index(value = ["thread_id"], unique = true))]
)
data class ArchivedConversation(
@PrimaryKey @ColumnInfo(name = "thread_id") var threadId: Long,
@ColumnInfo(name = "deleted_ts") var deletedTs: Long
)

View file

@ -16,7 +16,8 @@ data class Conversation(
@ColumnInfo(name = "is_group_conversation") var isGroupConversation: Boolean,
@ColumnInfo(name = "phone_number") var phoneNumber: String,
@ColumnInfo(name = "is_scheduled") var isScheduled: Boolean = false,
@ColumnInfo(name = "uses_custom_title") var usesCustomTitle: Boolean = false
@ColumnInfo(name = "uses_custom_title") var usesCustomTitle: Boolean = false,
@ColumnInfo(name = "archived") var isArchived: Boolean = false
) {
companion object {

View file

@ -113,6 +113,7 @@ class SmsReceiver : BroadcastReceiver() {
subscriptionId
)
context.messagesDB.insertOrUpdate(message)
context.updateConversationArchivedStatus(threadId, false)
refreshMessages()
context.showReceivedMessageNotification(newMessageId, address, body, threadId, bitmap)
}