From 555b6ebea30b32391e8267e729b61bec2bcbb32f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ensar=20Saraj=C4=8Di=C4=87?= Date: Tue, 18 Jul 2023 11:34:25 +0200 Subject: [PATCH] Add support for recycle bin for messages This adds support for moving messages to recycle bin instead of deleting them right away. The feature is not active by default. This closes #451 --- .../smsmessenger/activities/MainActivity.kt | 1 + .../activities/SettingsActivity.kt | 43 ++++++++++++++++ .../smsmessenger/activities/ThreadActivity.kt | 11 +++-- .../smsmessenger/adapters/ThreadAdapter.kt | 9 ++-- .../databases/MessagesDatabase.kt | 17 +++++-- .../dialogs/DeleteConfirmationDialog.kt | 39 +++++++++++++++ .../smsmessenger/extensions/Context.kt | 30 ++++++++++++ .../smsmessenger/helpers/Config.kt | 8 +++ .../smsmessenger/helpers/Constants.kt | 2 + .../smsmessenger/interfaces/MessagesDao.kt | 44 ++++++++++++++--- .../smsmessenger/models/RecycleBinMessage.kt | 15 ++++++ app/src/main/res/layout/activity_settings.xml | 49 +++++++++++++++++++ .../res/layout/dialog_delete_confirmation.xml | 26 ++++++++++ 13 files changed, 274 insertions(+), 20 deletions(-) create mode 100644 app/src/main/kotlin/com/simplemobiletools/smsmessenger/dialogs/DeleteConfirmationDialog.kt create mode 100644 app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/RecycleBinMessage.kt create mode 100644 app/src/main/res/layout/dialog_delete_confirmation.xml 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 5c6a1b5b..f9c558e6 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) { + checkAndDeleteOldRecycleBinMessages() handleAppPasswordProtection { wasProtectionHandled = it if (it) { 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 38cc7388..ff0d5ffe 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.emptyMessagesRecycleBin +import com.simplemobiletools.smsmessenger.extensions.messagesDB import com.simplemobiletools.smsmessenger.helpers.* import kotlinx.android.synthetic.main.activity_settings.* import java.util.* class SettingsActivity : SimpleActivity() { private var blockedNumbersAtPause = -1 + private var recycleBinMessages = 0 override fun onCreate(savedInstanceState: Bundle?) { isMaterialActivity = true @@ -48,6 +51,8 @@ class SettingsActivity : SimpleActivity() { setupGroupMessageAsMMS() setupLockScreenVisibility() setupMMSFileSizeLimit() + setupUseRecycleBin() + setupEmptyRecycleBin() setupAppPasswordProtection() updateTextColors(settings_nested_scrollview) @@ -60,6 +65,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()) @@ -259,6 +265,43 @@ class SettingsActivity : SimpleActivity() { } } + private fun setupUseRecycleBin() { + updateRecycleBinButtons() + settings_use_recycle_bin.isChecked = config.useRecycleBin + settings_use_recycle_bin_holder.setOnClickListener { + settings_use_recycle_bin.toggle() + config.useRecycleBin = settings_use_recycle_bin.isChecked + updateRecycleBinButtons() + } + } + + private fun updateRecycleBinButtons() { + settings_empty_recycle_bin_holder.beVisibleIf(config.useRecycleBin) + } + + private fun setupEmptyRecycleBin() { + ensureBackgroundThread { + recycleBinMessages = messagesDB.getArchivedCount() + runOnUiThread { + settings_empty_recycle_bin_size.text = + resources.getQuantityString(R.plurals.delete_messages, recycleBinMessages, recycleBinMessages) + } + } + + settings_empty_recycle_bin_holder.setOnClickListener { + if (recycleBinMessages == 0) { + toast(R.string.recycle_bin_empty) + } else { + ConfirmationDialog(this, "", R.string.empty_recycle_bin_confirmation, R.string.yes, R.string.no) { + emptyMessagesRecycleBin() + recycleBinMessages = 0 + settings_empty_recycle_bin_size.text = + resources.getQuantityString(R.plurals.delete_messages, recycleBinMessages, recycleBinMessages) + } + } + } + } + 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..52c5acd1 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt @@ -402,7 +402,7 @@ class ThreadActivity : SimpleActivity() { activity = this, recyclerView = thread_messages_list, itemClick = { handleItemClick(it) }, - deleteMessages = { deleteMessages(it) } + deleteMessages = { messages, toRecycleBin -> deleteMessages(messages, toRecycleBin) } ) thread_messages_list.adapter = currAdapter @@ -489,7 +489,7 @@ class ThreadActivity : SimpleActivity() { } } - private fun deleteMessages(messagesToRemove: List) { + private fun deleteMessages(messagesToRemove: List, toRecycleBin: Boolean) { val deletePosition = threadItems.indexOf(messagesToRemove.first()) messages.removeAll(messagesToRemove.toSet()) threadItems = getThreadItems() @@ -508,10 +508,15 @@ 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 { - deleteMessage(messageId, message.isMMS) + if (toRecycleBin) { + moveMessageToRecycleBin(messageId) + } else { + deleteMessage(messageId, message.isMMS) + } } } updateLastConversationMessage(threadId) 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 82da0e28..a68ac478 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt @@ -23,7 +23,6 @@ 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 @@ -33,6 +32,7 @@ import com.simplemobiletools.smsmessenger.activities.NewConversationActivity import com.simplemobiletools.smsmessenger.activities.SimpleActivity import com.simplemobiletools.smsmessenger.activities.ThreadActivity import com.simplemobiletools.smsmessenger.activities.VCardViewerActivity +import com.simplemobiletools.smsmessenger.dialogs.DeleteConfirmationDialog import com.simplemobiletools.smsmessenger.dialogs.MessageDetailsDialog import com.simplemobiletools.smsmessenger.dialogs.SelectTextDialog import com.simplemobiletools.smsmessenger.extensions.* @@ -58,7 +58,7 @@ class ThreadAdapter( activity: SimpleActivity, recyclerView: MyRecyclerView, itemClick: (Any) -> Unit, - val deleteMessages: (messages: List) -> Unit + val deleteMessages: (messages: List, toRecycleBin: Boolean) -> Unit ) : MyRecyclerViewListAdapter(activity, recyclerView, ThreadItemDiffCallback(), itemClick) { private var fontSize = activity.getTextSize() @@ -206,11 +206,12 @@ class ThreadAdapter( val baseString = R.string.deletion_confirmation val question = String.format(resources.getString(baseString), items) - ConfirmationDialog(activity, question) { + DeleteConfirmationDialog(activity, question, activity.config.useRecycleBin) { skipRecycleBin -> ensureBackgroundThread { val messagesToRemove = getSelectedItems() if (messagesToRemove.isNotEmpty()) { - deleteMessages(messagesToRemove.filterIsInstance()) + val toRecycleBin = !skipRecycleBin && activity.config.useRecycleBin + deleteMessages(messagesToRemove.filterIsInstance(), toRecycleBin) } } } 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..c91ebf43 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, Attachment::class, MessageAttachment::class, Message::class, RecycleBinMessage::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,14 @@ abstract class MessagesDatabase : RoomDatabase() { } } } + + private val MIGRATION_7_8 = object : Migration(7, 8) { + override fun migrate(database: SupportSQLiteDatabase) { + database.apply { + execSQL("CREATE TABLE IF NOT EXISTS `recycle_bin_messages` (`id` INTEGER PRIMARY KEY, `deleted_ts` INTEGER NOT NULL)") + execSQL("CREATE UNIQUE INDEX `index_recycle_bin_messages_id` ON `recycle_bin_messages` (`id`)") + } + } + } } } 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 d07b3c8d..2c4a45c6 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/extensions/Context.kt @@ -641,6 +641,36 @@ fun Context.deleteConversation(threadId: Long) { messagesDB.deleteThreadMessages(threadId) } +fun Context.checkAndDeleteOldRecycleBinMessages(callback: (() -> Unit)? = null) { + if (config.useRecycleBin && config.lastRecycleBinCheck < System.currentTimeMillis() - DAY_SECONDS * 1000) { + config.lastRecycleBinCheck = System.currentTimeMillis() + ensureBackgroundThread { + try { + for (message in messagesDB.getOldRecycleBinMessages(System.currentTimeMillis() - MONTH_SECONDS * 1000L)) { + deleteMessage(message.id, message.isMMS) + } + callback?.invoke() + } catch (e: Exception) { + } + } + } +} + +fun Context.emptyMessagesRecycleBin() { + val messages = messagesDB.getAllRecycleBinMessages() + for (message in messages) { + deleteMessage(message.id, message.isMMS) + } +} + +fun Context.moveMessageToRecycleBin(id: Long) { + try { + messagesDB.insertRecycleBinEntry(RecycleBinMessage(id, System.currentTimeMillis())) + } catch (e: Exception) { + showErrorToast(e) + } +} + 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 18859f76..88eb740a 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Config.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Config.kt @@ -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 useRecycleBin: Boolean + get() = prefs.getBoolean(USE_RECYCLE_BIN, false) + set(useRecycleBin) = prefs.edit().putBoolean(USE_RECYCLE_BIN, useRecycleBin).apply() + + var lastRecycleBinCheck: Long + get() = prefs.getLong(LAST_RECYCLE_BIN_CHECK, 0L) + set(lastRecycleBinCheck) = prefs.edit().putLong(LAST_RECYCLE_BIN_CHECK, lastRecycleBinCheck).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 3bade2ac..947cc219 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/Constants.kt @@ -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_RECYCLE_BIN = "use_recycle_bin" +const val LAST_RECYCLE_BIN_CHECK = "last_recycle_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/MessagesDao.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/MessagesDao.kt index 7036167b..9cb6fe68 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/MessagesDao.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/interfaces/MessagesDao.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.RecycleBinMessage import com.simplemobiletools.smsmessenger.models.Message @Dao @@ -11,6 +9,9 @@ interface MessagesDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertOrUpdate(message: Message) + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertRecycleBinEntry(recycleBinMessage: RecycleBinMessage) + @Insert(onConflict = OnConflictStrategy.IGNORE) fun insertOrIgnore(message: Message): Long @@ -20,15 +21,24 @@ interface MessagesDao { @Query("SELECT * FROM messages") fun getAll(): List - @Query("SELECT * FROM messages WHERE thread_id = :threadId") + @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") + fun getAllRecycleBinMessages(): 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 recycle_bin_messages.deleted_ts < :timestamp") + fun getOldRecycleBinMessages(timestamp: 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") fun getThreadMessages(threadId: Long): List - @Query("SELECT * FROM messages WHERE thread_id = :threadId AND is_scheduled = 1") + @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 @Query("SELECT * FROM messages WHERE thread_id = :threadId AND id = :messageId AND is_scheduled = 1") fun getScheduledMessageWithId(threadId: Long, messageId: Long): Message + @Query("SELECT COUNT(*) FROM recycle_bin_messages") + fun getArchivedCount(): Int + @Query("SELECT * FROM messages WHERE body LIKE :text") fun getMessagesWithText(text: String): List @@ -44,11 +54,29 @@ interface MessagesDao { @Query("UPDATE messages SET status = :status WHERE id = :id") fun updateStatus(id: Long, status: Int): Int + @Transaction + fun delete(id: Long) { + deleteFromMessages(id) + deleteFromRecycleBin(id) + } + @Query("DELETE FROM messages WHERE id = :id") - fun delete(id: Long) + fun deleteFromMessages(id: Long) + + @Query("DELETE FROM recycle_bin_messages WHERE id = :id") + fun deleteFromRecycleBin(id: Long) + + @Transaction + fun deleteThreadMessages(threadId: Long) { + deleteThreadMessagesFromRecycleBin(threadId) + deleteAllThreadMessages(threadId) + } @Query("DELETE FROM messages WHERE thread_id = :threadId") - fun deleteThreadMessages(threadId: Long) + fun deleteAllThreadMessages(threadId: Long) + + @Query("DELETE FROM recycle_bin_messages WHERE id IN (SELECT id FROM messages WHERE thread_id = :threadId)") + fun deleteThreadMessagesFromRecycleBin(threadId: Long) @Query("DELETE FROM messages") fun deleteAll() diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/RecycleBinMessage.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/RecycleBinMessage.kt new file mode 100644 index 00000000..e8c1dbe6 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/models/RecycleBinMessage.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 = "recycle_bin_messages", + indices = [(Index(value = ["id"], unique = true))] +) +data class RecycleBinMessage( + @PrimaryKey val id: Long, + @ColumnInfo(name = "deleted_ts") var deletedTS: Long +) diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 7f7f562e..65c9682b 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -361,6 +361,55 @@ android:id="@+id/settings_outgoing_messages_divider" layout="@layout/divider" /> + + + + + + + + + + + + + + + + + + + + + + + + +