From e9d98b7d39ebf147de1138690cca270604cd793e Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 14 Dec 2022 14:52:54 -0500 Subject: [PATCH] Migrate SMS messages into the MMS table. --- ...est_gifts.kt => MessageTableTest_gifts.kt} | 4 +- .../database/MmsTableTest_stories.kt | 2 +- ...est_collapseJoinRequestEventsIfPossible.kt | 2 +- .../MmsNotificationAttachment.java | 8 +- .../securesms/backup/BackupCountQueries.kt | 15 +- .../securesms/backup/FullBackupExporter.java | 42 +- .../securesms/backup/FullBackupImporter.java | 3 +- .../conversation/ConversationFragment.java | 6 +- .../error/SafetyNumberChangeRepository.java | 9 +- .../ConversationListDataSource.java | 6 +- .../ConversationListItem.java | 41 +- .../securesms/database/AttachmentTable.java | 2 +- .../securesms/database/GroupReceiptTable.kt | 2 +- .../securesms/database/GroupTable.java | 7 +- .../securesms/database/MediaTable.java | 40 +- .../securesms/database/MentionTable.java | 2 +- .../database/MessageSendLogTables.kt | 10 +- .../securesms/database/MessageTable.java | 4097 ++++++++++++++++- .../securesms/database/MmsSmsColumns.java | 12 +- .../securesms/database/MmsSmsTable.java | 370 +- .../securesms/database/MmsTable.java | 3152 ------------- .../securesms/database/ReactionTable.kt | 40 +- .../securesms/database/RecipientTable.kt | 3 +- .../securesms/database/SearchTable.kt | 96 +- .../securesms/database/SignalDatabase.kt | 16 +- .../securesms/database/SmsTable.java | 2062 --------- .../securesms/database/StorySendTable.kt | 24 +- .../database/helpers/ClassicOpenHelper.java | 24 +- .../helpers/SignalDatabaseMigrations.kt | 7 +- .../V168_SingleMessageTableMigration.kt | 91 + .../database/model/DisplayRecord.java | 45 +- .../database/model/MediaMmsMessageRecord.java | 12 +- .../securesms/database/model/MessageId.kt | 15 + .../database/model/MessageRecord.java | 23 +- .../model/NotificationMmsMessageRecord.java | 8 +- .../database/model/SmsMessageRecord.java | 11 +- .../securesms/database/model/StatusUtil.java | 9 +- .../database/model/ThreadRecord.java | 7 +- .../exporter/SignalSmsExportReader.kt | 10 +- .../securesms/jobs/MmsDownloadJob.java | 15 +- .../securesms/jobs/PushGroupSendJob.java | 3 +- .../longmessage/LongMessageRepository.java | 6 +- .../MessageDetailsFragment.java | 5 +- .../MessageDetailsRepository.java | 4 +- .../MessageDetailsViewModel.java | 10 +- .../messages/MessageContentProcessor.java | 5 +- .../migrations/LegacyMigrationJob.java | 5 +- .../service/ExpiringMessageManager.java | 6 +- .../service/SmsDeliveryListener.java | 8 +- .../securesms/sms/MessageSender.java | 9 +- .../reply/group/StoryGroupReplyDataSource.kt | 6 +- .../database/GV2UpdateTransformer.kt | 2 +- .../securesms/database/IsStoryTransformer.kt | 6 +- .../securesms/database/MmsDatabaseTest.kt | 30 +- .../securesms/database/MmsSmsDatabaseTest.kt | 9 +- .../securesms/database/SmsDatabaseTest.kt | 32 +- .../securesms/database/TestMms.kt | 30 +- .../securesms/database/TestSms.kt | 8 +- .../main/java/org/signal/core/util/SqlUtil.kt | 11 + 59 files changed, 4399 insertions(+), 6146 deletions(-) rename app/src/androidTest/java/org/thoughtcrime/securesms/database/{MmsTableTest_gifts.kt => MessageTableTest_gifts.kt} (98%) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/MmsTable.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/SmsTable.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V168_SingleMessageTableMigration.kt diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/MmsTableTest_gifts.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/MessageTableTest_gifts.kt similarity index 98% rename from app/src/androidTest/java/org/thoughtcrime/securesms/database/MmsTableTest_gifts.kt rename to app/src/androidTest/java/org/thoughtcrime/securesms/database/MessageTableTest_gifts.kt index 83f10a75eb..1c2ef9216b 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/MmsTableTest_gifts.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/MessageTableTest_gifts.kt @@ -17,8 +17,8 @@ import java.util.UUID @Suppress("ClassName") @RunWith(AndroidJUnit4::class) -class MmsTableTest_gifts { - private lateinit var mms: MmsTable +class MessageTableTest_gifts { + private lateinit var mms: MessageTable private val localAci = ACI.from(UUID.randomUUID()) private val localPni = PNI.from(UUID.randomUUID()) diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/MmsTableTest_stories.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/MmsTableTest_stories.kt index 267034a9bc..66817e0147 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/MmsTableTest_stories.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/MmsTableTest_stories.kt @@ -26,7 +26,7 @@ import java.util.concurrent.TimeUnit @RunWith(AndroidJUnit4::class) class MmsTableTest_stories { - private lateinit var mms: MmsTable + private lateinit var mms: MessageTable private val localAci = ACI.from(UUID.randomUUID()) private val localPni = PNI.from(UUID.randomUUID()) diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/SmsDatabaseTest_collapseJoinRequestEventsIfPossible.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/SmsDatabaseTest_collapseJoinRequestEventsIfPossible.kt index db14edf409..01745ee3c7 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/SmsDatabaseTest_collapseJoinRequestEventsIfPossible.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/SmsDatabaseTest_collapseJoinRequestEventsIfPossible.kt @@ -32,7 +32,7 @@ import java.util.UUID class SmsDatabaseTest_collapseJoinRequestEventsIfPossible { private lateinit var recipients: RecipientTable - private lateinit var sms: SmsTable + private lateinit var sms: MessageTable private val localAci = ACI.from(UUID.randomUUID()) private val localPni = PNI.from(UUID.randomUUID()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java index 807692c991..3135101c4f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java @@ -6,7 +6,7 @@ import android.net.Uri; import androidx.annotation.Nullable; import org.thoughtcrime.securesms.database.AttachmentTable; -import org.thoughtcrime.securesms.database.MmsTable; +import org.thoughtcrime.securesms.database.MessageTable; public class MmsNotificationAttachment extends Attachment { @@ -26,11 +26,11 @@ public class MmsNotificationAttachment extends Attachment { } private static int getTransferStateFromStatus(int status) { - if (status == MmsTable.Status.DOWNLOAD_INITIALIZED || - status == MmsTable.Status.DOWNLOAD_NO_CONNECTIVITY) + if (status == MessageTable.MmsStatus.DOWNLOAD_INITIALIZED || + status == MessageTable.MmsStatus.DOWNLOAD_NO_CONNECTIVITY) { return AttachmentTable.TRANSFER_PROGRESS_PENDING; - } else if (status == MmsTable.Status.DOWNLOAD_CONNECTING) { + } else if (status == MessageTable.MmsStatus.DOWNLOAD_CONNECTING) { return AttachmentTable.TRANSFER_PROGRESS_STARTED; } else { return AttachmentTable.TRANSFER_PROGRESS_FAILED; diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupCountQueries.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupCountQueries.kt index 88b57b0349..e9d5a3dde6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupCountQueries.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupCountQueries.kt @@ -2,29 +2,26 @@ package org.thoughtcrime.securesms.backup import org.thoughtcrime.securesms.database.AttachmentTable import org.thoughtcrime.securesms.database.GroupReceiptTable -import org.thoughtcrime.securesms.database.MmsTable -import org.thoughtcrime.securesms.database.SmsTable +import org.thoughtcrime.securesms.database.MessageTable /** * Queries used by backup exporter to estimate total counts for various complicated tables. */ object BackupCountQueries { - const val mmsCount: String = "SELECT COUNT(*) FROM ${MmsTable.TABLE_NAME} WHERE ${MmsTable.EXPIRES_IN} <= 0 AND ${MmsTable.VIEW_ONCE} <= 0" - - const val smsCount: String = "SELECT COUNT(*) FROM ${SmsTable.TABLE_NAME} WHERE ${SmsTable.EXPIRES_IN} <= 0" + const val mmsCount: String = "SELECT COUNT(*) FROM ${MessageTable.TABLE_NAME} WHERE ${MessageTable.EXPIRES_IN} <= 0 AND ${MessageTable.VIEW_ONCE} <= 0" @get:JvmStatic val groupReceiptCount: String = """ SELECT COUNT(*) FROM ${GroupReceiptTable.TABLE_NAME} - INNER JOIN ${MmsTable.TABLE_NAME} ON ${GroupReceiptTable.TABLE_NAME}.${GroupReceiptTable.MMS_ID} = ${MmsTable.TABLE_NAME}.${MmsTable.ID} - WHERE ${MmsTable.TABLE_NAME}.${MmsTable.EXPIRES_IN} <= 0 AND ${MmsTable.TABLE_NAME}.${MmsTable.VIEW_ONCE} <= 0 + INNER JOIN ${MessageTable.TABLE_NAME} ON ${GroupReceiptTable.TABLE_NAME}.${GroupReceiptTable.MMS_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.ID} + WHERE ${MessageTable.TABLE_NAME}.${MessageTable.EXPIRES_IN} <= 0 AND ${MessageTable.TABLE_NAME}.${MessageTable.VIEW_ONCE} <= 0 """.trimIndent() @get:JvmStatic val attachmentCount: String = """ SELECT COUNT(*) FROM ${AttachmentTable.TABLE_NAME} - INNER JOIN ${MmsTable.TABLE_NAME} ON ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MMS_ID} = ${MmsTable.TABLE_NAME}.${MmsTable.ID} - WHERE ${MmsTable.TABLE_NAME}.${MmsTable.EXPIRES_IN} <= 0 AND ${MmsTable.TABLE_NAME}.${MmsTable.VIEW_ONCE} <= 0 + INNER JOIN ${MessageTable.TABLE_NAME} ON ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MMS_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.ID} + WHERE ${MessageTable.TABLE_NAME}.${MessageTable.EXPIRES_IN} <= 0 AND ${MessageTable.TABLE_NAME}.${MessageTable.VIEW_ONCE} <= 0 """.trimIndent() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java index 5d547adc7e..3830b3c901 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java @@ -34,7 +34,7 @@ import org.thoughtcrime.securesms.database.EmojiSearchTable; import org.thoughtcrime.securesms.database.GroupReceiptTable; import org.thoughtcrime.securesms.database.KeyValueDatabase; import org.thoughtcrime.securesms.database.MentionTable; -import org.thoughtcrime.securesms.database.MmsTable; +import org.thoughtcrime.securesms.database.MessageTable; import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.database.OneTimePreKeyTable; import org.thoughtcrime.securesms.database.PendingRetryReceiptTable; @@ -44,7 +44,6 @@ import org.thoughtcrime.securesms.database.SenderKeyTable; import org.thoughtcrime.securesms.database.SenderKeySharedTable; import org.thoughtcrime.securesms.database.SessionTable; import org.thoughtcrime.securesms.database.SignedPreKeyTable; -import org.thoughtcrime.securesms.database.SmsTable; import org.thoughtcrime.securesms.database.StickerTable; import org.thoughtcrime.securesms.database.model.AvatarPickerDatabase; import org.thoughtcrime.securesms.database.model.MessageId; @@ -99,7 +98,6 @@ public class FullBackupExporter extends FullBackupBase { SignedPreKeyTable.TABLE_NAME, OneTimePreKeyTable.TABLE_NAME, SessionTable.TABLE_NAME, - SearchTable.SMS_FTS_TABLE_NAME, SearchTable.MMS_FTS_TABLE_NAME, EmojiSearchTable.TABLE_NAME, SenderKeyTable.TABLE_NAME, @@ -172,10 +170,8 @@ public class FullBackupExporter extends FullBackupBase { for (String table : tables) { throwIfCanceled(cancellationSignal); - if (table.equals(MmsTable.TABLE_NAME)) { + if (table.equals(MessageTable.TABLE_NAME)) { count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringMmsMessage, null, count, estimatedCount, cancellationSignal); - } else if (table.equals(SmsTable.TABLE_NAME)) { - count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringSmsMessage, null, count, estimatedCount, cancellationSignal); } else if (table.equals(ReactionTable.TABLE_NAME)) { count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, new MessageId(CursorUtil.requireLong(cursor, ReactionTable.MESSAGE_ID), CursorUtil.requireBoolean(cursor, ReactionTable.IS_MMS))), null, count, estimatedCount, cancellationSignal); } else if (table.equals(MentionTable.TABLE_NAME)) { @@ -230,10 +226,8 @@ public class FullBackupExporter extends FullBackupBase { long count = DATABASE_VERSION_RECORD_COUNT + TABLE_RECORD_COUNT_MULTIPLIER * tables.size(); for (String table : tables) { - if (table.equals(MmsTable.TABLE_NAME)) { + if (table.equals(MessageTable.TABLE_NAME)) { count += getCount(input, BackupCountQueries.mmsCount); - } else if (table.equals(SmsTable.TABLE_NAME)) { - count += getCount(input, BackupCountQueries.smsCount); } else if (table.equals(GroupReceiptTable.TABLE_NAME)) { count += getCount(input, BackupCountQueries.getGroupReceiptCount()); } else if (table.equals(AttachmentTable.TABLE_NAME)) { @@ -375,12 +369,10 @@ public class FullBackupExporter extends FullBackupBase { } boolean isReservedTable = table.startsWith("sqlite_"); - boolean isSmsFtsSecretTable = !table.equals(SearchTable.SMS_FTS_TABLE_NAME) && table.startsWith(SearchTable.SMS_FTS_TABLE_NAME); boolean isMmsFtsSecretTable = !table.equals(SearchTable.MMS_FTS_TABLE_NAME) && table.startsWith(SearchTable.MMS_FTS_TABLE_NAME); boolean isEmojiFtsSecretTable = !table.equals(EmojiSearchTable.TABLE_NAME) && table.startsWith(EmojiSearchTable.TABLE_NAME); return !isReservedTable && - !isSmsFtsSecretTable && !isMmsFtsSecretTable && !isEmojiFtsSecretTable; } @@ -587,7 +579,7 @@ public class FullBackupExporter extends FullBackupBase { private static boolean isNonExpiringMmsMessage(@NonNull Cursor cursor) { return cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0 && - cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.VIEW_ONCE)) <= 0; + cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.VIEW_ONCE)) <= 0; } private static boolean isNonExpiringSmsMessage(@NonNull Cursor cursor) { @@ -595,33 +587,15 @@ public class FullBackupExporter extends FullBackupBase { } private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, @NonNull MessageId messageId) { - if (messageId.isMms()) { - return isForNonExpiringMmsMessage(db, messageId.getId()); - } else { - return isForNonExpiringSmsMessage(db, messageId.getId()); - } - } - - private static boolean isForNonExpiringSmsMessage(@NonNull SQLiteDatabase db, long smsId) { - String[] columns = new String[] { SmsTable.EXPIRES_IN }; - String where = SmsTable.ID + " = ?"; - String[] args = new String[] { String.valueOf(smsId) }; - - try (Cursor cursor = db.query(SmsTable.TABLE_NAME, columns, where, args, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - return isNonExpiringSmsMessage(cursor); - } - } - - return false; + return isForNonExpiringMmsMessage(db, messageId.getId()); } private static boolean isForNonExpiringMmsMessage(@NonNull SQLiteDatabase db, long mmsId) { - String[] columns = new String[] { MmsTable.RECIPIENT_ID, MmsTable.EXPIRES_IN, MmsTable.VIEW_ONCE }; - String where = MmsTable.ID + " = ?"; + String[] columns = new String[] { MessageTable.RECIPIENT_ID, MessageTable.EXPIRES_IN, MessageTable.VIEW_ONCE }; + String where = MessageTable.ID + " = ?"; String[] args = new String[] { String.valueOf(mmsId) }; - try (Cursor mmsCursor = db.query(MmsTable.TABLE_NAME, columns, where, args, null, null, null)) { + try (Cursor mmsCursor = db.query(MessageTable.TABLE_NAME, columns, where, args, null, null, null)) { if (mmsCursor != null && mmsCursor.moveToFirst()) { return isNonExpiringMmsMessage(mmsCursor); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java index 1a5c22889a..bd96588c44 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java @@ -127,12 +127,11 @@ public class FullBackupImporter extends FullBackupBase { } private static void processStatement(@NonNull SQLiteDatabase db, SqlStatement statement) { - boolean isForSmsFtsSecretTable = statement.getStatement().contains(SearchTable.SMS_FTS_TABLE_NAME + "_"); boolean isForMmsFtsSecretTable = statement.getStatement().contains(SearchTable.MMS_FTS_TABLE_NAME + "_"); boolean isForEmojiSecretTable = statement.getStatement().contains(EmojiSearchTable.TABLE_NAME + "_"); boolean isForSqliteSecretTable = statement.getStatement().toLowerCase().startsWith("create table sqlite_"); - if (isForSmsFtsSecretTable || isForMmsFtsSecretTable || isForEmojiSecretTable || isForSqliteSecretTable) { + if (isForMmsFtsSecretTable || isForEmojiSecretTable || isForSqliteSecretTable) { Log.i(TAG, "Ignoring import for statement: " + statement.getStatement()); return; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index e16809bd5f..eafbab58ce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -110,9 +110,7 @@ import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectFor import org.thoughtcrime.securesms.conversation.quotes.MessageQuotesBottomSheet; import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSettingsDialog; import org.thoughtcrime.securesms.database.MessageTable; -import org.thoughtcrime.securesms.database.MmsTable; import org.thoughtcrime.securesms.database.SignalDatabase; -import org.thoughtcrime.securesms.database.SmsTable; import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageId; @@ -1198,7 +1196,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect } public long stageOutgoingMessage(OutgoingMediaMessage message) { - MessageRecord messageRecord = MmsTable.readerFor(message, threadId).getCurrent(); + MessageRecord messageRecord = MessageTable.readerFor(message, threadId).getCurrent(); if (getListAdapter() != null) { setLastSeen(0); @@ -1209,7 +1207,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect } public long stageOutgoingMessage(OutgoingTextMessage message, long messageId) { - MessageRecord messageRecord = SmsTable.readerFor(message, threadId, messageId).getCurrent(); + MessageRecord messageRecord = MessageTable.readerFor(message, threadId, messageId).getCurrent(); if (getListAdapter() != null) { setLastSeen(0); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeRepository.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeRepository.java index 3c93cfb7cc..c56b99098e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeRepository.java @@ -113,14 +113,7 @@ public final class SafetyNumberChangeRepository { @WorkerThread private @Nullable MessageRecord getMessageRecord(Long messageId, String messageType) { try { - switch (messageType) { - case MmsSmsTable.SMS_TRANSPORT: - return SignalDatabase.sms().getMessageRecord(messageId); - case MmsSmsTable.MMS_TRANSPORT: - return SignalDatabase.mms().getMessageRecord(messageId); - default: - throw new AssertionError("no valid message type specified"); - } + return SignalDatabase.mms().getMessageRecord(messageId); } catch (NoSuchMessageException e) { Log.i(TAG, e); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java index a3de4cfd12..b03a9c2796 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java @@ -14,8 +14,8 @@ import org.signal.paging.PagedDataSource; import org.thoughtcrime.securesms.conversationlist.model.Conversation; import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter; import org.thoughtcrime.securesms.conversationlist.model.ConversationReader; +import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.database.SignalDatabase; -import org.thoughtcrime.securesms.database.SmsTable; import org.thoughtcrime.securesms.database.ThreadTable; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord; @@ -77,9 +77,9 @@ abstract class ConversationListDataSource implements PagedDataSource new SpannableString(context.getString(R.string.ThreadRecord_s_is_on_signal, r.getDisplayName(context))))); - } else if (SmsTable.Types.isExpirationTimerUpdate(thread.getType())) { + } else if (MmsSmsColumns.Types.isExpirationTimerUpdate(thread.getType())) { int seconds = (int) (thread.getExpiresIn() / 1000); if (seconds <= 0) { return emphasisAdded(context, context.getString(R.string.ThreadRecord_disappearing_messages_disabled), R.drawable.ic_update_timer_disabled_16, defaultTint); } String time = ExpirationUtil.getExpirationDisplayValue(context, seconds); return emphasisAdded(context, context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time), R.drawable.ic_update_timer_16, defaultTint); - } else if (SmsTable.Types.isIdentityUpdate(thread.getType())) { + } else if (MmsSmsColumns.Types.isIdentityUpdate(thread.getType())) { return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> { if (r.isGroup()) { return new SpannableString(context.getString(R.string.ThreadRecord_safety_number_changed)); @@ -561,15 +560,15 @@ public final class ConversationListItem extends ConstraintLayout implements Bind return new SpannableString(context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context))); } })); - } else if (SmsTable.Types.isIdentityVerified(thread.getType())) { + } else if (MmsSmsColumns.Types.isIdentityVerified(thread.getType())) { return emphasisAdded(context, context.getString(R.string.ThreadRecord_you_marked_verified), defaultTint); - } else if (SmsTable.Types.isIdentityDefault(thread.getType())) { + } else if (MmsSmsColumns.Types.isIdentityDefault(thread.getType())) { return emphasisAdded(context, context.getString(R.string.ThreadRecord_you_marked_unverified), defaultTint); - } else if (SmsTable.Types.isUnsupportedMessageType(thread.getType())) { + } else if (MmsSmsColumns.Types.isUnsupportedMessageType(thread.getType())) { return emphasisAdded(context, context.getString(R.string.ThreadRecord_message_could_not_be_processed), defaultTint); - } else if (SmsTable.Types.isProfileChange(thread.getType())) { + } else if (MmsSmsColumns.Types.isProfileChange(thread.getType())) { return emphasisAdded(context, "", defaultTint); - } else if (SmsTable.Types.isChangeNumber(thread.getType()) || SmsTable.Types.isBoostRequest(thread.getType())) { + } else if (MmsSmsColumns.Types.isChangeNumber(thread.getType()) || MmsSmsColumns.Types.isBoostRequest(thread.getType())) { return emphasisAdded(context, "", defaultTint); } else if (MmsSmsColumns.Types.isBadDecryptType(thread.getType())) { return emphasisAdded(context, context.getString(R.string.ThreadRecord_delivery_issue), defaultTint); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.java index 45d3c8de44..e09a77b0d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.java @@ -476,7 +476,7 @@ public class AttachmentTable extends DatabaseTable { public void trimAllAbandonedAttachments() { SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - String selectAllMmsIds = "SELECT " + MmsTable.ID + " FROM " + MmsTable.TABLE_NAME; + String selectAllMmsIds = "SELECT " + MessageTable.ID + " FROM " + MessageTable.TABLE_NAME; String where = MMS_ID + " != " + PREUPLOAD_MESSAGE_ID + " AND " + MMS_ID + " NOT IN (" + selectAllMmsIds + ")"; int deletes = db.delete(TABLE_NAME, where, null); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptTable.kt index 66be58294c..41a67006e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptTable.kt @@ -123,7 +123,7 @@ class GroupReceiptTable(context: Context?, databaseHelper: SignalDatabase?) : Da fun deleteAbandonedRows() { writableDatabase .delete(TABLE_NAME) - .where("$MMS_ID NOT IN (SELECT ${MmsTable.ID} FROM ${MmsTable.TABLE_NAME})") + .where("$MMS_ID NOT IN (SELECT ${MessageTable.ID} FROM ${MessageTable.TABLE_NAME})") .run() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.java index 2c729005de..7e201f1aab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupTable.java @@ -50,7 +50,6 @@ import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.Closeable; import java.security.SecureRandom; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -1511,9 +1510,9 @@ public class GroupTable extends DatabaseTable implements RecipientIdDatabaseRefe public @NonNull List getGroupsToDisplayAsStories() throws BadGroupIdException { String query = "SELECT " + GROUP_ID + ", (" + - "SELECT " + MmsTable.TABLE_NAME + "." + MmsTable.DATE_RECEIVED + " FROM " + MmsTable.TABLE_NAME + - " WHERE " + MmsTable.TABLE_NAME + "." + MmsTable.RECIPIENT_ID + " = " + ThreadTable.TABLE_NAME + "." + ThreadTable.RECIPIENT_ID + - " AND " + MmsTable.STORY_TYPE + " > 1 ORDER BY " + MmsTable.TABLE_NAME + "." + MmsTable.DATE_RECEIVED + " DESC LIMIT 1" + + "SELECT " + MessageTable.TABLE_NAME + "." + MessageTable.DATE_RECEIVED + " FROM " + MessageTable.TABLE_NAME + + " WHERE " + MessageTable.TABLE_NAME + "." + MessageTable.RECIPIENT_ID + " = " + ThreadTable.TABLE_NAME + "." + ThreadTable.RECIPIENT_ID + + " AND " + MessageTable.STORY_TYPE + " > 1 ORDER BY " + MessageTable.TABLE_NAME + "." + MessageTable.DATE_RECEIVED + " DESC LIMIT 1" + ") as active_timestamp" + " FROM " + TABLE_NAME + " INNER JOIN " + ThreadTable.TABLE_NAME + " ON " + ThreadTable.TABLE_NAME + "." + ThreadTable.RECIPIENT_ID + " = " + TABLE_NAME + "." + RECIPIENT_ID + diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.java b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.java index e5323428ba..c0227f37e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.java @@ -48,26 +48,26 @@ public class MediaTable extends DatabaseTable { + AttachmentTable.TABLE_NAME + "." + AttachmentTable.CAPTION + ", " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.NAME + ", " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.UPLOAD_TIMESTAMP + ", " - + MmsTable.TABLE_NAME + "." + MmsTable.TYPE + ", " - + MmsTable.TABLE_NAME + "." + MmsTable.DATE_SENT + ", " - + MmsTable.TABLE_NAME + "." + MmsTable.DATE_RECEIVED + ", " - + MmsTable.TABLE_NAME + "." + MmsTable.DATE_SERVER + ", " - + MmsTable.TABLE_NAME + "." + MmsTable.THREAD_ID + ", " - + MmsTable.TABLE_NAME + "." + MmsTable.RECIPIENT_ID + ", " + + MessageTable.TABLE_NAME + "." + MessageTable.TYPE + ", " + + MessageTable.TABLE_NAME + "." + MessageTable.DATE_SENT + ", " + + MessageTable.TABLE_NAME + "." + MessageTable.DATE_RECEIVED + ", " + + MessageTable.TABLE_NAME + "." + MessageTable.DATE_SERVER + ", " + + MessageTable.TABLE_NAME + "." + MessageTable.THREAD_ID + ", " + + MessageTable.TABLE_NAME + "." + MessageTable.RECIPIENT_ID + ", " + ThreadTable.TABLE_NAME + "." + ThreadTable.RECIPIENT_ID + " as " + THREAD_RECIPIENT_ID + " " - + "FROM " + AttachmentTable.TABLE_NAME + " LEFT JOIN " + MmsTable.TABLE_NAME - + " ON " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.MMS_ID + " = " + MmsTable.TABLE_NAME + "." + MmsTable.ID + " " + + "FROM " + AttachmentTable.TABLE_NAME + " LEFT JOIN " + MessageTable.TABLE_NAME + + " ON " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.MMS_ID + " = " + MessageTable.TABLE_NAME + "." + MessageTable.ID + " " + "LEFT JOIN " + ThreadTable.TABLE_NAME - + " ON " + ThreadTable.TABLE_NAME + "." + ThreadTable.ID + " = " + MmsTable.TABLE_NAME + "." + MmsTable.THREAD_ID + " " + + " ON " + ThreadTable.TABLE_NAME + "." + ThreadTable.ID + " = " + MessageTable.TABLE_NAME + "." + MessageTable.THREAD_ID + " " + "WHERE " + AttachmentTable.MMS_ID + " IN (SELECT " + MmsSmsColumns.ID - + " FROM " + MmsTable.TABLE_NAME - + " WHERE " + MmsTable.THREAD_ID + " __EQUALITY__ ?) AND (%s) AND " - + MmsTable.VIEW_ONCE + " = 0 AND " - + MmsTable.STORY_TYPE + " = 0 AND " + + " FROM " + MessageTable.TABLE_NAME + + " WHERE " + MessageTable.THREAD_ID + " __EQUALITY__ ?) AND (%s) AND " + + MessageTable.VIEW_ONCE + " = 0 AND " + + MessageTable.STORY_TYPE + " = 0 AND " + AttachmentTable.DATA + " IS NOT NULL AND " + "(" + AttachmentTable.QUOTE + " = 0 OR (" + AttachmentTable.QUOTE + " = 1 AND " + AttachmentTable.DATA_HASH + " IS NULL)) AND " + AttachmentTable.STICKER_PACK_ID + " IS NULL AND " - + MmsTable.TABLE_NAME + "." + MmsTable.RECIPIENT_ID + " > 0 AND " + + MessageTable.TABLE_NAME + "." + MessageTable.RECIPIENT_ID + " > 0 AND " + THREAD_RECIPIENT_ID + " > 0"; private static final String UNIQUE_MEDIA_QUERY = "SELECT " @@ -192,16 +192,16 @@ public class MediaTable extends DatabaseTable { public static MediaRecord from(@NonNull Cursor cursor) { AttachmentTable attachmentDatabase = SignalDatabase.attachments(); List attachments = attachmentDatabase.getAttachments(cursor); - RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.RECIPIENT_ID))); - long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.THREAD_ID)); - boolean outgoing = MessageTable.Types.isOutgoingMessageType(cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.TYPE))); + RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.RECIPIENT_ID))); + long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.THREAD_ID)); + boolean outgoing = MessageTable.Types.isOutgoingMessageType(cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.TYPE))); long date; - if (MmsTable.Types.isPushType(cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.TYPE)))) { - date = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.DATE_SENT)); + if (MessageTable.Types.isPushType(cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.TYPE)))) { + date = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.DATE_SENT)); } else { - date = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.DATE_RECEIVED)); + date = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.DATE_RECEIVED)); } RecipientId threadRecipient = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_RECIPIENT_ID))); 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 3dbb0a6aff..e0558ff3ff 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 " + MmsTable.ID + " FROM " + MmsTable.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 + ")"; db.delete(TABLE_NAME, where, null); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageSendLogTables.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageSendLogTables.kt index 1f5e004b90..3df4d934bf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageSendLogTables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageSendLogTables.kt @@ -88,15 +88,9 @@ class MessageSendLogTables constructor(context: Context?, databaseHelper: Signal val CREATE_TRIGGERS = arrayOf( """ - CREATE TRIGGER msl_sms_delete AFTER DELETE ON ${SmsTable.TABLE_NAME} + CREATE TRIGGER msl_mms_delete AFTER DELETE ON ${org.thoughtcrime.securesms.database.MessageTable.TABLE_NAME} BEGIN - DELETE FROM $TABLE_NAME WHERE $ID IN (SELECT ${MessageTable.PAYLOAD_ID} FROM ${MessageTable.TABLE_NAME} WHERE ${MessageTable.MESSAGE_ID} = old.${SmsTable.ID} AND ${MessageTable.IS_MMS} = 0); - END - """, - """ - CREATE TRIGGER msl_mms_delete AFTER DELETE ON ${MmsTable.TABLE_NAME} - BEGIN - DELETE FROM $TABLE_NAME WHERE $ID IN (SELECT ${MessageTable.PAYLOAD_ID} FROM ${MessageTable.TABLE_NAME} WHERE ${MessageTable.MESSAGE_ID} = old.${MmsTable.ID} AND ${MessageTable.IS_MMS} = 1); + DELETE FROM $TABLE_NAME WHERE $ID IN (SELECT ${MessageTable.PAYLOAD_ID} FROM ${MessageTable.TABLE_NAME} WHERE ${MessageTable.MESSAGE_ID} = old.${org.thoughtcrime.securesms.database.MessageTable.ID} AND ${MessageTable.IS_MMS} = 1); END """, """ diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.java index 162de317fc..e56c584bcd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.java @@ -1,3 +1,19 @@ +/** + * Copyright (C) 2011 Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package org.thoughtcrime.securesms.database; import android.content.ContentValues; @@ -7,11 +23,18 @@ import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.annimon.stream.Collectors; +import com.annimon.stream.Stream; import com.google.android.mms.pdu_alt.NotificationInd; +import com.google.android.mms.pdu_alt.PduHeaders; +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; -import net.zetetic.database.sqlcipher.SQLiteStatement; - +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; import org.signal.core.util.CursorExtensionsKt; import org.signal.core.util.CursorUtil; import org.signal.core.util.SQLiteDatabaseExtensionsKt; @@ -19,220 +42,3392 @@ import org.signal.core.util.SqlUtil; import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.util.Pair; +import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.attachments.AttachmentId; +import org.thoughtcrime.securesms.attachments.DatabaseAttachment; +import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment; +import org.thoughtcrime.securesms.contactshare.Contact; import org.thoughtcrime.securesms.database.documents.Document; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet; import org.thoughtcrime.securesms.database.documents.NetworkFailure; +import org.thoughtcrime.securesms.database.documents.NetworkFailureSet; +import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil; +import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; +import org.thoughtcrime.securesms.database.model.Mention; import org.thoughtcrime.securesms.database.model.MessageExportStatus; import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord; import org.thoughtcrime.securesms.database.model.ParentStoryId; +import org.thoughtcrime.securesms.database.model.Quote; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.database.model.StoryResult; import org.thoughtcrime.securesms.database.model.StoryType; import org.thoughtcrime.securesms.database.model.StoryViewState; +import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList; +import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge; +import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails; import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExportState; +import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails; import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange; import org.thoughtcrime.securesms.insights.InsightsConstants; +import org.thoughtcrime.securesms.jobs.TrimThreadJob; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.mms.IncomingMediaMessage; +import org.thoughtcrime.securesms.mms.MessageGroupContext; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; +import org.thoughtcrime.securesms.mms.QuoteModel; +import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.revealable.ViewOnceExpirationInfo; +import org.thoughtcrime.securesms.revealable.ViewOnceUtil; +import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.sms.OutgoingTextMessage; +import org.thoughtcrime.securesms.stories.Stories; +import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.JsonUtils; +import org.thoughtcrime.securesms.util.MediaUtil; +import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; +import org.whispersystems.signalservice.api.push.ServiceId; import java.io.Closeable; import java.io.IOException; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; -public abstract class MessageTable extends DatabaseTable implements MmsSmsColumns, RecipientIdDatabaseReference, ThreadIdDatabaseReference { +import static org.thoughtcrime.securesms.contactshare.Contact.Avatar; + +public class MessageTable extends DatabaseTable implements MmsSmsColumns, RecipientIdDatabaseReference, ThreadIdDatabaseReference { private static final String TAG = Log.tag(MessageTable.class); - protected static final String THREAD_ID_WHERE = THREAD_ID + " = ?"; - protected static final String[] THREAD_ID_PROJECTION = new String[] { THREAD_ID }; + public static final String TABLE_NAME = "mms"; + static final String MMS_CONTENT_LOCATION = "ct_l"; + static final String MMS_EXPIRY = "exp"; + public static final String MMS_MESSAGE_TYPE = "m_type"; + static final String MMS_MESSAGE_SIZE = "m_size"; + static final String MMS_STATUS = "st"; + static final String MMS_TRANSACTION_ID = "tr_id"; + static final String NETWORK_FAILURES = "network_failures"; + + static final String QUOTE_ID = "quote_id"; + static final String QUOTE_AUTHOR = "quote_author"; + static final String QUOTE_BODY = "quote_body"; + static final String QUOTE_MISSING = "quote_missing"; + static final String QUOTE_MENTIONS = "quote_mentions"; + static final String QUOTE_TYPE = "quote_type"; + + static final String SHARED_CONTACTS = "shared_contacts"; + static final String LINK_PREVIEWS = "link_previews"; + static final String MENTIONS_SELF = "mentions_self"; + static final String MESSAGE_RANGES = "message_ranges"; + + public static final String VIEW_ONCE = "view_once"; + public static final String STORY_TYPE = "story_type"; + static final String PARENT_STORY_ID = "parent_story_id"; + + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + DATE_SENT + " INTEGER NOT NULL, " + + DATE_RECEIVED + " INTEGER NOT NULL, " + + DATE_SERVER + " INTEGER DEFAULT -1, " + + THREAD_ID + " INTEGER NOT NULL REFERENCES " + ThreadTable.TABLE_NAME + " (" + ThreadTable.ID + ") ON DELETE CASCADE, " + + RECIPIENT_ID + " INTEGER NOT NULL REFERENCES " + RecipientTable.TABLE_NAME + " (" + RecipientTable.ID + ") ON DELETE CASCADE, " + + RECIPIENT_DEVICE_ID + " INTEGER, " + + TYPE + " INTEGER NOT NULL, " + + BODY + " TEXT, " + + READ + " INTEGER DEFAULT 0, " + + MMS_CONTENT_LOCATION + " TEXT, " + + MMS_EXPIRY + " INTEGER, " + + MMS_MESSAGE_TYPE + " INTEGER, " + + MMS_MESSAGE_SIZE + " INTEGER, " + + MMS_STATUS + " INTEGER, " + + MMS_TRANSACTION_ID + " TEXT, " + + SMS_SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " + + RECEIPT_TIMESTAMP + " INTEGER DEFAULT -1, " + + DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + + READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + + VIEWED_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + + MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " + + NETWORK_FAILURES + " TEXT DEFAULT NULL," + + EXPIRES_IN + " INTEGER DEFAULT 0, " + + EXPIRE_STARTED + " INTEGER DEFAULT 0, " + + NOTIFIED + " INTEGER DEFAULT 0, " + + QUOTE_ID + " INTEGER DEFAULT 0, " + + QUOTE_AUTHOR + " INTEGER DEFAULT 0, " + + QUOTE_BODY + " TEXT DEFAULT NULL, " + + QUOTE_MISSING + " INTEGER DEFAULT 0, " + + QUOTE_MENTIONS + " BLOB DEFAULT NULL," + + QUOTE_TYPE + " INTEGER DEFAULT 0," + + SHARED_CONTACTS + " TEXT DEFAULT NULL, " + + UNIDENTIFIED + " INTEGER DEFAULT 0, " + + LINK_PREVIEWS + " TEXT DEFAULT NULL, " + + VIEW_ONCE + " INTEGER DEFAULT 0, " + + REACTIONS_UNREAD + " INTEGER DEFAULT 0, " + + REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1, " + + REMOTE_DELETED + " INTEGER DEFAULT 0, " + + MENTIONS_SELF + " INTEGER DEFAULT 0, " + + NOTIFIED_TIMESTAMP + " INTEGER DEFAULT 0, " + + SERVER_GUID + " TEXT DEFAULT NULL, " + + MESSAGE_RANGES + " BLOB DEFAULT NULL, " + + STORY_TYPE + " INTEGER DEFAULT 0, " + + PARENT_STORY_ID + " INTEGER DEFAULT 0, " + + EXPORT_STATE + " BLOB DEFAULT NULL, " + + EXPORTED + " INTEGER DEFAULT 0);"; + + public static final String[] CREATE_INDEXS = { + "CREATE INDEX IF NOT EXISTS mms_read_and_notified_and_thread_id_index ON " + TABLE_NAME + "(" + READ + "," + NOTIFIED + "," + THREAD_ID + ");", + "CREATE INDEX IF NOT EXISTS mms_type_index ON " + TABLE_NAME + " (" + TYPE + ");", + "CREATE INDEX IF NOT EXISTS mms_date_sent_index ON " + TABLE_NAME + " (" + DATE_SENT + ", " + RECIPIENT_ID + ", " + THREAD_ID + ");", + "CREATE INDEX IF NOT EXISTS mms_date_server_index ON " + TABLE_NAME + " (" + DATE_SERVER + ");", + "CREATE INDEX IF NOT EXISTS mms_thread_date_index ON " + TABLE_NAME + " (" + THREAD_ID + ", " + DATE_RECEIVED + ");", + "CREATE INDEX IF NOT EXISTS mms_reactions_unread_index ON " + TABLE_NAME + " (" + REACTIONS_UNREAD + ");", + "CREATE INDEX IF NOT EXISTS mms_story_type_index ON " + TABLE_NAME + " (" + STORY_TYPE + ");", + "CREATE INDEX IF NOT EXISTS mms_parent_story_id_index ON " + TABLE_NAME + " (" + PARENT_STORY_ID + ");", + "CREATE INDEX IF NOT EXISTS mms_thread_story_parent_story_index ON " + TABLE_NAME + " (" + THREAD_ID + ", " + DATE_RECEIVED + "," + STORY_TYPE + "," + PARENT_STORY_ID + ");", + "CREATE INDEX IF NOT EXISTS mms_quote_id_quote_author_index ON " + TABLE_NAME + "(" + QUOTE_ID + ", " + QUOTE_AUTHOR + ");", + "CREATE INDEX IF NOT EXISTS mms_exported_index ON " + TABLE_NAME + " (" + EXPORTED + ");", + "CREATE INDEX IF NOT EXISTS mms_id_type_payment_transactions_index ON " + TABLE_NAME + " (" + ID + "," + TYPE + ") WHERE " + TYPE + " & " + Types.SPECIAL_TYPE_PAYMENTS_NOTIFICATION + " != 0;" + }; + + private static final String[] MMS_PROJECTION = new String[] { + MessageTable.TABLE_NAME + "." + ID + " AS " + ID, + THREAD_ID, + DATE_SENT, + DATE_RECEIVED, + DATE_SERVER, + TYPE, + READ, + MMS_CONTENT_LOCATION, + MMS_EXPIRY, + MMS_MESSAGE_TYPE, + MMS_MESSAGE_SIZE, + MMS_STATUS, + MMS_TRANSACTION_ID, + BODY, + RECIPIENT_ID, + RECIPIENT_DEVICE_ID, + DELIVERY_RECEIPT_COUNT, + READ_RECEIPT_COUNT, + MISMATCHED_IDENTITIES, + NETWORK_FAILURES, + SMS_SUBSCRIPTION_ID, + EXPIRES_IN, + EXPIRE_STARTED, + NOTIFIED, + QUOTE_ID, + QUOTE_AUTHOR, + QUOTE_BODY, + QUOTE_TYPE, + QUOTE_MISSING, + QUOTE_MENTIONS, + SHARED_CONTACTS, + LINK_PREVIEWS, + UNIDENTIFIED, + VIEW_ONCE, + REACTIONS_UNREAD, + REACTIONS_LAST_SEEN, + REMOTE_DELETED, + MENTIONS_SELF, + NOTIFIED_TIMESTAMP, + VIEWED_RECEIPT_COUNT, + RECEIPT_TIMESTAMP, + MESSAGE_RANGES, + STORY_TYPE, + PARENT_STORY_ID, + "json_group_array(json_object(" + + "'" + AttachmentTable.ROW_ID + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.ROW_ID + ", " + + "'" + AttachmentTable.UNIQUE_ID + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.UNIQUE_ID + ", " + + "'" + AttachmentTable.MMS_ID + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.MMS_ID + ", " + + "'" + AttachmentTable.SIZE + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.SIZE + ", " + + "'" + AttachmentTable.FILE_NAME + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.FILE_NAME + ", " + + "'" + AttachmentTable.DATA + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.DATA + ", " + + "'" + AttachmentTable.CONTENT_TYPE + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.CONTENT_TYPE + ", " + + "'" + AttachmentTable.CDN_NUMBER + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.CDN_NUMBER + ", " + + "'" + AttachmentTable.CONTENT_LOCATION + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.CONTENT_LOCATION + ", " + + "'" + AttachmentTable.FAST_PREFLIGHT_ID + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.FAST_PREFLIGHT_ID + "," + + "'" + AttachmentTable.VOICE_NOTE + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.VOICE_NOTE + "," + + "'" + AttachmentTable.BORDERLESS + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.BORDERLESS + "," + + "'" + AttachmentTable.VIDEO_GIF + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.VIDEO_GIF + "," + + "'" + AttachmentTable.WIDTH + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.WIDTH + "," + + "'" + AttachmentTable.HEIGHT + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.HEIGHT + "," + + "'" + AttachmentTable.QUOTE + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.QUOTE + ", " + + "'" + AttachmentTable.CONTENT_DISPOSITION + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.CONTENT_DISPOSITION + ", " + + "'" + AttachmentTable.NAME + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.NAME + ", " + + "'" + AttachmentTable.TRANSFER_STATE + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.TRANSFER_STATE + ", " + + "'" + AttachmentTable.CAPTION + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.CAPTION + ", " + + "'" + AttachmentTable.STICKER_PACK_ID + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.STICKER_PACK_ID + ", " + + "'" + AttachmentTable.STICKER_PACK_KEY + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.STICKER_PACK_KEY + ", " + + "'" + AttachmentTable.STICKER_ID + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.STICKER_ID + ", " + + "'" + AttachmentTable.STICKER_EMOJI + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.STICKER_EMOJI + ", " + + "'" + AttachmentTable.VISUAL_HASH + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.VISUAL_HASH + ", " + + "'" + AttachmentTable.TRANSFORM_PROPERTIES + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.TRANSFORM_PROPERTIES + ", " + + "'" + AttachmentTable.DISPLAY_ORDER + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.DISPLAY_ORDER + ", " + + "'" + AttachmentTable.UPLOAD_TIMESTAMP + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.UPLOAD_TIMESTAMP + + ")) AS " + AttachmentTable.ATTACHMENT_JSON_ALIAS, + }; + + + private static final String[] SMS_PROJECTION = new String[] { + ID, + THREAD_ID, + RECIPIENT_ID, + RECIPIENT_DEVICE_ID, + DATE_RECEIVED, + DATE_SENT, + DATE_SERVER, + READ, + MMS_STATUS, + TYPE, + BODY, + DELIVERY_RECEIPT_COUNT, + MISMATCHED_IDENTITIES, + SMS_SUBSCRIPTION_ID, + EXPIRES_IN, + EXPIRE_STARTED, + NOTIFIED, + READ_RECEIPT_COUNT, + UNIDENTIFIED, + REACTIONS_UNREAD, + REACTIONS_LAST_SEEN, + REMOTE_DELETED, + NOTIFIED_TIMESTAMP, + RECEIPT_TIMESTAMP + }; + + private static final String THREAD_ID_WHERE = THREAD_ID + " = ?"; + private static final String[] THREAD_ID_PROJECTION = new String[] { THREAD_ID }; + private static final String IS_STORY_CLAUSE = STORY_TYPE + " > 0 AND " + REMOTE_DELETED + " = 0"; + private static final String RAW_ID_WHERE = TABLE_NAME + "._id = ?"; + + private final EarlyReceiptCache earlyDeliveryReceiptCache = new EarlyReceiptCache("MmsDelivery"); public MessageTable(Context context, SignalDatabase databaseHelper) { super(context, databaseHelper); } - protected abstract String getTableName(); - protected abstract String getTypeField(); - protected abstract String getDateSentColumnName(); - protected abstract String getDateReceivedColumnName(); + public @Nullable RecipientId getOldestGroupUpdateSender(long threadId, long minimumDateReceived) { + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - public abstract @Nullable RecipientId getOldestGroupUpdateSender(long threadId, long minimumDateReceived); - public abstract long getLatestGroupQuitTimestamp(long threadId, long quitTimeBarrier); - public abstract boolean isGroupQuitMessage(long messageId); - public abstract @Nullable Pair getOldestUnreadMentionDetails(long threadId); - public abstract int getUnreadMentionCount(long threadId); - public abstract long getThreadIdForMessage(long id); - public abstract int getMessageCountForThread(long threadId); - public abstract int getMessageCountForThread(long threadId, long beforeTime); - public abstract boolean hasMeaningfulMessage(long threadId); - public abstract int getIncomingMeaningfulMessageCountSince(long threadId, long afterTime); - public abstract Optional getNotification(long messageId); + String[] columns = new String[]{RECIPIENT_ID}; + String query = THREAD_ID + " = ? AND " + TYPE + " & ? AND " + DATE_RECEIVED + " >= ?"; + long type = Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT | Types.GROUP_UPDATE_BIT | Types.BASE_INBOX_TYPE; + String[] args = new String[]{String.valueOf(threadId), String.valueOf(type), String.valueOf(minimumDateReceived)}; + String limit = "1"; - public abstract Cursor getExpirationStartedMessages(); - public abstract SmsMessageRecord getSmsMessage(long messageId) throws NoSuchMessageException; - public abstract Reader getMessages(Collection messageIds); - public abstract Cursor getMessageCursor(long messageId); - public abstract OutgoingMediaMessage getOutgoingMessage(long messageId) throws MmsException, NoSuchMessageException; - public abstract MessageRecord getMessageRecord(long messageId) throws NoSuchMessageException; - public abstract @Nullable MessageRecord getMessageRecordOrNull(long messageId); - public abstract boolean hasReceivedAnyCallsSince(long threadId, long timestamp); - public abstract @Nullable ViewOnceExpirationInfo getNearestExpiringViewOnceMessage(); - public abstract boolean isSent(long messageId); - public abstract List getProfileChangeDetailsRecords(long threadId, long afterTimestamp); - public abstract Set getAllRateLimitedMessageIds(); - public abstract Cursor getUnexportedInsecureMessages(int limit); - public abstract long getUnexportedInsecureMessagesEstimatedSize(); - public abstract void deleteExportedMessages(); + try (Cursor cursor = db.query(TABLE_NAME, columns, query, args, null, null, limit)) { + if (cursor.moveToFirst()) { + return RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); + } + } - public abstract void markExpireStarted(long messageId); - public abstract void markExpireStarted(long messageId, long startTime); - public abstract void markExpireStarted(Collection messageId, long startTime); + return null; + } - public abstract void markAsEndSession(long id); - public abstract void markAsInvalidVersionKeyExchange(long id); - public abstract void markAsSecure(long id); - public abstract void markAsInsecure(long id); - public abstract void markAsPush(long id); - public abstract void markAsForcedSms(long id); - public abstract void markAsRateLimited(long id); - public abstract void clearRateLimitStatus(Collection ids); - public abstract void markAsDecryptFailed(long id); - public abstract void markAsNoSession(long id); - public abstract void markAsUnsupportedProtocolVersion(long id); - public abstract void markAsInvalidMessage(long id); - public abstract void markAsLegacyVersion(long id); - public abstract void markAsOutbox(long id); - public abstract void markAsPendingInsecureSmsFallback(long id); - public abstract void markAsSent(long messageId, boolean secure); - public abstract void markUnidentified(long messageId, boolean unidentified); - public abstract void markAsSentFailed(long id); - public abstract void markAsSending(long messageId); - public abstract void markAsRemoteDelete(long messageId); - public abstract void markAsMissedCall(long id, boolean isVideoOffer); - public abstract void markAsNotified(long id); - public abstract void markSmsStatus(long id, int status); - public abstract void markDownloadState(long messageId, long state); - public abstract void markIncomingNotificationReceived(long threadId); - public abstract void markGiftRedemptionCompleted(long messageId); - public abstract void markGiftRedemptionStarted(long messageId); - public abstract void markGiftRedemptionFailed(long messageId); + public Cursor getExpirationStartedMessages() { + String where = EXPIRE_STARTED + " > 0"; + return rawQuery(where, null); + } - public abstract Set incrementReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType, @NonNull MessageQualifier messageType); + public SmsMessageRecord getSmsMessage(long messageId) throws NoSuchMessageException { + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); + Cursor cursor = db.query(TABLE_NAME, SMS_PROJECTION, ID_WHERE, new String[]{messageId + ""}, null, null, null); + SmsReader reader = new SmsReader(cursor); + SmsMessageRecord record = reader.getNext(); - public abstract List setEntireThreadRead(long threadId); - public abstract List setMessagesReadSince(long threadId, long timestamp); - public abstract List setAllMessagesRead(); - public abstract InsertResult updateBundleMessageBody(long messageId, String body); - public abstract @NonNull List getViewedIncomingMessages(long threadId); - public abstract @Nullable MarkedMessageInfo setIncomingMessageViewed(long messageId); - public abstract @NonNull List setIncomingMessagesViewed(@NonNull List messageIds); - public abstract @NonNull List setOutgoingGiftsRevealed(@NonNull List messageIds); + reader.close(); - public abstract void addFailures(long messageId, List failure); - public abstract void setNetworkFailures(long messageId, Set failures); + if (record == null) throw new NoSuchMessageException("No message for ID: " + messageId); + else return record; + } - public abstract @NonNull Pair insertReceivedCall(@NonNull RecipientId address, boolean isVideoOffer); - public abstract @NonNull Pair insertOutgoingCall(@NonNull RecipientId address, boolean isVideoOffer); - public abstract @NonNull Pair insertMissedCall(@NonNull RecipientId address, long timestamp, boolean isVideoOffer); - public abstract void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId, - @NonNull RecipientId sender, - long timestamp, - @Nullable String peekGroupCallEraId, - @NonNull Collection peekJoinedUuids, - boolean isCallFull); - public abstract void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId, - @NonNull RecipientId sender, - long timestamp, - @Nullable String messageGroupCallEraId); - public abstract boolean updatePreviousGroupCall(long threadId, @Nullable String peekGroupCallEraId, @NonNull Collection peekJoinedUuids, boolean isCallFull); + public Cursor getMessageCursor(long messageId) { + return internalGetMessage(messageId); + } - public abstract Optional insertMessageInbox(IncomingTextMessage message, long type); - public abstract Optional insertMessageInbox(IncomingTextMessage message); - public abstract Optional insertMessageInbox(IncomingMediaMessage retrieved, String contentLocation, long threadId) throws MmsException; - public abstract Pair insertMessageInbox(@NonNull NotificationInd notification, int subscriptionId); - public abstract Optional insertSecureDecryptedMessageInbox(IncomingMediaMessage retrieved, long threadId) throws MmsException; - public abstract @NonNull InsertResult insertChatSessionRefreshedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp); - public abstract void insertBadDecryptMessage(@NonNull RecipientId recipientId, int senderDevice, long sentTimestamp, long receivedTimestamp, long threadId); - public abstract long insertMessageOutbox(long threadId, OutgoingTextMessage message, boolean forceSms, long date, InsertListener insertListener); - public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, @Nullable SmsTable.InsertListener insertListener) throws MmsException; - public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, int defaultReceiptStatus, @Nullable SmsTable.InsertListener insertListener) throws MmsException; - public abstract void insertProfileNameChangeMessages(@NonNull Recipient recipient, @NonNull String newProfileName, @NonNull String previousProfileName); - public abstract void insertGroupV1MigrationEvents(@NonNull RecipientId recipientId, long threadId, @NonNull GroupMigrationMembershipChange membershipChange); - public abstract void insertNumberChangeMessages(@NonNull RecipientId recipientId); - public abstract void insertBoostRequestMessage(@NonNull RecipientId recipientId, long threadId); - public abstract void insertThreadMergeEvent(@NonNull RecipientId recipientId, long threadId, @NonNull ThreadMergeEvent event); - public abstract void insertSmsExportMessage(@NonNull RecipientId recipientId, long threadId); + public boolean hasReceivedAnyCallsSince(long threadId, long timestamp) { + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); + String[] projection = SqlUtil.buildArgs(MmsSmsColumns.TYPE); + String selection = THREAD_ID + " = ? AND " + DATE_RECEIVED + " > ? AND (" + TYPE + " = ? OR " + TYPE + " = ? OR " + TYPE + " = ? OR " + TYPE + " =?)"; + String[] selectionArgs = SqlUtil.buildArgs(threadId, + timestamp, + Types.INCOMING_AUDIO_CALL_TYPE, + Types.INCOMING_VIDEO_CALL_TYPE, + Types.MISSED_AUDIO_CALL_TYPE, + Types.MISSED_VIDEO_CALL_TYPE); - public abstract boolean deleteMessage(long messageId); - abstract void deleteThread(long threadId); - abstract int deleteMessagesInThreadBeforeDate(long threadId, long date); - abstract void deleteThreads(@NonNull Set threadIds); - abstract void deleteAllThreads(); - abstract void deleteAbandonedMessages(); - public abstract void deleteRemotelyDeletedStory(long messageId); + try (Cursor cursor = db.query(TABLE_NAME, projection, selection, selectionArgs, null, null, null)) { + return cursor != null && cursor.moveToFirst(); + } + } - public abstract List getMessagesInThreadAfterInclusive(long threadId, long timestamp, long limit); + public void markAsEndSession(long id) { + updateTypeBitmask(id, Types.KEY_EXCHANGE_MASK, Types.END_SESSION_BIT); + } - public abstract SQLiteDatabase beginTransaction(); - public abstract void endTransaction(SQLiteDatabase database); - public abstract void setTransactionSuccessful(); - public abstract void endTransaction(); + public void markAsInvalidVersionKeyExchange(long id) { + updateTypeBitmask(id, 0, Types.KEY_EXCHANGE_INVALID_VERSION_BIT); + } - public abstract void ensureMigration(); + public void markAsSecure(long id) { + updateTypeBitmask(id, 0, Types.SECURE_MESSAGE_BIT); + } - public abstract boolean isStory(long messageId); - public abstract @NonNull Reader getOutgoingStoriesTo(@NonNull RecipientId recipientId); - public abstract @NonNull Reader getAllOutgoingStories(boolean reverse, int limit); - public abstract @NonNull Reader getAllOutgoingStoriesAt(long sentTimestamp); - public abstract @NonNull List markAllIncomingStoriesRead(); - public abstract @NonNull List getOrderedStoryRecipientsAndIds(boolean isOutgoingOnly); + public void markAsPush(long id) { + updateTypeBitmask(id, 0, Types.PUSH_MESSAGE_BIT); + } - public abstract void markOnboardingStoryRead(); + public void markAsDecryptFailed(long id) { + updateTypeBitmask(id, Types.ENCRYPTION_MASK, Types.ENCRYPTION_REMOTE_FAILED_BIT); + } - public abstract @NonNull Reader getAllStoriesFor(@NonNull RecipientId recipientId, int limit); - public abstract @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException; - public abstract int getNumberOfStoryReplies(long parentStoryId); - public abstract @NonNull List getUnreadStoryThreadRecipientIds(); - public abstract boolean containsStories(long threadId); - public abstract boolean hasSelfReplyInStory(long parentStoryId); - public abstract boolean hasGroupReplyOrReactionInStory(long parentStoryId); - public abstract @NonNull Cursor getStoryReplies(long parentStoryId); - public abstract @Nullable Long getOldestStorySendTimestamp(boolean hasSeenReleaseChannelStories); - public abstract int deleteStoriesOlderThan(long timestamp, boolean hasSeenReleaseChannelStories); - public abstract @NonNull MessageTable.Reader getUnreadStories(@NonNull RecipientId recipientId, int limit); - public abstract @Nullable ParentStoryId.GroupReply getParentStoryIdForGroupReply(long messageId); - public abstract void deleteGroupStoryReplies(long parentStoryId); - public abstract boolean isOutgoingStoryAlreadyInDatabase(@NonNull RecipientId recipientId, long sentTimestamp); - public abstract @NonNull List setGroupStoryMessagesReadSince(long threadId, long groupStoryId, long sinceTimestamp); - public abstract @NonNull List getStoryTypes(@NonNull List messageIds); + public void markAsNoSession(long id) { + updateTypeBitmask(id, Types.ENCRYPTION_MASK, Types.ENCRYPTION_REMOTE_NO_SESSION_BIT); + } - public abstract @NonNull StoryViewState getStoryViewState(@NonNull RecipientId recipientId); - public abstract void updateViewedStories(@NonNull Set syncMessageIds); + public void markAsUnsupportedProtocolVersion(long id) { + updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.UNSUPPORTED_MESSAGE_TYPE); + } + + public void markAsInvalidMessage(long id) { + updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.INVALID_MESSAGE_TYPE); + } + + public void markAsLegacyVersion(long id) { + updateTypeBitmask(id, Types.ENCRYPTION_MASK, Types.ENCRYPTION_REMOTE_LEGACY_BIT); + } + + public void markAsMissedCall(long id, boolean isVideoOffer) { + updateTypeBitmask(id, Types.TOTAL_MASK, isVideoOffer ? Types.MISSED_VIDEO_CALL_TYPE : Types.MISSED_AUDIO_CALL_TYPE); + } + + public void markSmsStatus(long id, int status) { + Log.i(TAG, "Updating ID: " + id + " to status: " + status); + ContentValues contentValues = new ContentValues(); + contentValues.put(MMS_STATUS, status); + + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {id+""}); + + long threadId = getThreadIdForMessage(id); + SignalDatabase.threads().update(threadId, false); + notifyConversationListeners(threadId); + } + + private void updateTypeBitmask(long id, long maskOff, long maskOn) { + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + + long threadId; + + db.beginTransaction(); + try { + db.execSQL("UPDATE " + TABLE_NAME + + " SET " + TYPE + " = (" + TYPE + " & " + (Types.TOTAL_MASK - maskOff) + " | " + maskOn + " )" + + " WHERE " + ID + " = ?", SqlUtil.buildArgs(id)); + + threadId = getThreadIdForMessage(id); + + SignalDatabase.threads().updateSnippetTypeSilently(threadId); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(id, false)); + ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners(); + } + + private InsertResult updateMessageBodyAndType(long messageId, String body, long maskOff, long maskOn) { + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + db.execSQL("UPDATE " + TABLE_NAME + " SET " + BODY + " = ?, " + + TYPE + " = (" + TYPE + " & " + (Types.TOTAL_MASK - maskOff) + " | " + maskOn + ") " + + "WHERE " + ID + " = ?", + new String[] {body, messageId + ""}); + + long threadId = getThreadIdForMessage(messageId); + + SignalDatabase.threads().update(threadId, true); + notifyConversationListeners(threadId); + + return new InsertResult(messageId, threadId); + } + + public InsertResult updateBundleMessageBody(long messageId, String body) { + long type = Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT; + return updateMessageBodyAndType(messageId, body, Types.TOTAL_MASK, type); + } + + public @NonNull List getViewedIncomingMessages(long threadId) { + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); + String[] columns = new String[]{ ID, RECIPIENT_ID, DATE_SENT, TYPE, THREAD_ID, STORY_TYPE}; + String where = THREAD_ID + " = ? AND " + VIEWED_RECEIPT_COUNT + " > 0 AND " + TYPE + " & " + Types.BASE_INBOX_TYPE + " = " + Types.BASE_INBOX_TYPE; + String[] args = SqlUtil.buildArgs(threadId); + + + try (Cursor cursor = db.query(TABLE_NAME, columns, where, args, null, null, null, null)) { + if (cursor == null) { + return Collections.emptyList(); + } + + List results = new ArrayList<>(cursor.getCount()); + while (cursor.moveToNext()) { + long messageId = CursorUtil.requireLong(cursor, ID); + RecipientId recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); + long dateSent = CursorUtil.requireLong(cursor, DATE_SENT); + SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent); + StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE)); + + results.add(new MarkedMessageInfo(threadId, syncMessageId, new MessageId(messageId, true), null)); + } + + return results; + } + } + + public @Nullable MarkedMessageInfo setIncomingMessageViewed(long messageId) { + List results = setIncomingMessagesViewed(Collections.singletonList(messageId)); + + if (results.isEmpty()) { + return null; + } else { + return results.get(0); + } + } + + public @NonNull List setIncomingMessagesViewed(@NonNull List messageIds) { + if (messageIds.isEmpty()) { + return Collections.emptyList(); + } + + SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); + String[] columns = new String[]{ ID, RECIPIENT_ID, DATE_SENT, TYPE, THREAD_ID, STORY_TYPE}; + String where = ID + " IN (" + Util.join(messageIds, ",") + ") AND " + VIEWED_RECEIPT_COUNT + " = 0"; + List results = new LinkedList<>(); + + database.beginTransaction(); + try (Cursor cursor = database.query(TABLE_NAME, columns, where, null, null, null, null)) { + while (cursor != null && cursor.moveToNext()) { + long type = CursorUtil.requireLong(cursor, TYPE); + if (Types.isSecureType(type) && Types.isInboxType(type)) { + long messageId = CursorUtil.requireLong(cursor, ID); + long threadId = CursorUtil.requireLong(cursor, THREAD_ID); + RecipientId recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); + long dateSent = CursorUtil.requireLong(cursor, DATE_SENT); + SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent); + + results.add(new MarkedMessageInfo(threadId, syncMessageId, new MessageId(messageId, true), null)); + + ContentValues contentValues = new ContentValues(); + contentValues.put(VIEWED_RECEIPT_COUNT, 1); + contentValues.put(RECEIPT_TIMESTAMP, System.currentTimeMillis()); + + database.update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(CursorUtil.requireLong(cursor, ID))); + } + } + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); + } + + Set threadsUpdated = Stream.of(results) + .map(MarkedMessageInfo::getThreadId) + .collect(Collectors.toSet()); + + notifyConversationListeners(threadsUpdated); + notifyConversationListListeners(); + + return results; + } + + public @NonNull List setOutgoingGiftsRevealed(@NonNull List messageIds) { + String[] projection = SqlUtil.buildArgs(ID, RECIPIENT_ID, DATE_SENT, THREAD_ID, STORY_TYPE); + String where = ID + " IN (" + Util.join(messageIds, ",") + ") AND (" + getOutgoingTypeClause() + ") AND (" + TYPE + " & " + Types.SPECIAL_TYPES_MASK + " = " + Types.SPECIAL_TYPE_GIFT_BADGE + ") AND " + VIEWED_RECEIPT_COUNT + " = 0"; + List results = new LinkedList<>(); + + getWritableDatabase().beginTransaction(); + try (Cursor cursor = getWritableDatabase().query(TABLE_NAME, projection, where, null, null, null, null)) { + while (cursor.moveToNext()) { + long messageId = CursorUtil.requireLong(cursor, ID); + long threadId = CursorUtil.requireLong(cursor, THREAD_ID); + RecipientId recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); + long dateSent = CursorUtil.requireLong(cursor, DATE_SENT); + SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent); + StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE)); + + results.add(new MarkedMessageInfo(threadId, syncMessageId, new MessageId(messageId, true), null)); + + ContentValues contentValues = new ContentValues(); + contentValues.put(VIEWED_RECEIPT_COUNT, 1); + contentValues.put(RECEIPT_TIMESTAMP, System.currentTimeMillis()); + + getWritableDatabase().update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(messageId)); + } + getWritableDatabase().setTransactionSuccessful(); + } finally { + getWritableDatabase().endTransaction(); + } + + Set threadsUpdated = Stream.of(results) + .map(MarkedMessageInfo::getThreadId) + .collect(Collectors.toSet()); + + notifyConversationListeners(threadsUpdated); + + return results; + } + + public @NonNull Pair insertReceivedCall(@NonNull RecipientId address, boolean isVideoOffer) { + return insertCallLog(address, isVideoOffer ? Types.INCOMING_VIDEO_CALL_TYPE : Types.INCOMING_AUDIO_CALL_TYPE, false, System.currentTimeMillis()); + } + + public @NonNull Pair insertOutgoingCall(@NonNull RecipientId address, boolean isVideoOffer) { + return insertCallLog(address, isVideoOffer ? Types.OUTGOING_VIDEO_CALL_TYPE : Types.OUTGOING_AUDIO_CALL_TYPE, false, System.currentTimeMillis()); + } + + public @NonNull Pair insertMissedCall(@NonNull RecipientId address, long timestamp, boolean isVideoOffer) { + return insertCallLog(address, isVideoOffer ? Types.MISSED_VIDEO_CALL_TYPE : Types.MISSED_AUDIO_CALL_TYPE, true, timestamp); + } + + private @NonNull Pair insertCallLog(@NonNull RecipientId recipientId, long type, boolean unread, long timestamp) { + Recipient recipient = Recipient.resolved(recipientId); + long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); + + ContentValues values = new ContentValues(6); + values.put(RECIPIENT_ID, recipientId.serialize()); + values.put(RECIPIENT_DEVICE_ID, 1); + values.put(DATE_RECEIVED, System.currentTimeMillis()); + values.put(DATE_SENT, timestamp); + values.put(READ, unread ? 0 : 1); + values.put(TYPE, type); + values.put(THREAD_ID, threadId); + + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + long messageId = db.insert(TABLE_NAME, null, values); + + if (unread) { + SignalDatabase.threads().incrementUnread(threadId, 1, 0); + } + boolean keepThreadArchived = SignalStore.settings().shouldKeepMutedChatsArchived() && Recipient.resolved(recipientId).isMuted(); + SignalDatabase.threads().update(threadId, !keepThreadArchived); + + notifyConversationListeners(threadId); + TrimThreadJob.enqueueAsync(threadId); + + return new Pair<>(messageId, threadId); + } + + public void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId, + @NonNull RecipientId sender, + long timestamp, + @Nullable String peekGroupCallEraId, + @NonNull Collection peekJoinedUuids, + boolean isCallFull) + { + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + Recipient recipient = Recipient.resolved(groupRecipientId); + long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); + boolean peerEraIdSameAsPrevious = updatePreviousGroupCall(threadId, peekGroupCallEraId, peekJoinedUuids, isCallFull); + + try { + db.beginTransaction(); + + if (!peerEraIdSameAsPrevious && !Util.isEmpty(peekGroupCallEraId)) { + Recipient self = Recipient.self(); + boolean markRead = peekJoinedUuids.contains(self.requireServiceId().uuid()) || self.getId().equals(sender); + + byte[] updateDetails = GroupCallUpdateDetails.newBuilder() + .setEraId(Util.emptyIfNull(peekGroupCallEraId)) + .setStartedCallUuid(Recipient.resolved(sender).requireServiceId().toString()) + .setStartedCallTimestamp(timestamp) + .addAllInCallUuids(Stream.of(peekJoinedUuids).map(UUID::toString).toList()) + .setIsCallFull(isCallFull) + .build() + .toByteArray(); + + String body = Base64.encodeBytes(updateDetails); + + ContentValues values = new ContentValues(); + values.put(RECIPIENT_ID, sender.serialize()); + values.put(RECIPIENT_DEVICE_ID, 1); + values.put(DATE_RECEIVED, timestamp); + values.put(DATE_SENT, timestamp); + values.put(READ, markRead ? 1 : 0); + values.put(BODY, body); + values.put(TYPE, Types.GROUP_CALL_TYPE); + values.put(THREAD_ID, threadId); + + db.insert(TABLE_NAME, null, values); + + SignalDatabase.threads().incrementUnread(threadId, 1, 0); + } + boolean keepThreadArchived = SignalStore.settings().shouldKeepMutedChatsArchived() && recipient.isMuted(); + SignalDatabase.threads().update(threadId, !keepThreadArchived); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + notifyConversationListeners(threadId); + TrimThreadJob.enqueueAsync(threadId); + } + + public void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId, + @NonNull RecipientId sender, + long timestamp, + @Nullable String messageGroupCallEraId) + { + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + + long threadId; + + try { + db.beginTransaction(); + + Recipient recipient = Recipient.resolved(groupRecipientId); + + threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); + + String where = TYPE + " = ? AND " + THREAD_ID + " = ?"; + String[] args = SqlUtil.buildArgs(Types.GROUP_CALL_TYPE, threadId); + boolean sameEraId = false; + + try (SmsReader reader = new SmsReader(db.query(TABLE_NAME, SMS_PROJECTION, where, args, null, null, DATE_RECEIVED + " DESC", "1"))) { + MessageRecord record = reader.getNext(); + if (record != null) { + GroupCallUpdateDetails groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(record.getBody()); + + sameEraId = groupCallUpdateDetails.getEraId().equals(messageGroupCallEraId) && !Util.isEmpty(messageGroupCallEraId); + + if (!sameEraId) { + String body = GroupCallUpdateDetailsUtil.createUpdatedBody(groupCallUpdateDetails, Collections.emptyList(), false); + + ContentValues contentValues = new ContentValues(); + contentValues.put(BODY, body); + + db.update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(record.getId())); + } + } + } + + if (!sameEraId && !Util.isEmpty(messageGroupCallEraId)) { + byte[] updateDetails = GroupCallUpdateDetails.newBuilder() + .setEraId(Util.emptyIfNull(messageGroupCallEraId)) + .setStartedCallUuid(Recipient.resolved(sender).requireServiceId().toString()) + .setStartedCallTimestamp(timestamp) + .addAllInCallUuids(Collections.emptyList()) + .setIsCallFull(false) + .build() + .toByteArray(); + + String body = Base64.encodeBytes(updateDetails); + + ContentValues values = new ContentValues(); + values.put(RECIPIENT_ID, sender.serialize()); + values.put(RECIPIENT_DEVICE_ID, 1); + values.put(DATE_RECEIVED, timestamp); + values.put(DATE_SENT, timestamp); + values.put(READ, 0); + values.put(BODY, body); + values.put(TYPE, Types.GROUP_CALL_TYPE); + values.put(THREAD_ID, threadId); + + db.insert(TABLE_NAME, null, values); + + SignalDatabase.threads().incrementUnread(threadId, 1, 0); + } + + final boolean keepThreadArchived = SignalStore.settings().shouldKeepMutedChatsArchived() && recipient.isMuted(); + SignalDatabase.threads().update(threadId, !keepThreadArchived); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + notifyConversationListeners(threadId); + TrimThreadJob.enqueueAsync(threadId); + } + + public boolean updatePreviousGroupCall(long threadId, @Nullable String peekGroupCallEraId, @NonNull Collection peekJoinedUuids, boolean isCallFull) { + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + String where = TYPE + " = ? AND " + THREAD_ID + " = ?"; + String[] args = SqlUtil.buildArgs(Types.GROUP_CALL_TYPE, threadId); + boolean sameEraId = false; + + try (SmsReader reader = new SmsReader(db.query(TABLE_NAME, SMS_PROJECTION, where, args, null, null, DATE_RECEIVED + " DESC", "1"))) { + MessageRecord record = reader.getNext(); + if (record == null) { + return false; + } + + GroupCallUpdateDetails groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(record.getBody()); + boolean containsSelf = peekJoinedUuids.contains(SignalStore.account().requireAci().uuid()); + + sameEraId = groupCallUpdateDetails.getEraId().equals(peekGroupCallEraId) && !Util.isEmpty(peekGroupCallEraId); + + List inCallUuids = sameEraId ? Stream.of(peekJoinedUuids).map(UUID::toString).toList() + : Collections.emptyList(); + + String body = GroupCallUpdateDetailsUtil.createUpdatedBody(groupCallUpdateDetails, inCallUuids, isCallFull); + + ContentValues contentValues = new ContentValues(); + contentValues.put(BODY, body); + + if (sameEraId && containsSelf) { + contentValues.put(READ, 1); + } + + SqlUtil.Query query = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(record.getId()), contentValues); + boolean updated = db.update(TABLE_NAME, contentValues, query.getWhere(), query.getWhereArgs()) > 0; + + if (updated) { + notifyConversationListeners(threadId); + } + } + + return sameEraId; + } + + public Optional insertMessageInbox(IncomingTextMessage message, long type) { + boolean tryToCollapseJoinRequestEvents = false; + + if (message.isJoined()) { + type = (type & (Types.TOTAL_MASK - Types.BASE_TYPE_MASK)) | Types.JOINED_TYPE; + } else if (message.isPreKeyBundle()) { + type |= Types.KEY_EXCHANGE_BIT | Types.KEY_EXCHANGE_BUNDLE_BIT; + } else if (message.isSecureMessage()) { + type |= Types.SECURE_MESSAGE_BIT; + } else if (message.isGroup()) { + IncomingGroupUpdateMessage incomingGroupUpdateMessage = (IncomingGroupUpdateMessage) message; + + type |= Types.SECURE_MESSAGE_BIT; + + if (incomingGroupUpdateMessage.isGroupV2()) { + type |= Types.GROUP_V2_BIT | Types.GROUP_UPDATE_BIT; + if (incomingGroupUpdateMessage.isJustAGroupLeave()) { + type |= Types.GROUP_LEAVE_BIT; + } else if (incomingGroupUpdateMessage.isCancelJoinRequest()) { + tryToCollapseJoinRequestEvents = true; + } + } else if (incomingGroupUpdateMessage.isUpdate()) { + type |= Types.GROUP_UPDATE_BIT; + } else if (incomingGroupUpdateMessage.isQuit()) { + type |= Types.GROUP_LEAVE_BIT; + } + + } else if (message.isEndSession()) { + type |= Types.SECURE_MESSAGE_BIT; + type |= Types.END_SESSION_BIT; + } + + if (message.isPush()) type |= Types.PUSH_MESSAGE_BIT; + if (message.isIdentityUpdate()) type |= Types.KEY_EXCHANGE_IDENTITY_UPDATE_BIT; + if (message.isContentPreKeyBundle()) type |= Types.KEY_EXCHANGE_CONTENT_FORMAT; + + if (message.isIdentityVerified()) type |= Types.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT; + else if (message.isIdentityDefault()) type |= Types.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT; + + Recipient recipient = Recipient.resolved(message.getSender()); + + Recipient groupRecipient; + + if (message.getGroupId() == null) { + groupRecipient = null; + } else { + RecipientId id = SignalDatabase.recipients().getOrInsertFromPossiblyMigratedGroupId(message.getGroupId()); + groupRecipient = Recipient.resolved(id); + } + + boolean silent = message.isIdentityUpdate() || + message.isIdentityVerified() || + message.isIdentityDefault() || + message.isJustAGroupLeave() || + (type & Types.GROUP_UPDATE_BIT) > 0; + + boolean unread = !silent && (Util.isDefaultSmsProvider(context) || + message.isSecureMessage() || + message.isGroup() || + message.isPreKeyBundle()); + + long threadId; + + if (groupRecipient == null) threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); + else threadId = SignalDatabase.threads().getOrCreateThreadIdFor(groupRecipient); + + if (tryToCollapseJoinRequestEvents) { + final Optional result = collapseJoinRequestEventsIfPossible(threadId, (IncomingGroupUpdateMessage) message); + if (result.isPresent()) { + return result; + } + } + + ContentValues values = new ContentValues(); + values.put(RECIPIENT_ID, message.getSender().serialize()); + values.put(RECIPIENT_DEVICE_ID, message.getSenderDeviceId()); + values.put(DATE_RECEIVED, message.getReceivedTimestampMillis()); + values.put(DATE_SENT, message.getSentTimestampMillis()); + values.put(DATE_SERVER, message.getServerTimestampMillis()); + values.put(READ, unread ? 0 : 1); + values.put(SMS_SUBSCRIPTION_ID, message.getSubscriptionId()); + values.put(EXPIRES_IN, message.getExpiresIn()); + values.put(UNIDENTIFIED, message.isUnidentified()); + values.put(BODY, message.getMessageBody()); + values.put(TYPE, type); + values.put(THREAD_ID, threadId); + values.put(SERVER_GUID, message.getServerGuid()); + + if (message.isPush() && isDuplicate(message, threadId)) { + Log.w(TAG, "Duplicate message (" + message.getSentTimestampMillis() + "), ignoring..."); + return Optional.empty(); + } else { + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + long messageId = db.insert(TABLE_NAME, null, values); + + if (unread) { + SignalDatabase.threads().incrementUnread(threadId, 1, 0); + } + + if (!silent) { + final boolean keepThreadArchived = SignalStore.settings().shouldKeepMutedChatsArchived() && recipient.isMuted(); + SignalDatabase.threads().update(threadId, !keepThreadArchived); + } + + if (message.getSubscriptionId() != -1) { + SignalDatabase.recipients().setDefaultSubscriptionId(recipient.getId(), message.getSubscriptionId()); + } + + notifyConversationListeners(threadId); + + if (!silent) { + TrimThreadJob.enqueueAsync(threadId); + } + + return Optional.of(new InsertResult(messageId, threadId)); + } + } + + public Optional insertMessageInbox(IncomingTextMessage message) { + return insertMessageInbox(message, Types.BASE_INBOX_TYPE); + } + + public long insertMessageOutbox(long threadId, OutgoingTextMessage message, boolean forceSms, long date, InsertListener insertListener) { + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + + long type = Types.BASE_SENDING_TYPE; + + if (message.isKeyExchange()) type |= Types.KEY_EXCHANGE_BIT; + else if (message.isSecureMessage()) type |= (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT); + else if (message.isEndSession()) type |= Types.END_SESSION_BIT; + if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT; + + if (message.isIdentityVerified()) type |= Types.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT; + else if (message.isIdentityDefault()) type |= Types.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT; + + RecipientId recipientId = message.getRecipient().getId(); + Map earlyDeliveryReceipts = earlyDeliveryReceiptCache.remove(date); + + ContentValues contentValues = new ContentValues(6); + contentValues.put(RECIPIENT_ID, recipientId.serialize()); + contentValues.put(THREAD_ID, threadId); + contentValues.put(BODY, message.getMessageBody()); + contentValues.put(DATE_RECEIVED, System.currentTimeMillis()); + contentValues.put(DATE_SENT, date); + contentValues.put(READ, 1); + contentValues.put(TYPE, type); + contentValues.put(SMS_SUBSCRIPTION_ID, message.getSubscriptionId()); + contentValues.put(EXPIRES_IN, message.getExpiresIn()); + contentValues.put(DELIVERY_RECEIPT_COUNT, Stream.of(earlyDeliveryReceipts.values()).mapToLong(EarlyReceiptCache.Receipt::getCount).sum()); + contentValues.put(RECEIPT_TIMESTAMP, Stream.of(earlyDeliveryReceipts.values()).mapToLong(EarlyReceiptCache.Receipt::getTimestamp).max().orElse(-1)); + + long messageId = db.insert(TABLE_NAME, null, contentValues); + + if (insertListener != null) { + insertListener.onComplete(); + } + + if (!message.isIdentityVerified() && !message.isIdentityDefault()) { + SignalDatabase.threads().setLastScrolled(threadId, 0); + SignalDatabase.threads().setLastSeenSilently(threadId); + } + + SignalDatabase.threads().setHasSentSilently(threadId, true); + + ApplicationDependencies.getDatabaseObserver().notifyMessageInsertObservers(threadId, new MessageId(messageId, false)); + + if (!message.isIdentityVerified() && !message.isIdentityDefault()) { + TrimThreadJob.enqueueAsync(threadId); + } + + return messageId; + } + + public void insertProfileNameChangeMessages(@NonNull Recipient recipient, @NonNull String newProfileName, @NonNull String previousProfileName) { + ThreadTable threadTable = SignalDatabase.threads(); + List groupRecords = SignalDatabase.groups().getGroupsContainingMember(recipient.getId(), false); + List threadIdsToUpdate = new LinkedList<>(); + + byte[] profileChangeDetails = ProfileChangeDetails.newBuilder() + .setProfileNameChange(ProfileChangeDetails.StringChange.newBuilder() + .setNew(newProfileName) + .setPrevious(previousProfileName)) + .build() + .toByteArray(); + + String body = Base64.encodeBytes(profileChangeDetails); + + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + db.beginTransaction(); + + try { + threadIdsToUpdate.add(threadTable.getThreadIdFor(recipient.getId())); + for (GroupTable.GroupRecord groupRecord : groupRecords) { + if (groupRecord.isActive()) { + threadIdsToUpdate.add(threadTable.getThreadIdFor(groupRecord.getRecipientId())); + } + } + + Stream.of(threadIdsToUpdate) + .withoutNulls() + .forEach(threadId -> { + ContentValues values = new ContentValues(); + values.put(RECIPIENT_ID, recipient.getId().serialize()); + values.put(RECIPIENT_DEVICE_ID, 1); + values.put(DATE_RECEIVED, System.currentTimeMillis()); + values.put(DATE_SENT, System.currentTimeMillis()); + values.put(READ, 1); + values.put(TYPE, Types.PROFILE_CHANGE_TYPE); + values.put(THREAD_ID, threadId); + values.put(BODY, body); + + db.insert(TABLE_NAME, null, values); + + notifyConversationListeners(threadId); + }); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + Stream.of(threadIdsToUpdate) + .withoutNulls() + .forEach(TrimThreadJob::enqueueAsync); + } + + public void insertGroupV1MigrationEvents(@NonNull RecipientId recipientId, + long threadId, + @NonNull GroupMigrationMembershipChange membershipChange) + { + insertGroupV1MigrationNotification(recipientId, threadId); + + if (!membershipChange.isEmpty()) { + insertGroupV1MigrationMembershipChanges(recipientId, threadId, membershipChange); + } + + notifyConversationListeners(threadId); + TrimThreadJob.enqueueAsync(threadId); + } + + private void insertGroupV1MigrationNotification(@NonNull RecipientId recipientId, long threadId) { + insertGroupV1MigrationMembershipChanges(recipientId, threadId, GroupMigrationMembershipChange.empty()); + } + + private void insertGroupV1MigrationMembershipChanges(@NonNull RecipientId recipientId, + long threadId, + @NonNull GroupMigrationMembershipChange membershipChange) + { + ContentValues values = new ContentValues(); + values.put(RECIPIENT_ID, recipientId.serialize()); + values.put(RECIPIENT_DEVICE_ID, 1); + values.put(DATE_RECEIVED, System.currentTimeMillis()); + values.put(DATE_SENT, System.currentTimeMillis()); + values.put(READ, 1); + values.put(TYPE, Types.GV1_MIGRATION_TYPE); + values.put(THREAD_ID, threadId); + + if (!membershipChange.isEmpty()) { + values.put(BODY, membershipChange.serialize()); + } + + databaseHelper.getSignalWritableDatabase().insert(TABLE_NAME, null, values); + } + + public void insertNumberChangeMessages(@NonNull RecipientId recipientId) { + ThreadTable threadTable = SignalDatabase.threads(); + List groupRecords = SignalDatabase.groups().getGroupsContainingMember(recipientId, false); + List threadIdsToUpdate = new LinkedList<>(); + + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + db.beginTransaction(); + + try { + threadIdsToUpdate.add(threadTable.getThreadIdFor(recipientId)); + for (GroupTable.GroupRecord groupRecord : groupRecords) { + if (groupRecord.isActive()) { + threadIdsToUpdate.add(threadTable.getThreadIdFor(groupRecord.getRecipientId())); + } + } + + threadIdsToUpdate.stream() + .filter(Objects::nonNull) + .forEach(threadId -> { + ContentValues values = new ContentValues(); + values.put(RECIPIENT_ID, recipientId.serialize()); + values.put(RECIPIENT_DEVICE_ID, 1); + values.put(DATE_RECEIVED, System.currentTimeMillis()); + values.put(DATE_SENT, System.currentTimeMillis()); + values.put(READ, 1); + values.put(TYPE, Types.CHANGE_NUMBER_TYPE); + values.put(THREAD_ID, threadId); + values.putNull(BODY); + + db.insert(TABLE_NAME, null, values); + }); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + threadIdsToUpdate.stream() + .filter(Objects::nonNull) + .forEach(threadId -> { + TrimThreadJob.enqueueAsync(threadId); + SignalDatabase.threads().update(threadId, true); + notifyConversationListeners(threadId); + }); + } + + public void insertBoostRequestMessage(@NonNull RecipientId recipientId, long threadId) { + ContentValues values = new ContentValues(); + values.put(RECIPIENT_ID, recipientId.serialize()); + values.put(RECIPIENT_DEVICE_ID, 1); + values.put(DATE_RECEIVED, System.currentTimeMillis()); + values.put(DATE_SENT, System.currentTimeMillis()); + values.put(READ, 1); + values.put(TYPE, Types.BOOST_REQUEST_TYPE); + values.put(THREAD_ID, threadId); + values.putNull(BODY); + + getWritableDatabase().insert(TABLE_NAME, null, values); + } + + public void insertThreadMergeEvent(@NonNull RecipientId recipientId, long threadId, @NonNull ThreadMergeEvent event) { + ContentValues values = new ContentValues(); + values.put(RECIPIENT_ID, recipientId.serialize()); + values.put(RECIPIENT_DEVICE_ID, 1); + values.put(DATE_RECEIVED, System.currentTimeMillis()); + values.put(DATE_SENT, System.currentTimeMillis()); + values.put(READ, 1); + values.put(TYPE, Types.THREAD_MERGE_TYPE); + values.put(THREAD_ID, threadId); + values.put(BODY, Base64.encodeBytes(event.toByteArray())); + + getWritableDatabase().insert(TABLE_NAME, null, values); + + ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId); + } + + public void insertSmsExportMessage(@NonNull RecipientId recipientId, long threadId) { + ContentValues values = new ContentValues(); + values.put(RECIPIENT_ID, recipientId.serialize()); + values.put(RECIPIENT_DEVICE_ID, 1); + values.put(DATE_RECEIVED, System.currentTimeMillis()); + values.put(DATE_SENT, System.currentTimeMillis()); + values.put(READ, 1); + values.put(TYPE, Types.SMS_EXPORT_TYPE); + values.put(THREAD_ID, threadId); + values.putNull(BODY); + + boolean updated = SQLiteDatabaseExtensionsKt.withinTransaction(getWritableDatabase(), db -> { + if (SignalDatabase.sms().hasSmsExportMessage(threadId)) { + return false; + } else { + db.insert(TABLE_NAME, null, values); + return true; + } + }); + + if (updated) { + ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId); + } + } + + public void endTransaction(SQLiteDatabase database) { + database.endTransaction(); + } + + public void ensureMigration() { + databaseHelper.getSignalWritableDatabase(); + } + + public boolean isStory(long messageId) { + SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); + String[] projection = new String[]{"1"}; + String where = IS_STORY_CLAUSE + " AND " + ID + " = ?"; + String[] whereArgs = SqlUtil.buildArgs(messageId); + + try (Cursor cursor = database.query(TABLE_NAME, projection, where, whereArgs, null, null, null, "1")) { + return cursor != null && cursor.moveToFirst(); + } + } + + public @NonNull MessageTable.Reader getOutgoingStoriesTo(@NonNull RecipientId recipientId) { + Recipient recipient = Recipient.resolved(recipientId); + Long threadId = null; + + if (recipient.isGroup()) { + threadId = SignalDatabase.threads().getThreadIdFor(recipientId); + } + + String where = IS_STORY_CLAUSE + " AND (" + getOutgoingTypeClause() + ")"; + + final String[] whereArgs; + if (threadId == null) { + where += " AND " + RECIPIENT_ID + " = ?"; + whereArgs = SqlUtil.buildArgs(recipientId); + } else { + where += " AND " + THREAD_ID_WHERE; + whereArgs = SqlUtil.buildArgs(threadId); + } + + return new MmsReader(rawQuery(where, whereArgs)); + } + + public @NonNull MessageTable.Reader getAllOutgoingStories(boolean reverse, int limit) { + String where = IS_STORY_CLAUSE + " AND (" + getOutgoingTypeClause() + ")"; + + return new MmsReader(rawQuery(where, null, reverse, limit)); + } + + public @NonNull MessageTable.Reader getAllOutgoingStoriesAt(long sentTimestamp) { + String where = IS_STORY_CLAUSE + " AND " + DATE_SENT + " = ? AND (" + getOutgoingTypeClause() + ")"; + String[] whereArgs = SqlUtil.buildArgs(sentTimestamp); + Cursor cursor = rawQuery(where, whereArgs, false, -1L); + + return new MmsReader(cursor); + } + + public @NonNull List markAllIncomingStoriesRead() { + String where = IS_STORY_CLAUSE + " AND NOT (" + getOutgoingTypeClause() + ") AND " + READ + " = 0"; + + List markedMessageInfos = setMessagesRead(where, null); + notifyConversationListListeners(); + + return markedMessageInfos; + } + + public void markOnboardingStoryRead() { + RecipientId recipientId = SignalStore.releaseChannelValues().getReleaseChannelRecipientId(); + if (recipientId == null) { + return; + } + + String where = IS_STORY_CLAUSE + " AND NOT (" + getOutgoingTypeClause() + ") AND " + READ + " = 0 AND " + RECIPIENT_ID + " = ?"; + + List markedMessageInfos = setMessagesRead(where, SqlUtil.buildArgs(recipientId)); + if (!markedMessageInfos.isEmpty()) { + notifyConversationListListeners(); + } + } + + public @NonNull MessageTable.Reader getAllStoriesFor(@NonNull RecipientId recipientId, int limit) { + long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId); + String where = IS_STORY_CLAUSE + " AND " + THREAD_ID_WHERE; + String[] whereArgs = SqlUtil.buildArgs(threadId); + Cursor cursor = rawQuery(where, whereArgs, false, limit); + + return new MmsReader(cursor); + } + + public @NonNull MessageTable.Reader getUnreadStories(@NonNull RecipientId recipientId, int limit) { + final long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId); + final String query = IS_STORY_CLAUSE + + " AND NOT (" + getOutgoingTypeClause() + ") " + + " AND " + THREAD_ID_WHERE + + " AND " + VIEWED_RECEIPT_COUNT + " = ?"; + final String[] args = SqlUtil.buildArgs(threadId, 0); + + return new MmsReader(rawQuery(query, args, false, limit)); + } + + public @Nullable ParentStoryId.GroupReply getParentStoryIdForGroupReply(long messageId) { + String[] projection = SqlUtil.buildArgs(PARENT_STORY_ID); + String[] args = SqlUtil.buildArgs(messageId); + + try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, projection, ID_WHERE, args, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + ParentStoryId parentStoryId = ParentStoryId.deserialize(CursorUtil.requireLong(cursor, PARENT_STORY_ID)); + if (parentStoryId != null && parentStoryId.isGroupReply()) { + return (ParentStoryId.GroupReply) parentStoryId; + } else { + return null; + } + } + } + + return null; + } + + public @NonNull StoryViewState getStoryViewState(@NonNull RecipientId recipientId) { + if (!Stories.isFeatureEnabled()) { + return StoryViewState.NONE; + } + + long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId); + + return getStoryViewState(threadId); + } + + /** + * Synchronizes whether we've viewed a recipient's story based on incoming sync messages. + */ + public void updateViewedStories(@NonNull Set syncMessageIds) { + final String timestamps = Util.join(syncMessageIds.stream().map(SyncMessageId::getTimetamp).collect(java.util.stream.Collectors.toList()), ","); + final String[] projection = SqlUtil.buildArgs(RECIPIENT_ID); + final String where = IS_STORY_CLAUSE + " AND " + DATE_SENT + " IN (" + timestamps + ") AND NOT (" + getOutgoingTypeClause() + ") AND " + VIEWED_RECEIPT_COUNT + " > 0"; + + try { + getWritableDatabase().beginTransaction(); + try (Cursor cursor = getWritableDatabase().query(TABLE_NAME, projection, where, null, null, null, null)) { + while (cursor != null && cursor.moveToNext()) { + Recipient recipient = Recipient.resolved(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID))); + SignalDatabase.recipients().updateLastStoryViewTimestamp(recipient.getId()); + } + } + getWritableDatabase().setTransactionSuccessful(); + } finally { + getWritableDatabase().endTransaction(); + } + } + + @VisibleForTesting + @NonNull StoryViewState getStoryViewState(long threadId) { + final String hasStoryQuery = "SELECT EXISTS(SELECT 1 FROM " + TABLE_NAME + " WHERE " + IS_STORY_CLAUSE + " AND " + THREAD_ID_WHERE + " LIMIT 1)"; + final String[] hasStoryArgs = SqlUtil.buildArgs(threadId); + final boolean hasStories; + + try (Cursor cursor = getReadableDatabase().rawQuery(hasStoryQuery, hasStoryArgs)) { + hasStories = cursor != null && cursor.moveToFirst() && !cursor.isNull(0) && cursor.getInt(0) == 1; + } + + if (!hasStories) { + return StoryViewState.NONE; + } + + final String hasUnviewedStoriesQuery = "SELECT EXISTS(SELECT 1 FROM " + TABLE_NAME + " WHERE " + IS_STORY_CLAUSE + " AND " + THREAD_ID_WHERE + " AND " + VIEWED_RECEIPT_COUNT + " = ? " + "AND NOT (" + getOutgoingTypeClause() + ") LIMIT 1)"; + final String[] hasUnviewedStoriesArgs = SqlUtil.buildArgs(threadId, 0); + final boolean hasUnviewedStories; + + try (Cursor cursor = getReadableDatabase().rawQuery(hasUnviewedStoriesQuery, hasUnviewedStoriesArgs)) { + hasUnviewedStories = cursor != null && cursor.moveToFirst() && !cursor.isNull(0) && cursor.getInt(0) == 1; + } + + if (hasUnviewedStories) { + return StoryViewState.UNVIEWED; + } else { + return StoryViewState.VIEWED; + } + } + + public boolean isOutgoingStoryAlreadyInDatabase(@NonNull RecipientId recipientId, long sentTimestamp) { + SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); + String[] projection = new String[]{"COUNT(*)"}; + String where = RECIPIENT_ID + " = ? AND " + STORY_TYPE + " > 0 AND " + DATE_SENT + " = ? AND (" + getOutgoingTypeClause() + ")"; + String[] whereArgs = SqlUtil.buildArgs(recipientId, sentTimestamp); + + try (Cursor cursor = database.query(TABLE_NAME, projection, where, whereArgs, null, null, null, "1")) { + if (cursor != null && cursor.moveToFirst()) { + return cursor.getInt(0) > 0; + } + } + + return false; + } + + public @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException { + SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); + String[] projection = new String[]{ID, RECIPIENT_ID}; + String where = IS_STORY_CLAUSE + " AND " + DATE_SENT + " = ?"; + String[] whereArgs = SqlUtil.buildArgs(sentTimestamp); + + try (Cursor cursor = database.query(TABLE_NAME, projection, where, whereArgs, null, null, null, "1")) { + if (cursor != null && cursor.moveToFirst()) { + RecipientId rowRecipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))); + + if (Recipient.self().getId().equals(authorId) || rowRecipientId.equals(authorId)) { + return new MessageId(CursorUtil.requireLong(cursor, ID), true); + } + } + } + + throw new NoSuchMessageException("No story sent at " + sentTimestamp); + } + + public @NonNull List getUnreadStoryThreadRecipientIds() { + SQLiteDatabase db = getReadableDatabase(); + String query = "SELECT DISTINCT " + ThreadTable.TABLE_NAME + "." + ThreadTable.RECIPIENT_ID + "\n" + + "FROM " + TABLE_NAME + "\n" + + "JOIN " + ThreadTable.TABLE_NAME + "\n" + + "ON " + TABLE_NAME + "." + THREAD_ID + " = " + ThreadTable.TABLE_NAME + "." + ThreadTable.ID + "\n" + + "WHERE " + IS_STORY_CLAUSE + " AND (" + getOutgoingTypeClause() + ") = 0 AND " + VIEWED_RECEIPT_COUNT + " = 0 AND " + TABLE_NAME + "." + READ + " = 0"; + + try (Cursor cursor = db.rawQuery(query, null)) { + if (cursor != null) { + List recipientIds = new ArrayList<>(cursor.getCount()); + while (cursor.moveToNext()) { + recipientIds.add(RecipientId.from(cursor.getLong(0))); + } + + return recipientIds; + } + } + + return Collections.emptyList(); + } + + public @NonNull List getOrderedStoryRecipientsAndIds(boolean isOutgoingOnly) { + String where = "WHERE " + STORY_TYPE + " > 0 AND " + REMOTE_DELETED + " = 0" + (isOutgoingOnly ? " AND is_outgoing != 0" : "") + "\n"; + SQLiteDatabase db = getReadableDatabase(); + String query = "SELECT\n" + + " " + TABLE_NAME + "." + DATE_SENT + " AS sent_timestamp,\n" + + " " + TABLE_NAME + "." + ID + " AS mms_id,\n" + + " " + ThreadTable.TABLE_NAME + "." + ThreadTable.RECIPIENT_ID + ",\n" + + " (" + getOutgoingTypeClause() + ") AS is_outgoing,\n" + + " " + VIEWED_RECEIPT_COUNT + ",\n" + + " " + TABLE_NAME + "." + DATE_SENT + ",\n" + + " " + RECEIPT_TIMESTAMP + ",\n" + + " (" + getOutgoingTypeClause() + ") = 0 AND " + VIEWED_RECEIPT_COUNT + " = 0 AS is_unread\n" + + "FROM " + TABLE_NAME + "\n" + + "JOIN " + ThreadTable.TABLE_NAME + "\n" + + "ON " + TABLE_NAME + "." + THREAD_ID + " = " + ThreadTable.TABLE_NAME + "." + ThreadTable.ID + "\n" + + where + + "ORDER BY\n" + + "is_unread DESC,\n" + + "CASE\n" + + "WHEN is_outgoing = 0 AND " + VIEWED_RECEIPT_COUNT + " = 0 THEN " + MessageTable.TABLE_NAME + "." + MessageTable.DATE_SENT + "\n" + + "WHEN is_outgoing = 0 AND viewed_receipt_count > 0 THEN " + MessageTable.RECEIPT_TIMESTAMP + "\n" + + "WHEN is_outgoing = 1 THEN " + MessageTable.TABLE_NAME + "." + MessageTable.DATE_SENT + "\n" + + "END DESC"; + + List results; + try (Cursor cursor = db.rawQuery(query, null)) { + if (cursor != null) { + results = new ArrayList<>(cursor.getCount()); + + while (cursor.moveToNext()) { + results.add(new StoryResult(RecipientId.from(CursorUtil.requireLong(cursor, ThreadTable.RECIPIENT_ID)), + CursorUtil.requireLong(cursor, "mms_id"), + CursorUtil.requireLong(cursor, "sent_timestamp"), + CursorUtil.requireBoolean(cursor, "is_outgoing"))); + } + + return results; + } + } + + return Collections.emptyList(); + } + + public @NonNull Cursor getStoryReplies(long parentStoryId) { + String where = PARENT_STORY_ID + " = ?"; + String[] whereArgs = SqlUtil.buildArgs(parentStoryId); + + return rawQuery(where, whereArgs, false, 0); + } + + public int getNumberOfStoryReplies(long parentStoryId) { + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); + String[] columns = new String[]{"COUNT(*)"}; + String where = PARENT_STORY_ID + " = ?"; + String[] whereArgs = SqlUtil.buildArgs(parentStoryId); + + try (Cursor cursor = db.query(TABLE_NAME, columns, where, whereArgs, null, null, null, null)) { + return cursor != null && cursor.moveToNext() ? cursor.getInt(0) : 0; + } + } + + public boolean containsStories(long threadId) { + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); + String[] columns = new String[]{"1"}; + String where = THREAD_ID_WHERE + " AND " + STORY_TYPE + " > 0"; + String[] whereArgs = SqlUtil.buildArgs(threadId); + + try (Cursor cursor = db.query(TABLE_NAME, columns, where, whereArgs, null, null, null, "1")) { + return cursor != null && cursor.moveToNext(); + } + } + + public boolean hasSelfReplyInStory(long parentStoryId) { + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); + String[] columns = new String[]{"COUNT(*)"}; + String where = PARENT_STORY_ID + " = ? AND (" + getOutgoingTypeClause() + ")"; + String[] whereArgs = SqlUtil.buildArgs(-parentStoryId); + + try (Cursor cursor = db.query(TABLE_NAME, columns, where, whereArgs, null, null, null, null)) { + return cursor != null && cursor.moveToNext() && cursor.getInt(0) > 0; + } + } + + public boolean hasGroupReplyOrReactionInStory(long parentStoryId) { + return hasSelfReplyInStory(-parentStoryId); + } + + public @Nullable Long getOldestStorySendTimestamp(boolean hasSeenReleaseChannelStories) { + long releaseChannelThreadId = getReleaseChannelThreadId(hasSeenReleaseChannelStories); + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); + String[] columns = new String[] { DATE_SENT }; + String where = IS_STORY_CLAUSE + " AND " + THREAD_ID + " != ?"; + String orderBy = DATE_SENT + " ASC"; + String limit = "1"; + + try (Cursor cursor = db.query(TABLE_NAME, columns, where, SqlUtil.buildArgs(releaseChannelThreadId), null, null, orderBy, limit)) { + return cursor != null && cursor.moveToNext() ? cursor.getLong(0) : null; + } + } + + private static long getReleaseChannelThreadId(boolean hasSeenReleaseChannelStories) { + if (hasSeenReleaseChannelStories) { + return -1L; + } + + RecipientId releaseChannelRecipientId = SignalStore.releaseChannelValues().getReleaseChannelRecipientId(); + if (releaseChannelRecipientId == null) { + return -1L; + } + + Long releaseChannelThreadId = SignalDatabase.threads().getThreadIdFor(releaseChannelRecipientId); + if (releaseChannelThreadId == null) { + return -1L; + } + + return releaseChannelThreadId; + } + + public void deleteGroupStoryReplies(long parentStoryId) { + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + String[] args = SqlUtil.buildArgs(parentStoryId); + + db.delete(TABLE_NAME, PARENT_STORY_ID + " = ?", args); + } + + public int deleteStoriesOlderThan(long timestamp, boolean hasSeenReleaseChannelStories) { + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + + db.beginTransaction(); + try { + long releaseChannelThreadId = getReleaseChannelThreadId(hasSeenReleaseChannelStories); + String storiesBeforeTimestampWhere = IS_STORY_CLAUSE + " AND " + DATE_SENT + " < ? AND " + THREAD_ID + " != ?"; + String[] sharedArgs = SqlUtil.buildArgs(timestamp, releaseChannelThreadId); + String deleteStoryRepliesQuery = "DELETE FROM " + TABLE_NAME + " " + + "WHERE " + PARENT_STORY_ID + " > 0 AND " + PARENT_STORY_ID + " IN (" + + "SELECT " + ID + " " + + "FROM " + TABLE_NAME + " " + + "WHERE " + storiesBeforeTimestampWhere + + ")"; + String disassociateQuoteQuery = "UPDATE " + TABLE_NAME + " " + + "SET " + QUOTE_MISSING + " = 1, " + QUOTE_BODY + " = '' " + + "WHERE " + PARENT_STORY_ID + " < 0 AND ABS(" + PARENT_STORY_ID + ") IN (" + + "SELECT " + ID + " " + + "FROM " + TABLE_NAME + " " + + "WHERE " + storiesBeforeTimestampWhere + + ")"; + + db.execSQL(deleteStoryRepliesQuery, sharedArgs); + db.execSQL(disassociateQuoteQuery, sharedArgs); + + try (Cursor cursor = db.query(TABLE_NAME, new String[]{RECIPIENT_ID}, storiesBeforeTimestampWhere, sharedArgs, null, null, null)) { + while (cursor != null && cursor.moveToNext()) { + RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))); + ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(recipientId); + } + } + + int deletedStoryCount; + try (Cursor cursor = db.query(TABLE_NAME, new String[]{ID}, storiesBeforeTimestampWhere, sharedArgs, null, null, null)) { + deletedStoryCount = cursor.getCount(); + + while (cursor.moveToNext()) { + long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID)); + deleteMessage(id); + } + } + + db.setTransactionSuccessful(); + return deletedStoryCount; + } finally { + db.endTransaction(); + } + } + + private void disassociateStoryQuotes(long storyId) { + ContentValues contentValues = new ContentValues(2); + contentValues.put(QUOTE_MISSING, 1); + contentValues.putNull(QUOTE_BODY); + + getWritableDatabase().update(TABLE_NAME, + contentValues, + PARENT_STORY_ID + " = ?", + SqlUtil.buildArgs(new ParentStoryId.DirectReply(storyId).serialize())); + } + + public boolean isGroupQuitMessage(long messageId) { + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); + + String[] columns = new String[]{ID}; + long type = Types.getOutgoingEncryptedMessageType() | Types.GROUP_LEAVE_BIT; + String query = ID + " = ? AND " + TYPE + " & " + type + " = " + type + " AND " + TYPE + " & " + Types.GROUP_V2_BIT + " = 0"; + String[] args = SqlUtil.buildArgs(messageId); + + try (Cursor cursor = db.query(TABLE_NAME, columns, query, args, null, null, null, null)) { + if (cursor.getCount() == 1) { + return true; + } + } + + return false; + } + + public long getLatestGroupQuitTimestamp(long threadId, long quitTimeBarrier) { + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); + + String[] columns = new String[]{DATE_SENT}; + long type = Types.getOutgoingEncryptedMessageType() | Types.GROUP_LEAVE_BIT; + String query = THREAD_ID + " = ? AND " + TYPE + " & " + type + " = " + type + " AND " + TYPE + " & " + Types.GROUP_V2_BIT + " = 0 AND " + DATE_SENT + " < ?"; + String[] args = new String[]{String.valueOf(threadId), String.valueOf(quitTimeBarrier)}; + String orderBy = DATE_SENT + " DESC"; + String limit = "1"; + + try (Cursor cursor = db.query(TABLE_NAME, columns, query, args, null, null, orderBy, limit)) { + if (cursor.moveToFirst()) { + return CursorUtil.requireLong(cursor, DATE_SENT); + } + } + + return -1; + } + + public int getMessageCountForThread(long threadId) { + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); + + String query = THREAD_ID + " = ? AND " + STORY_TYPE + " = ? AND " + PARENT_STORY_ID + " <= ?"; + String[] args = SqlUtil.buildArgs(threadId, 0, 0); + + try (Cursor cursor = db.query(TABLE_NAME, COUNT, query, args, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + return cursor.getInt(0); + } + } + + return 0; + } + + public int getMessageCountForThread(long threadId, long beforeTime) { + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); + + String query = THREAD_ID + " = ? AND " + DATE_RECEIVED + " < ? AND " + STORY_TYPE + " = ? AND " + PARENT_STORY_ID + " <= ?"; + String[] args = SqlUtil.buildArgs(threadId, beforeTime, 0, 0); + + try (Cursor cursor = db.query(TABLE_NAME, COUNT, query, args, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + return cursor.getInt(0); + } + } + + return 0; + } + + public boolean hasMeaningfulMessage(long threadId) { + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); + SqlUtil.Query query = buildMeaningfulMessagesQuery(threadId); + + try (Cursor cursor = db.query(TABLE_NAME, new String[] { "1" }, query.getWhere(), query.getWhereArgs(), null, null, null, "1")) { + return cursor != null && cursor.moveToFirst(); + } + } + + public int getIncomingMeaningfulMessageCountSince(long threadId, long afterTime) { + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); + String[] projection = SqlUtil.COUNT; + SqlUtil.Query meaningfulMessagesQuery = buildMeaningfulMessagesQuery(threadId); + String where = meaningfulMessagesQuery.getWhere() + " AND " + DATE_RECEIVED + " >= ?"; + String[] whereArgs = SqlUtil.appendArg(meaningfulMessagesQuery.getWhereArgs(), String.valueOf(afterTime)); + + try (Cursor cursor = db.query(TABLE_NAME, projection, where, whereArgs, null, null, null, "1")) { + if (cursor != null && cursor.moveToFirst()) { + return cursor.getInt(0); + } else { + return 0; + } + } + } + + private @NonNull SqlUtil.Query buildMeaningfulMessagesQuery(long threadId) { + String query = THREAD_ID + " = ? AND " + STORY_TYPE + " = ? AND " + PARENT_STORY_ID + " <= ? AND (NOT " + TYPE + " & ? AND " + TYPE + " != ? AND " + TYPE + " != ? AND " + TYPE + " != ? AND " + TYPE + " != ? AND " + TYPE + " & " + MmsSmsColumns.Types.GROUP_V2_LEAVE_BITS + " != " + MmsSmsColumns.Types.GROUP_V2_LEAVE_BITS + ")"; + return SqlUtil.buildQuery(query, threadId, 0, 0, MmsSmsColumns.Types.IGNORABLE_TYPESMASK_WHEN_COUNTING, Types.PROFILE_CHANGE_TYPE, Types.CHANGE_NUMBER_TYPE, Types.SMS_EXPORT_TYPE, Types.BOOST_REQUEST_TYPE); + } + + public void addFailures(long messageId, List failure) { + try { + addToDocument(messageId, NETWORK_FAILURES, failure, NetworkFailureSet.class); + } catch (IOException e) { + Log.w(TAG, e); + } + } + + public void setNetworkFailures(long messageId, Set failures) { + try { + setDocument(databaseHelper.getSignalWritableDatabase(), messageId, NETWORK_FAILURES, new NetworkFailureSet(failures)); + } catch (IOException e) { + Log.w(TAG, e); + } + } + + public Set incrementReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType, MessageQualifier messageQualifier) { + SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); + Set messageUpdates = new HashSet<>(); + + final String qualifierWhere; + switch (messageQualifier) { + case NORMAL: + qualifierWhere = " AND NOT (" + IS_STORY_CLAUSE + ")"; + break; + case STORY: + qualifierWhere = " AND " + IS_STORY_CLAUSE; + break; + case ALL: + qualifierWhere = ""; + break; + default: + throw new IllegalArgumentException("Unsupported qualifier: " + messageQualifier); + } + + try (Cursor cursor = SQLiteDatabaseExtensionsKt.select(database, ID, THREAD_ID, TYPE, RECIPIENT_ID, receiptType.getColumnName(), RECEIPT_TIMESTAMP) + .from(TABLE_NAME) + .where(DATE_SENT + " = ?" + qualifierWhere, messageId.getTimetamp()) + .run()) + { + while (cursor.moveToNext()) { + if (Types.isOutgoingMessageType(CursorUtil.requireLong(cursor, TYPE))) { + RecipientId theirRecipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); + RecipientId ourRecipientId = messageId.getRecipientId(); + String columnName = receiptType.getColumnName(); + + if (ourRecipientId.equals(theirRecipientId) || Recipient.resolved(theirRecipientId).isGroup()) { + long id = CursorUtil.requireLong(cursor, ID); + long threadId = CursorUtil.requireLong(cursor, THREAD_ID); + int status = receiptType.getGroupStatus(); + boolean isFirstIncrement = CursorUtil.requireLong(cursor, columnName) == 0; + long savedTimestamp = CursorUtil.requireLong(cursor, RECEIPT_TIMESTAMP); + long updatedTimestamp = isFirstIncrement ? Math.max(savedTimestamp, timestamp) : savedTimestamp; + + database.execSQL("UPDATE " + TABLE_NAME + " SET " + + columnName + " = " + columnName + " + 1, " + + RECEIPT_TIMESTAMP + " = ? WHERE " + + ID + " = ?", + SqlUtil.buildArgs(updatedTimestamp, id)); + + SignalDatabase.groupReceipts().update(ourRecipientId, id, status, timestamp); + + messageUpdates.add(new MessageUpdate(threadId, new MessageId(id, true))); + } + } + } + + if (messageUpdates.size() > 0 && receiptType == ReceiptType.DELIVERY) { + earlyDeliveryReceiptCache.increment(messageId.getTimetamp(), messageId.getRecipientId(), timestamp); + } + } + + messageUpdates.addAll(incrementStoryReceiptCount(messageId, timestamp, receiptType)); + + return messageUpdates; + } + + private Set incrementStoryReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType) { + SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); + Set messageUpdates = new HashSet<>(); + String columnName = receiptType.getColumnName(); + + for (MessageId storyMessageId : SignalDatabase.storySends().getStoryMessagesFor(messageId)) { + database.execSQL("UPDATE " + TABLE_NAME + " SET " + + columnName + " = " + columnName + " + 1, " + + RECEIPT_TIMESTAMP + " = CASE " + + "WHEN " + columnName + " = 0 THEN MAX(" + RECEIPT_TIMESTAMP + ", ?) " + + "ELSE " + RECEIPT_TIMESTAMP + " " + + "END " + + "WHERE " + ID + " = ?", + SqlUtil.buildArgs(timestamp, storyMessageId.getId())); + + SignalDatabase.groupReceipts().update(messageId.getRecipientId(), storyMessageId.getId(), receiptType.getGroupStatus(), timestamp); + + messageUpdates.add(new MessageUpdate(-1, storyMessageId)); + } + + return messageUpdates; + } + + public long getThreadIdForMessage(long id) { + String sql = "SELECT " + THREAD_ID + " FROM " + TABLE_NAME + " WHERE " + ID + " = ?"; + String[] sqlArgs = new String[] {id+""}; + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); + + Cursor cursor = null; + + try { + cursor = db.rawQuery(sql, sqlArgs); + if (cursor != null && cursor.moveToFirst()) + return cursor.getLong(0); + else + return -1; + } finally { + if (cursor != null) + cursor.close(); + } + } + + private long getThreadIdFor(@NonNull IncomingMediaMessage retrieved) { + if (retrieved.getGroupId() != null) { + RecipientId groupRecipientId = SignalDatabase.recipients().getOrInsertFromPossiblyMigratedGroupId(retrieved.getGroupId()); + Recipient groupRecipients = Recipient.resolved(groupRecipientId); + return SignalDatabase.threads().getOrCreateThreadIdFor(groupRecipients); + } else { + Recipient sender = Recipient.resolved(retrieved.getFrom()); + return SignalDatabase.threads().getOrCreateThreadIdFor(sender); + } + } + + private long getThreadIdFor(@NonNull NotificationInd notification) { + String fromString = notification.getFrom() != null && notification.getFrom().getTextString() != null + ? Util.toIsoString(notification.getFrom().getTextString()) + : ""; + Recipient recipient = Recipient.external(context, fromString); + return SignalDatabase.threads().getOrCreateThreadIdFor(recipient); + } + + private Cursor rawQuery(@NonNull String where, @Nullable String[] arguments) { + return rawQuery(where, arguments, false, 0); + } + + private Cursor rawQuery(@NonNull String where, @Nullable String[] arguments, boolean reverse, long limit) { + return rawQuery(MMS_PROJECTION, where, arguments, reverse, limit); + } + + private Cursor rawQuery(@NonNull String[] projection, @NonNull String where, @Nullable String[] arguments, boolean reverse, long limit) { + SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); + String rawQueryString = "SELECT " + Util.join(projection, ",") + + " FROM " + MessageTable.TABLE_NAME + " LEFT OUTER JOIN " + AttachmentTable.TABLE_NAME + + " ON (" + MessageTable.TABLE_NAME + "." + MessageTable.ID + " = " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.MMS_ID + ")" + + " WHERE " + where + " GROUP BY " + MessageTable.TABLE_NAME + "." + MessageTable.ID; + + if (reverse) { + rawQueryString += " ORDER BY " + MessageTable.TABLE_NAME + "." + MessageTable.ID + " DESC"; + } + + if (limit > 0) { + rawQueryString += " LIMIT " + limit; + } + + return database.rawQuery(rawQueryString, arguments); + } + + private Cursor internalGetMessage(long messageId) { + return rawQuery(RAW_ID_WHERE, new String[] {messageId + ""}); + } + + public MessageRecord getMessageRecord(long messageId) throws NoSuchMessageException { + try (Cursor cursor = rawQuery(RAW_ID_WHERE, new String[] {messageId + ""})) { + MessageRecord record = new MmsReader(cursor).getNext(); + + if (record == null) { + throw new NoSuchMessageException("No message for ID: " + messageId); + } + + return record; + } + } + + public @Nullable MessageRecord getMessageRecordOrNull(long messageId) { + try (Cursor cursor = rawQuery(RAW_ID_WHERE, new String[] {messageId + ""})) { + return new MmsReader(cursor).getNext(); + } + } + + public MmsReader getMessages(Collection messageIds) { + String ids = TextUtils.join(",", messageIds); + return mmsReaderFor(rawQuery(MessageTable.TABLE_NAME + "." + MessageTable.ID + " IN (" + ids + ")", null)); + } + + private void updateMailboxBitmask(long id, long maskOff, long maskOn, Optional threadId) { + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + + db.beginTransaction(); + try { + db.execSQL("UPDATE " + TABLE_NAME + + " SET " + TYPE + " = (" + TYPE + " & " + (Types.TOTAL_MASK - maskOff) + " | " + maskOn + " )" + + " WHERE " + ID + " = ?", new String[] { id + "" }); + + if (threadId.isPresent()) { + SignalDatabase.threads().updateSnippetTypeSilently(threadId.get()); + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + public void markAsOutbox(long messageId) { + long threadId = getThreadIdForMessage(messageId); + updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_OUTBOX_TYPE, Optional.of(threadId)); + } + + public void markAsForcedSms(long messageId) { + long threadId = getThreadIdForMessage(messageId); + updateMailboxBitmask(messageId, Types.PUSH_MESSAGE_BIT, Types.MESSAGE_FORCE_SMS_BIT, Optional.of(threadId)); + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); + } + + public void markAsRateLimited(long messageId) { + long threadId = getThreadIdForMessage(messageId); + updateMailboxBitmask(messageId, 0, Types.MESSAGE_RATE_LIMITED_BIT, Optional.of(threadId)); + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); + } + + public void clearRateLimitStatus(@NonNull Collection ids) { + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + + db.beginTransaction(); + try { + for (long id : ids) { + long threadId = getThreadIdForMessage(id); + updateMailboxBitmask(id, Types.MESSAGE_RATE_LIMITED_BIT, 0, Optional.of(threadId)); + } + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + public void markAsPendingInsecureSmsFallback(long messageId) { + long threadId = getThreadIdForMessage(messageId); + updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_PENDING_INSECURE_SMS_FALLBACK, Optional.of(threadId)); + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); + } + + public void markAsSending(long messageId) { + long threadId = getThreadIdForMessage(messageId); + updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_SENDING_TYPE, Optional.of(threadId)); + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); + ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners(); + } + + public void markAsSentFailed(long messageId) { + long threadId = getThreadIdForMessage(messageId); + updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_SENT_FAILED_TYPE, Optional.of(threadId)); + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); + ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners(); + } + + public void markAsSent(long messageId, boolean secure) { + long threadId = getThreadIdForMessage(messageId); + updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_SENT_TYPE | (secure ? Types.PUSH_MESSAGE_BIT | Types.SECURE_MESSAGE_BIT : 0), Optional.of(threadId)); + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); + ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners(); + } + + public void markAsRemoteDelete(long messageId) { + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + + long threadId; + + boolean deletedAttachments = false; + + db.beginTransaction(); + try { + ContentValues values = new ContentValues(); + values.put(REMOTE_DELETED, 1); + values.putNull(BODY); + values.putNull(QUOTE_BODY); + values.putNull(QUOTE_AUTHOR); + values.putNull(QUOTE_TYPE); + values.putNull(QUOTE_ID); + values.putNull(LINK_PREVIEWS); + values.putNull(SHARED_CONTACTS); + db.update(TABLE_NAME, values, ID_WHERE, new String[] { String.valueOf(messageId) }); + + deletedAttachments = SignalDatabase.attachments().deleteAttachmentsForMessage(messageId); + SignalDatabase.mentions().deleteMentionsForMessage(messageId); + SignalDatabase.messageLog().deleteAllRelatedToMessage(messageId, true); + SignalDatabase.reactions().deleteReactions(new MessageId(messageId, true)); + deleteGroupStoryReplies(messageId); + disassociateStoryQuotes(messageId); + + threadId = getThreadIdForMessage(messageId); + SignalDatabase.threads().update(threadId, false); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); + ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners(); + + if (deletedAttachments) { + ApplicationDependencies.getDatabaseObserver().notifyAttachmentObservers(); + } + } + + public void markDownloadState(long messageId, long state) { + SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); + ContentValues contentValues = new ContentValues(); + contentValues.put(MMS_STATUS, state); + + database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {messageId + ""}); + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); + } + + public void markAsInsecure(long messageId) { + updateMailboxBitmask(messageId, Types.SECURE_MESSAGE_BIT, 0, Optional.empty()); + } + + public void markUnidentified(long messageId, boolean unidentified) { + ContentValues contentValues = new ContentValues(); + contentValues.put(UNIDENTIFIED, unidentified ? 1 : 0); + + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)}); + } + + public void markExpireStarted(long id) { + markExpireStarted(id, System.currentTimeMillis()); + } + + public void markExpireStarted(long id, long startedTimestamp) { + markExpireStarted(Collections.singleton(id), startedTimestamp); + } + + public void markExpireStarted(Collection ids, long startedAtTimestamp) { + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + long threadId = -1; + + db.beginTransaction(); + try { + String query = ID + " = ? AND (" + EXPIRE_STARTED + " = 0 OR " + EXPIRE_STARTED + " > ?)"; + + for (long id : ids) { + ContentValues contentValues = new ContentValues(); + contentValues.put(EXPIRE_STARTED, startedAtTimestamp); + + db.update(TABLE_NAME, contentValues, query, new String[]{String.valueOf(id), String.valueOf(startedAtTimestamp)}); + + if (threadId < 0) { + threadId = getThreadIdForMessage(id); + } + } + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + SignalDatabase.threads().update(threadId, false); + notifyConversationListeners(threadId); + } + + public void markAsNotified(long id) { + SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); + ContentValues contentValues = new ContentValues(); + + contentValues.put(NOTIFIED, 1); + contentValues.put(REACTIONS_LAST_SEEN, System.currentTimeMillis()); + + database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(id)}); + } + + public List setMessagesReadSince(long threadId, long sinceTimestamp) { + if (sinceTimestamp == -1) { + return setMessagesRead(THREAD_ID + " = ? AND " + STORY_TYPE + " = 0 AND " + PARENT_STORY_ID + " <= 0 AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND (" + getOutgoingTypeClause() + ")))", new String[] { String.valueOf(threadId)}); + } else { + return setMessagesRead(THREAD_ID + " = ? AND " + STORY_TYPE + " = 0 AND " + PARENT_STORY_ID + " <= 0 AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND ( " + getOutgoingTypeClause() + " ))) AND " + DATE_RECEIVED + " <= ?", new String[]{ String.valueOf(threadId), String.valueOf(sinceTimestamp)}); + } + } + + public @NonNull List setGroupStoryMessagesReadSince(long threadId, long groupStoryId, long sinceTimestamp) { + if (sinceTimestamp == -1) { + return setMessagesRead(THREAD_ID + " = ? AND " + STORY_TYPE + " = 0 AND " + PARENT_STORY_ID + " = ? AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND (" + getOutgoingTypeClause() + ")))", SqlUtil.buildArgs(threadId, groupStoryId)); + } else { + return setMessagesRead(THREAD_ID + " = ? AND " + STORY_TYPE + " = 0 AND " + PARENT_STORY_ID + " = ? AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND ( " + getOutgoingTypeClause() + " ))) AND " + DATE_RECEIVED + " <= ?", SqlUtil.buildArgs(threadId, groupStoryId, sinceTimestamp)); + } + } + + public @NonNull List getStoryTypes(@NonNull List messageIds) { + List mmsMessages = messageIds.stream() + .filter(MessageId::isMms) + .map(MessageId::getId) + .collect(java.util.stream.Collectors.toList()); + + if (mmsMessages.isEmpty()) { + return Collections.emptyList(); + } + + String[] projection = SqlUtil.buildArgs(ID, STORY_TYPE); + List queries = SqlUtil.buildCollectionQuery(ID, mmsMessages); + HashMap storyTypes = new HashMap<>(); + + for (final SqlUtil.Query query : queries) { + try (Cursor cursor = getWritableDatabase().query(TABLE_NAME, projection, query.getWhere(), query.getWhereArgs(), null, null, null)) { + while (cursor != null && cursor.moveToNext()) { + storyTypes.put(CursorUtil.requireLong(cursor, ID), StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE))); + } + } + } + + return messageIds.stream().map(id -> { + if (id.isMms() && storyTypes.containsKey(id.getId())) { + return storyTypes.get(id.getId()); + } else { + return StoryType.NONE; + } + }).collect(java.util.stream.Collectors.toList()); + } + + public List setEntireThreadRead(long threadId) { + return setMessagesRead(THREAD_ID + " = ? AND " + STORY_TYPE + " = 0 AND " + PARENT_STORY_ID + " <= 0", new String[] { String.valueOf(threadId)}); + } + + public List setAllMessagesRead() { + return setMessagesRead(STORY_TYPE + " = 0 AND " + PARENT_STORY_ID + " <= 0 AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND (" + getOutgoingTypeClause() + ")))", null); + } + + private List setMessagesRead(String where, String[] arguments) { + SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); + List result = new LinkedList<>(); + Cursor cursor = null; + RecipientId releaseChannelId = SignalStore.releaseChannelValues().getReleaseChannelRecipientId(); + + database.beginTransaction(); + + try { + cursor = database.query(TABLE_NAME, new String[] { ID, RECIPIENT_ID, DATE_SENT, TYPE, EXPIRES_IN, EXPIRE_STARTED, THREAD_ID, STORY_TYPE }, where, arguments, null, null, null); + + while(cursor != null && cursor.moveToNext()) { + if (Types.isSecureType(CursorUtil.requireLong(cursor, TYPE))) { + long threadId = CursorUtil.requireLong(cursor, THREAD_ID); + RecipientId recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); + long dateSent = CursorUtil.requireLong(cursor, DATE_SENT); + long messageId = CursorUtil.requireLong(cursor, ID); + long expiresIn = CursorUtil.requireLong(cursor, EXPIRES_IN); + long expireStarted = CursorUtil.requireLong(cursor, EXPIRE_STARTED); + SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent); + ExpirationInfo expirationInfo = new ExpirationInfo(messageId, expiresIn, expireStarted, true); + StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE)); + + if (!recipientId.equals(releaseChannelId)) { + result.add(new MarkedMessageInfo(threadId, syncMessageId, new MessageId(messageId, true), expirationInfo)); + } + } + } + + ContentValues contentValues = new ContentValues(); + contentValues.put(READ, 1); + contentValues.put(REACTIONS_UNREAD, 0); + contentValues.put(REACTIONS_LAST_SEEN, System.currentTimeMillis()); + + database.update(TABLE_NAME, contentValues, where, arguments); + database.setTransactionSuccessful(); + } finally { + if (cursor != null) cursor.close(); + database.endTransaction(); + } + + return result; + } + + public @Nullable Pair getOldestUnreadMentionDetails(long threadId) { + SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); + String[] projection = new String[]{RECIPIENT_ID,DATE_RECEIVED}; + String selection = THREAD_ID + " = ? AND " + READ + " = 0 AND " + MENTIONS_SELF + " = 1"; + String[] args = SqlUtil.buildArgs(threadId); + + try (Cursor cursor = database.query(TABLE_NAME, projection, selection, args, null, null, DATE_RECEIVED + " ASC", "1")) { + if (cursor != null && cursor.moveToFirst()) { + return new Pair<>(RecipientId.from(CursorUtil.requireString(cursor, RECIPIENT_ID)), CursorUtil.requireLong(cursor, DATE_RECEIVED)); + } + } + + return null; + } + + public int getUnreadMentionCount(long threadId) { + SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); + String[] projection = new String[]{"COUNT(*)"}; + String selection = THREAD_ID + " = ? AND " + READ + " = 0 AND " + MENTIONS_SELF + " = 1"; + String[] args = SqlUtil.buildArgs(threadId); + + try (Cursor cursor = database.query(TABLE_NAME, projection, selection, args, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + return cursor.getInt(0); + } + } + + return 0; + } + + /** + * Trims data related to expired messages. Only intended to be run after a backup restore. + */ + void trimEntriesForExpiredMessages() { + SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); + String trimmedCondition = " NOT IN (SELECT " + MessageTable.ID + " FROM " + MessageTable.TABLE_NAME + ")"; + + database.delete(GroupReceiptTable.TABLE_NAME, GroupReceiptTable.MMS_ID + trimmedCondition, null); + + String[] columns = new String[] { AttachmentTable.ROW_ID, AttachmentTable.UNIQUE_ID }; + String where = AttachmentTable.MMS_ID + trimmedCondition; + + try (Cursor cursor = database.query(AttachmentTable.TABLE_NAME, columns, where, null, null, null, null)) { + while (cursor != null && cursor.moveToNext()) { + SignalDatabase.attachments().deleteAttachment(new AttachmentId(cursor.getLong(0), cursor.getLong(1))); + } + } + + SignalDatabase.mentions().deleteAbandonedMentions(); + + try (Cursor cursor = database.query(ThreadTable.TABLE_NAME, new String[] { ThreadTable.ID }, ThreadTable.EXPIRES_IN + " > 0", null, null, null, null)) { + while (cursor != null && cursor.moveToNext()) { + SignalDatabase.threads().setLastScrolled(cursor.getLong(0), 0); + SignalDatabase.threads().update(cursor.getLong(0), false); + } + } + } + + public Optional getNotification(long messageId) { + Cursor cursor = null; + + try { + cursor = rawQuery(RAW_ID_WHERE, new String[] {String.valueOf(messageId)}); + + if (cursor != null && cursor.moveToNext()) { + return Optional.of(new MmsNotificationInfo(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))), + cursor.getString(cursor.getColumnIndexOrThrow(MMS_CONTENT_LOCATION)), + cursor.getString(cursor.getColumnIndexOrThrow(MMS_TRANSACTION_ID)), + cursor.getInt(cursor.getColumnIndexOrThrow(SMS_SUBSCRIPTION_ID)))); + } else { + return Optional.empty(); + } + } finally { + if (cursor != null) + cursor.close(); + } + } + + public OutgoingMediaMessage getOutgoingMessage(long messageId) + throws MmsException, NoSuchMessageException + { + AttachmentTable attachmentDatabase = SignalDatabase.attachments(); + MentionTable mentionDatabase = SignalDatabase.mentions(); + Cursor cursor = null; + + try { + cursor = rawQuery(RAW_ID_WHERE, new String[] {String.valueOf(messageId)}); + + if (cursor != null && cursor.moveToNext()) { + List associatedAttachments = attachmentDatabase.getAttachmentsForMessage(messageId); + List mentions = mentionDatabase.getMentionsForMessage(messageId); + + long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(TYPE)); + String body = cursor.getString(cursor.getColumnIndexOrThrow(BODY)); + long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(DATE_SENT)); + int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SMS_SUBSCRIPTION_ID)); + long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN)); + boolean viewOnce = cursor.getLong(cursor.getColumnIndexOrThrow(VIEW_ONCE)) == 1; + long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID)); + long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID)); + int distributionType = SignalDatabase.threads().getDistributionType(threadId); + String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(MessageTable.MISMATCHED_IDENTITIES)); + String networkDocument = cursor.getString(cursor.getColumnIndexOrThrow(MessageTable.NETWORK_FAILURES)); + StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE)); + ParentStoryId parentStoryId = ParentStoryId.deserialize(CursorUtil.requireLong(cursor, PARENT_STORY_ID)); + + long quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID)); + long quoteAuthor = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR)); + String quoteText = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_BODY)); + int quoteType = cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE_TYPE)); + boolean quoteMissing = cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE_MISSING)) == 1; + List quoteAttachments = Stream.of(associatedAttachments).filter(Attachment::isQuote).map(a -> (Attachment)a).toList(); + List quoteMentions = parseQuoteMentions(context, cursor); + List contacts = getSharedContacts(cursor, associatedAttachments); + Set contactAttachments = new HashSet<>(Stream.of(contacts).map(Contact::getAvatarAttachment).filter(a -> a != null).toList()); + List previews = getLinkPreviews(cursor, associatedAttachments); + Set previewAttachments = Stream.of(previews).filter(lp -> lp.getThumbnail().isPresent()).map(lp -> lp.getThumbnail().get()).collect(Collectors.toSet()); + List attachments = Stream.of(associatedAttachments).filterNot(Attachment::isQuote) + .filterNot(contactAttachments::contains) + .filterNot(previewAttachments::contains) + .sorted(new DatabaseAttachment.DisplayOrderComparator()) + .map(a -> (Attachment)a).toList(); + + Recipient recipient = Recipient.resolved(RecipientId.from(recipientId)); + Set networkFailures = new HashSet<>(); + Set mismatches = new HashSet<>(); + QuoteModel quote = null; + + if (quoteId > 0 && quoteAuthor > 0 && (!TextUtils.isEmpty(quoteText) || !quoteAttachments.isEmpty())) { + quote = new QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText, quoteMissing, quoteAttachments, quoteMentions, QuoteModel.Type.fromCode(quoteType)); + } + + if (!TextUtils.isEmpty(mismatchDocument)) { + try { + mismatches = JsonUtils.fromJson(mismatchDocument, IdentityKeyMismatchSet.class).getItems(); + } catch (IOException e) { + Log.w(TAG, e); + } + } + + if (!TextUtils.isEmpty(networkDocument)) { + try { + networkFailures = JsonUtils.fromJson(networkDocument, NetworkFailureSet.class).getItems(); + } catch (IOException e) { + Log.w(TAG, e); + } + } + + if (body != null && (Types.isGroupQuit(outboxType) || Types.isGroupUpdate(outboxType))) { + return OutgoingMediaMessage.groupUpdateMessage(recipient, new MessageGroupContext(body, Types.isGroupV2(outboxType)), attachments, timestamp, 0, false, quote, contacts, previews, mentions); + } else if (Types.isExpirationTimerUpdate(outboxType)) { + return OutgoingMediaMessage.expirationUpdateMessage(recipient, timestamp, expiresIn); + } else if (Types.isPaymentsNotification(outboxType)) { + return OutgoingMediaMessage.paymentNotificationMessage(recipient, Objects.requireNonNull(body), timestamp, expiresIn); + } else if (Types.isPaymentsRequestToActivate(outboxType)) { + return OutgoingMediaMessage.requestToActivatePaymentsMessage(recipient, timestamp, expiresIn); + } else if (Types.isPaymentsActivated(outboxType)) { + return OutgoingMediaMessage.paymentsActivatedMessage(recipient, timestamp, expiresIn); + } + + GiftBadge giftBadge = null; + if (body != null && Types.isGiftBadge(outboxType)) { + giftBadge = GiftBadge.parseFrom(Base64.decode(body)); + } + + OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, + body, + attachments, + timestamp, + subscriptionId, + expiresIn, + viewOnce, + distributionType, + storyType, + parentStoryId, + Types.isStoryReaction(outboxType), + quote, + contacts, + previews, + mentions, + networkFailures, + mismatches, + giftBadge, + Types.isSecureType(outboxType)); + + return message; + } + + throw new NoSuchMessageException("No record found for id: " + messageId); + } catch (IOException e) { + throw new MmsException(e); + } finally { + if (cursor != null) + cursor.close(); + } + } + + private static List getSharedContacts(@NonNull Cursor cursor, @NonNull List attachments) { + String serializedContacts = cursor.getString(cursor.getColumnIndexOrThrow(SHARED_CONTACTS)); + + if (TextUtils.isEmpty(serializedContacts)) { + return Collections.emptyList(); + } + + Map attachmentIdMap = new HashMap<>(); + for (DatabaseAttachment attachment : attachments) { + attachmentIdMap.put(attachment.getAttachmentId(), attachment); + } + + try { + List contacts = new LinkedList<>(); + JSONArray jsonContacts = new JSONArray(serializedContacts); + + for (int i = 0; i < jsonContacts.length(); i++) { + Contact contact = Contact.deserialize(jsonContacts.getJSONObject(i).toString()); + + if (contact.getAvatar() != null && contact.getAvatar().getAttachmentId() != null) { + DatabaseAttachment attachment = attachmentIdMap.get(contact.getAvatar().getAttachmentId()); + Avatar updatedAvatar = new Avatar(contact.getAvatar().getAttachmentId(), + attachment, + contact.getAvatar().isProfile()); + + contacts.add(new Contact(contact, updatedAvatar)); + } else { + contacts.add(contact); + } + } + + return contacts; + } catch (JSONException | IOException e) { + Log.w(TAG, "Failed to parse shared contacts.", e); + } + + return Collections.emptyList(); + } + + private static List getLinkPreviews(@NonNull Cursor cursor, @NonNull List attachments) { + String serializedPreviews = cursor.getString(cursor.getColumnIndexOrThrow(LINK_PREVIEWS)); + + if (TextUtils.isEmpty(serializedPreviews)) { + return Collections.emptyList(); + } + + Map attachmentIdMap = new HashMap<>(); + for (DatabaseAttachment attachment : attachments) { + attachmentIdMap.put(attachment.getAttachmentId(), attachment); + } + + try { + List previews = new LinkedList<>(); + JSONArray jsonPreviews = new JSONArray(serializedPreviews); + + for (int i = 0; i < jsonPreviews.length(); i++) { + LinkPreview preview = LinkPreview.deserialize(jsonPreviews.getJSONObject(i).toString()); + + if (preview.getAttachmentId() != null) { + DatabaseAttachment attachment = attachmentIdMap.get(preview.getAttachmentId()); + if (attachment != null) { + previews.add(new LinkPreview(preview.getUrl(), preview.getTitle(), preview.getDescription(), preview.getDate(), attachment)); + } else { + previews.add(preview); + } + } else { + previews.add(preview); + } + } + + return previews; + } catch (JSONException | IOException e) { + Log.w(TAG, "Failed to parse shared contacts.", e); + } + + return Collections.emptyList(); + } + + private Optional insertMessageInbox(IncomingMediaMessage retrieved, + String contentLocation, + long threadId, long mailbox) + throws MmsException + { + if (threadId == -1 || retrieved.isGroupMessage()) { + threadId = getThreadIdFor(retrieved); + } + + ContentValues contentValues = new ContentValues(); + + boolean silentUpdate = (mailbox & Types.GROUP_UPDATE_BIT) > 0; + + contentValues.put(DATE_SENT, retrieved.getSentTimeMillis()); + contentValues.put(DATE_SERVER, retrieved.getServerTimeMillis()); + contentValues.put(RECIPIENT_ID, retrieved.getFrom().serialize()); + + contentValues.put(TYPE, mailbox); + contentValues.put(MMS_MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF); + contentValues.put(THREAD_ID, threadId); + contentValues.put(MMS_CONTENT_LOCATION, contentLocation); + contentValues.put(MMS_STATUS, MmsStatus.DOWNLOAD_INITIALIZED); + contentValues.put(DATE_RECEIVED, retrieved.isPushMessage() ? retrieved.getReceivedTimeMillis() : generatePduCompatTimestamp(retrieved.getReceivedTimeMillis())); + contentValues.put(SMS_SUBSCRIPTION_ID, retrieved.getSubscriptionId()); + contentValues.put(EXPIRES_IN, retrieved.getExpiresIn()); + contentValues.put(VIEW_ONCE, retrieved.isViewOnce() ? 1 : 0); + contentValues.put(STORY_TYPE, retrieved.getStoryType().getCode()); + contentValues.put(PARENT_STORY_ID, retrieved.getParentStoryId() != null ? retrieved.getParentStoryId().serialize() : 0); + contentValues.put(READ, (silentUpdate || retrieved.isExpirationUpdate()) ? 1 : 0); + contentValues.put(UNIDENTIFIED, retrieved.isUnidentified()); + contentValues.put(SERVER_GUID, retrieved.getServerGuid()); + + if (!contentValues.containsKey(DATE_SENT)) { + contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED)); + } + + List quoteAttachments = new LinkedList<>(); + + if (retrieved.getQuote() != null) { + contentValues.put(QUOTE_ID, retrieved.getQuote().getId()); + contentValues.put(QUOTE_BODY, retrieved.getQuote().getText().toString()); + contentValues.put(QUOTE_AUTHOR, retrieved.getQuote().getAuthor().serialize()); + contentValues.put(QUOTE_TYPE, retrieved.getQuote().getType().getCode()); + contentValues.put(QUOTE_MISSING, retrieved.getQuote().isOriginalMissing() ? 1 : 0); + + BodyRangeList mentionsList = MentionUtil.mentionsToBodyRangeList(retrieved.getQuote().getMentions()); + if (mentionsList != null) { + contentValues.put(QUOTE_MENTIONS, mentionsList.toByteArray()); + } + + quoteAttachments = retrieved.getQuote().getAttachments(); + } + + if (retrieved.isPushMessage() && isDuplicate(retrieved, threadId)) { + Log.w(TAG, "Ignoring duplicate media message (" + retrieved.getSentTimeMillis() + ")"); + return Optional.empty(); + } + + boolean updateThread = retrieved.getStoryType() == StoryType.NONE; + boolean keepThreadArchived = SignalStore.settings().shouldKeepMutedChatsArchived() && Recipient.resolved(retrieved.getFrom()).isMuted(); + long messageId = insertMediaMessage(threadId, + retrieved.getBody(), + retrieved.getAttachments(), + quoteAttachments, + retrieved.getSharedContacts(), + retrieved.getLinkPreviews(), + retrieved.getMentions(), + retrieved.getMessageRanges(), + contentValues, + null, + updateThread, + !keepThreadArchived); + + boolean isNotStoryGroupReply = retrieved.getParentStoryId() == null || !retrieved.getParentStoryId().isGroupReply(); + if (!Types.isPaymentsActivated(mailbox) && !Types.isPaymentsRequestToActivate(mailbox) && !Types.isExpirationTimerUpdate(mailbox) && !retrieved.getStoryType().isStory() && isNotStoryGroupReply) { + boolean incrementUnreadMentions = !retrieved.getMentions().isEmpty() && retrieved.getMentions().stream().anyMatch(m -> m.getRecipientId().equals(Recipient.self().getId())); + SignalDatabase.threads().incrementUnread(threadId, 1, incrementUnreadMentions ? 1 : 0); + SignalDatabase.threads().update(threadId, !keepThreadArchived); + } + + notifyConversationListeners(threadId); + + return Optional.of(new InsertResult(messageId, threadId)); + } + + public Optional insertMessageInbox(IncomingMediaMessage retrieved, + String contentLocation, long threadId) + throws MmsException + { + long type = Types.BASE_INBOX_TYPE; + + if (retrieved.isPushMessage()) { + type |= Types.PUSH_MESSAGE_BIT; + } + + if (retrieved.isExpirationUpdate()) { + type |= Types.EXPIRATION_TIMER_UPDATE_BIT; + } + + if (retrieved.isPaymentsNotification()) { + type |= Types.SPECIAL_TYPE_PAYMENTS_NOTIFICATION; + } + + if (retrieved.isActivatePaymentsRequest()) { + type |= Types.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST; + } + + if (retrieved.isPaymentsActivated()) { + type |= Types.SPECIAL_TYPE_PAYMENTS_ACTIVATED; + } + + return insertMessageInbox(retrieved, contentLocation, threadId, type); + } + + public Optional insertSecureDecryptedMessageInbox(IncomingMediaMessage retrieved, long threadId) + throws MmsException + { + long type = Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT; + + if (retrieved.isPushMessage()) { + type |= Types.PUSH_MESSAGE_BIT; + } + + if (retrieved.isExpirationUpdate()) { + type |= Types.EXPIRATION_TIMER_UPDATE_BIT; + } + + boolean hasSpecialType = false; + if (retrieved.isStoryReaction()) { + hasSpecialType = true; + type |= Types.SPECIAL_TYPE_STORY_REACTION; + } + + if (retrieved.getGiftBadge() != null) { + if (hasSpecialType) { + throw new MmsException("Cannot insert message with multiple special types."); + } + + type |= Types.SPECIAL_TYPE_GIFT_BADGE; + } + + if (retrieved.isPaymentsNotification()) { + if (hasSpecialType) { + throw new MmsException("Cannot insert message with multiple special types."); + } + type |= Types.SPECIAL_TYPE_PAYMENTS_NOTIFICATION; + } + + if (retrieved.isActivatePaymentsRequest()) { + if (hasSpecialType) { + throw new MmsException("Cannot insert message with multiple special types."); + } + type |= Types.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST; + } + + if (retrieved.isPaymentsActivated()) { + if (hasSpecialType) { + throw new MmsException("Cannot insert message with multiple special types."); + } + type |= Types.SPECIAL_TYPE_PAYMENTS_ACTIVATED; + } + + return insertMessageInbox(retrieved, "", threadId, type); + } + + public Pair insertMessageInbox(@NonNull NotificationInd notification, int subscriptionId) { + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + long threadId = getThreadIdFor(notification); + ContentValues contentValues = new ContentValues(); + ContentValuesBuilder contentBuilder = new ContentValuesBuilder(contentValues); + + Log.i(TAG, "Message received type: " + notification.getMessageType()); + + contentBuilder.add(MMS_CONTENT_LOCATION, notification.getContentLocation()); + contentBuilder.add(DATE_SENT, System.currentTimeMillis()); + contentBuilder.add(MMS_EXPIRY, notification.getExpiry()); + contentBuilder.add(MMS_MESSAGE_SIZE, notification.getMessageSize()); + contentBuilder.add(MMS_TRANSACTION_ID, notification.getTransactionId()); + contentBuilder.add(MMS_MESSAGE_TYPE, notification.getMessageType()); + + if (notification.getFrom() != null) { + Recipient recipient = Recipient.external(context, Util.toIsoString(notification.getFrom().getTextString())); + contentValues.put(RECIPIENT_ID, recipient.getId().serialize()); + } else { + contentValues.put(RECIPIENT_ID, RecipientId.UNKNOWN.serialize()); + } + + contentValues.put(TYPE, Types.BASE_INBOX_TYPE); + contentValues.put(THREAD_ID, threadId); + contentValues.put(MMS_STATUS, MmsStatus.DOWNLOAD_INITIALIZED); + contentValues.put(DATE_RECEIVED, generatePduCompatTimestamp(System.currentTimeMillis())); + contentValues.put(READ, Util.isDefaultSmsProvider(context) ? 0 : 1); + contentValues.put(SMS_SUBSCRIPTION_ID, subscriptionId); + + if (!contentValues.containsKey(DATE_SENT)) + contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED)); + + long messageId = db.insert(TABLE_NAME, null, contentValues); + + return new Pair<>(messageId, threadId); + } + + public @NonNull InsertResult insertChatSessionRefreshedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp) { + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(Recipient.resolved(recipientId)); + long type = Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT; + + type = type & (Types.TOTAL_MASK - Types.ENCRYPTION_MASK) | Types.ENCRYPTION_REMOTE_FAILED_BIT; + + ContentValues values = new ContentValues(); + values.put(RECIPIENT_ID, recipientId.serialize()); + values.put(RECIPIENT_DEVICE_ID, senderDeviceId); + values.put(DATE_RECEIVED, System.currentTimeMillis()); + values.put(DATE_SENT, sentTimestamp); + values.put(DATE_SERVER, -1); + values.put(READ, 0); + values.put(TYPE, type); + values.put(THREAD_ID, threadId); + + long messageId = db.insert(TABLE_NAME, null, values); + + SignalDatabase.threads().incrementUnread(threadId, 1, 0); + boolean keepThreadArchived = SignalStore.settings().shouldKeepMutedChatsArchived() && Recipient.resolved(recipientId).isMuted(); + SignalDatabase.threads().update(threadId, !keepThreadArchived); + + notifyConversationListeners(threadId); + + TrimThreadJob.enqueueAsync(threadId); + + return new InsertResult(messageId, threadId); + } + + public void insertBadDecryptMessage(@NonNull RecipientId recipientId, int senderDevice, long sentTimestamp, long receivedTimestamp, long threadId) { + ContentValues values = new ContentValues(); + values.put(RECIPIENT_ID, recipientId.serialize()); + values.put(RECIPIENT_DEVICE_ID, senderDevice); + values.put(DATE_SENT, sentTimestamp); + values.put(DATE_RECEIVED, receivedTimestamp); + values.put(DATE_SERVER, -1); + values.put(READ, 0); + values.put(TYPE, Types.BAD_DECRYPT_TYPE); + values.put(THREAD_ID, threadId); + + databaseHelper.getSignalWritableDatabase().insert(TABLE_NAME, null, values); + + SignalDatabase.threads().incrementUnread(threadId, 1, 0); + boolean keepThreadArchived = SignalStore.settings().shouldKeepMutedChatsArchived() && Recipient.resolved(recipientId).isMuted(); + SignalDatabase.threads().update(threadId, !keepThreadArchived); + + notifyConversationListeners(threadId); + + TrimThreadJob.enqueueAsync(threadId); + } + + public void markIncomingNotificationReceived(long threadId) { + notifyConversationListeners(threadId); + + if (org.thoughtcrime.securesms.util.Util.isDefaultSmsProvider(context)) { + SignalDatabase.threads().incrementUnread(threadId, 1, 0); + } + + SignalDatabase.threads().update(threadId, true); + + TrimThreadJob.enqueueAsync(threadId); + } + + public void markGiftRedemptionCompleted(long messageId) { + markGiftRedemptionState(messageId, GiftBadge.RedemptionState.REDEEMED); + } + + public void markGiftRedemptionStarted(long messageId) { + markGiftRedemptionState(messageId, GiftBadge.RedemptionState.STARTED); + } + + public void markGiftRedemptionFailed(long messageId) { + markGiftRedemptionState(messageId, GiftBadge.RedemptionState.FAILED); + } + + private void markGiftRedemptionState(long messageId, @NonNull GiftBadge.RedemptionState redemptionState) { + String[] projection = SqlUtil.buildArgs(BODY, THREAD_ID); + String where = "(" + TYPE + " & " + Types.SPECIAL_TYPES_MASK + " = " + Types.SPECIAL_TYPE_GIFT_BADGE + ") AND " + + ID + " = ?"; + String[] args = SqlUtil.buildArgs(messageId); + boolean updated = false; + long threadId = -1; + + getWritableDatabase().beginTransaction(); + try (Cursor cursor = getWritableDatabase().query(TABLE_NAME, projection, where, args, null, null, null)) { + if (cursor.moveToFirst()) { + GiftBadge giftBadge = GiftBadge.parseFrom(Base64.decode(CursorUtil.requireString(cursor, BODY))); + GiftBadge updatedBadge = giftBadge.toBuilder().setRedemptionState(redemptionState).build(); + ContentValues contentValues = new ContentValues(1); + + contentValues.put(BODY, Base64.encodeBytes(updatedBadge.toByteArray())); + + updated = getWritableDatabase().update(TABLE_NAME, contentValues, ID_WHERE, args) > 0; + threadId = CursorUtil.requireLong(cursor, THREAD_ID); + + getWritableDatabase().setTransactionSuccessful(); + } + } catch (IOException e) { + Log.w(TAG, "Failed to mark gift badge " + redemptionState.name(), e, true); + } finally { + getWritableDatabase().endTransaction(); + } + + if (updated) { + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); + notifyConversationListeners(threadId); + } + } + + public long insertMessageOutbox(@NonNull OutgoingMediaMessage message, + long threadId, + boolean forceSms, + @Nullable InsertListener insertListener) + throws MmsException + { + return insertMessageOutbox(message, threadId, forceSms, GroupReceiptTable.STATUS_UNDELIVERED, insertListener); + } + + public long insertMessageOutbox(@NonNull OutgoingMediaMessage message, + long threadId, boolean forceSms, int defaultReceiptStatus, + @Nullable InsertListener insertListener) + throws MmsException + { + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + + long type = Types.BASE_SENDING_TYPE; + + if (message.isSecure()) type |= (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT); + if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT; + + if (message.isGroup()) { + if (message.isV2Group()) { + type |= Types.GROUP_V2_BIT | Types.GROUP_UPDATE_BIT; + if (message.isJustAGroupLeave()) { + type |= Types.GROUP_LEAVE_BIT; + } + } else { + MessageGroupContext.GroupV1Properties properties = message.requireGroupV1Properties(); + if (properties.isUpdate()) type |= Types.GROUP_UPDATE_BIT; + else if (properties.isQuit()) type |= Types.GROUP_LEAVE_BIT; + } + } + + if (message.isExpirationUpdate()) { + type |= Types.EXPIRATION_TIMER_UPDATE_BIT; + } + + boolean hasSpecialType = false; + if (message.isStoryReaction()) { + hasSpecialType = true; + type |= Types.SPECIAL_TYPE_STORY_REACTION; + } + + if (message.getGiftBadge() != null) { + if (hasSpecialType) { + throw new MmsException("Cannot insert message with multiple special types."); + } + + type |= Types.SPECIAL_TYPE_GIFT_BADGE; + } + + if (message.isPaymentsNotification()) { + if (hasSpecialType) { + throw new MmsException("Cannot insert message with multiple special types."); + } + type |= Types.SPECIAL_TYPE_PAYMENTS_NOTIFICATION; + } + + if (message.isRequestToActivatePayments()) { + if (hasSpecialType) { + throw new MmsException("Cannot insert message with multiple special types."); + } + type |= Types.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST; + } + + if (message.isPaymentsActivated()) { + if (hasSpecialType) { + throw new MmsException("Cannot insert message with multiple special types."); + } + type |= Types.SPECIAL_TYPE_PAYMENTS_ACTIVATED; + } + + Map earlyDeliveryReceipts = earlyDeliveryReceiptCache.remove(message.getSentTimeMillis()); + + ContentValues contentValues = new ContentValues(); + contentValues.put(DATE_SENT, message.getSentTimeMillis()); + contentValues.put(MMS_MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ); + + contentValues.put(TYPE, type); + contentValues.put(THREAD_ID, threadId); + contentValues.put(READ, 1); + contentValues.put(DATE_RECEIVED, System.currentTimeMillis()); + contentValues.put(SMS_SUBSCRIPTION_ID, message.getSubscriptionId()); + contentValues.put(EXPIRES_IN, message.getExpiresIn()); + contentValues.put(VIEW_ONCE, message.isViewOnce()); + contentValues.put(RECIPIENT_ID, message.getRecipient().getId().serialize()); + contentValues.put(DELIVERY_RECEIPT_COUNT, Stream.of(earlyDeliveryReceipts.values()).mapToLong(EarlyReceiptCache.Receipt::getCount).sum()); + contentValues.put(RECEIPT_TIMESTAMP, Stream.of(earlyDeliveryReceipts.values()).mapToLong(EarlyReceiptCache.Receipt::getTimestamp).max().orElse(-1)); + contentValues.put(STORY_TYPE, message.getStoryType().getCode()); + contentValues.put(PARENT_STORY_ID, message.getParentStoryId() != null ? message.getParentStoryId().serialize() : 0); + + if (message.getRecipient().isSelf() && hasAudioAttachment(message.getAttachments())) { + contentValues.put(VIEWED_RECEIPT_COUNT, 1L); + } + + List quoteAttachments = new LinkedList<>(); + + if (message.getOutgoingQuote() != null) { + MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithPlaceholders(message.getOutgoingQuote().getText(), message.getOutgoingQuote().getMentions()); + + contentValues.put(QUOTE_ID, message.getOutgoingQuote().getId()); + contentValues.put(QUOTE_AUTHOR, message.getOutgoingQuote().getAuthor().serialize()); + contentValues.put(QUOTE_BODY, updated.getBodyAsString()); + contentValues.put(QUOTE_TYPE, message.getOutgoingQuote().getType().getCode()); + contentValues.put(QUOTE_MISSING, message.getOutgoingQuote().isOriginalMissing() ? 1 : 0); + + BodyRangeList mentionsList = MentionUtil.mentionsToBodyRangeList(updated.getMentions()); + if (mentionsList != null) { + contentValues.put(QUOTE_MENTIONS, mentionsList.toByteArray()); + } + + quoteAttachments.addAll(message.getOutgoingQuote().getAttachments()); + } + + MentionUtil.UpdatedBodyAndMentions updatedBodyAndMentions = MentionUtil.updateBodyAndMentionsWithPlaceholders(message.getBody(), message.getMentions()); + + long messageId = insertMediaMessage(threadId, updatedBodyAndMentions.getBodyAsString(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), updatedBodyAndMentions.getMentions(), null, contentValues, insertListener, false, false); + + if (message.getRecipient().isGroup()) { + GroupReceiptTable receiptDatabase = SignalDatabase.groupReceipts(); + Set members = new HashSet<>(); + + if (message.isGroupUpdate() && message.isV2Group()) { + MessageGroupContext.GroupV2Properties groupV2Properties = message.requireGroupV2Properties(); + members.addAll(Stream.of(groupV2Properties.getAllActivePendingAndRemovedMembers()) + .distinct() + .map(uuid -> RecipientId.from(ServiceId.from(uuid))) + .toList()); + members.remove(Recipient.self().getId()); + } else { + members.addAll(Stream.of(SignalDatabase.groups().getGroupMembers(message.getRecipient().requireGroupId(), GroupTable.MemberSet.FULL_MEMBERS_EXCLUDING_SELF)).map(Recipient::getId).toList()); + } + + receiptDatabase.insert(members, messageId, defaultReceiptStatus, message.getSentTimeMillis()); + + for (RecipientId recipientId : earlyDeliveryReceipts.keySet()) { + receiptDatabase.update(recipientId, messageId, GroupReceiptTable.STATUS_DELIVERED, -1); + } + } else if (message.getRecipient().isDistributionList()) { + GroupReceiptTable receiptDatabase = SignalDatabase.groupReceipts(); + List members = SignalDatabase.distributionLists().getMembers(message.getRecipient().requireDistributionListId()); + + receiptDatabase.insert(members, messageId, defaultReceiptStatus, message.getSentTimeMillis()); + + for (RecipientId recipientId : earlyDeliveryReceipts.keySet()) { + receiptDatabase.update(recipientId, messageId, GroupReceiptTable.STATUS_DELIVERED, -1); + } + } + + SignalDatabase.threads().updateLastSeenAndMarkSentAndLastScrolledSilenty(threadId); + + if (!message.getStoryType().isStory()) { + if (message.getOutgoingQuote() == null) { + ApplicationDependencies.getDatabaseObserver().notifyMessageInsertObservers(threadId, new MessageId(messageId, true)); + } else { + ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId); + } + } else { + ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(message.getRecipient().getId()); + } + + notifyConversationListListeners(); + + TrimThreadJob.enqueueAsync(threadId); + + return messageId; + } + + private boolean hasAudioAttachment(@NonNull List attachments) { + for (Attachment attachment : attachments) { + if (MediaUtil.isAudio(attachment)) { + return true; + } + } + + return false; + } + + private long insertMediaMessage(long threadId, + @Nullable String body, + @NonNull List attachments, + @NonNull List quoteAttachments, + @NonNull List sharedContacts, + @NonNull List linkPreviews, + @NonNull List mentions, + @Nullable BodyRangeList messageRanges, + @NonNull ContentValues contentValues, + @Nullable InsertListener insertListener, + boolean updateThread, + boolean unarchive) + throws MmsException + { + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + AttachmentTable partsDatabase = SignalDatabase.attachments(); + MentionTable mentionDatabase = SignalDatabase.mentions(); + + boolean mentionsSelf = Stream.of(mentions).filter(m -> Recipient.resolved(m.getRecipientId()).isSelf()).findFirst().isPresent(); + + List allAttachments = new LinkedList<>(); + List contactAttachments = Stream.of(sharedContacts).map(Contact::getAvatarAttachment).filter(a -> a != null).toList(); + List previewAttachments = Stream.of(linkPreviews).filter(lp -> lp.getThumbnail().isPresent()).map(lp -> lp.getThumbnail().get()).toList(); + + allAttachments.addAll(attachments); + allAttachments.addAll(contactAttachments); + allAttachments.addAll(previewAttachments); + + contentValues.put(BODY, body); + contentValues.put(MENTIONS_SELF, mentionsSelf ? 1 : 0); + + if (messageRanges != null) { + contentValues.put(MESSAGE_RANGES, messageRanges.toByteArray()); + } + + db.beginTransaction(); + try { + long messageId = db.insert(TABLE_NAME, null, contentValues); + + mentionDatabase.insert(threadId, messageId, mentions); + + Map insertedAttachments = partsDatabase.insertAttachmentsForMessage(messageId, allAttachments, quoteAttachments); + String serializedContacts = getSerializedSharedContacts(insertedAttachments, sharedContacts); + String serializedPreviews = getSerializedLinkPreviews(insertedAttachments, linkPreviews); + + if (!TextUtils.isEmpty(serializedContacts)) { + ContentValues contactValues = new ContentValues(); + contactValues.put(SHARED_CONTACTS, serializedContacts); + + SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); + int rows = database.update(TABLE_NAME, contactValues, ID + " = ?", new String[]{ String.valueOf(messageId) }); + + if (rows <= 0) { + Log.w(TAG, "Failed to update message with shared contact data."); + } + } + + if (!TextUtils.isEmpty(serializedPreviews)) { + ContentValues contactValues = new ContentValues(); + contactValues.put(LINK_PREVIEWS, serializedPreviews); + + SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); + int rows = database.update(TABLE_NAME, contactValues, ID + " = ?", new String[]{ String.valueOf(messageId) }); + + if (rows <= 0) { + Log.w(TAG, "Failed to update message with link preview data."); + } + } + + db.setTransactionSuccessful(); + return messageId; + } finally { + db.endTransaction(); + + if (insertListener != null) { + insertListener.onComplete(); + } + + long contentValuesThreadId = contentValues.getAsLong(THREAD_ID); + + if (updateThread) { + SignalDatabase.threads().setLastScrolled(contentValuesThreadId, 0); + SignalDatabase.threads().update(threadId, unarchive); + } + } + } + + public boolean deleteMessage(long messageId) { + Log.d(TAG, "deleteMessage(" + messageId + ")"); + + long threadId = getThreadIdForMessage(messageId); + AttachmentTable attachmentDatabase = SignalDatabase.attachments(); + attachmentDatabase.deleteAttachmentsForMessage(messageId); + + GroupReceiptTable groupReceiptDatabase = SignalDatabase.groupReceipts(); + groupReceiptDatabase.deleteRowsForMessage(messageId); + + MentionTable mentionDatabase = SignalDatabase.mentions(); + mentionDatabase.deleteMentionsForMessage(messageId); + + SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); + database.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""}); + + SignalDatabase.threads().setLastScrolled(threadId, 0); + boolean threadDeleted = SignalDatabase.threads().update(threadId, false); + notifyConversationListeners(threadId); + notifyStickerListeners(); + notifyStickerPackListeners(); + return threadDeleted; + } + + public void deleteThread(long threadId) { + Log.d(TAG, "deleteThread(" + threadId + ")"); + Set singleThreadSet = new HashSet<>(); + singleThreadSet.add(threadId); + deleteThreads(singleThreadSet); + } + + private @Nullable String getSerializedSharedContacts(@NonNull Map insertedAttachmentIds, @NonNull List contacts) { + if (contacts.isEmpty()) return null; + + JSONArray sharedContactJson = new JSONArray(); + + for (Contact contact : contacts) { + try { + AttachmentId attachmentId = null; + + if (contact.getAvatarAttachment() != null) { + attachmentId = insertedAttachmentIds.get(contact.getAvatarAttachment()); + } + + Avatar updatedAvatar = new Avatar(attachmentId, + contact.getAvatarAttachment(), + contact.getAvatar() != null && contact.getAvatar().isProfile()); + Contact updatedContact = new Contact(contact, updatedAvatar); + + sharedContactJson.put(new JSONObject(updatedContact.serialize())); + } catch (JSONException | IOException e) { + Log.w(TAG, "Failed to serialize shared contact. Skipping it.", e); + } + } + return sharedContactJson.toString(); + } + + private @Nullable String getSerializedLinkPreviews(@NonNull Map insertedAttachmentIds, @NonNull List previews) { + if (previews.isEmpty()) return null; + + JSONArray linkPreviewJson = new JSONArray(); + + for (LinkPreview preview : previews) { + try { + AttachmentId attachmentId = null; + + if (preview.getThumbnail().isPresent()) { + attachmentId = insertedAttachmentIds.get(preview.getThumbnail().get()); + } + + LinkPreview updatedPreview = new LinkPreview(preview.getUrl(), preview.getTitle(), preview.getDescription(), preview.getDate(), attachmentId); + linkPreviewJson.put(new JSONObject(updatedPreview.serialize())); + } catch (JSONException | IOException e) { + Log.w(TAG, "Failed to serialize shared contact. Skipping it.", e); + } + } + return linkPreviewJson.toString(); + } + + private boolean isDuplicate(IncomingMediaMessage message, long threadId) { + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); + String query = DATE_SENT + " = ? AND " + RECIPIENT_ID + " = ? AND " + THREAD_ID + " = ?"; + String[] args = SqlUtil.buildArgs(message.getSentTimeMillis(), message.getFrom().serialize(), threadId); + + try (Cursor cursor = db.query(TABLE_NAME, new String[] { "1" }, query, args, null, null, null, "1")) { + return cursor.moveToFirst(); + } + } + + private boolean isDuplicate(IncomingTextMessage message, long threadId) { + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); + String query = DATE_SENT + " = ? AND " + RECIPIENT_ID + " = ? AND " + THREAD_ID + " = ?"; + String[] args = SqlUtil.buildArgs(message.getSentTimestampMillis(), message.getSender().serialize(), threadId); + + try (Cursor cursor = db.query(TABLE_NAME, new String[] { "1" }, query, args, null, null, null, "1")) { + return cursor.moveToFirst(); + } + } + + public boolean isSent(long messageId) { + SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); + try (Cursor cursor = database.query(TABLE_NAME, new String[] { TYPE }, ID + " = ?", new String[] { String.valueOf(messageId)}, null, null, null)) { + if (cursor != null && cursor.moveToNext()) { + long type = cursor.getLong(cursor.getColumnIndexOrThrow(TYPE)); + return Types.isSentType(type); + } + } + return false; + } + + public List getProfileChangeDetailsRecords(long threadId, long afterTimestamp) { + String where = THREAD_ID + " = ? AND " + DATE_RECEIVED + " >= ? AND " + TYPE + " = ?"; + String[] args = SqlUtil.buildArgs(threadId, afterTimestamp, Types.PROFILE_CHANGE_TYPE); + + try (SmsReader reader = smsReaderFor(queryMessages(where, args, true, -1))) { + List results = new ArrayList<>(reader.getCount()); + while (reader.getNext() != null) { + results.add(reader.getCurrent()); + } + + return results; + } + } + + private Cursor queryMessages(@NonNull String where, @Nullable String[] args, boolean reverse, long limit) { + return queryMessages(SMS_PROJECTION, where, args, reverse, limit); + } + + private Cursor queryMessages(@NonNull String[] projection, @NonNull String where, @Nullable String[] args, boolean reverse, long limit) { + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); + + return db.query(TABLE_NAME, + projection, + where, + args, + null, + null, + reverse ? ID + " DESC" : null, + limit > 0 ? String.valueOf(limit) : null); + } + + public Set getAllRateLimitedMessageIds() { + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); + String where = "(" + TYPE + " & " + Types.TOTAL_MASK + " & " + Types.MESSAGE_RATE_LIMITED_BIT + ") > 0"; + + Set ids = new HashSet<>(); + + try (Cursor cursor = db.query(TABLE_NAME, new String[] { ID }, where, null, null, null, null)) { + while (cursor.moveToNext()) { + ids.add(CursorUtil.requireLong(cursor, ID)); + } + } + + return ids; + } + + public Cursor getUnexportedInsecureMessages(int limit) { + return rawQuery( + SqlUtil.appendArg(MMS_PROJECTION, EXPORT_STATE), + getInsecureMessageClause() + " AND NOT " + EXPORTED, + null, + false, + limit + ); + } + + public long getUnexportedInsecureMessagesEstimatedSize() { + Cursor messageTextSize = SQLiteDatabaseExtensionsKt.select(getReadableDatabase(), "SUM(LENGTH(" + BODY + "))") + .from(TABLE_NAME) + .where(getInsecureMessageClause() + " AND " + EXPORTED + " < ?", MessageExportStatus.EXPORTED) + .run(); + + long bodyTextSize = CursorExtensionsKt.readToSingleLong(messageTextSize); + + String select = "SUM(" + AttachmentTable.TABLE_NAME + "." + AttachmentTable.SIZE + ") AS s"; + String fromJoin = TABLE_NAME + " INNER JOIN " + AttachmentTable.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.MMS_ID; + String where = getInsecureMessageClause() + " AND " + EXPORTED + " < " + MessageExportStatus.EXPORTED.serialize(); + + long fileSize = CursorExtensionsKt.readToSingleLong(getReadableDatabase().rawQuery("SELECT " + select + " FROM " + fromJoin + " WHERE " + where, null)); + + return bodyTextSize + fileSize; + } + + public void deleteExportedMessages() { + beginTransaction(); + try { + List threadsToUpdate = new LinkedList<>(); + try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, THREAD_ID_PROJECTION, EXPORTED + " = ?", SqlUtil.buildArgs(MessageExportStatus.EXPORTED), THREAD_ID, null, null, null)) { + while (cursor.moveToNext()) { + threadsToUpdate.add(CursorUtil.requireLong(cursor, THREAD_ID)); + } + } + + getWritableDatabase().delete(TABLE_NAME, EXPORTED + " = ?", SqlUtil.buildArgs(MessageExportStatus.EXPORTED)); + + for (final long threadId : threadsToUpdate) { + SignalDatabase.threads().update(threadId, false); + } + + SignalDatabase.attachments().deleteAbandonedAttachmentFiles(); + + setTransactionSuccessful(); + } finally { + endTransaction(); + } + } + + void deleteThreads(@NonNull Set threadIds) { + Log.d(TAG, "deleteThreads(count: " + threadIds.size() + ")"); + + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + String where = ""; + + for (long threadId : threadIds) { + where += THREAD_ID + " = '" + threadId + "' OR "; + } + + where = where.substring(0, where.length() - 4); + + try (Cursor cursor = db.query(TABLE_NAME, new String[] {ID}, where, null, null, null, null)) { + while (cursor != null && cursor.moveToNext()) { + deleteMessage(cursor.getLong(0)); + } + } + } + + int deleteMessagesInThreadBeforeDate(long threadId, long date) { + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + String where = THREAD_ID + " = ? AND " + DATE_RECEIVED + " < " + date; + + return db.delete(TABLE_NAME, where, SqlUtil.buildArgs(threadId)); + } + + void deleteAbandonedMessages() { + SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + String where = THREAD_ID + " NOT IN (SELECT _id FROM " + ThreadTable.TABLE_NAME + ")"; + + int deletes = db.delete(TABLE_NAME, where, null); + if (deletes > 0) { + Log.i(TAG, "Deleted " + deletes + " abandoned messages"); + } + } + + public void deleteRemotelyDeletedStory(long messageId) { + try (Cursor cursor = getMessageCursor(messageId)) { + if (cursor.moveToFirst() && CursorUtil.requireBoolean(cursor, REMOTE_DELETED)) { + deleteMessage(messageId); + } else { + Log.i(TAG, "Unable to delete remotely deleted story: " + messageId); + } + } + } + + public List getMessagesInThreadAfterInclusive(long threadId, long timestamp, long limit) { + String where = TABLE_NAME + "." + MmsSmsColumns.THREAD_ID + " = ? AND " + + TABLE_NAME + "." + DATE_RECEIVED + " >= ?"; + String[] args = SqlUtil.buildArgs(threadId, timestamp); + + try (MmsReader reader = mmsReaderFor(rawQuery(where, args, false, limit))) { + List results = new ArrayList<>(reader.cursor.getCount()); + + while (reader.getNext() != null) { + results.add(reader.getCurrent()); + } + + return results; + } + } + + public void deleteAllThreads() { + Log.d(TAG, "deleteAllThreads()"); + SignalDatabase.attachments().deleteAllAttachments(); + SignalDatabase.groupReceipts().deleteAllRows(); + SignalDatabase.mentions().deleteAllMentions(); + + SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); + database.delete(TABLE_NAME, null, null); + } + + public @Nullable ViewOnceExpirationInfo getNearestExpiringViewOnceMessage() { + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); + ViewOnceExpirationInfo info = null; + long nearestExpiration = Long.MAX_VALUE; + + String query = "SELECT " + + TABLE_NAME + "." + ID + ", " + + VIEW_ONCE + ", " + + DATE_RECEIVED + " " + + "FROM " + TABLE_NAME + " INNER JOIN " + AttachmentTable.TABLE_NAME + " " + + "ON " + TABLE_NAME + "." + ID + " = " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.MMS_ID + " " + + "WHERE " + + VIEW_ONCE + " > 0 AND " + + "(" + AttachmentTable.DATA + " NOT NULL OR " + AttachmentTable.TRANSFER_STATE + " != ?)"; + String[] args = new String[] { String.valueOf(AttachmentTable.TRANSFER_PROGRESS_DONE) }; + + try (Cursor cursor = db.rawQuery(query, args)) { + while (cursor != null && cursor.moveToNext()) { + long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID)); + long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(DATE_RECEIVED)); + long expiresAt = dateReceived + ViewOnceUtil.MAX_LIFESPAN; + + if (info == null || expiresAt < nearestExpiration) { + info = new ViewOnceExpirationInfo(id, dateReceived); + nearestExpiration = expiresAt; + } + } + } + + return info; + } + + /** + * The number of change number messages in the thread. + * Currently only used for tests. + */ + @VisibleForTesting + int getChangeNumberMessageCount(@NonNull RecipientId recipientId) { + try (Cursor cursor = SQLiteDatabaseExtensionsKt + .select(getReadableDatabase(), "COUNT(*)") + .from(TABLE_NAME) + .where(RECIPIENT_ID + " = ? AND " + TYPE + " = ?", recipientId, MmsSmsColumns.Types.CHANGE_NUMBER_TYPE) + .run()) + { + if (cursor.moveToFirst()) { + return cursor.getInt(0); + } else { + return 0; + } + } + } + + private static @NonNull List parseQuoteMentions(@NonNull Context context, Cursor cursor) { + byte[] raw = cursor.getBlob(cursor.getColumnIndexOrThrow(QUOTE_MENTIONS)); + + return MentionUtil.bodyRangeListToMentions(context, raw); + } + + public SQLiteDatabase beginTransaction() { + databaseHelper.getSignalWritableDatabase().beginTransaction(); + return databaseHelper.getSignalWritableDatabase(); + } + + public void setTransactionSuccessful() { + databaseHelper.getSignalWritableDatabase().setTransactionSuccessful(); + } + + public void endTransaction() { + databaseHelper.getSignalWritableDatabase().endTransaction(); + } + + public static MmsReader mmsReaderFor(Cursor cursor) { + return new MmsReader(cursor); + } + + public static SmsReader smsReaderFor(Cursor cursor) { + return new SmsReader(cursor); + } + + public static OutgoingMmsReader readerFor(OutgoingMediaMessage message, long threadId) { + return new OutgoingMmsReader(message, threadId); + } + + @VisibleForTesting + Optional collapseJoinRequestEventsIfPossible(long threadId, IncomingGroupUpdateMessage message) { + InsertResult result = null; + + + SQLiteDatabase db = getWritableDatabase(); + db.beginTransaction(); + + try { + try (MmsSmsTable.Reader reader = MmsSmsTable.readerFor(SignalDatabase.mmsSms().getConversation(threadId, 0, 2))) { + MessageRecord latestMessage = reader.getNext(); + if (latestMessage != null && latestMessage.isGroupV2()) { + Optional changeEditor = message.getChangeEditor(); + if (changeEditor.isPresent() && latestMessage.isGroupV2JoinRequest(changeEditor.get())) { + String encodedBody; + long id; + + MessageRecord secondLatestMessage = reader.getNext(); + if (secondLatestMessage != null && secondLatestMessage.isGroupV2JoinRequest(changeEditor.get())) { + id = secondLatestMessage.getId(); + encodedBody = MessageRecord.createNewContextWithAppendedDeleteJoinRequest(secondLatestMessage, message.getChangeRevision(), changeEditor.get()); + deleteMessage(latestMessage.getId()); + } else { + id = latestMessage.getId(); + encodedBody = MessageRecord.createNewContextWithAppendedDeleteJoinRequest(latestMessage, message.getChangeRevision(), changeEditor.get()); + } + + ContentValues values = new ContentValues(1); + values.put(BODY, encodedBody); + getWritableDatabase().update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(id)); + result = new InsertResult(id, threadId); + } + } + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + return Optional.ofNullable(result); + } final @NonNull String getOutgoingTypeClause() { List segments = new ArrayList<>(Types.OUTGOING_MESSAGE_TYPES.length); for (long outgoingMessageType : Types.OUTGOING_MESSAGE_TYPES) { - segments.add("(" + getTableName() + "." + getTypeField() + " & " + Types.BASE_TYPE_MASK + " = " + outgoingMessageType + ")"); + segments.add("(" + TABLE_NAME + "." + TYPE + " & " + Types.BASE_TYPE_MASK + " = " + outgoingMessageType + ")"); } return Util.join(segments, " OR "); @@ -241,10 +3436,10 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn final int getInsecureMessagesSentForThread(long threadId) { SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); String[] projection = new String[]{"COUNT(*)"}; - String query = THREAD_ID + " = ? AND " + getOutgoingInsecureMessageClause() + " AND " + getDateSentColumnName() + " > ?"; + String query = THREAD_ID + " = ? AND " + getOutgoingInsecureMessageClause() + " AND " + DATE_SENT + " > ?"; String[] args = new String[]{String.valueOf(threadId), String.valueOf(System.currentTimeMillis() - InsightsConstants.PERIOD_IN_MILLIS)}; - try (Cursor cursor = db.query(getTableName(), projection, query, args, null, null, null, null)) { + try (Cursor cursor = db.query(TABLE_NAME, projection, query, args, null, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { return cursor.getInt(0); } else { @@ -258,7 +3453,7 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn } public int getInsecureMessageCount() { - try (Cursor cursor = getReadableDatabase().query(getTableName(), SqlUtil.COUNT, getInsecureMessageClause(), null, null, null, null)) { + try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, SqlUtil.COUNT, getInsecureMessageClause(), null, null, null, null)) { if (cursor.moveToFirst()) { return cursor.getInt(0); } @@ -268,9 +3463,9 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn } public boolean hasSmsExportMessage(long threadId) { - return SQLiteDatabaseExtensionsKt.exists(getReadableDatabase(), getTableName()) - .where(THREAD_ID_WHERE + " AND " + getTypeField() + " = ?", threadId, Types.SMS_EXPORT_TYPE) - .run(); + return SQLiteDatabaseExtensionsKt.exists(getReadableDatabase(), TABLE_NAME) + .where(THREAD_ID_WHERE + " AND " + TYPE + " = ?", threadId, Types.SMS_EXPORT_TYPE) + .run(); } final int getSecureMessageCountForInsights() { @@ -283,7 +3478,7 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn String query = getSecureMessageClause() + "AND " + MmsSmsColumns.THREAD_ID + " = ?"; String[] args = new String[]{String.valueOf(threadId)}; - try (Cursor cursor = db.query(getTableName(), projection, query, args, null, null, null, null)) { + try (Cursor cursor = db.query(TABLE_NAME, projection, query, args, null, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { return cursor.getInt(0); } else { @@ -297,10 +3492,10 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn String[] projection = new String[] {"COUNT(*)"}; String query = getOutgoingSecureMessageClause() + "AND " + MmsSmsColumns.THREAD_ID + " = ? " + - "AND (" + getTypeField() + " & " + Types.GROUP_LEAVE_BIT + " = 0 OR " + getTypeField() + " & " + Types.GROUP_V2_BIT + " = " + Types.GROUP_V2_BIT + ")"; + "AND (" + TYPE + " & " + Types.GROUP_LEAVE_BIT + " = 0 OR " + TYPE + " & " + Types.GROUP_V2_BIT + " = " + Types.GROUP_V2_BIT + ")"; String[] args = new String[]{String.valueOf(threadId)}; - try (Cursor cursor = db.query(getTableName(), projection, query, args, null, null, null, null)) { + try (Cursor cursor = db.query(TABLE_NAME, projection, query, args, null, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { return cursor.getInt(0); } else { @@ -318,11 +3513,11 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); List> expiring = new LinkedList<>(); String[] projection = new String[] { ID, THREAD_ID, EXPIRES_IN, EXPIRE_STARTED }; - String query = getDateSentColumnName() + " = ? AND (" + RECIPIENT_ID + " = ? OR (" + RECIPIENT_ID + " = ? AND " + getOutgoingTypeClause() + "))"; + String query = DATE_SENT + " = ? AND (" + RECIPIENT_ID + " = ? OR (" + RECIPIENT_ID + " = ? AND " + getOutgoingTypeClause() + "))"; String[] args = SqlUtil.buildArgs(messageId.getTimetamp(), messageId.getRecipientId(), Recipient.self().getId()); List threads = new LinkedList<>(); - try (Cursor cursor = database.query(getTableName(), projection, query, args, null, null, null)) { + try (Cursor cursor = database.query(TABLE_NAME, projection, query, args, null, null, null)) { while (cursor.moveToNext()) { long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID)); long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID)); @@ -341,7 +3536,7 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn expiring.add(new Pair<>(id, expiresIn)); } - database.update(getTableName(), values, ID_WHERE, SqlUtil.buildArgs(id)); + database.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(id)); threads.add(threadId); @@ -357,10 +3552,10 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); String[] projection = new String[] {"COUNT(*)"}; - String query = typeClause + " AND " + getDateSentColumnName() + " > ?"; + String query = typeClause + " AND " + DATE_SENT + " > ?"; String[] args = new String[]{String.valueOf(System.currentTimeMillis() - InsightsConstants.PERIOD_IN_MILLIS)}; - try (Cursor cursor = db.query(getTableName(), projection, query, args, null, null, null, null)) { + try (Cursor cursor = db.query(TABLE_NAME, projection, query, args, null, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { return cursor.getInt(0); } else { @@ -370,17 +3565,17 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn } private String getOutgoingInsecureMessageClause() { - return "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE + " AND NOT (" + getTypeField() + " & " + Types.SECURE_MESSAGE_BIT + ")"; + return "(" + TYPE + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE + " AND NOT (" + TYPE + " & " + Types.SECURE_MESSAGE_BIT + ")"; } private String getOutgoingSecureMessageClause() { - return "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE + " AND (" + getTypeField() + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")"; + return "(" + TYPE + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE + " AND (" + TYPE + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")"; } private String getSecureMessageClause() { - String isSent = "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE; - String isReceived = "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_INBOX_TYPE; - String isSecure = "(" + getTypeField() + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")"; + String isSent = "(" + TYPE + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE; + String isReceived = "(" + TYPE + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_INBOX_TYPE; + String isSecure = "(" + TYPE + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")"; return String.format(Locale.ENGLISH, "(%s OR %s) AND %s", isSent, isReceived, isSecure); } @@ -390,15 +3585,15 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn } protected String getInsecureMessageClause(long threadId) { - String isSent = "(" + getTableName() + "." + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE; - String isReceived = "(" + getTableName() + "." + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_INBOX_TYPE; - String isSecure = "(" + getTableName() + "." + getTypeField() + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")"; - String isNotSecure = "(" + getTableName() + "." + getTypeField() + " <= " + (Types.BASE_TYPE_MASK | Types.MESSAGE_ATTRIBUTE_MASK) + ")"; + String isSent = "(" + TABLE_NAME + "." + TYPE + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE; + String isReceived = "(" + TABLE_NAME + "." + TYPE + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_INBOX_TYPE; + String isSecure = "(" + TABLE_NAME + "." + TYPE + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")"; + String isNotSecure = "(" + TABLE_NAME + "." + TYPE + " <= " + (Types.BASE_TYPE_MASK | Types.MESSAGE_ATTRIBUTE_MASK) + ")"; String whereClause = String.format(Locale.ENGLISH, "(%s OR %s) AND NOT %s AND %s", isSent, isReceived, isSecure, isNotSecure); if (threadId != -1) { - whereClause += " AND " + getTableName() + "." + THREAD_ID + " = " + threadId; + whereClause += " AND " + TABLE_NAME + "." + THREAD_ID + " = " + threadId; } return whereClause; @@ -409,7 +3604,7 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn } public int getUnexportedInsecureMessagesCount(long threadId) { - try (Cursor cursor = getWritableDatabase().query(getTableName(), SqlUtil.COUNT, getInsecureMessageClause(threadId) + " AND " + EXPORTED + " < ?", SqlUtil.buildArgs(MessageExportStatus.EXPORTED), null, null, null)) { + try (Cursor cursor = getWritableDatabase().query(TABLE_NAME, SqlUtil.COUNT, getInsecureMessageClause(threadId) + " AND " + EXPORTED + " < ?", SqlUtil.buildArgs(MessageExportStatus.EXPORTED), null, null, null)) { if (cursor.moveToFirst()) { return cursor.getInt(0); } @@ -426,7 +3621,7 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn values.putNull(EXPORT_STATE); values.put(EXPORTED, MessageExportStatus.UNEXPORTED.serialize()); - SQLiteDatabaseExtensionsKt.update(getWritableDatabase(), getTableName()) + SQLiteDatabaseExtensionsKt.update(getWritableDatabase(), TABLE_NAME) .values(values) .where(EXPORT_STATE + " IS NOT NULL OR " + EXPORTED + " != ?", MessageExportStatus.UNEXPORTED) .run(); @@ -439,7 +3634,7 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn ContentValues values = new ContentValues(1); values.put(EXPORTED, MessageExportStatus.UNEXPORTED.getCode()); - SQLiteDatabaseExtensionsKt.update(getWritableDatabase(), getTableName()) + SQLiteDatabaseExtensionsKt.update(getWritableDatabase(), TABLE_NAME) .values(values) .where(EXPORTED + " < ?", MessageExportStatus.UNEXPORTED) .run(); @@ -452,13 +3647,13 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn String[] whereArgs = new String[]{String.valueOf(threadId), "1"}; if (sinceTimestamp > -1) { - whereClause += " AND " + getDateReceivedColumnName() + " <= " + sinceTimestamp; + whereClause += " AND " + DATE_RECEIVED + " <= " + sinceTimestamp; } values.put(REACTIONS_UNREAD, 0); values.put(REACTIONS_LAST_SEEN, System.currentTimeMillis()); - db.update(getTableName(), values, whereClause, whereArgs); + db.update(TABLE_NAME, values, whereClause, whereArgs); } public void setAllReactionsSeen() { @@ -470,7 +3665,7 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn values.put(REACTIONS_UNREAD, 0); values.put(REACTIONS_LAST_SEEN, System.currentTimeMillis()); - db.update(getTableName(), values, query, args); + db.update(TABLE_NAME, values, query, args); } public void setNotifiedTimestamp(long timestamp, @NonNull List ids) { @@ -484,7 +3679,7 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn values.put(NOTIFIED_TIMESTAMP, timestamp); - db.update(getTableName(), values, where.getWhere(), where.getWhereArgs()); + db.update(TABLE_NAME, values, where.getWhere(), where.getWhereArgs()); } public void addMismatchedIdentity(long messageId, @NonNull RecipientId recipientId, IdentityKey identityKey) { @@ -517,15 +3712,15 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn public @NonNull List getReportSpamMessageServerGuids(long threadId, long timestamp) { SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String query = THREAD_ID + " = ? AND " + getDateReceivedColumnName() + " <= ?"; + String query = THREAD_ID + " = ? AND " + DATE_RECEIVED + " <= ?"; String[] args = SqlUtil.buildArgs(threadId, timestamp); List data = new ArrayList<>(); - try (Cursor cursor = db.query(getTableName(), new String[] { RECIPIENT_ID, SERVER_GUID, getDateReceivedColumnName() }, query, args, null, null, getDateReceivedColumnName() + " DESC", "3")) { + try (Cursor cursor = db.query(TABLE_NAME, new String[] { RECIPIENT_ID, SERVER_GUID, DATE_RECEIVED }, query, args, null, null, DATE_RECEIVED + " DESC", "3")) { while (cursor.moveToNext()) { RecipientId id = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); String serverGuid = CursorUtil.requireString(cursor, SERVER_GUID); - long dateReceived = CursorUtil.requireLong(cursor, getDateReceivedColumnName()); + long dateReceived = CursorUtil.requireLong(cursor, DATE_RECEIVED); if (!Util.isEmpty(serverGuid)) { data.add(new ReportSpamData(id, serverGuid, dateReceived)); } @@ -536,22 +3731,22 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn public List getIncomingPaymentRequestThreads() { Cursor cursor = SQLiteDatabaseExtensionsKt.select(getReadableDatabase(), "DISTINCT " + THREAD_ID) - .from(getTableName()) - .where("(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_INBOX_TYPE + " AND (" + getTypeField() + " & ?) != 0", Types.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST) - .run(); + .from(TABLE_NAME) + .where("(" + TYPE + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_INBOX_TYPE + " AND (" + TYPE + " & ?) != 0", Types.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST) + .run(); return CursorExtensionsKt.readToList(cursor, c -> CursorUtil.requireLong(c, THREAD_ID)); } public @Nullable MessageId getPaymentMessage(@NonNull UUID paymentUuid) { Cursor cursor = SQLiteDatabaseExtensionsKt.select(getReadableDatabase(), ID) - .from(getTableName()) - .where(getTypeField() + " & ? != 0 AND body = ?", Types.SPECIAL_TYPE_PAYMENTS_NOTIFICATION, paymentUuid) + .from(TABLE_NAME) + .where(TYPE + " & ? != 0 AND body = ?", Types.SPECIAL_TYPE_PAYMENTS_NOTIFICATION, paymentUuid) .run(); long id = CursorExtensionsKt.readToSingleLong(cursor, -1); if (id != -1) { - return new MessageId(id, getTableName().equals(MmsTable.TABLE_NAME)); + return new MessageId(id, TABLE_NAME.equals(MessageTable.TABLE_NAME)); } else { return null; } @@ -561,14 +3756,14 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn public void remapRecipient(@NonNull RecipientId fromId, @NonNull RecipientId toId) { ContentValues values = new ContentValues(); values.put(RECIPIENT_ID, toId.serialize()); - getWritableDatabase().update(getTableName(), values, RECIPIENT_ID + " = ?", SqlUtil.buildArgs(fromId)); + getWritableDatabase().update(TABLE_NAME, values, RECIPIENT_ID + " = ?", SqlUtil.buildArgs(fromId)); } @Override public void remapThread(long fromId, long toId) { ContentValues values = new ContentValues(); - values.put(SmsTable.THREAD_ID, toId); - getWritableDatabase().update(getTableName(), values, THREAD_ID + " = ?", SqlUtil.buildArgs(fromId)); + values.put(MmsSmsColumns.THREAD_ID, toId); + getWritableDatabase().update(TABLE_NAME, values, THREAD_ID + " = ?", SqlUtil.buildArgs(fromId)); } void updateReactionsUnread(SQLiteDatabase db, long messageId, boolean hasReactions, boolean isRemoval) { @@ -587,7 +3782,7 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn } if (values.size() > 0) { - db.update(getTableName(), values, ID_WHERE, SqlUtil.buildArgs(messageId)); + db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(messageId)); } } catch (NoSuchMessageException e) { Log.w(TAG, "Failed to find message " + messageId); @@ -650,7 +3845,7 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn contentValues.put(column, JsonUtils.toJson(document)); } - database.update(getTableName(), contentValues, ID_WHERE, new String[] {String.valueOf(messageId)}); + database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)}); } private D getDocument(SQLiteDatabase database, long messageId, @@ -659,7 +3854,7 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn Cursor cursor = null; try { - cursor = database.query(getTableName(), new String[] {column}, + cursor = database.query(TABLE_NAME, new String[] {column}, ID_WHERE, new String[] {String.valueOf(messageId)}, null, null, null); @@ -694,7 +3889,7 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn String query = ID + " = ?"; String[] args = new String[]{ String.valueOf(messageId) }; - try (Cursor cursor = db.query(getTableName(), projection, query, args, null, null, null)) { + try (Cursor cursor = db.query(TABLE_NAME, projection, query, args, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { return cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID)); } @@ -985,4 +4180,520 @@ public abstract class MessageTable extends DatabaseTable implements MmsSmsColumn */ ALL } + + public static class SmsReader implements MessageTable.Reader { + + private final Cursor cursor; + private final Context context; + + public SmsReader(Cursor cursor) { + this.cursor = cursor; + this.context = ApplicationDependencies.getApplication(); + } + + public SmsMessageRecord getNext() { + if (cursor == null || !cursor.moveToNext()) + return null; + + return getCurrent(); + } + + public int getCount() { + if (cursor == null) return 0; + else return cursor.getCount(); + } + + @Override + public @NonNull MessageExportState getMessageExportStateForCurrentRecord() { + byte[] messageExportState = CursorUtil.requireBlob(cursor, MmsSmsColumns.EXPORT_STATE); + if (messageExportState == null) { + return MessageExportState.getDefaultInstance(); + } + + try { + return MessageExportState.parseFrom(messageExportState); + } catch (InvalidProtocolBufferException e) { + return MessageExportState.getDefaultInstance(); + } + } + + public SmsMessageRecord getCurrent() { + long messageId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.ID)); + long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.RECIPIENT_ID)); + int addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.RECIPIENT_DEVICE_ID)); + long type = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.TYPE)); + long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.DATE_RECEIVED)); + long dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.DATE_SENT)); + long dateServer = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.DATE_SERVER)); + long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.THREAD_ID)); + int status = cursor.getInt(cursor.getColumnIndexOrThrow(MessageTable.MMS_STATUS)); + int deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.DELIVERY_RECEIPT_COUNT)); + int readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.READ_RECEIPT_COUNT)); + String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsColumns.MISMATCHED_IDENTITIES)); + int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.SMS_SUBSCRIPTION_ID)); + long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)); + long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRE_STARTED)); + String body = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsColumns.BODY)); + boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.UNIDENTIFIED)) == 1; + boolean remoteDelete = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.REMOTE_DELETED)) == 1; + long notifiedTimestamp = CursorUtil.requireLong(cursor, NOTIFIED_TIMESTAMP); + long receiptTimestamp = CursorUtil.requireLong(cursor, RECEIPT_TIMESTAMP); + + if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { + readReceiptCount = 0; + } + + Set mismatches = getMismatches(mismatchDocument); + Recipient recipient = Recipient.live(RecipientId.from(recipientId)).get(); + + return new SmsMessageRecord(messageId, body, recipient, + recipient, + addressDeviceId, + dateSent, dateReceived, dateServer, deliveryReceiptCount, type, + threadId, status, mismatches, subscriptionId, + expiresIn, expireStarted, + readReceiptCount, unidentified, Collections.emptyList(), remoteDelete, + notifiedTimestamp, receiptTimestamp); + } + + private Set getMismatches(String document) { + try { + if (!TextUtils.isEmpty(document)) { + return JsonUtils.fromJson(document, IdentityKeyMismatchSet.class).getItems(); + } + } catch (IOException e) { + Log.w(TAG, e); + } + + return Collections.emptySet(); + } + + @Override + public void close() { + cursor.close(); + } + + @Override + public @NonNull Iterator iterator() { + return new ReaderIterator(); + } + + private class ReaderIterator implements Iterator { + @Override + public boolean hasNext() { + return cursor != null && cursor.getCount() != 0 && !cursor.isLast(); + } + + @Override + public MessageRecord next() { + MessageRecord record = getNext(); + if (record == null) { + throw new NoSuchElementException(); + } + + return record; + } + } + } + + public static OutgoingSmsReader readerFor(OutgoingTextMessage message, long threadId, long messageId) { + return new OutgoingSmsReader(message, threadId, messageId); + } + + public static class OutgoingSmsReader { + + private final OutgoingTextMessage message; + private final long id; + private final long threadId; + + public OutgoingSmsReader(OutgoingTextMessage message, long threadId, long messageId) { + this.message = message; + this.threadId = threadId; + this.id = messageId; + } + + public MessageRecord getCurrent() { + return new SmsMessageRecord(id, + message.getMessageBody(), + message.getRecipient(), + message.getRecipient(), + 1, + System.currentTimeMillis(), + System.currentTimeMillis(), + -1, + 0, + message.isSecureMessage() ? MmsSmsColumns.Types.getOutgoingEncryptedMessageType() : MmsSmsColumns.Types.getOutgoingSmsMessageType(), + threadId, + 0, + new HashSet<>(), + message.getSubscriptionId(), + message.getExpiresIn(), + System.currentTimeMillis(), + 0, + false, + Collections.emptyList(), + false, + 0, + -1); + } + } + + public static class MmsStatus { + public static final int DOWNLOAD_INITIALIZED = 1; + public static final int DOWNLOAD_NO_CONNECTIVITY = 2; + public static final int DOWNLOAD_CONNECTING = 3; + public static final int DOWNLOAD_SOFT_FAILURE = 4; + public static final int DOWNLOAD_HARD_FAILURE = 5; + public static final int DOWNLOAD_APN_UNAVAILABLE = 6; + } + + public static class OutgoingMmsReader { + + private final Context context; + private final OutgoingMediaMessage message; + private final long id; + private final long threadId; + + public OutgoingMmsReader(OutgoingMediaMessage message, long threadId) { + this.context = ApplicationDependencies.getApplication(); + this.message = message; + this.id = new SecureRandom().nextLong(); + this.threadId = threadId; + } + + public MessageRecord getCurrent() { + SlideDeck slideDeck = new SlideDeck(context, message.getAttachments()); + + CharSequence quoteText = message.getOutgoingQuote() != null ? message.getOutgoingQuote().getText() : null; + List quoteMentions = message.getOutgoingQuote() != null ? message.getOutgoingQuote().getMentions() : Collections.emptyList(); + + if (quoteText != null && !quoteMentions.isEmpty()) { + MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, quoteText, quoteMentions); + + quoteText = updated.getBody(); + quoteMentions = updated.getMentions(); + } + + return new MediaMmsMessageRecord(id, + message.getRecipient(), + message.getRecipient(), + 1, + System.currentTimeMillis(), + System.currentTimeMillis(), + -1, + 0, + threadId, message.getBody(), + slideDeck, + message.isSecure() ? MmsSmsColumns.Types.getOutgoingEncryptedMessageType() : MmsSmsColumns.Types.getOutgoingSmsMessageType(), + Collections.emptySet(), + Collections.emptySet(), + message.getSubscriptionId(), + message.getExpiresIn(), + System.currentTimeMillis(), + message.isViewOnce(), + 0, + message.getOutgoingQuote() != null ? + new Quote(message.getOutgoingQuote().getId(), + message.getOutgoingQuote().getAuthor(), + quoteText, + message.getOutgoingQuote().isOriginalMissing(), + new SlideDeck(context, message.getOutgoingQuote().getAttachments()), + quoteMentions, + message.getOutgoingQuote().getType()) : + null, + message.getSharedContacts(), + message.getLinkPreviews(), + false, + Collections.emptyList(), + false, + false, + 0, + 0, + -1, + null, + message.getStoryType(), + message.getParentStoryId(), + message.getGiftBadge(), + null); + } + } + + /** + * MessageRecord reader which implements the Iterable interface. This allows it to + * be used with many Kotlin Extension Functions as well as with for-each loops. + * + * Note that it's the responsibility of the developer using the reader to ensure that: + * + * 1. They only utilize one of the two interfaces (legacy or iterator) + * 1. They close this reader after use, preferably via try-with-resources or a use block. + */ + public static class MmsReader implements MessageTable.Reader { + + private final Cursor cursor; + private final Context context; + + public MmsReader(Cursor cursor) { + this.cursor = cursor; + this.context = ApplicationDependencies.getApplication(); + } + + @Override + public MessageRecord getNext() { + if (cursor == null || !cursor.moveToNext()) + return null; + + return getCurrent(); + } + + @Override + public MessageRecord getCurrent() { + long mmsType = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.MMS_MESSAGE_TYPE)); + + if (mmsType == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) { + return getNotificationMmsMessageRecord(cursor); + } else { + return getMediaMmsMessageRecord(cursor); + } + } + + @Override + public @NonNull MessageExportState getMessageExportStateForCurrentRecord() { + byte[] messageExportState = CursorUtil.requireBlob(cursor, MessageTable.EXPORT_STATE); + if (messageExportState == null) { + return MessageExportState.getDefaultInstance(); + } + + try { + return MessageExportState.parseFrom(messageExportState); + } catch (InvalidProtocolBufferException e) { + return MessageExportState.getDefaultInstance(); + } + } + + public int getCount() { + if (cursor == null) return 0; + else return cursor.getCount(); + } + + private NotificationMmsMessageRecord getNotificationMmsMessageRecord(Cursor cursor) { + long id = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.ID)); + long dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.DATE_SENT)); + long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.DATE_RECEIVED)); + long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.THREAD_ID)); + long mailbox = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.TYPE)); + long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.RECIPIENT_ID)); + int addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(MessageTable.RECIPIENT_DEVICE_ID)); + Recipient recipient = Recipient.live(RecipientId.from(recipientId)).get(); + + String contentLocation = cursor.getString(cursor.getColumnIndexOrThrow(MessageTable.MMS_CONTENT_LOCATION)); + String transactionId = cursor.getString(cursor.getColumnIndexOrThrow(MessageTable.MMS_TRANSACTION_ID)); + long messageSize = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.MMS_MESSAGE_SIZE)); + long expiry = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.MMS_EXPIRY)); + int status = cursor.getInt(cursor.getColumnIndexOrThrow(MessageTable.MMS_STATUS)); + int deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MessageTable.DELIVERY_RECEIPT_COUNT)); + int readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MessageTable.READ_RECEIPT_COUNT)); + int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(MessageTable.SMS_SUBSCRIPTION_ID)); + int viewedReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.VIEWED_RECEIPT_COUNT)); + long receiptTimestamp = CursorUtil.requireLong(cursor, MmsSmsColumns.RECEIPT_TIMESTAMP); + StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE)); + ParentStoryId parentStoryId = ParentStoryId.deserialize(CursorUtil.requireLong(cursor, PARENT_STORY_ID)); + String body = cursor.getString(cursor.getColumnIndexOrThrow(MessageTable.BODY)); + + if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { + readReceiptCount = 0; + } + + byte[]contentLocationBytes = null; + byte[]transactionIdBytes = null; + + if (!TextUtils.isEmpty(contentLocation)) + contentLocationBytes = org.thoughtcrime.securesms.util.Util.toIsoBytes(contentLocation); + + if (!TextUtils.isEmpty(transactionId)) + transactionIdBytes = org.thoughtcrime.securesms.util.Util.toIsoBytes(transactionId); + + SlideDeck slideDeck = new SlideDeck(context, new MmsNotificationAttachment(status, messageSize)); + + GiftBadge giftBadge = null; + if (body != null && Types.isGiftBadge(mailbox)) { + try { + giftBadge = GiftBadge.parseFrom(Base64.decode(body)); + } catch (IOException e) { + Log.w(TAG, "Error parsing gift badge", e); + } + } + + return new NotificationMmsMessageRecord(id, recipient, recipient, + addressDeviceId, dateSent, dateReceived, deliveryReceiptCount, threadId, + contentLocationBytes, messageSize, expiry, status, + transactionIdBytes, mailbox, subscriptionId, slideDeck, + readReceiptCount, viewedReceiptCount, receiptTimestamp, storyType, + parentStoryId, giftBadge); + } + + private MediaMmsMessageRecord getMediaMmsMessageRecord(Cursor cursor) { + long id = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.ID)); + long dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.DATE_SENT)); + long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.DATE_RECEIVED)); + long dateServer = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.DATE_SERVER)); + long box = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.TYPE)); + long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.THREAD_ID)); + long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.RECIPIENT_ID)); + int addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(MessageTable.RECIPIENT_DEVICE_ID)); + int deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MessageTable.DELIVERY_RECEIPT_COUNT)); + int readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MessageTable.READ_RECEIPT_COUNT)); + String body = cursor.getString(cursor.getColumnIndexOrThrow(MessageTable.BODY)); + String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(MessageTable.MISMATCHED_IDENTITIES)); + String networkDocument = cursor.getString(cursor.getColumnIndexOrThrow(MessageTable.NETWORK_FAILURES)); + int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(MessageTable.SMS_SUBSCRIPTION_ID)); + long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.EXPIRES_IN)); + long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.EXPIRE_STARTED)); + boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(MessageTable.UNIDENTIFIED)) == 1; + boolean isViewOnce = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.VIEW_ONCE)) == 1; + boolean remoteDelete = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.REMOTE_DELETED)) == 1; + boolean mentionsSelf = CursorUtil.requireBoolean(cursor, MENTIONS_SELF); + long notifiedTimestamp = CursorUtil.requireLong(cursor, NOTIFIED_TIMESTAMP); + int viewedReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.VIEWED_RECEIPT_COUNT)); + long receiptTimestamp = CursorUtil.requireLong(cursor, MmsSmsColumns.RECEIPT_TIMESTAMP); + byte[] messageRangesData = CursorUtil.requireBlob(cursor, MESSAGE_RANGES); + StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE)); + ParentStoryId parentStoryId = ParentStoryId.deserialize(CursorUtil.requireLong(cursor, PARENT_STORY_ID)); + + if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { + readReceiptCount = 0; + + if (MmsSmsColumns.Types.isOutgoingMessageType(box) && !storyType.isStory()) { + viewedReceiptCount = 0; + } + } + + Recipient recipient = Recipient.live(RecipientId.from(recipientId)).get(); + Set mismatches = getMismatchedIdentities(mismatchDocument); + Set networkFailures = getFailures(networkDocument); + List attachments = SignalDatabase.attachments().getAttachments(cursor); + List contacts = getSharedContacts(cursor, attachments); + Set contactAttachments = Stream.of(contacts).map(Contact::getAvatarAttachment).withoutNulls().collect(Collectors.toSet()); + List previews = getLinkPreviews(cursor, attachments); + Set previewAttachments = Stream.of(previews).filter(lp -> lp.getThumbnail().isPresent()).map(lp -> lp.getThumbnail().get()).collect(Collectors.toSet()); + SlideDeck slideDeck = buildSlideDeck(context, Stream.of(attachments).filterNot(contactAttachments::contains).filterNot(previewAttachments::contains).toList()); + Quote quote = getQuote(cursor); + BodyRangeList messageRanges = null; + + try { + if (messageRangesData != null) { + messageRanges = BodyRangeList.parseFrom(messageRangesData); + } + } catch (InvalidProtocolBufferException e) { + Log.w(TAG, "Error parsing message ranges", e); + } + + GiftBadge giftBadge = null; + if (body != null && Types.isGiftBadge(box)) { + try { + giftBadge = GiftBadge.parseFrom(Base64.decode(body)); + } catch (IOException e) { + Log.w(TAG, "Error parsing gift badge", e); + } + } + + return new MediaMmsMessageRecord(id, recipient, recipient, + addressDeviceId, dateSent, dateReceived, dateServer, deliveryReceiptCount, + threadId, body, slideDeck, box, mismatches, + networkFailures, subscriptionId, expiresIn, expireStarted, + isViewOnce, readReceiptCount, quote, contacts, previews, unidentified, Collections.emptyList(), + remoteDelete, mentionsSelf, notifiedTimestamp, viewedReceiptCount, receiptTimestamp, messageRanges, + storyType, parentStoryId, giftBadge, null); + } + + private Set getMismatchedIdentities(String document) { + if (!TextUtils.isEmpty(document)) { + try { + return JsonUtils.fromJson(document, IdentityKeyMismatchSet.class).getItems(); + } catch (IOException e) { + Log.w(TAG, e); + } + } + + return Collections.emptySet(); + } + + private Set getFailures(String document) { + if (!TextUtils.isEmpty(document)) { + try { + return JsonUtils.fromJson(document, NetworkFailureSet.class).getItems(); + } catch (IOException ioe) { + Log.w(TAG, ioe); + } + } + + return Collections.emptySet(); + } + + public static SlideDeck buildSlideDeck(@NonNull Context context, @NonNull List attachments) { + List messageAttachments = Stream.of(attachments) + .filterNot(Attachment::isQuote) + .sorted(new DatabaseAttachment.DisplayOrderComparator()) + .toList(); + return new SlideDeck(context, messageAttachments); + } + + private @Nullable Quote getQuote(@NonNull Cursor cursor) { + long quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.QUOTE_ID)); + long quoteAuthor = cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.QUOTE_AUTHOR)); + CharSequence quoteText = cursor.getString(cursor.getColumnIndexOrThrow(MessageTable.QUOTE_BODY)); + int quoteType = cursor.getInt(cursor.getColumnIndexOrThrow(MessageTable.QUOTE_TYPE)); + boolean quoteMissing = cursor.getInt(cursor.getColumnIndexOrThrow(MessageTable.QUOTE_MISSING)) == 1; + List quoteMentions = parseQuoteMentions(context, cursor); + List attachments = SignalDatabase.attachments().getAttachments(cursor); + List quoteAttachments = Stream.of(attachments).filter(Attachment::isQuote).toList(); + SlideDeck quoteDeck = new SlideDeck(context, quoteAttachments); + + if (quoteId > 0 && quoteAuthor > 0) { + if (quoteText != null && !quoteMentions.isEmpty()) { + MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, quoteText, quoteMentions); + + quoteText = updated.getBody(); + quoteMentions = updated.getMentions(); + } + + return new Quote(quoteId, RecipientId.from(quoteAuthor), quoteText, quoteMissing, quoteDeck, quoteMentions, QuoteModel.Type.fromCode(quoteType)); + } else { + return null; + } + } + + @Override + public void close() { + if (cursor != null) { + cursor.close(); + } + } + + @NonNull + @Override + public Iterator iterator() { + return new ReaderIterator(); + } + + private class ReaderIterator implements Iterator { + @Override + public boolean hasNext() { + return cursor != null && cursor.getCount() != 0 && !cursor.isLast(); + } + + @Override + public MessageRecord next() { + MessageRecord record = getNext(); + if (record == null) { + throw new NoSuchElementException(); + } + + return record; + } + } + } + + private long generatePduCompatTimestamp(long time) { + return time - (time % 1000); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java index 5dbd4d0a5c..c8af7d3c9f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -1,5 +1,7 @@ package org.thoughtcrime.securesms.database; +import androidx.annotation.VisibleForTesting; + @SuppressWarnings("UnnecessaryInterfaceModifier") public interface MmsSmsColumns { @@ -32,6 +34,13 @@ public interface MmsSmsColumns { public static final String EXPORT_STATE = "export_state"; public static final String EXPORTED = "exported"; + public static class Status { + public static final int STATUS_NONE = -1; + public static final int STATUS_COMPLETE = 0; + public static final int STATUS_PENDING = 0x20; + public static final int STATUS_FAILED = 0x40; + } + /** * For storage efficiency, all types are stored within a single 64-bit integer column in the * database. There are various areas reserved for different classes of data. @@ -147,6 +156,8 @@ public interface MmsSmsColumns { protected static final long SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST = 0x400000000L; protected static final long SPECIAL_TYPE_PAYMENTS_ACTIVATED = 0x800000000L; + static final long IGNORABLE_TYPESMASK_WHEN_COUNTING = Types.END_SESSION_BIT | Types.KEY_EXCHANGE_IDENTITY_UPDATE_BIT | Types.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT; + public static boolean isStoryReaction(long type) { return (type & SPECIAL_TYPES_MASK) == SPECIAL_TYPE_STORY_REACTION; } @@ -417,7 +428,6 @@ public interface MmsSmsColumns { return 1; } - // // // diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsTable.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsTable.java index bbc0f65909..3023759075 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsTable.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsTable.java @@ -35,6 +35,7 @@ import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.util.Pair; import org.thoughtcrime.securesms.database.MessageTable.MessageUpdate; import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId; +import org.thoughtcrime.securesms.database.model.DisplayRecord; import org.thoughtcrime.securesms.database.model.MessageExportStatus; import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageRecord; @@ -50,6 +51,7 @@ import java.io.Closeable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -65,64 +67,53 @@ public class MmsSmsTable extends DatabaseTable { @SuppressWarnings("unused") private static final String TAG = Log.tag(MmsSmsTable.class); - public static final String TRANSPORT = "transport_type"; - public static final String MMS_TRANSPORT = "mms"; - public static final String SMS_TRANSPORT = "sms"; - - private static final String[] PROJECTION = { MmsSmsColumns.ID, - MmsSmsColumns.UNIQUE_ROW_ID, + private static final String[] PROJECTION = { MessageTable.TABLE_NAME + "." + MessageTable.ID + " AS " + MmsSmsColumns.ID, MmsSmsColumns.BODY, MmsSmsColumns.TYPE, MmsSmsColumns.THREAD_ID, - SmsTable.RECIPIENT_ID, - SmsTable.RECIPIENT_DEVICE_ID, + MmsSmsColumns.RECIPIENT_ID, + MmsSmsColumns.RECIPIENT_DEVICE_ID, MmsSmsColumns.DATE_SENT, MmsSmsColumns.DATE_RECEIVED, MmsSmsColumns.DATE_SERVER, - MmsTable.MMS_MESSAGE_TYPE, - SmsTable.SMS_STATUS, + MessageTable.MMS_MESSAGE_TYPE, MmsSmsColumns.UNIDENTIFIED, - MmsTable.MMS_CONTENT_LOCATION, - MmsTable.MMS_TRANSACTION_ID, - MmsTable.MMS_MESSAGE_SIZE, - MmsTable.MMS_EXPIRY, - MmsTable.MMS_STATUS, + MessageTable.MMS_CONTENT_LOCATION, + MessageTable.MMS_TRANSACTION_ID, + MessageTable.MMS_MESSAGE_SIZE, + MessageTable.MMS_EXPIRY, + MessageTable.MMS_STATUS, MmsSmsColumns.DELIVERY_RECEIPT_COUNT, MmsSmsColumns.READ_RECEIPT_COUNT, MmsSmsColumns.MISMATCHED_IDENTITIES, - MmsTable.NETWORK_FAILURES, + MessageTable.NETWORK_FAILURES, MmsSmsColumns.SMS_SUBSCRIPTION_ID, MmsSmsColumns.EXPIRES_IN, MmsSmsColumns.EXPIRE_STARTED, MmsSmsColumns.NOTIFIED, - TRANSPORT, - AttachmentTable.ATTACHMENT_JSON_ALIAS, - MmsTable.QUOTE_ID, - MmsTable.QUOTE_AUTHOR, - MmsTable.QUOTE_BODY, - MmsTable.QUOTE_MISSING, - MmsTable.QUOTE_TYPE, - MmsTable.QUOTE_MENTIONS, - MmsTable.SHARED_CONTACTS, - MmsTable.LINK_PREVIEWS, - MmsTable.VIEW_ONCE, + MessageTable.QUOTE_ID, + MessageTable.QUOTE_AUTHOR, + MessageTable.QUOTE_BODY, + MessageTable.QUOTE_MISSING, + MessageTable.QUOTE_TYPE, + MessageTable.QUOTE_MENTIONS, + MessageTable.SHARED_CONTACTS, + MessageTable.LINK_PREVIEWS, + MessageTable.VIEW_ONCE, MmsSmsColumns.READ, MmsSmsColumns.REACTIONS_UNREAD, MmsSmsColumns.REACTIONS_LAST_SEEN, MmsSmsColumns.REMOTE_DELETED, - MmsTable.MENTIONS_SELF, + MessageTable.MENTIONS_SELF, MmsSmsColumns.NOTIFIED_TIMESTAMP, MmsSmsColumns.VIEWED_RECEIPT_COUNT, MmsSmsColumns.RECEIPT_TIMESTAMP, - MmsTable.MESSAGE_RANGES, - MmsTable.STORY_TYPE, - MmsTable.PARENT_STORY_ID}; + MessageTable.MESSAGE_RANGES, + MessageTable.STORY_TYPE, + MessageTable.PARENT_STORY_ID}; - private static final String SNIPPET_QUERY = "SELECT " + MmsSmsColumns.ID + ", 0 AS " + TRANSPORT + ", " + MmsSmsColumns.TYPE + ", " + MmsSmsColumns.DATE_RECEIVED + " FROM " + SmsTable.TABLE_NAME + " " + - "WHERE " + MmsSmsColumns.THREAD_ID + " = ? AND " + MmsSmsColumns.TYPE + " NOT IN (" + SmsTable.Types.PROFILE_CHANGE_TYPE + ", " + SmsTable.Types.GV1_MIGRATION_TYPE + ", " + SmsTable.Types.CHANGE_NUMBER_TYPE + ", " + SmsTable.Types.BOOST_REQUEST_TYPE + ", " + SmsTable.Types.SMS_EXPORT_TYPE + ") AND " + SmsTable.TYPE + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + " " + - "UNION ALL " + - "SELECT " + MmsSmsColumns.ID + ", 1 AS " + TRANSPORT + ", " + MmsSmsColumns.TYPE + ", " + MmsTable.DATE_RECEIVED + " FROM " + MmsTable.TABLE_NAME + " " + - "WHERE " + MmsSmsColumns.THREAD_ID + " = ? AND " + MmsTable.TYPE + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + " AND " + MmsTable.STORY_TYPE + " = 0 AND " + MmsTable.PARENT_STORY_ID + " <= 0 " + + private static final String SNIPPET_QUERY = "SELECT " + MmsSmsColumns.ID + ", " + MmsSmsColumns.TYPE + ", " + MessageTable.DATE_RECEIVED + " FROM " + MessageTable.TABLE_NAME + " " + + "WHERE " + MmsSmsColumns.THREAD_ID + " = ? AND " + MessageTable.TYPE + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + " AND " + MessageTable.STORY_TYPE + " = 0 AND " + MessageTable.PARENT_STORY_ID + " <= 0 " + "ORDER BY " + MmsSmsColumns.DATE_RECEIVED + " DESC " + "LIMIT 1"; @@ -163,7 +154,7 @@ public class MmsSmsTable extends DatabaseTable { String[] projection = new String[] { "COUNT(*)" }; String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.DATE_RECEIVED + " >= " + timestamp + " AND " + - MmsTable.STORY_TYPE + " = 0 AND " + MmsTable.PARENT_STORY_ID + " <= 0"; + MessageTable.STORY_TYPE + " = 0 AND " + MessageTable.PARENT_STORY_ID + " <= 0"; try (Cursor cursor = queryTables(projection, selection, null, null, false)) { if (cursor != null && cursor.moveToNext()) { @@ -199,7 +190,7 @@ public class MmsSmsTable extends DatabaseTable { List sms = SignalDatabase.sms().getMessagesInThreadAfterInclusive(origin.getThreadId(), origin.getDateReceived(), limit); mms.addAll(sms); - Collections.sort(mms, (a, b) -> Long.compare(a.getDateReceived(), b.getDateReceived())); + Collections.sort(mms, Comparator.comparingLong(DisplayRecord::getDateReceived)); return Stream.of(mms).limit(limit).toList(); } @@ -208,7 +199,7 @@ public class MmsSmsTable extends DatabaseTable { public Cursor getConversation(long threadId, long offset, long limit) { SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); String order = MmsSmsColumns.DATE_RECEIVED + " DESC"; - String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsTable.STORY_TYPE + " = 0 AND " + MmsTable.PARENT_STORY_ID + " <= 0"; + String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MessageTable.STORY_TYPE + " = 0 AND " + MessageTable.PARENT_STORY_ID + " <= 0"; String limitStr = limit > 0 || offset > 0 ? offset + ", " + limit : null; String query = buildQuery(PROJECTION, selection, order, limitStr, false); @@ -222,14 +213,8 @@ public class MmsSmsTable extends DatabaseTable { public @NonNull MessageRecord getConversationSnippet(long threadId) throws NoSuchMessageException { try (Cursor cursor = getConversationSnippetCursor(threadId)) { if (cursor.moveToFirst()) { - boolean isMms = CursorUtil.requireBoolean(cursor, TRANSPORT); - long id = CursorUtil.requireLong(cursor, MmsSmsColumns.ID); - - if (isMms) { - return SignalDatabase.mms().getMessageRecord(id); - } else { - return SignalDatabase.sms().getMessageRecord(id); - } + long id = CursorUtil.requireLong(cursor, MmsSmsColumns.ID); + return SignalDatabase.mms().getMessageRecord(id); } else { throw new NoSuchMessageException("no message"); } @@ -239,12 +224,12 @@ public class MmsSmsTable extends DatabaseTable { @VisibleForTesting @NonNull Cursor getConversationSnippetCursor(long threadId) { SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - return db.rawQuery(SNIPPET_QUERY, SqlUtil.buildArgs(threadId, threadId)); + return db.rawQuery(SNIPPET_QUERY, SqlUtil.buildArgs(threadId)); } public long getConversationSnippetType(long threadId) throws NoSuchMessageException { SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - try (Cursor cursor = db.rawQuery(SNIPPET_QUERY, SqlUtil.buildArgs(threadId, threadId))) { + try (Cursor cursor = db.rawQuery(SNIPPET_QUERY, SqlUtil.buildArgs(threadId))) { if (cursor.moveToFirst()) { return CursorUtil.requireLong(cursor, MmsSmsColumns.TYPE); } else { @@ -271,7 +256,7 @@ public class MmsSmsTable extends DatabaseTable { } String order = MmsSmsColumns.DATE_RECEIVED + " ASC"; - String selection = MmsSmsColumns.NOTIFIED + " = 0 AND " + MmsTable.STORY_TYPE + " = 0 AND (" + MmsSmsColumns.READ + " = 0 OR " + MmsSmsColumns.REACTIONS_UNREAD + " = 1" + (stickyQuery.length() > 0 ? " OR (" + stickyQuery + ")" : "") + ")"; + String selection = MmsSmsColumns.NOTIFIED + " = 0 AND " + MessageTable.STORY_TYPE + " = 0 AND (" + MmsSmsColumns.READ + " = 0 OR " + MmsSmsColumns.REACTIONS_UNREAD + " = 1" + (stickyQuery.length() > 0 ? " OR (" + stickyQuery + ")" : "") + ")"; return queryTables(PROJECTION, selection, order, null, true); } @@ -283,10 +268,10 @@ public class MmsSmsTable extends DatabaseTable { RecipientId author = messageRecord.isOutgoing() ? Recipient.self().getId() : messageRecord.getRecipient().getId(); long timestamp = messageRecord.getDateSent(); - String where = MmsTable.QUOTE_ID + " = ? AND " + MmsTable.QUOTE_AUTHOR + " = ?"; + String where = MessageTable.QUOTE_ID + " = ? AND " + MessageTable.QUOTE_AUTHOR + " = ?"; String[] whereArgs = SqlUtil.buildArgs(timestamp, author); - try (Cursor cursor = getReadableDatabase().query(MmsTable.TABLE_NAME, new String[]{ "1" }, where, whereArgs, null, null, null, "1")) { + try (Cursor cursor = getReadableDatabase().query(MessageTable.TABLE_NAME, new String[]{ "1" }, where, whereArgs, null, null, null, "1")) { return cursor.moveToFirst(); } } @@ -309,9 +294,9 @@ public class MmsSmsTable extends DatabaseTable { String query; if (targetMessage.getQuote().getAuthor().equals(Recipient.self().getId())) { - query = MmsTable.DATE_SENT + " = " + targetMessage.getQuote().getId() + " AND (" + MmsSmsColumns.TYPE + " & " + MmsSmsColumns.Types.BASE_TYPE_MASK + ") = " + MmsSmsColumns.Types.BASE_SENT_TYPE; + query = MessageTable.DATE_SENT + " = " + targetMessage.getQuote().getId() + " AND (" + MmsSmsColumns.TYPE + " & " + MmsSmsColumns.Types.BASE_TYPE_MASK + ") = " + MmsSmsColumns.Types.BASE_SENT_TYPE; } else { - query = MmsTable.DATE_SENT + " = " + targetMessage.getQuote().getId() + " AND " + MmsTable.RECIPIENT_ID + " = '" + targetMessage.getQuote().getAuthor().serialize() + "'"; + query = MessageTable.DATE_SENT + " = " + targetMessage.getQuote().getId() + " AND " + MessageTable.RECIPIENT_ID + " = '" + targetMessage.getQuote().getAuthor().serialize() + "'"; } try (Reader reader = new Reader(queryTables(PROJECTION, query, null, "1", false))) { @@ -333,7 +318,7 @@ public class MmsSmsTable extends DatabaseTable { } RecipientId author = targetMessage.isOutgoing() ? Recipient.self().getId() : targetMessage.getRecipient().getId(); - String query = MmsTable.QUOTE_ID + " = " + targetMessage.getDateSent() + " AND " + MmsTable.QUOTE_AUTHOR + " = " + author.serialize(); + String query = MessageTable.QUOTE_ID + " = " + targetMessage.getDateSent() + " AND " + MessageTable.QUOTE_AUTHOR + " = " + author.serialize(); String order = MmsSmsColumns.DATE_RECEIVED + " DESC"; List records = new ArrayList<>(); @@ -361,14 +346,14 @@ public class MmsSmsTable extends DatabaseTable { private @NonNull String getStickyWherePartForParentStoryId(@Nullable Long parentStoryId) { if (parentStoryId == null) { - return " AND " + MmsTable.PARENT_STORY_ID + " <= 0"; + return " AND " + MessageTable.PARENT_STORY_ID + " <= 0"; } - return " AND " + MmsTable.PARENT_STORY_ID + " = " + parentStoryId; + return " AND " + MessageTable.PARENT_STORY_ID + " = " + parentStoryId; } public int getUnreadCount(long threadId) { - String selection = MmsSmsColumns.READ + " = 0 AND " + MmsTable.STORY_TYPE + " = 0 AND " + MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsTable.PARENT_STORY_ID + " <= 0"; + String selection = MmsSmsColumns.READ + " = 0 AND " + MessageTable.STORY_TYPE + " = 0 AND " + MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MessageTable.PARENT_STORY_ID + " <= 0"; try (Cursor cursor = queryTables(PROJECTION, selection, null, null, false)) { return cursor != null ? cursor.getCount() : 0; @@ -389,10 +374,7 @@ public class MmsSmsTable extends DatabaseTable { return 0; } - int count = SignalDatabase.sms().getSecureMessageCount(threadId); - count += SignalDatabase.mms().getSecureMessageCount(threadId); - - return count; + return SignalDatabase.mms().getSecureMessageCount(threadId); } public int getOutgoingSecureConversationCount(long threadId) { @@ -400,36 +382,23 @@ public class MmsSmsTable extends DatabaseTable { return 0; } - int count = SignalDatabase.sms().getOutgoingSecureMessageCount(threadId); - count += SignalDatabase.mms().getOutgoingSecureMessageCount(threadId); - - return count; + return SignalDatabase.mms().getOutgoingSecureMessageCount(threadId); } public int getConversationCount(long threadId) { - int count = SignalDatabase.sms().getMessageCountForThread(threadId); - count += SignalDatabase.mms().getMessageCountForThread(threadId); - - return count; + return SignalDatabase.mms().getMessageCountForThread(threadId); } public int getConversationCount(long threadId, long beforeTime) { - return SignalDatabase.sms().getMessageCountForThread(threadId, beforeTime) + - SignalDatabase.mms().getMessageCountForThread(threadId, beforeTime); + return SignalDatabase.mms().getMessageCountForThread(threadId, beforeTime); } public int getInsecureSentCount(long threadId) { - int count = SignalDatabase.sms().getInsecureMessagesSentForThread(threadId); - count += SignalDatabase.mms().getInsecureMessagesSentForThread(threadId); - - return count; + return SignalDatabase.mms().getInsecureMessagesSentForThread(threadId); } public int getInsecureMessageCountForInsights() { - int count = SignalDatabase.sms().getInsecureMessageCountForInsights(); - count += SignalDatabase.mms().getInsecureMessageCountForInsights(); - - return count; + return SignalDatabase.mms().getInsecureMessageCountForInsights(); } public int getUnexportedInsecureMessagesCount() { @@ -437,17 +406,11 @@ public class MmsSmsTable extends DatabaseTable { } public int getUnexportedInsecureMessagesCount(long threadId) { - int count = SignalDatabase.sms().getUnexportedInsecureMessagesCount(threadId); - count += SignalDatabase.mms().getUnexportedInsecureMessagesCount(threadId); - - return count; + return SignalDatabase.mms().getUnexportedInsecureMessagesCount(threadId); } public int getIncomingMeaningfulMessageCountSince(long threadId, long afterTime) { - int count = SignalDatabase.sms().getIncomingMeaningfulMessageCountSince(threadId, afterTime); - count += SignalDatabase.mms().getIncomingMeaningfulMessageCountSince(threadId, afterTime); - - return count; + return SignalDatabase.mms().getIncomingMeaningfulMessageCountSince(threadId, afterTime); } public int getMessageCountBeforeDate(long date) { @@ -665,7 +628,7 @@ public class MmsSmsTable extends DatabaseTable { } private @NonNull MessageExportState getMessageExportState(@NonNull MessageId messageId) throws NoSuchMessageException { - String table = messageId.isMms() ? MmsTable.TABLE_NAME : SmsTable.TABLE_NAME; + String table = MessageTable.TABLE_NAME; String[] projection = SqlUtil.buildArgs(MmsSmsColumns.EXPORT_STATE); String[] args = SqlUtil.buildArgs(messageId.getId()); @@ -704,7 +667,7 @@ public class MmsSmsTable extends DatabaseTable { } public void markMessageExported(@NonNull MessageId messageId) { - String table = messageId.isMms() ? MmsTable.TABLE_NAME : SmsTable.TABLE_NAME; + String table = MessageTable.TABLE_NAME; ContentValues contentValues = new ContentValues(1); contentValues.put(MmsSmsColumns.EXPORTED, MessageExportStatus.EXPORTED.getCode()); @@ -713,7 +676,7 @@ public class MmsSmsTable extends DatabaseTable { } public void markMessageExportFailed(@NonNull MessageId messageId) { - String table = messageId.isMms() ? MmsTable.TABLE_NAME : SmsTable.TABLE_NAME; + String table = MessageTable.TABLE_NAME; ContentValues contentValues = new ContentValues(1); contentValues.put(MmsSmsColumns.EXPORTED, MessageExportStatus.ERROR.getCode()); @@ -722,7 +685,7 @@ public class MmsSmsTable extends DatabaseTable { } private void setMessageExportState(@NonNull MessageId messageId, @NonNull MessageExportState messageExportState) { - String table = messageId.isMms() ? MmsTable.TABLE_NAME : SmsTable.TABLE_NAME; + String table = MessageTable.TABLE_NAME; ContentValues contentValues = new ContentValues(1); contentValues.put(MmsSmsColumns.EXPORT_STATE, messageExportState.toByteArray()); @@ -792,7 +755,7 @@ public class MmsSmsTable extends DatabaseTable { public int getQuotedMessagePosition(long threadId, long quoteId, @NonNull RecipientId recipientId) { String order = MmsSmsColumns.DATE_RECEIVED + " DESC"; - String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsTable.STORY_TYPE + " = 0" + " AND " + MmsTable.PARENT_STORY_ID + " <= 0"; + String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MessageTable.STORY_TYPE + " = 0" + " AND " + MessageTable.PARENT_STORY_ID + " <= 0"; try (Cursor cursor = queryTables(new String[]{ MmsSmsColumns.DATE_SENT, MmsSmsColumns.RECIPIENT_ID, MmsSmsColumns.REMOTE_DELETED}, selection, order, null, false)) { boolean isOwnNumber = Recipient.resolved(recipientId).isSelf(); @@ -815,7 +778,7 @@ public class MmsSmsTable extends DatabaseTable { public int getMessagePositionInConversation(long threadId, long receivedTimestamp, @NonNull RecipientId recipientId) { String order = MmsSmsColumns.DATE_RECEIVED + " DESC"; - String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsTable.STORY_TYPE + " = 0" + " AND " + MmsTable.PARENT_STORY_ID + " <= 0"; + String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MessageTable.STORY_TYPE + " = 0" + " AND " + MessageTable.PARENT_STORY_ID + " <= 0"; try (Cursor cursor = queryTables(new String[]{ MmsSmsColumns.DATE_RECEIVED, MmsSmsColumns.RECIPIENT_ID, MmsSmsColumns.REMOTE_DELETED}, selection, order, null, false)) { boolean isOwnNumber = Recipient.resolved(recipientId).isSelf(); @@ -864,12 +827,12 @@ public class MmsSmsTable extends DatabaseTable { order = MmsSmsColumns.DATE_RECEIVED + " ASC"; selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.DATE_RECEIVED + " < " + receivedTimestamp + " AND " + - MmsTable.STORY_TYPE + " = 0 AND " + MmsTable.PARENT_STORY_ID + " = " + groupStoryId; + MessageTable.STORY_TYPE + " = 0 AND " + MessageTable.PARENT_STORY_ID + " = " + groupStoryId; } else { order = MmsSmsColumns.DATE_RECEIVED + " DESC"; selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.DATE_RECEIVED + " > " + receivedTimestamp + " AND " + - MmsTable.STORY_TYPE + " = 0 AND " + MmsTable.PARENT_STORY_ID + " <= 0"; + MessageTable.STORY_TYPE + " = 0 AND " + MessageTable.PARENT_STORY_ID + " <= 0"; } try (Cursor cursor = queryTables(new String[]{ "COUNT(*)" }, selection, order, null, false)) { @@ -956,204 +919,24 @@ public class MmsSmsTable extends DatabaseTable { attachmentJsonJoin = "NULL"; } - String[] mmsProjection = { MmsSmsColumns.DATE_SENT, - MmsSmsColumns.DATE_RECEIVED, - MmsTable.TABLE_NAME + "." + MmsTable.ID + " AS " + MmsSmsColumns.ID, - "'MMS::' || " + MmsTable.TABLE_NAME + "." + MmsTable.ID + " || '::' || " + MmsTable.DATE_SENT + " AS " + MmsSmsColumns.UNIQUE_ROW_ID, - attachmentJsonJoin + " AS " + AttachmentTable.ATTACHMENT_JSON_ALIAS, - SmsTable.BODY, - MmsSmsColumns.READ, - MmsSmsColumns.THREAD_ID, - MmsSmsColumns.TYPE, - MmsSmsColumns.RECIPIENT_ID, - MmsSmsColumns.RECIPIENT_DEVICE_ID, - MmsTable.MMS_MESSAGE_TYPE, - SmsTable.SMS_STATUS, - MmsTable.MMS_CONTENT_LOCATION, - MmsTable.MMS_TRANSACTION_ID, - MmsTable.MMS_MESSAGE_SIZE, - MmsTable.MMS_EXPIRY, - MmsTable.MMS_STATUS, - MmsTable.UNIDENTIFIED, - MmsSmsColumns.DELIVERY_RECEIPT_COUNT, - MmsSmsColumns.READ_RECEIPT_COUNT, - MmsSmsColumns.MISMATCHED_IDENTITIES, - MmsSmsColumns.SMS_SUBSCRIPTION_ID, - MmsSmsColumns.EXPIRES_IN, - MmsSmsColumns.EXPIRE_STARTED, - MmsSmsColumns.NOTIFIED, - MmsTable.NETWORK_FAILURES, TRANSPORT, - MmsTable.QUOTE_ID, - MmsTable.QUOTE_AUTHOR, - MmsTable.QUOTE_BODY, - MmsTable.QUOTE_MISSING, - MmsTable.QUOTE_TYPE, - MmsTable.QUOTE_MENTIONS, - MmsTable.SHARED_CONTACTS, - MmsTable.LINK_PREVIEWS, - MmsTable.VIEW_ONCE, - MmsSmsColumns.REACTIONS_UNREAD, - MmsSmsColumns.REACTIONS_LAST_SEEN, - MmsSmsColumns.DATE_SERVER, - MmsSmsColumns.REMOTE_DELETED, - MmsTable.MENTIONS_SELF, - MmsSmsColumns.NOTIFIED_TIMESTAMP, - MmsSmsColumns.VIEWED_RECEIPT_COUNT, - MmsSmsColumns.RECEIPT_TIMESTAMP, - MmsTable.MESSAGE_RANGES, - MmsTable.STORY_TYPE, - MmsTable.PARENT_STORY_ID}; - - String[] smsProjection = { MmsSmsColumns.DATE_SENT, - MmsSmsColumns.DATE_RECEIVED, - MmsSmsColumns.ID, "'SMS::' || " + MmsSmsColumns.ID + " || '::' || " + SmsTable.DATE_SENT + " AS " + MmsSmsColumns.UNIQUE_ROW_ID, - "NULL AS " + AttachmentTable.ATTACHMENT_JSON_ALIAS, - SmsTable.BODY, - MmsSmsColumns.READ, - MmsSmsColumns.THREAD_ID, - MmsSmsColumns.TYPE, - SmsTable.RECIPIENT_ID, - SmsTable.RECIPIENT_DEVICE_ID, - MmsTable.MMS_MESSAGE_TYPE, - SmsTable.SMS_STATUS, - MmsTable.MMS_CONTENT_LOCATION, - MmsTable.MMS_TRANSACTION_ID, - MmsTable.MMS_MESSAGE_SIZE, - MmsTable.MMS_EXPIRY, - MmsTable.MMS_STATUS, - MmsTable.UNIDENTIFIED, - MmsSmsColumns.DELIVERY_RECEIPT_COUNT, - MmsSmsColumns.READ_RECEIPT_COUNT, - MmsSmsColumns.MISMATCHED_IDENTITIES, - MmsSmsColumns.SMS_SUBSCRIPTION_ID, - MmsSmsColumns.EXPIRES_IN, - MmsSmsColumns.EXPIRE_STARTED, - MmsSmsColumns.NOTIFIED, - MmsTable.NETWORK_FAILURES, TRANSPORT, - MmsTable.QUOTE_ID, - MmsTable.QUOTE_AUTHOR, - MmsTable.QUOTE_BODY, - MmsTable.QUOTE_MISSING, - MmsTable.QUOTE_TYPE, - MmsTable.QUOTE_MENTIONS, - MmsTable.SHARED_CONTACTS, - MmsTable.LINK_PREVIEWS, - MmsTable.VIEW_ONCE, - MmsSmsColumns.REACTIONS_UNREAD, - MmsSmsColumns.REACTIONS_LAST_SEEN, - MmsSmsColumns.DATE_SERVER, - MmsSmsColumns.REMOTE_DELETED, - MmsTable.MENTIONS_SELF, - MmsSmsColumns.NOTIFIED_TIMESTAMP, - MmsSmsColumns.VIEWED_RECEIPT_COUNT, - MmsSmsColumns.RECEIPT_TIMESTAMP, - MmsTable.MESSAGE_RANGES, - "0 AS " + MmsTable.STORY_TYPE, - "0 AS " + MmsTable.PARENT_STORY_ID}; + projection = SqlUtil.appendArg(projection, attachmentJsonJoin + " AS " + AttachmentTable.ATTACHMENT_JSON_ALIAS); SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); - SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder(); if (includeAttachments) { mmsQueryBuilder.setDistinct(true); - smsQueryBuilder.setDistinct(true); } - smsQueryBuilder.setTables(SmsTable.TABLE_NAME); - if (includeAttachments) { - mmsQueryBuilder.setTables(MmsTable.TABLE_NAME + " LEFT OUTER JOIN " + AttachmentTable.TABLE_NAME + - " ON " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.MMS_ID + " = " + MmsTable.TABLE_NAME + "." + MmsTable.ID); + mmsQueryBuilder.setTables(MessageTable.TABLE_NAME + " LEFT OUTER JOIN " + AttachmentTable.TABLE_NAME + + " ON " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.MMS_ID + " = " + MessageTable.TABLE_NAME + "." + MessageTable.ID); } else { - mmsQueryBuilder.setTables(MmsTable.TABLE_NAME); + mmsQueryBuilder.setTables(MessageTable.TABLE_NAME); } - Set mmsColumnsPresent = new HashSet<>(); - mmsColumnsPresent.add(MmsSmsColumns.ID); - mmsColumnsPresent.add(MmsSmsColumns.READ); - mmsColumnsPresent.add(MmsSmsColumns.THREAD_ID); - mmsColumnsPresent.add(MmsSmsColumns.BODY); - mmsColumnsPresent.add(MmsSmsColumns.RECIPIENT_ID); - mmsColumnsPresent.add(MmsSmsColumns.RECIPIENT_DEVICE_ID); - mmsColumnsPresent.add(MmsSmsColumns.DELIVERY_RECEIPT_COUNT); - mmsColumnsPresent.add(MmsSmsColumns.READ_RECEIPT_COUNT); - mmsColumnsPresent.add(MmsSmsColumns.MISMATCHED_IDENTITIES); - mmsColumnsPresent.add(MmsSmsColumns.SMS_SUBSCRIPTION_ID); - mmsColumnsPresent.add(MmsSmsColumns.EXPIRES_IN); - mmsColumnsPresent.add(MmsSmsColumns.EXPIRE_STARTED); - mmsColumnsPresent.add(MmsTable.MMS_MESSAGE_TYPE); - mmsColumnsPresent.add(MmsTable.TYPE); - mmsColumnsPresent.add(MmsTable.DATE_SENT); - mmsColumnsPresent.add(MmsTable.DATE_RECEIVED); - mmsColumnsPresent.add(MmsTable.DATE_SERVER); - mmsColumnsPresent.add(MmsTable.MMS_CONTENT_LOCATION); - mmsColumnsPresent.add(MmsTable.MMS_TRANSACTION_ID); - mmsColumnsPresent.add(MmsTable.MMS_MESSAGE_SIZE); - mmsColumnsPresent.add(MmsTable.MMS_EXPIRY); - mmsColumnsPresent.add(MmsTable.NOTIFIED); - mmsColumnsPresent.add(MmsTable.MMS_STATUS); - mmsColumnsPresent.add(MmsTable.UNIDENTIFIED); - mmsColumnsPresent.add(MmsTable.NETWORK_FAILURES); - mmsColumnsPresent.add(MmsTable.QUOTE_ID); - mmsColumnsPresent.add(MmsTable.QUOTE_AUTHOR); - mmsColumnsPresent.add(MmsTable.QUOTE_BODY); - mmsColumnsPresent.add(MmsTable.QUOTE_MISSING); - mmsColumnsPresent.add(MmsTable.QUOTE_TYPE); - mmsColumnsPresent.add(MmsTable.QUOTE_MENTIONS); - mmsColumnsPresent.add(MmsTable.SHARED_CONTACTS); - mmsColumnsPresent.add(MmsTable.LINK_PREVIEWS); - mmsColumnsPresent.add(MmsTable.VIEW_ONCE); - mmsColumnsPresent.add(MmsTable.REACTIONS_UNREAD); - mmsColumnsPresent.add(MmsTable.REACTIONS_LAST_SEEN); - mmsColumnsPresent.add(MmsTable.REMOTE_DELETED); - mmsColumnsPresent.add(MmsTable.MENTIONS_SELF); - mmsColumnsPresent.add(MmsSmsColumns.NOTIFIED_TIMESTAMP); - mmsColumnsPresent.add(MmsSmsColumns.VIEWED_RECEIPT_COUNT); - mmsColumnsPresent.add(MmsSmsColumns.RECEIPT_TIMESTAMP); - mmsColumnsPresent.add(MmsTable.MESSAGE_RANGES); - mmsColumnsPresent.add(MmsTable.STORY_TYPE); - mmsColumnsPresent.add(MmsTable.PARENT_STORY_ID); + String mmsGroupBy = includeAttachments ? MessageTable.TABLE_NAME + "." + MessageTable.ID : null; - Set smsColumnsPresent = new HashSet<>(); - smsColumnsPresent.add(MmsSmsColumns.ID); - smsColumnsPresent.add(MmsSmsColumns.BODY); - smsColumnsPresent.add(MmsSmsColumns.RECIPIENT_ID); - smsColumnsPresent.add(MmsSmsColumns.RECIPIENT_DEVICE_ID); - smsColumnsPresent.add(MmsSmsColumns.READ); - smsColumnsPresent.add(MmsSmsColumns.THREAD_ID); - smsColumnsPresent.add(MmsSmsColumns.DELIVERY_RECEIPT_COUNT); - smsColumnsPresent.add(MmsSmsColumns.READ_RECEIPT_COUNT); - smsColumnsPresent.add(MmsSmsColumns.MISMATCHED_IDENTITIES); - smsColumnsPresent.add(MmsSmsColumns.SMS_SUBSCRIPTION_ID); - smsColumnsPresent.add(MmsSmsColumns.EXPIRES_IN); - smsColumnsPresent.add(MmsSmsColumns.EXPIRE_STARTED); - smsColumnsPresent.add(MmsSmsColumns.NOTIFIED); - smsColumnsPresent.add(SmsTable.TYPE); - smsColumnsPresent.add(SmsTable.DATE_SENT); - smsColumnsPresent.add(SmsTable.DATE_RECEIVED); - smsColumnsPresent.add(SmsTable.DATE_SERVER); - smsColumnsPresent.add(SmsTable.SMS_STATUS); - smsColumnsPresent.add(SmsTable.UNIDENTIFIED); - smsColumnsPresent.add(SmsTable.REACTIONS_UNREAD); - smsColumnsPresent.add(SmsTable.REACTIONS_LAST_SEEN); - smsColumnsPresent.add(MmsSmsColumns.REMOTE_DELETED); - smsColumnsPresent.add(MmsSmsColumns.NOTIFIED_TIMESTAMP); - smsColumnsPresent.add(MmsSmsColumns.RECEIPT_TIMESTAMP); - smsColumnsPresent.add("0 AS " + MmsTable.STORY_TYPE); - smsColumnsPresent.add("0 AS " + MmsTable.PARENT_STORY_ID); - - String mmsGroupBy = includeAttachments ? MmsTable.TABLE_NAME + "." + MmsTable.ID : null; - - String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(TRANSPORT, mmsProjection, mmsColumnsPresent, 4, MMS_TRANSPORT, selection, null, mmsGroupBy, null); - String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(TRANSPORT, smsProjection, smsColumnsPresent, 4, SMS_TRANSPORT, selection, null, null, null); - - SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder(); - String unionQuery = unionQueryBuilder.buildUnionQuery(new String[] { smsSubQuery, mmsSubQuery }, order, limit); - - SQLiteQueryBuilder outerQueryBuilder = new SQLiteQueryBuilder(); - outerQueryBuilder.setTables("(" + unionQuery + ")"); - - return outerQueryBuilder.buildQuery(projection, null, null, null, null, null, null); + return mmsQueryBuilder.buildQuery(projection, selection, null, mmsGroupBy, null, order, limit); } private Cursor queryTables(String[] projection, String selection, String order, String limit, boolean includeAttachments) { @@ -1168,25 +951,16 @@ public class MmsSmsTable extends DatabaseTable { public static class Reader implements Closeable { - private final Cursor cursor; - private SmsTable.Reader smsReader; - private MmsTable.Reader mmsReader; + private final Cursor cursor; + private MessageTable.MmsReader mmsReader; public Reader(Cursor cursor) { this.cursor = cursor; } - private SmsTable.Reader getSmsReader() { - if (smsReader == null) { - smsReader = SmsTable.readerFor(cursor); - } - - return smsReader; - } - - private MmsTable.Reader getMmsReader() { + private MessageTable.MmsReader getMmsReader() { if (mmsReader == null) { - mmsReader = MmsTable.readerFor(cursor); + mmsReader = MessageTable.mmsReaderFor(cursor); } return mmsReader; @@ -1200,11 +974,7 @@ public class MmsSmsTable extends DatabaseTable { } public MessageRecord getCurrent() { - String type = cursor.getString(cursor.getColumnIndexOrThrow(TRANSPORT)); - - if (MmsSmsTable.MMS_TRANSPORT.equals(type)) return getMmsReader().getCurrent(); - else if (MmsSmsTable.SMS_TRANSPORT.equals(type)) return getSmsReader().getCurrent(); - else throw new AssertionError("Bad type: " + type); + return getMmsReader().getCurrent(); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsTable.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsTable.java deleted file mode 100644 index ba3de9390f..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsTable.java +++ /dev/null @@ -1,3152 +0,0 @@ -/** - * Copyright (C) 2011 Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms.database; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import com.annimon.stream.Collectors; -import com.annimon.stream.Stream; -import com.google.android.mms.pdu_alt.NotificationInd; -import com.google.android.mms.pdu_alt.PduHeaders; -import com.google.protobuf.InvalidProtocolBufferException; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.signal.core.util.CursorExtensionsKt; -import org.signal.core.util.CursorUtil; -import org.signal.core.util.SQLiteDatabaseExtensionsKt; -import org.signal.core.util.SqlUtil; -import org.signal.core.util.logging.Log; -import org.signal.libsignal.protocol.util.Pair; -import org.thoughtcrime.securesms.attachments.Attachment; -import org.thoughtcrime.securesms.attachments.AttachmentId; -import org.thoughtcrime.securesms.attachments.DatabaseAttachment; -import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment; -import org.thoughtcrime.securesms.contactshare.Contact; -import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; -import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet; -import org.thoughtcrime.securesms.database.documents.NetworkFailure; -import org.thoughtcrime.securesms.database.documents.NetworkFailureSet; -import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; -import org.thoughtcrime.securesms.database.model.Mention; -import org.thoughtcrime.securesms.database.model.MessageExportStatus; -import org.thoughtcrime.securesms.database.model.MessageId; -import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord; -import org.thoughtcrime.securesms.database.model.ParentStoryId; -import org.thoughtcrime.securesms.database.model.Quote; -import org.thoughtcrime.securesms.database.model.SmsMessageRecord; -import org.thoughtcrime.securesms.database.model.StoryResult; -import org.thoughtcrime.securesms.database.model.StoryType; -import org.thoughtcrime.securesms.database.model.StoryViewState; -import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList; -import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge; -import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExportState; -import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange; -import org.thoughtcrime.securesms.jobs.TrimThreadJob; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.linkpreview.LinkPreview; -import org.thoughtcrime.securesms.mms.IncomingMediaMessage; -import org.thoughtcrime.securesms.mms.MessageGroupContext; -import org.thoughtcrime.securesms.mms.MmsException; -import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; -import org.thoughtcrime.securesms.mms.QuoteModel; -import org.thoughtcrime.securesms.mms.SlideDeck; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.revealable.ViewOnceExpirationInfo; -import org.thoughtcrime.securesms.revealable.ViewOnceUtil; -import org.thoughtcrime.securesms.sms.IncomingTextMessage; -import org.thoughtcrime.securesms.sms.OutgoingTextMessage; -import org.thoughtcrime.securesms.stories.Stories; -import org.thoughtcrime.securesms.util.Base64; -import org.thoughtcrime.securesms.util.JsonUtils; -import org.thoughtcrime.securesms.util.MediaUtil; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.util.Util; -import org.whispersystems.signalservice.api.push.ServiceId; - -import java.io.IOException; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; - -import static org.thoughtcrime.securesms.contactshare.Contact.Avatar; - -public class MmsTable extends MessageTable { - - private static final String TAG = Log.tag(MmsTable.class); - - public static final String TABLE_NAME = "mms"; - static final String MMS_CONTENT_LOCATION = "ct_l"; - static final String MMS_EXPIRY = "exp"; - public static final String MMS_MESSAGE_TYPE = "m_type"; - static final String MMS_MESSAGE_SIZE = "m_size"; - static final String MMS_STATUS = "st"; - static final String MMS_TRANSACTION_ID = "tr_id"; - static final String NETWORK_FAILURES = "network_failures"; - - static final String QUOTE_ID = "quote_id"; - static final String QUOTE_AUTHOR = "quote_author"; - static final String QUOTE_BODY = "quote_body"; - static final String QUOTE_MISSING = "quote_missing"; - static final String QUOTE_MENTIONS = "quote_mentions"; - static final String QUOTE_TYPE = "quote_type"; - - static final String SHARED_CONTACTS = "shared_contacts"; - static final String LINK_PREVIEWS = "link_previews"; - static final String MENTIONS_SELF = "mentions_self"; - static final String MESSAGE_RANGES = "message_ranges"; - - public static final String VIEW_ONCE = "view_once"; - public static final String STORY_TYPE = "story_type"; - static final String PARENT_STORY_ID = "parent_story_id"; - - public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + - DATE_SENT + " INTEGER NOT NULL, " + - DATE_RECEIVED + " INTEGER NOT NULL, " + - DATE_SERVER + " INTEGER DEFAULT -1, " + - THREAD_ID + " INTEGER NOT NULL REFERENCES " + ThreadTable.TABLE_NAME + " (" + ThreadTable.ID + ") ON DELETE CASCADE, " + - RECIPIENT_ID + " INTEGER NOT NULL REFERENCES " + RecipientTable.TABLE_NAME + " (" + RecipientTable.ID + ") ON DELETE CASCADE, " + - RECIPIENT_DEVICE_ID + " INTEGER, " + - TYPE + " INTEGER NOT NULL, " + - BODY + " TEXT, " + - READ + " INTEGER DEFAULT 0, " + - MMS_CONTENT_LOCATION + " TEXT, " + - MMS_EXPIRY + " INTEGER, " + - MMS_MESSAGE_TYPE + " INTEGER, " + - MMS_MESSAGE_SIZE + " INTEGER, " + - MMS_STATUS + " INTEGER, " + - MMS_TRANSACTION_ID + " TEXT, " + - SMS_SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " + - RECEIPT_TIMESTAMP + " INTEGER DEFAULT -1, " + - DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + - READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + - VIEWED_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + - MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " + - NETWORK_FAILURES + " TEXT DEFAULT NULL," + - EXPIRES_IN + " INTEGER DEFAULT 0, " + - EXPIRE_STARTED + " INTEGER DEFAULT 0, " + - NOTIFIED + " INTEGER DEFAULT 0, " + - QUOTE_ID + " INTEGER DEFAULT 0, " + - QUOTE_AUTHOR + " INTEGER DEFAULT 0, " + - QUOTE_BODY + " TEXT DEFAULT NULL, " + - QUOTE_MISSING + " INTEGER DEFAULT 0, " + - QUOTE_MENTIONS + " BLOB DEFAULT NULL," + - QUOTE_TYPE + " INTEGER DEFAULT 0," + - SHARED_CONTACTS + " TEXT DEFAULT NULL, " + - UNIDENTIFIED + " INTEGER DEFAULT 0, " + - LINK_PREVIEWS + " TEXT DEFAULT NULL, " + - VIEW_ONCE + " INTEGER DEFAULT 0, " + - REACTIONS_UNREAD + " INTEGER DEFAULT 0, " + - REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1, " + - REMOTE_DELETED + " INTEGER DEFAULT 0, " + - MENTIONS_SELF + " INTEGER DEFAULT 0, " + - NOTIFIED_TIMESTAMP + " INTEGER DEFAULT 0, " + - SERVER_GUID + " TEXT DEFAULT NULL, " + - MESSAGE_RANGES + " BLOB DEFAULT NULL, " + - STORY_TYPE + " INTEGER DEFAULT 0, " + - PARENT_STORY_ID + " INTEGER DEFAULT 0, " + - EXPORT_STATE + " BLOB DEFAULT NULL, " + - EXPORTED + " INTEGER DEFAULT 0);"; - - public static final String[] CREATE_INDEXS = { - "CREATE INDEX IF NOT EXISTS mms_read_and_notified_and_thread_id_index ON " + TABLE_NAME + "(" + READ + "," + NOTIFIED + "," + THREAD_ID + ");", - "CREATE INDEX IF NOT EXISTS mms_type_index ON " + TABLE_NAME + " (" + TYPE + ");", - "CREATE INDEX IF NOT EXISTS mms_date_sent_index ON " + TABLE_NAME + " (" + DATE_SENT + ", " + RECIPIENT_ID + ", " + THREAD_ID + ");", - "CREATE INDEX IF NOT EXISTS mms_date_server_index ON " + TABLE_NAME + " (" + DATE_SERVER + ");", - "CREATE INDEX IF NOT EXISTS mms_thread_date_index ON " + TABLE_NAME + " (" + THREAD_ID + ", " + DATE_RECEIVED + ");", - "CREATE INDEX IF NOT EXISTS mms_reactions_unread_index ON " + TABLE_NAME + " (" + REACTIONS_UNREAD + ");", - "CREATE INDEX IF NOT EXISTS mms_story_type_index ON " + TABLE_NAME + " (" + STORY_TYPE + ");", - "CREATE INDEX IF NOT EXISTS mms_parent_story_id_index ON " + TABLE_NAME + " (" + PARENT_STORY_ID + ");", - "CREATE INDEX IF NOT EXISTS mms_thread_story_parent_story_index ON " + TABLE_NAME + " (" + THREAD_ID + ", " + DATE_RECEIVED + "," + STORY_TYPE + "," + PARENT_STORY_ID + ");", - "CREATE INDEX IF NOT EXISTS mms_quote_id_quote_author_index ON " + TABLE_NAME + "(" + QUOTE_ID + ", " + QUOTE_AUTHOR + ");", - "CREATE INDEX IF NOT EXISTS mms_exported_index ON " + TABLE_NAME + " (" + EXPORTED + ");", - "CREATE INDEX IF NOT EXISTS mms_id_type_payment_transactions_index ON " + TABLE_NAME + " (" + ID + "," + TYPE + ") WHERE " + TYPE + " & " + Types.SPECIAL_TYPE_PAYMENTS_NOTIFICATION + " != 0;" - }; - - private static final String[] MMS_PROJECTION = new String[] { - MmsTable.TABLE_NAME + "." + ID + " AS " + ID, - THREAD_ID, - DATE_SENT, - DATE_RECEIVED, - DATE_SERVER, - TYPE, - READ, - MMS_CONTENT_LOCATION, - MMS_EXPIRY, - MMS_MESSAGE_TYPE, - MMS_MESSAGE_SIZE, - MMS_STATUS, - MMS_TRANSACTION_ID, - BODY, - RECIPIENT_ID, - RECIPIENT_DEVICE_ID, - DELIVERY_RECEIPT_COUNT, - READ_RECEIPT_COUNT, - MISMATCHED_IDENTITIES, - NETWORK_FAILURES, - SMS_SUBSCRIPTION_ID, - EXPIRES_IN, - EXPIRE_STARTED, - NOTIFIED, - QUOTE_ID, - QUOTE_AUTHOR, - QUOTE_BODY, - QUOTE_TYPE, - QUOTE_MISSING, - QUOTE_MENTIONS, - SHARED_CONTACTS, - LINK_PREVIEWS, - UNIDENTIFIED, - VIEW_ONCE, - REACTIONS_UNREAD, - REACTIONS_LAST_SEEN, - REMOTE_DELETED, - MENTIONS_SELF, - NOTIFIED_TIMESTAMP, - VIEWED_RECEIPT_COUNT, - RECEIPT_TIMESTAMP, - MESSAGE_RANGES, - STORY_TYPE, - PARENT_STORY_ID, - "json_group_array(json_object(" + - "'" + AttachmentTable.ROW_ID + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.ROW_ID + ", " + - "'" + AttachmentTable.UNIQUE_ID + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.UNIQUE_ID + ", " + - "'" + AttachmentTable.MMS_ID + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.MMS_ID + ", " + - "'" + AttachmentTable.SIZE + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.SIZE + ", " + - "'" + AttachmentTable.FILE_NAME + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.FILE_NAME + ", " + - "'" + AttachmentTable.DATA + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.DATA + ", " + - "'" + AttachmentTable.CONTENT_TYPE + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.CONTENT_TYPE + ", " + - "'" + AttachmentTable.CDN_NUMBER + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.CDN_NUMBER + ", " + - "'" + AttachmentTable.CONTENT_LOCATION + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.CONTENT_LOCATION + ", " + - "'" + AttachmentTable.FAST_PREFLIGHT_ID + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.FAST_PREFLIGHT_ID + "," + - "'" + AttachmentTable.VOICE_NOTE + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.VOICE_NOTE + "," + - "'" + AttachmentTable.BORDERLESS + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.BORDERLESS + "," + - "'" + AttachmentTable.VIDEO_GIF + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.VIDEO_GIF + "," + - "'" + AttachmentTable.WIDTH + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.WIDTH + "," + - "'" + AttachmentTable.HEIGHT + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.HEIGHT + "," + - "'" + AttachmentTable.QUOTE + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.QUOTE + ", " + - "'" + AttachmentTable.CONTENT_DISPOSITION + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.CONTENT_DISPOSITION + ", " + - "'" + AttachmentTable.NAME + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.NAME + ", " + - "'" + AttachmentTable.TRANSFER_STATE + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.TRANSFER_STATE + ", " + - "'" + AttachmentTable.CAPTION + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.CAPTION + ", " + - "'" + AttachmentTable.STICKER_PACK_ID + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.STICKER_PACK_ID + ", " + - "'" + AttachmentTable.STICKER_PACK_KEY + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.STICKER_PACK_KEY + ", " + - "'" + AttachmentTable.STICKER_ID + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.STICKER_ID + ", " + - "'" + AttachmentTable.STICKER_EMOJI + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.STICKER_EMOJI + ", " + - "'" + AttachmentTable.VISUAL_HASH + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.VISUAL_HASH + ", " + - "'" + AttachmentTable.TRANSFORM_PROPERTIES + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.TRANSFORM_PROPERTIES + ", " + - "'" + AttachmentTable.DISPLAY_ORDER + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.DISPLAY_ORDER + ", " + - "'" + AttachmentTable.UPLOAD_TIMESTAMP + "', " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.UPLOAD_TIMESTAMP + - ")) AS " + AttachmentTable.ATTACHMENT_JSON_ALIAS, - }; - - private static final String IS_STORY_CLAUSE = STORY_TYPE + " > 0 AND " + REMOTE_DELETED + " = 0"; - - private static final String RAW_ID_WHERE = TABLE_NAME + "._id = ?"; - - private final EarlyReceiptCache earlyDeliveryReceiptCache = new EarlyReceiptCache("MmsDelivery"); - - public MmsTable(Context context, SignalDatabase databaseHelper) { - super(context, databaseHelper); - } - - @Override - protected String getTableName() { - return TABLE_NAME; - } - - @Override - protected String getDateSentColumnName() { - return DATE_SENT; - } - - @Override - protected String getDateReceivedColumnName() { - return DATE_RECEIVED; - } - - @Override - protected String getTypeField() { - return TYPE; - } - - @Override - public @Nullable RecipientId getOldestGroupUpdateSender(long threadId, long minimumDateReceived) { - throw new UnsupportedOperationException(); - } - - @Override - public Cursor getExpirationStartedMessages() { - String where = EXPIRE_STARTED + " > 0"; - return rawQuery(where, null); - } - - @Override - public SmsMessageRecord getSmsMessage(long messageId) { - throw new UnsupportedOperationException(); - } - - @Override - public Cursor getMessageCursor(long messageId) { - return internalGetMessage(messageId); - } - - @Override - public boolean hasReceivedAnyCallsSince(long threadId, long timestamp) { - throw new UnsupportedOperationException(); - } - - @Override - public void markAsEndSession(long id) { - throw new UnsupportedOperationException(); - } - - @Override - public void markAsInvalidVersionKeyExchange(long id) { - throw new UnsupportedOperationException(); - } - - @Override - public void markAsSecure(long id) { - throw new UnsupportedOperationException(); - } - - @Override - public void markAsPush(long id) { - throw new UnsupportedOperationException(); - } - - @Override - public void markAsDecryptFailed(long id) { - throw new UnsupportedOperationException(); - } - - @Override - public void markAsNoSession(long id) { - throw new UnsupportedOperationException(); - } - - @Override - public void markAsUnsupportedProtocolVersion(long id) { - throw new UnsupportedOperationException(); - } - - @Override - public void markAsInvalidMessage(long id) { - throw new UnsupportedOperationException(); - } - - @Override - public void markAsLegacyVersion(long id) { - throw new UnsupportedOperationException(); - } - - @Override - public void markAsMissedCall(long id, boolean isVideoOffer) { - throw new UnsupportedOperationException(); - } - - @Override - public void markSmsStatus(long id, int status) { - throw new UnsupportedOperationException(); - } - - @Override - public InsertResult updateBundleMessageBody(long messageId, String body) { - throw new UnsupportedOperationException(); - } - - @Override - public @NonNull List getViewedIncomingMessages(long threadId) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String[] columns = new String[]{ ID, RECIPIENT_ID, DATE_SENT, TYPE, THREAD_ID, STORY_TYPE}; - String where = THREAD_ID + " = ? AND " + VIEWED_RECEIPT_COUNT + " > 0 AND " + TYPE + " & " + Types.BASE_INBOX_TYPE + " = " + Types.BASE_INBOX_TYPE; - String[] args = SqlUtil.buildArgs(threadId); - - - try (Cursor cursor = db.query(getTableName(), columns, where, args, null, null, null, null)) { - if (cursor == null) { - return Collections.emptyList(); - } - - List results = new ArrayList<>(cursor.getCount()); - while (cursor.moveToNext()) { - long messageId = CursorUtil.requireLong(cursor, ID); - RecipientId recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); - long dateSent = CursorUtil.requireLong(cursor, DATE_SENT); - SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent); - StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE)); - - results.add(new MarkedMessageInfo(threadId, syncMessageId, new MessageId(messageId, true), null)); - } - - return results; - } - } - - @Override - public @Nullable MarkedMessageInfo setIncomingMessageViewed(long messageId) { - List results = setIncomingMessagesViewed(Collections.singletonList(messageId)); - - if (results.isEmpty()) { - return null; - } else { - return results.get(0); - } - } - - @Override - public @NonNull List setIncomingMessagesViewed(@NonNull List messageIds) { - if (messageIds.isEmpty()) { - return Collections.emptyList(); - } - - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - String[] columns = new String[]{ ID, RECIPIENT_ID, DATE_SENT, TYPE, THREAD_ID, STORY_TYPE}; - String where = ID + " IN (" + Util.join(messageIds, ",") + ") AND " + VIEWED_RECEIPT_COUNT + " = 0"; - List results = new LinkedList<>(); - - database.beginTransaction(); - try (Cursor cursor = database.query(TABLE_NAME, columns, where, null, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - long type = CursorUtil.requireLong(cursor, TYPE); - if (Types.isSecureType(type) && Types.isInboxType(type)) { - long messageId = CursorUtil.requireLong(cursor, ID); - long threadId = CursorUtil.requireLong(cursor, THREAD_ID); - RecipientId recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); - long dateSent = CursorUtil.requireLong(cursor, DATE_SENT); - SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent); - - results.add(new MarkedMessageInfo(threadId, syncMessageId, new MessageId(messageId, true), null)); - - ContentValues contentValues = new ContentValues(); - contentValues.put(VIEWED_RECEIPT_COUNT, 1); - contentValues.put(RECEIPT_TIMESTAMP, System.currentTimeMillis()); - - database.update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(CursorUtil.requireLong(cursor, ID))); - } - } - database.setTransactionSuccessful(); - } finally { - database.endTransaction(); - } - - Set threadsUpdated = Stream.of(results) - .map(MarkedMessageInfo::getThreadId) - .collect(Collectors.toSet()); - - notifyConversationListeners(threadsUpdated); - notifyConversationListListeners(); - - return results; - } - - @Override - public @NonNull List - setOutgoingGiftsRevealed(@NonNull List messageIds) { - String[] projection = SqlUtil.buildArgs(ID, RECIPIENT_ID, DATE_SENT, THREAD_ID, STORY_TYPE); - String where = ID + " IN (" + Util.join(messageIds, ",") + ") AND (" + getOutgoingTypeClause() + ") AND (" + getTypeField() + " & " + Types.SPECIAL_TYPES_MASK + " = " + Types.SPECIAL_TYPE_GIFT_BADGE + ") AND " + VIEWED_RECEIPT_COUNT + " = 0"; - List results = new LinkedList<>(); - - getWritableDatabase().beginTransaction(); - try (Cursor cursor = getWritableDatabase().query(TABLE_NAME, projection, where, null, null, null, null)) { - while (cursor.moveToNext()) { - long messageId = CursorUtil.requireLong(cursor, ID); - long threadId = CursorUtil.requireLong(cursor, THREAD_ID); - RecipientId recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); - long dateSent = CursorUtil.requireLong(cursor, DATE_SENT); - SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent); - StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE)); - - results.add(new MarkedMessageInfo(threadId, syncMessageId, new MessageId(messageId, true), null)); - - ContentValues contentValues = new ContentValues(); - contentValues.put(VIEWED_RECEIPT_COUNT, 1); - contentValues.put(RECEIPT_TIMESTAMP, System.currentTimeMillis()); - - getWritableDatabase().update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(messageId)); - } - getWritableDatabase().setTransactionSuccessful(); - } finally { - getWritableDatabase().endTransaction(); - } - - Set threadsUpdated = Stream.of(results) - .map(MarkedMessageInfo::getThreadId) - .collect(Collectors.toSet()); - - notifyConversationListeners(threadsUpdated); - - return results; - } - - @Override - public @NonNull Pair insertReceivedCall(@NonNull RecipientId address, boolean isVideoOffer) { - throw new UnsupportedOperationException(); - } - - @Override - public @NonNull Pair insertOutgoingCall(@NonNull RecipientId address, boolean isVideoOffer) { - throw new UnsupportedOperationException(); - } - - @Override - public @NonNull Pair insertMissedCall(@NonNull RecipientId address, long timestamp, boolean isVideoOffer) { - throw new UnsupportedOperationException(); - } - - @Override - public void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId, - @NonNull RecipientId sender, - long timestamp, - @Nullable String peekGroupCallEraId, - @NonNull Collection peekJoinedUuids, - boolean isCallFull) - { - throw new UnsupportedOperationException(); - } - - @Override - public void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId, - @NonNull RecipientId sender, - long timestamp, - @Nullable String messageGroupCallEraId) - { - throw new UnsupportedOperationException(); - } - - @Override - public boolean updatePreviousGroupCall(long threadId, @Nullable String peekGroupCallEraId, @NonNull Collection peekJoinedUuids, boolean isCallFull) { - throw new UnsupportedOperationException(); - } - - @Override - public Optional insertMessageInbox(IncomingTextMessage message, long type) { - throw new UnsupportedOperationException(); - } - - @Override - public Optional insertMessageInbox(IncomingTextMessage message) { - throw new UnsupportedOperationException(); - } - - @Override - public long insertMessageOutbox(long threadId, OutgoingTextMessage message, boolean forceSms, long date, InsertListener insertListener) { - throw new UnsupportedOperationException(); - } - - @Override - public void insertProfileNameChangeMessages(@NonNull Recipient recipient, @NonNull String newProfileName, @NonNull String previousProfileName) { - throw new UnsupportedOperationException(); - } - - @Override - public void insertGroupV1MigrationEvents(@NonNull RecipientId recipientId, - long threadId, - @NonNull GroupMigrationMembershipChange membershipChange) - { - throw new UnsupportedOperationException(); - } - - @Override - public void insertNumberChangeMessages(@NonNull RecipientId recipientId) { - throw new UnsupportedOperationException(); - } - - @Override - public void insertBoostRequestMessage(@NonNull RecipientId recipientId, long threadId) { - throw new UnsupportedOperationException(); - } - - @Override - public void insertThreadMergeEvent(@NonNull RecipientId recipientId, long threadId, @NonNull ThreadMergeEvent event) { - throw new UnsupportedOperationException(); - } - - @Override - public void insertSmsExportMessage(@NonNull RecipientId recipientId, long threadId) { - throw new UnsupportedOperationException(); - } - - @Override - public void endTransaction(SQLiteDatabase database) { - database.endTransaction(); - } - - @Override - public void ensureMigration() { - databaseHelper.getSignalWritableDatabase(); - } - - @Override - public boolean isStory(long messageId) { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - String[] projection = new String[]{"1"}; - String where = IS_STORY_CLAUSE + " AND " + ID + " = ?"; - String[] whereArgs = SqlUtil.buildArgs(messageId); - - try (Cursor cursor = database.query(TABLE_NAME, projection, where, whereArgs, null, null, null, "1")) { - return cursor != null && cursor.moveToFirst(); - } - } - - @Override - public @NonNull MessageTable.Reader getOutgoingStoriesTo(@NonNull RecipientId recipientId) { - Recipient recipient = Recipient.resolved(recipientId); - Long threadId = null; - - if (recipient.isGroup()) { - threadId = SignalDatabase.threads().getThreadIdFor(recipientId); - } - - String where = IS_STORY_CLAUSE + " AND (" + getOutgoingTypeClause() + ")"; - - final String[] whereArgs; - if (threadId == null) { - where += " AND " + RECIPIENT_ID + " = ?"; - whereArgs = SqlUtil.buildArgs(recipientId); - } else { - where += " AND " + THREAD_ID_WHERE; - whereArgs = SqlUtil.buildArgs(threadId); - } - - return new Reader(rawQuery(where, whereArgs)); - } - - @Override - public @NonNull MessageTable.Reader getAllOutgoingStories(boolean reverse, int limit) { - String where = IS_STORY_CLAUSE + " AND (" + getOutgoingTypeClause() + ")"; - - return new Reader(rawQuery(where, null, reverse, limit)); - } - - @Override - public @NonNull MessageTable.Reader getAllOutgoingStoriesAt(long sentTimestamp) { - String where = IS_STORY_CLAUSE + " AND " + DATE_SENT + " = ? AND (" + getOutgoingTypeClause() + ")"; - String[] whereArgs = SqlUtil.buildArgs(sentTimestamp); - Cursor cursor = rawQuery(where, whereArgs, false, -1L); - - return new Reader(cursor); - } - - @Override - public @NonNull List markAllIncomingStoriesRead() { - String where = IS_STORY_CLAUSE + " AND NOT (" + getOutgoingTypeClause() + ") AND " + READ + " = 0"; - - List markedMessageInfos = setMessagesRead(where, null); - notifyConversationListListeners(); - - return markedMessageInfos; - } - - @Override - public void markOnboardingStoryRead() { - RecipientId recipientId = SignalStore.releaseChannelValues().getReleaseChannelRecipientId(); - if (recipientId == null) { - return; - } - - String where = IS_STORY_CLAUSE + " AND NOT (" + getOutgoingTypeClause() + ") AND " + READ + " = 0 AND " + RECIPIENT_ID + " = ?"; - - List markedMessageInfos = setMessagesRead(where, SqlUtil.buildArgs(recipientId)); - if (!markedMessageInfos.isEmpty()) { - notifyConversationListListeners(); - } - } - - @Override - public @NonNull MessageTable.Reader getAllStoriesFor(@NonNull RecipientId recipientId, int limit) { - long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId); - String where = IS_STORY_CLAUSE + " AND " + THREAD_ID_WHERE; - String[] whereArgs = SqlUtil.buildArgs(threadId); - Cursor cursor = rawQuery(where, whereArgs, false, limit); - - return new Reader(cursor); - } - - @Override - public @NonNull MessageTable.Reader getUnreadStories(@NonNull RecipientId recipientId, int limit) { - final long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId); - final String query = IS_STORY_CLAUSE + - " AND NOT (" + getOutgoingTypeClause() + ") " + - " AND " + THREAD_ID_WHERE + - " AND " + VIEWED_RECEIPT_COUNT + " = ?"; - final String[] args = SqlUtil.buildArgs(threadId, 0); - - return new Reader(rawQuery(query, args, false, limit)); - } - - @Override - public @Nullable ParentStoryId.GroupReply getParentStoryIdForGroupReply(long messageId) { - String[] projection = SqlUtil.buildArgs(PARENT_STORY_ID); - String[] args = SqlUtil.buildArgs(messageId); - - try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, projection, ID_WHERE, args, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - ParentStoryId parentStoryId = ParentStoryId.deserialize(CursorUtil.requireLong(cursor, PARENT_STORY_ID)); - if (parentStoryId != null && parentStoryId.isGroupReply()) { - return (ParentStoryId.GroupReply) parentStoryId; - } else { - return null; - } - } - } - - return null; - } - - @Override - public @NonNull StoryViewState getStoryViewState(@NonNull RecipientId recipientId) { - if (!Stories.isFeatureEnabled()) { - return StoryViewState.NONE; - } - - long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId); - - return getStoryViewState(threadId); - } - - /** - * Synchronizes whether we've viewed a recipient's story based on incoming sync messages. - */ - public void updateViewedStories(@NonNull Set syncMessageIds) { - final String timestamps = Util.join(syncMessageIds.stream().map(SyncMessageId::getTimetamp).collect(java.util.stream.Collectors.toList()), ","); - final String[] projection = SqlUtil.buildArgs(RECIPIENT_ID); - final String where = IS_STORY_CLAUSE + " AND " + DATE_SENT + " IN (" + timestamps + ") AND NOT (" + getOutgoingTypeClause() + ") AND " + VIEWED_RECEIPT_COUNT + " > 0"; - - try { - getWritableDatabase().beginTransaction(); - try (Cursor cursor = getWritableDatabase().query(TABLE_NAME, projection, where, null, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - Recipient recipient = Recipient.resolved(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID))); - SignalDatabase.recipients().updateLastStoryViewTimestamp(recipient.getId()); - } - } - getWritableDatabase().setTransactionSuccessful(); - } finally { - getWritableDatabase().endTransaction(); - } - } - - @VisibleForTesting - @NonNull StoryViewState getStoryViewState(long threadId) { - final String hasStoryQuery = "SELECT EXISTS(SELECT 1 FROM " + TABLE_NAME + " WHERE " + IS_STORY_CLAUSE + " AND " + THREAD_ID_WHERE + " LIMIT 1)"; - final String[] hasStoryArgs = SqlUtil.buildArgs(threadId); - final boolean hasStories; - - try (Cursor cursor = getReadableDatabase().rawQuery(hasStoryQuery, hasStoryArgs)) { - hasStories = cursor != null && cursor.moveToFirst() && !cursor.isNull(0) && cursor.getInt(0) == 1; - } - - if (!hasStories) { - return StoryViewState.NONE; - } - - final String hasUnviewedStoriesQuery = "SELECT EXISTS(SELECT 1 FROM " + TABLE_NAME + " WHERE " + IS_STORY_CLAUSE + " AND " + THREAD_ID_WHERE + " AND " + VIEWED_RECEIPT_COUNT + " = ? " + "AND NOT (" + getOutgoingTypeClause() + ") LIMIT 1)"; - final String[] hasUnviewedStoriesArgs = SqlUtil.buildArgs(threadId, 0); - final boolean hasUnviewedStories; - - try (Cursor cursor = getReadableDatabase().rawQuery(hasUnviewedStoriesQuery, hasUnviewedStoriesArgs)) { - hasUnviewedStories = cursor != null && cursor.moveToFirst() && !cursor.isNull(0) && cursor.getInt(0) == 1; - } - - if (hasUnviewedStories) { - return StoryViewState.UNVIEWED; - } else { - return StoryViewState.VIEWED; - } - } - - @Override - public boolean isOutgoingStoryAlreadyInDatabase(@NonNull RecipientId recipientId, long sentTimestamp) { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - String[] projection = new String[]{"COUNT(*)"}; - String where = RECIPIENT_ID + " = ? AND " + STORY_TYPE + " > 0 AND " + DATE_SENT + " = ? AND (" + getOutgoingTypeClause() + ")"; - String[] whereArgs = SqlUtil.buildArgs(recipientId, sentTimestamp); - - try (Cursor cursor = database.query(TABLE_NAME, projection, where, whereArgs, null, null, null, "1")) { - if (cursor != null && cursor.moveToFirst()) { - return cursor.getInt(0) > 0; - } - } - - return false; - } - - @Override - public @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - String[] projection = new String[]{ID, RECIPIENT_ID}; - String where = IS_STORY_CLAUSE + " AND " + DATE_SENT + " = ?"; - String[] whereArgs = SqlUtil.buildArgs(sentTimestamp); - - try (Cursor cursor = database.query(TABLE_NAME, projection, where, whereArgs, null, null, null, "1")) { - if (cursor != null && cursor.moveToFirst()) { - RecipientId rowRecipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))); - - if (Recipient.self().getId().equals(authorId) || rowRecipientId.equals(authorId)) { - return new MessageId(CursorUtil.requireLong(cursor, ID), true); - } - } - } - - throw new NoSuchMessageException("No story sent at " + sentTimestamp); - } - - @Override - public @NonNull List getUnreadStoryThreadRecipientIds() { - SQLiteDatabase db = getReadableDatabase(); - String query = "SELECT DISTINCT " + ThreadTable.TABLE_NAME + "." + ThreadTable.RECIPIENT_ID + "\n" - + "FROM " + TABLE_NAME + "\n" - + "JOIN " + ThreadTable.TABLE_NAME + "\n" - + "ON " + TABLE_NAME + "." + THREAD_ID + " = " + ThreadTable.TABLE_NAME + "." + ThreadTable.ID + "\n" - + "WHERE " + IS_STORY_CLAUSE + " AND (" + getOutgoingTypeClause() + ") = 0 AND " + VIEWED_RECEIPT_COUNT + " = 0 AND " + TABLE_NAME + "." + READ + " = 0"; - - try (Cursor cursor = db.rawQuery(query, null)) { - if (cursor != null) { - List recipientIds = new ArrayList<>(cursor.getCount()); - while (cursor.moveToNext()) { - recipientIds.add(RecipientId.from(cursor.getLong(0))); - } - - return recipientIds; - } - } - - return Collections.emptyList(); - } - - @Override - public @NonNull List getOrderedStoryRecipientsAndIds(boolean isOutgoingOnly) { - String where = "WHERE " + STORY_TYPE + " > 0 AND " + REMOTE_DELETED + " = 0" + (isOutgoingOnly ? " AND is_outgoing != 0" : "") + "\n"; - SQLiteDatabase db = getReadableDatabase(); - String query = "SELECT\n" - + " " + TABLE_NAME + "." + DATE_SENT + " AS sent_timestamp,\n" - + " " + TABLE_NAME + "." + ID + " AS mms_id,\n" - + " " + ThreadTable.TABLE_NAME + "." + ThreadTable.RECIPIENT_ID + ",\n" - + " (" + getOutgoingTypeClause() + ") AS is_outgoing,\n" - + " " + VIEWED_RECEIPT_COUNT + ",\n" - + " " + TABLE_NAME + "." + DATE_SENT + ",\n" - + " " + RECEIPT_TIMESTAMP + ",\n" - + " (" + getOutgoingTypeClause() + ") = 0 AND " + VIEWED_RECEIPT_COUNT + " = 0 AS is_unread\n" - + "FROM " + TABLE_NAME + "\n" - + "JOIN " + ThreadTable.TABLE_NAME + "\n" - + "ON " + TABLE_NAME + "." + THREAD_ID + " = " + ThreadTable.TABLE_NAME + "." + ThreadTable.ID + "\n" - + where - + "ORDER BY\n" - + "is_unread DESC,\n" - + "CASE\n" - + "WHEN is_outgoing = 0 AND " + VIEWED_RECEIPT_COUNT + " = 0 THEN " + MmsTable.TABLE_NAME + "." + MmsTable.DATE_SENT + "\n" - + "WHEN is_outgoing = 0 AND viewed_receipt_count > 0 THEN " + MmsTable.RECEIPT_TIMESTAMP + "\n" - + "WHEN is_outgoing = 1 THEN " + MmsTable.TABLE_NAME + "." + MmsTable.DATE_SENT + "\n" - + "END DESC"; - - List results; - try (Cursor cursor = db.rawQuery(query, null)) { - if (cursor != null) { - results = new ArrayList<>(cursor.getCount()); - - while (cursor.moveToNext()) { - results.add(new StoryResult(RecipientId.from(CursorUtil.requireLong(cursor, ThreadTable.RECIPIENT_ID)), - CursorUtil.requireLong(cursor, "mms_id"), - CursorUtil.requireLong(cursor, "sent_timestamp"), - CursorUtil.requireBoolean(cursor, "is_outgoing"))); - } - - return results; - } - } - - return Collections.emptyList(); - } - - @Override - public @NonNull Cursor getStoryReplies(long parentStoryId) { - String where = PARENT_STORY_ID + " = ?"; - String[] whereArgs = SqlUtil.buildArgs(parentStoryId); - - return rawQuery(where, whereArgs, false, 0); - } - - @Override - public int getNumberOfStoryReplies(long parentStoryId) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String[] columns = new String[]{"COUNT(*)"}; - String where = PARENT_STORY_ID + " = ?"; - String[] whereArgs = SqlUtil.buildArgs(parentStoryId); - - try (Cursor cursor = db.query(TABLE_NAME, columns, where, whereArgs, null, null, null, null)) { - return cursor != null && cursor.moveToNext() ? cursor.getInt(0) : 0; - } - } - - @Override - public boolean containsStories(long threadId) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String[] columns = new String[]{"1"}; - String where = THREAD_ID_WHERE + " AND " + STORY_TYPE + " > 0"; - String[] whereArgs = SqlUtil.buildArgs(threadId); - - try (Cursor cursor = db.query(TABLE_NAME, columns, where, whereArgs, null, null, null, "1")) { - return cursor != null && cursor.moveToNext(); - } - } - - @Override - public boolean hasSelfReplyInStory(long parentStoryId) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String[] columns = new String[]{"COUNT(*)"}; - String where = PARENT_STORY_ID + " = ? AND (" + getOutgoingTypeClause() + ")"; - String[] whereArgs = SqlUtil.buildArgs(-parentStoryId); - - try (Cursor cursor = db.query(TABLE_NAME, columns, where, whereArgs, null, null, null, null)) { - return cursor != null && cursor.moveToNext() && cursor.getInt(0) > 0; - } - } - - @Override - public boolean hasGroupReplyOrReactionInStory(long parentStoryId) { - return hasSelfReplyInStory(-parentStoryId); - } - - @Override - public @Nullable Long getOldestStorySendTimestamp(boolean hasSeenReleaseChannelStories) { - long releaseChannelThreadId = getReleaseChannelThreadId(hasSeenReleaseChannelStories); - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String[] columns = new String[] { DATE_SENT }; - String where = IS_STORY_CLAUSE + " AND " + THREAD_ID + " != ?"; - String orderBy = DATE_SENT + " ASC"; - String limit = "1"; - - try (Cursor cursor = db.query(TABLE_NAME, columns, where, SqlUtil.buildArgs(releaseChannelThreadId), null, null, orderBy, limit)) { - return cursor != null && cursor.moveToNext() ? cursor.getLong(0) : null; - } - } - - private static long getReleaseChannelThreadId(boolean hasSeenReleaseChannelStories) { - if (hasSeenReleaseChannelStories) { - return -1L; - } - - RecipientId releaseChannelRecipientId = SignalStore.releaseChannelValues().getReleaseChannelRecipientId(); - if (releaseChannelRecipientId == null) { - return -1L; - } - - Long releaseChannelThreadId = SignalDatabase.threads().getThreadIdFor(releaseChannelRecipientId); - if (releaseChannelThreadId == null) { - return -1L; - } - - return releaseChannelThreadId; - } - - @Override - public void deleteGroupStoryReplies(long parentStoryId) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - String[] args = SqlUtil.buildArgs(parentStoryId); - - db.delete(TABLE_NAME, PARENT_STORY_ID + " = ?", args); - } - - @Override - public int deleteStoriesOlderThan(long timestamp, boolean hasSeenReleaseChannelStories) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - db.beginTransaction(); - try { - long releaseChannelThreadId = getReleaseChannelThreadId(hasSeenReleaseChannelStories); - String storiesBeforeTimestampWhere = IS_STORY_CLAUSE + " AND " + DATE_SENT + " < ? AND " + THREAD_ID + " != ?"; - String[] sharedArgs = SqlUtil.buildArgs(timestamp, releaseChannelThreadId); - String deleteStoryRepliesQuery = "DELETE FROM " + TABLE_NAME + " " + - "WHERE " + PARENT_STORY_ID + " > 0 AND " + PARENT_STORY_ID + " IN (" + - "SELECT " + ID + " " + - "FROM " + TABLE_NAME + " " + - "WHERE " + storiesBeforeTimestampWhere + - ")"; - String disassociateQuoteQuery = "UPDATE " + TABLE_NAME + " " + - "SET " + QUOTE_MISSING + " = 1, " + QUOTE_BODY + " = '' " + - "WHERE " + PARENT_STORY_ID + " < 0 AND ABS(" + PARENT_STORY_ID + ") IN (" + - "SELECT " + ID + " " + - "FROM " + TABLE_NAME + " " + - "WHERE " + storiesBeforeTimestampWhere + - ")"; - - db.execSQL(deleteStoryRepliesQuery, sharedArgs); - db.execSQL(disassociateQuoteQuery, sharedArgs); - - try (Cursor cursor = db.query(TABLE_NAME, new String[]{RECIPIENT_ID}, storiesBeforeTimestampWhere, sharedArgs, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))); - ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(recipientId); - } - } - - int deletedStoryCount; - try (Cursor cursor = db.query(TABLE_NAME, new String[]{ID}, storiesBeforeTimestampWhere, sharedArgs, null, null, null)) { - deletedStoryCount = cursor.getCount(); - - while (cursor.moveToNext()) { - long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID)); - deleteMessage(id); - } - } - - db.setTransactionSuccessful(); - return deletedStoryCount; - } finally { - db.endTransaction(); - } - } - - private void disassociateStoryQuotes(long storyId) { - ContentValues contentValues = new ContentValues(2); - contentValues.put(QUOTE_MISSING, 1); - contentValues.putNull(QUOTE_BODY); - - getWritableDatabase().update(TABLE_NAME, - contentValues, - PARENT_STORY_ID + " = ?", - SqlUtil.buildArgs(new ParentStoryId.DirectReply(storyId).serialize())); - } - - @Override - public boolean isGroupQuitMessage(long messageId) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - - String[] columns = new String[]{ID}; - long type = Types.getOutgoingEncryptedMessageType() | Types.GROUP_LEAVE_BIT; - String query = ID + " = ? AND " + TYPE + " & " + type + " = " + type + " AND " + TYPE + " & " + Types.GROUP_V2_BIT + " = 0"; - String[] args = SqlUtil.buildArgs(messageId); - - try (Cursor cursor = db.query(TABLE_NAME, columns, query, args, null, null, null, null)) { - if (cursor.getCount() == 1) { - return true; - } - } - - return false; - } - - @Override - public long getLatestGroupQuitTimestamp(long threadId, long quitTimeBarrier) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - - String[] columns = new String[]{DATE_SENT}; - long type = Types.getOutgoingEncryptedMessageType() | Types.GROUP_LEAVE_BIT; - String query = THREAD_ID + " = ? AND " + TYPE + " & " + type + " = " + type + " AND " + TYPE + " & " + Types.GROUP_V2_BIT + " = 0 AND " + DATE_SENT + " < ?"; - String[] args = new String[]{String.valueOf(threadId), String.valueOf(quitTimeBarrier)}; - String orderBy = DATE_SENT + " DESC"; - String limit = "1"; - - try (Cursor cursor = db.query(TABLE_NAME, columns, query, args, null, null, orderBy, limit)) { - if (cursor.moveToFirst()) { - return CursorUtil.requireLong(cursor, DATE_SENT); - } - } - - return -1; - } - - @Override - public int getMessageCountForThread(long threadId) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - - String query = THREAD_ID + " = ? AND " + STORY_TYPE + " = ? AND " + PARENT_STORY_ID + " <= ?"; - String[] args = SqlUtil.buildArgs(threadId, 0, 0); - - try (Cursor cursor = db.query(TABLE_NAME, COUNT, query, args, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - return cursor.getInt(0); - } - } - - return 0; - } - - @Override - public int getMessageCountForThread(long threadId, long beforeTime) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - - String query = THREAD_ID + " = ? AND " + DATE_RECEIVED + " < ? AND " + STORY_TYPE + " = ? AND " + PARENT_STORY_ID + " <= ?"; - String[] args = SqlUtil.buildArgs(threadId, beforeTime, 0, 0); - - try (Cursor cursor = db.query(TABLE_NAME, COUNT, query, args, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - return cursor.getInt(0); - } - } - - return 0; - } - - @Override - public boolean hasMeaningfulMessage(long threadId) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - - try (Cursor cursor = db.query(TABLE_NAME, new String[] { "1" }, THREAD_ID + " = ? AND " + STORY_TYPE + " = ? AND " + PARENT_STORY_ID + " <= ?", SqlUtil.buildArgs(threadId, 0, 0), null, null, null, "1")) { - return cursor != null && cursor.moveToFirst(); - } - } - - @Override - public int getIncomingMeaningfulMessageCountSince(long threadId, long afterTime) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String[] projection = SqlUtil.COUNT; - String where = THREAD_ID + " = ? AND " + STORY_TYPE + " = ? AND " + PARENT_STORY_ID + " <= ? AND " + DATE_RECEIVED + " >= ?"; - String[] whereArgs = SqlUtil.buildArgs(threadId, 0, 0, afterTime); - - try (Cursor cursor = db.query(TABLE_NAME, projection, where, whereArgs, null, null, null, "1")) { - if (cursor != null && cursor.moveToFirst()) { - return cursor.getInt(0); - } else { - return 0; - } - } - } - - @Override - public void addFailures(long messageId, List failure) { - try { - addToDocument(messageId, NETWORK_FAILURES, failure, NetworkFailureSet.class); - } catch (IOException e) { - Log.w(TAG, e); - } - } - - @Override - public void setNetworkFailures(long messageId, Set failures) { - try { - setDocument(databaseHelper.getSignalWritableDatabase(), messageId, NETWORK_FAILURES, new NetworkFailureSet(failures)); - } catch (IOException e) { - Log.w(TAG, e); - } - } - - @Override - public Set incrementReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType, MessageQualifier messageQualifier) { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - Set messageUpdates = new HashSet<>(); - - final String qualifierWhere; - switch (messageQualifier) { - case NORMAL: - qualifierWhere = " AND NOT (" + IS_STORY_CLAUSE + ")"; - break; - case STORY: - qualifierWhere = " AND " + IS_STORY_CLAUSE; - break; - case ALL: - qualifierWhere = ""; - break; - default: - throw new IllegalArgumentException("Unsupported qualifier: " + messageQualifier); - } - - try (Cursor cursor = SQLiteDatabaseExtensionsKt.select(database, ID, THREAD_ID, TYPE, RECIPIENT_ID, receiptType.getColumnName(), RECEIPT_TIMESTAMP) - .from(TABLE_NAME) - .where(DATE_SENT + " = ?" + qualifierWhere, messageId.getTimetamp()) - .run()) - { - while (cursor.moveToNext()) { - if (Types.isOutgoingMessageType(CursorUtil.requireLong(cursor, TYPE))) { - RecipientId theirRecipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); - RecipientId ourRecipientId = messageId.getRecipientId(); - String columnName = receiptType.getColumnName(); - - if (ourRecipientId.equals(theirRecipientId) || Recipient.resolved(theirRecipientId).isGroup()) { - long id = CursorUtil.requireLong(cursor, ID); - long threadId = CursorUtil.requireLong(cursor, THREAD_ID); - int status = receiptType.getGroupStatus(); - boolean isFirstIncrement = CursorUtil.requireLong(cursor, columnName) == 0; - long savedTimestamp = CursorUtil.requireLong(cursor, RECEIPT_TIMESTAMP); - long updatedTimestamp = isFirstIncrement ? Math.max(savedTimestamp, timestamp) : savedTimestamp; - - database.execSQL("UPDATE " + TABLE_NAME + " SET " + - columnName + " = " + columnName + " + 1, " + - RECEIPT_TIMESTAMP + " = ? WHERE " + - ID + " = ?", - SqlUtil.buildArgs(updatedTimestamp, id)); - - SignalDatabase.groupReceipts().update(ourRecipientId, id, status, timestamp); - - messageUpdates.add(new MessageUpdate(threadId, new MessageId(id, true))); - } - } - } - - if (messageUpdates.size() > 0 && receiptType == ReceiptType.DELIVERY) { - earlyDeliveryReceiptCache.increment(messageId.getTimetamp(), messageId.getRecipientId(), timestamp); - } - } - - messageUpdates.addAll(incrementStoryReceiptCount(messageId, timestamp, receiptType)); - - return messageUpdates; - } - - private Set incrementStoryReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType) { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - Set messageUpdates = new HashSet<>(); - String columnName = receiptType.getColumnName(); - - for (MessageId storyMessageId : SignalDatabase.storySends().getStoryMessagesFor(messageId)) { - database.execSQL("UPDATE " + TABLE_NAME + " SET " + - columnName + " = " + columnName + " + 1, " + - RECEIPT_TIMESTAMP + " = CASE " + - "WHEN " + columnName + " = 0 THEN MAX(" + RECEIPT_TIMESTAMP + ", ?) " + - "ELSE " + RECEIPT_TIMESTAMP + " " + - "END " + - "WHERE " + ID + " = ?", - SqlUtil.buildArgs(timestamp, storyMessageId.getId())); - - SignalDatabase.groupReceipts().update(messageId.getRecipientId(), storyMessageId.getId(), receiptType.getGroupStatus(), timestamp); - - messageUpdates.add(new MessageUpdate(-1, storyMessageId)); - } - - return messageUpdates; - } - - @Override - public long getThreadIdForMessage(long id) { - String sql = "SELECT " + THREAD_ID + " FROM " + TABLE_NAME + " WHERE " + ID + " = ?"; - String[] sqlArgs = new String[] {id+""}; - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - - Cursor cursor = null; - - try { - cursor = db.rawQuery(sql, sqlArgs); - if (cursor != null && cursor.moveToFirst()) - return cursor.getLong(0); - else - return -1; - } finally { - if (cursor != null) - cursor.close(); - } - } - - private long getThreadIdFor(@NonNull IncomingMediaMessage retrieved) { - if (retrieved.getGroupId() != null) { - RecipientId groupRecipientId = SignalDatabase.recipients().getOrInsertFromPossiblyMigratedGroupId(retrieved.getGroupId()); - Recipient groupRecipients = Recipient.resolved(groupRecipientId); - return SignalDatabase.threads().getOrCreateThreadIdFor(groupRecipients); - } else { - Recipient sender = Recipient.resolved(retrieved.getFrom()); - return SignalDatabase.threads().getOrCreateThreadIdFor(sender); - } - } - - private long getThreadIdFor(@NonNull NotificationInd notification) { - String fromString = notification.getFrom() != null && notification.getFrom().getTextString() != null - ? Util.toIsoString(notification.getFrom().getTextString()) - : ""; - Recipient recipient = Recipient.external(context, fromString); - return SignalDatabase.threads().getOrCreateThreadIdFor(recipient); - } - - private Cursor rawQuery(@NonNull String where, @Nullable String[] arguments) { - return rawQuery(where, arguments, false, 0); - } - - private Cursor rawQuery(@NonNull String where, @Nullable String[] arguments, boolean reverse, long limit) { - return rawQuery(MMS_PROJECTION, where, arguments, reverse, limit); - } - - private Cursor rawQuery(@NonNull String[] projection, @NonNull String where, @Nullable String[] arguments, boolean reverse, long limit) { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - String rawQueryString = "SELECT " + Util.join(projection, ",") + - " FROM " + MmsTable.TABLE_NAME + " LEFT OUTER JOIN " + AttachmentTable.TABLE_NAME + - " ON (" + MmsTable.TABLE_NAME + "." + MmsTable.ID + " = " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.MMS_ID + ")" + - " WHERE " + where + " GROUP BY " + MmsTable.TABLE_NAME + "." + MmsTable.ID; - - if (reverse) { - rawQueryString += " ORDER BY " + MmsTable.TABLE_NAME + "." + MmsTable.ID + " DESC"; - } - - if (limit > 0) { - rawQueryString += " LIMIT " + limit; - } - - return database.rawQuery(rawQueryString, arguments); - } - - private Cursor internalGetMessage(long messageId) { - return rawQuery(RAW_ID_WHERE, new String[] {messageId + ""}); - } - - @Override - public MessageRecord getMessageRecord(long messageId) throws NoSuchMessageException { - try (Cursor cursor = rawQuery(RAW_ID_WHERE, new String[] {messageId + ""})) { - MessageRecord record = new Reader(cursor).getNext(); - - if (record == null) { - throw new NoSuchMessageException("No message for ID: " + messageId); - } - - return record; - } - } - - @Override - public @Nullable MessageRecord getMessageRecordOrNull(long messageId) { - try (Cursor cursor = rawQuery(RAW_ID_WHERE, new String[] {messageId + ""})) { - return new Reader(cursor).getNext(); - } - } - - @Override - public Reader getMessages(Collection messageIds) { - String ids = TextUtils.join(",", messageIds); - return readerFor(rawQuery(MmsTable.TABLE_NAME + "." + MmsTable.ID + " IN (" + ids + ")", null)); - } - - private void updateMailboxBitmask(long id, long maskOff, long maskOn, Optional threadId) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - db.beginTransaction(); - try { - db.execSQL("UPDATE " + TABLE_NAME + - " SET " + TYPE + " = (" + TYPE + " & " + (Types.TOTAL_MASK - maskOff) + " | " + maskOn + " )" + - " WHERE " + ID + " = ?", new String[] { id + "" }); - - if (threadId.isPresent()) { - SignalDatabase.threads().updateSnippetTypeSilently(threadId.get()); - } - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - @Override - public void markAsOutbox(long messageId) { - long threadId = getThreadIdForMessage(messageId); - updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_OUTBOX_TYPE, Optional.of(threadId)); - } - - @Override - public void markAsForcedSms(long messageId) { - long threadId = getThreadIdForMessage(messageId); - updateMailboxBitmask(messageId, Types.PUSH_MESSAGE_BIT, Types.MESSAGE_FORCE_SMS_BIT, Optional.of(threadId)); - ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); - } - - @Override - public void markAsRateLimited(long messageId) { - long threadId = getThreadIdForMessage(messageId); - updateMailboxBitmask(messageId, 0, Types.MESSAGE_RATE_LIMITED_BIT, Optional.of(threadId)); - ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); - } - - @Override - public void clearRateLimitStatus(@NonNull Collection ids) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - db.beginTransaction(); - try { - for (long id : ids) { - long threadId = getThreadIdForMessage(id); - updateMailboxBitmask(id, Types.MESSAGE_RATE_LIMITED_BIT, 0, Optional.of(threadId)); - } - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - @Override - public void markAsPendingInsecureSmsFallback(long messageId) { - long threadId = getThreadIdForMessage(messageId); - updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_PENDING_INSECURE_SMS_FALLBACK, Optional.of(threadId)); - ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); - } - - @Override - public void markAsSending(long messageId) { - long threadId = getThreadIdForMessage(messageId); - updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_SENDING_TYPE, Optional.of(threadId)); - ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); - ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners(); - } - - @Override - public void markAsSentFailed(long messageId) { - long threadId = getThreadIdForMessage(messageId); - updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_SENT_FAILED_TYPE, Optional.of(threadId)); - ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); - ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners(); - } - - @Override - public void markAsSent(long messageId, boolean secure) { - long threadId = getThreadIdForMessage(messageId); - updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_SENT_TYPE | (secure ? Types.PUSH_MESSAGE_BIT | Types.SECURE_MESSAGE_BIT : 0), Optional.of(threadId)); - ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); - ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners(); - } - - @Override - public void markAsRemoteDelete(long messageId) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - long threadId; - - boolean deletedAttachments = false; - - db.beginTransaction(); - try { - ContentValues values = new ContentValues(); - values.put(REMOTE_DELETED, 1); - values.putNull(BODY); - values.putNull(QUOTE_BODY); - values.putNull(QUOTE_AUTHOR); - values.putNull(QUOTE_TYPE); - values.putNull(QUOTE_ID); - values.putNull(LINK_PREVIEWS); - values.putNull(SHARED_CONTACTS); - db.update(TABLE_NAME, values, ID_WHERE, new String[] { String.valueOf(messageId) }); - - deletedAttachments = SignalDatabase.attachments().deleteAttachmentsForMessage(messageId); - SignalDatabase.mentions().deleteMentionsForMessage(messageId); - SignalDatabase.messageLog().deleteAllRelatedToMessage(messageId, true); - SignalDatabase.reactions().deleteReactions(new MessageId(messageId, true)); - deleteGroupStoryReplies(messageId); - disassociateStoryQuotes(messageId); - - threadId = getThreadIdForMessage(messageId); - SignalDatabase.threads().update(threadId, false); - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); - ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners(); - - if (deletedAttachments) { - ApplicationDependencies.getDatabaseObserver().notifyAttachmentObservers(); - } - } - - @Override - public void markDownloadState(long messageId, long state) { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - ContentValues contentValues = new ContentValues(); - contentValues.put(MMS_STATUS, state); - - database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {messageId + ""}); - ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); - } - - @Override - public void markAsInsecure(long messageId) { - updateMailboxBitmask(messageId, Types.SECURE_MESSAGE_BIT, 0, Optional.empty()); - } - - @Override - public void markUnidentified(long messageId, boolean unidentified) { - ContentValues contentValues = new ContentValues(); - contentValues.put(UNIDENTIFIED, unidentified ? 1 : 0); - - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)}); - } - - @Override - public void markExpireStarted(long id) { - markExpireStarted(id, System.currentTimeMillis()); - } - - @Override - public void markExpireStarted(long id, long startedTimestamp) { - markExpireStarted(Collections.singleton(id), startedTimestamp); - } - - @Override - public void markExpireStarted(Collection ids, long startedAtTimestamp) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - long threadId = -1; - - db.beginTransaction(); - try { - String query = ID + " = ? AND (" + EXPIRE_STARTED + " = 0 OR " + EXPIRE_STARTED + " > ?)"; - - for (long id : ids) { - ContentValues contentValues = new ContentValues(); - contentValues.put(EXPIRE_STARTED, startedAtTimestamp); - - db.update(TABLE_NAME, contentValues, query, new String[]{String.valueOf(id), String.valueOf(startedAtTimestamp)}); - - if (threadId < 0) { - threadId = getThreadIdForMessage(id); - } - } - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - SignalDatabase.threads().update(threadId, false); - notifyConversationListeners(threadId); - } - - @Override - public void markAsNotified(long id) { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - ContentValues contentValues = new ContentValues(); - - contentValues.put(NOTIFIED, 1); - contentValues.put(REACTIONS_LAST_SEEN, System.currentTimeMillis()); - - database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(id)}); - } - - @Override - public List setMessagesReadSince(long threadId, long sinceTimestamp) { - if (sinceTimestamp == -1) { - return setMessagesRead(THREAD_ID + " = ? AND " + STORY_TYPE + " = 0 AND " + PARENT_STORY_ID + " <= 0 AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND (" + getOutgoingTypeClause() + ")))", new String[] { String.valueOf(threadId)}); - } else { - return setMessagesRead(THREAD_ID + " = ? AND " + STORY_TYPE + " = 0 AND " + PARENT_STORY_ID + " <= 0 AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND ( " + getOutgoingTypeClause() + " ))) AND " + DATE_RECEIVED + " <= ?", new String[]{ String.valueOf(threadId), String.valueOf(sinceTimestamp)}); - } - } - - @Override - public @NonNull List setGroupStoryMessagesReadSince(long threadId, long groupStoryId, long sinceTimestamp) { - if (sinceTimestamp == -1) { - return setMessagesRead(THREAD_ID + " = ? AND " + STORY_TYPE + " = 0 AND " + PARENT_STORY_ID + " = ? AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND (" + getOutgoingTypeClause() + ")))", SqlUtil.buildArgs(threadId, groupStoryId)); - } else { - return setMessagesRead(THREAD_ID + " = ? AND " + STORY_TYPE + " = 0 AND " + PARENT_STORY_ID + " = ? AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND ( " + getOutgoingTypeClause() + " ))) AND " + DATE_RECEIVED + " <= ?", SqlUtil.buildArgs(threadId, groupStoryId, sinceTimestamp)); - } - } - - @Override - public @NonNull List getStoryTypes(@NonNull List messageIds) { - List mmsMessages = messageIds.stream() - .filter(MessageId::isMms) - .map(MessageId::getId) - .collect(java.util.stream.Collectors.toList()); - - if (mmsMessages.isEmpty()) { - return Collections.emptyList(); - } - - String[] projection = SqlUtil.buildArgs(ID, STORY_TYPE); - List queries = SqlUtil.buildCollectionQuery(ID, mmsMessages); - HashMap storyTypes = new HashMap<>(); - - for (final SqlUtil.Query query : queries) { - try (Cursor cursor = getWritableDatabase().query(TABLE_NAME, projection, query.getWhere(), query.getWhereArgs(), null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - storyTypes.put(CursorUtil.requireLong(cursor, ID), StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE))); - } - } - } - - return messageIds.stream().map(id -> { - if (id.isMms() && storyTypes.containsKey(id.getId())) { - return storyTypes.get(id.getId()); - } else { - return StoryType.NONE; - } - }).collect(java.util.stream.Collectors.toList()); - } - - @Override - public List setEntireThreadRead(long threadId) { - return setMessagesRead(THREAD_ID + " = ? AND " + STORY_TYPE + " = 0 AND " + PARENT_STORY_ID + " <= 0", new String[] { String.valueOf(threadId)}); - } - - @Override - public List setAllMessagesRead() { - return setMessagesRead(STORY_TYPE + " = 0 AND " + PARENT_STORY_ID + " <= 0 AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND (" + getOutgoingTypeClause() + ")))", null); - } - - private List setMessagesRead(String where, String[] arguments) { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - List result = new LinkedList<>(); - Cursor cursor = null; - RecipientId releaseChannelId = SignalStore.releaseChannelValues().getReleaseChannelRecipientId(); - - database.beginTransaction(); - - try { - cursor = database.query(TABLE_NAME, new String[] { ID, RECIPIENT_ID, DATE_SENT, TYPE, EXPIRES_IN, EXPIRE_STARTED, THREAD_ID, STORY_TYPE }, where, arguments, null, null, null); - - while(cursor != null && cursor.moveToNext()) { - if (Types.isSecureType(CursorUtil.requireLong(cursor, TYPE))) { - long threadId = CursorUtil.requireLong(cursor, THREAD_ID); - RecipientId recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); - long dateSent = CursorUtil.requireLong(cursor, DATE_SENT); - long messageId = CursorUtil.requireLong(cursor, ID); - long expiresIn = CursorUtil.requireLong(cursor, EXPIRES_IN); - long expireStarted = CursorUtil.requireLong(cursor, EXPIRE_STARTED); - SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent); - ExpirationInfo expirationInfo = new ExpirationInfo(messageId, expiresIn, expireStarted, true); - StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE)); - - if (!recipientId.equals(releaseChannelId)) { - result.add(new MarkedMessageInfo(threadId, syncMessageId, new MessageId(messageId, true), expirationInfo)); - } - } - } - - ContentValues contentValues = new ContentValues(); - contentValues.put(READ, 1); - contentValues.put(REACTIONS_UNREAD, 0); - contentValues.put(REACTIONS_LAST_SEEN, System.currentTimeMillis()); - - database.update(TABLE_NAME, contentValues, where, arguments); - database.setTransactionSuccessful(); - } finally { - if (cursor != null) cursor.close(); - database.endTransaction(); - } - - return result; - } - - @Override - public @Nullable Pair getOldestUnreadMentionDetails(long threadId) { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - String[] projection = new String[]{RECIPIENT_ID,DATE_RECEIVED}; - String selection = THREAD_ID + " = ? AND " + READ + " = 0 AND " + MENTIONS_SELF + " = 1"; - String[] args = SqlUtil.buildArgs(threadId); - - try (Cursor cursor = database.query(TABLE_NAME, projection, selection, args, null, null, DATE_RECEIVED + " ASC", "1")) { - if (cursor != null && cursor.moveToFirst()) { - return new Pair<>(RecipientId.from(CursorUtil.requireString(cursor, RECIPIENT_ID)), CursorUtil.requireLong(cursor, DATE_RECEIVED)); - } - } - - return null; - } - - @Override - public int getUnreadMentionCount(long threadId) { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - String[] projection = new String[]{"COUNT(*)"}; - String selection = THREAD_ID + " = ? AND " + READ + " = 0 AND " + MENTIONS_SELF + " = 1"; - String[] args = SqlUtil.buildArgs(threadId); - - try (Cursor cursor = database.query(TABLE_NAME, projection, selection, args, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - return cursor.getInt(0); - } - } - - return 0; - } - - /** - * Trims data related to expired messages. Only intended to be run after a backup restore. - */ - void trimEntriesForExpiredMessages() { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - String trimmedCondition = " NOT IN (SELECT " + MmsTable.ID + " FROM " + MmsTable.TABLE_NAME + ")"; - - database.delete(GroupReceiptTable.TABLE_NAME, GroupReceiptTable.MMS_ID + trimmedCondition, null); - - String[] columns = new String[] { AttachmentTable.ROW_ID, AttachmentTable.UNIQUE_ID }; - String where = AttachmentTable.MMS_ID + trimmedCondition; - - try (Cursor cursor = database.query(AttachmentTable.TABLE_NAME, columns, where, null, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - SignalDatabase.attachments().deleteAttachment(new AttachmentId(cursor.getLong(0), cursor.getLong(1))); - } - } - - SignalDatabase.mentions().deleteAbandonedMentions(); - - try (Cursor cursor = database.query(ThreadTable.TABLE_NAME, new String[] { ThreadTable.ID }, ThreadTable.EXPIRES_IN + " > 0", null, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - SignalDatabase.threads().setLastScrolled(cursor.getLong(0), 0); - SignalDatabase.threads().update(cursor.getLong(0), false); - } - } - } - - @Override - public Optional getNotification(long messageId) { - Cursor cursor = null; - - try { - cursor = rawQuery(RAW_ID_WHERE, new String[] {String.valueOf(messageId)}); - - if (cursor != null && cursor.moveToNext()) { - return Optional.of(new MmsNotificationInfo(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))), - cursor.getString(cursor.getColumnIndexOrThrow(MMS_CONTENT_LOCATION)), - cursor.getString(cursor.getColumnIndexOrThrow(MMS_TRANSACTION_ID)), - cursor.getInt(cursor.getColumnIndexOrThrow(SMS_SUBSCRIPTION_ID)))); - } else { - return Optional.empty(); - } - } finally { - if (cursor != null) - cursor.close(); - } - } - - @Override - public OutgoingMediaMessage getOutgoingMessage(long messageId) - throws MmsException, NoSuchMessageException - { - AttachmentTable attachmentDatabase = SignalDatabase.attachments(); - MentionTable mentionDatabase = SignalDatabase.mentions(); - Cursor cursor = null; - - try { - cursor = rawQuery(RAW_ID_WHERE, new String[] {String.valueOf(messageId)}); - - if (cursor != null && cursor.moveToNext()) { - List associatedAttachments = attachmentDatabase.getAttachmentsForMessage(messageId); - List mentions = mentionDatabase.getMentionsForMessage(messageId); - - long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(TYPE)); - String body = cursor.getString(cursor.getColumnIndexOrThrow(BODY)); - long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(DATE_SENT)); - int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SMS_SUBSCRIPTION_ID)); - long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN)); - boolean viewOnce = cursor.getLong(cursor.getColumnIndexOrThrow(VIEW_ONCE)) == 1; - long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID)); - long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID)); - int distributionType = SignalDatabase.threads().getDistributionType(threadId); - String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsTable.MISMATCHED_IDENTITIES)); - String networkDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsTable.NETWORK_FAILURES)); - StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE)); - ParentStoryId parentStoryId = ParentStoryId.deserialize(CursorUtil.requireLong(cursor, PARENT_STORY_ID)); - - long quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID)); - long quoteAuthor = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR)); - String quoteText = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_BODY)); - int quoteType = cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE_TYPE)); - boolean quoteMissing = cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE_MISSING)) == 1; - List quoteAttachments = Stream.of(associatedAttachments).filter(Attachment::isQuote).map(a -> (Attachment)a).toList(); - List quoteMentions = parseQuoteMentions(context, cursor); - List contacts = getSharedContacts(cursor, associatedAttachments); - Set contactAttachments = new HashSet<>(Stream.of(contacts).map(Contact::getAvatarAttachment).filter(a -> a != null).toList()); - List previews = getLinkPreviews(cursor, associatedAttachments); - Set previewAttachments = Stream.of(previews).filter(lp -> lp.getThumbnail().isPresent()).map(lp -> lp.getThumbnail().get()).collect(Collectors.toSet()); - List attachments = Stream.of(associatedAttachments).filterNot(Attachment::isQuote) - .filterNot(contactAttachments::contains) - .filterNot(previewAttachments::contains) - .sorted(new DatabaseAttachment.DisplayOrderComparator()) - .map(a -> (Attachment)a).toList(); - - Recipient recipient = Recipient.resolved(RecipientId.from(recipientId)); - Set networkFailures = new HashSet<>(); - Set mismatches = new HashSet<>(); - QuoteModel quote = null; - - if (quoteId > 0 && quoteAuthor > 0 && (!TextUtils.isEmpty(quoteText) || !quoteAttachments.isEmpty())) { - quote = new QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText, quoteMissing, quoteAttachments, quoteMentions, QuoteModel.Type.fromCode(quoteType)); - } - - if (!TextUtils.isEmpty(mismatchDocument)) { - try { - mismatches = JsonUtils.fromJson(mismatchDocument, IdentityKeyMismatchSet.class).getItems(); - } catch (IOException e) { - Log.w(TAG, e); - } - } - - if (!TextUtils.isEmpty(networkDocument)) { - try { - networkFailures = JsonUtils.fromJson(networkDocument, NetworkFailureSet.class).getItems(); - } catch (IOException e) { - Log.w(TAG, e); - } - } - - if (body != null && (Types.isGroupQuit(outboxType) || Types.isGroupUpdate(outboxType))) { - return OutgoingMediaMessage.groupUpdateMessage(recipient, new MessageGroupContext(body, Types.isGroupV2(outboxType)), attachments, timestamp, 0, false, quote, contacts, previews, mentions); - } else if (Types.isExpirationTimerUpdate(outboxType)) { - return OutgoingMediaMessage.expirationUpdateMessage(recipient, timestamp, expiresIn); - } else if (Types.isPaymentsNotification(outboxType)) { - return OutgoingMediaMessage.paymentNotificationMessage(recipient, Objects.requireNonNull(body), timestamp, expiresIn); - } else if (Types.isPaymentsRequestToActivate(outboxType)) { - return OutgoingMediaMessage.requestToActivatePaymentsMessage(recipient, timestamp, expiresIn); - } else if (Types.isPaymentsActivated(outboxType)) { - return OutgoingMediaMessage.paymentsActivatedMessage(recipient, timestamp, expiresIn); - } - - GiftBadge giftBadge = null; - if (body != null && Types.isGiftBadge(outboxType)) { - giftBadge = GiftBadge.parseFrom(Base64.decode(body)); - } - - OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, - body, - attachments, - timestamp, - subscriptionId, - expiresIn, - viewOnce, - distributionType, - storyType, - parentStoryId, - Types.isStoryReaction(outboxType), - quote, - contacts, - previews, - mentions, - networkFailures, - mismatches, - giftBadge, - Types.isSecureType(outboxType)); - - return message; - } - - throw new NoSuchMessageException("No record found for id: " + messageId); - } catch (IOException e) { - throw new MmsException(e); - } finally { - if (cursor != null) - cursor.close(); - } - } - - private static List getSharedContacts(@NonNull Cursor cursor, @NonNull List attachments) { - String serializedContacts = cursor.getString(cursor.getColumnIndexOrThrow(SHARED_CONTACTS)); - - if (TextUtils.isEmpty(serializedContacts)) { - return Collections.emptyList(); - } - - Map attachmentIdMap = new HashMap<>(); - for (DatabaseAttachment attachment : attachments) { - attachmentIdMap.put(attachment.getAttachmentId(), attachment); - } - - try { - List contacts = new LinkedList<>(); - JSONArray jsonContacts = new JSONArray(serializedContacts); - - for (int i = 0; i < jsonContacts.length(); i++) { - Contact contact = Contact.deserialize(jsonContacts.getJSONObject(i).toString()); - - if (contact.getAvatar() != null && contact.getAvatar().getAttachmentId() != null) { - DatabaseAttachment attachment = attachmentIdMap.get(contact.getAvatar().getAttachmentId()); - Avatar updatedAvatar = new Avatar(contact.getAvatar().getAttachmentId(), - attachment, - contact.getAvatar().isProfile()); - - contacts.add(new Contact(contact, updatedAvatar)); - } else { - contacts.add(contact); - } - } - - return contacts; - } catch (JSONException | IOException e) { - Log.w(TAG, "Failed to parse shared contacts.", e); - } - - return Collections.emptyList(); - } - - private static List getLinkPreviews(@NonNull Cursor cursor, @NonNull List attachments) { - String serializedPreviews = cursor.getString(cursor.getColumnIndexOrThrow(LINK_PREVIEWS)); - - if (TextUtils.isEmpty(serializedPreviews)) { - return Collections.emptyList(); - } - - Map attachmentIdMap = new HashMap<>(); - for (DatabaseAttachment attachment : attachments) { - attachmentIdMap.put(attachment.getAttachmentId(), attachment); - } - - try { - List previews = new LinkedList<>(); - JSONArray jsonPreviews = new JSONArray(serializedPreviews); - - for (int i = 0; i < jsonPreviews.length(); i++) { - LinkPreview preview = LinkPreview.deserialize(jsonPreviews.getJSONObject(i).toString()); - - if (preview.getAttachmentId() != null) { - DatabaseAttachment attachment = attachmentIdMap.get(preview.getAttachmentId()); - if (attachment != null) { - previews.add(new LinkPreview(preview.getUrl(), preview.getTitle(), preview.getDescription(), preview.getDate(), attachment)); - } else { - previews.add(preview); - } - } else { - previews.add(preview); - } - } - - return previews; - } catch (JSONException | IOException e) { - Log.w(TAG, "Failed to parse shared contacts.", e); - } - - return Collections.emptyList(); - } - - private Optional insertMessageInbox(IncomingMediaMessage retrieved, - String contentLocation, - long threadId, long mailbox) - throws MmsException - { - if (threadId == -1 || retrieved.isGroupMessage()) { - threadId = getThreadIdFor(retrieved); - } - - ContentValues contentValues = new ContentValues(); - - boolean silentUpdate = (mailbox & Types.GROUP_UPDATE_BIT) > 0; - - contentValues.put(DATE_SENT, retrieved.getSentTimeMillis()); - contentValues.put(DATE_SERVER, retrieved.getServerTimeMillis()); - contentValues.put(RECIPIENT_ID, retrieved.getFrom().serialize()); - - contentValues.put(TYPE, mailbox); - contentValues.put(MMS_MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF); - contentValues.put(THREAD_ID, threadId); - contentValues.put(MMS_CONTENT_LOCATION, contentLocation); - contentValues.put(MMS_STATUS, Status.DOWNLOAD_INITIALIZED); - contentValues.put(DATE_RECEIVED, retrieved.isPushMessage() ? retrieved.getReceivedTimeMillis() : generatePduCompatTimestamp(retrieved.getReceivedTimeMillis())); - contentValues.put(SMS_SUBSCRIPTION_ID, retrieved.getSubscriptionId()); - contentValues.put(EXPIRES_IN, retrieved.getExpiresIn()); - contentValues.put(VIEW_ONCE, retrieved.isViewOnce() ? 1 : 0); - contentValues.put(STORY_TYPE, retrieved.getStoryType().getCode()); - contentValues.put(PARENT_STORY_ID, retrieved.getParentStoryId() != null ? retrieved.getParentStoryId().serialize() : 0); - contentValues.put(READ, (silentUpdate || retrieved.isExpirationUpdate()) ? 1 : 0); - contentValues.put(UNIDENTIFIED, retrieved.isUnidentified()); - contentValues.put(SERVER_GUID, retrieved.getServerGuid()); - - if (!contentValues.containsKey(DATE_SENT)) { - contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED)); - } - - List quoteAttachments = new LinkedList<>(); - - if (retrieved.getQuote() != null) { - contentValues.put(QUOTE_ID, retrieved.getQuote().getId()); - contentValues.put(QUOTE_BODY, retrieved.getQuote().getText().toString()); - contentValues.put(QUOTE_AUTHOR, retrieved.getQuote().getAuthor().serialize()); - contentValues.put(QUOTE_TYPE, retrieved.getQuote().getType().getCode()); - contentValues.put(QUOTE_MISSING, retrieved.getQuote().isOriginalMissing() ? 1 : 0); - - BodyRangeList mentionsList = MentionUtil.mentionsToBodyRangeList(retrieved.getQuote().getMentions()); - if (mentionsList != null) { - contentValues.put(QUOTE_MENTIONS, mentionsList.toByteArray()); - } - - quoteAttachments = retrieved.getQuote().getAttachments(); - } - - if (retrieved.isPushMessage() && isDuplicate(retrieved, threadId)) { - Log.w(TAG, "Ignoring duplicate media message (" + retrieved.getSentTimeMillis() + ")"); - return Optional.empty(); - } - - boolean updateThread = retrieved.getStoryType() == StoryType.NONE; - boolean keepThreadArchived = SignalStore.settings().shouldKeepMutedChatsArchived() && Recipient.resolved(retrieved.getFrom()).isMuted(); - long messageId = insertMediaMessage(threadId, - retrieved.getBody(), - retrieved.getAttachments(), - quoteAttachments, - retrieved.getSharedContacts(), - retrieved.getLinkPreviews(), - retrieved.getMentions(), - retrieved.getMessageRanges(), - contentValues, - null, - updateThread, - !keepThreadArchived); - - boolean isNotStoryGroupReply = retrieved.getParentStoryId() == null || !retrieved.getParentStoryId().isGroupReply(); - if (!Types.isPaymentsActivated(mailbox) && !Types.isPaymentsRequestToActivate(mailbox) && !Types.isExpirationTimerUpdate(mailbox) && !retrieved.getStoryType().isStory() && isNotStoryGroupReply) { - boolean incrementUnreadMentions = !retrieved.getMentions().isEmpty() && retrieved.getMentions().stream().anyMatch(m -> m.getRecipientId().equals(Recipient.self().getId())); - SignalDatabase.threads().incrementUnread(threadId, 1, incrementUnreadMentions ? 1 : 0); - SignalDatabase.threads().update(threadId, !keepThreadArchived); - } - - notifyConversationListeners(threadId); - - return Optional.of(new InsertResult(messageId, threadId)); - } - - @Override - public Optional insertMessageInbox(IncomingMediaMessage retrieved, - String contentLocation, long threadId) - throws MmsException - { - long type = Types.BASE_INBOX_TYPE; - - if (retrieved.isPushMessage()) { - type |= Types.PUSH_MESSAGE_BIT; - } - - if (retrieved.isExpirationUpdate()) { - type |= Types.EXPIRATION_TIMER_UPDATE_BIT; - } - - if (retrieved.isPaymentsNotification()) { - type |= Types.SPECIAL_TYPE_PAYMENTS_NOTIFICATION; - } - - if (retrieved.isActivatePaymentsRequest()) { - type |= Types.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST; - } - - if (retrieved.isPaymentsActivated()) { - type |= Types.SPECIAL_TYPE_PAYMENTS_ACTIVATED; - } - - return insertMessageInbox(retrieved, contentLocation, threadId, type); - } - - @Override - public Optional insertSecureDecryptedMessageInbox(IncomingMediaMessage retrieved, long threadId) - throws MmsException - { - long type = Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT; - - if (retrieved.isPushMessage()) { - type |= Types.PUSH_MESSAGE_BIT; - } - - if (retrieved.isExpirationUpdate()) { - type |= Types.EXPIRATION_TIMER_UPDATE_BIT; - } - - boolean hasSpecialType = false; - if (retrieved.isStoryReaction()) { - hasSpecialType = true; - type |= Types.SPECIAL_TYPE_STORY_REACTION; - } - - if (retrieved.getGiftBadge() != null) { - if (hasSpecialType) { - throw new MmsException("Cannot insert message with multiple special types."); - } - - type |= Types.SPECIAL_TYPE_GIFT_BADGE; - } - - if (retrieved.isPaymentsNotification()) { - if (hasSpecialType) { - throw new MmsException("Cannot insert message with multiple special types."); - } - type |= Types.SPECIAL_TYPE_PAYMENTS_NOTIFICATION; - } - - if (retrieved.isActivatePaymentsRequest()) { - if (hasSpecialType) { - throw new MmsException("Cannot insert message with multiple special types."); - } - type |= Types.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST; - } - - if (retrieved.isPaymentsActivated()) { - if (hasSpecialType) { - throw new MmsException("Cannot insert message with multiple special types."); - } - type |= Types.SPECIAL_TYPE_PAYMENTS_ACTIVATED; - } - - return insertMessageInbox(retrieved, "", threadId, type); - } - - public Pair insertMessageInbox(@NonNull NotificationInd notification, int subscriptionId) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - long threadId = getThreadIdFor(notification); - ContentValues contentValues = new ContentValues(); - ContentValuesBuilder contentBuilder = new ContentValuesBuilder(contentValues); - - Log.i(TAG, "Message received type: " + notification.getMessageType()); - - contentBuilder.add(MMS_CONTENT_LOCATION, notification.getContentLocation()); - contentBuilder.add(DATE_SENT, System.currentTimeMillis()); - contentBuilder.add(MMS_EXPIRY, notification.getExpiry()); - contentBuilder.add(MMS_MESSAGE_SIZE, notification.getMessageSize()); - contentBuilder.add(MMS_TRANSACTION_ID, notification.getTransactionId()); - contentBuilder.add(MMS_MESSAGE_TYPE, notification.getMessageType()); - - if (notification.getFrom() != null) { - Recipient recipient = Recipient.external(context, Util.toIsoString(notification.getFrom().getTextString())); - contentValues.put(RECIPIENT_ID, recipient.getId().serialize()); - } else { - contentValues.put(RECIPIENT_ID, RecipientId.UNKNOWN.serialize()); - } - - contentValues.put(TYPE, Types.BASE_INBOX_TYPE); - contentValues.put(THREAD_ID, threadId); - contentValues.put(MMS_STATUS, Status.DOWNLOAD_INITIALIZED); - contentValues.put(DATE_RECEIVED, generatePduCompatTimestamp(System.currentTimeMillis())); - contentValues.put(READ, Util.isDefaultSmsProvider(context) ? 0 : 1); - contentValues.put(SMS_SUBSCRIPTION_ID, subscriptionId); - - if (!contentValues.containsKey(DATE_SENT)) - contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED)); - - long messageId = db.insert(TABLE_NAME, null, contentValues); - - return new Pair<>(messageId, threadId); - } - - @Override - public @NonNull InsertResult insertChatSessionRefreshedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp) { - throw new UnsupportedOperationException(); - } - - @Override - public void insertBadDecryptMessage(@NonNull RecipientId recipientId, int senderDevice, long sentTimestamp, long receivedTimestamp, long threadId) { - throw new UnsupportedOperationException(); - } - - @Override - public void markIncomingNotificationReceived(long threadId) { - notifyConversationListeners(threadId); - - if (org.thoughtcrime.securesms.util.Util.isDefaultSmsProvider(context)) { - SignalDatabase.threads().incrementUnread(threadId, 1, 0); - } - - SignalDatabase.threads().update(threadId, true); - - TrimThreadJob.enqueueAsync(threadId); - } - - @Override - public void markGiftRedemptionCompleted(long messageId) { - markGiftRedemptionState(messageId, GiftBadge.RedemptionState.REDEEMED); - } - - @Override - public void markGiftRedemptionStarted(long messageId) { - markGiftRedemptionState(messageId, GiftBadge.RedemptionState.STARTED); - } - - @Override - public void markGiftRedemptionFailed(long messageId) { - markGiftRedemptionState(messageId, GiftBadge.RedemptionState.FAILED); - } - - private void markGiftRedemptionState(long messageId, @NonNull GiftBadge.RedemptionState redemptionState) { - String[] projection = SqlUtil.buildArgs(BODY, THREAD_ID); - String where = "(" + TYPE + " & " + Types.SPECIAL_TYPES_MASK + " = " + Types.SPECIAL_TYPE_GIFT_BADGE + ") AND " + - ID + " = ?"; - String[] args = SqlUtil.buildArgs(messageId); - boolean updated = false; - long threadId = -1; - - getWritableDatabase().beginTransaction(); - try (Cursor cursor = getWritableDatabase().query(TABLE_NAME, projection, where, args, null, null, null)) { - if (cursor.moveToFirst()) { - GiftBadge giftBadge = GiftBadge.parseFrom(Base64.decode(CursorUtil.requireString(cursor, BODY))); - GiftBadge updatedBadge = giftBadge.toBuilder().setRedemptionState(redemptionState).build(); - ContentValues contentValues = new ContentValues(1); - - contentValues.put(BODY, Base64.encodeBytes(updatedBadge.toByteArray())); - - updated = getWritableDatabase().update(TABLE_NAME, contentValues, ID_WHERE, args) > 0; - threadId = CursorUtil.requireLong(cursor, THREAD_ID); - - getWritableDatabase().setTransactionSuccessful(); - } - } catch (IOException e) { - Log.w(TAG, "Failed to mark gift badge " + redemptionState.name(), e, true); - } finally { - getWritableDatabase().endTransaction(); - } - - if (updated) { - ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); - notifyConversationListeners(threadId); - } - } - - @Override - public long insertMessageOutbox(@NonNull OutgoingMediaMessage message, - long threadId, - boolean forceSms, - @Nullable SmsTable.InsertListener insertListener) - throws MmsException - { - return insertMessageOutbox(message, threadId, forceSms, GroupReceiptTable.STATUS_UNDELIVERED, insertListener); - } - - @Override - public long insertMessageOutbox(@NonNull OutgoingMediaMessage message, - long threadId, boolean forceSms, int defaultReceiptStatus, - @Nullable SmsTable.InsertListener insertListener) - throws MmsException - { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - long type = Types.BASE_SENDING_TYPE; - - if (message.isSecure()) type |= (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT); - if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT; - - if (message.isGroup()) { - if (message.isV2Group()) { - type |= Types.GROUP_V2_BIT | Types.GROUP_UPDATE_BIT; - if (message.isJustAGroupLeave()) { - type |= Types.GROUP_LEAVE_BIT; - } - } else { - MessageGroupContext.GroupV1Properties properties = message.requireGroupV1Properties(); - if (properties.isUpdate()) type |= Types.GROUP_UPDATE_BIT; - else if (properties.isQuit()) type |= Types.GROUP_LEAVE_BIT; - } - } - - if (message.isExpirationUpdate()) { - type |= Types.EXPIRATION_TIMER_UPDATE_BIT; - } - - boolean hasSpecialType = false; - if (message.isStoryReaction()) { - hasSpecialType = true; - type |= Types.SPECIAL_TYPE_STORY_REACTION; - } - - if (message.getGiftBadge() != null) { - if (hasSpecialType) { - throw new MmsException("Cannot insert message with multiple special types."); - } - - type |= Types.SPECIAL_TYPE_GIFT_BADGE; - } - - if (message.isPaymentsNotification()) { - if (hasSpecialType) { - throw new MmsException("Cannot insert message with multiple special types."); - } - type |= Types.SPECIAL_TYPE_PAYMENTS_NOTIFICATION; - } - - if (message.isRequestToActivatePayments()) { - if (hasSpecialType) { - throw new MmsException("Cannot insert message with multiple special types."); - } - type |= Types.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST; - } - - if (message.isPaymentsActivated()) { - if (hasSpecialType) { - throw new MmsException("Cannot insert message with multiple special types."); - } - type |= Types.SPECIAL_TYPE_PAYMENTS_ACTIVATED; - } - - Map earlyDeliveryReceipts = earlyDeliveryReceiptCache.remove(message.getSentTimeMillis()); - - ContentValues contentValues = new ContentValues(); - contentValues.put(DATE_SENT, message.getSentTimeMillis()); - contentValues.put(MMS_MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ); - - contentValues.put(TYPE, type); - contentValues.put(THREAD_ID, threadId); - contentValues.put(READ, 1); - contentValues.put(DATE_RECEIVED, System.currentTimeMillis()); - contentValues.put(SMS_SUBSCRIPTION_ID, message.getSubscriptionId()); - contentValues.put(EXPIRES_IN, message.getExpiresIn()); - contentValues.put(VIEW_ONCE, message.isViewOnce()); - contentValues.put(RECIPIENT_ID, message.getRecipient().getId().serialize()); - contentValues.put(DELIVERY_RECEIPT_COUNT, Stream.of(earlyDeliveryReceipts.values()).mapToLong(EarlyReceiptCache.Receipt::getCount).sum()); - contentValues.put(RECEIPT_TIMESTAMP, Stream.of(earlyDeliveryReceipts.values()).mapToLong(EarlyReceiptCache.Receipt::getTimestamp).max().orElse(-1)); - contentValues.put(STORY_TYPE, message.getStoryType().getCode()); - contentValues.put(PARENT_STORY_ID, message.getParentStoryId() != null ? message.getParentStoryId().serialize() : 0); - - if (message.getRecipient().isSelf() && hasAudioAttachment(message.getAttachments())) { - contentValues.put(VIEWED_RECEIPT_COUNT, 1L); - } - - List quoteAttachments = new LinkedList<>(); - - if (message.getOutgoingQuote() != null) { - MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithPlaceholders(message.getOutgoingQuote().getText(), message.getOutgoingQuote().getMentions()); - - contentValues.put(QUOTE_ID, message.getOutgoingQuote().getId()); - contentValues.put(QUOTE_AUTHOR, message.getOutgoingQuote().getAuthor().serialize()); - contentValues.put(QUOTE_BODY, updated.getBodyAsString()); - contentValues.put(QUOTE_TYPE, message.getOutgoingQuote().getType().getCode()); - contentValues.put(QUOTE_MISSING, message.getOutgoingQuote().isOriginalMissing() ? 1 : 0); - - BodyRangeList mentionsList = MentionUtil.mentionsToBodyRangeList(updated.getMentions()); - if (mentionsList != null) { - contentValues.put(QUOTE_MENTIONS, mentionsList.toByteArray()); - } - - quoteAttachments.addAll(message.getOutgoingQuote().getAttachments()); - } - - MentionUtil.UpdatedBodyAndMentions updatedBodyAndMentions = MentionUtil.updateBodyAndMentionsWithPlaceholders(message.getBody(), message.getMentions()); - - long messageId = insertMediaMessage(threadId, updatedBodyAndMentions.getBodyAsString(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), updatedBodyAndMentions.getMentions(), null, contentValues, insertListener, false, false); - - if (message.getRecipient().isGroup()) { - GroupReceiptTable receiptDatabase = SignalDatabase.groupReceipts(); - Set members = new HashSet<>(); - - if (message.isGroupUpdate() && message.isV2Group()) { - MessageGroupContext.GroupV2Properties groupV2Properties = message.requireGroupV2Properties(); - members.addAll(Stream.of(groupV2Properties.getAllActivePendingAndRemovedMembers()) - .distinct() - .map(uuid -> RecipientId.from(ServiceId.from(uuid))) - .toList()); - members.remove(Recipient.self().getId()); - } else { - members.addAll(Stream.of(SignalDatabase.groups().getGroupMembers(message.getRecipient().requireGroupId(), GroupTable.MemberSet.FULL_MEMBERS_EXCLUDING_SELF)).map(Recipient::getId).toList()); - } - - receiptDatabase.insert(members, messageId, defaultReceiptStatus, message.getSentTimeMillis()); - - for (RecipientId recipientId : earlyDeliveryReceipts.keySet()) { - receiptDatabase.update(recipientId, messageId, GroupReceiptTable.STATUS_DELIVERED, -1); - } - } else if (message.getRecipient().isDistributionList()) { - GroupReceiptTable receiptDatabase = SignalDatabase.groupReceipts(); - List members = SignalDatabase.distributionLists().getMembers(message.getRecipient().requireDistributionListId()); - - receiptDatabase.insert(members, messageId, defaultReceiptStatus, message.getSentTimeMillis()); - - for (RecipientId recipientId : earlyDeliveryReceipts.keySet()) { - receiptDatabase.update(recipientId, messageId, GroupReceiptTable.STATUS_DELIVERED, -1); - } - } - - SignalDatabase.threads().updateLastSeenAndMarkSentAndLastScrolledSilenty(threadId); - - if (!message.getStoryType().isStory()) { - if (message.getOutgoingQuote() == null) { - ApplicationDependencies.getDatabaseObserver().notifyMessageInsertObservers(threadId, new MessageId(messageId, true)); - } else { - ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId); - } - } else { - ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(message.getRecipient().getId()); - } - - notifyConversationListListeners(); - - TrimThreadJob.enqueueAsync(threadId); - - return messageId; - } - - private boolean hasAudioAttachment(@NonNull List attachments) { - for (Attachment attachment : attachments) { - if (MediaUtil.isAudio(attachment)) { - return true; - } - } - - return false; - } - - private long insertMediaMessage(long threadId, - @Nullable String body, - @NonNull List attachments, - @NonNull List quoteAttachments, - @NonNull List sharedContacts, - @NonNull List linkPreviews, - @NonNull List mentions, - @Nullable BodyRangeList messageRanges, - @NonNull ContentValues contentValues, - @Nullable InsertListener insertListener, - boolean updateThread, - boolean unarchive) - throws MmsException - { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - AttachmentTable partsDatabase = SignalDatabase.attachments(); - MentionTable mentionDatabase = SignalDatabase.mentions(); - - boolean mentionsSelf = Stream.of(mentions).filter(m -> Recipient.resolved(m.getRecipientId()).isSelf()).findFirst().isPresent(); - - List allAttachments = new LinkedList<>(); - List contactAttachments = Stream.of(sharedContacts).map(Contact::getAvatarAttachment).filter(a -> a != null).toList(); - List previewAttachments = Stream.of(linkPreviews).filter(lp -> lp.getThumbnail().isPresent()).map(lp -> lp.getThumbnail().get()).toList(); - - allAttachments.addAll(attachments); - allAttachments.addAll(contactAttachments); - allAttachments.addAll(previewAttachments); - - contentValues.put(BODY, body); - contentValues.put(MENTIONS_SELF, mentionsSelf ? 1 : 0); - - if (messageRanges != null) { - contentValues.put(MESSAGE_RANGES, messageRanges.toByteArray()); - } - - db.beginTransaction(); - try { - long messageId = db.insert(TABLE_NAME, null, contentValues); - - mentionDatabase.insert(threadId, messageId, mentions); - - Map insertedAttachments = partsDatabase.insertAttachmentsForMessage(messageId, allAttachments, quoteAttachments); - String serializedContacts = getSerializedSharedContacts(insertedAttachments, sharedContacts); - String serializedPreviews = getSerializedLinkPreviews(insertedAttachments, linkPreviews); - - if (!TextUtils.isEmpty(serializedContacts)) { - ContentValues contactValues = new ContentValues(); - contactValues.put(SHARED_CONTACTS, serializedContacts); - - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - int rows = database.update(TABLE_NAME, contactValues, ID + " = ?", new String[]{ String.valueOf(messageId) }); - - if (rows <= 0) { - Log.w(TAG, "Failed to update message with shared contact data."); - } - } - - if (!TextUtils.isEmpty(serializedPreviews)) { - ContentValues contactValues = new ContentValues(); - contactValues.put(LINK_PREVIEWS, serializedPreviews); - - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - int rows = database.update(TABLE_NAME, contactValues, ID + " = ?", new String[]{ String.valueOf(messageId) }); - - if (rows <= 0) { - Log.w(TAG, "Failed to update message with link preview data."); - } - } - - db.setTransactionSuccessful(); - return messageId; - } finally { - db.endTransaction(); - - if (insertListener != null) { - insertListener.onComplete(); - } - - long contentValuesThreadId = contentValues.getAsLong(THREAD_ID); - - if (updateThread) { - SignalDatabase.threads().setLastScrolled(contentValuesThreadId, 0); - SignalDatabase.threads().update(threadId, unarchive); - } - } - } - - @Override - public boolean deleteMessage(long messageId) { - Log.d(TAG, "deleteMessage(" + messageId + ")"); - - long threadId = getThreadIdForMessage(messageId); - AttachmentTable attachmentDatabase = SignalDatabase.attachments(); - attachmentDatabase.deleteAttachmentsForMessage(messageId); - - GroupReceiptTable groupReceiptDatabase = SignalDatabase.groupReceipts(); - groupReceiptDatabase.deleteRowsForMessage(messageId); - - MentionTable mentionDatabase = SignalDatabase.mentions(); - mentionDatabase.deleteMentionsForMessage(messageId); - - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - database.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""}); - - SignalDatabase.threads().setLastScrolled(threadId, 0); - boolean threadDeleted = SignalDatabase.threads().update(threadId, false); - notifyConversationListeners(threadId); - notifyStickerListeners(); - notifyStickerPackListeners(); - return threadDeleted; - } - - @Override - public void deleteThread(long threadId) { - Log.d(TAG, "deleteThread(" + threadId + ")"); - Set singleThreadSet = new HashSet<>(); - singleThreadSet.add(threadId); - deleteThreads(singleThreadSet); - } - - private @Nullable String getSerializedSharedContacts(@NonNull Map insertedAttachmentIds, @NonNull List contacts) { - if (contacts.isEmpty()) return null; - - JSONArray sharedContactJson = new JSONArray(); - - for (Contact contact : contacts) { - try { - AttachmentId attachmentId = null; - - if (contact.getAvatarAttachment() != null) { - attachmentId = insertedAttachmentIds.get(contact.getAvatarAttachment()); - } - - Avatar updatedAvatar = new Avatar(attachmentId, - contact.getAvatarAttachment(), - contact.getAvatar() != null && contact.getAvatar().isProfile()); - Contact updatedContact = new Contact(contact, updatedAvatar); - - sharedContactJson.put(new JSONObject(updatedContact.serialize())); - } catch (JSONException | IOException e) { - Log.w(TAG, "Failed to serialize shared contact. Skipping it.", e); - } - } - return sharedContactJson.toString(); - } - - private @Nullable String getSerializedLinkPreviews(@NonNull Map insertedAttachmentIds, @NonNull List previews) { - if (previews.isEmpty()) return null; - - JSONArray linkPreviewJson = new JSONArray(); - - for (LinkPreview preview : previews) { - try { - AttachmentId attachmentId = null; - - if (preview.getThumbnail().isPresent()) { - attachmentId = insertedAttachmentIds.get(preview.getThumbnail().get()); - } - - LinkPreview updatedPreview = new LinkPreview(preview.getUrl(), preview.getTitle(), preview.getDescription(), preview.getDate(), attachmentId); - linkPreviewJson.put(new JSONObject(updatedPreview.serialize())); - } catch (JSONException | IOException e) { - Log.w(TAG, "Failed to serialize shared contact. Skipping it.", e); - } - } - return linkPreviewJson.toString(); - } - - private boolean isDuplicate(IncomingMediaMessage message, long threadId) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String query = DATE_SENT + " = ? AND " + RECIPIENT_ID + " = ? AND " + THREAD_ID + " = ?"; - String[] args = SqlUtil.buildArgs(message.getSentTimeMillis(), message.getFrom().serialize(), threadId); - - try (Cursor cursor = db.query(TABLE_NAME, new String[] { "1" }, query, args, null, null, null, "1")) { - return cursor.moveToFirst(); - } - } - - @Override - public boolean isSent(long messageId) { - SQLiteDatabase database = databaseHelper.getSignalReadableDatabase(); - try (Cursor cursor = database.query(TABLE_NAME, new String[] { TYPE }, ID + " = ?", new String[] { String.valueOf(messageId)}, null, null, null)) { - if (cursor != null && cursor.moveToNext()) { - long type = cursor.getLong(cursor.getColumnIndexOrThrow(TYPE)); - return Types.isSentType(type); - } - } - return false; - } - - @Override - public List getProfileChangeDetailsRecords(long threadId, long afterTimestamp) { - throw new UnsupportedOperationException(); - } - - @Override - public Set getAllRateLimitedMessageIds() { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String where = "(" + TYPE + " & " + Types.TOTAL_MASK + " & " + Types.MESSAGE_RATE_LIMITED_BIT + ") > 0"; - - Set ids = new HashSet<>(); - - try (Cursor cursor = db.query(TABLE_NAME, new String[] { ID }, where, null, null, null, null)) { - while (cursor.moveToNext()) { - ids.add(CursorUtil.requireLong(cursor, ID)); - } - } - - return ids; - } - - @Override - public Cursor getUnexportedInsecureMessages(int limit) { - return rawQuery( - SqlUtil.appendArg(MMS_PROJECTION, EXPORT_STATE), - getInsecureMessageClause() + " AND NOT " + EXPORTED, - null, - false, - limit - ); - } - - @Override - public long getUnexportedInsecureMessagesEstimatedSize() { - Cursor messageTextSize = SQLiteDatabaseExtensionsKt.select(getReadableDatabase(), "SUM(LENGTH(" + BODY + "))") - .from(TABLE_NAME) - .where(getInsecureMessageClause() + " AND " + EXPORTED + " < ?", MessageExportStatus.EXPORTED) - .run(); - - long bodyTextSize = CursorExtensionsKt.readToSingleLong(messageTextSize); - - String select = "SUM(" + AttachmentTable.TABLE_NAME + "." + AttachmentTable.SIZE + ") AS s"; - String fromJoin = TABLE_NAME + " INNER JOIN " + AttachmentTable.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.MMS_ID; - String where = getInsecureMessageClause() + " AND " + EXPORTED + " < " + MessageExportStatus.EXPORTED.serialize(); - - long fileSize = CursorExtensionsKt.readToSingleLong(getReadableDatabase().rawQuery("SELECT " + select + " FROM " + fromJoin + " WHERE " + where, null)); - - return bodyTextSize + fileSize; - } - - @Override - public void deleteExportedMessages() { - beginTransaction(); - try { - List threadsToUpdate = new LinkedList<>(); - try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, THREAD_ID_PROJECTION, EXPORTED + " = ?", SqlUtil.buildArgs(MessageExportStatus.EXPORTED), THREAD_ID, null, null, null)) { - while (cursor.moveToNext()) { - threadsToUpdate.add(CursorUtil.requireLong(cursor, THREAD_ID)); - } - } - - getWritableDatabase().delete(TABLE_NAME, EXPORTED + " = ?", SqlUtil.buildArgs(MessageExportStatus.EXPORTED)); - - for (final long threadId : threadsToUpdate) { - SignalDatabase.threads().update(threadId, false); - } - - SignalDatabase.attachments().deleteAbandonedAttachmentFiles(); - - setTransactionSuccessful(); - } finally { - endTransaction(); - } - } - - @Override - void deleteThreads(@NonNull Set threadIds) { - Log.d(TAG, "deleteThreads(count: " + threadIds.size() + ")"); - - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - String where = ""; - - for (long threadId : threadIds) { - where += THREAD_ID + " = '" + threadId + "' OR "; - } - - where = where.substring(0, where.length() - 4); - - try (Cursor cursor = db.query(TABLE_NAME, new String[] {ID}, where, null, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - deleteMessage(cursor.getLong(0)); - } - } - } - - @Override - int deleteMessagesInThreadBeforeDate(long threadId, long date) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - String where = THREAD_ID + " = ? AND " + DATE_RECEIVED + " < " + date; - - return db.delete(TABLE_NAME, where, SqlUtil.buildArgs(threadId)); - } - - @Override - void deleteAbandonedMessages() { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - String where = THREAD_ID + " NOT IN (SELECT _id FROM " + ThreadTable.TABLE_NAME + ")"; - - int deletes = db.delete(TABLE_NAME, where, null); - if (deletes > 0) { - Log.i(TAG, "Deleted " + deletes + " abandoned messages"); - } - } - - @Override - public void deleteRemotelyDeletedStory(long messageId) { - try (Cursor cursor = getMessageCursor(messageId)) { - if (cursor.moveToFirst() && CursorUtil.requireBoolean(cursor, REMOTE_DELETED)) { - deleteMessage(messageId); - } else { - Log.i(TAG, "Unable to delete remotely deleted story: " + messageId); - } - } - } - - @Override - public List getMessagesInThreadAfterInclusive(long threadId, long timestamp, long limit) { - String where = TABLE_NAME + "." + MmsSmsColumns.THREAD_ID + " = ? AND " + - TABLE_NAME + "." + getDateReceivedColumnName() + " >= ?"; - String[] args = SqlUtil.buildArgs(threadId, timestamp); - - try (Reader reader = readerFor(rawQuery(where, args, false, limit))) { - List results = new ArrayList<>(reader.cursor.getCount()); - - while (reader.getNext() != null) { - results.add(reader.getCurrent()); - } - - return results; - } - } - - @Override - public void deleteAllThreads() { - Log.d(TAG, "deleteAllThreads()"); - SignalDatabase.attachments().deleteAllAttachments(); - SignalDatabase.groupReceipts().deleteAllRows(); - SignalDatabase.mentions().deleteAllMentions(); - - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - database.delete(TABLE_NAME, null, null); - } - - @Override - public @Nullable ViewOnceExpirationInfo getNearestExpiringViewOnceMessage() { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - ViewOnceExpirationInfo info = null; - long nearestExpiration = Long.MAX_VALUE; - - String query = "SELECT " + - TABLE_NAME + "." + ID + ", " + - VIEW_ONCE + ", " + - DATE_RECEIVED + " " + - "FROM " + TABLE_NAME + " INNER JOIN " + AttachmentTable.TABLE_NAME + " " + - "ON " + TABLE_NAME + "." + ID + " = " + AttachmentTable.TABLE_NAME + "." + AttachmentTable.MMS_ID + " " + - "WHERE " + - VIEW_ONCE + " > 0 AND " + - "(" + AttachmentTable.DATA + " NOT NULL OR " + AttachmentTable.TRANSFER_STATE + " != ?)"; - String[] args = new String[] { String.valueOf(AttachmentTable.TRANSFER_PROGRESS_DONE) }; - - try (Cursor cursor = db.rawQuery(query, args)) { - while (cursor != null && cursor.moveToNext()) { - long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID)); - long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(DATE_RECEIVED)); - long expiresAt = dateReceived + ViewOnceUtil.MAX_LIFESPAN; - - if (info == null || expiresAt < nearestExpiration) { - info = new ViewOnceExpirationInfo(id, dateReceived); - nearestExpiration = expiresAt; - } - } - } - - return info; - } - - private static @NonNull List parseQuoteMentions(@NonNull Context context, Cursor cursor) { - byte[] raw = cursor.getBlob(cursor.getColumnIndexOrThrow(QUOTE_MENTIONS)); - - return MentionUtil.bodyRangeListToMentions(context, raw); - } - - @Override - public SQLiteDatabase beginTransaction() { - databaseHelper.getSignalWritableDatabase().beginTransaction(); - return databaseHelper.getSignalWritableDatabase(); - } - - @Override - public void setTransactionSuccessful() { - databaseHelper.getSignalWritableDatabase().setTransactionSuccessful(); - } - - @Override - public void endTransaction() { - databaseHelper.getSignalWritableDatabase().endTransaction(); - } - - public static Reader readerFor(Cursor cursor) { - return new Reader(cursor); - } - - public static OutgoingMessageReader readerFor(OutgoingMediaMessage message, long threadId) { - return new OutgoingMessageReader(message, threadId); - } - - public static class Status { - public static final int DOWNLOAD_INITIALIZED = 1; - public static final int DOWNLOAD_NO_CONNECTIVITY = 2; - public static final int DOWNLOAD_CONNECTING = 3; - public static final int DOWNLOAD_SOFT_FAILURE = 4; - public static final int DOWNLOAD_HARD_FAILURE = 5; - public static final int DOWNLOAD_APN_UNAVAILABLE = 6; - } - - public static class OutgoingMessageReader { - - private final Context context; - private final OutgoingMediaMessage message; - private final long id; - private final long threadId; - - public OutgoingMessageReader(OutgoingMediaMessage message, long threadId) { - this.context = ApplicationDependencies.getApplication(); - this.message = message; - this.id = new SecureRandom().nextLong(); - this.threadId = threadId; - } - - public MessageRecord getCurrent() { - SlideDeck slideDeck = new SlideDeck(context, message.getAttachments()); - - CharSequence quoteText = message.getOutgoingQuote() != null ? message.getOutgoingQuote().getText() : null; - List quoteMentions = message.getOutgoingQuote() != null ? message.getOutgoingQuote().getMentions() : Collections.emptyList(); - - if (quoteText != null && !quoteMentions.isEmpty()) { - MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, quoteText, quoteMentions); - - quoteText = updated.getBody(); - quoteMentions = updated.getMentions(); - } - - return new MediaMmsMessageRecord(id, - message.getRecipient(), - message.getRecipient(), - 1, - System.currentTimeMillis(), - System.currentTimeMillis(), - -1, - 0, - threadId, message.getBody(), - slideDeck, - message.isSecure() ? MmsSmsColumns.Types.getOutgoingEncryptedMessageType() : MmsSmsColumns.Types.getOutgoingSmsMessageType(), - Collections.emptySet(), - Collections.emptySet(), - message.getSubscriptionId(), - message.getExpiresIn(), - System.currentTimeMillis(), - message.isViewOnce(), - 0, - message.getOutgoingQuote() != null ? - new Quote(message.getOutgoingQuote().getId(), - message.getOutgoingQuote().getAuthor(), - quoteText, - message.getOutgoingQuote().isOriginalMissing(), - new SlideDeck(context, message.getOutgoingQuote().getAttachments()), - quoteMentions, - message.getOutgoingQuote().getType()) : - null, - message.getSharedContacts(), - message.getLinkPreviews(), - false, - Collections.emptyList(), - false, - false, - 0, - 0, - -1, - null, - message.getStoryType(), - message.getParentStoryId(), - message.getGiftBadge(), - null); - } - } - - /** - * MessageRecord reader which implements the Iterable interface. This allows it to - * be used with many Kotlin Extension Functions as well as with for-each loops. - * - * Note that it's the responsibility of the developer using the reader to ensure that: - * - * 1. They only utilize one of the two interfaces (legacy or iterator) - * 1. They close this reader after use, preferably via try-with-resources or a use block. - */ - public static class Reader implements MessageTable.Reader { - - private final Cursor cursor; - private final Context context; - - public Reader(Cursor cursor) { - this.cursor = cursor; - this.context = ApplicationDependencies.getApplication(); - } - - @Override - public MessageRecord getNext() { - if (cursor == null || !cursor.moveToNext()) - return null; - - return getCurrent(); - } - - @Override - public MessageRecord getCurrent() { - long mmsType = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.MMS_MESSAGE_TYPE)); - - if (mmsType == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) { - return getNotificationMmsMessageRecord(cursor); - } else { - return getMediaMmsMessageRecord(cursor); - } - } - - @Override - public @NonNull MessageExportState getMessageExportStateForCurrentRecord() { - byte[] messageExportState = CursorUtil.requireBlob(cursor, MmsTable.EXPORT_STATE); - if (messageExportState == null) { - return MessageExportState.getDefaultInstance(); - } - - try { - return MessageExportState.parseFrom(messageExportState); - } catch (InvalidProtocolBufferException e) { - return MessageExportState.getDefaultInstance(); - } - } - - public int getCount() { - if (cursor == null) return 0; - else return cursor.getCount(); - } - - private NotificationMmsMessageRecord getNotificationMmsMessageRecord(Cursor cursor) { - long id = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.ID)); - long dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.DATE_SENT)); - long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.DATE_RECEIVED)); - long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.THREAD_ID)); - long mailbox = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.TYPE)); - long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.RECIPIENT_ID)); - int addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsTable.RECIPIENT_DEVICE_ID)); - Recipient recipient = Recipient.live(RecipientId.from(recipientId)).get(); - - String contentLocation = cursor.getString(cursor.getColumnIndexOrThrow(MmsTable.MMS_CONTENT_LOCATION)); - String transactionId = cursor.getString(cursor.getColumnIndexOrThrow(MmsTable.MMS_TRANSACTION_ID)); - long messageSize = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.MMS_MESSAGE_SIZE)); - long expiry = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.MMS_EXPIRY)); - int status = cursor.getInt(cursor.getColumnIndexOrThrow(MmsTable.MMS_STATUS)); - int deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsTable.DELIVERY_RECEIPT_COUNT)); - int readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsTable.READ_RECEIPT_COUNT)); - int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsTable.SMS_SUBSCRIPTION_ID)); - int viewedReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.VIEWED_RECEIPT_COUNT)); - long receiptTimestamp = CursorUtil.requireLong(cursor, MmsSmsColumns.RECEIPT_TIMESTAMP); - StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE)); - ParentStoryId parentStoryId = ParentStoryId.deserialize(CursorUtil.requireLong(cursor, PARENT_STORY_ID)); - String body = cursor.getString(cursor.getColumnIndexOrThrow(MmsTable.BODY)); - - if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { - readReceiptCount = 0; - } - - byte[]contentLocationBytes = null; - byte[]transactionIdBytes = null; - - if (!TextUtils.isEmpty(contentLocation)) - contentLocationBytes = org.thoughtcrime.securesms.util.Util.toIsoBytes(contentLocation); - - if (!TextUtils.isEmpty(transactionId)) - transactionIdBytes = org.thoughtcrime.securesms.util.Util.toIsoBytes(transactionId); - - SlideDeck slideDeck = new SlideDeck(context, new MmsNotificationAttachment(status, messageSize)); - - GiftBadge giftBadge = null; - if (body != null && Types.isGiftBadge(mailbox)) { - try { - giftBadge = GiftBadge.parseFrom(Base64.decode(body)); - } catch (IOException e) { - Log.w(TAG, "Error parsing gift badge", e); - } - } - - return new NotificationMmsMessageRecord(id, recipient, recipient, - addressDeviceId, dateSent, dateReceived, deliveryReceiptCount, threadId, - contentLocationBytes, messageSize, expiry, status, - transactionIdBytes, mailbox, subscriptionId, slideDeck, - readReceiptCount, viewedReceiptCount, receiptTimestamp, storyType, - parentStoryId, giftBadge); - } - - private MediaMmsMessageRecord getMediaMmsMessageRecord(Cursor cursor) { - long id = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.ID)); - long dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.DATE_SENT)); - long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.DATE_RECEIVED)); - long dateServer = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.DATE_SERVER)); - long box = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.TYPE)); - long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.THREAD_ID)); - long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.RECIPIENT_ID)); - int addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsTable.RECIPIENT_DEVICE_ID)); - int deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsTable.DELIVERY_RECEIPT_COUNT)); - int readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsTable.READ_RECEIPT_COUNT)); - String body = cursor.getString(cursor.getColumnIndexOrThrow(MmsTable.BODY)); - String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsTable.MISMATCHED_IDENTITIES)); - String networkDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsTable.NETWORK_FAILURES)); - int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsTable.SMS_SUBSCRIPTION_ID)); - long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.EXPIRES_IN)); - long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.EXPIRE_STARTED)); - boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(MmsTable.UNIDENTIFIED)) == 1; - boolean isViewOnce = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.VIEW_ONCE)) == 1; - boolean remoteDelete = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.REMOTE_DELETED)) == 1; - boolean mentionsSelf = CursorUtil.requireBoolean(cursor, MENTIONS_SELF); - long notifiedTimestamp = CursorUtil.requireLong(cursor, NOTIFIED_TIMESTAMP); - int viewedReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.VIEWED_RECEIPT_COUNT)); - long receiptTimestamp = CursorUtil.requireLong(cursor, MmsSmsColumns.RECEIPT_TIMESTAMP); - byte[] messageRangesData = CursorUtil.requireBlob(cursor, MESSAGE_RANGES); - StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE)); - ParentStoryId parentStoryId = ParentStoryId.deserialize(CursorUtil.requireLong(cursor, PARENT_STORY_ID)); - - if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { - readReceiptCount = 0; - - if (MmsSmsColumns.Types.isOutgoingMessageType(box) && !storyType.isStory()) { - viewedReceiptCount = 0; - } - } - - Recipient recipient = Recipient.live(RecipientId.from(recipientId)).get(); - Set mismatches = getMismatchedIdentities(mismatchDocument); - Set networkFailures = getFailures(networkDocument); - List attachments = SignalDatabase.attachments().getAttachments(cursor); - List contacts = getSharedContacts(cursor, attachments); - Set contactAttachments = Stream.of(contacts).map(Contact::getAvatarAttachment).withoutNulls().collect(Collectors.toSet()); - List previews = getLinkPreviews(cursor, attachments); - Set previewAttachments = Stream.of(previews).filter(lp -> lp.getThumbnail().isPresent()).map(lp -> lp.getThumbnail().get()).collect(Collectors.toSet()); - SlideDeck slideDeck = buildSlideDeck(context, Stream.of(attachments).filterNot(contactAttachments::contains).filterNot(previewAttachments::contains).toList()); - Quote quote = getQuote(cursor); - BodyRangeList messageRanges = null; - - try { - if (messageRangesData != null) { - messageRanges = BodyRangeList.parseFrom(messageRangesData); - } - } catch (InvalidProtocolBufferException e) { - Log.w(TAG, "Error parsing message ranges", e); - } - - GiftBadge giftBadge = null; - if (body != null && Types.isGiftBadge(box)) { - try { - giftBadge = GiftBadge.parseFrom(Base64.decode(body)); - } catch (IOException e) { - Log.w(TAG, "Error parsing gift badge", e); - } - } - - return new MediaMmsMessageRecord(id, recipient, recipient, - addressDeviceId, dateSent, dateReceived, dateServer, deliveryReceiptCount, - threadId, body, slideDeck, box, mismatches, - networkFailures, subscriptionId, expiresIn, expireStarted, - isViewOnce, readReceiptCount, quote, contacts, previews, unidentified, Collections.emptyList(), - remoteDelete, mentionsSelf, notifiedTimestamp, viewedReceiptCount, receiptTimestamp, messageRanges, - storyType, parentStoryId, giftBadge, null); - } - - private Set getMismatchedIdentities(String document) { - if (!TextUtils.isEmpty(document)) { - try { - return JsonUtils.fromJson(document, IdentityKeyMismatchSet.class).getItems(); - } catch (IOException e) { - Log.w(TAG, e); - } - } - - return Collections.emptySet(); - } - - private Set getFailures(String document) { - if (!TextUtils.isEmpty(document)) { - try { - return JsonUtils.fromJson(document, NetworkFailureSet.class).getItems(); - } catch (IOException ioe) { - Log.w(TAG, ioe); - } - } - - return Collections.emptySet(); - } - - public static SlideDeck buildSlideDeck(@NonNull Context context, @NonNull List attachments) { - List messageAttachments = Stream.of(attachments) - .filterNot(Attachment::isQuote) - .sorted(new DatabaseAttachment.DisplayOrderComparator()) - .toList(); - return new SlideDeck(context, messageAttachments); - } - - private @Nullable Quote getQuote(@NonNull Cursor cursor) { - long quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.QUOTE_ID)); - long quoteAuthor = cursor.getLong(cursor.getColumnIndexOrThrow(MmsTable.QUOTE_AUTHOR)); - CharSequence quoteText = cursor.getString(cursor.getColumnIndexOrThrow(MmsTable.QUOTE_BODY)); - int quoteType = cursor.getInt(cursor.getColumnIndexOrThrow(MmsTable.QUOTE_TYPE)); - boolean quoteMissing = cursor.getInt(cursor.getColumnIndexOrThrow(MmsTable.QUOTE_MISSING)) == 1; - List quoteMentions = parseQuoteMentions(context, cursor); - List attachments = SignalDatabase.attachments().getAttachments(cursor); - List quoteAttachments = Stream.of(attachments).filter(Attachment::isQuote).toList(); - SlideDeck quoteDeck = new SlideDeck(context, quoteAttachments); - - if (quoteId > 0 && quoteAuthor > 0) { - if (quoteText != null && !quoteMentions.isEmpty()) { - MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, quoteText, quoteMentions); - - quoteText = updated.getBody(); - quoteMentions = updated.getMentions(); - } - - return new Quote(quoteId, RecipientId.from(quoteAuthor), quoteText, quoteMissing, quoteDeck, quoteMentions, QuoteModel.Type.fromCode(quoteType)); - } else { - return null; - } - } - - @Override - public void close() { - if (cursor != null) { - cursor.close(); - } - } - - @NonNull - @Override - public Iterator iterator() { - return new ReaderIterator(); - } - - private class ReaderIterator implements Iterator { - @Override - public boolean hasNext() { - return cursor != null && cursor.getCount() != 0 && !cursor.isLast(); - } - - @Override - public MessageRecord next() { - MessageRecord record = getNext(); - if (record == null) { - throw new NoSuchElementException(); - } - - return record; - } - } - } - - private long generatePduCompatTimestamp(long time) { - return time - (time % 1000); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ReactionTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ReactionTable.kt index de6388f718..d235f26208 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ReactionTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ReactionTable.kt @@ -5,6 +5,7 @@ import android.content.Context import android.database.Cursor import org.signal.core.util.CursorUtil import org.signal.core.util.SqlUtil +import org.signal.core.util.delete import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.ReactionRecord import org.thoughtcrime.securesms.dependencies.ApplicationDependencies @@ -43,15 +44,9 @@ class ReactionTable(context: Context, databaseHelper: SignalDatabase) : Database @JvmField val CREATE_TRIGGERS = arrayOf( """ - CREATE TRIGGER reactions_sms_delete AFTER DELETE ON ${SmsTable.TABLE_NAME} + CREATE TRIGGER reactions_mms_delete AFTER DELETE ON ${MessageTable.TABLE_NAME} BEGIN - DELETE FROM $TABLE_NAME WHERE $MESSAGE_ID = old.${SmsTable.ID} AND $IS_MMS = 0; - END - """, - """ - CREATE TRIGGER reactions_mms_delete AFTER DELETE ON ${MmsTable.TABLE_NAME} - BEGIN - DELETE FROM $TABLE_NAME WHERE $MESSAGE_ID = old.${MmsTable.ID} AND $IS_MMS = 1; + DELETE FROM $TABLE_NAME WHERE $MESSAGE_ID = old.${MessageTable.ID} AND $IS_MMS = 1; END """ ) @@ -67,8 +62,8 @@ class ReactionTable(context: Context, databaseHelper: SignalDatabase) : Database } fun getReactions(messageId: MessageId): List { - val query = "$MESSAGE_ID = ? AND $IS_MMS = ?" - val args = SqlUtil.buildArgs(messageId.id, if (messageId.mms) 1 else 0) + val query = "$MESSAGE_ID = ?" + val args = SqlUtil.buildArgs(messageId.id) val reactions: MutableList = mutableListOf() @@ -88,9 +83,9 @@ class ReactionTable(context: Context, databaseHelper: SignalDatabase) : Database val messageIdToReactions: MutableMap> = mutableMapOf() - val args: List> = messageIds.map { SqlUtil.buildArgs(it.id, if (it.mms) 1 else 0) } + val args: List> = messageIds.map { SqlUtil.buildArgs(it.id) } - for (query: SqlUtil.Query in SqlUtil.buildCustomCollectionQuery("$MESSAGE_ID = ? AND $IS_MMS = ?", args)) { + for (query: SqlUtil.Query in SqlUtil.buildCustomCollectionQuery("$MESSAGE_ID = ?", args)) { readableDatabase.query(TABLE_NAME, null, query.where, query.whereArgs, null, null, null).use { cursor -> while (cursor.moveToNext()) { val reaction: ReactionRecord = readReaction(cursor) @@ -115,7 +110,6 @@ class ReactionTable(context: Context, databaseHelper: SignalDatabase) : Database } fun addReaction(messageId: MessageId, reaction: ReactionRecord) { - writableDatabase.beginTransaction() try { val values = ContentValues().apply { @@ -144,7 +138,6 @@ class ReactionTable(context: Context, databaseHelper: SignalDatabase) : Database } fun deleteReaction(messageId: MessageId, recipientId: RecipientId) { - writableDatabase.beginTransaction() try { val query = "$MESSAGE_ID = ? AND $IS_MMS = ? AND $AUTHOR_ID = ?" @@ -171,8 +164,8 @@ class ReactionTable(context: Context, databaseHelper: SignalDatabase) : Database } fun hasReaction(messageId: MessageId, reaction: ReactionRecord): Boolean { - val query = "$MESSAGE_ID = ? AND $IS_MMS = ? AND $AUTHOR_ID = ? AND $EMOJI = ?" - val args = SqlUtil.buildArgs(messageId.id, if (messageId.mms) 1 else 0, reaction.author, reaction.emoji) + val query = "$MESSAGE_ID = ? AND $AUTHOR_ID = ? AND $EMOJI = ?" + val args = SqlUtil.buildArgs(messageId.id, reaction.author, reaction.emoji) readableDatabase.query(TABLE_NAME, arrayOf(MESSAGE_ID), query, args, null, null, null).use { cursor -> return cursor.moveToFirst() @@ -180,8 +173,8 @@ class ReactionTable(context: Context, databaseHelper: SignalDatabase) : Database } private fun hasReactions(messageId: MessageId): Boolean { - val query = "$MESSAGE_ID = ? AND $IS_MMS = ?" - val args = SqlUtil.buildArgs(messageId.id, if (messageId.mms) 1 else 0) + val query = "$MESSAGE_ID = ?" + val args = SqlUtil.buildArgs(messageId.id) readableDatabase.query(TABLE_NAME, arrayOf(MESSAGE_ID), query, args, null, null, null).use { cursor -> return cursor.moveToFirst() @@ -199,12 +192,9 @@ class ReactionTable(context: Context, databaseHelper: SignalDatabase) : Database } fun deleteAbandonedReactions() { - val query = """ - ($IS_MMS = 0 AND $MESSAGE_ID NOT IN (SELECT ${SmsTable.ID} FROM ${SmsTable.TABLE_NAME})) - OR - ($IS_MMS = 1 AND $MESSAGE_ID NOT IN (SELECT ${MmsTable.ID} FROM ${MmsTable.TABLE_NAME})) - """.trimIndent() - - writableDatabase.delete(TABLE_NAME, query, null) + writableDatabase + .delete(TABLE_NAME) + .where("$MESSAGE_ID NOT IN (SELECT ${MessageTable.ID} FROM ${MessageTable.TABLE_NAME})") + .run() } } 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 e48676a7d9..1c9a351cbc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -3305,8 +3305,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da INNER JOIN ${ThreadTable.TABLE_NAME} AS t ON t.${ThreadTable.RECIPIENT_ID} = r.$ID WHERE r.$PROFILE_SHARING = 0 AND ( - EXISTS(SELECT 1 FROM ${SmsTable.TABLE_NAME} WHERE ${SmsTable.THREAD_ID} = t.${ThreadTable.ID} AND ${SmsTable.DATE_RECEIVED} < ?) OR - EXISTS(SELECT 1 FROM ${MmsTable.TABLE_NAME} WHERE ${MmsTable.THREAD_ID} = t.${ThreadTable.ID} AND ${MmsTable.DATE_RECEIVED} < ?) + EXISTS(SELECT 1 FROM ${MessageTable.TABLE_NAME} WHERE ${MessageTable.THREAD_ID} = t.${ThreadTable.ID} AND ${MessageTable.DATE_RECEIVED} < ?) ) """.trimIndent() diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SearchTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SearchTable.kt index 49dc9664c3..810a2978c0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SearchTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SearchTable.kt @@ -13,7 +13,6 @@ import org.signal.core.util.SqlUtil @SuppressLint("RecipientIdDatabaseReferenceUsage", "ThreadIdDatabaseReferenceUsage") // Handles updates via triggers class SearchTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper) { companion object { - const val SMS_FTS_TABLE_NAME = "sms_fts" const val MMS_FTS_TABLE_NAME = "mms_fts" const val ID = "rowid" const val BODY = MmsSmsColumns.BODY @@ -27,42 +26,25 @@ class SearchTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa @Language("sql") val CREATE_TABLE = arrayOf( - "CREATE VIRTUAL TABLE $SMS_FTS_TABLE_NAME USING fts5($BODY, $THREAD_ID UNINDEXED, content=${SmsTable.TABLE_NAME}, content_rowid=${SmsTable.ID})", - "CREATE VIRTUAL TABLE $MMS_FTS_TABLE_NAME USING fts5($BODY, $THREAD_ID UNINDEXED, content=${MmsTable.TABLE_NAME}, content_rowid=${MmsTable.ID})", + "CREATE VIRTUAL TABLE $MMS_FTS_TABLE_NAME USING fts5($BODY, $THREAD_ID UNINDEXED, content=${MessageTable.TABLE_NAME}, content_rowid=${MessageTable.ID})", ) @Language("sql") val CREATE_TRIGGERS = arrayOf( """ - CREATE TRIGGER sms_ai AFTER INSERT ON ${SmsTable.TABLE_NAME} BEGIN - INSERT INTO $SMS_FTS_TABLE_NAME($ID, $BODY, $THREAD_ID) VALUES (new.${SmsTable.ID}, new.${SmsTable.BODY}, new.${SmsTable.THREAD_ID}); + CREATE TRIGGER mms_ai AFTER INSERT ON ${MessageTable.TABLE_NAME} BEGIN + INSERT INTO $MMS_FTS_TABLE_NAME($ID, $BODY, $THREAD_ID) VALUES (new.${MessageTable.ID}, new.${MessageTable.BODY}, new.${MessageTable.THREAD_ID}); END; """, """ - CREATE TRIGGER sms_ad AFTER DELETE ON ${SmsTable.TABLE_NAME} BEGIN - INSERT INTO $SMS_FTS_TABLE_NAME($SMS_FTS_TABLE_NAME, $ID, $BODY, $THREAD_ID) VALUES('delete', old.${SmsTable.ID}, old.${SmsTable.BODY}, old.${SmsTable.THREAD_ID}); + CREATE TRIGGER mms_ad AFTER DELETE ON ${MessageTable.TABLE_NAME} BEGIN + INSERT INTO $MMS_FTS_TABLE_NAME($MMS_FTS_TABLE_NAME, $ID, $BODY, $THREAD_ID) VALUES('delete', old.${MessageTable.ID}, old.${MessageTable.BODY}, old.${MessageTable.THREAD_ID}); END; """, """ - CREATE TRIGGER sms_au AFTER UPDATE ON ${SmsTable.TABLE_NAME} BEGIN - INSERT INTO $SMS_FTS_TABLE_NAME($SMS_FTS_TABLE_NAME, $ID, $BODY, $THREAD_ID) VALUES('delete', old.${SmsTable.ID}, old.${SmsTable.BODY}, old.${SmsTable.THREAD_ID}); - INSERT INTO $SMS_FTS_TABLE_NAME($ID, $BODY, $THREAD_ID) VALUES(new.${SmsTable.ID}, new.${SmsTable.BODY}, new.${SmsTable.THREAD_ID}); - END; - """, - """ - CREATE TRIGGER mms_ai AFTER INSERT ON ${MmsTable.TABLE_NAME} BEGIN - INSERT INTO $MMS_FTS_TABLE_NAME($ID, $BODY, $THREAD_ID) VALUES (new.${MmsTable.ID}, new.${MmsTable.BODY}, new.${MmsTable.THREAD_ID}); - END; - """, - """ - CREATE TRIGGER mms_ad AFTER DELETE ON ${MmsTable.TABLE_NAME} BEGIN - INSERT INTO $MMS_FTS_TABLE_NAME($MMS_FTS_TABLE_NAME, $ID, $BODY, $THREAD_ID) VALUES('delete', old.${MmsTable.ID}, old.${MmsTable.BODY}, old.${MmsTable.THREAD_ID}); - END; - """, - """ - CREATE TRIGGER mms_au AFTER UPDATE ON ${MmsTable.TABLE_NAME} BEGIN - INSERT INTO $MMS_FTS_TABLE_NAME($MMS_FTS_TABLE_NAME, $ID, $BODY, $THREAD_ID) VALUES('delete', old.${MmsTable.ID}, old.${MmsTable.BODY}, old.${MmsTable.THREAD_ID}); - INSERT INTO $MMS_FTS_TABLE_NAME($ID, $BODY, $THREAD_ID) VALUES (new.${MmsTable.ID}, new.${MmsTable.BODY}, new.${MmsTable.THREAD_ID}); + CREATE TRIGGER mms_au AFTER UPDATE ON ${MessageTable.TABLE_NAME} BEGIN + INSERT INTO $MMS_FTS_TABLE_NAME($MMS_FTS_TABLE_NAME, $ID, $BODY, $THREAD_ID) VALUES('delete', old.${MessageTable.ID}, old.${MessageTable.BODY}, old.${MessageTable.THREAD_ID}); + INSERT INTO $MMS_FTS_TABLE_NAME($ID, $BODY, $THREAD_ID) VALUES (new.${MessageTable.ID}, new.${MessageTable.BODY}, new.${MessageTable.THREAD_ID}); END; """ ) @@ -70,40 +52,21 @@ class SearchTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa private const val MESSAGES_QUERY = """ SELECT ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} AS $CONVERSATION_RECIPIENT, - ${SmsTable.TABLE_NAME}.${MmsSmsColumns.RECIPIENT_ID} AS $MESSAGE_RECIPIENT, - snippet($SMS_FTS_TABLE_NAME, -1, '', '', '$SNIPPET_WRAP', 7) AS $SNIPPET, - ${SmsTable.TABLE_NAME}.${MmsSmsColumns.DATE_RECEIVED}, - $SMS_FTS_TABLE_NAME.$THREAD_ID, - $SMS_FTS_TABLE_NAME.$BODY, - $SMS_FTS_TABLE_NAME.$ID AS $MESSAGE_ID, - 0 AS $IS_MMS - FROM - ${SmsTable.TABLE_NAME} - INNER JOIN $SMS_FTS_TABLE_NAME ON $SMS_FTS_TABLE_NAME.$ID = ${SmsTable.TABLE_NAME}.${SmsTable.ID} - INNER JOIN ${ThreadTable.TABLE_NAME} ON $SMS_FTS_TABLE_NAME.$THREAD_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID} - WHERE - $SMS_FTS_TABLE_NAME MATCH ? AND - ${SmsTable.TABLE_NAME}.${SmsTable.TYPE} & ${MmsSmsColumns.Types.GROUP_V2_BIT} = 0 AND - ${SmsTable.TABLE_NAME}.${SmsTable.TYPE} & ${MmsSmsColumns.Types.BASE_TYPE_MASK} != ${MmsSmsColumns.Types.PROFILE_CHANGE_TYPE} AND - ${SmsTable.TABLE_NAME}.${SmsTable.TYPE} & ${MmsSmsColumns.Types.BASE_TYPE_MASK} != ${MmsSmsColumns.Types.GROUP_CALL_TYPE} - UNION ALL - SELECT - ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} AS $CONVERSATION_RECIPIENT, - ${MmsTable.TABLE_NAME}.${MmsSmsColumns.RECIPIENT_ID} AS $MESSAGE_RECIPIENT, + ${MessageTable.TABLE_NAME}.${MmsSmsColumns.RECIPIENT_ID} AS $MESSAGE_RECIPIENT, snippet($MMS_FTS_TABLE_NAME, -1, '', '', '$SNIPPET_WRAP', 7) AS $SNIPPET, - ${MmsTable.TABLE_NAME}.${MmsSmsColumns.DATE_RECEIVED}, + ${MessageTable.TABLE_NAME}.${MmsSmsColumns.DATE_RECEIVED}, $MMS_FTS_TABLE_NAME.$THREAD_ID, $MMS_FTS_TABLE_NAME.$BODY, $MMS_FTS_TABLE_NAME.$ID AS $MESSAGE_ID, 1 AS $IS_MMS FROM - ${MmsTable.TABLE_NAME} - INNER JOIN $MMS_FTS_TABLE_NAME ON $MMS_FTS_TABLE_NAME.$ID = ${MmsTable.TABLE_NAME}.${MmsTable.ID} + ${MessageTable.TABLE_NAME} + INNER JOIN $MMS_FTS_TABLE_NAME ON $MMS_FTS_TABLE_NAME.$ID = ${MessageTable.TABLE_NAME}.${MessageTable.ID} INNER JOIN ${ThreadTable.TABLE_NAME} ON $MMS_FTS_TABLE_NAME.$THREAD_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID} WHERE $MMS_FTS_TABLE_NAME MATCH ? AND - ${MmsTable.TABLE_NAME}.${MmsTable.TYPE} & ${MmsSmsColumns.Types.GROUP_V2_BIT} = 0 AND - ${MmsTable.TABLE_NAME}.${MmsTable.TYPE} & ${MmsSmsColumns.Types.SPECIAL_TYPE_PAYMENTS_NOTIFICATION} = 0 + ${MessageTable.TABLE_NAME}.${MessageTable.TYPE} & ${MmsSmsColumns.Types.GROUP_V2_BIT} = 0 AND + ${MessageTable.TABLE_NAME}.${MessageTable.TYPE} & ${MmsSmsColumns.Types.SPECIAL_TYPE_PAYMENTS_NOTIFICATION} = 0 ORDER BY ${MmsSmsColumns.DATE_RECEIVED} DESC LIMIT 500 """ @@ -111,37 +74,20 @@ class SearchTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa private const val MESSAGES_FOR_THREAD_QUERY = """ SELECT ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} AS $CONVERSATION_RECIPIENT, - ${SmsTable.TABLE_NAME}.${MmsSmsColumns.RECIPIENT_ID} AS $MESSAGE_RECIPIENT, - snippet($SMS_FTS_TABLE_NAME, -1, '', '', '$SNIPPET_WRAP', 7) AS $SNIPPET, - ${SmsTable.TABLE_NAME}.${MmsSmsColumns.DATE_RECEIVED}, - $SMS_FTS_TABLE_NAME.$THREAD_ID, - $SMS_FTS_TABLE_NAME.$BODY, - $SMS_FTS_TABLE_NAME.$ID AS $MESSAGE_ID, - 0 AS $IS_MMS - FROM - ${SmsTable.TABLE_NAME} - INNER JOIN $SMS_FTS_TABLE_NAME ON $SMS_FTS_TABLE_NAME.$ID = ${SmsTable.TABLE_NAME}.${SmsTable.ID} - INNER JOIN ${ThreadTable.TABLE_NAME} ON $SMS_FTS_TABLE_NAME.$THREAD_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID} - WHERE - $SMS_FTS_TABLE_NAME MATCH ? AND - ${SmsTable.TABLE_NAME}.${MmsSmsColumns.THREAD_ID} = ? - UNION ALL - SELECT - ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} AS $CONVERSATION_RECIPIENT, - ${MmsTable.TABLE_NAME}.${MmsSmsColumns.RECIPIENT_ID} AS $MESSAGE_RECIPIENT, + ${MessageTable.TABLE_NAME}.${MmsSmsColumns.RECIPIENT_ID} AS $MESSAGE_RECIPIENT, snippet($MMS_FTS_TABLE_NAME, -1, '', '', '$SNIPPET_WRAP', 7) AS $SNIPPET, - ${MmsTable.TABLE_NAME}.${MmsSmsColumns.DATE_RECEIVED}, + ${MessageTable.TABLE_NAME}.${MmsSmsColumns.DATE_RECEIVED}, $MMS_FTS_TABLE_NAME.$THREAD_ID, $MMS_FTS_TABLE_NAME.$BODY, $MMS_FTS_TABLE_NAME.$ID AS $MESSAGE_ID, 1 AS $IS_MMS FROM - ${MmsTable.TABLE_NAME} - INNER JOIN $MMS_FTS_TABLE_NAME ON $MMS_FTS_TABLE_NAME.$ID = ${MmsTable.TABLE_NAME}.${MmsTable.ID} + ${MessageTable.TABLE_NAME} + INNER JOIN $MMS_FTS_TABLE_NAME ON $MMS_FTS_TABLE_NAME.$ID = ${MessageTable.TABLE_NAME}.${MessageTable.ID} INNER JOIN ${ThreadTable.TABLE_NAME} ON $MMS_FTS_TABLE_NAME.$THREAD_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID} WHERE $MMS_FTS_TABLE_NAME MATCH ? AND - ${MmsTable.TABLE_NAME}.${MmsSmsColumns.THREAD_ID} = ? + ${MessageTable.TABLE_NAME}.${MmsSmsColumns.THREAD_ID} = ? ORDER BY ${MmsSmsColumns.DATE_RECEIVED} DESC LIMIT 500 """ @@ -152,7 +98,7 @@ class SearchTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa return if (fullTextSearchQuery.isEmpty()) { null } else { - readableDatabase.rawQuery(MESSAGES_QUERY, SqlUtil.buildArgs(fullTextSearchQuery, fullTextSearchQuery)) + readableDatabase.rawQuery(MESSAGES_QUERY, SqlUtil.buildArgs(fullTextSearchQuery)) } } @@ -161,7 +107,7 @@ class SearchTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa return if (TextUtils.isEmpty(fullTextSearchQuery)) { null } else { - readableDatabase.rawQuery(MESSAGES_FOR_THREAD_QUERY, SqlUtil.buildArgs(fullTextSearchQuery, threadId, fullTextSearchQuery, threadId)) + readableDatabase.rawQuery(MESSAGES_FOR_THREAD_QUERY, SqlUtil.buildArgs(fullTextSearchQuery, threadId)) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt index 2b63d79629..56d5634283 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.kt @@ -38,8 +38,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data ), SignalDatabaseOpenHelper { - val sms: SmsTable = SmsTable(context, this) - val mms: MmsTable = MmsTable(context, this) + val mms: MessageTable = MessageTable(context, this) val attachments: AttachmentTable = AttachmentTable(context, this, attachmentSecret) val media: MediaTable = MediaTable(context, this) val thread: ThreadTable = ThreadTable(context, this) @@ -81,8 +80,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data } override fun onCreate(db: net.zetetic.database.sqlcipher.SQLiteDatabase) { - db.execSQL(SmsTable.CREATE_TABLE) - db.execSQL(MmsTable.CREATE_TABLE) + db.execSQL(MessageTable.CREATE_TABLE) db.execSQL(AttachmentTable.CREATE_TABLE) db.execSQL(ThreadTable.CREATE_TABLE) db.execSQL(IdentityTable.CREATE_TABLE) @@ -118,8 +116,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data executeStatements(db, DistributionListTables.CREATE_TABLE) executeStatements(db, RecipientTable.CREATE_INDEXS) - executeStatements(db, SmsTable.CREATE_INDEXS) - executeStatements(db, MmsTable.CREATE_INDEXS) + executeStatements(db, MessageTable.CREATE_INDEXS) executeStatements(db, AttachmentTable.CREATE_INDEXS) executeStatements(db, ThreadTable.CREATE_INDEXS) executeStatements(db, DraftTable.CREATE_INDEXS) @@ -280,7 +277,6 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data database.withinTransaction { db -> instance!!.onUpgrade(db, db.getVersion(), -1) instance!!.markCurrent(db) - instance!!.sms.deleteAbandonedMessages() instance!!.mms.deleteAbandonedMessages() instance!!.mms.trimEntriesForExpiredMessages() instance!!.reactionTable.deleteAbandonedReactions() @@ -429,7 +425,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data @get:JvmStatic @get:JvmName("mms") - val mms: MmsTable + val mms: MessageTable get() = instance!!.mms @get:JvmStatic @@ -475,8 +471,8 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data @get:JvmStatic @get:JvmName("sms") - val sms: SmsTable - get() = instance!!.sms + val sms: MessageTable + get() = instance!!.mms @get:JvmStatic @get:JvmName("threads") diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsTable.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsTable.java deleted file mode 100644 index 42dc2fd039..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsTable.java +++ /dev/null @@ -1,2062 +0,0 @@ -/* - * Copyright (C) 2011 Whisper Systems - * Copyright (C) 2013 - 2017 Open Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms.database; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import com.annimon.stream.Stream; -import com.google.android.mms.pdu_alt.NotificationInd; -import com.google.protobuf.ByteString; -import com.google.protobuf.InvalidProtocolBufferException; - -import org.signal.core.util.CursorExtensionsKt; -import org.signal.core.util.CursorUtil; -import org.signal.core.util.SQLiteDatabaseExtensionsKt; -import org.signal.core.util.SqlUtil; -import org.signal.core.util.logging.Log; -import org.signal.libsignal.protocol.util.Pair; -import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; -import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet; -import org.thoughtcrime.securesms.database.documents.NetworkFailure; -import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil; -import org.thoughtcrime.securesms.database.model.MessageExportStatus; -import org.thoughtcrime.securesms.database.model.MessageId; -import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.database.model.ParentStoryId; -import org.thoughtcrime.securesms.database.model.SmsMessageRecord; -import org.thoughtcrime.securesms.database.model.StoryResult; -import org.thoughtcrime.securesms.database.model.StoryType; -import org.thoughtcrime.securesms.database.model.StoryViewState; -import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails; -import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExportState; -import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails; -import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange; -import org.thoughtcrime.securesms.jobs.TrimThreadJob; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.mms.IncomingMediaMessage; -import org.thoughtcrime.securesms.mms.MmsException; -import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.revealable.ViewOnceExpirationInfo; -import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage; -import org.thoughtcrime.securesms.sms.IncomingTextMessage; -import org.thoughtcrime.securesms.sms.OutgoingTextMessage; -import org.thoughtcrime.securesms.util.Base64; -import org.thoughtcrime.securesms.util.JsonUtils; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.util.Util; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; - -import static org.thoughtcrime.securesms.database.MmsSmsColumns.Types.CHANGE_NUMBER_TYPE; -import static org.thoughtcrime.securesms.database.MmsSmsColumns.Types.GROUP_V2_LEAVE_BITS; - -/** - * Database for storage of SMS messages. - * - * @author Moxie Marlinspike - */ -public class SmsTable extends MessageTable { - - private static final String TAG = Log.tag(SmsTable.class); - - public static final String TABLE_NAME = "sms"; - public static final String SMS_STATUS = "status"; - - public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + - DATE_SENT + " INTEGER NOT NULL, " + - DATE_RECEIVED + " INTEGER NOT NULL, " + - DATE_SERVER + " INTEGER DEFAULT -1, " + - THREAD_ID + " INTEGER NOT NULL REFERENCES " + ThreadTable.TABLE_NAME + " (" + ThreadTable.ID + ") ON DELETE CASCADE, " + - RECIPIENT_ID + " INTEGER NOT NULL REFERENCES " + RecipientTable.TABLE_NAME + " (" + RecipientTable.ID + ") ON DELETE CASCADE, " + - RECIPIENT_DEVICE_ID + " INTEGER DEFAULT 1, " + - TYPE + " INTEGER, " + - BODY + " TEXT, " + - READ + " INTEGER DEFAULT 0, " + - SMS_STATUS + " INTEGER DEFAULT -1," + - DELIVERY_RECEIPT_COUNT + " INTEGER DEFAULT 0," + - MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " + - SMS_SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " + - EXPIRES_IN + " INTEGER DEFAULT 0, " + - EXPIRE_STARTED + " INTEGER DEFAULT 0, " + - NOTIFIED + " DEFAULT 0, " + - READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + - UNIDENTIFIED + " INTEGER DEFAULT 0, " + - REACTIONS_UNREAD + " INTEGER DEFAULT 0, " + - REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1, " + - REMOTE_DELETED + " INTEGER DEFAULT 0, " + - NOTIFIED_TIMESTAMP + " INTEGER DEFAULT 0, " + - SERVER_GUID + " TEXT DEFAULT NULL, " + - RECEIPT_TIMESTAMP + " INTEGER DEFAULT -1, " + - EXPORT_STATE + " BLOB DEFAULT NULL, " + - EXPORTED + " INTEGER DEFAULT 0);"; - - public static final String[] CREATE_INDEXS = { - "CREATE INDEX IF NOT EXISTS sms_read_and_notified_and_thread_id_index ON " + TABLE_NAME + "(" + READ + "," + NOTIFIED + "," + THREAD_ID + ");", - "CREATE INDEX IF NOT EXISTS sms_type_index ON " + TABLE_NAME + " (" + TYPE + ");", - "CREATE INDEX IF NOT EXISTS sms_date_sent_index ON " + TABLE_NAME + " (" + DATE_SENT + ", " + RECIPIENT_ID + ", " + THREAD_ID + ");", - "CREATE INDEX IF NOT EXISTS sms_date_server_index ON " + TABLE_NAME + " (" + DATE_SERVER + ");", - "CREATE INDEX IF NOT EXISTS sms_thread_date_index ON " + TABLE_NAME + " (" + THREAD_ID + ", " + DATE_RECEIVED + ");", - "CREATE INDEX IF NOT EXISTS sms_reactions_unread_index ON " + TABLE_NAME + " (" + REACTIONS_UNREAD + ");", - "CREATE INDEX IF NOT EXISTS sms_exported_index ON " + TABLE_NAME + " (" + EXPORTED + ");" - }; - - private static final String[] MESSAGE_PROJECTION = new String[] { - ID, - THREAD_ID, - RECIPIENT_ID, - RECIPIENT_DEVICE_ID, - DATE_RECEIVED, - DATE_SENT, - DATE_SERVER, - READ, - SMS_STATUS, - TYPE, - BODY, - DELIVERY_RECEIPT_COUNT, - MISMATCHED_IDENTITIES, - SMS_SUBSCRIPTION_ID, - EXPIRES_IN, - EXPIRE_STARTED, - NOTIFIED, - READ_RECEIPT_COUNT, - UNIDENTIFIED, - REACTIONS_UNREAD, - REACTIONS_LAST_SEEN, - REMOTE_DELETED, - NOTIFIED_TIMESTAMP, - RECEIPT_TIMESTAMP - }; - - @VisibleForTesting - static final long IGNORABLE_TYPESMASK_WHEN_COUNTING = Types.END_SESSION_BIT | Types.KEY_EXCHANGE_IDENTITY_UPDATE_BIT | Types.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT; - - private static final EarlyReceiptCache earlyDeliveryReceiptCache = new EarlyReceiptCache("SmsDelivery"); - - public SmsTable(Context context, SignalDatabase databaseHelper) { - super(context, databaseHelper); - } - - @Override - protected String getTableName() { - return TABLE_NAME; - } - - @Override - protected String getDateSentColumnName() { - return DATE_SENT; - } - - @Override - protected String getDateReceivedColumnName() { - return DATE_RECEIVED; - } - - @Override - protected String getTypeField() { - return TYPE; - } - - private void updateTypeBitmask(long id, long maskOff, long maskOn) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - long threadId; - - db.beginTransaction(); - try { - db.execSQL("UPDATE " + TABLE_NAME + - " SET " + TYPE + " = (" + TYPE + " & " + (Types.TOTAL_MASK - maskOff) + " | " + maskOn + " )" + - " WHERE " + ID + " = ?", SqlUtil.buildArgs(id)); - - threadId = getThreadIdForMessage(id); - - SignalDatabase.threads().updateSnippetTypeSilently(threadId); - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(id, false)); - ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners(); - } - - @Override - public @Nullable RecipientId getOldestGroupUpdateSender(long threadId, long minimumDateReceived) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - - String[] columns = new String[]{RECIPIENT_ID}; - String query = THREAD_ID + " = ? AND " + TYPE + " & ? AND " + DATE_RECEIVED + " >= ?"; - long type = Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT | Types.GROUP_UPDATE_BIT | Types.BASE_INBOX_TYPE; - String[] args = new String[]{String.valueOf(threadId), String.valueOf(type), String.valueOf(minimumDateReceived)}; - String limit = "1"; - - try (Cursor cursor = db.query(TABLE_NAME, columns, query, args, null, null, limit)) { - if (cursor.moveToFirst()) { - return RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); - } - } - - return null; - } - - @Override - public long getThreadIdForMessage(long id) { - try (Cursor cursor = databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, THREAD_ID_PROJECTION, ID_WHERE, SqlUtil.buildArgs(id), null, null, null)) { - if (cursor.moveToFirst()) { - return CursorUtil.requireLong(cursor, THREAD_ID); - } - } - return -1; - } - - @Override - public int getMessageCountForThread(long threadId) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - - try (Cursor cursor = db.query(TABLE_NAME, COUNT, THREAD_ID_WHERE, SqlUtil.buildArgs(threadId), null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - return cursor.getInt(0); - } - } - - return 0; - } - - @Override - public int getMessageCountForThread(long threadId, long beforeTime) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - - String[] cols = new String[] {"COUNT(*)"}; - String query = THREAD_ID + " = ? AND " + DATE_RECEIVED + " < ?"; - String[] args = new String[]{String.valueOf(threadId), String.valueOf(beforeTime)}; - - try (Cursor cursor = db.query(TABLE_NAME, cols, query, args, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - return cursor.getInt(0); - } - } - - return 0; - } - - @Override - public boolean hasMeaningfulMessage(long threadId) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - SqlUtil.Query query = buildMeaningfulMessagesQuery(threadId); - - try (Cursor cursor = db.query(TABLE_NAME, new String[] { "1" }, query.getWhere(), query.getWhereArgs(), null, null, null, "1")) { - return cursor != null && cursor.moveToFirst(); - } - } - - @Override - public int getIncomingMeaningfulMessageCountSince(long threadId, long afterTime) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String[] projection = SqlUtil.COUNT; - SqlUtil.Query meaningfulMessagesQuery = buildMeaningfulMessagesQuery(threadId); - String where = meaningfulMessagesQuery.getWhere() + " AND " + DATE_RECEIVED + " >= ?"; - String[] whereArgs = SqlUtil.appendArg(meaningfulMessagesQuery.getWhereArgs(), String.valueOf(afterTime)); - - try (Cursor cursor = db.query(TABLE_NAME, projection, where, whereArgs, null, null, null, "1")) { - if (cursor != null && cursor.moveToFirst()) { - return cursor.getInt(0); - } else { - return 0; - } - } - } - - private @NonNull SqlUtil.Query buildMeaningfulMessagesQuery(long threadId) { - String query = THREAD_ID + " = ? AND (NOT " + TYPE + " & ? AND " + TYPE + " != ? AND " + TYPE + " != ? AND " + TYPE + " != ? AND " + TYPE + " != ? AND " + TYPE + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + ")"; - return SqlUtil.buildQuery(query, threadId, IGNORABLE_TYPESMASK_WHEN_COUNTING, Types.PROFILE_CHANGE_TYPE, Types.CHANGE_NUMBER_TYPE, Types.SMS_EXPORT_TYPE, Types.BOOST_REQUEST_TYPE); - } - - @Override - public void markAsEndSession(long id) { - updateTypeBitmask(id, Types.KEY_EXCHANGE_MASK, Types.END_SESSION_BIT); - } - - @Override - public void markAsInvalidVersionKeyExchange(long id) { - updateTypeBitmask(id, 0, Types.KEY_EXCHANGE_INVALID_VERSION_BIT); - } - - @Override - public void markAsSecure(long id) { - updateTypeBitmask(id, 0, Types.SECURE_MESSAGE_BIT); - } - - @Override - public void markAsInsecure(long id) { - updateTypeBitmask(id, Types.SECURE_MESSAGE_BIT, 0); - } - - @Override - public void markAsPush(long id) { - updateTypeBitmask(id, 0, Types.PUSH_MESSAGE_BIT); - } - - @Override - public void markAsForcedSms(long id) { - updateTypeBitmask(id, Types.PUSH_MESSAGE_BIT, Types.MESSAGE_FORCE_SMS_BIT); - } - - @Override - public void markAsRateLimited(long id) { - updateTypeBitmask(id, 0, Types.MESSAGE_RATE_LIMITED_BIT); - } - - @Override - public void clearRateLimitStatus(@NonNull Collection ids) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - db.beginTransaction(); - try { - for (long id : ids) { - updateTypeBitmask(id, Types.MESSAGE_RATE_LIMITED_BIT, 0); - } - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - @Override - public void markAsDecryptFailed(long id) { - updateTypeBitmask(id, Types.ENCRYPTION_MASK, Types.ENCRYPTION_REMOTE_FAILED_BIT); - } - - @Override - public void markAsNoSession(long id) { - updateTypeBitmask(id, Types.ENCRYPTION_MASK, Types.ENCRYPTION_REMOTE_NO_SESSION_BIT); - } - - @Override - public void markAsUnsupportedProtocolVersion(long id) { - updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.UNSUPPORTED_MESSAGE_TYPE); - } - - @Override - public void markAsInvalidMessage(long id) { - updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.INVALID_MESSAGE_TYPE); - } - - @Override - public void markAsLegacyVersion(long id) { - updateTypeBitmask(id, Types.ENCRYPTION_MASK, Types.ENCRYPTION_REMOTE_LEGACY_BIT); - } - - @Override - public void markAsOutbox(long id) { - updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_OUTBOX_TYPE); - } - - @Override - public void markAsPendingInsecureSmsFallback(long id) { - updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_PENDING_INSECURE_SMS_FALLBACK); - } - - @Override - public void markAsSent(long id, boolean isSecure) { - updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENT_TYPE | (isSecure ? Types.PUSH_MESSAGE_BIT | Types.SECURE_MESSAGE_BIT : 0)); - } - - @Override - public void markAsSending(long id) { - updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENDING_TYPE); - } - - @Override - public void markAsMissedCall(long id, boolean isVideoOffer) { - updateTypeBitmask(id, Types.TOTAL_MASK, isVideoOffer ? Types.MISSED_VIDEO_CALL_TYPE : Types.MISSED_AUDIO_CALL_TYPE); - } - - @Override - public void markAsRemoteDelete(long id) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - long threadId; - - db.beginTransaction(); - try { - ContentValues values = new ContentValues(); - values.put(REMOTE_DELETED, 1); - values.putNull(BODY); - db.update(TABLE_NAME, values, ID_WHERE, new String[] { String.valueOf(id) }); - - threadId = getThreadIdForMessage(id); - - SignalDatabase.reactions().deleteReactions(new MessageId(id, false)); - SignalDatabase.threads().update(threadId, false); - SignalDatabase.messageLog().deleteAllRelatedToMessage(id, false); - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - notifyConversationListeners(threadId); - } - - @Override - public void markUnidentified(long id, boolean unidentified) { - ContentValues contentValues = new ContentValues(1); - contentValues.put(UNIDENTIFIED, unidentified ? 1 : 0); - - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(id)}); - } - - @Override - public void markExpireStarted(long id) { - markExpireStarted(id, System.currentTimeMillis()); - } - - @Override - public void markExpireStarted(long id, long startedAtTimestamp) { - markExpireStarted(Collections.singleton(id), startedAtTimestamp); - } - - @Override - public void markExpireStarted(Collection ids, long startedAtTimestamp) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - long threadId = -1; - - db.beginTransaction(); - try { - String query = ID + " = ? AND (" + EXPIRE_STARTED + " = 0 OR " + EXPIRE_STARTED + " > ?)"; - - for (long id : ids) { - ContentValues contentValues = new ContentValues(); - contentValues.put(EXPIRE_STARTED, startedAtTimestamp); - - db.update(TABLE_NAME, contentValues, query, new String[]{String.valueOf(id), String.valueOf(startedAtTimestamp)}); - - if (threadId < 0) { - threadId = getThreadIdForMessage(id); - } - } - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - SignalDatabase.threads().update(threadId, false); - notifyConversationListeners(threadId); - } - - @Override - public void markSmsStatus(long id, int status) { - Log.i(TAG, "Updating ID: " + id + " to status: " + status); - ContentValues contentValues = new ContentValues(); - contentValues.put(SMS_STATUS, status); - - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {id+""}); - - long threadId = getThreadIdForMessage(id); - SignalDatabase.threads().update(threadId, false); - notifyConversationListeners(threadId); - } - - @Override - public void markAsSentFailed(long id) { - updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENT_FAILED_TYPE); - } - - @Override - public void markAsNotified(long id) { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - ContentValues contentValues = new ContentValues(); - - contentValues.put(NOTIFIED, 1); - contentValues.put(REACTIONS_LAST_SEEN, System.currentTimeMillis()); - - database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(id)}); - } - - @Override - public @NonNull Set incrementReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType, @NonNull MessageQualifier messageQualifier) { - if (messageQualifier == MessageQualifier.STORY) { - return Collections.emptySet(); - } - - if (receiptType == ReceiptType.VIEWED) { - return Collections.emptySet(); - } - - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - Set messageUpdates = new HashSet<>(); - - try (Cursor cursor = database.query(TABLE_NAME, new String[] {ID, THREAD_ID, RECIPIENT_ID, TYPE, DELIVERY_RECEIPT_COUNT, READ_RECEIPT_COUNT, RECEIPT_TIMESTAMP}, - DATE_SENT + " = ?", new String[] {String.valueOf(messageId.getTimetamp())}, - null, null, null, null)) - { - while (cursor.moveToNext()) { - if (Types.isOutgoingMessageType(CursorUtil.requireLong(cursor, TYPE))) { - RecipientId theirRecipientId = messageId.getRecipientId(); - RecipientId outRecipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); - - if (outRecipientId.equals(theirRecipientId)) { - long id = CursorUtil.requireLong(cursor, ID); - long threadId = CursorUtil.requireLong(cursor, THREAD_ID); - String columnName = receiptType.getColumnName(); - boolean isFirstIncrement = CursorUtil.requireLong(cursor, columnName) == 0; - long savedTimestamp = CursorUtil.requireLong(cursor, RECEIPT_TIMESTAMP); - long updatedTimestamp = isFirstIncrement ? Math.max(savedTimestamp, timestamp) : savedTimestamp; - - database.execSQL("UPDATE " + TABLE_NAME + - " SET " + columnName + " = " + columnName + " + 1, " + - RECEIPT_TIMESTAMP + " = ? WHERE " + - ID + " = ?", - SqlUtil.buildArgs(updatedTimestamp, id)); - - messageUpdates.add(new MessageUpdate(threadId, new MessageId(id, false))); - } - } - } - - if (messageUpdates.isEmpty() && receiptType == ReceiptType.DELIVERY) { - earlyDeliveryReceiptCache.increment(messageId.getTimetamp(), messageId.getRecipientId(), timestamp); - } - - return messageUpdates; - } - } - - @Override - public List setEntireThreadRead(long threadId) { - return setMessagesRead(THREAD_ID + " = ?", new String[] {String.valueOf(threadId)}); - } - - @Override - public List setMessagesReadSince(long threadId, long sinceTimestamp) { - if (sinceTimestamp == -1) { - return setMessagesRead(THREAD_ID + " = ? AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND (" + getOutgoingTypeClause() + ")))", new String[] {String.valueOf(threadId)}); - } else { - return setMessagesRead(THREAD_ID + " = ? AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND ( " + getOutgoingTypeClause() + " ))) AND " + DATE_RECEIVED + " <= ?", new String[] {String.valueOf(threadId),String.valueOf(sinceTimestamp)}); - } - } - - @Override - public List setAllMessagesRead() { - return setMessagesRead(READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND (" + getOutgoingTypeClause() + "))", null); - } - - private List setMessagesRead(String where, String[] arguments) { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - List results = new LinkedList<>(); - Cursor cursor = null; - RecipientId releaseChannelId = SignalStore.releaseChannelValues().getReleaseChannelRecipientId(); - - database.beginTransaction(); - try { - cursor = database.query(TABLE_NAME, new String[] {ID, RECIPIENT_ID, DATE_SENT, TYPE, EXPIRES_IN, EXPIRE_STARTED, THREAD_ID}, where, arguments, null, null, null); - - while (cursor != null && cursor.moveToNext()) { - if (Types.isSecureType(CursorUtil.requireLong(cursor, TYPE))) { - long threadId = CursorUtil.requireLong(cursor, THREAD_ID); - RecipientId recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); - long dateSent = CursorUtil.requireLong(cursor, DATE_SENT); - long messageId = CursorUtil.requireLong(cursor, ID); - long expiresIn = CursorUtil.requireLong(cursor, EXPIRES_IN); - long expireStarted = CursorUtil.requireLong(cursor, EXPIRE_STARTED); - SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent); - ExpirationInfo expirationInfo = new ExpirationInfo(messageId, expiresIn, expireStarted, false); - - if (!recipientId.equals(releaseChannelId)) { - results.add(new MarkedMessageInfo(threadId, syncMessageId, new MessageId(messageId, false), expirationInfo)); - } - } - } - - ContentValues contentValues = new ContentValues(); - contentValues.put(READ, 1); - contentValues.put(REACTIONS_UNREAD, 0); - contentValues.put(REACTIONS_LAST_SEEN, System.currentTimeMillis()); - - database.update(TABLE_NAME, contentValues, where, arguments); - database.setTransactionSuccessful(); - } finally { - if (cursor != null) cursor.close(); - database.endTransaction(); - } - - return results; - } - - @Override - public InsertResult updateBundleMessageBody(long messageId, String body) { - long type = Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT; - return updateMessageBodyAndType(messageId, body, Types.TOTAL_MASK, type); - } - - @Override - public @NonNull List getViewedIncomingMessages(long threadId) { - return Collections.emptyList(); - } - - @Override - public @Nullable MarkedMessageInfo setIncomingMessageViewed(long messageId) { - return null; - } - - @Override - public @NonNull List setIncomingMessagesViewed(@NonNull List messageIds) { - return Collections.emptyList(); - } - - @Override - public @NonNull List setOutgoingGiftsRevealed(@NonNull List messageIds) { - throw new UnsupportedOperationException(); - } - - private InsertResult updateMessageBodyAndType(long messageId, String body, long maskOff, long maskOn) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - db.execSQL("UPDATE " + TABLE_NAME + " SET " + BODY + " = ?, " + - TYPE + " = (" + TYPE + " & " + (Types.TOTAL_MASK - maskOff) + " | " + maskOn + ") " + - "WHERE " + ID + " = ?", - new String[] {body, messageId + ""}); - - long threadId = getThreadIdForMessage(messageId); - - SignalDatabase.threads().update(threadId, true); - notifyConversationListeners(threadId); - - return new InsertResult(messageId, threadId); - } - - @Override - public boolean hasReceivedAnyCallsSince(long threadId, long timestamp) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String[] projection = SqlUtil.buildArgs(SmsTable.TYPE); - String selection = THREAD_ID + " = ? AND " + DATE_RECEIVED + " > ? AND (" + TYPE + " = ? OR " + TYPE + " = ? OR " + TYPE + " = ? OR " + TYPE + " =?)"; - String[] selectionArgs = SqlUtil.buildArgs(threadId, - timestamp, - Types.INCOMING_AUDIO_CALL_TYPE, - Types.INCOMING_VIDEO_CALL_TYPE, - Types.MISSED_AUDIO_CALL_TYPE, - Types.MISSED_VIDEO_CALL_TYPE); - - try (Cursor cursor = db.query(TABLE_NAME, projection, selection, selectionArgs, null, null, null)) { - return cursor != null && cursor.moveToFirst(); - } - } - - @Override - public @NonNull Pair insertReceivedCall(@NonNull RecipientId address, boolean isVideoOffer) { - return insertCallLog(address, isVideoOffer ? Types.INCOMING_VIDEO_CALL_TYPE : Types.INCOMING_AUDIO_CALL_TYPE, false, System.currentTimeMillis()); - } - - @Override - public @NonNull Pair insertOutgoingCall(@NonNull RecipientId address, boolean isVideoOffer) { - return insertCallLog(address, isVideoOffer ? Types.OUTGOING_VIDEO_CALL_TYPE : Types.OUTGOING_AUDIO_CALL_TYPE, false, System.currentTimeMillis()); - } - - @Override - public @NonNull Pair insertMissedCall(@NonNull RecipientId address, long timestamp, boolean isVideoOffer) { - return insertCallLog(address, isVideoOffer ? Types.MISSED_VIDEO_CALL_TYPE : Types.MISSED_AUDIO_CALL_TYPE, true, timestamp); - } - - @Override - public void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId, - @NonNull RecipientId sender, - long timestamp, - @Nullable String peekGroupCallEraId, - @NonNull Collection peekJoinedUuids, - boolean isCallFull) - { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - Recipient recipient = Recipient.resolved(groupRecipientId); - long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); - boolean peerEraIdSameAsPrevious = updatePreviousGroupCall(threadId, peekGroupCallEraId, peekJoinedUuids, isCallFull); - - try { - db.beginTransaction(); - - if (!peerEraIdSameAsPrevious && !Util.isEmpty(peekGroupCallEraId)) { - Recipient self = Recipient.self(); - boolean markRead = peekJoinedUuids.contains(self.requireServiceId().uuid()) || self.getId().equals(sender); - - byte[] updateDetails = GroupCallUpdateDetails.newBuilder() - .setEraId(Util.emptyIfNull(peekGroupCallEraId)) - .setStartedCallUuid(Recipient.resolved(sender).requireServiceId().toString()) - .setStartedCallTimestamp(timestamp) - .addAllInCallUuids(Stream.of(peekJoinedUuids).map(UUID::toString).toList()) - .setIsCallFull(isCallFull) - .build() - .toByteArray(); - - String body = Base64.encodeBytes(updateDetails); - - ContentValues values = new ContentValues(); - values.put(RECIPIENT_ID, sender.serialize()); - values.put(RECIPIENT_DEVICE_ID, 1); - values.put(DATE_RECEIVED, timestamp); - values.put(DATE_SENT, timestamp); - values.put(READ, markRead ? 1 : 0); - values.put(BODY, body); - values.put(TYPE, Types.GROUP_CALL_TYPE); - values.put(THREAD_ID, threadId); - - db.insert(TABLE_NAME, null, values); - - SignalDatabase.threads().incrementUnread(threadId, 1, 0); - } - boolean keepThreadArchived = SignalStore.settings().shouldKeepMutedChatsArchived() && recipient.isMuted(); - SignalDatabase.threads().update(threadId, !keepThreadArchived); - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - notifyConversationListeners(threadId); - TrimThreadJob.enqueueAsync(threadId); - } - - @Override - public void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId, - @NonNull RecipientId sender, - long timestamp, - @Nullable String messageGroupCallEraId) - { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - long threadId; - - try { - db.beginTransaction(); - - Recipient recipient = Recipient.resolved(groupRecipientId); - - threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); - - String where = TYPE + " = ? AND " + THREAD_ID + " = ?"; - String[] args = SqlUtil.buildArgs(Types.GROUP_CALL_TYPE, threadId); - boolean sameEraId = false; - - try (Reader reader = new Reader(db.query(TABLE_NAME, MESSAGE_PROJECTION, where, args, null, null, DATE_RECEIVED + " DESC", "1"))) { - MessageRecord record = reader.getNext(); - if (record != null) { - GroupCallUpdateDetails groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(record.getBody()); - - sameEraId = groupCallUpdateDetails.getEraId().equals(messageGroupCallEraId) && !Util.isEmpty(messageGroupCallEraId); - - if (!sameEraId) { - String body = GroupCallUpdateDetailsUtil.createUpdatedBody(groupCallUpdateDetails, Collections.emptyList(), false); - - ContentValues contentValues = new ContentValues(); - contentValues.put(BODY, body); - - db.update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(record.getId())); - } - } - } - - if (!sameEraId && !Util.isEmpty(messageGroupCallEraId)) { - byte[] updateDetails = GroupCallUpdateDetails.newBuilder() - .setEraId(Util.emptyIfNull(messageGroupCallEraId)) - .setStartedCallUuid(Recipient.resolved(sender).requireServiceId().toString()) - .setStartedCallTimestamp(timestamp) - .addAllInCallUuids(Collections.emptyList()) - .setIsCallFull(false) - .build() - .toByteArray(); - - String body = Base64.encodeBytes(updateDetails); - - ContentValues values = new ContentValues(); - values.put(RECIPIENT_ID, sender.serialize()); - values.put(RECIPIENT_DEVICE_ID, 1); - values.put(DATE_RECEIVED, timestamp); - values.put(DATE_SENT, timestamp); - values.put(READ, 0); - values.put(BODY, body); - values.put(TYPE, Types.GROUP_CALL_TYPE); - values.put(THREAD_ID, threadId); - - db.insert(TABLE_NAME, null, values); - - SignalDatabase.threads().incrementUnread(threadId, 1, 0); - } - - final boolean keepThreadArchived = SignalStore.settings().shouldKeepMutedChatsArchived() && recipient.isMuted(); - SignalDatabase.threads().update(threadId, !keepThreadArchived); - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - notifyConversationListeners(threadId); - TrimThreadJob.enqueueAsync(threadId); - } - - @Override - public boolean updatePreviousGroupCall(long threadId, @Nullable String peekGroupCallEraId, @NonNull Collection peekJoinedUuids, boolean isCallFull) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - String where = TYPE + " = ? AND " + THREAD_ID + " = ?"; - String[] args = SqlUtil.buildArgs(Types.GROUP_CALL_TYPE, threadId); - boolean sameEraId = false; - - try (Reader reader = new Reader(db.query(TABLE_NAME, MESSAGE_PROJECTION, where, args, null, null, DATE_RECEIVED + " DESC", "1"))) { - MessageRecord record = reader.getNext(); - if (record == null) { - return false; - } - - GroupCallUpdateDetails groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(record.getBody()); - boolean containsSelf = peekJoinedUuids.contains(SignalStore.account().requireAci().uuid()); - - sameEraId = groupCallUpdateDetails.getEraId().equals(peekGroupCallEraId) && !Util.isEmpty(peekGroupCallEraId); - - List inCallUuids = sameEraId ? Stream.of(peekJoinedUuids).map(UUID::toString).toList() - : Collections.emptyList(); - - String body = GroupCallUpdateDetailsUtil.createUpdatedBody(groupCallUpdateDetails, inCallUuids, isCallFull); - - ContentValues contentValues = new ContentValues(); - contentValues.put(BODY, body); - - if (sameEraId && containsSelf) { - contentValues.put(READ, 1); - } - - SqlUtil.Query query = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(record.getId()), contentValues); - boolean updated = db.update(TABLE_NAME, contentValues, query.getWhere(), query.getWhereArgs()) > 0; - - if (updated) { - notifyConversationListeners(threadId); - } - } - - return sameEraId; - } - - private @NonNull Pair insertCallLog(@NonNull RecipientId recipientId, long type, boolean unread, long timestamp) { - Recipient recipient = Recipient.resolved(recipientId); - long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); - - ContentValues values = new ContentValues(6); - values.put(RECIPIENT_ID, recipientId.serialize()); - values.put(RECIPIENT_DEVICE_ID, 1); - values.put(DATE_RECEIVED, System.currentTimeMillis()); - values.put(DATE_SENT, timestamp); - values.put(READ, unread ? 0 : 1); - values.put(TYPE, type); - values.put(THREAD_ID, threadId); - - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - long messageId = db.insert(TABLE_NAME, null, values); - - if (unread) { - SignalDatabase.threads().incrementUnread(threadId, 1, 0); - } - boolean keepThreadArchived = SignalStore.settings().shouldKeepMutedChatsArchived() && Recipient.resolved(recipientId).isMuted(); - SignalDatabase.threads().update(threadId, !keepThreadArchived); - - notifyConversationListeners(threadId); - TrimThreadJob.enqueueAsync(threadId); - - return new Pair<>(messageId, threadId); - } - - @Override - public Set getAllRateLimitedMessageIds() { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String where = "(" + TYPE + " & " + Types.TOTAL_MASK + " & " + Types.MESSAGE_RATE_LIMITED_BIT + ") > 0"; - - Set ids = new HashSet<>(); - - try (Cursor cursor = db.query(TABLE_NAME, new String[] { ID }, where, null, null, null, null)) { - while (cursor.moveToNext()) { - ids.add(CursorUtil.requireLong(cursor, ID)); - } - } - - return ids; - } - - @Override - public Cursor getUnexportedInsecureMessages(int limit) { - return queryMessages( - SqlUtil.appendArg(MESSAGE_PROJECTION, EXPORT_STATE), - getInsecureMessageClause() + " AND NOT " + EXPORTED, - null, - false, - limit - ); - } - - @Override - public long getUnexportedInsecureMessagesEstimatedSize() { - Cursor cursor = SQLiteDatabaseExtensionsKt.select(getReadableDatabase(), "SUM(LENGTH(" + BODY + "))") - .from(TABLE_NAME) - .where(getInsecureMessageClause() + " AND " + EXPORTED + " < ?", MessageExportStatus.EXPORTED) - .run(); - - return CursorExtensionsKt.readToSingleLong(cursor); - } - - @Override - public void deleteExportedMessages() { - beginTransaction(); - try { - List threadsToUpdate = new LinkedList<>(); - try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, THREAD_ID_PROJECTION, EXPORTED + " = ?", SqlUtil.buildArgs(MessageExportStatus.EXPORTED), THREAD_ID, null, null, null)) { - while (cursor.moveToNext()) { - threadsToUpdate.add(CursorUtil.requireLong(cursor, THREAD_ID)); - } - } - - getWritableDatabase().delete(TABLE_NAME, EXPORTED + " = ?", SqlUtil.buildArgs(MessageExportStatus.EXPORTED)); - - for (final long threadId : threadsToUpdate) { - SignalDatabase.threads().update(threadId, false); - } - - setTransactionSuccessful(); - } finally { - endTransaction(); - } - } - - @Override - public List getProfileChangeDetailsRecords(long threadId, long afterTimestamp) { - String where = THREAD_ID + " = ? AND " + DATE_RECEIVED + " >= ? AND " + TYPE + " = ?"; - String[] args = SqlUtil.buildArgs(threadId, afterTimestamp, Types.PROFILE_CHANGE_TYPE); - - try (Reader reader = readerFor(queryMessages(where, args, true, -1))) { - List results = new ArrayList<>(reader.getCount()); - while (reader.getNext() != null) { - results.add(reader.getCurrent()); - } - - return results; - } - } - - @Override - public void insertProfileNameChangeMessages(@NonNull Recipient recipient, @NonNull String newProfileName, @NonNull String previousProfileName) { - ThreadTable threadTable = SignalDatabase.threads(); - List groupRecords = SignalDatabase.groups().getGroupsContainingMember(recipient.getId(), false); - List threadIdsToUpdate = new LinkedList<>(); - - byte[] profileChangeDetails = ProfileChangeDetails.newBuilder() - .setProfileNameChange(ProfileChangeDetails.StringChange.newBuilder() - .setNew(newProfileName) - .setPrevious(previousProfileName)) - .build() - .toByteArray(); - - String body = Base64.encodeBytes(profileChangeDetails); - - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - db.beginTransaction(); - - try { - threadIdsToUpdate.add(threadTable.getThreadIdFor(recipient.getId())); - for (GroupTable.GroupRecord groupRecord : groupRecords) { - if (groupRecord.isActive()) { - threadIdsToUpdate.add(threadTable.getThreadIdFor(groupRecord.getRecipientId())); - } - } - - Stream.of(threadIdsToUpdate) - .withoutNulls() - .forEach(threadId -> { - ContentValues values = new ContentValues(); - values.put(RECIPIENT_ID, recipient.getId().serialize()); - values.put(RECIPIENT_DEVICE_ID, 1); - values.put(DATE_RECEIVED, System.currentTimeMillis()); - values.put(DATE_SENT, System.currentTimeMillis()); - values.put(READ, 1); - values.put(TYPE, Types.PROFILE_CHANGE_TYPE); - values.put(THREAD_ID, threadId); - values.put(BODY, body); - - db.insert(TABLE_NAME, null, values); - - notifyConversationListeners(threadId); - }); - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - Stream.of(threadIdsToUpdate) - .withoutNulls() - .forEach(TrimThreadJob::enqueueAsync); - } - - @Override - public void insertGroupV1MigrationEvents(@NonNull RecipientId recipientId, - long threadId, - @NonNull GroupMigrationMembershipChange membershipChange) - { - insertGroupV1MigrationNotification(recipientId, threadId); - - if (!membershipChange.isEmpty()) { - insertGroupV1MigrationMembershipChanges(recipientId, threadId, membershipChange); - } - - notifyConversationListeners(threadId); - TrimThreadJob.enqueueAsync(threadId); - } - - private void insertGroupV1MigrationNotification(@NonNull RecipientId recipientId, long threadId) { - insertGroupV1MigrationMembershipChanges(recipientId, threadId, GroupMigrationMembershipChange.empty()); - } - - private void insertGroupV1MigrationMembershipChanges(@NonNull RecipientId recipientId, - long threadId, - @NonNull GroupMigrationMembershipChange membershipChange) - { - ContentValues values = new ContentValues(); - values.put(RECIPIENT_ID, recipientId.serialize()); - values.put(RECIPIENT_DEVICE_ID, 1); - values.put(DATE_RECEIVED, System.currentTimeMillis()); - values.put(DATE_SENT, System.currentTimeMillis()); - values.put(READ, 1); - values.put(TYPE, Types.GV1_MIGRATION_TYPE); - values.put(THREAD_ID, threadId); - - if (!membershipChange.isEmpty()) { - values.put(BODY, membershipChange.serialize()); - } - - databaseHelper.getSignalWritableDatabase().insert(TABLE_NAME, null, values); - } - - @Override - public void insertNumberChangeMessages(@NonNull RecipientId recipientId) { - ThreadTable threadTable = SignalDatabase.threads(); - List groupRecords = SignalDatabase.groups().getGroupsContainingMember(recipientId, false); - List threadIdsToUpdate = new LinkedList<>(); - - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - db.beginTransaction(); - - try { - threadIdsToUpdate.add(threadTable.getThreadIdFor(recipientId)); - for (GroupTable.GroupRecord groupRecord : groupRecords) { - if (groupRecord.isActive()) { - threadIdsToUpdate.add(threadTable.getThreadIdFor(groupRecord.getRecipientId())); - } - } - - threadIdsToUpdate.stream() - .filter(Objects::nonNull) - .forEach(threadId -> { - ContentValues values = new ContentValues(); - values.put(RECIPIENT_ID, recipientId.serialize()); - values.put(RECIPIENT_DEVICE_ID, 1); - values.put(DATE_RECEIVED, System.currentTimeMillis()); - values.put(DATE_SENT, System.currentTimeMillis()); - values.put(READ, 1); - values.put(TYPE, Types.CHANGE_NUMBER_TYPE); - values.put(THREAD_ID, threadId); - values.putNull(BODY); - - db.insert(TABLE_NAME, null, values); - }); - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - threadIdsToUpdate.stream() - .filter(Objects::nonNull) - .forEach(threadId -> { - TrimThreadJob.enqueueAsync(threadId); - SignalDatabase.threads().update(threadId, true); - notifyConversationListeners(threadId); - }); - } - - @Override - public void insertBoostRequestMessage(@NonNull RecipientId recipientId, long threadId) { - ContentValues values = new ContentValues(); - values.put(RECIPIENT_ID, recipientId.serialize()); - values.put(RECIPIENT_DEVICE_ID, 1); - values.put(DATE_RECEIVED, System.currentTimeMillis()); - values.put(DATE_SENT, System.currentTimeMillis()); - values.put(READ, 1); - values.put(TYPE, Types.BOOST_REQUEST_TYPE); - values.put(THREAD_ID, threadId); - values.putNull(BODY); - - getWritableDatabase().insert(TABLE_NAME, null, values); - } - - @Override - public void insertThreadMergeEvent(@NonNull RecipientId recipientId, long threadId, @NonNull ThreadMergeEvent event) { - ContentValues values = new ContentValues(); - values.put(RECIPIENT_ID, recipientId.serialize()); - values.put(RECIPIENT_DEVICE_ID, 1); - values.put(DATE_RECEIVED, System.currentTimeMillis()); - values.put(DATE_SENT, System.currentTimeMillis()); - values.put(READ, 1); - values.put(TYPE, Types.THREAD_MERGE_TYPE); - values.put(THREAD_ID, threadId); - values.put(BODY, Base64.encodeBytes(event.toByteArray())); - - getWritableDatabase().insert(TABLE_NAME, null, values); - - ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId); - } - - @Override - public void insertSmsExportMessage(@NonNull RecipientId recipientId, long threadId) { - ContentValues values = new ContentValues(); - values.put(RECIPIENT_ID, recipientId.serialize()); - values.put(RECIPIENT_DEVICE_ID, 1); - values.put(DATE_RECEIVED, System.currentTimeMillis()); - values.put(DATE_SENT, System.currentTimeMillis()); - values.put(READ, 1); - values.put(TYPE, Types.SMS_EXPORT_TYPE); - values.put(THREAD_ID, threadId); - values.putNull(BODY); - - boolean updated = SQLiteDatabaseExtensionsKt.withinTransaction(getWritableDatabase(), db -> { - if (SignalDatabase.sms().hasSmsExportMessage(threadId)) { - return false; - } else { - db.insert(TABLE_NAME, null, values); - return true; - } - }); - - if (updated) { - ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId); - } - } - - @Override - public Optional insertMessageInbox(IncomingTextMessage message, long type) { - boolean tryToCollapseJoinRequestEvents = false; - - if (message.isJoined()) { - type = (type & (Types.TOTAL_MASK - Types.BASE_TYPE_MASK)) | Types.JOINED_TYPE; - } else if (message.isPreKeyBundle()) { - type |= Types.KEY_EXCHANGE_BIT | Types.KEY_EXCHANGE_BUNDLE_BIT; - } else if (message.isSecureMessage()) { - type |= Types.SECURE_MESSAGE_BIT; - } else if (message.isGroup()) { - IncomingGroupUpdateMessage incomingGroupUpdateMessage = (IncomingGroupUpdateMessage) message; - - type |= Types.SECURE_MESSAGE_BIT; - - if (incomingGroupUpdateMessage.isGroupV2()) { - type |= Types.GROUP_V2_BIT | Types.GROUP_UPDATE_BIT; - if (incomingGroupUpdateMessage.isJustAGroupLeave()) { - type |= Types.GROUP_LEAVE_BIT; - } else if (incomingGroupUpdateMessage.isCancelJoinRequest()) { - tryToCollapseJoinRequestEvents = true; - } - } else if (incomingGroupUpdateMessage.isUpdate()) { - type |= Types.GROUP_UPDATE_BIT; - } else if (incomingGroupUpdateMessage.isQuit()) { - type |= Types.GROUP_LEAVE_BIT; - } - - } else if (message.isEndSession()) { - type |= Types.SECURE_MESSAGE_BIT; - type |= Types.END_SESSION_BIT; - } - - if (message.isPush()) type |= Types.PUSH_MESSAGE_BIT; - if (message.isIdentityUpdate()) type |= Types.KEY_EXCHANGE_IDENTITY_UPDATE_BIT; - if (message.isContentPreKeyBundle()) type |= Types.KEY_EXCHANGE_CONTENT_FORMAT; - - if (message.isIdentityVerified()) type |= Types.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT; - else if (message.isIdentityDefault()) type |= Types.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT; - - Recipient recipient = Recipient.resolved(message.getSender()); - - Recipient groupRecipient; - - if (message.getGroupId() == null) { - groupRecipient = null; - } else { - RecipientId id = SignalDatabase.recipients().getOrInsertFromPossiblyMigratedGroupId(message.getGroupId()); - groupRecipient = Recipient.resolved(id); - } - - boolean silent = message.isIdentityUpdate() || - message.isIdentityVerified() || - message.isIdentityDefault() || - message.isJustAGroupLeave() || - (type & Types.GROUP_UPDATE_BIT) > 0; - - boolean unread = !silent && (Util.isDefaultSmsProvider(context) || - message.isSecureMessage() || - message.isGroup() || - message.isPreKeyBundle()); - - long threadId; - - if (groupRecipient == null) threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); - else threadId = SignalDatabase.threads().getOrCreateThreadIdFor(groupRecipient); - - if (tryToCollapseJoinRequestEvents) { - final Optional result = collapseJoinRequestEventsIfPossible(threadId, (IncomingGroupUpdateMessage) message); - if (result.isPresent()) { - return result; - } - } - - ContentValues values = new ContentValues(); - values.put(RECIPIENT_ID, message.getSender().serialize()); - values.put(RECIPIENT_DEVICE_ID, message.getSenderDeviceId()); - values.put(DATE_RECEIVED, message.getReceivedTimestampMillis()); - values.put(DATE_SENT, message.getSentTimestampMillis()); - values.put(DATE_SERVER, message.getServerTimestampMillis()); - values.put(READ, unread ? 0 : 1); - values.put(SMS_SUBSCRIPTION_ID, message.getSubscriptionId()); - values.put(EXPIRES_IN, message.getExpiresIn()); - values.put(UNIDENTIFIED, message.isUnidentified()); - values.put(BODY, message.getMessageBody()); - values.put(TYPE, type); - values.put(THREAD_ID, threadId); - values.put(SERVER_GUID, message.getServerGuid()); - - if (message.isPush() && isDuplicate(message, threadId)) { - Log.w(TAG, "Duplicate message (" + message.getSentTimestampMillis() + "), ignoring..."); - return Optional.empty(); - } else { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - long messageId = db.insert(TABLE_NAME, null, values); - - if (unread) { - SignalDatabase.threads().incrementUnread(threadId, 1, 0); - } - - if (!silent) { - final boolean keepThreadArchived = SignalStore.settings().shouldKeepMutedChatsArchived() && recipient.isMuted(); - SignalDatabase.threads().update(threadId, !keepThreadArchived); - } - - if (message.getSubscriptionId() != -1) { - SignalDatabase.recipients().setDefaultSubscriptionId(recipient.getId(), message.getSubscriptionId()); - } - - notifyConversationListeners(threadId); - - if (!silent) { - TrimThreadJob.enqueueAsync(threadId); - } - - return Optional.of(new InsertResult(messageId, threadId)); - } - } - - @Override - public Optional insertMessageInbox(IncomingTextMessage message) { - return insertMessageInbox(message, Types.BASE_INBOX_TYPE); - } - - @Override - public @NonNull InsertResult insertChatSessionRefreshedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(Recipient.resolved(recipientId)); - long type = Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT; - - type = type & (Types.TOTAL_MASK - Types.ENCRYPTION_MASK) | Types.ENCRYPTION_REMOTE_FAILED_BIT; - - ContentValues values = new ContentValues(); - values.put(RECIPIENT_ID, recipientId.serialize()); - values.put(RECIPIENT_DEVICE_ID, senderDeviceId); - values.put(DATE_RECEIVED, System.currentTimeMillis()); - values.put(DATE_SENT, sentTimestamp); - values.put(DATE_SERVER, -1); - values.put(READ, 0); - values.put(TYPE, type); - values.put(THREAD_ID, threadId); - - long messageId = db.insert(TABLE_NAME, null, values); - - SignalDatabase.threads().incrementUnread(threadId, 1, 0); - boolean keepThreadArchived = SignalStore.settings().shouldKeepMutedChatsArchived() && Recipient.resolved(recipientId).isMuted(); - SignalDatabase.threads().update(threadId, !keepThreadArchived); - - notifyConversationListeners(threadId); - - TrimThreadJob.enqueueAsync(threadId); - - return new InsertResult(messageId, threadId); - } - - @Override - public void insertBadDecryptMessage(@NonNull RecipientId recipientId, int senderDevice, long sentTimestamp, long receivedTimestamp, long threadId) { - ContentValues values = new ContentValues(); - values.put(RECIPIENT_ID, recipientId.serialize()); - values.put(RECIPIENT_DEVICE_ID, senderDevice); - values.put(DATE_SENT, sentTimestamp); - values.put(DATE_RECEIVED, receivedTimestamp); - values.put(DATE_SERVER, -1); - values.put(READ, 0); - values.put(TYPE, Types.BAD_DECRYPT_TYPE); - values.put(THREAD_ID, threadId); - - databaseHelper.getSignalWritableDatabase().insert(TABLE_NAME, null, values); - - SignalDatabase.threads().incrementUnread(threadId, 1, 0); - boolean keepThreadArchived = SignalStore.settings().shouldKeepMutedChatsArchived() && Recipient.resolved(recipientId).isMuted(); - SignalDatabase.threads().update(threadId, !keepThreadArchived); - - notifyConversationListeners(threadId); - - TrimThreadJob.enqueueAsync(threadId); - } - - @Override - public long insertMessageOutbox(long threadId, OutgoingTextMessage message, - boolean forceSms, long date, InsertListener insertListener) - { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - long type = Types.BASE_SENDING_TYPE; - - if (message.isKeyExchange()) type |= Types.KEY_EXCHANGE_BIT; - else if (message.isSecureMessage()) type |= (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT); - else if (message.isEndSession()) type |= Types.END_SESSION_BIT; - if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT; - - if (message.isIdentityVerified()) type |= Types.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT; - else if (message.isIdentityDefault()) type |= Types.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT; - - RecipientId recipientId = message.getRecipient().getId(); - Map earlyDeliveryReceipts = earlyDeliveryReceiptCache.remove(date); - - ContentValues contentValues = new ContentValues(6); - contentValues.put(RECIPIENT_ID, recipientId.serialize()); - contentValues.put(THREAD_ID, threadId); - contentValues.put(BODY, message.getMessageBody()); - contentValues.put(DATE_RECEIVED, System.currentTimeMillis()); - contentValues.put(DATE_SENT, date); - contentValues.put(READ, 1); - contentValues.put(TYPE, type); - contentValues.put(SMS_SUBSCRIPTION_ID, message.getSubscriptionId()); - contentValues.put(EXPIRES_IN, message.getExpiresIn()); - contentValues.put(DELIVERY_RECEIPT_COUNT, Stream.of(earlyDeliveryReceipts.values()).mapToLong(EarlyReceiptCache.Receipt::getCount).sum()); - contentValues.put(RECEIPT_TIMESTAMP, Stream.of(earlyDeliveryReceipts.values()).mapToLong(EarlyReceiptCache.Receipt::getTimestamp).max().orElse(-1)); - - long messageId = db.insert(TABLE_NAME, null, contentValues); - - if (insertListener != null) { - insertListener.onComplete(); - } - - if (!message.isIdentityVerified() && !message.isIdentityDefault()) { - SignalDatabase.threads().setLastScrolled(threadId, 0); - SignalDatabase.threads().setLastSeenSilently(threadId); - } - - SignalDatabase.threads().setHasSentSilently(threadId, true); - - ApplicationDependencies.getDatabaseObserver().notifyMessageInsertObservers(threadId, new MessageId(messageId, false)); - - if (!message.isIdentityVerified() && !message.isIdentityDefault()) { - TrimThreadJob.enqueueAsync(threadId); - } - - return messageId; - } - - @Override - public Cursor getExpirationStartedMessages() { - String where = EXPIRE_STARTED + " > 0"; - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - return db.query(TABLE_NAME, MESSAGE_PROJECTION, where, null, null, null, null); - } - - @Override - public SmsMessageRecord getSmsMessage(long messageId) throws NoSuchMessageException { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - Cursor cursor = db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[]{messageId + ""}, null, null, null); - Reader reader = new Reader(cursor); - SmsMessageRecord record = reader.getNext(); - - reader.close(); - - if (record == null) throw new NoSuchMessageException("No message for ID: " + messageId); - else return record; - } - - @Override - public Cursor getMessageCursor(long messageId) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - return db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[] {messageId + ""}, null, null, null); - } - - @Override - public boolean deleteMessage(long messageId) { - Log.d(TAG, "deleteMessage(" + messageId + ")"); - - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - long threadId; - boolean threadDeleted; - - db.beginTransaction(); - try { - threadId = getThreadIdForMessage(messageId); - - db.delete(TABLE_NAME, ID_WHERE, new String[] { messageId + "" }); - - SignalDatabase.threads().setLastScrolled(threadId, 0); - threadDeleted = SignalDatabase.threads().update(threadId, false, true); - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - notifyConversationListeners(threadId); - return threadDeleted; - } - - @Override - public void ensureMigration() { - databaseHelper.getSignalWritableDatabase(); - } - - @Override - public boolean isStory(long messageId) { - throw new UnsupportedOperationException(); - } - - @Override - public @NonNull MessageTable.Reader getOutgoingStoriesTo(@NonNull RecipientId recipientId) { - throw new UnsupportedOperationException(); - } - - @Override - public @NonNull MessageTable.Reader getAllOutgoingStories(boolean reverse, int limit) { - throw new UnsupportedOperationException(); - } - - @Override - public @NonNull MessageTable.Reader getAllOutgoingStoriesAt(long sentTimestamp) { - throw new UnsupportedOperationException(); - } - - @Override - public @NonNull List markAllIncomingStoriesRead() { - throw new UnsupportedOperationException(); - } - - @Override - public @NonNull List getOrderedStoryRecipientsAndIds(boolean isOutgoingOnly) { - throw new UnsupportedOperationException(); - } - - @Override - public void markOnboardingStoryRead() { - throw new UnsupportedOperationException(); - } - - @Override - public @NonNull MessageTable.Reader getAllStoriesFor(@NonNull RecipientId recipientId, int limit) { - throw new UnsupportedOperationException(); - } - - public boolean isOutgoingStoryAlreadyInDatabase(@NonNull RecipientId recipientId, long sentTimestamp) { - throw new UnsupportedOperationException(); - } - - @Override - public @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException { - throw new UnsupportedOperationException(); - } - - @Override - public int getNumberOfStoryReplies(long parentStoryId) { - throw new UnsupportedOperationException(); - } - - @Override - public @NonNull List getUnreadStoryThreadRecipientIds() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean containsStories(long threadId) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean hasSelfReplyInStory(long parentStoryId) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean hasGroupReplyOrReactionInStory(long parentStoryId) { - throw new UnsupportedOperationException(); - } - - @Override - public @NonNull Cursor getStoryReplies(long parentStoryId) { - throw new UnsupportedOperationException(); - } - - @Override - public @Nullable Long getOldestStorySendTimestamp(boolean hasSeenReleaseChannelStories) { - throw new UnsupportedOperationException(); - } - - @Override - public @NonNull StoryViewState getStoryViewState(@NonNull RecipientId recipientId) { - throw new UnsupportedOperationException(); - } - - @Override - public void updateViewedStories(@NonNull Set syncMessageIds) { - throw new UnsupportedOperationException(); - } - - @Override - public int deleteStoriesOlderThan(long timestamp, boolean hasSeenReleaseChannelStories) { - throw new UnsupportedOperationException(); - } - - @Override - public @NonNull MessageTable.Reader getUnreadStories(@NonNull RecipientId recipientId, int limit) { - throw new UnsupportedOperationException(); - } - - @Override - public @Nullable ParentStoryId.GroupReply getParentStoryIdForGroupReply(long messageId) { - throw new UnsupportedOperationException(); - } - - @Override - public @NonNull List setGroupStoryMessagesReadSince(long threadId, long groupStoryId, long sinceTimestamp) { - throw new UnsupportedOperationException(); - } - - @Override - public @NonNull List getStoryTypes(@NonNull List messageIds) { - throw new UnsupportedOperationException(); - } - - @Override - public void deleteGroupStoryReplies(long parentStoryId) { - throw new UnsupportedOperationException(); - } - - @Override - public MessageRecord getMessageRecord(long messageId) throws NoSuchMessageException { - return getSmsMessage(messageId); - } - - @Override - public @Nullable MessageRecord getMessageRecordOrNull(long messageId) { - try { - return getSmsMessage(messageId); - } catch (NoSuchMessageException e) { - return null; - } - } - - private boolean isDuplicate(IncomingTextMessage message, long threadId) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String query = DATE_SENT + " = ? AND " + RECIPIENT_ID + " = ? AND " + THREAD_ID + " = ?"; - String[] args = SqlUtil.buildArgs(message.getSentTimestampMillis(), message.getSender().serialize(), threadId); - - try (Cursor cursor = db.query(TABLE_NAME, new String[] { "1" }, query, args, null, null, null, "1")) { - return cursor.moveToFirst(); - } - } - - @Override - void deleteThread(long threadId) { - Log.d(TAG, "deleteThread(" + threadId + ")"); - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - db.delete(TABLE_NAME, THREAD_ID + " = ?", new String[] {threadId+""}); - } - - @Override - int deleteMessagesInThreadBeforeDate(long threadId, long date) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - String where = THREAD_ID + " = ? AND " + DATE_RECEIVED + " < " + date; - - return db.delete(TABLE_NAME, where, SqlUtil.buildArgs(threadId)); - } - - @Override - void deleteAbandonedMessages() { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - String where = THREAD_ID + " NOT IN (SELECT _id FROM " + ThreadTable.TABLE_NAME + ")"; - - int deletes = db.delete(TABLE_NAME, where, null); - if (deletes > 0) { - Log.i(TAG, "Deleted " + deletes + " abandoned messages"); - } - } - - @Override - public void deleteRemotelyDeletedStory(long messageId) { - throw new UnsupportedOperationException(); - } - - @Override - public List getMessagesInThreadAfterInclusive(long threadId, long timestamp, long limit) { - String where = TABLE_NAME + "." + MmsSmsColumns.THREAD_ID + " = ? AND " + - TABLE_NAME + "." + getDateReceivedColumnName() + " >= ?"; - String[] args = SqlUtil.buildArgs(threadId, timestamp); - - try (Reader reader = readerFor(queryMessages(where, args, false, limit))) { - List results = new ArrayList<>(reader.cursor.getCount()); - - while (reader.getNext() != null) { - results.add(reader.getCurrent()); - } - - return results; - } - } - - private Cursor queryMessages(@NonNull String where, @Nullable String[] args, boolean reverse, long limit) { - return queryMessages(MESSAGE_PROJECTION, where, args, reverse, limit); - } - - private Cursor queryMessages(@NonNull String[] projection, @NonNull String where, @Nullable String[] args, boolean reverse, long limit) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - - return db.query(TABLE_NAME, - projection, - where, - args, - null, - null, - reverse ? ID + " DESC" : null, - limit > 0 ? String.valueOf(limit) : null); - } - - @Override - void deleteThreads(@NonNull Set threadIds) { - Log.d(TAG, "deleteThreads(count: " + threadIds.size() + ")"); - - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - String where = ""; - - for (long threadId : threadIds) { - where += THREAD_ID + " = '" + threadId + "' OR "; - } - - where = where.substring(0, where.length() - 4); - - db.delete(TABLE_NAME, where, null); - } - - @Override - void deleteAllThreads() { - Log.d(TAG, "deleteAllThreads()"); - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - db.delete(TABLE_NAME, null, null); - } - - @Override - public SQLiteDatabase beginTransaction() { - SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - database.beginTransaction(); - return database; - } - - @Override - public void setTransactionSuccessful() { - databaseHelper.getSignalWritableDatabase().setTransactionSuccessful(); - } - - @Override - public void endTransaction(SQLiteDatabase database) { - database.setTransactionSuccessful(); - database.endTransaction(); - } - - @Override - public void endTransaction() { - databaseHelper.getSignalWritableDatabase().endTransaction(); - } - - @Override - public @Nullable ViewOnceExpirationInfo getNearestExpiringViewOnceMessage() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isSent(long messageId) { - throw new UnsupportedOperationException(); - } - - @Override - public long getLatestGroupQuitTimestamp(long threadId, long quitTimeBarrier) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isGroupQuitMessage(long messageId) { - throw new UnsupportedOperationException(); - } - - @Override - public @Nullable Pair getOldestUnreadMentionDetails(long threadId) { - throw new UnsupportedOperationException(); - } - - @Override - public int getUnreadMentionCount(long threadId) { - throw new UnsupportedOperationException(); - } - - @Override - public void addFailures(long messageId, List failure) { - throw new UnsupportedOperationException(); - } - - @Override - public void setNetworkFailures(long messageId, Set failures) { - throw new UnsupportedOperationException(); - } - - @Override - public void markDownloadState(long messageId, long state) { - throw new UnsupportedOperationException(); - } - - @Override - public Optional getNotification(long messageId) { - throw new UnsupportedOperationException(); - } - - @Override - public OutgoingMediaMessage getOutgoingMessage(long messageId) { - throw new UnsupportedOperationException(); - } - - @Override - public Optional insertMessageInbox(IncomingMediaMessage retrieved, String contentLocation, long threadId) throws MmsException { - throw new UnsupportedOperationException(); - } - - @Override - public Pair insertMessageInbox(@NonNull NotificationInd notification, int subscriptionId) { - throw new UnsupportedOperationException(); - } - - @Override - public Optional insertSecureDecryptedMessageInbox(IncomingMediaMessage retrieved, long threadId) throws MmsException { - throw new UnsupportedOperationException(); - } - - @Override - public long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, @Nullable InsertListener insertListener) throws MmsException { - throw new UnsupportedOperationException(); - } - - @Override - public long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, int defaultReceiptStatus, @Nullable InsertListener insertListener) throws MmsException { - throw new UnsupportedOperationException(); - } - - @Override - public void markIncomingNotificationReceived(long threadId) { - throw new UnsupportedOperationException(); - } - - @Override - public void markGiftRedemptionCompleted(long messageId) { - throw new UnsupportedOperationException(); - } - - @Override - public void markGiftRedemptionStarted(long messageId) { - throw new UnsupportedOperationException(); - } - - @Override - public void markGiftRedemptionFailed(long messageId) { - throw new UnsupportedOperationException(); - } - - @Override - public MessageTable.Reader getMessages(Collection messageIds) { - throw new UnsupportedOperationException(); - } - - - public static class Status { - public static final int STATUS_NONE = -1; - public static final int STATUS_COMPLETE = 0; - public static final int STATUS_PENDING = 0x20; - public static final int STATUS_FAILED = 0x40; - } - - public static Reader readerFor(Cursor cursor) { - return new Reader(cursor); - } - - public static OutgoingMessageReader readerFor(OutgoingTextMessage message, long threadId, long messageId) { - return new OutgoingMessageReader(message, threadId, messageId); - } - - public static class OutgoingMessageReader { - - private final OutgoingTextMessage message; - private final long id; - private final long threadId; - - public OutgoingMessageReader(OutgoingTextMessage message, long threadId, long messageId) { - this.message = message; - this.threadId = threadId; - this.id = messageId; - } - - public MessageRecord getCurrent() { - return new SmsMessageRecord(id, - message.getMessageBody(), - message.getRecipient(), - message.getRecipient(), - 1, - System.currentTimeMillis(), - System.currentTimeMillis(), - -1, - 0, - message.isSecureMessage() ? MmsSmsColumns.Types.getOutgoingEncryptedMessageType() : MmsSmsColumns.Types.getOutgoingSmsMessageType(), - threadId, - 0, - new HashSet<>(), - message.getSubscriptionId(), - message.getExpiresIn(), - System.currentTimeMillis(), - 0, - false, - Collections.emptyList(), - false, - 0, - -1); - } - } - - public static class Reader implements MessageTable.Reader { - - private final Cursor cursor; - private final Context context; - - public Reader(Cursor cursor) { - this.cursor = cursor; - this.context = ApplicationDependencies.getApplication(); - } - - public SmsMessageRecord getNext() { - if (cursor == null || !cursor.moveToNext()) - return null; - - return getCurrent(); - } - - public int getCount() { - if (cursor == null) return 0; - else return cursor.getCount(); - } - - @Override - public @NonNull MessageExportState getMessageExportStateForCurrentRecord() { - byte[] messageExportState = CursorUtil.requireBlob(cursor, SmsTable.EXPORT_STATE); - if (messageExportState == null) { - return MessageExportState.getDefaultInstance(); - } - - try { - return MessageExportState.parseFrom(messageExportState); - } catch (InvalidProtocolBufferException e) { - return MessageExportState.getDefaultInstance(); - } - } - - public SmsMessageRecord getCurrent() { - long messageId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsTable.ID)); - long recipientId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsTable.RECIPIENT_ID)); - int addressDeviceId = cursor.getInt(cursor.getColumnIndexOrThrow(SmsTable.RECIPIENT_DEVICE_ID)); - long type = cursor.getLong(cursor.getColumnIndexOrThrow(SmsTable.TYPE)); - long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(SmsTable.DATE_RECEIVED)); - long dateSent = cursor.getLong(cursor.getColumnIndexOrThrow(SmsTable.DATE_SENT)); - long dateServer = cursor.getLong(cursor.getColumnIndexOrThrow(SmsTable.DATE_SERVER)); - long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsTable.THREAD_ID)); - int status = cursor.getInt(cursor.getColumnIndexOrThrow(SmsTable.SMS_STATUS)); - int deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(SmsTable.DELIVERY_RECEIPT_COUNT)); - int readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(SmsTable.READ_RECEIPT_COUNT)); - String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(SmsTable.MISMATCHED_IDENTITIES)); - int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(SmsTable.SMS_SUBSCRIPTION_ID)); - long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(SmsTable.EXPIRES_IN)); - long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(SmsTable.EXPIRE_STARTED)); - String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsTable.BODY)); - boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(SmsTable.UNIDENTIFIED)) == 1; - boolean remoteDelete = cursor.getInt(cursor.getColumnIndexOrThrow(SmsTable.REMOTE_DELETED)) == 1; - long notifiedTimestamp = CursorUtil.requireLong(cursor, NOTIFIED_TIMESTAMP); - long receiptTimestamp = CursorUtil.requireLong(cursor, RECEIPT_TIMESTAMP); - - if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { - readReceiptCount = 0; - } - - Set mismatches = getMismatches(mismatchDocument); - Recipient recipient = Recipient.live(RecipientId.from(recipientId)).get(); - - return new SmsMessageRecord(messageId, body, recipient, - recipient, - addressDeviceId, - dateSent, dateReceived, dateServer, deliveryReceiptCount, type, - threadId, status, mismatches, subscriptionId, - expiresIn, expireStarted, - readReceiptCount, unidentified, Collections.emptyList(), remoteDelete, - notifiedTimestamp, receiptTimestamp); - } - - private Set getMismatches(String document) { - try { - if (!TextUtils.isEmpty(document)) { - return JsonUtils.fromJson(document, IdentityKeyMismatchSet.class).getItems(); - } - } catch (IOException e) { - Log.w(TAG, e); - } - - return Collections.emptySet(); - } - - @Override - public void close() { - cursor.close(); - } - - @Override - public @NonNull Iterator iterator() { - return new ReaderIterator(); - } - - private class ReaderIterator implements Iterator { - @Override - public boolean hasNext() { - return cursor != null && cursor.getCount() != 0 && !cursor.isLast(); - } - - @Override - public MessageRecord next() { - MessageRecord record = getNext(); - if (record == null) { - throw new NoSuchElementException(); - } - - return record; - } - } - } - - /** - * The number of change number messages in the thread. - * Currently only used for tests. - */ - @VisibleForTesting - int getChangeNumberMessageCount(@NonNull RecipientId recipientId) { - try (Cursor cursor = SQLiteDatabaseExtensionsKt - .select(getReadableDatabase(), "COUNT(*)") - .from(TABLE_NAME) - .where(RECIPIENT_ID + " = ? AND " + TYPE + " = ?", recipientId, CHANGE_NUMBER_TYPE) - .run()) - { - if (cursor.moveToFirst()) { - return cursor.getInt(0); - } else { - return 0; - } - } - } - - @VisibleForTesting - Optional collapseJoinRequestEventsIfPossible(long threadId, IncomingGroupUpdateMessage message) { - InsertResult result = null; - - - SQLiteDatabase db = getWritableDatabase(); - db.beginTransaction(); - - try { - try (MmsSmsTable.Reader reader = MmsSmsTable.readerFor(SignalDatabase.mmsSms().getConversation(threadId, 0, 2))) { - MessageRecord latestMessage = reader.getNext(); - if (latestMessage != null && latestMessage.isGroupV2()) { - Optional changeEditor = message.getChangeEditor(); - if (changeEditor.isPresent() && latestMessage.isGroupV2JoinRequest(changeEditor.get())) { - String encodedBody; - long id; - - MessageRecord secondLatestMessage = reader.getNext(); - if (secondLatestMessage != null && secondLatestMessage.isGroupV2JoinRequest(changeEditor.get())) { - id = secondLatestMessage.getId(); - encodedBody = MessageRecord.createNewContextWithAppendedDeleteJoinRequest(secondLatestMessage, message.getChangeRevision(), changeEditor.get()); - deleteMessage(latestMessage.getId()); - } else { - id = latestMessage.getId(); - encodedBody = MessageRecord.createNewContextWithAppendedDeleteJoinRequest(latestMessage, message.getChangeRevision(), changeEditor.get()); - } - - ContentValues values = new ContentValues(1); - values.put(BODY, encodedBody); - getWritableDatabase().update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(id)); - result = new InsertResult(id, threadId); - } - } - } - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - return Optional.ofNullable(result); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/StorySendTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/StorySendTable.kt index afc9e41492..15fd131c83 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/StorySendTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/StorySendTable.kt @@ -36,7 +36,7 @@ class StorySendTable(context: Context, databaseHelper: SignalDatabase) : Databas val CREATE_TABLE = """ CREATE TABLE $TABLE_NAME ( $ID INTEGER PRIMARY KEY, - $MESSAGE_ID INTEGER NOT NULL REFERENCES ${MmsTable.TABLE_NAME} (${MmsTable.ID}) ON DELETE CASCADE, + $MESSAGE_ID INTEGER NOT NULL REFERENCES ${MessageTable.TABLE_NAME} (${MessageTable.ID}) ON DELETE CASCADE, $RECIPIENT_ID INTEGER NOT NULL REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE, $SENT_TIMESTAMP INTEGER NOT NULL, $ALLOWS_REPLIES INTEGER NOT NULL, @@ -141,9 +141,9 @@ class StorySendTable(context: Context, databaseHelper: SignalDatabase) : Databas WHERE $MESSAGE_ID != $messageId AND $SENT_TIMESTAMP = $sentTimestamp AND $MESSAGE_ID IN ( - SELECT ${MmsTable.ID} - FROM ${MmsTable.TABLE_NAME} - WHERE ${MmsTable.REMOTE_DELETED} = 0 + SELECT ${MessageTable.ID} + FROM ${MessageTable.TABLE_NAME} + WHERE ${MessageTable.REMOTE_DELETED} = 0 ) ) """.trimIndent() @@ -208,7 +208,7 @@ class StorySendTable(context: Context, databaseHelper: SignalDatabase) : Databas .where( """ $SENT_TIMESTAMP = ? AND - (SELECT ${MmsTable.REMOTE_DELETED} FROM ${MmsTable.TABLE_NAME} WHERE ${MmsTable.ID} = $MESSAGE_ID) = 0 + (SELECT ${MessageTable.REMOTE_DELETED} FROM ${MessageTable.TABLE_NAME} WHERE ${MessageTable.ID} = $MESSAGE_ID) = 0 """.trimIndent(), sentTimestamp ) @@ -250,10 +250,10 @@ class StorySendTable(context: Context, databaseHelper: SignalDatabase) : Databas val localManifest: SentStorySyncManifest = getLocalManifest(sentTimestamp) val query = """ - SELECT ${MmsTable.TABLE_NAME}.${MmsTable.ID} as $MESSAGE_ID, ${DistributionListTables.DISTRIBUTION_ID} - FROM ${MmsTable.TABLE_NAME} - INNER JOIN ${DistributionListTables.LIST_TABLE_NAME} ON ${DistributionListTables.LIST_TABLE_NAME}.${DistributionListTables.RECIPIENT_ID} = ${MmsTable.TABLE_NAME}.${MmsTable.RECIPIENT_ID} - WHERE ${MmsTable.DATE_SENT} = $sentTimestamp AND ${DistributionListTables.DISTRIBUTION_ID} IS NOT NULL + SELECT ${MessageTable.TABLE_NAME}.${MessageTable.ID} as $MESSAGE_ID, ${DistributionListTables.DISTRIBUTION_ID} + FROM ${MessageTable.TABLE_NAME} + INNER JOIN ${DistributionListTables.LIST_TABLE_NAME} ON ${DistributionListTables.LIST_TABLE_NAME}.${DistributionListTables.RECIPIENT_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.RECIPIENT_ID} + WHERE ${MessageTable.DATE_SENT} = $sentTimestamp AND ${DistributionListTables.DISTRIBUTION_ID} IS NOT NULL """.trimIndent() val distributionIdToMessageId = readableDatabase.query(query).use { cursor -> @@ -329,16 +329,16 @@ class StorySendTable(context: Context, databaseHelper: SignalDatabase) : Databas $TABLE_NAME.$RECIPIENT_ID, $ALLOWS_REPLIES, $DISTRIBUTION_ID, - ${MmsTable.REMOTE_DELETED} + ${MessageTable.REMOTE_DELETED} FROM $TABLE_NAME - INNER JOIN ${MmsTable.TABLE_NAME} ON ${MmsTable.TABLE_NAME}.${MmsTable.ID} = $TABLE_NAME.$MESSAGE_ID + INNER JOIN ${MessageTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.ID} = $TABLE_NAME.$MESSAGE_ID WHERE $TABLE_NAME.$SENT_TIMESTAMP = ? """.trimIndent(), arrayOf(sentTimestamp) ).use { cursor -> val results: MutableMap = mutableMapOf() while (cursor.moveToNext()) { - val isRemoteDeleted = CursorUtil.requireBoolean(cursor, MmsTable.REMOTE_DELETED) + val isRemoteDeleted = CursorUtil.requireBoolean(cursor, MessageTable.REMOTE_DELETED) val recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)) val distributionId = DistributionId.from(CursorUtil.requireString(cursor, DISTRIBUTION_ID)) val distributionIdList: List = if (isRemoteDeleted) emptyList() else listOf(distributionId) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java index 8129696a8d..ceda538c53 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java @@ -34,10 +34,10 @@ import org.thoughtcrime.securesms.database.DraftTable; import org.thoughtcrime.securesms.database.GroupTable; import org.thoughtcrime.securesms.database.GroupReceiptTable; import org.thoughtcrime.securesms.database.IdentityTable; -import org.thoughtcrime.securesms.database.MmsTable; +import org.thoughtcrime.securesms.database.MmsSmsColumns; +import org.thoughtcrime.securesms.database.MessageTable; import org.thoughtcrime.securesms.database.PushTable; import org.thoughtcrime.securesms.database.RecipientTable; -import org.thoughtcrime.securesms.database.SmsTable; import org.thoughtcrime.securesms.database.ThreadTable; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; @@ -128,8 +128,7 @@ public class ClassicOpenHelper extends SQLiteOpenHelper { @Override public void onCreate(SQLiteDatabase db) { - db.execSQL(SmsTable.CREATE_TABLE); - db.execSQL(MmsTable.CREATE_TABLE); + db.execSQL(MessageTable.CREATE_TABLE); db.execSQL(AttachmentTable.CREATE_TABLE); db.execSQL(ThreadTable.CREATE_TABLE); db.execSQL(IdentityTable.CREATE_TABLE); @@ -139,8 +138,7 @@ public class ClassicOpenHelper extends SQLiteOpenHelper { db.execSQL(RecipientTable.CREATE_TABLE); db.execSQL(GroupReceiptTable.CREATE_TABLE); - executeStatements(db, SmsTable.CREATE_INDEXS); - executeStatements(db, MmsTable.CREATE_INDEXS); + executeStatements(db, MessageTable.CREATE_INDEXS); executeStatements(db, AttachmentTable.CREATE_INDEXS); executeStatements(db, ThreadTable.CREATE_INDEXS); executeStatements(db, DraftTable.CREATE_INDEXS); @@ -404,10 +402,10 @@ public class ClassicOpenHelper extends SQLiteOpenHelper { Cursor cursor = null; try { - cursor = db.query(SmsTable.TABLE_NAME, - new String[] { SmsTable.ID, SmsTable.BODY, SmsTable.TYPE}, - SmsTable.TYPE + " & ? == 0", - new String[] {String.valueOf(SmsTable.Types.ENCRYPTION_MASK)}, + cursor = db.query("sms", + new String[] { "_id", "body", "type"}, + "type & ? == 0", + new String[] {String.valueOf(MmsSmsColumns.Types.ENCRYPTION_MASK)}, null, null, null); while (cursor.moveToNext()) { @@ -418,10 +416,10 @@ public class ClassicOpenHelper extends SQLiteOpenHelper { String encryptedBody = masterCipher.encryptBody(body); ContentValues update = new ContentValues(); - update.put(SmsTable.BODY, encryptedBody); - update.put(SmsTable.TYPE, type | 0x80000000); // Inline now deprecated symmetric encryption type + update.put("body", encryptedBody); + update.put("type", type | 0x80000000); // Inline now deprecated symmetric encryption type - db.update(SmsTable.TABLE_NAME, update, SmsTable.ID + " = ?", + db.update("sms", update, "_id = ?", new String[] {String.valueOf(id)}); } } finally { 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 6b78fa2c21..b60a64a691 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 @@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V164_ThreadDatabase import org.thoughtcrime.securesms.database.helpers.migration.V165_MmsMessageBoxPaymentTransactionIndexMigration import org.thoughtcrime.securesms.database.helpers.migration.V166_ThreadAndMessageForeignKeys import org.thoughtcrime.securesms.database.helpers.migration.V167_RecreateReactionTriggers +import org.thoughtcrime.securesms.database.helpers.migration.V168_SingleMessageTableMigration /** * Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness. @@ -31,7 +32,7 @@ object SignalDatabaseMigrations { val TAG: String = Log.tag(SignalDatabaseMigrations.javaClass) - const val DATABASE_VERSION = 167 + const val DATABASE_VERSION = 168 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { @@ -110,6 +111,10 @@ object SignalDatabaseMigrations { if (oldVersion < 167) { V167_RecreateReactionTriggers.migrate(context, db, oldVersion, newVersion) } + + if (oldVersion < 168) { + V168_SingleMessageTableMigration.migrate(context, db, oldVersion, newVersion) + } } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V168_SingleMessageTableMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V168_SingleMessageTableMigration.kt new file mode 100644 index 0000000000..053668769e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V168_SingleMessageTableMigration.kt @@ -0,0 +1,91 @@ +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import net.zetetic.database.sqlcipher.SQLiteDatabase +import org.signal.core.util.SqlUtil + +object V168_SingleMessageTableMigration : SignalDatabaseMigration { + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + val nextMmsId = SqlUtil.getNextAutoIncrementId(db, "mms") + + db.execSQL("DROP TRIGGER msl_sms_delete") + db.execSQL("DROP TRIGGER reactions_sms_delete") + db.execSQL("DROP TABLE sms_fts") // Will drop all other related fts tables + + db.execSQL( + """ + INSERT INTO mms + SELECT + _id + $nextMmsId, + date_sent, + date_received, + date_server, + thread_id, + recipient_id, + recipient_device_id, + type, + body, + read, + null, + 0, + 0, + 0, + status, + null, + subscription_id, + receipt_timestamp, + delivery_receipt_count, + read_receipt_count, + 0, + mismatched_identities, + null, + expires_in, + expire_started, + notified, + 0, + 0, + null, + 0, + null, + 0, + null, + unidentified, + null, + 0, + reactions_unread, + reactions_last_seen, + remote_deleted, + 0, + notified_timestamp, + server_guid, + null, + 0, + 0, + export_state, + exported + FROM sms + """ + ) + + db.execSQL("DROP TABLE sms") + + db.execSQL( + """ + UPDATE reaction + SET message_id = message_id + $nextMmsId + WHERE is_mms = 0 + """ + ) + + db.execSQL( + """ + UPDATE msl_message + SET message_id = message_id + $nextMmsId + WHERE is_mms = 0 + """ + ) + + // TODO search index? + // TODO jobs? + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java index cab57efe63..a4df887ab5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java @@ -23,7 +23,6 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import org.thoughtcrime.securesms.database.MmsSmsColumns; -import org.thoughtcrime.securesms.database.SmsTable; import org.thoughtcrime.securesms.recipients.Recipient; /** @@ -72,7 +71,7 @@ public abstract class DisplayRecord { return MmsSmsColumns.Types.isFailedMessageType(type) || MmsSmsColumns.Types.isPendingSecureSmsFallbackType(type) || - deliveryStatus >= SmsTable.Status.STATUS_FAILED; + deliveryStatus >= MmsSmsColumns.Status.STATUS_FAILED; } public boolean isPending() { @@ -113,23 +112,23 @@ public abstract class DisplayRecord { } public boolean isKeyExchange() { - return SmsTable.Types.isKeyExchangeType(type); + return MmsSmsColumns.Types.isKeyExchangeType(type); } public boolean isEndSession() { - return SmsTable.Types.isEndSessionType(type); + return MmsSmsColumns.Types.isEndSessionType(type); } public boolean isGroupUpdate() { - return SmsTable.Types.isGroupUpdate(type); + return MmsSmsColumns.Types.isGroupUpdate(type); } public boolean isGroupV2() { - return SmsTable.Types.isGroupV2(type); + return MmsSmsColumns.Types.isGroupV2(type); } public boolean isGroupQuit() { - return SmsTable.Types.isGroupQuit(type); + return MmsSmsColumns.Types.isGroupQuit(type); } public boolean isGroupAction() { @@ -137,55 +136,55 @@ public abstract class DisplayRecord { } public boolean isExpirationTimerUpdate() { - return SmsTable.Types.isExpirationTimerUpdate(type); + return MmsSmsColumns.Types.isExpirationTimerUpdate(type); } public boolean isCallLog() { - return SmsTable.Types.isCallLog(type); + return MmsSmsColumns.Types.isCallLog(type); } public boolean isJoined() { - return SmsTable.Types.isJoinedType(type); + return MmsSmsColumns.Types.isJoinedType(type); } public boolean isIncomingAudioCall() { - return SmsTable.Types.isIncomingAudioCall(type); + return MmsSmsColumns.Types.isIncomingAudioCall(type); } public boolean isIncomingVideoCall() { - return SmsTable.Types.isIncomingVideoCall(type); + return MmsSmsColumns.Types.isIncomingVideoCall(type); } public boolean isOutgoingAudioCall() { - return SmsTable.Types.isOutgoingAudioCall(type); + return MmsSmsColumns.Types.isOutgoingAudioCall(type); } public boolean isOutgoingVideoCall() { - return SmsTable.Types.isOutgoingVideoCall(type); + return MmsSmsColumns.Types.isOutgoingVideoCall(type); } public final boolean isMissedAudioCall() { - return SmsTable.Types.isMissedAudioCall(type); + return MmsSmsColumns.Types.isMissedAudioCall(type); } public final boolean isMissedVideoCall() { - return SmsTable.Types.isMissedVideoCall(type); + return MmsSmsColumns.Types.isMissedVideoCall(type); } public final boolean isGroupCall() { - return SmsTable.Types.isGroupCall(type); + return MmsSmsColumns.Types.isGroupCall(type); } public boolean isVerificationStatusChange() { - return SmsTable.Types.isIdentityDefault(type) || SmsTable.Types.isIdentityVerified(type); + return MmsSmsColumns.Types.isIdentityDefault(type) || MmsSmsColumns.Types.isIdentityVerified(type); } public boolean isProfileChange() { - return SmsTable.Types.isProfileChange(type); + return MmsSmsColumns.Types.isProfileChange(type); } public boolean isChangeNumber() { - return SmsTable.Types.isChangeNumber(type); + return MmsSmsColumns.Types.isChangeNumber(type); } public boolean isBoostRequest() { @@ -216,8 +215,8 @@ public abstract class DisplayRecord { } public boolean isDelivered() { - return (deliveryStatus >= SmsTable.Status.STATUS_COMPLETE && - deliveryStatus < SmsTable.Status.STATUS_PENDING) || deliveryReceiptCount > 0; + return (deliveryStatus >= MmsSmsColumns.Status.STATUS_COMPLETE && + deliveryStatus < MmsSmsColumns.Status.STATUS_PENDING) || deliveryReceiptCount > 0; } public boolean isRemoteViewed() { @@ -229,7 +228,7 @@ public abstract class DisplayRecord { } public boolean isPendingInsecureSmsFallback() { - return SmsTable.Types.isPendingInsecureSmsFallbackType(type); + return MmsSmsColumns.Types.isPendingInsecureSmsFallbackType(type); } public boolean isPaymentNotification() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java index 4d49357963..bc4ca4a39d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java @@ -29,8 +29,8 @@ import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.contactshare.Contact; -import org.thoughtcrime.securesms.database.MmsTable; -import org.thoughtcrime.securesms.database.SmsTable.Status; +import org.thoughtcrime.securesms.database.MessageTable; +import org.thoughtcrime.securesms.database.MmsSmsColumns.Status; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.NetworkFailure; import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList; @@ -122,11 +122,11 @@ public class MediaMmsMessageRecord extends MmsMessageRecord { @Override @WorkerThread public SpannableString getDisplayBody(@NonNull Context context) { - if (MmsTable.Types.isChatSessionRefresh(type)) { + if (MessageTable.Types.isChatSessionRefresh(type)) { return emphasisAdded(context.getString(R.string.MmsMessageRecord_bad_encrypted_mms_message)); - } else if (MmsTable.Types.isDuplicateMessageType(type)) { + } else if (MessageTable.Types.isDuplicateMessageType(type)) { return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message)); - } else if (MmsTable.Types.isNoRemoteSessionType(type)) { + } else if (MessageTable.Types.isNoRemoteSessionType(type)) { return emphasisAdded(context.getString(R.string.MmsMessageRecord_mms_message_encrypted_for_non_existing_session)); } else if (isLegacyMessage()) { return emphasisAdded(context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported)); @@ -182,7 +182,7 @@ public class MediaMmsMessageRecord extends MmsMessageRecord { Quote quote = updateQuote(context, getQuote(), attachments); List slideAttachments = attachments.stream().filter(a -> !contactAttachments.contains(a)).filter(a -> !linkPreviewAttachments.contains(a)).collect(Collectors.toList()); - SlideDeck slideDeck = MmsTable.Reader.buildSlideDeck(context, slideAttachments); + SlideDeck slideDeck = MessageTable.MmsReader.buildSlideDeck(context, slideAttachments); return new MediaMmsMessageRecord(getId(), getRecipient(), getIndividualRecipient(), getRecipientDeviceId(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), slideDeck, getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageId.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageId.kt index d4c1dceb36..222d7b0f14 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageId.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageId.kt @@ -16,6 +16,21 @@ data class MessageId( return "$id|$mms" } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MessageId + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + return id.hashCode() + } + companion object { /** * Returns null for invalid IDs. Useful when pulling a possibly-unset ID from a database, or something like that. diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index f4853db491..3ad5a260af 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -42,7 +42,6 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.emoji.EmojiProvider; import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser; import org.thoughtcrime.securesms.database.MmsSmsColumns; -import org.thoughtcrime.securesms.database.SmsTable; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.NetworkFailure; import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList; @@ -494,7 +493,7 @@ public abstract class MessageRecord extends DisplayRecord { } public boolean isPush() { - return SmsTable.Types.isPushType(type) && !SmsTable.Types.isForcedSms(type); + return MmsSmsColumns.Types.isPushType(type) && !MmsSmsColumns.Types.isForcedSms(type); } public long getTimestamp() { @@ -509,15 +508,15 @@ public abstract class MessageRecord extends DisplayRecord { } public boolean isForcedSms() { - return SmsTable.Types.isForcedSms(type); + return MmsSmsColumns.Types.isForcedSms(type); } public boolean isIdentityVerified() { - return SmsTable.Types.isIdentityVerified(type); + return MmsSmsColumns.Types.isIdentityVerified(type); } public boolean isIdentityDefault() { - return SmsTable.Types.isIdentityDefault(type); + return MmsSmsColumns.Types.isIdentityDefault(type); } public boolean isIdentityMismatchFailure() { @@ -525,23 +524,23 @@ public abstract class MessageRecord extends DisplayRecord { } public boolean isBundleKeyExchange() { - return SmsTable.Types.isBundleKeyExchange(type); + return MmsSmsColumns.Types.isBundleKeyExchange(type); } public boolean isContentBundleKeyExchange() { - return SmsTable.Types.isContentBundleKeyExchange(type); + return MmsSmsColumns.Types.isContentBundleKeyExchange(type); } public boolean isRateLimited() { - return SmsTable.Types.isRateLimited(type); + return MmsSmsColumns.Types.isRateLimited(type); } public boolean isIdentityUpdate() { - return SmsTable.Types.isIdentityUpdate(type); + return MmsSmsColumns.Types.isIdentityUpdate(type); } public boolean isCorruptedKeyExchange() { - return SmsTable.Types.isCorruptedKeyExchange(type); + return MmsSmsColumns.Types.isCorruptedKeyExchange(type); } public boolean isBadDecryptType() { @@ -557,11 +556,11 @@ public abstract class MessageRecord extends DisplayRecord { } public boolean isInvalidVersionKeyExchange() { - return SmsTable.Types.isInvalidVersionKeyExchange(type); + return MmsSmsColumns.Types.isInvalidVersionKeyExchange(type); } public boolean isGroupV1MigrationEvent() { - return SmsTable.Types.isGroupV1MigrationEvent(type); + return MmsSmsColumns.Types.isGroupV1MigrationEvent(type); } public @NonNull GroupMigrationMembershipChange getGroupV1MigrationMembershipChanges() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java index 4c0a417aa4..07bf00472b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java @@ -23,8 +23,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.database.MmsTable; -import org.thoughtcrime.securesms.database.SmsTable.Status; +import org.thoughtcrime.securesms.database.MessageTable; +import org.thoughtcrime.securesms.database.MmsSmsColumns.Status; import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.recipients.Recipient; @@ -117,9 +117,9 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord { @Override public SpannableString getDisplayBody(@NonNull Context context) { - if (status == MmsTable.Status.DOWNLOAD_INITIALIZED) { + if (status == MessageTable.MmsStatus.DOWNLOAD_INITIALIZED) { return emphasisAdded(context.getString(R.string.NotificationMmsMessageRecord_multimedia_message)); - } else if (status == MmsTable.Status.DOWNLOAD_CONNECTING) { + } else if (status == MessageTable.MmsStatus.DOWNLOAD_CONNECTING) { return emphasisAdded(context.getString(R.string.NotificationMmsMessageRecord_downloading_mms_message)); } else { return emphasisAdded(context.getString(R.string.NotificationMmsMessageRecord_error_downloading_mms_message)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java index 82618d4cac..c217bac7a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java @@ -25,7 +25,6 @@ import androidx.annotation.WorkerThread; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.MmsSmsColumns; -import org.thoughtcrime.securesms.database.SmsTable; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.recipients.Recipient; @@ -68,7 +67,7 @@ public class SmsMessageRecord extends MessageRecord { @Override @WorkerThread public SpannableString getDisplayBody(@NonNull Context context) { - if (SmsTable.Types.isChatSessionRefresh(type)) { + if (MmsSmsColumns.Types.isChatSessionRefresh(type)) { return emphasisAdded(context.getString(R.string.MessageRecord_chat_session_refreshed)); } else if (isCorruptedKeyExchange()) { return emphasisAdded(context.getString(R.string.SmsMessageRecord_received_corrupted_key_exchange_message)); @@ -82,17 +81,17 @@ public class SmsMessageRecord extends MessageRecord { return new SpannableString(""); } else if (isKeyExchange() && !isOutgoing()) { return emphasisAdded(context.getString(R.string.ConversationItem_received_key_exchange_message_tap_to_process)); - } else if (SmsTable.Types.isDuplicateMessageType(type)) { + } else if (MmsSmsColumns.Types.isDuplicateMessageType(type)) { return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message)); - } else if (SmsTable.Types.isNoRemoteSessionType(type)) { + } else if (MmsSmsColumns.Types.isNoRemoteSessionType(type)) { return emphasisAdded(context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session)); } else if (isEndSession() && isOutgoing()) { return emphasisAdded(context.getString(R.string.SmsMessageRecord_secure_session_reset)); } else if (isEndSession()) { return emphasisAdded(context.getString(R.string.SmsMessageRecord_secure_session_reset_s, getIndividualRecipient().getDisplayName(context))); - } else if (SmsTable.Types.isUnsupportedMessageType(type)) { + } else if (MmsSmsColumns.Types.isUnsupportedMessageType(type)) { return emphasisAdded(context.getString(R.string.SmsMessageRecord_this_message_could_not_be_processed_because_it_was_sent_from_a_newer_version)); - } else if (SmsTable.Types.isInvalidMessageType(type)) { + } else if (MmsSmsColumns.Types.isInvalidMessageType(type)) { return emphasisAdded(context.getString(R.string.SmsMessageRecord_error_handling_incoming_message)); } else { return super.getDisplayBody(context); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/StatusUtil.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/StatusUtil.java index 9198ce43c2..fb4b2b1219 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/StatusUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/StatusUtil.java @@ -1,14 +1,13 @@ package org.thoughtcrime.securesms.database.model; import org.thoughtcrime.securesms.database.MmsSmsColumns; -import org.thoughtcrime.securesms.database.SmsTable; final class StatusUtil { private StatusUtil() {} static boolean isDelivered(long deliveryStatus, int deliveryReceiptCount) { - return (deliveryStatus >= SmsTable.Status.STATUS_COMPLETE && - deliveryStatus < SmsTable.Status.STATUS_PENDING) || deliveryReceiptCount > 0; + return (deliveryStatus >= MmsSmsColumns.Status.STATUS_COMPLETE && + deliveryStatus < MmsSmsColumns.Status.STATUS_PENDING) || deliveryReceiptCount > 0; } static boolean isPending(long type) { @@ -20,10 +19,10 @@ final class StatusUtil { static boolean isFailed(long type, long deliveryStatus) { return MmsSmsColumns.Types.isFailedMessageType(type) || MmsSmsColumns.Types.isPendingSecureSmsFallbackType(type) || - deliveryStatus >= SmsTable.Status.STATUS_FAILED; + deliveryStatus >= MmsSmsColumns.Status.STATUS_FAILED; } static boolean isVerificationStatusChange(long type) { - return SmsTable.Types.isIdentityDefault(type) || SmsTable.Types.isIdentityVerified(type); + return MmsSmsColumns.Types.isIdentityDefault(type) || MmsSmsColumns.Types.isIdentityVerified(type); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java index e83269de30..2ebcfd4061 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -23,7 +23,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.thoughtcrime.securesms.database.MmsSmsColumns; -import org.thoughtcrime.securesms.database.SmsTable; import org.thoughtcrime.securesms.database.ThreadTable; import org.thoughtcrime.securesms.database.ThreadTable.Extra; import org.thoughtcrime.securesms.recipients.Recipient; @@ -146,11 +145,11 @@ public final class ThreadRecord { } public boolean isOutgoingAudioCall() { - return SmsTable.Types.isOutgoingAudioCall(type); + return MmsSmsColumns.Types.isOutgoingAudioCall(type); } public boolean isOutgoingVideoCall() { - return SmsTable.Types.isOutgoingVideoCall(type); + return MmsSmsColumns.Types.isOutgoingVideoCall(type); } public boolean isVerificationStatusChange() { @@ -170,7 +169,7 @@ public final class ThreadRecord { } public boolean isPendingInsecureSmsFallback() { - return SmsTable.Types.isPendingInsecureSmsFallbackType(type); + return MmsSmsColumns.Types.isPendingInsecureSmsFallbackType(type); } public boolean isDelivered() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportReader.kt b/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportReader.kt index cc47a1a54e..6d41980393 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportReader.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/exporter/SignalSmsExportReader.kt @@ -5,9 +5,7 @@ import org.signal.smsexporter.ExportableMessage import org.signal.smsexporter.SmsExportState import org.thoughtcrime.securesms.attachments.DatabaseAttachment import org.thoughtcrime.securesms.database.MessageTable -import org.thoughtcrime.securesms.database.MmsTable import org.thoughtcrime.securesms.database.SignalDatabase -import org.thoughtcrime.securesms.database.SmsTable import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord @@ -32,9 +30,9 @@ class SignalSmsExportReader( private const val CURSOR_LIMIT = 1000 } - private var smsReader: SmsTable.Reader? = null + private var smsReader: MessageTable.SmsReader? = null private var smsDone: Boolean = false - private var mmsReader: MmsTable.Reader? = null + private var mmsReader: MessageTable.MmsReader? = null private var mmsDone: Boolean = false override fun iterator(): Iterator { @@ -55,7 +53,7 @@ class SignalSmsExportReader( smsReader?.close() smsReader = null - val refreshedSmsReader = SmsTable.readerFor(smsDatabase.getUnexportedInsecureMessages(CURSOR_LIMIT)) + val refreshedSmsReader = MessageTable.smsReaderFor(smsDatabase.getUnexportedInsecureMessages(CURSOR_LIMIT)) if (refreshedSmsReader.count > 0) { smsReader = refreshedSmsReader return @@ -69,7 +67,7 @@ class SignalSmsExportReader( mmsReader?.close() mmsReader = null - val refreshedMmsReader = MmsTable.readerFor(mmsDatabase.getUnexportedInsecureMessages(CURSOR_LIMIT)) + val refreshedMmsReader = MessageTable.mmsReaderFor(mmsDatabase.getUnexportedInsecureMessages(CURSOR_LIMIT)) if (refreshedMmsReader.count > 0) { mmsReader = refreshedMmsReader return diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java index 82aa72c797..11b5ebff96 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java @@ -19,7 +19,6 @@ import org.thoughtcrime.securesms.contactshare.VCardUtil; import org.thoughtcrime.securesms.database.AttachmentTable; import org.thoughtcrime.securesms.database.MessageTable; import org.thoughtcrime.securesms.database.MessageTable.InsertResult; -import org.thoughtcrime.securesms.database.MmsTable; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; @@ -110,8 +109,8 @@ public class MmsDownloadJob extends BaseJob { throw new NotReadyException(); } - MessageTable database = SignalDatabase.mms(); - Optional notification = database.getNotification(messageId); + MessageTable database = SignalDatabase.mms(); + Optional notification = database.getNotification(messageId); if (!notification.isPresent()) { Log.w(TAG, "No notification for ID: " + messageId); @@ -127,7 +126,7 @@ public class MmsDownloadJob extends BaseJob { throw new MmsException("Not registered"); } - database.markDownloadState(messageId, MmsTable.Status.DOWNLOAD_CONNECTING); + database.markDownloadState(messageId, MessageTable.MmsStatus.DOWNLOAD_CONNECTING); String contentLocation = notification.get().getContentLocation(); byte[] transactionId = new byte[0]; @@ -153,17 +152,17 @@ public class MmsDownloadJob extends BaseJob { storeRetrievedMms(contentLocation, messageId, threadId, retrieveConf, notification.get().getSubscriptionId(), notification.get().getFrom()); } catch (ApnUnavailableException e) { Log.w(TAG, e); - handleDownloadError(messageId, threadId, MmsTable.Status.DOWNLOAD_APN_UNAVAILABLE, + handleDownloadError(messageId, threadId, MessageTable.MmsStatus.DOWNLOAD_APN_UNAVAILABLE, automatic); } catch (MmsException e) { Log.w(TAG, e); handleDownloadError(messageId, threadId, - MmsTable.Status.DOWNLOAD_HARD_FAILURE, + MessageTable.MmsStatus.DOWNLOAD_HARD_FAILURE, automatic); } catch (MmsRadioException | IOException e) { Log.w(TAG, e); handleDownloadError(messageId, threadId, - MmsTable.Status.DOWNLOAD_SOFT_FAILURE, + MessageTable.MmsStatus.DOWNLOAD_SOFT_FAILURE, automatic); } } @@ -171,7 +170,7 @@ public class MmsDownloadJob extends BaseJob { @Override public void onFailure() { MessageTable database = SignalDatabase.mms(); - database.markDownloadState(messageId, MmsTable.Status.DOWNLOAD_SOFT_FAILURE); + database.markDownloadState(messageId, MessageTable.MmsStatus.DOWNLOAD_SOFT_FAILURE); if (automatic) { database.markIncomingNotificationReceived(threadId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 931a2135e0..17ae19313d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -18,7 +18,6 @@ import org.thoughtcrime.securesms.database.GroupTable; import org.thoughtcrime.securesms.database.GroupReceiptTable; import org.thoughtcrime.securesms.database.GroupReceiptTable.GroupReceiptInfo; import org.thoughtcrime.securesms.database.MessageTable; -import org.thoughtcrime.securesms.database.MmsTable; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.RecipientTable; import org.thoughtcrime.securesms.database.SignalDatabase; @@ -382,7 +381,7 @@ public final class PushGroupSendJob extends PushSendJob { @NonNull Set existingIdentityMismatches) throws RetryLaterException, ProofRequiredException { - MmsTable database = SignalDatabase.mms(); + MessageTable database = SignalDatabase.mms(); RecipientAccessList accessList = new RecipientAccessList(target); List networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(accessList.requireIdByAddress(result.getAddress()))).toList(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/longmessage/LongMessageRepository.java b/app/src/main/java/org/thoughtcrime/securesms/longmessage/LongMessageRepository.java index 5e510dca0a..46cbb65c3e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/longmessage/LongMessageRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/longmessage/LongMessageRepository.java @@ -12,9 +12,7 @@ import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory; import org.thoughtcrime.securesms.database.MessageTable; -import org.thoughtcrime.securesms.database.MmsTable; import org.thoughtcrime.securesms.database.SignalDatabase; -import org.thoughtcrime.securesms.database.SmsTable; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.mms.PartAuthority; @@ -78,14 +76,14 @@ class LongMessageRepository { @WorkerThread private Optional getMmsMessage(@NonNull MessageTable mmsDatabase, long messageId) { try (Cursor cursor = mmsDatabase.getMessageCursor(messageId)) { - return Optional.ofNullable((MmsMessageRecord) MmsTable.readerFor(cursor).getNext()); + return Optional.ofNullable((MmsMessageRecord) MessageTable.mmsReaderFor(cursor).getNext()); } } @WorkerThread private Optional getSmsMessage(@NonNull MessageTable smsDatabase, long messageId) { try (Cursor cursor = smsDatabase.getMessageCursor(messageId)) { - return Optional.ofNullable(SmsTable.readerFor(cursor).getNext()); + return Optional.ofNullable(MessageTable.smsReaderFor(cursor).getNext()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsFragment.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsFragment.java index 9fc4870e61..063a5a791b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsFragment.java @@ -35,7 +35,6 @@ import java.util.List; public final class MessageDetailsFragment extends FullScreenDialogFragment { private static final String MESSAGE_ID_EXTRA = "message_id"; - private static final String TYPE_EXTRA = "type"; private static final String RECIPIENT_EXTRA = "recipient_id"; private GlideRequests glideRequests; @@ -49,7 +48,6 @@ public final class MessageDetailsFragment extends FullScreenDialogFragment { Bundle args = new Bundle(); args.putLong(MESSAGE_ID_EXTRA, message.getId()); - args.putString(TYPE_EXTRA, message.isMms() ? MmsSmsTable.MMS_TRANSPORT : MmsSmsTable.SMS_TRANSPORT); args.putParcelable(RECIPIENT_EXTRA, recipientId); dialogFragment.setArguments(args); @@ -102,9 +100,8 @@ public final class MessageDetailsFragment extends FullScreenDialogFragment { private void initializeViewModel() { final RecipientId recipientId = requireArguments().getParcelable(RECIPIENT_EXTRA); - final String type = requireArguments().getString(TYPE_EXTRA); final Long messageId = requireArguments().getLong(MESSAGE_ID_EXTRA, -1); - final Factory factory = new Factory(recipientId, type, messageId); + final Factory factory = new Factory(recipientId, messageId); viewModel = new ViewModelProvider(this, factory).get(MessageDetailsViewModel.class); viewModel.getMessageDetails().observe(this, details -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsRepository.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsRepository.java index 2b731edb0f..d7b1632996 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsRepository.java @@ -37,8 +37,8 @@ public final class MessageDetailsRepository { private final Context context = ApplicationDependencies.getApplication(); - @NonNull LiveData getMessageRecord(String type, Long messageId) { - return new MessageRecordLiveData(new MessageId(messageId, type.equals(MmsSmsTable.MMS_TRANSPORT))); + @NonNull LiveData getMessageRecord(Long messageId) { + return new MessageRecordLiveData(new MessageId(messageId, true)); } @NonNull LiveData getMessageDetails(@Nullable MessageRecord messageRecord) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsViewModel.java index 3cb9c0274b..07d7cd789d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsViewModel.java @@ -17,11 +17,11 @@ final class MessageDetailsViewModel extends ViewModel { private final LiveData recipient; private final LiveData messageDetails; - private MessageDetailsViewModel(RecipientId recipientId, String type, Long messageId) { + private MessageDetailsViewModel(RecipientId recipientId, Long messageId) { recipient = Recipient.live(recipientId).getLiveData(); MessageDetailsRepository repository = new MessageDetailsRepository(); - LiveData messageRecord = repository.getMessageRecord(type, messageId); + LiveData messageRecord = repository.getMessageRecord(messageId); messageDetails = Transformations.switchMap(messageRecord, repository::getMessageDetails); } @@ -37,18 +37,16 @@ final class MessageDetailsViewModel extends ViewModel { static final class Factory implements ViewModelProvider.Factory { private final RecipientId recipientId; - private final String type; private final Long messageId; - Factory(RecipientId recipientId, String type, Long messageId) { + Factory(RecipientId recipientId, Long messageId) { this.recipientId = recipientId; - this.type = type; this.messageId = messageId; } @Override public @NonNull T create(@NonNull Class modelClass) { - return Objects.requireNonNull(modelClass.cast(new MessageDetailsViewModel(recipientId, type, messageId))); + return Objects.requireNonNull(modelClass.cast(new MessageDetailsViewModel(recipientId, messageId))); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java index a42be412e8..a0208b02c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java @@ -40,7 +40,6 @@ import org.thoughtcrime.securesms.database.GroupReceiptTable.GroupReceiptInfo; import org.thoughtcrime.securesms.database.MessageTable; import org.thoughtcrime.securesms.database.MessageTable.InsertResult; import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId; -import org.thoughtcrime.securesms.database.MmsTable; import org.thoughtcrime.securesms.database.MmsSmsTable; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.PaymentTable; @@ -2164,8 +2163,8 @@ public final class MessageContentProcessor { null, true); - MmsTable database = SignalDatabase.mms(); - long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); + MessageTable database = SignalDatabase.mms(); + long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); long messageId; List attachments; diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java index 5ecce91ec3..139d3ef942 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/LegacyMigrationJob.java @@ -10,8 +10,7 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.AttachmentTable; import org.thoughtcrime.securesms.database.MessageTable; -import org.thoughtcrime.securesms.database.MmsTable; -import org.thoughtcrime.securesms.database.MmsTable.Reader; +import org.thoughtcrime.securesms.database.MessageTable.MmsReader; import org.thoughtcrime.securesms.database.PushTable; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.model.MessageRecord; @@ -251,7 +250,7 @@ public class LegacyMigrationJob extends MigrationJob { Log.i(TAG, pendingAttachments.size() + " pending parts."); for (DatabaseAttachment attachment : pendingAttachments) { - final Reader reader = MmsTable.readerFor(mmsDb.getMessageCursor(attachment.getMmsId())); + final MmsReader reader = MessageTable.mmsReaderFor(mmsDb.getMessageCursor(attachment.getMmsId())); final MessageRecord record = reader.getNext(); if (attachment.hasData()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java index 1595ba02fd..94014a1f3a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java @@ -4,9 +4,7 @@ import android.content.Context; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.database.MessageTable; -import org.thoughtcrime.securesms.database.MmsTable; import org.thoughtcrime.securesms.database.SignalDatabase; -import org.thoughtcrime.securesms.database.SmsTable; import org.thoughtcrime.securesms.database.model.MessageRecord; import java.util.Comparator; @@ -55,8 +53,8 @@ public class ExpiringMessageManager { private class LoadTask implements Runnable { public void run() { - SmsTable.Reader smsReader = SmsTable.readerFor(smsDatabase.getExpirationStartedMessages()); - MmsTable.Reader mmsReader = MmsTable.readerFor(mmsDatabase.getExpirationStartedMessages()); + MessageTable.SmsReader smsReader = MessageTable.smsReaderFor(smsDatabase.getExpirationStartedMessages()); + MessageTable.MmsReader mmsReader = MessageTable.mmsReaderFor(mmsDatabase.getExpirationStartedMessages()); MessageRecord messageRecord; diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/SmsDeliveryListener.java b/app/src/main/java/org/thoughtcrime/securesms/service/SmsDeliveryListener.java index cace9629f6..1739cea8c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/SmsDeliveryListener.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/SmsDeliveryListener.java @@ -6,7 +6,7 @@ import android.content.Intent; import android.telephony.SmsMessage; import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.database.SmsTable; +import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobs.SmsSentJob; @@ -55,9 +55,9 @@ public class SmsDeliveryListener extends BroadcastReceiver { // Note: https://stackoverflow.com/a/33240109 if ("3gpp2".equals(intent.getStringExtra("format"))) { Log.w(TAG, "Correcting for CDMA delivery receipt..."); - if (status >> 24 <= 0) status = SmsTable.Status.STATUS_COMPLETE; - else if (status >> 24 == 2) status = SmsTable.Status.STATUS_PENDING; - else if (status >> 24 == 3) status = SmsTable.Status.STATUS_FAILED; + if (status >> 24 <= 0) status = MmsSmsColumns.Status.STATUS_COMPLETE; + else if (status >> 24 == 2) status = MmsSmsColumns.Status.STATUS_PENDING; + else if (status >> 24 == 3) status = MmsSmsColumns.Status.STATUS_FAILED; } jobManager.add(new SmsSentJob(messageId, isMultipart, DELIVERED_SMS_ACTION, status, runAttempt)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java index 649dcb544f..9e568c32a7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java @@ -40,7 +40,6 @@ import org.thoughtcrime.securesms.database.MmsSmsTable; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.RecipientTable; import org.thoughtcrime.securesms.database.SignalDatabase; -import org.thoughtcrime.securesms.database.SmsTable; import org.thoughtcrime.securesms.database.ThreadTable; import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageRecord; @@ -113,7 +112,7 @@ public class MessageSender { final long threadId, final boolean forceSms, @Nullable final String metricId, - final SmsTable.InsertListener insertListener) + final MessageTable.InsertListener insertListener) { Log.i(TAG, "Sending text message to " + message.getRecipient().getId() + ", thread: " + threadId); MessageTable database = SignalDatabase.sms(); @@ -139,7 +138,7 @@ public class MessageSender { public static void sendStories(@NonNull final Context context, @NonNull final List messages, @Nullable final String metricId, - @Nullable final SmsTable.InsertListener insertListener) + @Nullable final MessageTable.InsertListener insertListener) { Log.i(TAG, "Sending story messages to " + messages.size() + " targets."); ThreadTable threadTable = SignalDatabase.threads(); @@ -253,7 +252,7 @@ public class MessageSender { final long threadId, final boolean forceSms, @Nullable final String metricId, - @Nullable final SmsTable.InsertListener insertListener) + @Nullable final MessageTable.InsertListener insertListener) { Log.i(TAG, "Sending media message to " + message.getRecipient().getId() + ", thread: " + threadId); try { @@ -285,7 +284,7 @@ public class MessageSender { final OutgoingMediaMessage message, final Collection preUploadResults, final long threadId, - final SmsTable.InsertListener insertListener) + final MessageTable.InsertListener insertListener) { Log.i(TAG, "Sending media message with pre-uploads to " + message.getRecipient().getId() + ", thread: " + threadId + ", pre-uploads: " + preUploadResults); Preconditions.checkArgument(message.getAttachments().isEmpty(), "If the media is pre-uploaded, there should be no attachments on the message."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyDataSource.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyDataSource.kt index bd1af8e864..8efbea024b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyDataSource.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyDataSource.kt @@ -2,8 +2,8 @@ package org.thoughtcrime.securesms.stories.viewer.reply.group import org.signal.paging.PagedDataSource import org.thoughtcrime.securesms.conversation.ConversationMessage +import org.thoughtcrime.securesms.database.MessageTable import org.thoughtcrime.securesms.database.MmsSmsColumns -import org.thoughtcrime.securesms.database.MmsTable import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MmsMessageRecord @@ -18,9 +18,9 @@ class StoryGroupReplyDataSource(private val parentStoryId: Long) : PagedDataSour val results: MutableList = ArrayList(length) SignalDatabase.mms.getStoryReplies(parentStoryId).use { cursor -> cursor.moveToPosition(start - 1) - val reader = MmsTable.Reader(cursor) + val mmsReader = MessageTable.MmsReader(cursor) while (cursor.moveToNext() && cursor.position < start + length) { - results.add(readRowFromRecord(reader.current as MmsMessageRecord)) + results.add(readRowFromRecord(mmsReader.current as MmsMessageRecord)) } } diff --git a/app/src/spinner/java/org/thoughtcrime/securesms/database/GV2UpdateTransformer.kt b/app/src/spinner/java/org/thoughtcrime/securesms/database/GV2UpdateTransformer.kt index 29d27a4333..0883fb4d3b 100644 --- a/app/src/spinner/java/org/thoughtcrime/securesms/database/GV2UpdateTransformer.kt +++ b/app/src/spinner/java/org/thoughtcrime/securesms/database/GV2UpdateTransformer.kt @@ -13,7 +13,7 @@ import org.thoughtcrime.securesms.util.Base64 object GV2UpdateTransformer : ColumnTransformer { override fun matches(tableName: String?, columnName: String): Boolean { - return columnName == MmsSmsColumns.BODY && (tableName == null || (tableName == SmsTable.TABLE_NAME || tableName == MmsTable.TABLE_NAME)) + return columnName == MmsSmsColumns.BODY && (tableName == null || tableName == MessageTable.TABLE_NAME) } override fun transform(tableName: String?, columnName: String, cursor: Cursor): String { diff --git a/app/src/spinner/java/org/thoughtcrime/securesms/database/IsStoryTransformer.kt b/app/src/spinner/java/org/thoughtcrime/securesms/database/IsStoryTransformer.kt index 00f1e91399..388a141b33 100644 --- a/app/src/spinner/java/org/thoughtcrime/securesms/database/IsStoryTransformer.kt +++ b/app/src/spinner/java/org/thoughtcrime/securesms/database/IsStoryTransformer.kt @@ -7,11 +7,11 @@ import org.thoughtcrime.securesms.database.model.StoryType.Companion.fromCode object IsStoryTransformer : ColumnTransformer { override fun matches(tableName: String?, columnName: String): Boolean { - return columnName == MmsTable.STORY_TYPE && (tableName == null || tableName == MmsTable.TABLE_NAME) + return columnName == MessageTable.STORY_TYPE && (tableName == null || tableName == MessageTable.TABLE_NAME) } override fun transform(tableName: String?, columnName: String, cursor: Cursor): String { - val storyType = fromCode(cursor.requireInt(MmsTable.STORY_TYPE)) - return "${cursor.requireInt(MmsTable.STORY_TYPE)}

$storyType" + val storyType = fromCode(cursor.requireInt(MessageTable.STORY_TYPE)) + return "${cursor.requireInt(MessageTable.STORY_TYPE)}

$storyType" } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/MmsDatabaseTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/MmsDatabaseTest.kt index 1972317328..72809301b8 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/MmsDatabaseTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/MmsDatabaseTest.kt @@ -21,16 +21,16 @@ import org.thoughtcrime.securesms.testing.TestDatabaseUtil @Config(manifest = Config.NONE, application = Application::class) class MmsDatabaseTest { private lateinit var db: SQLiteDatabase - private lateinit var mmsTable: MmsTable + private lateinit var messageTable: MessageTable @Before fun setup() { val sqlCipher = TestDatabaseUtil.inMemoryDatabase { - execSQL(MmsTable.CREATE_TABLE) + execSQL(MessageTable.CREATE_TABLE) } db = sqlCipher.writableDatabase - mmsTable = MmsTable(ApplicationProvider.getApplicationContext(), sqlCipher) + messageTable = MessageTable(ApplicationProvider.getApplicationContext(), sqlCipher) } @After @@ -41,75 +41,75 @@ class MmsDatabaseTest { @Test fun `isGroupQuitMessage when normal message, return false`() { val id = TestMms.insert(db, type = Types.BASE_SENDING_TYPE or Types.SECURE_MESSAGE_BIT or Types.PUSH_MESSAGE_BIT) - assertFalse(mmsTable.isGroupQuitMessage(id)) + assertFalse(messageTable.isGroupQuitMessage(id)) } @Test fun `isGroupQuitMessage when legacy quit message, return true`() { val id = TestMms.insert(db, type = Types.BASE_SENDING_TYPE or Types.SECURE_MESSAGE_BIT or Types.PUSH_MESSAGE_BIT or Types.GROUP_LEAVE_BIT) - assertTrue(mmsTable.isGroupQuitMessage(id)) + assertTrue(messageTable.isGroupQuitMessage(id)) } @Test fun `isGroupQuitMessage when GV2 leave update, return false`() { val id = TestMms.insert(db, type = Types.BASE_SENDING_TYPE or Types.SECURE_MESSAGE_BIT or Types.PUSH_MESSAGE_BIT or Types.GROUP_LEAVE_BIT or Types.GROUP_V2_BIT or Types.GROUP_UPDATE_BIT) - assertFalse(mmsTable.isGroupQuitMessage(id)) + assertFalse(messageTable.isGroupQuitMessage(id)) } @Test fun `getLatestGroupQuitTimestamp when only normal message, return -1`() { TestMms.insert(db, threadId = 1, sentTimeMillis = 1, type = Types.BASE_SENDING_TYPE or Types.SECURE_MESSAGE_BIT or Types.PUSH_MESSAGE_BIT) - assertEquals(-1, mmsTable.getLatestGroupQuitTimestamp(1, 4)) + assertEquals(-1, messageTable.getLatestGroupQuitTimestamp(1, 4)) } @Test fun `getLatestGroupQuitTimestamp when legacy quit, return message timestamp`() { TestMms.insert(db, threadId = 1, sentTimeMillis = 2, type = Types.BASE_SENDING_TYPE or Types.SECURE_MESSAGE_BIT or Types.PUSH_MESSAGE_BIT or Types.GROUP_LEAVE_BIT) - assertEquals(2, mmsTable.getLatestGroupQuitTimestamp(1, 4)) + assertEquals(2, messageTable.getLatestGroupQuitTimestamp(1, 4)) } @Test fun `getLatestGroupQuitTimestamp when GV2 leave update message, return -1`() { TestMms.insert(db, threadId = 1, sentTimeMillis = 3, type = Types.BASE_SENDING_TYPE or Types.SECURE_MESSAGE_BIT or Types.PUSH_MESSAGE_BIT or Types.GROUP_LEAVE_BIT or Types.GROUP_V2_BIT or Types.GROUP_UPDATE_BIT) - assertEquals(-1, mmsTable.getLatestGroupQuitTimestamp(1, 4)) + assertEquals(-1, messageTable.getLatestGroupQuitTimestamp(1, 4)) } @Test fun `Given no stories in database, when I getStoryViewState, then I expect NONE`() { - assertEquals(StoryViewState.NONE, mmsTable.getStoryViewState(1)) + assertEquals(StoryViewState.NONE, messageTable.getStoryViewState(1)) } @Test fun `Given stories in database not in thread 1, when I getStoryViewState for thread 1, then I expect NONE`() { TestMms.insert(db, threadId = 2, storyType = StoryType.STORY_WITH_REPLIES) TestMms.insert(db, threadId = 2, storyType = StoryType.STORY_WITH_REPLIES) - assertEquals(StoryViewState.NONE, mmsTable.getStoryViewState(1)) + assertEquals(StoryViewState.NONE, messageTable.getStoryViewState(1)) } @Test fun `Given viewed incoming stories in database, when I getStoryViewState, then I expect VIEWED`() { TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = true) TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = true) - assertEquals(StoryViewState.VIEWED, mmsTable.getStoryViewState(1)) + assertEquals(StoryViewState.VIEWED, messageTable.getStoryViewState(1)) } @Test fun `Given unviewed incoming stories in database, when I getStoryViewState, then I expect UNVIEWED`() { TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = false) TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = false) - assertEquals(StoryViewState.UNVIEWED, mmsTable.getStoryViewState(1)) + assertEquals(StoryViewState.UNVIEWED, messageTable.getStoryViewState(1)) } @Test fun `Given mix of viewed and unviewed incoming stories in database, when I getStoryViewState, then I expect UNVIEWED`() { TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = true) TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = false) - assertEquals(StoryViewState.UNVIEWED, mmsTable.getStoryViewState(1)) + assertEquals(StoryViewState.UNVIEWED, messageTable.getStoryViewState(1)) } @Test fun `Given only outgoing story in database, when I getStoryViewState, then I expect VIEWED`() { TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, type = Types.BASE_OUTBOX_TYPE) - assertEquals(StoryViewState.VIEWED, mmsTable.getStoryViewState(1)) + assertEquals(StoryViewState.VIEWED, messageTable.getStoryViewState(1)) } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/MmsSmsDatabaseTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/MmsSmsDatabaseTest.kt index c4140c4022..88152830b8 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/MmsSmsDatabaseTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/MmsSmsDatabaseTest.kt @@ -5,8 +5,6 @@ import android.database.sqlite.SQLiteDatabase import androidx.test.core.app.ApplicationProvider import org.junit.After import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -25,8 +23,7 @@ class MmsSmsDatabaseTest { @Before fun setup() { val sqlCipher = TestDatabaseUtil.inMemoryDatabase { - execSQL(MmsTable.CREATE_TABLE) - execSQL(SmsTable.CREATE_TABLE) + execSQL(MessageTable.CREATE_TABLE) } db = sqlCipher.writableDatabase @@ -44,7 +41,6 @@ class MmsSmsDatabaseTest { mmsSmsTable.getConversationSnippetCursor(1).use { cursor -> cursor.moveToFirst() assertEquals(1, CursorUtil.requireLong(cursor, MmsSmsColumns.ID)) - assertFalse(CursorUtil.requireBoolean(cursor, MmsSmsTable.TRANSPORT)) } } @@ -54,7 +50,6 @@ class MmsSmsDatabaseTest { mmsSmsTable.getConversationSnippetCursor(1).use { cursor -> cursor.moveToFirst() assertEquals(1, CursorUtil.requireLong(cursor, MmsSmsColumns.ID)) - assertTrue(CursorUtil.requireBoolean(cursor, MmsSmsTable.TRANSPORT)) } } @@ -66,14 +61,12 @@ class MmsSmsDatabaseTest { mmsSmsTable.getConversationSnippetCursor(1).use { cursor -> cursor.moveToFirst() assertEquals(1, CursorUtil.requireLong(cursor, MmsSmsColumns.ID)) - assertTrue(CursorUtil.requireBoolean(cursor, MmsSmsTable.TRANSPORT)) } TestSms.insert(db, receivedTimestampMillis = timestamp + 3, type = MmsSmsColumns.Types.BASE_SENDING_TYPE or MmsSmsColumns.Types.SECURE_MESSAGE_BIT or MmsSmsColumns.Types.PUSH_MESSAGE_BIT or MmsSmsColumns.Types.GROUP_V2_LEAVE_BITS) mmsSmsTable.getConversationSnippetCursor(1).use { cursor -> cursor.moveToFirst() assertEquals(1, CursorUtil.requireLong(cursor, MmsSmsColumns.ID)) - assertTrue(CursorUtil.requireBoolean(cursor, MmsSmsTable.TRANSPORT)) } } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/SmsDatabaseTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/SmsDatabaseTest.kt index cf5beed891..2864cdf04f 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/SmsDatabaseTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/SmsDatabaseTest.kt @@ -20,16 +20,16 @@ import org.hamcrest.CoreMatchers.`is` as isEqual class SmsDatabaseTest { private lateinit var db: AndroidSQLiteDatabase - private lateinit var smsTable: SmsTable + private lateinit var messageTable: MessageTable @Before fun setup() { val sqlCipher = TestDatabaseUtil.inMemoryDatabase { - execSQL(SmsTable.CREATE_TABLE) + execSQL(MessageTable.CREATE_TABLE) } db = sqlCipher.writableDatabase - smsTable = SmsTable(ApplicationProvider.getApplicationContext(), sqlCipher) + messageTable = MessageTable(ApplicationProvider.getApplicationContext(), sqlCipher) } @After @@ -39,52 +39,52 @@ class SmsDatabaseTest { @Test fun `getThreadIdForMessage when no message absent for id, return -1`() { - assertThat(smsTable.getThreadIdForMessage(1), isEqual(-1)) + assertThat(messageTable.getThreadIdForMessage(1), isEqual(-1)) } @Test fun `getThreadIdForMessage when message present for id, return thread id`() { TestSms.insert(db) - assertThat(smsTable.getThreadIdForMessage(1), isEqual(1)) + assertThat(messageTable.getThreadIdForMessage(1), isEqual(1)) } @Test fun `hasMeaningfulMessage when no messages, return false`() { - assertFalse(smsTable.hasMeaningfulMessage(1)) + assertFalse(messageTable.hasMeaningfulMessage(1)) } @Test fun `hasMeaningfulMessage when normal message, return true`() { TestSms.insert(db) - assertTrue(smsTable.hasMeaningfulMessage(1)) + assertTrue(messageTable.hasMeaningfulMessage(1)) } @Test fun `hasMeaningfulMessage when GV2 create message only, return true`() { TestSms.insert(db, type = MmsSmsColumns.Types.BASE_INBOX_TYPE or MmsSmsColumns.Types.SECURE_MESSAGE_BIT or MmsSmsColumns.Types.GROUP_V2_BIT or MmsSmsColumns.Types.GROUP_UPDATE_BIT) - assertTrue(smsTable.hasMeaningfulMessage(1)) + assertTrue(messageTable.hasMeaningfulMessage(1)) } @Test fun `hasMeaningfulMessage when empty and then with ignored types, always return false`() { - assertFalse(smsTable.hasMeaningfulMessage(1)) + assertFalse(messageTable.hasMeaningfulMessage(1)) - TestSms.insert(db, type = SmsTable.IGNORABLE_TYPESMASK_WHEN_COUNTING) - assertFalse(smsTable.hasMeaningfulMessage(1)) + TestSms.insert(db, type = MmsSmsColumns.Types.IGNORABLE_TYPESMASK_WHEN_COUNTING) + assertFalse(messageTable.hasMeaningfulMessage(1)) TestSms.insert(db, type = MmsSmsColumns.Types.PROFILE_CHANGE_TYPE) - assertFalse(smsTable.hasMeaningfulMessage(1)) + assertFalse(messageTable.hasMeaningfulMessage(1)) TestSms.insert(db, type = MmsSmsColumns.Types.CHANGE_NUMBER_TYPE) - assertFalse(smsTable.hasMeaningfulMessage(1)) + assertFalse(messageTable.hasMeaningfulMessage(1)) TestSms.insert(db, type = MmsSmsColumns.Types.BOOST_REQUEST_TYPE) - assertFalse(smsTable.hasMeaningfulMessage(1)) + assertFalse(messageTable.hasMeaningfulMessage(1)) TestSms.insert(db, type = MmsSmsColumns.Types.SMS_EXPORT_TYPE) - assertFalse(smsTable.hasMeaningfulMessage(1)) + assertFalse(messageTable.hasMeaningfulMessage(1)) TestSms.insert(db, type = MmsSmsColumns.Types.BASE_INBOX_TYPE or MmsSmsColumns.Types.GROUP_V2_LEAVE_BITS) - assertFalse(smsTable.hasMeaningfulMessage(1)) + assertFalse(messageTable.hasMeaningfulMessage(1)) } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/TestMms.kt b/app/src/test/java/org/thoughtcrime/securesms/database/TestMms.kt index ca34c31208..ffec7bda78 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/TestMms.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/TestMms.kt @@ -76,39 +76,39 @@ object TestMms { receivedTimestampMillis: Long = System.currentTimeMillis(), ): Long { val contentValues = ContentValues().apply { - put(MmsTable.DATE_SENT, message.sentTimeMillis) - put(MmsTable.MMS_MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ) + put(MessageTable.DATE_SENT, message.sentTimeMillis) + put(MessageTable.MMS_MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ) - put(MmsTable.TYPE, type) + put(MessageTable.TYPE, type) put(MmsSmsColumns.THREAD_ID, threadId) put(MmsSmsColumns.READ, if (unread) 0 else 1) - put(MmsTable.DATE_RECEIVED, receivedTimestampMillis) + put(MessageTable.DATE_RECEIVED, receivedTimestampMillis) put(MmsSmsColumns.SMS_SUBSCRIPTION_ID, message.subscriptionId) put(MmsSmsColumns.EXPIRES_IN, message.expiresIn) - put(MmsTable.VIEW_ONCE, message.isViewOnce) + put(MessageTable.VIEW_ONCE, message.isViewOnce) put(MmsSmsColumns.RECIPIENT_ID, recipientId.serialize()) put(MmsSmsColumns.DELIVERY_RECEIPT_COUNT, 0) put(MmsSmsColumns.RECEIPT_TIMESTAMP, 0) put(MmsSmsColumns.VIEWED_RECEIPT_COUNT, if (viewed) 1 else 0) - put(MmsTable.STORY_TYPE, message.storyType.code) + put(MessageTable.STORY_TYPE, message.storyType.code) put(MmsSmsColumns.BODY, body) - put(MmsTable.MENTIONS_SELF, 0) + put(MessageTable.MENTIONS_SELF, 0) } - return db.insert(MmsTable.TABLE_NAME, null, contentValues) + return db.insert(MessageTable.TABLE_NAME, null, contentValues) } fun markAsRemoteDelete(db: SQLiteDatabase, messageId: Long) { val values = ContentValues() values.put(MmsSmsColumns.REMOTE_DELETED, 1) values.putNull(MmsSmsColumns.BODY) - values.putNull(MmsTable.QUOTE_BODY) - values.putNull(MmsTable.QUOTE_AUTHOR) - values.put(MmsTable.QUOTE_TYPE, -1) - values.putNull(MmsTable.QUOTE_ID) - values.putNull(MmsTable.LINK_PREVIEWS) - values.putNull(MmsTable.SHARED_CONTACTS) - db.update(MmsTable.TABLE_NAME, values, DatabaseTable.ID_WHERE, arrayOf(messageId.toString())) + values.putNull(MessageTable.QUOTE_BODY) + values.putNull(MessageTable.QUOTE_AUTHOR) + values.put(MessageTable.QUOTE_TYPE, -1) + values.putNull(MessageTable.QUOTE_ID) + values.putNull(MessageTable.LINK_PREVIEWS) + values.putNull(MessageTable.SHARED_CONTACTS) + db.update(MessageTable.TABLE_NAME, values, DatabaseTable.ID_WHERE, arrayOf(messageId.toString())) } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/TestSms.kt b/app/src/test/java/org/thoughtcrime/securesms/database/TestSms.kt index 566d42befc..717b075f65 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/TestSms.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/TestSms.kt @@ -61,19 +61,19 @@ object TestSms { val values = ContentValues().apply { put(MmsSmsColumns.RECIPIENT_ID, message.sender.serialize()) put(MmsSmsColumns.RECIPIENT_DEVICE_ID, message.senderDeviceId) - put(SmsTable.DATE_RECEIVED, message.receivedTimestampMillis) - put(SmsTable.DATE_SENT, message.sentTimestampMillis) + put(MmsSmsColumns.DATE_RECEIVED, message.receivedTimestampMillis) + put(MmsSmsColumns.DATE_SENT, message.sentTimestampMillis) put(MmsSmsColumns.DATE_SERVER, message.serverTimestampMillis) put(MmsSmsColumns.READ, if (unread) 0 else 1) put(MmsSmsColumns.SMS_SUBSCRIPTION_ID, message.subscriptionId) put(MmsSmsColumns.EXPIRES_IN, message.expiresIn) put(MmsSmsColumns.UNIDENTIFIED, message.isUnidentified) put(MmsSmsColumns.BODY, message.messageBody) - put(SmsTable.TYPE, type) + put(MmsSmsColumns.TYPE, type) put(MmsSmsColumns.THREAD_ID, threadId) put(MmsSmsColumns.SERVER_GUID, message.serverGuid) } - return db.insert(SmsTable.TABLE_NAME, null, values) + return db.insert(MessageTable.TABLE_NAME, null, values) } } diff --git a/core-util/src/main/java/org/signal/core/util/SqlUtil.kt b/core-util/src/main/java/org/signal/core/util/SqlUtil.kt index d4cbd98d96..2a2384a775 100644 --- a/core-util/src/main/java/org/signal/core/util/SqlUtil.kt +++ b/core-util/src/main/java/org/signal/core/util/SqlUtil.kt @@ -44,6 +44,17 @@ object SqlUtil { return tables } + fun getNextAutoIncrementId(db: SupportSQLiteDatabase, table: String): Long { + db.query("SELECT * FROM sqlite_sequence WHERE name = ?", arrayOf(table)).use { cursor -> + if (cursor.moveToFirst()) { + val current = cursor.requireLong("seq") + return current + 1 + } else { + throw IllegalArgumentException("Table must have an auto-incrementing primary key!") + } + } + } + /** * Given a table, this will return a set of tables that it has a foreign key dependency on. */