diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt index 742f00f31..adf9e6aa1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt @@ -187,7 +187,7 @@ class ConversationSettingsRepository( } fun disableProfileSharingForInternalUser(recipientId: RecipientId) { - Preconditions.checkArgument(FeatureFlags.internalUser(), "Internal users only!"); + Preconditions.checkArgument(FeatureFlags.internalUser(), "Internal users only!") SignalExecutors.BOUNDED.execute { DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipientId, false) @@ -195,7 +195,7 @@ class ConversationSettingsRepository( } fun deleteSessionForInternalUser(recipientId: RecipientId) { - Preconditions.checkArgument(FeatureFlags.internalUser(), "Internal users only!"); + Preconditions.checkArgument(FeatureFlags.internalUser(), "Internal users only!") SignalExecutors.BOUNDED.execute { DatabaseFactory.getSessionDatabase(context).deleteAllFor(recipientId) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackService.java b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackService.java index f0570816a..7bd9e468e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackService.java @@ -40,6 +40,7 @@ import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessageDatabase; +import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.MultiDeviceViewedUpdateJob; import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob; @@ -256,7 +257,8 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat { if (markedMessageInfo != null) { ApplicationDependencies.getJobManager().add(new SendViewedReceiptJob(markedMessageInfo.getThreadId(), recipientId, - markedMessageInfo.getSyncMessageId().getTimetamp())); + markedMessageInfo.getSyncMessageId().getTimetamp(), + new MessageId(messageId, true))); MultiDeviceViewedUpdateJob.enqueue(Collections.singletonList(markedMessageInfo.getSyncMessageId())); } }); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java index 4be2b23ae..480924162 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java @@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList; import org.thoughtcrime.securesms.database.documents.NetworkFailure; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; +import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; @@ -682,11 +683,13 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns private final long threadId; private final SyncMessageId syncMessageId; + private final MessageId messageId; private final ExpirationInfo expirationInfo; - public MarkedMessageInfo(long threadId, SyncMessageId syncMessageId, ExpirationInfo expirationInfo) { + public MarkedMessageInfo(long threadId, @NonNull SyncMessageId syncMessageId, @NonNull MessageId messageId, @Nullable ExpirationInfo expirationInfo) { this.threadId = threadId; this.syncMessageId = syncMessageId; + this.messageId = messageId; this.expirationInfo = expirationInfo; } @@ -694,11 +697,15 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns return threadId; } - public SyncMessageId getSyncMessageId() { + public @NonNull SyncMessageId getSyncMessageId() { return syncMessageId; } - public ExpirationInfo getExpirationInfo() { + public @NonNull MessageId getMessageId() { + return messageId; + } + + public @Nullable ExpirationInfo getExpirationInfo() { return expirationInfo; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageSendLogDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageSendLogDatabase.kt index 326d5d5d7..e354c66ec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageSendLogDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageSendLogDatabase.kt @@ -151,6 +151,15 @@ class MessageSendLogDatabase constructor(context: Context?, databaseHelper: SQLC } } + fun insertIfPossible(recipientId: RecipientId, sentTimestamp: Long, sendMessageResult: SendMessageResult, contentHint: ContentHint, messageIds: List) { + if (!FeatureFlags.senderKey()) return + + if (sendMessageResult.isSuccess && sendMessageResult.success.content.isPresent) { + val recipientDevice = listOf(RecipientDevice(recipientId, sendMessageResult.success.devices)) + insert(recipientDevice, sentTimestamp, sendMessageResult.success.content.get(), contentHint, messageIds) + } + } + fun insertIfPossible(sentTimestamp: Long, possibleRecipients: List, results: List, contentHint: ContentHint, relatedMessageId: Long, isRelatedMessageMms: Boolean) { if (!FeatureFlags.senderKey()) return 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 bed7f9de6..2218a6781 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -47,6 +47,7 @@ import org.thoughtcrime.securesms.database.documents.NetworkFailureList; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.Mention; +import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord; import org.thoughtcrime.securesms.database.model.Quote; @@ -399,11 +400,12 @@ public class MmsDatabase extends MessageDatabase { List results = new ArrayList<>(cursor.getCount()); while (cursor.moveToNext()) { + long messageId = CursorUtil.requireLong(cursor, ID); RecipientId recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); long dateSent = CursorUtil.requireLong(cursor, DATE_SENT); SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent); - results.add(new MarkedMessageInfo(threadId, syncMessageId, null)); + results.add(new MarkedMessageInfo(threadId, syncMessageId, new MessageId(messageId, true), null)); } return results; @@ -437,12 +439,13 @@ public class MmsDatabase extends MessageDatabase { while (cursor != null && cursor.moveToNext()) { long type = CursorUtil.requireLong(cursor, MESSAGE_BOX); if (Types.isSecureType(type) && Types.isInboxType(type)) { + long messageId = CursorUtil.requireLong(cursor, ID); long threadId = CursorUtil.requireLong(cursor, THREAD_ID); RecipientId recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); long dateSent = CursorUtil.requireLong(cursor, DATE_SENT); SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent); - results.add(new MarkedMessageInfo(threadId, syncMessageId, null)); + results.add(new MarkedMessageInfo(threadId, syncMessageId, new MessageId(messageId, true), null)); ContentValues contentValues = new ContentValues(); contentValues.put(VIEWED_RECEIPT_COUNT, 1); @@ -1004,7 +1007,7 @@ public class MmsDatabase extends MessageDatabase { SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent); ExpirationInfo expirationInfo = new ExpirationInfo(messageId, expiresIn, expireStarted, true); - result.add(new MarkedMessageInfo(threadId, syncMessageId, expirationInfo)); + result.add(new MarkedMessageInfo(threadId, syncMessageId, new MessageId(messageId, true), expirationInfo)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index 73e2fba7a..e8a5615b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList; import org.thoughtcrime.securesms.database.documents.NetworkFailure; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil; +import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; @@ -644,7 +645,7 @@ public class SmsDatabase extends MessageDatabase { SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent); ExpirationInfo expirationInfo = new ExpirationInfo(messageId, expiresIn, expireStarted, false); - results.add(new MarkedMessageInfo(threadId, syncMessageId, expirationInfo)); + results.add(new MarkedMessageInfo(threadId, syncMessageId, new MessageId(messageId, false), expirationInfo)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java index 137207aa0..953506524 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendReadReceiptJob.java @@ -7,6 +7,10 @@ import androidx.annotation.NonNull; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.MessageDatabase; +import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo; +import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; @@ -19,8 +23,11 @@ import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; +import org.whispersystems.libsignal.util.guava.Preconditions; import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; @@ -28,8 +35,11 @@ import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedExcept import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; public class SendReadReceiptJob extends BaseJob { @@ -39,18 +49,20 @@ public class SendReadReceiptJob extends BaseJob { static final int MAX_TIMESTAMPS = 500; - private static final String KEY_THREAD = "thread"; - private static final String KEY_ADDRESS = "address"; - private static final String KEY_RECIPIENT = "recipient"; - private static final String KEY_MESSAGE_IDS = "message_ids"; - private static final String KEY_TIMESTAMP = "timestamp"; + private static final String KEY_THREAD = "thread"; + private static final String KEY_ADDRESS = "address"; + private static final String KEY_RECIPIENT = "recipient"; + private static final String KEY_MESSAGE_SENT_TIMESTAMPS = "message_ids"; + private static final String KEY_MESSAGE_IDS = "message_db_ids"; + private static final String KEY_TIMESTAMP = "timestamp"; - private final long threadId; - private final RecipientId recipientId; - private final List messageIds; - private final long timestamp; + private final long threadId; + private final RecipientId recipientId; + private final List messageSentTimestamps; + private final long timestamp; + private final List messageIds; - public SendReadReceiptJob(long threadId, @NonNull RecipientId recipientId, List messageIds) { + public SendReadReceiptJob(long threadId, @NonNull RecipientId recipientId, List messageSentTimestamps, List messageIds) { this(new Job.Parameters.Builder() .addConstraint(NetworkConstraint.KEY) .setLifespan(TimeUnit.DAYS.toMillis(1)) @@ -59,6 +71,7 @@ public class SendReadReceiptJob extends BaseJob { .build(), threadId, recipientId, + ensureSize(messageSentTimestamps, MAX_TIMESTAMPS), ensureSize(messageIds, MAX_TIMESTAMPS), System.currentTimeMillis()); } @@ -66,43 +79,51 @@ public class SendReadReceiptJob extends BaseJob { private SendReadReceiptJob(@NonNull Job.Parameters parameters, long threadId, @NonNull RecipientId recipientId, - @NonNull List messageIds, + @NonNull List messageSentTimestamps, + @NonNull List messageIds, long timestamp) { super(parameters); - this.threadId = threadId; - this.recipientId = recipientId; - this.messageIds = messageIds; - this.timestamp = timestamp; + this.threadId = threadId; + this.recipientId = recipientId; + this.messageSentTimestamps = messageSentTimestamps; + this.messageIds = messageIds; + this.timestamp = timestamp; } /** * Enqueues all the necessary jobs for read receipts, ensuring that they're all within the * maximum size. */ - public static void enqueue(long threadId, @NonNull RecipientId recipientId, List messageIds) { - JobManager jobManager = ApplicationDependencies.getJobManager(); - List> messageIdChunks = Util.chunk(messageIds, MAX_TIMESTAMPS); + public static void enqueue(long threadId, @NonNull RecipientId recipientId, List markedMessageInfos) { + JobManager jobManager = ApplicationDependencies.getJobManager(); + List> messageIdChunks = Util.chunk(markedMessageInfos, MAX_TIMESTAMPS); if (messageIdChunks.size() > 1) { - Log.w(TAG, "Large receipt count! Had to break into multiple chunks. Total count: " + messageIds.size()); + Log.w(TAG, "Large receipt count! Had to break into multiple chunks. Total count: " + markedMessageInfos.size()); } - for (List chunk : messageIdChunks) { - jobManager.add(new SendReadReceiptJob(threadId, recipientId, chunk)); + for (List chunk : messageIdChunks) { + List sentTimestamps = chunk.stream().map(info -> info.getSyncMessageId().getTimetamp()).collect(Collectors.toList()); + List messageIds = chunk.stream().map(MarkedMessageInfo::getMessageId).collect(Collectors.toList()); + + jobManager.add(new SendReadReceiptJob(threadId, recipientId, sentTimestamps, messageIds)); } } @Override public @NonNull Data serialize() { - long[] ids = new long[messageIds.size()]; - for (int i = 0; i < ids.length; i++) { - ids[i] = messageIds.get(i); + long[] sentTimestamps = new long[messageSentTimestamps.size()]; + for (int i = 0; i < sentTimestamps.length; i++) { + sentTimestamps[i] = messageSentTimestamps.get(i); } + List serializedMessageIds = messageIds.stream().map(MessageId::serialize).collect(Collectors.toList()); + return new Data.Builder().putString(KEY_RECIPIENT, recipientId.serialize()) - .putLongArray(KEY_MESSAGE_IDS, ids) + .putLongArray(KEY_MESSAGE_SENT_TIMESTAMPS, sentTimestamps) + .putStringListAsArray(KEY_MESSAGE_IDS, serializedMessageIds) .putLong(KEY_TIMESTAMP, timestamp) .putLong(KEY_THREAD, threadId) .build(); @@ -119,7 +140,7 @@ public class SendReadReceiptJob extends BaseJob { throw new NotPushRegisteredException(); } - if (!TextSecurePreferences.isReadReceiptsEnabled(context) || messageIds.isEmpty()) return; + if (!TextSecurePreferences.isReadReceiptsEnabled(context) || messageSentTimestamps.isEmpty()) return; if (!RecipientUtil.isMessageRequestAccepted(context, threadId)) { Log.w(TAG, "Refusing to send receipts to untrusted recipient"); @@ -139,11 +160,15 @@ public class SendReadReceiptJob extends BaseJob { SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); SignalServiceAddress remoteAddress = RecipientUtil.toSignalServiceAddress(context, recipient); - SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, messageIds, timestamp); + SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, messageSentTimestamps, timestamp); - messageSender.sendReceipt(remoteAddress, - UnidentifiedAccessUtil.getAccessFor(context, Recipient.resolved(recipientId)), - receiptMessage); + SendMessageResult result = messageSender.sendReceipt(remoteAddress, + UnidentifiedAccessUtil.getAccessFor(context, Recipient.resolved(recipientId)), + receiptMessage); + + if (Util.hasItems(messageIds)) { + DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(recipientId, timestamp, result, ContentHint.IMPLICIT, messageIds); + } } @Override @@ -175,18 +200,20 @@ public class SendReadReceiptJob extends BaseJob { @Override public @NonNull SendReadReceiptJob create(@NonNull Parameters parameters, @NonNull Data data) { - long timestamp = data.getLong(KEY_TIMESTAMP); - long[] ids = data.hasLongArray(KEY_MESSAGE_IDS) ? data.getLongArray(KEY_MESSAGE_IDS) : new long[0]; - List messageIds = new ArrayList<>(ids.length); - RecipientId recipientId = data.hasString(KEY_RECIPIENT) ? RecipientId.from(data.getString(KEY_RECIPIENT)) - : Recipient.external(application, data.getString(KEY_ADDRESS)).getId(); - long threadId = data.getLong(KEY_THREAD); + long timestamp = data.getLong(KEY_TIMESTAMP); + long[] ids = data.hasLongArray(KEY_MESSAGE_SENT_TIMESTAMPS) ? data.getLongArray(KEY_MESSAGE_SENT_TIMESTAMPS) : new long[0]; + List sentTimestamps = new ArrayList<>(ids.length); + List rawMessageIds = data.hasStringArray(KEY_MESSAGE_IDS) ? data.getStringArrayAsList(KEY_MESSAGE_IDS) : Collections.emptyList(); + List messageIds = rawMessageIds.stream().map(MessageId::deserialize).collect(Collectors.toList()); + long threadId = data.getLong(KEY_THREAD); + RecipientId recipientId = data.hasString(KEY_RECIPIENT) ? RecipientId.from(data.getString(KEY_RECIPIENT)) + : Recipient.external(application, data.getString(KEY_ADDRESS)).getId(); for (long id : ids) { - messageIds.add(id); + sentTimestamps.add(id); } - return new SendReadReceiptJob(parameters, threadId, recipientId, messageIds, timestamp); + return new SendReadReceiptJob(parameters, threadId, recipientId, sentTimestamps, messageIds, timestamp); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java index 3e61f08ab..4d9dfb2b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java @@ -7,18 +7,24 @@ import androidx.annotation.NonNull; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo; +import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; -import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; @@ -28,6 +34,9 @@ import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.thoughtcrime.securesms.jobs.SendReadReceiptJob.MAX_TIMESTAMPS; public class SendViewedReceiptJob extends BaseJob { @@ -35,22 +44,24 @@ public class SendViewedReceiptJob extends BaseJob { private static final String TAG = Log.tag(SendViewedReceiptJob.class); - private static final String KEY_THREAD = "thread"; - private static final String KEY_ADDRESS = "address"; - private static final String KEY_RECIPIENT = "recipient"; - private static final String KEY_SYNC_TIMESTAMPS = "message_ids"; - private static final String KEY_TIMESTAMP = "timestamp"; + private static final String KEY_THREAD = "thread"; + private static final String KEY_ADDRESS = "address"; + private static final String KEY_RECIPIENT = "recipient"; + private static final String KEY_MESSAGE_SENT_TIMESTAMPS = "message_ids"; + private static final String KEY_MESSAGE_IDS = "message_db_ids"; + private static final String KEY_TIMESTAMP = "timestamp"; - private long threadId; - private RecipientId recipientId; - private List syncTimestamps; - private long timestamp; + private final long threadId; + private final RecipientId recipientId; + private final List messageSentTimestamps; + private final List messageIds; + private final long timestamp; - public SendViewedReceiptJob(long threadId, @NonNull RecipientId recipientId, long syncTimestamp) { - this(threadId, recipientId, Collections.singletonList(syncTimestamp)); + public SendViewedReceiptJob(long threadId, @NonNull RecipientId recipientId, long syncTimestamp, @NonNull MessageId messageId) { + this(threadId, recipientId, Collections.singletonList(syncTimestamp), Collections.singletonList(messageId)); } - public SendViewedReceiptJob(long threadId, @NonNull RecipientId recipientId, @NonNull List syncTimestamps) { + private SendViewedReceiptJob(long threadId, @NonNull RecipientId recipientId, @NonNull List messageSentTimestamps, @NonNull List messageIds) { this(new Parameters.Builder() .addConstraint(NetworkConstraint.KEY) .setLifespan(TimeUnit.DAYS.toMillis(1)) @@ -58,28 +69,54 @@ public class SendViewedReceiptJob extends BaseJob { .build(), threadId, recipientId, - SendReadReceiptJob.ensureSize(syncTimestamps, SendReadReceiptJob.MAX_TIMESTAMPS), + SendReadReceiptJob.ensureSize(messageSentTimestamps, MAX_TIMESTAMPS), + SendReadReceiptJob.ensureSize(messageIds, MAX_TIMESTAMPS), System.currentTimeMillis()); } private SendViewedReceiptJob(@NonNull Parameters parameters, long threadId, @NonNull RecipientId recipientId, - @NonNull List syncTimestamps, + @NonNull List messageSentTimestamps, + @NonNull List messageIds, long timestamp) { super(parameters); - this.threadId = threadId; - this.recipientId = recipientId; - this.syncTimestamps = syncTimestamps; - this.timestamp = timestamp; + this.threadId = threadId; + this.recipientId = recipientId; + this.messageSentTimestamps = messageSentTimestamps; + this.messageIds = messageIds; + this.timestamp = timestamp; + } + + /** + * Enqueues all the necessary jobs for viewed receipts, ensuring that they're all within the + * maximum size. + */ + public static void enqueue(long threadId, @NonNull RecipientId recipientId, List markedMessageInfos) { + JobManager jobManager = ApplicationDependencies.getJobManager(); + List> messageIdChunks = Util.chunk(markedMessageInfos, MAX_TIMESTAMPS); + + if (messageIdChunks.size() > 1) { + Log.w(TAG, "Large receipt count! Had to break into multiple chunks. Total count: " + markedMessageInfos.size()); + } + + for (List chunk : messageIdChunks) { + List sentTimestamps = chunk.stream().map(info -> info.getSyncMessageId().getTimetamp()).collect(Collectors.toList()); + List messageIds = chunk.stream().map(MarkedMessageInfo::getMessageId).collect(Collectors.toList()); + + jobManager.add(new SendViewedReceiptJob(threadId, recipientId, sentTimestamps, messageIds)); + } } @Override public @NonNull Data serialize() { + List serializedMessageIds = messageIds.stream().map(MessageId::serialize).collect(Collectors.toList()); + return new Data.Builder().putString(KEY_RECIPIENT, recipientId.serialize()) - .putLongListAsArray(KEY_SYNC_TIMESTAMPS, syncTimestamps) + .putLongListAsArray(KEY_MESSAGE_SENT_TIMESTAMPS, messageSentTimestamps) + .putStringListAsArray(KEY_MESSAGE_IDS, serializedMessageIds) .putLong(KEY_TIMESTAMP, timestamp) .putLong(KEY_THREAD, threadId) .build(); @@ -101,7 +138,7 @@ public class SendViewedReceiptJob extends BaseJob { return; } - if (syncTimestamps.isEmpty()) { + if (messageSentTimestamps.isEmpty()) { Log.w(TAG, "No sync timestamps!"); return; } @@ -125,12 +162,16 @@ public class SendViewedReceiptJob extends BaseJob { SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); SignalServiceAddress remoteAddress = RecipientUtil.toSignalServiceAddress(context, recipient); SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED, - syncTimestamps, + messageSentTimestamps, timestamp); - messageSender.sendReceipt(remoteAddress, - UnidentifiedAccessUtil.getAccessFor(context, Recipient.resolved(recipientId)), - receiptMessage); + SendMessageResult result = messageSender.sendReceipt(remoteAddress, + UnidentifiedAccessUtil.getAccessFor(context, Recipient.resolved(recipientId)), + receiptMessage); + + if (Util.hasItems(messageIds)) { + DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(recipientId, timestamp, result, ContentHint.IMPLICIT, messageIds); + } } @Override @@ -156,13 +197,15 @@ public class SendViewedReceiptJob extends BaseJob { @Override public @NonNull SendViewedReceiptJob create(@NonNull Parameters parameters, @NonNull Data data) { - long timestamp = data.getLong(KEY_TIMESTAMP); - List syncTimestamps = data.getLongArrayAsList(KEY_SYNC_TIMESTAMPS); - RecipientId recipientId = data.hasString(KEY_RECIPIENT) ? RecipientId.from(data.getString(KEY_RECIPIENT)) - : Recipient.external(application, data.getString(KEY_ADDRESS)).getId(); - long threadId = data.getLong(KEY_THREAD); + long timestamp = data.getLong(KEY_TIMESTAMP); + List syncTimestamps = data.getLongArrayAsList(KEY_MESSAGE_SENT_TIMESTAMPS); + List rawMessageIds = data.hasStringArray(KEY_MESSAGE_IDS) ? data.getStringArrayAsList(KEY_MESSAGE_IDS) : Collections.emptyList(); + List messageIds = rawMessageIds.stream().map(MessageId::deserialize).collect(Collectors.toList()); + long threadId = data.getLong(KEY_THREAD); + RecipientId recipientId = data.hasString(KEY_RECIPIENT) ? RecipientId.from(data.getString(KEY_RECIPIENT)) + : Recipient.external(application, data.getString(KEY_ADDRESS)).getId(); - return new SendViewedReceiptJob(parameters, threadId, recipientId, syncTimestamps, timestamp); + return new SendViewedReceiptJob(parameters, threadId, recipientId, syncTimestamps, messageIds, timestamp); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java index c0df3d7b5..600e2e0b8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java @@ -155,12 +155,7 @@ final class MessageRequestRepository { List viewedInfos = DatabaseFactory.getMmsDatabase(context) .getViewedIncomingMessages(threadId); - ApplicationDependencies.getJobManager() - .add(new SendViewedReceiptJob(threadId, - liveRecipient.getId(), - Stream.of(viewedInfos) - .map(info -> info.getSyncMessageId().getTimetamp()) - .toList())); + SendViewedReceiptJob.enqueue(threadId, liveRecipient.getId(), viewedInfos); if (TextSecurePreferences.isMultiDevice(context)) { ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forAccept(liveRecipient.getId())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java index e14bf978c..d2e17d903 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java @@ -92,14 +92,16 @@ public class MarkReadReceiver extends BroadcastReceiver { .collect(Collectors.groupingBy(MarkedMessageInfo::getThreadId)); Stream.of(threadToInfo).forEach(threadToInfoEntry -> { - Map> idMapForThread = Stream.of(threadToInfoEntry.getValue()) - .map(MarkedMessageInfo::getSyncMessageId) - .collect(Collectors.groupingBy(SyncMessageId::getRecipientId)); + Map> recipientIdToInfo = Stream.of(threadToInfoEntry.getValue()) + .map(info -> info) + .collect(Collectors.groupingBy(info -> info.getSyncMessageId().getRecipientId())); - Stream.of(idMapForThread).forEach(entry -> { - List timestamps = Stream.of(entry.getValue()).map(SyncMessageId::getTimetamp).toList(); + Stream.of(recipientIdToInfo).forEach(entry -> { + long threadId = threadToInfoEntry.getKey(); + RecipientId recipientId = entry.getKey(); + List infos = entry.getValue(); - SendReadReceiptJob.enqueue(threadToInfoEntry.getKey(), entry.getKey(), timestamps); + SendReadReceiptJob.enqueue(threadId, recipientId, infos); }); }); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageRepository.java b/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageRepository.java index 1b7094a3c..755fc44ae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageRepository.java @@ -9,6 +9,7 @@ import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessageDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.MultiDeviceViewedUpdateJob; @@ -37,7 +38,8 @@ class ViewOnceMessageRepository { if (info != null) { ApplicationDependencies.getJobManager().add(new SendViewedReceiptJob(record.getThreadId(), info.getSyncMessageId().getRecipientId(), - info.getSyncMessageId().getTimetamp())); + info.getSyncMessageId().getTimetamp(), + info.getMessageId())); MultiDeviceViewedUpdateJob.enqueue(Collections.singletonList(info.getSyncMessageId())); } diff --git a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/SendReadReceiptsJobMigrationTest.java b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/SendReadReceiptsJobMigrationTest.java index 5eb84ec07..55d844cc9 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/SendReadReceiptsJobMigrationTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/jobmanager/migrations/SendReadReceiptsJobMigrationTest.java @@ -23,7 +23,7 @@ public class SendReadReceiptsJobMigrationTest { @Test public void givenSendReadReceiptJobDataWithoutThreadIdAndThreadIdFound_whenIMigrate_thenIInsertThreadId() { // GIVEN - SendReadReceiptJob job = new SendReadReceiptJob(1, RecipientId.from(2), new ArrayList<>()); + SendReadReceiptJob job = new SendReadReceiptJob(1, RecipientId.from(2), new ArrayList<>(), new ArrayList<>()); JobMigration.JobData jobData = new JobMigration.JobData(job.getFactoryKey(), "asdf", new Data.Builder() @@ -45,7 +45,7 @@ public class SendReadReceiptsJobMigrationTest { @Test public void givenSendReadReceiptJobDataWithoutThreadIdAndThreadIdNotFound_whenIMigrate_thenIGetAFailingJob() { // GIVEN - SendReadReceiptJob job = new SendReadReceiptJob(1, RecipientId.from(2), new ArrayList<>()); + SendReadReceiptJob job = new SendReadReceiptJob(1, RecipientId.from(2), new ArrayList<>(), new ArrayList<>()); JobMigration.JobData jobData = new JobMigration.JobData(job.getFactoryKey(), "asdf", new Data.Builder() @@ -64,7 +64,7 @@ public class SendReadReceiptsJobMigrationTest { @Test public void givenSendReadReceiptJobDataWithThreadId_whenIMigrate_thenIDoNotReplace() { // GIVEN - SendReadReceiptJob job = new SendReadReceiptJob(1, RecipientId.from(2), new ArrayList<>()); + SendReadReceiptJob job = new SendReadReceiptJob(1, RecipientId.from(2), new ArrayList<>(), new ArrayList<>()); JobMigration.JobData jobData = new JobMigration.JobData(job.getFactoryKey(), "asdf", job.serialize()); // WHEN diff --git a/app/src/test/java/org/thoughtcrime/securesms/notifications/MarkReadReceiverTest.java b/app/src/test/java/org/thoughtcrime/securesms/notifications/MarkReadReceiverTest.java index e04467470..aef3eb629 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/notifications/MarkReadReceiverTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/notifications/MarkReadReceiverTest.java @@ -13,6 +13,7 @@ import org.mockito.stubbing.Answer; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import org.thoughtcrime.securesms.database.MessageDatabase; +import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; @@ -94,7 +95,8 @@ public class MarkReadReceiverTest { private MessageDatabase.MarkedMessageInfo createMarkedMessageInfo(long threadId, @NonNull RecipientId recipientId) { return new MessageDatabase.MarkedMessageInfo(threadId, - new MessageDatabase.SyncMessageId(recipientId, 0), - new MessageDatabase.ExpirationInfo(0, 0, 0, false)); + new MessageDatabase.SyncMessageId(recipientId, 0), + new MessageId(1, true), + new MessageDatabase.ExpirationInfo(0, 0, 0, false)); } } \ No newline at end of file