diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 6e2486e128..2a56e9b0b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.migrations.ApplyUnknownFieldsToSelfMigrationJo import org.thoughtcrime.securesms.migrations.AttachmentCleanupMigrationJob; import org.thoughtcrime.securesms.migrations.AttachmentHashBackfillMigrationJob; import org.thoughtcrime.securesms.migrations.AttributesMigrationJob; +import org.thoughtcrime.securesms.migrations.AvatarColorStorageServiceMigrationJob; import org.thoughtcrime.securesms.migrations.AvatarIdRemovalMigrationJob; import org.thoughtcrime.securesms.migrations.AvatarMigrationJob; import org.thoughtcrime.securesms.migrations.BackfillDigestsForDuplicatesMigrationJob; @@ -271,6 +272,7 @@ public final class JobManagerFactories { put(AttachmentCleanupMigrationJob.KEY, new AttachmentCleanupMigrationJob.Factory()); put(AttachmentHashBackfillMigrationJob.KEY, new AttachmentHashBackfillMigrationJob.Factory()); put(AttributesMigrationJob.KEY, new AttributesMigrationJob.Factory()); + put(AvatarColorStorageServiceMigrationJob.KEY, new AvatarColorStorageServiceMigrationJob.Factory()); put(AvatarIdRemovalMigrationJob.KEY, new AvatarIdRemovalMigrationJob.Factory()); put(AvatarMigrationJob.KEY, new AvatarMigrationJob.Factory()); put(BackfillDigestsMigrationJob.KEY, new BackfillDigestsMigrationJob.Factory()); 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 6ad7183e64..509d8d2dad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.kt @@ -107,9 +107,7 @@ import java.util.stream.Collectors * == Syncing a new field on an existing record == * * - Add the field the the respective proto - * - Update the respective model (i.e. [SignalContactRecord]) - * - Add getters - * - Update the builder + * - Update [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() diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 145fd67dab..1419875c60 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -173,9 +173,10 @@ public class ApplicationMigrations { static final int FTS_TRIGGER_FIX = 129; static final int THREAD_TABLE_PINNED_MIGRATION = 130; static final int GROUP_DECLINE_INVITE_FIX = 131; + static final int AVATAR_COLOR_MIGRATION_JOB = 132; } - public static final int CURRENT_VERSION = 131; + public static final int CURRENT_VERSION = 132; /** * This *must* be called after the {@link JobManager} has been instantiated, but *before* the call @@ -798,6 +799,10 @@ public class ApplicationMigrations { jobs.put(Version.GROUP_DECLINE_INVITE_FIX, new DatabaseMigrationJob()); } + if (lastSeenVersion < Version.AVATAR_COLOR_MIGRATION_JOB) { + jobs.put(Version.AVATAR_COLOR_MIGRATION_JOB, new AvatarColorStorageServiceMigrationJob()); + } + return jobs; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/AvatarColorStorageServiceMigrationJob.kt b/app/src/main/java/org/thoughtcrime/securesms/migrations/AvatarColorStorageServiceMigrationJob.kt new file mode 100644 index 0000000000..d775d68abe --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/AvatarColorStorageServiceMigrationJob.kt @@ -0,0 +1,71 @@ +package org.thoughtcrime.securesms.migrations + +import org.signal.core.util.logging.Log +import org.signal.core.util.requireLong +import org.signal.core.util.select +import org.signal.core.util.withinTransaction +import org.thoughtcrime.securesms.database.RecipientTable +import org.thoughtcrime.securesms.database.RecipientTable.Companion.ID +import org.thoughtcrime.securesms.database.RecipientTable.Companion.STORAGE_SERVICE_ID +import org.thoughtcrime.securesms.database.RecipientTable.Companion.TABLE_NAME +import org.thoughtcrime.securesms.database.RecipientTable.Companion.TYPE +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.storage.StorageSyncHelper + +/** + * A job that marks all contacts and groups as needing to be synced, so that we'll update the + * storage records with the new avatar color field. + */ +internal class AvatarColorStorageServiceMigrationJob( + parameters: Parameters = Parameters.Builder().build() +) : MigrationJob(parameters) { + + companion object { + val TAG = Log.tag(AvatarColorStorageServiceMigrationJob::class.java) + const val KEY = "AvatarColorStorageServiceMigrationJob" + } + + override fun getFactoryKey(): String = KEY + + override fun isUiBlocking(): Boolean = false + + override fun performMigration() { + if (!Recipient.isSelfSet) { + return + } + + if (!SignalStore.account.isRegistered) { + return + } + + SignalDatabase.recipients.markNeedsSync(Recipient.self().id) + SignalDatabase.recipients.markAllContactsAndGroupsAsNeedsSync() + StorageSyncHelper.scheduleSyncForDataChange() + } + + override fun shouldRetry(e: Exception): Boolean = false + + private fun RecipientTable.markAllContactsAndGroupsAsNeedsSync() { + writableDatabase.withinTransaction { db -> + db.select(ID) + .from(TABLE_NAME) + .where("$STORAGE_SERVICE_ID NOT NULL AND $TYPE IN (${RecipientTable.RecipientType.INDIVIDUAL.id}, ${RecipientTable.RecipientType.GV2.id})") + .run() + .use { cursor -> + while (cursor.moveToNext()) { + rotateStorageId(RecipientId.from(cursor.requireLong(ID))) + } + } + } + } + + class Factory : Job.Factory { + override fun create(parameters: Parameters, serializedData: ByteArray?): AvatarColorStorageServiceMigrationJob { + return AvatarColorStorageServiceMigrationJob(parameters) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/ContactRecordProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/storage/ContactRecordProcessor.kt index c9c826b40c..95f43f57fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/ContactRecordProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/ContactRecordProcessor.kt @@ -231,6 +231,7 @@ class ContactRecordProcessor( nickname = remote.proto.nickname pniSignatureVerified = remote.proto.pniSignatureVerified || local.proto.pniSignatureVerified note = remote.proto.note.nullIfBlank() ?: "" + avatarColor = local.proto.avatarColor }.build().toSignalContactRecord(StorageId.forContact(keyGenerator.generate())) val matchesRemote = doParamsMatch(remote, merged) diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2RecordProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2RecordProcessor.kt index 1aab13fe53..b24845c310 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2RecordProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2RecordProcessor.kt @@ -58,6 +58,7 @@ class GroupV2RecordProcessor(private val recipientTable: RecipientTable, private dontNotifyForMentionsIfMuted = remote.proto.dontNotifyForMentionsIfMuted hideStory = remote.proto.hideStory storySendMode = remote.proto.storySendMode + avatarColor = local.proto.avatarColor }.build().toSignalGroupV2Record(StorageId.forGroupV2(keyGenerator.generate())) val matchesRemote = doParamsMatch(remote, merged) 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 67c3e16ea1..10e7b536bc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.kt @@ -162,6 +162,7 @@ object StorageSyncHelper { storyViewReceiptsEnabled = storyViewReceiptsState hasSeenGroupStoryEducationSheet = SignalStore.story.userHasSeenGroupStoryEducationSheet hasCompletedUsernameOnboarding = SignalStore.uiHints.hasCompletedUsernameOnboarding() + avatarColor = StorageSyncModels.localToRemoteAvatarColor(self.avatarColor) username = SignalStore.account.username ?: "" usernameLink = SignalStore.account.usernameLink?.let { linkComponents -> AccountRecord.UsernameLink( 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 ab6b7481c1..568c44aea2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.kt @@ -6,6 +6,7 @@ import org.signal.core.util.isNotEmpty import org.signal.core.util.isNullOrEmpty import org.signal.libsignal.zkgroup.groups.GroupMasterKey import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme +import org.thoughtcrime.securesms.conversation.colors.AvatarColor import org.thoughtcrime.securesms.database.GroupTable.ShowAsStoryState import org.thoughtcrime.securesms.database.IdentityTable.VerifiedStatus import org.thoughtcrime.securesms.database.RecipientTable @@ -41,6 +42,7 @@ import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.Id import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record import java.util.Currency import kotlin.math.max +import org.whispersystems.signalservice.internal.storage.protos.AvatarColor as RemoteAvatarColor object StorageSyncModels { @@ -182,6 +184,7 @@ object StorageSyncModels { pniSignatureVerified = recipient.syncExtras.pniSignatureVerified nickname = recipient.nickname.takeUnless { it.isEmpty }?.let { ContactRecord.Name(given = it.givenName, family = it.familyName) } note = recipient.note ?: "" + avatarColor = localToRemoteAvatarColor(recipient.avatarColor) }.build().toSignalContactRecord(StorageId.forContact(rawStorageId)) } @@ -218,6 +221,7 @@ object StorageSyncModels { mutedUntilTimestamp = recipient.muteUntil dontNotifyForMentionsIfMuted = recipient.mentionSetting == RecipientTable.MentionSetting.ALWAYS_NOTIFY hideStory = recipient.extras != null && recipient.extras.hideStory() + avatarColor = localToRemoteAvatarColor(recipient.avatarColor) storySendMode = when (groups.getShowAsStoryState(groupId)) { ShowAsStoryState.ALWAYS -> GroupV2Record.StorySendMode.ENABLED ShowAsStoryState.NEVER -> GroupV2Record.StorySendMode.DISABLED @@ -341,4 +345,23 @@ object StorageSyncModels { return null } } + + fun localToRemoteAvatarColor(avatarColor: AvatarColor): RemoteAvatarColor { + return when (avatarColor) { + AvatarColor.A100 -> RemoteAvatarColor.A100 + AvatarColor.A110 -> RemoteAvatarColor.A110 + AvatarColor.A120 -> RemoteAvatarColor.A120 + AvatarColor.A130 -> RemoteAvatarColor.A130 + AvatarColor.A140 -> RemoteAvatarColor.A140 + AvatarColor.A150 -> RemoteAvatarColor.A150 + AvatarColor.A160 -> RemoteAvatarColor.A160 + AvatarColor.A170 -> RemoteAvatarColor.A170 + AvatarColor.A180 -> RemoteAvatarColor.A180 + AvatarColor.A190 -> RemoteAvatarColor.A190 + AvatarColor.A200 -> RemoteAvatarColor.A200 + AvatarColor.A210 -> RemoteAvatarColor.A210 + AvatarColor.UNKNOWN -> RemoteAvatarColor.A100 + AvatarColor.ON_SURFACE_VARIANT -> RemoteAvatarColor.A100 + } + } } diff --git a/libsignal-service/src/main/protowire/StorageService.proto b/libsignal-service/src/main/protowire/StorageService.proto index 1fbe0e58ee..04046e825b 100644 --- a/libsignal-service/src/main/protowire/StorageService.proto +++ b/libsignal-service/src/main/protowire/StorageService.proto @@ -75,6 +75,31 @@ message StorageRecord { } } + +// If unset - computed as the value of the first byte of SHA-256(msg=CONTACT_ID) +// modulo the count of colors. Once set the avatar color for a recipient is +// never recomputed or changed. +// +// `CONTACT_ID` is the first available identifier from the list: +// - ServiceIdToBinary(ACI) +// - E164 +// - ServiceIdToBinary(PNI) +// - Group Id +enum AvatarColor { + A100 = 0; + A110 = 1; + A120 = 2; + A130 = 3; + A140 = 4; + A150 = 5; + A160 = 6; + A170 = 7; + A180 = 8; + A190 = 9; + A200 = 10; + A210 = 11; +} + message ContactRecord { enum IdentityState { DEFAULT = 0; @@ -110,7 +135,8 @@ message ContactRecord { bool pniSignatureVerified = 21; Name nickname = 22; string note = 23; - // NEXT ID: 24 + optional AvatarColor avatarColor = 24; + // Next ID: 25 } message GroupV1Record { @@ -139,6 +165,7 @@ message GroupV2Record { bool hideStory = 8; reserved /* storySendEnabled */ 9; StorySendMode storySendMode = 10; + optional AvatarColor avatarColor = 11; } message Payments { @@ -237,6 +264,7 @@ message AccountRecord { optional bool hasBackup = 39; // Set to true after backups are enabled and one is uploaded. optional uint64 backupTier = 40; // See zkgroup for integer particular values IAPSubscriberData backupSubscriberData = 41; + optional AvatarColor avatarColor = 42; } message StoryDistributionListRecord {