diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest_applyStorageSyncContactUpdate.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest_applyStorageSyncContactUpdate.kt index dfd59bf614..e7ef1645e4 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest_applyStorageSyncContactUpdate.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest_applyStorageSyncContactUpdate.kt @@ -37,7 +37,7 @@ class RecipientTableTest_applyStorageSyncContactUpdate { val oldRecord: SignalContactRecord = StorageSyncModels.localToRemoteRecord(SignalDatabase.recipients.getRecordForSync(harness.others[0])!!).contact.get() val newProto = oldRecord - .toProto() + .proto .newBuilder() .identityState(ContactRecord.IdentityState.DEFAULT) .build() diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index b706bc1b47..ee194e8f8a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -17,8 +17,11 @@ import org.signal.core.util.SqlUtil import org.signal.core.util.delete import org.signal.core.util.exists import org.signal.core.util.forEach +import org.signal.core.util.hasUnknownFields +import org.signal.core.util.isNotEmpty import org.signal.core.util.logging.Log import org.signal.core.util.nullIfBlank +import org.signal.core.util.nullIfEmpty import org.signal.core.util.optionalString import org.signal.core.util.or import org.signal.core.util.orNull @@ -1024,12 +1027,13 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da } fun applyStorageSyncAccountUpdate(update: StorageRecordUpdate) { - val profileName = ProfileName.fromParts(update.new.givenName.orElse(null), update.new.familyName.orElse(null)) - val localKey = ProfileKeyUtil.profileKeyOptional(update.old.profileKey.orElse(null)) - val remoteKey = ProfileKeyUtil.profileKeyOptional(update.new.profileKey.orElse(null)) - val profileKey: String? = remoteKey.or(localKey).map { obj: ProfileKey -> obj.serialize() }.map { source: ByteArray? -> Base64.encodeWithPadding(source!!) }.orElse(null) - if (!remoteKey.isPresent) { - Log.w(TAG, "Got an empty profile key while applying an account record update! The parsed local key is ${if (localKey.isPresent) "present" else "not present"}. The raw local key is ${if (update.old.profileKey.isPresent) "present" else "not present"}. The resulting key is ${if (profileKey != null) "present" else "not present"}.") + val profileName = ProfileName.fromParts(update.new.proto.givenName, update.new.proto.familyName) + val localKey = update.old.proto.profileKey.nullIfEmpty()?.toByteArray()?.let { ProfileKeyUtil.profileKeyOrNull(it) } + val remoteKey = update.new.proto.profileKey.nullIfEmpty()?.toByteArray()?.let { ProfileKeyUtil.profileKeyOrNull(it) } + val profileKey: String? = (remoteKey ?: localKey)?.let { Base64.encodeWithPadding(it.serialize()) } + + if (remoteKey == null) { + Log.w(TAG, "Got an empty profile key while applying an account record update! The parsed local key is ${if (localKey != null) "present" else "not present"}. The raw local key is ${if (update.old.proto.profileKey.isNotEmpty()) "present" else "not present"}. The resulting key is ${if (profileKey != null) "present" else "not present"}.") } val values = ContentValues().apply { @@ -1043,21 +1047,21 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da Log.w(TAG, "Avoided attempt to apply null profile key in account record update!") } - put(USERNAME, update.new.username) + put(USERNAME, update.new.proto.username.nullIfBlank()) put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(update.new.id.raw)) - if (update.new.hasUnknownFields()) { - put(STORAGE_SERVICE_PROTO, Base64.encodeWithPadding(Objects.requireNonNull(update.new.serializeUnknownFields()))) + if (update.new.proto.hasUnknownFields()) { + put(STORAGE_SERVICE_PROTO, Base64.encodeWithPadding(update.new.serializeUnknownFields()!!)) } else { putNull(STORAGE_SERVICE_PROTO) } } - if (update.new.username != null) { + if (update.new.proto.username.nullIfBlank() != null) { writableDatabase .update(TABLE_NAME) .values(USERNAME to null) - .where("$USERNAME = ?", update.new.username!!) + .where("$USERNAME = ?", update.new.proto.username) .run() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt index 73e7a51523..36ef522540 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt @@ -71,10 +71,11 @@ import org.thoughtcrime.securesms.util.LRUCache import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.isScheduled import org.whispersystems.signalservice.api.storage.SignalAccountRecord -import org.whispersystems.signalservice.api.storage.SignalAccountRecord.PinnedConversation import org.whispersystems.signalservice.api.storage.SignalContactRecord import org.whispersystems.signalservice.api.storage.SignalGroupV1Record import org.whispersystems.signalservice.api.storage.SignalGroupV2Record +import org.whispersystems.signalservice.api.storage.toSignalServiceAddress +import org.whispersystems.signalservice.internal.storage.protos.AccountRecord import java.io.Closeable import java.io.IOException import java.util.Collections @@ -1522,7 +1523,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa fun applyStorageSyncUpdate(recipientId: RecipientId, record: SignalAccountRecord) { writableDatabase.withinTransaction { db -> - applyStorageSyncUpdate(recipientId, record.isNoteToSelfArchived, record.isNoteToSelfForcedUnread) + applyStorageSyncUpdate(recipientId, record.proto.noteToSelfArchived, record.proto.noteToSelfMarkedUnread) db.updateAll(TABLE_NAME) .values(PINNED to 0) @@ -1530,19 +1531,19 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa var pinnedPosition = 1 - for (pinned: PinnedConversation in record.pinnedConversations) { - val pinnedRecipient: Recipient? = if (pinned.contact.isPresent) { - Recipient.externalPush(pinned.contact.get()) - } else if (pinned.groupV1Id.isPresent) { + for (pinned: AccountRecord.PinnedConversation in record.proto.pinnedConversations) { + val pinnedRecipient: Recipient? = if (pinned.contact != null) { + Recipient.externalPush(pinned.contact!!.toSignalServiceAddress()) + } else if (pinned.legacyGroupId != null) { try { - Recipient.externalGroupExact(GroupId.v1(pinned.groupV1Id.get())) + Recipient.externalGroupExact(GroupId.v1(pinned.legacyGroupId!!.toByteArray())) } catch (e: BadGroupIdException) { Log.w(TAG, "Failed to parse pinned groupV1 ID!", e) null } - } else if (pinned.groupV2MasterKey.isPresent) { + } else if (pinned.groupMasterKey != null) { try { - Recipient.externalGroupExact(GroupId.v2(GroupMasterKey(pinned.groupV2MasterKey.get()))) + Recipient.externalGroupExact(GroupId.v2(GroupMasterKey(pinned.groupMasterKey!!.toByteArray()))) } catch (e: InvalidInputException) { Log.w(TAG, "Failed to parse pinned groupV2 master key!", e) null diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.java index 99e77bea83..f83d0e27dd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.java @@ -124,9 +124,9 @@ public class StorageAccountRestoreJob extends BaseJob { JobManager jobManager = AppDependencies.getJobManager(); - if (accountRecord.getAvatarUrlPath().isPresent()) { + if (!accountRecord.getProto().avatarUrlPath.isEmpty()) { Log.i(TAG, "Fetching avatar..."); - Optional state = jobManager.runSynchronously(new RetrieveProfileAvatarJob(Recipient.self(), accountRecord.getAvatarUrlPath().get()), LIFESPAN/2); + Optional state = jobManager.runSynchronously(new RetrieveProfileAvatarJob(Recipient.self(), accountRecord.getProto().avatarUrlPath), LIFESPAN / 2); if (state.isPresent()) { Log.i(TAG, "Avatar retrieved successfully. " + state.get()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.kt index a1854222bf..7ec7c5b0f9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.kt @@ -100,13 +100,11 @@ import java.util.stream.Collectors * - Update the respective model (i.e. [SignalContactRecord]) * - Add getters * - Update the builder - * - Update [SignalRecord.describeDiff]. - * - Update the respective record processor (i.e [ContactRecordProcessor]). You need to make - * sure that you're: - * - Merging the attributes, likely preferring remote - * - Adding to doParamsMatch() - * - Adding the parameter to the builder chain when creating a merged model - * - Update builder usage in StorageSyncModels + * - Update the respective record processor (i.e [ContactRecordProcessor]). You need to make sure that you're: + * - Merging the attributes, likely preferring remote + * - Adding to doParamsMatch() + * - Adding the parameter to the builder chain when creating a merged model + * - Update builder usage in StorageSyncModels * - Handle the new data when writing to the local storage * (i.e. [RecipientTable.applyStorageSyncContactUpdate]). * - Make sure that whenever you change the field in the UI, we rotate the storageId for that row diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt index e7fa9b0b1e..98289b8602 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt @@ -6,6 +6,7 @@ import android.content.SharedPreferences import android.preference.PreferenceManager import org.signal.core.util.Base64 import org.signal.core.util.logging.Log +import org.signal.core.util.nullIfBlank import org.signal.libsignal.protocol.IdentityKey import org.signal.libsignal.protocol.IdentityKeyPair import org.signal.libsignal.protocol.ecc.Curve @@ -401,10 +402,10 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context) var username: String? get() { val value = getString(KEY_USERNAME, null) - return if (value.isNullOrBlank()) null else value + return value.nullIfBlank() } set(value) { - putString(KEY_USERNAME, value) + putString(KEY_USERNAME, value.nullIfBlank()) } /** The local user's username link components, if set. */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.kt index ee24b7cd52..e38f0a12ef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.kt @@ -1,14 +1,20 @@ package org.thoughtcrime.securesms.storage import android.content.Context +import okio.ByteString +import org.signal.core.util.isNotEmpty import org.signal.core.util.logging.Log +import org.signal.core.util.nullIfEmpty 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.api.storage.StorageId +import org.whispersystems.signalservice.api.storage.safeSetBackupsSubscriber +import org.whispersystems.signalservice.api.storage.safeSetPayments +import org.whispersystems.signalservice.api.storage.safeSetSubscriber +import org.whispersystems.signalservice.api.storage.toSignalAccountRecord import org.whispersystems.signalservice.internal.storage.protos.OptionalBool import java.util.Optional @@ -45,184 +51,100 @@ class AccountRecordProcessor( return false } - override fun getMatching(record: SignalAccountRecord, keyGenerator: StorageKeyGenerator): Optional { + override fun getMatching(remote: SignalAccountRecord, keyGenerator: StorageKeyGenerator): Optional { return Optional.of(localAccountRecord) } override fun merge(remote: SignalAccountRecord, local: SignalAccountRecord, keyGenerator: StorageKeyGenerator): SignalAccountRecord { - val givenName: String - val familyName: String + val mergedGivenName: String + val mergedFamilyName: String - if (remote.givenName.isPresent || remote.familyName.isPresent) { - givenName = remote.givenName.orElse("") - familyName = remote.familyName.orElse("") + if (remote.proto.givenName.isNotBlank() || remote.proto.familyName.isNotBlank()) { + mergedGivenName = remote.proto.givenName + mergedFamilyName = remote.proto.familyName } else { - givenName = local.givenName.orElse("") - familyName = local.familyName.orElse("") + mergedGivenName = local.proto.givenName + mergedFamilyName = local.proto.familyName } - val payments = if (remote.payments.entropy.isPresent) { - remote.payments + val payments = if (remote.proto.payments?.entropy != null) { + remote.proto.payments } else { - local.payments + local.proto.payments } - val subscriber = if (remote.subscriber.id.isPresent) { - remote.subscriber + val donationSubscriberId: ByteString + val donationSubscriberCurrencyCode: String + + if (remote.proto.subscriberId.isNotEmpty()) { + donationSubscriberId = remote.proto.subscriberId + donationSubscriberCurrencyCode = remote.proto.subscriberCurrencyCode } else { - local.subscriber + donationSubscriberId = local.proto.subscriberId + donationSubscriberCurrencyCode = remote.proto.subscriberCurrencyCode } - val backupsSubscriber = if (remote.subscriber.id.isPresent) { - remote.subscriber + val backupsSubscriberId: ByteString + val backupsSubscriberCurrencyCode: String + + if (remote.proto.backupsSubscriberId.isNotEmpty()) { + backupsSubscriberId = remote.proto.backupsSubscriberId + backupsSubscriberCurrencyCode = remote.proto.backupsSubscriberCurrencyCode } else { - local.subscriber + backupsSubscriberId = local.proto.backupsSubscriberId + backupsSubscriberCurrencyCode = remote.proto.backupsSubscriberCurrencyCode } - val storyViewReceiptsState = if (remote.storyViewReceiptsState == OptionalBool.UNSET) { - local.storyViewReceiptsState + + val storyViewReceiptsState = if (remote.proto.storyViewReceiptsEnabled == OptionalBool.UNSET) { + local.proto.storyViewReceiptsEnabled } else { - remote.storyViewReceiptsState + remote.proto.storyViewReceiptsEnabled } 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 - ) + val merged = SignalAccountRecord.newBuilder(unknownFields).apply { + givenName = mergedGivenName + familyName = mergedFamilyName + avatarUrlPath = remote.proto.avatarUrlPath.nullIfEmpty() ?: local.proto.avatarUrlPath + profileKey = remote.proto.profileKey.nullIfEmpty() ?: local.proto.profileKey + noteToSelfArchived = remote.proto.noteToSelfArchived + noteToSelfMarkedUnread = remote.proto.noteToSelfMarkedUnread + readReceipts = remote.proto.readReceipts + typingIndicators = remote.proto.typingIndicators + sealedSenderIndicators = remote.proto.sealedSenderIndicators + linkPreviews = remote.proto.linkPreviews + unlistedPhoneNumber = remote.proto.unlistedPhoneNumber + pinnedConversations = remote.proto.pinnedConversations + phoneNumberSharingMode = remote.proto.phoneNumberSharingMode + preferContactAvatars = remote.proto.preferContactAvatars + universalExpireTimer = remote.proto.universalExpireTimer + primarySendsSms = false + e164 = if (SignalStore.account.isPrimaryDevice) local.proto.e164 else remote.proto.e164 + preferredReactionEmoji = remote.proto.preferredReactionEmoji.takeIf { it.isNotEmpty() } ?: local.proto.preferredReactionEmoji + displayBadgesOnProfile = remote.proto.displayBadgesOnProfile + subscriptionManuallyCancelled = remote.proto.subscriptionManuallyCancelled + keepMutedChatsArchived = remote.proto.keepMutedChatsArchived + hasSetMyStoriesPrivacy = remote.proto.hasSetMyStoriesPrivacy + hasViewedOnboardingStory = remote.proto.hasViewedOnboardingStory || local.proto.hasViewedOnboardingStory + storiesDisabled = remote.proto.storiesDisabled + storyViewReceiptsEnabled = storyViewReceiptsState + hasSeenGroupStoryEducationSheet = remote.proto.hasSeenGroupStoryEducationSheet || local.proto.hasSeenGroupStoryEducationSheet + hasCompletedUsernameOnboarding = remote.proto.hasCompletedUsernameOnboarding || local.proto.hasCompletedUsernameOnboarding + username = remote.proto.username + usernameLink = remote.proto.usernameLink - if (matchesRemote) { - return remote - } else if (matchesLocal) { - return local + safeSetPayments(payments?.enabled == true, payments?.entropy?.toByteArray()) + safeSetSubscriber(donationSubscriberId, donationSubscriberCurrencyCode) + safeSetBackupsSubscriber(backupsSubscriberId, backupsSubscriberCurrencyCode) + }.toSignalAccountRecord(StorageId.forAccount(keyGenerator.generate())) + + return if (doParamsMatch(remote, merged)) { + remote + } else if (doParamsMatch(local, merged)) { + 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() + merged } } @@ -238,72 +160,7 @@ class AccountRecordProcessor( 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 + private fun doParamsMatch(base: SignalAccountRecord, test: SignalAccountRecord): Boolean { + return base.serializeUnknownFields().contentEquals(test.serializeUnknownFields()) && base.proto == test.proto } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/DefaultStorageRecordProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/storage/DefaultStorageRecordProcessor.kt index 62f9f86a8d..d1de30cf73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/DefaultStorageRecordProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/DefaultStorageRecordProcessor.kt @@ -15,7 +15,7 @@ import java.util.TreeSet * 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 { +abstract class DefaultStorageRecordProcessor> : StorageRecordProcessor, Comparator { companion object { private val TAG = Log.tag(DefaultStorageRecordProcessor::class.java) } @@ -37,16 +37,15 @@ abstract class DefaultStorageRecordProcessor : StorageRecordPr @Throws(IOException::class) override fun process(remoteRecords: Collection, keyGenerator: StorageKeyGenerator) { val matchedRecords: MutableSet = TreeSet(this) - var i = 0 - for (remote in remoteRecords) { + for ((i, remote) in remoteRecords.withIndex()) { 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) + val merged: E = 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.") @@ -54,7 +53,7 @@ abstract class DefaultStorageRecordProcessor : StorageRecordPr matchedRecords.add(local.get()) if (merged != remote) { - info(i, remote, "[Remote Update] " + StorageRecordUpdate(remote, merged).toString()) + info(i, remote, "[Remote Update] " + remote.describeDiff(merged)) } if (merged != local.get()) { @@ -68,8 +67,6 @@ abstract class DefaultStorageRecordProcessor : StorageRecordPr insertLocal(remote) } } - - i++ } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageRecordProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageRecordProcessor.kt index 35ec247b53..32c2576e12 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageRecordProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageRecordProcessor.kt @@ -7,7 +7,7 @@ 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 { +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 deleted file mode 100644 index a4b94c6318..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageRecordUpdate.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.thoughtcrime.securesms.storage; - -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; - -import org.whispersystems.signalservice.api.storage.SignalRecord; - -import java.util.Objects; - -/** - * Represents a pair of records: one old, and one new. The new record should replace the old. - */ -public class StorageRecordUpdate { - private final E oldRecord; - private final E newRecord; - - public StorageRecordUpdate(@NonNull E oldRecord, @NonNull E newRecord) { - this.oldRecord = oldRecord; - this.newRecord = newRecord; - } - - public @NonNull E getOld() { - return oldRecord; - } - - public @NonNull E getNew() { - return newRecord; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - StorageRecordUpdate that = (StorageRecordUpdate) o; - return oldRecord.equals(that.oldRecord) && - newRecord.equals(that.newRecord); - } - - @Override - public int hashCode() { - return Objects.hash(oldRecord, newRecord); - } - - @Override - public @NonNull String toString() { - return newRecord.describeDiff(oldRecord); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageRecordUpdate.kt b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageRecordUpdate.kt new file mode 100644 index 0000000000..7fa6b55403 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageRecordUpdate.kt @@ -0,0 +1,27 @@ +package org.thoughtcrime.securesms.storage + +import org.whispersystems.signalservice.api.storage.SignalRecord + +/** + * Represents a pair of records: one old, and one new. The new record should replace the old. + */ +class StorageRecordUpdate>(val old: E, val new: E) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as StorageRecordUpdate<*> + + if (old != other.old) return false + if (new != other.new) return false + + return true + } + + override fun hashCode(): Int { + var result = old.hashCode() + result = 31 * result + new.hashCode() + return result + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.kt index 03e04562c1..981e3a0257 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.storage import android.content.Context import androidx.annotation.VisibleForTesting +import okio.ByteString import okio.ByteString.Companion.toByteString import org.signal.core.util.Base64.encodeWithPadding import org.signal.core.util.logging.Log @@ -28,6 +29,10 @@ import org.whispersystems.signalservice.api.storage.SignalContactRecord 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.safeSetBackupsSubscriber +import org.whispersystems.signalservice.api.storage.safeSetPayments +import org.whispersystems.signalservice.api.storage.safeSetSubscriber +import org.whispersystems.signalservice.api.storage.toSignalAccountRecord import org.whispersystems.signalservice.api.util.OptionalUtil.byteArrayEquals import org.whispersystems.signalservice.api.util.UuidUtil import org.whispersystems.signalservice.api.util.toByteArray @@ -130,52 +135,54 @@ object StorageSyncHelper { val storageId = selfRecord?.storageId ?: self.storageId - val account = SignalAccountRecord.Builder(storageId, selfRecord?.syncExtras?.storageProto) - .setProfileKey(self.profileKey) - .setGivenName(self.profileName.givenName) - .setFamilyName(self.profileName.familyName) - .setAvatarUrlPath(self.profileAvatar) - .setNoteToSelfArchived(selfRecord != null && selfRecord.syncExtras.isArchived) - .setNoteToSelfForcedUnread(selfRecord != null && selfRecord.syncExtras.isForcedUnread) - .setTypingIndicatorsEnabled(TextSecurePreferences.isTypingIndicatorsEnabled(context)) - .setReadReceiptsEnabled(TextSecurePreferences.isReadReceiptsEnabled(context)) - .setSealedSenderIndicatorsEnabled(TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context)) - .setLinkPreviewsEnabled(SignalStore.settings.isLinkPreviewsEnabled) - .setUnlistedPhoneNumber(SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode == PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE) - .setPhoneNumberSharingMode(StorageSyncModels.localToRemotePhoneNumberSharingMode(SignalStore.phoneNumberPrivacy.phoneNumberSharingMode)) - .setPinnedConversations(StorageSyncModels.localToRemotePinnedConversations(pinned)) - .setPreferContactAvatars(SignalStore.settings.isPreferSystemContactPhotos) - .setPayments(SignalStore.payments.mobileCoinPaymentsEnabled(), Optional.ofNullable(SignalStore.payments.paymentsEntropy).map { obj: Entropy -> obj.bytes }.orElse(null)) - .setPrimarySendsSms(false) - .setUniversalExpireTimer(SignalStore.settings.universalExpireTimer) - .setDefaultReactions(SignalStore.emoji.reactions) - .setSubscriber(StorageSyncModels.localToRemoteSubscriber(getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION))) - .setBackupsSubscriber(StorageSyncModels.localToRemoteSubscriber(getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP))) - .setDisplayBadgesOnProfile(SignalStore.inAppPayments.getDisplayBadgesOnProfile()) - .setSubscriptionManuallyCancelled(isUserManuallyCancelled(InAppPaymentSubscriberRecord.Type.DONATION)) - .setKeepMutedChatsArchived(SignalStore.settings.shouldKeepMutedChatsArchived()) - .setHasSetMyStoriesPrivacy(SignalStore.story.userHasBeenNotifiedAboutStories) - .setHasViewedOnboardingStory(SignalStore.story.userHasViewedOnboardingStory) - .setStoriesDisabled(SignalStore.story.isFeatureDisabled) - .setStoryViewReceiptsState(storyViewReceiptsState) - .setHasSeenGroupStoryEducationSheet(SignalStore.story.userHasSeenGroupStoryEducationSheet) - .setUsername(SignalStore.account.username) - .setHasCompletedUsernameOnboarding(SignalStore.uiHints.hasCompletedUsernameOnboarding()) + val accountRecord = SignalAccountRecord.newBuilder(selfRecord?.syncExtras?.storageProto).apply { + profileKey = self.profileKey?.toByteString() ?: ByteString.EMPTY + givenName = self.profileName.givenName + familyName = self.profileName.familyName + avatarUrlPath = self.profileAvatar ?: "" + noteToSelfArchived = selfRecord != null && selfRecord.syncExtras.isArchived + noteToSelfMarkedUnread = selfRecord != null && selfRecord.syncExtras.isForcedUnread + typingIndicators = TextSecurePreferences.isTypingIndicatorsEnabled(context) + readReceipts = TextSecurePreferences.isReadReceiptsEnabled(context) + sealedSenderIndicators = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context) + linkPreviews = SignalStore.settings.isLinkPreviewsEnabled + unlistedPhoneNumber = SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode == PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE + phoneNumberSharingMode = StorageSyncModels.localToRemotePhoneNumberSharingMode(SignalStore.phoneNumberPrivacy.phoneNumberSharingMode) + pinnedConversations = StorageSyncModels.localToRemotePinnedConversations(pinned) + preferContactAvatars = SignalStore.settings.isPreferSystemContactPhotos + primarySendsSms = false + universalExpireTimer = SignalStore.settings.universalExpireTimer + preferredReactionEmoji = SignalStore.emoji.reactions + displayBadgesOnProfile = SignalStore.inAppPayments.getDisplayBadgesOnProfile() + subscriptionManuallyCancelled = isUserManuallyCancelled(InAppPaymentSubscriberRecord.Type.DONATION) + keepMutedChatsArchived = SignalStore.settings.shouldKeepMutedChatsArchived() + hasSetMyStoriesPrivacy = SignalStore.story.userHasBeenNotifiedAboutStories + hasViewedOnboardingStory = SignalStore.story.userHasViewedOnboardingStory + storiesDisabled = SignalStore.story.isFeatureDisabled + storyViewReceiptsEnabled = storyViewReceiptsState + hasSeenGroupStoryEducationSheet = SignalStore.story.userHasSeenGroupStoryEducationSheet + hasCompletedUsernameOnboarding = SignalStore.uiHints.hasCompletedUsernameOnboarding() + username = SignalStore.account.username ?: "" + usernameLink = SignalStore.account.usernameLink?.let { linkComponents -> + AccountRecord.UsernameLink( + entropy = linkComponents.entropy.toByteString(), + serverId = linkComponents.serverId.toByteArray().toByteString(), + color = StorageSyncModels.localToRemoteUsernameColor(SignalStore.misc.usernameQrCodeColorScheme) + ) + } - val linkComponents = SignalStore.account.usernameLink - if (linkComponents != null) { - account.setUsernameLink( - AccountRecord.UsernameLink.Builder() - .entropy(linkComponents.entropy.toByteString()) - .serverId(linkComponents.serverId.toByteArray().toByteString()) - .color(StorageSyncModels.localToRemoteUsernameColor(SignalStore.misc.usernameQrCodeColorScheme)) - .build() - ) - } else { - account.setUsernameLink(null) + getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION)?.let { + safeSetSubscriber(it.subscriberId.bytes.toByteString(), it.currency.currencyCode) + } + + getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP)?.let { + safeSetBackupsSubscriber(it.subscriberId.bytes.toByteString(), it.currency.currencyCode) + } + + safeSetPayments(SignalStore.payments.mobileCoinPaymentsEnabled(), Optional.ofNullable(SignalStore.payments.paymentsEntropy).map { obj: Entropy -> obj.bytes }.orElse(null)) } - return SignalStorageRecord.forAccount(account.build()) + return SignalStorageRecord.forAccount(accountRecord.toSignalAccountRecord(StorageId.forAccount(storageId))) } @JvmStatic @@ -188,62 +195,56 @@ object StorageSyncHelper { fun applyAccountStorageSyncUpdates(context: Context, self: Recipient, update: StorageRecordUpdate, fetchProfile: Boolean) { SignalDatabase.recipients.applyStorageSyncAccountUpdate(update) - TextSecurePreferences.setReadReceiptsEnabled(context, update.new.isReadReceiptsEnabled) - TextSecurePreferences.setTypingIndicatorsEnabled(context, update.new.isTypingIndicatorsEnabled) - TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, update.new.isSealedSenderIndicatorsEnabled) - SignalStore.settings.isLinkPreviewsEnabled = update.new.isLinkPreviewsEnabled - SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode = if (update.new.isPhoneNumberUnlisted) PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE else PhoneNumberDiscoverabilityMode.DISCOVERABLE - SignalStore.phoneNumberPrivacy.phoneNumberSharingMode = StorageSyncModels.remoteToLocalPhoneNumberSharingMode(update.new.phoneNumberSharingMode) - SignalStore.settings.isPreferSystemContactPhotos = update.new.isPreferContactAvatars - SignalStore.payments.setEnabledAndEntropy(update.new.payments.isEnabled, Entropy.fromBytes(update.new.payments.entropy.orElse(null))) - SignalStore.settings.universalExpireTimer = update.new.universalExpireTimer - SignalStore.emoji.reactions = update.new.defaultReactions - SignalStore.inAppPayments.setDisplayBadgesOnProfile(update.new.isDisplayBadgesOnProfile) - SignalStore.settings.setKeepMutedChatsArchived(update.new.isKeepMutedChatsArchived) - SignalStore.story.userHasBeenNotifiedAboutStories = update.new.hasSetMyStoriesPrivacy() - SignalStore.story.userHasViewedOnboardingStory = update.new.hasViewedOnboardingStory() - SignalStore.story.isFeatureDisabled = update.new.isStoriesDisabled - SignalStore.story.userHasSeenGroupStoryEducationSheet = update.new.hasSeenGroupStoryEducationSheet() - SignalStore.uiHints.setHasCompletedUsernameOnboarding(update.new.hasCompletedUsernameOnboarding()) + TextSecurePreferences.setReadReceiptsEnabled(context, update.new.proto.readReceipts) + TextSecurePreferences.setTypingIndicatorsEnabled(context, update.new.proto.typingIndicators) + TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, update.new.proto.sealedSenderIndicators) + SignalStore.settings.isLinkPreviewsEnabled = update.new.proto.linkPreviews + SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode = if (update.new.proto.unlistedPhoneNumber) PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE else PhoneNumberDiscoverabilityMode.DISCOVERABLE + SignalStore.phoneNumberPrivacy.phoneNumberSharingMode = StorageSyncModels.remoteToLocalPhoneNumberSharingMode(update.new.proto.phoneNumberSharingMode) + SignalStore.settings.isPreferSystemContactPhotos = update.new.proto.preferContactAvatars + SignalStore.payments.setEnabledAndEntropy(update.new.proto.payments?.enabled == true, Entropy.fromBytes(update.new.proto.payments?.entropy?.toByteArray())) + SignalStore.settings.universalExpireTimer = update.new.proto.universalExpireTimer + SignalStore.emoji.reactions = update.new.proto.preferredReactionEmoji + SignalStore.inAppPayments.setDisplayBadgesOnProfile(update.new.proto.displayBadgesOnProfile) + SignalStore.settings.setKeepMutedChatsArchived(update.new.proto.keepMutedChatsArchived) + SignalStore.story.userHasBeenNotifiedAboutStories = update.new.proto.hasSetMyStoriesPrivacy + SignalStore.story.userHasViewedOnboardingStory = update.new.proto.hasViewedOnboardingStory + SignalStore.story.isFeatureDisabled = update.new.proto.storiesDisabled + SignalStore.story.userHasSeenGroupStoryEducationSheet = update.new.proto.hasSeenGroupStoryEducationSheet + SignalStore.uiHints.setHasCompletedUsernameOnboarding(update.new.proto.hasCompletedUsernameOnboarding) - if (update.new.storyViewReceiptsState == OptionalBool.UNSET) { - SignalStore.story.viewedReceiptsEnabled = update.new.isReadReceiptsEnabled + if (update.new.proto.storyViewReceiptsEnabled == OptionalBool.UNSET) { + SignalStore.story.viewedReceiptsEnabled = update.new.proto.readReceipts } else { - SignalStore.story.viewedReceiptsEnabled = update.new.storyViewReceiptsState == OptionalBool.ENABLED + SignalStore.story.viewedReceiptsEnabled = update.new.proto.storyViewReceiptsEnabled == OptionalBool.ENABLED } - if (update.new.storyViewReceiptsState == OptionalBool.UNSET) { - SignalStore.story.viewedReceiptsEnabled = update.new.isReadReceiptsEnabled - } else { - SignalStore.story.viewedReceiptsEnabled = update.new.storyViewReceiptsState == OptionalBool.ENABLED - } - - val remoteSubscriber = StorageSyncModels.remoteToLocalSubscriber(update.new.subscriber, InAppPaymentSubscriberRecord.Type.DONATION) + val remoteSubscriber = StorageSyncModels.remoteToLocalSubscriber(update.new.proto.subscriberId, update.new.proto.subscriberCurrencyCode, InAppPaymentSubscriberRecord.Type.DONATION) if (remoteSubscriber != null) { setSubscriber(remoteSubscriber) } - if (update.new.isSubscriptionManuallyCancelled && !update.old.isSubscriptionManuallyCancelled) { + if (update.new.proto.subscriptionManuallyCancelled && !update.old.proto.subscriptionManuallyCancelled) { SignalStore.inAppPayments.updateLocalStateForManualCancellation(InAppPaymentSubscriberRecord.Type.DONATION) } - if (fetchProfile && update.new.avatarUrlPath.isPresent) { - AppDependencies.jobManager.add(RetrieveProfileAvatarJob(self, update.new.avatarUrlPath.get())) + if (fetchProfile && update.new.proto.avatarUrlPath.isNotBlank()) { + AppDependencies.jobManager.add(RetrieveProfileAvatarJob(self, update.new.proto.avatarUrlPath)) } - if (update.new.username != update.old.username) { - SignalStore.account.username = update.new.username + if (update.new.proto.username != update.old.proto.username) { + SignalStore.account.username = update.new.proto.username SignalStore.account.usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC SignalStore.account.usernameSyncErrorCount = 0 } - if (update.new.usernameLink != null) { + if (update.new.proto.usernameLink != null) { SignalStore.account.usernameLink = UsernameLinkComponents( - update.new.usernameLink!!.entropy.toByteArray(), - UuidUtil.parseOrThrow(update.new.usernameLink!!.serverId.toByteArray()) + update.new.proto.usernameLink!!.entropy.toByteArray(), + UuidUtil.parseOrThrow(update.new.proto.usernameLink!!.serverId.toByteArray()) ) - SignalStore.misc.usernameQrCodeColorScheme = StorageSyncModels.remoteToLocalUsernameColor(update.new.usernameLink!!.color) + SignalStore.misc.usernameQrCodeColorScheme = StorageSyncModels.remoteToLocalUsernameColor(update.new.proto.usernameLink!!.color) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.kt b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.kt index 427e9e00d7..5debbdadae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.kt @@ -1,5 +1,8 @@ package org.thoughtcrime.securesms.storage +import okio.ByteString +import okio.ByteString.Companion.toByteString +import org.signal.core.util.isNotEmpty import org.signal.libsignal.zkgroup.groups.GroupMasterKey import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme import org.thoughtcrime.securesms.database.GroupTable.ShowAsStoryState @@ -16,7 +19,6 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues import org.thoughtcrime.securesms.recipients.Recipient import org.whispersystems.signalservice.api.push.SignalServiceAddress -import org.whispersystems.signalservice.api.storage.SignalAccountRecord import org.whispersystems.signalservice.api.storage.SignalCallLinkRecord import org.whispersystems.signalservice.api.storage.SignalContactRecord import org.whispersystems.signalservice.api.storage.SignalGroupV1Record @@ -82,18 +84,33 @@ object StorageSyncModels { } @JvmStatic - fun localToRemotePinnedConversations(records: List): List { + fun localToRemotePinnedConversations(records: List): List { return records .filter { it.recipientType == RecipientType.GV1 || it.recipientType == RecipientType.GV2 || it.registered == RecipientTable.RegisteredState.REGISTERED } .map { localToRemotePinnedConversation(it) } } @JvmStatic - private fun localToRemotePinnedConversation(settings: RecipientRecord): SignalAccountRecord.PinnedConversation { + private fun localToRemotePinnedConversation(settings: RecipientRecord): AccountRecord.PinnedConversation { return when (settings.recipientType) { - RecipientType.INDIVIDUAL -> SignalAccountRecord.PinnedConversation.forContact(SignalServiceAddress(settings.serviceId, settings.e164)) - RecipientType.GV1 -> SignalAccountRecord.PinnedConversation.forGroupV1(settings.groupId!!.requireV1().decodedId) - RecipientType.GV2 -> SignalAccountRecord.PinnedConversation.forGroupV2(settings.syncExtras.groupMasterKey!!.serialize()) + RecipientType.INDIVIDUAL -> { + AccountRecord.PinnedConversation( + contact = AccountRecord.PinnedConversation.Contact( + serviceId = settings.serviceId?.toString() ?: "", + e164 = settings.e164 ?: "" + ) + ) + } + RecipientType.GV1 -> { + AccountRecord.PinnedConversation( + legacyGroupId = settings.groupId!!.requireV1().decodedId.toByteString() + ) + } + RecipientType.GV2 -> { + AccountRecord.PinnedConversation( + groupMasterKey = settings.syncExtras.groupMasterKey!!.serialize().toByteString() + ) + } else -> throw AssertionError("Unexpected group type!") } } @@ -271,33 +288,23 @@ object StorageSyncModels { } } - /** - * TODO - need to store the subscriber type - */ - fun localToRemoteSubscriber(subscriber: InAppPaymentSubscriberRecord?): SignalAccountRecord.Subscriber { - return if (subscriber == null) { - SignalAccountRecord.Subscriber(null, null) - } else { - SignalAccountRecord.Subscriber(subscriber.currency.currencyCode, subscriber.subscriberId.bytes) - } - } - fun remoteToLocalSubscriber( - subscriber: SignalAccountRecord.Subscriber, + subscriberId: ByteString, + subscriberCurrencyCode: String, type: InAppPaymentSubscriberRecord.Type ): InAppPaymentSubscriberRecord? { - if (subscriber.id.isPresent) { - val subscriberId = SubscriberId.fromBytes(subscriber.id.get()) + if (subscriberId.isNotEmpty()) { + val subscriberId = SubscriberId.fromBytes(subscriberId.toByteArray()) val localSubscriberRecord = inAppPaymentSubscribers.getBySubscriberId(subscriberId) val requiresCancel = localSubscriberRecord != null && localSubscriberRecord.requiresCancel val paymentMethodType = localSubscriberRecord?.paymentMethodType ?: InAppPaymentData.PaymentMethodType.UNKNOWN val currency: Currency - if (subscriber.currencyCode.isEmpty) { + if (subscriberCurrencyCode.isBlank()) { return null } else { try { - currency = Currency.getInstance(subscriber.currencyCode.get()) + currency = Currency.getInstance(subscriberCurrencyCode) } catch (e: IllegalArgumentException) { return null } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncValidations.java b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncValidations.java index b0b9a2538e..1845e657fb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncValidations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncValidations.java @@ -177,7 +177,7 @@ public final class StorageSyncValidations { } } - if (insert.getAccount().isPresent() && !insert.getAccount().get().getProfileKey().isPresent()) { + if (insert.getAccount().isPresent() && insert.getAccount().get().getProto().profileKey.size() == 0) { Log.w(TAG, "Uploading a null profile key in our AccountRecord!"); } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/storage/StorageRecordTest.kt b/app/src/test/java/org/thoughtcrime/securesms/storage/StorageRecordTest.kt new file mode 100644 index 0000000000..4d75100fa4 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/storage/StorageRecordTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.storage + +import junit.framework.TestCase.assertEquals +import okio.ByteString +import okio.ByteString.Companion.toByteString +import org.junit.Test +import org.thoughtcrime.securesms.util.Util +import org.whispersystems.signalservice.api.storage.SignalAccountRecord +import org.whispersystems.signalservice.api.storage.SignalContactRecord +import org.whispersystems.signalservice.api.storage.StorageId +import org.whispersystems.signalservice.internal.storage.protos.AccountRecord +import org.whispersystems.signalservice.internal.storage.protos.ContactRecord + +class StorageRecordTest { + + @Test + fun `describeDiff - general test`() { + val a = SignalAccountRecord( + StorageId.forAccount(Util.getSecretBytes(16)), + AccountRecord( + profileKey = ByteString.EMPTY, + givenName = "First", + familyName = "Last" + ) + ) + + val b = SignalAccountRecord( + StorageId.forAccount(Util.getSecretBytes(16)), + AccountRecord( + profileKey = Util.getSecretBytes(16).toByteString(), + givenName = "First", + familyName = "LastB" + ) + ) + + assertEquals("Some fields differ: familyName, id, profileKey", a.describeDiff(b)) + } + + @Test + fun `describeDiff - different class`() { + val a = SignalAccountRecord( + StorageId.forAccount(Util.getSecretBytes(16)), + AccountRecord() + ) + + val b = SignalContactRecord( + StorageId.forAccount(Util.getSecretBytes(16)), + ContactRecord() + ) + + assertEquals("Classes are different!", a.describeDiff(b)) + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/storage/StorageSyncHelperTest.java b/app/src/test/java/org/thoughtcrime/securesms/storage/StorageSyncHelperTest.java index 4ef06fa1f3..ab7145e2a7 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/storage/StorageSyncHelperTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/storage/StorageSyncHelperTest.java @@ -180,7 +180,7 @@ public final class StorageSyncHelperTest { .setProfileGivenName(profileName); } - private static StorageRecordUpdate update(E oldRecord, E newRecord) { + private static > StorageRecordUpdate update(E oldRecord, E newRecord) { return new StorageRecordUpdate<>(oldRecord, newRecord); } diff --git a/core-util-jvm/src/main/java/org/signal/core/util/ProtoExtensions.kt b/core-util-jvm/src/main/java/org/signal/core/util/ProtoExtensions.kt index 5f8b6e3bae..2deace46ad 100644 --- a/core-util-jvm/src/main/java/org/signal/core/util/ProtoExtensions.kt +++ b/core-util-jvm/src/main/java/org/signal/core/util/ProtoExtensions.kt @@ -32,6 +32,14 @@ fun ByteString?.isNullOrEmpty(): Boolean { return this == null || this.size == 0 } +fun ByteString.nullIfEmpty(): ByteString? { + return if (this.isEmpty()) { + null + } else { + this + } +} + /** * Performs the common pattern of attempting to decode a serialized proto and returning null if it fails to decode. */ diff --git a/libsignal-service/build.gradle.kts b/libsignal-service/build.gradle.kts index 5ae5af34e8..eb8a1c6417 100644 --- a/libsignal-service/build.gradle.kts +++ b/libsignal-service/build.gradle.kts @@ -33,6 +33,7 @@ java { tasks.withType().configureEach { kotlinOptions { jvmTarget = signalKotlinJvmTarget + freeCompilerArgs = listOf("-Xjvm-default=all") } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/AccountRecordExtensions.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/AccountRecordExtensions.kt new file mode 100644 index 0000000000..03b8824d2c --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/AccountRecordExtensions.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.storage + +import okio.ByteString +import okio.ByteString.Companion.toByteString +import org.signal.core.util.isNotEmpty +import org.whispersystems.signalservice.api.payments.PaymentsConstants +import org.whispersystems.signalservice.api.push.ServiceId +import org.whispersystems.signalservice.api.push.SignalServiceAddress +import org.whispersystems.signalservice.api.storage.StorageRecordProtoUtil.defaultAccountRecord +import org.whispersystems.signalservice.internal.storage.protos.AccountRecord +import org.whispersystems.signalservice.internal.storage.protos.Payments + +fun AccountRecord.Builder.safeSetPayments(enabled: Boolean, entropy: ByteArray?): AccountRecord.Builder { + val paymentsBuilder = Payments.Builder() + val entropyPresent = entropy != null && entropy.size == PaymentsConstants.PAYMENTS_ENTROPY_LENGTH + + paymentsBuilder.enabled(enabled && entropyPresent) + + if (entropyPresent) { + paymentsBuilder.entropy(entropy!!.toByteString()) + } + + this.payments = paymentsBuilder.build() + + return this +} +fun AccountRecord.Builder.safeSetSubscriber(subscriberId: ByteString, subscriberCurrencyCode: String): AccountRecord.Builder { + if (subscriberId.isNotEmpty() && subscriberId.size == 32 && subscriberCurrencyCode.isNotBlank()) { + this.subscriberId = subscriberId + this.subscriberCurrencyCode = subscriberCurrencyCode + } else { + this.subscriberId = defaultAccountRecord.subscriberId + this.subscriberCurrencyCode = defaultAccountRecord.subscriberCurrencyCode + } + + return this +} + +fun AccountRecord.Builder.safeSetBackupsSubscriber(subscriberId: ByteString, subscriberCurrencyCode: String): AccountRecord.Builder { + if (subscriberId.isNotEmpty() && subscriberId.size == 32 && subscriberCurrencyCode.isNotBlank()) { + this.backupsSubscriberId = subscriberId + this.backupsSubscriberCurrencyCode = subscriberCurrencyCode + } else { + this.backupsSubscriberId = defaultAccountRecord.backupsSubscriberId + this.backupsSubscriberCurrencyCode = defaultAccountRecord.backupsSubscriberCurrencyCode + } + + return this +} + +fun AccountRecord.Builder.toSignalAccountRecord(storageId: StorageId): SignalAccountRecord { + return SignalAccountRecord(storageId, this.build()) +} + +fun AccountRecord.PinnedConversation.Contact.toSignalServiceAddress(): SignalServiceAddress { + val serviceId = ServiceId.parseOrNull(this.serviceId) + return SignalServiceAddress(serviceId, this.e164) +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java deleted file mode 100644 index 8ae972e164..0000000000 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java +++ /dev/null @@ -1,769 +0,0 @@ -package org.whispersystems.signalservice.api.storage; - -import org.signal.core.util.ProtoUtil; -import org.signal.libsignal.protocol.logging.Log; -import org.whispersystems.signalservice.api.payments.PaymentsConstants; -import org.whispersystems.signalservice.api.push.ServiceId; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; -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.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; - -import javax.annotation.Nullable; - -import okio.ByteString; - -public final class SignalAccountRecord implements SignalRecord { - - private static final String TAG = SignalAccountRecord.class.getSimpleName(); - - private final StorageId id; - private final AccountRecord proto; - private final boolean hasUnknownFields; - - private final Optional givenName; - private final Optional familyName; - private final Optional avatarUrlPath; - private final Optional profileKey; - private final List pinnedConversations; - private final Payments payments; - private final List defaultReactions; - private final Subscriber subscriber; - private final Subscriber backupsSubscriber; - - public SignalAccountRecord(StorageId id, AccountRecord proto) { - this.id = id; - this.proto = proto; - this.hasUnknownFields = ProtoUtil.hasUnknownFields(proto); - - this.givenName = OptionalUtil.absentIfEmpty(proto.givenName); - this.familyName = OptionalUtil.absentIfEmpty(proto.familyName); - this.profileKey = OptionalUtil.absentIfEmpty(proto.profileKey); - this.avatarUrlPath = OptionalUtil.absentIfEmpty(proto.avatarUrlPath); - this.pinnedConversations = new ArrayList<>(proto.pinnedConversations.size()); - this.defaultReactions = new ArrayList<>(proto.preferredReactionEmoji); - this.subscriber = new Subscriber(proto.subscriberCurrencyCode, proto.subscriberId.toByteArray()); - this.backupsSubscriber = new Subscriber(proto.backupsSubscriberCurrencyCode, proto.backupsSubscriberId.toByteArray()); - - if (proto.payments != null) { - this.payments = new Payments(proto.payments.enabled, OptionalUtil.absentIfEmpty(proto.payments.entropy)); - } else { - this.payments = new Payments(false, Optional.empty()); - } - - for (AccountRecord.PinnedConversation conversation : proto.pinnedConversations) { - pinnedConversations.add(PinnedConversation.fromRemote(conversation)); - } - } - - @Override - public StorageId getId() { - return id; - } - - @Override - public SignalStorageRecord asStorageRecord() { - return SignalStorageRecord.forAccount(this); - } - - @Override - public String describeDiff(SignalRecord other) { - if (other instanceof SignalAccountRecord) { - SignalAccountRecord that = (SignalAccountRecord) other; - List diff = new LinkedList<>(); - - if (!Arrays.equals(this.id.getRaw(), that.id.getRaw())) { - diff.add("ID"); - } - - if (!Objects.equals(this.givenName, that.givenName)) { - diff.add("GivenName"); - } - - if (!Objects.equals(this.familyName, that.familyName)) { - diff.add("FamilyName"); - } - - if (!OptionalUtil.byteArrayEquals(this.profileKey, that.profileKey)) { - diff.add("ProfileKey"); - } - - if (!Objects.equals(this.avatarUrlPath, that.avatarUrlPath)) { - diff.add("AvatarUrlPath"); - } - - if (!Objects.equals(this.isNoteToSelfArchived(), that.isNoteToSelfArchived())) { - diff.add("NoteToSelfArchived"); - } - - if (!Objects.equals(this.isNoteToSelfForcedUnread(), that.isNoteToSelfForcedUnread())) { - diff.add("NoteToSelfForcedUnread"); - } - - if (!Objects.equals(this.isReadReceiptsEnabled(), that.isReadReceiptsEnabled())) { - diff.add("ReadReceipts"); - } - - if (!Objects.equals(this.isTypingIndicatorsEnabled(), that.isTypingIndicatorsEnabled())) { - diff.add("TypingIndicators"); - } - - if (!Objects.equals(this.isSealedSenderIndicatorsEnabled(), that.isSealedSenderIndicatorsEnabled())) { - diff.add("SealedSenderIndicators"); - } - - if (!Objects.equals(this.isLinkPreviewsEnabled(), that.isLinkPreviewsEnabled())) { - diff.add("LinkPreviews"); - } - - if (!Objects.equals(this.getPhoneNumberSharingMode(), that.getPhoneNumberSharingMode())) { - diff.add("PhoneNumberSharingMode"); - } - - if (!Objects.equals(this.isPhoneNumberUnlisted(), that.isPhoneNumberUnlisted())) { - diff.add("PhoneNumberUnlisted"); - } - - if (!Objects.equals(this.pinnedConversations, that.pinnedConversations)) { - diff.add("PinnedConversations"); - } - - if (!Objects.equals(this.isPreferContactAvatars(), that.isPreferContactAvatars())) { - diff.add("PreferContactAvatars"); - } - - if (!Objects.equals(this.payments, that.payments)) { - diff.add("Payments"); - } - - if (this.getUniversalExpireTimer() != that.getUniversalExpireTimer()) { - diff.add("UniversalExpireTimer"); - } - - if (!Objects.equals(this.isPrimarySendsSms(), that.isPrimarySendsSms())) { - diff.add("PrimarySendsSms"); - } - - if (!Objects.equals(this.getE164(), that.getE164())) { - diff.add("E164"); - } - - if (!Objects.equals(this.getDefaultReactions(), that.getDefaultReactions())) { - diff.add("DefaultReactions"); - } - - if (!Objects.equals(this.hasUnknownFields(), that.hasUnknownFields())) { - diff.add("UnknownFields"); - } - - if (!Objects.equals(this.getSubscriber(), that.getSubscriber())) { - diff.add("Subscriber"); - } - - if (!Objects.equals(this.isDisplayBadgesOnProfile(), that.isDisplayBadgesOnProfile())) { - diff.add("DisplayBadgesOnProfile"); - } - - if (!Objects.equals(this.isSubscriptionManuallyCancelled(), that.isSubscriptionManuallyCancelled())) { - diff.add("SubscriptionManuallyCancelled"); - } - - if (isKeepMutedChatsArchived() != that.isKeepMutedChatsArchived()) { - diff.add("KeepMutedChatsArchived"); - } - - if (hasSetMyStoriesPrivacy() != that.hasSetMyStoriesPrivacy()) { - diff.add("HasSetMyStoryPrivacy"); - } - - if (hasViewedOnboardingStory() != that.hasViewedOnboardingStory()) { - diff.add("HasViewedOnboardingStory"); - } - - if (isStoriesDisabled() != that.isStoriesDisabled()) { - diff.add("StoriesDisabled"); - } - - if (getStoryViewReceiptsState() != that.getStoryViewReceiptsState()) { - diff.add("StoryViewedReceipts"); - } - - if (hasSeenGroupStoryEducationSheet() != that.hasSeenGroupStoryEducationSheet()) { - diff.add("HasSeenGroupStoryEducationSheet"); - } - - if (!Objects.equals(getUsername(), that.getUsername())) { - diff.add("Username"); - } - - if (hasCompletedUsernameOnboarding() != that.hasCompletedUsernameOnboarding()) { - diff.add("HasCompletedUsernameOnboarding"); - } - - if (!Objects.equals(this.getBackupsSubscriber(), that.getBackupsSubscriber())) { - diff.add("BackupsSubscriber"); - } - - return diff.toString(); - } else { - return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName(); - } - } - - public boolean hasUnknownFields() { - return hasUnknownFields; - } - - public byte[] serializeUnknownFields() { - return hasUnknownFields ? proto.encode() : null; - } - - public Optional getGivenName() { - return givenName; - } - - public Optional getFamilyName() { - return familyName; - } - - public Optional getProfileKey() { - return profileKey; - } - - public Optional getAvatarUrlPath() { - return avatarUrlPath; - } - - public boolean isNoteToSelfArchived() { - return proto.noteToSelfArchived; - } - - public boolean isNoteToSelfForcedUnread() { - return proto.noteToSelfMarkedUnread; - } - - public boolean isReadReceiptsEnabled() { - return proto.readReceipts; - } - - public boolean isTypingIndicatorsEnabled() { - return proto.typingIndicators; - } - - public boolean isSealedSenderIndicatorsEnabled() { - return proto.sealedSenderIndicators; - } - - public boolean isLinkPreviewsEnabled() { - return proto.linkPreviews; - } - - public AccountRecord.PhoneNumberSharingMode getPhoneNumberSharingMode() { - return proto.phoneNumberSharingMode; - } - - public boolean isPhoneNumberUnlisted() { - return proto.unlistedPhoneNumber; - } - - public List getPinnedConversations() { - return pinnedConversations; - } - - public boolean isPreferContactAvatars() { - return proto.preferContactAvatars; - } - - public Payments getPayments() { - return payments; - } - - public int getUniversalExpireTimer() { - return proto.universalExpireTimer; - } - - public boolean isPrimarySendsSms() { - return proto.primarySendsSms; - } - - public String getE164() { - return proto.e164; - } - - public List getDefaultReactions() { - return defaultReactions; - } - - public Subscriber getSubscriber() { - return subscriber; - } - - public Subscriber getBackupsSubscriber() { - return backupsSubscriber; - } - - public boolean isDisplayBadgesOnProfile() { - return proto.displayBadgesOnProfile; - } - - public boolean isSubscriptionManuallyCancelled() { - return proto.subscriptionManuallyCancelled; - } - - public boolean isKeepMutedChatsArchived() { - return proto.keepMutedChatsArchived; - } - - public boolean hasSetMyStoriesPrivacy() { - return proto.hasSetMyStoriesPrivacy; - } - - public boolean hasViewedOnboardingStory() { - return proto.hasViewedOnboardingStory; - } - - public boolean isStoriesDisabled() { - return proto.storiesDisabled; - } - - public OptionalBool getStoryViewReceiptsState() { - return proto.storyViewReceiptsEnabled; - } - - public boolean hasSeenGroupStoryEducationSheet() { - return proto.hasSeenGroupStoryEducationSheet; - } - - public boolean hasCompletedUsernameOnboarding() { - return proto.hasCompletedUsernameOnboarding; - } - - public @Nullable String getUsername() { - return proto.username; - } - - public @Nullable AccountRecord.UsernameLink getUsernameLink() { - return proto.usernameLink; - } - - public 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 class PinnedConversation { - private final Optional contact; - private final Optional groupV1Id; - private final Optional groupV2MasterKey; - - private PinnedConversation(Optional contact, Optional groupV1Id, Optional groupV2MasterKey) { - this.contact = contact; - this.groupV1Id = groupV1Id; - this.groupV2MasterKey = groupV2MasterKey; - } - - public static PinnedConversation forContact(SignalServiceAddress address) { - return new PinnedConversation(Optional.of(address), Optional.empty(), Optional.empty()); - } - - public static PinnedConversation forGroupV1(byte[] groupId) { - return new PinnedConversation(Optional.empty(), Optional.of(groupId), Optional.empty()); - } - - public static PinnedConversation forGroupV2(byte[] masterKey) { - return new PinnedConversation(Optional.empty(), Optional.empty(), Optional.of(masterKey)); - } - - private static PinnedConversation forEmpty() { - return new PinnedConversation(Optional.empty(), Optional.empty(), Optional.empty()); - } - - static PinnedConversation fromRemote(AccountRecord.PinnedConversation remote) { - if (remote.contact != null) { - ServiceId serviceId = ServiceId.parseOrNull(remote.contact.serviceId); - if (serviceId != null) { - return forContact(new SignalServiceAddress(serviceId, remote.contact.e164)); - } else { - Log.w(TAG, "Bad serviceId on pinned contact! Length: " + remote.contact.serviceId); - return PinnedConversation.forEmpty(); - } - } else if (remote.legacyGroupId != null && remote.legacyGroupId.size() > 0) { - return forGroupV1(remote.legacyGroupId.toByteArray()); - } else if (remote.groupMasterKey != null && remote.groupMasterKey.size() > 0) { - return forGroupV2(remote.groupMasterKey.toByteArray()); - } else { - return PinnedConversation.forEmpty(); - } - } - - public Optional getContact() { - return contact; - } - - public Optional getGroupV1Id() { - return groupV1Id; - } - - public Optional getGroupV2MasterKey() { - return groupV2MasterKey; - } - - public boolean isValid() { - return contact.isPresent() || groupV1Id.isPresent() || groupV2MasterKey.isPresent(); - } - - private AccountRecord.PinnedConversation toRemote() { - if (contact.isPresent()) { - AccountRecord.PinnedConversation.Contact.Builder contactBuilder = new AccountRecord.PinnedConversation.Contact.Builder(); - - contactBuilder.serviceId(contact.get().getServiceId().toString()); - - if (contact.get().getNumber().isPresent()) { - contactBuilder.e164(contact.get().getNumber().get()); - } - return new AccountRecord.PinnedConversation.Builder().contact(contactBuilder.build()).build(); - } else if (groupV1Id.isPresent()) { - return new AccountRecord.PinnedConversation.Builder().legacyGroupId(ByteString.of(groupV1Id.get())).build(); - } else if (groupV2MasterKey.isPresent()) { - return new AccountRecord.PinnedConversation.Builder().groupMasterKey(ByteString.of(groupV2MasterKey.get())).build(); - } else { - return new AccountRecord.PinnedConversation.Builder().build(); - } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PinnedConversation that = (PinnedConversation) o; - return contact.equals(that.contact) && - groupV1Id.equals(that.groupV1Id) && - groupV2MasterKey.equals(that.groupV2MasterKey); - } - - @Override - public int hashCode() { - return Objects.hash(contact, groupV1Id, groupV2MasterKey); - } - } - - public static class Subscriber { - private final Optional currencyCode; - private final Optional id; - - public Subscriber(String currencyCode, byte[] id) { - if (currencyCode != null && id != null && id.length == 32) { - this.currencyCode = Optional.of(currencyCode); - this.id = Optional.of(id); - } else { - this.currencyCode = Optional.empty(); - this.id = Optional.empty(); - } - } - - public Optional getCurrencyCode() { - return currencyCode; - } - - public Optional getId() { - return id; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final Subscriber that = (Subscriber) o; - return Objects.equals(currencyCode, that.currencyCode) && OptionalUtil.byteArrayEquals(id, that.id); - } - - @Override - public int hashCode() { - return Objects.hash(currencyCode, id); - } - } - - public static class Payments { - private static final String TAG = Payments.class.getSimpleName(); - - private final boolean enabled; - private final Optional entropy; - - public Payments(boolean enabled, Optional entropy) { - byte[] entropyBytes = entropy.orElse(null); - if (entropyBytes != null && entropyBytes.length != PaymentsConstants.PAYMENTS_ENTROPY_LENGTH) { - Log.w(TAG, "Blocked entropy of length " + entropyBytes.length); - entropyBytes = null; - } - this.entropy = Optional.ofNullable(entropyBytes); - this.enabled = enabled && this.entropy.isPresent(); - } - - public boolean isEnabled() { - return enabled; - } - - public Optional getEntropy() { - return entropy; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Payments payments = (Payments) o; - return enabled == payments.enabled && - OptionalUtil.byteArrayEquals(entropy, payments.entropy); - } - - @Override - public int hashCode() { - return Objects.hash(enabled, entropy); - } - } - - public static final class Builder { - private final StorageId id; - private final AccountRecord.Builder builder; - - public Builder(byte[] rawId, byte[] serializedUnknowns) { - this.id = StorageId.forAccount(rawId); - - if (serializedUnknowns != null) { - this.builder = parseUnknowns(serializedUnknowns); - } else { - this.builder = new AccountRecord.Builder(); - } - } - - public Builder setGivenName(String givenName) { - builder.givenName(givenName == null ? "" : givenName); - return this; - } - - public Builder setFamilyName(String familyName) { - builder.familyName(familyName == null ? "" : familyName); - return this; - } - - public Builder setProfileKey(byte[] profileKey) { - builder.profileKey(profileKey == null ? ByteString.EMPTY : ByteString.of(profileKey)); - return this; - } - - public Builder setAvatarUrlPath(String urlPath) { - builder.avatarUrlPath(urlPath == null ? "" : urlPath); - return this; - } - - public Builder setNoteToSelfArchived(boolean archived) { - builder.noteToSelfArchived(archived); - return this; - } - - public Builder setNoteToSelfForcedUnread(boolean forcedUnread) { - builder.noteToSelfMarkedUnread(forcedUnread); - return this; - } - - public Builder setReadReceiptsEnabled(boolean enabled) { - builder.readReceipts(enabled); - return this; - } - - public Builder setTypingIndicatorsEnabled(boolean enabled) { - builder.typingIndicators(enabled); - return this; - } - - public Builder setSealedSenderIndicatorsEnabled(boolean enabled) { - builder.sealedSenderIndicators(enabled); - return this; - } - - public Builder setLinkPreviewsEnabled(boolean enabled) { - builder.linkPreviews(enabled); - return this; - } - - public Builder setPhoneNumberSharingMode(AccountRecord.PhoneNumberSharingMode mode) { - builder.phoneNumberSharingMode(mode); - return this; - } - - public Builder setUnlistedPhoneNumber(boolean unlisted) { - builder.unlistedPhoneNumber(unlisted); - return this; - } - - public Builder setPinnedConversations(List pinnedConversations) { - builder.pinnedConversations(pinnedConversations.stream().map(PinnedConversation::toRemote).collect(Collectors.toList())); - return this; - } - - public Builder setPreferContactAvatars(boolean preferContactAvatars) { - builder.preferContactAvatars(preferContactAvatars); - return this; - } - - public Builder setPayments(boolean enabled, byte[] entropy) { - org.whispersystems.signalservice.internal.storage.protos.Payments.Builder paymentsBuilder = new org.whispersystems.signalservice.internal.storage.protos.Payments.Builder(); - - boolean entropyPresent = entropy != null && entropy.length == PaymentsConstants.PAYMENTS_ENTROPY_LENGTH; - - paymentsBuilder.enabled(enabled && entropyPresent); - - if (entropyPresent) { - paymentsBuilder.entropy(ByteString.of(entropy)); - } - - builder.payments(paymentsBuilder.build()); - - return this; - } - - public Builder setUniversalExpireTimer(int timer) { - builder.universalExpireTimer(timer); - return this; - } - - public Builder setPrimarySendsSms(boolean primarySendsSms) { - builder.primarySendsSms(primarySendsSms); - return this; - } - - public Builder setE164(String e164) { - builder.e164(e164); - return this; - } - - public Builder setDefaultReactions(List defaultReactions) { - builder.preferredReactionEmoji(new ArrayList<>(defaultReactions)); - return this; - } - - public Builder setSubscriber(Subscriber subscriber) { - if (subscriber.id.isPresent() && subscriber.currencyCode.isPresent()) { - builder.subscriberId(ByteString.of(subscriber.id.get())); - builder.subscriberCurrencyCode(subscriber.currencyCode.get()); - } else { - builder.subscriberId(StorageRecordProtoUtil.getDefaultAccountRecord().subscriberId); - builder.subscriberCurrencyCode(StorageRecordProtoUtil.getDefaultAccountRecord().subscriberCurrencyCode); - } - - return this; - } - - public Builder setBackupsSubscriber(Subscriber subscriber) { - if (subscriber.id.isPresent() && subscriber.currencyCode.isPresent()) { - builder.backupsSubscriberId(ByteString.of(subscriber.id.get())); - builder.backupsSubscriberCurrencyCode(subscriber.currencyCode.get()); - } else { - builder.backupsSubscriberId(StorageRecordProtoUtil.getDefaultAccountRecord().subscriberId); - builder.backupsSubscriberCurrencyCode(StorageRecordProtoUtil.getDefaultAccountRecord().subscriberCurrencyCode); - } - - return this; - } - - public Builder setDisplayBadgesOnProfile(boolean displayBadgesOnProfile) { - builder.displayBadgesOnProfile(displayBadgesOnProfile); - return this; - } - - public Builder setSubscriptionManuallyCancelled(boolean subscriptionManuallyCancelled) { - builder.subscriptionManuallyCancelled(subscriptionManuallyCancelled); - return this; - } - - public Builder setKeepMutedChatsArchived(boolean keepMutedChatsArchived) { - builder.keepMutedChatsArchived(keepMutedChatsArchived); - return this; - } - - public Builder setHasSetMyStoriesPrivacy(boolean hasSetMyStoriesPrivacy) { - builder.hasSetMyStoriesPrivacy(hasSetMyStoriesPrivacy); - return this; - } - - public Builder setHasViewedOnboardingStory(boolean hasViewedOnboardingStory) { - builder.hasViewedOnboardingStory(hasViewedOnboardingStory); - return this; - } - - public Builder setStoriesDisabled(boolean storiesDisabled) { - builder.storiesDisabled(storiesDisabled); - return this; - } - - public Builder setStoryViewReceiptsState(OptionalBool storyViewedReceiptsEnabled) { - builder.storyViewReceiptsEnabled(storyViewedReceiptsEnabled); - return this; - } - - public Builder setHasSeenGroupStoryEducationSheet(boolean hasSeenGroupStoryEducationSheet) { - builder.hasSeenGroupStoryEducationSheet(hasSeenGroupStoryEducationSheet); - return this; - } - - public Builder setHasCompletedUsernameOnboarding(boolean hasCompletedUsernameOnboarding) { - builder.hasCompletedUsernameOnboarding(hasCompletedUsernameOnboarding); - return this; - } - - public Builder setUsername(@Nullable String username) { - if (username == null || username.isEmpty()) { - builder.username(StorageRecordProtoUtil.getDefaultAccountRecord().username); - } else { - builder.username(username); - } - - return this; - } - - public Builder setUsernameLink(@Nullable AccountRecord.UsernameLink link) { - if (link == null) { - builder.usernameLink(StorageRecordProtoUtil.getDefaultAccountRecord().usernameLink); - } else { - builder.usernameLink(link); - } - - return this; - } - - private static AccountRecord.Builder parseUnknowns(byte[] serializedUnknowns) { - try { - return AccountRecord.ADAPTER.decode(serializedUnknowns).newBuilder(); - } catch (IOException e) { - Log.w(TAG, "Failed to combine unknown fields!", e); - return new AccountRecord.Builder(); - } - } - - public SignalAccountRecord build() { - return new SignalAccountRecord(id, builder.build()); - } - } -} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.kt new file mode 100644 index 0000000000..acf6a4963c --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.kt @@ -0,0 +1,59 @@ +package org.whispersystems.signalservice.api.storage + +import org.signal.core.util.hasUnknownFields +import org.signal.libsignal.protocol.logging.Log +import org.whispersystems.signalservice.internal.storage.protos.AccountRecord +import java.io.IOException + +class SignalAccountRecord( + override val id: StorageId, + override val proto: AccountRecord +) : SignalRecord { + + companion object { + private val TAG: String = SignalAccountRecord::class.java.simpleName + + fun newBuilder(serializedUnknowns: ByteArray?): AccountRecord.Builder { + return if (serializedUnknowns != null) { + parseUnknowns(serializedUnknowns) + } else { + AccountRecord.Builder() + } + } + + private fun parseUnknowns(serializedUnknowns: ByteArray): AccountRecord.Builder { + try { + return AccountRecord.ADAPTER.decode(serializedUnknowns).newBuilder() + } catch (e: IOException) { + Log.w(TAG, "Failed to combine unknown fields!", e) + return AccountRecord.Builder() + } + } + } + + override fun asStorageRecord(): SignalStorageRecord { + return SignalStorageRecord.forAccount(this) + } + + fun serializeUnknownFields(): ByteArray? { + return if (proto.hasUnknownFields()) proto.encode() else null + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SignalAccountRecord + + if (id != other.id) return false + if (proto != other.proto) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + proto.hashCode() + return result + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalCallLinkRecord.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalCallLinkRecord.kt index 07668e6968..c9e8b4868b 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalCallLinkRecord.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalCallLinkRecord.kt @@ -8,58 +8,23 @@ package org.whispersystems.signalservice.api.storage import okio.ByteString.Companion.toByteString import org.whispersystems.signalservice.internal.storage.protos.CallLinkRecord import java.io.IOException -import java.util.LinkedList /** * A record in storage service that represents a call link that was already created. */ -class SignalCallLinkRecord(private val id: StorageId, private val proto: CallLinkRecord) : SignalRecord { +class SignalCallLinkRecord( + override val id: StorageId, + override val proto: CallLinkRecord +) : SignalRecord { val rootKey: ByteArray = proto.rootKey.toByteArray() val adminPassKey: ByteArray = proto.adminPasskey.toByteArray() val deletionTimestamp: Long = proto.deletedAtTimestampMs - fun toProto(): CallLinkRecord { - return proto - } - - override fun getId(): StorageId { - return id - } - override fun asStorageRecord(): SignalStorageRecord { return SignalStorageRecord.forCallLink(this) } - override fun describeDiff(other: SignalRecord?): String { - return when (other) { - is SignalCallLinkRecord -> { - val diff = LinkedList() - if (!rootKey.contentEquals(other.rootKey)) { - diff.add("RootKey") - } - - if (!adminPassKey.contentEquals(other.adminPassKey)) { - diff.add("AdminPassKey") - } - - if (deletionTimestamp != other.deletionTimestamp) { - diff.add("DeletionTimestamp") - } - - diff.toString() - } - - null -> { - "Other was null!" - } - - else -> { - "Different class. ${this::class.java.getSimpleName()} | ${other::class.java.getSimpleName()}" - } - } - } - fun isDeleted(): Boolean { return deletionTimestamp > 0 } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalContactRecord.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalContactRecord.java index 90e468a8c8..bde89f4c70 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalContactRecord.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalContactRecord.java @@ -1,5 +1,6 @@ package org.whispersystems.signalservice.api.storage; +import org.jetbrains.annotations.NotNull; import org.signal.core.util.ProtoUtil; import org.signal.libsignal.protocol.logging.Log; import org.whispersystems.signalservice.api.push.ServiceId; @@ -20,7 +21,7 @@ import javax.annotation.Nullable; import okio.ByteString; -public final class SignalContactRecord implements SignalRecord { +public final class SignalContactRecord implements SignalRecord { private static final String TAG = SignalContactRecord.class.getSimpleName(); @@ -69,124 +70,13 @@ public final class SignalContactRecord implements SignalRecord { } @Override - public SignalStorageRecord asStorageRecord() { - return SignalStorageRecord.forContact(this); + public ContactRecord getProto() { + return proto; } @Override - public String describeDiff(SignalRecord other) { - if (other instanceof SignalContactRecord) { - SignalContactRecord that = (SignalContactRecord) other; - List diff = new LinkedList<>(); - - if (!Arrays.equals(this.id.getRaw(), that.id.getRaw())) { - diff.add("ID"); - } - - if (!Objects.equals(this.getAci(), that.getAci())) { - diff.add("ACI"); - } - - if (!Objects.equals(this.getPni(), that.getPni())) { - diff.add("PNI"); - } - - if (!Objects.equals(this.getNumber(), that.getNumber())) { - diff.add("E164"); - } - - if (!Objects.equals(this.profileGivenName, that.profileGivenName)) { - diff.add("ProfileGivenName"); - } - - if (!Objects.equals(this.profileFamilyName, that.profileFamilyName)) { - diff.add("ProfileFamilyName"); - } - - if (!Objects.equals(this.systemGivenName, that.systemGivenName)) { - diff.add("SystemGivenName"); - } - - if (!Objects.equals(this.systemFamilyName, that.systemFamilyName)) { - diff.add("SystemFamilyName"); - } - - if (!Objects.equals(this.systemNickname, that.systemNickname)) { - diff.add("SystemNickname"); - } - - if (!OptionalUtil.byteArrayEquals(this.profileKey, that.profileKey)) { - diff.add("ProfileKey"); - } - - if (!Objects.equals(this.username, that.username)) { - diff.add("Username"); - } - - if (!OptionalUtil.byteArrayEquals(this.identityKey, that.identityKey)) { - diff.add("IdentityKey"); - } - - if (!Objects.equals(this.getIdentityState(), that.getIdentityState())) { - diff.add("IdentityState"); - } - - if (!Objects.equals(this.isBlocked(), that.isBlocked())) { - diff.add("Blocked"); - } - - if (!Objects.equals(this.isProfileSharingEnabled(), that.isProfileSharingEnabled())) { - diff.add("ProfileSharing"); - } - - if (!Objects.equals(this.isArchived(), that.isArchived())) { - diff.add("Archived"); - } - - if (!Objects.equals(this.isForcedUnread(), that.isForcedUnread())) { - diff.add("ForcedUnread"); - } - - if (!Objects.equals(this.getMuteUntil(), that.getMuteUntil())) { - diff.add("MuteUntil"); - } - - if (shouldHideStory() != that.shouldHideStory()) { - diff.add("HideStory"); - } - - if (getUnregisteredTimestamp() != that.getUnregisteredTimestamp()) { - diff.add("UnregisteredTimestamp"); - } - - if (isHidden() != that.isHidden()) { - diff.add("Hidden"); - } - - if (isPniSignatureVerified() != that.isPniSignatureVerified()) { - diff.add("PniSignatureVerified"); - } - - if (!Objects.equals(this.hasUnknownFields(), that.hasUnknownFields())) { - diff.add("UnknownFields"); - } - - if (!Objects.equals(this.nicknameGivenName, that.nicknameGivenName)) { - diff.add("NicknameGivenName"); - } - - if (!Objects.equals(this.nicknameFamilyName, that.nicknameFamilyName)) { - diff.add("NicknameFamilyName"); - } - - if (!Objects.equals(this.note, that.note)) { - diff.add("Note"); - } - - return diff.toString(); - } else { - return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName(); - } + public SignalStorageRecord asStorageRecord() { + return SignalStorageRecord.forContact(this); } public boolean hasUnknownFields() { @@ -310,10 +200,6 @@ public final class SignalContactRecord implements SignalRecord { return new SignalContactRecord(id, proto.newBuilder().pni("").build()); } - public ContactRecord toProto() { - return proto; - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV1Record.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV1Record.java index 3537171cc9..b43aa08830 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV1Record.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV1Record.java @@ -12,7 +12,7 @@ import java.util.Objects; import okio.ByteString; -public final class SignalGroupV1Record implements SignalRecord { +public final class SignalGroupV1Record implements SignalRecord { private static final String TAG = SignalGroupV1Record.class.getSimpleName(); @@ -33,53 +33,13 @@ public final class SignalGroupV1Record implements SignalRecord { return id; } - @Override - public SignalStorageRecord asStorageRecord() { - return SignalStorageRecord.forGroupV1(this); + @Override public GroupV1Record getProto() { + return proto; } @Override - public String describeDiff(SignalRecord other) { - if (other instanceof SignalGroupV1Record) { - SignalGroupV1Record that = (SignalGroupV1Record) other; - List diff = new LinkedList<>(); - - if (!Arrays.equals(this.id.getRaw(), that.id.getRaw())) { - diff.add("ID"); - } - - if (!Arrays.equals(this.groupId, that.groupId)) { - diff.add("MasterKey"); - } - - if (!Objects.equals(this.isBlocked(), that.isBlocked())) { - diff.add("Blocked"); - } - - if (!Objects.equals(this.isProfileSharingEnabled(), that.isProfileSharingEnabled())) { - diff.add("ProfileSharing"); - } - - if (!Objects.equals(this.isArchived(), that.isArchived())) { - diff.add("Archived"); - } - - if (!Objects.equals(this.isForcedUnread(), that.isForcedUnread())) { - diff.add("ForcedUnread"); - } - - if (!Objects.equals(this.getMuteUntil(), that.getMuteUntil())) { - diff.add("MuteUntil"); - } - - if (!Objects.equals(this.hasUnknownFields(), that.hasUnknownFields())) { - diff.add("UnknownFields"); - } - - return diff.toString(); - } else { - return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName(); - } + public SignalStorageRecord asStorageRecord() { + return SignalStorageRecord.forGroupV1(this); } public boolean hasUnknownFields() { @@ -114,10 +74,6 @@ public final class SignalGroupV1Record implements SignalRecord { return proto.mutedUntilTimestamp; } - public GroupV1Record toProto() { - return proto; - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV2Record.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV2Record.java index 059a8979ad..6506cbb060 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV2Record.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV2Record.java @@ -1,5 +1,6 @@ package org.whispersystems.signalservice.api.storage; +import org.jetbrains.annotations.NotNull; import org.signal.core.util.ProtoUtil; import org.signal.libsignal.protocol.logging.Log; import org.signal.libsignal.zkgroup.InvalidInputException; @@ -14,7 +15,7 @@ import java.util.Objects; import okio.ByteString; -public final class SignalGroupV2Record implements SignalRecord { +public final class SignalGroupV2Record implements SignalRecord { private static final String TAG = SignalGroupV2Record.class.getSimpleName(); @@ -35,65 +36,13 @@ public final class SignalGroupV2Record implements SignalRecord { return id; } - @Override - public SignalStorageRecord asStorageRecord() { - return SignalStorageRecord.forGroupV2(this); + @Override public GroupV2Record getProto() { + return proto; } @Override - public String describeDiff(SignalRecord other) { - if (other instanceof SignalGroupV2Record) { - SignalGroupV2Record that = (SignalGroupV2Record) other; - List diff = new LinkedList<>(); - - if (!Arrays.equals(this.id.getRaw(), that.id.getRaw())) { - diff.add("ID"); - } - - if (!Arrays.equals(this.getMasterKeyBytes(), that.getMasterKeyBytes())) { - diff.add("MasterKey"); - } - - if (!Objects.equals(this.isBlocked(), that.isBlocked())) { - diff.add("Blocked"); - } - - if (!Objects.equals(this.isProfileSharingEnabled(), that.isProfileSharingEnabled())) { - diff.add("ProfileSharing"); - } - - if (!Objects.equals(this.isArchived(), that.isArchived())) { - diff.add("Archived"); - } - - if (!Objects.equals(this.isForcedUnread(), that.isForcedUnread())) { - diff.add("ForcedUnread"); - } - - if (!Objects.equals(this.getMuteUntil(), that.getMuteUntil())) { - diff.add("MuteUntil"); - } - - if (!Objects.equals(this.notifyForMentionsWhenMuted(), that.notifyForMentionsWhenMuted())) { - diff.add("NotifyForMentionsWhenMuted"); - } - - if (shouldHideStory() != that.shouldHideStory()) { - diff.add("HideStory"); - } - - if (!Objects.equals(this.getStorySendMode(), that.getStorySendMode())) { - diff.add("StorySendMode"); - } - - if (!Objects.equals(this.hasUnknownFields(), that.hasUnknownFields())) { - diff.add("UnknownFields"); - } - - return diff.toString(); - } else { - return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName(); - } + public SignalStorageRecord asStorageRecord() { + return SignalStorageRecord.forGroupV2(this); } public boolean hasUnknownFields() { @@ -148,10 +97,6 @@ public final class SignalGroupV2Record implements SignalRecord { return proto.storySendMode; } - public GroupV2Record toProto() { - return proto; - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalRecord.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalRecord.java deleted file mode 100644 index ebe060990d..0000000000 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalRecord.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.whispersystems.signalservice.api.storage; - -public interface SignalRecord { - StorageId getId(); - SignalStorageRecord asStorageRecord(); - String describeDiff(SignalRecord other); -} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalRecord.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalRecord.kt new file mode 100644 index 0000000000..dc12d9985e --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalRecord.kt @@ -0,0 +1,52 @@ +package org.whispersystems.signalservice.api.storage + +import kotlin.reflect.KVisibility +import kotlin.reflect.full.memberProperties + +interface SignalRecord { + val id: StorageId + val proto: E + fun asStorageRecord(): SignalStorageRecord + + fun describeDiff(other: SignalRecord<*>): String { + if (this::class != other::class) { + return "Classes are different!" + } + + if (this.proto!!::class != other.proto!!::class) { + return "Proto classes are different!" + } + + val myFields = this.proto!!::class.memberProperties + val otherFields = other.proto!!::class.memberProperties + + val myFieldsByName = myFields + .filter { it.isFinal && it.visibility == KVisibility.PUBLIC } + .associate { it.name to it.getter.call(this.proto!!) } + + val otherFieldsByName = otherFields + .filter { it.isFinal && it.visibility == KVisibility.PUBLIC } + .associate { it.name to it.getter.call(other.proto!!) } + + val mismatching = mutableListOf() + + if (this.id != other.id) { + mismatching += "id" + } + + for (key in myFieldsByName.keys) { + val myValue = myFieldsByName[key] + val otherValue = otherFieldsByName[key] + + if (myValue != otherValue) { + mismatching += key + } + } + + return if (mismatching.isEmpty()) { + "All fields match." + } else { + mismatching.sorted().joinToString(prefix = "Some fields differ: ", separator = ", ") + } + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageModels.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageModels.java index fca879f2b5..9cefaad638 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageModels.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageModels.java @@ -64,17 +64,17 @@ public final class SignalStorageModels { StorageRecord.Builder builder = new StorageRecord.Builder(); if (record.getContact().isPresent()) { - builder.contact(record.getContact().get().toProto()); + builder.contact(record.getContact().get().getProto()); } else if (record.getGroupV1().isPresent()) { - builder.groupV1(record.getGroupV1().get().toProto()); + builder.groupV1(record.getGroupV1().get().getProto()); } else if (record.getGroupV2().isPresent()) { - builder.groupV2(record.getGroupV2().get().toProto()); + builder.groupV2(record.getGroupV2().get().getProto()); } else if (record.getAccount().isPresent()) { - builder.account(record.getAccount().get().toProto()); + builder.account(record.getAccount().get().getProto()); } else if (record.getStoryDistributionList().isPresent()) { - builder.storyDistributionList(record.getStoryDistributionList().get().toProto()); + builder.storyDistributionList(record.getStoryDistributionList().get().getProto()); } else if (record.getCallLink().isPresent()) { - builder.callLink(record.getCallLink().get().toProto()); + builder.callLink(record.getCallLink().get().getProto()); } else { throw new InvalidStorageWriteError(); } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageRecord.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageRecord.java index 2e39298e9e..7ad71cbe8d 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageRecord.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageRecord.java @@ -7,7 +7,7 @@ import org.whispersystems.signalservice.internal.storage.protos.CallLinkRecord; import java.util.Objects; import java.util.Optional; -public class SignalStorageRecord implements SignalRecord { +public class SignalStorageRecord { private final StorageId id; private final Optional storyDistributionList; @@ -89,21 +89,10 @@ public class SignalStorageRecord implements SignalRecord { this.callLink = callLink; } - @Override public StorageId getId() { return id; } - @Override - public SignalStorageRecord asStorageRecord() { - return this; - } - - @Override - public String describeDiff(SignalRecord other) { - return "Diffs not supported."; - } - public int getType() { return id.getType(); } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStoryDistributionListRecord.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStoryDistributionListRecord.java index e7c497c2ff..25c7904c2e 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStoryDistributionListRecord.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStoryDistributionListRecord.java @@ -1,5 +1,6 @@ package org.whispersystems.signalservice.api.storage; +import org.jetbrains.annotations.NotNull; import org.signal.core.util.ProtoUtil; import org.signal.libsignal.protocol.logging.Log; import org.whispersystems.signalservice.api.push.ServiceId; @@ -15,7 +16,7 @@ import java.util.stream.Collectors; import okio.ByteString; -public class SignalStoryDistributionListRecord implements SignalRecord { +public class SignalStoryDistributionListRecord implements SignalRecord { private static final String TAG = SignalStoryDistributionListRecord.class.getSimpleName(); @@ -42,12 +43,13 @@ public class SignalStoryDistributionListRecord implements SignalRecord { } @Override - public SignalStorageRecord asStorageRecord() { - return SignalStorageRecord.forStoryDistributionList(this); + public StoryDistributionListRecord getProto() { + return proto; } - public StoryDistributionListRecord toProto() { - return proto; + @Override + public SignalStorageRecord asStorageRecord() { + return SignalStorageRecord.forStoryDistributionList(this); } public byte[] serializeUnknownFields() { @@ -78,46 +80,6 @@ public class SignalStoryDistributionListRecord implements SignalRecord { return proto.isBlockList; } - @Override - public String describeDiff(SignalRecord other) { - if (other instanceof SignalStoryDistributionListRecord) { - SignalStoryDistributionListRecord that = (SignalStoryDistributionListRecord) other; - List diff = new LinkedList<>(); - - if (!Arrays.equals(this.id.getRaw(), that.id.getRaw())) { - diff.add("ID"); - } - - if (!Arrays.equals(this.getIdentifier(), that.getIdentifier())) { - diff.add("Identifier"); - } - - if (!Objects.equals(this.getName(), that.getName())) { - diff.add("Name"); - } - - if (!Objects.equals(this.recipients, that.recipients)) { - diff.add("RecipientUuids"); - } - - if (this.getDeletedAtTimestamp() != that.getDeletedAtTimestamp()) { - diff.add("DeletedAtTimestamp"); - } - - if (this.allowsReplies() != that.allowsReplies()) { - diff.add("AllowsReplies"); - } - - if (this.isBlockList() != that.isBlockList()) { - diff.add("BlockList"); - } - - return diff.toString(); - } else { - return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName(); - } - } - @Override public boolean equals(Object o) { if (this == o) return true;