Add new VIEWED item in RecieptMessage enumeration.

Also includes necessary Database changes for supporting this as well as View-Once receipt support.
This commit is contained in:
Alex Hart
2020-11-20 09:16:37 -04:00
parent 7bb1262571
commit ce44e3949c
26 changed files with 432 additions and 33 deletions

View File

@@ -34,6 +34,7 @@ public class GroupReceiptDatabase extends Database {
public static final int STATUS_UNDELIVERED = 0; public static final int STATUS_UNDELIVERED = 0;
public static final int STATUS_DELIVERED = 1; public static final int STATUS_DELIVERED = 1;
public static final int STATUS_READ = 2; 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, " + 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);"; MMS_ID + " INTEGER, " + RECIPIENT_ID + " INTEGER, " + STATUS + " INTEGER, " + TIMESTAMP + " INTEGER, " + UNIDENTIFIED + " INTEGER DEFAULT 0);";

View File

@@ -117,12 +117,14 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
public abstract void markDownloadState(long messageId, long state); public abstract void markDownloadState(long messageId, long state);
public abstract void markIncomingNotificationReceived(long threadId); 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<Pair<Long, Long>> setTimestampRead(SyncMessageId messageId, long proposedExpireStarted); public abstract List<Pair<Long, Long>> setTimestampRead(SyncMessageId messageId, long proposedExpireStarted);
public abstract List<MarkedMessageInfo> setEntireThreadRead(long threadId); public abstract List<MarkedMessageInfo> setEntireThreadRead(long threadId);
public abstract List<MarkedMessageInfo> setMessagesReadSince(long threadId, long timestamp); public abstract List<MarkedMessageInfo> setMessagesReadSince(long threadId, long timestamp);
public abstract List<MarkedMessageInfo> setAllMessagesRead(); public abstract List<MarkedMessageInfo> setAllMessagesRead();
public abstract Pair<Long, Long> updateBundleMessageBody(long messageId, String body); public abstract Pair<Long, Long> updateBundleMessageBody(long messageId, String body);
public abstract @NonNull List<MarkedMessageInfo> getViewedIncomingMessages(long threadId);
public abstract @Nullable MarkedMessageInfo setIncomingMessageViewed(long messageId);
public abstract void addFailures(long messageId, List<NetworkFailure> failure); public abstract void addFailures(long messageId, List<NetworkFailure> failure);
public abstract void removeFailure(long messageId, NetworkFailure failure); public abstract void removeFailure(long messageId, NetworkFailure failure);
@@ -555,6 +557,28 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
return -1; 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 { public static class SyncMessageId {
private final RecipientId recipientId; private final RecipientId recipientId;

View File

@@ -185,7 +185,8 @@ public class MmsDatabase extends MessageDatabase {
REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1, " + REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1, " +
REMOTE_DELETED + " INTEGER DEFAULT 0, " + REMOTE_DELETED + " INTEGER DEFAULT 0, " +
MENTIONS_SELF + " 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 = { public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS mms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");", "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, 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, 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, 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(" + "json_group_array(json_object(" +
"'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " + "'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " +
"'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " + "'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " +
@@ -383,6 +384,69 @@ public class MmsDatabase extends MessageDatabase {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override
public @NonNull List<MarkedMessageInfo> 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<MarkedMessageInfo> 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 @Override
public @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address, boolean isVideoOffer) { public @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address, boolean isVideoOffer) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
@@ -540,23 +604,23 @@ public class MmsDatabase extends MessageDatabase {
} }
@Override @Override
public boolean incrementReceiptCount(SyncMessageId messageId, long timestamp, boolean deliveryReceipt) { public boolean incrementReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType) {
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
boolean found = false; 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())}, DATE_SENT + " = ?", new String[] {String.valueOf(messageId.getTimetamp())},
null, null, null, null)) { null, null, null, null)) {
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
if (Types.isOutgoingMessageType(cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX)))) { if (Types.isOutgoingMessageType(cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX)))) {
RecipientId theirRecipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))); RecipientId theirRecipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID)));
RecipientId ourRecipientId = messageId.getRecipientId(); 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()) { if (ourRecipientId.equals(theirRecipientId) || Recipient.resolved(theirRecipientId).isGroup()) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID)); long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_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; boolean isFirstIncrement = cursor.getLong(cursor.getColumnIndexOrThrow(columnName)) == 0;
found = true; 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()); earlyDeliveryReceiptCache.increment(messageId.getTimetamp(), messageId.getRecipientId());
return true; return true;
} }
@@ -1808,6 +1872,7 @@ public class MmsDatabase extends MessageDatabase {
Collections.emptyList(), Collections.emptyList(),
false, false,
false, false,
0,
0); 0);
} }
} }
@@ -1859,6 +1924,7 @@ public class MmsDatabase extends MessageDatabase {
int deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.DELIVERY_RECEIPT_COUNT)); int deliveryReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.DELIVERY_RECEIPT_COUNT));
int readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.READ_RECEIPT_COUNT)); int readReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.READ_RECEIPT_COUNT));
int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.SUBSCRIPTION_ID)); int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.SUBSCRIPTION_ID));
int viewedReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.VIEWED_RECEIPT_COUNT));
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
readReceiptCount = 0; readReceiptCount = 0;
@@ -1880,7 +1946,7 @@ public class MmsDatabase extends MessageDatabase {
addressDeviceId, dateSent, dateReceived, deliveryReceiptCount, threadId, addressDeviceId, dateSent, dateReceived, deliveryReceiptCount, threadId,
contentLocationBytes, messageSize, expiry, status, contentLocationBytes, messageSize, expiry, status,
transactionIdBytes, mailbox, subscriptionId, slideDeck, transactionIdBytes, mailbox, subscriptionId, slideDeck,
readReceiptCount); readReceiptCount, viewedReceiptCount);
} }
private MediaMmsMessageRecord getMediaMmsMessageRecord(Cursor cursor) { private MediaMmsMessageRecord getMediaMmsMessageRecord(Cursor cursor) {
@@ -1907,9 +1973,11 @@ public class MmsDatabase extends MessageDatabase {
List<ReactionRecord> reactions = parseReactions(cursor); List<ReactionRecord> reactions = parseReactions(cursor);
boolean mentionsSelf = CursorUtil.requireBoolean(cursor, MENTIONS_SELF); boolean mentionsSelf = CursorUtil.requireBoolean(cursor, MENTIONS_SELF);
long notifiedTimestamp = CursorUtil.requireLong(cursor, NOTIFIED_TIMESTAMP); long notifiedTimestamp = CursorUtil.requireLong(cursor, NOTIFIED_TIMESTAMP);
int viewedReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.VIEWED_RECEIPT_COUNT));
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
readReceiptCount = 0; readReceiptCount = 0;
viewedReceiptCount = 0;
} }
Recipient recipient = Recipient.live(RecipientId.from(recipientId)).get(); Recipient recipient = Recipient.live(RecipientId.from(recipientId)).get();
@@ -1928,7 +1996,7 @@ public class MmsDatabase extends MessageDatabase {
threadId, body, slideDeck, partCount, box, mismatches, threadId, body, slideDeck, partCount, box, mismatches,
networkFailures, subscriptionId, expiresIn, expireStarted, networkFailures, subscriptionId, expiresIn, expireStarted,
isViewOnce, readReceiptCount, quote, contacts, previews, unidentified, reactions, isViewOnce, readReceiptCount, quote, contacts, previews, unidentified, reactions,
remoteDelete, mentionsSelf, notifiedTimestamp); remoteDelete, mentionsSelf, notifiedTimestamp, viewedReceiptCount);
} }
private List<IdentityKeyMismatch> getMismatchedIdentities(String document) { private List<IdentityKeyMismatch> getMismatchedIdentities(String document) {

View File

@@ -14,6 +14,7 @@ public interface MmsSmsColumns {
public static final String ADDRESS_DEVICE_ID = "address_device_id"; public static final String ADDRESS_DEVICE_ID = "address_device_id";
public static final String DELIVERY_RECEIPT_COUNT = "delivery_receipt_count"; public static final String DELIVERY_RECEIPT_COUNT = "delivery_receipt_count";
public static final String READ_RECEIPT_COUNT = "read_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 MISMATCHED_IDENTITIES = "mismatched_identities";
public static final String UNIQUE_ROW_ID = "unique_row_id"; public static final String UNIQUE_ROW_ID = "unique_row_id";
public static final String SUBSCRIPTION_ID = "subscription_id"; public static final String SUBSCRIPTION_ID = "subscription_id";

View File

@@ -101,7 +101,8 @@ public class MmsSmsDatabase extends Database {
MmsSmsColumns.REACTIONS_LAST_SEEN, MmsSmsColumns.REACTIONS_LAST_SEEN,
MmsSmsColumns.REMOTE_DELETED, MmsSmsColumns.REMOTE_DELETED,
MmsDatabase.MENTIONS_SELF, MmsDatabase.MENTIONS_SELF,
MmsSmsColumns.NOTIFIED_TIMESTAMP}; MmsSmsColumns.NOTIFIED_TIMESTAMP,
MmsSmsColumns.VIEWED_RECEIPT_COUNT};
public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) { public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper); super(context, databaseHelper);
@@ -339,8 +340,8 @@ public class MmsSmsDatabase extends Database {
db.beginTransaction(); db.beginTransaction();
try { try {
DatabaseFactory.getSmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, true); DatabaseFactory.getSmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, MessageDatabase.ReceiptType.DELIVERY);
DatabaseFactory.getMmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, true); DatabaseFactory.getMmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, MessageDatabase.ReceiptType.DELIVERY);
db.setTransactionSuccessful(); db.setTransactionSuccessful();
} finally { } finally {
db.endTransaction(); db.endTransaction();
@@ -379,8 +380,8 @@ public class MmsSmsDatabase extends Database {
try { try {
boolean handled = false; boolean handled = false;
handled |= DatabaseFactory.getSmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, false); handled |= DatabaseFactory.getSmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, MessageDatabase.ReceiptType.READ);
handled |= DatabaseFactory.getMmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, false); handled |= DatabaseFactory.getMmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, MessageDatabase.ReceiptType.READ);
db.setTransactionSuccessful(); db.setTransactionSuccessful();
@@ -390,6 +391,35 @@ public class MmsSmsDatabase extends Database {
} }
} }
/**
* @return A list of ID's that were not updated.
*/
public @NonNull Collection<SyncMessageId> incrementViewedReceiptCounts(@NonNull List<SyncMessageId> syncMessageIds, long timestamp) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
List<SyncMessageId> 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) { public int getQuotedMessagePosition(long threadId, long quoteId, @NonNull RecipientId recipientId) {
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC"; String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.REMOTE_DELETED + " = 0"; String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.REMOTE_DELETED + " = 0";
@@ -546,7 +576,8 @@ public class MmsSmsDatabase extends Database {
MmsSmsColumns.DATE_SERVER, MmsSmsColumns.DATE_SERVER,
MmsSmsColumns.REMOTE_DELETED, MmsSmsColumns.REMOTE_DELETED,
MmsDatabase.MENTIONS_SELF, MmsDatabase.MENTIONS_SELF,
MmsSmsColumns.NOTIFIED_TIMESTAMP }; MmsSmsColumns.NOTIFIED_TIMESTAMP,
MmsSmsColumns.VIEWED_RECEIPT_COUNT};
String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT, String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED, SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
@@ -581,7 +612,8 @@ public class MmsSmsDatabase extends Database {
MmsSmsColumns.DATE_SERVER, MmsSmsColumns.DATE_SERVER,
MmsSmsColumns.REMOTE_DELETED, MmsSmsColumns.REMOTE_DELETED,
MmsDatabase.MENTIONS_SELF, MmsDatabase.MENTIONS_SELF,
MmsSmsColumns.NOTIFIED_TIMESTAMP }; MmsSmsColumns.NOTIFIED_TIMESTAMP,
MmsSmsColumns.VIEWED_RECEIPT_COUNT};
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder(); SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
@@ -637,6 +669,7 @@ public class MmsSmsDatabase extends Database {
mmsColumnsPresent.add(MmsDatabase.REMOTE_DELETED); mmsColumnsPresent.add(MmsDatabase.REMOTE_DELETED);
mmsColumnsPresent.add(MmsDatabase.MENTIONS_SELF); mmsColumnsPresent.add(MmsDatabase.MENTIONS_SELF);
mmsColumnsPresent.add(MmsSmsColumns.NOTIFIED_TIMESTAMP); mmsColumnsPresent.add(MmsSmsColumns.NOTIFIED_TIMESTAMP);
mmsColumnsPresent.add(MmsSmsColumns.VIEWED_RECEIPT_COUNT);
Set<String> smsColumnsPresent = new HashSet<>(); Set<String> smsColumnsPresent = new HashSet<>();
smsColumnsPresent.add(MmsSmsColumns.ID); smsColumnsPresent.add(MmsSmsColumns.ID);

View File

@@ -471,7 +471,11 @@ public class SmsDatabase extends MessageDatabase {
} }
@Override @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(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
boolean foundMessage = false; boolean foundMessage = false;
@@ -483,7 +487,7 @@ public class SmsDatabase extends MessageDatabase {
if (Types.isOutgoingMessageType(cursor.getLong(cursor.getColumnIndexOrThrow(TYPE)))) { if (Types.isOutgoingMessageType(cursor.getLong(cursor.getColumnIndexOrThrow(TYPE)))) {
RecipientId theirRecipientId = messageId.getRecipientId(); RecipientId theirRecipientId = messageId.getRecipientId();
RecipientId outRecipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))); 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; boolean isFirstIncrement = cursor.getLong(cursor.getColumnIndexOrThrow(columnName)) == 0;
if (outRecipientId.equals(theirRecipientId)) { 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()); earlyDeliveryReceiptCache.increment(messageId.getTimetamp(), messageId.getRecipientId());
return true; return true;
} }
@@ -623,6 +627,16 @@ public class SmsDatabase extends MessageDatabase {
return updateMessageBodyAndType(messageId, body, Types.TOTAL_MASK, type); return updateMessageBodyAndType(messageId, body, Types.TOTAL_MASK, type);
} }
@Override
public @NonNull List<MarkedMessageInfo> getViewedIncomingMessages(long threadId) {
return Collections.emptyList();
}
@Override
public @Nullable MarkedMessageInfo setIncomingMessageViewed(long messageId) {
return null;
}
private Pair<Long, Long> updateMessageBodyAndType(long messageId, String body, long maskOff, long maskOn) { private Pair<Long, Long> updateMessageBodyAndType(long messageId, String body, long maskOff, long maskOn) {
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.execSQL("UPDATE " + TABLE_NAME + " SET " + BODY + " = ?, " + db.execSQL("UPDATE " + TABLE_NAME + " SET " + BODY + " = ?, " +

View File

@@ -160,8 +160,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int GV1_MIGRATION = 80; private static final int GV1_MIGRATION = 80;
private static final int NOTIFIED_TIMESTAMP = 81; private static final int NOTIFIED_TIMESTAMP = 81;
private static final int GV1_MIGRATION_LAST_SEEN = 82; 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 static final String DATABASE_NAME = "signal.db";
private final Context context; 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"); 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(); db.setTransactionSuccessful();
} finally { } finally {
db.endTransaction(); db.endTransaction();

View File

@@ -45,10 +45,11 @@ public abstract class DisplayRecord {
private final int deliveryStatus; private final int deliveryStatus;
private final int deliveryReceiptCount; private final int deliveryReceiptCount;
private final int readReceiptCount; private final int readReceiptCount;
private final int viewReceiptCount;
DisplayRecord(String body, Recipient recipient, long dateSent, DisplayRecord(String body, Recipient recipient, long dateSent,
long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount, long dateReceived, long threadId, int deliveryStatus, int deliveryReceiptCount,
long type, int readReceiptCount) long type, int readReceiptCount, int viewReceiptCount)
{ {
this.threadId = threadId; this.threadId = threadId;
this.recipient = recipient; this.recipient = recipient;
@@ -59,6 +60,7 @@ public abstract class DisplayRecord {
this.deliveryReceiptCount = deliveryReceiptCount; this.deliveryReceiptCount = deliveryReceiptCount;
this.readReceiptCount = readReceiptCount; this.readReceiptCount = readReceiptCount;
this.deliveryStatus = deliveryStatus; this.deliveryStatus = deliveryStatus;
this.viewReceiptCount = viewReceiptCount;
} }
public @NonNull String getBody() { public @NonNull String getBody() {
@@ -188,6 +190,17 @@ public abstract class DisplayRecord {
return readReceiptCount; 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() { public boolean isDelivered() {
return (deliveryStatus >= SmsDatabase.Status.STATUS_COMPLETE && return (deliveryStatus >= SmsDatabase.Status.STATUS_COMPLETE &&
deliveryStatus < SmsDatabase.Status.STATUS_PENDING) || deliveryReceiptCount > 0; deliveryStatus < SmsDatabase.Status.STATUS_PENDING) || deliveryReceiptCount > 0;

View File

@@ -74,12 +74,13 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
@NonNull List<ReactionRecord> reactions, @NonNull List<ReactionRecord> reactions,
boolean remoteDelete, boolean remoteDelete,
boolean mentionsSelf, boolean mentionsSelf,
long notifiedTimestamp) long notifiedTimestamp,
int viewedReceiptCount)
{ {
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent, super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent,
dateReceived, dateServer, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures, dateReceived, dateServer, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures,
subscriptionId, expiresIn, expireStarted, viewOnce, slideDeck, 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.partCount = partCount;
this.mentionsSelf = mentionsSelf; this.mentionsSelf = mentionsSelf;
} }

View File

@@ -93,10 +93,12 @@ public abstract class MessageRecord extends DisplayRecord {
List<NetworkFailure> networkFailures, List<NetworkFailure> networkFailures,
int subscriptionId, long expiresIn, long expireStarted, int subscriptionId, long expiresIn, long expireStarted,
int readReceiptCount, boolean unidentified, int readReceiptCount, boolean unidentified,
@NonNull List<ReactionRecord> reactions, boolean remoteDelete, long notifiedTimestamp) @NonNull List<ReactionRecord> reactions, boolean remoteDelete, long notifiedTimestamp,
int viewedReceiptCount)
{ {
super(body, conversationRecipient, dateSent, dateReceived, super(body, conversationRecipient, dateSent, dateReceived,
threadId, deliveryStatus, deliveryReceiptCount, type, readReceiptCount); threadId, deliveryStatus, deliveryReceiptCount, type,
readReceiptCount, viewedReceiptCount);
this.id = id; this.id = id;
this.individualRecipient = individualRecipient; this.individualRecipient = individualRecipient;
this.recipientDeviceId = recipientDeviceId; this.recipientDeviceId = recipientDeviceId;

View File

@@ -33,9 +33,10 @@ public abstract class MmsMessageRecord extends MessageRecord {
@NonNull SlideDeck slideDeck, int readReceiptCount, @NonNull SlideDeck slideDeck, int readReceiptCount,
@Nullable Quote quote, @NonNull List<Contact> contacts, @Nullable Quote quote, @NonNull List<Contact> contacts,
@NonNull List<LinkPreview> linkPreviews, boolean unidentified, @NonNull List<LinkPreview> linkPreviews, boolean unidentified,
@NonNull List<ReactionRecord> reactions, boolean remoteDelete, long notifiedTimestamp) @NonNull List<ReactionRecord> 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.slideDeck = slideDeck;
this.quote = quote; this.quote = quote;

View File

@@ -50,13 +50,14 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord {
long dateSent, long dateReceived, int deliveryReceiptCount, long dateSent, long dateReceived, int deliveryReceiptCount,
long threadId, byte[] contentLocation, long messageSize, long threadId, byte[] contentLocation, long messageSize,
long expiry, int status, byte[] transactionId, long mailbox, 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, super(id, "", conversationRecipient, individualRecipient, recipientDeviceId,
dateSent, dateReceived, -1, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, dateSent, dateReceived, -1, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox,
new LinkedList<>(), new LinkedList<>(), subscriptionId, new LinkedList<>(), new LinkedList<>(), subscriptionId,
0, 0, false, slideDeck, readReceiptCount, null, Collections.emptyList(), Collections.emptyList(), false, 0, 0, false, slideDeck, readReceiptCount, null, Collections.emptyList(), Collections.emptyList(), false,
Collections.emptyList(), false, 0); Collections.emptyList(), false, 0, viewedReceiptCount);
this.contentLocation = contentLocation; this.contentLocation = contentLocation;
this.messageSize = messageSize; this.messageSize = messageSize;

View File

@@ -55,7 +55,7 @@ public class SmsMessageRecord extends MessageRecord {
super(id, body, recipient, individualRecipient, recipientDeviceId, super(id, body, recipient, individualRecipient, recipientDeviceId,
dateSent, dateReceived, dateServer, threadId, status, deliveryReceiptCount, type, dateSent, dateReceived, dateServer, threadId, status, deliveryReceiptCount, type,
mismatches, new LinkedList<>(), subscriptionId, mismatches, new LinkedList<>(), subscriptionId,
expiresIn, expireStarted, readReceiptCount, unidentified, reactions, remoteDelete, notifiedTimestamp); expiresIn, expireStarted, readReceiptCount, unidentified, reactions, remoteDelete, notifiedTimestamp, 0);
} }
public long getType() { public long getType() {

View File

@@ -8,9 +8,11 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
public class Data { public class Data {
@@ -138,6 +140,19 @@ public class Data {
return longArrays.get(key); return longArrays.get(key);
} }
public List<Long> getLongArrayAsList(@NonNull String key) {
throwIfAbsent(longArrays, key);
long[] array = Objects.requireNonNull(longArrays.get(key));
List<Long> longs = new ArrayList<>(array.length);
for (long l : array) {
longs.add(l);
}
return longs;
}
public boolean hasFloat(@NonNull String key) { public boolean hasFloat(@NonNull String key) {
return floats.containsKey(key); return floats.containsKey(key);
@@ -295,6 +310,17 @@ public class Data {
return this; return this;
} }
public Builder putLongListAsArray(@NonNull String key, @NonNull List<Long> 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) { public Builder putFloat(@NonNull String key, float value) {
floats.put(key, value); floats.put(key, value);
return this; return this;

View File

@@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob;
import org.thoughtcrime.securesms.jobs.RotateSignedPreKeyJob; import org.thoughtcrime.securesms.jobs.RotateSignedPreKeyJob;
import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob; import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob;
import org.thoughtcrime.securesms.jobs.SendReadReceiptJob; import org.thoughtcrime.securesms.jobs.SendReadReceiptJob;
import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob;
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob; import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
import org.thoughtcrime.securesms.jobs.SmsReceiveJob; import org.thoughtcrime.securesms.jobs.SmsReceiveJob;
import org.thoughtcrime.securesms.jobs.SmsSendJob; import org.thoughtcrime.securesms.jobs.SmsSendJob;
@@ -90,6 +91,7 @@ public class WorkManagerFactoryMappings {
put("RotateSignedPreKeyJob", RotateSignedPreKeyJob.KEY); put("RotateSignedPreKeyJob", RotateSignedPreKeyJob.KEY);
put("SendDeliveryReceiptJob", SendDeliveryReceiptJob.KEY); put("SendDeliveryReceiptJob", SendDeliveryReceiptJob.KEY);
put("SendReadReceiptJob", SendReadReceiptJob.KEY); put("SendReadReceiptJob", SendReadReceiptJob.KEY);
put("SendViewedReceiptJob", SendViewedReceiptJob.KEY);
put("ServiceOutageDetectionJob", ServiceOutageDetectionJob.KEY); put("ServiceOutageDetectionJob", ServiceOutageDetectionJob.KEY);
put("SmsReceiveJob", SmsReceiveJob.KEY); put("SmsReceiveJob", SmsReceiveJob.KEY);
put("SmsSendJob", SmsSendJob.KEY); put("SmsSendJob", SmsSendJob.KEY);

View File

@@ -120,6 +120,7 @@ public final class JobManagerFactories {
put(RotateSignedPreKeyJob.KEY, new RotateSignedPreKeyJob.Factory()); put(RotateSignedPreKeyJob.KEY, new RotateSignedPreKeyJob.Factory());
put(SendDeliveryReceiptJob.KEY, new SendDeliveryReceiptJob.Factory()); put(SendDeliveryReceiptJob.KEY, new SendDeliveryReceiptJob.Factory());
put(SendReadReceiptJob.KEY, new SendReadReceiptJob.Factory(application)); put(SendReadReceiptJob.KEY, new SendReadReceiptJob.Factory(application));
put(SendViewedReceiptJob.KEY, new SendViewedReceiptJob.Factory(application));
put(ServiceOutageDetectionJob.KEY, new ServiceOutageDetectionJob.Factory()); put(ServiceOutageDetectionJob.KEY, new ServiceOutageDetectionJob.Factory());
put(SmsReceiveJob.KEY, new SmsReceiveJob.Factory()); put(SmsReceiveJob.KEY, new SmsReceiveJob.Factory());
put(SmsSendJob.KEY, new SmsSendJob.Factory()); put(SmsSendJob.KEY, new SmsSendJob.Factory());

View File

@@ -137,6 +137,7 @@ public class PushMediaSendJob extends PushSendJob {
SyncMessageId id = new SyncMessageId(recipient.getId(), message.getSentTimeMillis()); SyncMessageId id = new SyncMessageId(recipient.getId(), message.getSentTimeMillis());
DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(id, System.currentTimeMillis()); DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(id, System.currentTimeMillis());
DatabaseFactory.getMmsSmsDatabase(context).incrementReadReceiptCount(id, System.currentTimeMillis()); DatabaseFactory.getMmsSmsDatabase(context).incrementReadReceiptCount(id, System.currentTimeMillis());
DatabaseFactory.getMmsSmsDatabase(context).incrementViewedReceiptCount(id, System.currentTimeMillis());
} }
if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN && profileKey == null) { if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN && profileKey == null) {

View File

@@ -433,6 +433,7 @@ public final class PushProcessMessageJob extends BaseJob {
if (message.isReadReceipt()) handleReadReceipt(content, message); if (message.isReadReceipt()) handleReadReceipt(content, message);
else if (message.isDeliveryReceipt()) handleDeliveryReceipt(content, message); else if (message.isDeliveryReceipt()) handleDeliveryReceipt(content, message);
else if (message.isViewedReceipt()) handleViewedReceipt(content, message);
} else if (content.getTypingMessage().isPresent()) { } else if (content.getTypingMessage().isPresent()) {
handleTypingMessage(content, content.getTypingMessage().get()); handleTypingMessage(content, content.getTypingMessage().get());
} else { } else {
@@ -1572,6 +1573,29 @@ public final class PushProcessMessageJob extends BaseJob {
ApplicationDependencies.getJobManager().add(new SendDeliveryReceiptJob(RecipientId.fromHighTrust(content.getSender()), message.getTimestamp())); 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<SyncMessageId> ids = Stream.of(message.getTimestamps())
.map(t -> new SyncMessageId(sender.getId(), t))
.toList();
Collection<SyncMessageId> 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") @SuppressLint("DefaultLocale")
private void handleDeliveryReceipt(@NonNull SignalServiceContent content, private void handleDeliveryReceipt(@NonNull SignalServiceContent content,
@NonNull SignalServiceReceiptMessage message) @NonNull SignalServiceReceiptMessage message)

View File

@@ -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<Long> 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<Long> 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<Long> 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<SendViewedReceiptJob> {
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<Long> 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);
}
}
}

View File

@@ -6,6 +6,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import androidx.core.util.Consumer; import androidx.core.util.Consumer;
import com.annimon.stream.Stream;
import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase; 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.GroupChangeErrorCallback;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason; import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.jobs.MultiDeviceMessageRequestResponseJob; import org.thoughtcrime.securesms.jobs.MultiDeviceMessageRequestResponseJob;
import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.LiveRecipient;
@@ -140,6 +143,16 @@ final class MessageRequestRepository {
ApplicationDependencies.getMessageNotifier().updateNotification(context); ApplicationDependencies.getMessageNotifier().updateNotification(context);
MarkReadReceiver.process(context, messageIds); MarkReadReceiver.process(context, messageIds);
List<MessageDatabase.MarkedMessageInfo> 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)) { if (TextSecurePreferences.isMultiDevice(context)) {
ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forAccept(liveRecipient.getId())); ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forAccept(liveRecipient.getId()));
} }

View File

@@ -8,6 +8,8 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageDatabase; import org.thoughtcrime.securesms.database.MessageDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord; 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.logging.Log;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
@@ -26,6 +28,12 @@ class ViewOnceMessageRepository {
SignalExecutors.BOUNDED.execute(() -> { SignalExecutors.BOUNDED.execute(() -> {
try (MmsDatabase.Reader reader = MmsDatabase.readerFor(mmsDatabase.getMessageCursor(messageId))) { try (MmsDatabase.Reader reader = MmsDatabase.readerFor(mmsDatabase.getMessageCursor(messageId))) {
MmsMessageRecord record = (MmsMessageRecord) reader.getNext(); 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)); callback.onComplete(Optional.fromNullable(record));
} }
}); });

View File

@@ -505,6 +505,7 @@ public class MessageSender {
mmsSmsDatabase.incrementDeliveryReceiptCount(syncId, System.currentTimeMillis()); mmsSmsDatabase.incrementDeliveryReceiptCount(syncId, System.currentTimeMillis());
mmsSmsDatabase.incrementReadReceiptCount(syncId, System.currentTimeMillis()); mmsSmsDatabase.incrementReadReceiptCount(syncId, System.currentTimeMillis());
mmsSmsDatabase.incrementViewedReceiptCount(syncId, System.currentTimeMillis());
if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) { if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) {
mmsDatabase.markExpireStarted(messageId); mmsDatabase.markExpireStarted(messageId);

View File

@@ -534,6 +534,7 @@ public class SignalServiceMessageSender {
if (message.isDeliveryReceipt()) builder.setType(ReceiptMessage.Type.DELIVERY); if (message.isDeliveryReceipt()) builder.setType(ReceiptMessage.Type.DELIVERY);
else if (message.isReadReceipt()) builder.setType(ReceiptMessage.Type.READ); else if (message.isReadReceipt()) builder.setType(ReceiptMessage.Type.READ);
else if (message.isViewedReceipt()) builder.setType(ReceiptMessage.Type.VIEWED);
return container.setReceiptMessage(builder).build().toByteArray(); return container.setReceiptMessage(builder).build().toByteArray();
} }

View File

@@ -632,6 +632,7 @@ public final class SignalServiceContent {
if (content.getType() == SignalServiceProtos.ReceiptMessage.Type.DELIVERY) type = SignalServiceReceiptMessage.Type.DELIVERY; 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.READ) type = SignalServiceReceiptMessage.Type.READ;
else if (content.getType() == SignalServiceProtos.ReceiptMessage.Type.VIEWED) type = SignalServiceReceiptMessage.Type.VIEWED;
else type = SignalServiceReceiptMessage.Type.UNKNOWN; else type = SignalServiceReceiptMessage.Type.UNKNOWN;
return new SignalServiceReceiptMessage(type, content.getTimestampList(), metadata.getTimestamp()); return new SignalServiceReceiptMessage(type, content.getTimestampList(), metadata.getTimestamp());

View File

@@ -6,7 +6,7 @@ import java.util.List;
public class SignalServiceReceiptMessage { public class SignalServiceReceiptMessage {
public enum Type { public enum Type {
UNKNOWN, DELIVERY, READ UNKNOWN, DELIVERY, READ, VIEWED
} }
private final Type type; private final Type type;
@@ -38,4 +38,8 @@ public class SignalServiceReceiptMessage {
public boolean isReadReceipt() { public boolean isReadReceipt() {
return type == Type.READ; return type == Type.READ;
} }
public boolean isViewedReceipt() {
return type == Type.VIEWED;
}
} }

View File

@@ -279,6 +279,7 @@ message ReceiptMessage {
enum Type { enum Type {
DELIVERY = 0; DELIVERY = 0;
READ = 1; READ = 1;
VIEWED = 2;
} }
optional Type type = 1; optional Type type = 1;