diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.java deleted file mode 100644 index 6621bc92ce..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.java +++ /dev/null @@ -1,267 +0,0 @@ -package org.thoughtcrime.securesms.storage; - -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.signal.core.util.StringUtil; -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.whispersystems.signalservice.api.storage.SignalAccountRecord; -import org.whispersystems.signalservice.api.storage.SignalAccountRecord.PinnedConversation; -import org.whispersystems.signalservice.api.util.OptionalUtil; -import org.whispersystems.signalservice.internal.storage.protos.AccountRecord; -import org.whispersystems.signalservice.internal.storage.protos.OptionalBool; - -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -/** - * Processes {@link SignalAccountRecord}s. Unlike some other {@link StorageRecordProcessor}s, this - * one has some statefulness in order to reject all but one account record (since we should have - * exactly one account record). - */ -public class AccountRecordProcessor extends DefaultStorageRecordProcessor { - - private static final String TAG = Log.tag(AccountRecordProcessor.class); - - private final Context context; - private final SignalAccountRecord localAccountRecord; - private final Recipient self; - - private boolean foundAccountRecord = false; - - public AccountRecordProcessor(@NonNull Context context, @NonNull Recipient self) { - this(context, self, StorageSyncHelper.buildAccountRecord(context, self).getAccount().get()); - } - - AccountRecordProcessor(@NonNull Context context, @NonNull Recipient self, @NonNull SignalAccountRecord localAccountRecord) { - this.context = context; - this.self = self; - this.localAccountRecord = localAccountRecord; - } - - /** - * We want to catch: - * - Multiple account records - */ - @Override - boolean isInvalid(@NonNull SignalAccountRecord remote) { - if (foundAccountRecord) { - Log.w(TAG, "Found an additional account record! Considering it invalid."); - return true; - } - - foundAccountRecord = true; - return false; - } - - @Override - public @NonNull Optional getMatching(@NonNull SignalAccountRecord record, @NonNull StorageKeyGenerator keyGenerator) { - return Optional.of(localAccountRecord); - } - - @Override - public @NonNull SignalAccountRecord merge(@NonNull SignalAccountRecord remote, @NonNull SignalAccountRecord local, @NonNull StorageKeyGenerator keyGenerator) { - String givenName; - String familyName; - - if (remote.getGivenName().isPresent() || remote.getFamilyName().isPresent()) { - givenName = remote.getGivenName().orElse(""); - familyName = remote.getFamilyName().orElse(""); - } else { - givenName = local.getGivenName().orElse(""); - familyName = local.getFamilyName().orElse(""); - } - - SignalAccountRecord.Payments payments; - - if (remote.getPayments().getEntropy().isPresent()) { - payments = remote.getPayments(); - } else { - payments = local.getPayments(); - } - - SignalAccountRecord.Subscriber subscriber; - - if (remote.getSubscriber().getId().isPresent()) { - subscriber = remote.getSubscriber(); - } else { - subscriber = local.getSubscriber(); - } - - SignalAccountRecord.Subscriber backupsSubscriber; - - if (remote.getSubscriber().getId().isPresent()) { - backupsSubscriber = remote.getSubscriber(); - } else { - backupsSubscriber = local.getSubscriber(); - } - - OptionalBool storyViewReceiptsState; - if (remote.getStoryViewReceiptsState() == OptionalBool.UNSET) { - storyViewReceiptsState = local.getStoryViewReceiptsState(); - } else { - storyViewReceiptsState = remote.getStoryViewReceiptsState(); - } - - byte[] unknownFields = remote.serializeUnknownFields(); - String avatarUrlPath = OptionalUtil.or(remote.getAvatarUrlPath(), local.getAvatarUrlPath()).orElse(""); - byte[] profileKey = OptionalUtil.or(remote.getProfileKey(), local.getProfileKey()).orElse(null); - boolean noteToSelfArchived = remote.isNoteToSelfArchived(); - boolean noteToSelfForcedUnread = remote.isNoteToSelfForcedUnread(); - boolean readReceipts = remote.isReadReceiptsEnabled(); - boolean typingIndicators = remote.isTypingIndicatorsEnabled(); - boolean sealedSenderIndicators = remote.isSealedSenderIndicatorsEnabled(); - boolean linkPreviews = remote.isLinkPreviewsEnabled(); - boolean unlisted = remote.isPhoneNumberUnlisted(); - List pinnedConversations = remote.getPinnedConversations(); - AccountRecord.PhoneNumberSharingMode phoneNumberSharingMode = remote.getPhoneNumberSharingMode(); - boolean preferContactAvatars = remote.isPreferContactAvatars(); - int universalExpireTimer = remote.getUniversalExpireTimer(); - boolean primarySendsSms = SignalStore.account().isPrimaryDevice() ? local.isPrimarySendsSms() : remote.isPrimarySendsSms(); - String e164 = SignalStore.account().isPrimaryDevice() ? local.getE164() : remote.getE164(); - List defaultReactions = remote.getDefaultReactions().size() > 0 ? remote.getDefaultReactions() : local.getDefaultReactions(); - boolean displayBadgesOnProfile = remote.isDisplayBadgesOnProfile(); - boolean subscriptionManuallyCancelled = remote.isSubscriptionManuallyCancelled(); - boolean keepMutedChatsArchived = remote.isKeepMutedChatsArchived(); - boolean hasSetMyStoriesPrivacy = remote.hasSetMyStoriesPrivacy(); - boolean hasViewedOnboardingStory = remote.hasViewedOnboardingStory() || local.hasViewedOnboardingStory(); - boolean storiesDisabled = remote.isStoriesDisabled(); - boolean hasSeenGroupStoryEducation = remote.hasSeenGroupStoryEducationSheet() || local.hasSeenGroupStoryEducationSheet(); - boolean hasSeenUsernameOnboarding = remote.hasCompletedUsernameOnboarding() || local.hasCompletedUsernameOnboarding(); - String username = remote.getUsername(); - AccountRecord.UsernameLink usernameLink = remote.getUsernameLink(); - boolean matchesRemote = doParamsMatch(remote, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations, preferContactAvatars, payments, universalExpireTimer, primarySendsSms, e164, defaultReactions, subscriber, displayBadgesOnProfile, subscriptionManuallyCancelled, keepMutedChatsArchived, hasSetMyStoriesPrivacy, hasViewedOnboardingStory, hasSeenUsernameOnboarding, storiesDisabled, storyViewReceiptsState, username, usernameLink, backupsSubscriber); - boolean matchesLocal = doParamsMatch(local, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations, preferContactAvatars, payments, universalExpireTimer, primarySendsSms, e164, defaultReactions, subscriber, displayBadgesOnProfile, subscriptionManuallyCancelled, keepMutedChatsArchived, hasSetMyStoriesPrivacy, hasViewedOnboardingStory, hasSeenUsernameOnboarding, storiesDisabled, storyViewReceiptsState, username, usernameLink, backupsSubscriber); - - if (matchesRemote) { - return remote; - } else if (matchesLocal) { - return local; - } else { - SignalAccountRecord.Builder builder = new SignalAccountRecord.Builder(keyGenerator.generate(), unknownFields) - .setGivenName(givenName) - .setFamilyName(familyName) - .setAvatarUrlPath(avatarUrlPath) - .setProfileKey(profileKey) - .setNoteToSelfArchived(noteToSelfArchived) - .setNoteToSelfForcedUnread(noteToSelfForcedUnread) - .setReadReceiptsEnabled(readReceipts) - .setTypingIndicatorsEnabled(typingIndicators) - .setSealedSenderIndicatorsEnabled(sealedSenderIndicators) - .setLinkPreviewsEnabled(linkPreviews) - .setUnlistedPhoneNumber(unlisted) - .setPhoneNumberSharingMode(phoneNumberSharingMode) - .setUnlistedPhoneNumber(unlisted) - .setPinnedConversations(pinnedConversations) - .setPreferContactAvatars(preferContactAvatars) - .setPayments(payments.isEnabled(), payments.getEntropy().orElse(null)) - .setUniversalExpireTimer(universalExpireTimer) - .setPrimarySendsSms(primarySendsSms) - .setDefaultReactions(defaultReactions) - .setSubscriber(subscriber) - .setStoryViewReceiptsState(storyViewReceiptsState) - .setDisplayBadgesOnProfile(displayBadgesOnProfile) - .setSubscriptionManuallyCancelled(subscriptionManuallyCancelled) - .setKeepMutedChatsArchived(keepMutedChatsArchived) - .setHasSetMyStoriesPrivacy(hasSetMyStoriesPrivacy) - .setHasViewedOnboardingStory(hasViewedOnboardingStory) - .setStoriesDisabled(storiesDisabled) - .setHasSeenGroupStoryEducationSheet(hasSeenGroupStoryEducation) - .setHasCompletedUsernameOnboarding(hasSeenUsernameOnboarding) - .setUsername(username) - .setUsernameLink(usernameLink) - .setBackupsSubscriber(backupsSubscriber); - - return builder.build(); - } - } - - @Override - void insertLocal(@NonNull SignalAccountRecord record) { - throw new UnsupportedOperationException("We should always have a local AccountRecord, so we should never been inserting a new one."); - } - - @Override - void updateLocal(@NonNull StorageRecordUpdate update) { - StorageSyncHelper.applyAccountStorageSyncUpdates(context, self, update, true); - } - - @Override - public int compare(@NonNull SignalAccountRecord lhs, @NonNull SignalAccountRecord rhs) { - return 0; - } - - private static boolean doParamsMatch(@NonNull SignalAccountRecord contact, - @Nullable byte[] unknownFields, - @NonNull String givenName, - @NonNull String familyName, - @NonNull String avatarUrlPath, - @Nullable byte[] profileKey, - boolean noteToSelfArchived, - boolean noteToSelfForcedUnread, - boolean readReceipts, - boolean typingIndicators, - boolean sealedSenderIndicators, - boolean linkPreviewsEnabled, - AccountRecord.PhoneNumberSharingMode phoneNumberSharingMode, - boolean unlistedPhoneNumber, - @NonNull List pinnedConversations, - boolean preferContactAvatars, - SignalAccountRecord.Payments payments, - int universalExpireTimer, - boolean primarySendsSms, - String e164, - @NonNull List defaultReactions, - @NonNull SignalAccountRecord.Subscriber subscriber, - boolean displayBadgesOnProfile, - boolean subscriptionManuallyCancelled, - boolean keepMutedChatsArchived, - boolean hasSetMyStoriesPrivacy, - boolean hasViewedOnboardingStory, - boolean hasCompletedUsernameOnboarding, - boolean storiesDisabled, - @NonNull OptionalBool storyViewReceiptsState, - @Nullable String username, - @Nullable AccountRecord.UsernameLink usernameLink, - @NonNull SignalAccountRecord.Subscriber backupsSubscriber) - { - return Arrays.equals(contact.serializeUnknownFields(), unknownFields) && - Objects.equals(contact.getGivenName().orElse(""), givenName) && - Objects.equals(contact.getFamilyName().orElse(""), familyName) && - Objects.equals(contact.getAvatarUrlPath().orElse(""), avatarUrlPath) && - Objects.equals(contact.getPayments(), payments) && - Objects.equals(contact.getE164(), e164) && - Objects.equals(contact.getDefaultReactions(), defaultReactions) && - Arrays.equals(contact.getProfileKey().orElse(null), profileKey) && - contact.isNoteToSelfArchived() == noteToSelfArchived && - contact.isNoteToSelfForcedUnread() == noteToSelfForcedUnread && - contact.isReadReceiptsEnabled() == readReceipts && - contact.isTypingIndicatorsEnabled() == typingIndicators && - contact.isSealedSenderIndicatorsEnabled() == sealedSenderIndicators && - contact.isLinkPreviewsEnabled() == linkPreviewsEnabled && - contact.getPhoneNumberSharingMode() == phoneNumberSharingMode && - contact.isPhoneNumberUnlisted() == unlistedPhoneNumber && - contact.isPreferContactAvatars() == preferContactAvatars && - contact.getUniversalExpireTimer() == universalExpireTimer && - contact.isPrimarySendsSms() == primarySendsSms && - Objects.equals(contact.getPinnedConversations(), pinnedConversations) && - Objects.equals(contact.getSubscriber(), subscriber) && - contact.isDisplayBadgesOnProfile() == displayBadgesOnProfile && - contact.isSubscriptionManuallyCancelled() == subscriptionManuallyCancelled && - contact.isKeepMutedChatsArchived() == keepMutedChatsArchived && - contact.hasSetMyStoriesPrivacy() == hasSetMyStoriesPrivacy && - contact.hasViewedOnboardingStory() == hasViewedOnboardingStory && - contact.hasCompletedUsernameOnboarding() == hasCompletedUsernameOnboarding && - contact.isStoriesDisabled() == storiesDisabled && - contact.getStoryViewReceiptsState().equals(storyViewReceiptsState) && - Objects.equals(contact.getUsername(), username) && - Objects.equals(contact.getUsernameLink(), usernameLink) && - Objects.equals(contact.getBackupsSubscriber(), backupsSubscriber); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.kt new file mode 100644 index 0000000000..ee24b7cd52 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.kt @@ -0,0 +1,309 @@ +package org.thoughtcrime.securesms.storage + +import android.content.Context +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.storage.StorageSyncHelper.applyAccountStorageSyncUpdates +import org.thoughtcrime.securesms.storage.StorageSyncHelper.buildAccountRecord +import org.whispersystems.signalservice.api.storage.SignalAccountRecord +import org.whispersystems.signalservice.api.util.OptionalUtil +import org.whispersystems.signalservice.internal.storage.protos.AccountRecord +import org.whispersystems.signalservice.internal.storage.protos.OptionalBool +import java.util.Optional + +/** + * Processes [SignalAccountRecord]s. Unlike some other [StorageRecordProcessor]s, this + * one has some statefulness in order to reject all but one account record (since we should have + * exactly one account record). + */ +class AccountRecordProcessor( + private val context: Context, + private val self: Recipient, + private val localAccountRecord: SignalAccountRecord +) : DefaultStorageRecordProcessor() { + + companion object { + private val TAG = Log.tag(AccountRecordProcessor::class.java) + } + + private var foundAccountRecord = false + + constructor(context: Context, self: Recipient) : this(context, self, buildAccountRecord(context, self).account.get()) + + /** + * We want to catch: + * - Multiple account records + */ + override fun isInvalid(remote: SignalAccountRecord): Boolean { + if (foundAccountRecord) { + Log.w(TAG, "Found an additional account record! Considering it invalid.") + return true + } + + foundAccountRecord = true + return false + } + + override fun getMatching(record: SignalAccountRecord, keyGenerator: StorageKeyGenerator): Optional { + return Optional.of(localAccountRecord) + } + + override fun merge(remote: SignalAccountRecord, local: SignalAccountRecord, keyGenerator: StorageKeyGenerator): SignalAccountRecord { + val givenName: String + val familyName: String + + if (remote.givenName.isPresent || remote.familyName.isPresent) { + givenName = remote.givenName.orElse("") + familyName = remote.familyName.orElse("") + } else { + givenName = local.givenName.orElse("") + familyName = local.familyName.orElse("") + } + + val payments = if (remote.payments.entropy.isPresent) { + remote.payments + } else { + local.payments + } + + val subscriber = if (remote.subscriber.id.isPresent) { + remote.subscriber + } else { + local.subscriber + } + + val backupsSubscriber = if (remote.subscriber.id.isPresent) { + remote.subscriber + } else { + local.subscriber + } + val storyViewReceiptsState = if (remote.storyViewReceiptsState == OptionalBool.UNSET) { + local.storyViewReceiptsState + } else { + remote.storyViewReceiptsState + } + + val unknownFields = remote.serializeUnknownFields() + val avatarUrlPath = OptionalUtil.or(remote.avatarUrlPath, local.avatarUrlPath).orElse("") + val profileKey = OptionalUtil.or(remote.profileKey, local.profileKey).orElse(null) + val noteToSelfArchived = remote.isNoteToSelfArchived + val noteToSelfForcedUnread = remote.isNoteToSelfForcedUnread + val readReceipts = remote.isReadReceiptsEnabled + val typingIndicators = remote.isTypingIndicatorsEnabled + val sealedSenderIndicators = remote.isSealedSenderIndicatorsEnabled + val linkPreviews = remote.isLinkPreviewsEnabled + val unlisted = remote.isPhoneNumberUnlisted + val pinnedConversations = remote.pinnedConversations + val phoneNumberSharingMode = remote.phoneNumberSharingMode + val preferContactAvatars = remote.isPreferContactAvatars + val universalExpireTimer = remote.universalExpireTimer + val primarySendsSms = if (SignalStore.account.isPrimaryDevice) local.isPrimarySendsSms else remote.isPrimarySendsSms + val e164 = if (SignalStore.account.isPrimaryDevice) local.e164 else remote.e164 + val defaultReactions = if (remote.defaultReactions.size > 0) remote.defaultReactions else local.defaultReactions + val displayBadgesOnProfile = remote.isDisplayBadgesOnProfile + val subscriptionManuallyCancelled = remote.isSubscriptionManuallyCancelled + val keepMutedChatsArchived = remote.isKeepMutedChatsArchived + val hasSetMyStoriesPrivacy = remote.hasSetMyStoriesPrivacy() + val hasViewedOnboardingStory = remote.hasViewedOnboardingStory() || local.hasViewedOnboardingStory() + val storiesDisabled = remote.isStoriesDisabled + val hasSeenGroupStoryEducation = remote.hasSeenGroupStoryEducationSheet() || local.hasSeenGroupStoryEducationSheet() + val hasSeenUsernameOnboarding = remote.hasCompletedUsernameOnboarding() || local.hasCompletedUsernameOnboarding() + val username = remote.username + val usernameLink = remote.usernameLink + + val matchesRemote = doParamsMatch( + contact = remote, + unknownFields = unknownFields, + givenName = givenName, + familyName = familyName, + avatarUrlPath = avatarUrlPath, + profileKey = profileKey, + noteToSelfArchived = noteToSelfArchived, + noteToSelfForcedUnread = noteToSelfForcedUnread, + readReceipts = readReceipts, + typingIndicators = typingIndicators, + sealedSenderIndicators = sealedSenderIndicators, + linkPreviewsEnabled = linkPreviews, + phoneNumberSharingMode = phoneNumberSharingMode, + unlistedPhoneNumber = unlisted, + pinnedConversations = pinnedConversations, + preferContactAvatars = preferContactAvatars, + payments = payments, + universalExpireTimer = universalExpireTimer, + primarySendsSms = primarySendsSms, + e164 = e164, + defaultReactions = defaultReactions, + subscriber = subscriber, + displayBadgesOnProfile = displayBadgesOnProfile, + subscriptionManuallyCancelled = subscriptionManuallyCancelled, + keepMutedChatsArchived = keepMutedChatsArchived, + hasSetMyStoriesPrivacy = hasSetMyStoriesPrivacy, + hasViewedOnboardingStory = hasViewedOnboardingStory, + hasCompletedUsernameOnboarding = hasSeenUsernameOnboarding, + storiesDisabled = storiesDisabled, + storyViewReceiptsState = storyViewReceiptsState, + username = username, + usernameLink = usernameLink, + backupsSubscriber = backupsSubscriber + ) + val matchesLocal = doParamsMatch( + contact = local, + unknownFields = unknownFields, + givenName = givenName, + familyName = familyName, + avatarUrlPath = avatarUrlPath, + profileKey = profileKey, + noteToSelfArchived = noteToSelfArchived, + noteToSelfForcedUnread = noteToSelfForcedUnread, + readReceipts = readReceipts, + typingIndicators = typingIndicators, + sealedSenderIndicators = sealedSenderIndicators, + linkPreviewsEnabled = linkPreviews, + phoneNumberSharingMode = phoneNumberSharingMode, + unlistedPhoneNumber = unlisted, + pinnedConversations = pinnedConversations, + preferContactAvatars = preferContactAvatars, + payments = payments, + universalExpireTimer = universalExpireTimer, + primarySendsSms = primarySendsSms, + e164 = e164, + defaultReactions = defaultReactions, + subscriber = subscriber, + displayBadgesOnProfile = displayBadgesOnProfile, + subscriptionManuallyCancelled = subscriptionManuallyCancelled, + keepMutedChatsArchived = keepMutedChatsArchived, + hasSetMyStoriesPrivacy = hasSetMyStoriesPrivacy, + hasViewedOnboardingStory = hasViewedOnboardingStory, + hasCompletedUsernameOnboarding = hasSeenUsernameOnboarding, + storiesDisabled = storiesDisabled, + storyViewReceiptsState = storyViewReceiptsState, + username = username, + usernameLink = usernameLink, + backupsSubscriber = backupsSubscriber + ) + + if (matchesRemote) { + return remote + } else if (matchesLocal) { + return local + } else { + val builder = SignalAccountRecord.Builder(keyGenerator.generate(), unknownFields) + .setGivenName(givenName) + .setFamilyName(familyName) + .setAvatarUrlPath(avatarUrlPath) + .setProfileKey(profileKey) + .setNoteToSelfArchived(noteToSelfArchived) + .setNoteToSelfForcedUnread(noteToSelfForcedUnread) + .setReadReceiptsEnabled(readReceipts) + .setTypingIndicatorsEnabled(typingIndicators) + .setSealedSenderIndicatorsEnabled(sealedSenderIndicators) + .setLinkPreviewsEnabled(linkPreviews) + .setUnlistedPhoneNumber(unlisted) + .setPhoneNumberSharingMode(phoneNumberSharingMode) + .setUnlistedPhoneNumber(unlisted) + .setPinnedConversations(pinnedConversations) + .setPreferContactAvatars(preferContactAvatars) + .setPayments(payments.isEnabled, payments.entropy.orElse(null)) + .setUniversalExpireTimer(universalExpireTimer) + .setPrimarySendsSms(primarySendsSms) + .setDefaultReactions(defaultReactions) + .setSubscriber(subscriber) + .setStoryViewReceiptsState(storyViewReceiptsState) + .setDisplayBadgesOnProfile(displayBadgesOnProfile) + .setSubscriptionManuallyCancelled(subscriptionManuallyCancelled) + .setKeepMutedChatsArchived(keepMutedChatsArchived) + .setHasSetMyStoriesPrivacy(hasSetMyStoriesPrivacy) + .setHasViewedOnboardingStory(hasViewedOnboardingStory) + .setStoriesDisabled(storiesDisabled) + .setHasSeenGroupStoryEducationSheet(hasSeenGroupStoryEducation) + .setHasCompletedUsernameOnboarding(hasSeenUsernameOnboarding) + .setUsername(username) + .setUsernameLink(usernameLink) + .setBackupsSubscriber(backupsSubscriber) + + return builder.build() + } + } + + override fun insertLocal(record: SignalAccountRecord) { + throw UnsupportedOperationException("We should always have a local AccountRecord, so we should never been inserting a new one.") + } + + override fun updateLocal(update: StorageRecordUpdate) { + applyAccountStorageSyncUpdates(context, self, update, true) + } + + override fun compare(lhs: SignalAccountRecord, rhs: SignalAccountRecord): Int { + return 0 + } + + private fun doParamsMatch( + contact: SignalAccountRecord, + unknownFields: ByteArray?, + givenName: String, + familyName: String, + avatarUrlPath: String, + profileKey: ByteArray?, + noteToSelfArchived: Boolean, + noteToSelfForcedUnread: Boolean, + readReceipts: Boolean, + typingIndicators: Boolean, + sealedSenderIndicators: Boolean, + linkPreviewsEnabled: Boolean, + phoneNumberSharingMode: AccountRecord.PhoneNumberSharingMode, + unlistedPhoneNumber: Boolean, + pinnedConversations: List, + preferContactAvatars: Boolean, + payments: SignalAccountRecord.Payments, + universalExpireTimer: Int, + primarySendsSms: Boolean, + e164: String, + defaultReactions: List, + subscriber: SignalAccountRecord.Subscriber, + displayBadgesOnProfile: Boolean, + subscriptionManuallyCancelled: Boolean, + keepMutedChatsArchived: Boolean, + hasSetMyStoriesPrivacy: Boolean, + hasViewedOnboardingStory: Boolean, + hasCompletedUsernameOnboarding: Boolean, + storiesDisabled: Boolean, + storyViewReceiptsState: OptionalBool, + username: String?, + usernameLink: AccountRecord.UsernameLink?, + backupsSubscriber: SignalAccountRecord.Subscriber + ): Boolean { + return contact.serializeUnknownFields().contentEquals(unknownFields) && + contact.givenName.orElse("") == givenName && + contact.familyName.orElse("") == familyName && + contact.avatarUrlPath.orElse("") == avatarUrlPath && + contact.payments == payments && + contact.e164 == e164 && + contact.defaultReactions == defaultReactions && + contact.profileKey.orElse(null).contentEquals(profileKey) && + contact.isNoteToSelfArchived == noteToSelfArchived && + contact.isNoteToSelfForcedUnread == noteToSelfForcedUnread && + contact.isReadReceiptsEnabled == readReceipts && + contact.isTypingIndicatorsEnabled == typingIndicators && + contact.isSealedSenderIndicatorsEnabled == sealedSenderIndicators && + contact.isLinkPreviewsEnabled == linkPreviewsEnabled && + contact.phoneNumberSharingMode == phoneNumberSharingMode && + contact.isPhoneNumberUnlisted == unlistedPhoneNumber && + contact.isPreferContactAvatars == preferContactAvatars && + contact.universalExpireTimer == universalExpireTimer && + contact.isPrimarySendsSms == primarySendsSms && + contact.pinnedConversations == pinnedConversations && + contact.subscriber == subscriber && + contact.isDisplayBadgesOnProfile == displayBadgesOnProfile && + contact.isSubscriptionManuallyCancelled == subscriptionManuallyCancelled && + contact.isKeepMutedChatsArchived == keepMutedChatsArchived && + contact.hasSetMyStoriesPrivacy() == hasSetMyStoriesPrivacy && + contact.hasViewedOnboardingStory() == hasViewedOnboardingStory && + contact.hasCompletedUsernameOnboarding() == hasCompletedUsernameOnboarding && + contact.isStoriesDisabled == storiesDisabled && + contact.storyViewReceiptsState == storyViewReceiptsState && + contact.username == username && + contact.usernameLink == usernameLink && + contact.backupsSubscriber == backupsSubscriber + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/CallLinkRecordProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/storage/CallLinkRecordProcessor.kt index adb77a5673..d446df0560 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/CallLinkRecordProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/CallLinkRecordProcessor.kt @@ -26,11 +26,11 @@ internal class CallLinkRecordProcessor : DefaultStorageRecordProcessor 0L } - internal override fun getMatching(remote: SignalCallLinkRecord, keyGenerator: StorageKeyGenerator): Optional { + override fun getMatching(remote: SignalCallLinkRecord, keyGenerator: StorageKeyGenerator): Optional { Log.d(TAG, "Attempting to get matching record...") val rootKey = CallLinkRootKey(remote.rootKey) val roomId = CallLinkRoomId.fromCallLinkRootKey(rootKey) @@ -52,7 +52,7 @@ internal class CallLinkRecordProcessor : DefaultStorageRecordProcessor remoteRecords, @NonNull StorageKeyGenerator keyGenerator) throws IOException { + public void process(@NonNull Collection remoteRecords, @NonNull StorageKeyGenerator keyGenerator) throws IOException { List unregisteredAciOnly = new ArrayList<>(); for (SignalContactRecord remoteRecord : remoteRecords) { @@ -92,7 +93,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor getMatching(@NonNull SignalContactRecord remote, @NonNull StorageKeyGenerator keyGenerator) { + public @NonNull Optional getMatching(@NonNull SignalContactRecord remote, @NonNull StorageKeyGenerator keyGenerator) { Optional found = remote.getAci().isPresent() ? recipientTable.getByAci(remote.getAci().get()) : Optional.empty(); if (found.isEmpty() && remote.getNumber().isPresent()) { @@ -141,7 +142,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor update) { + public void updateLocal(@NonNull StorageRecordUpdate update) { recipientTable.applyStorageSyncContactUpdate(update); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/DefaultStorageRecordProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/storage/DefaultStorageRecordProcessor.java deleted file mode 100644 index 9e21eaf629..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/DefaultStorageRecordProcessor.java +++ /dev/null @@ -1,103 +0,0 @@ -package org.thoughtcrime.securesms.storage; - -import androidx.annotation.NonNull; - -import org.signal.core.util.logging.Log; -import org.whispersystems.signalservice.api.storage.SignalRecord; - -import java.io.IOException; -import java.util.Collection; -import java.util.Comparator; -import java.util.Optional; -import java.util.Set; -import java.util.TreeSet; - -/** - * An implementation of {@link StorageRecordProcessor} that solidifies a pattern and reduces - * duplicate code in individual implementations. - * - * Concerning the implementation of {@link #compare(Object, Object)}, it's purpose is to detect if - * two items would map to the same logical entity (i.e. they would correspond to the same record in - * our local store). We use it for a {@link TreeSet}, so mainly it's just important that the '0' - * case is correct. Other cases are whatever, just make it something stable. - */ -abstract class DefaultStorageRecordProcessor implements StorageRecordProcessor, Comparator { - - private static final String TAG = Log.tag(DefaultStorageRecordProcessor.class); - - /** - * One type of invalid remote data this handles is two records mapping to the same local data. We - * have to trim this bad data out, because if we don't, we'll upload an ID set that only has one - * of the IDs in it, but won't properly delete the dupes, which will then fail our validation - * checks. - * - * This is a bit tricky -- as we process records, ID's are written back to the local store, so we - * can't easily be like "oh multiple records are mapping to the same local storage ID". And in - * general we rely on SignalRecords to implement an equals() that includes the StorageId, so using - * a regular set is out. Instead, we use a {@link TreeSet}, which allows us to define a custom - * comparator for checking equality. Then we delegate to the subclass to tell us if two items are - * the same based on their actual data (i.e. two contacts having the same UUID, or two groups - * having the same MasterKey). - */ - @Override - public void process(@NonNull Collection remoteRecords, @NonNull StorageKeyGenerator keyGenerator) throws IOException { - Set matchedRecords = new TreeSet<>(this); - int i = 0; - - for (E remote : remoteRecords) { - if (isInvalid(remote)) { - warn(i, remote, "Found invalid key! Ignoring it."); - } else { - Optional local = getMatching(remote, keyGenerator); - - if (local.isPresent()) { - E merged = merge(remote, local.get(), keyGenerator); - - if (matchedRecords.contains(local.get())) { - warn(i, remote, "Multiple remote records map to the same local record! Ignoring this one."); - } else { - matchedRecords.add(local.get()); - - if (!merged.equals(remote)) { - info(i, remote, "[Remote Update] " + new StorageRecordUpdate<>(remote, merged).toString()); - } - - if (!merged.equals(local.get())) { - StorageRecordUpdate update = new StorageRecordUpdate<>(local.get(), merged); - info(i, remote, "[Local Update] " + update.toString()); - updateLocal(update); - } - } - } else { - info(i, remote, "No matching local record. Inserting."); - insertLocal(remote); - } - } - - i++; - } - } - - private void info(int i, E record, String message) { - Log.i(TAG, "[" + i + "][" + record.getClass().getSimpleName() + "] " + message); - } - - private void warn(int i, E record, String message) { - Log.w(TAG, "[" + i + "][" + record.getClass().getSimpleName() + "] " + message); - } - - /** - * @return True if the record is invalid and should be removed from storage service, otherwise false. - */ - abstract boolean isInvalid(@NonNull E remote); - - /** - * Only records that pass the validity check (i.e. return false from {@link #isInvalid(SignalRecord)} - * make it to here, so you can assume all records are valid. - */ - abstract @NonNull Optional getMatching(@NonNull E remote, @NonNull StorageKeyGenerator keyGenerator); - - abstract @NonNull E merge(@NonNull E remote, @NonNull E local, @NonNull StorageKeyGenerator keyGenerator); - abstract void insertLocal(@NonNull E record) throws IOException; - abstract void updateLocal(@NonNull StorageRecordUpdate update); -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/DefaultStorageRecordProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/storage/DefaultStorageRecordProcessor.kt new file mode 100644 index 0000000000..62f9f86a8d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/DefaultStorageRecordProcessor.kt @@ -0,0 +1,100 @@ +package org.thoughtcrime.securesms.storage + +import org.signal.core.util.logging.Log +import org.whispersystems.signalservice.api.storage.SignalRecord +import java.io.IOException +import java.util.Optional +import java.util.TreeSet + +/** + * An implementation of [StorageRecordProcessor] that solidifies a pattern and reduces + * duplicate code in individual implementations. + * + * Concerning the implementation of [.compare], it's purpose is to detect if + * two items would map to the same logical entity (i.e. they would correspond to the same record in + * our local store). We use it for a [TreeSet], so mainly it's just important that the '0' + * case is correct. Other cases are whatever, just make it something stable. + */ +abstract class DefaultStorageRecordProcessor : StorageRecordProcessor, Comparator { + companion object { + private val TAG = Log.tag(DefaultStorageRecordProcessor::class.java) + } + + /** + * One type of invalid remote data this handles is two records mapping to the same local data. We + * have to trim this bad data out, because if we don't, we'll upload an ID set that only has one + * of the IDs in it, but won't properly delete the dupes, which will then fail our validation + * checks. + * + * This is a bit tricky -- as we process records, ID's are written back to the local store, so we + * can't easily be like "oh multiple records are mapping to the same local storage ID". And in + * general we rely on SignalRecords to implement an equals() that includes the StorageId, so using + * a regular set is out. Instead, we use a [TreeSet], which allows us to define a custom + * comparator for checking equality. Then we delegate to the subclass to tell us if two items are + * the same based on their actual data (i.e. two contacts having the same UUID, or two groups + * having the same MasterKey). + */ + @Throws(IOException::class) + override fun process(remoteRecords: Collection, keyGenerator: StorageKeyGenerator) { + val matchedRecords: MutableSet = TreeSet(this) + var i = 0 + + for (remote in remoteRecords) { + if (isInvalid(remote)) { + warn(i, remote, "Found invalid key! Ignoring it.") + } else { + val local = getMatching(remote, keyGenerator) + + if (local.isPresent) { + val merged = merge(remote, local.get(), keyGenerator) + + if (matchedRecords.contains(local.get())) { + warn(i, remote, "Multiple remote records map to the same local record! Ignoring this one.") + } else { + matchedRecords.add(local.get()) + + if (merged != remote) { + info(i, remote, "[Remote Update] " + StorageRecordUpdate(remote, merged).toString()) + } + + if (merged != local.get()) { + val update = StorageRecordUpdate(local.get(), merged) + info(i, remote, "[Local Update] $update") + updateLocal(update) + } + } + } else { + info(i, remote, "No matching local record. Inserting.") + insertLocal(remote) + } + } + + i++ + } + } + + private fun info(i: Int, record: E, message: String) { + Log.i(TAG, "[$i][${record.javaClass.getSimpleName()}] $message") + } + + private fun warn(i: Int, record: E, message: String) { + Log.w(TAG, "[$i][${record.javaClass.getSimpleName()}] $message") + } + + /** + * @return True if the record is invalid and should be removed from storage service, otherwise false. + */ + abstract fun isInvalid(remote: E): Boolean + + /** + * Only records that pass the validity check (i.e. return false from [.isInvalid] + * make it to here, so you can assume all records are valid. + */ + abstract fun getMatching(remote: E, keyGenerator: StorageKeyGenerator): Optional + + abstract fun merge(remote: E, local: E, keyGenerator: StorageKeyGenerator): E + + @Throws(IOException::class) + abstract fun insertLocal(record: E) + abstract fun updateLocal(update: StorageRecordUpdate) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV1RecordProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV1RecordProcessor.java index f7c05af275..577ea3c361 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV1RecordProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV1RecordProcessor.java @@ -45,7 +45,7 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor< * Note: This method could be written more succinctly, but the logs are useful :) */ @Override - boolean isInvalid(@NonNull SignalGroupV1Record remote) { + public boolean isInvalid(@NonNull SignalGroupV1Record remote) { try { GroupId.V1 id = GroupId.v1(remote.getGroupId()); Optional v2Record = groupDatabase.getGroup(id.deriveV2MigrationGroupId()); @@ -63,7 +63,7 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor< } @Override - @NonNull Optional getMatching(@NonNull SignalGroupV1Record record, @NonNull StorageKeyGenerator keyGenerator) { + public @NonNull Optional getMatching(@NonNull SignalGroupV1Record record, @NonNull StorageKeyGenerator keyGenerator) { GroupId.V1 groupId = GroupId.v1orThrow(record.getGroupId()); Optional recipientId = recipientTable.getByGroupId(groupId); @@ -74,7 +74,7 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor< } @Override - @NonNull SignalGroupV1Record merge(@NonNull SignalGroupV1Record remote, @NonNull SignalGroupV1Record local, @NonNull StorageKeyGenerator keyGenerator) { + public @NonNull SignalGroupV1Record merge(@NonNull SignalGroupV1Record remote, @NonNull SignalGroupV1Record local, @NonNull StorageKeyGenerator keyGenerator) { byte[] unknownFields = remote.serializeUnknownFields(); boolean blocked = remote.isBlocked(); boolean profileSharing = remote.isProfileSharingEnabled(); @@ -101,12 +101,12 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor< } @Override - void insertLocal(@NonNull SignalGroupV1Record record) { + public void insertLocal(@NonNull SignalGroupV1Record record) { recipientTable.applyStorageSyncGroupV1Insert(record); } @Override - void updateLocal(@NonNull StorageRecordUpdate update) { + public void updateLocal(@NonNull StorageRecordUpdate update) { recipientTable.applyStorageSyncGroupV1Update(update); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2RecordProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2RecordProcessor.java index c0909f5b88..dde55f91d0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2RecordProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2RecordProcessor.java @@ -40,12 +40,12 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor< } @Override - boolean isInvalid(@NonNull SignalGroupV2Record remote) { + public boolean isInvalid(@NonNull SignalGroupV2Record remote) { return remote.getMasterKeyBytes().length != GroupMasterKey.SIZE; } @Override - @NonNull Optional getMatching(@NonNull SignalGroupV2Record record, @NonNull StorageKeyGenerator keyGenerator) { + public @NonNull Optional getMatching(@NonNull SignalGroupV2Record record, @NonNull StorageKeyGenerator keyGenerator) { GroupId.V2 groupId = GroupId.v2(record.getMasterKeyOrThrow()); Optional recipientId = recipientTable.getByGroupId(groupId); @@ -64,7 +64,7 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor< } @Override - @NonNull SignalGroupV2Record merge(@NonNull SignalGroupV2Record remote, @NonNull SignalGroupV2Record local, @NonNull StorageKeyGenerator keyGenerator) { + public @NonNull SignalGroupV2Record merge(@NonNull SignalGroupV2Record remote, @NonNull SignalGroupV2Record local, @NonNull StorageKeyGenerator keyGenerator) { byte[] unknownFields = remote.serializeUnknownFields(); boolean blocked = remote.isBlocked(); boolean profileSharing = remote.isProfileSharingEnabled(); @@ -97,12 +97,12 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor< } @Override - void insertLocal(@NonNull SignalGroupV2Record record) { + public void insertLocal(@NonNull SignalGroupV2Record record) { recipientTable.applyStorageSyncGroupV2Insert(record); } @Override - void updateLocal(@NonNull StorageRecordUpdate update) { + public void updateLocal(@NonNull StorageRecordUpdate update) { recipientTable.applyStorageSyncGroupV2Update(update); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageRecordProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageRecordProcessor.java deleted file mode 100644 index 43f860d134..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageRecordProcessor.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.thoughtcrime.securesms.storage; - -import androidx.annotation.NonNull; - -import org.whispersystems.signalservice.api.storage.SignalRecord; - -import java.io.IOException; -import java.util.Collection; - -/** - * Handles processing a remote record, which involves applying any local changes that need to be - * made based on the remote records. - */ -public interface StorageRecordProcessor { - void process(@NonNull Collection remoteRecords, @NonNull StorageKeyGenerator keyGenerator) throws IOException; -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageRecordProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageRecordProcessor.kt new file mode 100644 index 0000000000..35ec247b53 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageRecordProcessor.kt @@ -0,0 +1,13 @@ +package org.thoughtcrime.securesms.storage + +import org.whispersystems.signalservice.api.storage.SignalRecord +import java.io.IOException + +/** + * Handles processing a remote record, which involves applying any local changes that need to be + * made based on the remote records. + */ +interface StorageRecordProcessor { + @Throws(IOException::class) + fun process(remoteRecords: Collection, keyGenerator: StorageKeyGenerator) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageRecordUpdate.java b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageRecordUpdate.java index b66da90d5e..a4b94c6318 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageRecordUpdate.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageRecordUpdate.java @@ -14,7 +14,6 @@ public class StorageRecordUpdate { private final E oldRecord; private final E newRecord; - @VisibleForTesting public StorageRecordUpdate(@NonNull E oldRecord, @NonNull E newRecord) { this.oldRecord = oldRecord; this.newRecord = newRecord; diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StoryDistributionListRecordProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/storage/StoryDistributionListRecordProcessor.java index 3bcc6a3aef..6c191ec6c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StoryDistributionListRecordProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StoryDistributionListRecordProcessor.java @@ -16,6 +16,7 @@ import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.IOException; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -35,7 +36,7 @@ public class StoryDistributionListRecordProcessor extends DefaultStorageRecordPr * */ @Override - boolean isInvalid(@NonNull SignalStoryDistributionListRecord remote) { + public boolean isInvalid(@NonNull SignalStoryDistributionListRecord remote) { UUID remoteUuid = UuidUtil.parseOrNull(remote.getIdentifier()); if (remoteUuid == null) { Log.d(TAG, "Bad distribution list identifier -- marking as invalid"); @@ -68,7 +69,7 @@ public class StoryDistributionListRecordProcessor extends DefaultStorageRecordPr } @Override - @NonNull Optional getMatching(@NonNull SignalStoryDistributionListRecord remote, @NonNull StorageKeyGenerator keyGenerator) { + public @NonNull Optional getMatching(@NonNull SignalStoryDistributionListRecord remote, @NonNull StorageKeyGenerator keyGenerator) { Log.d(TAG, "Attempting to get matching record..."); RecipientId matching = SignalDatabase.distributionLists().getRecipientIdForSyncRecord(remote); if (matching == null && UuidUtil.parseOrThrow(remote.getIdentifier()).equals(DistributionId.MY_STORY.asUuid())) { @@ -104,7 +105,7 @@ public class StoryDistributionListRecordProcessor extends DefaultStorageRecordPr } @Override - @NonNull SignalStoryDistributionListRecord merge(@NonNull SignalStoryDistributionListRecord remote, @NonNull SignalStoryDistributionListRecord local, @NonNull StorageKeyGenerator keyGenerator) { + public @NonNull SignalStoryDistributionListRecord merge(@NonNull SignalStoryDistributionListRecord remote, @NonNull SignalStoryDistributionListRecord local, @NonNull StorageKeyGenerator keyGenerator) { byte[] unknownFields = remote.serializeUnknownFields(); byte[] identifier = remote.getIdentifier(); String name = remote.getName(); @@ -133,12 +134,12 @@ public class StoryDistributionListRecordProcessor extends DefaultStorageRecordPr } @Override - void insertLocal(@NonNull SignalStoryDistributionListRecord record) throws IOException { + public void insertLocal(@NonNull SignalStoryDistributionListRecord record) throws IOException { SignalDatabase.distributionLists().applyStorageSyncStoryDistributionListInsert(record); } @Override - void updateLocal(@NonNull StorageRecordUpdate update) { + public void updateLocal(@NonNull StorageRecordUpdate update) { SignalDatabase.distributionLists().applyStorageSyncStoryDistributionListUpdate(update); }