From ce44e3949ce5f086ec59d8c3a926d24be02cf6ee Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 20 Nov 2020 09:16:37 -0400 Subject: [PATCH] Add new VIEWED item in RecieptMessage enumeration. Also includes necessary Database changes for supporting this as well as View-Once receipt support. --- .../database/GroupReceiptDatabase.java | 1 + .../securesms/database/MessageDatabase.java | 26 ++- .../securesms/database/MmsDatabase.java | 88 ++++++++-- .../securesms/database/MmsSmsColumns.java | 1 + .../securesms/database/MmsSmsDatabase.java | 47 +++++- .../securesms/database/SmsDatabase.java | 20 ++- .../database/helpers/SQLCipherOpenHelper.java | 7 +- .../database/model/DisplayRecord.java | 15 +- .../database/model/MediaMmsMessageRecord.java | 5 +- .../database/model/MessageRecord.java | 6 +- .../database/model/MmsMessageRecord.java | 5 +- .../model/NotificationMmsMessageRecord.java | 5 +- .../database/model/SmsMessageRecord.java | 2 +- .../securesms/jobmanager/Data.java | 26 +++ .../WorkManagerFactoryMappings.java | 2 + .../securesms/jobs/JobManagerFactories.java | 1 + .../securesms/jobs/PushMediaSendJob.java | 1 + .../securesms/jobs/PushProcessMessageJob.java | 24 +++ .../securesms/jobs/SendViewedReceiptJob.java | 152 ++++++++++++++++++ .../MessageRequestRepository.java | 13 ++ .../revealable/ViewOnceMessageRepository.java | 8 + .../securesms/sms/MessageSender.java | 1 + .../api/SignalServiceMessageSender.java | 1 + .../api/messages/SignalServiceContent.java | 1 + .../messages/SignalServiceReceiptMessage.java | 6 +- .../src/main/proto/SignalService.proto | 1 + 26 files changed, 432 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java index d2e33ddf80..c7c91040e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java @@ -34,6 +34,7 @@ public class GroupReceiptDatabase extends Database { public static final int STATUS_UNDELIVERED = 0; public static final int STATUS_DELIVERED = 1; public static final int STATUS_READ = 2; + public static final int STATUS_VIEWED = 3; public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + MMS_ID + " INTEGER, " + RECIPIENT_ID + " INTEGER, " + STATUS + " INTEGER, " + TIMESTAMP + " INTEGER, " + UNIDENTIFIED + " INTEGER DEFAULT 0);"; 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 0c3e73b691..caaa2d9960 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java @@ -117,12 +117,14 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns public abstract void markDownloadState(long messageId, long state); public abstract void markIncomingNotificationReceived(long threadId); - public abstract boolean incrementReceiptCount(SyncMessageId messageId, long timestamp, boolean deliveryReceipt); + public abstract boolean incrementReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType); public abstract List> setTimestampRead(SyncMessageId messageId, long proposedExpireStarted); public abstract List setEntireThreadRead(long threadId); public abstract List setMessagesReadSince(long threadId, long timestamp); public abstract List setAllMessagesRead(); public abstract Pair updateBundleMessageBody(long messageId, String body); + public abstract @NonNull List getViewedIncomingMessages(long threadId); + public abstract @Nullable MarkedMessageInfo setIncomingMessageViewed(long messageId); public abstract void addFailures(long messageId, List failure); public abstract void removeFailure(long messageId, NetworkFailure failure); @@ -555,6 +557,28 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns return -1; } + protected enum ReceiptType { + READ(READ_RECEIPT_COUNT, GroupReceiptDatabase.STATUS_READ), + DELIVERY(DELIVERY_RECEIPT_COUNT, GroupReceiptDatabase.STATUS_DELIVERED), + VIEWED(VIEWED_RECEIPT_COUNT, GroupReceiptDatabase.STATUS_VIEWED); + + private final String columnName; + private final int groupStatus; + + ReceiptType(String columnName, int groupStatus) { + this.columnName = columnName; + this.groupStatus = groupStatus; + } + + public String getColumnName() { + return columnName; + } + + public int getGroupStatus() { + return groupStatus; + } + } + public static class SyncMessageId { private final RecipientId recipientId; 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 4caaa2a764..c64fc6e1b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -185,7 +185,8 @@ public class MmsDatabase extends MessageDatabase { REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1, " + REMOTE_DELETED + " INTEGER DEFAULT 0, " + MENTIONS_SELF + " INTEGER DEFAULT 0, " + - NOTIFIED_TIMESTAMP + " INTEGER DEFAULT 0);"; + NOTIFIED_TIMESTAMP + " INTEGER DEFAULT 0, " + + VIEWED_RECEIPT_COUNT + " INTEGER DEFAULT 0);"; public static final String[] CREATE_INDEXS = { "CREATE INDEX IF NOT EXISTS mms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");", @@ -210,7 +211,7 @@ public class MmsDatabase extends MessageDatabase { DELIVERY_RECEIPT_COUNT, READ_RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE, SUBSCRIPTION_ID, EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, QUOTE_ID, QUOTE_AUTHOR, QUOTE_BODY, QUOTE_ATTACHMENT, QUOTE_MISSING, QUOTE_MENTIONS, SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED, VIEW_ONCE, REACTIONS, REACTIONS_UNREAD, REACTIONS_LAST_SEEN, - REMOTE_DELETED, MENTIONS_SELF, NOTIFIED_TIMESTAMP, + REMOTE_DELETED, MENTIONS_SELF, NOTIFIED_TIMESTAMP, VIEWED_RECEIPT_COUNT, "json_group_array(json_object(" + "'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " + "'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " + @@ -383,6 +384,69 @@ public class MmsDatabase extends MessageDatabase { throw new UnsupportedOperationException(); } + @Override + public @NonNull List getViewedIncomingMessages(long threadId) { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + String[] columns = new String[]{ID, RECIPIENT_ID, DATE_SENT, MESSAGE_BOX, THREAD_ID}; + String where = THREAD_ID + " = ? AND " + VIEWED_RECEIPT_COUNT + " > 0 AND " + MESSAGE_BOX + " & " + Types.BASE_INBOX_TYPE + " = " + Types.BASE_INBOX_TYPE; + String[] args = SqlUtil.buildArgs(threadId); + + + try (Cursor cursor = db.query(getTableName(), columns, where, args, null, null, null, null)) { + if (cursor == null) { + return Collections.emptyList(); + } + + List results = new ArrayList<>(cursor.getCount()); + while (cursor.moveToNext()) { + RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndex(RECIPIENT_ID))); + long dateSent = cursor.getLong(cursor.getColumnIndex(DATE_SENT)); + SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent); + + results.add(new MarkedMessageInfo(threadId, syncMessageId, null)); + } + + return results; + } + } + + @Override + public @Nullable MarkedMessageInfo setIncomingMessageViewed(long messageId) { + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + String[] columns = new String[]{ID, RECIPIENT_ID, DATE_SENT, MESSAGE_BOX, THREAD_ID}; + String where = ID_WHERE + " AND " + VIEWED_RECEIPT_COUNT + " = 0"; + String[] args = SqlUtil.buildArgs(messageId); + + database.beginTransaction(); + try (Cursor cursor = database.query(TABLE_NAME, columns, where, args, null, null, null)) { + if (cursor == null || !cursor.moveToFirst()) { + return null; + } + + long type = CursorUtil.requireLong(cursor, MESSAGE_BOX); + if (Types.isSecureType(type) && Types.isInboxType(type)) { + long threadId = cursor.getLong(cursor.getColumnIndex(THREAD_ID)); + RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndex(RECIPIENT_ID))); + long dateSent = cursor.getLong(cursor.getColumnIndex(DATE_SENT)); + SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent); + + MarkedMessageInfo result = new MarkedMessageInfo(threadId, syncMessageId, null); + + ContentValues contentValues = new ContentValues(); + contentValues.put(VIEWED_RECEIPT_COUNT, 1); + + database.update(TABLE_NAME, contentValues, where, args); + database.setTransactionSuccessful(); + + return result; + } else { + return null; + } + } finally { + database.endTransaction(); + } + } + @Override public @NonNull Pair insertReceivedCall(@NonNull RecipientId address, boolean isVideoOffer) { throw new UnsupportedOperationException(); @@ -540,23 +604,23 @@ public class MmsDatabase extends MessageDatabase { } @Override - public boolean incrementReceiptCount(SyncMessageId messageId, long timestamp, boolean deliveryReceipt) { + public boolean incrementReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType) { SQLiteDatabase database = databaseHelper.getWritableDatabase(); boolean found = false; - try (Cursor cursor = database.query(TABLE_NAME, new String[] {ID, THREAD_ID, MESSAGE_BOX, RECIPIENT_ID, DELIVERY_RECEIPT_COUNT, READ_RECEIPT_COUNT}, + try (Cursor cursor = database.query(TABLE_NAME, new String[] {ID, THREAD_ID, MESSAGE_BOX, RECIPIENT_ID, receiptType.getColumnName()}, DATE_SENT + " = ?", new String[] {String.valueOf(messageId.getTimetamp())}, null, null, null, null)) { while (cursor.moveToNext()) { if (Types.isOutgoingMessageType(cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX)))) { RecipientId theirRecipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))); RecipientId ourRecipientId = messageId.getRecipientId(); - String columnName = deliveryReceipt ? DELIVERY_RECEIPT_COUNT : READ_RECEIPT_COUNT; + String columnName = receiptType.getColumnName(); if (ourRecipientId.equals(theirRecipientId) || Recipient.resolved(theirRecipientId).isGroup()) { long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID)); long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID)); - int status = deliveryReceipt ? GroupReceiptDatabase.STATUS_DELIVERED : GroupReceiptDatabase.STATUS_READ; + int status = receiptType.getGroupStatus(); boolean isFirstIncrement = cursor.getLong(cursor.getColumnIndexOrThrow(columnName)) == 0; found = true; @@ -577,7 +641,7 @@ public class MmsDatabase extends MessageDatabase { } } - if (!found && deliveryReceipt) { + if (!found && receiptType == ReceiptType.DELIVERY) { earlyDeliveryReceiptCache.increment(messageId.getTimetamp(), messageId.getRecipientId()); return true; } @@ -1808,6 +1872,7 @@ public class MmsDatabase extends MessageDatabase { Collections.emptyList(), false, false, + 0, 0); } } @@ -1859,6 +1924,7 @@ public class MmsDatabase extends MessageDatabase { int deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.DELIVERY_RECEIPT_COUNT)); int readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.READ_RECEIPT_COUNT)); int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.SUBSCRIPTION_ID)); + int viewedReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.VIEWED_RECEIPT_COUNT)); if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { readReceiptCount = 0; @@ -1880,7 +1946,7 @@ public class MmsDatabase extends MessageDatabase { addressDeviceId, dateSent, dateReceived, deliveryReceiptCount, threadId, contentLocationBytes, messageSize, expiry, status, transactionIdBytes, mailbox, subscriptionId, slideDeck, - readReceiptCount); + readReceiptCount, viewedReceiptCount); } private MediaMmsMessageRecord getMediaMmsMessageRecord(Cursor cursor) { @@ -1907,9 +1973,11 @@ public class MmsDatabase extends MessageDatabase { List reactions = parseReactions(cursor); boolean mentionsSelf = CursorUtil.requireBoolean(cursor, MENTIONS_SELF); long notifiedTimestamp = CursorUtil.requireLong(cursor, NOTIFIED_TIMESTAMP); + int viewedReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.VIEWED_RECEIPT_COUNT)); if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { - readReceiptCount = 0; + readReceiptCount = 0; + viewedReceiptCount = 0; } Recipient recipient = Recipient.live(RecipientId.from(recipientId)).get(); @@ -1928,7 +1996,7 @@ public class MmsDatabase extends MessageDatabase { threadId, body, slideDeck, partCount, box, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, isViewOnce, readReceiptCount, quote, contacts, previews, unidentified, reactions, - remoteDelete, mentionsSelf, notifiedTimestamp); + remoteDelete, mentionsSelf, notifiedTimestamp, viewedReceiptCount); } private List getMismatchedIdentities(String document) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java index 1d788f99d5..8e657ee292 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -14,6 +14,7 @@ public interface MmsSmsColumns { public static final String ADDRESS_DEVICE_ID = "address_device_id"; public static final String DELIVERY_RECEIPT_COUNT = "delivery_receipt_count"; public static final String READ_RECEIPT_COUNT = "read_receipt_count"; + public static final String VIEWED_RECEIPT_COUNT = "viewed_receipt_count"; public static final String MISMATCHED_IDENTITIES = "mismatched_identities"; public static final String UNIQUE_ROW_ID = "unique_row_id"; public static final String SUBSCRIPTION_ID = "subscription_id"; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index fdd94860fd..030e482f37 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -101,7 +101,8 @@ public class MmsSmsDatabase extends Database { MmsSmsColumns.REACTIONS_LAST_SEEN, MmsSmsColumns.REMOTE_DELETED, MmsDatabase.MENTIONS_SELF, - MmsSmsColumns.NOTIFIED_TIMESTAMP}; + MmsSmsColumns.NOTIFIED_TIMESTAMP, + MmsSmsColumns.VIEWED_RECEIPT_COUNT}; public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) { super(context, databaseHelper); @@ -339,8 +340,8 @@ public class MmsSmsDatabase extends Database { db.beginTransaction(); try { - DatabaseFactory.getSmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, true); - DatabaseFactory.getMmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, true); + DatabaseFactory.getSmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, MessageDatabase.ReceiptType.DELIVERY); + DatabaseFactory.getMmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, MessageDatabase.ReceiptType.DELIVERY); db.setTransactionSuccessful(); } finally { db.endTransaction(); @@ -379,8 +380,8 @@ public class MmsSmsDatabase extends Database { try { boolean handled = false; - handled |= DatabaseFactory.getSmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, false); - handled |= DatabaseFactory.getMmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, false); + handled |= DatabaseFactory.getSmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, MessageDatabase.ReceiptType.READ); + handled |= DatabaseFactory.getMmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, MessageDatabase.ReceiptType.READ); db.setTransactionSuccessful(); @@ -390,6 +391,35 @@ public class MmsSmsDatabase extends Database { } } + /** + * @return A list of ID's that were not updated. + */ + public @NonNull Collection incrementViewedReceiptCounts(@NonNull List syncMessageIds, long timestamp) { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + List unhandled = new LinkedList<>(); + + db.beginTransaction(); + try { + for (SyncMessageId id : syncMessageIds) { + boolean handled = incrementViewedReceiptCount(id, timestamp); + + if (!handled) { + unhandled.add(id); + } + } + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + return unhandled; + } + + public boolean incrementViewedReceiptCount(SyncMessageId syncMessageId, long timestamp) { + return DatabaseFactory.getMmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, MessageDatabase.ReceiptType.VIEWED); + } + public int getQuotedMessagePosition(long threadId, long quoteId, @NonNull RecipientId recipientId) { String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC"; String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.REMOTE_DELETED + " = 0"; @@ -546,7 +576,8 @@ public class MmsSmsDatabase extends Database { MmsSmsColumns.DATE_SERVER, MmsSmsColumns.REMOTE_DELETED, MmsDatabase.MENTIONS_SELF, - MmsSmsColumns.NOTIFIED_TIMESTAMP }; + MmsSmsColumns.NOTIFIED_TIMESTAMP, + MmsSmsColumns.VIEWED_RECEIPT_COUNT}; String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT, SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED, @@ -581,7 +612,8 @@ public class MmsSmsDatabase extends Database { MmsSmsColumns.DATE_SERVER, MmsSmsColumns.REMOTE_DELETED, MmsDatabase.MENTIONS_SELF, - MmsSmsColumns.NOTIFIED_TIMESTAMP }; + MmsSmsColumns.NOTIFIED_TIMESTAMP, + MmsSmsColumns.VIEWED_RECEIPT_COUNT}; SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder(); @@ -637,6 +669,7 @@ public class MmsSmsDatabase extends Database { mmsColumnsPresent.add(MmsDatabase.REMOTE_DELETED); mmsColumnsPresent.add(MmsDatabase.MENTIONS_SELF); mmsColumnsPresent.add(MmsSmsColumns.NOTIFIED_TIMESTAMP); + mmsColumnsPresent.add(MmsSmsColumns.VIEWED_RECEIPT_COUNT); Set smsColumnsPresent = new HashSet<>(); smsColumnsPresent.add(MmsSmsColumns.ID); 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 e208528fc6..f0254b0e83 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -471,7 +471,11 @@ public class SmsDatabase extends MessageDatabase { } @Override - public boolean incrementReceiptCount(SyncMessageId messageId, long timestamp, boolean deliveryReceipt) { + public boolean incrementReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType) { + if (receiptType == ReceiptType.VIEWED) { + return false; + } + SQLiteDatabase database = databaseHelper.getWritableDatabase(); boolean foundMessage = false; @@ -483,7 +487,7 @@ public class SmsDatabase extends MessageDatabase { if (Types.isOutgoingMessageType(cursor.getLong(cursor.getColumnIndexOrThrow(TYPE)))) { RecipientId theirRecipientId = messageId.getRecipientId(); RecipientId outRecipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))); - String columnName = deliveryReceipt ? DELIVERY_RECEIPT_COUNT : READ_RECEIPT_COUNT; + String columnName = receiptType.getColumnName(); boolean isFirstIncrement = cursor.getLong(cursor.getColumnIndexOrThrow(columnName)) == 0; if (outRecipientId.equals(theirRecipientId)) { @@ -507,7 +511,7 @@ public class SmsDatabase extends MessageDatabase { } } - if (!foundMessage && deliveryReceipt) { + if (!foundMessage && receiptType == ReceiptType.DELIVERY) { earlyDeliveryReceiptCache.increment(messageId.getTimetamp(), messageId.getRecipientId()); return true; } @@ -623,6 +627,16 @@ public class SmsDatabase extends MessageDatabase { return updateMessageBodyAndType(messageId, body, Types.TOTAL_MASK, type); } + @Override + public @NonNull List getViewedIncomingMessages(long threadId) { + return Collections.emptyList(); + } + + @Override + public @Nullable MarkedMessageInfo setIncomingMessageViewed(long messageId) { + return null; + } + private Pair updateMessageBodyAndType(long messageId, String body, long maskOff, long maskOn) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); db.execSQL("UPDATE " + TABLE_NAME + " SET " + BODY + " = ?, " + diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 1b891f57bf..3271b51b4e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -160,8 +160,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int GV1_MIGRATION = 80; private static final int NOTIFIED_TIMESTAMP = 81; private static final int GV1_MIGRATION_LAST_SEEN = 82; + private static final int VIEWED_RECEIPTS = 83; - private static final int DATABASE_VERSION = 82; + private static final int DATABASE_VERSION = 83; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -1170,6 +1171,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL("ALTER TABLE recipient ADD COLUMN last_gv1_migrate_reminder INTEGER DEFAULT 0"); } + if (oldVersion < VIEWED_RECEIPTS) { + db.execSQL("ALTER TABLE mms ADD COLUMN viewed_receipt_count INTEGER DEFAULT 0"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java index 43f66a5aeb..d1e22a3239 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java @@ -45,10 +45,11 @@ public abstract class DisplayRecord { private final int deliveryStatus; private final int deliveryReceiptCount; private final int readReceiptCount; + private final int viewReceiptCount; DisplayRecord(String body, Recipient recipient, long dateSent, long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount, - long type, int readReceiptCount) + long type, int readReceiptCount, int viewReceiptCount) { this.threadId = threadId; this.recipient = recipient; @@ -59,6 +60,7 @@ public abstract class DisplayRecord { this.deliveryReceiptCount = deliveryReceiptCount; this.readReceiptCount = readReceiptCount; this.deliveryStatus = deliveryStatus; + this.viewReceiptCount = viewReceiptCount; } public @NonNull String getBody() { @@ -188,6 +190,17 @@ public abstract class DisplayRecord { return readReceiptCount; } + /** + * For outgoing messages, this is incremented whenever a remote recipient has viewed our message + * and sends us a VIEWED receipt. For incoming messages, this is an indication of whether local + * user has viewed a piece of content. + * + * @return the number of times this has been viewed. + */ + public int getViewedReceiptCount() { + return viewReceiptCount; + } + public boolean isDelivered() { return (deliveryStatus >= SmsDatabase.Status.STATUS_COMPLETE && deliveryStatus < SmsDatabase.Status.STATUS_PENDING) || deliveryReceiptCount > 0; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java index e7e1bdd72a..376827ea48 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java @@ -74,12 +74,13 @@ public class MediaMmsMessageRecord extends MmsMessageRecord { @NonNull List reactions, boolean remoteDelete, boolean mentionsSelf, - long notifiedTimestamp) + long notifiedTimestamp, + int viewedReceiptCount) { super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, dateServer, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures, subscriptionId, expiresIn, expireStarted, viewOnce, slideDeck, - readReceiptCount, quote, contacts, linkPreviews, unidentified, reactions, remoteDelete, notifiedTimestamp); + readReceiptCount, quote, contacts, linkPreviews, unidentified, reactions, remoteDelete, notifiedTimestamp, viewedReceiptCount); this.partCount = partCount; this.mentionsSelf = mentionsSelf; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index c2dcba9dc8..2e7021f3df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -93,10 +93,12 @@ public abstract class MessageRecord extends DisplayRecord { List networkFailures, int subscriptionId, long expiresIn, long expireStarted, int readReceiptCount, boolean unidentified, - @NonNull List reactions, boolean remoteDelete, long notifiedTimestamp) + @NonNull List reactions, boolean remoteDelete, long notifiedTimestamp, + int viewedReceiptCount) { super(body, conversationRecipient, dateSent, dateReceived, - threadId, deliveryStatus, deliveryReceiptCount, type, readReceiptCount); + threadId, deliveryStatus, deliveryReceiptCount, type, + readReceiptCount, viewedReceiptCount); this.id = id; this.individualRecipient = individualRecipient; this.recipientDeviceId = recipientDeviceId; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java index c95e620535..996b1f79a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java @@ -33,9 +33,10 @@ public abstract class MmsMessageRecord extends MessageRecord { @NonNull SlideDeck slideDeck, int readReceiptCount, @Nullable Quote quote, @NonNull List contacts, @NonNull List linkPreviews, boolean unidentified, - @NonNull List reactions, boolean remoteDelete, long notifiedTimestamp) + @NonNull List reactions, boolean remoteDelete, long notifiedTimestamp, + int viewedReceiptCount) { - super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, dateServer, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, readReceiptCount, unidentified, reactions, remoteDelete, notifiedTimestamp); + super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, dateServer, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, readReceiptCount, unidentified, reactions, remoteDelete, notifiedTimestamp, viewedReceiptCount); this.slideDeck = slideDeck; this.quote = quote; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java index 3df9a9fd1a..7d708529a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java @@ -50,13 +50,14 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord { long dateSent, long dateReceived, int deliveryReceiptCount, long threadId, byte[] contentLocation, long messageSize, long expiry, int status, byte[] transactionId, long mailbox, - int subscriptionId, SlideDeck slideDeck, int readReceiptCount) + int subscriptionId, SlideDeck slideDeck, int readReceiptCount, + int viewedReceiptCount) { super(id, "", conversationRecipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, -1, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, new LinkedList<>(), new LinkedList<>(), subscriptionId, 0, 0, false, slideDeck, readReceiptCount, null, Collections.emptyList(), Collections.emptyList(), false, - Collections.emptyList(), false, 0); + Collections.emptyList(), false, 0, viewedReceiptCount); this.contentLocation = contentLocation; this.messageSize = messageSize; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java index f64466160b..d0fa3bfed5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java @@ -55,7 +55,7 @@ public class SmsMessageRecord extends MessageRecord { super(id, body, recipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, dateServer, threadId, status, deliveryReceiptCount, type, mismatches, new LinkedList<>(), subscriptionId, - expiresIn, expireStarted, readReceiptCount, unidentified, reactions, remoteDelete, notifiedTimestamp); + expiresIn, expireStarted, readReceiptCount, unidentified, reactions, remoteDelete, notifiedTimestamp, 0); } public long getType() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Data.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Data.java index 3a87ef5e84..9f59818451 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Data.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Data.java @@ -8,9 +8,11 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; public class Data { @@ -138,6 +140,19 @@ public class Data { return longArrays.get(key); } + public List getLongArrayAsList(@NonNull String key) { + throwIfAbsent(longArrays, key); + + long[] array = Objects.requireNonNull(longArrays.get(key)); + List longs = new ArrayList<>(array.length); + + for (long l : array) { + longs.add(l); + } + + return longs; + } + public boolean hasFloat(@NonNull String key) { return floats.containsKey(key); @@ -295,6 +310,17 @@ public class Data { return this; } + public Builder putLongListAsArray(@NonNull String key, @NonNull List value) { + long[] longs = new long[value.size()]; + + for (int i = 0; i < value.size(); i++) { + longs[i] = value.get(i); + } + + longArrays.put(key, longs); + return this; + } + public Builder putFloat(@NonNull String key, float value) { floats.put(key, value); return this; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/workmanager/WorkManagerFactoryMappings.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/workmanager/WorkManagerFactoryMappings.java index 55f62d2e36..5ed84820ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/workmanager/WorkManagerFactoryMappings.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/workmanager/WorkManagerFactoryMappings.java @@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob; import org.thoughtcrime.securesms.jobs.RotateSignedPreKeyJob; import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob; import org.thoughtcrime.securesms.jobs.SendReadReceiptJob; +import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob; import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob; import org.thoughtcrime.securesms.jobs.SmsReceiveJob; import org.thoughtcrime.securesms.jobs.SmsSendJob; @@ -90,6 +91,7 @@ public class WorkManagerFactoryMappings { put("RotateSignedPreKeyJob", RotateSignedPreKeyJob.KEY); put("SendDeliveryReceiptJob", SendDeliveryReceiptJob.KEY); put("SendReadReceiptJob", SendReadReceiptJob.KEY); + put("SendViewedReceiptJob", SendViewedReceiptJob.KEY); put("ServiceOutageDetectionJob", ServiceOutageDetectionJob.KEY); put("SmsReceiveJob", SmsReceiveJob.KEY); put("SmsSendJob", SmsSendJob.KEY); 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 113bedd0d3..a5212312ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -120,6 +120,7 @@ public final class JobManagerFactories { put(RotateSignedPreKeyJob.KEY, new RotateSignedPreKeyJob.Factory()); put(SendDeliveryReceiptJob.KEY, new SendDeliveryReceiptJob.Factory()); put(SendReadReceiptJob.KEY, new SendReadReceiptJob.Factory(application)); + put(SendViewedReceiptJob.KEY, new SendViewedReceiptJob.Factory(application)); put(ServiceOutageDetectionJob.KEY, new ServiceOutageDetectionJob.Factory()); put(SmsReceiveJob.KEY, new SmsReceiveJob.Factory()); put(SmsSendJob.KEY, new SmsSendJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java index 6c7c6c0dc8..b0c8354a67 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java @@ -137,6 +137,7 @@ public class PushMediaSendJob extends PushSendJob { SyncMessageId id = new SyncMessageId(recipient.getId(), message.getSentTimeMillis()); DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(id, System.currentTimeMillis()); DatabaseFactory.getMmsSmsDatabase(context).incrementReadReceiptCount(id, System.currentTimeMillis()); + DatabaseFactory.getMmsSmsDatabase(context).incrementViewedReceiptCount(id, System.currentTimeMillis()); } if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN && profileKey == null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java index 6b0e4d9a90..3d474c41eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java @@ -433,6 +433,7 @@ public final class PushProcessMessageJob extends BaseJob { if (message.isReadReceipt()) handleReadReceipt(content, message); else if (message.isDeliveryReceipt()) handleDeliveryReceipt(content, message); + else if (message.isViewedReceipt()) handleViewedReceipt(content, message); } else if (content.getTypingMessage().isPresent()) { handleTypingMessage(content, content.getTypingMessage().get()); } else { @@ -1572,6 +1573,29 @@ public final class PushProcessMessageJob extends BaseJob { ApplicationDependencies.getJobManager().add(new SendDeliveryReceiptJob(RecipientId.fromHighTrust(content.getSender()), message.getTimestamp())); } + private void handleViewedReceipt(@NonNull SignalServiceContent content, + @NonNull SignalServiceReceiptMessage message) + { + if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { + log(TAG, "Ignoring viewed receipts for IDs: " + Util.join(message.getTimestamps(), ", ")); + return; + } + + log(TAG, "Processing viewed reciepts for IDs: " + Util.join(message.getTimestamps(), ",")); + + Recipient sender = Recipient.externalHighTrustPush(context, content.getSender()); + List ids = Stream.of(message.getTimestamps()) + .map(t -> new SyncMessageId(sender.getId(), t)) + .toList(); + Collection unhandled = DatabaseFactory.getMmsSmsDatabase(context) + .incrementViewedReceiptCounts(ids, content.getTimestamp()); + + for (SyncMessageId id : unhandled) { + warn(TAG, String.valueOf(content.getTimestamp()), "[handleViewedReceipt] Could not find matching message! timestamp: " + id.getTimetamp() + " author: " + sender.getId()); + ApplicationDependencies.getEarlyMessageCache().store(sender.getId(), id.getTimetamp(), content); + } + } + @SuppressLint("DefaultLocale") private void handleDeliveryReceipt(@NonNull SignalServiceContent content, @NonNull SignalServiceReceiptMessage message) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java new file mode 100644 index 0000000000..fe711de403 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java @@ -0,0 +1,152 @@ +package org.thoughtcrime.securesms.jobs; + + +import android.app.Application; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.recipients.RecipientUtil; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class SendViewedReceiptJob extends BaseJob { + + public static final String KEY = "SendViewedReceiptJob"; + + private static final String TAG = SendViewedReceiptJob.class.getSimpleName(); + + 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 long threadId; + private RecipientId recipientId; + private List syncTimestamps; + private long timestamp; + + public SendViewedReceiptJob(long threadId, @NonNull RecipientId recipientId, long syncTimestamp) { + this(threadId, recipientId, Collections.singletonList(syncTimestamp)); + } + + public SendViewedReceiptJob(long threadId, @NonNull RecipientId recipientId, @NonNull List syncTimestamps) { + this(new Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .build(), + threadId, + recipientId, + syncTimestamps, + System.currentTimeMillis()); + } + + private SendViewedReceiptJob(@NonNull Parameters parameters, + long threadId, + @NonNull RecipientId recipientId, + @NonNull List syncTimestamps, + long timestamp) + { + super(parameters); + + this.threadId = threadId; + this.recipientId = recipientId; + this.syncTimestamps = syncTimestamps; + this.timestamp = timestamp; + } + + @Override + public @NonNull Data serialize() { + return new Data.Builder().putString(KEY_RECIPIENT, recipientId.serialize()) + .putLongListAsArray(KEY_SYNC_TIMESTAMPS, syncTimestamps) + .putLong(KEY_TIMESTAMP, timestamp) + .putLong(KEY_THREAD, threadId) + .build(); + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public void onRun() throws IOException, UntrustedIdentityException { + if (!TextSecurePreferences.isReadReceiptsEnabled(context) || syncTimestamps.isEmpty()) return; + + if (!RecipientUtil.isMessageRequestAccepted(context, threadId)) { + Log.w(TAG, "Refusing to send receipts to untrusted recipient"); + return; + } + + Recipient recipient = Recipient.resolved(recipientId); + if (recipient.isBlocked()) { + Log.w(TAG, "Refusing to send receipts to blocked recipient"); + return; + } + + if (recipient.isGroup()) { + Log.w(TAG, "Refusing to send receipts to group"); + return; + } + + SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); + SignalServiceAddress remoteAddress = RecipientUtil.toSignalServiceAddress(context, recipient); + SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED, + syncTimestamps, + timestamp); + + messageSender.sendReceipt(remoteAddress, + UnidentifiedAccessUtil.getAccessFor(context, Recipient.resolved(recipientId)), + receiptMessage); + } + + @Override + public boolean onShouldRetry(@NonNull Exception e) { + if (e instanceof PushNetworkException) return true; + return false; + } + + @Override + public void onFailure() { + Log.w(TAG, "Failed to send read receipts to: " + recipientId); + } + + public static final class Factory implements Job.Factory { + + private final Application application; + + public Factory(@NonNull Application application) { + this.application = application; + } + + @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); + + return new SendViewedReceiptJob(parameters, threadId, recipientId, syncTimestamps, 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 2d14253d3d..75fd9d9b44 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java @@ -6,6 +6,8 @@ import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import androidx.core.util.Consumer; +import com.annimon.stream.Stream; + import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; @@ -19,6 +21,7 @@ import org.thoughtcrime.securesms.groups.GroupManager; import org.thoughtcrime.securesms.groups.ui.GroupChangeErrorCallback; import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason; import org.thoughtcrime.securesms.jobs.MultiDeviceMessageRequestResponseJob; +import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.recipients.LiveRecipient; @@ -140,6 +143,16 @@ final class MessageRequestRepository { ApplicationDependencies.getMessageNotifier().updateNotification(context); MarkReadReceiver.process(context, messageIds); + List viewedInfos = DatabaseFactory.getMmsDatabase(context) + .getViewedIncomingMessages(threadId); + + ApplicationDependencies.getJobManager() + .add(new SendViewedReceiptJob(threadId, + liveRecipient.getId(), + Stream.of(viewedInfos) + .map(info -> info.getSyncMessageId().getTimetamp()) + .toList())); + if (TextSecurePreferences.isMultiDevice(context)) { ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forAccept(liveRecipient.getId())); } 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 97970df2c9..0b368e58cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageRepository.java @@ -8,6 +8,8 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessageDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.whispersystems.libsignal.util.guava.Optional; @@ -26,6 +28,12 @@ class ViewOnceMessageRepository { SignalExecutors.BOUNDED.execute(() -> { try (MmsDatabase.Reader reader = MmsDatabase.readerFor(mmsDatabase.getMessageCursor(messageId))) { MmsMessageRecord record = (MmsMessageRecord) reader.getNext(); + MessageDatabase.MarkedMessageInfo info = mmsDatabase.setIncomingMessageViewed(record.getId()); + if (info != null) { + ApplicationDependencies.getJobManager().add(new SendViewedReceiptJob(record.getThreadId(), + info.getSyncMessageId().getRecipientId(), + info.getSyncMessageId().getTimetamp())); + } callback.onComplete(Optional.fromNullable(record)); } }); diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java index 32fb2fcc21..cbea756bff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java @@ -505,6 +505,7 @@ public class MessageSender { mmsSmsDatabase.incrementDeliveryReceiptCount(syncId, System.currentTimeMillis()); mmsSmsDatabase.incrementReadReceiptCount(syncId, System.currentTimeMillis()); + mmsSmsDatabase.incrementViewedReceiptCount(syncId, System.currentTimeMillis()); if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) { mmsDatabase.markExpireStarted(messageId); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 72ee050a2e..84be65fa14 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -534,6 +534,7 @@ public class SignalServiceMessageSender { if (message.isDeliveryReceipt()) builder.setType(ReceiptMessage.Type.DELIVERY); else if (message.isReadReceipt()) builder.setType(ReceiptMessage.Type.READ); + else if (message.isViewedReceipt()) builder.setType(ReceiptMessage.Type.VIEWED); return container.setReceiptMessage(builder).build().toByteArray(); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java index 2bbf94e1d7..b4b418873a 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java @@ -632,6 +632,7 @@ public final class SignalServiceContent { if (content.getType() == SignalServiceProtos.ReceiptMessage.Type.DELIVERY) type = SignalServiceReceiptMessage.Type.DELIVERY; else if (content.getType() == SignalServiceProtos.ReceiptMessage.Type.READ) type = SignalServiceReceiptMessage.Type.READ; + else if (content.getType() == SignalServiceProtos.ReceiptMessage.Type.VIEWED) type = SignalServiceReceiptMessage.Type.VIEWED; else type = SignalServiceReceiptMessage.Type.UNKNOWN; return new SignalServiceReceiptMessage(type, content.getTimestampList(), metadata.getTimestamp()); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceReceiptMessage.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceReceiptMessage.java index be9475c96d..d3490babbd 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceReceiptMessage.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceReceiptMessage.java @@ -6,7 +6,7 @@ import java.util.List; public class SignalServiceReceiptMessage { public enum Type { - UNKNOWN, DELIVERY, READ + UNKNOWN, DELIVERY, READ, VIEWED } private final Type type; @@ -38,4 +38,8 @@ public class SignalServiceReceiptMessage { public boolean isReadReceipt() { return type == Type.READ; } + + public boolean isViewedReceipt() { + return type == Type.VIEWED; + } } diff --git a/libsignal/service/src/main/proto/SignalService.proto b/libsignal/service/src/main/proto/SignalService.proto index 74ff8864e1..e2cfea3e7e 100644 --- a/libsignal/service/src/main/proto/SignalService.proto +++ b/libsignal/service/src/main/proto/SignalService.proto @@ -279,6 +279,7 @@ message ReceiptMessage { enum Type { DELIVERY = 0; READ = 1; + VIEWED = 2; } optional Type type = 1;