Further simplify storage service syncing.

This commit is contained in:
Greyson Parrelli 2021-04-28 10:57:25 -04:00 committed by Alex Hart
parent 1493581a4d
commit cdc7f1565e
18 changed files with 261 additions and 507 deletions

View file

@ -166,7 +166,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return STATE_UI_BLOCKING_UPGRADE;
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
return STATE_WELCOME_PUSH_SCREEN;
} else if (SignalStore.storageServiceValues().needsAccountRestore()) {
} else if (SignalStore.storageService().needsAccountRestore()) {
return STATE_ENTER_SIGNAL_PIN;
} else if (userMustSetProfileName()) {
return STATE_CREATE_PROFILE_NAME;

View file

@ -143,7 +143,7 @@ public class IdentityDatabase extends Database {
boolean firstUse, long timestamp, boolean nonBlockingApproval)
{
saveIdentityInternal(recipientId, identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval);
DatabaseFactory.getRecipientDatabase(context).markDirty(recipientId, RecipientDatabase.DirtyState.UPDATE);
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipientId);
}
public void setApproval(@NonNull RecipientId recipientId, boolean nonBlockingApproval) {
@ -154,7 +154,7 @@ public class IdentityDatabase extends Database {
database.update(TABLE_NAME, contentValues, RECIPIENT_ID + " = ?", new String[] {recipientId.serialize()});
DatabaseFactory.getRecipientDatabase(context).markDirty(recipientId, RecipientDatabase.DirtyState.UPDATE);
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipientId);
}
public void setVerified(@NonNull RecipientId recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus) {
@ -169,7 +169,7 @@ public class IdentityDatabase extends Database {
if (updated > 0) {
Optional<IdentityRecord> record = getIdentity(recipientId);
if (record.isPresent()) EventBus.getDefault().post(record.get());
DatabaseFactory.getRecipientDatabase(context).markDirty(recipientId, RecipientDatabase.DirtyState.UPDATE);
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipientId);
}
}

View file

