diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/KyberPreKeyTableTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/KyberPreKeyTableTest.kt index f5582006db..4736aac117 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/KyberPreKeyTableTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/KyberPreKeyTableTest.kt @@ -51,7 +51,7 @@ class KyberPreKeyTableTest { insertTestRecord(aci, id = 4, staleTime = 15) insertTestRecord(aci, id = 5, staleTime = 0) - SignalDatabase.oneTimePreKeys.deleteAllStaleBefore(aci, threshold = 11, minCount = 0) + SignalDatabase.kyberPreKeys.deleteAllStaleBefore(aci, threshold = 11, minCount = 0) assertNull(getStaleTime(aci, 1)) assertNull(getStaleTime(aci, 2)) 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 71a726c30b..f7938c1f6a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.kt @@ -83,15 +83,14 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT const val AVATAR_ID = "avatar_id" const val AVATAR_KEY = "avatar_key" const val AVATAR_CONTENT_TYPE = "avatar_content_type" - const val AVATAR_RELAY = "avatar_relay" const val AVATAR_DIGEST = "avatar_digest" const val TIMESTAMP = "timestamp" const val ACTIVE = "active" const val MMS = "mms" const val EXPECTED_V2_ID = "expected_v2_id" - const val UNMIGRATED_V1_MEMBERS = "former_v1_members" + const val UNMIGRATED_V1_MEMBERS = "unmigrated_v1_members" const val DISTRIBUTION_ID = "distribution_id" - const val SHOW_AS_STORY_STATE = "display_as_story" + const val SHOW_AS_STORY_STATE = "show_as_story_state" const val LAST_FORCE_UPDATE_TIMESTAMP = "last_force_update_timestamp" /** 32 bytes serialized [GroupMasterKey] */ @@ -103,44 +102,33 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT /** Serialized [DecryptedGroup] protobuf */ const val V2_DECRYPTED_GROUP = "decrypted_group" - /** Was temporarily used for PNP accept by pni but is no longer needed/updated */ - @Deprecated("") - private val AUTH_SERVICE_ID = "auth_service_id" - @JvmField val CREATE_TABLE = """ CREATE TABLE $TABLE_NAME ( $ID INTEGER PRIMARY KEY, - $GROUP_ID TEXT, - $RECIPIENT_ID INTEGER, - $TITLE TEXT, - $AVATAR_ID INTEGER, - $AVATAR_KEY BLOB, - $AVATAR_CONTENT_TYPE TEXT, - $AVATAR_RELAY TEXT, - $TIMESTAMP INTEGER, + $GROUP_ID TEXT NOT NULL UNIQUE, + $RECIPIENT_ID INTEGER NOT NULL UNIQUE REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE, + $TITLE TEXT DEFAULT NULL, + $AVATAR_ID INTEGER DEFAULT 0, + $AVATAR_KEY BLOB DEFAULT NULL, + $AVATAR_CONTENT_TYPE TEXT DEFAULT NULL, + $AVATAR_DIGEST BLOB DEFAULT NULL, + $TIMESTAMP INTEGER DEFAULT 0, $ACTIVE INTEGER DEFAULT 1, - $AVATAR_DIGEST BLOB, $MMS INTEGER DEFAULT 0, - $V2_MASTER_KEY BLOB, - $V2_REVISION BLOB, - $V2_DECRYPTED_GROUP BLOB, - $EXPECTED_V2_ID TEXT DEFAULT NULL, + $V2_MASTER_KEY BLOB DEFAULT NULL, + $V2_REVISION BLOB DEFAULT NULL, + $V2_DECRYPTED_GROUP BLOB DEFAULT NULL, + $EXPECTED_V2_ID TEXT UNIQUE DEFAULT NULL, $UNMIGRATED_V1_MEMBERS TEXT DEFAULT NULL, - $DISTRIBUTION_ID TEXT DEFAULT NULL, - $SHOW_AS_STORY_STATE INTEGER DEFAULT 0, - $AUTH_SERVICE_ID TEXT DEFAULT NULL, + $DISTRIBUTION_ID TEXT UNIQUE DEFAULT NULL, + $SHOW_AS_STORY_STATE INTEGER DEFAULT ${ShowAsStoryState.IF_ACTIVE.code}, $LAST_FORCE_UPDATE_TIMESTAMP INTEGER DEFAULT 0 ) """ @JvmField - val CREATE_INDEXS = arrayOf( - "CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON $TABLE_NAME ($GROUP_ID);", - "CREATE UNIQUE INDEX IF NOT EXISTS group_recipient_id_index ON $TABLE_NAME ($RECIPIENT_ID);", - "CREATE UNIQUE INDEX IF NOT EXISTS expected_v2_id_index ON $TABLE_NAME ($EXPECTED_V2_ID);", - "CREATE UNIQUE INDEX IF NOT EXISTS group_distribution_id_index ON $TABLE_NAME($DISTRIBUTION_ID);" - ) + MembershipTable.CREATE_INDEXES + val CREATE_INDEXS = MembershipTable.CREATE_INDEXES private val GROUP_PROJECTION = arrayOf( GROUP_ID, @@ -150,7 +138,6 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, - AVATAR_RELAY, AVATAR_DIGEST, TIMESTAMP, ACTIVE, @@ -194,7 +181,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT val CREATE_TABLE = """ CREATE TABLE $TABLE_NAME ( $ID INTEGER PRIMARY KEY, - $GROUP_ID TEXT NOT NULL, + $GROUP_ID TEXT NOT NULL REFERENCES ${GroupTable.TABLE_NAME} (${GroupTable.GROUP_ID}) ON DELETE CASCADE, $RECIPIENT_ID INTEGER NOT NULL REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE, UNIQUE($GROUP_ID, $RECIPIENT_ID) ) @@ -656,17 +643,17 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT } @CheckReturnValue - fun create(groupId: GroupId.V1, title: String?, members: Collection, avatar: SignalServiceAttachmentPointer?, relay: String?): Boolean { + fun create(groupId: GroupId.V1, title: String?, members: Collection, avatar: SignalServiceAttachmentPointer?): Boolean { if (groupExists(groupId.deriveV2MigrationGroupId())) { throw LegacyGroupInsertException(groupId) } - return create(groupId, title, members, avatar, relay, null, null) + return create(groupId, title, members, avatar, null, null) } @CheckReturnValue fun create(groupId: GroupId.Mms, title: String?, members: Collection): Boolean { - return create(groupId, if (title.isNullOrEmpty()) null else title, members, null, null, null, null) + return create(groupId, if (title.isNullOrEmpty()) null else title, members, null, null, null) } @JvmOverloads @@ -680,7 +667,7 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT Log.w(TAG, "Forcing the creation of a group even though we already have a V1 ID!") } - return if (create(groupId = groupId, title = groupState.title, memberCollection = emptyList(), avatar = null, relay = null, groupMasterKey = groupMasterKey, groupState = groupState)) { + return if (create(groupId = groupId, title = groupState.title, memberCollection = emptyList(), avatar = null, groupMasterKey = groupMasterKey, groupState = groupState)) { groupId } else { null @@ -731,7 +718,6 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT title: String?, memberCollection: Collection, avatar: SignalServiceAttachmentPointer?, - relay: String?, groupMasterKey: GroupMasterKey?, groupState: DecryptedGroup? ): Boolean { @@ -757,7 +743,6 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT values.put(AVATAR_ID, 0) } - values.put(AVATAR_RELAY, relay) values.put(TIMESTAMP, System.currentTimeMillis()) if (groupId.isV2) { @@ -1176,7 +1161,6 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT avatarId = cursor.requireLong(AVATAR_ID), avatarKey = cursor.requireBlob(AVATAR_KEY), avatarContentType = cursor.requireString(AVATAR_CONTENT_TYPE), - relay = cursor.requireString(AVATAR_RELAY), isActive = cursor.requireBoolean(ACTIVE), avatarDigest = cursor.requireBlob(AVATAR_DIGEST), isMms = cursor.requireBoolean(MMS), 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 ad72f536d3..918ce7fb47 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 @@ -59,6 +59,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V200_ResetPniColumn import org.thoughtcrime.securesms.database.helpers.migration.V201_RecipientTableValidations import org.thoughtcrime.securesms.database.helpers.migration.V202_DropMessageTableThreadDateIndex import org.thoughtcrime.securesms.database.helpers.migration.V203_PreKeyStaleTimestamp +import org.thoughtcrime.securesms.database.helpers.migration.V204_GroupForeignKeyMigration /** * Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness. @@ -67,7 +68,7 @@ object SignalDatabaseMigrations { val TAG: String = Log.tag(SignalDatabaseMigrations.javaClass) - const val DATABASE_VERSION = 203 + const val DATABASE_VERSION = 204 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { @@ -290,6 +291,10 @@ object SignalDatabaseMigrations { if (oldVersion < 203) { V203_PreKeyStaleTimestamp.migrate(context, db, oldVersion, newVersion) } + + if (oldVersion < 204) { + V204_GroupForeignKeyMigration.migrate(context, db, oldVersion, newVersion) + } } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V204_GroupForeignKeyMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V204_GroupForeignKeyMigration.kt new file mode 100644 index 0000000000..0125bf55cb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V204_GroupForeignKeyMigration.kt @@ -0,0 +1,121 @@ +/* + * 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 +import org.signal.core.util.SqlUtil +import org.signal.core.util.Stopwatch +import org.signal.core.util.logging.Log + +/** + * Back CallLinks with a Recipient to ease integration and ensure we can support + * different features which would require that relation in the future. + */ +@Suppress("ClassName") +object V204_GroupForeignKeyMigration : SignalDatabaseMigration { + + private val TAG = Log.tag(V204_GroupForeignKeyMigration::class.java) + + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + val stopwatch = Stopwatch("migration") + db.execSQL( + """ + CREATE TABLE groups_tmp ( + _id INTEGER PRIMARY KEY, + group_id TEXT NOT NULL UNIQUE, + recipient_id INTEGER NOT NULL UNIQUE REFERENCES recipient (_id) ON DELETE CASCADE, + title TEXT DEFAULT NULL, + avatar_id INTEGER DEFAULT 0, + avatar_key BLOB DEFAULT NULL, + avatar_content_type TEXT DEFAULT NULL, + avatar_digest BLOB DEFAULT NULL, + timestamp INTEGER DEFAULT 0, + active INTEGER DEFAULT 1, + mms INTEGER DEFAULT 0, + master_key BLOB DEFAULT NULL, + revision BLOB DEFAULT NULL, + decrypted_group BLOB DEFAULT NULL, + expected_v2_id TEXT UNIQUE DEFAULT NULL, + unmigrated_v1_members TEXT DEFAULT NULL, + distribution_id TEXT UNIQUE DEFAULT NULL, + show_as_story_state INTEGER DEFAULT 0, + last_force_update_timestamp INTEGER DEFAULT 0 + ) + """ + ) + + val danglingRecipientCount = db.delete("groups", "recipient_id NOT IN (SELECT _id FROM recipient)", null) + Log.i(TAG, "There were $danglingRecipientCount groups that referenced non-existent recipients.") + + db.execSQL( + """ + INSERT INTO groups_tmp SELECT + _id, + group_id, + recipient_id, + title, + avatar_id, + avatar_key, + avatar_content_Type, + avatar_digest, + timestamp, + active, + mms, + master_key, + revision, + decrypted_group, + expected_v2_id, + former_v1_members, + distribution_id, + display_as_story, + last_force_update_timestamp + FROM groups + """ + ) + stopwatch.split("groups-table") + + db.execSQL("DROP TABLE groups") + db.execSQL("ALTER TABLE groups_tmp RENAME TO groups") + + db.execSQL( + """ + CREATE TABLE group_membership_tmp ( + _id INTEGER PRIMARY KEY, + group_id TEXT NOT NULL REFERENCES groups (group_id) ON DELETE CASCADE, + recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, + UNIQUE(group_id, recipient_id) + ) + """ + ) + + val danglingMemberCount = db.delete("group_membership", "group_id NOT IN (SELECT group_id FROM groups)", null) + Log.i(TAG, "There were $danglingMemberCount members that referenced non-existent groups.") + + db.execSQL( + """ + INSERT INTO group_membership_tmp SELECT * FROM group_membership + """ + ) + stopwatch.split("membership-table") + + db.execSQL("DROP TABLE group_membership") + db.execSQL("ALTER TABLE group_membership_tmp RENAME TO group_membership") + + db.execSQL("CREATE INDEX IF NOT EXISTS group_membership_recipient_id ON group_membership (recipient_id)") + + stopwatch.split("membership-index") + + val foreignKeyViolations: List = SqlUtil.getForeignKeyViolations(db, "groups") + SqlUtil.getForeignKeyViolations(db, "group_membership") + if (foreignKeyViolations.isNotEmpty()) { + Log.w(TAG, "Foreign key violations!\n${foreignKeyViolations.joinToString(separator = "\n")}") + throw IllegalStateException("Foreign key violations!") + } + stopwatch.split("fk-check") + + stopwatch.stop(TAG) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupRecord.kt index 387c11a09b..b7b8942b39 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupRecord.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupRecord.kt @@ -24,7 +24,6 @@ class GroupRecord( val avatarId: Long, val avatarKey: ByteArray?, val avatarContentType: String?, - val relay: String?, val isActive: Boolean, val avatarDigest: ByteArray?, val isMms: Boolean, diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java index 3bf846a979..3d4046c74f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java @@ -59,7 +59,7 @@ final class GroupManagerV1 { if (groupId.isV1()) { GroupId.V1 groupIdV1 = groupId.requireV1(); - groupDatabase.create(groupIdV1, name, memberIds, null, null); + groupDatabase.create(groupIdV1, name, memberIds, null); try { AvatarHelper.setAvatar(context, groupRecipientId, avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV1DownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV1DownloadJob.java index 359f9fea48..3eaf2370b8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV1DownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarGroupsV1DownloadJob.java @@ -70,7 +70,6 @@ public final class AvatarGroupsV1DownloadJob extends BaseJob { long avatarId = record.get().getAvatarId(); String contentType = record.get().getAvatarContentType(); byte[] key = record.get().getAvatarKey(); - String relay = record.get().getRelay(); Optional digest = Optional.ofNullable(record.get().getAvatarDigest()); Optional fileName = Optional.empty(); diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/GroupTestUtil.kt b/app/src/test/java/org/thoughtcrime/securesms/database/GroupTestUtil.kt index 239beccc59..815f72dd70 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/GroupTestUtil.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/GroupTestUtil.kt @@ -168,7 +168,6 @@ fun groupRecord( avatarId: Long = 1, avatarKey: ByteArray = ByteArray(0), avatarContentType: String = "", - relay: String = "", active: Boolean = true, avatarDigest: ByteArray = ByteArray(0), mms: Boolean = false, @@ -184,7 +183,6 @@ fun groupRecord( avatarId, avatarKey, avatarContentType, - relay, active, avatarDigest, mms,