Rewrite storage service change processing.

This commit is contained in:
Greyson Parrelli
2021-03-16 10:01:46 -04:00
parent 552b19cbb0
commit 0e200b1fb6
42 changed files with 1913 additions and 208 deletions

View File

@@ -61,7 +61,6 @@ public abstract class Database {
protected void notifyConversationListListeners() {
ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners();
context.getContentResolver().notifyChange(DatabaseContentProviders.ConversationList.CONTENT_URI, null);
}
protected void notifyStickerListeners() {
@@ -87,11 +86,6 @@ public abstract class Database {
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.Conversation.getVerboseUriForThread(threadId));
}
@Deprecated
protected void setNotifyConversationListListeners(Cursor cursor) {
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.ConversationList.CONTENT_URI);
}
@Deprecated
protected void setNotifyStickerListeners(Cursor cursor) {
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.Sticker.CONTENT_URI);

View File

@@ -16,13 +16,6 @@ import org.thoughtcrime.securesms.BuildConfig;
*/
public class DatabaseContentProviders {
public static class ConversationList extends NoopContentProvider {
private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".database.conversationlist";
private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY;
public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);
}
public static class Conversation extends NoopContentProvider {
private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".database.conversation";
private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY + "/";

View File

@@ -117,8 +117,6 @@ public final class DatabaseObserver {
listener.onChanged();
}
});
application.getContentResolver().notifyChange(DatabaseContentProviders.ConversationList.CONTENT_URI, null);
}
public void notifyPaymentListeners(@NonNull UUID paymentId) {

View File

@@ -44,7 +44,7 @@ import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.storage.StorageSyncHelper.RecordUpdate;
import org.thoughtcrime.securesms.storage.StorageRecordUpdate;
import org.thoughtcrime.securesms.storage.StorageSyncModels;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.Bitmask;
@@ -61,12 +61,15 @@ import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.libsignal.util.guava.Preconditions;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
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.SignalRecord;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.api.util.UuidUtil;
@@ -812,12 +815,170 @@ public class RecipientDatabase extends Database {
}
}
public boolean applyStorageSyncUpdates(@NonNull Collection<SignalContactRecord> contactInserts,
@NonNull Collection<RecordUpdate<SignalContactRecord>> contactUpdates,
@NonNull Collection<SignalGroupV1Record> groupV1Inserts,
@NonNull Collection<RecordUpdate<SignalGroupV1Record>> groupV1Updates,
@NonNull Collection<SignalGroupV2Record> groupV2Inserts,
@NonNull Collection<RecordUpdate<SignalGroupV2Record>> groupV2Updates)
public void applyStorageSyncContactInsert(@NonNull SignalContactRecord insert) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
ContentValues values = getValuesForStorageContact(insert, true);
long id = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
RecipientId recipientId = null;
if (id < 0) {
Log.w(TAG, "[applyStorageSyncContactInsert] Failed to insert. Possibly merging.");
recipientId = getAndPossiblyMerge(insert.getAddress().getUuid().get(), insert.getAddress().getNumber().get(), true);
db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(recipientId));
} else {
recipientId = RecipientId.from(id);
}
if (insert.getIdentityKey().isPresent()) {
try {
IdentityKey identityKey = new IdentityKey(insert.getIdentityKey().get(), 0);
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(insert.getIdentityState()));
} catch (InvalidKeyException e) {
Log.w(TAG, "Failed to process identity key during insert! Skipping.", e);
}
}
threadDatabase.applyStorageSyncUpdate(recipientId, insert);
}
public void applyStorageSyncContactUpdate(@NonNull StorageRecordUpdate<SignalContactRecord> update) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
ContentValues values = getValuesForStorageContact(update.getNew(), false);
try {
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(update.getOld().getId().getRaw())});
if (updateCount < 1) {
throw new AssertionError("Had an update, but it didn't match any rows!");
}
} catch (SQLiteConstraintException e) {
Log.w(TAG, "[applyStorageSyncContactUpdate] Failed to update a user by storageId.");
RecipientId recipientId = getByColumn(STORAGE_SERVICE_ID, Base64.encodeBytes(update.getOld().getId().getRaw())).get();
Log.w(TAG, "[applyStorageSyncContactUpdate] Found user " + recipientId + ". Possibly merging.");
recipientId = getAndPossiblyMerge(update.getNew().getAddress().getUuid().orNull(), update.getNew().getAddress().getNumber().orNull(), true);
Log.w(TAG, "[applyStorageSyncContactUpdate] Merged into " + recipientId);
db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(recipientId));
}
RecipientId recipientId = getByStorageKeyOrThrow(update.getNew().getId().getRaw());
if (StorageSyncHelper.profileKeyChanged(update)) {
ContentValues clearValues = new ContentValues(1);
clearValues.putNull(PROFILE_KEY_CREDENTIAL);
db.update(TABLE_NAME, clearValues, ID_WHERE, SqlUtil.buildArgs(recipientId));
}
try {
Optional<IdentityRecord> oldIdentityRecord = identityDatabase.getIdentity(recipientId);
if (update.getNew().getIdentityKey().isPresent()) {
IdentityKey identityKey = new IdentityKey(update.getNew().getIdentityKey().get(), 0);
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(update.getNew().getIdentityState()));
}
Optional<IdentityRecord> newIdentityRecord = identityDatabase.getIdentity(recipientId);
if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED) &&
(!oldIdentityRecord.isPresent() || oldIdentityRecord.get().getVerifiedStatus() != VerifiedStatus.VERIFIED))
{
IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), true, true);
} else if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() != VerifiedStatus.VERIFIED) &&
(oldIdentityRecord.isPresent() && oldIdentityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED))
{
IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), false, true);
}
} catch (InvalidKeyException e) {
Log.w(TAG, "Failed to process identity key during update! Skipping.", e);
}
DatabaseFactory.getThreadDatabase(context).applyStorageSyncUpdate(recipientId, update.getNew());
Recipient.live(recipientId).refresh();
}
public void applyStorageSyncGroupV1Insert(@NonNull SignalGroupV1Record insert) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long id = db.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV1(insert));
RecipientId recipientId = RecipientId.from(id);
DatabaseFactory.getThreadDatabase(context).applyStorageSyncUpdate(recipientId, insert);
Recipient.live(recipientId).refresh();
}
public void applyStorageSyncGroupV1Update(@NonNull StorageRecordUpdate<SignalGroupV1Record> update) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ContentValues values = getValuesForStorageGroupV1(update.getNew());
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(update.getOld().getId().getRaw())});
if (updateCount < 1) {
throw new AssertionError("Had an update, but it didn't match any rows!");
}
Recipient recipient = Recipient.externalGroupExact(context, GroupId.v1orThrow(update.getOld().getGroupId()));
DatabaseFactory.getThreadDatabase(context).applyStorageSyncUpdate(recipient.getId(), update.getNew());
recipient.live().refresh();
}
public void applyStorageSyncGroupV2Insert(@NonNull SignalGroupV2Record insert) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
GroupMasterKey masterKey = insert.getMasterKeyOrThrow();
GroupId.V2 groupId = GroupId.v2(masterKey);
ContentValues values = getValuesForStorageGroupV2(insert);
long id = db.insertOrThrow(TABLE_NAME, null, values);
Recipient recipient = Recipient.externalGroupExact(context, groupId);
Log.i(TAG, "Creating restore placeholder for " + groupId);
DatabaseFactory.getGroupDatabase(context)
.create(masterKey,
DecryptedGroup.newBuilder()
.setRevision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION)
.build());
Log.i(TAG, "Scheduling request for latest group info for " + groupId);
ApplicationDependencies.getJobManager().add(new RequestGroupV2InfoJob(groupId));
DatabaseFactory.getThreadDatabase(context).applyStorageSyncUpdate(recipient.getId(), insert);
recipient.live().refresh();
}
public void applyStorageSyncGroupV2Update(@NonNull StorageRecordUpdate<SignalGroupV2Record> update) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ContentValues values = getValuesForStorageGroupV2(update.getNew());
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(update.getOld().getId().getRaw())});
if (updateCount < 1) {
throw new AssertionError("Had an update, but it didn't match any rows!");
}
GroupMasterKey masterKey = update.getOld().getMasterKeyOrThrow();
Recipient recipient = Recipient.externalGroupExact(context, GroupId.v2(masterKey));
DatabaseFactory.getThreadDatabase(context).applyStorageSyncUpdate(recipient.getId(), update.getNew());
recipient.live().refresh();
}
public boolean applyStorageSyncUpdates(@NonNull Collection<SignalContactRecord> contactInserts,
@NonNull Collection<StorageRecordUpdate<SignalContactRecord>> contactUpdates,
@NonNull Collection<SignalGroupV1Record> groupV1Inserts,
@NonNull Collection<StorageRecordUpdate<SignalGroupV1Record>> groupV1Updates,
@NonNull Collection<SignalGroupV2Record> groupV2Inserts,
@NonNull Collection<StorageRecordUpdate<SignalGroupV2Record>> groupV2Updates)
{
SQLiteDatabase db = databaseHelper.getWritableDatabase();
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
@@ -889,7 +1050,7 @@ public class RecipientDatabase extends Database {
needsRefresh.add(recipientId);
}
for (RecordUpdate<SignalContactRecord> update : contactUpdates) {
for (StorageRecordUpdate<SignalContactRecord> update : contactUpdates) {
ContentValues values = getValuesForStorageContact(update.getNew(), false);
try {
@@ -932,7 +1093,7 @@ public class RecipientDatabase extends Database {
{
IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), true, true);
} else if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() != VerifiedStatus.VERIFIED) &&
(oldIdentityRecord.isPresent() && oldIdentityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED))
(oldIdentityRecord.isPresent() && oldIdentityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED))
{
IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), false, true);
}
@@ -958,7 +1119,7 @@ public class RecipientDatabase extends Database {
}
}
for (RecordUpdate<SignalGroupV1Record> update : groupV1Updates) {
for (StorageRecordUpdate<SignalGroupV1Record> update : groupV1Updates) {
ContentValues values = getValuesForStorageGroupV1(update.getNew());
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(update.getOld().getId().getRaw())});
@@ -971,7 +1132,7 @@ public class RecipientDatabase extends Database {
threadDatabase.applyStorageSyncUpdate(recipient.getId(), update.getNew());
needsRefresh.add(recipient.getId());
}
for (SignalGroupV2Record insert : groupV2Inserts) {
GroupMasterKey masterKey = insert.getMasterKeyOrThrow();
GroupId.V2 groupId = GroupId.v2(masterKey);
@@ -988,9 +1149,9 @@ public class RecipientDatabase extends Database {
Log.i(TAG, "Creating restore placeholder for " + groupId);
DatabaseFactory.getGroupDatabase(context)
.create(masterKey,
DecryptedGroup.newBuilder()
.setRevision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION)
.build());
DecryptedGroup.newBuilder()
.setRevision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION)
.build());
Log.i(TAG, "Scheduling request for latest group info for " + groupId);
@@ -1000,7 +1161,7 @@ public class RecipientDatabase extends Database {
needsRefresh.add(recipient.getId());
}
for (RecordUpdate<SignalGroupV2Record> update : groupV2Updates) {
for (StorageRecordUpdate<SignalGroupV2Record> update : groupV2Updates) {
ContentValues values = getValuesForStorageGroupV2(update.getNew());
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(update.getOld().getId().getRaw())});
@@ -2571,6 +2732,22 @@ public class RecipientDatabase extends Database {
}
}
public void clearDirtyStateForRecords(@NonNull List<SignalStorageRecord> records) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
Preconditions.checkArgument(db.inTransaction(), "Database should already be in a transaction.");
ContentValues values = new ContentValues();
values.put(DIRTY, DirtyState.CLEAN.getId());
String query = STORAGE_SERVICE_ID + " = ?";
for (SignalRecord record : records) {
String[] args = SqlUtil.buildArgs(Base64.encodeBytes(record.getId().getRaw()));
db.update(TABLE_NAME, values, query, args);
}
}
public void clearDirtyState(@NonNull List<RecipientId> recipients) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();