@ -8,7 +8,6 @@ import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.arch.core.util.Function;
import com.annimon.stream.Stream;
import com.google.protobuf.ByteString;
@ -90,6 +89,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
public class RecipientDatabase extends Database {
@ -132,7 +132,6 @@ public class RecipientDatabase extends Database {
static final String FORCE_SMS_SELECTION = "force_sms_selection";
private static final String CAPABILITIES = "capabilities";
private static final String STORAGE_SERVICE_ID = "storage_service_key";
private static final String DIRTY = "dirty";
private static final String PROFILE_GIVEN_NAME = "signal_profile_name";
private static final String PROFILE_FAMILY_NAME = "profile_family_name";
private static final String PROFILE_JOINED_NAME = "profile_joined_name";
@ -169,7 +168,7 @@ public class RecipientDatabase extends Database {
UNIDENTIFIED_ACCESS_MODE,
FORCE_SMS_SELECTION,
CAPABILITIES,
STORAGE_SERVICE_ID, DIRTY,
STORAGE_SERVICE_ID,
MENTION_SETTING, WALLPAPER, WALLPAPER_URI,
MENTION_SETTING,
ABOUT, ABOUT_EMOJI,
@ -188,7 +187,6 @@ public class RecipientDatabase extends Database {
private static final String[] MENTION_SEARCH_PROJECTION = new String[]{ID, removeWhitespace("COALESCE(" + nullIfEmpty(SYSTEM_JOINED_NAME) + ", " + nullIfEmpty(SYSTEM_GIVEN_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ", " + nullIfEmpty(PHONE) + ")") + " AS " + SORT_NAME};
public static final String[] CREATE_INDEXS = new String[] {
"CREATE INDEX IF NOT EXISTS recipient_dirty_index ON " + TABLE_NAME + " (" + DIRTY + ");",
"CREATE INDEX IF NOT EXISTS recipient_group_type_index ON " + TABLE_NAME + " (" + GROUP_TYPE + ");",
};
@ -272,24 +270,6 @@ public class RecipientDatabase extends Database {
}
}
public enum DirtyState {
CLEAN(0), UPDATE(1), INSERT(2), DELETE(3);
private final int id;
DirtyState(int id) {
this.id = id;
}
int getId() {
return id;
}
public static DirtyState fromId(int id) {
return values()[id];
}
}
public enum GroupType {
NONE(0), MMS(1), SIGNAL_V1(2), SIGNAL_V2(3);
@ -365,7 +345,6 @@ public class RecipientDatabase extends Database {
UNIDENTIFIED_ACCESS_MODE + " INTEGER DEFAULT 0, " +
FORCE_SMS_SELECTION + " INTEGER DEFAULT 0, " +
STORAGE_SERVICE_ID + " TEXT UNIQUE DEFAULT NULL, " +
DIRTY + " INTEGER DEFAULT " + DirtyState.CLEAN.getId() + ", " +
MENTION_SETTING + " INTEGER DEFAULT " + MentionSetting.ALWAYS_NOTIFY.getId() + ", " +
STORAGE_PROTO + " TEXT DEFAULT NULL, " +
CAPABILITIES + " INTEGER DEFAULT 0, " +
@ -404,24 +383,29 @@ public class RecipientDatabase extends Database {
}
}
public @NonNull Optional<RecipientId> getByE164(@NonNull String e164) {
public @NonNull
Optional<RecipientId> getByE164(@NonNull String e164) {
return getByColumn(PHONE, e164);
}
public @NonNull Optional<RecipientId> getByEmail(@NonNull String email) {
public @NonNull
Optional<RecipientId> getByEmail(@NonNull String email) {
return getByColumn(EMAIL, email);
}
public @NonNull Optional<RecipientId> getByGroupId(@NonNull GroupId groupId) {
public @NonNull
Optional<RecipientId> getByGroupId(@NonNull GroupId groupId) {
return getByColumn(GROUP_ID, groupId.toString());
}
public @NonNull Optional<RecipientId> getByUuid(@NonNull UUID uuid) {
public @NonNull
Optional<RecipientId> getByUuid(@NonNull UUID uuid) {
return getByColumn(UUID, uuid.toString());
}
public @NonNull Optional<RecipientId> getByUsername(@NonNull String username) {
public @NonNull
Optional<RecipientId> getByUsername(@NonNull String username) {
return getByColumn(USERNAME, username);
}
@ -568,7 +552,6 @@ public class RecipientDatabase extends Database {
if (uuid != null) {
values.put(UUID, uuid.toString().toLowerCase());
values.put(REGISTERED, RegisteredState.REGISTERED.getId());
values.put(DIRTY, DirtyState.INSERT.getId());
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()));
}
@ -626,7 +609,6 @@ public class RecipientDatabase extends Database {
} else {
groupUpdates.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId());
}
groupUpdates.put(DIRTY, DirtyState.INSERT.getId());
groupUpdates.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()));
}
@ -718,18 +700,6 @@ public class RecipientDatabase extends Database {
}
}
public @NonNull DirtyState getDirtyState(@NonNull RecipientId recipientId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
try (Cursor cursor = db.query(TABLE_NAME, new String[] { DIRTY }, ID_WHERE, new String[] { recipientId.serialize() }, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
return DirtyState.fromId(cursor.getInt(cursor.getColumnIndexOrThrow(DIRTY)));
}
}
return DirtyState.CLEAN;
}
public @Nullable RecipientSettings getRecipientSettingsForSync(@NonNull RecipientId id) {
String query = TABLE_NAME + "." + ID + " = ?";
String[] args = new String[]{id.serialize()};
@ -747,27 +717,6 @@ public class RecipientDatabase extends Database {
return recipientSettingsForSync.get(0);
}
public @NonNull List<RecipientSettings> getPendingRecipientSyncUpdates() {
String query = TABLE_NAME + "." + DIRTY + " = ? AND " + TABLE_NAME + "." + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
String[] args = new String[] { String.valueOf(DirtyState.UPDATE.getId()), Recipient.self().getId().serialize() };
return getRecipientSettingsForSync(query, args);
}
public @NonNull List<RecipientSettings> getPendingRecipientSyncInsertions() {
String query = TABLE_NAME + "." + DIRTY + " = ? AND " + TABLE_NAME + "." + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
String[] args = new String[] { String.valueOf(DirtyState.INSERT.getId()), Recipient.self().getId().serialize() };
return getRecipientSettingsForSync(query, args);
}
public @NonNull List<RecipientSettings> getPendingRecipientSyncDeletions() {
String query = TABLE_NAME + "." + DIRTY + " = ? AND " + TABLE_NAME + "." + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
String[] args = new String[] { String.valueOf(DirtyState.DELETE.getId()), Recipient.self().getId().serialize() };
return getRecipientSettingsForSync(query, args);
}
public @Nullable RecipientSettings getByStorageId(@NonNull byte[] storageId) {
List<RecipientSettings> result = getRecipientSettingsForSync(TABLE_NAME + "." + STORAGE_SERVICE_ID + " = ?", new String[] { Base64.encodeBytes(storageId) });
@ -784,7 +733,8 @@ public class RecipientDatabase extends Database {
db.beginTransaction();
try {
for (RecipientId recipientId : recipientIds) {
markDirty(recipientId, DirtyState.UPDATE);
rotateStorageId(recipientId);
Recipient.live(recipientId).refresh();
}
db.setTransactionSuccessful();
} finally {
@ -793,7 +743,8 @@ public class RecipientDatabase extends Database {
}
public void markNeedsSync(@NonNull RecipientId recipientId) {
markDirty(recipientId, DirtyState.UPDATE);
rotateStorageId(recipientId);
Recipient.live(recipientId).refresh();
}
public void applyStorageIdUpdates(@NonNull Map<RecipientId, StorageId> storageIds) {
@ -806,7 +757,6 @@ public class RecipientDatabase extends Database {
for (Map.Entry<RecipientId, StorageId> entry : storageIds.entrySet()) {
ContentValues values = new ContentValues();
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(entry.getValue().getRaw()));
values.put(DIRTY, DirtyState.CLEAN.getId());
db.update(TABLE_NAME, values, query, new String[] { entry.getKey().serialize() });
}
@ -996,7 +946,6 @@ public class RecipientDatabase extends Database {
values.put(PROFILE_JOINED_NAME, profileName.toString());
values.put(PROFILE_KEY, profileKey);
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(update.getId().getRaw()));
values.put(DIRTY, DirtyState.CLEAN.getId());
if (update.hasUnknownFields()) {
values.put(STORAGE_PROTO, Base64.encodeBytes(update.serializeUnknownFields()));
@ -1075,7 +1024,6 @@ public class RecipientDatabase extends Database {
values.put(BLOCKED, contact.isBlocked() ? "1" : "0");
values.put(MUTE_UNTIL, contact.getMuteUntil());
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(contact.getId().getRaw()));
values.put(DIRTY, DirtyState.CLEAN.getId());
if (contact.isProfileSharingEnabled() && isInsert && !profileName.isEmpty()) {
values.put(COLOR, ContactColors.generateFor(profileName.toString()).serialize());
@ -1098,7 +1046,6 @@ public class RecipientDatabase extends Database {
values.put(BLOCKED, groupV1.isBlocked() ? "1" : "0");
values.put(MUTE_UNTIL, groupV1.getMuteUntil());
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(groupV1.getId().getRaw()));
values.put(DIRTY, DirtyState.CLEAN.getId());
if (groupV1.hasUnknownFields()) {
values.put(STORAGE_PROTO, Base64.encodeBytes(groupV1.serializeUnknownFields()));
@ -1117,7 +1064,6 @@ public class RecipientDatabase extends Database {
values.put(BLOCKED, groupV2.isBlocked() ? "1" : "0");
values.put(MUTE_UNTIL, groupV2.getMuteUntil());
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(groupV2.getId().getRaw()));
values.put(DIRTY, DirtyState.CLEAN.getId());
if (groupV2.hasUnknownFields()) {
values.put(STORAGE_PROTO, Base64.encodeBytes(groupV2.serializeUnknownFields()));
@ -1166,8 +1112,8 @@ public class RecipientDatabase extends Database {
*/
public @NonNull Map<RecipientId, StorageId> getContactStorageSyncIdsMap() {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String query = STORAGE_SERVICE_ID + " NOT NULL AND " + DIRTY + " != ? AND " + ID + " != ? AND " + GROUP_TYPE + " != ?";
String[] args = { String.valueOf(DirtyState.DELETE.getId()), Recipient.self().getId().serialize(), String.valueOf(GroupType.SIGNAL_V2.getId()) };
String query = STORAGE_SERVICE_ID + " NOT NULL AND " + ID + " != ? AND " + GROUP_TYPE + " != ?";
String[] args = SqlUtil.buildArgs(Recipient.self().getId(), String.valueOf(GroupType.SIGNAL_V2.getId()));
Map<RecipientId, StorageId> out = new HashMap<>();
try (Cursor cursor = db.query(TABLE_NAME, new String[] { ID, STORAGE_SERVICE_ID, GROUP_TYPE }, query, args, null, null, null)) {
@ -1432,7 +1378,7 @@ public class RecipientDatabase extends Database {
ContentValues values = new ContentValues();
values.put(BLOCKED, blocked ? 1 : 0);
if (update(id, values)) {
markDirty(id, DirtyState.UPDATE);
rotateStorageId(id);
Recipient.live(id).refresh();
}
}
@ -1473,8 +1419,8 @@ public class RecipientDatabase extends Database {
ContentValues values = new ContentValues();
values.put(MUTE_UNTIL, until);
if (update(id, values)) {
rotateStorageId(id);
Recipient.live(id).refresh();
markDirty(id, DirtyState.UPDATE);
}
StorageSyncHelper.scheduleSyncForDataChange();
}
@ -1514,7 +1460,7 @@ public class RecipientDatabase extends Database {
ContentValues values = new ContentValues(1);
values.put(UNIDENTIFIED_ACCESS_MODE, unidentifiedAccessMode.getMode());
if (update(id, values)) {
markDirty(id, DirtyState.UPDATE);
rotateStorageId(id);
Recipient.live(id).refresh();
}
}
@ -1612,7 +1558,7 @@ public class RecipientDatabase extends Database {
SqlUtil.Query updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, valuesToCompare);
if (update(updateQuery, valuesToSet)) {
markDirty(id, DirtyState.UPDATE);
rotateStorageId(id);
Recipient.live(id).refresh();
StorageSyncHelper.scheduleSyncForDataChange();
return true;
@ -1637,7 +1583,7 @@ public class RecipientDatabase extends Database {
valuesToSet.put(UNIDENTIFIED_ACCESS_MODE, UnidentifiedAccessMode.UNKNOWN.getMode());
if (database.update(TABLE_NAME, valuesToSet, selection, args) > 0) {
markDirty(id, DirtyState.UPDATE);
rotateStorageId(id);
Recipient.live(id).refresh();
return true;
} else {
@ -1678,7 +1624,7 @@ public class RecipientDatabase extends Database {
ContentValues values = new ContentValues(1);
values.putNull(PROFILE_KEY_CREDENTIAL);
if (update(id, values)) {
markDirty(id, DirtyState.UPDATE);
rotateStorageId(id);
Recipient.live(id).refresh();
}
}
@ -1760,7 +1706,7 @@ public class RecipientDatabase extends Database {
contentValues.put(PROFILE_FAMILY_NAME, profileName.getFamilyName());
contentValues.put(PROFILE_JOINED_NAME, profileName.toString());
if (update(id, contentValues)) {
markDirty(id, DirtyState.UPDATE);
rotateStorageId(id);
Recipient.live(id).refresh();
StorageSyncHelper.scheduleSyncForDataChange();
}
@ -1773,7 +1719,7 @@ public class RecipientDatabase extends Database {
Recipient.live(id).refresh();
if (id.equals(Recipient.self().getId())) {
markDirty(id, DirtyState.UPDATE);
rotateStorageId(id);
StorageSyncHelper.scheduleSyncForDataChange();
}
}
@ -1805,7 +1751,7 @@ public class RecipientDatabase extends Database {
}
if (profiledUpdated || colorUpdated) {
markDirty(id, DirtyState.UPDATE);
rotateStorageId(id);
Recipient.live(id).refresh();
StorageSyncHelper.scheduleSyncForDataChange();
}
@ -1986,7 +1932,7 @@ public class RecipientDatabase extends Database {
contentValues.put(PHONE, e164);
if (update(id, contentValues)) {
markDirty(id, DirtyState.UPDATE);
rotateStorageId(id);
Recipient.live(id).refresh();
StorageSyncHelper.scheduleSyncForDataChange();
}
@ -2069,7 +2015,7 @@ public class RecipientDatabase extends Database {
contentValues.put(UUID, uuid.toString().toLowerCase());
if (update(id, contentValues)) {
markDirty(id, DirtyState.INSERT);
setStorageIdIfNotSet(id);
Recipient.live(id).refresh();
}
}
@ -2082,8 +2028,9 @@ public class RecipientDatabase extends Database {
public void markRegistered(@NonNull RecipientId id) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(REGISTERED, RegisteredState.REGISTERED.getId());
if (update(id, contentValues)) {
markDirty(id, DirtyState.INSERT);
setStorageIdIfNotSet(id);
Recipient.live(id).refresh();
}
}
@ -2091,8 +2038,9 @@ public class RecipientDatabase extends Database {
public void markUnregistered(@NonNull RecipientId id) {
ContentValues contentValues = new ContentValues(2);
contentValues.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId());
contentValues.putNull(STORAGE_SERVICE_ID);
if (update(id, contentValues)) {
markDirty(id, DirtyState.DELETE);
Recipient.live(id).refresh();
}
}
@ -2112,7 +2060,7 @@ public class RecipientDatabase extends Database {
try {
if (update(entry.getKey(), values)) {
markDirty(entry.getKey(), DirtyState.INSERT);
setStorageIdIfNotSet(entry.getKey());
}
} catch (SQLiteConstraintException e) {
Log.w(TAG, "[bulkUpdateRegisteredStatus] Hit a conflict when trying to update " + entry.getKey() + ". Possibly merging.");
@ -2126,9 +2074,9 @@ public class RecipientDatabase extends Database {
for (RecipientId id : unregistered) {
ContentValues values = new ContentValues(2);
values.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId());
if (update(id, values)) {
markDirty(id, DirtyState.DELETE);
}
values.putNull(STORAGE_SERVICE_ID);
update(id, values);
}
db.setTransactionSuccessful();
@ -2142,11 +2090,13 @@ public class RecipientDatabase extends Database {
ContentValues contentValues = new ContentValues(1);
contentValues.put(REGISTERED, registeredState.getId());
if (registeredState == RegisteredState.NOT_REGISTERED) {
contentValues.putNull(STORAGE_SERVICE_ID);
}
if (update(id, contentValues)) {
if (registeredState == RegisteredState.REGISTERED) {
markDirty(id, DirtyState.INSERT);
} else if (registeredState == RegisteredState.NOT_REGISTERED) {
markDirty(id, DirtyState.DELETE);
setStorageIdIfNotSet(id);
}
Recipient.live(id).refresh();
@ -2162,7 +2112,7 @@ public class RecipientDatabase extends Database {
registeredValues.put(REGISTERED, RegisteredState.REGISTERED.getId());
if (update(activeId, registeredValues)) {
markDirty(activeId, DirtyState.INSERT);
setStorageIdIfNotSet(activeId);
Recipient.live(activeId).refresh();
}
}
@ -2170,9 +2120,9 @@ public class RecipientDatabase extends Database {
for (RecipientId inactiveId : inactiveIds) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId());
contentValues.putNull(STORAGE_SERVICE_ID);
if (update(inactiveId, contentValues)) {
markDirty(inactiveId, DirtyState.DELETE);
Recipient.live(inactiveId).refresh();
}
}
@ -2619,46 +2569,6 @@ public class RecipientDatabase extends Database {
}
}
public void clearDirtyStateForStorageIds(@NonNull Collection<StorageId> ids) {
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 (StorageId id : ids) {
String[] args = SqlUtil.buildArgs(Base64.encodeBytes(id.getRaw()));
db.update(TABLE_NAME, values, query, args);
}
}
public void clearDirtyState(@NonNull List<RecipientId> recipients) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();
try {
ContentValues values = new ContentValues();
values.put(DIRTY, DirtyState.CLEAN.getId());
for (RecipientId id : recipients) {
Optional<RecipientId> remapped = RemappedRecords.getInstance().getRecipient(context, id);
if (remapped.isPresent()) {
Log.w(TAG, "While clearing dirty state, noticed we have a remapped contact (" + id + " to " + remapped.get() + "). Safe to delete now.");
db.delete(TABLE_NAME, ID_WHERE, new String[]{id.serialize()});
} else {
db.update(TABLE_NAME, values, ID_WHERE, new String[]{id.serialize()});
}
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public void markPreMessageRequestRecipientsAsProfileSharingEnabled(long messageRequestEnableTime) {
String[] whereArgs = SqlUtil.buildArgs(messageRequestEnableTime, messageRequestEnableTime);
@ -2750,31 +2660,27 @@ public class RecipientDatabase extends Database {
Recipient.live(recipientId).refresh();
}
void markDirty(@NonNull RecipientId recipientId, @NonNull DirtyState dirtyState) {
Log.d(TAG, "Attempting to mark " + recipientId + " with dirty state " + dirtyState);
/**
* Does not trigger any recipient refreshes -- it is assumed the caller handles this.
*/
void rotateStorageId(@NonNull RecipientId recipientId) {
ContentValues values = new ContentValues(1);
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()));
ContentValues contentValues = new ContentValues(1);
contentValues.put(DIRTY, dirtyState.getId());
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(recipientId));
}
String query = ID + " = ? AND (" + UUID + " NOT NULL OR " + PHONE + " NOT NULL OR " + GROUP_ID + " NOT NULL) AND ";
String[] args = new String[] { recipientId.serialize(), String.valueOf(dirtyState.id) };
/**
* Does not trigger any recipient refreshes -- it is assumed the caller handles this.
*/
void setStorageIdIfNotSet(@NonNull RecipientId recipientId) {
ContentValues values = new ContentValues(1);
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()));
switch (dirtyState) {
case INSERT:
query += "(" + DIRTY + " < ? OR " + DIRTY + " = ?)";
args = SqlUtil.appendArg(args, String.valueOf(DirtyState.DELETE.getId()));
String query = ID + " = ? AND " + STORAGE_SERVICE_ID + " IS NULL";
String[] args = SqlUtil.buildArgs(recipientId);
contentValues.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()));
break;
case DELETE:
query += "(" + DIRTY + " < ? OR " + DIRTY + " = ?)";
args = SqlUtil.appendArg(args, String.valueOf(DirtyState.INSERT.getId()));
break;
default:
query += DIRTY + " < ?";
}
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, query, args);
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, query, args);
}
/**
@ -2790,7 +2696,7 @@ public class RecipientDatabase extends Database {
if (update(query, values)) {
RecipientId id = getByGroupId(v2Id).get();
markDirty(id, DirtyState.UPDATE);
rotateStorageId(id);
Recipient.live(id).refresh();
}
}
@ -2816,7 +2722,8 @@ public class RecipientDatabase extends Database {
return database.update(TABLE_NAME, contentValues, updateQuery.getWhere(), updateQuery.getWhereArgs()) > 0;
}
private @NonNull Optional<RecipientId> getByColumn(@NonNull String column, String value) {
private @NonNull
Optional<RecipientId> getByColumn(@NonNull String column, String value) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String query = column + " = ?";
String[] args = new String[] { value };
@ -2873,17 +2780,8 @@ public class RecipientDatabase extends Database {
RecipientSettings e164Settings = getRecipientSettings(byE164);
// Recipient
if (e164Settings.getStorageId() == null) {
Log.w(TAG, "No storageId on the E164 recipient. Can delete right away.");
db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(byE164));
} else {
Log.w(TAG, "The E164 recipient has a storageId. Clearing data and marking for deletion.");
ContentValues values = new ContentValues();
values.putNull(PHONE);
values.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId());
values.put(DIRTY, DirtyState.DELETE.getId());
db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(byE164));
}
Log.w(TAG, "Deleting recipient " + byE164);
db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(byE164));
RemappedRecords.getInstance().addRecipient(context, byE164, byUuid);
ContentValues uuidValues = new ContentValues();
@ -3020,18 +2918,12 @@ public class RecipientDatabase extends Database {
int systemPhoneType,
@Nullable String systemContactUri)
{
ContentValues dirtyQualifyingValues = new ContentValues();
String joinedName = Util.firstNonNull(systemDisplayName, systemProfileName.toString());
dirtyQualifyingValues.put(SYSTEM_GIVEN_NAME, systemProfileName.getGivenName());
dirtyQualifyingValues.put(SYSTEM_FAMILY_NAME, systemProfileName.getFamilyName());
dirtyQualifyingValues.put(SYSTEM_JOINED_NAME, joinedName);
if (update(id, dirtyQualifyingValues)) {
markDirty(id, DirtyState.UPDATE);
}
String joinedName = Util.firstNonNull(systemDisplayName, systemProfileName.toString());
ContentValues refreshQualifyingValues = new ContentValues();
refreshQualifyingValues.put(SYSTEM_GIVEN_NAME, systemProfileName.getGivenName());
refreshQualifyingValues.put(SYSTEM_FAMILY_NAME, systemProfileName.getFamilyName());
refreshQualifyingValues.put(SYSTEM_JOINED_NAME, joinedName);
refreshQualifyingValues.put(SYSTEM_PHOTO_URI, photoUri);
refreshQualifyingValues.put(SYSTEM_PHONE_LABEL, systemPhoneLabel);
refreshQualifyingValues.put(SYSTEM_PHONE_TYPE, systemPhoneType);
@ -3060,13 +2952,15 @@ public class RecipientDatabase extends Database {
}
private void markAllRelevantEntriesDirty() {
String query = SYSTEM_INFO_PENDING + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + DIRTY + " < ?";
String[] args = new String[] { "1", String.valueOf(DirtyState.UPDATE.getId()) };
String query = SYSTEM_INFO_PENDING + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL";
String[] args = SqlUtil.buildArgs("1");
ContentValues values = new ContentValues(1);
values.put(DIRTY, DirtyState.UPDATE.getId());
database.update(TABLE_NAME, values, query, args);
try (Cursor cursor = database.query(TABLE_NAME, ID_PROJECTION, query, args, null, null, null)) {
while (cursor.moveToNext()) {
RecipientId id = RecipientId.from(CursorUtil.requireString(cursor, ID));
rotateStorageId(id);
}
}
}
private void clearSystemDataForPendingInfo() {

View file

@ -178,8 +178,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
private static final int BLUR_AVATARS = 94;
private static final int CLEAN_STORAGE_IDS_WITHOUT_INFO = 95;
private static final int CLEAN_REACTION_NOTIFICATIONS = 96;
private static final int STORAGE_SERVICE_REFACTOR = 97;
private static final int DATABASE_VERSION = 96;
private static final int DATABASE_VERSION = 97;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@ -1396,6 +1397,47 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
}
}
if (oldVersion < STORAGE_SERVICE_REFACTOR) {
int deleteCount;
int insertCount;
int updateCount;
int dirtyCount;
ContentValues deleteValues = new ContentValues();
deleteValues.putNull("storage_service_key");
deleteCount = db.update("recipient", deleteValues, "storage_service_key NOT NULL AND (dirty = 3 OR group_type = 1 OR (group_type = 0 AND registered = 2))", null);
try (Cursor cursor = db.query("recipient", new String[]{"_id"}, "storage_service_key IS NULL AND (dirty = 2 OR registered = 1)", null, null, null, null)) {
insertCount = cursor.getCount();
while (cursor.moveToNext()) {
ContentValues insertValues = new ContentValues();
insertValues.put("storage_service_key", Base64.encodeBytes(StorageSyncHelper.generateKey()));
long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
db.update("recipient", insertValues, "_id = ?", SqlUtil.buildArgs(id));
}
}
try (Cursor cursor = db.query("recipient", new String[]{"_id"}, "storage_service_key NOT NULL AND dirty = 1", null, null, null, null)) {
updateCount = cursor.getCount();
while (cursor.moveToNext()) {
ContentValues updateValues = new ContentValues();
updateValues.put("storage_service_key", Base64.encodeBytes(StorageSyncHelper.generateKey()));
long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
db.update("recipient", updateValues, "_id = ?", SqlUtil.buildArgs(id));
}
}
ContentValues clearDirtyValues = new ContentValues();
clearDirtyValues.put("dirty", 0);
dirtyCount = db.update("recipient", clearDirtyValues, "dirty != 0", null);
Log.d(TAG, String.format(Locale.US, "For storage service refactor migration, there were %d inserts, %d updated, and %d deletes. Cleared the dirty status on %d rows.", insertCount, updateCount, deleteCount, dirtyCount));
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View file

@ -59,7 +59,7 @@ public class MultiDeviceKeysUpdateJob extends BaseJob {
}
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
StorageKey storageServiceKey = SignalStore.storageServiceValues().getOrCreateStorageKey();
StorageKey storageServiceKey = SignalStore.storageService().getOrCreateStorageKey();
messageSender.sendMessage(SignalServiceSyncMessage.forKeys(new KeysMessage(Optional.fromNullable(storageServiceKey))),
UnidentifiedAccessUtil.getAccessForSync(context));

View file

@ -65,7 +65,7 @@ public class StorageAccountRestoreJob extends BaseJob {
@Override
protected void onRun() throws Exception {
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
StorageKey storageServiceKey = SignalStore.storageServiceValues().getOrCreateStorageKey();
StorageKey storageServiceKey = SignalStore.storageService().getOrCreateStorageKey();
Log.i(TAG, "Retrieving manifest...");
Optional<SignalStorageManifest> manifest = accountManager.getStorageManifest(storageServiceKey);
@ -76,8 +76,8 @@ public class StorageAccountRestoreJob extends BaseJob {
return;
}
Log.i(TAG, "Updating local manifest version to 0.");
TextSecurePreferences.setStorageManifestVersion(context, 0);
Log.i(TAG, "Resetting the local manifest to an empty state so that it will sync later.");
SignalStore.storageService().setManifest(SignalStorageManifest.EMPTY);
Optional<StorageId> accountId = manifest.get().getAccountStorageId();

View file

@ -72,7 +72,7 @@ public class StorageForcePushJob extends BaseJob {
@Override
protected void onRun() throws IOException, RetryLaterException {
StorageKey storageServiceKey = SignalStore.storageServiceValues().getOrCreateStorageKey();
StorageKey storageServiceKey = SignalStore.storageService().getOrCreateStorageKey();
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
UnknownStorageIdDatabase storageIdDatabase = DatabaseFactory.getUnknownStorageIdDatabase(context);
@ -117,7 +117,7 @@ public class StorageForcePushJob extends BaseJob {
}
Log.i(TAG, "Force push succeeded. Updating local manifest version to: " + newVersion);
TextSecurePreferences.setStorageManifestVersion(context, newVersion);
SignalStore.storageService().setManifest(manifest);
recipientDatabase.applyStorageIdUpdates(newContactStorageIds);
recipientDatabase.applyStorageIdUpdates(Collections.singletonMap(Recipient.self().getId(), accountRecord.getId()));
storageIdDatabase.deleteAll();

View file

@ -28,7 +28,6 @@ import org.thoughtcrime.securesms.storage.GroupV2RecordProcessor;
import org.thoughtcrime.securesms.storage.StorageRecordUpdate;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.storage.StorageSyncHelper.IdDifferenceResult;
import org.thoughtcrime.securesms.storage.StorageSyncHelper.LocalWriteResult;
import org.thoughtcrime.securesms.storage.StorageSyncHelper.WriteOperationResult;
import org.thoughtcrime.securesms.storage.StorageSyncModels;
import org.thoughtcrime.securesms.storage.StorageSyncValidations;
@ -52,6 +51,7 @@ import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.api.storage.StorageKey;
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
import org.whispersystems.signalservice.internal.storage.protos.SignalStorage;
import java.io.IOException;
import java.util.ArrayList;
@ -105,10 +105,9 @@ import java.util.concurrent.TimeUnit;
* the diff in IDs.
* - Then, we fetch the actual records that correspond to the remote-only IDs.
* - Afterwards, we take those records and merge them into our local data store.
* - The merging process could result in changes that need to be written back to the service, so
* we write those back.
* - Finally, we look at any other local changes that were made (independent of the ID diff) and
* make sure those are written to the service.
* - Finally, we assume that our local state represents the most up-to-date information, and so we
* calculate and write a change set that represents the diff between our state and the remote
* state.
*
* Of course, you'll notice that there's a lot of code to support that goal. That's mostly because
* converting local data into a format that can be compared with, merged, and eventually written
@ -130,8 +129,8 @@ import java.util.concurrent.TimeUnit;
* - Update builder usage in StorageSyncModels
* - Handle the new data when writing to the local storage
* (i.e. {@link RecipientDatabase#applyStorageSyncContactUpdate(StorageRecordUpdate)}).
* - Make sure that whenever you change the field in the UI, we mark the row as dirty and call
* {@link StorageSyncHelper#scheduleSyncForDataChange()}.
* - Make sure that whenever you change the field in the UI, we rotate the storageId for that row
* and call {@link StorageSyncHelper#scheduleSyncForDataChange()}.
* - If you're syncing a field that was otherwise already present in the UI, you'll probably want
* to enqueue a {@link StorageServiceMigrationJob} as an app migration to make sure it gets
* synced.
@ -185,7 +184,7 @@ public class StorageSyncJob extends BaseJob {
ApplicationDependencies.getJobManager().add(new MultiDeviceStorageSyncRequestJob());
}
SignalStore.storageServiceValues().onSyncCompleted();
SignalStore.storageService().onSyncCompleted();
} catch (InvalidKeyException e) {
Log.w(TAG, "Failed to decrypt remote storage! Force-pushing and syncing the storage key to linked devices.", e);
@ -206,29 +205,28 @@ public class StorageSyncJob extends BaseJob {
}
private boolean performSync() throws IOException, RetryLaterException, InvalidKeyException {
Stopwatch stopwatch = new Stopwatch("StorageSync");
Recipient self = Recipient.self().fresh();
SQLiteDatabase db = DatabaseFactory.getInstance(context).getRawDatabase();
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
UnknownStorageIdDatabase storageIdDatabase = DatabaseFactory.getUnknownStorageIdDatabase(context);
StorageKey storageServiceKey = SignalStore.storageServiceValues().getOrCreateStorageKey();
final Stopwatch stopwatch = new Stopwatch("StorageSync");
final SQLiteDatabase db = DatabaseFactory.getInstance(context).getRawDatabase();
final SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
final UnknownStorageIdDatabase storageIdDatabase = DatabaseFactory.getUnknownStorageIdDatabase(context);
final StorageKey storageServiceKey = SignalStore.storageService().getOrCreateStorageKey();
boolean needsMultiDeviceSync = false;
boolean needsForcePush = false;
long localManifestVersion = TextSecurePreferences.getStorageManifestVersion(context);
Optional<SignalStorageManifest> remoteManifest = accountManager.getStorageManifestIfDifferentVersion(storageServiceKey, localManifestVersion);
long remoteManifestVersion = remoteManifest.transform(SignalStorageManifest::getVersion).or(localManifestVersion);
final SignalStorageManifest localManifest = SignalStore.storageService().getManifest();
final SignalStorageManifest remoteManifest = accountManager.getStorageManifestIfDifferentVersion(storageServiceKey, localManifest.getVersion()).or(localManifest);
Recipient self = Recipient.self().fresh();
boolean needsMultiDeviceSync = false;
boolean needsForcePush = false;
stopwatch.split("remote-manifest");
Log.i(TAG, "Our version: " + localManifestVersion + ", their version: " + remoteManifestVersion);
Log.i(TAG, "Our version: " + localManifest.getVersion() + ", their version: " + remoteManifest.getVersion());
if (remoteManifest.isPresent() && remoteManifestVersion > localManifestVersion) {
if (remoteManifest.getVersion() > localManifest.getVersion()) {
Log.i(TAG, "[Remote Sync] Newer manifest version found!");
List<StorageId> localStorageIdsBeforeMerge = getAllLocalStorageIds(context, Recipient.self().fresh());
IdDifferenceResult idDifference = StorageSyncHelper.findIdDifference(remoteManifest.get().getStorageIds(), localStorageIdsBeforeMerge);
IdDifferenceResult idDifference = StorageSyncHelper.findIdDifference(remoteManifest.getStorageIds(), localStorageIdsBeforeMerge);
if (idDifference.hasTypeMismatches()) {
Log.w(TAG, "[Remote Sync] Found type mismatches in the ID sets! Scheduling a force push after this sync completes.");
@ -247,8 +245,7 @@ public class StorageSyncJob extends BaseJob {
stopwatch.split("remote-records");
if (remoteOnly.size() != idDifference.getRemoteOnlyIds().size()) {
Log.w(TAG, "[Remote Sync] Could not find all remote-only records! Requested: " + idDifference.getRemoteOnlyIds().size() + ", Found: " + remoteOnly.size() + ". Scheduling a force push after this sync completes.");
needsForcePush = true;
Log.w(TAG, "[Remote Sync] Could not find all remote-only records! Requested: " + idDifference.getRemoteOnlyIds().size() + ", Found: " + remoteOnly.size() + ". These stragglers should naturally get deleted during the sync.");
}
List<SignalContactRecord> remoteContacts = new LinkedList<>();
@ -271,8 +268,6 @@ public class StorageSyncJob extends BaseJob {
}
}
WriteOperationResult mergeWriteOperation;
db.beginTransaction();
try {
new ContactRecordProcessor(context, self).process(remoteContacts, StorageSyncHelper.KEY_GENERATOR);
@ -285,150 +280,73 @@ public class StorageSyncJob extends BaseJob {
List<SignalStorageRecord> unknownInserts = remoteUnknown;
List<StorageId> unknownDeletes = Stream.of(idDifference.getLocalOnlyIds()).filter(StorageId::isUnknown).toList();
storageIdDatabase.insert(unknownInserts);
storageIdDatabase.delete(unknownDeletes);
Log.i(TAG, "[Remote Sync] Unknowns :: " + unknownInserts.size() + " inserts, " + unknownDeletes.size() + " deletes");
List<StorageId> localStorageIdsAfterMerge = getAllLocalStorageIds(context, Recipient.self().fresh());
Set<StorageId> localIdsAdded = SetUtil.difference(localStorageIdsAfterMerge, localStorageIdsBeforeMerge);
Set<StorageId> localIdsRemoved = SetUtil.difference(localStorageIdsBeforeMerge, localStorageIdsAfterMerge);
Log.i(TAG, "[Remote Sync] Local ID Changes :: " + localIdsAdded.size() + " inserts, " + localIdsRemoved.size() + " deletes");
IdDifferenceResult postMergeIdDifference = StorageSyncHelper.findIdDifference(remoteManifest.get().getStorageIds(), localStorageIdsAfterMerge);
List<SignalStorageRecord> remoteInserts = buildLocalStorageRecords(context, self, postMergeIdDifference.getLocalOnlyIds());
List<byte[]> remoteDeletes = Stream.of(postMergeIdDifference.getRemoteOnlyIds()).map(StorageId::getRaw).toList();
Log.i(TAG, "[Remote Sync] Post-Merge ID Difference :: " + postMergeIdDifference);
mergeWriteOperation = new WriteOperationResult(new SignalStorageManifest(remoteManifestVersion + 1, localStorageIdsAfterMerge),
remoteInserts,
remoteDeletes);
recipientDatabase.clearDirtyStateForStorageIds(Util.concatenatedList(localIdsAdded, localIdsRemoved));
storageIdDatabase.insert(unknownInserts);
storageIdDatabase.delete(unknownDeletes);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners();
stopwatch.split("remote-merge-transaction");
}
stopwatch.split("remote-merge-transaction");
Log.i(TAG, "[Remote Sync] WriteOperationResult :: " + mergeWriteOperation);
if (!mergeWriteOperation.isEmpty()) {
Log.i(TAG, "[Remote Sync] We have something to write remotely.");
StorageSyncValidations.validate(mergeWriteOperation, remoteManifest, needsForcePush, self);
Optional<SignalStorageManifest> conflict = accountManager.writeStorageRecords(storageServiceKey, mergeWriteOperation.getManifest(), mergeWriteOperation.getInserts(), mergeWriteOperation.getDeletes());
if (conflict.isPresent()) {
Log.w(TAG, "[Remote Sync] Hit a conflict when trying to resolve the conflict! Retrying.");
throw new RetryLaterException();
}
stopwatch.split("remote-merge-write");
remoteManifestVersion = mergeWriteOperation.getManifest().getVersion();
remoteManifest = Optional.of(mergeWriteOperation.getManifest());
needsMultiDeviceSync = true;
} else {
Log.i(TAG, "[Remote Sync] No remote writes needed.");
}
Log.i(TAG, "[Remote Sync] Updating local manifest version to: " + remoteManifestVersion);
TextSecurePreferences.setStorageManifestVersion(context, remoteManifestVersion);
} else {
Log.i(TAG, "[Remote Sync] Remote version was newer, there were no remote-only IDs.");
Log.i(TAG, "[Remote Sync] Updating local manifest version to: " + remoteManifest.get().getVersion());
TextSecurePreferences.setStorageManifestVersion(context, remoteManifest.get().getVersion());
Log.i(TAG, "[Remote Sync] Remote version was newer, but there were no remote-only IDs.");
}
} else if (remoteManifest.isPresent() && remoteManifestVersion < localManifestVersion) {
Log.w(TAG, "[Remote Sync] Remote version was older. User might have switched accounts. Making our version match.");
TextSecurePreferences.setStorageManifestVersion(context, remoteManifestVersion);
} else if (remoteManifest.getVersion() < localManifest.getVersion()) {
Log.w(TAG, "[Remote Sync] Remote version was older. User might have switched accounts.");
}
localManifestVersion = TextSecurePreferences.getStorageManifestVersion(context);
if (remoteManifest != localManifest) {
Log.i(TAG, "[Remote Sync] Saved new manifest. Now at version: " + remoteManifest.getVersion());
SignalStore.storageService().setManifest(remoteManifest);
}
List<StorageId> allLocalStorageIds;
List<RecipientSettings> pendingUpdates;
List<RecipientSettings> pendingInsertions;
List<RecipientSettings> pendingDeletions;
Optional<SignalAccountRecord> pendingAccountInsert;
Optional<SignalAccountRecord> pendingAccountUpdate;
Optional<LocalWriteResult> localWriteResult;
Log.i(TAG, "We are up-to-date with the remote storage state.");
final WriteOperationResult remoteWriteOperation;
db.beginTransaction();
try {
allLocalStorageIds = getAllLocalStorageIds(context, self);
pendingUpdates = recipientDatabase.getPendingRecipientSyncUpdates();
pendingInsertions = recipientDatabase.getPendingRecipientSyncInsertions();
pendingDeletions = recipientDatabase.getPendingRecipientSyncDeletions();
pendingAccountInsert = StorageSyncHelper.getPendingAccountSyncInsert(context, self);
pendingAccountUpdate = StorageSyncHelper.getPendingAccountSyncUpdate(context, self);
localWriteResult = StorageSyncHelper.buildStorageUpdatesForLocal(localManifestVersion,
allLocalStorageIds,
pendingUpdates,
pendingInsertions,
pendingDeletions,
pendingAccountUpdate,
pendingAccountInsert);
List<StorageId> localStorageIds = getAllLocalStorageIds(context, Recipient.self().fresh());
IdDifferenceResult idDifference = StorageSyncHelper.findIdDifference(remoteManifest.getStorageIds(), localStorageIds);
List<SignalStorageRecord> remoteInserts = buildLocalStorageRecords(context, self, idDifference.getLocalOnlyIds());
List<byte[]> remoteDeletes = Stream.of(idDifference.getRemoteOnlyIds()).map(StorageId::getRaw).toList();
Log.i(TAG, "ID Difference :: " + idDifference);
remoteWriteOperation = new WriteOperationResult(new SignalStorageManifest(remoteManifest.getVersion() + 1, localStorageIds),
remoteInserts,
remoteDeletes);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
stopwatch.split("local-data-transaction");
}
stopwatch.split("local-changes-transaction");
if (!remoteWriteOperation.isEmpty()) {
Log.i(TAG, "We have something to write remotely.");
Log.i(TAG, "WriteOperationResult :: " + remoteWriteOperation);
if (localWriteResult.isPresent()) {
Log.i(TAG, String.format(Locale.ENGLISH, "[Local Changes] Local changes present. %d updates, %d inserts, %d deletes, account update: %b, account insert: %b.", pendingUpdates.size(), pendingInsertions.size(), pendingDeletions.size(), pendingAccountUpdate.isPresent(), pendingAccountInsert.isPresent()));
StorageSyncValidations.validate(remoteWriteOperation, remoteManifest, needsForcePush, self);
WriteOperationResult localWrite = localWriteResult.get().getWriteResult();
Log.i(TAG, "[Local Changes] WriteOperationResult :: " + localWrite);
if (localWrite.isEmpty()) {
throw new AssertionError("Decided there were local writes, but our write result was empty!");
}
StorageSyncValidations.validate(localWrite, Optional.absent(), needsForcePush, self);
Optional<SignalStorageManifest> conflict = accountManager.writeStorageRecords(storageServiceKey, localWrite.getManifest(), localWrite.getInserts(), localWrite.getDeletes());
Optional<SignalStorageManifest> conflict = accountManager.writeStorageRecords(storageServiceKey, remoteWriteOperation.getManifest(), remoteWriteOperation.getInserts(), remoteWriteOperation.getDeletes());
if (conflict.isPresent()) {
Log.w(TAG, "[Local Changes] Hit a conflict when trying to upload our local writes! Retrying.");
Log.w(TAG, "Hit a conflict when trying to resolve the conflict! Retrying.");
throw new RetryLaterException();
}
stopwatch.split("remote-change-write");
Log.i(TAG, "Saved new manifest. Now at version: " + remoteWriteOperation.getManifest().getVersion());
SignalStore.storageService().setManifest(remoteWriteOperation.getManifest());
List<RecipientId> clearIds = Util.concatenatedList(Stream.of(pendingUpdates).map(RecipientSettings::getId).toList(),
Stream.of(pendingInsertions).map(RecipientSettings::getId).toList(),
Stream.of(pendingDeletions).map(RecipientSettings::getId).toList(),
Collections.singletonList(Recipient.self().getId()));
db.beginTransaction();
try {
recipientDatabase.clearDirtyState(clearIds);
recipientDatabase.updateStorageIds(localWriteResult.get().getStorageKeyUpdates());
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
stopwatch.split("local-db-clean");
stopwatch.split("remote-write");
needsMultiDeviceSync = true;
Log.i(TAG, "[Local Changes] Updating local manifest version to: " + localWriteResult.get().getWriteResult().getManifest().getVersion());
TextSecurePreferences.setStorageManifestVersion(context, localWriteResult.get().getWriteResult().getManifest().getVersion());
} else {
Log.i(TAG, "[Local Changes] No local changes.");
Log.i(TAG, "No remote writes needed. Still at version: " + remoteManifest.getVersion());
}
if (needsForcePush) {

View file

@ -62,7 +62,7 @@ public final class SignalStore {
registrationValues().onFirstEverAppLaunch();
pinValues().onFirstEverAppLaunch();
remoteConfigValues().onFirstEverAppLaunch();
storageServiceValues().onFirstEverAppLaunch();
storageService().onFirstEverAppLaunch();
uiHints().onFirstEverAppLaunch();
tooltips().onFirstEverAppLaunch();
misc().onFirstEverAppLaunch();
@ -83,7 +83,7 @@ public final class SignalStore {
keys.addAll(registrationValues().getKeysToIncludeInBackup());
keys.addAll(pinValues().getKeysToIncludeInBackup());
keys.addAll(remoteConfigValues().getKeysToIncludeInBackup());
keys.addAll(storageServiceValues().getKeysToIncludeInBackup());
keys.addAll(storageService().getKeysToIncludeInBackup());
keys.addAll(uiHints().getKeysToIncludeInBackup());
keys.addAll(tooltips().getKeysToIncludeInBackup());
keys.addAll(misc().getKeysToIncludeInBackup());
@ -124,7 +124,7 @@ public final class SignalStore {
return INSTANCE.remoteConfigValues;
}
public static @NonNull StorageServiceValues storageServiceValues() {
public static @NonNull StorageServiceValues storageService() {
return INSTANCE.storageServiceValues;
}

View file

@ -1,7 +1,9 @@
package org.thoughtcrime.securesms.keyvalue;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.StorageKey;
import java.util.Collections;
@ -11,6 +13,7 @@ public class StorageServiceValues extends SignalStoreValues {
private static final String LAST_SYNC_TIME = "storage.last_sync_time";
private static final String NEEDS_ACCOUNT_RESTORE = "storage.needs_account_restore";
private static final String MANIFEST = "storage.manifest";
StorageServiceValues(@NonNull KeyValueStore store) {
super(store);
@ -44,4 +47,18 @@ public class StorageServiceValues extends SignalStoreValues {
public void setNeedsAccountRestore(boolean value) {
putBoolean(NEEDS_ACCOUNT_RESTORE, value);
}
public void setManifest(@NonNull SignalStorageManifest manifest) {
putBlob(MANIFEST, manifest.serialize());
}
public @NonNull SignalStorageManifest getManifest() {
byte[] data = getBlob(MANIFEST, null);
if (data != null) {
return SignalStorageManifest.deserialize(data);
} else {
return SignalStorageManifest.EMPTY;
}
}
}

View file

@ -24,7 +24,7 @@ public class LogSectionPin implements LogSection {
.append("Signal PIN: ").append(SignalStore.kbsValues().hasPin()).append("\n")
.append("Opted Out: ").append(SignalStore.kbsValues().hasOptedOut()).append("\n")
.append("Last Creation Failed: ").append(SignalStore.kbsValues().lastPinCreateFailed()).append("\n")
.append("Needs Account Restore: ").append(SignalStore.storageServiceValues().needsAccountRestore()).append("\n")
.append("Needs Account Restore: ").append(SignalStore.storageService().needsAccountRestore()).append("\n")
.append("PIN Required at Registration: ").append(SignalStore.registrationValues().pinWasRequiredAtRegistration()).append("\n")
.append("Registration Complete: ").append(SignalStore.registrationValues().isRegistrationComplete());

View file

@ -51,7 +51,7 @@ public class PinRestoreViewModel extends ViewModel {
switch (result.getResult()) {
case SUCCESS:
SignalStore.pinValues().setKeyboardType(pinKeyboardType);
SignalStore.storageServiceValues().setNeedsAccountRestore(false);
SignalStore.storageService().setNeedsAccountRestore(false);
event.postValue(Event.SUCCESS);
break;
case LOCKED:

View file

@ -126,7 +126,7 @@ public final class PinState {
Log.i(TAG, "Has a PIN to restore.");
SignalStore.kbsValues().clearRegistrationLockAndPin();
TextSecurePreferences.setV1RegistrationLockEnabled(context, false);
SignalStore.storageServiceValues().setNeedsAccountRestore(true);
SignalStore.storageService().setNeedsAccountRestore(true);
} else {
Log.i(TAG, "No registration lock or PIN at all.");
SignalStore.kbsValues().clearRegistrationLockAndPin();
@ -145,7 +145,7 @@ public final class PinState {
SignalStore.kbsValues().setKbsMasterKey(kbsData, pin);
SignalStore.kbsValues().setV2RegistrationLockEnabled(false);
SignalStore.pinValues().resetPinReminders();
SignalStore.storageServiceValues().setNeedsAccountRestore(false);
SignalStore.storageService().setNeedsAccountRestore(false);
resetPinRetryCount(context, pin);
ClearFallbackKbsEnclaveJob.clearAll();
@ -157,7 +157,7 @@ public final class PinState {
*/
public static synchronized void onPinRestoreForgottenOrSkipped() {
SignalStore.kbsValues().clearRegistrationLockAndPin();
SignalStore.storageServiceValues().setNeedsAccountRestore(false);
SignalStore.storageService().setNeedsAccountRestore(false);
updateState(buildInferredStateFromOtherFields());
}

View file

@ -32,7 +32,7 @@ public final class RegistrationCompleteFragment extends BaseRegistrationFragment
FragmentActivity activity = requireActivity();
if (SignalStore.storageServiceValues().needsAccountRestore()) {
if (SignalStore.storageService().needsAccountRestore()) {
activity.startActivity(new Intent(activity, PinRestoreActivity.class));
} else if (!isReregister()) {
final Intent main = MainActivity.clearTop(activity);

View file

@ -57,123 +57,6 @@ public final class StorageSyncHelper {
private static final long REFRESH_INTERVAL = TimeUnit.HOURS.toMillis(2);
/**
* Given the local state of pending storage mutations, this will generate a result that will
* include that data that needs to be written to the storage service, as well as any changes you
* need to write back to local storage (like storage keys that might have changed for updated
* contacts).
*
* @param currentManifestVersion What you think the version is locally.
* @param currentLocalKeys All local keys you have. This assumes that 'inserts' were given keys
* already, and that deletes still have keys.
* @param updates Contacts that have been altered.
* @param inserts Contacts that have been inserted (or newly marked as registered).
* @param deletes Contacts that are no longer registered.
*
* @return If changes need to be written, then it will return those changes. If no changes need
* to be written, this will return {@link Optional#absent()}.
*/
public static @NonNull Optional<LocalWriteResult> buildStorageUpdatesForLocal(long currentManifestVersion,
@NonNull List<StorageId> currentLocalKeys,
@NonNull List<RecipientSettings> updates,
@NonNull List<RecipientSettings> inserts,
@NonNull List<RecipientSettings> deletes,
@NonNull Optional<SignalAccountRecord> accountUpdate,
@NonNull Optional<SignalAccountRecord> accountInsert)
{
int accountCount = Stream.of(currentLocalKeys)
.filter(id -> id.getType() == ManifestRecord.Identifier.Type.ACCOUNT_VALUE)
.toList()
.size();
if (accountCount > 1) {
throw new MultipleExistingAccountsException();
}
Optional<StorageId> accountId = Optional.fromNullable(Stream.of(currentLocalKeys)
.filter(id -> id.getType() == ManifestRecord.Identifier.Type.ACCOUNT_VALUE)
.findFirst()
.orElse(null));
if (accountId.isPresent() && accountInsert.isPresent() && !accountInsert.get().getId().equals(accountId.get())) {
throw new InvalidAccountInsertException();
}
if (accountId.isPresent() && accountUpdate.isPresent() && !accountUpdate.get().getId().equals(accountId.get())) {
throw new InvalidAccountUpdateException();
}
if (accountUpdate.isPresent() && accountInsert.isPresent()) {
throw new InvalidAccountDualInsertUpdateException();
}
Set<StorageId> completeIds = new LinkedHashSet<>(currentLocalKeys);
Set<SignalStorageRecord> storageInserts = new LinkedHashSet<>();
Set<ByteBuffer> storageDeletes = new LinkedHashSet<>();
Map<RecipientId, byte[]> storageKeyUpdates = new HashMap<>();
for (RecipientSettings insert : inserts) {
if (insert.getGroupType() == RecipientDatabase.GroupType.SIGNAL_V2 && insert.getSyncExtras().getGroupMasterKey() == null) {
Log.w(TAG, "Missing master key on gv2 recipient");
continue;
}
SignalStorageRecord insertRecord = StorageSyncModels.localToRemoteRecord(insert);
storageInserts.add(insertRecord);
completeIds.add(insertRecord.getId());
}
if (accountInsert.isPresent()) {
storageInserts.add(SignalStorageRecord.forAccount(accountInsert.get()));
completeIds.add(accountInsert.get().getId());
}
for (RecipientSettings delete : deletes) {
byte[] key = Objects.requireNonNull(delete.getStorageId());
storageDeletes.add(ByteBuffer.wrap(key));
completeIds.removeIf(id -> Arrays.equals(id.getRaw(), key));
}
for (RecipientSettings update : updates) {
byte[] oldId = update.getStorageId();
byte[] newId = generateKey();
SignalStorageRecord insert = StorageSyncModels.localToRemoteRecord(update, newId);
storageInserts.add(insert);
storageDeletes.add(ByteBuffer.wrap(oldId));
completeIds.add(insert.getId());
completeIds.removeIf(id -> Arrays.equals(id.getRaw(), oldId));
storageKeyUpdates.put(update.getId(), newId);
}
if (accountUpdate.isPresent()) {
StorageId oldId = accountUpdate.get().getId();
StorageId newId = StorageId.forAccount(generateKey());
storageInserts.add(SignalStorageRecord.forAccount(newId, accountUpdate.get()));
storageDeletes.add(ByteBuffer.wrap(oldId.getRaw()));
completeIds.remove(oldId);
completeIds.add(newId);
storageKeyUpdates.put(Recipient.self().getId(), newId.getRaw());
}
if (storageInserts.isEmpty() && storageDeletes.isEmpty()) {
return Optional.absent();
} else {
List<byte[]> storageDeleteBytes = Stream.of(storageDeletes).map(ByteBuffer::array).toList();
SignalStorageManifest manifest = new SignalStorageManifest(currentManifestVersion + 1, new ArrayList<>(completeIds));
WriteOperationResult writeOperationResult = new WriteOperationResult(manifest, new ArrayList<>(storageInserts), storageDeleteBytes);
return Optional.of(new LocalWriteResult(writeOperationResult, storageKeyUpdates));
}
}
/**
* Given a list of all the local and remote keys you know about, this will return a result telling
* you which keys are exclusively remote and which are exclusively local.
@ -227,20 +110,6 @@ public final class StorageSyncHelper {
return !OptionalUtil.byteArrayEquals(update.getOld().getProfileKey(), update.getNew().getProfileKey());
}
public static Optional<SignalAccountRecord> getPendingAccountSyncUpdate(@NonNull Context context, @NonNull Recipient self) {
if (DatabaseFactory.getRecipientDatabase(context).getDirtyState(self.getId()) != RecipientDatabase.DirtyState.UPDATE) {
return Optional.absent();
}
return Optional.of(buildAccountRecord(context, self).getAccount().get());
}
public static Optional<SignalAccountRecord> getPendingAccountSyncInsert(@NonNull Context context, @NonNull Recipient self) {
if (DatabaseFactory.getRecipientDatabase(context).getDirtyState(self.getId()) != RecipientDatabase.DirtyState.INSERT) {
return Optional.absent();
}
return Optional.of(buildAccountRecord(context, self).getAccount().get());
}
public static SignalStorageRecord buildAccountRecord(@NonNull Context context, @NonNull Recipient self) {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
RecipientSettings settings = recipientDatabase.getRecipientSettingsForSync(self.getId());
@ -296,7 +165,7 @@ public final class StorageSyncHelper {
}
public static void scheduleRoutineSync() {
long timeSinceLastSync = System.currentTimeMillis() - SignalStore.storageServiceValues().getLastSyncTime();
long timeSinceLastSync = System.currentTimeMillis() - SignalStore.storageService().getLastSyncTime();
if (timeSinceLastSync > REFRESH_INTERVAL) {
Log.d(TAG, "Scheduling a sync. Last sync was " + timeSinceLastSync + " ms ago.");
@ -390,27 +259,4 @@ public final class StorageSyncHelper {
}
}
}
public static class LocalWriteResult {
private final WriteOperationResult writeResult;
private final Map<RecipientId, byte[]> storageKeyUpdates;
private LocalWriteResult(WriteOperationResult writeResult, Map<RecipientId, byte[]> storageKeyUpdates) {
this.writeResult = writeResult;
this.storageKeyUpdates = storageKeyUpdates;
}
public @NonNull WriteOperationResult getWriteResult() {
return writeResult;
}
public @NonNull Map<RecipientId, byte[]> getStorageKeyUpdates() {
return storageKeyUpdates;
}
}
private static final class MultipleExistingAccountsException extends IllegalArgumentException {}
private static final class InvalidAccountInsertException extends IllegalArgumentException {}
private static final class InvalidAccountUpdateException extends IllegalArgumentException {}
private static final class InvalidAccountDualInsertUpdateException extends IllegalArgumentException {}
}

View file

@ -30,7 +30,7 @@ public final class StorageSyncValidations {
private StorageSyncValidations() {}
public static void validate(@NonNull StorageSyncHelper.WriteOperationResult result,
@NonNull Optional<SignalStorageManifest> previousManifest,
@NonNull SignalStorageManifest previousManifest,
boolean forcePushPending,
@NonNull Recipient self)
{
@ -47,12 +47,12 @@ public final class StorageSyncValidations {
}
}
if (!previousManifest.isPresent()) {
Log.i(TAG, "No previous manifest, not bothering with additional validations around the diffs between the two manifests.");
if (previousManifest.getVersion() == 0) {
Log.i(TAG, "Previous manifest is empty, not bothering with additional validations around the diffs between the two manifests.");
return;
}
if (result.getManifest().getVersion() != previousManifest.get().getVersion() + 1) {
if (result.getManifest().getVersion() != previousManifest.getVersion() + 1) {
throw new IncorrectManifestVersionError();
}
@ -61,7 +61,7 @@ public final class StorageSyncValidations {
return;
}
Set<ByteBuffer> previousIds = Stream.of(previousManifest.get().getStorageIds()).map(id -> ByteBuffer.wrap(id.getRaw())).collect(Collectors.toSet());
Set<ByteBuffer> previousIds = Stream.of(previousManifest.getStorageIds()).map(id -> ByteBuffer.wrap(id.getRaw())).collect(Collectors.toSet());
Set<ByteBuffer> newIds = Stream.of(result.getManifest().getStorageIds()).map(id -> ByteBuffer.wrap(id.getRaw())).collect(Collectors.toSet());
Set<ByteBuffer> manifestInserts = SetUtil.difference(newIds, previousIds);

View file

@ -1224,10 +1224,6 @@ public class TextSecurePreferences {
setBooleanPreference(context, HAS_SEEN_VIDEO_RECORDING_TOOLTIP, value);
}
public static long getStorageManifestVersion(Context context) {
return getLongPreference(context, STORAGE_MANIFEST_VERSION, 0);
}
public static void setStorageManifestVersion(Context context, long version) {
setLongPreference(context, STORAGE_MANIFEST_VERSION, version);
}

View file

@ -1,16 +1,22 @@
package org.whispersystems.signalservice.api.storage;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
import org.whispersystems.signalservice.internal.storage.protos.StorageManifest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class SignalStorageManifest {
public static final SignalStorageManifest EMPTY = new SignalStorageManifest(0, Collections.emptyList());
private final long version;
private final List<StorageId> storageIds;
private final Map<Integer, List<StorageId>> storageIdsByType;
@ -30,6 +36,22 @@ public class SignalStorageManifest {
}
}
public static SignalStorageManifest deserialize(byte[] serialized) {
try {
StorageManifest manifest = StorageManifest.parseFrom(serialized);
ManifestRecord manifestRecord = ManifestRecord.parseFrom(manifest.getValue());
List<StorageId> ids = new ArrayList<>(manifestRecord.getIdentifiersCount());
for (ManifestRecord.Identifier id : manifestRecord.getIdentifiersList()) {
ids.add(StorageId.forType(id.getRaw().toByteArray(), id.getTypeValue()));
}
return new SignalStorageManifest(manifest.getVersion(), ids);
} catch (InvalidProtocolBufferException e) {
throw new AssertionError(e);
}
}
public long getVersion() {
return version;
}
@ -47,4 +69,23 @@ public class SignalStorageManifest {
return Optional.absent();
}
}
public byte[] serialize() {
List<ManifestRecord.Identifier> ids = new ArrayList<>(storageIds.size());
for (StorageId id : storageIds) {
ids.add(ManifestRecord.Identifier.newBuilder()
.setTypeValue(id.getType())
.setRaw(ByteString.copyFrom(id.getRaw()))
.build());
}
ManifestRecord manifestRecord = ManifestRecord.newBuilder().addAllIdentifiers(ids).build();
return StorageManifest.newBuilder()
.setVersion(version)
.setValue(manifestRecord.toByteString())
.build()
.toByteArray();
}
}