Convert AccountRecordProcessor to kotlin.

This commit is contained in:
Greyson Parrelli 2024-11-07 13:03:54 -05:00
parent 4273d9e3d7
commit 0f8580c398
12 changed files with 448 additions and 411 deletions

View file

@ -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<SignalAccountRecord> {
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<SignalAccountRecord> 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<PinnedConversation> 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<String> 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<SignalAccountRecord> 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<PinnedConversation> pinnedConversations,
boolean preferContactAvatars,
SignalAccountRecord.Payments payments,
int universalExpireTimer,
boolean primarySendsSms,
String e164,
@NonNull List <String> 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);
}
}

View file

@ -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<SignalAccountRecord>() {
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<SignalAccountRecord> {
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<SignalAccountRecord>) {
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<SignalAccountRecord.PinnedConversation>,
preferContactAvatars: Boolean,
payments: SignalAccountRecord.Payments,
universalExpireTimer: Int,
primarySendsSms: Boolean,
e164: String,
defaultReactions: List<String>,
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
}
}

View file

@ -26,11 +26,11 @@ internal class CallLinkRecordProcessor : DefaultStorageRecordProcessor<SignalCal
}
}
internal override fun isInvalid(remote: SignalCallLinkRecord): Boolean {
override fun isInvalid(remote: SignalCallLinkRecord): Boolean {
return remote.adminPassKey.isNotEmpty() && remote.deletionTimestamp > 0L
}
internal override fun getMatching(remote: SignalCallLinkRecord, keyGenerator: StorageKeyGenerator): Optional<SignalCallLinkRecord> {
override fun getMatching(remote: SignalCallLinkRecord, keyGenerator: StorageKeyGenerator): Optional<SignalCallLinkRecord> {
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<SignalCal
* An earlier deletion takes precedence over a later deletion
* Other fields should not change, except for the clearing of the admin passkey on deletion
*/
internal override fun merge(remote: SignalCallLinkRecord, local: SignalCallLinkRecord, keyGenerator: StorageKeyGenerator): SignalCallLinkRecord {
override fun merge(remote: SignalCallLinkRecord, local: SignalCallLinkRecord, keyGenerator: StorageKeyGenerator): SignalCallLinkRecord {
return if (remote.isDeleted() && local.isDeleted()) {
if (remote.deletionTimestamp < local.deletionTimestamp) {
remote

View file

@ -61,8 +61,9 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
* The reasons are nuanced, but the TL;DR is that we want to split unregistered users into separate rows so that a user
* could re-register and get a different ACI.
*/
@Override
public void process(@NonNull Collection<SignalContactRecord> remoteRecords, @NonNull StorageKeyGenerator keyGenerator) throws IOException {
public void process(@NonNull Collection<? extends SignalContactRecord> remoteRecords, @NonNull StorageKeyGenerator keyGenerator) throws IOException {
List<SignalContactRecord> unregisteredAciOnly = new ArrayList<>();
for (SignalContactRecord remoteRecord : remoteRecords) {
@ -92,7 +93,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
* Note: This method could be written more succinctly, but the logs are useful :)
*/
@Override
boolean isInvalid(@NonNull SignalContactRecord remote) {
public boolean isInvalid(@NonNull SignalContactRecord remote) {
boolean hasAci = remote.getAci().isPresent() && remote.getAci().get().isValid();
boolean hasPni = remote.getPni().isPresent() && remote.getPni().get().isValid();
@ -114,7 +115,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
}
@Override
@NonNull Optional<SignalContactRecord> getMatching(@NonNull SignalContactRecord remote, @NonNull StorageKeyGenerator keyGenerator) {
public @NonNull Optional<SignalContactRecord> getMatching(@NonNull SignalContactRecord remote, @NonNull StorageKeyGenerator keyGenerator) {
Optional<RecipientId> 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<Signal
}
@Override
@NonNull SignalContactRecord merge(@NonNull SignalContactRecord remote, @NonNull SignalContactRecord local, @NonNull StorageKeyGenerator keyGenerator) {
public @NonNull SignalContactRecord merge(@NonNull SignalContactRecord remote, @NonNull SignalContactRecord local, @NonNull StorageKeyGenerator keyGenerator) {
String profileGivenName;
String profileFamilyName;
@ -258,12 +259,12 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
}
@Override
void insertLocal(@NonNull SignalContactRecord record) {
public void insertLocal(@NonNull SignalContactRecord record) {
recipientTable.applyStorageSyncContactInsert(record);
}
@Override
void updateLocal(@NonNull StorageRecordUpdate<SignalContactRecord> update) {
public void updateLocal(@NonNull StorageRecordUpdate<SignalContactRecord> update) {
recipientTable.applyStorageSyncContactUpdate(update);
}

View file

@ -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<E extends SignalRecord> implements StorageRecordProcessor<E>, Comparator<E> {
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<E> remoteRecords, @NonNull StorageKeyGenerator keyGenerator) throws IOException {
Set<E> matchedRecords = new TreeSet<>(this);
int i = 0;
for (E remote : remoteRecords) {
if (isInvalid(remote)) {
warn(i, remote, "Found invalid key! Ignoring it.");
} else {
Optional<E> 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<E> 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<E> 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<E> update);
}

View file

@ -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<E : SignalRecord> : StorageRecordProcessor<E>, Comparator<E> {
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<E>, keyGenerator: StorageKeyGenerator) {
val matchedRecords: MutableSet<E> = 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<E>
abstract fun merge(remote: E, local: E, keyGenerator: StorageKeyGenerator): E
@Throws(IOException::class)
abstract fun insertLocal(record: E)
abstract fun updateLocal(update: StorageRecordUpdate<E>)
}

View file

@ -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<GroupRecord> v2Record = groupDatabase.getGroup(id.deriveV2MigrationGroupId());
@ -63,7 +63,7 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
}
@Override
@NonNull Optional<SignalGroupV1Record> getMatching(@NonNull SignalGroupV1Record record, @NonNull StorageKeyGenerator keyGenerator) {
public @NonNull Optional<SignalGroupV1Record> getMatching(@NonNull SignalGroupV1Record record, @NonNull StorageKeyGenerator keyGenerator) {
GroupId.V1 groupId = GroupId.v1orThrow(record.getGroupId());
Optional<RecipientId> 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<SignalGroupV1Record> update) {
public void updateLocal(@NonNull StorageRecordUpdate<SignalGroupV1Record> update) {
recipientTable.applyStorageSyncGroupV1Update(update);
}

View file

@ -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<SignalGroupV2Record> getMatching(@NonNull SignalGroupV2Record record, @NonNull StorageKeyGenerator keyGenerator) {
public @NonNull Optional<SignalGroupV2Record> getMatching(@NonNull SignalGroupV2Record record, @NonNull StorageKeyGenerator keyGenerator) {
GroupId.V2 groupId = GroupId.v2(record.getMasterKeyOrThrow());
Optional<RecipientId> 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<SignalGroupV2Record> update) {
public void updateLocal(@NonNull StorageRecordUpdate<SignalGroupV2Record> update) {
recipientTable.applyStorageSyncGroupV2Update(update);
}

View file

@ -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<E extends SignalRecord> {
void process(@NonNull Collection<E> remoteRecords, @NonNull StorageKeyGenerator keyGenerator) throws IOException;
}

View file

@ -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<E : SignalRecord?> {
@Throws(IOException::class)
fun process(remoteRecords: Collection<E>, keyGenerator: StorageKeyGenerator)
}

View file

@ -14,7 +14,6 @@ public class StorageRecordUpdate<E extends SignalRecord> {
private final E oldRecord;
private final E newRecord;
@VisibleForTesting
public StorageRecordUpdate(@NonNull E oldRecord, @NonNull E newRecord) {
this.oldRecord = oldRecord;
this.newRecord = newRecord;

View file

@ -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
* </ul>
*/
@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<SignalStoryDistributionListRecord> getMatching(@NonNull SignalStoryDistributionListRecord remote, @NonNull StorageKeyGenerator keyGenerator) {
public @NonNull Optional<SignalStoryDistributionListRecord> 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<SignalStoryDistributionListRecord> update) {
public void updateLocal(@NonNull StorageRecordUpdate<SignalStoryDistributionListRecord> update) {
SignalDatabase.distributionLists().applyStorageSyncStoryDistributionListUpdate(update);
}