View File

@@ -133,11 +133,7 @@ public class SearchDatabase extends Database {
return null;
}
Cursor cursor = db.rawQuery(MESSAGES_QUERY, new String[] { fullTextSearchQuery,
fullTextSearchQuery });
setNotifyConversationListListeners(cursor);
return cursor;
return db.rawQuery(MESSAGES_QUERY, new String[] { fullTextSearchQuery, fullTextSearchQuery });
}
public Cursor queryMessages(@NonNull String query, long threadId) {
@@ -148,13 +144,10 @@ public class SearchDatabase extends Database {
return null;
}
Cursor cursor = db.rawQuery(MESSAGES_FOR_THREAD_QUERY, new String[] { fullTextSearchQuery,
String.valueOf(threadId),
fullTextSearchQuery,
String.valueOf(threadId) });
setNotifyConversationListListeners(cursor);
return cursor;
return db.rawQuery(MESSAGES_FOR_THREAD_QUERY, new String[] { fullTextSearchQuery,
String.valueOf(threadId),
fullTextSearchQuery,
String.valueOf(threadId) });
}
private static String createFullTextSearchQuery(@NonNull String query) {

View File

@@ -7,14 +7,20 @@ import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.whispersystems.libsignal.util.guava.Preconditions;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.internal.storage.protos.SignalStorage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
@@ -79,26 +85,39 @@ public class StorageKeyDatabase extends Database {
db.beginTransaction();
try {
for (SignalStorageRecord insert : inserts) {
ContentValues values = new ContentValues();
values.put(TYPE, insert.getType());
values.put(STORAGE_ID, Base64.encodeBytes(insert.getId().getRaw()));
db.insert(TABLE_NAME, null, values);
}
String deleteQuery = STORAGE_ID + " = ?";
for (SignalStorageRecord delete : deletes) {
String[] args = new String[] { Base64.encodeBytes(delete.getId().getRaw()) };
db.delete(TABLE_NAME, deleteQuery, args);
}
insert(inserts);
delete(Stream.of(deletes).map(SignalStorageRecord::getId).toList());
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public void insert(@NonNull Collection<SignalStorageRecord> inserts) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
Preconditions.checkArgument(db.inTransaction(), "Must be in a transaction!");
for (SignalStorageRecord insert : inserts) {
ContentValues values = new ContentValues();
values.put(TYPE, insert.getType());
values.put(STORAGE_ID, Base64.encodeBytes(insert.getId().getRaw()));
db.insert(TABLE_NAME, null, values);
}
}
public void delete(@NonNull Collection<StorageId> deletes) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String deleteQuery = STORAGE_ID + " = ?";
Preconditions.checkArgument(db.inTransaction(), "Must be in a transaction!");
for (StorageId id : deletes) {
String[] args = SqlUtil.buildArgs(Base64.encodeBytes(id.getRaw()));
db.delete(TABLE_NAME, deleteQuery, args);
}
}
public void deleteByType(int type) {

View File

@@ -514,7 +514,6 @@ public class ThreadDatabase extends Database {
}
Cursor cursor = cursors.size() > 1 ? new MergeCursor(cursors.toArray(new Cursor[cursors.size()])) : cursors.get(0);
setNotifyConversationListListeners(cursor);
return cursor;
}
@@ -707,8 +706,6 @@ public class ThreadDatabase extends Database {
Cursor cursor = db.rawQuery(query, new String[]{});
setNotifyConversationListListeners(cursor);
return cursor;
}
@@ -717,8 +714,6 @@ public class ThreadDatabase extends Database {
String query = createQuery(ARCHIVED + " = ? AND " + MESSAGE_COUNT + " != 0", offset, limit, false);
Cursor cursor = db.rawQuery(query, new String[]{archived});
setNotifyConversationListListeners(cursor);
return cursor;
}
@@ -1225,12 +1220,13 @@ public class ThreadDatabase extends Database {
private void applyStorageSyncUpdate(@NonNull RecipientId recipientId, boolean archived, boolean forcedUnread) {
ContentValues values = new ContentValues();
values.put(ARCHIVED, archived);
values.put(ARCHIVED, archived ? 1 : 0);
Long threadId = getThreadIdFor(recipientId);
if (forcedUnread) {
values.put(READ, ReadStatus.FORCED_UNREAD.serialize());
} else {
Long threadId = getThreadIdFor(recipientId);
if (threadId != null) {
int unreadCount = DatabaseFactory.getMmsSmsDatabase(context).getUnreadCount(threadId);
@@ -1240,6 +1236,10 @@ public class ThreadDatabase extends Database {
}
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, RECIPIENT_ID + " = ?", SqlUtil.buildArgs(recipientId));
if (threadId != null) {
notifyConversationListeners(threadId);
}
}
public boolean update(long threadId, boolean unarchive) {