diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsRepository.kt
index 048f58f9d3..4a0e71b5fb 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsRepository.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/sms/SmsSettingsRepository.kt
@@ -18,7 +18,7 @@ class SmsSettingsRepository(
@WorkerThread
private fun checkInsecureMessageCount(): SmsExportState? {
- val totalSmsMmsCount = smsDatabase.insecureMessageCount + mmsDatabase.insecureMessageCount
+ val totalSmsMmsCount = smsDatabase.getInsecureMessageCount() + mmsDatabase.getInsecureMessageCount()
return if (totalSmsMmsCount == 0) {
SmsExportState.NO_SMS_MESSAGES_IN_DATABASE
@@ -29,7 +29,7 @@ class SmsSettingsRepository(
@WorkerThread
private fun checkUnexportedInsecureMessageCount(): SmsExportState {
- val totalUnexportedCount = smsDatabase.unexportedInsecureMessagesCount + mmsDatabase.unexportedInsecureMessagesCount
+ val totalUnexportedCount = smsDatabase.getUnexportedInsecureMessagesCount() + mmsDatabase.getUnexportedInsecureMessagesCount()
return if (totalUnexportedCount > 0) {
SmsExportState.HAS_UNEXPORTED_MESSAGES
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 7a4613c5b5..0c27dd525b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
@@ -1193,7 +1193,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
if (message.getScheduledDate() != -1) {
return;
}
- MessageRecord messageRecord = MessageTable.readerFor(message, threadId).getCurrent();
if (getListAdapter() != null) {
setLastSeen(0);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.java
deleted file mode 100644
index b7fb19aacf..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.java
+++ /dev/null
@@ -1,5544 +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.SpannableString;
-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 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.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.conversation.MessageStyler;
-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.DisplayRecord;
-import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil;
-import org.thoughtcrime.securesms.database.model.GroupRecord;
-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.MmsMessageRecord;
-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.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.database.model.databaseprotos.SessionSwitchoverEvent;
-import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
-import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
-import org.thoughtcrime.securesms.insights.InsightsConstants;
-import org.thoughtcrime.securesms.jobs.OptimizeMessageSearchIndexJob;
-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.OutgoingMessage;
-import org.thoughtcrime.securesms.mms.QuoteModel;
-import org.thoughtcrime.securesms.mms.SlideDeck;
-import org.thoughtcrime.securesms.notifications.v2.DefaultMessageNotifier;
-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.stories.Stories;
-import org.thoughtcrime.securesms.util.Base64;
-import org.thoughtcrime.securesms.util.FeatureFlags;
-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.messages.multidevice.ReadMessage;
-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.Comparator;
-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;
-import java.util.function.Function;
-
-import kotlin.Unit;
-
-import static org.thoughtcrime.securesms.contactshare.Contact.Avatar;
-
-public class MessageTable extends DatabaseTable implements MessageTypes, RecipientIdDatabaseReference, ThreadIdDatabaseReference {
-
- private static final String TAG = Log.tag(MessageTable.class);
-
- public static final String TABLE_NAME = "message";
- public static final String ID = "_id";
- public static final String DATE_SENT = "date_sent";
- public static final String DATE_RECEIVED = "date_received";
- public static final String TYPE = "type";
- public static final String DATE_SERVER = "date_server";
- public static final String THREAD_ID = "thread_id";
- public static final String READ = "read";
- public static final String BODY = "body";
- public static final String RECIPIENT_ID = "recipient_id";
- public static final String RECIPIENT_DEVICE_ID = "recipient_device_id";
- public static final String DELIVERY_RECEIPT_COUNT = "delivery_receipt_count";
- public static final String READ_RECEIPT_COUNT = "read_receipt_count";
- public static final String VIEWED_RECEIPT_COUNT = "viewed_receipt_count";
- public static final String MISMATCHED_IDENTITIES = "mismatched_identities";
- public static final String SMS_SUBSCRIPTION_ID = "subscription_id";
- public static final String EXPIRES_IN = "expires_in";
- public static final String EXPIRE_STARTED = "expire_started";
- public static final String NOTIFIED = "notified";
- public static final String NOTIFIED_TIMESTAMP = "notified_timestamp";
- public static final String UNIDENTIFIED = "unidentified";
- public static final String REACTIONS_UNREAD = "reactions_unread";
- public static final String REACTIONS_LAST_SEEN = "reactions_last_seen";
- public static final String REMOTE_DELETED = "remote_deleted";
- public static final String SERVER_GUID = "server_guid";
- public static final String RECEIPT_TIMESTAMP = "receipt_timestamp";
- public static final String EXPORT_STATE = "export_state";
- public static final String EXPORTED = "exported";
- public static final String MMS_CONTENT_LOCATION = "ct_l";
- public static final String MMS_EXPIRY = "exp";
- public static final String MMS_MESSAGE_TYPE = "m_type";
- public static final String MMS_MESSAGE_SIZE = "m_size";
- public static final String MMS_STATUS = "st";
- public static final String MMS_TRANSACTION_ID = "tr_id";
- public static final String NETWORK_FAILURES = "network_failures";
- public static final String QUOTE_ID = "quote_id";
- public static final String QUOTE_AUTHOR = "quote_author";
- public static final String QUOTE_BODY = "quote_body";
- public static final String QUOTE_MISSING = "quote_missing";
- public static final String QUOTE_BODY_RANGES = "quote_mentions";
- public static final String QUOTE_TYPE = "quote_type";
- public static final String SHARED_CONTACTS = "shared_contacts";
- public static final String LINK_PREVIEWS = "link_previews";
- public static final String MENTIONS_SELF = "mentions_self";
- public static final String MESSAGE_RANGES = "message_ranges";
- public static final String VIEW_ONCE = "view_once";
- public static final String STORY_TYPE = "story_type";
- public static final String PARENT_STORY_ID = "parent_story_id";
- public static final String SCHEDULED_DATE = "scheduled_date";
-
- 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 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_BODY_RANGES + " 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, " +
- SCHEDULED_DATE + " INTEGER DEFAULT -1);";
-
- private static final String INDEX_THREAD_DATE = "mms_thread_date_index";
- private static final String INDEX_THREAD_STORY_SCHEDULED_DATE = "mms_thread_story_parent_story_scheduled_date_index";
-
- 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 " + INDEX_THREAD_DATE + " 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 " + INDEX_THREAD_STORY_SCHEDULED_DATE + " ON " + TABLE_NAME + " (" + THREAD_ID + ", " + DATE_RECEIVED + "," + STORY_TYPE + "," + PARENT_STORY_ID + "," + SCHEDULED_DATE + ");",
- "CREATE INDEX IF NOT EXISTS message_quote_id_quote_author_scheduled_date_index ON " + TABLE_NAME + " (" + QUOTE_ID + ", " + QUOTE_AUTHOR + ", " + SCHEDULED_DATE + ");",
- "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 + " & " + MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION + " != 0;"
- };
-
- private static final String[] MMS_PROJECTION_BASE = 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_BODY_RANGES,
- 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,
- SCHEDULED_DATE,
- };
-
- private static final String[] MMS_PROJECTION = SqlUtil.appendArg(MMS_PROJECTION_BASE, "NULL AS " + AttachmentTable.ATTACHMENT_JSON_ALIAS);
-
- private static final String[] MMS_PROJECTION_WITH_ATTACHMENTS = SqlUtil.appendArg(MMS_PROJECTION_BASE, "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 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 static final String SNIPPET_QUERY = "SELECT " + MessageTable.ID + ", " + MessageTable.TYPE + ", " + MessageTable.DATE_RECEIVED + " FROM " + MessageTable.TABLE_NAME + " " +
- "WHERE " + MessageTable.THREAD_ID + " = ? AND " +
- MessageTable.TYPE + " & " + MessageTypes.GROUP_V2_LEAVE_BITS + " != " + MessageTypes.GROUP_V2_LEAVE_BITS + " AND " +
- MessageTable.STORY_TYPE + " = 0 AND " +
- MessageTable.PARENT_STORY_ID + " <= 0 AND " +
- MessageTable.SCHEDULED_DATE + " = -1 AND " +
- MessageTable.TYPE + " NOT IN (" + MessageTypes.PROFILE_CHANGE_TYPE + ", " + MessageTypes.GV1_MIGRATION_TYPE + ", " + MessageTypes.CHANGE_NUMBER_TYPE + ", " + MessageTypes.BOOST_REQUEST_TYPE + ", " + MessageTypes.SMS_EXPORT_TYPE + ") " +
- "ORDER BY " + MessageTable.DATE_RECEIVED + " DESC " +
- "LIMIT 1";
-
- private final EarlyReceiptCache earlyDeliveryReceiptCache = new EarlyReceiptCache("MmsDelivery");
-
- public MessageTable(Context context, SignalDatabase databaseHelper) {
- super(context, databaseHelper);
- }
-
- 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 = MessageTypes.SECURE_MESSAGE_BIT | MessageTypes.PUSH_MESSAGE_BIT | MessageTypes.GROUP_UPDATE_BIT | MessageTypes.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;
- }
-
- public Cursor getExpirationStartedMessages() {
- String where = EXPIRE_STARTED + " > 0";
- return rawQuery(where, null);
- }
-
- public Cursor getMessageCursor(long messageId) {
- return internalGetMessage(messageId);
- }
-
- public boolean hasReceivedAnyCallsSince(long threadId, long timestamp) {
- SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
- String[] projection = SqlUtil.buildArgs(TYPE);
- String selection = THREAD_ID + " = ? AND " + DATE_RECEIVED + " > ? AND (" + TYPE + " = ? OR " + TYPE + " = ? OR " + TYPE + " = ? OR " + TYPE + " =?)";
- String[] selectionArgs = SqlUtil.buildArgs(threadId,
- timestamp,
- MessageTypes.INCOMING_AUDIO_CALL_TYPE,
- MessageTypes.INCOMING_VIDEO_CALL_TYPE,
- MessageTypes.MISSED_AUDIO_CALL_TYPE,
- MessageTypes.MISSED_VIDEO_CALL_TYPE);
-
- try (Cursor cursor = db.query(TABLE_NAME, projection, selection, selectionArgs, null, null, null)) {
- return cursor != null && cursor.moveToFirst();
- }
- }
-
- public void markAsEndSession(long id) {
- updateTypeBitmask(id, MessageTypes.KEY_EXCHANGE_MASK, MessageTypes.END_SESSION_BIT);
- }
-
- public void markAsInvalidVersionKeyExchange(long id) {
- updateTypeBitmask(id, 0, MessageTypes.KEY_EXCHANGE_INVALID_VERSION_BIT);
- }
-
- public void markAsSecure(long id) {
- updateTypeBitmask(id, 0, MessageTypes.SECURE_MESSAGE_BIT);
- }
-
- public void markAsDecryptFailed(long id) {
- updateTypeBitmask(id, MessageTypes.ENCRYPTION_MASK, MessageTypes.ENCRYPTION_REMOTE_FAILED_BIT);
- }
-
- public void markAsNoSession(long id) {
- updateTypeBitmask(id, MessageTypes.ENCRYPTION_MASK, MessageTypes.ENCRYPTION_REMOTE_NO_SESSION_BIT);
- }
-
- public void markAsUnsupportedProtocolVersion(long id) {
- updateTypeBitmask(id, MessageTypes.BASE_TYPE_MASK, MessageTypes.UNSUPPORTED_MESSAGE_TYPE);
- }
-
- public void markAsInvalidMessage(long id) {
- updateTypeBitmask(id, MessageTypes.BASE_TYPE_MASK, MessageTypes.INVALID_MESSAGE_TYPE);
- }
-
- public void markAsLegacyVersion(long id) {
- updateTypeBitmask(id, MessageTypes.ENCRYPTION_MASK, MessageTypes.ENCRYPTION_REMOTE_LEGACY_BIT);
- }
-
- public void markAsMissedCall(long id, boolean isVideoOffer) {
- updateTypeBitmask(id, MessageTypes.TOTAL_MASK, isVideoOffer ? MessageTypes.MISSED_VIDEO_CALL_TYPE : MessageTypes.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 + " & " + (MessageTypes.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));
- 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 + " & " + (MessageTypes.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 = MessageTypes.BASE_INBOX_TYPE | MessageTypes.SECURE_MESSAGE_BIT | MessageTypes.PUSH_MESSAGE_BIT;
- return updateMessageBodyAndType(messageId, body, MessageTypes.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 + " & " + MessageTypes.BASE_INBOX_TYPE + " = " + MessageTypes.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), null, storyType));
- }
-
- 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 (MessageTypes.isSecureType(type) && MessageTypes.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);
- StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE));
-
- results.add(new MarkedMessageInfo(threadId, syncMessageId, new MessageId(messageId), null, storyType));
-
- 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());
-
- Set storyRecipientsUpdated = results.stream()
- .filter(it -> it.storyType.isStory())
- .map(it -> SignalDatabase.threads().getRecipientIdForThreadId(it.getThreadId()))
- .filter(it -> it != null)
- .collect(java.util.stream.Collectors.toSet());
-
- notifyConversationListeners(threadsUpdated);
- notifyConversationListListeners();
-
- ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(storyRecipientsUpdated);
-
- 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 + " & " + MessageTypes.SPECIAL_TYPES_MASK + " = " + MessageTypes.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), null, storyType));
-
- 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 InsertResult insertCallLog(@NonNull RecipientId recipientId, long type, long timestamp) {
- boolean unread = MessageTypes.isMissedAudioCall(type) || MessageTypes.isMissedVideoCall(type);
- Recipient recipient = Recipient.resolved(recipientId);
- long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
-
- ContentValues values = new ContentValues(7);
- 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);
-
- long messageId = getWritableDatabase().insert(TABLE_NAME, null, values);
-
- if (unread) {
- SignalDatabase.threads().incrementUnread(threadId, 1, 0);
- }
-
- SignalDatabase.threads().update(threadId, true);
-
- notifyConversationListeners(threadId);
- TrimThreadJob.enqueueAsync(threadId);
-
- return new InsertResult(messageId, threadId);
- }
-
- public void updateCallLog(long messageId, long type) {
- boolean unread = MessageTypes.isMissedAudioCall(type) || MessageTypes.isMissedVideoCall(type);
- ContentValues values = new ContentValues(2);
- values.put(TYPE, type);
- values.put(READ, unread ? 0 : 1);
- getWritableDatabase().update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(messageId));
-
- long threadId = getThreadIdForMessage(messageId);
- Recipient recipient = SignalDatabase.threads().getRecipientForThreadId(threadId);
-
- if (unread) {
- SignalDatabase.threads().incrementUnread(threadId, 1, 0);
- }
-
- SignalDatabase.threads().update(threadId, true);
-
- notifyConversationListeners(threadId);
- ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId));
- }
-
- 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, MessageTypes.GROUP_CALL_TYPE);
- values.put(THREAD_ID, threadId);
-
- db.insert(TABLE_NAME, null, values);
-
- SignalDatabase.threads().incrementUnread(threadId, 1, 0);
- }
-
- SignalDatabase.threads().update(threadId, true);
-
- 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(MessageTypes.GROUP_CALL_TYPE, threadId);
- boolean sameEraId = false;
-
- try (MmsReader reader = new MmsReader(db.query(TABLE_NAME, MMS_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, MessageTypes.GROUP_CALL_TYPE);
- values.put(THREAD_ID, threadId);
-
- db.insert(TABLE_NAME, null, values);
-
- SignalDatabase.threads().incrementUnread(threadId, 1, 0);
- }
-
- SignalDatabase.threads().update(threadId, true);
-
- 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(MessageTypes.GROUP_CALL_TYPE, threadId);
- boolean sameEraId = false;
-
- try (MmsReader reader = new MmsReader(db.query(TABLE_NAME, MMS_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 & (MessageTypes.TOTAL_MASK - MessageTypes.BASE_TYPE_MASK)) | MessageTypes.JOINED_TYPE;
- } else if (message.isPreKeyBundle()) {
- type |= MessageTypes.KEY_EXCHANGE_BIT | MessageTypes.KEY_EXCHANGE_BUNDLE_BIT;
- } else if (message.isSecureMessage()) {
- type |= MessageTypes.SECURE_MESSAGE_BIT;
- } else if (message.isGroup()) {
- IncomingGroupUpdateMessage incomingGroupUpdateMessage = (IncomingGroupUpdateMessage) message;
-
- type |= MessageTypes.SECURE_MESSAGE_BIT;
-
- if (incomingGroupUpdateMessage.isGroupV2()) {
- type |= MessageTypes.GROUP_V2_BIT | MessageTypes.GROUP_UPDATE_BIT;
- if (incomingGroupUpdateMessage.isJustAGroupLeave()) {
- type |= MessageTypes.GROUP_LEAVE_BIT;
- } else if (incomingGroupUpdateMessage.isCancelJoinRequest()) {
- tryToCollapseJoinRequestEvents = true;
- }
- } else if (incomingGroupUpdateMessage.isUpdate()) {
- type |= MessageTypes.GROUP_UPDATE_BIT;
- } else if (incomingGroupUpdateMessage.isQuit()) {
- type |= MessageTypes.GROUP_LEAVE_BIT;
- }
-
- } else if (message.isEndSession()) {
- type |= MessageTypes.SECURE_MESSAGE_BIT;
- type |= MessageTypes.END_SESSION_BIT;
- }
-
- if (message.isPush()) type |= MessageTypes.PUSH_MESSAGE_BIT;
- if (message.isIdentityUpdate()) type |= MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT;
- if (message.isContentPreKeyBundle()) type |= MessageTypes.KEY_EXCHANGE_CONTENT_FORMAT;
-
- if (message.isIdentityVerified()) type |= MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT;
- else if (message.isIdentityDefault()) type |= MessageTypes.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 & MessageTypes.GROUP_UPDATE_BIT) > 0;
-
- boolean unread = !silent && (message.isSecureMessage() ||
- message.isGroup() ||
- message.isPreKeyBundle() ||
- Util.isDefaultSmsProvider(context));
-
- 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) {
- SignalDatabase.threads().update(threadId, true);
- }
-
- 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, MessageTypes.BASE_INBOX_TYPE);
- }
-
- 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 (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, MessageTypes.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, MessageTypes.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 (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, MessageTypes.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, MessageTypes.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, MessageTypes.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 insertSessionSwitchoverEvent(@NonNull RecipientId recipientId, long threadId, @NonNull SessionSwitchoverEvent event) {
- if (!FeatureFlags.phoneNumberPrivacy()) {
- throw new IllegalStateException("Should not occur in a non-PNP world!");
- }
-
- 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, MessageTypes.SESSION_SWITCHOVER_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, MessageTypes.SMS_EXPORT_TYPE);
- values.put(THREAD_ID, threadId);
- values.putNull(BODY);
-
- boolean updated = SQLiteDatabaseExtensionsKt.withinTransaction(getWritableDatabase(), db -> {
- if (SignalDatabase.messages().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));
- }
- }
- }
-
- 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;
- }
-
- @VisibleForTesting
- 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);
- }
- }
-
- if (deletedStoryCount > 0) {
- OptimizeMessageSearchIndexJob.enqueue();
- }
-
- 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 = MessageTypes.getOutgoingEncryptedMessageType() | MessageTypes.GROUP_LEAVE_BIT;
- String query = ID + " = ? AND " + TYPE + " & " + type + " = " + type + " AND " + TYPE + " & " + MessageTypes.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 = MessageTypes.getOutgoingEncryptedMessageType() | MessageTypes.GROUP_LEAVE_BIT;
- String query = THREAD_ID + " = ? AND " + TYPE + " & " + type + " = " + type + " AND " + TYPE + " & " + MessageTypes.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 getScheduledMessageCountForThread(long threadId) {
- SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
-
- String query = THREAD_ID + " = ? AND " + STORY_TYPE + " = ? AND " + PARENT_STORY_ID + " <= ? AND " + SCHEDULED_DATE + " != ?";
- String[] args = SqlUtil.buildArgs(threadId, 0, 0, -1);
-
- try (Cursor cursor = db.query(TABLE_NAME + " INDEXED BY " + INDEX_THREAD_STORY_SCHEDULED_DATE, COUNT, query, args, null, null, null)) {
- if (cursor.moveToFirst()) {
- return cursor.getInt(0);
- }
- }
-
- return 0;
- }
-
- public int getMessageCountForThread(long threadId) {
- SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
-
- String query = THREAD_ID + " = ? AND " + STORY_TYPE + " = ? AND " + PARENT_STORY_ID + " <= ? AND " + SCHEDULED_DATE + " = ?";
- String[] args = SqlUtil.buildArgs(threadId, 0, 0, -1);
-
- try (Cursor cursor = db.query(TABLE_NAME + " INDEXED BY " + INDEX_THREAD_STORY_SCHEDULED_DATE, 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 + " <= ? AND " + SCHEDULED_DATE + " = ?";
- String[] args = SqlUtil.buildArgs(threadId, beforeTime, 0, 0, -1);
-
- 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) {
- if (threadId == -1) {
- return false;
- }
-
- 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 + " >= ? AND NOT (" + getOutgoingTypeClause() + ")";
- 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 + " & " + MessageTypes.GROUP_V2_LEAVE_BITS + " != " + MessageTypes.GROUP_V2_LEAVE_BITS + ")";
- return SqlUtil.buildQuery(query, threadId, 0, 0, MessageTypes.IGNORABLE_TYPESMASK_WHEN_COUNTING, MessageTypes.PROFILE_CHANGE_TYPE, MessageTypes.CHANGE_NUMBER_TYPE, MessageTypes.SMS_EXPORT_TYPE, MessageTypes.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 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_WITH_ATTACHMENTS, 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 + " & " + (MessageTypes.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, MessageTypes.BASE_TYPE_MASK, MessageTypes.BASE_OUTBOX_TYPE, Optional.of(threadId));
- }
-
- public void markAsForcedSms(long messageId) {
- long threadId = getThreadIdForMessage(messageId);
- updateMailboxBitmask(messageId, MessageTypes.PUSH_MESSAGE_BIT, MessageTypes.MESSAGE_FORCE_SMS_BIT, Optional.of(threadId));
- ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId));
- }
-
- public void markAsRateLimited(long messageId) {
- long threadId = getThreadIdForMessage(messageId);
- updateMailboxBitmask(messageId, 0, MessageTypes.MESSAGE_RATE_LIMITED_BIT, Optional.of(threadId));
- ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId));
- }
-
- public void clearRateLimitStatus(@NonNull Collection ids) {
- SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
-
- db.beginTransaction();
- try {
- for (long id : ids) {
- long threadId = getThreadIdForMessage(id);
- updateMailboxBitmask(id, MessageTypes.MESSAGE_RATE_LIMITED_BIT, 0, Optional.of(threadId));
- }
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- }
-
- public void markAsPendingInsecureSmsFallback(long messageId) {
- long threadId = getThreadIdForMessage(messageId);
- updateMailboxBitmask(messageId, MessageTypes.BASE_TYPE_MASK, MessageTypes.BASE_PENDING_INSECURE_SMS_FALLBACK, Optional.of(threadId));
- ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId));
- }
-
- public void markAsSending(long messageId) {
- long threadId = getThreadIdForMessage(messageId);
- updateMailboxBitmask(messageId, MessageTypes.BASE_TYPE_MASK, MessageTypes.BASE_SENDING_TYPE, Optional.of(threadId));
- ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId));
- ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners();
- }
-
- public void markAsSentFailed(long messageId) {
- long threadId = getThreadIdForMessage(messageId);
- updateMailboxBitmask(messageId, MessageTypes.BASE_TYPE_MASK, MessageTypes.BASE_SENT_FAILED_TYPE, Optional.of(threadId));
- ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId));
- ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners();
- }
-
- public void markAsSent(long messageId, boolean secure) {
- long threadId = getThreadIdForMessage(messageId);
- updateMailboxBitmask(messageId, MessageTypes.BASE_TYPE_MASK, MessageTypes.BASE_SENT_TYPE | (secure ? MessageTypes.PUSH_MESSAGE_BIT | MessageTypes.SECURE_MESSAGE_BIT : 0), Optional.of(threadId));
- ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId));
- 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);
- SignalDatabase.reactions().deleteReactions(new MessageId(messageId));
- deleteGroupStoryReplies(messageId);
- disassociateStoryQuotes(messageId);
-
- threadId = getThreadIdForMessage(messageId);
- SignalDatabase.threads().update(threadId, false);
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
-
- OptimizeMessageSearchIndexJob.enqueue();
-
- ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId));
- 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));
- }
-
- public boolean clearScheduledStatus(long threadId, long messageId, long expiresIn) {
- SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
- ContentValues contentValues = new ContentValues();
- contentValues.put(SCHEDULED_DATE, -1);
- contentValues.put(DATE_SENT, System.currentTimeMillis());
- contentValues.put(DATE_RECEIVED, System.currentTimeMillis());
- contentValues.put(EXPIRES_IN, expiresIn);
-
- int rowsUpdated = database.update(TABLE_NAME, contentValues, ID_WHERE + " AND " + SCHEDULED_DATE + "!= ?", SqlUtil.buildArgs(messageId, -1));
- ApplicationDependencies.getDatabaseObserver().notifyMessageInsertObservers(threadId, new MessageId(messageId));
- ApplicationDependencies.getDatabaseObserver().notifyScheduledMessageObservers(threadId);
-
- return rowsUpdated > 0;
- }
-
- public void rescheduleMessage(long threadId, long messageId, long time) {
- SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
- ContentValues contentValues = new ContentValues();
- contentValues.put(SCHEDULED_DATE, time);
-
- int rowsUpdated = database.update(TABLE_NAME, contentValues, ID_WHERE + " AND " + SCHEDULED_DATE + "!= ?", SqlUtil.buildArgs(messageId, -1));
- ApplicationDependencies.getDatabaseObserver().notifyScheduledMessageObservers(threadId);
- ApplicationDependencies.getScheduledMessageManager().scheduleIfNecessary();
- if (rowsUpdated == 0) {
- Log.w(TAG, "Failed to reschedule messageId=" + messageId + " to new time " + time + ". may have been sent already");
- }
- }
-
- public void markAsInsecure(long messageId) {
- updateMailboxBitmask(messageId, MessageTypes.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()
- .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 (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 + " INDEXED BY " + INDEX_THREAD_DATE, 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 (MessageTypes.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), expirationInfo, storyType));
- }
- }
- }
-
- ContentValues contentValues = new ContentValues();
- contentValues.put(READ, 1);
- contentValues.put(REACTIONS_UNREAD, 0);
- contentValues.put(REACTIONS_LAST_SEEN, System.currentTimeMillis());
-
- database.update(TABLE_NAME + " INDEXED BY " + INDEX_THREAD_DATE, 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 OutgoingMessage 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));
- byte[] messageRangesData = CursorUtil.requireBlob(cursor, MESSAGE_RANGES);
- long scheduledDate = cursor.getLong(cursor.getColumnIndexOrThrow(SCHEDULED_DATE));
-
- 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(cursor);
- BodyRangeList quoteBodyRanges = parseQuoteBodyRanges(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), quoteBodyRanges);
- }
-
- 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 && (MessageTypes.isGroupQuit(outboxType) || MessageTypes.isGroupUpdate(outboxType))) {
- return OutgoingMessage.groupUpdateMessage(recipient, new MessageGroupContext(body, MessageTypes.isGroupV2(outboxType)), attachments, timestamp, 0, false, quote, contacts, previews, mentions);
- } else if (MessageTypes.isExpirationTimerUpdate(outboxType)) {
- return OutgoingMessage.expirationUpdateMessage(recipient, timestamp, expiresIn);
- } else if (MessageTypes.isPaymentsNotification(outboxType)) {
- return OutgoingMessage.paymentNotificationMessage(recipient, Objects.requireNonNull(body), timestamp, expiresIn);
- } else if (MessageTypes.isPaymentsRequestToActivate(outboxType)) {
- return OutgoingMessage.requestToActivatePaymentsMessage(recipient, timestamp, expiresIn);
- } else if (MessageTypes.isPaymentsActivated(outboxType)) {
- return OutgoingMessage.paymentsActivatedMessage(recipient, timestamp, expiresIn);
- }
-
- GiftBadge giftBadge = null;
- if (body != null && MessageTypes.isGiftBadge(outboxType)) {
- giftBadge = GiftBadge.parseFrom(Base64.decode(body));
- }
-
- BodyRangeList messageRanges = null;
- if (messageRangesData != null) {
- try {
- messageRanges = BodyRangeList.parseFrom(messageRangesData);
- } catch (InvalidProtocolBufferException e) {
- Log.w(TAG, "Error parsing message ranges", e);
- }
- }
-
- OutgoingMessage message = new OutgoingMessage(recipient,
- body,
- attachments,
- timestamp,
- subscriptionId,
- expiresIn,
- viewOnce,
- distributionType,
- storyType,
- parentStoryId,
- MessageTypes.isStoryReaction(outboxType),
- quote,
- contacts,
- previews,
- mentions,
- networkFailures,
- mismatches,
- giftBadge,
- MessageTypes.isSecureType(outboxType),
- messageRanges,
- scheduledDate);
-
- 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 & MessageTypes.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());
- 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.Builder quoteBodyRanges = retrieved.getQuote().getBodyRanges() != null ? retrieved.getQuote().getBodyRanges().toBuilder()
- : BodyRangeList.newBuilder();
-
- BodyRangeList mentionsList = MentionUtil.mentionsToBodyRangeList(retrieved.getQuote().getMentions());
- if (mentionsList != null) {
- quoteBodyRanges.addAllRanges(mentionsList.getRangesList());
- }
-
- if (quoteBodyRanges.getRangesCount() > 0) {
- contentValues.put(QUOTE_BODY_RANGES, quoteBodyRanges.build().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;
-
- RecipientId threadRecipientId = SignalDatabase.threads().getRecipientIdForThreadId(threadId);
- if (threadRecipientId == null) {
- threadRecipientId = retrieved.getFrom();
- }
- long messageId = insertMediaMessage(threadId,
- retrieved.getBody(),
- retrieved.getAttachments(),
- quoteAttachments,
- retrieved.getSharedContacts(),
- retrieved.getLinkPreviews(),
- retrieved.getMentions(),
- retrieved.getMessageRanges(),
- contentValues,
- null,
- updateThread,
- true);
-
- boolean isNotStoryGroupReply = retrieved.getParentStoryId() == null || !retrieved.getParentStoryId().isGroupReply();
-
- if (!MessageTypes.isPaymentsActivated(mailbox) && !MessageTypes.isPaymentsRequestToActivate(mailbox) && !MessageTypes.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, true);
- }
-
- notifyConversationListeners(threadId);
-
- if (retrieved.getStoryType().isStory()) {
- ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(Objects.requireNonNull(SignalDatabase.threads().getRecipientIdForThreadId(threadId)));
- }
-
- return Optional.of(new InsertResult(messageId, threadId));
- }
-
- public Optional insertMessageInbox(IncomingMediaMessage retrieved,
- String contentLocation, long threadId)
- throws MmsException
- {
- long type = MessageTypes.BASE_INBOX_TYPE;
-
- if (retrieved.isPushMessage()) {
- type |= MessageTypes.PUSH_MESSAGE_BIT;
- }
-
- if (retrieved.isExpirationUpdate()) {
- type |= MessageTypes.EXPIRATION_TIMER_UPDATE_BIT;
- }
-
- if (retrieved.isPaymentsNotification()) {
- type |= MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION;
- }
-
- if (retrieved.isActivatePaymentsRequest()) {
- type |= MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST;
- }
-
- if (retrieved.isPaymentsActivated()) {
- type |= MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED;
- }
-
- return insertMessageInbox(retrieved, contentLocation, threadId, type);
- }
-
- public Optional insertSecureDecryptedMessageInbox(IncomingMediaMessage retrieved, long threadId)
- throws MmsException
- {
- long type = MessageTypes.BASE_INBOX_TYPE | MessageTypes.SECURE_MESSAGE_BIT;
-
- if (retrieved.isPushMessage()) {
- type |= MessageTypes.PUSH_MESSAGE_BIT;
- }
-
- if (retrieved.isExpirationUpdate()) {
- type |= MessageTypes.EXPIRATION_TIMER_UPDATE_BIT;
- }
-
- boolean hasSpecialType = false;
- if (retrieved.isStoryReaction()) {
- hasSpecialType = true;
- type |= MessageTypes.SPECIAL_TYPE_STORY_REACTION;
- }
-
- if (retrieved.getGiftBadge() != null) {
- if (hasSpecialType) {
- throw new MmsException("Cannot insert message with multiple special types.");
- }
-
- type |= MessageTypes.SPECIAL_TYPE_GIFT_BADGE;
- }
-
- if (retrieved.isPaymentsNotification()) {
- if (hasSpecialType) {
- throw new MmsException("Cannot insert message with multiple special types.");
- }
- type |= MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION;
- }
-
- if (retrieved.isActivatePaymentsRequest()) {
- if (hasSpecialType) {
- throw new MmsException("Cannot insert message with multiple special types.");
- }
- type |= MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST;
- }
-
- if (retrieved.isPaymentsActivated()) {
- if (hasSpecialType) {
- throw new MmsException("Cannot insert message with multiple special types.");
- }
- type |= MessageTypes.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, MessageTypes.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 = MessageTypes.SECURE_MESSAGE_BIT | MessageTypes.PUSH_MESSAGE_BIT;
-
- type = type & (MessageTypes.TOTAL_MASK - MessageTypes.ENCRYPTION_MASK) | MessageTypes.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);
- SignalDatabase.threads().update(threadId, true);
-
- 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, MessageTypes.BAD_DECRYPT_TYPE);
- values.put(THREAD_ID, threadId);
-
- databaseHelper.getSignalWritableDatabase().insert(TABLE_NAME, null, values);
-
- SignalDatabase.threads().incrementUnread(threadId, 1, 0);
- SignalDatabase.threads().update(threadId, true);
-
- 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 + " & " + MessageTypes.SPECIAL_TYPES_MASK + " = " + MessageTypes.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));
- notifyConversationListeners(threadId);
- }
- }
-
- public long insertMessageOutbox(@NonNull OutgoingMessage message,
- long threadId,
- boolean forceSms,
- @Nullable InsertListener insertListener)
- throws MmsException
- {
- return insertMessageOutbox(message, threadId, forceSms, GroupReceiptTable.STATUS_UNDELIVERED, insertListener);
- }
-
- public long insertMessageOutbox(@NonNull OutgoingMessage message,
- long threadId, boolean forceSms, int defaultReceiptStatus,
- @Nullable InsertListener insertListener)
- throws MmsException
- {
- long type = MessageTypes.BASE_SENDING_TYPE;
-
- if (message.isSecure()) type |= (MessageTypes.SECURE_MESSAGE_BIT | MessageTypes.PUSH_MESSAGE_BIT);
- if (forceSms) type |= MessageTypes.MESSAGE_FORCE_SMS_BIT;
-
- if (message.isSecure()) type |= (MessageTypes.SECURE_MESSAGE_BIT | MessageTypes.PUSH_MESSAGE_BIT);
- else if (message.isEndSession()) type |= MessageTypes.END_SESSION_BIT;
-
- if (message.isIdentityVerified()) type |= MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT;
- else if (message.isIdentityDefault()) type |= MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT;
-
- if (message.isGroup()) {
- if (message.isV2Group()) {
- type |= MessageTypes.GROUP_V2_BIT | MessageTypes.GROUP_UPDATE_BIT;
- if (message.isJustAGroupLeave()) {
- type |= MessageTypes.GROUP_LEAVE_BIT;
- }
- } else {
- MessageGroupContext.GroupV1Properties properties = message.requireGroupV1Properties();
- if (properties.isUpdate()) type |= MessageTypes.GROUP_UPDATE_BIT;
- else if (properties.isQuit()) type |= MessageTypes.GROUP_LEAVE_BIT;
- }
- }
-
- if (message.isExpirationUpdate()) {
- type |= MessageTypes.EXPIRATION_TIMER_UPDATE_BIT;
- }
-
- boolean hasSpecialType = false;
- if (message.isStoryReaction()) {
- hasSpecialType = true;
- type |= MessageTypes.SPECIAL_TYPE_STORY_REACTION;
- }
-
- if (message.getGiftBadge() != null) {
- if (hasSpecialType) {
- throw new MmsException("Cannot insert message with multiple special types.");
- }
-
- type |= MessageTypes.SPECIAL_TYPE_GIFT_BADGE;
- }
-
- if (message.isPaymentsNotification()) {
- if (hasSpecialType) {
- throw new MmsException("Cannot insert message with multiple special types.");
- }
- type |= MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION;
- }
-
- if (message.isRequestToActivatePayments()) {
- if (hasSpecialType) {
- throw new MmsException("Cannot insert message with multiple special types.");
- }
- type |= MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST;
- }
-
- if (message.isPaymentsActivated()) {
- if (hasSpecialType) {
- throw new MmsException("Cannot insert message with multiple special types.");
- }
- type |= MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED;
- }
-
- Map earlyDeliveryReceipts = earlyDeliveryReceiptCache.remove(message.getSentTimeMillis());
-
- if (earlyDeliveryReceipts.size() > 0) {
- Log.w(TAG, "Found early delivery receipts for " + message.getSentTimeMillis() + ". Applying them.");
- }
-
- 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);
- contentValues.put(SCHEDULED_DATE, message.getScheduledDate());
-
- 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 adjustedQuoteBodyRanges = BodyRangeUtil.adjustBodyRanges(message.getOutgoingQuote().getBodyRanges(), updated.getBodyAdjustments());
- BodyRangeList.Builder quoteBodyRanges;
- if (adjustedQuoteBodyRanges != null) {
- quoteBodyRanges = adjustedQuoteBodyRanges.toBuilder();
- } else {
- quoteBodyRanges = BodyRangeList.newBuilder();
- }
-
- BodyRangeList mentionsList = MentionUtil.mentionsToBodyRangeList(updated.getMentions());
- if (mentionsList != null) {
- quoteBodyRanges.addAllRanges(mentionsList.getRangesList());
- }
-
- if (quoteBodyRanges.getRangesCount() > 0) {
- contentValues.put(QUOTE_BODY_RANGES, quoteBodyRanges.build().toByteArray());
- }
-
- quoteAttachments.addAll(message.getOutgoingQuote().getAttachments());
- }
-
- MentionUtil.UpdatedBodyAndMentions updatedBodyAndMentions = MentionUtil.updateBodyAndMentionsWithPlaceholders(message.getBody(), message.getMentions());
- BodyRangeList bodyRanges = BodyRangeUtil.adjustBodyRanges(message.getBodyRanges(), updatedBodyAndMentions.getBodyAdjustments());
-
- long messageId = insertMediaMessage(threadId, updatedBodyAndMentions.getBodyAsString(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), updatedBodyAndMentions.getMentions(), bodyRanges, 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));
- } else {
- ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId);
- }
- if (message.getScheduledDate() != -1) {
- ApplicationDependencies.getDatabaseObserver().notifyScheduledMessageObservers(threadId);
- }
- } else {
- ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(message.getRecipient().getId());
- }
-
- notifyConversationListListeners();
-
- if (!message.isIdentityVerified() && !message.isIdentityDefault()) {
- 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) {
- long threadId = getThreadIdForMessage(messageId);
- return deleteMessage(messageId, threadId);
- }
-
- public boolean deleteMessage(long messageId, boolean notify) {
- long threadId = getThreadIdForMessage(messageId);
- return deleteMessage(messageId, threadId, notify);
- }
-
- public boolean deleteMessage(long messageId, long threadId) {
- return deleteMessage(messageId, threadId, true);
- }
-
- private boolean deleteMessage(long messageId, long threadId, boolean notify) {
- Log.d(TAG, "deleteMessage(" + 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);
-
- if (notify) {
- notifyConversationListeners(threadId);
- notifyStickerListeners();
- notifyStickerPackListeners();
- OptimizeMessageSearchIndexJob.enqueue();
- }
-
- return threadDeleted;
- }
-
- public void deleteScheduledMessage(long messageId) {
- Log.d(TAG, "deleteScheduledMessage(" + messageId + ")");
- SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
- long threadId = getThreadIdForMessage(messageId);
- db.beginTransaction();
- try {
- ContentValues contentValues = new ContentValues();
- contentValues.put(SCHEDULED_DATE, -1);
- contentValues.put(DATE_SENT, System.currentTimeMillis());
- contentValues.put(DATE_RECEIVED, System.currentTimeMillis());
-
- int rowsUpdated = db.update(TABLE_NAME, contentValues, ID_WHERE + " AND " + SCHEDULED_DATE + "!= ?", SqlUtil.buildArgs(messageId, -1));
- if (rowsUpdated > 0) {
- deleteMessage(messageId, threadId);
- db.setTransactionSuccessful();
- } else {
- Log.w(TAG, "tried to delete scheduled message but it may have already been sent");
- }
- } finally {
- db.endTransaction();
- }
- ApplicationDependencies.getScheduledMessageManager().scheduleIfNecessary();
- ApplicationDependencies.getDatabaseObserver().notifyScheduledMessageObservers(threadId);
- }
-
- public void deleteScheduledMessages(@NonNull RecipientId recipientId) {
- Log.d(TAG, "deleteScheduledMessages(" + recipientId + ")");
- Long threadId = SignalDatabase.threads().getThreadIdFor(recipientId);
- if (threadId != null) {
- SQLiteDatabaseExtensionsKt.withinTransaction(getWritableDatabase(), d -> {
- List scheduledMessages = getScheduledMessagesInThread(threadId);
- for (MessageRecord record : scheduledMessages) {
- deleteScheduledMessage(record.getId());
- }
- return Unit.INSTANCE;
- });
- } else {
- Log.i(TAG, "No thread exists for " + recipientId);
- }
- }
-
- 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 MessageTypes.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, MessageTypes.PROFILE_CHANGE_TYPE);
-
- try (MmsReader reader = mmsReaderFor(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(MMS_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 + " & " + MessageTypes.TOTAL_MASK + " & " + MessageTypes.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_WITH_ATTACHMENTS, 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();
- OptimizeMessageSearchIndexJob.enqueue();
- }
- }
-
- 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), false);
- }
- }
-
- notifyConversationListeners(threadIds);
- notifyStickerListeners();
- notifyStickerPackListeners();
- OptimizeMessageSearchIndexJob.enqueue();
- }
-
- 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));
- }
-
- int 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");
- }
- return deletes;
- }
-
- 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 + "." + THREAD_ID + " = ? AND " +
- TABLE_NAME + "." + DATE_RECEIVED + " >= ? AND " +
- TABLE_NAME + "." + SCHEDULED_DATE + " = -1";
- 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);
- OptimizeMessageSearchIndexJob.enqueue();
- }
-
- 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, MessageTypes.CHANGE_NUMBER_TYPE)
- .run())
- {
- if (cursor.moveToFirst()) {
- return cursor.getInt(0);
- } else {
- return 0;
- }
- }
- }
-
- private static @NonNull List parseQuoteMentions(@NonNull Cursor cursor) {
- byte[] raw = cursor.getBlob(cursor.getColumnIndexOrThrow(QUOTE_BODY_RANGES));
- BodyRangeList bodyRanges = null;
-
- if (raw != null) {
- try {
- bodyRanges = BodyRangeList.parseFrom(raw);
- } catch (InvalidProtocolBufferException e) {
- Log.w(TAG, "Unable to parse quote body ranges", e);
- }
- }
-
- return MentionUtil.bodyRangeListToMentions(bodyRanges);
- }
-
- private static @Nullable BodyRangeList parseQuoteBodyRanges(@NonNull Cursor cursor) {
- byte[] data = cursor.getBlob(cursor.getColumnIndexOrThrow(QUOTE_BODY_RANGES));
-
- if (data != null) {
- try {
- final List bodyRanges = Stream.of(BodyRangeList.parseFrom(data).getRangesList())
- .filter(bodyRange -> bodyRange.getAssociatedValueCase() != BodyRangeList.BodyRange.AssociatedValueCase.MENTIONUUID)
- .toList();
-
- return BodyRangeList.newBuilder().addAllRanges(bodyRanges).build();
- } catch (InvalidProtocolBufferException e) {
- // Intentionally left blank
- }
- }
-
- return null;
- }
-
- 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 OutgoingMmsReader readerFor(OutgoingMessage 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 (MessageTable.Reader reader = MessageTable.mmsReaderFor(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<>(MessageTypes.OUTGOING_MESSAGE_TYPES.length);
- for (long outgoingMessageType : MessageTypes.OUTGOING_MESSAGE_TYPES) {
- segments.add("(" + TABLE_NAME + "." + TYPE + " & " + MessageTypes.BASE_TYPE_MASK + " = " + outgoingMessageType + ")");
- }
-
- return Util.join(segments, " OR ");
- }
-
- public final int getInsecureMessageSentCount(long threadId) {
- SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
- String[] projection = new String[]{"COUNT(*)"};
- 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(TABLE_NAME, projection, query, args, null, null, null, null)) {
- if (cursor != null && cursor.moveToFirst()) {
- return cursor.getInt(0);
- } else {
- return 0;
- }
- }
- }
-
- public final int getInsecureMessageCountForInsights() {
- return getMessageCountForRecipientsAndType(getOutgoingInsecureMessageClause());
- }
-
- public int getInsecureMessageCount() {
- try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, SqlUtil.COUNT, getInsecureMessageClause(), null, null, null, null)) {
- if (cursor.moveToFirst()) {
- return cursor.getInt(0);
- }
- }
-
- return 0;
- }
-
- public boolean hasSmsExportMessage(long threadId) {
- return SQLiteDatabaseExtensionsKt.exists(getReadableDatabase(), TABLE_NAME)
- .where(THREAD_ID_WHERE + " AND " + TYPE + " = ?", threadId, MessageTypes.SMS_EXPORT_TYPE)
- .run();
- }
-
- public final int getSecureMessageCountForInsights() {
- return getMessageCountForRecipientsAndType(getOutgoingSecureMessageClause());
- }
-
- public final int getSecureMessageCount(long threadId) {
- SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
- String[] projection = new String[] {"COUNT(*)"};
- String query = getSecureMessageClause() + "AND " + THREAD_ID + " = ?";
- String[] args = new String[]{String.valueOf(threadId)};
-
- try (Cursor cursor = db.query(TABLE_NAME, projection, query, args, null, null, null, null)) {
- if (cursor != null && cursor.moveToFirst()) {
- return cursor.getInt(0);
- } else {
- return 0;
- }
- }
- }
-
- public final int getOutgoingSecureMessageCount(long threadId) {
- SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
- String[] projection = new String[] {"COUNT(*)"};
- String query = getOutgoingSecureMessageClause() +
- "AND " + THREAD_ID + " = ? " +
- "AND (" + TYPE + " & " + MessageTypes.GROUP_LEAVE_BIT + " = 0 OR " + TYPE + " & " + MessageTypes.GROUP_V2_BIT + " = " + MessageTypes.GROUP_V2_BIT + ")";
- String[] args = new String[]{String.valueOf(threadId)};
-
- try (Cursor cursor = db.query(TABLE_NAME, projection, query, args, null, null, null, null)) {
- if (cursor != null && cursor.moveToFirst()) {
- return cursor.getInt(0);
- } else {
- return 0;
- }
- }
- }
-
- private int getMessageCountForRecipientsAndType(String typeClause) {
-
- SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
- String[] projection = new String[] {"COUNT(*)"};
- String query = typeClause + " AND " + DATE_SENT + " > ?";
- String[] args = new String[]{String.valueOf(System.currentTimeMillis() - InsightsConstants.PERIOD_IN_MILLIS)};
-
- try (Cursor cursor = db.query(TABLE_NAME, projection, query, args, null, null, null, null)) {
- if (cursor != null && cursor.moveToFirst()) {
- return cursor.getInt(0);
- } else {
- return 0;
- }
- }
- }
-
- private String getOutgoingInsecureMessageClause() {
- return "(" + TYPE + " & " + MessageTypes.BASE_TYPE_MASK + ") = " + MessageTypes.BASE_SENT_TYPE + " AND NOT (" + TYPE + " & " + MessageTypes.SECURE_MESSAGE_BIT + ")";
- }
-
- private String getOutgoingSecureMessageClause() {
- return "(" + TYPE + " & " + MessageTypes.BASE_TYPE_MASK + ") = " + MessageTypes.BASE_SENT_TYPE + " AND (" + TYPE + " & " + (MessageTypes.SECURE_MESSAGE_BIT | MessageTypes.PUSH_MESSAGE_BIT) + ")";
- }
-
- private String getSecureMessageClause() {
- String isSent = "(" + TYPE + " & " + MessageTypes.BASE_TYPE_MASK + ") = " + MessageTypes.BASE_SENT_TYPE;
- String isReceived = "(" + TYPE + " & " + MessageTypes.BASE_TYPE_MASK + ") = " + MessageTypes.BASE_INBOX_TYPE;
- String isSecure = "(" + TYPE + " & " + (MessageTypes.SECURE_MESSAGE_BIT | MessageTypes.PUSH_MESSAGE_BIT) + ")";
-
- return String.format(Locale.ENGLISH, "(%s OR %s) AND %s", isSent, isReceived, isSecure);
- }
-
- protected String getInsecureMessageClause() {
- return getInsecureMessageClause(-1);
- }
-
- protected String getInsecureMessageClause(long threadId) {
- String isSent = "(" + TABLE_NAME + "." + TYPE + " & " + MessageTypes.BASE_TYPE_MASK + ") = " + MessageTypes.BASE_SENT_TYPE;
- String isReceived = "(" + TABLE_NAME + "." + TYPE + " & " + MessageTypes.BASE_TYPE_MASK + ") = " + MessageTypes.BASE_INBOX_TYPE;
- String isSecure = "(" + TABLE_NAME + "." + TYPE + " & " + (MessageTypes.SECURE_MESSAGE_BIT | MessageTypes.PUSH_MESSAGE_BIT) + ")";
- String isNotSecure = "(" + TABLE_NAME + "." + TYPE + " <= " + (MessageTypes.BASE_TYPE_MASK | MessageTypes.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 " + TABLE_NAME + "." + THREAD_ID + " = " + threadId;
- }
-
- return whereClause;
- }
-
- public int getUnexportedInsecureMessagesCount() {
- return getUnexportedInsecureMessagesCount(-1);
- }
-
- public int getUnexportedInsecureMessagesCount(long threadId) {
- 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);
- }
- }
-
- return 0;
- }
-
- /**
- * Resets the exported state and exported flag so messages can be re-exported.
- */
- public void clearExportState() {
- ContentValues values = new ContentValues(2);
- values.putNull(EXPORT_STATE);
- values.put(EXPORTED, MessageExportStatus.UNEXPORTED.serialize());
-
- SQLiteDatabaseExtensionsKt.update(getWritableDatabase(), TABLE_NAME)
- .values(values)
- .where(EXPORT_STATE + " IS NOT NULL OR " + EXPORTED + " != ?", MessageExportStatus.UNEXPORTED)
- .run();
- }
-
- /**
- * Reset the exported status (not state) to the default for clearing errors.
- */
- public void clearInsecureMessageExportedErrorStatus() {
- ContentValues values = new ContentValues(1);
- values.put(EXPORTED, MessageExportStatus.UNEXPORTED.getCode());
-
- SQLiteDatabaseExtensionsKt.update(getWritableDatabase(), TABLE_NAME)
- .values(values)
- .where(EXPORTED + " < ?", MessageExportStatus.UNEXPORTED)
- .run();
- }
-
- public void setReactionsSeen(long threadId, long sinceTimestamp) {
- SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
- ContentValues values = new ContentValues();
- String whereClause = THREAD_ID + " = ? AND " + REACTIONS_UNREAD + " = ?";
- String[] whereArgs = new String[]{String.valueOf(threadId), "1"};
-
- if (sinceTimestamp > -1) {
- whereClause += " AND " + DATE_RECEIVED + " <= " + sinceTimestamp;
- }
-
- values.put(REACTIONS_UNREAD, 0);
- values.put(REACTIONS_LAST_SEEN, System.currentTimeMillis());
-
- db.update(TABLE_NAME, values, whereClause, whereArgs);
- }
-
- public void setAllReactionsSeen() {
- SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
- ContentValues values = new ContentValues();
- String query = REACTIONS_UNREAD + " != ?";
- String[] args = new String[] { "0" };
-
- values.put(REACTIONS_UNREAD, 0);
- values.put(REACTIONS_LAST_SEEN, System.currentTimeMillis());
-
- db.update(TABLE_NAME, values, query, args);
- }
-
- public void setNotifiedTimestamp(long timestamp, @NonNull List ids) {
- if (ids.isEmpty()) {
- return;
- }
-
- SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
- SqlUtil.Query where = SqlUtil.buildSingleCollectionQuery(ID, ids);
- ContentValues values = new ContentValues();
-
- values.put(NOTIFIED_TIMESTAMP, timestamp);
-
- db.update(TABLE_NAME, values, where.getWhere(), where.getWhereArgs());
- }
-
- public void addMismatchedIdentity(long messageId, @NonNull RecipientId recipientId, IdentityKey identityKey) {
- try {
- addToDocument(messageId, MISMATCHED_IDENTITIES,
- new IdentityKeyMismatch(recipientId, identityKey),
- IdentityKeyMismatchSet.class);
- } catch (IOException e) {
- Log.w(TAG, e);
- }
- }
-
- public void removeMismatchedIdentity(long messageId, @NonNull RecipientId recipientId, IdentityKey identityKey) {
- try {
- removeFromDocument(messageId, MISMATCHED_IDENTITIES,
- new IdentityKeyMismatch(recipientId, identityKey),
- IdentityKeyMismatchSet.class);
- } catch (IOException e) {
- Log.w(TAG, e);
- }
- }
-
- public void setMismatchedIdentities(long messageId, @NonNull Set mismatches) {
- try {
- setDocument(databaseHelper.getSignalWritableDatabase(), messageId, MISMATCHED_IDENTITIES, new IdentityKeyMismatchSet(mismatches));
- } catch (IOException e) {
- Log.w(TAG, e);
- }
- }
-
- public @NonNull List getReportSpamMessageServerGuids(long threadId, long timestamp) {
- SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
- String query = THREAD_ID + " = ? AND " + DATE_RECEIVED + " <= ?";
- String[] args = SqlUtil.buildArgs(threadId, timestamp);
-
- List data = new ArrayList<>();
- 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, DATE_RECEIVED);
-
- if (!Util.isEmpty(serverGuid)) {
- data.add(new ReportSpamData(id, serverGuid, dateReceived));
- }
- }
- }
- return data;
- }
-
- public List getIncomingPaymentRequestThreads() {
- Cursor cursor = SQLiteDatabaseExtensionsKt.select(getReadableDatabase(), "DISTINCT " + THREAD_ID)
- .from(TABLE_NAME)
- .where("(" + TYPE + " & " + MessageTypes.BASE_TYPE_MASK + ") = " + MessageTypes.BASE_INBOX_TYPE + " AND (" + TYPE + " & ?) != 0", MessageTypes.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(TABLE_NAME)
- .where(TYPE + " & ? != 0 AND body = ?", MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION, paymentUuid)
- .run();
-
- long id = CursorExtensionsKt.readToSingleLong(cursor, -1);
- if (id != -1) {
- return new MessageId(id);
- } else {
- return null;
- }
- }
-
- /**
- * @return The user that added you to the group, otherwise null.
- */
- public @Nullable RecipientId getGroupAddedBy(long threadId) {
- long lastQuitChecked = System.currentTimeMillis();
- Pair pair;
-
- do {
- pair = getGroupAddedBy(threadId, lastQuitChecked);
- if (pair.first() != null) {
- return pair.first();
- } else {
- lastQuitChecked = pair.second();
- }
-
- } while (pair.second() != -1);
-
- return null;
- }
-
- private @NonNull Pair getGroupAddedBy(long threadId, long lastQuitChecked) {
- long latestQuit = SignalDatabase.messages().getLatestGroupQuitTimestamp(threadId, lastQuitChecked);
- RecipientId id = SignalDatabase.messages().getOldestGroupUpdateSender(threadId, latestQuit);
-
- return new Pair<>(id, latestQuit);
- }
-
- /**
- * Whether or not the message has been quoted by another message.
- */
- public boolean isQuoted(@NonNull MessageRecord messageRecord) {
- RecipientId author = messageRecord.isOutgoing() ? Recipient.self().getId() : messageRecord.getRecipient().getId();
- long timestamp = messageRecord.getDateSent();
-
- String where = MessageTable.QUOTE_ID + " = ? AND " + MessageTable.QUOTE_AUTHOR + " = ? AND " + SCHEDULED_DATE + " = ?";
- String[] whereArgs = SqlUtil.buildArgs(timestamp, author, -1);
-
- try (Cursor cursor = getReadableDatabase().query(MessageTable.TABLE_NAME, new String[]{ "1" }, where, whereArgs, null, null, null, "1")) {
- return cursor.moveToFirst();
- }
- }
-
- /**
- * Given a collection of MessageRecords, this will return a set of the IDs of the records that have been quoted by another message.
- * Does an efficient bulk lookup that makes it faster than {@link #isQuoted(MessageRecord)} for multiple records.
- */
- public Set isQuoted(@NonNull Collection records) {
- if (records.isEmpty()) {
- return Collections.emptySet();
- }
-
- Map byQuoteDescriptor = new HashMap<>(records.size());
- List args = new ArrayList<>(records.size());
-
- for (MessageRecord record : records) {
- long timestamp = record.getDateSent();
- RecipientId author = record.isOutgoing() ? Recipient.self().getId() : record.getRecipient().getId();
-
- byQuoteDescriptor.put(new QuoteDescriptor(timestamp, author), record);
- args.add(SqlUtil.buildArgs(timestamp, author, -1));
- }
-
-
- String[] projection = new String[] { QUOTE_ID, QUOTE_AUTHOR };
- List queries = SqlUtil.buildCustomCollectionQuery(QUOTE_ID + " = ? AND " + QUOTE_AUTHOR + " = ? AND " + SCHEDULED_DATE + " = ?", args);
- Set quotedIds = new HashSet<>();
-
- for (SqlUtil.Query query : queries) {
- try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, projection, query.getWhere(), query.getWhereArgs(), null, null, null)) {
- while (cursor.moveToNext()) {
- long timestamp = CursorUtil.requireLong(cursor, QUOTE_ID);
- RecipientId author = RecipientId.from(CursorUtil.requireString(cursor, QUOTE_AUTHOR));
- QuoteDescriptor quoteLocator = new QuoteDescriptor(timestamp, author);
-
- quotedIds.add(byQuoteDescriptor.get(quoteLocator).getId());
- }
- }
- }
-
- return quotedIds;
- }
-
- public MessageId getRootOfQuoteChain(@NonNull MessageId id) {
- MmsMessageRecord targetMessage;
- try {
- targetMessage = (MmsMessageRecord) SignalDatabase.messages().getMessageRecord(id.getId());
- } catch (NoSuchMessageException e) {
- throw new IllegalArgumentException("Invalid message ID!");
- }
-
- if (targetMessage.getQuote() == null) {
- return id;
- }
-
- String query;
- if (targetMessage.getQuote().getAuthor().equals(Recipient.self().getId())) {
- query = DATE_SENT + " = " + targetMessage.getQuote().getId() + " AND (" + TYPE + " & " + MessageTypes.BASE_TYPE_MASK + ") = " + MessageTypes.BASE_SENT_TYPE;
- } else {
- query = DATE_SENT + " = " + targetMessage.getQuote().getId() + " AND " + RECIPIENT_ID + " = '" + targetMessage.getQuote().getAuthor().serialize() + "'";
- }
-
- try (Reader reader = new MmsReader(getReadableDatabase().query(TABLE_NAME, MMS_PROJECTION, query, null, null, null, "1"))) {
- MessageRecord record;
- if ((record = reader.getNext()) != null) {
- return getRootOfQuoteChain(new MessageId(record.getId()));
- }
- }
-
- return id;
- }
-
- public List getAllMessagesThatQuote(@NonNull MessageId id) {
- MessageRecord targetMessage;
- try {
- targetMessage = getMessageRecord(id.getId());
- } catch (NoSuchMessageException e) {
- throw new IllegalArgumentException("Invalid message ID!");
- }
-
- RecipientId author = targetMessage.isOutgoing() ? Recipient.self().getId() : targetMessage.getRecipient().getId();
- String query = QUOTE_ID + " = " + targetMessage.getDateSent() + " AND " + QUOTE_AUTHOR + " = " + author.serialize() + " AND " + SCHEDULED_DATE + " = -1";
- String order = DATE_RECEIVED + " DESC";
-
- List records = new ArrayList<>();
-
- try (Reader reader = new MmsReader(getReadableDatabase().query(TABLE_NAME, MMS_PROJECTION, query, null, null, null, order))) {
- MessageRecord record;
- while ((record = reader.getNext()) != null) {
- records.add(record);
- records.addAll(getAllMessagesThatQuote(new MessageId(record.getId())));
- }
- }
-
- Collections.sort(records, (lhs, rhs) -> {
- if (lhs.getDateReceived() > rhs.getDateReceived()) {
- return -1;
- } else if (lhs.getDateReceived() < rhs.getDateReceived()) {
- return 1;
- } else {
- return 0;
- }
- });
-
- return records;
- }
-
- public int getQuotedMessagePosition(long threadId, long quoteId, @NonNull RecipientId recipientId) {
- String[] projection = new String[]{ DATE_SENT, RECIPIENT_ID, REMOTE_DELETED};
- String order = DATE_RECEIVED + " DESC";
- String selection = THREAD_ID + " = " + threadId + " AND " + STORY_TYPE + " = 0" + " AND " + PARENT_STORY_ID + " <= 0 AND " + SCHEDULED_DATE + " = -1";
-
- try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, projection, selection, null, null, null, order)) {
- boolean isOwnNumber = Recipient.resolved(recipientId).isSelf();
-
- while (cursor != null && cursor.moveToNext()) {
- boolean quoteIdMatches = cursor.getLong(0) == quoteId;
- boolean recipientIdMatches = recipientId.equals(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)));
-
- if (quoteIdMatches && (recipientIdMatches || isOwnNumber)) {
- if (CursorUtil.requireBoolean(cursor, REMOTE_DELETED)) {
- return -1;
- } else {
- return cursor.getPosition();
- }
- }
- }
- }
- return -1;
- }
-
- public int getMessagePositionInConversation(long threadId, long receivedTimestamp, @NonNull RecipientId recipientId) {
- String[] projection = new String[]{ DATE_RECEIVED, RECIPIENT_ID, REMOTE_DELETED};
- String order = DATE_RECEIVED + " DESC";
- String selection = THREAD_ID + " = " + threadId + " AND " + STORY_TYPE + " = 0" + " AND " + PARENT_STORY_ID + " <= 0 AND " + SCHEDULED_DATE + " = -1";
-
- try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, projection, selection, null, null, null, order)) {
- boolean isOwnNumber = Recipient.resolved(recipientId).isSelf();
-
- while (cursor != null && cursor.moveToNext()) {
- boolean timestampMatches = cursor.getLong(0) == receivedTimestamp;
- boolean recipientIdMatches = recipientId.equals(RecipientId.from(cursor.getLong(1)));
-
- if (timestampMatches && (recipientIdMatches || isOwnNumber)) {
- if (CursorUtil.requireBoolean(cursor, REMOTE_DELETED)) {
- return -1;
- } else {
- return cursor.getPosition();
- }
- }
- }
- }
- return -1;
- }
-
- public int getMessagePositionInConversation(long threadId, long receivedTimestamp) {
- return getMessagePositionInConversation(threadId, 0, receivedTimestamp);
- }
-
- /**
- * Retrieves the position of the message with the provided timestamp in the query results you'd
- * get from calling {@link #getConversation(long)}.
- *
- * Note: This could give back incorrect results in the situation where multiple messages have the
- * same received timestamp. However, because this was designed to determine where to scroll to,
- * you'll still wind up in about the right spot.
- *
- * @param groupStoryId Ignored if passed value is <= 0
- */
- public int getMessagePositionInConversation(long threadId, long groupStoryId, long receivedTimestamp) {
- final String order;
- final String selection;
-
- if (groupStoryId > 0) {
- order = MessageTable.DATE_RECEIVED + " ASC";
- selection = MessageTable.THREAD_ID + " = " + threadId + " AND " +
- MessageTable.DATE_RECEIVED + " < " + receivedTimestamp + " AND " +
- MessageTable.STORY_TYPE + " = 0 AND " + MessageTable.PARENT_STORY_ID + " = " + groupStoryId + " AND " +
- MessageTable.SCHEDULED_DATE + " = -1";
- } else {
- order = MessageTable.DATE_RECEIVED + " DESC";
- selection = MessageTable.THREAD_ID + " = " + threadId + " AND " +
- MessageTable.DATE_RECEIVED + " > " + receivedTimestamp + " AND " +
- MessageTable.STORY_TYPE + " = 0 AND " + MessageTable.PARENT_STORY_ID + " <= 0 AND " +
- MessageTable.SCHEDULED_DATE + " = -1";
- }
-
- try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, new String[] { "COUNT(*)" }, selection, null, null, null, order)) {
- if (cursor != null && cursor.moveToFirst()) {
- return cursor.getInt(0);
- }
- }
- return -1;
- }
-
- public long getTimestampForFirstMessageAfterDate(long date) {
- String[] projection = new String[] { MessageTable.DATE_RECEIVED };
- String order = MessageTable.DATE_RECEIVED + " ASC";
- String selection = MessageTable.DATE_RECEIVED + " > " + date + " AND " + MessageTable.SCHEDULED_DATE + " = -1";
-
- try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, projection, selection, null, null, null, order, "1")) {
- if (cursor != null && cursor.moveToFirst()) {
- return cursor.getLong(0);
- }
- }
-
- return 0;
- }
-
- public int getMessageCountBeforeDate(long date) {
- String selection = MessageTable.DATE_RECEIVED + " < " + date + " AND " + MessageTable.SCHEDULED_DATE + " = -1";
-
- try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, COUNT, selection, null, null, null, null)) {
- if (cursor != null && cursor.moveToFirst()) {
- return cursor.getInt(0);
- }
- }
-
- return 0;
- }
-
- public @NonNull List getMessagesAfterVoiceNoteInclusive(long messageId, long limit) throws NoSuchMessageException {
- MessageRecord origin = getMessageRecord(messageId);
- List mms = getMessagesInThreadAfterInclusive(origin.getThreadId(), origin.getDateReceived(), limit);
-
- Collections.sort(mms, Comparator.comparingLong(DisplayRecord::getDateReceived));
-
- return Stream.of(mms).limit(limit).toList();
- }
-
- public int getMessagePositionOnOrAfterTimestamp(long threadId, long timestamp) {
- String[] projection = new String[] { "COUNT(*)" };
- String selection = MessageTable.THREAD_ID + " = " + threadId + " AND " +
- MessageTable.DATE_RECEIVED + " >= " + timestamp + " AND " +
- MessageTable.STORY_TYPE + " = 0 AND " + MessageTable.PARENT_STORY_ID + " <= 0 AND " +
- MessageTable.SCHEDULED_DATE + " = -1";
-
- try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, projection, selection, null, null, null, null)) {
- if (cursor != null && cursor.moveToNext()) {
- return cursor.getInt(0);
- }
- }
- return 0;
- }
-
- public long getConversationSnippetType(long threadId) throws NoSuchMessageException {
- SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
- try (Cursor cursor = db.rawQuery(SNIPPET_QUERY, SqlUtil.buildArgs(threadId))) {
- if (cursor.moveToFirst()) {
- return CursorUtil.requireLong(cursor, MessageTable.TYPE);
- } else {
- throw new NoSuchMessageException("no message");
- }
- }
- }
-
- public @NonNull MessageRecord getConversationSnippet(long threadId) throws NoSuchMessageException {
- try (Cursor cursor = getConversationSnippetCursor(threadId)) {
- if (cursor.moveToFirst()) {
- long id = CursorUtil.requireLong(cursor, MessageTable.ID);
- return SignalDatabase.messages().getMessageRecord(id);
- } else {
- throw new NoSuchMessageException("no message");
- }
- }
- }
-
- @VisibleForTesting
- @NonNull Cursor getConversationSnippetCursor(long threadId) {
- SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
- return db.rawQuery(SNIPPET_QUERY, SqlUtil.buildArgs(threadId));
- }
-
- public int getUnreadCount(long threadId) {
- String selection = READ + " = 0 AND " + STORY_TYPE + " = 0 AND " + THREAD_ID + " = " + threadId + " AND " + PARENT_STORY_ID + " <= 0";
-
- try (Cursor cursor = getReadableDatabase().query(TABLE_NAME + " INDEXED BY " + INDEX_THREAD_DATE, COUNT, selection, null, null, null, null)) {
- if (cursor.moveToFirst()) {
- return cursor.getInt(0);
- } else {
- return 0;
- }
- }
- }
-
- public boolean checkMessageExists(@NonNull MessageRecord messageRecord) {
- return SQLiteDatabaseExtensionsKt
- .exists(getReadableDatabase(), TABLE_NAME)
- .where(ID + " = ?", messageRecord.getId())
- .run();
- }
-
- public @NonNull List getReportSpamMessageServerData(long threadId, long timestamp, int limit) {
- return getReportSpamMessageServerGuids(threadId, timestamp)
- .stream()
- .sorted((l, r) -> -Long.compare(l.getDateReceived(), r.getDateReceived()))
- .limit(limit)
- .collect(java.util.stream.Collectors.toList());
- }
-
- private @NonNull MessageExportState getMessageExportState(@NonNull MessageId messageId) throws NoSuchMessageException {
- String table = MessageTable.TABLE_NAME;
- String[] projection = SqlUtil.buildArgs(MessageTable.EXPORT_STATE);
- String[] args = SqlUtil.buildArgs(messageId.getId());
-
- try (Cursor cursor = getReadableDatabase().query(table, projection, ID_WHERE, args, null, null, null, null)) {
- if (cursor.moveToFirst()) {
- byte[] bytes = CursorUtil.requireBlob(cursor, MessageTable.EXPORT_STATE);
- if (bytes == null) {
- return MessageExportState.getDefaultInstance();
- } else {
- try {
- return MessageExportState.parseFrom(bytes);
- } catch (InvalidProtocolBufferException e) {
- return MessageExportState.getDefaultInstance();
- }
- }
- } else {
- throw new NoSuchMessageException("The requested message does not exist.");
- }
- }
- }
-
- public void updateMessageExportState(@NonNull MessageId messageId, @NonNull Function transform) throws NoSuchMessageException {
- SQLiteDatabase database = getWritableDatabase();
-
- database.beginTransaction();
- try {
- MessageExportState oldState = getMessageExportState(messageId);
- MessageExportState newState = transform.apply(oldState);
-
- setMessageExportState(messageId, newState);
-
- database.setTransactionSuccessful();
- } finally {
- database.endTransaction();
- }
- }
-
- public void markMessageExported(@NonNull MessageId messageId) {
- ContentValues contentValues = new ContentValues(1);
-
- contentValues.put(MessageTable.EXPORTED, MessageExportStatus.EXPORTED.getCode());
-
- getWritableDatabase().update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(messageId.getId()));
- }
-
- public void markMessageExportFailed(@NonNull MessageId messageId) {
- ContentValues contentValues = new ContentValues(1);
-
- contentValues.put(MessageTable.EXPORTED, MessageExportStatus.ERROR.getCode());
-
- getWritableDatabase().update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(messageId.getId()));
- }
-
- private void setMessageExportState(@NonNull MessageId messageId, @NonNull MessageExportState messageExportState) {
- ContentValues contentValues = new ContentValues(1);
-
- contentValues.put(MessageTable.EXPORT_STATE, messageExportState.toByteArray());
-
- getWritableDatabase().update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(messageId.getId()));
- }
-
- public Collection incrementDeliveryReceiptCounts(@NonNull List syncMessageIds, long timestamp) {
- return incrementReceiptCounts(syncMessageIds, timestamp, ReceiptType.DELIVERY);
- }
-
- public boolean incrementDeliveryReceiptCount(SyncMessageId syncMessageId, long timestamp) {
- return incrementReceiptCount(syncMessageId, timestamp, ReceiptType.DELIVERY);
- }
-
- /**
- * @return A list of ID's that were not updated.
- */
- public @NonNull Collection incrementReadReceiptCounts(@NonNull List syncMessageIds, long timestamp) {
- return incrementReceiptCounts(syncMessageIds, timestamp, ReceiptType.READ);
- }
-
- public boolean incrementReadReceiptCount(SyncMessageId syncMessageId, long timestamp) {
- return incrementReceiptCount(syncMessageId, timestamp, ReceiptType.READ);
- }
-
- /**
- * @return A list of ID's that were not updated.
- */
- public @NonNull Collection incrementViewedReceiptCounts(@NonNull List syncMessageIds, long timestamp) {
- return incrementReceiptCounts(syncMessageIds, timestamp, ReceiptType.VIEWED);
- }
-
- public @NonNull Collection incrementViewedNonStoryReceiptCounts(@NonNull List syncMessageIds, long timestamp) {
- return incrementReceiptCounts(syncMessageIds, timestamp, ReceiptType.VIEWED, MessageQualifier.NORMAL);
- }
-
- public boolean incrementViewedReceiptCount(SyncMessageId syncMessageId, long timestamp) {
- return incrementReceiptCount(syncMessageId, timestamp, ReceiptType.VIEWED);
- }
-
- public @NonNull Collection incrementViewedStoryReceiptCounts(@NonNull List syncMessageIds, long timestamp) {
- SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
- Set messageUpdates = new HashSet<>();
- Collection unhandled = new HashSet<>();
-
- db.beginTransaction();
- try {
- for (SyncMessageId id : syncMessageIds) {
- Set updates = incrementReceiptCountInternal(id, timestamp, ReceiptType.VIEWED, MessageQualifier.STORY);
-
- if (updates.size() > 0) {
- messageUpdates.addAll(updates);
- } else {
- unhandled.add(id);
- }
- }
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
-
- for (MessageUpdate update : messageUpdates) {
- ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(update.getMessageId());
- ApplicationDependencies.getDatabaseObserver().notifyVerboseConversationListeners(Collections.singleton(update.getThreadId()));
- }
-
- if (messageUpdates.size() > 0) {
- notifyConversationListListeners();
- }
- }
-
- return unhandled;
- }
-
- /**
- * Wraps a single receipt update in a transaction and triggers the proper updates.
- *
- * @return Whether or not some thread was updated.
- */
- private boolean incrementReceiptCount(SyncMessageId syncMessageId, long timestamp, @NonNull MessageTable.ReceiptType receiptType) {
- return incrementReceiptCount(syncMessageId, timestamp, receiptType, MessageTable.MessageQualifier.ALL);
- }
-
- private boolean incrementReceiptCount(SyncMessageId syncMessageId, long timestamp, @NonNull MessageTable.ReceiptType receiptType, @NonNull MessageTable.MessageQualifier messageQualifier) {
- SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
- ThreadTable threadTable = SignalDatabase.threads();
- Set messageUpdates = new HashSet<>();
-
- db.beginTransaction();
- try {
- messageUpdates = incrementReceiptCountInternal(syncMessageId, timestamp, receiptType, messageQualifier);
-
- for (MessageUpdate messageUpdate : messageUpdates) {
- threadTable.update(messageUpdate.getThreadId(), false);
- }
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
-
- for (MessageUpdate threadUpdate : messageUpdates) {
- ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(threadUpdate.getMessageId());
- }
- }
-
- return messageUpdates.size() > 0;
- }
-
- /**
- * Wraps multiple receipt updates in a transaction and triggers the proper updates.
- *
- * @return All of the messages that didn't result in updates.
- */
- private @NonNull Collection incrementReceiptCounts(@NonNull List syncMessageIds, long timestamp, @NonNull MessageTable.ReceiptType receiptType) {
- return incrementReceiptCounts(syncMessageIds, timestamp, receiptType, MessageTable.MessageQualifier.ALL);
- }
-
- private @NonNull Collection incrementReceiptCounts(@NonNull List syncMessageIds, long timestamp, @NonNull MessageTable.ReceiptType receiptType, @NonNull MessageTable.MessageQualifier messageQualifier) {
- SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
- ThreadTable threadTable = SignalDatabase.threads();
- Set messageUpdates = new HashSet<>();
- Collection unhandled = new HashSet<>();
-
- db.beginTransaction();
- try {
- for (SyncMessageId id : syncMessageIds) {
- Set updates = incrementReceiptCountInternal(id, timestamp, receiptType, messageQualifier);
-
- if (updates.size() > 0) {
- messageUpdates.addAll(updates);
- } else {
- unhandled.add(id);
- }
- }
-
- for (MessageUpdate update : messageUpdates) {
- threadTable.updateSilently(update.getThreadId(), false);
- }
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
-
- for (MessageUpdate update : messageUpdates) {
- ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(update.getMessageId());
- ApplicationDependencies.getDatabaseObserver().notifyVerboseConversationListeners(Collections.singleton(update.getThreadId()));
-
- if (messageQualifier == MessageQualifier.STORY) {
- ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(Objects.requireNonNull(threadTable.getRecipientIdForThreadId(update.getThreadId())));
- }
- }
-
- if (messageUpdates.size() > 0) {
- notifyConversationListListeners();
- }
- }
-
- return unhandled;
- }
-
- private @NonNull Set incrementReceiptCountInternal(SyncMessageId messageId, long timestamp, MessageTable.ReceiptType receiptType, @NonNull MessageTable.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 (MessageTypes.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)));
- }
- }
- }
-
- 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;
- }
-
- /**
- * @return Unhandled ids
- */
- public Collection setTimestampReadFromSyncMessage(@NonNull List readMessages, long proposedExpireStarted, @NonNull Map threadToLatestRead) {
- SQLiteDatabase db = getWritableDatabase();
-
- List> expiringMessages = new LinkedList<>();
- Set updatedThreads = new HashSet<>();
- Collection unhandled = new LinkedList<>();
-
- db.beginTransaction();
- try {
- for (ReadMessage readMessage : readMessages) {
- RecipientId authorId = Recipient.externalPush(readMessage.getSender()).getId();
- TimestampReadResult result = setTimestampReadFromSyncMessageInternal(new SyncMessageId(authorId, readMessage.getTimestamp()),
- proposedExpireStarted,
- threadToLatestRead);
-
- expiringMessages.addAll(result.expiring);
- updatedThreads.addAll(result.threads);
-
- if (result.threads.isEmpty()) {
- unhandled.add(new SyncMessageId(authorId, readMessage.getTimestamp()));
- }
- }
-
- for (long threadId : updatedThreads) {
- SignalDatabase.threads().updateReadState(threadId);
- SignalDatabase.threads().setLastSeen(threadId);
- }
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
-
- for (Pair expiringMessage : expiringMessages) {
- ApplicationDependencies.getExpiringMessageManager()
- .scheduleDeletion(expiringMessage.first(), true, proposedExpireStarted, expiringMessage.second());
- }
-
- for (long threadId : updatedThreads) {
- notifyConversationListeners(threadId);
- }
-
- return unhandled;
- }
-
- /**
- * Handles a synchronized read message.
- * @param messageId An id representing the author-timestamp pair of the message that was read on a linked device. Note that the author could be self when
- * syncing read receipts for reactions.
- */
- private final @NonNull TimestampReadResult setTimestampReadFromSyncMessageInternal(SyncMessageId messageId, long proposedExpireStarted, @NonNull Map threadToLatestRead) {
- SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
- List> expiring = new LinkedList<>();
- String[] projection = new String[] { ID, THREAD_ID, EXPIRES_IN, EXPIRE_STARTED };
- 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(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));
- long expiresIn = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRES_IN));
- long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(EXPIRE_STARTED));
-
- expireStarted = expireStarted > 0 ? Math.min(proposedExpireStarted, expireStarted) : proposedExpireStarted;
-
- ContentValues values = new ContentValues();
- values.put(READ, 1);
- values.put(REACTIONS_UNREAD, 0);
- values.put(REACTIONS_LAST_SEEN, System.currentTimeMillis());
-
- if (expiresIn > 0) {
- values.put(EXPIRE_STARTED, expireStarted);
- expiring.add(new Pair<>(id, expiresIn));
- }
-
- database.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(id));
-
- threads.add(threadId);
-
- Long latest = threadToLatestRead.get(threadId);
- threadToLatestRead.put(threadId, (latest != null) ? Math.max(latest, messageId.getTimetamp()) : messageId.getTimetamp());
- }
- }
-
- return new TimestampReadResult(expiring, threads);
- }
-
- /**
- * Finds a message by timestamp+author.
- * Does *not* include attachments.
- */
- public @Nullable MessageRecord getMessageFor(long timestamp, RecipientId authorId) {
- Recipient author = Recipient.resolved(authorId);
-
- String query = DATE_SENT + " = ?";
- String[] args = SqlUtil.buildArgs(timestamp);
-
- try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, MMS_PROJECTION, query, args, null, null, null)) {
- MessageTable.Reader reader = MessageTable.mmsReaderFor(cursor);
-
- MessageRecord messageRecord;
-
- while ((messageRecord = reader.getNext()) != null) {
- if ((author.isSelf() && messageRecord.isOutgoing()) ||
- (!author.isSelf() && messageRecord.getIndividualRecipient().getId().equals(authorId)))
- {
- return messageRecord;
- }
- }
- }
-
- return null;
- }
-
- /**
- * A cursor containing all of the messages in a given thread, in the proper order.
- * This does *not* have attachments in it.
- */
- public Cursor getConversation(long threadId) {
- return getConversation(threadId, 0, 0);
- }
-
- /**
- * A cursor containing all of the messages in a given thread, in the proper order, respecting offset/limit.
- * This does *not* have attachments in it.
- */
- public Cursor getConversation(long threadId, long offset, long limit) {
- String selection = THREAD_ID + " = ? AND " + STORY_TYPE + " = ? AND " + PARENT_STORY_ID + " <= ? AND " + SCHEDULED_DATE + " = ?";
- String[] args = SqlUtil.buildArgs(threadId, 0, 0, -1);
- String order = DATE_RECEIVED + " DESC";
- String limitStr = limit > 0 || offset > 0 ? offset + ", " + limit : null;
-
- return getReadableDatabase().query(TABLE_NAME, MMS_PROJECTION, selection, args, null, null, order, limitStr);
- }
-
- /**
- * Returns messages ordered for display in a reverse list (newest first).
- */
- public List getScheduledMessagesInThread(long threadId) {
- String selection = THREAD_ID + " = ? AND " + STORY_TYPE + " = ? AND " + PARENT_STORY_ID + " <= ? AND " + SCHEDULED_DATE + " != ?";
- String[] args = SqlUtil.buildArgs(threadId, 0, 0, -1);
- String order = SCHEDULED_DATE + " DESC, " + ID + " DESC";
-
- try (MmsReader reader = mmsReaderFor(getReadableDatabase().query(TABLE_NAME + " INDEXED BY " + INDEX_THREAD_STORY_SCHEDULED_DATE, MMS_PROJECTION, selection, args, null, null, order))) {
- List results = new ArrayList<>(reader.getCount());
- while (reader.getNext() != null) {
- results.add(reader.getCurrent());
- }
-
- return results;
- }
- }
-
- /**
- * Returns messages order for sending (oldest first).
- */
- public List getScheduledMessagesBefore(long time) {
- String selection = STORY_TYPE + " = ? AND " + PARENT_STORY_ID + " <= ? AND " + SCHEDULED_DATE + " != ? AND " + SCHEDULED_DATE + " <= ?";
- String[] args = SqlUtil.buildArgs(0, 0, -1, time);
- String order = SCHEDULED_DATE + " ASC, " + ID + " ASC";
-
- try (MmsReader reader = mmsReaderFor(getReadableDatabase().query(TABLE_NAME, MMS_PROJECTION, selection, args, null, null, order))) {
- List results = new ArrayList<>(reader.getCount());
- while (reader.getNext() != null) {
- results.add(reader.getCurrent());
- }
-
- return results;
- }
- }
-
- public @Nullable MessageRecord getOldestScheduledSendTimestamp() {
- String[] columns = new String[] { SCHEDULED_DATE };
- String selection = STORY_TYPE + " = ? AND " + PARENT_STORY_ID + " <= ? AND " + SCHEDULED_DATE + " != ?";
- String[] args = SqlUtil.buildArgs(0, 0, -1);
- String order = SCHEDULED_DATE + " ASC, " + ID + " ASC";
- String limit = "1";
-
- try (MmsReader reader = mmsReaderFor(getReadableDatabase().query(TABLE_NAME, MMS_PROJECTION, selection, args, null, null, order, limit))) {
- if (reader.getNext() != null) {
- return reader.getCurrent();
- }
- }
-
- return null;
- }
-
- public Cursor getMessagesForNotificationState(Collection stickyThreads) {
- StringBuilder stickyQuery = new StringBuilder();
- for (DefaultMessageNotifier.StickyThread stickyThread : stickyThreads) {
- if (stickyQuery.length() > 0) {
- stickyQuery.append(" OR ");
- }
- stickyQuery.append("(")
- .append(MessageTable.THREAD_ID + " = ")
- .append(stickyThread.getConversationId().getThreadId())
- .append(" AND ")
- .append(MessageTable.DATE_RECEIVED)
- .append(" >= ")
- .append(stickyThread.getEarliestTimestamp())
- .append(getStickyWherePartForParentStoryId(stickyThread.getConversationId().getGroupStoryId()))
- .append(")");
- }
-
- String order = MessageTable.DATE_RECEIVED + " ASC";
- String selection = MessageTable.NOTIFIED + " = 0 AND " + MessageTable.STORY_TYPE + " = 0 AND (" + MessageTable.READ + " = 0 OR " + MessageTable.REACTIONS_UNREAD + " = 1" + (stickyQuery.length() > 0 ? " OR (" + stickyQuery + ")" : "") + ")";
-
- return getReadableDatabase().query(TABLE_NAME, MMS_PROJECTION, selection, null, null, null, order);
- }
-
- private @NonNull String getStickyWherePartForParentStoryId(@Nullable Long parentStoryId) {
- if (parentStoryId == null) {
- return " AND " + MessageTable.PARENT_STORY_ID + " <= 0";
- }
-
- return " AND " + MessageTable.PARENT_STORY_ID + " = " + parentStoryId;
- }
-
- @Override
- public void remapRecipient(@NonNull RecipientId fromId, @NonNull RecipientId toId) {
- ContentValues values = new ContentValues();
- values.put(RECIPIENT_ID, toId.serialize());
- getWritableDatabase().update(TABLE_NAME, values, RECIPIENT_ID + " = ?", SqlUtil.buildArgs(fromId));
- }
-
- @Override
- public void remapThread(long fromId, long toId) {
- ContentValues values = new ContentValues();
- values.put(THREAD_ID, toId);
- getWritableDatabase().update(TABLE_NAME, values, THREAD_ID + " = ?", SqlUtil.buildArgs(fromId));
- }
-
- /**
- * Returns the next ID that would be generated if an insert was done on this table.
- * You should *not* use this for actually generating an ID to use. That will happen automatically!
- * This was added for a very narrow usecase, and you probably don't need to use it.
- */
- public long getNextId() {
- return SqlUtil.getNextAutoIncrementId(getWritableDatabase(), TABLE_NAME);
- }
-
- void updateReactionsUnread(SQLiteDatabase db, long messageId, boolean hasReactions, boolean isRemoval) {
- try {
- boolean isOutgoing = getMessageRecord(messageId).isOutgoing();
- ContentValues values = new ContentValues();
-
- if (!hasReactions) {
- values.put(REACTIONS_UNREAD, 0);
- } else if (!isRemoval) {
- values.put(REACTIONS_UNREAD, 1);
- }
-
- if (isOutgoing && hasReactions) {
- values.put(NOTIFIED, 0);
- }
-
- if (values.size() > 0) {
- db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(messageId));
- }
- } catch (NoSuchMessageException e) {
- Log.w(TAG, "Failed to find message " + messageId);
- }
- }
-
- protected , I> void removeFromDocument(long messageId, String column, I object, Class clazz) throws IOException {
- SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
- database.beginTransaction();
-
- try {
- D document = getDocument(database, messageId, column, clazz);
- Iterator iterator = document.getItems().iterator();
-
- while (iterator.hasNext()) {
- I item = iterator.next();
-
- if (item.equals(object)) {
- iterator.remove();
- break;
- }
- }
-
- setDocument(database, messageId, column, document);
- database.setTransactionSuccessful();
- } finally {
- database.endTransaction();
- }
- }
-
- protected , I> void addToDocument(long messageId, String column, final I object, Class clazz) throws IOException {
- List list = new ArrayList() {{
- add(object);
- }};
-
- addToDocument(messageId, column, list, clazz);
- }
-
- protected , I> void addToDocument(long messageId, String column, List objects, Class clazz) throws IOException {
- SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
- database.beginTransaction();
-
- try {
- T document = getDocument(database, messageId, column, clazz);
- document.getItems().addAll(objects);
- setDocument(database, messageId, column, document);
-
- database.setTransactionSuccessful();
- } finally {
- database.endTransaction();
- }
- }
-
- protected void setDocument(SQLiteDatabase database, long messageId, String column, Document document) throws IOException {
- ContentValues contentValues = new ContentValues();
-
- if (document == null || document.size() == 0) {
- contentValues.put(column, (String)null);
- } else {
- contentValues.put(column, JsonUtils.toJson(document));
- }
-
- database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)});
- }
-
- private D getDocument(SQLiteDatabase database, long messageId,
- String column, Class clazz)
- {
-
- try (Cursor cursor = database.query(TABLE_NAME, new String[] { column }, ID_WHERE, new String[] { String.valueOf(messageId) }, null, null, null)) {
- if (cursor != null && cursor.moveToNext()) {
- String document = cursor.getString(cursor.getColumnIndexOrThrow(column));
-
- try {
- if (!TextUtils.isEmpty(document)) {
- return JsonUtils.fromJson(document, clazz);
- }
- } catch (IOException e) {
- Log.w(TAG, e);
- }
- }
-
- try {
- return clazz.newInstance();
- } catch (InstantiationException | IllegalAccessException e) {
- throw new AssertionError(e);
- }
-
- }
- }
-
- public @NonNull Map getBodyRangesForMessages(@NonNull List messageIds) {
- List queries = SqlUtil.buildCollectionQuery(ID, messageIds);
- Map bodyRanges = new HashMap<>();
-
- for (SqlUtil.Query query : queries) {
- try (Cursor cursor = SQLiteDatabaseExtensionsKt.select(getReadableDatabase(), ID, MESSAGE_RANGES)
- .from(TABLE_NAME)
- .where(query.getWhere(), query.getWhereArgs())
- .run())
- {
- while (cursor.moveToNext()) {
- byte[] data = CursorUtil.requireBlob(cursor, MESSAGE_RANGES);
- if (data != null) {
- try {
- bodyRanges.put(CursorUtil.requireLong(cursor, ID), BodyRangeList.parseFrom(data));
- } catch (InvalidProtocolBufferException e) {
- Log.w(TAG, "Unable to parse body ranges for search", e);
- }
- }
- }
- }
- }
-
- return bodyRanges;
- }
-
- protected enum ReceiptType {
- READ(READ_RECEIPT_COUNT, GroupReceiptTable.STATUS_READ),
- DELIVERY(DELIVERY_RECEIPT_COUNT, GroupReceiptTable.STATUS_DELIVERED),
- VIEWED(VIEWED_RECEIPT_COUNT, GroupReceiptTable.STATUS_VIEWED);
-
- private final String columnName;
- private final int groupStatus;
-
- ReceiptType(String columnName, int groupStatus) {
- this.columnName = columnName;
- this.groupStatus = groupStatus;
- }
-
- public String getColumnName() {
- return columnName;
- }
-
- public int getGroupStatus() {
- return groupStatus;
- }
- }
-
- public static class SyncMessageId {
-
- private final RecipientId recipientId;
- private final long timetamp;
-
- public SyncMessageId(@NonNull RecipientId recipientId, long timetamp) {
- this.recipientId = recipientId;
- this.timetamp = timetamp;
- }
-
- public RecipientId getRecipientId() {
- return recipientId;
- }
-
- public long getTimetamp() {
- return timetamp;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- final SyncMessageId that = (SyncMessageId) o;
- return timetamp == that.timetamp && Objects.equals(recipientId, that.recipientId);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(recipientId, timetamp);
- }
- }
-
- public static class ExpirationInfo {
-
- private final long id;
- private final long expiresIn;
- private final long expireStarted;
- private final boolean mms;
-
- public ExpirationInfo(long id, long expiresIn, long expireStarted, boolean mms) {
- this.id = id;
- this.expiresIn = expiresIn;
- this.expireStarted = expireStarted;
- this.mms = mms;
- }
-
- public long getId() {
- return id;
- }
-
- public long getExpiresIn() {
- return expiresIn;
- }
-
- public long getExpireStarted() {
- return expireStarted;
- }
-
- public boolean isMms() {
- return mms;
- }
- }
-
- public static class MarkedMessageInfo {
-
- private final long threadId;
- private final SyncMessageId syncMessageId;
- private final MessageId messageId;
- private final ExpirationInfo expirationInfo;
- private final StoryType storyType;
-
- public MarkedMessageInfo(long threadId, @NonNull SyncMessageId syncMessageId, @NonNull MessageId messageId, @Nullable ExpirationInfo expirationInfo, @NonNull StoryType storyType) {
- this.threadId = threadId;
- this.syncMessageId = syncMessageId;
- this.messageId = messageId;
- this.expirationInfo = expirationInfo;
- this.storyType = storyType;
- }
-
- public long getThreadId() {
- return threadId;
- }
-
- public @NonNull SyncMessageId getSyncMessageId() {
- return syncMessageId;
- }
-
- public @NonNull MessageId getMessageId() {
- return messageId;
- }
-
- public @Nullable ExpirationInfo getExpirationInfo() {
- return expirationInfo;
- }
-
- public @NonNull StoryType getStoryType() {
- return storyType;
- }
- }
-
- public static class InsertResult {
- private final long messageId;
- private final long threadId;
-
- public InsertResult(long messageId, long threadId) {
- this.messageId = messageId;
- this.threadId = threadId;
- }
-
- public long getMessageId() {
- return messageId;
- }
-
- public long getThreadId() {
- return threadId;
- }
- }
-
- public static class MmsNotificationInfo {
- private final RecipientId from;
- private final String contentLocation;
- private final String transactionId;
- private final int subscriptionId;
-
- MmsNotificationInfo(@NonNull RecipientId from, String contentLocation, String transactionId, int subscriptionId) {
- this.from = from;
- this.contentLocation = contentLocation;
- this.transactionId = transactionId;
- this.subscriptionId = subscriptionId;
- }
-
- public String getContentLocation() {
- return contentLocation;
- }
-
- public String getTransactionId() {
- return transactionId;
- }
-
- public int getSubscriptionId() {
- return subscriptionId;
- }
-
- public @NonNull RecipientId getFrom() {
- return from;
- }
- }
-
- static class MessageUpdate {
- private final long threadId;
- private final MessageId messageId;
-
- MessageUpdate(long threadId, @NonNull MessageId messageId) {
- this.threadId = threadId;
- this.messageId = messageId;
- }
-
- public long getThreadId() {
- return threadId;
- }
-
- public @NonNull MessageId getMessageId() {
- return messageId;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- final MessageUpdate that = (MessageUpdate) o;
- return threadId == that.threadId && messageId.equals(that.messageId);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(threadId, messageId);
- }
- }
-
-
- public interface InsertListener {
- void onComplete();
- }
-
- /**
- * Allows the developer to safely iterate over and close a cursor containing
- * data for MessageRecord objects. Supports for-each loops as well as try-with-resources
- * blocks.
- *
- * Readers are considered "one-shot" and it's on the caller to decide what needs
- * to be done with the data. Once read, a reader cannot be read from again. This
- * is by design, since reading data out of a cursor involves object creations and
- * lookups, so it is in the best interest of app performance to only read out the
- * data once. If you need to parse the list multiple times, it is recommended that
- * you copy the iterable out into a normal List, or use extension methods such as
- * partition.
- *
- * This reader does not support removal, since this would be considered a destructive
- * database call.
- */
- public interface Reader extends Closeable, Iterable {
- /**
- * @deprecated Use the Iterable interface instead.
- */
- @Deprecated
- MessageRecord getNext();
-
- /**
- * @deprecated Use the Iterable interface instead.
- */
- @Deprecated
- MessageRecord getCurrent();
-
- /**
- * Pulls the export state out of the query, if it is present.
- */
- @NonNull MessageExportState getMessageExportStateForCurrentRecord();
-
- /**
- * From the {@link Closeable} interface, removing the IOException requirement.
- */
- void close();
- }
-
- public static class ReportSpamData {
- private final RecipientId recipientId;
- private final String serverGuid;
- private final long dateReceived;
-
- public ReportSpamData(RecipientId recipientId, String serverGuid, long dateReceived) {
- this.recipientId = recipientId;
- this.serverGuid = serverGuid;
- this.dateReceived = dateReceived;
- }
-
- public @NonNull RecipientId getRecipientId() {
- return recipientId;
- }
-
- public @NonNull String getServerGuid() {
- return serverGuid;
- }
-
- public long getDateReceived() {
- return dateReceived;
- }
- }
-
- private static class QuoteDescriptor {
- private final long timestamp;
- private final RecipientId author;
-
- private QuoteDescriptor(long timestamp, RecipientId author) {
- this.author = author;
- this.timestamp = timestamp;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- final QuoteDescriptor that = (QuoteDescriptor) o;
- return timestamp == that.timestamp && author.equals(that.author);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(author, timestamp);
- }
- }
-
- static final class TimestampReadResult {
- final List> expiring;
- final List threads;
-
- TimestampReadResult(@NonNull List> expiring, @NonNull List threads) {
- this.expiring = expiring;
- this.threads = threads;
- }
- }
-
- /**
- * Describes which messages to act on. This is used when incrementing receipts.
- * Specifically, this was added to support stories having separate viewed receipt settings.
- */
- public enum MessageQualifier {
- /**
- * A normal database message (i.e. not a story)
- */
- NORMAL,
- /**
- * A story message
- */
- STORY,
- /**
- * Both normal and story message
- */
- ALL
- }
-
- 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 OutgoingMessage message;
- private final long id;
- private final long threadId;
-
- public OutgoingMmsReader(OutgoingMessage 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();
- BodyRangeList quoteBodyRanges = message.getOutgoingQuote() != null ? message.getOutgoingQuote().getBodyRanges() : null;
-
- if (quoteText != null && (Util.hasItems(quoteMentions) || quoteBodyRanges != null)) {
- MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, quoteText, quoteMentions);
-
- SpannableString styledText = new SpannableString(updated.getBody());
- MessageStyler.style(BodyRangeUtil.adjustBodyRanges(quoteBodyRanges, updated.getBodyAdjustments()), styledText);
-
- quoteText = styledText;
- quoteMentions = updated.getMentions();
- }
-
- return new MediaMmsMessageRecord(id,
- message.getRecipient(),
- message.getRecipient(),
- 1,
- System.currentTimeMillis(),
- System.currentTimeMillis(),
- -1,
- 0,
- threadId, message.getBody(),
- slideDeck,
- message.isSecure() ? MessageTypes.getOutgoingEncryptedMessageType() : MessageTypes.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,
- null,
- -1);
- }
- }
-
- /**
- * 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);
- }
- }
-
- public MessageId getCurrentId() {
- return new MessageId(CursorUtil.requireLong(cursor, ID));
- }
-
- @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(MessageTable.VIEWED_RECEIPT_COUNT));
- long receiptTimestamp = CursorUtil.requireLong(cursor, MessageTable.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 && MessageTypes.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(VIEWED_RECEIPT_COUNT));
- long receiptTimestamp = CursorUtil.requireLong(cursor, 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));
- long scheduledDate = CursorUtil.requireLong(cursor, SCHEDULED_DATE);
-
- if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
- readReceiptCount = 0;
-
- if (MessageTypes.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 && MessageTypes.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, null, scheduledDate);
- }
-
- 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(cursor);
- BodyRangeList bodyRanges = parseQuoteBodyRanges(cursor);
- List attachments = SignalDatabase.attachments().getAttachments(cursor);
- List extends Attachment> quoteAttachments = Stream.of(attachments).filter(Attachment::isQuote).toList();
- SlideDeck quoteDeck = new SlideDeck(context, quoteAttachments);
-
- if (quoteId > 0 && quoteAuthor > 0) {
- if (quoteText != null && (Util.hasItems(quoteMentions) || bodyRanges != null)) {
- MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, quoteText, quoteMentions);
-
- SpannableString styledText = new SpannableString(updated.getBody());
- MessageStyler.style(BodyRangeUtil.adjustBodyRanges(bodyRanges, updated.getBodyAdjustments()), styledText);
-
- quoteText = styledText;
- 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/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt
new file mode 100644
index 0000000000..1bd1942c6d
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt
@@ -0,0 +1,5058 @@
+/**
+ * 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 //www.gnu.org/licenses/>.
+ */
+package org.thoughtcrime.securesms.database
+
+import android.content.ContentValues
+import android.content.Context
+import android.database.Cursor
+import android.text.SpannableString
+import android.text.TextUtils
+import androidx.annotation.VisibleForTesting
+import androidx.core.content.contentValuesOf
+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.CursorUtil
+import org.signal.core.util.SqlUtil
+import org.signal.core.util.SqlUtil.appendArg
+import org.signal.core.util.SqlUtil.buildArgs
+import org.signal.core.util.SqlUtil.buildCustomCollectionQuery
+import org.signal.core.util.SqlUtil.buildSingleCollectionQuery
+import org.signal.core.util.SqlUtil.buildTrueUpdateQuery
+import org.signal.core.util.SqlUtil.getNextAutoIncrementId
+import org.signal.core.util.delete
+import org.signal.core.util.emptyIfNull
+import org.signal.core.util.exists
+import org.signal.core.util.forEach
+import org.signal.core.util.insertInto
+import org.signal.core.util.logging.Log
+import org.signal.core.util.readToList
+import org.signal.core.util.readToSingleInt
+import org.signal.core.util.readToSingleLong
+import org.signal.core.util.readToSingleObject
+import org.signal.core.util.requireBlob
+import org.signal.core.util.requireBoolean
+import org.signal.core.util.requireInt
+import org.signal.core.util.requireLong
+import org.signal.core.util.requireNonNullString
+import org.signal.core.util.requireString
+import org.signal.core.util.select
+import org.signal.core.util.toOptional
+import org.signal.core.util.toSingleLine
+import org.signal.core.util.update
+import org.signal.core.util.withinTransaction
+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.DatabaseAttachment.DisplayOrderComparator
+import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment
+import org.thoughtcrime.securesms.contactshare.Contact
+import org.thoughtcrime.securesms.conversation.MessageStyler.style
+import org.thoughtcrime.securesms.database.EarlyReceiptCache.Receipt
+import org.thoughtcrime.securesms.database.MentionUtil.UpdatedBodyAndMentions
+import org.thoughtcrime.securesms.database.SignalDatabase.Companion.attachments
+import org.thoughtcrime.securesms.database.SignalDatabase.Companion.distributionLists
+import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groupReceipts
+import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groups
+import org.thoughtcrime.securesms.database.SignalDatabase.Companion.mentions
+import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messageLog
+import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messages
+import org.thoughtcrime.securesms.database.SignalDatabase.Companion.reactions
+import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients
+import org.thoughtcrime.securesms.database.SignalDatabase.Companion.storySends
+import org.thoughtcrime.securesms.database.SignalDatabase.Companion.threads
+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.MmsMessageRecord
+import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord
+import org.thoughtcrime.securesms.database.model.ParentStoryId
+import org.thoughtcrime.securesms.database.model.ParentStoryId.DirectReply
+import org.thoughtcrime.securesms.database.model.ParentStoryId.GroupReply
+import org.thoughtcrime.securesms.database.model.Quote
+import org.thoughtcrime.securesms.database.model.StoryResult
+import org.thoughtcrime.securesms.database.model.StoryType
+import org.thoughtcrime.securesms.database.model.StoryType.Companion.fromCode
+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.SessionSwitchoverEvent
+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.OptimizeMessageSearchIndexJob
+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.OutgoingMessage
+import org.thoughtcrime.securesms.mms.QuoteModel
+import org.thoughtcrime.securesms.mms.SlideDeck
+import org.thoughtcrime.securesms.notifications.v2.DefaultMessageNotifier.StickyThread
+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.stories.Stories.isFeatureEnabled
+import org.thoughtcrime.securesms.util.Base64
+import org.thoughtcrime.securesms.util.FeatureFlags
+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.messages.multidevice.ReadMessage
+import org.whispersystems.signalservice.api.push.ServiceId
+import java.io.Closeable
+import java.io.IOException
+import java.util.LinkedList
+import java.util.Optional
+import java.util.UUID
+import java.util.function.Function
+import kotlin.math.max
+import kotlin.math.min
+
+open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper), MessageTypes, RecipientIdDatabaseReference, ThreadIdDatabaseReference {
+
+ companion object {
+ private val TAG = Log.tag(MessageTable::class.java)
+ const val TABLE_NAME = "message"
+ const val ID = "_id"
+ const val DATE_SENT = "date_sent"
+ const val DATE_RECEIVED = "date_received"
+ const val TYPE = "type"
+ const val DATE_SERVER = "date_server"
+ const val THREAD_ID = "thread_id"
+ const val READ = "read"
+ const val BODY = "body"
+ const val RECIPIENT_ID = "recipient_id"
+ const val RECIPIENT_DEVICE_ID = "recipient_device_id"
+ const val DELIVERY_RECEIPT_COUNT = "delivery_receipt_count"
+ const val READ_RECEIPT_COUNT = "read_receipt_count"
+ const val VIEWED_RECEIPT_COUNT = "viewed_receipt_count"
+ const val MISMATCHED_IDENTITIES = "mismatched_identities"
+ const val SMS_SUBSCRIPTION_ID = "subscription_id"
+ const val EXPIRES_IN = "expires_in"
+ const val EXPIRE_STARTED = "expire_started"
+ const val NOTIFIED = "notified"
+ const val NOTIFIED_TIMESTAMP = "notified_timestamp"
+ const val UNIDENTIFIED = "unidentified"
+ const val REACTIONS_UNREAD = "reactions_unread"
+ const val REACTIONS_LAST_SEEN = "reactions_last_seen"
+ const val REMOTE_DELETED = "remote_deleted"
+ const val SERVER_GUID = "server_guid"
+ const val RECEIPT_TIMESTAMP = "receipt_timestamp"
+ const val EXPORT_STATE = "export_state"
+ const val EXPORTED = "exported"
+ const val MMS_CONTENT_LOCATION = "ct_l"
+ const val MMS_EXPIRY = "exp"
+ const val MMS_MESSAGE_TYPE = "m_type"
+ const val MMS_MESSAGE_SIZE = "m_size"
+ const val MMS_STATUS = "st"
+ const val MMS_TRANSACTION_ID = "tr_id"
+ const val NETWORK_FAILURES = "network_failures"
+ const val QUOTE_ID = "quote_id"
+ const val QUOTE_AUTHOR = "quote_author"
+ const val QUOTE_BODY = "quote_body"
+ const val QUOTE_MISSING = "quote_missing"
+ const val QUOTE_BODY_RANGES = "quote_mentions"
+ const val QUOTE_TYPE = "quote_type"
+ const val SHARED_CONTACTS = "shared_contacts"
+ const val LINK_PREVIEWS = "link_previews"
+ const val MENTIONS_SELF = "mentions_self"
+ const val MESSAGE_RANGES = "message_ranges"
+ const val VIEW_ONCE = "view_once"
+ const val STORY_TYPE = "story_type"
+ const val PARENT_STORY_ID = "parent_story_id"
+ const val SCHEDULED_DATE = "scheduled_date"
+
+ const val 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_BODY_RANGES 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,
+ $SCHEDULED_DATE INTEGER DEFAULT -1
+ )
+ """
+
+ private const val INDEX_THREAD_DATE = "mms_thread_date_index"
+ private const val INDEX_THREAD_STORY_SCHEDULED_DATE = "mms_thread_story_parent_story_scheduled_date_index"
+
+ @JvmField
+ val CREATE_INDEXS = arrayOf(
+ "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 $INDEX_THREAD_DATE 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 $INDEX_THREAD_STORY_SCHEDULED_DATE ON $TABLE_NAME ($THREAD_ID, $DATE_RECEIVED, $STORY_TYPE, $PARENT_STORY_ID, $SCHEDULED_DATE);",
+ "CREATE INDEX IF NOT EXISTS message_quote_id_quote_author_scheduled_date_index ON $TABLE_NAME ($QUOTE_ID, $QUOTE_AUTHOR, $SCHEDULED_DATE);",
+ "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 & ${MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION} != 0;"
+ )
+
+ private val MMS_PROJECTION_BASE = arrayOf(
+ "$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_BODY_RANGES,
+ 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,
+ SCHEDULED_DATE
+ )
+
+ private val MMS_PROJECTION: Array = MMS_PROJECTION_BASE + "NULL AS ${AttachmentTable.ATTACHMENT_JSON_ALIAS}"
+
+ private val MMS_PROJECTION_WITH_ATTACHMENTS: Array = MMS_PROJECTION_BASE +
+ """
+ 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}
+ """.toSingleLine()
+
+ private const val IS_STORY_CLAUSE = "$STORY_TYPE > 0 AND $REMOTE_DELETED = 0"
+ private const val RAW_ID_WHERE = "$TABLE_NAME.$ID = ?"
+
+ private val SNIPPET_QUERY =
+ """
+ SELECT
+ $ID,
+ $TYPE,
+ $DATE_RECEIVED
+ FROM
+ $TABLE_NAME
+ WHERE
+ $THREAD_ID = ? AND
+ $TYPE & ${MessageTypes.GROUP_V2_LEAVE_BITS} != ${MessageTypes.GROUP_V2_LEAVE_BITS} AND
+ $STORY_TYPE = 0 AND
+ $PARENT_STORY_ID <= 0 AND
+ $SCHEDULED_DATE = -1 AND
+ $TYPE NOT IN (
+ ${MessageTypes.PROFILE_CHANGE_TYPE},
+ ${MessageTypes.GV1_MIGRATION_TYPE},
+ ${MessageTypes.CHANGE_NUMBER_TYPE},
+ ${MessageTypes.BOOST_REQUEST_TYPE},
+ ${MessageTypes.SMS_EXPORT_TYPE}
+ )
+ ORDER BY $DATE_RECEIVED DESC LIMIT 1
+ """.toSingleLine()
+
+ @JvmStatic
+ fun mmsReaderFor(cursor: Cursor): MmsReader {
+ return MmsReader(cursor)
+ }
+
+ private fun getSharedContacts(cursor: Cursor, attachments: List): List {
+ val serializedContacts: String? = cursor.requireString(SHARED_CONTACTS)
+
+ if (serializedContacts.isNullOrEmpty()) {
+ return emptyList()
+ }
+
+ val attachmentIdMap: Map = attachments.associateBy { it.attachmentId }
+
+ try {
+ val contacts: MutableList = LinkedList()
+ val jsonContacts = JSONArray(serializedContacts)
+
+ for (i in 0 until jsonContacts.length()) {
+ val contact: Contact = Contact.deserialize(jsonContacts.getJSONObject(i).toString())
+
+ if (contact.avatar != null && contact.avatar!!.attachmentId != null) {
+ val attachment = attachmentIdMap[contact.avatar!!.attachmentId]
+
+ val updatedAvatar = Contact.Avatar(
+ contact.avatar!!.attachmentId,
+ attachment,
+ contact.avatar!!.isProfile
+ )
+
+ contacts += Contact(contact, updatedAvatar)
+ } else {
+ contacts += contact
+ }
+ }
+
+ return contacts
+ } catch (e: JSONException) {
+ Log.w(TAG, "Failed to parse shared contacts.", e)
+ } catch (e: IOException) {
+ Log.w(TAG, "Failed to parse shared contacts.", e)
+ }
+
+ return emptyList()
+ }
+
+ private fun getLinkPreviews(cursor: Cursor, attachments: List): List {
+ val serializedPreviews: String? = cursor.requireString(LINK_PREVIEWS)
+
+ if (serializedPreviews.isNullOrEmpty()) {
+ return emptyList()
+ }
+
+ val attachmentIdMap: Map = attachments.associateBy { it.attachmentId }
+
+ try {
+ val previews: MutableList = LinkedList()
+ val jsonPreviews = JSONArray(serializedPreviews)
+
+ for (i in 0 until jsonPreviews.length()) {
+ val preview = LinkPreview.deserialize(jsonPreviews.getJSONObject(i).toString())
+
+ if (preview.attachmentId != null) {
+ val attachment = attachmentIdMap[preview.attachmentId]
+
+ if (attachment != null) {
+ previews += LinkPreview(preview.url, preview.title, preview.description, preview.date, attachment)
+ } else {
+ previews += preview
+ }
+ } else {
+ previews += preview
+ }
+ }
+
+ return previews
+ } catch (e: JSONException) {
+ Log.w(TAG, "Failed to parse shared contacts.", e)
+ } catch (e: IOException) {
+ Log.w(TAG, "Failed to parse shared contacts.", e)
+ }
+
+ return emptyList()
+ }
+
+ private fun parseQuoteMentions(cursor: Cursor): List {
+ val data: ByteArray? = cursor.requireBlob(QUOTE_BODY_RANGES)
+
+ val bodyRanges: BodyRangeList? = if (data != null) {
+ try {
+ BodyRangeList.parseFrom(data)
+ } catch (e: InvalidProtocolBufferException) {
+ Log.w(TAG, "Unable to parse quote body ranges", e)
+ null
+ }
+ } else {
+ null
+ }
+
+ return MentionUtil.bodyRangeListToMentions(bodyRanges)
+ }
+
+ private fun parseQuoteBodyRanges(cursor: Cursor): BodyRangeList? {
+ val data: ByteArray? = cursor.requireBlob(QUOTE_BODY_RANGES)
+
+ if (data != null) {
+ try {
+ val bodyRanges = BodyRangeList
+ .parseFrom(data)
+ .rangesList
+ .filter { bodyRange -> bodyRange.associatedValueCase != BodyRangeList.BodyRange.AssociatedValueCase.MENTIONUUID }
+
+ return BodyRangeList.newBuilder().addAllRanges(bodyRanges).build()
+ } catch (e: InvalidProtocolBufferException) {
+ Log.w(TAG, "Unable to parse quote body ranges", e)
+ }
+ }
+ return null
+ }
+ }
+
+ private val earlyDeliveryReceiptCache = EarlyReceiptCache("MmsDelivery")
+
+ private fun getOldestGroupUpdateSender(threadId: Long, minimumDateReceived: Long): RecipientId? {
+ val type = MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT or MessageTypes.GROUP_UPDATE_BIT or MessageTypes.BASE_INBOX_TYPE
+
+ return readableDatabase
+ .select(RECIPIENT_ID)
+ .from(TABLE_NAME)
+ .where("$THREAD_ID = ? AND $TYPE & ? AND $DATE_RECEIVED >= ?", threadId.toString(), type.toString(), minimumDateReceived.toString())
+ .limit(1)
+ .run()
+ .readToSingleObject { RecipientId.from(it.requireLong(RECIPIENT_ID)) }
+ }
+
+ fun getExpirationStartedMessages(): Cursor {
+ val where = "$EXPIRE_STARTED > 0"
+ return rawQueryWithAttachments(where, null)
+ }
+
+ fun getMessageCursor(messageId: Long): Cursor {
+ return internalGetMessage(messageId)
+ }
+
+ fun hasReceivedAnyCallsSince(threadId: Long, timestamp: Long): Boolean {
+ return readableDatabase
+ .exists(TABLE_NAME)
+ .where(
+ "$THREAD_ID = ? AND $DATE_RECEIVED > ? AND ($TYPE = ? OR $TYPE = ? OR $TYPE = ? OR $TYPE =?)",
+ threadId,
+ timestamp,
+ MessageTypes.INCOMING_AUDIO_CALL_TYPE,
+ MessageTypes.INCOMING_VIDEO_CALL_TYPE,
+ MessageTypes.MISSED_AUDIO_CALL_TYPE,
+ MessageTypes.MISSED_VIDEO_CALL_TYPE
+ )
+ .run()
+ }
+
+ fun markAsEndSession(id: Long) {
+ updateTypeBitmask(id, MessageTypes.KEY_EXCHANGE_MASK, MessageTypes.END_SESSION_BIT)
+ }
+
+ fun markAsInvalidVersionKeyExchange(id: Long) {
+ updateTypeBitmask(id, 0, MessageTypes.KEY_EXCHANGE_INVALID_VERSION_BIT)
+ }
+
+ fun markAsDecryptFailed(id: Long) {
+ updateTypeBitmask(id, MessageTypes.ENCRYPTION_MASK, MessageTypes.ENCRYPTION_REMOTE_FAILED_BIT)
+ }
+
+ fun markAsNoSession(id: Long) {
+ updateTypeBitmask(id, MessageTypes.ENCRYPTION_MASK, MessageTypes.ENCRYPTION_REMOTE_NO_SESSION_BIT)
+ }
+
+ fun markAsUnsupportedProtocolVersion(id: Long) {
+ updateTypeBitmask(id, MessageTypes.BASE_TYPE_MASK, MessageTypes.UNSUPPORTED_MESSAGE_TYPE)
+ }
+
+ fun markAsInvalidMessage(id: Long) {
+ updateTypeBitmask(id, MessageTypes.BASE_TYPE_MASK, MessageTypes.INVALID_MESSAGE_TYPE)
+ }
+
+ fun markAsLegacyVersion(id: Long) {
+ updateTypeBitmask(id, MessageTypes.ENCRYPTION_MASK, MessageTypes.ENCRYPTION_REMOTE_LEGACY_BIT)
+ }
+
+ fun markAsMissedCall(id: Long, isVideoOffer: Boolean) {
+ updateTypeBitmask(id, MessageTypes.TOTAL_MASK, if (isVideoOffer) MessageTypes.MISSED_VIDEO_CALL_TYPE else MessageTypes.MISSED_AUDIO_CALL_TYPE)
+ }
+
+ fun markSmsStatus(id: Long, status: Int) {
+ Log.i(TAG, "Updating ID: $id to status: $status")
+
+ writableDatabase
+ .update(TABLE_NAME)
+ .values(MMS_STATUS to status)
+ .where("$ID = ?", id)
+ .run()
+
+ val threadId = getThreadIdForMessage(id)
+ threads.update(threadId, false)
+ notifyConversationListeners(threadId)
+ }
+
+ private fun updateTypeBitmask(id: Long, maskOff: Long, maskOn: Long) {
+ writableDatabase.withinTransaction { db ->
+ db.execSQL(
+ """
+ UPDATE $TABLE_NAME
+ SET $TYPE = ($TYPE & ${MessageTypes.TOTAL_MASK - maskOff} | $maskOn )
+ WHERE $ID = ?
+ """.toSingleLine(),
+ buildArgs(id)
+ )
+
+ val threadId = getThreadIdForMessage(id)
+ threads.updateSnippetTypeSilently(threadId)
+ }
+
+ ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(id))
+ ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners()
+ }
+
+ private fun updateMessageBodyAndType(messageId: Long, body: String, maskOff: Long, maskOn: Long): InsertResult {
+ writableDatabase.execSQL(
+ """
+ UPDATE $TABLE_NAME
+ SET
+ $BODY = ?,
+ $TYPE = ($TYPE & ${MessageTypes.TOTAL_MASK - maskOff} | $maskOn)
+ WHERE $ID = ?
+ """.toSingleLine(),
+ arrayOf(body, messageId.toString() + "")
+ )
+
+ val threadId = getThreadIdForMessage(messageId)
+ threads.update(threadId, true)
+ notifyConversationListeners(threadId)
+
+ return InsertResult(messageId, threadId)
+ }
+
+ fun updateBundleMessageBody(messageId: Long, body: String): InsertResult {
+ val type = MessageTypes.BASE_INBOX_TYPE or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT
+ return updateMessageBodyAndType(messageId, body, MessageTypes.TOTAL_MASK, type)
+ }
+
+ fun getViewedIncomingMessages(threadId: Long): List {
+ return readableDatabase
+ .select(ID, RECIPIENT_ID, DATE_SENT, TYPE, THREAD_ID, STORY_TYPE)
+ .from(TABLE_NAME)
+ .where("$THREAD_ID = ? AND $VIEWED_RECEIPT_COUNT > 0 AND $TYPE & ${MessageTypes.BASE_INBOX_TYPE} = ${MessageTypes.BASE_INBOX_TYPE}", threadId)
+ .run()
+ .readToList { it.toMarkedMessageInfo() }
+ }
+
+ fun setIncomingMessageViewed(messageId: Long): MarkedMessageInfo? {
+ val results = setIncomingMessagesViewed(listOf(messageId))
+ return if (results.isEmpty()) {
+ null
+ } else {
+ results[0]
+ }
+ }
+
+ fun setIncomingMessagesViewed(messageIds: List): List {
+ if (messageIds.isEmpty()) {
+ return emptyList()
+ }
+
+ val results: List = readableDatabase
+ .select(ID, RECIPIENT_ID, DATE_SENT, TYPE, THREAD_ID, STORY_TYPE)
+ .from(TABLE_NAME)
+ .where("$ID IN (${Util.join(messageIds, ",")}) AND $VIEWED_RECEIPT_COUNT = 0")
+ .run()
+ .readToList { cursor ->
+ val type = cursor.requireLong(TYPE)
+
+ if (MessageTypes.isSecureType(type) && MessageTypes.isInboxType(type)) {
+ cursor.toMarkedMessageInfo()
+ } else {
+ null
+ }
+ }
+ .filterNotNull()
+
+ val currentTime = System.currentTimeMillis()
+ SqlUtil
+ .buildCollectionQuery(ID, results.map { it.messageId.id })
+ .forEach { query ->
+ writableDatabase
+ .update(TABLE_NAME)
+ .values(
+ VIEWED_RECEIPT_COUNT to 1,
+ RECEIPT_TIMESTAMP to currentTime
+ )
+ .where(query.where, query.whereArgs)
+ .run()
+ }
+
+ val threadsUpdated: Set = results
+ .map { it.threadId }
+ .toSet()
+
+ val storyRecipientsUpdated: Set = results
+ .filter { it.storyType.isStory }
+ .mapNotNull { threads.getRecipientIdForThreadId(it.threadId) }
+ .toSet()
+
+ notifyConversationListeners(threadsUpdated)
+ notifyConversationListListeners()
+ ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(storyRecipientsUpdated)
+
+ return results
+ }
+
+ fun setOutgoingGiftsRevealed(messageIds: List): List {
+ val results: List = readableDatabase
+ .select(ID, RECIPIENT_ID, DATE_SENT, THREAD_ID, STORY_TYPE)
+ .from(TABLE_NAME)
+ .where("""$ID IN (${Util.join(messageIds, ",")}) AND (${getOutgoingTypeClause()}) AND ($TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} = ${MessageTypes.SPECIAL_TYPE_GIFT_BADGE}) AND $VIEWED_RECEIPT_COUNT = 0""")
+ .run()
+ .readToList { it.toMarkedMessageInfo() }
+
+ val currentTime = System.currentTimeMillis()
+ SqlUtil
+ .buildCollectionQuery(ID, results.map { it.messageId.id })
+ .forEach { query ->
+ writableDatabase
+ .update(TABLE_NAME)
+ .values(
+ VIEWED_RECEIPT_COUNT to 1,
+ RECEIPT_TIMESTAMP to currentTime
+ )
+ .where(query.where, query.whereArgs)
+ .run()
+ }
+
+ val threadsUpdated = results
+ .map { it.threadId }
+ .toSet()
+
+ notifyConversationListeners(threadsUpdated)
+ return results
+ }
+
+ fun insertCallLog(recipientId: RecipientId, type: Long, timestamp: Long): InsertResult {
+ val unread = MessageTypes.isMissedAudioCall(type) || MessageTypes.isMissedVideoCall(type)
+ val recipient = Recipient.resolved(recipientId)
+ val threadId = threads.getOrCreateThreadIdFor(recipient)
+
+ val values = contentValuesOf(
+ RECIPIENT_ID to recipientId.serialize(),
+ RECIPIENT_DEVICE_ID to 1,
+ DATE_RECEIVED to System.currentTimeMillis(),
+ DATE_SENT to timestamp,
+ READ to if (unread) 0 else 1,
+ TYPE to type,
+ THREAD_ID to threadId
+ )
+
+ val messageId = writableDatabase.insert(TABLE_NAME, null, values)
+
+ if (unread) {
+ threads.incrementUnread(threadId, 1, 0)
+ }
+
+ threads.update(threadId, true)
+
+ notifyConversationListeners(threadId)
+ TrimThreadJob.enqueueAsync(threadId)
+
+ return InsertResult(messageId, threadId)
+ }
+
+ fun updateCallLog(messageId: Long, type: Long) {
+ val unread = MessageTypes.isMissedAudioCall(type) || MessageTypes.isMissedVideoCall(type)
+
+ writableDatabase
+ .update(TABLE_NAME)
+ .values(
+ TYPE to type,
+ READ to if (unread) 0 else 1
+ )
+ .where("$ID = ?", messageId)
+ .run()
+
+ val threadId = getThreadIdForMessage(messageId)
+
+ if (unread) {
+ threads.incrementUnread(threadId, 1, 0)
+ }
+
+ threads.update(threadId, true)
+
+ notifyConversationListeners(threadId)
+ ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId))
+ }
+
+ fun insertOrUpdateGroupCall(
+ groupRecipientId: RecipientId,
+ sender: RecipientId,
+ timestamp: Long,
+ peekGroupCallEraId: String?,
+ peekJoinedUuids: Collection,
+ isCallFull: Boolean
+ ) {
+ val recipient = Recipient.resolved(groupRecipientId)
+ val threadId = threads.getOrCreateThreadIdFor(recipient)
+ val peerEraIdSameAsPrevious = updatePreviousGroupCall(threadId, peekGroupCallEraId, peekJoinedUuids, isCallFull)
+
+ writableDatabase.withinTransaction { db ->
+ if (!peerEraIdSameAsPrevious && !Util.isEmpty(peekGroupCallEraId)) {
+ val self = Recipient.self()
+ val markRead = peekJoinedUuids.contains(self.requireServiceId().uuid()) || self.id == sender
+ val updateDetails = GroupCallUpdateDetails.newBuilder()
+ .setEraId(peekGroupCallEraId.emptyIfNull())
+ .setStartedCallUuid(Recipient.resolved(sender).requireServiceId().toString())
+ .setStartedCallTimestamp(timestamp)
+ .addAllInCallUuids(peekJoinedUuids.map { it.toString() }.toList())
+ .setIsCallFull(isCallFull)
+ .build()
+ .toByteArray()
+
+ val values = contentValuesOf(
+ RECIPIENT_ID to sender.serialize(),
+ RECIPIENT_DEVICE_ID to 1,
+ DATE_RECEIVED to timestamp,
+ DATE_SENT to timestamp,
+ READ to if (markRead) 1 else 0,
+ BODY to Base64.encodeBytes(updateDetails),
+ TYPE to MessageTypes.GROUP_CALL_TYPE,
+ THREAD_ID to threadId
+ )
+
+ db.insert(TABLE_NAME, null, values)
+
+ threads.incrementUnread(threadId, 1, 0)
+ }
+
+ threads.update(threadId, true)
+ }
+
+ notifyConversationListeners(threadId)
+ TrimThreadJob.enqueueAsync(threadId)
+ }
+
+ fun insertOrUpdateGroupCall(
+ groupRecipientId: RecipientId,
+ sender: RecipientId,
+ timestamp: Long,
+ messageGroupCallEraId: String?
+ ) {
+ val threadId = writableDatabase.withinTransaction { db ->
+ val recipient = Recipient.resolved(groupRecipientId)
+ val threadId = threads.getOrCreateThreadIdFor(recipient)
+
+ val cursor = db
+ .select(*MMS_PROJECTION)
+ .from(TABLE_NAME)
+ .where("$TYPE = ? AND $THREAD_ID = ?", MessageTypes.GROUP_CALL_TYPE, threadId)
+ .orderBy("$DATE_RECEIVED DESC")
+ .limit(1)
+ .run()
+
+ var sameEraId = false
+
+ MmsReader(cursor).use { reader ->
+ val record: MessageRecord? = reader.firstOrNull()
+
+ if (record != null) {
+ val groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(record.body)
+ sameEraId = groupCallUpdateDetails.eraId == messageGroupCallEraId && !Util.isEmpty(messageGroupCallEraId)
+
+ if (!sameEraId) {
+ db.update(TABLE_NAME)
+ .values(BODY to GroupCallUpdateDetailsUtil.createUpdatedBody(groupCallUpdateDetails, emptyList(), false))
+ .where("$ID = ?", record.id)
+ .run()
+ }
+ }
+ }
+
+ if (!sameEraId && !Util.isEmpty(messageGroupCallEraId)) {
+ val updateDetails = GroupCallUpdateDetails.newBuilder()
+ .setEraId(Util.emptyIfNull(messageGroupCallEraId))
+ .setStartedCallUuid(Recipient.resolved(sender).requireServiceId().toString())
+ .setStartedCallTimestamp(timestamp)
+ .addAllInCallUuids(emptyList())
+ .setIsCallFull(false)
+ .build()
+ .toByteArray()
+
+ val values = contentValuesOf(
+ RECIPIENT_ID to sender.serialize(),
+ RECIPIENT_DEVICE_ID to 1,
+ DATE_RECEIVED to timestamp,
+ DATE_SENT to timestamp,
+ READ to 0,
+ BODY to Base64.encodeBytes(updateDetails),
+ TYPE to MessageTypes.GROUP_CALL_TYPE,
+ THREAD_ID to threadId
+ )
+
+ db.insert(TABLE_NAME, null, values)
+ threads.incrementUnread(threadId, 1, 0)
+ }
+
+ threads.update(threadId, true)
+
+ threadId
+ }
+
+ notifyConversationListeners(threadId)
+ TrimThreadJob.enqueueAsync(threadId)
+ }
+
+ fun updatePreviousGroupCall(threadId: Long, peekGroupCallEraId: String?, peekJoinedUuids: Collection, isCallFull: Boolean): Boolean {
+ return writableDatabase.withinTransaction { db ->
+ val cursor = db
+ .select(*MMS_PROJECTION)
+ .from(TABLE_NAME)
+ .where("$TYPE = ? AND $THREAD_ID = ?", MessageTypes.GROUP_CALL_TYPE, threadId)
+ .orderBy("$DATE_RECEIVED DESC")
+ .limit(1)
+ .run()
+
+ MmsReader(cursor).use { reader ->
+ val record = reader.getNext() ?: return@withinTransaction false
+ val groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(record.body)
+ val containsSelf = peekJoinedUuids.contains(SignalStore.account().requireAci().uuid())
+ val sameEraId = groupCallUpdateDetails.eraId == peekGroupCallEraId && !Util.isEmpty(peekGroupCallEraId)
+
+ val inCallUuids = if (sameEraId) {
+ peekJoinedUuids.map { it.toString() }.toList()
+ } else {
+ emptyList()
+ }
+
+ val contentValues = contentValuesOf(
+ BODY to GroupCallUpdateDetailsUtil.createUpdatedBody(groupCallUpdateDetails, inCallUuids, isCallFull)
+ )
+
+ if (sameEraId && containsSelf) {
+ contentValues.put(READ, 1)
+ }
+
+ val query = buildTrueUpdateQuery(ID_WHERE, buildArgs(record.id), contentValues)
+ val updated = db.update(TABLE_NAME, contentValues, query.where, query.whereArgs) > 0
+
+ if (updated) {
+ notifyConversationListeners(threadId)
+ }
+
+ sameEraId
+ }
+ }
+ }
+
+ @JvmOverloads
+ fun insertMessageInbox(message: IncomingTextMessage, type: Long = MessageTypes.BASE_INBOX_TYPE): Optional {
+ var type = type
+ var tryToCollapseJoinRequestEvents = false
+
+ if (message.isJoined) {
+ type = type and MessageTypes.TOTAL_MASK - MessageTypes.BASE_TYPE_MASK or MessageTypes.JOINED_TYPE
+ } else if (message.isPreKeyBundle) {
+ type = type or (MessageTypes.KEY_EXCHANGE_BIT or MessageTypes.KEY_EXCHANGE_BUNDLE_BIT)
+ } else if (message.isSecureMessage) {
+ type = type or MessageTypes.SECURE_MESSAGE_BIT
+ } else if (message.isGroup) {
+ val incomingGroupUpdateMessage = message as IncomingGroupUpdateMessage
+ type = type or MessageTypes.SECURE_MESSAGE_BIT
+ if (incomingGroupUpdateMessage.isGroupV2) {
+ type = type or (MessageTypes.GROUP_V2_BIT or MessageTypes.GROUP_UPDATE_BIT)
+ if (incomingGroupUpdateMessage.isJustAGroupLeave) {
+ type = type or MessageTypes.GROUP_LEAVE_BIT
+ } else if (incomingGroupUpdateMessage.isCancelJoinRequest) {
+ tryToCollapseJoinRequestEvents = true
+ }
+ } else if (incomingGroupUpdateMessage.isUpdate) {
+ type = type or MessageTypes.GROUP_UPDATE_BIT
+ } else if (incomingGroupUpdateMessage.isQuit) {
+ type = type or MessageTypes.GROUP_LEAVE_BIT
+ }
+ } else if (message.isEndSession) {
+ type = type or MessageTypes.SECURE_MESSAGE_BIT
+ type = type or MessageTypes.END_SESSION_BIT
+ }
+
+ if (message.isPush) {
+ type = type or MessageTypes.PUSH_MESSAGE_BIT
+ }
+
+ if (message.isIdentityUpdate) {
+ type = type or MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT
+ }
+
+ if (message.isContentPreKeyBundle) {
+ type = type or MessageTypes.KEY_EXCHANGE_CONTENT_FORMAT
+ }
+
+ if (message.isIdentityVerified) {
+ type = type or MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT
+ } else if (message.isIdentityDefault) {
+ type = type or MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT
+ }
+
+ val recipient = Recipient.resolved(message.sender)
+
+ val groupRecipient: Recipient? = if (message.groupId == null) {
+ null
+ } else {
+ val id = recipients.getOrInsertFromPossiblyMigratedGroupId(message.groupId!!)
+ Recipient.resolved(id)
+ }
+
+ val silent = message.isIdentityUpdate ||
+ message.isIdentityVerified ||
+ message.isIdentityDefault ||
+ message.isJustAGroupLeave || type and MessageTypes.GROUP_UPDATE_BIT > 0
+
+ val unread = !silent && (
+ message.isSecureMessage ||
+ message.isGroup ||
+ message.isPreKeyBundle ||
+ Util.isDefaultSmsProvider(context)
+ )
+
+ val threadId: Long = if (groupRecipient == null) threads.getOrCreateThreadIdFor(recipient) else threads.getOrCreateThreadIdFor(groupRecipient)
+
+ if (tryToCollapseJoinRequestEvents) {
+ val result = collapseJoinRequestEventsIfPossible(threadId, message as IncomingGroupUpdateMessage)
+ if (result.isPresent) {
+ return result
+ }
+ }
+
+ val values = ContentValues()
+ values.put(RECIPIENT_ID, message.sender.serialize())
+ values.put(RECIPIENT_DEVICE_ID, message.senderDeviceId)
+ values.put(DATE_RECEIVED, message.receivedTimestampMillis)
+ values.put(DATE_SENT, message.sentTimestampMillis)
+ values.put(DATE_SERVER, message.serverTimestampMillis)
+ values.put(READ, if (unread) 0 else 1)
+ values.put(SMS_SUBSCRIPTION_ID, message.subscriptionId)
+ values.put(EXPIRES_IN, message.expiresIn)
+ values.put(UNIDENTIFIED, message.isUnidentified)
+ values.put(BODY, message.messageBody)
+ values.put(TYPE, type)
+ values.put(THREAD_ID, threadId)
+ values.put(SERVER_GUID, message.serverGuid)
+
+ return if (message.isPush && isDuplicate(message, threadId)) {
+ Log.w(TAG, "Duplicate message (" + message.sentTimestampMillis + "), ignoring...")
+ Optional.empty()
+ } else {
+ val messageId = writableDatabase.insert(TABLE_NAME, null, values)
+
+ if (unread) {
+ threads.incrementUnread(threadId, 1, 0)
+ }
+
+ if (!silent) {
+ threads.update(threadId, true)
+ TrimThreadJob.enqueueAsync(threadId)
+ }
+
+ if (message.subscriptionId != -1) {
+ recipients.setDefaultSubscriptionId(recipient.id, message.subscriptionId)
+ }
+
+ notifyConversationListeners(threadId)
+
+ Optional.of(InsertResult(messageId, threadId))
+ }
+ }
+
+ fun insertProfileNameChangeMessages(recipient: Recipient, newProfileName: String, previousProfileName: String) {
+ writableDatabase.withinTransaction { db ->
+ val groupRecords = groups.getGroupsContainingMember(recipient.id, false)
+ val profileChangeDetails = ProfileChangeDetails.newBuilder()
+ .setProfileNameChange(
+ ProfileChangeDetails.StringChange.newBuilder()
+ .setNew(newProfileName)
+ .setPrevious(previousProfileName)
+ )
+ .build()
+ .toByteArray()
+
+ val threadIdsToUpdate = mutableListOf().apply {
+ add(threads.getThreadIdFor(recipient.id))
+ addAll(
+ groupRecords
+ .filter { it.isActive }
+ .map { threads.getThreadIdFor(it.recipientId) }
+ )
+ }
+
+ threadIdsToUpdate
+ .filterNotNull()
+ .forEach { threadId ->
+ val values = contentValuesOf(
+ RECIPIENT_ID to recipient.id.serialize(),
+ RECIPIENT_DEVICE_ID to 1,
+ DATE_RECEIVED to System.currentTimeMillis(),
+ DATE_SENT to System.currentTimeMillis(),
+ READ to 1,
+ TYPE to MessageTypes.PROFILE_CHANGE_TYPE,
+ THREAD_ID to threadId,
+ BODY to Base64.encodeBytes(profileChangeDetails)
+ )
+ db.insert(TABLE_NAME, null, values)
+ notifyConversationListeners(threadId)
+ TrimThreadJob.enqueueAsync(threadId)
+ }
+ }
+ }
+
+ fun insertGroupV1MigrationEvents(recipientId: RecipientId, threadId: Long, membershipChange: GroupMigrationMembershipChange) {
+ insertGroupV1MigrationNotification(recipientId, threadId)
+ if (!membershipChange.isEmpty) {
+ insertGroupV1MigrationMembershipChanges(recipientId, threadId, membershipChange)
+ }
+ notifyConversationListeners(threadId)
+ TrimThreadJob.enqueueAsync(threadId)
+ }
+
+ private fun insertGroupV1MigrationNotification(recipientId: RecipientId, threadId: Long) {
+ insertGroupV1MigrationMembershipChanges(recipientId, threadId, GroupMigrationMembershipChange.empty())
+ }
+
+ private fun insertGroupV1MigrationMembershipChanges(recipientId: RecipientId, threadId: Long, membershipChange: GroupMigrationMembershipChange) {
+ val values = contentValuesOf(
+ RECIPIENT_ID to recipientId.serialize(),
+ RECIPIENT_DEVICE_ID to 1,
+ DATE_RECEIVED to System.currentTimeMillis(),
+ DATE_SENT to System.currentTimeMillis(),
+ READ to 1,
+ TYPE to MessageTypes.GV1_MIGRATION_TYPE,
+ THREAD_ID to threadId
+ )
+
+ if (!membershipChange.isEmpty) {
+ values.put(BODY, membershipChange.serialize())
+ }
+
+ databaseHelper.signalWritableDatabase.insert(TABLE_NAME, null, values)
+ }
+
+ fun insertNumberChangeMessages(recipientId: RecipientId) {
+ val groupRecords = groups.getGroupsContainingMember(recipientId, false)
+
+ writableDatabase.withinTransaction { db ->
+ val threadIdsToUpdate = mutableListOf().apply {
+ add(threads.getThreadIdFor(recipientId))
+ addAll(
+ groupRecords
+ .filter { it.isActive }
+ .map { threads.getThreadIdFor(it.recipientId) }
+ )
+ }
+
+ threadIdsToUpdate
+ .filterNotNull()
+ .forEach { threadId: Long ->
+ val values = contentValuesOf(
+ RECIPIENT_ID to recipientId.serialize(),
+ RECIPIENT_DEVICE_ID to 1,
+ DATE_RECEIVED to System.currentTimeMillis(),
+ DATE_SENT to System.currentTimeMillis(),
+ READ to 1,
+ TYPE to MessageTypes.CHANGE_NUMBER_TYPE,
+ THREAD_ID to threadId,
+ BODY to null
+ )
+
+ db.insert(TABLE_NAME, null, values)
+ threads.update(threadId, true)
+
+ TrimThreadJob.enqueueAsync(threadId)
+ notifyConversationListeners(threadId)
+ }
+ }
+ }
+
+ fun insertBoostRequestMessage(recipientId: RecipientId, threadId: Long) {
+ writableDatabase
+ .insertInto(TABLE_NAME)
+ .values(
+ RECIPIENT_ID to recipientId.serialize(),
+ RECIPIENT_DEVICE_ID to 1,
+ DATE_RECEIVED to System.currentTimeMillis(),
+ DATE_SENT to System.currentTimeMillis(),
+ READ to 1,
+ TYPE to MessageTypes.BOOST_REQUEST_TYPE,
+ THREAD_ID to threadId,
+ BODY to null
+ )
+ .run()
+ }
+
+ fun insertThreadMergeEvent(recipientId: RecipientId, threadId: Long, event: ThreadMergeEvent) {
+ writableDatabase
+ .insertInto(TABLE_NAME)
+ .values(
+ RECIPIENT_ID to recipientId.serialize(),
+ RECIPIENT_DEVICE_ID to 1,
+ DATE_RECEIVED to System.currentTimeMillis(),
+ DATE_SENT to System.currentTimeMillis(),
+ READ to 1,
+ TYPE to MessageTypes.THREAD_MERGE_TYPE,
+ THREAD_ID to threadId,
+ BODY to Base64.encodeBytes(event.toByteArray())
+ )
+ .run()
+ ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId)
+ }
+
+ fun insertSessionSwitchoverEvent(recipientId: RecipientId, threadId: Long, event: SessionSwitchoverEvent) {
+ check(FeatureFlags.phoneNumberPrivacy()) { "Should not occur in a non-PNP world!" }
+ writableDatabase
+ .insertInto(TABLE_NAME)
+ .values(
+ RECIPIENT_ID to recipientId.serialize(),
+ RECIPIENT_DEVICE_ID to 1,
+ DATE_RECEIVED to System.currentTimeMillis(),
+ DATE_SENT to System.currentTimeMillis(),
+ READ to 1,
+ TYPE to MessageTypes.SESSION_SWITCHOVER_TYPE,
+ THREAD_ID to threadId,
+ BODY to Base64.encodeBytes(event.toByteArray())
+ )
+ .run()
+ ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId)
+ }
+
+ fun insertSmsExportMessage(recipientId: RecipientId, threadId: Long) {
+ val updated = writableDatabase.withinTransaction { db ->
+ if (messages.hasSmsExportMessage(threadId)) {
+ false
+ } else {
+ db.insertInto(TABLE_NAME)
+ .values(
+ RECIPIENT_ID to recipientId.serialize(),
+ RECIPIENT_DEVICE_ID to 1,
+ DATE_RECEIVED to System.currentTimeMillis(),
+ DATE_SENT to System.currentTimeMillis(),
+ READ to 1,
+ TYPE to MessageTypes.SMS_EXPORT_TYPE,
+ THREAD_ID to threadId,
+ BODY to null
+ )
+ .run()
+ true
+ }
+ }
+
+ if (updated) {
+ ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId)
+ }
+ }
+
+ fun endTransaction(database: SQLiteDatabase) {
+ database.endTransaction()
+ }
+
+ fun ensureMigration() {
+ databaseHelper.signalWritableDatabase
+ }
+
+ fun isStory(messageId: Long): Boolean {
+ return readableDatabase
+ .exists(TABLE_NAME)
+ .where("$IS_STORY_CLAUSE AND $ID = ?", messageId)
+ .run()
+ }
+
+ fun getOutgoingStoriesTo(recipientId: RecipientId): Reader {
+ val recipient = Recipient.resolved(recipientId)
+ val threadId: Long? = if (recipient.isGroup) {
+ threads.getThreadIdFor(recipientId)
+ } else {
+ null
+ }
+
+ var where = "$IS_STORY_CLAUSE AND (${getOutgoingTypeClause()})"
+ val whereArgs: Array
+
+ if (threadId == null) {
+ where += " AND $RECIPIENT_ID = ?"
+ whereArgs = buildArgs(recipientId)
+ } else {
+ where += " AND $THREAD_ID = ?"
+ whereArgs = buildArgs(threadId)
+ }
+
+ return MmsReader(rawQueryWithAttachments(where, whereArgs))
+ }
+
+ fun getAllOutgoingStories(reverse: Boolean, limit: Int): Reader {
+ val where = "$IS_STORY_CLAUSE AND (${getOutgoingTypeClause()})"
+ return MmsReader(rawQueryWithAttachments(where, null, reverse, limit.toLong()))
+ }
+
+ fun markAllIncomingStoriesRead(): List {
+ val where = "$IS_STORY_CLAUSE AND NOT (${getOutgoingTypeClause()}) AND $READ = 0"
+ val markedMessageInfos = setMessagesRead(where, null)
+ notifyConversationListListeners()
+ return markedMessageInfos
+ }
+
+ fun markOnboardingStoryRead() {
+ val recipientId = SignalStore.releaseChannelValues().releaseChannelRecipientId ?: return
+ val where = "$IS_STORY_CLAUSE AND NOT (${getOutgoingTypeClause()}) AND $READ = 0 AND $RECIPIENT_ID = ?"
+ val markedMessageInfos = setMessagesRead(where, buildArgs(recipientId))
+
+ if (markedMessageInfos.isNotEmpty()) {
+ notifyConversationListListeners()
+ }
+ }
+
+ fun getAllStoriesFor(recipientId: RecipientId, limit: Int): Reader {
+ val threadId = threads.getThreadIdIfExistsFor(recipientId)
+ val where = "$IS_STORY_CLAUSE AND $THREAD_ID = ?"
+ val whereArgs = buildArgs(threadId)
+ val cursor = rawQueryWithAttachments(where, whereArgs, false, limit.toLong())
+ return MmsReader(cursor)
+ }
+
+ fun getUnreadStories(recipientId: RecipientId, limit: Int): Reader {
+ val threadId = threads.getThreadIdIfExistsFor(recipientId)
+ val query = "$IS_STORY_CLAUSE AND NOT (${getOutgoingTypeClause()}) AND $THREAD_ID = ? AND $VIEWED_RECEIPT_COUNT = ?"
+ val args = buildArgs(threadId, 0)
+ return MmsReader(rawQueryWithAttachments(query, args, false, limit.toLong()))
+ }
+
+ fun getParentStoryIdForGroupReply(messageId: Long): GroupReply? {
+ return readableDatabase
+ .select(PARENT_STORY_ID)
+ .from(TABLE_NAME)
+ .where("$ID = ?", messageId)
+ .run()
+ .readToSingleObject { cursor ->
+ val parentStoryId: ParentStoryId? = ParentStoryId.deserialize(cursor.requireLong(PARENT_STORY_ID))
+ if (parentStoryId != null && parentStoryId.isGroupReply()) {
+ parentStoryId as GroupReply
+ } else {
+ null
+ }
+ }
+ }
+
+ fun getStoryViewState(recipientId: RecipientId): StoryViewState {
+ if (!isFeatureEnabled()) {
+ return StoryViewState.NONE
+ }
+ val threadId = threads.getThreadIdIfExistsFor(recipientId)
+ return getStoryViewState(threadId)
+ }
+
+ /**
+ * Synchronizes whether we've viewed a recipient's story based on incoming sync messages.
+ */
+ fun updateViewedStories(syncMessageIds: Set) {
+ val timestamps: String = syncMessageIds
+ .map { it.timetamp }
+ .joinToString(",")
+
+ writableDatabase.withinTransaction { db ->
+ db.select(RECIPIENT_ID)
+ .from(TABLE_NAME)
+ .where("$IS_STORY_CLAUSE AND $DATE_SENT IN ($timestamps) AND NOT (${getOutgoingTypeClause()}) AND $VIEWED_RECEIPT_COUNT > 0")
+ .run()
+ .readToList { cursor -> RecipientId.from(cursor.requireLong(RECIPIENT_ID)) }
+ .forEach { id -> recipients.updateLastStoryViewTimestamp(id) }
+ }
+ }
+
+ @VisibleForTesting
+ fun getStoryViewState(threadId: Long): StoryViewState {
+ val hasStories = readableDatabase
+ .exists(TABLE_NAME)
+ .where("$IS_STORY_CLAUSE AND $THREAD_ID = ?", threadId)
+ .run()
+
+ if (!hasStories) {
+ return StoryViewState.NONE
+ }
+
+ val hasUnviewedStories = readableDatabase
+ .exists(TABLE_NAME)
+ .where("$IS_STORY_CLAUSE AND $THREAD_ID = ? AND $VIEWED_RECEIPT_COUNT = ? AND NOT (${getOutgoingTypeClause()})", threadId, 0)
+ .run()
+
+ return if (hasUnviewedStories) {
+ StoryViewState.UNVIEWED
+ } else {
+ StoryViewState.VIEWED
+ }
+ }
+
+ fun isOutgoingStoryAlreadyInDatabase(recipientId: RecipientId, sentTimestamp: Long): Boolean {
+ return readableDatabase
+ .exists(TABLE_NAME)
+ .where("$RECIPIENT_ID = ? AND $STORY_TYPE > 0 AND $DATE_SENT = ? AND (${getOutgoingTypeClause()})", recipientId, sentTimestamp)
+ .run()
+ }
+
+ @Throws(NoSuchMessageException::class)
+ fun getStoryId(authorId: RecipientId, sentTimestamp: Long): MessageId {
+ return readableDatabase
+ .select(ID, RECIPIENT_ID)
+ .from(TABLE_NAME)
+ .where("$IS_STORY_CLAUSE AND $DATE_SENT = ?", sentTimestamp)
+ .run()
+ .readToSingleObject { cursor ->
+ val rowRecipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID)))
+ if (Recipient.self().id == authorId || rowRecipientId == authorId) {
+ MessageId(CursorUtil.requireLong(cursor, ID))
+ } else {
+ null
+ }
+ } ?: throw NoSuchMessageException("No story sent at $sentTimestamp")
+ }
+
+ fun getUnreadStoryThreadRecipientIds(): List {
+ val query = """
+ SELECT DISTINCT ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID}
+ FROM $TABLE_NAME
+ JOIN ${ThreadTable.TABLE_NAME} ON $TABLE_NAME.$THREAD_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID}
+ WHERE
+ $IS_STORY_CLAUSE AND
+ (${getOutgoingTypeClause()}) = 0 AND
+ $VIEWED_RECEIPT_COUNT = 0 AND
+ $TABLE_NAME.$READ = 0
+ """.toSingleLine()
+
+ return readableDatabase
+ .rawQuery(query, null)
+ .readToList { RecipientId.from(it.getLong(0)) }
+ }
+
+ fun getOrderedStoryRecipientsAndIds(isOutgoingOnly: Boolean): List {
+ val query = """
+ SELECT
+ $TABLE_NAME.$DATE_SENT AS sent_timestamp,
+ $TABLE_NAME.$ID AS mms_id,
+ ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID},
+ (${getOutgoingTypeClause()}) AS is_outgoing,
+ $VIEWED_RECEIPT_COUNT,
+ $TABLE_NAME.$DATE_SENT,
+ $RECEIPT_TIMESTAMP,
+ (${getOutgoingTypeClause()}) = 0 AND $VIEWED_RECEIPT_COUNT = 0 AS is_unread
+ FROM $TABLE_NAME
+ JOIN ${ThreadTable.TABLE_NAME} ON $TABLE_NAME.$THREAD_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID}
+ WHERE
+ $STORY_TYPE > 0 AND
+ $REMOTE_DELETED = 0
+ ${if (isOutgoingOnly) " AND is_outgoing != 0" else ""}
+ ORDER BY
+ is_unread DESC,
+ CASE
+ WHEN is_outgoing = 0 AND $VIEWED_RECEIPT_COUNT = 0 THEN $TABLE_NAME.$DATE_SENT
+ WHEN is_outgoing = 0 AND viewed_receipt_count > 0 THEN $RECEIPT_TIMESTAMP
+ WHEN is_outgoing = 1 THEN $TABLE_NAME.$DATE_SENT
+ END DESC
+ """.toSingleLine()
+
+ return readableDatabase
+ .rawQuery(query, null)
+ .readToList { cursor ->
+ StoryResult(
+ RecipientId.from(CursorUtil.requireLong(cursor, ThreadTable.RECIPIENT_ID)),
+ CursorUtil.requireLong(cursor, "mms_id"),
+ CursorUtil.requireLong(cursor, "sent_timestamp"),
+ CursorUtil.requireBoolean(cursor, "is_outgoing")
+ )
+ }
+ }
+
+ fun getStoryReplies(parentStoryId: Long): Cursor {
+ val where = "$PARENT_STORY_ID = ?"
+ val whereArgs = buildArgs(parentStoryId)
+ return rawQueryWithAttachments(where, whereArgs, false, 0)
+ }
+
+ fun getNumberOfStoryReplies(parentStoryId: Long): Int {
+ return readableDatabase
+ .select("COUNT(*)")
+ .from(TABLE_NAME)
+ .where("$PARENT_STORY_ID = ?", parentStoryId)
+ .run()
+ .readToSingleInt()
+ }
+
+ fun containsStories(threadId: Long): Boolean {
+ return readableDatabase
+ .exists(TABLE_NAME)
+ .where("$THREAD_ID = ? AND $STORY_TYPE > 0", threadId)
+ .run()
+ }
+
+ fun hasSelfReplyInStory(parentStoryId: Long): Boolean {
+ return readableDatabase
+ .exists(TABLE_NAME)
+ .where("$PARENT_STORY_ID = ? AND (${getOutgoingTypeClause()})", -parentStoryId)
+ .run()
+ }
+
+ fun hasGroupReplyOrReactionInStory(parentStoryId: Long): Boolean {
+ return hasSelfReplyInStory(-parentStoryId)
+ }
+
+ fun getOldestStorySendTimestamp(hasSeenReleaseChannelStories: Boolean): Long? {
+ val releaseChannelThreadId = getReleaseChannelThreadId(hasSeenReleaseChannelStories)
+
+ return readableDatabase
+ .select(DATE_SENT)
+ .from(TABLE_NAME)
+ .where("$IS_STORY_CLAUSE AND $THREAD_ID != ?", releaseChannelThreadId)
+ .limit(1)
+ .orderBy("$DATE_SENT ASC")
+ .run()
+ .readToSingleObject { it.getLong(0) }
+ }
+
+ @VisibleForTesting
+ fun deleteGroupStoryReplies(parentStoryId: Long) {
+ writableDatabase
+ .delete(TABLE_NAME)
+ .where("$PARENT_STORY_ID = ?", parentStoryId)
+ .run()
+ }
+
+ fun deleteStoriesOlderThan(timestamp: Long, hasSeenReleaseChannelStories: Boolean): Int {
+ return writableDatabase.withinTransaction { db ->
+ val releaseChannelThreadId = getReleaseChannelThreadId(hasSeenReleaseChannelStories)
+ val storiesBeforeTimestampWhere = "$IS_STORY_CLAUSE AND $DATE_SENT < ? AND $THREAD_ID != ?"
+ val sharedArgs = buildArgs(timestamp, releaseChannelThreadId)
+
+ val deleteStoryRepliesQuery = """
+ DELETE FROM $TABLE_NAME
+ WHERE
+ $PARENT_STORY_ID > 0 AND
+ $PARENT_STORY_ID IN (
+ SELECT $ID
+ FROM $TABLE_NAME
+ WHERE $storiesBeforeTimestampWhere
+ )
+ """.toSingleLine()
+
+ val 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
+ )
+ """.toSingleLine()
+
+ db.execSQL(deleteStoryRepliesQuery, sharedArgs)
+ db.execSQL(disassociateQuoteQuery, sharedArgs)
+
+ db.select(RECIPIENT_ID)
+ .from(TABLE_NAME)
+ .where(storiesBeforeTimestampWhere, sharedArgs)
+ .run()
+ .readToList { RecipientId.from(it.requireLong(RECIPIENT_ID)) }
+ .forEach { id -> ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(id) }
+
+ val deletedStoryCount = db.select(ID)
+ .from(TABLE_NAME)
+ .where(storiesBeforeTimestampWhere, sharedArgs)
+ .run()
+ .use { cursor ->
+ while (cursor.moveToNext()) {
+ deleteMessage(cursor.requireLong(ID))
+ }
+
+ cursor.count
+ }
+
+ if (deletedStoryCount > 0) {
+ OptimizeMessageSearchIndexJob.enqueue()
+ }
+
+ deletedStoryCount
+ }
+ }
+
+ private fun disassociateStoryQuotes(storyId: Long) {
+ writableDatabase
+ .update(TABLE_NAME)
+ .values(
+ QUOTE_MISSING to 1,
+ QUOTE_BODY to null
+ )
+ .where("$PARENT_STORY_ID = ?", DirectReply(storyId).serialize())
+ .run()
+ }
+
+ fun isGroupQuitMessage(messageId: Long): Boolean {
+ val type = MessageTypes.getOutgoingEncryptedMessageType() or MessageTypes.GROUP_LEAVE_BIT
+
+ return readableDatabase
+ .exists(TABLE_NAME)
+ .where("$ID = ? AND $TYPE & $type = $type AND $TYPE & ${MessageTypes.GROUP_V2_BIT} = 0", messageId)
+ .run()
+ }
+
+ fun getLatestGroupQuitTimestamp(threadId: Long, quitTimeBarrier: Long): Long {
+ val type = MessageTypes.getOutgoingEncryptedMessageType() or MessageTypes.GROUP_LEAVE_BIT
+
+ return readableDatabase
+ .select(DATE_SENT)
+ .from(TABLE_NAME)
+ .where("$THREAD_ID = ? AND $TYPE & $type = $type AND $TYPE & ${MessageTypes.GROUP_V2_BIT} = 0 AND $DATE_SENT < ?", threadId, quitTimeBarrier)
+ .orderBy("$DATE_SENT DESC")
+ .limit(1)
+ .run()
+ .readToSingleLong(-1)
+ }
+
+ fun getScheduledMessageCountForThread(threadId: Long): Int {
+ return readableDatabase
+ .select("COUNT(*)")
+ .from("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE")
+ .where("$THREAD_ID = ? AND $STORY_TYPE = ? AND $PARENT_STORY_ID <= ? AND $SCHEDULED_DATE != ?", threadId, 0, 0, -1)
+ .run()
+ .readToSingleInt()
+ }
+
+ fun getMessageCountForThread(threadId: Long): Int {
+ return readableDatabase
+ .select("COUNT(*)")
+ .from("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE")
+ .where("$THREAD_ID = ? AND $STORY_TYPE = ? AND $PARENT_STORY_ID <= ? AND $SCHEDULED_DATE = ?", threadId, 0, 0, -1)
+ .run()
+ .readToSingleInt()
+ }
+
+ fun getMessageCountForThread(threadId: Long, beforeTime: Long): Int {
+ return readableDatabase
+ .select("COUNT(*)")
+ .from("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE")
+ .where("$THREAD_ID = ? AND $DATE_RECEIVED < ? AND $STORY_TYPE = ? AND $PARENT_STORY_ID <= ? AND $SCHEDULED_DATE = ?", threadId, beforeTime, 0, 0, -1)
+ .run()
+ .readToSingleInt()
+ }
+
+ fun hasMeaningfulMessage(threadId: Long): Boolean {
+ if (threadId == -1L) {
+ return false
+ }
+
+ val query = buildMeaningfulMessagesQuery(threadId)
+ return readableDatabase
+ .exists(TABLE_NAME)
+ .where(query.where, query.whereArgs)
+ .run()
+ }
+
+ fun getIncomingMeaningfulMessageCountSince(threadId: Long, afterTime: Long): Int {
+ val meaningfulMessagesQuery = buildMeaningfulMessagesQuery(threadId)
+ val where = "${meaningfulMessagesQuery.where} AND $DATE_RECEIVED >= ? AND NOT (${getOutgoingTypeClause()})"
+ val whereArgs = appendArg(meaningfulMessagesQuery.whereArgs, afterTime.toString())
+
+ return readableDatabase
+ .select("COUNT(*)")
+ .from(TABLE_NAME)
+ .where(where, whereArgs)
+ .run()
+ .readToSingleInt()
+ }
+
+ private fun buildMeaningfulMessagesQuery(threadId: Long): SqlUtil.Query {
+ val query = """
+ $THREAD_ID = ? AND
+ $STORY_TYPE = ? AND
+ $PARENT_STORY_ID <= ? AND
+ (
+ NOT $TYPE & ? AND
+ $TYPE != ? AND
+ $TYPE != ? AND
+ $TYPE != ? AND
+ $TYPE != ? AND
+ $TYPE & ${MessageTypes.GROUP_V2_LEAVE_BITS} != ${MessageTypes.GROUP_V2_LEAVE_BITS}
+ )
+ """.toSingleLine()
+
+ return SqlUtil.buildQuery(query, threadId, 0, 0, MessageTypes.IGNORABLE_TYPESMASK_WHEN_COUNTING, MessageTypes.PROFILE_CHANGE_TYPE, MessageTypes.CHANGE_NUMBER_TYPE, MessageTypes.SMS_EXPORT_TYPE, MessageTypes.BOOST_REQUEST_TYPE)
+ }
+
+ fun setNetworkFailures(messageId: Long, failures: Set?) {
+ try {
+ setDocument(databaseHelper.signalWritableDatabase, messageId, NETWORK_FAILURES, NetworkFailureSet(failures))
+ } catch (e: IOException) {
+ Log.w(TAG, e)
+ }
+ }
+
+ fun getThreadIdForMessage(id: Long): Long {
+ return readableDatabase
+ .select(THREAD_ID)
+ .from(TABLE_NAME)
+ .where("$ID = ?", id)
+ .run()
+ .readToSingleLong(-1)
+ }
+
+ private fun getThreadIdFor(retrieved: IncomingMediaMessage): Long {
+ return if (retrieved.groupId != null) {
+ val groupRecipientId = recipients.getOrInsertFromPossiblyMigratedGroupId(retrieved.groupId)
+ val groupRecipients = Recipient.resolved(groupRecipientId)
+ threads.getOrCreateThreadIdFor(groupRecipients)
+ } else {
+ val sender = Recipient.resolved(retrieved.from!!)
+ threads.getOrCreateThreadIdFor(sender)
+ }
+ }
+
+ private fun getThreadIdFor(notification: NotificationInd): Long {
+ val fromString = if (notification.from != null && notification.from.textString != null) {
+ Util.toIsoString(notification.from.textString)
+ } else {
+ ""
+ }
+
+ val recipient = Recipient.external(context, fromString)
+ return threads.getOrCreateThreadIdFor(recipient)
+ }
+
+ private fun rawQueryWithAttachments(where: String, arguments: Array?, reverse: Boolean = false, limit: Long = 0): Cursor {
+ return rawQueryWithAttachments(MMS_PROJECTION_WITH_ATTACHMENTS, where, arguments, reverse, limit)
+ }
+
+ private fun rawQueryWithAttachments(projection: Array, where: String, arguments: Array?, reverse: Boolean, limit: Long): Cursor {
+ val database = databaseHelper.signalReadableDatabase
+ var rawQueryString = """
+ SELECT
+ ${Util.join(projection, ",")}
+ FROM
+ $TABLE_NAME LEFT OUTER JOIN ${AttachmentTable.TABLE_NAME} ON ($TABLE_NAME.$ID = ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MMS_ID})
+ WHERE
+ $where
+ GROUP BY
+ $TABLE_NAME.$ID
+ """.toSingleLine()
+
+ if (reverse) {
+ rawQueryString += " ORDER BY $TABLE_NAME.$ID DESC"
+ }
+
+ if (limit > 0) {
+ rawQueryString += " LIMIT $limit"
+ }
+
+ return database.rawQuery(rawQueryString, arguments)
+ }
+
+ private fun internalGetMessage(messageId: Long): Cursor {
+ return rawQueryWithAttachments(RAW_ID_WHERE, buildArgs(messageId))
+ }
+
+ @Throws(NoSuchMessageException::class)
+ fun getMessageRecord(messageId: Long): MessageRecord {
+ rawQueryWithAttachments(RAW_ID_WHERE, arrayOf(messageId.toString() + "")).use { cursor ->
+ return MmsReader(cursor).getNext() ?: throw NoSuchMessageException("No message for ID: $messageId")
+ }
+ }
+
+ fun getMessageRecordOrNull(messageId: Long): MessageRecord? {
+ rawQueryWithAttachments(RAW_ID_WHERE, buildArgs(messageId)).use { cursor ->
+ return MmsReader(cursor).firstOrNull()
+ }
+ }
+
+ fun getMessages(messageIds: Collection): MmsReader {
+ val ids = TextUtils.join(",", messageIds)
+ return mmsReaderFor(rawQueryWithAttachments("$TABLE_NAME.$ID IN ($ids)", null))
+ }
+
+ private fun updateMailboxBitmask(id: Long, maskOff: Long, maskOn: Long, threadId: Optional) {
+ writableDatabase.withinTransaction { db ->
+ db.execSQL(
+ """
+ UPDATE $TABLE_NAME
+ SET $TYPE = ($TYPE & ${MessageTypes.TOTAL_MASK - maskOff} | $maskOn )
+ WHERE $ID = ?
+ """.toSingleLine(),
+ buildArgs(id)
+ )
+
+ if (threadId.isPresent) {
+ threads.updateSnippetTypeSilently(threadId.get())
+ }
+ }
+ }
+
+ fun markAsOutbox(messageId: Long) {
+ val threadId = getThreadIdForMessage(messageId)
+ updateMailboxBitmask(messageId, MessageTypes.BASE_TYPE_MASK, MessageTypes.BASE_OUTBOX_TYPE, Optional.of(threadId))
+ }
+
+ fun markAsForcedSms(messageId: Long) {
+ val threadId = getThreadIdForMessage(messageId)
+ updateMailboxBitmask(messageId, MessageTypes.PUSH_MESSAGE_BIT, MessageTypes.MESSAGE_FORCE_SMS_BIT, Optional.of(threadId))
+ ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId))
+ }
+
+ fun markAsRateLimited(messageId: Long) {
+ val threadId = getThreadIdForMessage(messageId)
+ updateMailboxBitmask(messageId, 0, MessageTypes.MESSAGE_RATE_LIMITED_BIT, Optional.of(threadId))
+ ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId))
+ }
+
+ fun clearRateLimitStatus(ids: Collection) {
+ writableDatabase.withinTransaction {
+ for (id in ids) {
+ val threadId = getThreadIdForMessage(id)
+ updateMailboxBitmask(id, MessageTypes.MESSAGE_RATE_LIMITED_BIT, 0, Optional.of(threadId))
+ }
+ }
+ }
+
+ fun markAsPendingInsecureSmsFallback(messageId: Long) {
+ val threadId = getThreadIdForMessage(messageId)
+ updateMailboxBitmask(messageId, MessageTypes.BASE_TYPE_MASK, MessageTypes.BASE_PENDING_INSECURE_SMS_FALLBACK, Optional.of(threadId))
+ ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId))
+ }
+
+ fun markAsSending(messageId: Long) {
+ val threadId = getThreadIdForMessage(messageId)
+ updateMailboxBitmask(messageId, MessageTypes.BASE_TYPE_MASK, MessageTypes.BASE_SENDING_TYPE, Optional.of(threadId))
+ ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId))
+ ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners()
+ }
+
+ fun markAsSentFailed(messageId: Long) {
+ val threadId = getThreadIdForMessage(messageId)
+ updateMailboxBitmask(messageId, MessageTypes.BASE_TYPE_MASK, MessageTypes.BASE_SENT_FAILED_TYPE, Optional.of(threadId))
+ ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId))
+ ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners()
+ }
+
+ fun markAsSent(messageId: Long, secure: Boolean) {
+ val threadId = getThreadIdForMessage(messageId)
+ updateMailboxBitmask(messageId, MessageTypes.BASE_TYPE_MASK, MessageTypes.BASE_SENT_TYPE or if (secure) MessageTypes.PUSH_MESSAGE_BIT or MessageTypes.SECURE_MESSAGE_BIT else 0, Optional.of(threadId))
+ ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId))
+ ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners()
+ }
+
+ fun markAsRemoteDelete(messageId: Long) {
+ var deletedAttachments = false
+ writableDatabase.withinTransaction { db ->
+ db.update(TABLE_NAME)
+ .values(
+ REMOTE_DELETED to 1,
+ BODY to null,
+ QUOTE_BODY to null,
+ QUOTE_AUTHOR to null,
+ QUOTE_TYPE to null,
+ QUOTE_ID to null,
+ LINK_PREVIEWS to null,
+ SHARED_CONTACTS to null
+ )
+ .where("$ID = ?", messageId)
+ .run()
+
+ deletedAttachments = attachments.deleteAttachmentsForMessage(messageId)
+ mentions.deleteMentionsForMessage(messageId)
+ messageLog.deleteAllRelatedToMessage(messageId)
+ reactions.deleteReactions(MessageId(messageId))
+ deleteGroupStoryReplies(messageId)
+ disassociateStoryQuotes(messageId)
+
+ val threadId = getThreadIdForMessage(messageId)
+ threads.update(threadId, false)
+ }
+
+ OptimizeMessageSearchIndexJob.enqueue()
+ ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId))
+ ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners()
+
+ if (deletedAttachments) {
+ ApplicationDependencies.getDatabaseObserver().notifyAttachmentObservers()
+ }
+ }
+
+ fun markDownloadState(messageId: Long, state: Long) {
+ writableDatabase
+ .update(TABLE_NAME)
+ .values(MMS_STATUS to state)
+ .where("$ID = ?", messageId)
+ .run()
+
+ ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId))
+ }
+
+ fun clearScheduledStatus(threadId: Long, messageId: Long, expiresIn: Long): Boolean {
+ val rowsUpdated = writableDatabase
+ .update(TABLE_NAME)
+ .values(
+ SCHEDULED_DATE to -1,
+ DATE_SENT to System.currentTimeMillis(),
+ DATE_RECEIVED to System.currentTimeMillis(),
+ EXPIRES_IN to expiresIn
+ )
+ .where("$ID = ? AND $SCHEDULED_DATE != ?", messageId, -1)
+ .run()
+
+ ApplicationDependencies.getDatabaseObserver().notifyMessageInsertObservers(threadId, MessageId(messageId))
+ ApplicationDependencies.getDatabaseObserver().notifyScheduledMessageObservers(threadId)
+
+ return rowsUpdated > 0
+ }
+
+ fun rescheduleMessage(threadId: Long, messageId: Long, time: Long) {
+ val rowsUpdated = writableDatabase
+ .update(TABLE_NAME)
+ .values(SCHEDULED_DATE to time)
+ .where("$ID = ? AND $SCHEDULED_DATE != ?", messageId, -1)
+ .run()
+
+ ApplicationDependencies.getDatabaseObserver().notifyScheduledMessageObservers(threadId)
+ ApplicationDependencies.getScheduledMessageManager().scheduleIfNecessary()
+
+ if (rowsUpdated == 0) {
+ Log.w(TAG, "Failed to reschedule messageId=$messageId to new time $time. may have been sent already")
+ }
+ }
+
+ fun markAsInsecure(messageId: Long) {
+ updateMailboxBitmask(messageId, MessageTypes.SECURE_MESSAGE_BIT, 0, Optional.empty())
+ }
+
+ fun markUnidentified(messageId: Long, unidentified: Boolean) {
+ writableDatabase
+ .update(TABLE_NAME)
+ .values(UNIDENTIFIED to if (unidentified) 1 else 0)
+ .where("$ID = ?", messageId)
+ .run()
+ }
+
+ @JvmOverloads
+ fun markExpireStarted(id: Long, startedTimestamp: Long = System.currentTimeMillis()) {
+ markExpireStarted(setOf(id), startedTimestamp)
+ }
+
+ fun markExpireStarted(ids: Collection, startedAtTimestamp: Long) {
+ var threadId: Long = -1
+ writableDatabase.withinTransaction { db ->
+ for (id in ids) {
+ db.update(TABLE_NAME)
+ .values(EXPIRE_STARTED to startedAtTimestamp)
+ .where("$ID = ? AND ($EXPIRE_STARTED = 0 OR $EXPIRE_STARTED > ?)", id, startedAtTimestamp)
+ .run()
+
+ if (threadId < 0) {
+ threadId = getThreadIdForMessage(id)
+ }
+ }
+
+ threads.update(threadId, false)
+ }
+
+ notifyConversationListeners(threadId)
+ }
+
+ fun markAsNotified(id: Long) {
+ writableDatabase
+ .update(TABLE_NAME)
+ .values(
+ NOTIFIED to 1,
+ REACTIONS_LAST_SEEN to System.currentTimeMillis()
+ )
+ .where("$ID = ?", id)
+ .run()
+ }
+
+ fun setMessagesReadSince(threadId: Long, sinceTimestamp: Long): List {
+ var query = """
+ $THREAD_ID = ? AND
+ $STORY_TYPE = 0 AND
+ $PARENT_STORY_ID <= 0 AND
+ (
+ $READ = 0 OR
+ (
+ $REACTIONS_UNREAD = 1 AND
+ (${getOutgoingTypeClause()})
+ )
+ )
+ """.toSingleLine()
+
+ val args = mutableListOf(threadId.toString())
+
+ if (sinceTimestamp >= 0L) {
+ query += " AND $DATE_RECEIVED <= ?"
+ args += sinceTimestamp.toString()
+ }
+
+ return setMessagesRead(query, args.toTypedArray())
+ }
+
+ fun setGroupStoryMessagesReadSince(threadId: Long, groupStoryId: Long, sinceTimestamp: Long): List {
+ var query = """
+ $THREAD_ID = ? AND
+ $STORY_TYPE = 0 AND
+ $PARENT_STORY_ID = ? AND
+ (
+ $READ = 0 OR
+ (
+ $REACTIONS_UNREAD = 1 AND
+ (${getOutgoingTypeClause()})
+ )
+ )
+ """.toSingleLine()
+
+ val args = mutableListOf(threadId.toString(), groupStoryId.toString())
+
+ if (sinceTimestamp >= 0L) {
+ query += " AND $DATE_RECEIVED <= ?"
+ args += sinceTimestamp.toString()
+ }
+
+ return setMessagesRead(query, args.toTypedArray())
+ }
+
+ fun getStoryTypes(messageIds: List): List {
+ if (messageIds.isEmpty()) {
+ return emptyList()
+ }
+
+ val rawMessageIds: List = messageIds.map { it.id }
+ val storyTypes: MutableMap = mutableMapOf()
+
+ SqlUtil.buildCollectionQuery(ID, rawMessageIds).forEach { query ->
+ readableDatabase
+ .select(ID, STORY_TYPE)
+ .from(TABLE_NAME)
+ .where(query.where, query.whereArgs)
+ .run()
+ .use { cursor ->
+ while (cursor.moveToNext()) {
+ storyTypes[cursor.requireLong(ID)] = fromCode(cursor.requireInt(STORY_TYPE))
+ }
+ }
+ }
+
+ return rawMessageIds.map { id: Long ->
+ if (storyTypes.containsKey(id)) {
+ storyTypes[id]!!
+ } else {
+ StoryType.NONE
+ }
+ }
+ }
+
+ fun setEntireThreadRead(threadId: Long): List {
+ return setMessagesRead("$THREAD_ID = ? AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0", buildArgs(threadId))
+ }
+
+ fun setAllMessagesRead(): List {
+ return setMessagesRead("$STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND ($READ = 0 OR ($REACTIONS_UNREAD = 1 AND (${getOutgoingTypeClause()})))", null)
+ }
+
+ private fun setMessagesRead(where: String, arguments: Array?): List {
+ val releaseChannelId = SignalStore.releaseChannelValues().releaseChannelRecipientId
+ return writableDatabase.withinTransaction { db ->
+ val infos = db
+ .select(ID, RECIPIENT_ID, DATE_SENT, TYPE, EXPIRES_IN, EXPIRE_STARTED, THREAD_ID, STORY_TYPE)
+ .from("$TABLE_NAME INDEXED BY $INDEX_THREAD_DATE")
+ .where(where, arguments ?: emptyArray())
+ .run()
+ .readToList { cursor ->
+ val threadId = cursor.requireLong(THREAD_ID)
+ val recipientId = RecipientId.from(cursor.requireLong(RECIPIENT_ID))
+ val dateSent = cursor.requireLong(DATE_SENT)
+ val messageId = cursor.requireLong(ID)
+ val expiresIn = cursor.requireLong(EXPIRES_IN)
+ val expireStarted = cursor.requireLong(EXPIRE_STARTED)
+ val syncMessageId = SyncMessageId(recipientId, dateSent)
+ val expirationInfo = ExpirationInfo(messageId, expiresIn, expireStarted, true)
+ val storyType = fromCode(CursorUtil.requireInt(cursor, STORY_TYPE))
+
+ if (recipientId != releaseChannelId) {
+ MarkedMessageInfo(threadId, syncMessageId, MessageId(messageId), expirationInfo, storyType)
+ } else {
+ null
+ }
+ }
+ .filterNotNull()
+
+ db.update("$TABLE_NAME INDEXED BY $INDEX_THREAD_DATE")
+ .values(
+ READ to 1,
+ REACTIONS_UNREAD to 0,
+ REACTIONS_LAST_SEEN to System.currentTimeMillis()
+ )
+ .where(where, arguments ?: emptyArray())
+ .run()
+
+ infos
+ }
+ }
+
+ fun getOldestUnreadMentionDetails(threadId: Long): Pair? {
+ return readableDatabase
+ .select(RECIPIENT_ID, DATE_RECEIVED)
+ .from(TABLE_NAME)
+ .where("$THREAD_ID = ? AND $READ = 0 AND $MENTIONS_SELF = 1", threadId)
+ .orderBy("$DATE_RECEIVED ASC")
+ .limit(1)
+ .run()
+ .readToSingleObject { cursor ->
+ Pair(
+ RecipientId.from(cursor.requireLong(RECIPIENT_ID)),
+ cursor.requireLong(DATE_RECEIVED)
+ )
+ }
+ }
+
+ fun getUnreadMentionCount(threadId: Long): Int {
+ return readableDatabase
+ .select("COUNT(*)")
+ .from(TABLE_NAME)
+ .where("$THREAD_ID = ? AND $READ = 0 AND $MENTIONS_SELF = 1", threadId)
+ .run()
+ .readToSingleInt()
+ }
+
+ /**
+ * Trims data related to expired messages. Only intended to be run after a backup restore.
+ */
+ fun trimEntriesForExpiredMessages() {
+ writableDatabase
+ .delete(GroupReceiptTable.TABLE_NAME)
+ .where("${GroupReceiptTable.MMS_ID} NOT IN (SELECT $ID FROM $TABLE_NAME)")
+ .run()
+
+ readableDatabase
+ .select(AttachmentTable.ROW_ID, AttachmentTable.UNIQUE_ID)
+ .from(AttachmentTable.TABLE_NAME)
+ .where("${AttachmentTable.MMS_ID} NOT IN (SELECT $ID FROM $TABLE_NAME)")
+ .run()
+ .forEach { cursor ->
+ attachments.deleteAttachment(AttachmentId(cursor.requireLong(AttachmentTable.ROW_ID), cursor.requireLong(AttachmentTable.UNIQUE_ID)))
+ }
+
+ mentions.deleteAbandonedMentions()
+
+ readableDatabase
+ .select(ThreadTable.ID)
+ .from(ThreadTable.TABLE_NAME)
+ .where("${ThreadTable.EXPIRES_IN} > 0")
+ .run()
+ .forEach { cursor ->
+ val id = cursor.requireLong(ThreadTable.ID)
+ threads.setLastScrolled(id, 0)
+ threads.update(id, false)
+ }
+ }
+
+ fun getNotification(messageId: Long): Optional {
+ return readableDatabase
+ .select(RECIPIENT_ID, MMS_CONTENT_LOCATION, MMS_TRANSACTION_ID, SMS_SUBSCRIPTION_ID)
+ .from(TABLE_NAME)
+ .where("$ID = ?", messageId)
+ .run()
+ .readToSingleObject { cursor ->
+ MmsNotificationInfo(
+ from = RecipientId.from(cursor.requireLong(RECIPIENT_ID)),
+ contentLocation = cursor.requireNonNullString(MMS_CONTENT_LOCATION),
+ transactionId = cursor.requireNonNullString(MMS_TRANSACTION_ID),
+ subscriptionId = cursor.requireInt(SMS_SUBSCRIPTION_ID)
+ )
+ }
+ .toOptional()
+ }
+
+ @Throws(MmsException::class, NoSuchMessageException::class)
+ fun getOutgoingMessage(messageId: Long): OutgoingMessage {
+ return rawQueryWithAttachments(RAW_ID_WHERE, arrayOf(messageId.toString())).readToSingleObject { cursor ->
+ val associatedAttachments = attachments.getAttachmentsForMessage(messageId)
+ val mentions = mentions.getMentionsForMessage(messageId)
+ val outboxType = cursor.requireLong(TYPE)
+ val body = cursor.requireString(BODY)
+ val timestamp = cursor.requireLong(DATE_SENT)
+ val subscriptionId = cursor.requireInt(SMS_SUBSCRIPTION_ID)
+ val expiresIn = cursor.requireLong(EXPIRES_IN)
+ val viewOnce = cursor.requireLong(VIEW_ONCE) == 1L
+ val recipient = Recipient.resolved(RecipientId.from(cursor.requireLong(RECIPIENT_ID)))
+ val threadId = cursor.requireLong(THREAD_ID)
+ val distributionType = threads.getDistributionType(threadId)
+ val storyType = StoryType.fromCode(cursor.requireInt(STORY_TYPE))
+ val parentStoryId = ParentStoryId.deserialize(cursor.requireLong(PARENT_STORY_ID))
+ val messageRangesData = cursor.requireBlob(MESSAGE_RANGES)
+ val scheduledDate = cursor.requireLong(SCHEDULED_DATE)
+
+ val quoteId = cursor.requireLong(QUOTE_ID)
+ val quoteAuthor = cursor.requireLong(QUOTE_AUTHOR)
+ val quoteText = cursor.requireString(QUOTE_BODY)
+ val quoteType = cursor.requireInt(QUOTE_TYPE)
+ val quoteMissing = cursor.requireBoolean(QUOTE_MISSING)
+ val quoteAttachments: List = associatedAttachments.filter { it.isQuote }.toList()
+ val quoteMentions: List = parseQuoteMentions(cursor)
+ val quoteBodyRanges: BodyRangeList? = parseQuoteBodyRanges(cursor)
+ val quote: QuoteModel? = if (quoteId > 0 && quoteAuthor > 0 && (!TextUtils.isEmpty(quoteText) || quoteAttachments.isNotEmpty())) {
+ QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText ?: "", quoteMissing, quoteAttachments, quoteMentions, QuoteModel.Type.fromCode(quoteType), quoteBodyRanges)
+ } else {
+ null
+ }
+
+ val contacts: List