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 74fff402..d1a5fad3 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/MainActivity.kt @@ -191,6 +191,7 @@ class MainActivity : SimpleActivity() { private fun refreshMenuItems() { main_menu.getToolbar().menu.apply { findItem(R.id.more_apps_from_us).isVisible = !resources.getBoolean(R.bool.hide_google_relations) + findItem(R.id.show_recycle_bin).isVisible = config.useRecycleBin } } diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/RecycleBinConversationsActivity.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/RecycleBinConversationsActivity.kt index 91a61bf4..a660dd00 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/RecycleBinConversationsActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/RecycleBinConversationsActivity.kt @@ -8,6 +8,7 @@ import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.smsmessenger.R import com.simplemobiletools.smsmessenger.adapters.ConversationsAdapter +import com.simplemobiletools.smsmessenger.adapters.RecycleBinConversationsAdapter import com.simplemobiletools.smsmessenger.extensions.* import com.simplemobiletools.smsmessenger.helpers.* import com.simplemobiletools.smsmessenger.models.Conversation @@ -97,11 +98,11 @@ class RecycleBinConversationsActivity : SimpleActivity() { } } - private fun getOrCreateConversationsAdapter(): ConversationsAdapter { + private fun getOrCreateConversationsAdapter(): RecycleBinConversationsAdapter { var currAdapter = conversations_list.adapter if (currAdapter == null) { hideKeyboard() - currAdapter = ConversationsAdapter( + currAdapter = RecycleBinConversationsAdapter( activity = this, recyclerView = conversations_list, onRefresh = { notifyDatasetChanged() }, @@ -113,7 +114,7 @@ class RecycleBinConversationsActivity : SimpleActivity() { conversations_list.scheduleLayoutAnimation() } } - return currAdapter as ConversationsAdapter + return currAdapter as RecycleBinConversationsAdapter } private fun setupConversations(conversations: ArrayList) { @@ -150,6 +151,7 @@ class RecycleBinConversationsActivity : SimpleActivity() { putExtra(THREAD_ID, conversation.threadId) putExtra(THREAD_TITLE, conversation.title) putExtra(WAS_PROTECTION_HANDLED, true) + putExtra(IS_RECYCLE_BIN, true) startActivity(this) } } 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 da1cac18..e9a1d924 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt @@ -105,6 +105,7 @@ class ThreadActivity : SimpleActivity() { private var allMessagesFetched = false private var oldestMessageDate = -1 private var wasProtectionHandled = false + private var isRecycleBin = false private var isScheduledMessage: Boolean = false private var scheduledMessage: Message? = null @@ -140,6 +141,7 @@ class ThreadActivity : SimpleActivity() { intent.getStringExtra(THREAD_TITLE)?.let { thread_toolbar.title = it } + isRecycleBin = intent.getBooleanExtra(IS_RECYCLE_BIN, false) wasProtectionHandled = intent.getBooleanExtra(WAS_PROTECTION_HANDLED, false) bus = EventBus.getDefault() @@ -163,6 +165,7 @@ class ThreadActivity : SimpleActivity() { setupAttachmentPickerView() setupKeyboardListener() hideAttachmentPicker() + maybeSetupRecycleBinView() } override fun onResume() { @@ -247,20 +250,21 @@ 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.restore).isVisible = threadItems.isNotEmpty() && isRecycleBin + findItem(R.id.archive).isVisible = threadItems.isNotEmpty() && conversation?.isArchived == false && !isRecycleBin + findItem(R.id.unarchive).isVisible = threadItems.isNotEmpty() && conversation?.isArchived == true && !isRecycleBin + findItem(R.id.rename_conversation).isVisible = participants.size > 1 && conversation != null && !isRecycleBin + findItem(R.id.conversation_details).isVisible = conversation != null && !isRecycleBin findItem(R.id.block_number).title = addLockedLabelIfNeeded(R.string.block_number) - findItem(R.id.block_number).isVisible = isNougatPlus() - findItem(R.id.dial_number).isVisible = participants.size == 1 && !isSpecialNumber() - findItem(R.id.manage_people).isVisible = !isSpecialNumber() - findItem(R.id.mark_as_unread).isVisible = threadItems.isNotEmpty() + findItem(R.id.block_number).isVisible = isNougatPlus() && !isRecycleBin + findItem(R.id.dial_number).isVisible = participants.size == 1 && !isSpecialNumber() && !isRecycleBin + findItem(R.id.manage_people).isVisible = !isSpecialNumber() && !isRecycleBin + findItem(R.id.mark_as_unread).isVisible = threadItems.isNotEmpty() && !isRecycleBin // allow saving number in cases when we dont have it stored yet and it is a casual readable number findItem(R.id.add_number_to_contact).isVisible = participants.size == 1 && participants.first().name == firstPhoneNumber && firstPhoneNumber.any { it.isDigit() - } + } && !isRecycleBin } } @@ -273,6 +277,7 @@ class ThreadActivity : SimpleActivity() { when (menuItem.itemId) { R.id.block_number -> tryBlocking() R.id.delete -> askConfirmDelete() + R.id.restore -> askConfirmRestoreAll() R.id.archive -> archiveConversation() R.id.unarchive -> unarchiveConversation() R.id.rename_conversation -> renameConversation() @@ -306,7 +311,11 @@ class ThreadActivity : SimpleActivity() { private fun setupCachedMessages(callback: () -> Unit) { ensureBackgroundThread { messages = try { - messagesDB.getThreadMessages(threadId).toMutableList() as ArrayList + if (isRecycleBin) { + messagesDB.getThreadMessagesFromRecycleBin(threadId).toMutableList() as ArrayList + } else { + messagesDB.getThreadMessages(threadId).toMutableList() as ArrayList + } } catch (e: Exception) { ArrayList() } @@ -341,7 +350,10 @@ class ThreadActivity : SimpleActivity() { privateContacts = MyContactsContentProvider.getSimpleContacts(this, privateCursor) val cachedMessagesCode = messages.clone().hashCode() - messages = getMessages(threadId, true) + if (!isRecycleBin) { + val recycledMessages = messagesDB.getThreadMessagesFromRecycleBin(threadId).map { it.id } + messages = getMessages(threadId, true).filter { !recycledMessages.contains(it.id) }.toMutableList() as ArrayList + } val hasParticipantWithoutName = participants.any { contact -> contact.phoneNumbers.map { it.normalizedNumber }.contains(contact.name) @@ -389,8 +401,10 @@ class ThreadActivity : SimpleActivity() { participants.add(contact) } - messages.chunked(30).forEach { currentMessages -> - messagesDB.insertMessages(*currentMessages.toTypedArray()) + if (!isRecycleBin) { + messages.chunked(30).forEach { currentMessages -> + messagesDB.insertMessages(*currentMessages.toTypedArray()) + } } setupAttachmentSizes() @@ -409,7 +423,8 @@ class ThreadActivity : SimpleActivity() { activity = this, recyclerView = thread_messages_list, itemClick = { handleItemClick(it) }, - deleteMessages = { messages, toRecycleBin -> deleteMessages(messages, toRecycleBin) } + isRecycleBin = isRecycleBin, + deleteMessages = { messages, toRecycleBin, fromRecycleBin -> deleteMessages(messages, toRecycleBin, fromRecycleBin) } ) thread_messages_list.adapter = currAdapter @@ -496,7 +511,7 @@ class ThreadActivity : SimpleActivity() { } } - private fun deleteMessages(messagesToRemove: List, toRecycleBin: Boolean) { + private fun deleteMessages(messagesToRemove: List, toRecycleBin: Boolean, fromRecycleBin: Boolean) { val deletePosition = threadItems.indexOf(messagesToRemove.first()) messages.removeAll(messagesToRemove.toSet()) threadItems = getThreadItems() @@ -515,12 +530,13 @@ class ThreadActivity : SimpleActivity() { messagesToRemove.forEach { message -> val messageId = message.id if (message.isScheduled) { - // TODO: Moving scheduled messages to recycle bin maybe doesn't make sense deleteScheduledMessage(messageId) cancelScheduleSendPendingIntent(messageId) } else { if (toRecycleBin) { moveMessageToRecycleBin(messageId) + } else if (fromRecycleBin) { + restoreMessageFromRecycleBin(messageId) } else { deleteMessage(messageId, message.isMMS) } @@ -792,7 +808,7 @@ class ThreadActivity : SimpleActivity() { } private fun maybeDisableShortCodeReply() { - if (isSpecialNumber()) { + if (isSpecialNumber() && !isRecycleBin) { thread_send_message_holder.beGone() reply_disabled_info_holder.beVisible() val textColor = getProperTextColor() @@ -922,7 +938,23 @@ class ThreadActivity : SimpleActivity() { val confirmationMessage = R.string.delete_whole_conversation_confirmation ConfirmationDialog(this, getString(confirmationMessage)) { ensureBackgroundThread { - deleteConversation(threadId) + if (isRecycleBin) { + emptyMessagesRecycleBinForConversation(threadId) + } else { + deleteConversation(threadId) + } + runOnUiThread { + refreshMessages() + finish() + } + } + } + } + + private fun askConfirmRestoreAll() { + ConfirmationDialog(this, "Restore all messages from this conversation?") { + ensureBackgroundThread { + restoreAllMessagesFromRecycleBinForConversation(threadId) runOnUiThread { refreshMessages() finish() @@ -1485,6 +1517,10 @@ class ThreadActivity : SimpleActivity() { @Subscribe(threadMode = ThreadMode.ASYNC) fun refreshMessages(event: Events.RefreshMessages) { + if (isRecycleBin) { + return + } + refreshedSinceSent = true allMessagesFetched = false oldestMessageDate = -1 @@ -1747,6 +1783,12 @@ class ThreadActivity : SimpleActivity() { animateAttachmentButton(rotation = -135f) } + private fun maybeSetupRecycleBinView() { + if (isRecycleBin) { + thread_send_message_holder.beGone() + } + } + private fun hideAttachmentPicker() { attachment_picker_divider.beGone() attachment_picker_holder.apply { diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/RecycleBinConversationsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/RecycleBinConversationsAdapter.kt new file mode 100644 index 00000000..9f6505ca --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/RecycleBinConversationsAdapter.kt @@ -0,0 +1,107 @@ +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.restoreAllMessagesFromRecycleBinForConversation +import com.simplemobiletools.smsmessenger.helpers.refreshMessages +import com.simplemobiletools.smsmessenger.models.Conversation + +class RecycleBinConversationsAdapter( + activity: SimpleActivity, recyclerView: MyRecyclerView, onRefresh: () -> Unit, itemClick: (Any) -> Unit +) : BaseConversationsAdapter(activity, recyclerView, onRefresh, itemClick) { + override fun getActionMenuId() = R.menu.cab_recycle_bin_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_restore -> askConfirmRestore() + 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 + conversationsToRemove.forEach { + activity.deleteConversation(it.threadId) + activity.notificationManager.cancel(it.threadId.hashCode()) + } + + removeConversationsFromList(conversationsToRemove) + } + + private fun askConfirmRestore() { + val itemsCnt = selectedKeys.size + val items = resources.getQuantityString(R.plurals.delete_conversations, itemsCnt, itemsCnt) + + val question = String.format("Are you sure you want to restore %s?", items) + + ConfirmationDialog(activity, question) { + ensureBackgroundThread { + restoreConversations() + } + } + } + + private fun restoreConversations() { + if (selectedKeys.isEmpty()) { + return + } + + val conversationsToRemove = currentList.filter { selectedKeys.contains(it.hashCode()) } as ArrayList + conversationsToRemove.forEach { + activity.restoreAllMessagesFromRecycleBinForConversation(it.threadId) + } + + removeConversationsFromList(conversationsToRemove) + } + + 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/ThreadAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt index a68ac478..606bdb76 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt @@ -23,6 +23,7 @@ import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.target.Target import com.simplemobiletools.commons.adapters.MyRecyclerViewListAdapter +import com.simplemobiletools.commons.dialogs.ConfirmationDialog import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.SimpleContactsHelper import com.simplemobiletools.commons.helpers.ensureBackgroundThread @@ -58,7 +59,8 @@ class ThreadAdapter( activity: SimpleActivity, recyclerView: MyRecyclerView, itemClick: (Any) -> Unit, - val deleteMessages: (messages: List, toRecycleBin: Boolean) -> Unit + val isRecycleBin: Boolean, + val deleteMessages: (messages: List, toRecycleBin: Boolean, fromRecycleBin: Boolean) -> Unit ) : MyRecyclerViewListAdapter(activity, recyclerView, ThreadItemDiffCallback(), itemClick) { private var fontSize = activity.getTextSize() @@ -84,6 +86,7 @@ class ThreadAdapter( findItem(R.id.cab_forward_message).isVisible = isOneItemSelected findItem(R.id.cab_select_text).isVisible = isOneItemSelected && hasText findItem(R.id.cab_properties).isVisible = isOneItemSelected + findItem(R.id.cab_restore).isVisible = isRecycleBin } } @@ -99,6 +102,7 @@ class ThreadAdapter( R.id.cab_forward_message -> forwardMessage() R.id.cab_select_text -> selectText() R.id.cab_delete -> askConfirmDelete() + R.id.cab_restore -> askConfirmRestore() R.id.cab_select_all -> selectAll() R.id.cab_properties -> showMessageDetails() } @@ -206,12 +210,36 @@ class ThreadAdapter( val baseString = R.string.deletion_confirmation val question = String.format(resources.getString(baseString), items) - DeleteConfirmationDialog(activity, question, activity.config.useRecycleBin) { skipRecycleBin -> + DeleteConfirmationDialog(activity, question, activity.config.useRecycleBin && !isRecycleBin) { skipRecycleBin -> ensureBackgroundThread { val messagesToRemove = getSelectedItems() if (messagesToRemove.isNotEmpty()) { - val toRecycleBin = !skipRecycleBin && activity.config.useRecycleBin - deleteMessages(messagesToRemove.filterIsInstance(), toRecycleBin) + val toRecycleBin = !skipRecycleBin && activity.config.useRecycleBin && !isRecycleBin + deleteMessages(messagesToRemove.filterIsInstance(), toRecycleBin, false) + } + } + } + } + + private fun askConfirmRestore() { + val itemsCnt = selectedKeys.size + + // not sure how we can get UnknownFormatConversionException here, so show the error and hope that someone reports it + val items = try { + resources.getQuantityString(R.plurals.delete_messages, itemsCnt, itemsCnt) + } catch (e: Exception) { + activity.showErrorToast(e) + return + } + + val baseString = R.string.deletion_confirmation + val question = String.format("Are you sure you want to restore %s?", items) + + ConfirmationDialog(activity, question) { + ensureBackgroundThread { + val messagesToRestore = getSelectedItems() + if (messagesToRestore.isNotEmpty()) { + deleteMessages(messagesToRestore.filterIsInstance(), false, true) } } } 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 0e04d519..de7d6855 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt @@ -679,6 +679,17 @@ fun Context.emptyMessagesRecycleBin() { } } +fun Context.emptyMessagesRecycleBinForConversation(threadId: Long) { + val messages = messagesDB.getThreadMessagesFromRecycleBin(threadId) + for (message in messages) { + deleteMessage(message.id, message.isMMS) + } +} + +fun Context.restoreAllMessagesFromRecycleBinForConversation(threadId: Long) { + messagesDB.deleteThreadMessagesFromRecycleBin(threadId) +} + fun Context.moveMessageToRecycleBin(id: Long) { try { messagesDB.insertRecycleBinEntry(RecycleBinMessage(id, System.currentTimeMillis())) @@ -687,6 +698,14 @@ fun Context.moveMessageToRecycleBin(id: Long) { } } +fun Context.restoreMessageFromRecycleBin(id: Long) { + try { + messagesDB.deleteFromRecycleBin(id) + } catch (e: Exception) { + showErrorToast(e) + } +} + fun Context.updateConversationArchivedStatus(threadId: Long, archived: Boolean) { val uri = Threads.CONTENT_URI val values = ContentValues().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 859b555a..390ee5b6 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt @@ -42,7 +42,7 @@ const val IS_MMS = "is_mms" const val MESSAGE_ID = "message_id" const val USE_RECYCLE_BIN = "use_recycle_bin" const val LAST_RECYCLE_BIN_CHECK = "last_recycle_bin_check" -const val USE_ARCHIVE = "use_archive" +const val IS_RECYCLE_BIN = "is_recycle_bin" 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/MessagesDao.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/MessagesDao.kt index 9cb6fe68..f8bb9f38 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/MessagesDao.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/MessagesDao.kt @@ -30,6 +30,9 @@ interface MessagesDao { @Query("SELECT messages.* FROM messages LEFT OUTER JOIN recycle_bin_messages ON messages.id = recycle_bin_messages.id WHERE recycle_bin_messages.id IS NULL AND thread_id = :threadId") fun getThreadMessages(threadId: Long): List + @Query("SELECT messages.* FROM messages LEFT OUTER JOIN recycle_bin_messages ON messages.id = recycle_bin_messages.id WHERE recycle_bin_messages.id IS NOT NULL AND thread_id = :threadId") + fun getThreadMessagesFromRecycleBin(threadId: Long): List + @Query("SELECT messages.* FROM messages LEFT OUTER JOIN recycle_bin_messages ON messages.id = recycle_bin_messages.id WHERE recycle_bin_messages.id IS NULL AND thread_id = :threadId AND is_scheduled = 1") fun getScheduledThreadMessages(threadId: Long): List diff --git a/app/src/main/res/menu/cab_recycle_bin_conversations.xml b/app/src/main/res/menu/cab_recycle_bin_conversations.xml index 8ff99d41..2c9a90e5 100644 --- a/app/src/main/res/menu/cab_recycle_bin_conversations.xml +++ b/app/src/main/res/menu/cab_recycle_bin_conversations.xml @@ -12,7 +12,7 @@ + +