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" /> + + + + + + + + + + + + + + + + + + + + + + + + +