From 2c1337b33efe0063f8d39de974514567becd8be3 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Mon, 2 Oct 2017 14:54:55 -0700 Subject: [PATCH] Make early receipts work in group messages For both conversation item view and message details view // FREEBIE --- .../securesms/MessageRecipientListItem.java | 24 +++--- .../securesms/database/EarlyReceiptCache.java | 57 ++++++-------- .../securesms/database/MessagingDatabase.java | 5 ++ .../securesms/database/MmsDatabase.java | 16 +++- .../securesms/database/SmsDatabase.java | 14 +++- .../securesms/jobs/PushDecryptJob.java | 74 +++++++++---------- 6 files changed, 101 insertions(+), 89 deletions(-) diff --git a/src/org/thoughtcrime/securesms/MessageRecipientListItem.java b/src/org/thoughtcrime/securesms/MessageRecipientListItem.java index 4550b5b24b..a90884eba5 100644 --- a/src/org/thoughtcrime/securesms/MessageRecipientListItem.java +++ b/src/org/thoughtcrime/securesms/MessageRecipientListItem.java @@ -122,17 +122,21 @@ public class MessageRecipientListItem extends RelativeLayout new ResendAsyncTask(masterSecret, record, networkFailure).execute(); }); } else { - if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.PENDING || member.getDeliveryStatus() == RecipientDeliveryStatus.Status.UNKNOWN) { + if (record.isOutgoing()) { + if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.PENDING || member.getDeliveryStatus() == RecipientDeliveryStatus.Status.UNKNOWN) { + deliveryStatusView.setVisibility(View.GONE); + } else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.READ) { + deliveryStatusView.setRead(); + deliveryStatusView.setVisibility(View.VISIBLE); + } else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.DELIVERED) { + deliveryStatusView.setDelivered(); + deliveryStatusView.setVisibility(View.VISIBLE); + } else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.SENT) { + deliveryStatusView.setSent(); + deliveryStatusView.setVisibility(View.VISIBLE); + } + } else { deliveryStatusView.setVisibility(View.GONE); - } else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.READ) { - deliveryStatusView.setRead(); - deliveryStatusView.setVisibility(View.VISIBLE); - } else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.DELIVERED) { - deliveryStatusView.setDelivered(); - deliveryStatusView.setVisibility(View.VISIBLE); - } else if (member.getDeliveryStatus() == RecipientDeliveryStatus.Status.SENT) { - deliveryStatusView.setSent(); - deliveryStatusView.setVisibility(View.VISIBLE); } resendButton.setVisibility(View.GONE); diff --git a/src/org/thoughtcrime/securesms/database/EarlyReceiptCache.java b/src/org/thoughtcrime/securesms/database/EarlyReceiptCache.java index f1648397a2..94d7b79631 100644 --- a/src/org/thoughtcrime/securesms/database/EarlyReceiptCache.java +++ b/src/org/thoughtcrime/securesms/database/EarlyReceiptCache.java @@ -1,56 +1,45 @@ package org.thoughtcrime.securesms.database; -import android.support.annotation.NonNull; import android.util.Log; import org.thoughtcrime.securesms.util.LRUCache; +import java.util.HashMap; +import java.util.Map; + public class EarlyReceiptCache { private static final String TAG = EarlyReceiptCache.class.getSimpleName(); - private final LRUCache cache = new LRUCache<>(100); + private final LRUCache> cache = new LRUCache<>(100); - public synchronized void increment(long timestamp, Address address) { + public synchronized void increment(long timestamp, Address origin) { Log.w(TAG, this+""); - Log.w(TAG, String.format("Early receipt: %d,%s", timestamp, address)); - Placeholder tuple = new Placeholder(timestamp, address); - Long count = cache.get(tuple); + Log.w(TAG, String.format("Early receipt: (%d, %s)", timestamp, origin.serialize())); + + Map receipts = cache.get(timestamp); + + if (receipts == null) { + receipts = new HashMap<>(); + } + + Long count = receipts.get(origin); if (count != null) { - cache.put(tuple, ++count); + receipts.put(origin, ++count); } else { - cache.put(tuple, 1L); + receipts.put(origin, 1L); } + + cache.put(timestamp, receipts); } - public synchronized long remove(long timestamp, Address address) { - Long count = cache.remove(new Placeholder(timestamp, address)); + public synchronized Map remove(long timestamp) { + Map receipts = cache.remove(timestamp); + Log.w(TAG, this+""); - Log.w(TAG, String.format("Checking early receipts (%d, %s): %d", timestamp, address, count)); - return count != null ? count : 0; - } + Log.w(TAG, String.format("Checking early receipts (%d): %d", timestamp, receipts == null ? 0 : receipts.size())); - private class Placeholder { - - private final long timestamp; - private final @NonNull Address address; - - private Placeholder(long timestamp, @NonNull Address address) { - this.timestamp = timestamp; - this.address = address; - } - - @Override - public boolean equals(Object other) { - return other != null && other instanceof Placeholder && - ((Placeholder)other).timestamp == this.timestamp && - ((Placeholder)other).address.equals(this.address); - } - - @Override - public int hashCode() { - return (int)timestamp ^ address.hashCode(); - } + return receipts != null ? receipts : new HashMap<>(); } } diff --git a/src/org/thoughtcrime/securesms/database/MessagingDatabase.java b/src/org/thoughtcrime/securesms/database/MessagingDatabase.java index 775a7c2339..266baa650f 100644 --- a/src/org/thoughtcrime/securesms/database/MessagingDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MessagingDatabase.java @@ -29,6 +29,11 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn protected abstract String getTableName(); + public abstract void markExpireStarted(long messageId); + public abstract void markExpireStarted(long messageId, long startTime); + + public abstract void markAsSent(long messageId, boolean secure); + public void setMismatchedIdentity(long messageId, final Address address, final IdentityKey identityKey) { List items = new ArrayList() {{ add(new IdentityKeyMismatch(address, identityKey)); diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java index 87bc7c9bd5..05848b676c 100644 --- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -72,6 +72,7 @@ import java.security.SecureRandom; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; public class MmsDatabase extends MessagingDatabase { @@ -338,6 +339,7 @@ public class MmsDatabase extends MessagingDatabase { notifyConversationListeners(threadId); } + @Override public void markAsSent(long messageId, boolean secure) { long threadId = getThreadIdForMessage(messageId); updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_SENT_TYPE | (secure ? Types.PUSH_MESSAGE_BIT | Types.SECURE_MESSAGE_BIT : 0), Optional.of(threadId)); @@ -385,10 +387,12 @@ public class MmsDatabase extends MessagingDatabase { notifyConversationListeners(threadId); } + @Override public void markExpireStarted(long messageId) { markExpireStarted(messageId, System.currentTimeMillis()); } + @Override public void markExpireStarted(long messageId, long startedTimestamp) { ContentValues contentValues = new ContentValues(); contentValues.put(EXPIRE_STARTED, startedTimestamp); @@ -781,7 +785,7 @@ public class MmsDatabase extends MessagingDatabase { public long insertMessageOutbox(@NonNull MasterSecretUnion masterSecret, @NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, - SmsDatabase.InsertListener insertListener) + @Nullable SmsDatabase.InsertListener insertListener) throws MmsException { long type = Types.BASE_SENDING_TYPE; @@ -801,6 +805,9 @@ public class MmsDatabase extends MessagingDatabase { type |= Types.EXPIRATION_TIMER_UPDATE_BIT; } + Map earlyDeliveryReceipts = earlyDeliveryReceiptCache.remove(message.getSentTimeMillis()); + Map earlyReadReceipts = earlyReadReceiptCache.remove(message.getSentTimeMillis()); + ContentValues contentValues = new ContentValues(); contentValues.put(DATE_SENT, message.getSentTimeMillis()); contentValues.put(MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ); @@ -812,8 +819,8 @@ public class MmsDatabase extends MessagingDatabase { contentValues.put(SUBSCRIPTION_ID, message.getSubscriptionId()); contentValues.put(EXPIRES_IN, message.getExpiresIn()); contentValues.put(ADDRESS, message.getRecipient().getAddress().serialize()); - contentValues.put(DELIVERY_RECEIPT_COUNT, earlyDeliveryReceiptCache.remove(message.getSentTimeMillis(), message.getRecipient().getAddress())); - contentValues.put(READ_RECEIPT_COUNT, earlyReadReceiptCache.remove(message.getSentTimeMillis(), message.getRecipient().getAddress())); + contentValues.put(DELIVERY_RECEIPT_COUNT, Stream.of(earlyDeliveryReceipts.values()).mapToLong(Long::longValue).sum()); + contentValues.put(READ_RECEIPT_COUNT, Stream.of(earlyReadReceipts.values()).mapToLong(Long::longValue).sum()); long messageId = insertMediaMessage(masterSecret, message.getBody(), message.getAttachments(), contentValues, insertListener); @@ -823,6 +830,9 @@ public class MmsDatabase extends MessagingDatabase { receiptDatabase.insert(Stream.of(members).map(Recipient::getAddress).toList(), messageId, GroupReceiptDatabase.STATUS_UNDELIVERED, message.getSentTimeMillis()); + + for (Address address : earlyDeliveryReceipts.keySet()) receiptDatabase.update(address, messageId, GroupReceiptDatabase.STATUS_DELIVERED, -1); + for (Address address : earlyReadReceipts.keySet()) receiptDatabase.update(address, messageId, GroupReceiptDatabase.STATUS_READ, -1); } DatabaseFactory.getThreadDatabase(context).setLastSeen(threadId); diff --git a/src/org/thoughtcrime/securesms/database/SmsDatabase.java b/src/org/thoughtcrime/securesms/database/SmsDatabase.java index 1f3e5f0a7a..ee589add90 100644 --- a/src/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -27,6 +27,8 @@ import android.text.TextUtils; import android.util.Log; import android.util.Pair; +import com.annimon.stream.Stream; + import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList; @@ -48,6 +50,7 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -230,6 +233,7 @@ public class SmsDatabase extends MessagingDatabase { updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_PENDING_INSECURE_SMS_FALLBACK); } + @Override public void markAsSent(long id, boolean isSecure) { updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENT_TYPE | (isSecure ? Types.PUSH_MESSAGE_BIT | Types.SECURE_MESSAGE_BIT : 0)); } @@ -238,10 +242,12 @@ public class SmsDatabase extends MessagingDatabase { updateTypeBitmask(id, Types.TOTAL_MASK, Types.MISSED_CALL_TYPE); } + @Override public void markExpireStarted(long id) { markExpireStarted(id, System.currentTimeMillis()); } + @Override public void markExpireStarted(long id, long startedAtTimestamp) { ContentValues contentValues = new ContentValues(); contentValues.put(EXPIRE_STARTED, startedAtTimestamp); @@ -594,7 +600,9 @@ public class SmsDatabase extends MessagingDatabase { if (message.isIdentityVerified()) type |= Types.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT; else if (message.isIdentityDefault()) type |= Types.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT; - Address address = message.getRecipient().getAddress(); + Address address = message.getRecipient().getAddress(); + Map earlyDeliveryReceipts = earlyDeliveryReceiptCache.remove(date); + Map earlyReadReceipts = earlyReadReceiptCache.remove(date); ContentValues contentValues = new ContentValues(6); contentValues.put(ADDRESS, address.serialize()); @@ -606,8 +614,8 @@ public class SmsDatabase extends MessagingDatabase { contentValues.put(TYPE, type); contentValues.put(SUBSCRIPTION_ID, message.getSubscriptionId()); contentValues.put(EXPIRES_IN, message.getExpiresIn()); - contentValues.put(DELIVERY_RECEIPT_COUNT, earlyDeliveryReceiptCache.remove(date, address)); - contentValues.put(READ_RECEIPT_COUNT, earlyReadReceiptCache.remove(date, address)); + contentValues.put(DELIVERY_RECEIPT_COUNT, Stream.of(earlyDeliveryReceipts.values()).mapToLong(Long::longValue).sum()); + contentValues.put(READ_RECEIPT_COUNT, Stream.of(earlyReadReceipts.values()).mapToLong(Long::longValue).sum()); SQLiteDatabase db = databaseHelper.getWritableDatabase(); long messageId = db.insert(TABLE_NAME, ADDRESS, contentValues); diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index cffac26073..8d83cda019 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; import org.thoughtcrime.securesms.database.GroupDatabase; +import org.thoughtcrime.securesms.database.MessagingDatabase; import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult; import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; import org.thoughtcrime.securesms.database.MmsDatabase; @@ -34,6 +35,7 @@ import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage; +import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.KeyCachingService; @@ -42,6 +44,7 @@ import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage; import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage; import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage; +import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage; import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage; import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.thoughtcrime.securesms.util.Base64; @@ -173,7 +176,7 @@ public class PushDecryptJob extends ContextJob { } else if (content.getSyncMessage().isPresent()) { SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); - if (syncMessage.getSent().isPresent()) handleSynchronizeSentMessage(masterSecret, envelope, syncMessage.getSent().get(), smsMessageId); + if (syncMessage.getSent().isPresent()) handleSynchronizeSentMessage(masterSecret, envelope, syncMessage.getSent().get()); else if (syncMessage.getRequest().isPresent()) handleSynchronizeRequestMessage(masterSecret, syncMessage.getRequest().get()); else if (syncMessage.getRead().isPresent()) handleSynchronizeReadMessage(masterSecret, syncMessage.getRead().get(), envelope.getTimestamp()); else if (syncMessage.getVerified().isPresent()) handleSynchronizeVerifiedMessage(masterSecret, syncMessage.getVerified().get()); @@ -328,8 +331,7 @@ public class PushDecryptJob extends ContextJob { } private long handleSynchronizeSentEndSessionMessage(@NonNull MasterSecretUnion masterSecret, - @NonNull SentTranscriptMessage message, - @NonNull Optional smsMessageId) + @NonNull SentTranscriptMessage message) { EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); Recipient recipient = getSyncMessageDestination(message); @@ -349,10 +351,6 @@ public class PushDecryptJob extends ContextJob { database.markAsSent(messageId, true); } - if (smsMessageId.isPresent()) { - database.deleteMessage(smsMessageId.get()); - } - return threadId; } @@ -411,8 +409,7 @@ public class PushDecryptJob extends ContextJob { private void handleSynchronizeSentMessage(@NonNull MasterSecretUnion masterSecret, @NonNull SignalServiceEnvelope envelope, - @NonNull SentTranscriptMessage message, - @NonNull Optional smsMessageId) + @NonNull SentTranscriptMessage message) throws MmsException { GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); @@ -420,15 +417,15 @@ public class PushDecryptJob extends ContextJob { Long threadId; if (message.getMessage().isEndSession()) { - threadId = handleSynchronizeSentEndSessionMessage(masterSecret, message, smsMessageId); + threadId = handleSynchronizeSentEndSessionMessage(masterSecret, message); } else if (message.getMessage().isGroupUpdate()) { threadId = GroupMessageProcessor.process(context, masterSecret, envelope, message.getMessage(), true); } else if (message.getMessage().isExpirationUpdate()) { - threadId = handleSynchronizeSentExpirationUpdate(masterSecret, message, smsMessageId); + threadId = handleSynchronizeSentExpirationUpdate(masterSecret, message); } else if (message.getMessage().getAttachments().isPresent()) { - threadId = handleSynchronizeSentMediaMessage(masterSecret, message, smsMessageId); + threadId = handleSynchronizeSentMediaMessage(masterSecret, message); } else { - threadId = handleSynchronizeSentTextMessage(masterSecret, message, smsMessageId); + threadId = handleSynchronizeSentTextMessage(masterSecret, message); } if (message.getMessage().getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get().getGroupId(), false))) { @@ -556,8 +553,7 @@ public class PushDecryptJob extends ContextJob { } private long handleSynchronizeSentExpirationUpdate(@NonNull MasterSecretUnion masterSecret, - @NonNull SentTranscriptMessage message, - @NonNull Optional smsMessageId) + @NonNull SentTranscriptMessage message) throws MmsException { MmsDatabase database = DatabaseFactory.getMmsDatabase(context); @@ -574,16 +570,11 @@ public class PushDecryptJob extends ContextJob { DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient, message.getMessage().getExpiresInSeconds()); - if (smsMessageId.isPresent()) { - DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get()); - } - return threadId; } private long handleSynchronizeSentMediaMessage(@NonNull MasterSecretUnion masterSecret, - @NonNull SentTranscriptMessage message, - @NonNull Optional smsMessageId) + @NonNull SentTranscriptMessage message) throws MmsException { MmsDatabase database = DatabaseFactory.getMmsDatabase(context); @@ -597,7 +588,7 @@ public class PushDecryptJob extends ContextJob { mediaMessage = new OutgoingSecureMediaMessage(mediaMessage); if (recipients.getExpireMessages() != message.getMessage().getExpiresInSeconds()) { - handleSynchronizeSentExpirationUpdate(masterSecret, message, Optional.absent()); + handleSynchronizeSentExpirationUpdate(masterSecret, message); } long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients); @@ -611,10 +602,6 @@ public class PushDecryptJob extends ContextJob { .add(new AttachmentDownloadJob(context, messageId, attachment.getAttachmentId(), false)); } - if (smsMessageId.isPresent()) { - DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get()); - } - if (message.getMessage().getExpiresInSeconds() > 0) { database.markExpireStarted(messageId, message.getExpirationStartTimestamp()); ApplicationContext.getInstance(context) @@ -667,29 +654,38 @@ public class PushDecryptJob extends ContextJob { } private long handleSynchronizeSentTextMessage(@NonNull MasterSecretUnion masterSecret, - @NonNull SentTranscriptMessage message, - @NonNull Optional smsMessageId) + @NonNull SentTranscriptMessage message) throws MmsException { - EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); - Recipient recipient = getSyncMessageDestination(message); - String body = message.getMessage().getBody().or(""); - long expiresInMillis = message.getMessage().getExpiresInSeconds() * 1000; - OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipient, body, expiresInMillis, -1); + + Recipient recipient = getSyncMessageDestination(message); + String body = message.getMessage().getBody().or(""); + long expiresInMillis = message.getMessage().getExpiresInSeconds() * 1000; if (recipient.getExpireMessages() != message.getMessage().getExpiresInSeconds()) { - handleSynchronizeSentExpirationUpdate(masterSecret, message, Optional.absent()); + handleSynchronizeSentExpirationUpdate(masterSecret, message); } long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); - long messageId = database.insertMessageOutbox(masterSecret, threadId, outgoingTextMessage, false, message.getTimestamp(), null); + + MessagingDatabase database; + long messageId; + + if (recipient.getAddress().isGroup()) { + OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient, new SlideDeck(), body, message.getTimestamp(), -1, expiresInMillis, ThreadDatabase.DistributionTypes.DEFAULT); + outgoingMediaMessage = new OutgoingSecureMediaMessage(outgoingMediaMessage); + + messageId = DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(masterSecret, outgoingMediaMessage, threadId, false, null); + database = DatabaseFactory.getMmsDatabase(context); + } else { + OutgoingTextMessage outgoingTextMessage = new OutgoingEncryptedMessage(recipient, body, expiresInMillis); + + messageId = DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageOutbox(masterSecret, threadId, outgoingTextMessage, false, message.getTimestamp(), null); + database = DatabaseFactory.getSmsDatabase(context); + } database.markAsSent(messageId, true); - if (smsMessageId.isPresent()) { - database.deleteMessage(smsMessageId.get()); - } - if (expiresInMillis > 0) { database.markExpireStarted(messageId, message.getExpirationStartTimestamp()); ApplicationContext.getInstance(context)