From b0ca66cc1a77a6ba82c4ce8e900e099d09339008 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 13 Jul 2023 10:16:01 -0300 Subject: [PATCH] Add new active column to ThreadTable. --- .../database/ThreadTableTest_active.kt | 144 ++++++++++++++++++ .../securesms/testing/SignalDatabaseRule.kt | 2 +- .../securesms/database/GroupTable.kt | 14 +- .../securesms/database/MentionTable.java | 2 +- .../securesms/database/MessageTable.kt | 2 +- .../securesms/database/RecipientTable.kt | 6 +- .../securesms/database/ThreadTable.kt | 85 +++++++++-- .../helpers/SignalDatabaseMigrations.kt | 7 +- .../migration/V199_AddThreadActiveColumn.kt | 32 ++++ 9 files changed, 266 insertions(+), 28 deletions(-) create mode 100644 app/src/androidTest/java/org/thoughtcrime/securesms/database/ThreadTableTest_active.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V199_AddThreadActiveColumn.kt diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/ThreadTableTest_active.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/ThreadTableTest_active.kt new file mode 100644 index 0000000000..55e8251ea2 --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/ThreadTableTest_active.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.database + +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.testing.SignalDatabaseRule +import org.whispersystems.signalservice.api.push.ServiceId +import java.util.UUID + +@Suppress("ClassName") +class ThreadTableTest_active { + + @Rule + @JvmField + val databaseRule = SignalDatabaseRule() + + private lateinit var recipient: Recipient + + @Before + fun setUp() { + recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID()))) + } + + @Test + fun givenActiveUnarchivedThread_whenIGetUnarchivedConversationList_thenIExpectThread() { + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) + MmsHelper.insert(recipient = recipient, threadId = threadId) + SignalDatabase.threads.update(threadId, false) + + SignalDatabase.threads.getUnarchivedConversationList( + ConversationFilter.OFF, + false, + 0, + 10 + ).use { threads -> + assertEquals(1, threads.count) + + val record = ThreadTable.StaticReader(threads, InstrumentationRegistry.getInstrumentation().context).getNext() + + assertNotNull(record) + assertEquals(record!!.recipient.id, recipient.id) + } + } + + @Test + fun givenInactiveUnarchivedThread_whenIGetUnarchivedConversationList_thenIExpectNoThread() { + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) + MmsHelper.insert(recipient = recipient, threadId = threadId) + SignalDatabase.threads.update(threadId, false) + SignalDatabase.threads.deleteConversation(threadId) + + SignalDatabase.threads.getUnarchivedConversationList( + ConversationFilter.OFF, + false, + 0, + 10 + ).use { threads -> + assertEquals(0, threads.count) + } + + val threadId2 = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) + assertEquals(threadId2, threadId) + } + + @Test + fun givenActiveArchivedThread_whenIGetUnarchivedConversationList_thenIExpectNoThread() { + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) + MmsHelper.insert(recipient = recipient, threadId = threadId) + SignalDatabase.threads.update(threadId, false) + SignalDatabase.threads.setArchived(setOf(threadId), true) + + SignalDatabase.threads.getUnarchivedConversationList( + ConversationFilter.OFF, + false, + 0, + 10 + ).use { threads -> + assertEquals(0, threads.count) + } + } + + @Test + fun givenActiveArchivedThread_whenIGetArchivedConversationList_thenIExpectThread() { + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) + MmsHelper.insert(recipient = recipient, threadId = threadId) + SignalDatabase.threads.update(threadId, false) + SignalDatabase.threads.setArchived(setOf(threadId), true) + + SignalDatabase.threads.getArchivedConversationList( + ConversationFilter.OFF, + 0, + 10 + ).use { threads -> + assertEquals(1, threads.count) + } + } + + @Test + fun givenInactiveArchivedThread_whenIGetArchivedConversationList_thenIExpectNoThread() { + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) + MmsHelper.insert(recipient = recipient, threadId = threadId) + SignalDatabase.threads.update(threadId, false) + SignalDatabase.threads.deleteConversation(threadId) + SignalDatabase.threads.setArchived(setOf(threadId), true) + + SignalDatabase.threads.getArchivedConversationList( + ConversationFilter.OFF, + 0, + 10 + ).use { threads -> + assertEquals(0, threads.count) + } + + val threadId2 = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) + assertEquals(threadId2, threadId) + } + + @Test + fun givenActiveArchivedThread_whenIDeactivateThread_thenIExpectNoMessages() { + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient) + MmsHelper.insert(recipient = recipient, threadId = threadId) + SignalDatabase.threads.update(threadId, false) + + SignalDatabase.messages.getConversation(threadId).use { + assertEquals(1, it.count) + } + + SignalDatabase.threads.deleteConversation(threadId) + + SignalDatabase.messages.getConversation(threadId).use { + assertEquals(0, it.count) + } + } +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalDatabaseRule.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalDatabaseRule.kt index 0cd4d38881..343d5f864e 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalDatabaseRule.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalDatabaseRule.kt @@ -34,7 +34,7 @@ class SignalDatabaseRule( private fun deleteAllThreads() { if (deleteAllThreadsOnEachRun) { - SignalDatabase.messages.deleteAllThreads() + SignalDatabase.threads.clearForTests() } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt index 9dbcaba1bf..48dfa3955b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt @@ -369,7 +369,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT FROM $TABLE_NAME INNER JOIN ${MembershipTable.TABLE_NAME} ON ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} = $TABLE_NAME.$GROUP_ID INNER JOIN ${ThreadTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = $TABLE_NAME.$RECIPIENT_ID - WHERE $ACTIVE = 1 AND ${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID} IN (${subquery.where}) + WHERE $TABLE_NAME.$ACTIVE = 1 AND ${MembershipTable.TABLE_NAME}.${MembershipTable.RECIPIENT_ID} IN (${subquery.where}) GROUP BY ${MembershipTable.TABLE_NAME}.${MembershipTable.GROUP_ID} ORDER BY $TITLE COLLATE NOCASE ASC """ @@ -407,10 +407,10 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT val queryArgs: Array if (includeInactive) { - query = "${membershipQuery.where} AND ($ACTIVE = ? OR $TABLE_NAME.$RECIPIENT_ID IN (SELECT ${ThreadTable.RECIPIENT_ID} FROM ${ThreadTable.TABLE_NAME}))" + query = "${membershipQuery.where} AND ($TABLE_NAME.$ACTIVE = ? OR $TABLE_NAME.$RECIPIENT_ID IN (SELECT ${ThreadTable.RECIPIENT_ID} FROM ${ThreadTable.TABLE_NAME} WHERE ${ThreadTable.TABLE_NAME}.${ThreadTable.ACTIVE} = 1))" queryArgs = membershipQuery.whereArgs + buildArgs(1) } else { - query = "${membershipQuery.where} AND $ACTIVE = ?" + query = "${membershipQuery.where} AND $TABLE_NAME.$ACTIVE = ?" queryArgs = membershipQuery.whereArgs + buildArgs(1) } @@ -464,10 +464,10 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT val caseInsensitiveQuery = buildCaseInsensitiveGlobPattern(inputQuery) if (includeInactive) { - query = "$TITLE GLOB ? AND ($ACTIVE = ? OR $TABLE_NAME.$RECIPIENT_ID IN (SELECT ${ThreadTable.RECIPIENT_ID} FROM ${ThreadTable.TABLE_NAME}))" + query = "$TITLE GLOB ? AND ($TABLE_NAME.$ACTIVE = ? OR $TABLE_NAME.$RECIPIENT_ID IN (SELECT ${ThreadTable.RECIPIENT_ID} FROM ${ThreadTable.TABLE_NAME} WHERE ${ThreadTable.TABLE_NAME}.${ThreadTable.ACTIVE} = 1))" queryArgs = buildArgs(caseInsensitiveQuery, 1) } else { - query = "$TITLE GLOB ? AND $ACTIVE = ?" + query = "$TITLE GLOB ? AND $TABLE_NAME.$ACTIVE = ?" queryArgs = buildArgs(caseInsensitiveQuery, 1) } @@ -590,7 +590,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT } if (!includeInactive) { - query += " AND $ACTIVE = ?" + query += " AND $TABLE_NAME.$ACTIVE = ?" args = appendArg(args, "1") } @@ -1313,7 +1313,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT ) AS active_timestamp FROM $TABLE_NAME INNER JOIN ${ThreadTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = $TABLE_NAME.$RECIPIENT_ID WHERE - $ACTIVE = 1 AND + $TABLE_NAME.$ACTIVE = 1 AND ( $SHOW_AS_STORY_STATE = ${ShowAsStoryState.ALWAYS.code} OR ( diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MentionTable.java b/app/src/main/java/org/thoughtcrime/securesms/database/MentionTable.java index e0558ff3ff..a33425e6b8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MentionTable.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MentionTable.java @@ -131,7 +131,7 @@ public class MentionTable extends DatabaseTable implements RecipientIdDatabaseRe void deleteAbandonedMentions() { SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - String where = MESSAGE_ID + " NOT IN (SELECT " + MessageTable.ID + " FROM " + MessageTable.TABLE_NAME + ") OR " + THREAD_ID + " NOT IN (SELECT " + ThreadTable.ID + " FROM " + ThreadTable.TABLE_NAME + ")"; + String where = MESSAGE_ID + " NOT IN (SELECT " + MessageTable.ID + " FROM " + MessageTable.TABLE_NAME + ") OR " + THREAD_ID + " NOT IN (SELECT " + ThreadTable.ID + " FROM " + ThreadTable.TABLE_NAME + " WHERE " + ThreadTable.ACTIVE + " = 1)"; db.delete(TABLE_NAME, where, null); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt index 86c7e0064a..e86add957d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -3562,7 +3562,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat fun deleteAbandonedMessages(): Int { val deletes = writableDatabase .delete(TABLE_NAME) - .where("$THREAD_ID NOT IN (SELECT _id FROM ${ThreadTable.TABLE_NAME})") + .where("$THREAD_ID NOT IN (SELECT _id FROM ${ThreadTable.TABLE_NAME} WHERE ${ThreadTable.ACTIVE} = 1)") .run() if (deletes > 0) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index c3746567c3..025c0b075f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -406,6 +406,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da $TABLE_NAME.$SEEN_INVITE_REMINDER < ${InsightsBannerTier.TIER_TWO.id} AND ${ThreadTable.TABLE_NAME}.${ThreadTable.HAS_SENT} AND ${ThreadTable.TABLE_NAME}.${ThreadTable.DATE} > ? AND + ${ThreadTable.TABLE_NAME}.${ThreadTable.ACTIVE} = 1 AND $TABLE_NAME.$HIDDEN = 0 ORDER BY ${ThreadTable.TABLE_NAME}.${ThreadTable.DATE} DESC LIMIT 50 """ @@ -3344,13 +3345,16 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da return SqlUtil.Query(subquery, SqlUtil.buildArgs(0, 0, query, query, query, query)) } + /** + * Queries all contacts without an active thread. + */ fun getAllContactsWithoutThreads(inputQuery: String): Cursor { val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery) //language=sql val subquery = """ SELECT ${SEARCH_PROJECTION.joinToString(", ")} FROM $TABLE_NAME - WHERE $BLOCKED = ? AND $HIDDEN = ? AND NOT EXISTS (SELECT 1 FROM ${ThreadTable.TABLE_NAME} WHERE ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = $TABLE_NAME.$ID LIMIT 1) + WHERE $BLOCKED = ? AND $HIDDEN = ? AND NOT EXISTS (SELECT 1 FROM ${ThreadTable.TABLE_NAME} WHERE ${ThreadTable.TABLE_NAME}.${ThreadTable.ACTIVE} = 1 AND ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = $TABLE_NAME.$ID LIMIT 1) AND ( $SORT_NAME GLOB ? OR $USERNAME GLOB ? OR diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt index 4c94ab91ee..4e2c071132 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt @@ -6,6 +6,7 @@ import android.content.Context import android.database.Cursor import android.database.MergeCursor import android.net.Uri +import androidx.annotation.VisibleForTesting import androidx.core.content.contentValuesOf import com.fasterxml.jackson.annotation.JsonProperty import org.json.JSONObject @@ -106,6 +107,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa const val LAST_SCROLLED = "last_scrolled" const val PINNED = "pinned" const val UNREAD_SELF_MENTION_COUNT = "unread_self_mention_count" + const val ACTIVE = "active" const val MAX_CACHE_SIZE = 1000 @@ -134,16 +136,18 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa $HAS_SENT INTEGER DEFAULT 0, $LAST_SCROLLED INTEGER DEFAULT 0, $PINNED INTEGER DEFAULT 0, - $UNREAD_SELF_MENTION_COUNT INTEGER DEFAULT 0 + $UNREAD_SELF_MENTION_COUNT INTEGER DEFAULT 0, + $ACTIVE INTEGER DEFAULT 0 ) """ @JvmField val CREATE_INDEXS = arrayOf( - "CREATE INDEX IF NOT EXISTS thread_recipient_id_index ON $TABLE_NAME ($RECIPIENT_ID);", - "CREATE INDEX IF NOT EXISTS archived_count_index ON $TABLE_NAME ($ARCHIVED, $MEANINGFUL_MESSAGES);", + "CREATE INDEX IF NOT EXISTS thread_recipient_id_index ON $TABLE_NAME ($RECIPIENT_ID, $ACTIVE);", + "CREATE INDEX IF NOT EXISTS archived_count_index ON $TABLE_NAME ($ACTIVE, $ARCHIVED, $MEANINGFUL_MESSAGES, $PINNED);", "CREATE INDEX IF NOT EXISTS thread_pinned_index ON $TABLE_NAME ($PINNED);", - "CREATE INDEX IF NOT EXISTS thread_read ON $TABLE_NAME ($READ);" + "CREATE INDEX IF NOT EXISTS thread_read ON $TABLE_NAME ($READ);", + "CREATE INDEX IF NOT EXISTS thread_active ON $TABLE_NAME ($ACTIVE);" ) private val THREAD_PROJECTION = arrayOf( @@ -240,7 +244,8 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa STATUS to status, DELIVERY_RECEIPT_COUNT to deliveryReceiptCount, READ_RECEIPT_COUNT to readReceiptCount, - EXPIRES_IN to expiresIn + EXPIRES_IN to expiresIn, + ACTIVE to 1 ) writableDatabase @@ -873,7 +878,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa return readableDatabase .select("COUNT(*)") .from(TABLE_NAME) - .where("$ARCHIVED = 1 AND $MEANINGFUL_MESSAGES != 0 $filterQuery") + .where("$ACTIVE = 1 AND $ARCHIVED = 1 AND $MEANINGFUL_MESSAGES != 0 $filterQuery") .run() .use { cursor -> if (cursor.moveToFirst()) { @@ -889,7 +894,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa return readableDatabase .select("COUNT(*)") .from(TABLE_NAME) - .where("$ARCHIVED = 0 AND $PINNED != 0 $filterQuery") + .where("$ACTIVE = 1 AND $ARCHIVED = 0 AND $PINNED != 0 $filterQuery") .run() .use { cursor -> if (cursor.moveToFirst()) { @@ -905,7 +910,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa return readableDatabase .select("COUNT(*)") .from(TABLE_NAME) - .where("$ARCHIVED = 0 AND ($MEANINGFUL_MESSAGES != 0 OR $PINNED != 0) $filterQuery") + .where("$ACTIVE = 1 AND $ARCHIVED = 0 AND ($MEANINGFUL_MESSAGES != 0 OR $PINNED != 0) $filterQuery") .run() .use { cursor -> if (cursor.moveToFirst()) { @@ -968,7 +973,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa for (threadId in threadIds) { pinnedCount++ db.update(TABLE_NAME) - .values(PINNED to pinnedCount) + .values(PINNED to pinnedCount, ACTIVE to 1) .where("$ID = ?", threadId) .run() } @@ -1058,9 +1063,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa writableDatabase.withinTransaction { db -> messages.deleteThread(threadId) drafts.clearDrafts(threadId) - db.delete(TABLE_NAME) - .where("$ID = ?", threadId) - .run() + db.deactivateThread(threadId) synchronized(threadIdCache) { threadIdCache.remove(recipientIdForThreadId) } @@ -1078,7 +1081,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa val queries: List = SqlUtil.buildCollectionQuery(ID, selectedConversations) writableDatabase.withinTransaction { db -> for (query in queries) { - db.delete(TABLE_NAME, query.where, query.whereArgs) + db.deactivateThread(query) } messages.deleteAbandonedMessages() @@ -1100,13 +1103,21 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa ConversationUtil.clearShortcuts(context, recipientIds) } + @VisibleForTesting + fun clearForTests() { + writableDatabase.withinTransaction { + deleteAllConversations() + it.delete(TABLE_NAME).run() + } + } + @SuppressLint("DiscouragedApi") fun deleteAllConversations() { writableDatabase.withinTransaction { db -> messageLog.deleteAll() messages.deleteAllThreads() drafts.clearAllDrafts() - db.delete(TABLE_NAME, null, null) + db.deactivateThreads() calls.deleteAllCalls() synchronized(threadIdCache) { threadIdCache.clear() @@ -1316,7 +1327,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa if (pinnedRecipient != null) { db.update(TABLE_NAME) - .values(PINNED to pinnedPosition) + .values(PINNED to pinnedPosition, ACTIVE to 1) .where("$RECIPIENT_ID = ?", pinnedRecipient.id) .run() } @@ -1598,6 +1609,48 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa } } + private fun SQLiteDatabase.deactivateThreads() { + deactivateThread(query = null) + } + + private fun SQLiteDatabase.deactivateThread(threadId: Long) { + deactivateThread(SqlUtil.Query("$ID = ?", SqlUtil.buildArgs(threadId))) + } + + private fun SQLiteDatabase.deactivateThread(query: SqlUtil.Query?) { + val update = update(TABLE_NAME) + .values( + DATE to 0, + MEANINGFUL_MESSAGES to 0, + READ to ReadStatus.READ.serialize(), + TYPE to 0, + ERROR to 0, + SNIPPET to null, + SNIPPET_TYPE to 0, + SNIPPET_URI to null, + SNIPPET_CONTENT_TYPE to null, + SNIPPET_EXTRAS to null, + UNREAD_COUNT to 0, + ARCHIVED to 0, + STATUS to 0, + DELIVERY_RECEIPT_COUNT to 0, + READ_RECEIPT_COUNT to 0, + EXPIRES_IN to 0, + LAST_SEEN to 0, + HAS_SENT to 0, + LAST_SCROLLED to 0, + PINNED to 0, + UNREAD_SELF_MENTION_COUNT to 0, + ACTIVE to 0 + ) + + if (query != null) { + update.where(query.where, query.whereArgs).run() + } else { + update.run() + } + } + private fun getAttachmentUriFor(record: MessageRecord): Uri? { if (!record.isMms || record.isMmsNotification || record.isGroupAction) { return null @@ -1721,7 +1774,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa SELECT group_id, GROUP_CONCAT(${GroupTable.MembershipTable.TABLE_NAME}.${GroupTable.MembershipTable.RECIPIENT_ID}) as ${GroupTable.MEMBER_GROUP_CONCAT} FROM ${GroupTable.MembershipTable.TABLE_NAME} ) as MembershipAlias ON MembershipAlias.${GroupTable.MembershipTable.GROUP_ID} = ${GroupTable.TABLE_NAME}.${GroupTable.GROUP_ID} - WHERE $where + WHERE $TABLE_NAME.$ACTIVE = 1 AND $where ORDER BY $orderBy """ diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index 2bf69bf450..71b988c2ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -54,6 +54,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V195_GroupMemberFor import org.thoughtcrime.securesms.database.helpers.migration.V196_BackCallLinksWithRecipientV2 import org.thoughtcrime.securesms.database.helpers.migration.V197_DropAvatarColorFromCallLinks import org.thoughtcrime.securesms.database.helpers.migration.V198_AddMacDigestColumn +import org.thoughtcrime.securesms.database.helpers.migration.V199_AddThreadActiveColumn /** * Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness. @@ -62,7 +63,7 @@ object SignalDatabaseMigrations { val TAG: String = Log.tag(SignalDatabaseMigrations.javaClass) - const val DATABASE_VERSION = 198 + const val DATABASE_VERSION = 199 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { @@ -265,6 +266,10 @@ object SignalDatabaseMigrations { if (oldVersion < 198) { V198_AddMacDigestColumn.migrate(context, db, oldVersion, newVersion) } + + if (oldVersion < 199) { + V199_AddThreadActiveColumn.migrate(context, db, oldVersion, newVersion) + } } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V199_AddThreadActiveColumn.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V199_AddThreadActiveColumn.kt new file mode 100644 index 0000000000..f40cf50b6e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V199_AddThreadActiveColumn.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import net.zetetic.database.sqlcipher.SQLiteDatabase + +/** + * Adds the ACTIVE column to the THREAD table, and mark all current threads active. + * + * For performance and maintainability reasons, instead of explicitly deleting thread rows from the + * database, we instead want to mark them as inactive and remove any backing data (messages, etc.), + * essentially turning them into tombstones, which can be 'resurrected' when a new attempt is made + * to chat with whomever the thread is tied to. + */ +@Suppress("ClassName") +object V199_AddThreadActiveColumn : SignalDatabaseMigration { + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("ALTER TABLE thread ADD COLUMN active INTEGER DEFAULT 0") + db.execSQL("UPDATE thread SET active = 1") + + db.execSQL("DROP INDEX IF EXISTS thread_recipient_id_index") + db.execSQL("DROP INDEX IF EXISTS archived_count_index") + + db.execSQL("CREATE INDEX IF NOT EXISTS thread_recipient_id_index ON thread (recipient_id, active)") + db.execSQL("CREATE INDEX IF NOT EXISTS archived_count_index ON thread (active, archived, meaningful_messages, pinned)") + db.execSQL("CREATE INDEX IF NOT EXISTS thread_active ON thread (active)") + } +}