/* * 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.tm.archive.sms; import android.content.Context; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import com.annimon.stream.Stream; import org.greenrobot.eventbus.EventBus; import org.signal.core.util.logging.Log; import org.tm.archive.attachments.Attachment; import org.tm.archive.attachments.AttachmentId; import org.tm.archive.attachments.DatabaseAttachment; import org.tm.archive.contacts.sync.ContactDiscovery; import org.tm.archive.contactshare.Contact; import org.tm.archive.database.AttachmentTable; import org.tm.archive.database.MessageTable; import org.tm.archive.database.MessageTable.SyncMessageId; import org.tm.archive.database.NoSuchMessageException; import org.tm.archive.database.RecipientTable; import org.tm.archive.database.SignalDatabase; import org.tm.archive.database.ThreadTable; import org.tm.archive.database.model.MessageId; import org.tm.archive.database.model.MessageRecord; import org.tm.archive.database.model.MmsMessageRecord; import org.tm.archive.database.model.ReactionRecord; import org.tm.archive.database.model.StoryType; import org.tm.archive.dependencies.ApplicationDependencies; import org.tm.archive.jobmanager.Job; import org.tm.archive.jobmanager.JobManager; import org.tm.archive.jobs.AttachmentCompressionJob; import org.tm.archive.jobs.AttachmentCopyJob; import org.tm.archive.jobs.AttachmentMarkUploadedJob; import org.tm.archive.jobs.AttachmentUploadJob; import org.tm.archive.jobs.ProfileKeySendJob; import org.tm.archive.jobs.PushDistributionListSendJob; import org.tm.archive.jobs.PushGroupSendJob; import org.tm.archive.jobs.IndividualSendJob; import org.tm.archive.jobs.ReactionSendJob; import org.tm.archive.jobs.RemoteDeleteSendJob; import org.tm.archive.keyvalue.SignalStore; import org.tm.archive.linkpreview.LinkPreview; import org.tm.archive.mediasend.Media; import org.tm.archive.mms.MmsException; import org.tm.archive.mms.OutgoingMessage; import org.tm.archive.recipients.Recipient; import org.tm.archive.recipients.RecipientId; import org.tm.archive.recipients.RecipientUtil; import org.tm.archive.service.ExpiringMessageManager; import org.tm.archive.util.ParcelUtil; import org.tm.archive.util.SignalLocalMetrics; import org.tm.archive.util.TextSecurePreferences; import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.util.Preconditions; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; public class MessageSender { private static final String TAG = Log.tag(MessageSender.class); /** * Suitable for a 1:1 conversation or a GV1 group only. */ @WorkerThread public static void sendProfileKey(final long threadId) { ProfileKeySendJob job = ProfileKeySendJob.create(threadId, false); if (job != null) { ApplicationDependencies.getJobManager().add(job); } } public static void sendStories(@NonNull final Context context, @NonNull final List messages, @Nullable final String metricId, @Nullable final MessageTable.InsertListener insertListener) { Log.i(TAG, "Sending story messages to " + messages.size() + " targets."); ThreadTable threadTable = SignalDatabase.threads(); MessageTable database = SignalDatabase.messages(); List messageIds = new ArrayList<>(messages.size()); List threads = new ArrayList<>(messages.size()); UploadDependencyGraph dependencyGraph; try { database.beginTransaction(); for (OutgoingMessage message : messages) { long allocatedThreadId = threadTable.getOrCreateValidThreadId(message.getThreadRecipient(), -1L, message.getDistributionType()); long messageId = database.insertMessageOutbox(message.stripAttachments(), allocatedThreadId, false, insertListener); messageIds.add(messageId); threads.add(allocatedThreadId); if (message.getThreadRecipient().isGroup() && message.getAttachments().isEmpty() && message.getLinkPreviews().isEmpty() && message.getSharedContacts().isEmpty()) { SignalLocalMetrics.GroupMessageSend.onInsertedIntoDatabase(messageId, metricId); } else { SignalLocalMetrics.GroupMessageSend.cancel(metricId); } } for (int i = 0; i < messageIds.size(); i++) { long messageId = messageIds.get(i); OutgoingMessage message = messages.get(i); Recipient recipient = message.getThreadRecipient(); if (recipient.isDistributionList()) { DistributionId distributionId = Objects.requireNonNull(SignalDatabase.distributionLists().getDistributionId(recipient.requireDistributionListId())); List members = SignalDatabase.distributionLists().getMembers(recipient.requireDistributionListId()); SignalDatabase.storySends().insert(messageId, members, message.getSentTimeMillis(), message.getStoryType().isStoryWithReplies(), distributionId); } } dependencyGraph = UploadDependencyGraph.create( messages, ApplicationDependencies.getJobManager(), attachment -> { try { return SignalDatabase.attachments().insertAttachmentForPreUpload(attachment); } catch (MmsException e) { Log.e(TAG, e); throw new IllegalStateException(e); } } ); for (int i = 0; i < messageIds.size(); i++) { long messageId = messageIds.get(i); OutgoingMessage message = messages.get(i); List nodes = dependencyGraph.getDependencyMap().get(message); if (nodes == null || nodes.isEmpty()) { if (message.getStoryType().isTextStory()) { Log.d(TAG, "No attachments for given text story. Skipping."); continue; } else { Log.e(TAG, "No attachments for given media story. Aborting."); throw new MmsException("No attachment for story."); } } List attachmentIds = nodes.stream().map(UploadDependencyGraph.Node::getAttachmentId).collect(Collectors.toList()); SignalDatabase.attachments().updateMessageId(attachmentIds, messageId, true); for (final AttachmentId attachmentId : attachmentIds) { SignalDatabase.attachments().updateAttachmentCaption(attachmentId, message.getBody()); } } database.setTransactionSuccessful(); } catch (MmsException e) { Log.w(TAG, "Failed to send stories.", e); return; } finally { database.endTransaction(); } List chains = dependencyGraph.consumeDeferredQueue(); for (final JobManager.Chain chain : chains) { chain.enqueue(); } for (int i = 0; i < messageIds.size(); i++) { long messageId = messageIds.get(i); OutgoingMessage message = messages.get(i); Recipient recipient = message.getThreadRecipient(); List dependencies = dependencyGraph.getDependencyMap().get(message); List jobDependencyIds = (dependencies != null) ? dependencies.stream().map(UploadDependencyGraph.Node::getJobId).collect(Collectors.toList()) : Collections.emptyList(); sendMessageInternal(context, recipient, SendType.SIGNAL, messageId, jobDependencyIds, false); } onMessageSent(); for (long threadId : threads) { threadTable.update(threadId, true); } } public static long send(final Context context, final OutgoingMessage message, final long threadId, @NonNull SendType sendType, @Nullable final String metricId, @Nullable final MessageTable.InsertListener insertListener) { Log.i(TAG, "Sending media message to " + message.getThreadRecipient().getId() + ", thread: " + threadId); try { ThreadTable threadTable = SignalDatabase.threads(); MessageTable database = SignalDatabase.messages(); long allocatedThreadId = threadTable.getOrCreateValidThreadId(message.getThreadRecipient(), threadId, message.getDistributionType()); Recipient recipient = message.getThreadRecipient(); long messageId = database.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, recipient, message, allocatedThreadId), allocatedThreadId, sendType != SendType.SIGNAL, insertListener); if (message.getThreadRecipient().isGroup()) { if (message.getAttachments().isEmpty() && message.getLinkPreviews().isEmpty() && message.getSharedContacts().isEmpty()) { SignalLocalMetrics.GroupMessageSend.onInsertedIntoDatabase(messageId, metricId); } else { SignalLocalMetrics.GroupMessageSend.cancel(messageId); } } else { SignalLocalMetrics.IndividualMessageSend.onInsertedIntoDatabase(messageId, metricId); } sendMessageInternal(context, recipient, sendType, messageId, Collections.emptyList(), message.getScheduledDate() > 0); onMessageSent(); threadTable.update(allocatedThreadId, true); return allocatedThreadId; } catch (MmsException e) { Log.w(TAG, e); return threadId; } } public static long sendPushWithPreUploadedMedia(final Context context, final OutgoingMessage message, final Collection preUploadResults, final long threadId, final MessageTable.InsertListener insertListener) { Log.i(TAG, "Sending media message with pre-uploads to " + message.getThreadRecipient().getId() + ", thread: " + threadId + ", pre-uploads: " + preUploadResults); Preconditions.checkArgument(message.getAttachments().isEmpty(), "If the media is pre-uploaded, there should be no attachments on the message."); try { ThreadTable threadTable = SignalDatabase.threads(); MessageTable mmsDatabase = SignalDatabase.messages(); AttachmentTable attachmentDatabase = SignalDatabase.attachments(); Recipient recipient = message.getThreadRecipient(); long allocatedThreadId = threadTable.getOrCreateValidThreadId(message.getThreadRecipient(), threadId); long messageId = mmsDatabase.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, recipient, message, allocatedThreadId), allocatedThreadId, false, insertListener); List attachmentIds = Stream.of(preUploadResults).map(PreUploadResult::getAttachmentId).toList(); List jobIds = Stream.of(preUploadResults).map(PreUploadResult::getJobIds).flatMap(Stream::of).toList(); attachmentDatabase.updateMessageId(attachmentIds, messageId, message.getStoryType().isStory()); sendMessageInternal(context, recipient, SendType.SIGNAL, messageId, jobIds, false); onMessageSent(); threadTable.update(allocatedThreadId, true); return allocatedThreadId; } catch (MmsException e) { Log.w(TAG, e); return threadId; } } public static void sendMediaBroadcast(@NonNull Context context, @NonNull List messages, @NonNull Collection preUploadResults, boolean overwritePreUploadMessageIds) { Log.i(TAG, "Sending media broadcast (overwrite: " + overwritePreUploadMessageIds + ") to " + Stream.of(messages).map(m -> m.getThreadRecipient().getId()).toList()); Preconditions.checkArgument(messages.size() > 0, "No messages!"); Preconditions.checkArgument(Stream.of(messages).allMatch(m -> m.getAttachments().isEmpty()), "Messages can't have attachments! They should be pre-uploaded."); JobManager jobManager = ApplicationDependencies.getJobManager(); AttachmentTable attachmentDatabase = SignalDatabase.attachments(); MessageTable mmsDatabase = SignalDatabase.messages(); ThreadTable threadTable = SignalDatabase.threads(); List preUploadAttachmentIds = Stream.of(preUploadResults).map(PreUploadResult::getAttachmentId).toList(); List preUploadJobIds = Stream.of(preUploadResults).map(PreUploadResult::getJobIds).flatMap(Stream::of).toList(); List messageIds = new ArrayList<>(messages.size()); List messageDependsOnIds = new ArrayList<>(preUploadJobIds); OutgoingMessage primaryMessage = messages.get(0); mmsDatabase.beginTransaction(); try { if (overwritePreUploadMessageIds) { long primaryThreadId = threadTable.getOrCreateThreadIdFor(primaryMessage.getThreadRecipient(), primaryMessage.getDistributionType()); long primaryMessageId = mmsDatabase.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, primaryMessage.getThreadRecipient(), primaryMessage, primaryThreadId), primaryThreadId, false, null); attachmentDatabase.updateMessageId(preUploadAttachmentIds, primaryMessageId, primaryMessage.getStoryType().isStory()); if (primaryMessage.getStoryType() != StoryType.NONE) { for (final AttachmentId preUploadAttachmentId : preUploadAttachmentIds) { attachmentDatabase.updateAttachmentCaption(preUploadAttachmentId, primaryMessage.getBody()); } } messageIds.add(primaryMessageId); } List preUploadAttachments = Stream.of(preUploadAttachmentIds) .map(attachmentDatabase::getAttachment) .toList(); if (messages.size() > 0) { List secondaryMessages = overwritePreUploadMessageIds ? messages.subList(1, messages.size()) : messages; List> attachmentCopies = new ArrayList<>(); for (int i = 0; i < preUploadAttachmentIds.size(); i++) { attachmentCopies.add(new ArrayList<>(messages.size())); } for (OutgoingMessage secondaryMessage : secondaryMessages) { long allocatedThreadId = threadTable.getOrCreateThreadIdFor(secondaryMessage.getThreadRecipient(), secondaryMessage.getDistributionType()); long messageId = mmsDatabase.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, secondaryMessage.getThreadRecipient(), secondaryMessage, allocatedThreadId), allocatedThreadId, false, null); List attachmentIds = new ArrayList<>(preUploadAttachmentIds.size()); for (int i = 0; i < preUploadAttachments.size(); i++) { AttachmentId attachmentId = attachmentDatabase.insertAttachmentForPreUpload(preUploadAttachments.get(i)).attachmentId; attachmentCopies.get(i).add(attachmentId); attachmentIds.add(attachmentId); } attachmentDatabase.updateMessageId(attachmentIds, messageId, secondaryMessage.getStoryType().isStory()); if (primaryMessage.getStoryType() != StoryType.NONE) { for (final AttachmentId preUploadAttachmentId : attachmentIds) { attachmentDatabase.updateAttachmentCaption(preUploadAttachmentId, primaryMessage.getBody()); } } messageIds.add(messageId); } for (int i = 0; i < attachmentCopies.size(); i++) { Job copyJob = new AttachmentCopyJob(preUploadAttachmentIds.get(i), attachmentCopies.get(i)); jobManager.add(copyJob, preUploadJobIds); messageDependsOnIds.add(copyJob.getId()); } } for (int i = 0; i < messageIds.size(); i++) { long messageId = messageIds.get(i); OutgoingMessage message = messages.get(i); Recipient recipient = message.getThreadRecipient(); if (recipient.isDistributionList()) { List members = SignalDatabase.distributionLists().getMembers(recipient.requireDistributionListId()); DistributionId distributionId = Objects.requireNonNull(SignalDatabase.distributionLists().getDistributionId(recipient.requireDistributionListId())); SignalDatabase.storySends().insert(messageId, members, message.getSentTimeMillis(), message.getStoryType().isStoryWithReplies(), distributionId); } } onMessageSent(); mmsDatabase.setTransactionSuccessful(); } catch (MmsException e) { Log.w(TAG, "Failed to send messages.", e); return; } finally { mmsDatabase.endTransaction(); } for (int i = 0; i < messageIds.size(); i++) { long messageId = messageIds.get(i); Recipient recipient = messages.get(i).getThreadRecipient(); if (isLocalSelfSend(context, recipient, SendType.SIGNAL)) { sendLocalMediaSelf(context, messageId); } else if (recipient.isPushGroup()) { jobManager.add(new PushGroupSendJob(messageId, recipient.getId(), Collections.emptySet(), true, false), messageDependsOnIds, recipient.getId().toQueueKey()); } else if (recipient.isDistributionList()) { jobManager.add(new PushDistributionListSendJob(messageId, recipient.getId(), true, Collections.emptySet()), messageDependsOnIds, recipient.getId().toQueueKey()); } else { jobManager.add(IndividualSendJob.create(messageId, recipient, true, false), messageDependsOnIds, recipient.getId().toQueueKey()); } } } /** * @return A result if the attachment was enqueued, or null if it failed to enqueue or shouldn't * be enqueued (like in the case of a local self-send). */ public static @Nullable PreUploadResult preUploadPushAttachment(@NonNull Context context, @NonNull Attachment attachment, @Nullable Recipient recipient, @NonNull Media media) { if (isLocalSelfSend(context, recipient, SendType.SIGNAL)) { return null; } Log.i(TAG, "Pre-uploading attachment for " + (recipient != null ? recipient.getId() : "null")); try { AttachmentTable attachmentDatabase = SignalDatabase.attachments(); DatabaseAttachment databaseAttachment = attachmentDatabase.insertAttachmentForPreUpload(attachment); Job compressionJob = AttachmentCompressionJob.fromAttachment(databaseAttachment, false, -1); Job uploadJob = new AttachmentUploadJob(databaseAttachment.attachmentId); ApplicationDependencies.getJobManager() .startChain(compressionJob) .then(uploadJob) .enqueue(); return new PreUploadResult(media, databaseAttachment.attachmentId, Arrays.asList(compressionJob.getId(), uploadJob.getId())); } catch (MmsException e) { Log.w(TAG, "preUploadPushAttachment() - Failed to upload!", e); return null; } } public static void sendNewReaction(@NonNull Context context, @NonNull MessageId messageId, @NonNull String emoji) { ReactionRecord reaction = new ReactionRecord(emoji, Recipient.self().getId(), System.currentTimeMillis(), System.currentTimeMillis()); SignalDatabase.reactions().addReaction(messageId, reaction); try { ApplicationDependencies.getJobManager().add(ReactionSendJob.create(context, messageId, reaction, false)); onMessageSent(); } catch (NoSuchMessageException e) { Log.w(TAG, "[sendNewReaction] Could not find message! Ignoring."); } } public static void sendReactionRemoval(@NonNull Context context, @NonNull MessageId messageId, @NonNull ReactionRecord reaction) { SignalDatabase.reactions().deleteReaction(messageId, reaction.getAuthor()); try { ApplicationDependencies.getJobManager().add(ReactionSendJob.create(context, messageId, reaction, true)); onMessageSent(); } catch (NoSuchMessageException e) { Log.w(TAG, "[sendReactionRemoval] Could not find message! Ignoring."); } } public static void sendRemoteDelete(long messageId) { MessageTable db = SignalDatabase.messages(); db.markAsRemoteDelete(messageId); db.markAsSending(messageId); try { RemoteDeleteSendJob.create(messageId).enqueue(); onMessageSent(); } catch (NoSuchMessageException e) { Log.w(TAG, "[sendRemoteDelete] Could not find message! Ignoring."); } } public static void resendGroupMessage(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull Set filterRecipientIds) { if (!messageRecord.isMms()) throw new AssertionError("Not Group"); sendGroupPush(context, messageRecord.getToRecipient(), messageRecord.getId(), filterRecipientIds, Collections.emptyList()); onMessageSent(); } public static void resendDistributionList(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull Set filterRecipientIds) { if (!messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getStoryType().isStory()) { throw new AssertionError("Not a story"); } sendDistributionList(context, messageRecord.getToRecipient(), messageRecord.getId(), filterRecipientIds, Collections.emptyList()); onMessageSent(); } public static void resend(Context context, MessageRecord messageRecord) { long messageId = messageRecord.getId(); boolean forceSms = messageRecord.isForcedSms(); Recipient recipient = messageRecord.getToRecipient(); SendType sendType; if (forceSms) { Recipient threadRecipient = SignalDatabase.threads().getRecipientForThreadId(messageRecord.getThreadId()); if ((threadRecipient != null && threadRecipient.isGroup()) || SignalDatabase.attachments().getAttachmentsForMessage(messageId).size() > 0) { sendType = SendType.MMS; } else { sendType = SendType.SMS; } } else { sendType = SendType.SIGNAL; } sendMessageInternal(context, recipient, sendType, messageId, Collections.emptyList(), false); onMessageSent(); } public static void onMessageSent() { EventBus.getDefault().postSticky(MessageSentEvent.INSTANCE); } private static @NonNull OutgoingMessage applyUniversalExpireTimerIfNecessary(@NonNull Context context, @NonNull Recipient recipient, @NonNull OutgoingMessage outgoingMessage, long threadId) { if (!outgoingMessage.isExpirationUpdate() && outgoingMessage.getExpiresIn() == 0 && RecipientUtil.setAndSendUniversalExpireTimerIfNecessary(context, recipient, threadId)) { return outgoingMessage.withExpiry(TimeUnit.SECONDS.toMillis(SignalStore.settings().getUniversalExpireTimer())); } return outgoingMessage; } private static void sendMessageInternal(Context context, Recipient recipient, SendType sendType, long messageId, @NonNull Collection uploadJobIds, boolean isScheduledSend) { if (isLocalSelfSend(context, recipient, sendType) && !isScheduledSend) { sendLocalMediaSelf(context, messageId); } else if (recipient.isPushGroup()) { sendGroupPush(context, recipient, messageId, Collections.emptySet(), uploadJobIds); } else if (recipient.isDistributionList()) { sendDistributionList(context, recipient, messageId, Collections.emptySet(), uploadJobIds); } else if (sendType == SendType.SIGNAL && isPushMediaSend(context, recipient)) { sendMediaPush(context, recipient, messageId, uploadJobIds); } else { Log.w(TAG, "Unknown send type!"); } } private static void sendMediaPush(Context context, Recipient recipient, long messageId, @NonNull Collection uploadJobIds) { JobManager jobManager = ApplicationDependencies.getJobManager(); if (uploadJobIds.size() > 0) { Job mediaSend = IndividualSendJob.create(messageId, recipient, true, false); jobManager.add(mediaSend, uploadJobIds); } else { IndividualSendJob.enqueue(context, jobManager, messageId, recipient, false); } } private static void sendGroupPush(@NonNull Context context, @NonNull Recipient recipient, long messageId, @NonNull Set filterRecipientIds, @NonNull Collection uploadJobIds) { JobManager jobManager = ApplicationDependencies.getJobManager(); if (uploadJobIds.size() > 0) { Job groupSend = new PushGroupSendJob(messageId, recipient.getId(), filterRecipientIds, !uploadJobIds.isEmpty(), false); jobManager.add(groupSend, uploadJobIds, uploadJobIds.isEmpty() ? null : recipient.getId().toQueueKey()); } else { PushGroupSendJob.enqueue(context, jobManager, messageId, recipient.getId(), filterRecipientIds, false); } } private static void sendDistributionList(@NonNull Context context, @NonNull Recipient recipient, long messageId, @NonNull Set filterRecipientIds, @NonNull Collection uploadJobIds) { JobManager jobManager = ApplicationDependencies.getJobManager(); if (uploadJobIds.size() > 0) { Job groupSend = new PushDistributionListSendJob(messageId, recipient.getId(), !uploadJobIds.isEmpty(), filterRecipientIds); jobManager.add(groupSend, uploadJobIds, uploadJobIds.isEmpty() ? null : recipient.getId().toQueueKey()); } else { PushDistributionListSendJob.enqueue(context, jobManager, messageId, recipient.getId(), filterRecipientIds); } } private static boolean isPushMediaSend(Context context, Recipient recipient) { if (!SignalStore.account().isRegistered()) { return false; } if (recipient.isGroup()) { return false; } return isPushDestination(context, recipient); } private static boolean isPushDestination(Context context, Recipient destination) { if (destination.resolve().getRegistered() == RecipientTable.RegisteredState.REGISTERED) { return true; } else if (destination.resolve().getRegistered() == RecipientTable.RegisteredState.NOT_REGISTERED) { return false; } else { try { RecipientTable.RegisteredState state = ContactDiscovery.refresh(context, destination, false); return state == RecipientTable.RegisteredState.REGISTERED; } catch (IOException e1) { Log.w(TAG, e1); return false; } } } public static boolean isLocalSelfSend(@NonNull Context context, @Nullable Recipient recipient, SendType sendType) { return recipient != null && recipient.isSelf() && sendType == SendType.SIGNAL && SignalStore.account().isRegistered() && !TextSecurePreferences.isMultiDevice(context); } private static void sendLocalMediaSelf(Context context, long messageId) { try { ExpiringMessageManager expirationManager = ApplicationDependencies.getExpiringMessageManager(); MessageTable mmsDatabase = SignalDatabase.messages(); OutgoingMessage message = mmsDatabase.getOutgoingMessage(messageId); SyncMessageId syncId = new SyncMessageId(Recipient.self().getId(), message.getSentTimeMillis()); List attachments = new LinkedList<>(); attachments.addAll(message.getAttachments()); attachments.addAll(Stream.of(message.getLinkPreviews()) .map(LinkPreview::getThumbnail) .filter(Optional::isPresent) .map(Optional::get) .toList()); attachments.addAll(Stream.of(message.getSharedContacts()) .map(Contact::getAvatar).withoutNulls() .map(Contact.Avatar::getAttachment).withoutNulls() .toList()); List compressionJobs = Stream.of(attachments) .map(a -> AttachmentCompressionJob.fromAttachment((DatabaseAttachment) a, false, -1)) .toList(); List fakeUploadJobs = Stream.of(attachments) .map(a -> new AttachmentMarkUploadedJob(messageId, ((DatabaseAttachment) a).attachmentId)) .toList(); ApplicationDependencies.getJobManager().startChain(compressionJobs) .then(fakeUploadJobs) .enqueue(); mmsDatabase.markAsSent(messageId, true); mmsDatabase.markUnidentified(messageId, true); mmsDatabase.incrementDeliveryReceiptCount(message.getSentTimeMillis(), Recipient.self().getId(), System.currentTimeMillis()); mmsDatabase.incrementReadReceiptCount(message.getSentTimeMillis(), Recipient.self().getId(), System.currentTimeMillis()); mmsDatabase.incrementViewedReceiptCount(message.getSentTimeMillis(), Recipient.self().getId(), System.currentTimeMillis()); if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) { mmsDatabase.markExpireStarted(messageId); expirationManager.scheduleDeletion(messageId, true, message.getExpiresIn()); } } catch (NoSuchMessageException | MmsException e) { Log.w(TAG, "Failed to update self-sent message.", e); } } public static class PreUploadResult implements Parcelable { private final Media media; private final AttachmentId attachmentId; private final Collection jobIds; PreUploadResult(@NonNull Media media, @NonNull AttachmentId attachmentId, @NonNull Collection jobIds) { this.media = media; this.attachmentId = attachmentId; this.jobIds = jobIds; } private PreUploadResult(Parcel in) { this.attachmentId = in.readParcelable(AttachmentId.class.getClassLoader()); this.jobIds = ParcelUtil.readStringCollection(in); this.media = in.readParcelable(Media.class.getClassLoader()); } public @NonNull AttachmentId getAttachmentId() { return attachmentId; } public @NonNull Collection getJobIds() { return jobIds; } public @NonNull Media getMedia() { return media; } public static final Creator CREATOR = new Creator() { @Override public PreUploadResult createFromParcel(Parcel in) { return new PreUploadResult(in); } @Override public PreUploadResult[] newArray(int size) { return new PreUploadResult[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(attachmentId, flags); ParcelUtil.writeStringCollection(dest, jobIds); dest.writeParcelable(media, flags); } @Override public @NonNull String toString() { return "{ID: " + attachmentId.id + ", URI: " + media.getUri() + ", Jobs: " + jobIds.stream().map(j -> "JOB::" + j).collect(Collectors.toList()) + "}"; } } public enum MessageSentEvent { INSTANCE } public enum SendType { SIGNAL, SMS, MMS } }