diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index f9a8d3e38..1dbf48096 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -668,6 +668,20 @@ public class RecipientDatabase extends Database { return null; } + public void markNeedsSync(@NonNull Collection recipientIds) { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + + db.beginTransaction(); + try { + for (RecipientId recipientId : recipientIds) { + markDirty(recipientId, DirtyState.UPDATE); + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + public void markNeedsSync(@NonNull RecipientId recipientId) { markDirty(recipientId, DirtyState.UPDATE); } @@ -768,7 +782,7 @@ public class RecipientDatabase extends Database { } } - threadDatabase.setArchived(recipientId, insert.isArchived()); + threadDatabase.applyStorageSyncUpdate(recipientId, insert); needsRefresh.add(recipientId); } @@ -821,7 +835,7 @@ public class RecipientDatabase extends Database { Log.w(TAG, "Failed to process identity key during update! Skipping.", e); } - threadDatabase.setArchived(recipientId, update.getNew().isArchived()); + threadDatabase.applyStorageSyncUpdate(recipientId, update.getNew()); needsRefresh.add(recipientId); } @@ -830,7 +844,7 @@ public class RecipientDatabase extends Database { Recipient recipient = Recipient.externalGroup(context, GroupId.v1orThrow(insert.getGroupId())); - threadDatabase.setArchived(recipient.getId(), insert.isArchived()); + threadDatabase.applyStorageSyncUpdate(recipient.getId(), insert); needsRefresh.add(recipient.getId()); } @@ -844,7 +858,7 @@ public class RecipientDatabase extends Database { Recipient recipient = Recipient.externalGroup(context, GroupId.v1orThrow(update.getOld().getGroupId())); - threadDatabase.setArchived(recipient.getId(), update.getNew().isArchived()); + threadDatabase.applyStorageSyncUpdate(recipient.getId(), update.getNew()); needsRefresh.add(recipient.getId()); } @@ -872,7 +886,7 @@ public class RecipientDatabase extends Database { ApplicationDependencies.getJobManager().add(new RequestGroupV2InfoJob(groupId)); - threadDatabase.setArchived(recipient.getId(), insert.isArchived()); + threadDatabase.applyStorageSyncUpdate(recipient.getId(), insert); needsRefresh.add(recipient.getId()); } @@ -887,7 +901,7 @@ public class RecipientDatabase extends Database { GroupMasterKey masterKey = update.getOld().getMasterKeyOrThrow(); Recipient recipient = Recipient.externalGroup(context, GroupId.v2(masterKey)); - threadDatabase.setArchived(recipient.getId(), update.getNew().isArchived()); + threadDatabase.applyStorageSyncUpdate(recipient.getId(), update.getNew()); needsRefresh.add(recipient.getId()); } @@ -936,6 +950,8 @@ public class RecipientDatabase extends Database { ApplicationDependencies.getJobManager().add(new RefreshAttributesJob()); } + DatabaseFactory.getThreadDatabase(context).applyStorageSyncUpdate(Recipient.self().getId(), update); + Recipient.self().live().refresh(); } @@ -1236,12 +1252,13 @@ public class RecipientDatabase extends Database { String storageProtoRaw = CursorUtil.getString(cursor, STORAGE_PROTO).orNull(); byte[] storageProto = storageProtoRaw != null ? Base64.decodeOrThrow(storageProtoRaw) : null; boolean archived = CursorUtil.getBoolean(cursor, ThreadDatabase.ARCHIVED).or(false); + boolean forcedUnread = CursorUtil.getInt(cursor, ThreadDatabase.READ).transform(status -> status == ThreadDatabase.ReadStatus.FORCED_UNREAD.serialize()).or(false); GroupMasterKey groupMasterKey = CursorUtil.getBlob(cursor, GroupDatabase.V2_MASTER_KEY).transform(GroupUtil::requireMasterKey).orNull(); byte[] identityKey = CursorUtil.getString(cursor, IDENTITY_KEY).transform(Base64::decodeOrThrow).orNull(); VerifiedStatus identityStatus = CursorUtil.getInt(cursor, IDENTITY_STATUS).transform(VerifiedStatus::forState).or(VerifiedStatus.DEFAULT); - return new RecipientSettings.SyncExtras(storageProto, groupMasterKey, identityKey, identityStatus, archived); + return new RecipientSettings.SyncExtras(storageProto, groupMasterKey, identityKey, identityStatus, archived, forcedUnread); } public BulkOperationsHandle beginBulkSystemContactUpdate() { @@ -1422,7 +1439,7 @@ public class RecipientDatabase extends Database { valuesToSet.putNull(PROFILE_KEY_CREDENTIAL); valuesToSet.put(UNIDENTIFIED_ACCESS_MODE, UnidentifiedAccessMode.UNKNOWN.getMode()); - SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, valuesToCompare); + SqlUtil.Query updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, valuesToCompare); if (update(updateQuery, valuesToSet)) { markDirty(id, DirtyState.UPDATE); @@ -1471,7 +1488,7 @@ public class RecipientDatabase extends Database { values.put(PROFILE_KEY_CREDENTIAL, Base64.encodeBytes(profileKeyCredential.serialize())); - SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values); + SqlUtil.Query updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values); if (update(updateQuery, values)) { // TODO [greyson] If we sync this in future, mark dirty @@ -2250,9 +2267,7 @@ public class RecipientDatabase extends Database { * query such that this will only return true if a row was *actually* updated. */ private boolean update(@NonNull RecipientId id, @NonNull ContentValues contentValues) { - String selection = ID + " = ?"; - String[] args = new String[]{id.serialize()}; - SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, contentValues); + SqlUtil.Query updateQuery = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(id), contentValues); return update(updateQuery, contentValues); } @@ -2262,7 +2277,7 @@ public class RecipientDatabase extends Database { *

* This will only return true if a row was *actually* updated with respect to the where clause of the {@param updateQuery}. */ - private boolean update(@NonNull SqlUtil.UpdateQuery updateQuery, @NonNull ContentValues contentValues) { + private boolean update(@NonNull SqlUtil.Query updateQuery, @NonNull ContentValues contentValues) { SQLiteDatabase database = databaseHelper.getWritableDatabase(); return database.update(TABLE_NAME, contentValues, updateQuery.getWhere(), updateQuery.getWhereArgs()) > 0; @@ -2816,18 +2831,21 @@ public class RecipientDatabase extends Database { private final byte[] identityKey; private final VerifiedStatus identityStatus; private final boolean archived; + private final boolean forcedUnread; public SyncExtras(@Nullable byte[] storageProto, @Nullable GroupMasterKey groupMasterKey, @Nullable byte[] identityKey, @NonNull VerifiedStatus identityStatus, - boolean archived) + boolean archived, + boolean forcedUnread) { this.storageProto = storageProto; this.groupMasterKey = groupMasterKey; this.identityKey = identityKey; this.identityStatus = identityStatus; this.archived = archived; + this.forcedUnread = forcedUnread; } public @Nullable byte[] getStorageProto() { @@ -2849,6 +2867,10 @@ public class RecipientDatabase extends Database { public @NonNull VerifiedStatus getIdentityStatus() { return identityStatus; } + + public boolean isForcedUnread() { + return forcedUnread; + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 8bbe567f8..911c97de0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -32,8 +32,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import net.sqlcipher.database.SQLiteDatabase; import org.jsoup.helper.StringUtil; -import org.signal.storageservice.protos.groups.local.DecryptedGroup; -import org.signal.storageservice.protos.groups.local.DecryptedMember; import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo; import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; @@ -56,11 +54,15 @@ import org.thoughtcrime.securesms.util.SqlUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; -import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.api.storage.SignalAccountRecord; +import org.whispersystems.signalservice.api.storage.SignalContactRecord; +import org.whispersystems.signalservice.api.storage.SignalGroupV1Record; +import org.whispersystems.signalservice.api.storage.SignalGroupV2Record; +import org.whispersystems.signalservice.api.storage.SignalStorageRecord; import java.io.Closeable; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -70,7 +72,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.UUID; public class ThreadDatabase extends Database { @@ -102,7 +103,7 @@ public class ThreadDatabase extends Database { public static final String LAST_SEEN = "last_seen"; public static final String HAS_SENT = "has_sent"; private static final String LAST_SCROLLED = "last_scrolled"; - private static final String PINNED = "pinned"; + static final String PINNED = "pinned"; public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + DATE + " INTEGER DEFAULT 0, " + @@ -405,6 +406,7 @@ public class ThreadDatabase extends Database { List smsRecords = new LinkedList<>(); List mmsRecords = new LinkedList<>(); + boolean needsSync = false; db.beginTransaction(); @@ -417,6 +419,8 @@ public class ThreadDatabase extends Database { } for (long threadId : threadIds) { + ThreadRecord previous = getThreadRecord(threadId); + smsRecords.addAll(DatabaseFactory.getSmsDatabase(context).setMessagesReadSince(threadId, sinceTimestamp)); mmsRecords.addAll(DatabaseFactory.getMmsDatabase(context).setMessagesReadSince(threadId, sinceTimestamp)); @@ -427,7 +431,12 @@ public class ThreadDatabase extends Database { contentValues.put(UNREAD_COUNT, unreadCount); - db.update(TABLE_NAME, contentValues, ID_WHERE, new String[]{threadId + ""}); + db.update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(threadId)); + + if (previous != null && previous.isForcedUnread()) { + DatabaseFactory.getRecipientDatabase(context).markNeedsSync(previous.getRecipient().getId()); + needsSync = true; + } } db.setTransactionSuccessful(); @@ -437,6 +446,11 @@ public class ThreadDatabase extends Database { notifyConversationListeners(new HashSet<>(threadIds)); notifyConversationListListeners(); + + if (needsSync) { + StorageSyncHelper.scheduleSyncForDataChange(); + } + return Util.concatenatedList(smsRecords, mmsRecords); } @@ -445,19 +459,22 @@ public class ThreadDatabase extends Database { db.beginTransaction(); try { - ContentValues contentValues = new ContentValues(); + List recipientIds = getRecipientIdsForThreadIds(threadIds); + SqlUtil.Query query = SqlUtil.buildCollectionQuery(ID, threadIds); + ContentValues contentValues = new ContentValues(); + contentValues.put(READ, ReadStatus.FORCED_UNREAD.serialize()); - for (long threadId : threadIds) { - db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] { String.valueOf(threadId) }); - } + db.update(TABLE_NAME, contentValues, query.getWhere(), query.getWhereArgs()); + DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipientIds); db.setTransactionSuccessful(); } finally { db.endTransaction(); - } - notifyConversationListListeners(); + StorageSyncHelper.scheduleSyncForDataChange(); + notifyConversationListListeners(); + } } @@ -949,6 +966,20 @@ public class ThreadDatabase extends Database { return Recipient.resolved(id); } + public @NonNull List getRecipientIdsForThreadIds(Collection threadIds) { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + SqlUtil.Query query = SqlUtil.buildCollectionQuery(ID, threadIds); + List ids = new ArrayList<>(threadIds.size()); + + try (Cursor cursor = db.query(TABLE_NAME, new String[] { RECIPIENT_ID }, query.getWhere(), query.getWhereArgs(), null, null, null)) { + while (cursor != null && cursor.moveToNext()) { + ids.add(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID))); + } + } + + return ids; + } + public boolean hasThread(@NonNull RecipientId recipientId) { return getThreadIdIfExistsFor(recipientId) > -1; } @@ -964,16 +995,56 @@ public class ThreadDatabase extends Database { } void updateReadState(long threadId) { - int unreadCount = DatabaseFactory.getMmsSmsDatabase(context).getUnreadCount(threadId); + ThreadRecord previous = getThreadRecord(threadId); + int unreadCount = DatabaseFactory.getMmsSmsDatabase(context).getUnreadCount(threadId); ContentValues contentValues = new ContentValues(); - contentValues.put(READ, unreadCount == 0); + contentValues.put(READ, unreadCount == 0 ? ReadStatus.READ.serialize() : ReadStatus.UNREAD.serialize()); contentValues.put(UNREAD_COUNT, unreadCount); - databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues,ID_WHERE, - new String[] {String.valueOf(threadId)}); + databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(threadId)); notifyConversationListListeners(); + + if (previous != null && previous.isForcedUnread()) { + DatabaseFactory.getRecipientDatabase(context).markNeedsSync(previous.getRecipient().getId()); + StorageSyncHelper.scheduleSyncForDataChange(); + } + } + + public void applyStorageSyncUpdate(@NonNull RecipientId recipientId, @NonNull SignalContactRecord record) { + applyStorageSyncUpdate(recipientId, record.isArchived(), record.isForcedUnread()); + } + + public void applyStorageSyncUpdate(@NonNull RecipientId recipientId, @NonNull SignalGroupV1Record record) { + applyStorageSyncUpdate(recipientId, record.isArchived(), record.isForcedUnread()); + } + + public void applyStorageSyncUpdate(@NonNull RecipientId recipientId, @NonNull SignalGroupV2Record record) { + applyStorageSyncUpdate(recipientId, record.isArchived(), record.isForcedUnread()); + } + + public void applyStorageSyncUpdate(@NonNull RecipientId recipientId, @NonNull SignalAccountRecord record) { + applyStorageSyncUpdate(recipientId, record.isNoteToSelfArchived(), record.isNoteToSelfForcedUnread()); + } + + private void applyStorageSyncUpdate(@NonNull RecipientId recipientId, boolean archived, boolean forcedUnread) { + ContentValues values = new ContentValues(); + values.put(ARCHIVED, archived); + + if (forcedUnread) { + values.put(READ, ReadStatus.FORCED_UNREAD.serialize()); + } else { + Long threadId = getThreadIdFor(recipientId); + if (threadId != null) { + int unreadCount = DatabaseFactory.getMmsSmsDatabase(context).getUnreadCount(threadId); + + values.put(READ, unreadCount == 0 ? ReadStatus.READ.serialize() : ReadStatus.UNREAD.serialize()); + values.put(UNREAD_COUNT, unreadCount); + } + } + + databaseHelper.getWritableDatabase().update(TABLE_NAME, values, RECIPIENT_ID + " = ?", SqlUtil.buildArgs(recipientId)); } public boolean update(long threadId, boolean unarchive) { @@ -1404,7 +1475,7 @@ public class ThreadDatabase extends Database { } } - private enum ReadStatus { + enum ReadStatus { READ(1), UNREAD(0), FORCED_UNREAD(2); private final int value; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java index 0829ed13b..a4abe45fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java @@ -183,6 +183,8 @@ public class StorageSyncJob extends BaseJob { } remoteManifestVersion = writeOperationResult.getManifest().getVersion(); + + needsMultiDeviceSync = true; } else { Log.i(TAG, "[Remote Newer] After resolving the conflict, all changes are local. No remote writes needed."); } @@ -190,7 +192,6 @@ public class StorageSyncJob extends BaseJob { recipientDatabase.applyStorageSyncUpdates(mergeResult.getLocalContactInserts(), mergeResult.getLocalContactUpdates(), mergeResult.getLocalGroupV1Inserts(), mergeResult.getLocalGroupV1Updates(), mergeResult.getLocalGroupV2Inserts(), mergeResult.getLocalGroupV2Updates()); storageKeyDatabase.applyStorageSyncUpdates(mergeResult.getLocalUnknownInserts(), mergeResult.getLocalUnknownDeletes()); StorageSyncHelper.applyAccountStorageSyncUpdates(context, mergeResult.getLocalAccountUpdate()); - needsMultiDeviceSync = true; Log.i(TAG, "[Remote Newer] Updating local manifest version to: " + remoteManifestVersion); TextSecurePreferences.setStorageManifestVersion(context, remoteManifestVersion); diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java index 277a5210c..8a5ed2494 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java @@ -60,14 +60,15 @@ class AccountConflictMerger implements StorageSyncHelper.ConflictMerger 0, then you know something *actually* changed. */ - public static @NonNull UpdateQuery buildTrueUpdateQuery(@NonNull String selection, - @NonNull String[] args, - @NonNull ContentValues contentValues) + public static @NonNull Query buildTrueUpdateQuery(@NonNull String selection, + @NonNull String[] args, + @NonNull ContentValues contentValues) { StringBuilder qualifier = new StringBuilder(); Set> valueSet = contentValues.valueSet(); @@ -90,7 +92,29 @@ public final class SqlUtil { i++; } - return new UpdateQuery("(" + selection + ") AND (" + qualifier + ")", fullArgs.toArray(new String[0])); + return new Query("(" + selection + ") AND (" + qualifier + ")", fullArgs.toArray(new String[0])); + } + + public static @NonNull Query buildCollectionQuery(@NonNull String column, @NonNull Collection values) { + Preconditions.checkArgument(values.size() > 0); + + StringBuilder query = new StringBuilder(); + Object[] args = new Object[values.size()]; + + int i = 0; + + for (Object value : values) { + query.append("?"); + args[i] = value; + + if (i != values.size() - 1) { + query.append(", "); + } + + i++; + } + + return new Query(column + " IN (" + query.toString() + ")", buildArgs(args)); } public static String[] appendArg(@NonNull String[] args, String addition) { @@ -102,11 +126,11 @@ public final class SqlUtil { return output; } - public static class UpdateQuery { + public static class Query { private final String where; private final String[] whereArgs; - private UpdateQuery(@NonNull String where, @NonNull String[] whereArgs) { + private Query(@NonNull String where, @NonNull String[] whereArgs) { this.where = where; this.whereArgs = whereArgs; } diff --git a/app/src/test/java/org/thoughtcrime/securesms/storage/ContactConflictMergerTest.java b/app/src/test/java/org/thoughtcrime/securesms/storage/ContactConflictMergerTest.java index 329a58290..dbc2a137d 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/storage/ContactConflictMergerTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/storage/ContactConflictMergerTest.java @@ -55,6 +55,7 @@ public class ContactConflictMergerTest { .setUsername("username A") .setProfileSharingEnabled(false) .setArchived(false) + .setForcedUnread(false) .build(); SignalContactRecord local = new SignalContactRecord.Builder(byteArray(2), new SignalServiceAddress(UUID_B, E164_B)) .setBlocked(false) @@ -66,6 +67,7 @@ public class ContactConflictMergerTest { .setUsername("username B") .setProfileSharingEnabled(true) .setArchived(true) + .setForcedUnread(true) .build(); SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local), SELF).merge(remote, local, mock(KeyGenerator.class)); @@ -81,6 +83,7 @@ public class ContactConflictMergerTest { assertEquals("username A", merged.getUsername().get()); assertFalse(merged.isProfileSharingEnabled()); assertFalse(merged.isArchived()); + assertFalse(merged.isForcedUnread()); } @Test diff --git a/app/src/test/java/org/thoughtcrime/securesms/storage/GroupV1ConflictMergerTest.java b/app/src/test/java/org/thoughtcrime/securesms/storage/GroupV1ConflictMergerTest.java index dbe91c21e..8f45cc968 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/storage/GroupV1ConflictMergerTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/storage/GroupV1ConflictMergerTest.java @@ -30,11 +30,13 @@ public final class GroupV1ConflictMergerTest { .setBlocked(false) .setProfileSharingEnabled(false) .setArchived(false) + .setForcedUnread(false) .build(); SignalGroupV1Record local = new SignalGroupV1Record.Builder(byteArray(2), byteArray(100)) .setBlocked(true) .setProfileSharingEnabled(true) .setArchived(true) + .setForcedUnread(true) .build(); SignalGroupV1Record merged = new GroupV1ConflictMerger(Collections.singletonList(local)).merge(remote, local, KEY_GENERATOR); @@ -44,6 +46,7 @@ public final class GroupV1ConflictMergerTest { assertFalse(merged.isProfileSharingEnabled()); assertFalse(merged.isBlocked()); assertFalse(merged.isArchived()); + assertFalse(merged.isForcedUnread()); } @Test diff --git a/app/src/test/java/org/thoughtcrime/securesms/storage/GroupV2ConflictMergerTest.java b/app/src/test/java/org/thoughtcrime/securesms/storage/GroupV2ConflictMergerTest.java index 9a890ae86..d3bfd664d 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/storage/GroupV2ConflictMergerTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/storage/GroupV2ConflictMergerTest.java @@ -30,11 +30,13 @@ public final class GroupV2ConflictMergerTest { .setBlocked(false) .setProfileSharingEnabled(false) .setArchived(false) + .setForcedUnread(false) .build(); SignalGroupV2Record local = new SignalGroupV2Record.Builder(byteArray(2), groupKey(100)) .setBlocked(true) .setProfileSharingEnabled(true) .setArchived(true) + .setForcedUnread(true) .build(); SignalGroupV2Record merged = new GroupV2ConflictMerger(Collections.singletonList(local)).merge(remote, local, KEY_GENERATOR); @@ -44,6 +46,7 @@ public final class GroupV2ConflictMergerTest { assertFalse(merged.isProfileSharingEnabled()); assertFalse(merged.isBlocked()); assertFalse(merged.isArchived()); + assertFalse(merged.isForcedUnread()); } @Test diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/SqlUtilTest.java b/app/src/test/java/org/thoughtcrime/securesms/util/SqlUtilTest.java index c4b266e20..ee7b17f4c 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/util/SqlUtilTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/util/SqlUtilTest.java @@ -8,6 +8,10 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import java.util.Arrays; + +import edu.emory.mathcs.backport.java.util.Collections; + import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -23,7 +27,7 @@ public final class SqlUtilTest { ContentValues values = new ContentValues(); values.put("a", 2); - SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values); + SqlUtil.Query updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values); assertEquals("(_id = ?) AND (a != ? OR a IS NULL)", updateQuery.getWhere()); assertArrayEquals(new String[] { "1", "2" }, updateQuery.getWhereArgs()); @@ -37,7 +41,7 @@ public final class SqlUtilTest { ContentValues values = new ContentValues(); values.put("a", 4); - SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values); + SqlUtil.Query updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values); assertEquals("(_id = ? AND (foo = ? OR bar != ?)) AND (a != ? OR a IS NULL)", updateQuery.getWhere()); assertArrayEquals(new String[] { "1", "2", "3", "4" }, updateQuery.getWhereArgs()); @@ -53,7 +57,7 @@ public final class SqlUtilTest { values.put("b", 3); values.put("c", 4); - SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values); + SqlUtil.Query updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values); assertEquals("(_id = ?) AND (a != ? OR a IS NULL OR b != ? OR b IS NULL OR c != ? OR c IS NULL)", updateQuery.getWhere()); assertArrayEquals(new String[] { "1", "2", "3", "4"}, updateQuery.getWhereArgs()); @@ -67,7 +71,7 @@ public final class SqlUtilTest { ContentValues values = new ContentValues(); values.put("a", (String) null); - SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values); + SqlUtil.Query updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values); assertEquals("(_id = ?) AND (a NOT NULL)", updateQuery.getWhere()); assertArrayEquals(new String[] { "1" }, updateQuery.getWhereArgs()); @@ -85,9 +89,30 @@ public final class SqlUtilTest { values.put("d", (String) null); values.put("e", (String) null); - SqlUtil.UpdateQuery updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values); + SqlUtil.Query updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values); assertEquals("(_id = ?) AND (a NOT NULL OR b != ? OR b IS NULL OR c != ? OR c IS NULL OR d NOT NULL OR e NOT NULL)", updateQuery.getWhere()); assertArrayEquals(new String[] { "1", "2", "3" }, updateQuery.getWhereArgs()); } + + @Test + public void buildCollectionQuery_single() { + SqlUtil.Query updateQuery = SqlUtil.buildCollectionQuery("a", Arrays.asList(1)); + + assertEquals("a IN (?)", updateQuery.getWhere()); + assertArrayEquals(new String[] { "1" }, updateQuery.getWhereArgs()); + } + + @Test + public void buildCollectionQuery_multiple() { + SqlUtil.Query updateQuery = SqlUtil.buildCollectionQuery("a", Arrays.asList(1, 2, 3)); + + assertEquals("a IN (?, ?, ?)", updateQuery.getWhere()); + assertArrayEquals(new String[] { "1", "2", "3" }, updateQuery.getWhereArgs()); + } + + @Test(expected = IllegalArgumentException.class) + public void buildCollectionQuery_none() { + SqlUtil.buildCollectionQuery("a", Collections.emptyList()); + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java index 488e05dfe..b259e726c 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java @@ -64,6 +64,10 @@ public final class SignalAccountRecord implements SignalRecord { return proto.getNoteToSelfArchived(); } + public boolean isNoteToSelfForcedUnread() { + return proto.getNoteToSelfMarkedUnread(); + } + public boolean isReadReceiptsEnabled() { return proto.getReadReceipts(); } @@ -147,6 +151,11 @@ public final class SignalAccountRecord implements SignalRecord { return this; } + public Builder setNoteToSelfForcedUnread(boolean forcedUnread) { + builder.setNoteToSelfMarkedUnread(forcedUnread); + return this; + } + public Builder setReadReceiptsEnabled(boolean enabled) { builder.setReadReceipts(enabled); return this; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalContactRecord.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalContactRecord.java index 7d12a4e08..74948dc8d 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalContactRecord.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalContactRecord.java @@ -91,6 +91,10 @@ public final class SignalContactRecord implements SignalRecord { return proto.getArchived(); } + public boolean isForcedUnread() { + return proto.getMarkedUnread(); + } + ContactRecord toProto() { return proto; } @@ -173,6 +177,11 @@ public final class SignalContactRecord implements SignalRecord { return this; } + public Builder setForcedUnread(boolean forcedUnread) { + builder.setMarkedUnread(forcedUnread); + return this; + } + public SignalContactRecord build() { ContactRecord proto = builder.build(); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV1Record.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV1Record.java index c0db0b45c..6b3b6ef4d 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV1Record.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV1Record.java @@ -50,6 +50,10 @@ public final class SignalGroupV1Record implements SignalRecord { return proto.getArchived(); } + public boolean isForcedUnread() { + return proto.getMarkedUnread(); + } + GroupV1Record toProto() { return proto; } @@ -101,6 +105,11 @@ public final class SignalGroupV1Record implements SignalRecord { return this; } + public Builder setForcedUnread(boolean forcedUnread) { + builder.setMarkedUnread(forcedUnread); + return this; + } + public SignalGroupV1Record build() { GroupV1Record proto = builder.build(); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV2Record.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV2Record.java index da81ba2e7..fc0dcf4c0 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV2Record.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV2Record.java @@ -60,6 +60,10 @@ public final class SignalGroupV2Record implements SignalRecord { return proto.getArchived(); } + public boolean isForcedUnread() { + return proto.getMarkedUnread(); + } + GroupV2Record toProto() { return proto; } @@ -115,6 +119,11 @@ public final class SignalGroupV2Record implements SignalRecord { return this; } + public Builder setForcedUnread(boolean forcedUnread) { + builder.setMarkedUnread(forcedUnread); + return this; + } + public SignalGroupV2Record build() { GroupV2Record proto = builder.build(); diff --git a/libsignal/service/src/main/proto/SignalStorage.proto b/libsignal/service/src/main/proto/SignalStorage.proto index 33ab327e5..c679a3afd 100644 --- a/libsignal/service/src/main/proto/SignalStorage.proto +++ b/libsignal/service/src/main/proto/SignalStorage.proto @@ -80,20 +80,23 @@ message ContactRecord { bool blocked = 9; bool whitelisted = 10; bool archived = 11; + bool markedUnread = 12; } message GroupV1Record { - bytes id = 1; - bool blocked = 2; - bool whitelisted = 3; - bool archived = 4; + bytes id = 1; + bool blocked = 2; + bool whitelisted = 3; + bool archived = 4; + bool markedUnread = 5; } message GroupV2Record { - bytes masterKey = 1; - bool blocked = 2; - bool whitelisted = 3; - bool archived = 4; + bytes masterKey = 1; + bool blocked = 2; + bool whitelisted = 3; + bool archived = 4; + bool markedUnread = 5; } message AccountRecord { @@ -113,7 +116,7 @@ message AccountRecord { bool sealedSenderIndicators = 7; bool typingIndicators = 8; bool proxiedLinkPreviews = 9; - // 10 is reserved for unread + bool noteToSelfMarkedUnread = 10; bool linkPreviews = 11; PhoneNumberSharingMode phoneNumberSharingMode = 12; bool unlistedPhoneNumber = 13;