diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/helpers/migration/MyStoryMigrationTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/helpers/migration/MyStoryMigrationTest.kt new file mode 100644 index 0000000000..bf37a0504e --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/helpers/migration/MyStoryMigrationTest.kt @@ -0,0 +1,119 @@ +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import androidx.core.content.contentValuesOf +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Assert.fail +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.signal.core.util.SqlUtil +import org.thoughtcrime.securesms.database.DistributionListDatabase +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.database.model.DistributionListId +import org.thoughtcrime.securesms.testing.SignalDatabaseRule +import org.whispersystems.signalservice.api.push.DistributionId +import java.util.UUID + +@RunWith(AndroidJUnit4::class) +class MyStoryMigrationTest { + + @get:Rule val harness = SignalDatabaseRule(deleteAllThreadsOnEachRun = false) + + @Test + fun givenAValidMyStory_whenIMigrate_thenIExpectMyStoryToBeValid() { + // GIVEN + assertValidMyStoryExists() + + // WHEN + runMigration() + + // THEN + assertValidMyStoryExists() + } + + @Test + fun givenNoMyStory_whenIMigrate_thenIExpectMyStoryToBeCreated() { + // GIVEN + deleteMyStory() + + // WHEN + runMigration() + + // THEN + assertValidMyStoryExists() + } + + @Test + fun givenA00000000DistributionIdForMyStory_whenIMigrate_thenIExpectMyStoryToBeCreated() { + // GIVEN + setMyStoryDistributionId("0000-0000") + + // WHEN + runMigration() + + // THEN + assertValidMyStoryExists() + } + + @Test + fun givenARandomDistributionIdForMyStory_whenIMigrate_thenIExpectMyStoryToBeCreated() { + // GIVEN + setMyStoryDistributionId(UUID.randomUUID().toString()) + + // WHEN + runMigration() + + // THEN + assertValidMyStoryExists() + } + + private fun setMyStoryDistributionId(serializedId: String) { + SignalDatabase.rawDatabase.update( + DistributionListDatabase.LIST_TABLE_NAME, + contentValuesOf( + DistributionListDatabase.DISTRIBUTION_ID to serializedId + ), + "_id = ?", + SqlUtil.buildArgs(DistributionListId.MY_STORY) + ) + } + + private fun deleteMyStory() { + SignalDatabase.rawDatabase.delete( + DistributionListDatabase.LIST_TABLE_NAME, + "_id = ?", + SqlUtil.buildArgs(DistributionListId.MY_STORY) + ) + } + + private fun assertValidMyStoryExists() { + SignalDatabase.rawDatabase.query( + DistributionListDatabase.LIST_TABLE_NAME, + SqlUtil.COUNT, + "_id = ? AND ${DistributionListDatabase.DISTRIBUTION_ID} = ?", + SqlUtil.buildArgs(DistributionListId.MY_STORY, DistributionId.MY_STORY.toString()), + null, + null, + null + ).use { + if (it.moveToNext()) { + val count = it.getInt(0) + assertEquals("assertValidMyStoryExists: Query produced an unexpected count.", 1, count) + } else { + fail("assertValidMyStoryExists: Query did not produce a count.") + } + } + } + + private fun runMigration() { + MyStoryMigration.migrate( + InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application, + SignalDatabase.rawDatabase, + 0, + 1 + ) + } +} 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 81f99aa19a..38944de4ec 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 @@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColors import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper.entrySet import org.thoughtcrime.securesms.database.KeyValueDatabase import org.thoughtcrime.securesms.database.RecipientDatabase +import org.thoughtcrime.securesms.database.helpers.migration.MyStoryMigration import org.thoughtcrime.securesms.database.helpers.migration.UrgentMslFlagMigration import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList import org.thoughtcrime.securesms.dependencies.ApplicationDependencies @@ -205,8 +206,9 @@ object SignalDatabaseMigrations { private const val MY_STORY_PRIVACY_MODE = 148 private const val EXPIRING_PROFILE_CREDENTIALS = 149 private const val URGENT_FLAG = 150 + private const val MY_STORY_MIGRATION = 151 - const val DATABASE_VERSION = 150 + const val DATABASE_VERSION = 151 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { @@ -2669,6 +2671,10 @@ object SignalDatabaseMigrations { if (oldVersion < URGENT_FLAG) { UrgentMslFlagMigration.migrate(context, db, oldVersion, newVersion) } + + if (oldVersion < MY_STORY_MIGRATION) { + MyStoryMigration.migrate(context, db, oldVersion, newVersion) + } } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/MyStoryMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/MyStoryMigration.kt new file mode 100644 index 0000000000..8ebef7dd4c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/MyStoryMigration.kt @@ -0,0 +1,130 @@ +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import android.database.Cursor +import androidx.core.content.contentValuesOf +import net.zetetic.database.sqlcipher.SQLiteDatabase +import org.signal.core.util.CursorUtil +import org.signal.core.util.SqlUtil +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.storage.StorageSyncHelper +import org.thoughtcrime.securesms.util.Base64 + +/** + * Performs a check and ensures that MyStory exists at the correct distribution list id and correct distribution id. + */ +object MyStoryMigration : SignalDatabaseMigration { + + private val TAG = Log.tag(MyStoryMigration::class.java) + + private const val TABLE_NAME = "distribution_list" + private const val NAME = "name" + private const val DISTRIBUTION_LIST_ID = "_id" + private const val DISTRIBUTION_ID = "distribution_id" + private const val RECIPIENT_ID = "recipient_id" + private const val PRIVACY_MODE = "privacy_mode" + private const val MY_STORY_DISTRIBUTION_LIST_ID = 1 + private const val MY_STORY_DISTRIBUTION_ID = "00000000-0000-0000-0000-000000000000" + private const val MY_STORY_PRIVACY_MODE = 2 + + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + val result: MyStoryExistsResult = getMyStoryCursor(db).use { cursor -> + if (cursor.moveToNext()) { + val distributionId = CursorUtil.requireString(cursor, DISTRIBUTION_ID) + if (distributionId != MY_STORY_DISTRIBUTION_ID) { + Log.d(TAG, "[migrate] Invalid MyStory DistributionId: $distributionId") + MyStoryExistsResult.REQUIRES_DISTRIBUTION_ID_UPDATE + } else { + Log.d(TAG, "[migrate] MyStory DistributionId matches expected value.") + MyStoryExistsResult.MATCHES_EXPECTED_VALUE + } + } else { + Log.d(TAG, "[migrate] My Story does not exist.") + MyStoryExistsResult.DOES_NOT_EXIST + } + } + + when (result) { + MyStoryExistsResult.REQUIRES_DISTRIBUTION_ID_UPDATE -> updateDistributionIdToExpectedValue(db) + MyStoryExistsResult.MATCHES_EXPECTED_VALUE -> Unit + MyStoryExistsResult.DOES_NOT_EXIST -> createMyStory(db) + } + } + + private fun updateDistributionIdToExpectedValue(db: SQLiteDatabase) { + Log.d(TAG, "[updateDistributionIdToExpectedValue] Overwriting My Story DistributionId with expected value.") + db.update( + TABLE_NAME, + contentValuesOf(DISTRIBUTION_ID to MY_STORY_DISTRIBUTION_ID), + "$DISTRIBUTION_LIST_ID = ?", + arrayOf(MY_STORY_DISTRIBUTION_LIST_ID.toString()) + ) + } + + private fun createMyStory(db: SQLiteDatabase) { + Log.d(TAG, "[createMyStory] Attempting to create My Story.") + + val recipientId: Long = getMyStoryRecipientId(db) ?: createMyStoryRecipientId(db) + + db.insert( + TABLE_NAME, + null, + contentValuesOf( + DISTRIBUTION_LIST_ID to MY_STORY_DISTRIBUTION_LIST_ID, + NAME to MY_STORY_DISTRIBUTION_ID, + DISTRIBUTION_ID to MY_STORY_DISTRIBUTION_ID, + RECIPIENT_ID to recipientId, + PRIVACY_MODE to MY_STORY_PRIVACY_MODE + ) + ) + } + + private fun createMyStoryRecipientId(db: SQLiteDatabase): Long { + return db.insert( + "recipient", + null, + contentValuesOf( + "group_type" to 4, + "distribution_list_id" to MY_STORY_DISTRIBUTION_LIST_ID, + "storage_service_key" to Base64.encodeBytes(StorageSyncHelper.generateKey()), + "profile_sharing" to 1 + ) + ) + } + + private fun getMyStoryRecipientId(db: SQLiteDatabase): Long? { + return db.query( + "recipient", + arrayOf("_id"), + "distribution_list_id = ?", + SqlUtil.buildArgs(MY_STORY_DISTRIBUTION_LIST_ID), + null, + null, + null + ).use { + if (it.moveToNext()) { + CursorUtil.requireLong(it, "_id") + } else { + null + } + } + } + + private fun getMyStoryCursor(db: SQLiteDatabase): Cursor { + return db.query( + TABLE_NAME, + arrayOf(DISTRIBUTION_ID), + "$DISTRIBUTION_LIST_ID = ?", + arrayOf(MY_STORY_DISTRIBUTION_LIST_ID.toString()), + null, + null, + null + ) + } + + private enum class MyStoryExistsResult { + REQUIRES_DISTRIBUTION_ID_UPDATE, + MATCHES_EXPECTED_VALUE, + DOES_NOT_EXIST + } +}