Merge MediaMmsMessageRecord into MmsMessageRecord.

This commit is contained in:
Greyson Parrelli
2023-11-12 12:47:26 -05:00
parent 5f6fa73be9
commit 2f52664820
41 changed files with 407 additions and 473 deletions

View File

@@ -89,7 +89,6 @@ 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
@@ -964,7 +963,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
}
}
fun insertEditMessageInbox(mediaMessage: IncomingMessage, targetMessage: MediaMmsMessageRecord): Optional<InsertResult> {
fun insertEditMessageInbox(mediaMessage: IncomingMessage, targetMessage: MmsMessageRecord): Optional<InsertResult> {
val insertResult = insertMessageInbox(retrieved = mediaMessage, editedMessage = targetMessage, notifyObservers = false)
if (insertResult.isPresent) {
@@ -1924,7 +1923,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
fun markAsRemoteDelete(targetMessage: MessageRecord) {
writableDatabase.withinTransaction { db ->
if (targetMessage.isEditMessage) {
val latestRevisionId = (targetMessage as? MediaMmsMessageRecord)?.latestRevisionId?.id ?: targetMessage.id
val latestRevisionId = (targetMessage as? MmsMessageRecord)?.latestRevisionId?.id ?: targetMessage.id
markAsRemoteDeleteInternal(latestRevisionId)
getPreviousEditIds(latestRevisionId).map { id ->
db.update(TABLE_NAME)
@@ -2445,7 +2444,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
fun insertMessageInbox(
retrieved: IncomingMessage,
candidateThreadId: Long = -1,
editedMessage: MediaMmsMessageRecord? = null,
editedMessage: MmsMessageRecord? = null,
notifyObservers: Boolean = true
): Optional<InsertResult> {
val type = retrieved.toMessageType()
@@ -2924,8 +2923,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
.where("$ID_WHERE OR $LATEST_REVISION_ID = ?", message.messageToEdit, message.messageToEdit)
.run()
val textAttachments = (editedMessage as? MediaMmsMessageRecord)?.slideDeck?.asAttachments()?.filter { it.contentType == MediaUtil.LONG_TEXT }?.mapNotNull { (it as? DatabaseAttachment)?.attachmentId?.rowId } ?: emptyList()
val linkPreviewAttachments = (editedMessage as? MediaMmsMessageRecord)?.linkPreviews?.mapNotNull { it.attachmentId?.rowId } ?: emptyList()
val textAttachments = (editedMessage as? MmsMessageRecord)?.slideDeck?.asAttachments()?.filter { it.contentType == MediaUtil.LONG_TEXT }?.mapNotNull { (it as? DatabaseAttachment)?.attachmentId?.rowId } ?: emptyList()
val linkPreviewAttachments = (editedMessage as? MmsMessageRecord)?.linkPreviews?.mapNotNull { it.attachmentId?.rowId } ?: emptyList()
val excludeIds = HashSet<Long>()
excludeIds += textAttachments
excludeIds += linkPreviewAttachments
@@ -4933,7 +4932,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
return MessageId(cursor.requireLong(ID))
}
private fun getMediaMmsMessageRecord(cursor: Cursor): MediaMmsMessageRecord {
private fun getMediaMmsMessageRecord(cursor: Cursor): MmsMessageRecord {
val id = cursor.requireLong(ID)
val dateSent = cursor.requireLong(DATE_SENT)
val dateReceived = cursor.requireLong(DATE_RECEIVED)
@@ -5012,7 +5011,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
null
}
return MediaMmsMessageRecord(
return MmsMessageRecord(
id,
fromRecipient,
fromDeviceId,

View File

@@ -18,7 +18,7 @@ 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.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.databaseprotos.CryptoValue;
@@ -436,8 +436,8 @@ public final class PaymentTable extends DatabaseTable implements RecipientIdData
public @NonNull MessageRecord updateMessageWithPayment(@NonNull MessageRecord record) {
if (record.isPaymentNotification()) {
Payment payment = getPayment(UuidUtil.parseOrThrow(record.getBody()));
if (payment != null && record instanceof MediaMmsMessageRecord) {
return ((MediaMmsMessageRecord) record).withPayment(payment);
if (payment != null && record instanceof MmsMessageRecord) {
return ((MmsMessageRecord) record).withPayment(payment);
} else {
throw new AssertionError("Payment not found for message");
}

View File

@@ -38,7 +38,6 @@ import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messageLog
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messages
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients
import org.thoughtcrime.securesms.database.ThreadBodyUtil.ThreadBody
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.ThreadRecord
@@ -1699,7 +1698,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
return null
}
val slideDeck: SlideDeck = (record as MediaMmsMessageRecord).slideDeck
val slideDeck: SlideDeck = (record as MmsMessageRecord).slideDeck
val thumbnail = Optional.ofNullable(slideDeck.thumbnailSlide)
.or(Optional.ofNullable(slideDeck.stickerSlide))
.orElse(null)

View File

@@ -1,314 +0,0 @@
/**
* Copyright (C) 2012 Moxie Marlinspike
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database.model;
import android.content.Context;
import android.text.SpannableString;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.content.ContextCompat;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.database.CallTable;
import org.thoughtcrime.securesms.database.MessageTable;
import org.thoughtcrime.securesms.database.MessageTable.Status;
import org.thoughtcrime.securesms.database.MessageTypes;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.payments.Payment;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.signalservice.api.payments.FormatterOptions;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* Represents the message record model for MMS messages that contain
* media (ie: they've been downloaded).
*
* @author Moxie Marlinspike
*
*/
public class MediaMmsMessageRecord extends MmsMessageRecord {
private final static String TAG = Log.tag(MediaMmsMessageRecord.class);
private final boolean mentionsSelf;
private final BodyRangeList messageRanges;
private final Payment payment;
private final CallTable.Call call;
private final long scheduledDate;
private final MessageId latestRevisionId;
public MediaMmsMessageRecord(long id,
Recipient fromRecipient,
int fromDeviceId,
Recipient toRecipient,
long dateSent,
long dateReceived,
long dateServer,
int deliveryReceiptCount,
long threadId,
String body,
@NonNull SlideDeck slideDeck,
long mailbox,
Set<IdentityKeyMismatch> mismatches,
Set<NetworkFailure> failures,
int subscriptionId,
long expiresIn,
long expireStarted,
boolean viewOnce,
int readReceiptCount,
@Nullable Quote quote,
@NonNull List<Contact> contacts,
@NonNull List<LinkPreview> linkPreviews,
boolean unidentified,
@NonNull List<ReactionRecord> reactions,
boolean remoteDelete,
boolean mentionsSelf,
long notifiedTimestamp,
int viewedReceiptCount,
long receiptTimestamp,
@Nullable BodyRangeList messageRanges,
@NonNull StoryType storyType,
@Nullable ParentStoryId parentStoryId,
@Nullable GiftBadge giftBadge,
@Nullable Payment payment,
@Nullable CallTable.Call call,
long scheduledDate,
@Nullable MessageId latestRevisionId,
@Nullable MessageId originalMessageId,
int revisionNumber)
{
super(id, body, fromRecipient, fromDeviceId, toRecipient, dateSent,
dateReceived, dateServer, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures,
subscriptionId, expiresIn, expireStarted, viewOnce, slideDeck,
readReceiptCount, quote, contacts, linkPreviews, unidentified, reactions, remoteDelete, notifiedTimestamp, viewedReceiptCount, receiptTimestamp,
storyType, parentStoryId, giftBadge, originalMessageId, revisionNumber);
this.mentionsSelf = mentionsSelf;
this.messageRanges = messageRanges;
this.payment = payment;
this.call = call;
this.scheduledDate = scheduledDate;
this.latestRevisionId = latestRevisionId;
}
@Override
public boolean hasSelfMention() {
return mentionsSelf;
}
@Override
public boolean isMmsNotification() {
return false;
}
@Override
@WorkerThread
public SpannableString getDisplayBody(@NonNull Context context) {
if (MessageTypes.isChatSessionRefresh(type)) {
return emphasisAdded(context.getString(R.string.MmsMessageRecord_bad_encrypted_mms_message));
} else if (MessageTypes.isDuplicateMessageType(type)) {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message));
} else if (MessageTypes.isNoRemoteSessionType(type)) {
return emphasisAdded(context.getString(R.string.MmsMessageRecord_mms_message_encrypted_for_non_existing_session));
} else if (isLegacyMessage()) {
return emphasisAdded(context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported));
} else if (isPaymentNotification() && payment != null) {
return new SpannableString(context.getString(R.string.MessageRecord__payment_s, payment.getAmount().toString(FormatterOptions.defaults())));
}
return super.getDisplayBody(context);
}
@Override
public @Nullable UpdateDescription getUpdateDisplayBody(@NonNull Context context, @Nullable Consumer<RecipientId> recipientClickHandler) {
if (isCallLog() && call != null && !(call.getType() == CallTable.Type.GROUP_CALL)) {
boolean accepted = call.getEvent() == CallTable.Event.ACCEPTED;
String callDateString = getCallDateString(context);
if (call.getDirection() == CallTable.Direction.OUTGOING) {
if (call.getType() == CallTable.Type.AUDIO_CALL) {
int updateString = accepted ? R.string.MessageRecord_outgoing_voice_call : R.string.MessageRecord_unanswered_voice_call;
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), R.drawable.ic_update_audio_call_outgoing_16);
} else {
int updateString = accepted ? R.string.MessageRecord_outgoing_video_call : R.string.MessageRecord_unanswered_video_call;
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), R.drawable.ic_update_video_call_outgoing_16);
}
} else {
boolean isVideoCall = call.getType() == CallTable.Type.VIDEO_CALL;
boolean isMissed = call.getEvent() == CallTable.Event.MISSED;
if (accepted) {
int updateString = isVideoCall ? R.string.MessageRecord_incoming_video_call : R.string.MessageRecord_incoming_voice_call;
int icon = isVideoCall ? R.drawable.ic_update_video_call_incoming_16 : R.drawable.ic_update_audio_call_incoming_16;
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), icon);
} else if (isMissed) {
return isVideoCall ? staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(R.string.MessageRecord_missed_video_call), callDateString), R.drawable.ic_update_video_call_missed_16, ContextCompat.getColor(context, R.color.core_red_shade), ContextCompat.getColor(context, R.color.core_red))
: staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(R.string.MessageRecord_missed_voice_call), callDateString), R.drawable.ic_update_audio_call_missed_16, ContextCompat.getColor(context, R.color.core_red_shade), ContextCompat.getColor(context, R.color.core_red));
} else {
return isVideoCall ? staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(R.string.MessageRecord_you_declined_a_video_call), callDateString), R.drawable.ic_update_video_call_incoming_16)
: staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(R.string.MessageRecord_you_declined_a_voice_call), callDateString), R.drawable.ic_update_audio_call_incoming_16);
}
}
}
return super.getUpdateDisplayBody(context, recipientClickHandler);
}
@Override
public @Nullable BodyRangeList getMessageRanges() {
return messageRanges;
}
@Override
public @NonNull BodyRangeList requireMessageRanges() {
return Objects.requireNonNull(messageRanges);
}
public @Nullable Payment getPayment() {
return payment;
}
public @Nullable CallTable.Call getCall() {
return call;
}
public long getScheduledDate() {
return scheduledDate;
}
public @Nullable MessageId getLatestRevisionId() {
return latestRevisionId;
}
public @NonNull MediaMmsMessageRecord withReactions(@NonNull List<ReactionRecord> reactions) {
return new MediaMmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
getReadReceiptCount(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), reactions, isRemoteDelete(), mentionsSelf,
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber());
}
public @NonNull MediaMmsMessageRecord withoutQuote() {
return new MediaMmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
getReadReceiptCount(), null, getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber());
}
public @NonNull MediaMmsMessageRecord withAttachments(@NonNull List<DatabaseAttachment> attachments) {
Map<AttachmentId, DatabaseAttachment> attachmentIdMap = new HashMap<>();
for (DatabaseAttachment attachment : attachments) {
attachmentIdMap.put(attachment.getAttachmentId(), attachment);
}
List<Contact> contacts = updateContacts(getSharedContacts(), attachmentIdMap);
Set<Attachment> contactAttachments = contacts.stream().map(Contact::getAvatarAttachment).filter(Objects::nonNull).collect(Collectors.toSet());
List<LinkPreview> linkPreviews = updateLinkPreviews(getLinkPreviews(), attachmentIdMap);
Set<Attachment> linkPreviewAttachments = linkPreviews.stream().map(LinkPreview::getThumbnail).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toSet());
Quote quote = updateQuote(getQuote(), attachments);
List<DatabaseAttachment> slideAttachments = attachments.stream().filter(a -> !contactAttachments.contains(a)).filter(a -> !linkPreviewAttachments.contains(a)).collect(Collectors.toList());
SlideDeck slideDeck = MessageTable.MmsReader.buildSlideDeck(slideAttachments);
return new MediaMmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), slideDeck,
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
getReadReceiptCount(), quote, contacts, linkPreviews, isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber());
}
public @NonNull MediaMmsMessageRecord withPayment(@NonNull Payment payment) {
return new MediaMmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
getReadReceiptCount(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), payment, getCall(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber());
}
public @NonNull MediaMmsMessageRecord withCall(@Nullable CallTable.Call call) {
return new MediaMmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
getReadReceiptCount(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), call, getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber());
}
private static @NonNull List<Contact> updateContacts(@NonNull List<Contact> contacts, @NonNull Map<AttachmentId, DatabaseAttachment> attachmentIdMap) {
return contacts.stream()
.map(contact -> {
if (contact.getAvatar() != null) {
DatabaseAttachment attachment = attachmentIdMap.get(contact.getAvatar().getAttachmentId());
Contact.Avatar updatedAvatar = new Contact.Avatar(contact.getAvatar().getAttachmentId(),
attachment,
contact.getAvatar().isProfile());
return new Contact(contact, updatedAvatar);
} else {
return contact;
}
})
.collect(Collectors.toList());
}
private static @NonNull List<LinkPreview> updateLinkPreviews(@NonNull List<LinkPreview> linkPreviews, @NonNull Map<AttachmentId, DatabaseAttachment> attachmentIdMap) {
return linkPreviews.stream()
.map(preview -> {
if (preview.getAttachmentId() != null) {
DatabaseAttachment attachment = attachmentIdMap.get(preview.getAttachmentId());
if (attachment != null) {
return new LinkPreview(preview.getUrl(), preview.getTitle(), preview.getDescription(), preview.getDate(), attachment);
} else {
return preview;
}
} else {
return preview;
}
})
.collect(Collectors.toList());
}
private static @Nullable Quote updateQuote(@Nullable Quote quote, @NonNull List<DatabaseAttachment> attachments) {
if (quote == null) {
return null;
}
List<DatabaseAttachment> quoteAttachments = attachments.stream().filter(Attachment::isQuote).collect(Collectors.toList());
return quote.withAttachment(new SlideDeck(quoteAttachments));
}
}

