mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
Merge branch 'dev' of https://github.com/loki-project/session-android into specific-group-updates
This commit is contained in:
commit
df0ca11ec5
@ -20,33 +20,13 @@ package org.thoughtcrime.securesms;
|
|||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase.Reader;
|
|
||||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
|
||||||
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
|
|
||||||
import org.thoughtcrime.securesms.jobs.PushDecryptJob;
|
|
||||||
import org.session.libsignal.utilities.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.thoughtcrime.securesms.util.VersionTracker;
|
import org.thoughtcrime.securesms.util.VersionTracker;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.SortedSet;
|
|
||||||
import java.util.TreeSet;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class DatabaseUpgradeActivity extends BaseActivity {
|
public class DatabaseUpgradeActivity extends BaseActivity {
|
||||||
private static final String TAG = DatabaseUpgradeActivity.class.getSimpleName();
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle bundle) {
|
public void onCreate(Bundle bundle) {
|
||||||
|
@ -52,6 +52,8 @@ import androidx.annotation.Nullable;
|
|||||||
|
|
||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
|
import org.session.libsession.messaging.jobs.AttachmentDownloadJob;
|
||||||
|
import org.session.libsession.messaging.jobs.JobQueue;
|
||||||
import org.session.libsession.messaging.opengroups.OpenGroupAPI;
|
import org.session.libsession.messaging.opengroups.OpenGroupAPI;
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
||||||
@ -67,7 +69,6 @@ import org.session.libsession.utilities.views.Stub;
|
|||||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
import org.session.libsignal.libsignal.util.guava.Optional;
|
||||||
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
|
import org.session.libsignal.service.loki.api.opengroups.PublicChat;
|
||||||
import org.session.libsignal.utilities.logging.Log;
|
import org.session.libsignal.utilities.logging.Log;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
|
||||||
import org.thoughtcrime.securesms.BindableConversationItem;
|
import org.thoughtcrime.securesms.BindableConversationItem;
|
||||||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||||
import org.thoughtcrime.securesms.MessageDetailsActivity;
|
import org.thoughtcrime.securesms.MessageDetailsActivity;
|
||||||
@ -85,7 +86,6 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
|||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.Quote;
|
import org.thoughtcrime.securesms.database.model.Quote;
|
||||||
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
|
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||||
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
|
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
|
||||||
import org.thoughtcrime.securesms.loki.views.MessageAudioView;
|
import org.thoughtcrime.securesms.loki.views.MessageAudioView;
|
||||||
@ -1075,10 +1075,10 @@ public class ConversationItem extends LinearLayout
|
|||||||
Log.i(TAG, "Scheduling push attachment downloads for " + slides.size() + " items");
|
Log.i(TAG, "Scheduling push attachment downloads for " + slides.size() + " items");
|
||||||
|
|
||||||
for (Slide slide : slides) {
|
for (Slide slide : slides) {
|
||||||
ApplicationContext.getInstance(context)
|
JobQueue.getShared().add(
|
||||||
.getJobManager()
|
new AttachmentDownloadJob(messageRecord.getId(),
|
||||||
.add(new AttachmentDownloadJob(messageRecord.getId(),
|
((DatabaseAttachment)slide.asAttachment()).getAttachmentId().getRowId())
|
||||||
((DatabaseAttachment)slide.asAttachment()).getAttachmentId(), true));
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,12 +28,14 @@ import com.annimon.stream.Collectors;
|
|||||||
import com.annimon.stream.Stream;
|
import com.annimon.stream.Stream;
|
||||||
import com.google.android.mms.pdu_alt.NotificationInd;
|
import com.google.android.mms.pdu_alt.NotificationInd;
|
||||||
import com.google.android.mms.pdu_alt.PduHeaders;
|
import com.google.android.mms.pdu_alt.PduHeaders;
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.sqlcipher.database.SQLiteDatabase;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
import org.session.libsession.utilities.GroupUtil;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment;
|
import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment;
|
||||||
import org.session.libsession.database.documents.IdentityKeyMismatch;
|
import org.session.libsession.database.documents.IdentityKeyMismatch;
|
||||||
@ -686,7 +688,14 @@ public class MmsDatabase extends MessagingDatabase {
|
|||||||
throws MmsException
|
throws MmsException
|
||||||
{
|
{
|
||||||
if (threadId == -1) {
|
if (threadId == -1) {
|
||||||
threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(retrieved.getRecipient());
|
if(retrieved.isGroup()) {
|
||||||
|
ByteString decodedGroupId = ((OutgoingGroupMediaMessage)retrieved).getGroupContext().getId();
|
||||||
|
String groupId = GroupUtil.doubleEncodeGroupID(decodedGroupId.toByteArray());
|
||||||
|
Recipient group = Recipient.from(context, Address.fromSerialized(groupId), false);
|
||||||
|
threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(group);
|
||||||
|
} else {
|
||||||
|
threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(retrieved.getRecipient());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
long messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp);
|
long messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp);
|
||||||
if (messageId == -1) {
|
if (messageId == -1) {
|
||||||
|
@ -424,7 +424,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
.setName(name)
|
.setName(name)
|
||||||
.addAllMembers(members)
|
.addAllMembers(members)
|
||||||
.addAllAdmins(admins)
|
.addAllAdmins(admins)
|
||||||
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, sentTimestamp, 0, null, listOf(), listOf())
|
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, sentTimestamp, 0, false, null, listOf(), listOf())
|
||||||
val mmsDB = DatabaseFactory.getMmsDatabase(context)
|
val mmsDB = DatabaseFactory.getMmsDatabase(context)
|
||||||
val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context)
|
val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context)
|
||||||
if (mmsSmsDB.getMessageFor(sentTimestamp,userPublicKey) != null) return
|
if (mmsSmsDB.getMessageFor(sentTimestamp,userPublicKey) != null) return
|
||||||
|
@ -3,9 +3,7 @@ package org.thoughtcrime.securesms.dependencies;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
|
import org.session.libsignal.service.api.SignalServiceMessageReceiver;
|
||||||
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
|
|
||||||
import org.thoughtcrime.securesms.jobs.AvatarDownloadJob;
|
import org.thoughtcrime.securesms.jobs.AvatarDownloadJob;
|
||||||
import org.thoughtcrime.securesms.jobs.PushDecryptJob;
|
|
||||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
|
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
||||||
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
||||||
@ -13,11 +11,9 @@ import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
|||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
|
|
||||||
@Module(complete = false, injects = {AttachmentDownloadJob.class,
|
@Module(complete = false, injects = {AvatarDownloadJob.class,
|
||||||
AvatarDownloadJob.class,
|
|
||||||
RetrieveProfileAvatarJob.class,
|
RetrieveProfileAvatarJob.class,
|
||||||
AppProtectionPreferenceFragment.class,
|
AppProtectionPreferenceFragment.class,
|
||||||
PushDecryptJob.class,
|
|
||||||
LinkPreviewRepository.class})
|
LinkPreviewRepository.class})
|
||||||
|
|
||||||
public class SignalCommunicationModule {
|
public class SignalCommunicationModule {
|
||||||
|
@ -1,283 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.jobs;
|
|
||||||
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.VisibleForTesting;
|
|
||||||
|
|
||||||
import org.greenrobot.eventbus.EventBus;
|
|
||||||
import org.session.libsession.messaging.jobs.Data;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
|
||||||
import org.session.libsignal.libsignal.InvalidMessageException;
|
|
||||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
|
||||||
import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream;
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer;
|
|
||||||
import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException;
|
|
||||||
import org.session.libsignal.service.api.push.exceptions.PushNetworkException;
|
|
||||||
import org.session.libsignal.service.loki.utilities.DownloadUtilities;
|
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
||||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
|
||||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
|
||||||
import org.session.libsignal.utilities.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.mms.MmsException;
|
|
||||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
|
||||||
import org.session.libsignal.utilities.Base64;
|
|
||||||
import org.session.libsignal.utilities.Hex;
|
|
||||||
import org.session.libsession.utilities.Util;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
public class AttachmentDownloadJob extends BaseJob implements InjectableType {
|
|
||||||
|
|
||||||
public static final String KEY = "AttachmentDownloadJob";
|
|
||||||
|
|
||||||
private static final int MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024;
|
|
||||||
private static final String TAG = AttachmentDownloadJob.class.getSimpleName();
|
|
||||||
|
|
||||||
private static final String KEY_MESSAGE_ID = "message_id";
|
|
||||||
private static final String KEY_PART_ROW_ID = "part_row_id";
|
|
||||||
private static final String KEY_PAR_UNIQUE_ID = "part_unique_id";
|
|
||||||
private static final String KEY_MANUAL = "part_manual";
|
|
||||||
|
|
||||||
private long messageId;
|
|
||||||
private long partRowId;
|
|
||||||
private long partUniqueId;
|
|
||||||
private boolean manual;
|
|
||||||
|
|
||||||
public AttachmentDownloadJob(long messageId, AttachmentId attachmentId, boolean manual) {
|
|
||||||
this(new Job.Parameters.Builder()
|
|
||||||
.setQueue("AttachmentDownloadJob" + attachmentId.getRowId() + "-" + attachmentId.getUniqueId())
|
|
||||||
.addConstraint(NetworkConstraint.KEY)
|
|
||||||
.setMaxAttempts(5)
|
|
||||||
.build(),
|
|
||||||
messageId,
|
|
||||||
attachmentId,
|
|
||||||
manual);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private AttachmentDownloadJob(@NonNull Job.Parameters parameters, long messageId, AttachmentId attachmentId, boolean manual) {
|
|
||||||
super(parameters);
|
|
||||||
|
|
||||||
this.messageId = messageId;
|
|
||||||
this.partRowId = attachmentId.getRowId();
|
|
||||||
this.partUniqueId = attachmentId.getUniqueId();
|
|
||||||
this.manual = manual;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NonNull
|
|
||||||
Data serialize() {
|
|
||||||
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId)
|
|
||||||
.putLong(KEY_PART_ROW_ID, partRowId)
|
|
||||||
.putLong(KEY_PAR_UNIQUE_ID, partUniqueId)
|
|
||||||
.putBoolean(KEY_MANUAL, manual)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NonNull String getFactoryKey() {
|
|
||||||
return KEY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAdded() {
|
|
||||||
Log.i(TAG, "onAdded() messageId: " + messageId + " partRowId: " + partRowId + " partUniqueId: " + partUniqueId + " manual: " + manual);
|
|
||||||
|
|
||||||
final AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
|
|
||||||
final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId);
|
|
||||||
final DatabaseAttachment attachment = database.getAttachment(attachmentId);
|
|
||||||
final boolean pending = attachment != null && attachment.getTransferState() != AttachmentTransferProgress.TRANSFER_PROGRESS_DONE;
|
|
||||||
|
|
||||||
if (pending && (manual || AttachmentUtil.isAutoDownloadPermitted(context, attachment))) {
|
|
||||||
Log.i(TAG, "onAdded() Marking attachment progress as 'started'");
|
|
||||||
database.setTransferState(messageId, attachmentId, AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRun() throws IOException {
|
|
||||||
doWork();
|
|
||||||
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void doWork() throws IOException {
|
|
||||||
Log.i(TAG, "onRun() messageId: " + messageId + " partRowId: " + partRowId + " partUniqueId: " + partUniqueId + " manual: " + manual);
|
|
||||||
|
|
||||||
final AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
|
|
||||||
final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId);
|
|
||||||
final DatabaseAttachment attachment = database.getAttachment(attachmentId);
|
|
||||||
|
|
||||||
if (attachment == null) {
|
|
||||||
Log.w(TAG, "attachment no longer exists.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!attachment.isInProgress()) {
|
|
||||||
Log.w(TAG, "Attachment was already downloaded.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!manual && !AttachmentUtil.isAutoDownloadPermitted(context, attachment)) {
|
|
||||||
Log.w(TAG, "Attachment can't be auto downloaded...");
|
|
||||||
database.setTransferState(messageId, attachmentId, AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(TAG, "Downloading push part " + attachmentId);
|
|
||||||
database.setTransferState(messageId, attachmentId, AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED);
|
|
||||||
|
|
||||||
retrieveAttachment(messageId, attachmentId, attachment);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCanceled() {
|
|
||||||
Log.w(TAG, "onCanceled() messageId: " + messageId + " partRowId: " + partRowId + " partUniqueId: " + partUniqueId + " manual: " + manual);
|
|
||||||
|
|
||||||
final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId);
|
|
||||||
markFailed(messageId, attachmentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean onShouldRetry(@NonNull Exception exception) {
|
|
||||||
return (exception instanceof PushNetworkException);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void retrieveAttachment(long messageId,
|
|
||||||
final AttachmentId attachmentId,
|
|
||||||
final Attachment attachment)
|
|
||||||
throws IOException
|
|
||||||
{
|
|
||||||
|
|
||||||
AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
|
|
||||||
File attachmentFile = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
attachmentFile = createTempFile();
|
|
||||||
|
|
||||||
SignalServiceAttachmentPointer pointer = createAttachmentPointer(attachment);
|
|
||||||
// InputStream stream = messageReceiver.retrieveAttachment(pointer, attachmentFile, MAX_ATTACHMENT_SIZE, (total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress)));
|
|
||||||
|
|
||||||
if (pointer.getUrl().isEmpty()) throw new InvalidMessageException("Missing attachment URL.");
|
|
||||||
|
|
||||||
DownloadUtilities.downloadFile(attachmentFile, pointer.getUrl(), MAX_ATTACHMENT_SIZE, (total, progress) -> {
|
|
||||||
EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress));
|
|
||||||
});
|
|
||||||
|
|
||||||
final InputStream stream;
|
|
||||||
// Assume we're retrieving an attachment for an open group server if the digest is not set
|
|
||||||
if (!pointer.getDigest().isPresent()) {
|
|
||||||
stream = new FileInputStream(attachmentFile);
|
|
||||||
} else {
|
|
||||||
stream = AttachmentCipherInputStream.createForAttachment(attachmentFile, pointer.getSize().or(0), pointer.getKey(), pointer.getDigest().get());
|
|
||||||
}
|
|
||||||
|
|
||||||
database.insertAttachmentsForPlaceholder(messageId, attachmentId, stream);
|
|
||||||
} catch (InvalidPartException | NonSuccessfulResponseCodeException | InvalidMessageException | MmsException e) {
|
|
||||||
Log.w(TAG, "Experienced exception while trying to download an attachment.", e);
|
|
||||||
markFailed(messageId, attachmentId);
|
|
||||||
} finally {
|
|
||||||
if (attachmentFile != null) {
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
attachmentFile.delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
SignalServiceAttachmentPointer createAttachmentPointer(Attachment attachment)
|
|
||||||
throws InvalidPartException
|
|
||||||
{
|
|
||||||
boolean isOpenGroupContext = TextUtils.isEmpty(attachment.getKey()) && attachment.getDigest() == null;
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(attachment.getLocation())) {
|
|
||||||
throw new InvalidPartException("empty content id");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(attachment.getKey()) && !isOpenGroupContext) {
|
|
||||||
throw new InvalidPartException("empty encrypted key");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
long id = Long.parseLong(attachment.getLocation());
|
|
||||||
if (isOpenGroupContext) {
|
|
||||||
return new SignalServiceAttachmentPointer(id,
|
|
||||||
null,
|
|
||||||
new byte[0],
|
|
||||||
Optional.of(Util.toIntExact(attachment.getSize())),
|
|
||||||
Optional.absent(),
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
Optional.fromNullable(attachment.getDigest()),
|
|
||||||
Optional.fromNullable(attachment.getFileName()),
|
|
||||||
attachment.isVoiceNote(),
|
|
||||||
Optional.absent(), attachment.getUrl());
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] key = Base64.decode(attachment.getKey());
|
|
||||||
|
|
||||||
if (attachment.getDigest() != null) {
|
|
||||||
Log.i(TAG, "Downloading attachment with digest: " + Hex.toString(attachment.getDigest()));
|
|
||||||
} else {
|
|
||||||
Log.i(TAG, "Downloading attachment with no digest...");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SignalServiceAttachmentPointer(id, null, key,
|
|
||||||
Optional.of(Util.toIntExact(attachment.getSize())),
|
|
||||||
Optional.absent(),
|
|
||||||
0, 0,
|
|
||||||
Optional.fromNullable(attachment.getDigest()),
|
|
||||||
Optional.fromNullable(attachment.getFileName()),
|
|
||||||
attachment.isVoiceNote(),
|
|
||||||
Optional.absent(), attachment.getUrl());
|
|
||||||
} catch (IOException | ArithmeticException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
throw new InvalidPartException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private File createTempFile() throws InvalidPartException {
|
|
||||||
try {
|
|
||||||
File file = File.createTempFile("push-attachment", "tmp", context.getCacheDir());
|
|
||||||
file.deleteOnExit();
|
|
||||||
|
|
||||||
return file;
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new InvalidPartException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void markFailed(long messageId, AttachmentId attachmentId) {
|
|
||||||
try {
|
|
||||||
AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context);
|
|
||||||
database.setTransferProgressFailed(attachmentId, messageId);
|
|
||||||
} catch (MmsException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting static class InvalidPartException extends Exception {
|
|
||||||
InvalidPartException(String s) {super(s);}
|
|
||||||
InvalidPartException(Exception e) {super(e);}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class Factory implements Job.Factory<AttachmentDownloadJob> {
|
|
||||||
@Override
|
|
||||||
public @NonNull AttachmentDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
|
||||||
return new AttachmentDownloadJob(parameters,
|
|
||||||
data.getLong(KEY_MESSAGE_ID),
|
|
||||||
new AttachmentId(data.getLong(KEY_PART_ROW_ID), data.getLong(KEY_PAR_UNIQUE_ID)),
|
|
||||||
data.getBoolean(KEY_MANUAL));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -29,11 +29,8 @@ public final class JobManagerFactories {
|
|||||||
|
|
||||||
public static Map<String, Job.Factory> getJobFactories(@NonNull Application application) {
|
public static Map<String, Job.Factory> getJobFactories(@NonNull Application application) {
|
||||||
HashMap<String, Job.Factory> factoryHashMap = new HashMap<String, Job.Factory>() {{
|
HashMap<String, Job.Factory> factoryHashMap = new HashMap<String, Job.Factory>() {{
|
||||||
put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory());
|
|
||||||
put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory());
|
put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory());
|
||||||
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
|
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
|
||||||
put(PushContentReceiveJob.KEY, new PushContentReceiveJob.Factory());
|
|
||||||
put(PushDecryptJob.KEY, new PushDecryptJob.Factory());
|
|
||||||
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory(application));
|
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory(application));
|
||||||
put(TrimThreadJob.KEY, new TrimThreadJob.Factory());
|
put(TrimThreadJob.KEY, new TrimThreadJob.Factory());
|
||||||
put(UpdateApkJob.KEY, new UpdateApkJob.Factory());
|
put(UpdateApkJob.KEY, new UpdateApkJob.Factory());
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.jobs;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.jobs.Data;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
|
||||||
|
|
||||||
public class PushContentReceiveJob extends PushReceivedJob {
|
|
||||||
|
|
||||||
public static final String KEY = "PushContentReceiveJob";
|
|
||||||
|
|
||||||
public PushContentReceiveJob(Context context) {
|
|
||||||
this(new Job.Parameters.Builder().build());
|
|
||||||
setContext(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private PushContentReceiveJob(@NonNull Job.Parameters parameters) {
|
|
||||||
super(parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NonNull
|
|
||||||
Data serialize() {
|
|
||||||
return Data.EMPTY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NonNull String getFactoryKey() {
|
|
||||||
return KEY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRun() { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCanceled() { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onShouldRetry(@NonNull Exception exception) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class Factory implements Job.Factory<PushContentReceiveJob> {
|
|
||||||
@Override
|
|
||||||
public @NonNull PushContentReceiveJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
|
||||||
return new PushContentReceiveJob(parameters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,855 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.jobs;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
|
||||||
|
|
||||||
import com.annimon.stream.Collectors;
|
|
||||||
import com.annimon.stream.Stream;
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.jobs.Data;
|
|
||||||
import org.session.libsession.messaging.threads.DistributionTypes;
|
|
||||||
import org.session.libsignal.metadata.InvalidMetadataMessageException;
|
|
||||||
import org.session.libsignal.metadata.ProtocolInvalidMessageException;
|
|
||||||
import org.session.libsignal.service.api.crypto.SignalServiceCipher;
|
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
|
|
||||||
import org.session.libsession.messaging.threads.Address;
|
|
||||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
|
||||||
import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
|
|
||||||
import org.session.libsession.utilities.GroupUtil;
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
|
|
||||||
import org.session.libsession.utilities.IdentityKeyUtil;
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
|
||||||
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.NoSuchMessageException;
|
|
||||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
|
||||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
|
||||||
import org.thoughtcrime.securesms.linkpreview.Link;
|
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
|
||||||
import org.session.libsignal.utilities.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
|
||||||
import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl;
|
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase;
|
|
||||||
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
|
|
||||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2;
|
|
||||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol;
|
|
||||||
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
|
|
||||||
import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities;
|
|
||||||
import org.session.libsession.messaging.messages.signal.IncomingMediaMessage;
|
|
||||||
import org.thoughtcrime.securesms.mms.MmsException;
|
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
|
||||||
import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage;
|
|
||||||
import org.session.libsession.messaging.messages.signal.IncomingTextMessage;
|
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage;
|
|
||||||
import org.session.libsignal.libsignal.util.guava.Optional;
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceContent;
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceDataMessage.Preview;
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceEnvelope;
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceGroup;
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage;
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceTypingMessage;
|
|
||||||
import org.session.libsignal.service.api.messages.shared.SharedContact;
|
|
||||||
import org.session.libsignal.service.loki.utilities.mentions.MentionsManager;
|
|
||||||
import org.session.libsignal.service.loki.utilities.PublicKeyValidation;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
|
||||||
|
|
||||||
public class PushDecryptJob extends BaseJob implements InjectableType {
|
|
||||||
|
|
||||||
public static final String KEY = "PushDecryptJob";
|
|
||||||
|
|
||||||
public static final String TAG = PushDecryptJob.class.getSimpleName();
|
|
||||||
|
|
||||||
private static final String KEY_MESSAGE_ID = "message_id";
|
|
||||||
private static final String KEY_SMS_MESSAGE_ID = "sms_message_id";
|
|
||||||
|
|
||||||
private long messageId;
|
|
||||||
private long smsMessageId;
|
|
||||||
|
|
||||||
private MessageNotifier messageNotifier;
|
|
||||||
|
|
||||||
public PushDecryptJob(Context context) {
|
|
||||||
this(context, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public PushDecryptJob(Context context, long pushMessageId) {
|
|
||||||
this(context, pushMessageId, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public PushDecryptJob(Context context, long pushMessageId, long smsMessageId) {
|
|
||||||
this(new Job.Parameters.Builder()
|
|
||||||
.setQueue("__PUSH_DECRYPT_JOB__")
|
|
||||||
.setMaxAttempts(10)
|
|
||||||
.build(),
|
|
||||||
pushMessageId,
|
|
||||||
smsMessageId);
|
|
||||||
setContext(context);
|
|
||||||
this.messageNotifier = ApplicationContext.getInstance(context).messageNotifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
private PushDecryptJob(@NonNull Job.Parameters parameters, long pushMessageId, long smsMessageId) {
|
|
||||||
super(parameters);
|
|
||||||
|
|
||||||
this.messageId = pushMessageId;
|
|
||||||
this.smsMessageId = smsMessageId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NonNull
|
|
||||||
Data serialize() {
|
|
||||||
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId)
|
|
||||||
.putLong(KEY_SMS_MESSAGE_ID, smsMessageId)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NonNull String getFactoryKey() {
|
|
||||||
return KEY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRun() throws NoSuchMessageException {
|
|
||||||
synchronized (PushReceivedJob.RECEIVE_LOCK) {
|
|
||||||
if (needsMigration()) {
|
|
||||||
Log.w(TAG, "Skipping, waiting for migration...");
|
|
||||||
postMigrationNotification();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PushDatabase database = DatabaseFactory.getPushDatabase(context);
|
|
||||||
SignalServiceEnvelope envelope = database.get(messageId);
|
|
||||||
Optional<Long> optionalSmsMessageId = smsMessageId > 0 ? Optional.of(smsMessageId) : Optional.absent();
|
|
||||||
|
|
||||||
handleMessage(envelope, optionalSmsMessageId, false);
|
|
||||||
database.delete(messageId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onShouldRetry(@NonNull Exception exception) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCanceled() { }
|
|
||||||
|
|
||||||
public void processMessage(@NonNull SignalServiceEnvelope envelope, boolean isPushNotification) {
|
|
||||||
synchronized (PushReceivedJob.RECEIVE_LOCK) {
|
|
||||||
if (needsMigration()) {
|
|
||||||
Log.w(TAG, "Skipping and storing envelope, waiting for migration...");
|
|
||||||
DatabaseFactory.getPushDatabase(context).insert(envelope);
|
|
||||||
postMigrationNotification();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMessage(envelope, Optional.absent(), isPushNotification);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean needsMigration() {
|
|
||||||
return !IdentityKeyUtil.hasIdentityKey(context) || TextSecurePreferences.getNeedsSqlCipherMigration(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void postMigrationNotification() {
|
|
||||||
NotificationManagerCompat.from(context).notify(494949,
|
|
||||||
new NotificationCompat.Builder(context, NotificationChannels.getMessagesChannel(context))
|
|
||||||
.setSmallIcon(R.drawable.ic_notification)
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
||||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
|
||||||
.setContentTitle(context.getString(R.string.PushDecryptJob_new_locked_message))
|
|
||||||
.setContentText(context.getString(R.string.PushDecryptJob_unlock_to_view_pending_messages))
|
|
||||||
.setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, HomeActivity.class), 0))
|
|
||||||
.setDefaults(NotificationCompat.DEFAULT_SOUND | NotificationCompat.DEFAULT_VIBRATE)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleMessage(@NonNull SignalServiceEnvelope envelope, @NonNull Optional<Long> smsMessageId, boolean isPushNotification) {
|
|
||||||
try {
|
|
||||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
|
||||||
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(context);
|
|
||||||
SignalServiceCipher cipher = new SignalServiceCipher(new SessionProtocolImpl(context), apiDB);
|
|
||||||
|
|
||||||
SignalServiceContent content = cipher.decrypt(envelope);
|
|
||||||
|
|
||||||
if (shouldIgnore(content)) {
|
|
||||||
Log.i(TAG, "Ignoring message.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SessionMetaProtocol.handleProfileUpdateIfNeeded(context, content);
|
|
||||||
|
|
||||||
if (content.configurationMessageProto.isPresent()) {
|
|
||||||
MultiDeviceProtocol.handleConfigurationMessage(context, content.configurationMessageProto.get(), content.getSender(), content.getTimestamp());
|
|
||||||
} else if (content.getDataMessage().isPresent()) {
|
|
||||||
SignalServiceDataMessage message = content.getDataMessage().get();
|
|
||||||
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent();
|
|
||||||
|
|
||||||
if (message.getClosedGroupControlMessage().isPresent()) {
|
|
||||||
ClosedGroupsProtocolV2.handleMessage(context, message.getClosedGroupControlMessage().get(), message.getTimestamp(), envelope.getSource(), content.getSender());
|
|
||||||
}
|
|
||||||
if (message.isExpirationUpdate()) {
|
|
||||||
handleExpirationUpdate(content, message, smsMessageId);
|
|
||||||
} else if (isMediaMessage) {
|
|
||||||
handleMediaMessage(content, message, smsMessageId, Optional.absent());
|
|
||||||
} else if (message.getBody().isPresent()) {
|
|
||||||
handleTextMessage(content, message, smsMessageId, Optional.absent());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
|
|
||||||
SessionMetaProtocol.handleProfileKeyUpdate(context, content);
|
|
||||||
}
|
|
||||||
} 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.from(context, Address.fromSerialized(content.getSender()), false));
|
|
||||||
} catch (ProtocolInvalidMessageException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
if (!isPushNotification) { // This can be triggered if a PN encrypted with an old session comes in after the user performed a session reset
|
|
||||||
handleCorruptMessage(e.getSender(), e.getSenderDevice(), envelope.getTimestamp(), smsMessageId, e);
|
|
||||||
}
|
|
||||||
}catch (StorageFailedException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
handleCorruptMessage(e.getSender(), e.getSenderDevice(), envelope.getTimestamp(), smsMessageId, e);
|
|
||||||
} catch (InvalidMetadataMessageException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleExpirationUpdate(@NonNull SignalServiceContent content,
|
|
||||||
@NonNull SignalServiceDataMessage message,
|
|
||||||
@NonNull Optional<Long> smsMessageId)
|
|
||||||
throws StorageFailedException
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
|
||||||
Recipient recipient = getMessageDestination(content, message);
|
|
||||||
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(getMessageMasterDestination(content.getSender()).getAddress(),
|
|
||||||
message.getTimestamp(), -1,
|
|
||||||
message.getExpiresInSeconds() * 1000L, true,
|
|
||||||
content.isNeedsReceipt(),
|
|
||||||
Optional.absent(),
|
|
||||||
message.getGroupInfo(),
|
|
||||||
Optional.absent(),
|
|
||||||
Optional.absent(),
|
|
||||||
Optional.absent(),
|
|
||||||
Optional.absent());
|
|
||||||
|
|
||||||
database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
|
||||||
|
|
||||||
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient, message.getExpiresInSeconds());
|
|
||||||
|
|
||||||
if (smsMessageId.isPresent()) {
|
|
||||||
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
|
|
||||||
}
|
|
||||||
} catch (MmsException e) {
|
|
||||||
throw new StorageFailedException(e, content.getSender(), content.getSenderDevice());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void handleMediaMessage(@NonNull SignalServiceContent content,
|
|
||||||
@NonNull SignalServiceDataMessage message,
|
|
||||||
@NonNull Optional<Long> smsMessageId,
|
|
||||||
@NonNull Optional<Long> messageServerIDOrNull)
|
|
||||||
throws StorageFailedException
|
|
||||||
{
|
|
||||||
Recipient originalRecipient = getMessageDestination(content, message);
|
|
||||||
Recipient masterRecipient = getMessageMasterDestination(content.getSender());
|
|
||||||
String syncTarget = message.getSyncTarget().orNull();
|
|
||||||
|
|
||||||
|
|
||||||
notifyTypingStoppedFromIncomingMessage(masterRecipient, content.getSender(), content.getSenderDevice());
|
|
||||||
|
|
||||||
Optional<QuoteModel> quote = getValidatedQuote(message.getQuote());
|
|
||||||
Optional<List<Contact>> sharedContacts = getContacts(message.getSharedContacts());
|
|
||||||
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""));
|
|
||||||
|
|
||||||
Address masterAddress = masterRecipient.getAddress();
|
|
||||||
|
|
||||||
if (message.isGroupMessage()) {
|
|
||||||
masterAddress = getMessageMasterDestination(content.getSender()).getAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle sync message from ourselves
|
|
||||||
if (syncTarget != null && !syncTarget.isEmpty() || TextSecurePreferences.getLocalNumber(context).equals(content.getSender())) {
|
|
||||||
Address targetAddress = masterRecipient.getAddress();
|
|
||||||
if (message.getGroupInfo().isPresent()) {
|
|
||||||
targetAddress = Address.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get()));
|
|
||||||
} else if (syncTarget != null && !syncTarget.isEmpty()) {
|
|
||||||
targetAddress = Address.fromSerialized(syncTarget);
|
|
||||||
}
|
|
||||||
List<Attachment> attachments = PointerAttachment.forPointers(message.getAttachments());
|
|
||||||
|
|
||||||
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(masterRecipient, message.getBody().orNull(),
|
|
||||||
attachments,
|
|
||||||
message.getTimestamp(), -1,
|
|
||||||
message.getExpiresInSeconds() * 1000,
|
|
||||||
false,
|
|
||||||
DistributionTypes.DEFAULT, quote.orNull(),
|
|
||||||
sharedContacts.or(Collections.emptyList()),
|
|
||||||
linkPreviews.or(Collections.emptyList()),
|
|
||||||
Collections.emptyList(), Collections.emptyList());
|
|
||||||
|
|
||||||
if (DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(message.getTimestamp(), targetAddress) != null) {
|
|
||||||
Log.d("Loki","Message already exists, don't insert again");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
|
||||||
database.beginTransaction();
|
|
||||||
|
|
||||||
// Ignore message if it has no body and no attachments
|
|
||||||
if (mediaMessage.getBody().isEmpty() && mediaMessage.getAttachments().isEmpty() && mediaMessage.getLinkPreviews().isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<InsertResult> insertResult;
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
// Check if we have the thread already
|
|
||||||
long threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(targetAddress.serialize());
|
|
||||||
|
|
||||||
insertResult = database.insertSecureDecryptedMessageOutbox(mediaMessage, threadID, content.getTimestamp());
|
|
||||||
|
|
||||||
if (insertResult.isPresent()) {
|
|
||||||
List<DatabaseAttachment> allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId());
|
|
||||||
List<DatabaseAttachment> dbAttachments = Stream.of(allAttachments).toList();
|
|
||||||
|
|
||||||
for (DatabaseAttachment attachment : dbAttachments) {
|
|
||||||
ApplicationContext.getInstance(context).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(), content.getSenderDevice());
|
|
||||||
} finally {
|
|
||||||
database.endTransaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterAddress, message.getTimestamp(), -1,
|
|
||||||
message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(),
|
|
||||||
quote, sharedContacts, linkPreviews);
|
|
||||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
|
||||||
database.beginTransaction();
|
|
||||||
|
|
||||||
// Ignore message if it has no body and no attachments
|
|
||||||
if (mediaMessage.getBody().isEmpty() && mediaMessage.getAttachments().isEmpty() && mediaMessage.getLinkPreviews().isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<InsertResult> insertResult;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (message.isGroupMessage()) {
|
|
||||||
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1, content.getTimestamp());
|
|
||||||
} else {
|
|
||||||
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (insertResult.isPresent()) {
|
|
||||||
List<DatabaseAttachment> allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId());
|
|
||||||
List<DatabaseAttachment> attachments = Stream.of(allAttachments).toList();
|
|
||||||
|
|
||||||
for (DatabaseAttachment attachment : attachments) {
|
|
||||||
ApplicationContext.getInstance(context).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(), content.getSenderDevice());
|
|
||||||
} finally {
|
|
||||||
database.endTransaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (insertResult.isPresent()) {
|
|
||||||
messageNotifier.updateNotification(context, insertResult.get().getThreadId());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (insertResult.isPresent()) {
|
|
||||||
InsertResult result = insertResult.get();
|
|
||||||
|
|
||||||
// Loki - Cache the user hex encoded public key (for mentions)
|
|
||||||
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(result.getThreadId(), context);
|
|
||||||
MentionsManager.shared.cache(content.getSender(), result.getThreadId());
|
|
||||||
|
|
||||||
// Loki - Store message open group server ID if needed
|
|
||||||
if (messageServerIDOrNull.isPresent()) {
|
|
||||||
long messageID = result.getMessageId();
|
|
||||||
long messageServerID = messageServerIDOrNull.get();
|
|
||||||
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
|
|
||||||
lokiMessageDatabase.setServerID(messageID, messageServerID);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loki - Update mapping of message ID to original thread ID
|
|
||||||
if (result.getMessageId() > -1) {
|
|
||||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
|
||||||
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
|
|
||||||
long originalThreadId = threadDatabase.getOrCreateThreadIdFor(originalRecipient);
|
|
||||||
lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void handleTextMessage(@NonNull SignalServiceContent content,
|
|
||||||
@NonNull SignalServiceDataMessage message,
|
|
||||||
@NonNull Optional<Long> smsMessageId,
|
|
||||||
@NonNull Optional<Long> messageServerIDOrNull)
|
|
||||||
throws StorageFailedException
|
|
||||||
{
|
|
||||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
|
||||||
String body = message.getBody().isPresent() ? message.getBody().get() : "";
|
|
||||||
Recipient originalRecipient = getMessageDestination(content, message);
|
|
||||||
Recipient masterRecipient = getMessageMasterDestination(content.getSender());
|
|
||||||
String syncTarget = message.getSyncTarget().orNull();
|
|
||||||
|
|
||||||
Long threadId = null;
|
|
||||||
|
|
||||||
if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) {
|
|
||||||
threadId = database.updateBundleMessageBody(smsMessageId.get(), body).second;
|
|
||||||
} else if (syncTarget != null && !syncTarget.isEmpty() || TextSecurePreferences.getLocalNumber(context).equals(content.getSender())) {
|
|
||||||
Address targetAddress = masterRecipient.getAddress();
|
|
||||||
if (message.getGroupInfo().isPresent()) {
|
|
||||||
targetAddress = Address.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get()));
|
|
||||||
} else if (syncTarget != null && !syncTarget.isEmpty()) {
|
|
||||||
targetAddress = Address.fromSerialized(syncTarget);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(message.getTimestamp(), targetAddress) != null) {
|
|
||||||
Log.d("Loki","Message already exists, don't insert again");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
OutgoingTextMessage tm = new OutgoingTextMessage(Recipient.from(context, targetAddress, false),
|
|
||||||
body, message.getExpiresInSeconds(), -1, message.getTimestamp());
|
|
||||||
|
|
||||||
// Ignore the message if it has no body
|
|
||||||
if (tm.getMessageBody().length() == 0) { return; }
|
|
||||||
|
|
||||||
// Check if we have the thread already
|
|
||||||
long threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(targetAddress.serialize());
|
|
||||||
|
|
||||||
|
|
||||||
// Insert the message into the database
|
|
||||||
Optional<InsertResult> insertResult;
|
|
||||||
insertResult = database.insertMessageOutbox(threadID, tm, content.getTimestamp());
|
|
||||||
|
|
||||||
if (insertResult.isPresent()) {
|
|
||||||
threadId = insertResult.get().getThreadId();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (smsMessageId.isPresent()) database.deleteMessage(smsMessageId.get());
|
|
||||||
|
|
||||||
if (threadId != null) {
|
|
||||||
messageNotifier.updateNotification(context, threadId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (insertResult.isPresent()) {
|
|
||||||
InsertResult result = insertResult.get();
|
|
||||||
|
|
||||||
// Loki - Cache the user hex encoded public key (for mentions)
|
|
||||||
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(result.getThreadId(), context);
|
|
||||||
MentionsManager.shared.cache(content.getSender(), result.getThreadId());
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
notifyTypingStoppedFromIncomingMessage(masterRecipient, content.getSender(), content.getSenderDevice());
|
|
||||||
|
|
||||||
Address masterAddress = masterRecipient.getAddress();
|
|
||||||
|
|
||||||
IncomingTextMessage tm = new IncomingTextMessage(masterAddress,
|
|
||||||
content.getSenderDevice(),
|
|
||||||
message.getTimestamp(), body,
|
|
||||||
message.getGroupInfo(),
|
|
||||||
message.getExpiresInSeconds() * 1000L,
|
|
||||||
content.isNeedsReceipt());
|
|
||||||
|
|
||||||
IncomingEncryptedMessage textMessage = new IncomingEncryptedMessage(tm, body);
|
|
||||||
|
|
||||||
// Ignore the message if it has no body
|
|
||||||
if (textMessage.getMessageBody().length() == 0) { return; }
|
|
||||||
|
|
||||||
// Insert the message into the database
|
|
||||||
Optional<InsertResult> insertResult;
|
|
||||||
if (message.isGroupMessage()) {
|
|
||||||
insertResult = database.insertMessageInbox(textMessage, content.getTimestamp());
|
|
||||||
} else {
|
|
||||||
insertResult = database.insertMessageInbox(textMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (insertResult.isPresent()) {
|
|
||||||
threadId = insertResult.get().getThreadId();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (smsMessageId.isPresent()) database.deleteMessage(smsMessageId.get());
|
|
||||||
|
|
||||||
if (threadId != null) {
|
|
||||||
messageNotifier.updateNotification(context, threadId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (insertResult.isPresent()) {
|
|
||||||
InsertResult result = insertResult.get();
|
|
||||||
|
|
||||||
// Loki - Cache the user hex encoded public key (for mentions)
|
|
||||||
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(result.getThreadId(), context);
|
|
||||||
MentionsManager.shared.cache(content.getSender(), result.getThreadId());
|
|
||||||
|
|
||||||
// Loki - Store message open group server ID if needed
|
|
||||||
if (messageServerIDOrNull.isPresent()) {
|
|
||||||
long messageID = result.getMessageId();
|
|
||||||
long messageServerID = messageServerIDOrNull.get();
|
|
||||||
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
|
|
||||||
lokiMessageDatabase.setServerID(messageID, messageServerID);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loki - Update mapping of message ID to original thread ID
|
|
||||||
if (result.getMessageId() > -1) {
|
|
||||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
|
||||||
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
|
|
||||||
long originalThreadId = threadDatabase.getOrCreateThreadIdFor(originalRecipient);
|
|
||||||
lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleCorruptMessage(@NonNull String sender, int senderDevice, long timestamp,
|
|
||||||
@NonNull Optional<Long> smsMessageId, @NonNull Throwable e)
|
|
||||||
{
|
|
||||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
|
||||||
if (!SessionMetaProtocol.shouldIgnoreDecryptionException(context, timestamp)) {
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
|
||||||
private void handleDeliveryReceipt(@NonNull SignalServiceContent content,
|
|
||||||
@NonNull SignalServiceReceiptMessage message)
|
|
||||||
{
|
|
||||||
// Redirect message to master device conversation
|
|
||||||
Address masterAddress = Address.fromSerialized(content.getSender());
|
|
||||||
|
|
||||||
if (masterAddress.isContact()) {
|
|
||||||
Recipient masterRecipient = getMessageMasterDestination(content.getSender());
|
|
||||||
masterAddress = masterRecipient.getAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (long timestamp : message.getTimestamps()) {
|
|
||||||
Log.i(TAG, String.format("Received encrypted delivery receipt: (XXXXX, %d)", timestamp));
|
|
||||||
DatabaseFactory.getMmsSmsDatabase(context)
|
|
||||||
.incrementDeliveryReceiptCount(new SyncMessageId(masterAddress, timestamp), System.currentTimeMillis());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
|
||||||
private void handleReadReceipt(@NonNull SignalServiceContent content,
|
|
||||||
@NonNull SignalServiceReceiptMessage message)
|
|
||||||
{
|
|
||||||
if (TextSecurePreferences.isReadReceiptsEnabled(context)) {
|
|
||||||
|
|
||||||
// Redirect message to master device conversation
|
|
||||||
Address masterAddress = Address.fromSerialized(content.getSender());
|
|
||||||
|
|
||||||
if (masterAddress.isContact()) {
|
|
||||||
Recipient masterRecipient = getMessageMasterDestination(content.getSender());
|
|
||||||
masterAddress = masterRecipient.getAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (long timestamp : message.getTimestamps()) {
|
|
||||||
Log.i(TAG, String.format("Received encrypted read receipt: (XXXXX, %d)", timestamp));
|
|
||||||
|
|
||||||
DatabaseFactory.getMmsSmsDatabase(context)
|
|
||||||
.incrementReadReceiptCount(new SyncMessageId(masterAddress, timestamp), content.getTimestamp());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleTypingMessage(@NonNull SignalServiceContent content,
|
|
||||||
@NonNull SignalServiceTypingMessage typingMessage)
|
|
||||||
{
|
|
||||||
if (!TextSecurePreferences.isTypingIndicatorsEnabled(context)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
long threadId;
|
|
||||||
|
|
||||||
Recipient author = getMessageMasterDestination(content.getSender());
|
|
||||||
threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(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().didReceiveTypingStartedMessage(context,threadId, author.getAddress(), content.getSenderDevice());
|
|
||||||
} else {
|
|
||||||
Log.d(TAG, "Typing stopped on thread " + threadId);
|
|
||||||
ApplicationContext.getInstance(context).getTypingStatusRepository().didReceiveTypingStoppedMessage(context, threadId, author.getAddress(), content.getSenderDevice(), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
Address author = Address.fromSerialized(quote.get().getAuthor().getNumber());
|
|
||||||
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;
|
|
||||||
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.forPointersOfDataMessage(quote.get().getAttachments())));
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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 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.isValidLinkUrl(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) {
|
|
||||||
Recipient masterRecipient = getMessageMasterDestination(sender);
|
|
||||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
|
||||||
IncomingTextMessage textMessage = new IncomingTextMessage(masterRecipient.getAddress(),
|
|
||||||
senderDevice, timestamp, "",
|
|
||||||
Optional.absent(), 0, false);
|
|
||||||
|
|
||||||
textMessage = new IncomingEncryptedMessage(textMessage, "");
|
|
||||||
return database.insertMessageInbox(textMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Recipient getMessageDestination(SignalServiceContent content, SignalServiceDataMessage message) {
|
|
||||||
if (message.getGroupInfo().isPresent()) {
|
|
||||||
return Recipient.from(context, Address.fromExternal(context, GroupUtil.getEncodedClosedGroupID(message.getGroupInfo().get().getGroupId())), false);
|
|
||||||
} else {
|
|
||||||
return Recipient.from(context, Address.fromExternal(context, content.getSender()), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Recipient getMessageMasterDestination(String publicKey) {
|
|
||||||
if (!PublicKeyValidation.isValid(publicKey)) {
|
|
||||||
return Recipient.from(context, Address.fromSerialized(publicKey), false);
|
|
||||||
} else {
|
|
||||||
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
|
|
||||||
if (publicKey.equals(userPublicKey)) {
|
|
||||||
return Recipient.from(context, Address.fromSerialized(userPublicKey), false);
|
|
||||||
} else {
|
|
||||||
return Recipient.from(context, Address.fromSerialized(publicKey), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyTypingStoppedFromIncomingMessage(@NonNull Recipient conversationRecipient, @NonNull String sender, int device) {
|
|
||||||
Recipient author = Recipient.from(context, Address.fromSerialized(sender), false);
|
|
||||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(conversationRecipient);
|
|
||||||
|
|
||||||
if (threadId > 0) {
|
|
||||||
Log.d(TAG, "Typing stopped on thread " + threadId + " due to an incoming message.");
|
|
||||||
ApplicationContext.getInstance(context).getTypingStatusRepository().didReceiveTypingStoppedMessage(context, threadId, author.getAddress(), device, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean shouldIgnore(@Nullable SignalServiceContent content) {
|
|
||||||
if (content == null) {
|
|
||||||
Log.w(TAG, "Got a message with null content.");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SessionMetaProtocol.shouldIgnoreMessage(content.getTimestamp())) {
|
|
||||||
Log.d("Loki", "Ignoring duplicate message.");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.getSender().equals(TextSecurePreferences.getLocalNumber(context)) &&
|
|
||||||
DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(content.getTimestamp(), content.getSender()) != null) {
|
|
||||||
Log.d("Loki", "Skipping message from self we already have");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Recipient sender = Recipient.from(context, Address.fromSerialized(content.getSender()), false);
|
|
||||||
|
|
||||||
if (content.getDataMessage().isPresent()) {
|
|
||||||
SignalServiceDataMessage message = content.getDataMessage().get();
|
|
||||||
Recipient conversation = getMessageDestination(content, message);
|
|
||||||
|
|
||||||
if (conversation.isGroupRecipient() && conversation.isBlocked()) {
|
|
||||||
return true;
|
|
||||||
} else if (conversation.isGroupRecipient()) {
|
|
||||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
|
||||||
Optional<String> groupId = message.getGroupInfo().isPresent() ? Optional.of(GroupUtil.getEncodedId(message.getGroupInfo().get()))
|
|
||||||
: Optional.absent();
|
|
||||||
|
|
||||||
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.isGroupUpdate() && (isTextMessage || isMediaMessage || isExpireMessage);
|
|
||||||
boolean isGroupActive = groupId.isPresent() && groupDatabase.isActive(groupId.get());
|
|
||||||
boolean isLeaveMessage = message.getGroupInfo().isPresent() && message.getGroupInfo().get().getType() == SignalServiceGroup.Type.QUIT;
|
|
||||||
|
|
||||||
return (isContentMessage && !isGroupActive) || (sender.isBlocked() && !isLeaveMessage);
|
|
||||||
} else {
|
|
||||||
return sender.isBlocked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void resetRecipientToPush(@NonNull Recipient recipient) {
|
|
||||||
if (recipient.isForceSmsSelection()) {
|
|
||||||
DatabaseFactory.getRecipientDatabase(context).setForceSmsSelection(recipient, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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<PushDecryptJob> {
|
|
||||||
@Override
|
|
||||||
public @NonNull PushDecryptJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
|
||||||
return new PushDecryptJob(parameters, data.getLong(KEY_MESSAGE_ID), data.getLong(KEY_SMS_MESSAGE_ID));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.jobs;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.session.libsession.messaging.threads.Address;
|
|
||||||
import org.session.libsession.messaging.threads.recipients.Recipient;
|
|
||||||
import org.session.libsignal.service.api.messages.SignalServiceEnvelope;
|
|
||||||
import org.session.libsignal.utilities.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
|
||||||
|
|
||||||
public abstract class PushReceivedJob extends BaseJob {
|
|
||||||
|
|
||||||
private static final String TAG = PushReceivedJob.class.getSimpleName();
|
|
||||||
|
|
||||||
public static final Object RECEIVE_LOCK = new Object();
|
|
||||||
|
|
||||||
protected PushReceivedJob(Job.Parameters parameters) {
|
|
||||||
super(parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void processEnvelope(@NonNull SignalServiceEnvelope envelope, boolean isPushNotification) {
|
|
||||||
synchronized (RECEIVE_LOCK) {
|
|
||||||
try {
|
|
||||||
if (envelope.hasSource()) {
|
|
||||||
Address source = Address.fromExternal(context, envelope.getSource());
|
|
||||||
Recipient recipient = Recipient.from(context, source, false);
|
|
||||||
|
|
||||||
if (!isActiveNumber(recipient)) {
|
|
||||||
DatabaseFactory.getRecipientDatabase(context).setRegistered(recipient, Recipient.RegisteredState.REGISTERED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (envelope.isUnidentifiedSender() || envelope.isClosedGroupCiphertext()) {
|
|
||||||
handleMessage(envelope, isPushNotification);
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Received envelope of unknown type: " + envelope.getType());
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.d("Loki", "Failed to process envelope due to error: " + e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleMessage(SignalServiceEnvelope envelope, boolean isPushNotification) {
|
|
||||||
new PushDecryptJob(context).processMessage(envelope, isPushNotification);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isActiveNumber(@NonNull Recipient recipient) {
|
|
||||||
return recipient.resolve().getRegistered() == Recipient.RegisteredState.REGISTERED;
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,9 +2,11 @@ package org.thoughtcrime.securesms.service;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
|
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
|
||||||
|
import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessage;
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage;
|
||||||
import org.session.libsession.messaging.threads.Address;
|
import org.session.libsession.messaging.threads.Address;
|
||||||
import org.session.libsession.messaging.threads.DistributionTypes;
|
import org.session.libsession.messaging.threads.DistributionTypes;
|
||||||
@ -72,32 +74,88 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setExpirationTimer(@NotNull ExpirationTimerUpdate message) {
|
public void setExpirationTimer(@NotNull ExpirationTimerUpdate message) {
|
||||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
|
||||||
|
|
||||||
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
|
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
|
||||||
String senderPublicKey = message.getSender();
|
String senderPublicKey = message.getSender();
|
||||||
int duration = message.getDuration();
|
|
||||||
String groupPK = message.getGroupPublicKey();
|
// Notify the user
|
||||||
|
if (userPublicKey.equals(senderPublicKey)) {
|
||||||
|
// sender is a linked device
|
||||||
|
insertOutgoingExpirationTimerMessage(message);
|
||||||
|
} else {
|
||||||
|
insertIncomingExpirationTimerMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.getId() != null) {
|
||||||
|
DatabaseFactory.getSmsDatabase(context).deleteMessage(message.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertIncomingExpirationTimerMessage(ExpirationTimerUpdate message) {
|
||||||
|
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||||
|
|
||||||
|
String senderPublicKey = message.getSender();
|
||||||
Long sentTimestamp = message.getSentTimestamp();
|
Long sentTimestamp = message.getSentTimestamp();
|
||||||
|
String groupId = message.getGroupPublicKey();
|
||||||
|
int duration = message.getDuration();
|
||||||
|
|
||||||
Optional<SignalServiceGroup> groupInfo = Optional.absent();
|
Optional<SignalServiceGroup> groupInfo = Optional.absent();
|
||||||
Address address;
|
Address address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : senderPublicKey);
|
||||||
|
Recipient recipient = Recipient.from(context, address, false);
|
||||||
|
|
||||||
|
// if the sender is blocked, we don't display the update, except if it's in a closed group
|
||||||
|
if (recipient.isBlocked() && groupId == null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (groupPK != null) {
|
if (groupId != null) {
|
||||||
String groupID = GroupUtil.doubleEncodeGroupID(groupPK);
|
String groupID = GroupUtil.doubleEncodeGroupID(groupId);
|
||||||
groupInfo = Optional.of(new SignalServiceGroup(GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL));
|
groupInfo = Optional.of(new SignalServiceGroup(GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL));
|
||||||
address = Address.fromSerialized(groupID);
|
|
||||||
} else {
|
Address groupAddress = Address.fromSerialized(groupID);
|
||||||
address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : senderPublicKey);
|
recipient = Recipient.from(context, groupAddress, false);
|
||||||
}
|
}
|
||||||
Recipient recipient = Recipient.from(context, address, false);
|
|
||||||
|
|
||||||
if (recipient.isBlocked()) return;
|
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(address, sentTimestamp, -1,
|
||||||
|
duration * 1000L, true,
|
||||||
|
false,
|
||||||
|
Optional.absent(),
|
||||||
|
groupInfo,
|
||||||
|
Optional.absent(),
|
||||||
|
Optional.absent(),
|
||||||
|
Optional.absent(),
|
||||||
|
Optional.absent());
|
||||||
|
//insert the timer update message
|
||||||
|
database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
||||||
|
|
||||||
// Notify the user
|
//set the timer to the conversation
|
||||||
if (userPublicKey.equals(senderPublicKey)) {
|
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient, duration);
|
||||||
// sender is a linked device
|
|
||||||
|
} catch (IOException | MmsException ioe) {
|
||||||
|
Log.e("Loki", "Failed to insert expiration update message.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertOutgoingExpirationTimerMessage(ExpirationTimerUpdate message) {
|
||||||
|
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||||
|
|
||||||
|
String senderPublicKey = message.getSender();
|
||||||
|
Long sentTimestamp = message.getSentTimestamp();
|
||||||
|
String groupId = message.getGroupPublicKey();
|
||||||
|
int duration = message.getDuration();
|
||||||
|
|
||||||
|
Address address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : senderPublicKey);
|
||||||
|
Recipient recipient = Recipient.from(context, address, false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (groupId != null) {
|
||||||
|
// conversation is a closed group
|
||||||
|
GroupContext groupContext = SignalServiceProtos.GroupContext.newBuilder()
|
||||||
|
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupId))).build();
|
||||||
|
OutgoingGroupMediaMessage infoMessage = new OutgoingGroupMediaMessage(recipient, groupContext, null, sentTimestamp, duration * 1000L, true, null, Collections.emptyList(), Collections.emptyList());
|
||||||
|
database.insertSecureDecryptedMessageOutbox(infoMessage, -1, sentTimestamp);
|
||||||
|
// we need the group ID as recipient for setExpireMessages below
|
||||||
|
recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(groupId)), false);
|
||||||
|
} else {
|
||||||
|
// conversation is a 1-1
|
||||||
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipient,
|
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipient,
|
||||||
null,
|
null,
|
||||||
Collections.emptyList(),
|
Collections.emptyList(),
|
||||||
@ -112,29 +170,12 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
|||||||
Collections.emptyList(),
|
Collections.emptyList(),
|
||||||
Collections.emptyList());
|
Collections.emptyList());
|
||||||
database.insertSecureDecryptedMessageOutbox(mediaMessage, -1, sentTimestamp);
|
database.insertSecureDecryptedMessageOutbox(mediaMessage, -1, sentTimestamp);
|
||||||
} else {
|
|
||||||
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(address, sentTimestamp, -1,
|
|
||||||
duration * 1000L, true,
|
|
||||||
false,
|
|
||||||
Optional.absent(),
|
|
||||||
groupInfo,
|
|
||||||
Optional.absent(),
|
|
||||||
Optional.absent(),
|
|
||||||
Optional.absent(),
|
|
||||||
Optional.absent());
|
|
||||||
//insert the timer update message
|
|
||||||
database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//set the timer to the conversation
|
//set the timer to the conversation
|
||||||
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient, duration);
|
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient, duration);
|
||||||
|
|
||||||
if (message.getId() != null) {
|
} catch (MmsException | IOException ioe) {
|
||||||
DatabaseFactory.getSmsDatabase(context).deleteMessage(message.getId());
|
|
||||||
}
|
|
||||||
} catch (MmsException e) {
|
|
||||||
Log.e("Loki", "Failed to insert expiration update message.");
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
Log.e("Loki", "Failed to insert expiration update message.");
|
Log.e("Loki", "Failed to insert expiration update message.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,9 @@ import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
|
|||||||
import org.session.libsession.messaging.MessagingConfiguration
|
import org.session.libsession.messaging.MessagingConfiguration
|
||||||
import org.session.libsignal.utilities.logging.Log
|
import org.session.libsignal.utilities.logging.Log
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
@ -16,6 +18,8 @@ import kotlin.math.roundToLong
|
|||||||
class JobQueue : JobDelegate {
|
class JobQueue : JobDelegate {
|
||||||
private var hasResumedPendingJobs = false // Just for debugging
|
private var hasResumedPendingJobs = false // Just for debugging
|
||||||
|
|
||||||
|
private val jobTimestampMap = ConcurrentHashMap<Long, AtomicInteger>()
|
||||||
|
|
||||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||||
private val scope = GlobalScope + SupervisorJob()
|
private val scope = GlobalScope + SupervisorJob()
|
||||||
private val queue = Channel<Job>(UNLIMITED)
|
private val queue = Channel<Job>(UNLIMITED)
|
||||||
@ -44,7 +48,14 @@ class JobQueue : JobDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun addWithoutExecuting(job: Job) {
|
private fun addWithoutExecuting(job: Job) {
|
||||||
job.id = System.currentTimeMillis().toString()
|
// When adding multiple jobs in rapid succession, timestamps might not be good enough as a unique ID. To
|
||||||
|
// deal with this we keep track of the number of jobs with a given timestamp and that to the end of the
|
||||||
|
// timestamp to make it a unique ID. We can't use a random number because we do still want to keep track
|
||||||
|
// of the order in which the jobs were added.
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
jobTimestampMap.putIfAbsent(currentTime, AtomicInteger())
|
||||||
|
job.id = jobTimestampMap[currentTime]!!.getAndIncrement().toString()
|
||||||
|
|
||||||
MessagingConfiguration.shared.storage.persistJob(job)
|
MessagingConfiguration.shared.storage.persistJob(job)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +58,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
|||||||
@Nullable final Attachment avatar,
|
@Nullable final Attachment avatar,
|
||||||
long sentTime,
|
long sentTime,
|
||||||
long expireIn,
|
long expireIn,
|
||||||
|
boolean expirationUpdate,
|
||||||
@Nullable QuoteModel quote,
|
@Nullable QuoteModel quote,
|
||||||
@NonNull List<Contact> contacts,
|
@NonNull List<Contact> contacts,
|
||||||
@NonNull List<LinkPreview> previews)
|
@NonNull List<LinkPreview> previews)
|
||||||
@ -65,7 +66,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
|
|||||||
super(recipient, Base64.encodeBytes(group.toByteArray()),
|
super(recipient, Base64.encodeBytes(group.toByteArray()),
|
||||||
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
|
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
|
||||||
sentTime,
|
sentTime,
|
||||||
DistributionTypes.CONVERSATION, expireIn, false, quote, contacts, previews);
|
DistributionTypes.CONVERSATION, expireIn, expirationUpdate, quote, contacts, previews);
|
||||||
|
|
||||||
this.group = group;
|
this.group = group;
|
||||||
}
|
}
|
||||||
|
@ -78,6 +78,11 @@ object GroupUtil {
|
|||||||
return getEncodedClosedGroupID(getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray())
|
return getEncodedClosedGroupID(getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun doubleEncodeGroupID(groupID: ByteArray): String {
|
||||||
|
return getEncodedClosedGroupID(getEncodedClosedGroupID(groupID).toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun doubleDecodeGroupID(groupID: String): ByteArray {
|
fun doubleDecodeGroupID(groupID: String): ByteArray {
|
||||||
|
Loading…
Reference in New Issue
Block a user