diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3820008d..c94576e3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -51,6 +51,13 @@
android:configChanges="orientation"
android:exported="true" />
+
+
+ when (menuItem.itemId) {
+ R.id.empty_archive -> removeAll()
+ else -> return@setOnMenuItemClickListener false
+ }
+ return@setOnMenuItemClickListener true
+ }
+ }
+
+ private fun updateMenuColors() {
+ updateStatusbarColor(getProperBackgroundColor())
+ }
+
+ private fun loadArchivedConversations() {
+ ensureBackgroundThread {
+ val conversations = try {
+ conversationsDB.getAllArchived().toMutableList() as ArrayList
+ } catch (e: Exception) {
+ ArrayList()
+ }
+
+ runOnUiThread {
+ setupConversations(conversations)
+ }
+ }
+
+ bus = EventBus.getDefault()
+ try {
+ bus!!.register(this)
+ } catch (e: Exception) {
+ }
+ }
+
+ private fun removeAll() {
+ 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) {
+ val sortedConversations = conversations.sortedWith(
+ compareByDescending { config.pinnedConversations.contains(it.threadId.toString()) }
+ .thenByDescending { it.date }
+ ).toMutableList() as ArrayList
+
+ showOrHidePlaceholder(conversations.isEmpty())
+
+ 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_conversations_found)
+ }
+
+ @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()
+ }
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt
index 1204d748..119b357b 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt
@@ -64,6 +64,7 @@ class MainActivity : SimpleActivity() {
updateMaterialActivityViews(main_coordinator, conversations_list, useTransparentNavigation = true, useTopSearchMenu = true)
if (savedInstanceState == null) {
+ checkAndDeleteOldArchivedConversations()
handleAppPasswordProtection {
wasProtectionHandled = it
if (it) {
@@ -177,6 +178,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
@@ -289,15 +291,20 @@ class MainActivity : SimpleActivity() {
private fun getCachedConversations() {
ensureBackgroundThread {
val conversations = try {
- conversationsDB.getAll().toMutableList() as ArrayList
+ conversationsDB.getNonArchived().toMutableList() as ArrayList
} 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)
}
conversations.forEach {
clearExpiredScheduledMessages(it.threadId)
@@ -351,7 +358,7 @@ class MainActivity : SimpleActivity() {
}
}
- val allConversations = conversationsDB.getAll() as ArrayList
+ val allConversations = conversationsDB.getNonArchived() as ArrayList
runOnUiThread {
setupConversations(allConversations)
}
@@ -556,6 +563,11 @@ class MainActivity : SimpleActivity() {
}
}
+ private fun launchArchivedConversations() {
+ hideKeyboard()
+ startActivity(Intent(applicationContext, ArchivedConversationsActivity::class.java))
+ }
+
private fun launchSettings() {
hideKeyboard()
startActivity(Intent(applicationContext, SettingsActivity::class.java))
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/SettingsActivity.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/SettingsActivity.kt
index d97e67d0..5cdd53a6 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/SettingsActivity.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/SettingsActivity.kt
@@ -11,12 +11,15 @@ import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.RadioItem
import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.extensions.config
+import com.simplemobiletools.smsmessenger.extensions.conversationsDB
+import com.simplemobiletools.smsmessenger.extensions.removeAllArchivedConversations
import com.simplemobiletools.smsmessenger.helpers.*
import kotlinx.android.synthetic.main.activity_settings.*
import java.util.*
class SettingsActivity : SimpleActivity() {
private var blockedNumbersAtPause = -1
+ private var recycleBinConversations = 0
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
@@ -47,6 +50,8 @@ class SettingsActivity : SimpleActivity() {
setupGroupMessageAsMMS()
setupLockScreenVisibility()
setupMMSFileSizeLimit()
+ setupUseRecycleBin()
+ setupEmptyRecycleBin()
setupAppPasswordProtection()
updateTextColors(settings_nested_scrollview)
@@ -59,6 +64,7 @@ class SettingsActivity : SimpleActivity() {
settings_general_settings_label,
settings_outgoing_messages_label,
settings_notifications_label,
+ settings_recycle_bin_label,
settings_security_label
).forEach {
it.setTextColor(getProperPrimaryColor())
@@ -244,6 +250,43 @@ class SettingsActivity : SimpleActivity() {
}
}
+ private fun setupUseRecycleBin() {
+ updateRecycleBinButtons()
+ settings_use_recycle_bin.isChecked = config.useArchive
+ settings_use_recycle_bin_holder.setOnClickListener {
+ settings_use_recycle_bin.toggle()
+ config.useArchive = settings_use_recycle_bin.isChecked
+ updateRecycleBinButtons()
+ }
+ }
+
+ private fun updateRecycleBinButtons() {
+ settings_empty_recycle_bin_holder.beVisibleIf(config.useArchive)
+ }
+
+ private fun setupEmptyRecycleBin() {
+ ensureBackgroundThread {
+ recycleBinConversations = conversationsDB.getArchivedCount()
+ runOnUiThread {
+ settings_empty_recycle_bin_size.text =
+ resources.getQuantityString(R.plurals.delete_conversations, recycleBinConversations, recycleBinConversations)
+ }
+ }
+
+ settings_empty_recycle_bin_holder.setOnClickListener {
+ if (recycleBinConversations == 0) {
+ toast(R.string.recycle_bin_empty)
+ } else {
+ ConfirmationDialog(this, "", R.string.empty_recycle_bin_confirmation, R.string.yes, R.string.no) {
+ removeAllArchivedConversations()
+ recycleBinConversations = 0
+ settings_empty_recycle_bin_size.text =
+ resources.getQuantityString(R.plurals.delete_conversations, recycleBinConversations, recycleBinConversations)
+ }
+ }
+ }
+ }
+
private fun setupAppPasswordProtection() {
settings_app_password_protection.isChecked = config.isAppPasswordProtectionOn
settings_app_password_protection_holder.setOnClickListener {
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt
index 62e03a05..e524f349 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt
@@ -55,6 +55,7 @@ import com.simplemobiletools.smsmessenger.R
import com.simplemobiletools.smsmessenger.adapters.AttachmentsAdapter
import com.simplemobiletools.smsmessenger.adapters.AutoCompleteTextViewAdapter
import com.simplemobiletools.smsmessenger.adapters.ThreadAdapter
+import com.simplemobiletools.smsmessenger.dialogs.DeleteConfirmationDialog
import com.simplemobiletools.smsmessenger.dialogs.InvalidNumberDialog
import com.simplemobiletools.smsmessenger.dialogs.RenameConversationDialog
import com.simplemobiletools.smsmessenger.dialogs.ScheduleMessageDialog
@@ -888,9 +889,13 @@ class ThreadActivity : SimpleActivity() {
}
private fun askConfirmDelete() {
- ConfirmationDialog(this, getString(R.string.delete_whole_conversation_confirmation)) {
+ DeleteConfirmationDialog(this, getString(R.string.delete_whole_conversation_confirmation), config.useArchive) { skipRecycleBin ->
ensureBackgroundThread {
- deleteConversation(threadId)
+ if (skipRecycleBin || config.useArchive.not()) {
+ deleteConversation(threadId)
+ } else {
+ moveConversationToRecycleBin(threadId)
+ }
runOnUiThread {
refreshMessages()
finish()
@@ -1327,6 +1332,7 @@ class ThreadActivity : SimpleActivity() {
}
}
messagesDB.insertOrUpdate(message)
+ conversationsDB.deleteThreadFromArchivedConversations(message.threadId)
}
// show selected contacts, properly split to new lines when appropriate
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ArchivedConversationsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ArchivedConversationsAdapter.kt
new file mode 100644
index 00000000..b1050197
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ArchivedConversationsAdapter.kt
@@ -0,0 +1,94 @@
+package com.simplemobiletools.smsmessenger.adapters
+
+import android.view.Menu
+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.dialogs.DeleteConfirmationDialog
+import com.simplemobiletools.smsmessenger.extensions.conversationsDB
+import com.simplemobiletools.smsmessenger.extensions.deleteConversation
+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 -> ensureBackgroundThread { 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)
+
+ DeleteConfirmationDialog(activity, question, showSkipRecycleBinOption = false) { _ ->
+ 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.threadId.hashCode())
+ }
+
+ removeConversationsFromList(conversationsToRemove)
+ }
+
+ private fun unarchiveConversation() {
+ if (selectedKeys.isEmpty()) {
+ return
+ }
+
+ val conversationsToUnarchive = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList
+ conversationsToUnarchive.forEach {
+ activity.conversationsDB.deleteThreadFromArchivedConversations(it.threadId)
+ }
+
+ removeConversationsFromList(conversationsToUnarchive)
+ }
+
+ private fun removeConversationsFromList(removedConversations: List) {
+ 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()
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/BaseConversationsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/BaseConversationsAdapter.kt
new file mode 100644
index 00000000..17ba7623
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/BaseConversationsAdapter.kt
@@ -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(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()
+ })
+ }
+
+ 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()
+ }
+ }
+ }
+ }
+
+ override fun getSelectableItemCount() = itemCount
+
+ protected fun getSelectedItems() = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList
+
+ 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) {
+ 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(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() {
+ 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)
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt
index 4ba2f9e4..1b79472b 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ConversationsAdapter.kt
@@ -1,59 +1,27 @@
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.DeleteConfirmationDialog
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()
- })
- }
-
+) : BaseConversationsAdapter(activity, recyclerView, onRefresh, itemClick) {
override fun getActionMenuId() = R.menu.cab_conversations
override fun prepareActionMode(menu: Menu) {
@@ -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()
@@ -184,21 +121,25 @@ class ConversationsAdapter(
val baseString = R.string.deletion_confirmation
val question = String.format(resources.getString(baseString), items)
- ConfirmationDialog(activity, question) {
+ DeleteConfirmationDialog(activity, question, activity.config.useArchive) { skipRecycleBin ->
ensureBackgroundThread {
- deleteConversations()
+ deleteConversations(skipRecycleBin)
}
}
}
- private fun deleteConversations() {
+ private fun deleteConversations(skipRecycleBin: Boolean) {
if (selectedKeys.isEmpty()) {
return
}
val conversationsToRemove = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList
conversationsToRemove.forEach {
- activity.deleteConversation(it.threadId)
+ if (skipRecycleBin || activity.config.useArchive.not()) {
+ activity.deleteConversation(it.threadId)
+ } else {
+ activity.moveConversationToRecycleBin(it.threadId)
+ }
activity.notificationManager.cancel(it.threadId.hashCode())
}
@@ -276,8 +217,6 @@ class ConversationsAdapter(
}
}
- private fun getSelectedItems() = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList
-
private fun pinConversation(pin: Boolean) {
val conversations = getSelectedItems()
if (conversations.isEmpty()) {
@@ -303,113 +242,10 @@ class ConversationsAdapter(
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 {
- 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(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)
- }
- }
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/databases/MessagesDatabase.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/databases/MessagesDatabase.kt
index fca0754b..db698255 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/databases/MessagesDatabase.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/databases/MessagesDatabase.kt
@@ -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, ArchivedConversation::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("CREATE TABLE archived_conversations (`thread_id` INTEGER NOT NULL PRIMARY KEY, `deleted_ts` INTEGER NOT NULL)")
+ }
+ }
+ }
}
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/dialogs/DeleteConfirmationDialog.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/dialogs/DeleteConfirmationDialog.kt
new file mode 100644
index 00000000..f72a7513
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/dialogs/DeleteConfirmationDialog.kt
@@ -0,0 +1,39 @@
+package com.simplemobiletools.smsmessenger.dialogs
+
+import android.app.Activity
+import androidx.appcompat.app.AlertDialog
+import com.simplemobiletools.commons.extensions.beGoneIf
+import com.simplemobiletools.commons.extensions.getAlertDialogBuilder
+import com.simplemobiletools.commons.extensions.setupDialogStuff
+import com.simplemobiletools.smsmessenger.R
+import kotlinx.android.synthetic.main.dialog_delete_confirmation.view.delete_remember_title
+import kotlinx.android.synthetic.main.dialog_delete_confirmation.view.skip_the_recycle_bin_checkbox
+
+class DeleteConfirmationDialog(
+ private val activity: Activity,
+ private val message: String,
+ private val showSkipRecycleBinOption: Boolean,
+ private val callback: (skipRecycleBin: Boolean) -> Unit
+) {
+
+ private var dialog: AlertDialog? = null
+ val view = activity.layoutInflater.inflate(R.layout.dialog_delete_confirmation, null)!!
+
+ init {
+ view.delete_remember_title.text = message
+ view.skip_the_recycle_bin_checkbox.beGoneIf(!showSkipRecycleBinOption)
+ activity.getAlertDialogBuilder()
+ .setPositiveButton(R.string.yes) { _, _ -> dialogConfirmed() }
+ .setNegativeButton(R.string.no, null)
+ .apply {
+ activity.setupDialogStuff(view, this) { alertDialog ->
+ dialog = alertDialog
+ }
+ }
+ }
+
+ private fun dialogConfirmed() {
+ dialog?.dismiss()
+ callback(view.skip_the_recycle_bin_checkbox.isChecked)
+ }
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt
index c9619701..f9228f8e 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt
@@ -581,6 +581,35 @@ fun Context.insertNewSMS(address: String, subject: String, body: String, date: L
}
}
+fun Context.checkAndDeleteOldArchivedConversations(callback: (() -> Unit)? = null) {
+ if (config.useArchive && config.lastArchiveCheck < System.currentTimeMillis() - DAY_SECONDS * 1000) {
+ config.lastArchiveCheck = System.currentTimeMillis()
+ ensureBackgroundThread {
+ try {
+ for (conversation in conversationsDB.getOldArchived(System.currentTimeMillis() - MONTH_SECONDS * 1000L)) {
+ deleteConversation(conversation.threadId)
+ }
+ callback?.invoke()
+ } catch (e: Exception) {
+ }
+ }
+ }
+}
+
+fun Context.removeAllArchivedConversations(callback: (() -> Unit)? = null) {
+ ensureBackgroundThread {
+ try {
+ for (conversation in conversationsDB.getAllArchived()) {
+ deleteConversation(conversation.threadId)
+ }
+ toast(R.string.recycle_bin_emptied)
+ 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} = ?"
@@ -602,6 +631,15 @@ fun Context.deleteConversation(threadId: Long) {
messagesDB.deleteThreadMessages(threadId)
}
+fun Context.moveConversationToRecycleBin(threadId: Long) {
+ conversationsDB.archiveConversation(
+ ArchivedConversation(
+ threadId = threadId,
+ deletedTs = System.currentTimeMillis()
+ )
+ )
+}
+
fun Context.deleteMessage(id: Long, isMMS: Boolean) {
val uri = if (isMMS) Mms.CONTENT_URI else Sms.CONTENT_URI
val selection = "${Sms._ID} = ?"
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Config.kt
index b9b5c85b..a08c1466 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Config.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Config.kt
@@ -91,4 +91,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, true)
+ 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()
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt
index 4db9a2ad..dcc3e3f7 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt
@@ -37,6 +37,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_recycle_bin"
+const val LAST_ARCHIVE_CHECK = "last_bin_check"
private const val PATH = "com.simplemobiletools.smsmessenger.action."
const val MARK_AS_READ = PATH + "mark_as_read"
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/ConversationsDao.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/ConversationsDao.kt
index 0c8db25c..7c321f82 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/ConversationsDao.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/ConversationsDao.kt
@@ -1,9 +1,7 @@
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.ArchivedConversation
import com.simplemobiletools.smsmessenger.models.Conversation
@Dao
@@ -11,8 +9,20 @@ interface ConversationsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrUpdate(conversation: Conversation): Long
- @Query("SELECT * FROM conversations")
- fun getAll(): List
+ @Query("SELECT conversations.* FROM conversations LEFT OUTER JOIN archived_conversations ON conversations.thread_id = archived_conversations.thread_id WHERE archived_conversations.deleted_ts is NULL")
+ fun getNonArchived(): List
+
+ @Query("SELECT conversations.* FROM archived_conversations INNER JOIN conversations ON conversations.thread_id = archived_conversations.thread_id")
+ fun getAllArchived(): List
+
+ @Query("SELECT COUNT(*) FROM archived_conversations")
+ fun getArchivedCount(): Int
+
+ @Query("SELECT * FROM archived_conversations WHERE deleted_ts < :timestamp")
+ fun getOldArchived(timestamp: Long): List
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun archiveConversation(archivedConversation: ArchivedConversation)
@Query("SELECT * FROM conversations WHERE thread_id = :threadId")
fun getConversationWithThreadId(threadId: Long): Conversation?
@@ -30,5 +40,14 @@ interface ConversationsDao {
fun markUnread(threadId: Long)
@Query("DELETE FROM conversations WHERE thread_id = :threadId")
- fun deleteThreadId(threadId: Long)
+ fun deleteThreadFromConversations(threadId: Long)
+
+ @Query("DELETE FROM archived_conversations WHERE thread_id = :threadId")
+ fun deleteThreadFromArchivedConversations(threadId: Long)
+
+ @Transaction
+ fun deleteThreadId(threadId: Long) {
+ deleteThreadFromConversations(threadId)
+ deleteThreadFromArchivedConversations(threadId)
+ }
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ArchivedConversation.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ArchivedConversation.kt
new file mode 100644
index 00000000..bcf43692
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/ArchivedConversation.kt
@@ -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
+)
diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsReceiver.kt
index d947190e..2b0fb852 100644
--- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsReceiver.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/SmsReceiver.kt
@@ -100,6 +100,7 @@ class SmsReceiver : BroadcastReceiver() {
subscriptionId
)
context.messagesDB.insertOrUpdate(message)
+ context.conversationsDB.deleteThreadFromArchivedConversations(threadId)
refreshMessages()
context.showReceivedMessageNotification(newMessageId, address, body, threadId, bitmap)
}
diff --git a/app/src/main/res/layout/activity_archived_conversations.xml b/app/src/main/res/layout/activity_archived_conversations.xml
new file mode 100644
index 00000000..d699f386
--- /dev/null
+++ b/app/src/main/res/layout/activity_archived_conversations.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml
index 99dfaa9b..eb7cef7a 100644
--- a/app/src/main/res/layout/activity_settings.xml
+++ b/app/src/main/res/layout/activity_settings.xml
@@ -346,6 +346,55 @@
android:id="@+id/settings_outgoing_messages_divider"
layout="@layout/divider" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/archive_menu.xml b/app/src/main/res/menu/archive_menu.xml
new file mode 100644
index 00000000..a3f11163
--- /dev/null
+++ b/app/src/main/res/menu/archive_menu.xml
@@ -0,0 +1,11 @@
+
+
diff --git a/app/src/main/res/menu/cab_archived_conversations.xml b/app/src/main/res/menu/cab_archived_conversations.xml
new file mode 100644
index 00000000..19adebc2
--- /dev/null
+++ b/app/src/main/res/menu/cab_archived_conversations.xml
@@ -0,0 +1,22 @@
+
+
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
index d60f5871..b403927f 100644
--- a/app/src/main/res/menu/menu_main.xml
+++ b/app/src/main/res/menu/menu_main.xml
@@ -13,6 +13,11 @@
android:showAsAction="never"
android:title="@string/export_messages"
app:showAsAction="never" />
+