View File

@@ -735,8 +735,8 @@ public abstract class MessageRecord extends DisplayRecord {
}
public boolean isLatestRevision() {
if (this instanceof MediaMmsMessageRecord) {
return ((MediaMmsMessageRecord) this).getLatestRevisionId() == null;
if (this instanceof MmsMessageRecord) {
return ((MmsMessageRecord) this).getLatestRevisionId() == null;
}
return true;
}

View File

@@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.payments.Payment
fun MessageRecord.withReactions(reactions: List<ReactionRecord>): MessageRecord {
return if (this is MediaMmsMessageRecord) {
return if (this is MmsMessageRecord) {
this.withReactions(reactions)
} else {
this
@@ -18,14 +18,14 @@ fun MessageRecord.withReactions(reactions: List<ReactionRecord>): MessageRecord
}
fun MessageRecord.withAttachments(attachments: List<DatabaseAttachment>): MessageRecord {
return if (this is MediaMmsMessageRecord) {
return if (this is MmsMessageRecord) {
this.withAttachments(attachments)
} else {
this
}
}
fun MessageRecord.withPayment(payment: Payment): MessageRecord {
return if (this is MediaMmsMessageRecord) {
return if (this is MmsMessageRecord) {
this.withPayment(payment)
} else {
this
@@ -33,7 +33,7 @@ fun MessageRecord.withPayment(payment: Payment): MessageRecord {
}
fun MessageRecord.withCall(call: CallTable.Call): MessageRecord {
return if (this is MediaMmsMessageRecord) {
return if (this is MmsMessageRecord) {
this.withCall(call)
} else {
this

View File

@@ -1,63 +1,138 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.model;
import android.content.Context;
import android.text.SpannableString;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.content.ContextCompat;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.database.CallTable;
import org.thoughtcrime.securesms.database.MessageTable;
import org.thoughtcrime.securesms.database.MessageTable.Status;
import org.thoughtcrime.securesms.database.MessageTypes;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.payments.Payment;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.signalservice.api.payments.FormatterOptions;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
public abstract class MmsMessageRecord extends MessageRecord {
/**
* Represents the message record model for MMS messages that contain
* media (ie: they've been downloaded).
*
* @author Moxie Marlinspike
*
*/
private final @NonNull SlideDeck slideDeck;
private final @Nullable Quote quote;
private final @NonNull List<Contact> contacts = new LinkedList<>();
private final @NonNull List<LinkPreview> linkPreviews = new LinkedList<>();
private final @NonNull StoryType storyType;
private final @Nullable ParentStoryId parentStoryId;
private final @Nullable GiftBadge giftBadge;
public class MmsMessageRecord extends MessageRecord {
private final static String TAG = Log.tag(MmsMessageRecord.class);
private final boolean viewOnce;
private final SlideDeck slideDeck;
private final Quote quote;
private final List<Contact> contacts = new LinkedList<>();
private final List<LinkPreview> linkPreviews = new LinkedList<>();
private final StoryType storyType;
private final ParentStoryId parentStoryId;
private final GiftBadge giftBadge;
private final boolean viewOnce;
private final boolean mentionsSelf;
private final BodyRangeList messageRanges;
private final Payment payment;
private final CallTable.Call call;
private final long scheduledDate;
private final MessageId latestRevisionId;
MmsMessageRecord(long id, String body, Recipient fromRecipient, int fromDeviceId, Recipient toRecipient, long dateSent,
long dateReceived, long dateServer, long threadId, int deliveryStatus, int deliveryReceiptCount,
long type, Set<IdentityKeyMismatch> mismatches,
Set<NetworkFailure> networkFailures, int subscriptionId, long expiresIn,
long expireStarted, boolean viewOnce,
@NonNull SlideDeck slideDeck, int readReceiptCount,
@Nullable Quote quote, @NonNull List<Contact> contacts,
@NonNull List<LinkPreview> linkPreviews, boolean unidentified,
@NonNull List<ReactionRecord> reactions, boolean remoteDelete, long notifiedTimestamp,
int viewedReceiptCount, long receiptTimestamp, @NonNull StoryType storyType,
@Nullable ParentStoryId parentStoryId, @Nullable GiftBadge giftBadge, @Nullable MessageId originalMessageId,
int revisionNumber)
public MmsMessageRecord(long id,
Recipient fromRecipient,
int fromDeviceId,
Recipient toRecipient,
long dateSent,
long dateReceived,
long dateServer,
int deliveryReceiptCount,
long threadId,
String body,
@NonNull SlideDeck slideDeck,
long mailbox,
Set<IdentityKeyMismatch> mismatches,
Set<NetworkFailure> failures,
int subscriptionId,
long expiresIn,
long expireStarted,
boolean viewOnce,
int readReceiptCount,
@Nullable Quote quote,
@NonNull List<Contact> contacts,
@NonNull List<LinkPreview> linkPreviews,
boolean unidentified,
@NonNull List<ReactionRecord> reactions,
boolean remoteDelete,
boolean mentionsSelf,
long notifiedTimestamp,
int viewedReceiptCount,
long receiptTimestamp,
@Nullable BodyRangeList messageRanges,
@NonNull StoryType storyType,
@Nullable ParentStoryId parentStoryId,
@Nullable GiftBadge giftBadge,
@Nullable Payment payment,
@Nullable CallTable.Call call,
long scheduledDate,
@Nullable MessageId latestRevisionId,
@Nullable MessageId originalMessageId,
int revisionNumber)
{
super(id, body, fromRecipient, fromDeviceId, toRecipient,
dateSent, dateReceived, dateServer, threadId, deliveryStatus, deliveryReceiptCount,
type, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, readReceiptCount,
dateSent, dateReceived, dateServer, threadId, Status.STATUS_NONE, deliveryReceiptCount,
mailbox, mismatches, failures, subscriptionId, expiresIn, expireStarted, readReceiptCount,
unidentified, reactions, remoteDelete, notifiedTimestamp, viewedReceiptCount, receiptTimestamp, originalMessageId, revisionNumber);
this.slideDeck = slideDeck;
this.quote = quote;
this.viewOnce = viewOnce;
this.storyType = storyType;
this.parentStoryId = parentStoryId;
this.giftBadge = giftBadge;
this.slideDeck = slideDeck;
this.quote = quote;
this.viewOnce = viewOnce;
this.storyType = storyType;
this.parentStoryId = parentStoryId;
this.giftBadge = giftBadge;
this.mentionsSelf = mentionsSelf;
this.messageRanges = messageRanges;
this.payment = payment;
this.call = call;
this.scheduledDate = scheduledDate;
this.latestRevisionId = latestRevisionId;
this.contacts.addAll(contacts);
this.linkPreviews.addAll(linkPreviews);
}
@Override
public boolean isMms() {
return true;
@@ -111,4 +186,191 @@ public abstract class MmsMessageRecord extends MessageRecord {
public @Nullable GiftBadge getGiftBadge() {
return giftBadge;
}
@Override
public boolean hasSelfMention() {
return mentionsSelf;
}
@Override
public boolean isMmsNotification() {
return false;
}
@Override
@WorkerThread
public SpannableString getDisplayBody(@NonNull Context context) {
if (MessageTypes.isChatSessionRefresh(type)) {
return emphasisAdded(context.getString(R.string.MmsMessageRecord_bad_encrypted_mms_message));
} else if (MessageTypes.isDuplicateMessageType(type)) {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message));
} else if (MessageTypes.isNoRemoteSessionType(type)) {
return emphasisAdded(context.getString(R.string.MmsMessageRecord_mms_message_encrypted_for_non_existing_session));
} else if (isLegacyMessage()) {
return emphasisAdded(context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported));
} else if (isPaymentNotification() && payment != null) {
return new SpannableString(context.getString(R.string.MessageRecord__payment_s, payment.getAmount().toString(FormatterOptions.defaults())));
}
return super.getDisplayBody(context);
}
@Override
public @Nullable UpdateDescription getUpdateDisplayBody(@NonNull Context context, @Nullable Consumer<RecipientId> recipientClickHandler) {
if (isCallLog() && call != null && !(call.getType() == CallTable.Type.GROUP_CALL)) {
boolean accepted = call.getEvent() == CallTable.Event.ACCEPTED;
String callDateString = getCallDateString(context);
if (call.getDirection() == CallTable.Direction.OUTGOING) {
if (call.getType() == CallTable.Type.AUDIO_CALL) {
int updateString = accepted ? R.string.MessageRecord_outgoing_voice_call : R.string.MessageRecord_unanswered_voice_call;
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), R.drawable.ic_update_audio_call_outgoing_16);
} else {
int updateString = accepted ? R.string.MessageRecord_outgoing_video_call : R.string.MessageRecord_unanswered_video_call;
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), R.drawable.ic_update_video_call_outgoing_16);
}
} else {
boolean isVideoCall = call.getType() == CallTable.Type.VIDEO_CALL;
boolean isMissed = call.getEvent() == CallTable.Event.MISSED;
if (accepted) {
int updateString = isVideoCall ? R.string.MessageRecord_incoming_video_call : R.string.MessageRecord_incoming_voice_call;
int icon = isVideoCall ? R.drawable.ic_update_video_call_incoming_16 : R.drawable.ic_update_audio_call_incoming_16;
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), icon);
} else if (isMissed) {
return isVideoCall ? staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(R.string.MessageRecord_missed_video_call), callDateString), R.drawable.ic_update_video_call_missed_16, ContextCompat.getColor(context, R.color.core_red_shade), ContextCompat.getColor(context, R.color.core_red))
: staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(R.string.MessageRecord_missed_voice_call), callDateString), R.drawable.ic_update_audio_call_missed_16, ContextCompat.getColor(context, R.color.core_red_shade), ContextCompat.getColor(context, R.color.core_red));
} else {
return isVideoCall ? staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(R.string.MessageRecord_you_declined_a_video_call), callDateString), R.drawable.ic_update_video_call_incoming_16)
: staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(R.string.MessageRecord_you_declined_a_voice_call), callDateString), R.drawable.ic_update_audio_call_incoming_16);
}
}
}
return super.getUpdateDisplayBody(context, recipientClickHandler);
}
@Override
public @Nullable BodyRangeList getMessageRanges() {
return messageRanges;
}
@Override
public @NonNull BodyRangeList requireMessageRanges() {
return Objects.requireNonNull(messageRanges);
}
public @Nullable Payment getPayment() {
return payment;
}
public @Nullable CallTable.Call getCall() {
return call;
}
public long getScheduledDate() {
return scheduledDate;
}
public @Nullable MessageId getLatestRevisionId() {
return latestRevisionId;
}
public @NonNull MmsMessageRecord withReactions(@NonNull List<ReactionRecord> reactions) {
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
getReadReceiptCount(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), reactions, isRemoteDelete(), mentionsSelf,
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber());
}
public @NonNull MmsMessageRecord withoutQuote() {
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
getReadReceiptCount(), null, getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber());
}
public @NonNull MmsMessageRecord withAttachments(@NonNull List<DatabaseAttachment> attachments) {
Map<AttachmentId, DatabaseAttachment> attachmentIdMap = new HashMap<>();
for (DatabaseAttachment attachment : attachments) {
attachmentIdMap.put(attachment.getAttachmentId(), attachment);
}
List<Contact> contacts = updateContacts(getSharedContacts(), attachmentIdMap);
Set<Attachment> contactAttachments = contacts.stream().map(Contact::getAvatarAttachment).filter(Objects::nonNull).collect(Collectors.toSet());
List<LinkPreview> linkPreviews = updateLinkPreviews(getLinkPreviews(), attachmentIdMap);
Set<Attachment> linkPreviewAttachments = linkPreviews.stream().map(LinkPreview::getThumbnail).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toSet());
Quote quote = updateQuote(getQuote(), attachments);
List<DatabaseAttachment> slideAttachments = attachments.stream().filter(a -> !contactAttachments.contains(a)).filter(a -> !linkPreviewAttachments.contains(a)).collect(Collectors.toList());
SlideDeck slideDeck = MessageTable.MmsReader.buildSlideDeck(slideAttachments);
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), slideDeck,
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
getReadReceiptCount(), quote, contacts, linkPreviews, isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber());
}
public @NonNull MmsMessageRecord withPayment(@NonNull Payment payment) {
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
getReadReceiptCount(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), payment, getCall(), getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber());
}
public @NonNull MmsMessageRecord withCall(@Nullable CallTable.Call call) {
return new MmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
getReadReceiptCount(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), call, getScheduledDate(), getLatestRevisionId(),
getOriginalMessageId(), getRevisionNumber());
}
private static @NonNull List<Contact> updateContacts(@NonNull List<Contact> contacts, @NonNull Map<AttachmentId, DatabaseAttachment> attachmentIdMap) {
return contacts.stream()
.map(contact -> {
if (contact.getAvatar() != null) {
DatabaseAttachment attachment = attachmentIdMap.get(contact.getAvatar().getAttachmentId());
Contact.Avatar updatedAvatar = new Contact.Avatar(contact.getAvatar().getAttachmentId(),
attachment,
contact.getAvatar().isProfile());
return new Contact(contact, updatedAvatar);
} else {
return contact;
}
})
.collect(Collectors.toList());
}
private static @NonNull List<LinkPreview> updateLinkPreviews(@NonNull List<LinkPreview> linkPreviews, @NonNull Map<AttachmentId, DatabaseAttachment> attachmentIdMap) {
return linkPreviews.stream()
.map(preview -> {
if (preview.getAttachmentId() != null) {
DatabaseAttachment attachment = attachmentIdMap.get(preview.getAttachmentId());
if (attachment != null) {
return new LinkPreview(preview.getUrl(), preview.getTitle(), preview.getDescription(), preview.getDate(), attachment);
} else {
return preview;
}
} else {
return preview;
}
})
.collect(Collectors.toList());
}
private static @Nullable Quote updateQuote(@Nullable Quote quote, @NonNull List<DatabaseAttachment> attachments) {
if (quote == null) {
return null;
}
List<DatabaseAttachment> quoteAttachments = attachments.stream().filter(Attachment::isQuote).collect(Collectors.toList());
return quote.withAttachment(new SlideDeck(quoteAttachments));
}
}