diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/EarlyReceiptCache.java b/app/src/main/java/org/thoughtcrime/securesms/database/EarlyReceiptCache.java index 2c0656070..15633dee2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/EarlyReceiptCache.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/EarlyReceiptCache.java @@ -13,33 +13,53 @@ public class EarlyReceiptCache { private static final String TAG = Log.tag(EarlyReceiptCache.class); - private final LRUCache> cache = new LRUCache<>(100); + private final LRUCache> cache = new LRUCache<>(100); private final String name; public EarlyReceiptCache(@NonNull String name) { this.name = name; } - public synchronized void increment(long timestamp, @NonNull RecipientId origin) { - Map receipts = cache.get(timestamp); + public synchronized void increment(long timestamp, @NonNull RecipientId origin, long receiptTimestamp) { + Map receipts = cache.get(timestamp); if (receipts == null) { receipts = new HashMap<>(); } - Long count = receipts.get(origin); + Receipt receipt = receipts.get(origin); - if (count != null) { - receipts.put(origin, ++count); + if (receipt != null) { + receipt.count++; + receipt.timestamp = receiptTimestamp; } else { - receipts.put(origin, 1L); + receipt = new Receipt(1, receiptTimestamp); } + receipts.put(origin, receipt); cache.put(timestamp, receipts); } - public synchronized Map remove(long timestamp) { - Map receipts = cache.remove(timestamp); + public synchronized Map remove(long timestamp) { + Map receipts = cache.remove(timestamp); return receipts != null ? receipts : new HashMap<>(); } + + public class Receipt { + private long count; + private long timestamp; + + private Receipt(long count, long timestamp) { + this.count = count; + this.timestamp = timestamp; + } + + public long getCount() { + return count; + } + + public long getTimestamp() { + return timestamp; + } + } } 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 b7f5e0ee4..bcbeed506 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -170,7 +170,8 @@ public class MmsDatabase extends MessageDatabase { MENTIONS_SELF + " INTEGER DEFAULT 0, " + NOTIFIED_TIMESTAMP + " INTEGER DEFAULT 0, " + VIEWED_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + - SERVER_GUID + " TEXT DEFAULT NULL);"; + SERVER_GUID + " TEXT DEFAULT NULL, "+ + RECEIPT_TIMESTAMP + " INTEGER DEFAULT -1);"; public static final String[] CREATE_INDEXS = { "CREATE INDEX IF NOT EXISTS mms_read_and_notified_and_thread_id_index ON " + TABLE_NAME + "(" + READ + "," + NOTIFIED + "," + THREAD_ID + ");", @@ -617,25 +618,29 @@ public class MmsDatabase extends MessageDatabase { SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); Set threadUpdates = new HashSet<>(); - try (Cursor cursor = database.query(TABLE_NAME, new String[] {ID, THREAD_ID, MESSAGE_BOX, RECIPIENT_ID, receiptType.getColumnName()}, + try (Cursor cursor = database.query(TABLE_NAME, new String[] {ID, THREAD_ID, MESSAGE_BOX, RECIPIENT_ID, receiptType.getColumnName(), RECEIPT_TIMESTAMP}, 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))); + if (Types.isOutgoingMessageType(CursorUtil.requireLong(cursor, MESSAGE_BOX))) { + RecipientId theirRecipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); RecipientId ourRecipientId = messageId.getRecipientId(); 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)); + long id = CursorUtil.requireLong(cursor, ID); + long threadId = CursorUtil.requireLong(cursor, THREAD_ID); int status = receiptType.getGroupStatus(); - boolean isFirstIncrement = cursor.getLong(cursor.getColumnIndexOrThrow(columnName)) == 0; + boolean isFirstIncrement = CursorUtil.requireLong(cursor, columnName) == 0; + long savedTimestamp = CursorUtil.requireLong(cursor, RECEIPT_TIMESTAMP); + long updatedTimestamp = isFirstIncrement ? Math.max(savedTimestamp, timestamp) : savedTimestamp; database.execSQL("UPDATE " + TABLE_NAME + " SET " + - columnName + " = " + columnName + " + 1 WHERE " + ID + " = ?", - new String[] {String.valueOf(id)}); + columnName + " = " + columnName + " + 1, " + + RECEIPT_TIMESTAMP + " = ? WHERE " + + ID + " = ?", + SqlUtil.buildArgs(updatedTimestamp, id)); DatabaseFactory.getGroupReceiptDatabase(context).update(ourRecipientId, id, status, timestamp); @@ -645,7 +650,7 @@ public class MmsDatabase extends MessageDatabase { } if (threadUpdates.size() > 0 && receiptType == ReceiptType.DELIVERY) { - earlyDeliveryReceiptCache.increment(messageId.getTimetamp(), messageId.getRecipientId()); + earlyDeliveryReceiptCache.increment(messageId.getTimetamp(), messageId.getRecipientId(), timestamp); } return threadUpdates; @@ -1484,7 +1489,7 @@ public class MmsDatabase extends MessageDatabase { type |= Types.EXPIRATION_TIMER_UPDATE_BIT; } - Map earlyDeliveryReceipts = earlyDeliveryReceiptCache.remove(message.getSentTimeMillis()); + Map earlyDeliveryReceipts = earlyDeliveryReceiptCache.remove(message.getSentTimeMillis()); ContentValues contentValues = new ContentValues(); contentValues.put(DATE_SENT, message.getSentTimeMillis()); @@ -1498,7 +1503,8 @@ public class MmsDatabase extends MessageDatabase { contentValues.put(EXPIRES_IN, message.getExpiresIn()); contentValues.put(VIEW_ONCE, message.isViewOnce()); contentValues.put(RECIPIENT_ID, message.getRecipient().getId().serialize()); - contentValues.put(DELIVERY_RECEIPT_COUNT, Stream.of(earlyDeliveryReceipts.values()).mapToLong(Long::longValue).sum()); + contentValues.put(DELIVERY_RECEIPT_COUNT, Stream.of(earlyDeliveryReceipts.values()).mapToLong(EarlyReceiptCache.Receipt::getCount).sum()); + contentValues.put(RECEIPT_TIMESTAMP, Stream.of(earlyDeliveryReceipts.values()).mapToLong(EarlyReceiptCache.Receipt::getTimestamp).max().orElse(-1)); if (message.getRecipient().isSelf() && hasAudioAttachment(message.getAttachments())) { contentValues.put(VIEWED_RECEIPT_COUNT, 1L); 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 7f95a8ad7..079796fcc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -29,6 +29,7 @@ public interface MmsSmsColumns { public static final String REACTIONS_LAST_SEEN = "reactions_last_seen"; public static final String REMOTE_DELETED = "remote_deleted"; public static final String SERVER_GUID = "server_guid"; + public static final String RECEIPT_TIMESTAMP = "receipt_timestamp"; /** * For storage efficiency, all types are stored within a single 64-bit integer column in the 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 bfdd3a5b8..df0a9e71a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -125,8 +125,9 @@ public class SmsDatabase extends MessageDatabase { REACTIONS_UNREAD + " INTEGER DEFAULT 0, " + REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1, " + REMOTE_DELETED + " INTEGER DEFAULT 0, " + - NOTIFIED_TIMESTAMP + " INTEGER DEFAULT 0," + - SERVER_GUID + " TEXT DEFAULT NULL);"; + NOTIFIED_TIMESTAMP + " INTEGER DEFAULT 0, " + + SERVER_GUID + " TEXT DEFAULT NULL, " + + RECEIPT_TIMESTAMP + " INTEGER DEFAULT -1);"; public static final String[] CREATE_INDEXS = { "CREATE INDEX IF NOT EXISTS sms_read_and_notified_and_thread_id_index ON " + TABLE_NAME + "(" + READ + "," + NOTIFIED + "," + THREAD_ID + ");", @@ -488,24 +489,28 @@ public class SmsDatabase extends MessageDatabase { SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); Set threadUpdates = new HashSet<>(); - try (Cursor cursor = database.query(TABLE_NAME, new String[] {ID, THREAD_ID, RECIPIENT_ID, TYPE, DELIVERY_RECEIPT_COUNT, READ_RECEIPT_COUNT}, + try (Cursor cursor = database.query(TABLE_NAME, new String[] {ID, THREAD_ID, RECIPIENT_ID, TYPE, DELIVERY_RECEIPT_COUNT, READ_RECEIPT_COUNT, RECEIPT_TIMESTAMP}, DATE_SENT + " = ?", new String[] {String.valueOf(messageId.getTimetamp())}, null, null, null, null)) { while (cursor.moveToNext()) { - if (Types.isOutgoingMessageType(cursor.getLong(cursor.getColumnIndexOrThrow(TYPE)))) { + if (Types.isOutgoingMessageType(CursorUtil.requireLong(cursor, TYPE))) { RecipientId theirRecipientId = messageId.getRecipientId(); - RecipientId outRecipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))); + RecipientId outRecipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); if (outRecipientId.equals(theirRecipientId)) { - long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(THREAD_ID)); + long id = CursorUtil.requireLong(cursor, ID); + long threadId = CursorUtil.requireLong(cursor, THREAD_ID); String columnName = receiptType.getColumnName(); - boolean isFirstIncrement = cursor.getLong(cursor.getColumnIndexOrThrow(columnName)) == 0; + boolean isFirstIncrement = CursorUtil.requireLong(cursor, columnName) == 0; + long savedTimestamp = CursorUtil.requireLong(cursor, RECEIPT_TIMESTAMP); + long updatedTimestamp = isFirstIncrement ? Math.max(savedTimestamp, timestamp) : savedTimestamp; database.execSQL("UPDATE " + TABLE_NAME + - " SET " + columnName + " = " + columnName + " + 1 WHERE " + + " SET " + columnName + " = " + columnName + " + 1, " + + RECEIPT_TIMESTAMP + " = ? WHERE " + ID + " = ?", - new String[] {String.valueOf(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))}); + SqlUtil.buildArgs(updatedTimestamp, id)); threadUpdates.add(new ThreadUpdate(threadId, !isFirstIncrement)); } @@ -513,7 +518,7 @@ public class SmsDatabase extends MessageDatabase { } if (threadUpdates.size() > 0 && receiptType == ReceiptType.DELIVERY) { - earlyDeliveryReceiptCache.increment(messageId.getTimetamp(), messageId.getRecipientId()); + earlyDeliveryReceiptCache.increment(messageId.getTimetamp(), messageId.getRecipientId(), timestamp); } return threadUpdates; @@ -1201,8 +1206,8 @@ public class SmsDatabase extends MessageDatabase { if (message.isIdentityVerified()) type |= Types.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT; else if (message.isIdentityDefault()) type |= Types.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT; - RecipientId recipientId = message.getRecipient().getId(); - Map earlyDeliveryReceipts = earlyDeliveryReceiptCache.remove(date); + RecipientId recipientId = message.getRecipient().getId(); + Map earlyDeliveryReceipts = earlyDeliveryReceiptCache.remove(date); ContentValues contentValues = new ContentValues(6); contentValues.put(RECIPIENT_ID, recipientId.serialize()); @@ -1214,7 +1219,8 @@ public class SmsDatabase extends MessageDatabase { contentValues.put(TYPE, type); contentValues.put(SUBSCRIPTION_ID, message.getSubscriptionId()); contentValues.put(EXPIRES_IN, message.getExpiresIn()); - contentValues.put(DELIVERY_RECEIPT_COUNT, Stream.of(earlyDeliveryReceipts.values()).mapToLong(Long::longValue).sum()); + contentValues.put(DELIVERY_RECEIPT_COUNT, Stream.of(earlyDeliveryReceipts.values()).mapToLong(EarlyReceiptCache.Receipt::getCount).sum()); + contentValues.put(RECEIPT_TIMESTAMP, Stream.of(earlyDeliveryReceipts.values()).mapToLong(EarlyReceiptCache.Receipt::getTimestamp).max().orElse(-1)); long messageId = db.insert(TABLE_NAME, null, contentValues); 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 0ee6b7ca9..7025bb9cc 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 @@ -216,8 +216,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab private static final int IDENTITY_MIGRATION = 114; private static final int GROUP_CALL_RING_TABLE = 115; private static final int CLEANUP_SESSION_MIGRATION = 116; + private static final int RECEIPT_TIMESTAMP = 117; - private static final int DATABASE_VERSION = 116; + private static final int DATABASE_VERSION = 117; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -2040,6 +2041,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab Log.i(TAG, "Cleaned up " + storageCount + " storageIds."); } + if (oldVersion < RECEIPT_TIMESTAMP) { + db.execSQL("ALTER TABLE sms ADD COLUMN receipt_timestamp INTEGER DEFAULT -1"); + db.execSQL("ALTER TABLE mms ADD COLUMN receipt_timestamp INTEGER DEFAULT -1"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction();