kopia lustrzana https://github.com/ryukoposting/Signal-Android
779 wiersze
37 KiB
Java
779 wiersze
37 KiB
Java
/*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package org.thoughtcrime.securesms.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.thoughtcrime.securesms.attachments.Attachment;
|
|
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
|
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
|
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
|
import org.thoughtcrime.securesms.contactshare.Contact;
|
|
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
|
import org.thoughtcrime.securesms.database.MessageDatabase;
|
|
import org.thoughtcrime.securesms.database.MessageDatabase.SyncMessageId;
|
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
|
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
|
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
|
import org.thoughtcrime.securesms.database.SmsDatabase;
|
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
|
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.ReactionRecord;
|
|
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
|
import org.thoughtcrime.securesms.database.model.StoryType;
|
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
|
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
|
import org.thoughtcrime.securesms.jobs.AttachmentCompressionJob;
|
|
import org.thoughtcrime.securesms.jobs.AttachmentCopyJob;
|
|
import org.thoughtcrime.securesms.jobs.AttachmentMarkUploadedJob;
|
|
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob;
|
|
import org.thoughtcrime.securesms.jobs.MmsSendJob;
|
|
import org.thoughtcrime.securesms.jobs.ProfileKeySendJob;
|
|
import org.thoughtcrime.securesms.jobs.PushDistributionListSendJob;
|
|
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
|
|
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
|
|
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
|
|
import org.thoughtcrime.securesms.jobs.ReactionSendJob;
|
|
import org.thoughtcrime.securesms.jobs.RemoteDeleteSendJob;
|
|
import org.thoughtcrime.securesms.jobs.ResumableUploadSpecJob;
|
|
import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
|
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
|
import org.thoughtcrime.securesms.mms.MmsException;
|
|
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
|
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
|
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
|
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
|
import org.thoughtcrime.securesms.util.ParcelUtil;
|
|
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
|
import org.thoughtcrime.securesms.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;
|
|
|
|
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 Context context, final long threadId) {
|
|
ApplicationDependencies.getJobManager().add(ProfileKeySendJob.create(context, threadId, false));
|
|
}
|
|
|
|
public static long send(final Context context,
|
|
final OutgoingTextMessage message,
|
|
final long threadId,
|
|
final boolean forceSms,
|
|
@Nullable final String metricId,
|
|
final SmsDatabase.InsertListener insertListener)
|
|
{
|
|
Log.i(TAG, "Sending text message to " + message.getRecipient().getId() + ", thread: " + threadId);
|
|
MessageDatabase database = SignalDatabase.sms();
|
|
Recipient recipient = message.getRecipient();
|
|
boolean keyExchange = message.isKeyExchange();
|
|
|
|
long allocatedThreadId = SignalDatabase.threads().getOrCreateValidThreadId(recipient, threadId);
|
|
long messageId = database.insertMessageOutbox(allocatedThreadId,
|
|
applyUniversalExpireTimerIfNecessary(context, recipient, message, allocatedThreadId),
|
|
forceSms,
|
|
System.currentTimeMillis(),
|
|
insertListener);
|
|
|
|
SignalLocalMetrics.IndividualMessageSend.onInsertedIntoDatabase(messageId, metricId);
|
|
|
|
sendTextMessage(context, recipient, forceSms, keyExchange, messageId);
|
|
onMessageSent();
|
|
SignalDatabase.threads().update(threadId, true);
|
|
|
|
return allocatedThreadId;
|
|
}
|
|
|
|
public static void sendStories(@NonNull final Context context,
|
|
@NonNull final List<OutgoingSecureMediaMessage> messages,
|
|
@Nullable final String metricId,
|
|
@Nullable final SmsDatabase.InsertListener insertListener)
|
|
{
|
|
Log.i(TAG, "Sending story messages to " + messages.size() + " targets.");
|
|
ThreadDatabase threadDatabase = SignalDatabase.threads();
|
|
MessageDatabase database = SignalDatabase.mms();
|
|
List<Long> messageIds = new ArrayList<>(messages.size());
|
|
List<Long> threads = new ArrayList<>(messages.size());
|
|
|
|
try {
|
|
database.beginTransaction();
|
|
for (OutgoingMediaMessage message : messages) {
|
|
long allocatedThreadId = threadDatabase.getOrCreateValidThreadId(message.getRecipient(), -1L, message.getDistributionType());
|
|
Recipient recipient = message.getRecipient();
|
|
long messageId = database.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, recipient, message, allocatedThreadId), allocatedThreadId, false, insertListener);
|
|
|
|
messageIds.add(messageId);
|
|
threads.add(allocatedThreadId);
|
|
|
|
if (message.getRecipient().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);
|
|
OutgoingSecureMediaMessage message = messages.get(i);
|
|
Recipient recipient = message.getRecipient();
|
|
|
|
if (recipient.isDistributionList()) {
|
|
DistributionId distributionId = Objects.requireNonNull(SignalDatabase.distributionLists().getDistributionId(recipient.requireDistributionListId()));
|
|
List<RecipientId> members = SignalDatabase.distributionLists().getMembers(recipient.requireDistributionListId());
|
|
SignalDatabase.storySends().insert(messageId, members, message.getSentTimeMillis(), message.getStoryType().isStoryWithReplies(), distributionId);
|
|
}
|
|
}
|
|
|
|
database.setTransactionSuccessful();
|
|
} catch (MmsException e) {
|
|
Log.w(TAG, e);
|
|
} finally {
|
|
database.endTransaction();
|
|
}
|
|
|
|
for (int i = 0; i < messageIds.size(); i++) {
|
|
long messageId = messageIds.get(i);
|
|
OutgoingSecureMediaMessage message = messages.get(i);
|
|
Recipient recipient = message.getRecipient();
|
|
|
|
sendMediaMessage(context, recipient, false, messageId, Collections.emptyList());
|
|
}
|
|
|
|
onMessageSent();
|
|
|
|
for (long threadId : threads) {
|
|
threadDatabase.update(threadId, true);
|
|
}
|
|
}
|
|
|
|
public static long send(final Context context,
|
|
final OutgoingMediaMessage message,
|
|
final long threadId,
|
|
final boolean forceSms,
|
|
@Nullable final String metricId,
|
|
final SmsDatabase.InsertListener insertListener)
|
|
{
|
|
Log.i(TAG, "Sending media message to " + message.getRecipient().getId() + ", thread: " + threadId);
|
|
try {
|
|
ThreadDatabase threadDatabase = SignalDatabase.threads();
|
|
MessageDatabase database = SignalDatabase.mms();
|
|
|
|
long allocatedThreadId = threadDatabase.getOrCreateValidThreadId(message.getRecipient(), threadId, message.getDistributionType());
|
|
Recipient recipient = message.getRecipient();
|
|
long messageId = database.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, recipient, message, allocatedThreadId), allocatedThreadId, forceSms, insertListener);
|
|
|
|
if (message.getRecipient().isGroup() && message.getAttachments().isEmpty() && message.getLinkPreviews().isEmpty() && message.getSharedContacts().isEmpty()) {
|
|
SignalLocalMetrics.GroupMessageSend.onInsertedIntoDatabase(messageId, metricId);
|
|
} else {
|
|
SignalLocalMetrics.GroupMessageSend.cancel(metricId);
|
|
}
|
|
|
|
sendMediaMessage(context, recipient, forceSms, messageId, Collections.emptyList());
|
|
onMessageSent();
|
|
threadDatabase.update(threadId, true);
|
|
|
|
return allocatedThreadId;
|
|
} catch (MmsException e) {
|
|
Log.w(TAG, e);
|
|
return threadId;
|
|
}
|
|
}
|
|
|
|
public static long sendPushWithPreUploadedMedia(final Context context,
|
|
final OutgoingMediaMessage message,
|
|
final Collection<PreUploadResult> preUploadResults,
|
|
final long threadId,
|
|
final SmsDatabase.InsertListener insertListener)
|
|
{
|
|
Log.i(TAG, "Sending media message with pre-uploads to " + message.getRecipient().getId() + ", thread: " + threadId);
|
|
Preconditions.checkArgument(message.getAttachments().isEmpty(), "If the media is pre-uploaded, there should be no attachments on the message.");
|
|
|
|
try {
|
|
ThreadDatabase threadDatabase = SignalDatabase.threads();
|
|
MessageDatabase mmsDatabase = SignalDatabase.mms();
|
|
AttachmentDatabase attachmentDatabase = SignalDatabase.attachments();
|
|
|
|
long allocatedThreadId;
|
|
|
|
if (threadId == -1) {
|
|
allocatedThreadId = threadDatabase.getOrCreateThreadIdFor(message.getRecipient(), message.getDistributionType());
|
|
} else {
|
|
allocatedThreadId = threadId;
|
|
}
|
|
|
|
Recipient recipient = message.getRecipient();
|
|
long messageId = mmsDatabase.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, recipient, message, allocatedThreadId),
|
|
allocatedThreadId,
|
|
false,
|
|
insertListener);
|
|
|
|
List<AttachmentId> attachmentIds = Stream.of(preUploadResults).map(PreUploadResult::getAttachmentId).toList();
|
|
List<String> jobIds = Stream.of(preUploadResults).map(PreUploadResult::getJobIds).flatMap(Stream::of).toList();
|
|
|
|
attachmentDatabase.updateMessageId(attachmentIds, messageId, message.getStoryType().isStory());
|
|
|
|
sendMediaMessage(context, recipient, false, messageId, jobIds);
|
|
onMessageSent();
|
|
threadDatabase.update(threadId, true);
|
|
|
|
return allocatedThreadId;
|
|
} catch (MmsException e) {
|
|
Log.w(TAG, e);
|
|
return threadId;
|
|
}
|
|
}
|
|
|
|
public static void sendMediaBroadcast(@NonNull Context context,
|
|
@NonNull List<OutgoingSecureMediaMessage> messages,
|
|
@NonNull Collection<PreUploadResult> preUploadResults,
|
|
@NonNull List<OutgoingStoryMessage> storyMessages)
|
|
{
|
|
Log.i(TAG, "Sending media broadcast to " + Stream.of(messages).map(m -> m.getRecipient().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();
|
|
AttachmentDatabase attachmentDatabase = SignalDatabase.attachments();
|
|
MessageDatabase mmsDatabase = SignalDatabase.mms();
|
|
ThreadDatabase threadDatabase = SignalDatabase.threads();
|
|
List<AttachmentId> preUploadAttachmentIds = Stream.of(preUploadResults).map(PreUploadResult::getAttachmentId).toList();
|
|
List<String> preUploadJobIds = Stream.of(preUploadResults).map(PreUploadResult::getJobIds).flatMap(Stream::of).toList();
|
|
List<Long> messageIds = new ArrayList<>(messages.size());
|
|
List<String> messageDependsOnIds = new ArrayList<>(preUploadJobIds);
|
|
|
|
mmsDatabase.beginTransaction();
|
|
try {
|
|
OutgoingSecureMediaMessage primaryMessage = messages.get(0);
|
|
long primaryThreadId = threadDatabase.getOrCreateThreadIdFor(primaryMessage.getRecipient(), primaryMessage.getDistributionType());
|
|
long primaryMessageId = mmsDatabase.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, primaryMessage.getRecipient(), 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<DatabaseAttachment> preUploadAttachments = Stream.of(preUploadAttachmentIds)
|
|
.map(attachmentDatabase::getAttachment)
|
|
.toList();
|
|
|
|
if (messages.size() > 0) {
|
|
List<OutgoingSecureMediaMessage> secondaryMessages = messages.subList(1, messages.size());
|
|
List<List<AttachmentId>> attachmentCopies = new ArrayList<>();
|
|
|
|
for (int i = 0; i < preUploadAttachmentIds.size(); i++) {
|
|
attachmentCopies.add(new ArrayList<>(messages.size()));
|
|
}
|
|
|
|
for (OutgoingSecureMediaMessage secondaryMessage : secondaryMessages) {
|
|
long allocatedThreadId = threadDatabase.getOrCreateThreadIdFor(secondaryMessage.getRecipient(), secondaryMessage.getDistributionType());
|
|
long messageId = mmsDatabase.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, secondaryMessage.getRecipient(), secondaryMessage, allocatedThreadId),
|
|
allocatedThreadId,
|
|
false,
|
|
null);
|
|
List<AttachmentId> attachmentIds = new ArrayList<>(preUploadAttachmentIds.size());
|
|
|
|
for (int i = 0; i < preUploadAttachments.size(); i++) {
|
|
AttachmentId attachmentId = attachmentDatabase.insertAttachmentForPreUpload(preUploadAttachments.get(i)).getAttachmentId();
|
|
attachmentCopies.get(i).add(attachmentId);
|
|
attachmentIds.add(attachmentId);
|
|
}
|
|
|
|
attachmentDatabase.updateMessageId(attachmentIds, messageId, secondaryMessage.getStoryType().isStory());
|
|
if (primaryMessage.getStoryType() != StoryType.NONE) {
|
|
for (final AttachmentId preUploadAttachmentId : preUploadAttachmentIds) {
|
|
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 (final OutgoingStoryMessage storyMessage : storyMessages) {
|
|
OutgoingSecureMediaMessage message = storyMessage.getOutgoingSecureMediaMessage();
|
|
|
|
if (!message.getStoryType().isStory()) {
|
|
throw new AssertionError("Only story messages can be sent via this method.");
|
|
}
|
|
|
|
long allocatedThreadId = threadDatabase.getOrCreateThreadIdFor(message.getRecipient(), message.getDistributionType());
|
|
long messageId = mmsDatabase.insertMessageOutbox(storyMessage.getOutgoingSecureMediaMessage(), allocatedThreadId, false, null);
|
|
Optional<DatabaseAttachment> preUploadAttachment = preUploadAttachments.stream()
|
|
.filter(a -> a.getAttachmentId().equals(storyMessage.getPreUploadResult().getAttachmentId()))
|
|
.findFirst();
|
|
|
|
if (!preUploadAttachment.isPresent()) {
|
|
Log.w(TAG, "Dropping story message without pre-upload attachment.");
|
|
mmsDatabase.markAsSentFailed(messageId);
|
|
} else {
|
|
AttachmentId attachmentCopyId = attachmentDatabase.insertAttachmentForPreUpload(preUploadAttachment.get()).getAttachmentId();
|
|
attachmentDatabase.updateMessageId(Collections.singletonList(attachmentCopyId), messageId, true);
|
|
attachmentDatabase.updateAttachmentCaption(attachmentCopyId, storyMessage.getOutgoingSecureMediaMessage().getBody());
|
|
messageIds.add(messageId);
|
|
messages.add(storyMessage.getOutgoingSecureMediaMessage());
|
|
|
|
Job copyJob = new AttachmentCopyJob(storyMessage.getPreUploadResult().getAttachmentId(), Collections.singletonList(attachmentCopyId));
|
|
jobManager.add(copyJob, preUploadJobIds);
|
|
messageDependsOnIds.add(copyJob.getId());
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < messageIds.size(); i++) {
|
|
long messageId = messageIds.get(i);
|
|
OutgoingSecureMediaMessage message = messages.get(i);
|
|
Recipient recipient = message.getRecipient();
|
|
|
|
if (recipient.isDistributionList()) {
|
|
List<RecipientId> 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).getRecipient();
|
|
|
|
if (isLocalSelfSend(context, recipient, false)) {
|
|
sendLocalMediaSelf(context, messageId);
|
|
} else if (recipient.isPushGroup()) {
|
|
jobManager.add(new PushGroupSendJob(messageId, recipient.getId(), Collections.emptySet(), true), 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(new PushMediaSendJob(messageId, recipient, true), 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) {
|
|
if (isLocalSelfSend(context, recipient, false)) {
|
|
return null;
|
|
}
|
|
Log.i(TAG, "Pre-uploading attachment for " + (recipient != null ? recipient.getId() : "null"));
|
|
|
|
try {
|
|
AttachmentDatabase attachmentDatabase = SignalDatabase.attachments();
|
|
DatabaseAttachment databaseAttachment = attachmentDatabase.insertAttachmentForPreUpload(attachment);
|
|
|
|
Job compressionJob = AttachmentCompressionJob.fromAttachment(databaseAttachment, false, -1);
|
|
Job resumableUploadSpecJob = new ResumableUploadSpecJob();
|
|
Job uploadJob = new AttachmentUploadJob(databaseAttachment.getAttachmentId());
|
|
|
|
ApplicationDependencies.getJobManager()
|
|
.startChain(compressionJob)
|
|
.then(resumableUploadSpecJob)
|
|
.then(uploadJob)
|
|
.enqueue();
|
|
|
|
return new PreUploadResult(databaseAttachment.getAttachmentId(), Arrays.asList(compressionJob.getId(), resumableUploadSpecJob.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, boolean isMms) {
|
|
MessageDatabase db = isMms ? SignalDatabase.mms() : SignalDatabase.sms();
|
|
db.markAsRemoteDelete(messageId);
|
|
db.markAsSending(messageId);
|
|
|
|
try {
|
|
RemoteDeleteSendJob.create(messageId, isMms).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<RecipientId> filterRecipientIds) {
|
|
if (!messageRecord.isMms()) throw new AssertionError("Not Group");
|
|
sendGroupPush(context, messageRecord.getRecipient(), messageRecord.getId(), filterRecipientIds, Collections.emptyList());
|
|
onMessageSent();
|
|
}
|
|
|
|
public static void resendDistributionList(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull Set<RecipientId> filterRecipientIds) {
|
|
if (!messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getStoryType().isStory()) {
|
|
throw new AssertionError("Not a story");
|
|
}
|
|
sendDistributionList(context, messageRecord.getRecipient(), messageRecord.getId(), filterRecipientIds, Collections.emptyList());
|
|
onMessageSent();
|
|
}
|
|
|
|
public static void resend(Context context, MessageRecord messageRecord) {
|
|
long messageId = messageRecord.getId();
|
|
boolean forceSms = messageRecord.isForcedSms();
|
|
boolean keyExchange = messageRecord.isKeyExchange();
|
|
Recipient recipient = messageRecord.getRecipient();
|
|
|
|
if (messageRecord.isMms()) {
|
|
sendMediaMessage(context, recipient, forceSms, messageId, Collections.emptyList());
|
|
} else {
|
|
sendTextMessage(context, recipient, forceSms, keyExchange, messageId);
|
|
}
|
|
|
|
onMessageSent();
|
|
}
|
|
|
|
public static void onMessageSent() {
|
|
EventBus.getDefault().postSticky(MessageSentEvent.INSTANCE);
|
|
}
|
|
|
|
private static @NonNull OutgoingTextMessage applyUniversalExpireTimerIfNecessary(@NonNull Context context, @NonNull Recipient recipient, @NonNull OutgoingTextMessage outgoingTextMessage, long threadId) {
|
|
if (outgoingTextMessage.getExpiresIn() == 0 && RecipientUtil.setAndSendUniversalExpireTimerIfNecessary(context, recipient, threadId)) {
|
|
return outgoingTextMessage.withExpiry(TimeUnit.SECONDS.toMillis(SignalStore.settings().getUniversalExpireTimer()));
|
|
}
|
|
return outgoingTextMessage;
|
|
}
|
|
|
|
private static @NonNull OutgoingMediaMessage applyUniversalExpireTimerIfNecessary(@NonNull Context context, @NonNull Recipient recipient, @NonNull OutgoingMediaMessage outgoingMediaMessage, long threadId) {
|
|
if (!outgoingMediaMessage.isExpirationUpdate() && outgoingMediaMessage.getExpiresIn() == 0 && RecipientUtil.setAndSendUniversalExpireTimerIfNecessary(context, recipient, threadId)) {
|
|
return outgoingMediaMessage.withExpiry(TimeUnit.SECONDS.toMillis(SignalStore.settings().getUniversalExpireTimer()));
|
|
}
|
|
return outgoingMediaMessage;
|
|
}
|
|
|
|
private static void sendMediaMessage(Context context, Recipient recipient, boolean forceSms, long messageId, @NonNull Collection<String> uploadJobIds)
|
|
{
|
|
if (isLocalSelfSend(context, recipient, forceSms)) {
|
|
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 (!forceSms && isPushMediaSend(context, recipient)) {
|
|
sendMediaPush(context, recipient, messageId, uploadJobIds);
|
|
} else {
|
|
sendMms(context, messageId);
|
|
}
|
|
}
|
|
|
|
private static void sendTextMessage(Context context, Recipient recipient,
|
|
boolean forceSms, boolean keyExchange,
|
|
long messageId)
|
|
{
|
|
if (isLocalSelfSend(context, recipient, forceSms)) {
|
|
sendLocalTextSelf(context, messageId);
|
|
} else if (!forceSms && isPushTextSend(context, recipient, keyExchange)) {
|
|
sendTextPush(recipient, messageId);
|
|
} else {
|
|
sendSms(recipient, messageId);
|
|
}
|
|
}
|
|
|
|
private static void sendTextPush(Recipient recipient, long messageId) {
|
|
ApplicationDependencies.getJobManager().add(new PushTextSendJob(messageId, recipient));
|
|
}
|
|
|
|
private static void sendMediaPush(Context context, Recipient recipient, long messageId, @NonNull Collection<String> uploadJobIds) {
|
|
JobManager jobManager = ApplicationDependencies.getJobManager();
|
|
|
|
if (uploadJobIds.size() > 0) {
|
|
Job mediaSend = new PushMediaSendJob(messageId, recipient, true);
|
|
jobManager.add(mediaSend, uploadJobIds);
|
|
} else {
|
|
PushMediaSendJob.enqueue(context, jobManager, messageId, recipient);
|
|
}
|
|
}
|
|
|
|
private static void sendGroupPush(@NonNull Context context, @NonNull Recipient recipient, long messageId, @NonNull Set<RecipientId> filterRecipientIds, @NonNull Collection<String> uploadJobIds) {
|
|
JobManager jobManager = ApplicationDependencies.getJobManager();
|
|
|
|
if (uploadJobIds.size() > 0) {
|
|
Job groupSend = new PushGroupSendJob(messageId, recipient.getId(), filterRecipientIds, !uploadJobIds.isEmpty());
|
|
jobManager.add(groupSend, uploadJobIds, uploadJobIds.isEmpty() ? null : recipient.getId().toQueueKey());
|
|
} else {
|
|
PushGroupSendJob.enqueue(context, jobManager, messageId, recipient.getId(), filterRecipientIds);
|
|
}
|
|
}
|
|
|
|
private static void sendDistributionList(@NonNull Context context, @NonNull Recipient recipient, long messageId, @NonNull Set<RecipientId> filterRecipientIds, @NonNull Collection<String> 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 void sendSms(Recipient recipient, long messageId) {
|
|
JobManager jobManager = ApplicationDependencies.getJobManager();
|
|
jobManager.add(new SmsSendJob(messageId, recipient));
|
|
}
|
|
|
|
private static void sendMms(Context context, long messageId) {
|
|
JobManager jobManager = ApplicationDependencies.getJobManager();
|
|
MmsSendJob.enqueue(context, jobManager, messageId);
|
|
}
|
|
|
|
private static boolean isPushTextSend(Context context, Recipient recipient, boolean keyExchange) {
|
|
if (!SignalStore.account().isRegistered()) {
|
|
return false;
|
|
}
|
|
|
|
if (keyExchange) {
|
|
return false;
|
|
}
|
|
|
|
return isPushDestination(context, recipient);
|
|
}
|
|
|
|
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() == RecipientDatabase.RegisteredState.REGISTERED) {
|
|
return true;
|
|
} else if (destination.resolve().getRegistered() == RecipientDatabase.RegisteredState.NOT_REGISTERED) {
|
|
return false;
|
|
} else {
|
|
try {
|
|
RecipientDatabase.RegisteredState state = ContactDiscovery.refresh(context, destination, false);
|
|
return state == RecipientDatabase.RegisteredState.REGISTERED;
|
|
} catch (IOException e1) {
|
|
Log.w(TAG, e1);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static boolean isLocalSelfSend(@NonNull Context context, @Nullable Recipient recipient, boolean forceSms) {
|
|
return recipient != null &&
|
|
recipient.isSelf() &&
|
|
!forceSms &&
|
|
SignalStore.account().isRegistered() &&
|
|
!TextSecurePreferences.isMultiDevice(context);
|
|
}
|
|
|
|
private static void sendLocalMediaSelf(Context context, long messageId) {
|
|
try {
|
|
ExpiringMessageManager expirationManager = ApplicationDependencies.getExpiringMessageManager();
|
|
MessageDatabase mmsDatabase = SignalDatabase.mms();
|
|
MmsSmsDatabase mmsSmsDatabase = SignalDatabase.mmsSms();
|
|
OutgoingMediaMessage message = mmsDatabase.getOutgoingMessage(messageId);
|
|
SyncMessageId syncId = new SyncMessageId(Recipient.self().getId(), message.getSentTimeMillis());
|
|
List<Attachment> 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<AttachmentCompressionJob> compressionJobs = Stream.of(attachments)
|
|
.map(a -> AttachmentCompressionJob.fromAttachment((DatabaseAttachment) a, false, -1))
|
|
.toList();
|
|
|
|
List<AttachmentMarkUploadedJob> fakeUploadJobs = Stream.of(attachments)
|
|
.map(a -> new AttachmentMarkUploadedJob(messageId, ((DatabaseAttachment) a).getAttachmentId()))
|
|
.toList();
|
|
|
|
ApplicationDependencies.getJobManager().startChain(compressionJobs)
|
|
.then(fakeUploadJobs)
|
|
.enqueue();
|
|
|
|
mmsDatabase.markAsSent(messageId, true);
|
|
mmsDatabase.markUnidentified(messageId, true);
|
|
|
|
mmsSmsDatabase.incrementDeliveryReceiptCount(syncId, System.currentTimeMillis());
|
|
mmsSmsDatabase.incrementReadReceiptCount(syncId, System.currentTimeMillis());
|
|
mmsSmsDatabase.incrementViewedReceiptCount(syncId, 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);
|
|
}
|
|
}
|
|
|
|
private static void sendLocalTextSelf(Context context, long messageId) {
|
|
try {
|
|
ExpiringMessageManager expirationManager = ApplicationDependencies.getExpiringMessageManager();
|
|
MessageDatabase smsDatabase = SignalDatabase.sms();
|
|
MmsSmsDatabase mmsSmsDatabase = SignalDatabase.mmsSms();
|
|
SmsMessageRecord message = smsDatabase.getSmsMessage(messageId);
|
|
SyncMessageId syncId = new SyncMessageId(Recipient.self().getId(), message.getDateSent());
|
|
|
|
smsDatabase.markAsSent(messageId, true);
|
|
smsDatabase.markUnidentified(messageId, true);
|
|
|
|
mmsSmsDatabase.incrementDeliveryReceiptCount(syncId, System.currentTimeMillis());
|
|
mmsSmsDatabase.incrementReadReceiptCount(syncId, System.currentTimeMillis());
|
|
|
|
if (message.getExpiresIn() > 0) {
|
|
smsDatabase.markExpireStarted(messageId);
|
|
expirationManager.scheduleDeletion(message.getId(), message.isMms(), message.getExpiresIn());
|
|
}
|
|
} catch (NoSuchMessageException e) {
|
|
Log.w(TAG, "Failed to update self-sent message.", e);
|
|
}
|
|
}
|
|
|
|
public static class PreUploadResult implements Parcelable {
|
|
private final AttachmentId attachmentId;
|
|
private final Collection<String> jobIds;
|
|
|
|
PreUploadResult(@NonNull AttachmentId attachmentId, @NonNull Collection<String> jobIds) {
|
|
this.attachmentId = attachmentId;
|
|
this.jobIds = jobIds;
|
|
}
|
|
|
|
private PreUploadResult(Parcel in) {
|
|
this.attachmentId = in.readParcelable(AttachmentId.class.getClassLoader());
|
|
this.jobIds = ParcelUtil.readStringCollection(in);
|
|
}
|
|
|
|
public @NonNull AttachmentId getAttachmentId() {
|
|
return attachmentId;
|
|
}
|
|
|
|
public @NonNull Collection<String> getJobIds() {
|
|
return jobIds;
|
|
}
|
|
|
|
public static final Creator<PreUploadResult> CREATOR = new Creator<PreUploadResult>() {
|
|
@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);
|
|
}
|
|
}
|
|
|
|
public enum MessageSentEvent {
|
|
INSTANCE
|
|
}
|
|
}
|