diff --git a/app/src/main/java/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java b/app/src/main/java/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java index 6f924b862a..31d146c45f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java @@ -20,33 +20,13 @@ package org.thoughtcrime.securesms; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; -import android.database.Cursor; import android.os.AsyncTask; 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.VersionTracker; -import java.util.List; -import java.util.SortedSet; -import java.util.TreeSet; - -import network.loki.messenger.R; - public class DatabaseUpgradeActivity extends BaseActivity { - private static final String TAG = DatabaseUpgradeActivity.class.getSimpleName(); @Override public void onCreate(Bundle bundle) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index 44f93c8332..46601321db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -52,6 +52,8 @@ import androidx.annotation.Nullable; 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.sending_receiving.attachments.AttachmentTransferProgress; 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.service.loki.api.opengroups.PublicChat; import org.session.libsignal.utilities.logging.Log; -import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.BindableConversationItem; import org.thoughtcrime.securesms.MediaPreviewActivity; 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.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.Quote; -import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.loki.utilities.MentionUtilities; 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"); for (Slide slide : slides) { - ApplicationContext.getInstance(context) - .getJobManager() - .add(new AttachmentDownloadJob(messageRecord.getId(), - ((DatabaseAttachment)slide.asAttachment()).getAttachmentId(), true)); + JobQueue.getShared().add( + new AttachmentDownloadJob(messageRecord.getId(), + ((DatabaseAttachment)slide.asAttachment()).getAttachmentId().getRowId()) + ); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 62de9499a1..700d64101a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -28,12 +28,14 @@ import com.annimon.stream.Collectors; import com.annimon.stream.Stream; import com.google.android.mms.pdu_alt.NotificationInd; import com.google.android.mms.pdu_alt.PduHeaders; +import com.google.protobuf.ByteString; import net.sqlcipher.database.SQLiteDatabase; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import org.session.libsession.utilities.GroupUtil; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment; import org.session.libsession.database.documents.IdentityKeyMismatch; @@ -686,7 +688,14 @@ public class MmsDatabase extends MessagingDatabase { throws MmsException { 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); if (messageId == -1) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 56e37ed3e0..1c6a28b564 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -424,7 +424,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, .setName(name) .addAllMembers(members) .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 mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context) if (mmsSmsDB.getMessageFor(sentTimestamp,userPublicKey) != null) return diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java index f9d7fa6834..33122e0e83 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java @@ -3,9 +3,7 @@ package org.thoughtcrime.securesms.dependencies; import android.content.Context; import org.session.libsignal.service.api.SignalServiceMessageReceiver; -import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob; import org.thoughtcrime.securesms.jobs.AvatarDownloadJob; -import org.thoughtcrime.securesms.jobs.PushDecryptJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob; import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository; import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment; @@ -13,11 +11,9 @@ import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment; import dagger.Module; import dagger.Provides; -@Module(complete = false, injects = {AttachmentDownloadJob.class, - AvatarDownloadJob.class, +@Module(complete = false, injects = {AvatarDownloadJob.class, RetrieveProfileAvatarJob.class, AppProtectionPreferenceFragment.class, - PushDecryptJob.class, LinkPreviewRepository.class}) public class SignalCommunicationModule { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java deleted file mode 100644 index 82b452d85f..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java +++ /dev/null @@ -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 { - @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)); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index a3644d8ba7..0a078fc017 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -29,11 +29,8 @@ public final class JobManagerFactories { public static Map getJobFactories(@NonNull Application application) { HashMap factoryHashMap = new HashMap() {{ - put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory()); put(AvatarDownloadJob.KEY, new AvatarDownloadJob.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(TrimThreadJob.KEY, new TrimThreadJob.Factory()); put(UpdateApkJob.KEY, new UpdateApkJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushContentReceiveJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushContentReceiveJob.java deleted file mode 100644 index bd60efbbe8..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushContentReceiveJob.java +++ /dev/null @@ -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 { - @Override - public @NonNull PushContentReceiveJob create(@NonNull Parameters parameters, @NonNull Data data) { - return new PushContentReceiveJob(parameters); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java deleted file mode 100644 index b0af4cd793..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ /dev/null @@ -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 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 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 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 smsMessageId, - @NonNull Optional 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 quote = getValidatedQuote(message.getQuote()); - Optional> sharedContacts = getContacts(message.getSharedContacts()); - Optional> 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 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; - - 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 allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId()); - List 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; - - try { - if (message.isGroupMessage()) { - insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1, content.getTimestamp()); - } else { - insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1); - } - - if (insertResult.isPresent()) { - List allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId()); - List 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 smsMessageId, - @NonNull Optional 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 = 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; - 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 smsMessageId, @NonNull Throwable e) - { - SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context); - if (!SessionMetaProtocol.shouldIgnoreDecryptionException(context, timestamp)) { - if (!smsMessageId.isPresent()) { - Optional 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 getValidatedQuote(Optional 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 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> getContacts(Optional> sharedContacts) { - if (!sharedContacts.isPresent()) return Optional.absent(); - - List contacts = new ArrayList<>(sharedContacts.get().size()); - - for (SharedContact sharedContact : sharedContacts.get()) { - contacts.add(ContactModelMapper.remoteToLocal(sharedContact)); - } - - return Optional.of(contacts); - } - - private Optional> getLinkPreviews(Optional> previews, @NonNull String message) { - if (!previews.isPresent()) return Optional.absent(); - - List linkPreviews = new ArrayList<>(previews.get().size()); - - for (Preview preview : previews.get()) { - Optional thumbnail = PointerAttachment.forPointer(preview.getImage()); - Optional url = Optional.fromNullable(preview.getUrl()); - Optional 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 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 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 { - @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)); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushReceivedJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushReceivedJob.java deleted file mode 100644 index 70f9b755b1..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushReceivedJob.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java index cd3170efc5..8cc1224d79 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java @@ -2,9 +2,11 @@ package org.thoughtcrime.securesms.service; import android.content.Context; +import com.google.protobuf.ByteString; + import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; 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.threads.Address; import org.session.libsession.messaging.threads.DistributionTypes; @@ -72,32 +74,88 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM @Override public void setExpirationTimer(@NotNull ExpirationTimerUpdate message) { - MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - String userPublicKey = TextSecurePreferences.getLocalNumber(context); 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(); + String groupId = message.getGroupPublicKey(); + int duration = message.getDuration(); Optional 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 { - if (groupPK != null) { - String groupID = GroupUtil.doubleEncodeGroupID(groupPK); + if (groupId != null) { + String groupID = GroupUtil.doubleEncodeGroupID(groupId); groupInfo = Optional.of(new SignalServiceGroup(GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL)); - address = Address.fromSerialized(groupID); - } else { - address = Address.fromSerialized((message.getSyncTarget() != null && !message.getSyncTarget().isEmpty()) ? message.getSyncTarget() : senderPublicKey); + + Address groupAddress = Address.fromSerialized(groupID); + 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 - if (userPublicKey.equals(senderPublicKey)) { - // sender is a linked device + //set the timer to the conversation + DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient, duration); + + } 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, null, Collections.emptyList(), @@ -112,29 +170,12 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM Collections.emptyList(), Collections.emptyList()); 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 DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient, duration); - if (message.getId() != null) { - DatabaseFactory.getSmsDatabase(context).deleteMessage(message.getId()); - } - } catch (MmsException e) { - Log.e("Loki", "Failed to insert expiration update message."); - } catch (IOException ioe) { + } catch (MmsException | IOException ioe) { Log.e("Loki", "Failed to insert expiration update message."); } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt index 77eff071b0..43cefbd13c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt @@ -6,7 +6,9 @@ import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED import org.session.libsession.messaging.MessagingConfiguration import org.session.libsignal.utilities.logging.Log import java.util.* +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicInteger import kotlin.concurrent.schedule import kotlin.math.min import kotlin.math.pow @@ -16,6 +18,8 @@ import kotlin.math.roundToLong class JobQueue : JobDelegate { private var hasResumedPendingJobs = false // Just for debugging + private val jobTimestampMap = ConcurrentHashMap() + private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val scope = GlobalScope + SupervisorJob() private val queue = Channel(UNLIMITED) @@ -44,7 +48,14 @@ class JobQueue : JobDelegate { } 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) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingGroupMediaMessage.java b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingGroupMediaMessage.java index 125aa26228..7f151ae7ee 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingGroupMediaMessage.java +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/signal/OutgoingGroupMediaMessage.java @@ -58,6 +58,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage { @Nullable final Attachment avatar, long sentTime, long expireIn, + boolean expirationUpdate, @Nullable QuoteModel quote, @NonNull List contacts, @NonNull List previews) @@ -65,7 +66,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage { super(recipient, Base64.encodeBytes(group.toByteArray()), new LinkedList() {{if (avatar != null) add(avatar);}}, sentTime, - DistributionTypes.CONVERSATION, expireIn, false, quote, contacts, previews); + DistributionTypes.CONVERSATION, expireIn, expirationUpdate, quote, contacts, previews); this.group = group; } diff --git a/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt b/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt index 17aad994ea..7d7422494b 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt @@ -78,6 +78,11 @@ object GroupUtil { return getEncodedClosedGroupID(getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray()) } + @JvmStatic + fun doubleEncodeGroupID(groupID: ByteArray): String { + return getEncodedClosedGroupID(getEncodedClosedGroupID(groupID).toByteArray()) + } + @JvmStatic @Throws(IOException::class) fun doubleDecodeGroupID(groupID: String): ByteArray {