From be4bf27ede1beafda66515836d6b7d6665a60af4 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 19 Mar 2026 12:29:04 -0400 Subject: [PATCH] Remove attachment table JSON join. --- .../voice/VoiceNotePlayerCallback.kt | 5 +- .../conversation/drafts/DraftRepository.kt | 4 +- .../securesms/database/AttachmentTable.kt | 85 +------------ .../securesms/database/MediaTable.kt | 4 +- .../securesms/database/MessageTable.kt | 119 ++++++++---------- .../securesms/jobs/RetryPendingSendsJob.kt | 3 +- .../longmessage/LongMessageRepository.java | 7 +- .../mediapreview/MediaPreviewRepository.kt | 3 +- .../messages/EditMessageProcessor.kt | 4 +- .../archive/StoryArchivePagedDataSource.kt | 27 ++-- .../stories/my/MyStoriesRepository.kt | 13 +- .../stories/viewer/StoryViewerRepository.kt | 5 +- .../viewer/page/StoryViewerPageRepository.kt | 3 +- .../reply/group/StoryGroupReplyDataSource.kt | 9 +- 14 files changed, 106 insertions(+), 185 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlayerCallback.kt b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlayerCallback.kt index a2e2ca5df6..4dae9c1a75 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlayerCallback.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlayerCallback.kt @@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.database.NoSuchMessageException import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messages import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.database.withAttachments import org.thoughtcrime.securesms.util.hasAudio import java.util.concurrent.Executor import java.util.concurrent.Executors @@ -258,7 +259,7 @@ class VoiceNotePlayerCallback(val context: Context, val player: VoiceNotePlayer) private fun loadMediaItemsForSinglePlayback(messageId: Long): List { return try { - listOf(messages.getMessageRecord(messageId)).messageRecordsToVoiceNoteMediaItems() + listOf(messages.getMessageRecord(messageId)).withAttachments().messageRecordsToVoiceNoteMediaItems() } catch (e: NoSuchMessageException) { Log.w(TAG, "Could not find message.", e) emptyList() @@ -268,7 +269,7 @@ class VoiceNotePlayerCallback(val context: Context, val player: VoiceNotePlayer) @WorkerThread private fun loadMediaItemsForConsecutivePlayback(messageId: Long): List { return try { - messages.getMessagesAfterVoiceNoteInclusive(messageId, LIMIT).messageRecordsToVoiceNoteMediaItems() + messages.getMessagesAfterVoiceNoteInclusive(messageId, LIMIT).withAttachments().messageRecordsToVoiceNoteMediaItems() } catch (e: NoSuchMessageException) { Log.w(TAG, "Could not find message.", e) emptyList() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftRepository.kt index 1569d03423..64dfd4487f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftRepository.kt @@ -30,6 +30,7 @@ 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.databaseprotos.BodyRangeList +import org.thoughtcrime.securesms.database.withAttachments import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.keyboard.KeyboardUtil import org.thoughtcrime.securesms.mms.GifSlide @@ -219,7 +220,8 @@ class DraftRepository( private fun loadDraftMessageEditInternal(serialized: String): ConversationMessage? { val messageId = MessageId.deserialize(serialized) - val messageRecord: MessageRecord = SignalDatabase.messages.getMessageRecordOrNull(messageId.id) ?: return null + var messageRecord: MessageRecord = SignalDatabase.messages.getMessageRecordOrNull(messageId.id) ?: return null + messageRecord = messageRecord.withAttachments() val threadRecipient: Recipient = requireNotNull(SignalDatabase.threads.getRecipientForThreadId(messageRecord.threadId)) if (messageRecord.hasTextSlide()) { val textSlide = messageRecord.requireTextSlide() diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt index cf65295ed5..6dac7b4688 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt @@ -20,14 +20,11 @@ import android.content.ContentValues import android.content.Context import android.database.Cursor import android.media.MediaDataSource -import android.text.TextUtils import androidx.annotation.VisibleForTesting import androidx.annotation.WorkerThread import androidx.core.content.contentValuesOf import com.bumptech.glide.Glide import okio.ByteString.Companion.toByteString -import org.json.JSONArray -import org.json.JSONException import org.signal.blurhash.BlurHash import org.signal.core.models.backup.MediaId import org.signal.core.models.backup.MediaName @@ -107,7 +104,6 @@ import org.thoughtcrime.securesms.stickers.StickerLocator import org.thoughtcrime.securesms.util.BitmapDecodingException import org.thoughtcrime.securesms.util.FileUtils import org.thoughtcrime.securesms.util.ImageCompressionUtil -import org.thoughtcrime.securesms.util.JsonUtils.SaneJSONObject import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.video.EncryptedMediaDataSource @@ -122,7 +118,6 @@ import java.io.InputStream import java.security.DigestInputStream import java.security.MessageDigest import java.security.NoSuchAlgorithmException -import java.util.LinkedList import java.util.UUID import kotlin.text.appendLine import kotlin.time.Duration @@ -183,8 +178,6 @@ class AttachmentTable( const val QUOTE_TARGET_CONTENT_TYPE = "quote_target_content_type" const val METADATA_ID = "metadata_id" - const val ATTACHMENT_JSON_ALIAS = "attachment_json" - private const val DIRECTORY = "parts" const val TRANSFER_PROGRESS_DONE = 0 @@ -2327,80 +2320,6 @@ class AttachmentTable( notifyConversationListeners(threadId) } - fun getAttachments(cursor: Cursor): List { - return try { - if (cursor.getColumnIndex(ATTACHMENT_JSON_ALIAS) != -1) { - if (cursor.isNull(ATTACHMENT_JSON_ALIAS)) { - return LinkedList() - } - - val result: MutableList = mutableListOf() - val array = JSONArray(cursor.requireString(ATTACHMENT_JSON_ALIAS)) - - for (i in 0 until array.length()) { - val jsonObject = SaneJSONObject(array.getJSONObject(i)) - - if (!jsonObject.isNull(ID)) { - val contentType = jsonObject.getString(CONTENT_TYPE) - - result += DatabaseAttachment( - attachmentId = AttachmentId(jsonObject.getLong(ID)), - mmsId = jsonObject.getLong(MESSAGE_ID), - hasData = !TextUtils.isEmpty(jsonObject.getString(DATA_FILE)), - hasThumbnail = MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType), - contentType = contentType, - transferProgress = jsonObject.getInt(TRANSFER_STATE), - size = jsonObject.getLong(DATA_SIZE), - fileName = jsonObject.getString(FILE_NAME), - cdn = Cdn.deserialize(jsonObject.getInt(CDN_NUMBER)), - location = jsonObject.getString(REMOTE_LOCATION), - key = jsonObject.getString(REMOTE_KEY), - digest = null, - incrementalDigest = null, - incrementalMacChunkSize = 0, - fastPreflightId = jsonObject.getString(FAST_PREFLIGHT_ID), - voiceNote = jsonObject.getInt(VOICE_NOTE) != 0, - borderless = jsonObject.getInt(BORDERLESS) != 0, - videoGif = jsonObject.getInt(VIDEO_GIF) != 0, - width = jsonObject.getInt(WIDTH), - height = jsonObject.getInt(HEIGHT), - quote = jsonObject.getInt(QUOTE) != 0, - quoteTargetContentType = if (!jsonObject.isNull(QUOTE_TARGET_CONTENT_TYPE)) jsonObject.getString(QUOTE_TARGET_CONTENT_TYPE) else null, - caption = jsonObject.getString(CAPTION), - stickerLocator = if (jsonObject.getInt(STICKER_ID) >= 0) { - StickerLocator( - jsonObject.getString(STICKER_PACK_ID)!!, - jsonObject.getString(STICKER_PACK_KEY)!!, - jsonObject.getInt(STICKER_ID), - jsonObject.getString(STICKER_EMOJI) - ) - } else { - null - }, - blurHash = if (MediaUtil.isAudioType(contentType)) null else BlurHash.parseOrNull(jsonObject.getString(BLUR_HASH)), - audioHash = if (MediaUtil.isAudioType(contentType)) AudioHash.parseOrNull(jsonObject.getString(BLUR_HASH)) else null, - transformProperties = parseTransformProperties(jsonObject.getString(TRANSFORM_PROPERTIES)), - displayOrder = jsonObject.getInt(DISPLAY_ORDER), - uploadTimestamp = jsonObject.getLong(UPLOAD_TIMESTAMP), - dataHash = jsonObject.getString(DATA_HASH_END), - archiveCdn = if (jsonObject.isNull(ARCHIVE_CDN)) null else jsonObject.getInt(ARCHIVE_CDN), - thumbnailRestoreState = ThumbnailRestoreState.deserialize(jsonObject.getInt(THUMBNAIL_RESTORE_STATE)), - archiveTransferState = ArchiveTransferState.deserialize(jsonObject.getInt(ARCHIVE_TRANSFER_STATE)), - uuid = UuidUtil.parseOrNull(jsonObject.getString(ATTACHMENT_UUID)), - metadata = null - ) - } - } - - result - } else { - listOf(getAttachment(cursor)) - } - } catch (e: JSONException) { - throw AssertionError(e) - } - } - fun hasStickerAttachments(): Boolean { return readableDatabase .exists(TABLE_NAME) @@ -3395,7 +3314,7 @@ class AttachmentTable( """ } - private fun getAttachment(cursor: Cursor): DatabaseAttachment { + internal fun getAttachment(cursor: Cursor): DatabaseAttachment { val contentType = cursor.requireString(CONTENT_TYPE) return DatabaseAttachment( @@ -3438,7 +3357,7 @@ class AttachmentTable( } private fun Cursor.readAttachments(): List { - return getAttachments(this) + return listOf(getAttachment(this)) } private fun Cursor.readAttachment(): DatabaseAttachment { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt index 54fedf23f3..acbed7a198 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt @@ -245,10 +245,8 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD companion object { @JvmStatic fun from(cursor: Cursor): MediaRecord { - val attachments = SignalDatabase.attachments.getAttachments(cursor) - return MediaRecord( - attachment = if (attachments.isNotEmpty()) attachments[0] else null, + attachment = SignalDatabase.attachments.getAttachment(cursor), recipientId = RecipientId.from(cursor.requireLong(MessageTable.FROM_RECIPIENT_ID)), threadId = cursor.requireLong(MessageTable.THREAD_ID), threadRecipientId = RecipientId.from(cursor.requireLong(THREAD_RECIPIENT_ID)), diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt index d329bd1e2c..38fe4b2954 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -393,48 +393,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat DELETED_BY ) - 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.ID}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ID}, - '${AttachmentTable.MESSAGE_ID}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID}, - '${AttachmentTable.DATA_SIZE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_SIZE}, - '${AttachmentTable.FILE_NAME}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.FILE_NAME}, - '${AttachmentTable.DATA_FILE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_FILE}, - '${AttachmentTable.THUMBNAIL_FILE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.THUMBNAIL_FILE}, - '${AttachmentTable.CONTENT_TYPE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.CONTENT_TYPE}, - '${AttachmentTable.CDN_NUMBER}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.CDN_NUMBER}, - '${AttachmentTable.REMOTE_LOCATION}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_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.REMOTE_KEY}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_KEY}, - '${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.BLUR_HASH}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.BLUR_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}, - '${AttachmentTable.DATA_HASH_END}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_HASH_END}, - '${AttachmentTable.ARCHIVE_CDN}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ARCHIVE_CDN}, - '${AttachmentTable.THUMBNAIL_RESTORE_STATE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.THUMBNAIL_RESTORE_STATE}, - '${AttachmentTable.ARCHIVE_TRANSFER_STATE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ARCHIVE_TRANSFER_STATE}, - '${AttachmentTable.ATTACHMENT_UUID}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.ATTACHMENT_UUID}, - '${AttachmentTable.QUOTE_TARGET_CONTENT_TYPE}', ${AttachmentTable.TABLE_NAME}.${AttachmentTable.QUOTE_TARGET_CONTENT_TYPE} - ) - ) AS ${AttachmentTable.ATTACHMENT_JSON_ALIAS} - """.toSingleLine() + private val MMS_PROJECTION: Array = MMS_PROJECTION_BASE private const val IS_STORY_CLAUSE = "$STORY_TYPE > 0 AND $DELETED_BY IS NULL AND $STORY_ARCHIVED = 0" private const val IS_ARCHIVED_STORY_CLAUSE = "$STORY_TYPE > 0 AND $DELETED_BY IS NULL AND $STORY_ARCHIVED > 0" @@ -534,6 +493,11 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat return MmsReader(cursor) } + @JvmStatic + fun withAttachmentData(record: MessageRecord): MessageRecord { + return record.withAttachments() + } + private fun getSharedContacts(cursor: Cursor, attachments: List): List { val serializedContacts: String? = cursor.requireString(SHARED_CONTACTS) @@ -666,7 +630,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat fun getExpirationStartedMessages(): Cursor { val where = "$EXPIRE_STARTED > 0" - return rawQueryWithAttachments(where, null) + return queryMessages(where, null) } /** @@ -1418,12 +1382,12 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat whereArgs = buildArgs(threadId) } - return MmsReader(rawQueryWithAttachments(where, whereArgs)) + return MmsReader(queryMessages(where, whereArgs)) } fun getAllOutgoingStories(reverse: Boolean, limit: Int): Reader { val where = "$IS_STORY_CLAUSE AND ($outgoingTypeClause)" - return MmsReader(rawQueryWithAttachments(where, null, reverse, limit.toLong())) + return MmsReader(queryMessages(where, null, reverse, limit.toLong())) } fun markAllIncomingStoriesRead(): List { @@ -1465,7 +1429,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat 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()) + val cursor = queryMessages(where, whereArgs, false, limit.toLong()) return MmsReader(cursor) } @@ -1473,7 +1437,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat val threadId = threads.getThreadIdIfExistsFor(recipientId) val query = "$IS_STORY_CLAUSE AND NOT ($outgoingTypeClause) AND $THREAD_ID = ? AND $VIEWED_COLUMN = ?" val args = buildArgs(threadId, 0) - return MmsReader(rawQueryWithAttachments(query, args, false, limit.toLong())) + return MmsReader(queryMessages(query, args, false, limit.toLong())) } fun getUnreadMissedCallCount(): Long { @@ -1636,7 +1600,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat fun getStoryReplies(parentStoryId: Long): Cursor { val where = "$PARENT_STORY_ID = ?" val whereArgs = buildArgs(parentStoryId) - return rawQueryWithAttachments(where, whereArgs, false, 0) + return queryMessages(where, whereArgs, false, 0) } fun getNumberOfStoryReplies(parentStoryId: Long): Int { @@ -1785,7 +1749,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat val storyClause = if (includeActive) "$STORY_TYPE > 0 AND $DELETED_BY IS NULL" else IS_ARCHIVED_STORY_CLAUSE val where = "$storyClause AND ($outgoingTypeClause)" val order = if (sortNewest) "$TABLE_NAME.$DATE_SENT DESC" else "$TABLE_NAME.$DATE_SENT ASC" - return MmsReader(rawQueryWithAttachments(where, null, orderBy = order, limit = limit.toLong(), offset = offset.toLong())) + return MmsReader(queryMessages(where, null, orderBy = order, limit = limit.toLong(), offset = offset.toLong())) } fun getOldestArchivedStorySentTimestamp(): Long? { @@ -2121,17 +2085,15 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat /** * Note: [reverse] and [orderBy] are mutually exclusive. If you want the order to be reversed, explicitly use 'ASC' or 'DESC' */ - private fun rawQueryWithAttachments(where: String, arguments: Array?, reverse: Boolean = false, limit: Long = 0, offset: Long = 0, orderBy: String = ""): Cursor { + private fun queryMessages(where: String, arguments: Array?, reverse: Boolean = false, limit: Long = 0, offset: Long = 0, orderBy: String = ""): Cursor { val database = databaseHelper.signalReadableDatabase var rawQueryString = """ - SELECT - ${Util.join(MMS_PROJECTION_WITH_ATTACHMENTS, ",")} - FROM - $TABLE_NAME LEFT OUTER JOIN ${AttachmentTable.TABLE_NAME} ON ($TABLE_NAME.$ID = ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID}) - WHERE - $where - GROUP BY - $TABLE_NAME.$ID + SELECT + ${Util.join(MMS_PROJECTION, ",")} + FROM + $TABLE_NAME + WHERE + $where """.toSingleLine() if (orderBy.isNotEmpty()) { @@ -2151,24 +2113,24 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat } private fun internalGetMessage(messageId: Long): Cursor { - return rawQueryWithAttachments(RAW_ID_WHERE, buildArgs(messageId)) + return queryMessages(RAW_ID_WHERE, buildArgs(messageId)) } @Throws(NoSuchMessageException::class) fun getMessageRecord(messageId: Long): MessageRecord { - rawQueryWithAttachments(RAW_ID_WHERE, arrayOf(messageId.toString() + "")).use { cursor -> + queryMessages(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 -> + queryMessages(RAW_ID_WHERE, buildArgs(messageId)).use { cursor -> return MmsReader(cursor).firstOrNull() } } fun getPinnedMessages(threadId: Long, orderByPinned: Boolean): List { - val cursor = rawQueryWithAttachments( + val cursor = queryMessages( where = "$THREAD_ID = ? AND $PINNED_UNTIL > 0", arguments = buildArgs(threadId), reverse = true, @@ -2213,7 +2175,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat fun getMessages(messageIds: Collection): MmsReader { val ids = TextUtils.join(",", messageIds) - return mmsReaderFor(rawQueryWithAttachments("$TABLE_NAME.$ID IN ($ids)", null)) + return mmsReaderFor(queryMessages("$TABLE_NAME.$ID IN ($ids)", null)) } fun getMessageEditHistory(id: Long): MmsReader { @@ -2672,7 +2634,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat @Throws(MmsException::class, NoSuchMessageException::class) fun getOutgoingMessage(messageId: Long): OutgoingMessage { - return rawQueryWithAttachments(RAW_ID_WHERE, arrayOf(messageId.toString())).readToSingleObject { cursor -> + return queryMessages(RAW_ID_WHERE, arrayOf(messageId.toString())).readToSingleObject { cursor -> val associatedAttachments = attachments.getAttachmentsForMessage(messageId) val associatedPoll = polls.getPollForOutgoingMessage(messageId) val mentions = mentions.getMentionsForMessage(messageId) @@ -4236,7 +4198,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat val where = "$TABLE_NAME.$THREAD_ID = ? AND $TABLE_NAME.$DATE_RECEIVED >= ? AND $TABLE_NAME.$SCHEDULED_DATE = -1 AND $TABLE_NAME.$LATEST_REVISION_ID IS NULL" val args = buildArgs(threadId, timestamp) - return mmsReaderFor(rawQueryWithAttachments(where, args, false, limit)).use { reader -> + return mmsReaderFor(queryMessages(where, args, false, limit)).use { reader -> reader.filterNotNull() } } @@ -4896,7 +4858,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat return getConversationSnippetCursor(threadId) .readToSingleObject { cursor -> val id = cursor.requireLong(ID) - messages.getMessageRecord(id) + messages.getMessageRecord(id).withAttachments() } ?: throw NoSuchMessageException("no message") } @@ -6455,7 +6417,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat val mismatches = getMismatchedIdentities(mismatchDocument) val networkFailures = getFailures(networkDocument) - val attachments = attachments.getAttachments(cursor) + val attachments: List = emptyList() val contacts = getSharedContacts(cursor, attachments) val contactAttachments = contacts.mapNotNull { it.avatarAttachment }.toSet() @@ -6465,7 +6427,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat val slideDeck = buildSlideDeck(attachments.filterNot { contactAttachments.contains(it) }.filterNot { previewAttachments.contains(it) }) - val quote = getQuote(cursor) + val quote = getQuote(cursor, attachments) val messageRanges: BodyRangeList? = if (messageRangesData != null) { try { @@ -6563,7 +6525,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat return emptySet() } - private fun getQuote(cursor: Cursor): Quote? { + private fun getQuote(cursor: Cursor, attachments: List): Quote? { val quoteId = cursor.requireLong(QUOTE_ID) val quoteAuthor = cursor.requireLong(QUOTE_AUTHOR) var quoteText: CharSequence? = cursor.requireString(QUOTE_BODY) @@ -6572,7 +6534,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat var quoteMentions = parseQuoteMentions(cursor) val bodyRanges = parseQuoteBodyRanges(cursor) - val attachments = attachments.getAttachments(cursor) val quoteAttachments: List = attachments.filter { it.quote } val quoteDeck = SlideDeck(quoteAttachments) @@ -6632,3 +6593,21 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat } } } + +fun MessageRecord.withAttachments(): MessageRecord { + if (this !is MmsMessageRecord) return this + val fetchedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(id) + return if (fetchedAttachments.isNotEmpty()) withAttachments(fetchedAttachments) else this +} + +fun List.withAttachments(): List { + if (isEmpty()) return this + val allAttachments = SignalDatabase.attachments.getAttachmentsForMessages(map { it.id }) + return map { record -> + if (record is MmsMessageRecord) { + allAttachments[record.id]?.let { record.withAttachments(it) } ?: record + } else { + record + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetryPendingSendsJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetryPendingSendsJob.kt index b4ad8fe1e4..54092febb0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetryPendingSendsJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetryPendingSendsJob.kt @@ -8,7 +8,6 @@ package org.thoughtcrime.securesms.jobs import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.MessageId -import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobmanager.Job import kotlin.time.Duration.Companion.days @@ -52,7 +51,7 @@ class RetryPendingSendsJob private constructor(parameters: Parameters) : Job(par reader.forEach { message -> val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(message.threadId) if (threadRecipient != null) { - val hasMedia = (message as? MmsMessageRecord)?.slideDeck?.slides?.isNotEmpty() == true + val hasMedia = SignalDatabase.attachments.getAttachmentsForMessage(message.id).isNotEmpty() Log.d(TAG, "[${message.dateSent}] Found pending message MessageId::${message.id}, enqueueing second check job") AppDependencies.jobManager.add(RetryPendingSendSecondCheckJob(MessageId(message.id), threadRecipient, hasMedia)) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/longmessage/LongMessageRepository.java b/app/src/main/java/org/thoughtcrime/securesms/longmessage/LongMessageRepository.java index ba878b5d2c..51192a9752 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/longmessage/LongMessageRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/longmessage/LongMessageRepository.java @@ -11,6 +11,7 @@ import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.conversation.ConversationMessage; import org.thoughtcrime.securesms.database.MessageTable; import org.thoughtcrime.securesms.database.SignalDatabase; +import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import java.util.Optional; @@ -45,7 +46,11 @@ class LongMessageRepository { @WorkerThread private Optional getMmsMessage(@NonNull MessageTable mmsDatabase, long messageId) { try (Cursor cursor = mmsDatabase.getMessageCursor(messageId)) { - return Optional.ofNullable((MmsMessageRecord) MessageTable.mmsReaderFor(cursor).getNext()); + MessageRecord record = MessageTable.mmsReaderFor(cursor).getNext(); + if (record != null) { + record = MessageTable.withAttachmentData(record); + } + return Optional.ofNullable((MmsMessageRecord) record); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt index 568449c5a2..f3def7f4c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt @@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SignalDatabase.Companion.media import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.database.withAttachments import org.thoughtcrime.securesms.jobs.MultiDeviceDeleteSyncJob import org.thoughtcrime.securesms.longmessage.resolveBody import org.thoughtcrime.securesms.recipients.RecipientId @@ -85,7 +86,7 @@ class MediaPreviewRepository { } val messageIds = mediaRecords.mapNotNull { it.attachment?.mmsId }.toSet() - val messages: Map = SignalDatabase.messages.getMessages(messageIds) + val messages: Map = SignalDatabase.messages.getMessages(messageIds).toList().withAttachments() .map { it as MmsMessageRecord } .associate { it.id to it.resolveBody(context).getDisplayBody(context) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/EditMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/EditMessageProcessor.kt index ba4ca917a8..1f0ca1d1fa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/EditMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/EditMessageProcessor.kt @@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList import org.thoughtcrime.securesms.database.model.toBodyRangeList +import org.thoughtcrime.securesms.database.withAttachments import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob @@ -69,7 +70,8 @@ object EditMessageProcessor { val isMediaMessage = message.isMediaMessage val groupId: GroupId.V2? = message.groupV2?.groupId - val originalMessage = targetMessage.originalMessageId?.let { SignalDatabase.messages.getMessageRecord(it.id) } ?: targetMessage + val originalMessageWithoutAttachments = targetMessage.originalMessageId?.let { SignalDatabase.messages.getMessageRecord(it.id) } ?: targetMessage + val originalMessage = originalMessageWithoutAttachments.withAttachments() val validTiming = MessageConstraintsUtil.isValidEditMessageReceive(originalMessage, senderRecipient, envelope.serverTimestamp!!) val validAuthor = senderRecipient.id == originalMessage.fromRecipient.id val validGroup = groupId == targetThreadRecipient.groupId.orNull() diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/archive/StoryArchivePagedDataSource.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/archive/StoryArchivePagedDataSource.kt index 84b8fc5caf..7dc6a1b2d2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/archive/StoryArchivePagedDataSource.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/archive/StoryArchivePagedDataSource.kt @@ -4,6 +4,7 @@ import org.signal.paging.PagedDataSource import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.StoryType +import org.thoughtcrime.securesms.database.withAttachments import org.thoughtcrime.securesms.keyvalue.SignalStore class StoryArchivePagedDataSource( @@ -17,25 +18,29 @@ class StoryArchivePagedDataSource( } override fun load(start: Int, length: Int, totalSize: Int, cancellationSignal: PagedDataSource.CancellationSignal): List { - return SignalDatabase.messages.getArchiveScreenStoriesPage(includeActive, sortNewest, start, length).use { reader -> + val rawRecords = SignalDatabase.messages.getArchiveScreenStoriesPage(includeActive, sortNewest, start, length).use { reader -> reader.mapNotNull { record -> if (cancellationSignal.isCanceled) return@use emptyList() - val mmsRecord = record as? MmsMessageRecord - ArchivedStoryItem( - messageId = record.id, - dateSent = record.dateSent, - thumbnailUri = mmsRecord?.slideDeck?.thumbnailSlide?.uri, - blurHash = mmsRecord?.slideDeck?.thumbnailSlide?.placeholderBlur, - storyType = mmsRecord?.storyType ?: StoryType.NONE, - body = record.body - ) + record } } + + return rawRecords.withAttachments().map { record -> + val mmsRecord = record as? MmsMessageRecord + ArchivedStoryItem( + messageId = record.id, + dateSent = record.dateSent, + thumbnailUri = mmsRecord?.slideDeck?.thumbnailSlide?.uri, + blurHash = mmsRecord?.slideDeck?.thumbnailSlide?.placeholderBlur, + storyType = mmsRecord?.storyType ?: StoryType.NONE, + body = record.body + ) + } } override fun load(key: Long): ArchivedStoryItem? { val record = SignalDatabase.messages.getMessageRecordOrNull(key) ?: return null - val mmsRecord = record as? MmsMessageRecord + val mmsRecord = record.withAttachments() as? MmsMessageRecord return ArchivedStoryItem( messageId = record.id, dateSent = record.dateSent, diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesRepository.kt index acbabe3902..e486550acc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesRepository.kt @@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.database.GroupReceiptTable import org.thoughtcrime.securesms.database.RxDatabaseObserver import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.database.withAttachments import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.sms.MessageSender @@ -27,14 +28,20 @@ class MyStoriesRepository(context: Context) { .conversationList .toObservable() .map { - val storiesMap = mutableMapOf>() + val allRecords = mutableListOf() SignalDatabase.messages.getAllOutgoingStories(true, -1).use { for (messageRecord in it) { - val currentList = storiesMap[messageRecord.toRecipient] ?: emptyList() - storiesMap[messageRecord.toRecipient] = (currentList + messageRecord) + allRecords.add(messageRecord) } } + val withAttachments = allRecords.withAttachments() + val storiesMap = mutableMapOf>() + for (record in withAttachments) { + val currentList = storiesMap[record.toRecipient] ?: emptyList() + storiesMap[record.toRecipient] = (currentList + record) + } + storiesMap.toSortedMap(MyStoryBiasComparator()).map { (r, m) -> createDistributionSet(r, m) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerRepository.kt index 273356fc66..ee88c44cc9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerRepository.kt @@ -6,6 +6,7 @@ import org.thoughtcrime.securesms.database.MessageTable import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.DistributionListId import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.database.withAttachments import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId @@ -17,7 +18,7 @@ open class StoryViewerRepository { fun getFirstStory(recipientId: RecipientId, storyId: Long): Single { return if (storyId > 0) { Single.fromCallable { - SignalDatabase.messages.getMessageRecord(storyId) as MmsMessageRecord + SignalDatabase.messages.getMessageRecord(storyId).withAttachments() as MmsMessageRecord } } else { Single.fromCallable { @@ -32,7 +33,7 @@ open class StoryViewerRepository { SignalDatabase.messages.getAllStoriesFor(recipientId, 1) } } - reader.use { it.iterator().next() } as MmsMessageRecord + reader.use { it.iterator().next() }.withAttachments() as MmsMessageRecord } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt index ae18f1eba5..39bdc83cfd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt @@ -19,6 +19,7 @@ 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.databaseprotos.StoryTextPost +import org.thoughtcrime.securesms.database.withAttachments import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobs.MultiDeviceViewedUpdateJob import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob @@ -58,7 +59,7 @@ open class StoryViewerPageRepository(context: Context, private val storyViewStat recipient.isMyStory && it.toRecipient.isGroup } - emitter.onNext(results) + emitter.onNext(results.withAttachments()) } val storyObserver = DatabaseObserver.Observer { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyDataSource.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyDataSource.kt index 9a15b9f016..baed8ff790 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyDataSource.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyDataSource.kt @@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.database.MessageTypes import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.database.withAttachments import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.recipients.Recipient @@ -16,20 +17,20 @@ class StoryGroupReplyDataSource(private val parentStoryId: Long) : PagedDataSour } override fun load(start: Int, length: Int, totalSize: Int, cancellationSignal: PagedDataSource.CancellationSignal): MutableList { - val results: MutableList = ArrayList(length) + val rawRecords = mutableListOf() SignalDatabase.messages.getStoryReplies(parentStoryId).use { cursor -> cursor.moveToPosition(start - 1) val mmsReader = MessageTable.MmsReader(cursor) while (cursor.moveToNext() && cursor.position < start + length) { - results.add(readRowFromRecord(mmsReader.getCurrent() as MmsMessageRecord)) + rawRecords.add(mmsReader.getCurrent() as MmsMessageRecord) } } - return results + return rawRecords.withAttachments().map { readRowFromRecord(it as MmsMessageRecord) }.toMutableList() } override fun load(key: MessageId): ReplyBody { - return readRowFromRecord(SignalDatabase.messages.getMessageRecord(key.id) as MmsMessageRecord) + return readRowFromRecord(SignalDatabase.messages.getMessageRecord(key.id).withAttachments() as MmsMessageRecord) } override fun getKey(data: ReplyBody): MessageId {