Add storage support for the AccountRecord.
This commit is contained in:
parent
7a038ab09d
commit
951a61117a
38 changed files with 1290 additions and 335 deletions
|
@ -71,6 +71,7 @@ import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager;
|
|||
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
@ -136,7 +137,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||
FeatureFlags.init();
|
||||
NotificationChannels.create(this);
|
||||
RefreshPreKeysJob.scheduleIfNecessary();
|
||||
StorageSyncJob.scheduleIfNecessary();
|
||||
StorageSyncHelper.scheduleRoutineSync();
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
|
||||
|
||||
if (Build.VERSION.SDK_INT < 21) {
|
||||
|
|
|
@ -77,6 +77,7 @@ import org.thoughtcrime.securesms.qr.ScanningThread;
|
|||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.DynamicDarkActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
@ -605,7 +606,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
|
|||
remoteIdentity,
|
||||
isChecked ? VerifiedStatus.VERIFIED :
|
||||
VerifiedStatus.DEFAULT));
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
|
||||
IdentityUtil.markIdentityVerified(getActivity(), recipient.get(), isChecked, false);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
|||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -21,11 +22,6 @@ public class DirectoryHelper {
|
|||
|
||||
@WorkerThread
|
||||
public static void refreshDirectory(@NonNull Context context, boolean notifyOfNewUsers) throws IOException {
|
||||
if (!SignalStore.storageServiceValues().hasFirstStorageSyncCompleted()) {
|
||||
Log.i(TAG, "First storage sync has not completed. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (FeatureFlags.uuids()) {
|
||||
// TODO [greyson] Create a DirectoryHelperV2 when appropriate.
|
||||
DirectoryHelperV1.refreshDirectory(context, notifyOfNewUsers);
|
||||
|
@ -33,7 +29,7 @@ public class DirectoryHelper {
|
|||
DirectoryHelperV1.refreshDirectory(context, notifyOfNewUsers);
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
@ -49,7 +45,7 @@ public class DirectoryHelper {
|
|||
}
|
||||
|
||||
if (newRegisteredState != originalRegisteredState) {
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
|
||||
return newRegisteredState;
|
||||
|
|
|
@ -39,6 +39,7 @@ import org.whispersystems.libsignal.InvalidKeyException;
|
|||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
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.StorageId;
|
||||
|
@ -216,7 +217,7 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
enum DirtyState {
|
||||
public enum DirtyState {
|
||||
CLEAN(0), UPDATE(1), INSERT(2), DELETE(3);
|
||||
|
||||
private final int id;
|
||||
|
@ -228,6 +229,10 @@ public class RecipientDatabase extends Database {
|
|||
int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public static DirtyState fromId(int id) {
|
||||
return values()[id];
|
||||
}
|
||||
}
|
||||
|
||||
public enum GroupType {
|
||||
|
@ -401,23 +406,35 @@ 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 @NonNull List<RecipientSettings> getPendingRecipientSyncUpdates() {
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.UPDATE.getId()) };
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.UPDATE.getId()), Recipient.self().getId().serialize() };
|
||||
|
||||
return getRecipientSettings(query, args);
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientSettings> getPendingRecipientSyncInsertions() {
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.INSERT.getId()) };
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.INSERT.getId()), Recipient.self().getId().serialize() };
|
||||
|
||||
return getRecipientSettings(query, args);
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientSettings> getPendingRecipientSyncDeletions() {
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.DELETE.getId()) };
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.DELETE.getId()), Recipient.self().getId().serialize() };
|
||||
|
||||
return getRecipientSettings(query, args);
|
||||
}
|
||||
|
@ -432,6 +449,10 @@ public class RecipientDatabase extends Database {
|
|||
return null;
|
||||
}
|
||||
|
||||
public void markNeedsSync(@NonNull RecipientId recipientId) {
|
||||
markDirty(recipientId, DirtyState.UPDATE);
|
||||
}
|
||||
|
||||
public void applyStorageIdUpdates(@NonNull Map<RecipientId, StorageId> storageIds) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
|
@ -459,6 +480,7 @@ public class RecipientDatabase extends Database {
|
|||
{
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
|
@ -492,9 +514,8 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
if (Recipient.self().getId().equals(recipientId)) {
|
||||
TextSecurePreferences.setProfileName(context, ProfileName.fromParts(insert.getGivenName().orNull(), insert.getFamilyName().orNull()));
|
||||
}
|
||||
threadDatabase.setArchived(recipientId, insert.isArchived());
|
||||
Recipient.live(recipientId).refresh();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -534,10 +555,18 @@ public class RecipientDatabase extends Database {
|
|||
} catch (InvalidKeyException e) {
|
||||
Log.w(TAG, "Failed to process identity key during update! Skipping.", e);
|
||||
}
|
||||
|
||||
threadDatabase.setArchived(recipientId, update.getNew().isArchived());
|
||||
Recipient.live(recipientId).refresh();
|
||||
}
|
||||
|
||||
for (SignalGroupV1Record insert : groupV1Inserts) {
|
||||
db.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV1(insert));
|
||||
|
||||
Recipient recipient = Recipient.externalGroup(context, GroupUtil.getEncodedId(insert.getGroupId(), false));
|
||||
|
||||
threadDatabase.setArchived(recipient.getId(), insert.isArchived());
|
||||
recipient.live().refresh();
|
||||
}
|
||||
|
||||
for (RecordUpdate<SignalGroupV1Record> update : groupV1Updates) {
|
||||
|
@ -547,6 +576,11 @@ public class RecipientDatabase extends Database {
|
|||
if (updateCount < 1) {
|
||||
throw new AssertionError("Had an update, but it didn't match any rows!");
|
||||
}
|
||||
|
||||
Recipient recipient = Recipient.externalGroup(context, GroupUtil.getEncodedId(update.getOld().getGroupId(), false));
|
||||
|
||||
threadDatabase.setArchived(recipient.getId(), update.getNew().isArchived());
|
||||
recipient.live().refresh();
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
|
@ -555,6 +589,27 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
public void applyStorageSyncUpdates(@NonNull StorageId storageId, SignalAccountRecord update) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
ProfileName profileName = ProfileName.fromParts(update.getGivenName().orNull(), update.getFamilyName().orNull());
|
||||
|
||||
values.put(PROFILE_GIVEN_NAME, profileName.getGivenName());
|
||||
values.put(PROFILE_FAMILY_NAME, profileName.getFamilyName());
|
||||
values.put(PROFILE_JOINED_NAME, profileName.toString());
|
||||
values.put(PROFILE_KEY, update.getProfileKey().transform(Base64::encodeBytes).orNull());
|
||||
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(update.getId().getRaw()));
|
||||
values.put(DIRTY, DirtyState.CLEAN.getId());
|
||||
|
||||
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(storageId.getRaw())});
|
||||
if (updateCount < 1) {
|
||||
throw new AssertionError("Account update didn't match any rows!");
|
||||
}
|
||||
|
||||
Recipient.self().live().refresh();
|
||||
}
|
||||
|
||||
public void updatePhoneNumbers(@NonNull Map<String, String> mapping) {
|
||||
if (mapping.isEmpty()) return;
|
||||
|
||||
|
@ -641,19 +696,19 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
|
||||
/**
|
||||
* @return All storage keys, excluding the ones that need to be deleted.
|
||||
* @return All storage ids for ContactRecords, excluding the ones that need to be deleted.
|
||||
*/
|
||||
public List<StorageId> getAllStorageSyncKeys() {
|
||||
return new ArrayList<>(getAllStorageSyncKeysMap().values());
|
||||
public List<StorageId> getContactStorageSyncIds() {
|
||||
return new ArrayList<>(getContactStorageSyncIdsMap().values());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return All storage keys, excluding the ones that need to be deleted.
|
||||
* @return All storage IDs for ContactRecords, excluding the ones that need to be deleted.
|
||||
*/
|
||||
public @NonNull Map<RecipientId, StorageId> getAllStorageSyncKeysMap() {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String query = STORAGE_SERVICE_ID + " NOT NULL AND " + DIRTY + " != ?";
|
||||
String[] args = new String[]{String.valueOf(DirtyState.DELETE)};
|
||||
public @NonNull Map<RecipientId, StorageId> getContactStorageSyncIdsMap() {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String query = STORAGE_SERVICE_ID + " NOT NULL AND " + DIRTY + " != ? AND " + ID + " != ?";
|
||||
String[] args = new String[]{String.valueOf(DirtyState.DELETE), Recipient.self().getId().serialize() };
|
||||
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)) {
|
||||
|
@ -920,7 +975,7 @@ public class RecipientDatabase extends Database {
|
|||
if (update(updateQuery, valuesToSet)) {
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
Recipient.live(id).refresh();
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -966,7 +1021,7 @@ public class RecipientDatabase extends Database {
|
|||
if (update(id, contentValues)) {
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
Recipient.live(id).refresh();
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -975,6 +1030,11 @@ public class RecipientDatabase extends Database {
|
|||
contentValues.put(SIGNAL_PROFILE_AVATAR, profileAvatar);
|
||||
if (update(id, contentValues)) {
|
||||
Recipient.live(id).refresh();
|
||||
|
||||
if (id.equals(Recipient.self().getId())) {
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -984,7 +1044,7 @@ public class RecipientDatabase extends Database {
|
|||
if (update(id, contentValues)) {
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
Recipient.live(id).refresh();
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1002,6 +1062,7 @@ public class RecipientDatabase extends Database {
|
|||
if (update(id, contentValues)) {
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
Recipient.live(id).refresh();
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1017,8 +1078,10 @@ public class RecipientDatabase extends Database {
|
|||
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(USERNAME, username);
|
||||
update(id, contentValues);
|
||||
Recipient.live(id).refresh();
|
||||
if (update(id, contentValues)) {
|
||||
Recipient.live(id).refresh();
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void clearUsernameIfExists(@NonNull String username) {
|
||||
|
@ -1601,7 +1664,7 @@ public class RecipientDatabase extends Database {
|
|||
private final Recipient.Capability uuidCapability;
|
||||
private final Recipient.Capability groupsV2Capability;
|
||||
private final InsightsBannerTier insightsBannerTier;
|
||||
private final byte[] storageKey;
|
||||
private final byte[] storageId;
|
||||
private final byte[] identityKey;
|
||||
private final IdentityDatabase.VerifiedStatus identityStatus;
|
||||
|
||||
|
@ -1637,7 +1700,7 @@ public class RecipientDatabase extends Database {
|
|||
Recipient.Capability uuidCapability,
|
||||
Recipient.Capability groupsV2Capability,
|
||||
@NonNull InsightsBannerTier insightsBannerTier,
|
||||
@Nullable byte[] storageKey,
|
||||
@Nullable byte[] storageId,
|
||||
@Nullable byte[] identityKey,
|
||||
@NonNull IdentityDatabase.VerifiedStatus identityStatus)
|
||||
{
|
||||
|
@ -1673,7 +1736,7 @@ public class RecipientDatabase extends Database {
|
|||
this.uuidCapability = uuidCapability;
|
||||
this.groupsV2Capability = groupsV2Capability;
|
||||
this.insightsBannerTier = insightsBannerTier;
|
||||
this.storageKey = storageKey;
|
||||
this.storageId = storageId;
|
||||
this.identityKey = identityKey;
|
||||
this.identityStatus = identityStatus;
|
||||
}
|
||||
|
@ -1806,8 +1869,8 @@ public class RecipientDatabase extends Database {
|
|||
return groupsV2Capability;
|
||||
}
|
||||
|
||||
public @Nullable byte[] getStorageKey() {
|
||||
return storageKey;
|
||||
public @Nullable byte[] getStorageId() {
|
||||
return storageId;
|
||||
}
|
||||
|
||||
public @Nullable byte[] getIdentityKey() {
|
||||
|
|
|
@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.mms.SlideDeck;
|
|||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
@ -53,6 +54,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
|||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
|
@ -422,10 +424,48 @@ public class ThreadDatabase extends Database {
|
|||
return getConversationList("1");
|
||||
}
|
||||
|
||||
public boolean isArchived(@NonNull RecipientId recipientId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String query = RECIPIENT_ID + " = ?";
|
||||
String[] args = new String[]{ recipientId.serialize() };
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[] { ARCHIVED }, query, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getInt(cursor.getColumnIndexOrThrow(ARCHIVED)) == 1;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setArchived(@NonNull RecipientId recipientId, boolean status) {
|
||||
setArchived(Collections.singletonMap(recipientId, status));
|
||||
}
|
||||
|
||||
public void setArchived(@NonNull Map<RecipientId, Boolean> status) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
String query = RECIPIENT_ID + " = ?";
|
||||
|
||||
for (Map.Entry<RecipientId, Boolean> entry : status.entrySet()) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(ARCHIVED, entry.getValue() ? "1" : "0");
|
||||
db.update(TABLE_NAME, values, query, new String[] { entry.getKey().serialize() });
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
notifyConversationListListeners();
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull Set<RecipientId> getArchivedRecipients() {
|
||||
Set<RecipientId> archived = new HashSet<>();
|
||||
|
||||
try (Cursor cursor = DatabaseFactory.getThreadDatabase(context).getArchivedConversationList()) {
|
||||
try (Cursor cursor = getArchivedConversationList()) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
archived.add(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.RECIPIENT_ID))));
|
||||
}
|
||||
|
@ -488,6 +528,12 @@ public class ThreadDatabase extends Database {
|
|||
|
||||
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId + ""});
|
||||
notifyConversationListListeners();
|
||||
|
||||
Recipient recipient = getRecipientForThreadId(threadId);
|
||||
if (recipient != null) {
|
||||
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipient.getId());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void unarchiveConversation(long threadId) {
|
||||
|
@ -497,6 +543,12 @@ public class ThreadDatabase extends Database {
|
|||
|
||||
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId + ""});
|
||||
notifyConversationListListeners();
|
||||
|
||||
Recipient recipient = getRecipientForThreadId(threadId);
|
||||
if (recipient != null) {
|
||||
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipient.getId());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void setLastSeen(long threadId) {
|
||||
|
|
|
@ -85,6 +85,7 @@ public final class JobManagerFactories {
|
|||
put(RefreshPreKeysJob.KEY, new RefreshPreKeysJob.Factory());
|
||||
put(RemoteConfigRefreshJob.KEY, new RemoteConfigRefreshJob.Factory());
|
||||
put(RequestGroupInfoJob.KEY, new RequestGroupInfoJob.Factory());
|
||||
put(StorageAccountRestoreJob.KEY, new StorageAccountRestoreJob.Factory());
|
||||
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory());
|
||||
put(RetrieveProfileJob.KEY, new RetrieveProfileJob.Factory());
|
||||
put(RotateCertificateJob.KEY, new RotateCertificateJob.Factory());
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
|
|||
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
|
@ -14,8 +15,11 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
|||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
|
||||
public final class ProfileUploadJob extends BaseJob {
|
||||
|
@ -55,6 +59,9 @@ public final class ProfileUploadJob extends BaseJob {
|
|||
accountManager.setProfileAvatar(profileKey, avatar);
|
||||
}
|
||||
}
|
||||
|
||||
ProfileAndCredential profile = ProfileUtil.retrieveProfile(context, Recipient.self(), SignalServiceProfile.RequestType.PROFILE);
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileAvatar(Recipient.self().getId(), profile.getProfile().getAvatar());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -72,6 +72,7 @@ import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
|
|||
import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
|
@ -665,7 +666,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
ApplicationDependencies.getJobManager().add(new RefreshOwnProfileJob());
|
||||
break;
|
||||
case STORAGE_MANIFEST:
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "Received a fetch message for an unknown type.");
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Restored the AccountRecord present in the storage service, if any. This will overwrite any local
|
||||
* data that is stored in AccountRecord, so this should only be done immediately after registration.
|
||||
*/
|
||||
public class StorageAccountRestoreJob extends BaseJob {
|
||||
|
||||
public static String KEY = "StorageAccountRestoreJob";
|
||||
|
||||
public static long LIFESPAN = TimeUnit.SECONDS.toMillis(10);
|
||||
|
||||
private static final String TAG = Log.tag(StorageAccountRestoreJob.class);
|
||||
|
||||
public StorageAccountRestoreJob() {
|
||||
this(new Parameters.Builder()
|
||||
.setQueue(StorageSyncJob.QUEUE_KEY)
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setMaxInstances(1)
|
||||
.setMaxAttempts(1)
|
||||
.setLifespan(LIFESPAN)
|
||||
.build());
|
||||
}
|
||||
|
||||
private StorageAccountRestoreJob(@NonNull Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return Data.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRun() throws Exception {
|
||||
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
StorageKey storageServiceKey = SignalStore.storageServiceValues().getOrCreateStorageMasterKey().deriveStorageServiceKey();
|
||||
|
||||
Optional<SignalStorageManifest> manifest = accountManager.getStorageManifest(storageServiceKey);
|
||||
|
||||
if (!manifest.isPresent()) {
|
||||
Log.w(TAG, "Manifest did not exist or was undecryptable (bad key). Not restoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<StorageId> accountId = manifest.get().getAccountStorageId();
|
||||
|
||||
if (!accountId.isPresent()) {
|
||||
Log.w(TAG, "Manifest had no account record! Not restoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
List<SignalStorageRecord> records = accountManager.readStorageRecords(storageServiceKey, Collections.singletonList(accountId.get()));
|
||||
SignalStorageRecord record = records.size() > 0 ? records.get(0) : null;
|
||||
|
||||
if (record == null) {
|
||||
Log.w(TAG, "Could not find account record, even though we had an ID! Not restoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
SignalAccountRecord accountRecord = record.getAccount().orNull();
|
||||
if (accountRecord == null) {
|
||||
Log.w(TAG, "The storage record didn't actually have an account on it! Not restoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
StorageId selfStorageId = StorageId.forAccount(Recipient.self().getStorageServiceId());
|
||||
StorageSyncHelper.applyAccountStorageSyncUpdates(context, selfStorageId, accountRecord);
|
||||
|
||||
if (accountRecord.getAvatarUrlPath().isPresent()) {
|
||||
RetrieveProfileAvatarJob avatarJob = new RetrieveProfileAvatarJob(Recipient.self(), accountRecord.getAvatarUrlPath().get());
|
||||
try {
|
||||
avatarJob.setContext(context);
|
||||
avatarJob.onRun();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to download avatar. Scheduling for later.");
|
||||
ApplicationDependencies.getJobManager().add(avatarJob);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(@NonNull Exception e) {
|
||||
return e instanceof PushNetworkException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<StorageAccountRestoreJob> {
|
||||
@Override
|
||||
public @NonNull
|
||||
StorageAccountRestoreJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new StorageAccountRestoreJob(parameters);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,8 @@ import androidx.annotation.NonNull;
|
|||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncModels;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
|
@ -34,6 +36,7 @@ import java.util.List;
|
|||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
|
@ -76,15 +79,17 @@ public class StorageForcePushJob extends BaseJob {
|
|||
StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context);
|
||||
|
||||
long currentVersion = accountManager.getStorageManifestVersion();
|
||||
Map<RecipientId, StorageId> oldStorageKeys = recipientDatabase.getAllStorageSyncKeysMap();
|
||||
Map<RecipientId, StorageId> oldStorageKeys = recipientDatabase.getContactStorageSyncIdsMap();
|
||||
|
||||
long newVersion = currentVersion + 1;
|
||||
Map<RecipientId, StorageId> newStorageKeys = generateNewKeys(oldStorageKeys);
|
||||
List<SignalStorageRecord> inserts = Stream.of(oldStorageKeys.keySet())
|
||||
.map(recipientDatabase::getRecipientSettings)
|
||||
.withoutNulls()
|
||||
.map(s -> StorageSyncModels.localToRemoteRecord(s, Objects.requireNonNull(newStorageKeys.get(s.getId())).getRaw()))
|
||||
.toList();
|
||||
long newVersion = currentVersion + 1;
|
||||
Map<RecipientId, StorageId> newStorageKeys = generateNewKeys(oldStorageKeys);
|
||||
Set<RecipientId> archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
|
||||
List<SignalStorageRecord> inserts = Stream.of(oldStorageKeys.keySet())
|
||||
.map(recipientDatabase::getRecipientSettings)
|
||||
.withoutNulls()
|
||||
.map(s -> StorageSyncModels.localToRemoteRecord(s, Objects.requireNonNull(newStorageKeys.get(s.getId())).getRaw(), archivedRecipients))
|
||||
.toList();
|
||||
inserts.add(StorageSyncHelper.buildAccountRecord(context, StorageId.forAccount(Recipient.self().fresh().getStorageServiceId())));
|
||||
|
||||
SignalStorageManifest manifest = new SignalStorageManifest(newVersion, new ArrayList<>(newStorageKeys.values()));
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
|
|||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.KeyDifferenceResult;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.LocalWriteResult;
|
||||
|
@ -23,6 +24,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
|||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncValidations;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
@ -30,16 +32,20 @@ import org.thoughtcrime.securesms.util.Util;
|
|||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
|
@ -56,27 +62,14 @@ public class StorageSyncJob extends BaseJob {
|
|||
|
||||
private static final String TAG = Log.tag(StorageSyncJob.class);
|
||||
|
||||
private static final long REFRESH_INTERVAL = TimeUnit.HOURS.toMillis(2);
|
||||
|
||||
public StorageSyncJob() {
|
||||
this(new Job.Parameters.Builder().addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(QUEUE_KEY)
|
||||
.setMaxInstances(1)
|
||||
.setMaxInstances(2)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.build());
|
||||
}
|
||||
|
||||
public static void scheduleIfNecessary() {
|
||||
long timeSinceLastSync = System.currentTimeMillis() - SignalStore.storageServiceValues().getLastSyncTime();
|
||||
|
||||
if (timeSinceLastSync > REFRESH_INTERVAL) {
|
||||
Log.d(TAG, "Scheduling a sync. Last sync was " + timeSinceLastSync + " ms ago.");
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
} else {
|
||||
Log.d(TAG, "No need for sync. Last sync was " + timeSinceLastSync + " ms ago.");
|
||||
}
|
||||
}
|
||||
|
||||
private StorageSyncJob(@NonNull Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
@ -98,6 +91,11 @@ public class StorageSyncJob extends BaseJob {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!TextSecurePreferences.isPushRegistered(context)) {
|
||||
Log.i(TAG, "Not registered. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
boolean needsMultiDeviceSync = performSync();
|
||||
|
||||
|
@ -113,11 +111,6 @@ public class StorageSyncJob extends BaseJob {
|
|||
.then(new StorageForcePushJob())
|
||||
.then(new MultiDeviceStorageSyncRequestJob())
|
||||
.enqueue();
|
||||
} finally {
|
||||
if (!SignalStore.storageServiceValues().hasFirstStorageSyncCompleted()) {
|
||||
SignalStore.storageServiceValues().setFirstStorageSyncCompleted(true);
|
||||
ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,17 +139,20 @@ public class StorageSyncJob extends BaseJob {
|
|||
if (remoteManifest.isPresent() && remoteManifestVersion > localManifestVersion) {
|
||||
Log.i(TAG, "[Remote Newer] Newer manifest version found!");
|
||||
|
||||
List<StorageId> allLocalStorageKeys = getAllLocalStorageKeys(context);
|
||||
KeyDifferenceResult keyDifference = StorageSyncHelper.findKeyDifference(remoteManifest.get().getStorageIds(), allLocalStorageKeys);
|
||||
List<StorageId> allLocalStorageKeys = getAllLocalStorageIds(context);
|
||||
KeyDifferenceResult keyDifference = StorageSyncHelper.findKeyDifference(remoteManifest.get().getStorageIds(), allLocalStorageKeys);
|
||||
|
||||
if (!keyDifference.isEmpty()) {
|
||||
Log.i(TAG, "[Remote Newer] There's a difference in keys. Local-only: " + keyDifference.getLocalOnlyKeys().size() + ", Remote-only: " + keyDifference.getRemoteOnlyKeys().size());
|
||||
|
||||
List<SignalStorageRecord> localOnly = buildLocalStorageRecords(context, keyDifference.getLocalOnlyKeys());
|
||||
Set<RecipientId> archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
|
||||
List<SignalStorageRecord> localOnly = buildLocalStorageRecords(context, keyDifference.getLocalOnlyKeys(), archivedRecipients);
|
||||
List<SignalStorageRecord> remoteOnly = accountManager.readStorageRecords(storageServiceKey, keyDifference.getRemoteOnlyKeys());
|
||||
MergeResult mergeResult = StorageSyncHelper.resolveConflict(remoteOnly, localOnly);
|
||||
WriteOperationResult writeOperationResult = StorageSyncHelper.createWriteOperation(remoteManifest.get().getVersion(), allLocalStorageKeys, mergeResult);
|
||||
|
||||
StorageSyncValidations.validate(writeOperationResult);
|
||||
|
||||
Log.i(TAG, "[Remote Newer] MergeResult :: " + mergeResult);
|
||||
|
||||
if (!writeOperationResult.isEmpty()) {
|
||||
|
@ -182,6 +178,7 @@ public class StorageSyncJob extends BaseJob {
|
|||
|
||||
recipientDatabase.applyStorageSyncUpdates(mergeResult.getLocalContactInserts(), mergeResult.getLocalContactUpdates(), mergeResult.getLocalGroupV1Inserts(), mergeResult.getLocalGroupV1Updates());
|
||||
storageKeyDatabase.applyStorageSyncUpdates(mergeResult.getLocalUnknownInserts(), mergeResult.getLocalUnknownDeletes());
|
||||
StorageSyncHelper.applyAccountStorageSyncUpdates(context, mergeResult.getLocalAccountUpdate());
|
||||
needsMultiDeviceSync = true;
|
||||
|
||||
Log.i(TAG, "[Remote Newer] Updating local manifest version to: " + remoteManifestVersion);
|
||||
|
@ -195,20 +192,27 @@ public class StorageSyncJob extends BaseJob {
|
|||
|
||||
localManifestVersion = TextSecurePreferences.getStorageManifestVersion(context);
|
||||
|
||||
List<StorageId> allLocalStorageKeys = recipientDatabase.getAllStorageSyncKeys();
|
||||
List<RecipientSettings> pendingUpdates = recipientDatabase.getPendingRecipientSyncUpdates();
|
||||
List<RecipientSettings> pendingInsertions = recipientDatabase.getPendingRecipientSyncInsertions();
|
||||
List<RecipientSettings> pendingDeletions = recipientDatabase.getPendingRecipientSyncDeletions();
|
||||
Optional<LocalWriteResult> localWriteResult = StorageSyncHelper.buildStorageUpdatesForLocal(localManifestVersion,
|
||||
allLocalStorageKeys,
|
||||
pendingUpdates,
|
||||
pendingInsertions,
|
||||
pendingDeletions);
|
||||
List<StorageId> allLocalStorageKeys = getAllLocalStorageIds(context);
|
||||
List<RecipientSettings> pendingUpdates = recipientDatabase.getPendingRecipientSyncUpdates();
|
||||
List<RecipientSettings> pendingInsertions = recipientDatabase.getPendingRecipientSyncInsertions();
|
||||
List<RecipientSettings> pendingDeletions = recipientDatabase.getPendingRecipientSyncDeletions();
|
||||
Optional<SignalAccountRecord> pendingAccountUpdate = StorageSyncHelper.getPendingAccountSyncUpdate(context);
|
||||
Optional<SignalAccountRecord> pendingAccountInsert = StorageSyncHelper.getPendingAccountSyncInsert(context);
|
||||
Set<RecipientId> archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
|
||||
Optional<LocalWriteResult> localWriteResult = StorageSyncHelper.buildStorageUpdatesForLocal(localManifestVersion,
|
||||
allLocalStorageKeys,
|
||||
pendingUpdates,
|
||||
pendingInsertions,
|
||||
pendingDeletions,
|
||||
pendingAccountUpdate,
|
||||
pendingAccountInsert,
|
||||
archivedRecipients);
|
||||
|
||||
if (localWriteResult.isPresent()) {
|
||||
Log.i(TAG, String.format(Locale.ENGLISH, "[Local Changes] Local changes present. %d updates, %d inserts, %d deletes.", pendingUpdates.size(), pendingInsertions.size(), pendingDeletions.size()));
|
||||
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()));
|
||||
|
||||
WriteOperationResult localWrite = localWriteResult.get().getWriteResult();
|
||||
StorageSyncValidations.validate(localWrite);
|
||||
|
||||
Log.i(TAG, "[Local Changes] WriteOperationResult :: " + localWrite);
|
||||
|
||||
|
@ -216,18 +220,19 @@ public class StorageSyncJob extends BaseJob {
|
|||
throw new AssertionError("Decided there were local writes, but our write result was empty!");
|
||||
}
|
||||
|
||||
Optional<SignalStorageManifest> conflict = accountManager.writeStorageRecords(storageServiceKey, localWrite.getManifest(), localWrite.getInserts(), localWrite.getDeletes());
|
||||
Optional<SignalStorageManifest> conflict = accountManager.writeStorageRecords(storageServiceKey, localWrite.getManifest(), localWrite.getInserts(), localWrite.getDeletes());
|
||||
|
||||
if (conflict.isPresent()) {
|
||||
Log.w(TAG, "[Local Changes] Hit a conflict when trying to upload our local writes! Retrying.");
|
||||
throw new RetryLaterException();
|
||||
}
|
||||
|
||||
List<RecipientId> clearIds = new ArrayList<>(pendingUpdates.size() + pendingInsertions.size() + pendingDeletions.size());
|
||||
List<RecipientId> clearIds = new ArrayList<>(pendingUpdates.size() + pendingInsertions.size() + pendingDeletions.size() + 1);
|
||||
|
||||
clearIds.addAll(Stream.of(pendingUpdates).map(RecipientSettings::getId).toList());
|
||||
clearIds.addAll(Stream.of(pendingInsertions).map(RecipientSettings::getId).toList());
|
||||
clearIds.addAll(Stream.of(pendingDeletions).map(RecipientSettings::getId).toList());
|
||||
clearIds.add(Recipient.self().getId());
|
||||
|
||||
recipientDatabase.clearDirtyState(clearIds);
|
||||
recipientDatabase.updateStorageKeys(localWriteResult.get().getStorageKeyUpdates());
|
||||
|
@ -243,22 +248,44 @@ public class StorageSyncJob extends BaseJob {
|
|||
return needsMultiDeviceSync;
|
||||
}
|
||||
|
||||
private static @NonNull List<StorageId> getAllLocalStorageKeys(@NonNull Context context) {
|
||||
return Util.concatenatedList(DatabaseFactory.getRecipientDatabase(context).getAllStorageSyncKeys(),
|
||||
private static @NonNull List<StorageId> getAllLocalStorageIds(@NonNull Context context) {
|
||||
Recipient self = Recipient.self().fresh();
|
||||
|
||||
return Util.concatenatedList(DatabaseFactory.getRecipientDatabase(context).getContactStorageSyncIds(),
|
||||
Collections.singletonList(StorageId.forAccount(self.getStorageServiceId())),
|
||||
DatabaseFactory.getStorageKeyDatabase(context).getAllKeys());
|
||||
}
|
||||
|
||||
private static @NonNull List<SignalStorageRecord> buildLocalStorageRecords(@NonNull Context context, @NonNull List<StorageId> ids) {
|
||||
private static @NonNull List<SignalStorageRecord> buildLocalStorageRecords(@NonNull Context context, @NonNull List<StorageId> ids, @NonNull Set<RecipientId> archivedRecipients) {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context);
|
||||
|
||||
List<SignalStorageRecord> records = new ArrayList<>(ids.size());
|
||||
|
||||
for (StorageId id : ids) {
|
||||
SignalStorageRecord record = Optional.fromNullable(recipientDatabase.getByStorageId(id.getRaw()))
|
||||
.transform(StorageSyncModels::localToRemoteRecord)
|
||||
.or(() -> storageKeyDatabase.getById(id.getRaw()));
|
||||
records.add(record);
|
||||
switch (id.getType()) {
|
||||
case ManifestRecord.Identifier.Type.CONTACT_VALUE:
|
||||
case ManifestRecord.Identifier.Type.GROUPV1_VALUE:
|
||||
case ManifestRecord.Identifier.Type.GROUPV2_VALUE:
|
||||
RecipientSettings settings = recipientDatabase.getByStorageId(id.getRaw());
|
||||
if (settings != null) {
|
||||
records.add(StorageSyncModels.localToRemoteRecord(settings, archivedRecipients));
|
||||
} else {
|
||||
Log.w(TAG, "Missing local recipient model! Type: " + id.getType());
|
||||
}
|
||||
break;
|
||||
case ManifestRecord.Identifier.Type.ACCOUNT_VALUE:
|
||||
records.add(StorageSyncHelper.buildAccountRecord(context, id));
|
||||
break;
|
||||
default:
|
||||
SignalStorageRecord unknown = storageKeyDatabase.getById(id.getRaw());
|
||||
if (unknown != null) {
|
||||
records.add(unknown);
|
||||
} else {
|
||||
Log.w(TAG, "Missing local unknown model! Type: " + id.getType());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return records;
|
||||
|
|
|
@ -18,7 +18,6 @@ public final class SignalStore {
|
|||
|
||||
public static void onFirstEverAppLaunch() {
|
||||
registrationValues().onFirstEverAppLaunch();
|
||||
storageServiceValues().setFirstStorageSyncCompleted(false);
|
||||
}
|
||||
|
||||
public static @NonNull KbsValues kbsValues() {
|
||||
|
|
|
@ -9,9 +9,8 @@ import java.security.SecureRandom;
|
|||
|
||||
public class StorageServiceValues {
|
||||
|
||||
private static final String STORAGE_MASTER_KEY = "storage.storage_master_key";
|
||||
private static final String FIRST_STORAGE_SYNC_COMPLETED = "storage.first_storage_sync_completed";
|
||||
private static final String LAST_SYNC_TIME = "storage.last_sync_time";
|
||||
private static final String STORAGE_MASTER_KEY = "storage.storage_master_key";
|
||||
private static final String LAST_SYNC_TIME = "storage.last_sync_time";
|
||||
|
||||
private final KeyValueStore store;
|
||||
|
||||
|
@ -38,14 +37,6 @@ public class StorageServiceValues {
|
|||
.commit();
|
||||
}
|
||||
|
||||
public boolean hasFirstStorageSyncCompleted() {
|
||||
return !FeatureFlags.storageServiceRestore() || store.getBoolean(FIRST_STORAGE_SYNC_COMPLETED, true);
|
||||
}
|
||||
|
||||
public void setFirstStorageSyncCompleted(boolean completed) {
|
||||
store.beginWrite().putBoolean(FIRST_STORAGE_SYNC_COMPLETED, completed).apply();
|
||||
}
|
||||
|
||||
public long getLastSyncTime() {
|
||||
return store.getLong(LAST_SYNC_TIME, 0);
|
||||
}
|
||||
|
|
|
@ -20,16 +20,23 @@ import org.thoughtcrime.securesms.PassphraseChangeActivity;
|
|||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.database.Database;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.lock.RegistrationLockDialog;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||
import org.thoughtcrime.securesms.lock.v2.PinUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
@ -221,12 +228,16 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
|||
private class ReadReceiptToggleListener implements Preference.OnPreferenceChangeListener {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
boolean enabled = (boolean)newValue;
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(enabled,
|
||||
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
|
||||
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
boolean enabled = (boolean)newValue;
|
||||
DatabaseFactory.getRecipientDatabase(getContext()).markNeedsSync(Recipient.self().getId());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(enabled,
|
||||
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
|
||||
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
||||
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -234,16 +245,19 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
|||
private class TypingIndicatorsToggleListener implements Preference.OnPreferenceChangeListener {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
boolean enabled = (boolean)newValue;
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
|
||||
enabled,
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
|
||||
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
||||
|
||||
if (!enabled) {
|
||||
ApplicationContext.getInstance(requireContext()).getTypingStatusRepository().clear();
|
||||
}
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
boolean enabled = (boolean)newValue;
|
||||
DatabaseFactory.getRecipientDatabase(getContext()).markNeedsSync(Recipient.self().getId());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
|
||||
enabled,
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
|
||||
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
||||
|
||||
if (!enabled) {
|
||||
ApplicationContext.getInstance(requireContext()).getTypingStatusRepository().clear();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -251,12 +265,15 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
|||
private class LinkPreviewToggleListener implements Preference.OnPreferenceChangeListener {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
boolean enabled = (boolean)newValue;
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
|
||||
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(requireContext()),
|
||||
enabled));
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
boolean enabled = (boolean)newValue;
|
||||
DatabaseFactory.getRecipientDatabase(getContext()).markNeedsSync(Recipient.self().getId());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
|
||||
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(requireContext()),
|
||||
enabled));
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -355,10 +372,14 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
|||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
boolean enabled = (boolean) newValue;
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(getContext()),
|
||||
TextSecurePreferences.isTypingIndicatorsEnabled(getContext()),
|
||||
enabled,
|
||||
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
DatabaseFactory.getRecipientDatabase(getContext()).markNeedsSync(Recipient.self().getId());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(getContext()),
|
||||
TextSecurePreferences.isTypingIndicatorsEnabled(getContext()),
|
||||
enabled,
|
||||
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -93,7 +93,7 @@ public class Recipient {
|
|||
private final Capability uuidCapability;
|
||||
private final Capability groupsV2Capability;
|
||||
private final InsightsBannerTier insightsBannerTier;
|
||||
private final byte[] storageKey;
|
||||
private final byte[] storageId;
|
||||
private final byte[] identityKey;
|
||||
private final VerifiedStatus identityStatus;
|
||||
|
||||
|
@ -326,7 +326,7 @@ public class Recipient {
|
|||
this.forceSmsSelection = false;
|
||||
this.uuidCapability = Capability.UNKNOWN;
|
||||
this.groupsV2Capability = Capability.UNKNOWN;
|
||||
this.storageKey = null;
|
||||
this.storageId = null;
|
||||
this.identityKey = null;
|
||||
this.identityStatus = VerifiedStatus.DEFAULT;
|
||||
}
|
||||
|
@ -367,7 +367,7 @@ public class Recipient {
|
|||
this.forceSmsSelection = details.forceSmsSelection;
|
||||
this.uuidCapability = details.uuidCapability;
|
||||
this.groupsV2Capability = details.groupsV2Capability;
|
||||
this.storageKey = details.storageKey;
|
||||
this.storageId = details.storageId;
|
||||
this.identityKey = details.identityKey;
|
||||
this.identityStatus = details.identityStatus;
|
||||
}
|
||||
|
@ -706,8 +706,8 @@ public class Recipient {
|
|||
return profileKeyCredential != null;
|
||||
}
|
||||
|
||||
public @Nullable byte[] getStorageServiceKey() {
|
||||
return storageKey;
|
||||
public @Nullable byte[] getStorageServiceId() {
|
||||
return storageId;
|
||||
}
|
||||
|
||||
public @NonNull VerifiedStatus getIdentityVerifiedStatus() {
|
||||
|
|
|
@ -58,7 +58,7 @@ public class RecipientDetails {
|
|||
final Recipient.Capability uuidCapability;
|
||||
final Recipient.Capability groupsV2Capability;
|
||||
final InsightsBannerTier insightsBannerTier;
|
||||
final byte[] storageKey;
|
||||
final byte[] storageId;
|
||||
final byte[] identityKey;
|
||||
final VerifiedStatus identityStatus;
|
||||
|
||||
|
@ -103,7 +103,7 @@ public class RecipientDetails {
|
|||
this.uuidCapability = settings.getUuidCapability();
|
||||
this.groupsV2Capability = settings.getGroupsV2Capability();
|
||||
this.insightsBannerTier = settings.getInsightsBannerTier();
|
||||
this.storageKey = settings.getStorageKey();
|
||||
this.storageId = settings.getStorageId();
|
||||
this.identityKey = settings.getIdentityKey();
|
||||
this.identityStatus = settings.getIdentityStatus();
|
||||
|
||||
|
@ -149,7 +149,7 @@ public class RecipientDetails {
|
|||
this.name = null;
|
||||
this.uuidCapability = Recipient.Capability.UNKNOWN;
|
||||
this.groupsV2Capability = Recipient.Capability.UNKNOWN;
|
||||
this.storageKey = null;
|
||||
this.storageId = null;
|
||||
this.identityKey = null;
|
||||
this.identityStatus = VerifiedStatus.DEFAULT;
|
||||
}
|
||||
|
|
|
@ -20,8 +20,8 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
|
|||
import org.thoughtcrime.securesms.jobs.MultiDeviceMessageRequestResponseJob;
|
||||
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
|
@ -87,7 +87,7 @@ public class RecipientUtil {
|
|||
}
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob());
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
@ -98,7 +98,7 @@ public class RecipientUtil {
|
|||
|
||||
DatabaseFactory.getRecipientDatabase(context).setBlocked(recipient.getId(), false);
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob());
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
|
||||
if (FeatureFlags.messageRequests()) {
|
||||
ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forAccept(recipient.getId()));
|
||||
|
|
|
@ -21,7 +21,7 @@ import com.dd.CircularProgressButton;
|
|||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
@ -308,14 +308,14 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
|
|||
if (FeatureFlags.storageServiceRestore()) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
SimpleTask.run(() -> {
|
||||
return ApplicationDependencies.getJobManager().runSynchronously(new StorageSyncJob(), TimeUnit.SECONDS.toMillis(10));
|
||||
return ApplicationDependencies.getJobManager().runSynchronously(new StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN);
|
||||
}, result -> {
|
||||
long elapsedTime = System.currentTimeMillis() - startTime;
|
||||
|
||||
if (result.isPresent()) {
|
||||
Log.i(TAG, "Storage Service restore completed: " + result.get().name() + ". (Took " + elapsedTime + " ms)");
|
||||
Log.i(TAG, "Storage Service account restore completed: " + result.get().name() + ". (Took " + elapsedTime + " ms)");
|
||||
} else {
|
||||
Log.i(TAG, "Storage Service restore failed to complete in the allotted time. (" + elapsedTime + " ms elapsed)");
|
||||
Log.i(TAG, "Storage Service account restore failed to complete in the allotted time. (" + elapsedTime + " ms elapsed)");
|
||||
}
|
||||
cancelSpinning(pinButton);
|
||||
Navigation.findNavController(requireView()).navigate(RegistrationLockFragmentDirections.actionSuccessfulRegistration());
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
package org.thoughtcrime.securesms.storage;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
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.internal.storage.protos.ContactRecord.IdentityState;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
class AccountConflictMerger implements StorageSyncHelper.ConflictMerger<SignalAccountRecord> {
|
||||
|
||||
private static final String TAG = Log.tag(AccountConflictMerger.class);
|
||||
|
||||
private final Optional<SignalAccountRecord> local;
|
||||
|
||||
AccountConflictMerger(Optional<SignalAccountRecord> local) {
|
||||
this.local = local;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Optional<SignalAccountRecord> getMatching(@NonNull SignalAccountRecord record) {
|
||||
return local;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Collection<SignalAccountRecord> getInvalidEntries(@NonNull Collection<SignalAccountRecord> remoteRecords) {
|
||||
Set<SignalAccountRecord> invalid = new HashSet<>(remoteRecords);
|
||||
if (remoteRecords.size() > 0) {
|
||||
invalid.remove(remoteRecords.iterator().next());
|
||||
}
|
||||
|
||||
if (invalid.size() > 0) {
|
||||
Log.w(TAG, "Found invalid account entries! Count: " + invalid.size());
|
||||
}
|
||||
|
||||
return invalid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull SignalAccountRecord merge(@NonNull SignalAccountRecord remote, @NonNull SignalAccountRecord local, @NonNull StorageSyncHelper.KeyGenerator keyGenerator) {
|
||||
String givenName;
|
||||
String familyName;
|
||||
|
||||
if (remote.getGivenName().isPresent() || remote.getFamilyName().isPresent()) {
|
||||
givenName = remote.getGivenName().or("");
|
||||
familyName = remote.getFamilyName().or("");
|
||||
} else {
|
||||
givenName = local.getGivenName().or("");
|
||||
familyName = local.getFamilyName().or("");
|
||||
}
|
||||
|
||||
String avatarUrlPath = remote.getAvatarUrlPath().or(local.getAvatarUrlPath()).or("");
|
||||
byte[] profileKey = remote.getProfileKey().or(local.getProfileKey()).orNull();
|
||||
boolean noteToSelfArchived = remote.isNoteToSelfArchived();
|
||||
boolean readReceipts = remote.isReadReceiptsEnabled();
|
||||
boolean typingIndicators = remote.isTypingIndicatorsEnabled();
|
||||
boolean sealedSenderIndicators = remote.isSealedSenderIndicatorsEnabled();
|
||||
boolean linkPreviews = remote.isLinkPreviewsEnabled();
|
||||
boolean matchesRemote = doParamsMatch(remote, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews);
|
||||
boolean matchesLocal = doParamsMatch(local, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews);
|
||||
|
||||
if (matchesRemote) {
|
||||
return remote;
|
||||
} else if (matchesLocal) {
|
||||
return local;
|
||||
} else {
|
||||
return new SignalAccountRecord.Builder(keyGenerator.generate())
|
||||
.setGivenName(givenName)
|
||||
.setFamilyName(familyName)
|
||||
.setAvatarUrlPath(avatarUrlPath)
|
||||
.setProfileKey(profileKey)
|
||||
.setNoteToSelfArchived(noteToSelfArchived)
|
||||
.setReadReceiptsEnabled(readReceipts)
|
||||
.setTypingIndicatorsEnabled(typingIndicators)
|
||||
.setSealedSenderIndicatorsEnabled(sealedSenderIndicators)
|
||||
.setLinkPreviewsEnabled(linkPreviews)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean doParamsMatch(@NonNull SignalAccountRecord contact,
|
||||
@NonNull String givenName,
|
||||
@NonNull String familyName,
|
||||
@NonNull String avatarUrlPath,
|
||||
@Nullable byte[] profileKey,
|
||||
boolean noteToSelfArchived,
|
||||
boolean readReceipts,
|
||||
boolean typingIndicators,
|
||||
boolean sealedSenderIndicators,
|
||||
boolean linkPreviewsEnabled)
|
||||
{
|
||||
return Objects.equals(contact.getGivenName().or(""), givenName) &&
|
||||
Objects.equals(contact.getFamilyName().or(""), familyName) &&
|
||||
Objects.equals(contact.getAvatarUrlPath().or(""), avatarUrlPath) &&
|
||||
Arrays.equals(contact.getProfileKey().orNull(), profileKey) &&
|
||||
contact.isNoteToSelfArchived() == noteToSelfArchived &&
|
||||
contact.isReadReceiptsEnabled() == readReceipts &&
|
||||
contact.isTypingIndicatorsEnabled() == typingIndicators &&
|
||||
contact.isSealedSenderIndicatorsEnabled() == sealedSenderIndicators &&
|
||||
contact.isLinkPreviewsEnabled() == linkPreviewsEnabled;
|
||||
}
|
||||
}
|
|
@ -3,6 +3,10 @@ package org.thoughtcrime.securesms.storage;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
|
@ -11,16 +15,21 @@ import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.Id
|
|||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
class ContactConflictMerger implements StorageSyncHelper.ConflictMerger<SignalContactRecord> {
|
||||
|
||||
private static final String TAG = Log.tag(ContactConflictMerger.class);
|
||||
|
||||
private final Map<UUID, SignalContactRecord> localByUuid = new HashMap<>();
|
||||
private final Map<String, SignalContactRecord> localByE164 = new HashMap<>();
|
||||
|
||||
ContactConflictMerger(@NonNull Collection<SignalContactRecord> localOnly) {
|
||||
private final Recipient self;
|
||||
|
||||
ContactConflictMerger(@NonNull Collection<SignalContactRecord> localOnly, @NonNull Recipient self) {
|
||||
for (SignalContactRecord contact : localOnly) {
|
||||
if (contact.getAddress().getUuid().isPresent()) {
|
||||
localByUuid.put(contact.getAddress().getUuid().get(), contact);
|
||||
|
@ -29,6 +38,8 @@ class ContactConflictMerger implements StorageSyncHelper.ConflictMerger<SignalCo
|
|||
localByE164.put(contact.getAddress().getNumber().get(), contact);
|
||||
}
|
||||
}
|
||||
|
||||
this.self = self.resolve();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -39,6 +50,18 @@ class ContactConflictMerger implements StorageSyncHelper.ConflictMerger<SignalCo
|
|||
return Optional.fromNullable(localUuid).or(Optional.fromNullable(localE164));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Collection<SignalContactRecord> getInvalidEntries(@NonNull Collection<SignalContactRecord> remoteRecords) {
|
||||
List<SignalContactRecord> invalid = Stream.of(remoteRecords)
|
||||
.filter(r -> r.getAddress().getUuid().equals(self.getUuid()) || r.getAddress().getNumber().equals(self.getE164()))
|
||||
.toList();
|
||||
if (invalid.size() > 0) {
|
||||
Log.w(TAG, "Found invalid contact entries! Count: " + invalid.size());
|
||||
}
|
||||
|
||||
return invalid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull SignalContactRecord merge(@NonNull SignalContactRecord remote, @NonNull SignalContactRecord local, @NonNull StorageSyncHelper.KeyGenerator keyGenerator) {
|
||||
String givenName;
|
||||
|
|
|
@ -10,6 +10,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
|||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
class GroupV1ConflictMerger implements StorageSyncHelper.ConflictMerger<SignalGroupV1Record> {
|
||||
|
@ -25,6 +26,11 @@ class GroupV1ConflictMerger implements StorageSyncHelper.ConflictMerger<SignalGr
|
|||
return Optional.fromNullable(localByGroupId.get(GroupUtil.getEncodedId(record.getGroupId(), false)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Collection<SignalGroupV1Record> getInvalidEntries(@NonNull Collection<SignalGroupV1Record> remoteRecords) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull SignalGroupV1Record merge(@NonNull SignalGroupV1Record remote, @NonNull SignalGroupV1Record local, @NonNull StorageSyncHelper.KeyGenerator keyGenerator) {
|
||||
boolean blocked = remote.isBlocked();
|
||||
|
|
|
@ -10,6 +10,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
|||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
class GroupV2ConflictMerger implements StorageSyncHelper.ConflictMerger<SignalGroupV2Record> {
|
||||
|
@ -25,6 +26,11 @@ class GroupV2ConflictMerger implements StorageSyncHelper.ConflictMerger<SignalGr
|
|||
return Optional.fromNullable(localByGroupId.get(record.getMasterKey()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Collection<SignalGroupV2Record> getInvalidEntries(@NonNull Collection<SignalGroupV2Record> remoteRecords) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull SignalGroupV2Record merge(@NonNull SignalGroupV2Record remote, @NonNull SignalGroupV2Record local, @NonNull StorageSyncHelper.KeyGenerator keyGenerator) {
|
||||
boolean blocked = remote.isBlocked();
|
||||
|
|
|
@ -1,17 +1,29 @@
|
|||
package org.thoughtcrime.securesms.storage;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
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.SignalRecord;
|
||||
|
@ -31,6 +43,9 @@ import java.util.Locale;
|
|||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
|
||||
public final class StorageSyncHelper {
|
||||
|
||||
|
@ -40,6 +55,8 @@ public final class StorageSyncHelper {
|
|||
|
||||
private static KeyGenerator keyGenerator = KEY_GENERATOR;
|
||||
|
||||
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
|
||||
|
@ -56,11 +73,15 @@ public final class StorageSyncHelper {
|
|||
* @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()}.
|
||||
*/
|
||||
// TODO [greyson] [storage] Test this
|
||||
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 List<RecipientSettings> deletes,
|
||||
@NonNull Optional<SignalAccountRecord> accountUpdate,
|
||||
@NonNull Optional<SignalAccountRecord> accountInsert,
|
||||
@NonNull Set<RecipientId> archivedRecipients)
|
||||
{
|
||||
Set<StorageId> completeKeys = new LinkedHashSet<>(currentLocalKeys);
|
||||
Set<SignalStorageRecord> storageInserts = new LinkedHashSet<>();
|
||||
|
@ -68,33 +89,48 @@ public final class StorageSyncHelper {
|
|||
Map<RecipientId, byte[]> storageKeyUpdates = new HashMap<>();
|
||||
|
||||
for (RecipientSettings insert : inserts) {
|
||||
storageInserts.add(StorageSyncModels.localToRemoteRecord(insert));
|
||||
storageInserts.add(StorageSyncModels.localToRemoteRecord(insert, archivedRecipients));
|
||||
}
|
||||
|
||||
if (accountInsert.isPresent()) {
|
||||
storageInserts.add(SignalStorageRecord.forAccount(accountInsert.get()));
|
||||
}
|
||||
|
||||
for (RecipientSettings delete : deletes) {
|
||||
byte[] key = Objects.requireNonNull(delete.getStorageKey());
|
||||
byte[] key = Objects.requireNonNull(delete.getStorageId());
|
||||
storageDeletes.add(ByteBuffer.wrap(key));
|
||||
completeKeys.remove(StorageId.forContact(key));
|
||||
}
|
||||
|
||||
for (RecipientSettings update : updates) {
|
||||
byte[] oldKey = Objects.requireNonNull(update.getStorageKey());
|
||||
byte[] oldKey = update.getStorageId();
|
||||
byte[] newKey = generateKey();
|
||||
|
||||
storageInserts.add(StorageSyncModels.localToRemoteRecord(update, newKey));
|
||||
storageInserts.add(StorageSyncModels.localToRemoteRecord(update, newKey, archivedRecipients));
|
||||
storageDeletes.add(ByteBuffer.wrap(oldKey));
|
||||
completeKeys.remove(StorageId.forContact(oldKey));
|
||||
completeKeys.add(StorageId.forContact(newKey));
|
||||
storageKeyUpdates.put(update.getId(), newKey);
|
||||
}
|
||||
|
||||
if (accountUpdate.isPresent()) {
|
||||
byte[] oldKey = accountUpdate.get().getId().getRaw();
|
||||
byte[] newKey = generateKey();
|
||||
|
||||
storageInserts.add(SignalStorageRecord.forAccount(StorageId.forAccount(newKey), accountUpdate.get()));
|
||||
storageDeletes.add(ByteBuffer.wrap(oldKey));
|
||||
completeKeys.remove(StorageId.forAccount(oldKey));
|
||||
completeKeys.add(StorageId.forAccount(newKey));
|
||||
storageKeyUpdates.put(Recipient.self().getId(), newKey);
|
||||
}
|
||||
|
||||
if (storageInserts.isEmpty() && storageDeletes.isEmpty()) {
|
||||
return Optional.absent();
|
||||
} else {
|
||||
List<byte[]> contactDeleteBytes = Stream.of(storageDeletes).map(ByteBuffer::array).toList();
|
||||
List<StorageId> completeKeysBytes = new ArrayList<>(completeKeys);
|
||||
SignalStorageManifest manifest = new SignalStorageManifest(currentManifestVersion + 1, completeKeysBytes);
|
||||
WriteOperationResult writeOperationResult = new WriteOperationResult(manifest, new ArrayList<>(storageInserts), contactDeleteBytes);
|
||||
List<byte[]> contactDeleteBytes = Stream.of(storageDeletes).map(ByteBuffer::array).toList();
|
||||
List<StorageId> completeKeysBytes = new ArrayList<>(completeKeys);
|
||||
SignalStorageManifest manifest = new SignalStorageManifest(currentManifestVersion + 1, completeKeysBytes);
|
||||
WriteOperationResult writeOperationResult = new WriteOperationResult(manifest, new ArrayList<>(storageInserts), contactDeleteBytes);
|
||||
|
||||
return Optional.of(new LocalWriteResult(writeOperationResult, storageKeyUpdates));
|
||||
}
|
||||
|
@ -110,8 +146,8 @@ public final class StorageSyncHelper {
|
|||
* @return An object describing which keys are exclusive to the remote data set and which keys are
|
||||
* exclusive to the local data set.
|
||||
*/
|
||||
public static @NonNull KeyDifferenceResult findKeyDifference(@NonNull List<StorageId> remoteKeys,
|
||||
@NonNull List<StorageId> localKeys)
|
||||
public static @NonNull KeyDifferenceResult findKeyDifference(@NonNull Collection<StorageId> remoteKeys,
|
||||
@NonNull Collection<StorageId> localKeys)
|
||||
{
|
||||
Set<StorageId> remoteOnlyKeys = SetUtil.difference(remoteKeys, localKeys);
|
||||
Set<StorageId> localOnlyKeys = SetUtil.difference(localKeys, remoteKeys);
|
||||
|
@ -143,12 +179,23 @@ public final class StorageSyncHelper {
|
|||
List<SignalStorageRecord> remoteOnlyUnknowns = Stream.of(remoteOnlyRecords).filter(r -> r.isUnknown() || r.getGroupV2().isPresent()).toList();
|
||||
List<SignalStorageRecord> localOnlyUnknowns = Stream.of(localOnlyRecords).filter(r -> r.isUnknown() || r.getGroupV2().isPresent()).toList();
|
||||
|
||||
RecordMergeResult<SignalContactRecord, RecordUpdate<SignalContactRecord>> contactMergeResult = resolveRecordConflict(remoteOnlyContacts, localOnlyContacts, new ContactConflictMerger(localOnlyContacts));
|
||||
RecordMergeResult<SignalGroupV1Record, RecordUpdate<SignalGroupV1Record>> groupV1MergeResult = resolveRecordConflict(remoteOnlyGroupV1, localOnlyGroupV1, new GroupV1ConflictMerger(localOnlyGroupV1));
|
||||
List<SignalAccountRecord> remoteOnlyAccount = Stream.of(remoteOnlyRecords).filter(r -> r.getAccount().isPresent()).map(r -> r.getAccount().get()).toList();
|
||||
List<SignalAccountRecord> localOnlyAccount = Stream.of(localOnlyRecords).filter(r -> r.getAccount().isPresent()).map(r -> r.getAccount().get()).toList();
|
||||
if (remoteOnlyAccount.size() > 0 && localOnlyAccount.isEmpty()) {
|
||||
throw new AssertionError("Found a remote-only account, but no local-only account!");
|
||||
}
|
||||
if (localOnlyAccount.size() > 1) {
|
||||
throw new AssertionError("Multiple local accounts?");
|
||||
}
|
||||
|
||||
RecordMergeResult<SignalContactRecord> contactMergeResult = resolveRecordConflict(remoteOnlyContacts, localOnlyContacts, new ContactConflictMerger(localOnlyContacts, Recipient.self()));
|
||||
RecordMergeResult<SignalGroupV1Record> groupV1MergeResult = resolveRecordConflict(remoteOnlyGroupV1, localOnlyGroupV1, new GroupV1ConflictMerger(localOnlyGroupV1));
|
||||
RecordMergeResult<SignalAccountRecord> accountMergeResult = resolveRecordConflict(remoteOnlyAccount, localOnlyAccount, new AccountConflictMerger(localOnlyAccount.isEmpty() ? Optional.absent() : Optional.of(localOnlyAccount.get(0))));
|
||||
|
||||
Set<SignalStorageRecord> remoteInserts = new HashSet<>();
|
||||
remoteInserts.addAll(Stream.of(contactMergeResult.remoteInserts).map(SignalStorageRecord::forContact).toList());
|
||||
remoteInserts.addAll(Stream.of(groupV1MergeResult.remoteInserts).map(SignalStorageRecord::forGroupV1).toList());
|
||||
remoteInserts.addAll(Stream.of(accountMergeResult.remoteInserts).map(SignalStorageRecord::forAccount).toList());
|
||||
|
||||
Set<RecordUpdate<SignalStorageRecord>> remoteUpdates = new HashSet<>();
|
||||
remoteUpdates.addAll(Stream.of(contactMergeResult.remoteUpdates)
|
||||
|
@ -157,6 +204,14 @@ public final class StorageSyncHelper {
|
|||
remoteUpdates.addAll(Stream.of(groupV1MergeResult.remoteUpdates)
|
||||
.map(c -> new RecordUpdate<>(SignalStorageRecord.forGroupV1(c.getOld()), SignalStorageRecord.forGroupV1(c.getNew())))
|
||||
.toList());
|
||||
remoteUpdates.addAll(Stream.of(accountMergeResult.remoteUpdates)
|
||||
.map(c -> new RecordUpdate<>(SignalStorageRecord.forAccount(c.getOld()), SignalStorageRecord.forAccount(c.getNew())))
|
||||
.toList());
|
||||
|
||||
Set<SignalRecord> remoteDeletes = new HashSet<>();
|
||||
remoteDeletes.addAll(contactMergeResult.remoteDeletes);
|
||||
remoteDeletes.addAll(groupV1MergeResult.remoteDeletes);
|
||||
remoteDeletes.addAll(accountMergeResult.remoteDeletes);
|
||||
|
||||
return new MergeResult(contactMergeResult.localInserts,
|
||||
contactMergeResult.localUpdates,
|
||||
|
@ -164,8 +219,10 @@ public final class StorageSyncHelper {
|
|||
groupV1MergeResult.localUpdates,
|
||||
new LinkedHashSet<>(remoteOnlyUnknowns),
|
||||
new LinkedHashSet<>(localOnlyUnknowns),
|
||||
accountMergeResult.localUpdates.isEmpty() ? Optional.absent() : Optional.of(accountMergeResult.localUpdates.iterator().next()),
|
||||
remoteInserts,
|
||||
remoteUpdates);
|
||||
remoteUpdates,
|
||||
remoteDeletes);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -188,6 +245,7 @@ public final class StorageSyncHelper {
|
|||
inserts.addAll(Stream.of(mergeResult.getRemoteUpdates()).map(RecordUpdate::getNew).toList());
|
||||
|
||||
List<byte[]> deletes = Stream.of(mergeResult.getRemoteUpdates()).map(RecordUpdate::getOld).map(SignalStorageRecord::getId).map(StorageId::getRaw).toList();
|
||||
deletes.addAll(Stream.of(mergeResult.getRemoteDeletes()).map(SignalRecord::getId).map(StorageId::getRaw).toList());
|
||||
|
||||
return new WriteOperationResult(manifest, inserts, deletes);
|
||||
}
|
||||
|
@ -201,14 +259,18 @@ public final class StorageSyncHelper {
|
|||
keyGenerator = testKeyGenerator;
|
||||
}
|
||||
|
||||
private static @NonNull <E extends SignalRecord> RecordMergeResult<E, RecordUpdate<E>> resolveRecordConflict(@NonNull Collection<E> remoteOnlyRecords,
|
||||
@NonNull Collection<E> localOnlyRecords,
|
||||
@NonNull ConflictMerger<E> merger)
|
||||
private static @NonNull <E extends SignalRecord> RecordMergeResult<E> resolveRecordConflict(@NonNull Collection<E> remoteOnlyRecords,
|
||||
@NonNull Collection<E> localOnlyRecords,
|
||||
@NonNull ConflictMerger<E> merger)
|
||||
{
|
||||
Set<E> localInserts = new LinkedHashSet<>(remoteOnlyRecords);
|
||||
Set<E> remoteInserts = new LinkedHashSet<>(localOnlyRecords);
|
||||
Set<RecordUpdate<E>> localUpdates = new LinkedHashSet<>();
|
||||
Set<RecordUpdate<E>> remoteUpdates = new LinkedHashSet<>();
|
||||
Set<E> localInserts = new HashSet<>(remoteOnlyRecords);
|
||||
Set<E> remoteInserts = new HashSet<>(localOnlyRecords);
|
||||
Set<RecordUpdate<E>> localUpdates = new HashSet<>();
|
||||
Set<RecordUpdate<E>> remoteUpdates = new HashSet<>();
|
||||
Set<E> remoteDeletes = new HashSet<>(merger.getInvalidEntries(remoteOnlyRecords));
|
||||
|
||||
remoteOnlyRecords.removeAll(remoteDeletes);
|
||||
localInserts.removeAll(remoteDeletes);
|
||||
|
||||
for (E remote : remoteOnlyRecords) {
|
||||
Optional<E> local = merger.getMatching(remote);
|
||||
|
@ -229,13 +291,84 @@ public final class StorageSyncHelper {
|
|||
}
|
||||
}
|
||||
|
||||
return new RecordMergeResult<>(localInserts, localUpdates, remoteInserts, remoteUpdates);
|
||||
return new RecordMergeResult<>(localInserts, localUpdates, remoteInserts, remoteUpdates, remoteDeletes);
|
||||
}
|
||||
|
||||
public static boolean profileKeyChanged(RecordUpdate<SignalContactRecord> update) {
|
||||
return !OptionalUtil.byteArrayEquals(update.getOld().getProfileKey(), update.getNew().getProfileKey());
|
||||
}
|
||||
|
||||
public static Optional<SignalAccountRecord> getPendingAccountSyncUpdate(@NonNull Context context) {
|
||||
if (DatabaseFactory.getRecipientDatabase(context).getDirtyState(Recipient.self().getId()) != RecipientDatabase.DirtyState.UPDATE) {
|
||||
return Optional.absent();
|
||||
}
|
||||
return Optional.of(buildAccountRecord(context, null).getAccount().get());
|
||||
}
|
||||
|
||||
public static Optional<SignalAccountRecord> getPendingAccountSyncInsert(@NonNull Context context) {
|
||||
if (DatabaseFactory.getRecipientDatabase(context).getDirtyState(Recipient.self().getId()) != RecipientDatabase.DirtyState.INSERT) {
|
||||
return Optional.absent();
|
||||
}
|
||||
return Optional.of(buildAccountRecord(context, null).getAccount().get());
|
||||
}
|
||||
|
||||
public static SignalStorageRecord buildAccountRecord(@NonNull Context context, @Nullable StorageId id) {
|
||||
Recipient self = Recipient.self().fresh();
|
||||
SignalAccountRecord account = new SignalAccountRecord.Builder(id != null ? id.getRaw() : self.getStorageServiceId())
|
||||
.setProfileKey(self.getProfileKey())
|
||||
.setGivenName(self.getProfileName().getGivenName())
|
||||
.setFamilyName(self.getProfileName().getFamilyName())
|
||||
.setAvatarUrlPath(self.getProfileAvatar())
|
||||
.setNoteToSelfArchived(DatabaseFactory.getThreadDatabase(context).isArchived(self.getId()))
|
||||
.setTypingIndicatorsEnabled(TextSecurePreferences.isTypingIndicatorsEnabled(context))
|
||||
.setReadReceiptsEnabled(TextSecurePreferences.isReadReceiptsEnabled(context))
|
||||
.setSealedSenderIndicatorsEnabled(TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context))
|
||||
.setLinkPreviewsEnabled(TextSecurePreferences.isLinkPreviewsEnabled(context))
|
||||
.build();
|
||||
|
||||
return SignalStorageRecord.forAccount(account);
|
||||
}
|
||||
|
||||
public static void applyAccountStorageSyncUpdates(@NonNull Context context, Optional<StorageSyncHelper.RecordUpdate<SignalAccountRecord>> update) {
|
||||
if (!update.isPresent()) {
|
||||
return;
|
||||
}
|
||||
applyAccountStorageSyncUpdates(context, update.get().getOld().getId(), update.get().getNew());
|
||||
}
|
||||
|
||||
public static void applyAccountStorageSyncUpdates(@NonNull Context context, @NonNull StorageId storageId, @NonNull SignalAccountRecord update) {
|
||||
DatabaseFactory.getRecipientDatabase(context).applyStorageSyncUpdates(storageId, update);
|
||||
DatabaseFactory.getThreadDatabase(context).setArchived(Recipient.self().getId(), update.isNoteToSelfArchived());
|
||||
|
||||
TextSecurePreferences.setProfileName(context, ProfileName.fromParts(update.getGivenName().orNull(), update.getFamilyName().orNull()));
|
||||
TextSecurePreferences.setReadReceiptsEnabled(context, update.isReadReceiptsEnabled());
|
||||
TextSecurePreferences.setTypingIndicatorsEnabled(context, update.isTypingIndicatorsEnabled());
|
||||
TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, update.isSealedSenderIndicatorsEnabled());
|
||||
TextSecurePreferences.setLinkPreviewsEnabled(context, update.isLinkPreviewsEnabled());
|
||||
if (update.getAvatarUrlPath().isPresent()) {
|
||||
ApplicationDependencies.getJobManager().add(new RetrieveProfileAvatarJob(Recipient.self(), update.getAvatarUrlPath().get()));
|
||||
}
|
||||
}
|
||||
|
||||
public static void scheduleSyncForDataChange() {
|
||||
if (!SignalStore.registrationValues().isRegistrationComplete()) {
|
||||
Log.d(TAG, "Registration still ongoing. Ignore sync request.");
|
||||
return;
|
||||
}
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
|
||||
}
|
||||
|
||||
public static void scheduleRoutineSync() {
|
||||
long timeSinceLastSync = System.currentTimeMillis() - SignalStore.storageServiceValues().getLastSyncTime();
|
||||
|
||||
if (timeSinceLastSync > REFRESH_INTERVAL) {
|
||||
Log.d(TAG, "Scheduling a sync. Last sync was " + timeSinceLastSync + " ms ago.");
|
||||
scheduleSyncForDataChange();
|
||||
} else {
|
||||
Log.d(TAG, "No need for sync. Last sync was " + timeSinceLastSync + " ms ago.");
|
||||
}
|
||||
}
|
||||
|
||||
public static final class KeyDifferenceResult {
|
||||
private final List<StorageId> remoteOnlyKeys;
|
||||
private final List<StorageId> localOnlyKeys;
|
||||
|
@ -261,24 +394,28 @@ public final class StorageSyncHelper {
|
|||
}
|
||||
|
||||
public static final class MergeResult {
|
||||
private final Set<SignalContactRecord> localContactInserts;
|
||||
private final Set<RecordUpdate<SignalContactRecord>> localContactUpdates;
|
||||
private final Set<SignalGroupV1Record> localGroupV1Inserts;
|
||||
private final Set<RecordUpdate<SignalGroupV1Record>> localGroupV1Updates;
|
||||
private final Set<SignalStorageRecord> localUnknownInserts;
|
||||
private final Set<SignalStorageRecord> localUnknownDeletes;
|
||||
private final Set<SignalStorageRecord> remoteInserts;
|
||||
private final Set<RecordUpdate<SignalStorageRecord>> remoteUpdates;
|
||||
private final Set<SignalContactRecord> localContactInserts;
|
||||
private final Set<RecordUpdate<SignalContactRecord>> localContactUpdates;
|
||||
private final Set<SignalGroupV1Record> localGroupV1Inserts;
|
||||
private final Set<RecordUpdate<SignalGroupV1Record>> localGroupV1Updates;
|
||||
private final Set<SignalStorageRecord> localUnknownInserts;
|
||||
private final Set<SignalStorageRecord> localUnknownDeletes;
|
||||
private final Optional<RecordUpdate<SignalAccountRecord>> localAccountUpdate;
|
||||
private final Set<SignalStorageRecord> remoteInserts;
|
||||
private final Set<RecordUpdate<SignalStorageRecord>> remoteUpdates;
|
||||
private final Set<SignalRecord> remoteDeletes;
|
||||
|
||||
@VisibleForTesting
|
||||
MergeResult(@NonNull Set<SignalContactRecord> localContactInserts,
|
||||
@NonNull Set<RecordUpdate<SignalContactRecord>> localContactUpdates,
|
||||
@NonNull Set<SignalGroupV1Record> localGroupV1Inserts,
|
||||
@NonNull Set<RecordUpdate<SignalGroupV1Record>> localGroupV1Updates,
|
||||
@NonNull Set<SignalStorageRecord> localUnknownInserts,
|
||||
@NonNull Set<SignalStorageRecord> localUnknownDeletes,
|
||||
@NonNull Set<SignalStorageRecord> remoteInserts,
|
||||
@NonNull Set<RecordUpdate<SignalStorageRecord>> remoteUpdates)
|
||||
MergeResult(@NonNull Set<SignalContactRecord> localContactInserts,
|
||||
@NonNull Set<RecordUpdate<SignalContactRecord>> localContactUpdates,
|
||||
@NonNull Set<SignalGroupV1Record> localGroupV1Inserts,
|
||||
@NonNull Set<RecordUpdate<SignalGroupV1Record>> localGroupV1Updates,
|
||||
@NonNull Set<SignalStorageRecord> localUnknownInserts,
|
||||
@NonNull Set<SignalStorageRecord> localUnknownDeletes,
|
||||
@NonNull Optional<RecordUpdate<SignalAccountRecord>> localAccountUpdate,
|
||||
@NonNull Set<SignalStorageRecord> remoteInserts,
|
||||
@NonNull Set<RecordUpdate<SignalStorageRecord>> remoteUpdates,
|
||||
@NonNull Set<SignalRecord> remoteDeletes)
|
||||
{
|
||||
this.localContactInserts = localContactInserts;
|
||||
this.localContactUpdates = localContactUpdates;
|
||||
|
@ -286,8 +423,10 @@ public final class StorageSyncHelper {
|
|||
this.localGroupV1Updates = localGroupV1Updates;
|
||||
this.localUnknownInserts = localUnknownInserts;
|
||||
this.localUnknownDeletes = localUnknownDeletes;
|
||||
this.localAccountUpdate = localAccountUpdate;
|
||||
this.remoteInserts = remoteInserts;
|
||||
this.remoteUpdates = remoteUpdates;
|
||||
this.remoteDeletes = remoteDeletes;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalContactRecord> getLocalContactInserts() {
|
||||
|
@ -314,6 +453,10 @@ public final class StorageSyncHelper {
|
|||
return localUnknownDeletes;
|
||||
}
|
||||
|
||||
public @NonNull Optional<RecordUpdate<SignalAccountRecord>> getLocalAccountUpdate() {
|
||||
return localAccountUpdate;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalStorageRecord> getRemoteInserts() {
|
||||
return remoteInserts;
|
||||
}
|
||||
|
@ -322,6 +465,10 @@ public final class StorageSyncHelper {
|
|||
return remoteUpdates;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalRecord> getRemoteDeletes() {
|
||||
return remoteDeletes;
|
||||
}
|
||||
|
||||
@NonNull Set<SignalRecord> getAllNewRecords() {
|
||||
Set<SignalRecord> records = new HashSet<>();
|
||||
|
||||
|
@ -332,6 +479,7 @@ public final class StorageSyncHelper {
|
|||
records.addAll(Stream.of(localContactUpdates).map(RecordUpdate::getNew).toList());
|
||||
records.addAll(Stream.of(localGroupV1Updates).map(RecordUpdate::getNew).toList());
|
||||
records.addAll(Stream.of(remoteUpdates).map(RecordUpdate::getNew).toList());
|
||||
if (localAccountUpdate.isPresent()) records.add(localAccountUpdate.get().getNew());
|
||||
|
||||
return records;
|
||||
}
|
||||
|
@ -343,6 +491,8 @@ public final class StorageSyncHelper {
|
|||
records.addAll(Stream.of(localContactUpdates).map(RecordUpdate::getOld).toList());
|
||||
records.addAll(Stream.of(localGroupV1Updates).map(RecordUpdate::getOld).toList());
|
||||
records.addAll(Stream.of(remoteUpdates).map(RecordUpdate::getOld).toList());
|
||||
records.addAll(remoteDeletes);
|
||||
if (localAccountUpdate.isPresent()) records.add(localAccountUpdate.get().getOld());
|
||||
|
||||
return records;
|
||||
}
|
||||
|
@ -350,8 +500,8 @@ public final class StorageSyncHelper {
|
|||
@Override
|
||||
public @NonNull String toString() {
|
||||
return String.format(Locale.ENGLISH,
|
||||
"localContactInserts: %d, localContactUpdates: %d, localGroupInserts: %d, localGroupUpdates: %d, localUnknownInserts: %d, localUnknownDeletes: %d, remoteInserts: %d, remoteUpdates: %d",
|
||||
localContactInserts.size(), localContactUpdates.size(), localGroupV1Inserts.size(), localGroupV1Updates.size(), localUnknownInserts.size(), localUnknownDeletes.size(), remoteInserts.size(), remoteUpdates.size());
|
||||
"localContactInserts: %d, localContactUpdates: %d, localGroupV1Inserts: %d, localGroupV1Updates: %d, localUnknownInserts: %d, localUnknownDeletes: %d, localAccountUpdate: %b, remoteInserts: %d, remoteUpdates: %d",
|
||||
localContactInserts.size(), localContactUpdates.size(), localGroupV1Inserts.size(), localGroupV1Updates.size(), localUnknownInserts.size(), localUnknownDeletes.size(), localAccountUpdate.isPresent(), remoteInserts.size(), remoteUpdates.size());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -446,26 +596,30 @@ public final class StorageSyncHelper {
|
|||
}
|
||||
}
|
||||
|
||||
private static class RecordMergeResult<Record, Update> {
|
||||
final Set<Record> localInserts;
|
||||
final Set<Update> localUpdates;
|
||||
final Set<Record> remoteInserts;
|
||||
final Set<Update> remoteUpdates;
|
||||
private static class RecordMergeResult<Record extends SignalRecord> {
|
||||
final Set<Record> localInserts;
|
||||
final Set<RecordUpdate<Record>> localUpdates;
|
||||
final Set<Record> remoteInserts;
|
||||
final Set<RecordUpdate<Record>> remoteUpdates;
|
||||
final Set<Record> remoteDeletes;
|
||||
|
||||
RecordMergeResult(@NonNull Set<Record> localInserts,
|
||||
@NonNull Set<Update> localUpdates,
|
||||
@NonNull Set<Record> remoteInserts,
|
||||
@NonNull Set<Update> remoteUpdates)
|
||||
RecordMergeResult(@NonNull Set<Record> localInserts,
|
||||
@NonNull Set<RecordUpdate<Record>> localUpdates,
|
||||
@NonNull Set<Record> remoteInserts,
|
||||
@NonNull Set<RecordUpdate<Record>> remoteUpdates,
|
||||
@NonNull Set<Record> remoteDeletes)
|
||||
{
|
||||
this.localInserts = localInserts;
|
||||
this.localUpdates = localUpdates;
|
||||
this.remoteInserts = remoteInserts;
|
||||
this.remoteUpdates = remoteUpdates;
|
||||
this.remoteDeletes = remoteDeletes;
|
||||
}
|
||||
}
|
||||
|
||||
interface ConflictMerger<E extends SignalRecord> {
|
||||
@NonNull Optional<E> getMatching(@NonNull E record);
|
||||
@NonNull Collection<E> getInvalidEntries(@NonNull Collection<E> remoteRecords);
|
||||
@NonNull E merge(@NonNull E remote, @NonNull E local, @NonNull KeyGenerator keyGenerator);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
|
|||
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
|
@ -11,27 +12,29 @@ import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
|||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public final class StorageSyncModels {
|
||||
|
||||
private StorageSyncModels() {}
|
||||
|
||||
public static @NonNull SignalStorageRecord localToRemoteRecord(@NonNull RecipientSettings settings) {
|
||||
if (settings.getStorageKey() == null) {
|
||||
public static @NonNull SignalStorageRecord localToRemoteRecord(@NonNull RecipientSettings settings, @NonNull Set<RecipientId> archived) {
|
||||
if (settings.getStorageId() == null) {
|
||||
throw new AssertionError("Must have a storage key!");
|
||||
}
|
||||
|
||||
return localToRemoteRecord(settings, settings.getStorageKey());
|
||||
return localToRemoteRecord(settings, settings.getStorageId(), archived);
|
||||
}
|
||||
|
||||
public static @NonNull SignalStorageRecord localToRemoteRecord(@NonNull RecipientSettings settings, @NonNull byte[] rawStorageId) {
|
||||
public static @NonNull SignalStorageRecord localToRemoteRecord(@NonNull RecipientSettings settings, @NonNull byte[] rawStorageId, @NonNull Set<RecipientId> archived) {
|
||||
switch (settings.getGroupType()) {
|
||||
case NONE: return SignalStorageRecord.forContact(localToRemoteContact(settings, rawStorageId));
|
||||
case SIGNAL_V1: return SignalStorageRecord.forGroupV1(localToRemoteGroupV1(settings, rawStorageId));
|
||||
case NONE: return SignalStorageRecord.forContact(localToRemoteContact(settings, rawStorageId, archived));
|
||||
case SIGNAL_V1: return SignalStorageRecord.forGroupV1(localToRemoteGroupV1(settings, rawStorageId, archived));
|
||||
default: throw new AssertionError("Unsupported type!");
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull SignalContactRecord localToRemoteContact(@NonNull RecipientSettings recipient, byte[] rawStorageId) {
|
||||
private static @NonNull SignalContactRecord localToRemoteContact(@NonNull RecipientSettings recipient, byte[] rawStorageId, @NonNull Set<RecipientId> archived) {
|
||||
if (recipient.getUuid() == null && recipient.getE164() == null) {
|
||||
throw new AssertionError("Must have either a UUID or a phone number!");
|
||||
}
|
||||
|
@ -44,10 +47,11 @@ public final class StorageSyncModels {
|
|||
.setProfileSharingEnabled(recipient.isProfileSharing())
|
||||
.setIdentityKey(recipient.getIdentityKey())
|
||||
.setIdentityState(localToRemoteIdentityState(recipient.getIdentityStatus()))
|
||||
.setArchived(archived.contains(recipient.getId()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static @NonNull SignalGroupV1Record localToRemoteGroupV1(@NonNull RecipientSettings recipient, byte[] rawStorageId) {
|
||||
private static @NonNull SignalGroupV1Record localToRemoteGroupV1(@NonNull RecipientSettings recipient, byte[] rawStorageId, @NonNull Set<RecipientId> archived) {
|
||||
if (recipient.getGroupId() == null) {
|
||||
throw new AssertionError("Must have a groupId!");
|
||||
}
|
||||
|
@ -55,6 +59,7 @@ public final class StorageSyncModels {
|
|||
return new SignalGroupV1Record.Builder(rawStorageId, GroupUtil.getDecodedIdOrThrow(recipient.getGroupId()))
|
||||
.setBlocked(recipient.isBlocked())
|
||||
.setProfileSharingEnabled(recipient.isProfileSharing())
|
||||
.setArchived(archived.contains(recipient.getId()))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
package org.thoughtcrime.securesms.storage;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public final class StorageSyncValidations {
|
||||
|
||||
private StorageSyncValidations() {}
|
||||
|
||||
public static void validate(@NonNull StorageSyncHelper.WriteOperationResult result) {
|
||||
Set<StorageId> allSet = new HashSet<>(result.getManifest().getStorageIds());
|
||||
Set<StorageId> insertSet = new HashSet<>(Stream.of(result.getInserts()).map(SignalStorageRecord::getId).toList());
|
||||
|
||||
int accountCount = 0;
|
||||
for (StorageId id : result.getManifest().getStorageIds()) {
|
||||
accountCount += id.getType() == ManifestRecord.Identifier.Type.ACCOUNT_VALUE ? 1 : 0;
|
||||
}
|
||||
|
||||
if (result.getInserts().size() > insertSet.size()) {
|
||||
throw new DuplicateInsertInWriteError();
|
||||
}
|
||||
|
||||
if (accountCount > 1) {
|
||||
throw new MultipleAccountError();
|
||||
}
|
||||
|
||||
if (accountCount == 0) {
|
||||
throw new MissingAccountError();
|
||||
}
|
||||
|
||||
for (SignalStorageRecord insert : result.getInserts()) {
|
||||
if (!allSet.contains(insert.getId())) {
|
||||
throw new InsertNotPresentInFullIdSetError();
|
||||
}
|
||||
|
||||
if (insert.isUnknown()) {
|
||||
throw new UnknownInsertError();
|
||||
}
|
||||
|
||||
if (insert.getContact().isPresent()) {
|
||||
Recipient self = Recipient.self().fresh();
|
||||
SignalServiceAddress address = insert.getContact().get().getAddress();
|
||||
if (self.getE164().get().equals(address.getNumber().or("")) || self.getUuid().get().equals(address.getUuid().orNull())) {
|
||||
throw new SelfAddedAsContactError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.getDeletes().size() > 0) {
|
||||
Set<String> allSetEncoded = Stream.of(result.getManifest().getStorageIds()).map(StorageId::getRaw).map(Base64::encodeBytes).collect(Collectors.toSet());
|
||||
|
||||
for (byte[] delete : result.getDeletes()) {
|
||||
String encoded = Base64.encodeBytes(delete);
|
||||
if (allSetEncoded.contains(encoded)) {
|
||||
throw new DeletePresentInFullIdSetError();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DuplicateInsertInWriteError extends Error {
|
||||
}
|
||||
|
||||
private static final class InsertNotPresentInFullIdSetError extends Error {
|
||||
}
|
||||
|
||||
private static final class DeletePresentInFullIdSetError extends Error {
|
||||
}
|
||||
|
||||
private static final class UnknownInsertError extends Error {
|
||||
}
|
||||
|
||||
private static final class MultipleAccountError extends Error {
|
||||
}
|
||||
|
||||
private static final class MissingAccountError extends Error {
|
||||
}
|
||||
|
||||
private static final class SelfAddedAsContactError extends Error {
|
||||
}
|
||||
}
|
|
@ -39,6 +39,7 @@ import android.text.SpannableString;
|
|||
import android.text.TextUtils;
|
||||
import android.text.style.StyleSpan;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.mms.pdu_alt.CharacterSets;
|
||||
import com.google.android.mms.pdu_alt.EncodedStringValue;
|
||||
import com.google.i18n.phonenumbers.NumberParseException;
|
||||
|
@ -595,11 +596,13 @@ public class Util {
|
|||
return handler;
|
||||
}
|
||||
|
||||
public static <T> List<T> concatenatedList(List<T> first, List<T> second) {
|
||||
final List<T> concat = new ArrayList<>(first.size() + second.size());
|
||||
@SafeVarargs
|
||||
public static <T> List<T> concatenatedList(Collection <T>... items) {
|
||||
final List<T> concat = new ArrayList<>(Stream.of(items).reduce(0, (sum, list) -> sum + list.size()));
|
||||
|
||||
concat.addAll(first);
|
||||
concat.addAll(second);
|
||||
for (Collection<T> list : items) {
|
||||
concat.addAll(list);
|
||||
}
|
||||
|
||||
return concat;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
package org.thoughtcrime.securesms.storage;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.thoughtcrime.securesms.storage.ContactConflictMerger;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.KeyGenerator;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
|
||||
|
@ -15,16 +18,30 @@ import static junit.framework.TestCase.assertTrue;
|
|||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.powermock.api.mockito.PowerMockito.mock;
|
||||
import static org.thoughtcrime.securesms.testutil.TestHelpers.assertContentsEqual;
|
||||
import static org.thoughtcrime.securesms.testutil.TestHelpers.byteArray;
|
||||
import static org.thoughtcrime.securesms.testutil.TestHelpers.setOf;
|
||||
|
||||
public class ContactConflictMergerTest {
|
||||
|
||||
private static final UUID UUID_A = UuidUtil.parseOrThrow("ebef429e-695e-4f51-bcc4-526a60ac68c7");
|
||||
private static final UUID UUID_B = UuidUtil.parseOrThrow("32119989-77fb-4e18-af70-81d55185c6b1");
|
||||
private static final UUID UUID_A = UuidUtil.parseOrThrow("ebef429e-695e-4f51-bcc4-526a60ac68c7");
|
||||
private static final UUID UUID_B = UuidUtil.parseOrThrow("32119989-77fb-4e18-af70-81d55185c6b1");
|
||||
private static final UUID UUID_SELF = UuidUtil.parseOrThrow("1b2a2ca5-fc9e-4656-8c9f-22cc349ed3af");
|
||||
|
||||
private static final String E164_A = "+16108675309";
|
||||
private static final String E164_B = "+16101234567";
|
||||
|
||||
private static final String E164_A = "+16108675309";
|
||||
private static final String E164_B = "+16101234567";
|
||||
private static final String E164_SELF = "+16105555555";
|
||||
|
||||
private static final Recipient SELF = mock(Recipient.class);
|
||||
static {
|
||||
when(SELF.getUuid()).thenReturn(Optional.of(UUID_SELF));
|
||||
when(SELF.getE164()).thenReturn(Optional.of(E164_SELF));
|
||||
when(SELF.resolve()).thenReturn(SELF);
|
||||
Log.initialize(new Log.Logger[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void merge_alwaysPreferRemote_exceptProfileSharingIsEitherOr() {
|
||||
|
@ -51,7 +68,7 @@ public class ContactConflictMergerTest {
|
|||
.setArchived(true)
|
||||
.build();
|
||||
|
||||
SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local)).merge(remote, local, mock(KeyGenerator.class));
|
||||
SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local), SELF).merge(remote, local, mock(KeyGenerator.class));
|
||||
|
||||
assertEquals(UUID_A, merged.getAddress().getUuid().get());
|
||||
assertEquals(E164_A, merged.getAddress().getNumber().get());
|
||||
|
@ -83,7 +100,7 @@ public class ContactConflictMergerTest {
|
|||
.setUsername("username B")
|
||||
.setProfileSharingEnabled(false)
|
||||
.build();
|
||||
SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local)).merge(remote, local, mock(KeyGenerator.class));
|
||||
SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local), SELF).merge(remote, local, mock(KeyGenerator.class));
|
||||
|
||||
assertEquals(UUID_A, merged.getAddress().getUuid().get());
|
||||
assertEquals(E164_B, merged.getAddress().getNumber().get());
|
||||
|
@ -113,7 +130,7 @@ public class ContactConflictMergerTest {
|
|||
.setFamilyName("BLast")
|
||||
.setProfileSharingEnabled(false)
|
||||
.build();
|
||||
SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local)).merge(remote, local, mock(KeyGenerator.class));
|
||||
SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local), SELF).merge(remote, local, mock(KeyGenerator.class));
|
||||
|
||||
assertEquals(remote, merged);
|
||||
}
|
||||
|
@ -125,8 +142,19 @@ public class ContactConflictMergerTest {
|
|||
.setGivenName("AFirst")
|
||||
.setFamilyName("ALast")
|
||||
.build();
|
||||
SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local)).merge(remote, local, mock(KeyGenerator.class));
|
||||
SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local), SELF).merge(remote, local, mock(KeyGenerator.class));
|
||||
|
||||
assertEquals(local, merged);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getInvalidEntries_selfIsInvalid() {
|
||||
SignalContactRecord a = new SignalContactRecord.Builder(byteArray(1), new SignalServiceAddress(UUID_A, E164_A)).build();
|
||||
SignalContactRecord b = new SignalContactRecord.Builder(byteArray(2), new SignalServiceAddress(UUID_B, E164_B)).build();
|
||||
SignalContactRecord self = new SignalContactRecord.Builder(byteArray(3), new SignalServiceAddress(UUID_SELF, E164_SELF)).build();
|
||||
|
||||
Collection<SignalContactRecord> invalid = new ContactConflictMerger(Collections.emptyList(), SELF).getInvalidEntries(setOf(a, b, self));
|
||||
|
||||
assertContentsEqual(setOf(self), invalid);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,15 +2,23 @@ package org.thoughtcrime.securesms.storage;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.airbnb.lottie.L;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.modules.junit4.PowerMockRunner;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.KeyDifferenceResult;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.MergeResult;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
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;
|
||||
|
@ -29,31 +37,46 @@ import static junit.framework.TestCase.assertTrue;
|
|||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.powermock.api.mockito.PowerMockito.mock;
|
||||
import static org.powermock.api.mockito.PowerMockito.mockStatic;
|
||||
import static org.thoughtcrime.securesms.testutil.TestHelpers.assertByteListEquals;
|
||||
import static org.thoughtcrime.securesms.testutil.TestHelpers.assertContentsEqual;
|
||||
import static org.thoughtcrime.securesms.testutil.TestHelpers.byteArray;
|
||||
import static org.thoughtcrime.securesms.testutil.TestHelpers.byteListOf;
|
||||
import static org.thoughtcrime.securesms.testutil.TestHelpers.setOf;
|
||||
|
||||
@RunWith(PowerMockRunner.class)
|
||||
@PrepareForTest({ Recipient.class })
|
||||
public final class StorageSyncHelperTest {
|
||||
|
||||
private static final UUID UUID_A = UuidUtil.parseOrThrow("ebef429e-695e-4f51-bcc4-526a60ac68c7");
|
||||
private static final UUID UUID_B = UuidUtil.parseOrThrow("32119989-77fb-4e18-af70-81d55185c6b1");
|
||||
private static final UUID UUID_C = UuidUtil.parseOrThrow("b5552203-2bca-44aa-b6f5-9f5d87a335b6");
|
||||
private static final UUID UUID_D = UuidUtil.parseOrThrow("94829a32-7199-4a7b-8fb4-7e978509ab84");
|
||||
private static final UUID UUID_A = UuidUtil.parseOrThrow("ebef429e-695e-4f51-bcc4-526a60ac68c7");
|
||||
private static final UUID UUID_B = UuidUtil.parseOrThrow("32119989-77fb-4e18-af70-81d55185c6b1");
|
||||
private static final UUID UUID_C = UuidUtil.parseOrThrow("b5552203-2bca-44aa-b6f5-9f5d87a335b6");
|
||||
private static final UUID UUID_D = UuidUtil.parseOrThrow("94829a32-7199-4a7b-8fb4-7e978509ab84");
|
||||
private static final UUID UUID_SELF = UuidUtil.parseOrThrow("1b2a2ca5-fc9e-4656-8c9f-22cc349ed3af");
|
||||
|
||||
private static final String E164_A = "+16108675309";
|
||||
private static final String E164_B = "+16101234567";
|
||||
private static final String E164_C = "+16101112222";
|
||||
private static final String E164_D = "+16103334444";
|
||||
private static final String E164_A = "+16108675309";
|
||||
private static final String E164_B = "+16101234567";
|
||||
private static final String E164_C = "+16101112222";
|
||||
private static final String E164_D = "+16103334444";
|
||||
private static final String E164_SELF = "+16105555555";
|
||||
|
||||
private static final int UNKNOWN_TYPE = Integer.MAX_VALUE;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
StorageSyncHelper.setTestKeyGenerator(null);
|
||||
private static final Recipient SELF = mock(Recipient.class);
|
||||
static {
|
||||
when(SELF.getUuid()).thenReturn(Optional.of(UUID_SELF));
|
||||
when(SELF.getE164()).thenReturn(Optional.of(E164_SELF));
|
||||
when(SELF.resolve()).thenReturn(SELF);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
mockStatic(Recipient.class);
|
||||
when(Recipient.self()).thenReturn(SELF);
|
||||
Log.initialize(new Log.Logger[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findKeyDifference_allOverlap() {
|
||||
|
@ -87,6 +110,21 @@ public final class StorageSyncHelperTest {
|
|||
assertTrue(result.getLocalContactUpdates().isEmpty());
|
||||
assertEquals(setOf(SignalStorageRecord.forContact(local1)), result.getRemoteInserts());
|
||||
assertTrue(result.getRemoteUpdates().isEmpty());
|
||||
assertTrue(result.getRemoteDeletes().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveConflict_contact_deleteSelfContact() {
|
||||
SignalContactRecord remote1 = contact(1, UUID_SELF, E164_SELF, "self");
|
||||
SignalContactRecord local1 = contact(2, UUID_A, E164_A, "a");
|
||||
|
||||
MergeResult result = StorageSyncHelper.resolveConflict(recordSetOf(remote1), recordSetOf(local1));
|
||||
|
||||
assertTrue(result.getLocalContactInserts().isEmpty());
|
||||
assertTrue(result.getLocalContactUpdates().isEmpty());
|
||||
assertEquals(setOf(record(local1)), result.getRemoteInserts());
|
||||
assertTrue(result.getRemoteUpdates().isEmpty());
|
||||
assertEquals(setOf(remote1), result.getRemoteDeletes());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -99,9 +137,10 @@ public final class StorageSyncHelperTest {
|
|||
SignalContactRecord expectedMerge = contact(1, UUID_A, E164_A, "a");
|
||||
|
||||
assertTrue(result.getLocalContactInserts().isEmpty());
|
||||
assertEquals(setOf(contactUpdate(local1, expectedMerge)), result.getLocalContactUpdates());
|
||||
assertEquals(setOf(update(local1, expectedMerge)), result.getLocalContactUpdates());
|
||||
assertTrue(result.getRemoteInserts().isEmpty());
|
||||
assertTrue(result.getRemoteUpdates().isEmpty());
|
||||
assertTrue(result.getRemoteDeletes().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -114,9 +153,10 @@ public final class StorageSyncHelperTest {
|
|||
SignalGroupV1Record expectedMerge = groupV1(1, 1, true, false);
|
||||
|
||||
assertTrue(result.getLocalContactInserts().isEmpty());
|
||||
assertEquals(setOf(groupV1Update(local1, expectedMerge)), result.getLocalGroupV1Updates());
|
||||
assertEquals(setOf(update(local1, expectedMerge)), result.getLocalGroupV1Updates());
|
||||
assertTrue(result.getRemoteInserts().isEmpty());
|
||||
assertTrue(result.getRemoteUpdates().isEmpty());
|
||||
assertTrue(result.getRemoteDeletes().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -132,6 +172,7 @@ public final class StorageSyncHelperTest {
|
|||
assertTrue(result.getLocalContactUpdates().isEmpty());
|
||||
assertTrue(result.getRemoteInserts().isEmpty());
|
||||
assertEquals(setOf(recordUpdate(remote1, expectedMerge)), result.getRemoteUpdates());
|
||||
assertTrue(result.getRemoteDeletes().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -147,10 +188,12 @@ public final class StorageSyncHelperTest {
|
|||
assertTrue(result.getLocalGroupV1Updates().isEmpty());
|
||||
assertTrue(result.getRemoteInserts().isEmpty());
|
||||
assertEquals(setOf(recordUpdate(remote1, expectedMerge)), result.getRemoteUpdates());
|
||||
assertTrue(result.getRemoteDeletes().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveConflict_unknowns() {
|
||||
SignalStorageRecord account = SignalStorageRecord.forAccount(account(99));
|
||||
SignalStorageRecord remote1 = unknown(3);
|
||||
SignalStorageRecord remote2 = unknown(4);
|
||||
SignalStorageRecord remote3 = SignalStorageRecord.forGroupV2(groupV2(100, 200, true, true));
|
||||
|
@ -158,12 +201,13 @@ public final class StorageSyncHelperTest {
|
|||
SignalStorageRecord local2 = unknown(2);
|
||||
SignalStorageRecord local3 = SignalStorageRecord.forGroupV2(groupV2(101, 201, true, true));
|
||||
|
||||
MergeResult result = StorageSyncHelper.resolveConflict(setOf(remote1, remote2, remote3), setOf(local1, local2, local3));
|
||||
MergeResult result = StorageSyncHelper.resolveConflict(setOf(remote1, remote2, remote3, account), setOf(local1, local2, local3, account));
|
||||
|
||||
assertTrue(result.getLocalContactInserts().isEmpty());
|
||||
assertTrue(result.getLocalContactUpdates().isEmpty());
|
||||
assertEquals(setOf(remote1, remote2, remote3), result.getLocalUnknownInserts());
|
||||
assertEquals(setOf(local1, local2, local3), result.getLocalUnknownDeletes());
|
||||
assertTrue(result.getRemoteDeletes().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -180,13 +224,16 @@ public final class StorageSyncHelperTest {
|
|||
SignalGroupV1Record remote4 = groupV1(7, 1, true, false);
|
||||
SignalGroupV1Record local4 = groupV1(8, 1, false, true);
|
||||
|
||||
SignalStorageRecord unknownRemote = unknown(9);
|
||||
SignalStorageRecord unknownLocal = unknown(10);
|
||||
SignalAccountRecord remote5 = account(9);
|
||||
SignalAccountRecord local5 = account(10);
|
||||
|
||||
SignalStorageRecord unknownRemote = unknown(11);
|
||||
SignalStorageRecord unknownLocal = unknown(12);
|
||||
|
||||
StorageSyncHelper.setTestKeyGenerator(new TestGenerator(111, 222));
|
||||
|
||||
Set<SignalStorageRecord> remoteOnly = recordSetOf(remote1, remote2, remote3, remote4, unknownRemote);
|
||||
Set<SignalStorageRecord> localOnly = recordSetOf(local1, local2, local3, local4, unknownLocal);
|
||||
Set<SignalStorageRecord> remoteOnly = recordSetOf(remote1, remote2, remote3, remote4, remote5, unknownRemote);
|
||||
Set<SignalStorageRecord> localOnly = recordSetOf(local1, local2, local3, local4, local5, unknownLocal);
|
||||
|
||||
MergeResult result = StorageSyncHelper.resolveConflict(remoteOnly, localOnly);
|
||||
|
||||
|
@ -195,39 +242,43 @@ public final class StorageSyncHelperTest {
|
|||
SignalGroupV1Record merge4 = groupV1(222, 1, true, true);
|
||||
|
||||
assertEquals(setOf(remote3), result.getLocalContactInserts());
|
||||
assertEquals(setOf(contactUpdate(local2, merge2)), result.getLocalContactUpdates());
|
||||
assertEquals(setOf(groupV1Update(local4, merge4)), result.getLocalGroupV1Updates());
|
||||
assertEquals(setOf(update(local2, merge2)), result.getLocalContactUpdates());
|
||||
assertEquals(setOf(update(local4, merge4)), result.getLocalGroupV1Updates());
|
||||
assertEquals(setOf(SignalStorageRecord.forContact(local3)), result.getRemoteInserts());
|
||||
assertEquals(setOf(recordUpdate(remote1, merge1), recordUpdate(remote2, merge2), recordUpdate(remote4, merge4)), result.getRemoteUpdates());
|
||||
assertEquals(Optional.of(update(local5, remote5)), result.getLocalAccountUpdate());
|
||||
assertEquals(setOf(unknownRemote), result.getLocalUnknownInserts());
|
||||
assertEquals(setOf(unknownLocal), result.getLocalUnknownDeletes());
|
||||
assertTrue(result.getRemoteDeletes().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createWriteOperation_generic() {
|
||||
List<StorageId> localKeys = Arrays.asList(contactKey(1), contactKey(2), contactKey(3), contactKey(4), groupV1Key(100));
|
||||
SignalContactRecord insert1 = contact(6, UUID_A, E164_A, "a" );
|
||||
SignalContactRecord old1 = contact(1, UUID_B, E164_B, "b" );
|
||||
SignalContactRecord new1 = contact(5, UUID_B, E164_B, "z" );
|
||||
SignalContactRecord insert2 = contact(7, UUID_C, E164_C, "c" );
|
||||
SignalContactRecord old2 = contact(2, UUID_D, E164_D, "d" );
|
||||
SignalContactRecord new2 = contact(8, UUID_D, E164_D, "z2");
|
||||
SignalGroupV1Record insert3 = groupV1(9, 1, true, true );
|
||||
SignalGroupV1Record old3 = groupV1(100, 1, true, true );
|
||||
SignalGroupV1Record new3 = groupV1(10, 1, false, true);
|
||||
SignalStorageRecord unknownInsert = unknown(11);
|
||||
SignalStorageRecord unknownDelete = unknown(12);
|
||||
List<StorageId> localKeys = Arrays.asList(contactKey(1), contactKey(2), contactKey(3), contactKey(4), groupV1Key(100));
|
||||
SignalContactRecord insert1 = contact(6, UUID_A, E164_A, "a");
|
||||
SignalContactRecord old1 = contact(1, UUID_B, E164_B, "b");
|
||||
SignalContactRecord new1 = contact(5, UUID_B, E164_B, "z");
|
||||
SignalContactRecord insert2 = contact(7, UUID_C, E164_C, "c");
|
||||
SignalContactRecord old2 = contact(2, UUID_D, E164_D, "d");
|
||||
SignalContactRecord new2 = contact(8, UUID_D, E164_D, "z2");
|
||||
SignalGroupV1Record insert3 = groupV1(9, 1, true, true);
|
||||
SignalGroupV1Record old3 = groupV1(100, 1, true, true);
|
||||
SignalGroupV1Record new3 = groupV1(10, 1, false, true);
|
||||
SignalStorageRecord unknownInsert = unknown(11);
|
||||
SignalStorageRecord unknownDelete = unknown(12);
|
||||
|
||||
StorageSyncHelper.WriteOperationResult result = StorageSyncHelper.createWriteOperation(1,
|
||||
localKeys,
|
||||
new MergeResult(setOf(insert2),
|
||||
setOf(contactUpdate(old2, new2)),
|
||||
setOf(update(old2, new2)),
|
||||
setOf(insert3),
|
||||
setOf(groupV1Update(old3, new3)),
|
||||
setOf(update(old3, new3)),
|
||||
setOf(unknownInsert),
|
||||
setOf(unknownDelete),
|
||||
Optional.absent(),
|
||||
recordSetOf(insert1, insert3),
|
||||
setOf(recordUpdate(old1, new1), recordUpdate(old3, new3))));
|
||||
setOf(recordUpdate(old1, new1), recordUpdate(old3, new3)),
|
||||
setOf()));
|
||||
|
||||
assertEquals(2, result.getManifest().getVersion());
|
||||
assertContentsEqual(Arrays.asList(contactKey(3), contactKey(4), contactKey(5), contactKey(6), contactKey(7), contactKey(8), groupV1Key(9), groupV1Key(10), unknownKey(11)), result.getManifest().getStorageIds());
|
||||
|
@ -247,7 +298,7 @@ public final class StorageSyncHelperTest {
|
|||
assertEquals(a, b);
|
||||
assertEquals(a.hashCode(), b.hashCode());
|
||||
|
||||
assertFalse(StorageSyncHelper.profileKeyChanged(contactUpdate(a, b)));
|
||||
assertFalse(StorageSyncHelper.profileKeyChanged(update(a, b)));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -262,27 +313,33 @@ public final class StorageSyncHelperTest {
|
|||
assertNotEquals(a, b);
|
||||
assertNotEquals(a.hashCode(), b.hashCode());
|
||||
|
||||
assertTrue(StorageSyncHelper.profileKeyChanged(contactUpdate(a, b)));
|
||||
assertTrue(StorageSyncHelper.profileKeyChanged(update(a, b)));
|
||||
}
|
||||
|
||||
private static Set<SignalStorageRecord> recordSetOf(SignalRecord... records) {
|
||||
LinkedHashSet<SignalStorageRecord> storageRecords = new LinkedHashSet<>();
|
||||
|
||||
for (SignalRecord record : records) {
|
||||
if (record instanceof SignalContactRecord) {
|
||||
storageRecords.add(SignalStorageRecord.forContact(record.getId(), (SignalContactRecord) record));
|
||||
} else if (record instanceof SignalGroupV1Record) {
|
||||
storageRecords.add(SignalStorageRecord.forGroupV1(record.getId(), (SignalGroupV1Record) record));
|
||||
} else if (record instanceof SignalGroupV2Record) {
|
||||
storageRecords.add(SignalStorageRecord.forGroupV2(record.getId(), (SignalGroupV2Record) record));
|
||||
} else {
|
||||
storageRecords.add(SignalStorageRecord.forUnknown(record.getId()));
|
||||
}
|
||||
storageRecords.add(record(record));
|
||||
}
|
||||
|
||||
return storageRecords;
|
||||
}
|
||||
|
||||
private static SignalStorageRecord record(SignalRecord record) {
|
||||
if (record instanceof SignalContactRecord) {
|
||||
return SignalStorageRecord.forContact(record.getId(), (SignalContactRecord) record);
|
||||
} else if (record instanceof SignalGroupV1Record) {
|
||||
return SignalStorageRecord.forGroupV1(record.getId(), (SignalGroupV1Record) record);
|
||||
} else if (record instanceof SignalGroupV2Record) {
|
||||
return SignalStorageRecord.forGroupV2(record.getId(), (SignalGroupV2Record) record);
|
||||
} else if (record instanceof SignalAccountRecord) {
|
||||
return SignalStorageRecord.forAccount(record.getId(), (SignalAccountRecord) record);
|
||||
} else {
|
||||
return SignalStorageRecord.forUnknown(record.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<SignalStorageRecord> recordSetOf(SignalGroupV1Record... groupRecords) {
|
||||
LinkedHashSet<SignalStorageRecord> storageRecords = new LinkedHashSet<>();
|
||||
|
||||
|
@ -302,6 +359,10 @@ public final class StorageSyncHelperTest {
|
|||
.setGivenName(profileName);
|
||||
}
|
||||
|
||||
private static SignalAccountRecord account(int key) {
|
||||
return new SignalAccountRecord.Builder(byteArray(key)).build();
|
||||
}
|
||||
|
||||
private static SignalContactRecord contact(int key,
|
||||
UUID uuid,
|
||||
String e164,
|
||||
|
@ -330,20 +391,12 @@ public final class StorageSyncHelperTest {
|
|||
}
|
||||
}
|
||||
|
||||
private static StorageSyncHelper.RecordUpdate<SignalContactRecord> contactUpdate(SignalContactRecord oldContact, SignalContactRecord newContact) {
|
||||
return new StorageSyncHelper.RecordUpdate<>(oldContact, newContact);
|
||||
private static <E extends SignalRecord> StorageSyncHelper.RecordUpdate<E> update(E oldRecord, E newRecord) {
|
||||
return new StorageSyncHelper.RecordUpdate<>(oldRecord, newRecord);
|
||||
}
|
||||
|
||||
private static StorageSyncHelper.RecordUpdate<SignalGroupV1Record> groupV1Update(SignalGroupV1Record oldGroup, SignalGroupV1Record newGroup) {
|
||||
return new StorageSyncHelper.RecordUpdate<>(oldGroup, newGroup);
|
||||
}
|
||||
|
||||
private static StorageSyncHelper.RecordUpdate recordUpdate(SignalContactRecord oldContact, SignalContactRecord newContact) {
|
||||
return new StorageSyncHelper.RecordUpdate<>(SignalStorageRecord.forContact(oldContact), SignalStorageRecord.forContact(newContact));
|
||||
}
|
||||
|
||||
private static StorageSyncHelper.RecordUpdate recordUpdate(SignalGroupV1Record oldGroup, SignalGroupV1Record newGroup) {
|
||||
return new StorageSyncHelper.RecordUpdate<>(SignalStorageRecord.forGroupV1(oldGroup), SignalStorageRecord.forGroupV1(newGroup));
|
||||
private static <E extends SignalRecord> StorageSyncHelper.RecordUpdate<SignalStorageRecord> recordUpdate(E oldContact, E newContact) {
|
||||
return new StorageSyncHelper.RecordUpdate<>(record(oldContact), record(newContact));
|
||||
}
|
||||
|
||||
private static SignalStorageRecord unknown(int key) {
|
||||
|
|
|
@ -403,6 +403,18 @@ public class SignalServiceAccountManager {
|
|||
}
|
||||
}
|
||||
|
||||
public Optional<SignalStorageManifest> getStorageManifest(StorageKey storageKey) throws IOException {
|
||||
try {
|
||||
String authToken = this.pushServiceSocket.getStorageAuth();
|
||||
StorageManifest storageManifest = this.pushServiceSocket.getStorageManifest(authToken);
|
||||
|
||||
return Optional.of(SignalStorageModels.remoteToLocalStorageManifest(storageManifest, storageKey));
|
||||
} catch (InvalidKeyException | NotFoundException e) {
|
||||
Log.w(TAG, "Error while fetching manifest.", e);
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
public long getStorageManifestVersion() throws IOException {
|
||||
try {
|
||||
String authToken = this.pushServiceSocket.getStorageAuth();
|
||||
|
@ -431,6 +443,10 @@ public class SignalServiceAccountManager {
|
|||
}
|
||||
|
||||
public List<SignalStorageRecord> readStorageRecords(StorageKey storageKey, List<StorageId> storageKeys) throws IOException, InvalidKeyException {
|
||||
if (storageKeys.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<SignalStorageRecord> result = new ArrayList<>();
|
||||
ReadOperation.Builder operation = ReadOperation.newBuilder();
|
||||
Map<ByteString, Integer> typeMap = new HashMap<>();
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class SignalAccountRecord implements SignalRecord {
|
||||
|
||||
private final StorageId id;
|
||||
private final AccountRecord proto;
|
||||
|
||||
private final Optional<String> givenName;
|
||||
private final Optional<String> familyName;
|
||||
private final Optional<String> avatarUrlPath;
|
||||
private final Optional<byte[]> profileKey;
|
||||
|
||||
public SignalAccountRecord(StorageId id, AccountRecord proto) {
|
||||
this.id = id;
|
||||
this.proto = proto;
|
||||
|
||||
this.givenName = OptionalUtil.absentIfEmpty(proto.getGivenName());
|
||||
this.familyName = OptionalUtil.absentIfEmpty(proto.getFamilyName());
|
||||
this.profileKey = OptionalUtil.absentIfEmpty(proto.getProfileKey());
|
||||
this.avatarUrlPath = OptionalUtil.absentIfEmpty(proto.getAvatarUrlPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageId getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Optional<String> getGivenName() {
|
||||
return givenName;
|
||||
}
|
||||
|
||||
public Optional<String> getFamilyName() {
|
||||
return familyName;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getProfileKey() {
|
||||
return profileKey;
|
||||
}
|
||||
|
||||
public Optional<String> getAvatarUrlPath() {
|
||||
return avatarUrlPath;
|
||||
}
|
||||
|
||||
public boolean isNoteToSelfArchived() {
|
||||
return proto.getNoteToSelfArchived();
|
||||
}
|
||||
|
||||
public boolean isReadReceiptsEnabled() {
|
||||
return proto.getReadReceipts();
|
||||
}
|
||||
|
||||
public boolean isTypingIndicatorsEnabled() {
|
||||
return proto.getTypingIndicators();
|
||||
}
|
||||
|
||||
public boolean isSealedSenderIndicatorsEnabled() {
|
||||
return proto.getSealedSenderIndicators();
|
||||
}
|
||||
|
||||
public boolean isLinkPreviewsEnabled() {
|
||||
return proto.getLinkPreviews();
|
||||
}
|
||||
|
||||
AccountRecord toProto() {
|
||||
return proto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SignalAccountRecord that = (SignalAccountRecord) o;
|
||||
return id.equals(that.id) &&
|
||||
proto.equals(that.proto);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, proto);
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final StorageId id;
|
||||
private final AccountRecord.Builder builder;
|
||||
|
||||
public Builder(byte[] rawId) {
|
||||
this.id = StorageId.forAccount(rawId);
|
||||
this.builder = AccountRecord.newBuilder();
|
||||
}
|
||||
|
||||
public Builder setGivenName(String givenName) {
|
||||
builder.setGivenName(givenName == null ? "" : givenName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setFamilyName(String familyName) {
|
||||
builder.setFamilyName(familyName == null ? "" : familyName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setProfileKey(byte[] profileKey) {
|
||||
builder.setProfileKey(profileKey == null ? ByteString.EMPTY : ByteString.copyFrom(profileKey));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAvatarUrlPath(String urlPath) {
|
||||
builder.setAvatarUrlPath(urlPath == null ? "" : urlPath);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setNoteToSelfArchived(boolean archived) {
|
||||
builder.setNoteToSelfArchived(archived);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setReadReceiptsEnabled(boolean enabled) {
|
||||
builder.setReadReceipts(enabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setTypingIndicatorsEnabled(boolean enabled) {
|
||||
builder.setTypingIndicators(enabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setSealedSenderIndicatorsEnabled(boolean enabled) {
|
||||
builder.setSealedSenderIndicators(enabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setLinkPreviewsEnabled(boolean enabled) {
|
||||
builder.setLinkPreviews(enabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
public SignalAccountRecord build() {
|
||||
return new SignalAccountRecord(id, builder.build());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ public final class SignalContactRecord implements SignalRecord {
|
|||
private final Optional<String> username;
|
||||
private final Optional<byte[]> identityKey;
|
||||
|
||||
private SignalContactRecord(StorageId id, ContactRecord proto) {
|
||||
public SignalContactRecord(StorageId id, ContactRecord proto) {
|
||||
this.id = id;
|
||||
this.proto = proto;
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ public final class SignalGroupV1Record implements SignalRecord {
|
|||
private final GroupV1Record proto;
|
||||
private final byte[] groupId;
|
||||
|
||||
private SignalGroupV1Record(StorageId id, GroupV1Record proto) {
|
||||
public SignalGroupV1Record(StorageId id, GroupV1Record proto) {
|
||||
this.id = id;
|
||||
this.proto = proto;
|
||||
this.groupId = proto.getId().toByteArray();
|
||||
|
|
|
@ -15,7 +15,7 @@ public final class SignalGroupV2Record implements SignalRecord {
|
|||
private final GroupV2Record proto;
|
||||
private final GroupMasterKey masterKey;
|
||||
|
||||
private SignalGroupV2Record(StorageId id, GroupV2Record proto) {
|
||||
public SignalGroupV2Record(StorageId id, GroupV2Record proto) {
|
||||
this.id = id;
|
||||
this.proto = proto;
|
||||
try {
|
||||
|
|
|
@ -1,14 +1,33 @@
|
|||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
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.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class SignalStorageManifest {
|
||||
private final long version;
|
||||
private final List<StorageId> storageIds;
|
||||
private final long version;
|
||||
private final List<StorageId> storageIds;
|
||||
private final Map<Integer, List<StorageId>> storageIdsByType;
|
||||
|
||||
public SignalStorageManifest(long version, List<StorageId> storageIds) {
|
||||
this.version = version;
|
||||
this.storageIds = storageIds;
|
||||
this.version = version;
|
||||
this.storageIds = storageIds;
|
||||
this.storageIdsByType = new HashMap<>();
|
||||
|
||||
for (StorageId id : storageIds) {
|
||||
List<StorageId> list = storageIdsByType.get(id.getType());
|
||||
if (list == null) {
|
||||
list = new ArrayList<>();
|
||||
}
|
||||
list.add(id);
|
||||
storageIdsByType.put(id.getType(), list);
|
||||
}
|
||||
}
|
||||
|
||||
public long getVersion() {
|
||||
|
@ -18,4 +37,14 @@ public class SignalStorageManifest {
|
|||
public List<StorageId> getStorageIds() {
|
||||
return storageIds;
|
||||
}
|
||||
|
||||
public Optional<StorageId> getAccountStorageId() {
|
||||
List<StorageId> list = storageIdsByType.get(ManifestRecord.Identifier.Type.ACCOUNT_VALUE);
|
||||
|
||||
if (list.size() > 0) {
|
||||
return Optional.of(list.get(0));
|
||||
} else {
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,14 +2,8 @@ package org.whispersystems.signalservice.api.storage;
|
|||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StorageItem;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StorageManifest;
|
||||
|
@ -37,13 +31,16 @@ public final class SignalStorageModels {
|
|||
byte[] key = item.getKey().toByteArray();
|
||||
byte[] rawRecord = SignalStorageCipher.decrypt(storageKey.deriveItemKey(key), item.getValue().toByteArray());
|
||||
StorageRecord record = StorageRecord.parseFrom(rawRecord);
|
||||
StorageId id = StorageId.forType(key, type);
|
||||
|
||||
if (record.hasContact() && type == ManifestRecord.Identifier.Type.CONTACT_VALUE) {
|
||||
return SignalStorageRecord.forContact(StorageId.forContact(key), remoteToLocalContactRecord(key, record.getContact()));
|
||||
return SignalStorageRecord.forContact(id, new SignalContactRecord(id, record.getContact()));
|
||||
} else if (record.hasGroupV1() && type == ManifestRecord.Identifier.Type.GROUPV1_VALUE) {
|
||||
return SignalStorageRecord.forGroupV1(StorageId.forGroupV1(key), remoteToLocalGroupV1Record(key, record.getGroupV1()));
|
||||
return SignalStorageRecord.forGroupV1(id, new SignalGroupV1Record(id, record.getGroupV1()));
|
||||
} else if (record.hasGroupV2() && type == ManifestRecord.Identifier.Type.GROUPV2_VALUE && record.getGroupV2().getMasterKey().size() == GroupMasterKey.SIZE) {
|
||||
return SignalStorageRecord.forGroupV2(StorageId.forGroupV2(key), remoteToLocalGroupV2Record(key, record.getGroupV2()));
|
||||
return SignalStorageRecord.forGroupV2(id, new SignalGroupV2Record(id, record.getGroupV2()));
|
||||
} else if (record.hasAccount() && type == ManifestRecord.Identifier.Type.ACCOUNT_VALUE) {
|
||||
return SignalStorageRecord.forAccount(id, new SignalAccountRecord(id, record.getAccount()));
|
||||
} else {
|
||||
return SignalStorageRecord.forUnknown(StorageId.forType(key, type));
|
||||
}
|
||||
|
@ -58,6 +55,8 @@ public final class SignalStorageModels {
|
|||
builder.setGroupV1(record.getGroupV1().get().toProto());
|
||||
} else if (record.getGroupV2().isPresent()) {
|
||||
builder.setGroupV2(record.getGroupV2().get().toProto());
|
||||
} else if (record.getAccount().isPresent()) {
|
||||
builder.setAccount(record.getAccount().get().toProto());
|
||||
} else {
|
||||
throw new InvalidStorageWriteError();
|
||||
}
|
||||
|
@ -72,42 +71,6 @@ public final class SignalStorageModels {
|
|||
.build();
|
||||
}
|
||||
|
||||
private static SignalContactRecord remoteToLocalContactRecord(byte[] key, ContactRecord contact) {
|
||||
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(contact.getServiceUuid()), contact.getServiceE164());
|
||||
|
||||
return new SignalContactRecord.Builder(key, address)
|
||||
.setBlocked(contact.getBlocked())
|
||||
.setProfileSharingEnabled(contact.getWhitelisted())
|
||||
.setProfileKey(contact.getProfileKey().toByteArray())
|
||||
.setGivenName(contact.getGivenName())
|
||||
.setFamilyName(contact.getFamilyName())
|
||||
.setUsername(contact.getUsername())
|
||||
.setIdentityKey(contact.getIdentityKey().toByteArray())
|
||||
.setIdentityState(contact.getIdentityState())
|
||||
.setArchived(contact.getArchived())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static SignalGroupV1Record remoteToLocalGroupV1Record(byte[] key, GroupV1Record groupV1) {
|
||||
return new SignalGroupV1Record.Builder(key, groupV1.getId().toByteArray())
|
||||
.setBlocked(groupV1.getBlocked())
|
||||
.setProfileSharingEnabled(groupV1.getWhitelisted())
|
||||
.setArchived(groupV1.getArchived())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static SignalGroupV2Record remoteToLocalGroupV2Record(byte[] key, GroupV2Record groupV2) {
|
||||
try {
|
||||
return new SignalGroupV2Record.Builder(key, new GroupMasterKey(groupV2.getMasterKey().toByteArray()))
|
||||
.setBlocked(groupV2.getBlocked())
|
||||
.setProfileSharingEnabled(groupV2.getWhitelisted())
|
||||
.setArchived(groupV2.getArchived())
|
||||
.build();
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
private static class InvalidStorageWriteError extends Error {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
@ -10,13 +11,14 @@ public class SignalStorageRecord implements SignalRecord {
|
|||
private final Optional<SignalContactRecord> contact;
|
||||
private final Optional<SignalGroupV1Record> groupV1;
|
||||
private final Optional<SignalGroupV2Record> groupV2;
|
||||
private final Optional<SignalAccountRecord> account;
|
||||
|
||||
public static SignalStorageRecord forContact(SignalContactRecord contact) {
|
||||
return forContact(contact.getId(), contact);
|
||||
}
|
||||
|
||||
public static SignalStorageRecord forContact(StorageId key, SignalContactRecord contact) {
|
||||
return new SignalStorageRecord(key, Optional.of(contact), Optional.<SignalGroupV1Record>absent(), Optional.<SignalGroupV2Record>absent());
|
||||
return new SignalStorageRecord(key, Optional.of(contact), Optional.<SignalGroupV1Record>absent(), Optional.<SignalGroupV2Record>absent(), Optional.<SignalAccountRecord>absent());
|
||||
}
|
||||
|
||||
public static SignalStorageRecord forGroupV1(SignalGroupV1Record groupV1) {
|
||||
|
@ -24,7 +26,7 @@ public class SignalStorageRecord implements SignalRecord {
|
|||
}
|
||||
|
||||
public static SignalStorageRecord forGroupV1(StorageId key, SignalGroupV1Record groupV1) {
|
||||
return new SignalStorageRecord(key, Optional.<SignalContactRecord>absent(), Optional.of(groupV1), Optional.<SignalGroupV2Record>absent());
|
||||
return new SignalStorageRecord(key, Optional.<SignalContactRecord>absent(), Optional.of(groupV1), Optional.<SignalGroupV2Record>absent(), Optional.<SignalAccountRecord>absent());
|
||||
}
|
||||
|
||||
public static SignalStorageRecord forGroupV2(SignalGroupV2Record groupV2) {
|
||||
|
@ -32,22 +34,32 @@ public class SignalStorageRecord implements SignalRecord {
|
|||
}
|
||||
|
||||
public static SignalStorageRecord forGroupV2(StorageId key, SignalGroupV2Record groupV2) {
|
||||
return new SignalStorageRecord(key, Optional.<SignalContactRecord>absent(), Optional.<SignalGroupV1Record>absent(), Optional.of(groupV2));
|
||||
return new SignalStorageRecord(key, Optional.<SignalContactRecord>absent(), Optional.<SignalGroupV1Record>absent(), Optional.of(groupV2), Optional.<SignalAccountRecord>absent());
|
||||
}
|
||||
|
||||
public static SignalStorageRecord forAccount(SignalAccountRecord account) {
|
||||
return forAccount(account.getId(), account);
|
||||
}
|
||||
|
||||
public static SignalStorageRecord forAccount(StorageId key, SignalAccountRecord account) {
|
||||
return new SignalStorageRecord(key, Optional.<SignalContactRecord>absent(), Optional.<SignalGroupV1Record>absent(), Optional.<SignalGroupV2Record>absent(), Optional.of(account));
|
||||
}
|
||||
|
||||
public static SignalStorageRecord forUnknown(StorageId key) {
|
||||
return new SignalStorageRecord(key,Optional.<SignalContactRecord>absent(), Optional.<SignalGroupV1Record>absent(), Optional.<SignalGroupV2Record>absent());
|
||||
return new SignalStorageRecord(key,Optional.<SignalContactRecord>absent(), Optional.<SignalGroupV1Record>absent(), Optional.<SignalGroupV2Record>absent(), Optional.<SignalAccountRecord>absent());
|
||||
}
|
||||
|
||||
private SignalStorageRecord(StorageId id,
|
||||
Optional<SignalContactRecord> contact,
|
||||
Optional<SignalGroupV1Record> groupV1,
|
||||
Optional<SignalGroupV2Record> groupV2)
|
||||
Optional<SignalGroupV2Record> groupV2,
|
||||
Optional<SignalAccountRecord> account)
|
||||
{
|
||||
this.id = id;
|
||||
this.contact = contact;
|
||||
this.groupV1 = groupV1;
|
||||
this.groupV2 = groupV2;
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -71,8 +83,12 @@ public class SignalStorageRecord implements SignalRecord {
|
|||
return groupV2;
|
||||
}
|
||||
|
||||
public Optional<SignalAccountRecord> getAccount() {
|
||||
return account;
|
||||
}
|
||||
|
||||
public boolean isUnknown() {
|
||||
return !contact.isPresent() && !groupV1.isPresent();
|
||||
return !contact.isPresent() && !groupV1.isPresent() && !account.isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -21,6 +21,10 @@ public class StorageId {
|
|||
return new StorageId(ManifestRecord.Identifier.Type.GROUPV2_VALUE, raw);
|
||||
}
|
||||
|
||||
public static StorageId forAccount(byte[] raw) {
|
||||
return new StorageId(ManifestRecord.Identifier.Type.ACCOUNT_VALUE, raw);
|
||||
}
|
||||
|
||||
public static StorageId forType(byte[] raw, int type) {
|
||||
return new StorageId(type, raw);
|
||||
}
|
||||
|
|
|
@ -97,13 +97,13 @@ message GroupV2Record {
|
|||
}
|
||||
|
||||
message AccountRecord {
|
||||
message Config {
|
||||
bool readReceipts = 1;
|
||||
bool sealedSenderIndicators = 2;
|
||||
bool typingIndicators = 3;
|
||||
bool linkPreviews = 4;
|
||||
}
|
||||
|
||||
ContactRecord contact = 1;
|
||||
Config config = 2;
|
||||
bytes profileKey = 1;
|
||||
string givenName = 2;
|
||||
string familyName = 3;
|
||||
string avatarUrlPath = 4;
|
||||
bool noteToSelfArchived = 5;
|
||||
bool readReceipts = 6;
|
||||
bool sealedSenderIndicators = 7;
|
||||
bool typingIndicators = 8;
|
||||
bool linkPreviews = 9;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue