1671 lines
79 KiB
Java
1671 lines
79 KiB
Java
package org.thoughtcrime.securesms.jobs;
|
|
|
|
import android.annotation.SuppressLint;
|
|
import android.content.Intent;
|
|
import android.os.Build;
|
|
import android.text.TextUtils;
|
|
import android.util.Pair;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
|
|
import com.annimon.stream.Collectors;
|
|
import com.annimon.stream.Stream;
|
|
|
|
import org.signal.zkgroup.profiles.ProfileKey;
|
|
import org.thoughtcrime.securesms.ApplicationContext;
|
|
import org.thoughtcrime.securesms.attachments.Attachment;
|
|
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
|
import org.thoughtcrime.securesms.attachments.PointerAttachment;
|
|
import org.thoughtcrime.securesms.attachments.TombstoneAttachment;
|
|
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
|
import org.thoughtcrime.securesms.contactshare.Contact;
|
|
import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
|
|
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
|
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
|
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
|
|
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
|
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
|
import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo;
|
|
import org.thoughtcrime.securesms.database.MessagingDatabase;
|
|
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
|
|
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
|
import org.thoughtcrime.securesms.database.MmsDatabase;
|
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
|
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
|
import org.thoughtcrime.securesms.database.SmsDatabase;
|
|
import org.thoughtcrime.securesms.database.StickerDatabase;
|
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
|
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.StickerRecord;
|
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|
import org.thoughtcrime.securesms.groups.GroupId;
|
|
import org.thoughtcrime.securesms.groups.GroupV1MessageProcessor;
|
|
import org.thoughtcrime.securesms.jobmanager.Data;
|
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
|
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
|
import org.thoughtcrime.securesms.linkpreview.Link;
|
|
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
|
import org.thoughtcrime.securesms.logging.Log;
|
|
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
|
import org.thoughtcrime.securesms.mms.MmsException;
|
|
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
|
|
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
|
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
|
import org.thoughtcrime.securesms.mms.QuoteModel;
|
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
|
import org.thoughtcrime.securesms.mms.StickerSlide;
|
|
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
|
import org.thoughtcrime.securesms.ringrtc.IceCandidateParcel;
|
|
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
|
import org.thoughtcrime.securesms.service.WebRtcCallService;
|
|
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
|
|
import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage;
|
|
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
|
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
|
|
import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage;
|
|
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
|
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
|
import org.thoughtcrime.securesms.util.Base64;
|
|
import org.thoughtcrime.securesms.util.GroupUtil;
|
|
import org.thoughtcrime.securesms.util.Hex;
|
|
import org.thoughtcrime.securesms.util.IdentityUtil;
|
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|
import org.whispersystems.libsignal.state.SessionStore;
|
|
import org.whispersystems.libsignal.util.guava.Optional;
|
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
|
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
|
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview;
|
|
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
|
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
|
|
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
|
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
|
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
|
|
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
|
|
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
|
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
|
|
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
|
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
|
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
|
|
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
|
|
import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
|
|
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
|
|
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
|
|
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
|
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
|
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
|
|
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
|
|
import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage;
|
|
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
|
|
|
import java.io.IOException;
|
|
import java.security.SecureRandom;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
public final class PushProcessMessageJob extends BaseJob {
|
|
|
|
public static final String KEY = "PushProcessJob";
|
|
public static final String QUEUE = "__PUSH_PROCESS_JOB__";
|
|
|
|
public static final String TAG = Log.tag(PushProcessMessageJob.class);
|
|
|
|
private static final String KEY_MESSAGE_STATE = "message_state";
|
|
private static final String KEY_MESSAGE_PLAINTEXT = "message_content";
|
|
private static final String KEY_MESSAGE_ID = "message_id";
|
|
private static final String KEY_SMS_MESSAGE_ID = "sms_message_id";
|
|
private static final String KEY_TIMESTAMP = "timestamp";
|
|
private static final String KEY_EXCEPTION_SENDER = "exception_sender";
|
|
private static final String KEY_EXCEPTION_DEVICE = "exception_device";
|
|
private static final String KEY_EXCEPTION_GROUP_ID = "exception_groupId";
|
|
|
|
@NonNull private final MessageState messageState;
|
|
@Nullable private final byte[] serializedPlaintextContent;
|
|
@Nullable private final ExceptionMetadata exceptionMetadata;
|
|
private final long messageId;
|
|
private final long smsMessageId;
|
|
private final long timestamp;
|
|
|
|
PushProcessMessageJob(@NonNull byte[] serializedPlaintextContent,
|
|
long pushMessageId,
|
|
long smsMessageId,
|
|
long timestamp)
|
|
{
|
|
this(MessageState.DECRYPTED_OK,
|
|
serializedPlaintextContent,
|
|
null,
|
|
pushMessageId,
|
|
smsMessageId,
|
|
timestamp);
|
|
}
|
|
|
|
PushProcessMessageJob(@NonNull MessageState messageState,
|
|
@NonNull ExceptionMetadata exceptionMetadata,
|
|
long pushMessageId,
|
|
long smsMessageId,
|
|
long timestamp)
|
|
{
|
|
this(messageState,
|
|
null,
|
|
exceptionMetadata,
|
|
pushMessageId,
|
|
smsMessageId,
|
|
timestamp);
|
|
}
|
|
|
|
private PushProcessMessageJob(@NonNull MessageState messageState,
|
|
@Nullable byte[] serializedPlaintextContent,
|
|
@Nullable ExceptionMetadata exceptionMetadata,
|
|
long pushMessageId,
|
|
long smsMessageId,
|
|
long timestamp)
|
|
{
|
|
this(new Parameters.Builder()
|
|
.setQueue(QUEUE)
|
|
.setMaxAttempts(Parameters.UNLIMITED)
|
|
.build(),
|
|
messageState,
|
|
serializedPlaintextContent,
|
|
exceptionMetadata,
|
|
pushMessageId,
|
|
smsMessageId,
|
|
timestamp);
|
|
}
|
|
|
|
private PushProcessMessageJob(@NonNull Parameters parameters,
|
|
@NonNull MessageState messageState,
|
|
@Nullable byte[] serializedPlaintextContent,
|
|
@Nullable ExceptionMetadata exceptionMetadata,
|
|
long pushMessageId,
|
|
long smsMessageId,
|
|
long timestamp)
|
|
{
|
|
super(parameters);
|
|
|
|
this.messageState = messageState;
|
|
this.exceptionMetadata = exceptionMetadata;
|
|
this.serializedPlaintextContent = serializedPlaintextContent;
|
|
this.messageId = pushMessageId;
|
|
this.smsMessageId = smsMessageId;
|
|
this.timestamp = timestamp;
|
|
}
|
|
|
|
@Override
|
|
public @NonNull Data serialize() {
|
|
Data.Builder dataBuilder = new Data.Builder()
|
|
.putInt(KEY_MESSAGE_STATE, messageState.ordinal())
|
|
.putLong(KEY_MESSAGE_ID, messageId)
|
|
.putLong(KEY_SMS_MESSAGE_ID, smsMessageId)
|
|
.putLong(KEY_TIMESTAMP, timestamp);
|
|
|
|
if (messageState == MessageState.DECRYPTED_OK) {
|
|
//noinspection ConstantConditions
|
|
dataBuilder.putString(KEY_MESSAGE_PLAINTEXT, Base64.encodeBytes(serializedPlaintextContent));
|
|
} else {
|
|
//noinspection ConstantConditions
|
|
dataBuilder.putString(KEY_EXCEPTION_SENDER, exceptionMetadata.sender)
|
|
.putInt(KEY_EXCEPTION_DEVICE, exceptionMetadata.senderDevice)
|
|
.putString(KEY_EXCEPTION_GROUP_ID, exceptionMetadata.groupId == null ? null : exceptionMetadata.groupId.toString());
|
|
}
|
|
|
|
return dataBuilder.build();
|
|
}
|
|
|
|
@Override
|
|
public @NonNull String getFactoryKey() {
|
|
return KEY;
|
|
}
|
|
|
|
@Override
|
|
public void onRun() {
|
|
Optional<Long> optionalSmsMessageId = smsMessageId > 0 ? Optional.of(smsMessageId) : Optional.absent();
|
|
|
|
if (messageState == MessageState.DECRYPTED_OK) {
|
|
//noinspection ConstantConditions
|
|
handleMessage(serializedPlaintextContent, optionalSmsMessageId);
|
|
} else {
|
|
//noinspection ConstantConditions
|
|
handleExceptionMessage(exceptionMetadata, optionalSmsMessageId);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean onShouldRetry(@NonNull Exception exception) {
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public void onFailure() {
|
|
}
|
|
|
|
private void handleMessage(@NonNull byte[] plaintextDataBuffer, @NonNull Optional<Long> smsMessageId) {
|
|
try {
|
|
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
|
SignalServiceContent content = SignalServiceContent.deserialize(plaintextDataBuffer);
|
|
|
|
if (content == null || shouldIgnore(content)) {
|
|
Log.i(TAG, "Ignoring message.");
|
|
return;
|
|
}
|
|
|
|
if (content.getDataMessage().isPresent()) {
|
|
SignalServiceDataMessage message = content.getDataMessage().get();
|
|
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent();
|
|
Optional<GroupId> groupId = GroupUtil.idFromGroupContext(message.getGroupContext());
|
|
boolean isGv2Message = groupId.isPresent() && groupId.get().isV2();
|
|
|
|
if (isGv2Message) {
|
|
Log.w(TAG, "Ignoring GV2 message.");
|
|
return;
|
|
}
|
|
|
|
if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), groupId, content.getTimestamp(), smsMessageId);
|
|
else if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
|
|
else if (message.isGroupV1Update()) handleGroupV1Message(content, message, smsMessageId);
|
|
else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId);
|
|
else if (message.getReaction().isPresent()) handleReaction(content, message);
|
|
else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId);
|
|
else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId, groupId);
|
|
|
|
if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) {
|
|
handleUnknownGroupMessage(content, message.getGroupContext().get());
|
|
}
|
|
|
|
if (message.getProfileKey().isPresent()) {
|
|
handleProfileKey(content, message.getProfileKey().get());
|
|
}
|
|
|
|
if (content.isNeedsReceipt()) {
|
|
handleNeedsDeliveryReceipt(content, message);
|
|
}
|
|
} else if (content.getSyncMessage().isPresent()) {
|
|
TextSecurePreferences.setMultiDevice(context, true);
|
|
|
|
SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
|
|
|
|
if (syncMessage.getSent().isPresent()) handleSynchronizeSentMessage(content, syncMessage.getSent().get());
|
|
else if (syncMessage.getRequest().isPresent()) handleSynchronizeRequestMessage(syncMessage.getRequest().get());
|
|
else if (syncMessage.getRead().isPresent()) handleSynchronizeReadMessage(syncMessage.getRead().get(), content.getTimestamp());
|
|
else if (syncMessage.getViewOnceOpen().isPresent()) handleSynchronizeViewOnceOpenMessage(syncMessage.getViewOnceOpen().get(), content.getTimestamp());
|
|
else if (syncMessage.getVerified().isPresent()) handleSynchronizeVerifiedMessage(syncMessage.getVerified().get());
|
|
else if (syncMessage.getStickerPackOperations().isPresent()) handleSynchronizeStickerPackOperation(syncMessage.getStickerPackOperations().get());
|
|
else if (syncMessage.getConfiguration().isPresent()) handleSynchronizeConfigurationMessage(syncMessage.getConfiguration().get());
|
|
else if (syncMessage.getBlockedList().isPresent()) handleSynchronizeBlockedListMessage(syncMessage.getBlockedList().get());
|
|
else if (syncMessage.getFetchType().isPresent()) handleSynchronizeFetchMessage(syncMessage.getFetchType().get());
|
|
else if (syncMessage.getMessageRequestResponse().isPresent()) handleSynchronizeMessageRequestResponse(syncMessage.getMessageRequestResponse().get());
|
|
else Log.w(TAG, "Contains no known sync types...");
|
|
} else if (content.getCallMessage().isPresent()) {
|
|
Log.i(TAG, "Got call message...");
|
|
SignalServiceCallMessage message = content.getCallMessage().get();
|
|
|
|
if (message.getOfferMessage().isPresent()) handleCallOfferMessage(content, message.getOfferMessage().get(), smsMessageId);
|
|
else if (message.getAnswerMessage().isPresent()) handleCallAnswerMessage(content, message.getAnswerMessage().get());
|
|
else if (message.getIceUpdateMessages().isPresent()) handleCallIceUpdateMessage(content, message.getIceUpdateMessages().get());
|
|
else if (message.getHangupMessage().isPresent()) handleCallHangupMessage(content, message.getHangupMessage().get(), smsMessageId);
|
|
else if (message.getBusyMessage().isPresent()) handleCallBusyMessage(content, message.getBusyMessage().get());
|
|
} else if (content.getReceiptMessage().isPresent()) {
|
|
SignalServiceReceiptMessage message = content.getReceiptMessage().get();
|
|
|
|
if (message.isReadReceipt()) handleReadReceipt(content, message);
|
|
else if (message.isDeliveryReceipt()) handleDeliveryReceipt(content, message);
|
|
} else if (content.getTypingMessage().isPresent()) {
|
|
handleTypingMessage(content, content.getTypingMessage().get());
|
|
} else {
|
|
Log.w(TAG, "Got unrecognized message...");
|
|
}
|
|
|
|
resetRecipientToPush(Recipient.externalPush(context, content.getSender()));
|
|
|
|
} catch (StorageFailedException e) {
|
|
Log.w(TAG, e);
|
|
handleCorruptMessage(e.getSender(), e.getSenderDevice(), timestamp, smsMessageId);
|
|
}
|
|
}
|
|
|
|
private void handleExceptionMessage(@NonNull ExceptionMetadata e, @NonNull Optional<Long> smsMessageId) {
|
|
switch (messageState) {
|
|
|
|
case INVALID_VERSION:
|
|
Log.w(TAG, "Handling invalid version");
|
|
handleInvalidVersionMessage(e.sender, e.senderDevice, timestamp, smsMessageId);
|
|
break;
|
|
|
|
case CORRUPT_MESSAGE:
|
|
Log.w(TAG, "Handling corrupt message");
|
|
handleCorruptMessage(e.sender, e.senderDevice, timestamp, smsMessageId);
|
|
break;
|
|
|
|
case NO_SESSION:
|
|
Log.w(TAG, "Handling no session");
|
|
handleNoSessionMessage(e.sender, e.senderDevice, timestamp, smsMessageId);
|
|
break;
|
|
|
|
case LEGACY_MESSAGE:
|
|
Log.w(TAG, "Handling legacy message");
|
|
handleLegacyMessage(e.sender, e.senderDevice, timestamp, smsMessageId);
|
|
break;
|
|
|
|
case DUPLICATE_MESSAGE:
|
|
Log.w(TAG, "Handling duplicate message");
|
|
handleDuplicateMessage(e.sender, e.senderDevice, timestamp, smsMessageId);
|
|
break;
|
|
|
|
case UNSUPPORTED_DATA_MESSAGE:
|
|
Log.w(TAG, "Handling unsupported data message");
|
|
handleUnsupportedDataMessage(e.sender, e.senderDevice, Optional.fromNullable(e.groupId), timestamp, smsMessageId);
|
|
break;
|
|
|
|
default:
|
|
throw new AssertionError("Not handled " + messageState);
|
|
}
|
|
}
|
|
|
|
private void handleCallOfferMessage(@NonNull SignalServiceContent content,
|
|
@NonNull OfferMessage message,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
{
|
|
Log.i(TAG, "handleCallOfferMessage...");
|
|
|
|
if (smsMessageId.isPresent()) {
|
|
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
|
database.markAsMissedCall(smsMessageId.get());
|
|
} else {
|
|
Intent intent = new Intent(context, WebRtcCallService.class);
|
|
RemotePeer remotePeer = new RemotePeer(Recipient.externalPush(context, content.getSender()).getId());
|
|
|
|
intent.setAction(WebRtcCallService.ACTION_RECEIVE_OFFER)
|
|
.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId())
|
|
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer)
|
|
.putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice())
|
|
.putExtra(WebRtcCallService.EXTRA_OFFER_DESCRIPTION, message.getDescription())
|
|
.putExtra(WebRtcCallService.EXTRA_TIMESTAMP, content.getTimestamp());
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) context.startForegroundService(intent);
|
|
else context.startService(intent);
|
|
}
|
|
}
|
|
|
|
private void handleCallAnswerMessage(@NonNull SignalServiceContent content,
|
|
@NonNull AnswerMessage message)
|
|
{
|
|
Log.i(TAG, "handleCallAnswerMessage...");
|
|
Intent intent = new Intent(context, WebRtcCallService.class);
|
|
RemotePeer remotePeer = new RemotePeer(Recipient.externalPush(context, content.getSender()).getId());
|
|
|
|
intent.setAction(WebRtcCallService.ACTION_RECEIVE_ANSWER)
|
|
.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId())
|
|
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer)
|
|
.putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice())
|
|
.putExtra(WebRtcCallService.EXTRA_ANSWER_DESCRIPTION, message.getDescription());
|
|
|
|
context.startService(intent);
|
|
}
|
|
|
|
private void handleCallIceUpdateMessage(@NonNull SignalServiceContent content,
|
|
@NonNull List<IceUpdateMessage> messages)
|
|
{
|
|
Log.i(TAG, "handleCallIceUpdateMessage... " + messages.size());
|
|
|
|
ArrayList<IceCandidateParcel> iceCandidates = new ArrayList<>(messages.size());
|
|
long callId = -1;
|
|
for (IceUpdateMessage iceMessage : messages) {
|
|
iceCandidates.add(new IceCandidateParcel(iceMessage));
|
|
callId = iceMessage.getId();
|
|
}
|
|
|
|
Intent intent = new Intent(context, WebRtcCallService.class);
|
|
RemotePeer remotePeer = new RemotePeer(Recipient.externalPush(context, content.getSender()).getId());
|
|
|
|
intent.setAction(WebRtcCallService.ACTION_RECEIVE_ICE_CANDIDATES)
|
|
.putExtra(WebRtcCallService.EXTRA_CALL_ID, callId)
|
|
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer)
|
|
.putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice())
|
|
.putParcelableArrayListExtra(WebRtcCallService.EXTRA_ICE_CANDIDATES, iceCandidates);
|
|
|
|
context.startService(intent);
|
|
}
|
|
|
|
private void handleCallHangupMessage(@NonNull SignalServiceContent content,
|
|
@NonNull HangupMessage message,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
{
|
|
Log.i(TAG, "handleCallHangupMessage");
|
|
if (smsMessageId.isPresent()) {
|
|
DatabaseFactory.getSmsDatabase(context).markAsMissedCall(smsMessageId.get());
|
|
} else {
|
|
Intent intent = new Intent(context, WebRtcCallService.class);
|
|
RemotePeer remotePeer = new RemotePeer(Recipient.externalPush(context, content.getSender()).getId());
|
|
|
|
intent.setAction(WebRtcCallService.ACTION_RECEIVE_HANGUP)
|
|
.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId())
|
|
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer)
|
|
.putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice());
|
|
|
|
context.startService(intent);
|
|
}
|
|
}
|
|
|
|
private void handleCallBusyMessage(@NonNull SignalServiceContent content,
|
|
@NonNull BusyMessage message)
|
|
{
|
|
Log.i(TAG, "handleCallBusyMessage");
|
|
|
|
Intent intent = new Intent(context, WebRtcCallService.class);
|
|
RemotePeer remotePeer = new RemotePeer(Recipient.externalPush(context, content.getSender()).getId());
|
|
|
|
intent.setAction(WebRtcCallService.ACTION_RECEIVE_BUSY)
|
|
.putExtra(WebRtcCallService.EXTRA_CALL_ID, message.getId())
|
|
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, remotePeer)
|
|
.putExtra(WebRtcCallService.EXTRA_REMOTE_DEVICE, content.getSenderDevice());
|
|
|
|
context.startService(intent);
|
|
}
|
|
|
|
private void handleEndSessionMessage(@NonNull SignalServiceContent content,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
{
|
|
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
|
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(),
|
|
content.getSenderDevice(),
|
|
content.getTimestamp(),
|
|
"", Optional.absent(), 0,
|
|
content.isNeedsReceipt());
|
|
|
|
Long threadId;
|
|
|
|
if (!smsMessageId.isPresent()) {
|
|
IncomingEndSessionMessage incomingEndSessionMessage = new IncomingEndSessionMessage(incomingTextMessage);
|
|
Optional<InsertResult> insertResult = smsDatabase.insertMessageInbox(incomingEndSessionMessage);
|
|
|
|
if (insertResult.isPresent()) threadId = insertResult.get().getThreadId();
|
|
else threadId = null;
|
|
} else {
|
|
smsDatabase.markAsEndSession(smsMessageId.get());
|
|
threadId = smsDatabase.getThreadIdForMessage(smsMessageId.get());
|
|
}
|
|
|
|
if (threadId != null) {
|
|
SessionStore sessionStore = new TextSecureSessionStore(context);
|
|
sessionStore.deleteAllSessions(content.getSender().getIdentifier());
|
|
|
|
SecurityEvent.broadcastSecurityUpdateEvent(context);
|
|
MessageNotifier.updateNotification(context, threadId);
|
|
}
|
|
}
|
|
|
|
private long handleSynchronizeSentEndSessionMessage(@NonNull SentTranscriptMessage message)
|
|
{
|
|
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
|
Recipient recipient = getSyncMessageDestination(message);
|
|
OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipient, "", -1);
|
|
OutgoingEndSessionMessage outgoingEndSessionMessage = new OutgoingEndSessionMessage(outgoingTextMessage);
|
|
|
|
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
|
|
|
if (!recipient.isGroup()) {
|
|
SessionStore sessionStore = new TextSecureSessionStore(context);
|
|
sessionStore.deleteAllSessions(recipient.requireServiceId());
|
|
|
|
SecurityEvent.broadcastSecurityUpdateEvent(context);
|
|
|
|
long messageId = database.insertMessageOutbox(threadId, outgoingEndSessionMessage,
|
|
false, message.getTimestamp(),
|
|
null);
|
|
database.markAsSent(messageId, true);
|
|
}
|
|
|
|
return threadId;
|
|
}
|
|
|
|
private void handleGroupV1Message(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceDataMessage message,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
throws StorageFailedException
|
|
{
|
|
GroupV1MessageProcessor.process(context, content, message, false);
|
|
|
|
if (message.getExpiresInSeconds() != 0 && message.getExpiresInSeconds() != getMessageDestination(content, message).getExpireMessages()) {
|
|
handleExpirationUpdate(content, message, Optional.absent());
|
|
}
|
|
|
|
if (smsMessageId.isPresent()) {
|
|
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
|
|
}
|
|
}
|
|
|
|
private void handleUnknownGroupMessage(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceGroupContext group)
|
|
{
|
|
if (group.getGroupV1().isPresent()) {
|
|
SignalServiceGroup groupV1 = group.getGroupV1().get();
|
|
if (groupV1.getType() != SignalServiceGroup.Type.REQUEST_INFO) {
|
|
ApplicationDependencies.getJobManager().add(new RequestGroupInfoJob(Recipient.externalPush(context, content.getSender()).getId(), GroupId.v1(groupV1.getGroupId())));
|
|
} else {
|
|
Log.w(TAG, "Received a REQUEST_INFO message for a group we don't know about. Ignoring.");
|
|
}
|
|
} else {
|
|
Log.w(TAG, "Received a message for a group we don't know about without a GV1 context. Ignoring.");
|
|
}
|
|
}
|
|
|
|
private void handleExpirationUpdate(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceDataMessage message,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
throws StorageFailedException
|
|
{
|
|
try {
|
|
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
|
Recipient sender = Recipient.externalPush(context, content.getSender());
|
|
Recipient recipient = getMessageDestination(content, message);
|
|
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(sender.getId(),
|
|
message.getTimestamp(), -1,
|
|
message.getExpiresInSeconds() * 1000L, true,
|
|
false,
|
|
content.isNeedsReceipt(),
|
|
Optional.absent(),
|
|
message.getGroupContext(),
|
|
Optional.absent(),
|
|
Optional.absent(),
|
|
Optional.absent(),
|
|
Optional.absent(),
|
|
Optional.absent());
|
|
|
|
database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
|
|
|
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient.getId(), message.getExpiresInSeconds());
|
|
|
|
if (smsMessageId.isPresent()) {
|
|
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
|
|
}
|
|
} catch (MmsException e) {
|
|
throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice());
|
|
}
|
|
}
|
|
|
|
private void handleReaction(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
|
|
SignalServiceDataMessage.Reaction reaction = message.getReaction().get();
|
|
|
|
Recipient targetAuthor = Recipient.externalPush(context, reaction.getTargetAuthor());
|
|
MessageRecord targetMessage = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(reaction.getTargetSentTimestamp(), targetAuthor.getId());
|
|
|
|
if (targetMessage != null) {
|
|
Recipient reactionAuthor = Recipient.externalPush(context, content.getSender());
|
|
MessagingDatabase db = targetMessage.isMms() ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
|
|
|
|
if (reaction.isRemove()) {
|
|
db.deleteReaction(targetMessage.getId(), reactionAuthor.getId());
|
|
MessageNotifier.updateNotification(context);
|
|
} else {
|
|
ReactionRecord reactionRecord = new ReactionRecord(reaction.getEmoji(), reactionAuthor.getId(), message.getTimestamp(), System.currentTimeMillis());
|
|
db.addReaction(targetMessage.getId(), reactionRecord);
|
|
MessageNotifier.updateNotification(context, targetMessage.getThreadId(), false);
|
|
}
|
|
|
|
} else {
|
|
Log.w(TAG, "[handleReaction] Could not find matching message! timestamp: " + reaction.getTargetSentTimestamp() + " author: " + targetAuthor.getId());
|
|
}
|
|
}
|
|
|
|
private void handleSynchronizeVerifiedMessage(@NonNull VerifiedMessage verifiedMessage) {
|
|
IdentityUtil.processVerifiedMessage(context, verifiedMessage);
|
|
}
|
|
|
|
private void handleSynchronizeStickerPackOperation(@NonNull List<StickerPackOperationMessage> stickerPackOperations) {
|
|
JobManager jobManager = ApplicationDependencies.getJobManager();
|
|
|
|
for (StickerPackOperationMessage operation : stickerPackOperations) {
|
|
if (operation.getPackId().isPresent() && operation.getPackKey().isPresent() && operation.getType().isPresent()) {
|
|
String packId = Hex.toStringCondensed(operation.getPackId().get());
|
|
String packKey = Hex.toStringCondensed(operation.getPackKey().get());
|
|
|
|
switch (operation.getType().get()) {
|
|
case INSTALL:
|
|
jobManager.add(StickerPackDownloadJob.forInstall(packId, packKey, false));
|
|
break;
|
|
case REMOVE:
|
|
DatabaseFactory.getStickerDatabase(context).uninstallPack(packId);
|
|
break;
|
|
}
|
|
} else {
|
|
Log.w(TAG, "Received incomplete sticker pack operation sync.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void handleSynchronizeConfigurationMessage(@NonNull ConfigurationMessage configurationMessage) {
|
|
if (configurationMessage.getReadReceipts().isPresent()) {
|
|
TextSecurePreferences.setReadReceiptsEnabled(context, configurationMessage.getReadReceipts().get());
|
|
}
|
|
|
|
if (configurationMessage.getUnidentifiedDeliveryIndicators().isPresent()) {
|
|
TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, configurationMessage.getReadReceipts().get());
|
|
}
|
|
|
|
if (configurationMessage.getTypingIndicators().isPresent()) {
|
|
TextSecurePreferences.setTypingIndicatorsEnabled(context, configurationMessage.getTypingIndicators().get());
|
|
}
|
|
|
|
if (configurationMessage.getLinkPreviews().isPresent()) {
|
|
TextSecurePreferences.setLinkPreviewsEnabled(context, configurationMessage.getReadReceipts().get());
|
|
}
|
|
}
|
|
|
|
private void handleSynchronizeBlockedListMessage(@NonNull BlockedListMessage blockMessage) {
|
|
DatabaseFactory.getRecipientDatabase(context).applyBlockedUpdate(blockMessage.getAddresses(), blockMessage.getGroupIds());
|
|
}
|
|
|
|
private static void handleSynchronizeFetchMessage(@NonNull SignalServiceSyncMessage.FetchType fetchType) {
|
|
Log.i(TAG, "Received fetch request with type: " + fetchType);
|
|
|
|
switch (fetchType) {
|
|
case LOCAL_PROFILE:
|
|
ApplicationDependencies.getJobManager().add(new RefreshOwnProfileJob());
|
|
break;
|
|
case STORAGE_MANIFEST:
|
|
StorageSyncHelper.scheduleSyncForDataChange();
|
|
break;
|
|
default:
|
|
Log.w(TAG, "Received a fetch message for an unknown type.");
|
|
}
|
|
}
|
|
|
|
private void handleSynchronizeMessageRequestResponse(@NonNull MessageRequestResponseMessage response) {
|
|
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
|
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
|
|
|
Recipient recipient;
|
|
|
|
if (response.getPerson().isPresent()) {
|
|
recipient = Recipient.externalPush(context, response.getPerson().get());
|
|
} else if (response.getGroupId().isPresent()) {
|
|
GroupId groupId = GroupId.v1(response.getGroupId().get());
|
|
recipient = Recipient.externalGroup(context, groupId);
|
|
} else {
|
|
Log.w(TAG, "Message request response was missing a thread recipient! Skipping.");
|
|
return;
|
|
}
|
|
|
|
long threadId = threadDatabase.getThreadIdFor(recipient);
|
|
|
|
switch (response.getType()) {
|
|
case ACCEPT:
|
|
recipientDatabase.setProfileSharing(recipient.getId(), true);
|
|
recipientDatabase.setBlocked(recipient.getId(), false);
|
|
break;
|
|
case DELETE:
|
|
recipientDatabase.setProfileSharing(recipient.getId(), false);
|
|
if (threadId > 0) threadDatabase.deleteConversation(threadId);
|
|
break;
|
|
case BLOCK:
|
|
recipientDatabase.setBlocked(recipient.getId(), true);
|
|
recipientDatabase.setProfileSharing(recipient.getId(), false);
|
|
break;
|
|
case BLOCK_AND_DELETE:
|
|
recipientDatabase.setBlocked(recipient.getId(), true);
|
|
recipientDatabase.setProfileSharing(recipient.getId(), false);
|
|
if (threadId > 0) threadDatabase.deleteConversation(threadId);
|
|
break;
|
|
default:
|
|
Log.w(TAG, "Got an unknown response type! Skipping");
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void handleSynchronizeSentMessage(@NonNull SignalServiceContent content,
|
|
@NonNull SentTranscriptMessage message)
|
|
throws StorageFailedException
|
|
|
|
{
|
|
try {
|
|
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
|
|
|
Long threadId = null;
|
|
|
|
if (message.isRecipientUpdate()) {
|
|
handleGroupRecipientUpdate(message);
|
|
} else if (message.getMessage().isEndSession()) {
|
|
threadId = handleSynchronizeSentEndSessionMessage(message);
|
|
} else if (message.getMessage().isGroupV1Update()) {
|
|
threadId = GroupV1MessageProcessor.process(context, content, message.getMessage(), true);
|
|
} else if (message.getMessage().isExpirationUpdate()) {
|
|
threadId = handleSynchronizeSentExpirationUpdate(message);
|
|
} else if (message.getMessage().getReaction().isPresent()) {
|
|
handleReaction(content, message.getMessage());
|
|
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(getSyncMessageDestination(message));
|
|
threadId = threadId != -1 ? threadId : null;
|
|
} else if (message.getMessage().getAttachments().isPresent() || message.getMessage().getQuote().isPresent() || message.getMessage().getPreviews().isPresent() || message.getMessage().getSticker().isPresent() || message.getMessage().isViewOnce()) {
|
|
threadId = handleSynchronizeSentMediaMessage(message);
|
|
} else {
|
|
threadId = handleSynchronizeSentTextMessage(message);
|
|
}
|
|
|
|
if (message.getMessage().getGroupContext().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.idFromGroupContext(message.getMessage().getGroupContext().get()))) {
|
|
handleUnknownGroupMessage(content, message.getMessage().getGroupContext().get());
|
|
}
|
|
|
|
if (message.getMessage().getProfileKey().isPresent()) {
|
|
Recipient recipient = getSyncMessageDestination(message);
|
|
|
|
if (recipient != null && !recipient.isSystemContact() && !recipient.isProfileSharing()) {
|
|
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient.getId(), true);
|
|
}
|
|
}
|
|
|
|
if (threadId != null) {
|
|
DatabaseFactory.getThreadDatabase(context).setRead(threadId, true);
|
|
MessageNotifier.updateNotification(context);
|
|
}
|
|
|
|
MessageNotifier.setLastDesktopActivityTimestamp(message.getTimestamp());
|
|
} catch (MmsException e) {
|
|
throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice());
|
|
}
|
|
}
|
|
|
|
private void handleSynchronizeRequestMessage(@NonNull RequestMessage message)
|
|
{
|
|
if (message.isContactsRequest()) {
|
|
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(true));
|
|
}
|
|
|
|
if (message.isGroupsRequest()) {
|
|
ApplicationDependencies.getJobManager().add(new MultiDeviceGroupUpdateJob());
|
|
}
|
|
|
|
if (message.isBlockedListRequest()) {
|
|
ApplicationDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob());
|
|
}
|
|
|
|
if (message.isConfigurationRequest()) {
|
|
ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(context),
|
|
TextSecurePreferences.isTypingIndicatorsEnabled(context),
|
|
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context),
|
|
TextSecurePreferences.isLinkPreviewsEnabled(context)));
|
|
ApplicationDependencies.getJobManager().add(new MultiDeviceStickerPackSyncJob());
|
|
}
|
|
|
|
if (message.isKeysRequest()) {
|
|
ApplicationDependencies.getJobManager().add(new MultiDeviceKeysUpdateJob());
|
|
}
|
|
}
|
|
|
|
private void handleSynchronizeReadMessage(@NonNull List<ReadMessage> readMessages, long envelopeTimestamp)
|
|
{
|
|
for (ReadMessage readMessage : readMessages) {
|
|
List<Pair<Long, Long>> expiringText = DatabaseFactory.getSmsDatabase(context).setTimestampRead(new SyncMessageId(Recipient.externalPush(context, readMessage.getSender()).getId(), readMessage.getTimestamp()), envelopeTimestamp);
|
|
List<Pair<Long, Long>> expiringMedia = DatabaseFactory.getMmsDatabase(context).setTimestampRead(new SyncMessageId(Recipient.externalPush(context, readMessage.getSender()).getId(), readMessage.getTimestamp()), envelopeTimestamp);
|
|
|
|
for (Pair<Long, Long> expiringMessage : expiringText) {
|
|
ApplicationContext.getInstance(context)
|
|
.getExpiringMessageManager()
|
|
.scheduleDeletion(expiringMessage.first, false, envelopeTimestamp, expiringMessage.second);
|
|
}
|
|
|
|
for (Pair<Long, Long> expiringMessage : expiringMedia) {
|
|
ApplicationContext.getInstance(context)
|
|
.getExpiringMessageManager()
|
|
.scheduleDeletion(expiringMessage.first, true, envelopeTimestamp, expiringMessage.second);
|
|
}
|
|
}
|
|
|
|
MessageNotifier.setLastDesktopActivityTimestamp(envelopeTimestamp);
|
|
MessageNotifier.cancelDelayedNotifications();
|
|
MessageNotifier.updateNotification(context);
|
|
}
|
|
|
|
private void handleSynchronizeViewOnceOpenMessage(@NonNull ViewOnceOpenMessage openMessage, long envelopeTimestamp) {
|
|
RecipientId author = Recipient.externalPush(context, openMessage.getSender()).getId();
|
|
long timestamp = openMessage.getTimestamp();
|
|
MessageRecord record = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(timestamp, author);
|
|
|
|
if (record != null && record.isMms()) {
|
|
DatabaseFactory.getAttachmentDatabase(context).deleteAttachmentFilesForViewOnceMessage(record.getId());
|
|
}
|
|
|
|
MessageNotifier.setLastDesktopActivityTimestamp(envelopeTimestamp);
|
|
MessageNotifier.cancelDelayedNotifications();
|
|
MessageNotifier.updateNotification(context);
|
|
}
|
|
|
|
private void handleMediaMessage(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceDataMessage message,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
throws StorageFailedException
|
|
{
|
|
notifyTypingStoppedFromIncomingMessage(getMessageDestination(content, message), content.getSender(), content.getSenderDevice());
|
|
|
|
Optional<InsertResult> insertResult;
|
|
|
|
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
|
database.beginTransaction();
|
|
|
|
try {
|
|
Optional<QuoteModel> quote = getValidatedQuote(message.getQuote());
|
|
Optional<List<Contact>> sharedContacts = getContacts(message.getSharedContacts());
|
|
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""));
|
|
Optional<Attachment> sticker = getStickerAttachment(message.getSticker());
|
|
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(Recipient.externalPush(context, content.getSender()).getId(),
|
|
message.getTimestamp(), -1,
|
|
message.getExpiresInSeconds() * 1000L, false,
|
|
message.isViewOnce(),
|
|
content.isNeedsReceipt(),
|
|
message.getBody(),
|
|
message.getGroupContext(),
|
|
message.getAttachments(),
|
|
quote,
|
|
sharedContacts,
|
|
linkPreviews,
|
|
sticker);
|
|
|
|
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
|
|
|
if (insertResult.isPresent()) {
|
|
List<DatabaseAttachment> allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId());
|
|
List<DatabaseAttachment> stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList();
|
|
List<DatabaseAttachment> attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList();
|
|
|
|
forceStickerDownloadIfNecessary(stickerAttachments);
|
|
|
|
for (DatabaseAttachment attachment : attachments) {
|
|
ApplicationDependencies.getJobManager().add(new AttachmentDownloadJob(insertResult.get().getMessageId(), attachment.getAttachmentId(), false));
|
|
}
|
|
|
|
if (smsMessageId.isPresent()) {
|
|
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
|
|
}
|
|
|
|
database.setTransactionSuccessful();
|
|
}
|
|
} catch (MmsException e) {
|
|
throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice());
|
|
} finally {
|
|
database.endTransaction();
|
|
}
|
|
|
|
if (insertResult.isPresent()) {
|
|
MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
|
|
|
|
if (message.isViewOnce()) {
|
|
ApplicationContext.getInstance(context).getViewOnceMessageManager().scheduleIfNecessary();
|
|
}
|
|
}
|
|
}
|
|
|
|
private long handleSynchronizeSentExpirationUpdate(@NonNull SentTranscriptMessage message) throws MmsException {
|
|
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
|
Recipient recipient = getSyncMessageDestination(message);
|
|
|
|
OutgoingExpirationUpdateMessage expirationUpdateMessage = new OutgoingExpirationUpdateMessage(recipient,
|
|
message.getTimestamp(),
|
|
message.getMessage().getExpiresInSeconds() * 1000L);
|
|
|
|
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
|
long messageId = database.insertMessageOutbox(expirationUpdateMessage, threadId, false, null);
|
|
|
|
database.markAsSent(messageId, true);
|
|
|
|
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient.getId(), message.getMessage().getExpiresInSeconds());
|
|
|
|
return threadId;
|
|
}
|
|
|
|
private long handleSynchronizeSentMediaMessage(@NonNull SentTranscriptMessage message)
|
|
throws MmsException
|
|
{
|
|
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
|
Recipient recipients = getSyncMessageDestination(message);
|
|
Optional<QuoteModel> quote = getValidatedQuote(message.getMessage().getQuote());
|
|
Optional<Attachment> sticker = getStickerAttachment(message.getMessage().getSticker());
|
|
Optional<List<Contact>> sharedContacts = getContacts(message.getMessage().getSharedContacts());
|
|
Optional<List<LinkPreview>> previews = getLinkPreviews(message.getMessage().getPreviews(), message.getMessage().getBody().or(""));
|
|
boolean viewOnce = message.getMessage().isViewOnce();
|
|
List<Attachment> syncAttachments = viewOnce ? Collections.singletonList(new TombstoneAttachment(MediaUtil.VIEW_ONCE, false))
|
|
: PointerAttachment.forPointers(message.getMessage().getAttachments());
|
|
|
|
if (sticker.isPresent()) {
|
|
syncAttachments.add(sticker.get());
|
|
}
|
|
|
|
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipients, message.getMessage().getBody().orNull(),
|
|
syncAttachments,
|
|
message.getTimestamp(), -1,
|
|
message.getMessage().getExpiresInSeconds() * 1000,
|
|
viewOnce,
|
|
ThreadDatabase.DistributionTypes.DEFAULT, quote.orNull(),
|
|
sharedContacts.or(Collections.emptyList()),
|
|
previews.or(Collections.emptyList()),
|
|
Collections.emptyList(), Collections.emptyList());
|
|
|
|
mediaMessage = new OutgoingSecureMediaMessage(mediaMessage);
|
|
|
|
if (recipients.getExpireMessages() != message.getMessage().getExpiresInSeconds()) {
|
|
handleSynchronizeSentExpirationUpdate(message);
|
|
}
|
|
|
|
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
|
|
|
|
database.beginTransaction();
|
|
|
|
try {
|
|
long messageId = database.insertMessageOutbox(mediaMessage, threadId, false, GroupReceiptDatabase.STATUS_UNKNOWN, null);
|
|
|
|
if (recipients.isGroup()) {
|
|
updateGroupReceiptStatus(message, messageId, recipients.requireGroupId());
|
|
} else {
|
|
database.markUnidentified(messageId, message.isUnidentified(recipients.requireServiceId()));
|
|
}
|
|
|
|
database.markAsSent(messageId, true);
|
|
|
|
List<DatabaseAttachment> allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(messageId);
|
|
List<DatabaseAttachment> stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList();
|
|
List<DatabaseAttachment> attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList();
|
|
|
|
forceStickerDownloadIfNecessary(stickerAttachments);
|
|
|
|
for (DatabaseAttachment attachment : attachments) {
|
|
ApplicationDependencies.getJobManager().add(new AttachmentDownloadJob(messageId, attachment.getAttachmentId(), false));
|
|
}
|
|
|
|
if (message.getMessage().getExpiresInSeconds() > 0) {
|
|
database.markExpireStarted(messageId, message.getExpirationStartTimestamp());
|
|
ApplicationContext.getInstance(context)
|
|
.getExpiringMessageManager()
|
|
.scheduleDeletion(messageId, true,
|
|
message.getExpirationStartTimestamp(),
|
|
message.getMessage().getExpiresInSeconds() * 1000L);
|
|
}
|
|
|
|
if (recipients.isLocalNumber()) {
|
|
SyncMessageId id = new SyncMessageId(recipients.getId(), message.getTimestamp());
|
|
DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(id, System.currentTimeMillis());
|
|
DatabaseFactory.getMmsSmsDatabase(context).incrementReadReceiptCount(id, System.currentTimeMillis());
|
|
}
|
|
|
|
database.setTransactionSuccessful();
|
|
} finally {
|
|
database.endTransaction();
|
|
}
|
|
return threadId;
|
|
}
|
|
|
|
private void handleGroupRecipientUpdate(@NonNull SentTranscriptMessage message) {
|
|
Recipient recipient = getSyncMessageDestination(message);
|
|
|
|
if (!recipient.isGroup()) {
|
|
Log.w(TAG, "Got recipient update for a non-group message! Skipping.");
|
|
return;
|
|
}
|
|
|
|
MmsSmsDatabase database = DatabaseFactory.getMmsSmsDatabase(context);
|
|
MessageRecord record = database.getMessageFor(message.getTimestamp(), Recipient.self().getId());
|
|
|
|
if (record == null) {
|
|
Log.w(TAG, "Got recipient update for non-existing message! Skipping.");
|
|
return;
|
|
}
|
|
|
|
if (!record.isMms()) {
|
|
Log.w(TAG, "Recipient update matched a non-MMS message! Skipping.");
|
|
return;
|
|
}
|
|
|
|
updateGroupReceiptStatus(message, record.getId(), recipient.requireGroupId());
|
|
}
|
|
|
|
private void updateGroupReceiptStatus(@NonNull SentTranscriptMessage message, long messageId, @NonNull GroupId groupString) {
|
|
GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
|
|
List<Recipient> messageRecipients = Stream.of(message.getRecipients()).map(address -> Recipient.externalPush(context, address)).toList();
|
|
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupString, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
|
Map<RecipientId, Integer> localReceipts = Stream.of(receiptDatabase.getGroupReceiptInfo(messageId))
|
|
.collect(Collectors.toMap(GroupReceiptInfo::getRecipientId, GroupReceiptInfo::getStatus));
|
|
|
|
for (Recipient messageRecipient : messageRecipients) {
|
|
//noinspection ConstantConditions
|
|
if (localReceipts.containsKey(messageRecipient.getId()) && localReceipts.get(messageRecipient.getId()) < GroupReceiptDatabase.STATUS_UNDELIVERED) {
|
|
receiptDatabase.update(messageRecipient.getId(), messageId, GroupReceiptDatabase.STATUS_UNDELIVERED, message.getTimestamp());
|
|
} else if (!localReceipts.containsKey(messageRecipient.getId())) {
|
|
receiptDatabase.insert(Collections.singletonList(messageRecipient.getId()), messageId, GroupReceiptDatabase.STATUS_UNDELIVERED, message.getTimestamp());
|
|
}
|
|
}
|
|
|
|
for (Recipient member : members) {
|
|
receiptDatabase.setUnidentified(member.getId(), messageId, message.isUnidentified(member.requireServiceId()));
|
|
}
|
|
}
|
|
|
|
private void handleTextMessage(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceDataMessage message,
|
|
@NonNull Optional<Long> smsMessageId,
|
|
@NonNull Optional<GroupId> groupId)
|
|
throws StorageFailedException
|
|
{
|
|
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
|
String body = message.getBody().isPresent() ? message.getBody().get() : "";
|
|
Recipient recipient = getMessageDestination(content, message);
|
|
|
|
if (message.getExpiresInSeconds() != recipient.getExpireMessages()) {
|
|
handleExpirationUpdate(content, message, Optional.absent());
|
|
}
|
|
|
|
Long threadId;
|
|
|
|
if (smsMessageId.isPresent() && !message.getGroupContext().isPresent()) {
|
|
threadId = database.updateBundleMessageBody(smsMessageId.get(), body).second;
|
|
} else {
|
|
notifyTypingStoppedFromIncomingMessage(recipient, content.getSender(), content.getSenderDevice());
|
|
|
|
IncomingTextMessage textMessage = new IncomingTextMessage(Recipient.externalPush(context, content.getSender()).getId(),
|
|
content.getSenderDevice(),
|
|
message.getTimestamp(), body,
|
|
groupId,
|
|
message.getExpiresInSeconds() * 1000L,
|
|
content.isNeedsReceipt());
|
|
|
|
textMessage = new IncomingEncryptedMessage(textMessage, body);
|
|
Optional<InsertResult> insertResult = database.insertMessageInbox(textMessage);
|
|
|
|
if (insertResult.isPresent()) threadId = insertResult.get().getThreadId();
|
|
else threadId = null;
|
|
|
|
if (smsMessageId.isPresent()) database.deleteMessage(smsMessageId.get());
|
|
}
|
|
|
|
if (threadId != null) {
|
|
MessageNotifier.updateNotification(context, threadId);
|
|
}
|
|
}
|
|
|
|
private long handleSynchronizeSentTextMessage(@NonNull SentTranscriptMessage message)
|
|
throws MmsException
|
|
{
|
|
Recipient recipient = getSyncMessageDestination(message);
|
|
String body = message.getMessage().getBody().or("");
|
|
long expiresInMillis = message.getMessage().getExpiresInSeconds() * 1000L;
|
|
|
|
if (recipient.getExpireMessages() != message.getMessage().getExpiresInSeconds()) {
|
|
handleSynchronizeSentExpirationUpdate(message);
|
|
}
|
|
|
|
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
|
boolean isGroup = recipient.isGroup();
|
|
|
|
MessagingDatabase database;
|
|
long messageId;
|
|
|
|
if (isGroup) {
|
|
OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient, new SlideDeck(), body, message.getTimestamp(), -1, expiresInMillis, false, ThreadDatabase.DistributionTypes.DEFAULT, null, Collections.emptyList(), Collections.emptyList());
|
|
outgoingMediaMessage = new OutgoingSecureMediaMessage(outgoingMediaMessage);
|
|
|
|
messageId = DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(outgoingMediaMessage, threadId, false, GroupReceiptDatabase.STATUS_UNKNOWN, null);
|
|
database = DatabaseFactory.getMmsDatabase(context);
|
|
|
|
updateGroupReceiptStatus(message, messageId, recipient.requireGroupId());
|
|
} else {
|
|
OutgoingTextMessage outgoingTextMessage = new OutgoingEncryptedMessage(recipient, body, expiresInMillis);
|
|
|
|
messageId = DatabaseFactory.getSmsDatabase(context).insertMessageOutbox(threadId, outgoingTextMessage, false, message.getTimestamp(), null);
|
|
database = DatabaseFactory.getSmsDatabase(context);
|
|
database.markUnidentified(messageId, message.isUnidentified(recipient.requireServiceId()));
|
|
}
|
|
|
|
database.markAsSent(messageId, true);
|
|
|
|
if (expiresInMillis > 0) {
|
|
database.markExpireStarted(messageId, message.getExpirationStartTimestamp());
|
|
ApplicationContext.getInstance(context)
|
|
.getExpiringMessageManager()
|
|
.scheduleDeletion(messageId, isGroup, message.getExpirationStartTimestamp(), expiresInMillis);
|
|
}
|
|
|
|
if (recipient.isLocalNumber()) {
|
|
SyncMessageId id = new SyncMessageId(recipient.getId(), message.getTimestamp());
|
|
DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(id, System.currentTimeMillis());
|
|
DatabaseFactory.getMmsSmsDatabase(context).incrementReadReceiptCount(id, System.currentTimeMillis());
|
|
}
|
|
|
|
return threadId;
|
|
}
|
|
|
|
private void handleInvalidVersionMessage(@NonNull String sender, int senderDevice, long timestamp,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
{
|
|
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
|
|
|
if (!smsMessageId.isPresent()) {
|
|
Optional<InsertResult> insertResult = insertPlaceholder(sender, senderDevice, timestamp);
|
|
|
|
if (insertResult.isPresent()) {
|
|
smsDatabase.markAsInvalidVersionKeyExchange(insertResult.get().getMessageId());
|
|
MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
|
|
}
|
|
} else {
|
|
smsDatabase.markAsInvalidVersionKeyExchange(smsMessageId.get());
|
|
}
|
|
}
|
|
|
|
private void handleCorruptMessage(@NonNull String sender, int senderDevice, long timestamp,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
{
|
|
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
|
|
|
if (!smsMessageId.isPresent()) {
|
|
Optional<InsertResult> insertResult = insertPlaceholder(sender, senderDevice, timestamp);
|
|
|
|
if (insertResult.isPresent()) {
|
|
smsDatabase.markAsDecryptFailed(insertResult.get().getMessageId());
|
|
MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
|
|
}
|
|
} else {
|
|
smsDatabase.markAsDecryptFailed(smsMessageId.get());
|
|
}
|
|
}
|
|
|
|
private void handleNoSessionMessage(@NonNull String sender, int senderDevice, long timestamp,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
{
|
|
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
|
|
|
if (!smsMessageId.isPresent()) {
|
|
Optional<InsertResult> insertResult = insertPlaceholder(sender, senderDevice, timestamp);
|
|
|
|
if (insertResult.isPresent()) {
|
|
smsDatabase.markAsNoSession(insertResult.get().getMessageId());
|
|
MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
|
|
}
|
|
} else {
|
|
smsDatabase.markAsNoSession(smsMessageId.get());
|
|
}
|
|
}
|
|
|
|
private void handleUnsupportedDataMessage(@NonNull String sender,
|
|
int senderDevice,
|
|
@NonNull Optional<GroupId> groupId,
|
|
long timestamp,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
{
|
|
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
|
|
|
if (!smsMessageId.isPresent()) {
|
|
Optional<InsertResult> insertResult = insertPlaceholder(sender, senderDevice, timestamp, groupId);
|
|
|
|
if (insertResult.isPresent()) {
|
|
smsDatabase.markAsUnsupportedProtocolVersion(insertResult.get().getMessageId());
|
|
MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
|
|
}
|
|
} else {
|
|
smsDatabase.markAsNoSession(smsMessageId.get());
|
|
}
|
|
}
|
|
|
|
private void handleInvalidMessage(@NonNull SignalServiceAddress sender,
|
|
int senderDevice,
|
|
@NonNull Optional<GroupId> groupId,
|
|
long timestamp,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
{
|
|
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
|
|
|
if (!smsMessageId.isPresent()) {
|
|
Optional<InsertResult> insertResult = insertPlaceholder(sender.getIdentifier(), senderDevice, timestamp, groupId);
|
|
|
|
if (insertResult.isPresent()) {
|
|
smsDatabase.markAsInvalidMessage(insertResult.get().getMessageId());
|
|
MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
|
|
}
|
|
} else {
|
|
smsDatabase.markAsNoSession(smsMessageId.get());
|
|
}
|
|
}
|
|
|
|
private void handleLegacyMessage(@NonNull String sender, int senderDevice, long timestamp,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
{
|
|
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
|
|
|
if (!smsMessageId.isPresent()) {
|
|
Optional<InsertResult> insertResult = insertPlaceholder(sender, senderDevice, timestamp);
|
|
|
|
if (insertResult.isPresent()) {
|
|
smsDatabase.markAsLegacyVersion(insertResult.get().getMessageId());
|
|
MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
|
|
}
|
|
} else {
|
|
smsDatabase.markAsLegacyVersion(smsMessageId.get());
|
|
}
|
|
}
|
|
|
|
@SuppressWarnings("unused")
|
|
private void handleDuplicateMessage(@NonNull String sender, int senderDeviceId, long timestamp,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
{
|
|
// Let's start ignoring these now
|
|
// SmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
|
|
//
|
|
// if (smsMessageId <= 0) {
|
|
// Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
|
|
// smsDatabase.markAsDecryptDuplicate(messageAndThreadId.first);
|
|
// MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
|
|
// } else {
|
|
// smsDatabase.markAsDecryptDuplicate(smsMessageId);
|
|
// }
|
|
}
|
|
|
|
private void handleProfileKey(@NonNull SignalServiceContent content,
|
|
@NonNull byte[] messageProfileKeyBytes)
|
|
{
|
|
RecipientDatabase database = DatabaseFactory.getRecipientDatabase(context);
|
|
Recipient recipient = Recipient.externalPush(context, content.getSender());
|
|
ProfileKey messageProfileKey = ProfileKeyUtil.profileKeyOrNull(messageProfileKeyBytes);
|
|
|
|
if (messageProfileKey != null) {
|
|
if (database.setProfileKey(recipient.getId(), messageProfileKey)) {
|
|
ApplicationDependencies.getJobManager().add(new RetrieveProfileJob(recipient));
|
|
}
|
|
} else {
|
|
Log.w(TAG, "Ignored invalid profile key seen in message");
|
|
}
|
|
}
|
|
|
|
private void handleNeedsDeliveryReceipt(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceDataMessage message)
|
|
{
|
|
ApplicationDependencies.getJobManager().add(new SendDeliveryReceiptJob(Recipient.externalPush(context, content.getSender()).getId(), message.getTimestamp()));
|
|
}
|
|
|
|
@SuppressLint("DefaultLocale")
|
|
private void handleDeliveryReceipt(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceReceiptMessage message)
|
|
{
|
|
for (long timestamp : message.getTimestamps()) {
|
|
Log.i(TAG, String.format("Received encrypted delivery receipt: (XXXXX, %d)", timestamp));
|
|
DatabaseFactory.getMmsSmsDatabase(context)
|
|
.incrementDeliveryReceiptCount(new SyncMessageId(Recipient.externalPush(context, content.getSender()).getId(), timestamp), System.currentTimeMillis());
|
|
}
|
|
}
|
|
|
|
@SuppressLint("DefaultLocale")
|
|
private void handleReadReceipt(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceReceiptMessage message)
|
|
{
|
|
if (TextSecurePreferences.isReadReceiptsEnabled(context)) {
|
|
for (long timestamp : message.getTimestamps()) {
|
|
Log.i(TAG, String.format("Received encrypted read receipt: (XXXXX, %d)", timestamp));
|
|
|
|
DatabaseFactory.getMmsSmsDatabase(context)
|
|
.incrementReadReceiptCount(new SyncMessageId(Recipient.externalPush(context, content.getSender()).getId(), timestamp), content.getTimestamp());
|
|
}
|
|
}
|
|
}
|
|
|
|
private void handleTypingMessage(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceTypingMessage typingMessage)
|
|
{
|
|
if (!TextSecurePreferences.isTypingIndicatorsEnabled(context)) {
|
|
return;
|
|
}
|
|
|
|
Recipient author = Recipient.externalPush(context, content.getSender());
|
|
|
|
long threadId;
|
|
|
|
if (typingMessage.getGroupId().isPresent()) {
|
|
RecipientId recipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(GroupId.v1(typingMessage.getGroupId().get()));
|
|
Recipient groupRecipient = Recipient.resolved(recipientId);
|
|
|
|
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
|
|
} else {
|
|
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(author);
|
|
}
|
|
|
|
if (threadId <= 0) {
|
|
Log.w(TAG, "Couldn't find a matching thread for a typing message.");
|
|
return;
|
|
}
|
|
|
|
if (typingMessage.isTypingStarted()) {
|
|
Log.d(TAG, "Typing started on thread " + threadId);
|
|
ApplicationContext.getInstance(context).getTypingStatusRepository().onTypingStarted(context,threadId, author, content.getSenderDevice());
|
|
} else {
|
|
Log.d(TAG, "Typing stopped on thread " + threadId);
|
|
ApplicationContext.getInstance(context).getTypingStatusRepository().onTypingStopped(context, threadId, author, content.getSenderDevice(), false);
|
|
}
|
|
}
|
|
|
|
private static boolean isInvalidMessage(@NonNull SignalServiceDataMessage message) {
|
|
if (message.isViewOnce()) {
|
|
List<SignalServiceAttachment> attachments = message.getAttachments().or(Collections.emptyList());
|
|
|
|
return attachments.size() != 1 ||
|
|
!isViewOnceSupportedContentType(attachments.get(0).getContentType().toLowerCase());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static boolean isViewOnceSupportedContentType(@NonNull String contentType) {
|
|
return MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType);
|
|
}
|
|
|
|
private Optional<QuoteModel> getValidatedQuote(Optional<SignalServiceDataMessage.Quote> quote) {
|
|
if (!quote.isPresent()) return Optional.absent();
|
|
|
|
if (quote.get().getId() <= 0) {
|
|
Log.w(TAG, "Received quote without an ID! Ignoring...");
|
|
return Optional.absent();
|
|
}
|
|
|
|
if (quote.get().getAuthor() == null) {
|
|
Log.w(TAG, "Received quote without an author! Ignoring...");
|
|
return Optional.absent();
|
|
}
|
|
|
|
RecipientId author = Recipient.externalPush(context, quote.get().getAuthor()).getId();
|
|
MessageRecord message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quote.get().getId(), author);
|
|
|
|
if (message != null) {
|
|
Log.i(TAG, "Found matching message record...");
|
|
|
|
List<Attachment> attachments = new LinkedList<>();
|
|
|
|
if (message.isMms()) {
|
|
MmsMessageRecord mmsMessage = (MmsMessageRecord) message;
|
|
|
|
if (mmsMessage.isViewOnce()) {
|
|
attachments.add(new TombstoneAttachment(MediaUtil.VIEW_ONCE, true));
|
|
} else {
|
|
attachments = mmsMessage.getSlideDeck().asAttachments();
|
|
|
|
if (attachments.isEmpty()) {
|
|
attachments.addAll(Stream.of(mmsMessage.getLinkPreviews())
|
|
.filter(lp -> lp.getThumbnail().isPresent())
|
|
.map(lp -> lp.getThumbnail().get())
|
|
.toList());
|
|
}
|
|
}
|
|
}
|
|
|
|
return Optional.of(new QuoteModel(quote.get().getId(), author, message.getBody(), false, attachments));
|
|
}
|
|
|
|
Log.w(TAG, "Didn't find matching message record...");
|
|
|
|
return Optional.of(new QuoteModel(quote.get().getId(),
|
|
author,
|
|
quote.get().getText(),
|
|
true,
|
|
PointerAttachment.forPointers(quote.get().getAttachments())));
|
|
}
|
|
|
|
private Optional<Attachment> getStickerAttachment(Optional<SignalServiceDataMessage.Sticker> sticker) {
|
|
if (!sticker.isPresent()) {
|
|
return Optional.absent();
|
|
}
|
|
|
|
if (sticker.get().getPackId() == null || sticker.get().getPackKey() == null || sticker.get().getAttachment() == null) {
|
|
Log.w(TAG, "Malformed sticker!");
|
|
return Optional.absent();
|
|
}
|
|
|
|
String packId = Hex.toStringCondensed(sticker.get().getPackId());
|
|
String packKey = Hex.toStringCondensed(sticker.get().getPackKey());
|
|
int stickerId = sticker.get().getStickerId();
|
|
StickerLocator stickerLocator = new StickerLocator(packId, packKey, stickerId);
|
|
StickerDatabase stickerDatabase = DatabaseFactory.getStickerDatabase(context);
|
|
StickerRecord stickerRecord = stickerDatabase.getSticker(stickerLocator.getPackId(), stickerLocator.getStickerId(), false);
|
|
|
|
if (stickerRecord != null) {
|
|
return Optional.of(new UriAttachment(stickerRecord.getUri(),
|
|
stickerRecord.getUri(),
|
|
MediaUtil.IMAGE_WEBP,
|
|
AttachmentDatabase.TRANSFER_PROGRESS_DONE,
|
|
stickerRecord.getSize(),
|
|
StickerSlide.WIDTH,
|
|
StickerSlide.HEIGHT,
|
|
null,
|
|
String.valueOf(new SecureRandom().nextLong()),
|
|
false,
|
|
false,
|
|
null,
|
|
stickerLocator,
|
|
null,
|
|
null));
|
|
} else {
|
|
return Optional.of(PointerAttachment.forPointer(Optional.of(sticker.get().getAttachment()), stickerLocator).get());
|
|
}
|
|
}
|
|
|
|
private static Optional<List<Contact>> getContacts(Optional<List<SharedContact>> sharedContacts) {
|
|
if (!sharedContacts.isPresent()) return Optional.absent();
|
|
|
|
List<Contact> contacts = new ArrayList<>(sharedContacts.get().size());
|
|
|
|
for (SharedContact sharedContact : sharedContacts.get()) {
|
|
contacts.add(ContactModelMapper.remoteToLocal(sharedContact));
|
|
}
|
|
|
|
return Optional.of(contacts);
|
|
}
|
|
|
|
private static Optional<List<LinkPreview>> getLinkPreviews(Optional<List<Preview>> previews, @NonNull String message) {
|
|
if (!previews.isPresent()) return Optional.absent();
|
|
|
|
List<LinkPreview> linkPreviews = new ArrayList<>(previews.get().size());
|
|
|
|
for (Preview preview : previews.get()) {
|
|
Optional<Attachment> thumbnail = PointerAttachment.forPointer(preview.getImage());
|
|
Optional<String> url = Optional.fromNullable(preview.getUrl());
|
|
Optional<String> title = Optional.fromNullable(preview.getTitle());
|
|
boolean hasContent = !TextUtils.isEmpty(title.or("")) || thumbnail.isPresent();
|
|
boolean presentInBody = url.isPresent() && Stream.of(LinkPreviewUtil.findWhitelistedUrls(message)).map(Link::getUrl).collect(Collectors.toSet()).contains(url.get());
|
|
boolean validDomain = url.isPresent() && LinkPreviewUtil.isWhitelistedLinkUrl(url.get());
|
|
|
|
if (hasContent && presentInBody && validDomain) {
|
|
LinkPreview linkPreview = new LinkPreview(url.get(), title.or(""), thumbnail);
|
|
linkPreviews.add(linkPreview);
|
|
} else {
|
|
Log.w(TAG, String.format("Discarding an invalid link preview. hasContent: %b presentInBody: %b validDomain: %b", hasContent, presentInBody, validDomain));
|
|
}
|
|
}
|
|
|
|
return Optional.of(linkPreviews);
|
|
}
|
|
|
|
private Optional<InsertResult> insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp) {
|
|
return insertPlaceholder(sender, senderDevice, timestamp, Optional.absent());
|
|
}
|
|
|
|
private Optional<InsertResult> insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp, Optional<GroupId> groupId) {
|
|
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
|
IncomingTextMessage textMessage = new IncomingTextMessage(Recipient.external(context, sender).getId(),
|
|
senderDevice, timestamp, "",
|
|
groupId, 0, false);
|
|
|
|
textMessage = new IncomingEncryptedMessage(textMessage, "");
|
|
return database.insertMessageInbox(textMessage);
|
|
}
|
|
|
|
private Recipient getSyncMessageDestination(@NonNull SentTranscriptMessage message) {
|
|
return getGroupRecipient(message.getMessage().getGroupContext())
|
|
.or(() -> Recipient.externalPush(context, message.getDestination().get()));
|
|
}
|
|
|
|
private Recipient getMessageDestination(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceDataMessage message)
|
|
{
|
|
return getGroupRecipient(message.getGroupContext())
|
|
.or(() -> Recipient.externalPush(context, content.getSender()));
|
|
}
|
|
|
|
private Optional<Recipient> getGroupRecipient(Optional<SignalServiceGroupContext> message) {
|
|
return message.transform(groupContext -> Recipient.externalGroup(context, GroupUtil.idFromGroupContext(groupContext)));
|
|
}
|
|
|
|
private void notifyTypingStoppedFromIncomingMessage(@NonNull Recipient conversationRecipient, @NonNull SignalServiceAddress sender, int device) {
|
|
Recipient author = Recipient.externalPush(context, sender);
|
|
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(conversationRecipient);
|
|
|
|
if (threadId > 0) {
|
|
Log.d(TAG, "Typing stopped on thread " + threadId + " due to an incoming message.");
|
|
ApplicationContext.getInstance(context).getTypingStatusRepository().onTypingStopped(context, threadId, author, device, true);
|
|
}
|
|
}
|
|
|
|
private boolean shouldIgnore(@Nullable SignalServiceContent content) {
|
|
if (content == null) {
|
|
Log.w(TAG, "Got a message with null content.");
|
|
return true;
|
|
}
|
|
|
|
Recipient sender = Recipient.externalPush(context, content.getSender());
|
|
|
|
if (content.getDataMessage().isPresent()) {
|
|
SignalServiceDataMessage message = content.getDataMessage().get();
|
|
Recipient conversation = getMessageDestination(content, message);
|
|
|
|
if (conversation.isGroup() && conversation.isBlocked()) {
|
|
return true;
|
|
} else if (conversation.isGroup()) {
|
|
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
|
Optional<GroupId> groupId = message.getGroupContext().transform(GroupUtil::idFromGroupContext);
|
|
|
|
if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) {
|
|
return false;
|
|
}
|
|
|
|
boolean isTextMessage = message.getBody().isPresent();
|
|
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent();
|
|
boolean isExpireMessage = message.isExpirationUpdate();
|
|
boolean isContentMessage = !message.isGroupV1Update() && !isExpireMessage && (isTextMessage || isMediaMessage);
|
|
boolean isGroupActive = groupId.isPresent() && groupDatabase.isActive(groupId.get());
|
|
boolean isLeaveMessage = message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1Type() == SignalServiceGroup.Type.QUIT;
|
|
|
|
return (isContentMessage && !isGroupActive) || (sender.isBlocked() && !isLeaveMessage);
|
|
} else {
|
|
return sender.isBlocked();
|
|
}
|
|
} else if (content.getCallMessage().isPresent() || content.getTypingMessage().isPresent()) {
|
|
return sender.isBlocked();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void resetRecipientToPush(@NonNull Recipient recipient) {
|
|
if (recipient.isForceSmsSelection()) {
|
|
DatabaseFactory.getRecipientDatabase(context).setForceSmsSelection(recipient.getId(), false);
|
|
}
|
|
}
|
|
|
|
private void forceStickerDownloadIfNecessary(List<DatabaseAttachment> stickerAttachments) {
|
|
if (stickerAttachments.isEmpty()) return;
|
|
|
|
DatabaseAttachment stickerAttachment = stickerAttachments.get(0);
|
|
|
|
if (stickerAttachment.getTransferState() != AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
|
|
AttachmentDownloadJob downloadJob = new AttachmentDownloadJob(messageId, stickerAttachment.getAttachmentId(), true);
|
|
|
|
try {
|
|
downloadJob.setContext(context);
|
|
downloadJob.doWork();
|
|
} catch (Exception e) {
|
|
Log.w(TAG, "Failed to download sticker inline. Scheduling.");
|
|
ApplicationDependencies.getJobManager().add(downloadJob);
|
|
}
|
|
}
|
|
}
|
|
|
|
@SuppressWarnings("WeakerAccess")
|
|
private static class StorageFailedException extends Exception {
|
|
private final String sender;
|
|
private final int senderDevice;
|
|
|
|
private StorageFailedException(Exception e, String sender, int senderDevice) {
|
|
super(e);
|
|
this.sender = sender;
|
|
this.senderDevice = senderDevice;
|
|
}
|
|
|
|
public String getSender() {
|
|
return sender;
|
|
}
|
|
|
|
public int getSenderDevice() {
|
|
return senderDevice;
|
|
}
|
|
}
|
|
|
|
public static final class Factory implements Job.Factory<PushProcessMessageJob> {
|
|
@Override
|
|
public @NonNull PushProcessMessageJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
|
try {
|
|
MessageState state = MessageState.values()[data.getInt(KEY_MESSAGE_STATE)];
|
|
|
|
if (state == MessageState.DECRYPTED_OK) {
|
|
return new PushProcessMessageJob(parameters,
|
|
state,
|
|
Base64.decode(data.getString(KEY_MESSAGE_PLAINTEXT)),
|
|
null,
|
|
data.getLong(KEY_MESSAGE_ID),
|
|
data.getLong(KEY_SMS_MESSAGE_ID),
|
|
data.getLong(KEY_TIMESTAMP));
|
|
} else {
|
|
ExceptionMetadata exceptionMetadata = new ExceptionMetadata(data.getString(KEY_EXCEPTION_SENDER),
|
|
data.getInt(KEY_EXCEPTION_DEVICE),
|
|
GroupId.parseNullable(data.getStringOrDefault(KEY_EXCEPTION_GROUP_ID, null)));
|
|
|
|
return new PushProcessMessageJob(parameters,
|
|
state,
|
|
null,
|
|
exceptionMetadata,
|
|
data.getLong(KEY_MESSAGE_ID),
|
|
data.getLong(KEY_SMS_MESSAGE_ID),
|
|
data.getLong(KEY_TIMESTAMP));
|
|
}
|
|
} catch (IOException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum MessageState {
|
|
DECRYPTED_OK,
|
|
INVALID_VERSION,
|
|
CORRUPT_MESSAGE,
|
|
NO_SESSION,
|
|
LEGACY_MESSAGE,
|
|
DUPLICATE_MESSAGE,
|
|
UNSUPPORTED_DATA_MESSAGE
|
|
}
|
|
|
|
static class ExceptionMetadata {
|
|
@NonNull private final String sender;
|
|
private final int senderDevice;
|
|
@Nullable private final GroupId groupId;
|
|
|
|
ExceptionMetadata(@NonNull String sender, int senderDevice, @Nullable GroupId groupId) {
|
|
this.sender = sender;
|
|
this.senderDevice = senderDevice;
|
|
this.groupId = groupId;
|
|
}
|
|
|
|
ExceptionMetadata(@NonNull String sender, int senderDevice) {
|
|
this(sender, senderDevice, null);
|
|
}
|
|
}
|
|
}
|