From 290c107698975bb7cbce06e9b9f9a7f2867cff61 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Mon, 5 Jun 2023 10:07:10 -0300 Subject: [PATCH] Implement simple avatar color picking algorithm to align with iOS. --- .../links/create/CreateCallLinkViewModel.kt | 4 +- .../conversation/colors/AvatarColor.java | 2 +- .../conversation/colors/AvatarColorHash.kt | 56 +++++++++++++++++++ .../securesms/database/RecipientTable.kt | 23 +++++--- .../messages/SyncMessageProcessor.kt | 4 +- .../securesms/util/CommunicationActions.java | 4 +- 6 files changed, 78 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColorHash.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkViewModel.kt index 0b6ddf20f5..ecbfffa57f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/links/create/CreateCallLinkViewModel.kt @@ -16,7 +16,7 @@ import io.reactivex.rxjava3.kotlin.subscribeBy import org.signal.ringrtc.CallLinkState.Restrictions import org.thoughtcrime.securesms.calls.links.CallLinks import org.thoughtcrime.securesms.calls.links.UpdateCallLinkRepository -import org.thoughtcrime.securesms.conversation.colors.AvatarColor +import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash import org.thoughtcrime.securesms.database.CallLinkTable import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials @@ -29,7 +29,7 @@ class CreateCallLinkViewModel( private val mutationRepository: UpdateCallLinkRepository = UpdateCallLinkRepository() ) : ViewModel() { private val credentials = CallLinkCredentials.generate() - private val avatarColor = AvatarColor.random() + private val avatarColor = AvatarColorHash.forCallLink(credentials.linkKeyBytes) private val _callLink: MutableState = mutableStateOf( CallLinkTable.CallLink( recipientId = RecipientId.UNKNOWN, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColor.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColor.java index ff679e476d..854e48babe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColor.java @@ -98,7 +98,7 @@ public enum AvatarColor { } /** Colors that can be assigned via {@link #random()}. */ - private static final AvatarColor[] RANDOM_OPTIONS = new AvatarColor[] { + static final AvatarColor[] RANDOM_OPTIONS = new AvatarColor[] { A100, A110, A120, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColorHash.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColorHash.kt new file mode 100644 index 0000000000..0d8cf7c7f3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColorHash.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.conversation.colors + +import org.thoughtcrime.securesms.groups.GroupId + +/** + * Stolen from iOS. Utilizes a simple hash to map different characteristics to an avatar color index. + */ +object AvatarColorHash { + + /** + * Utilize Uppercase UUID of ServiceId. + * + * Uppercase is necessary here because iOS utilizes uppercase UUIDs by default. + */ + fun forAddress(serviceId: String?, e164: String?): AvatarColor { + if (!serviceId.isNullOrEmpty()) { + return forSeed(serviceId.toString().uppercase()) + } + + if (!e164.isNullOrEmpty()) { + return forSeed(e164) + } + + return AvatarColor.A100 + } + + fun forGroupId(group: GroupId): AvatarColor { + return forData(group.decodedId) + } + + fun forSeed(seed: String): AvatarColor { + return forData(seed.toByteArray()) + } + + fun forCallLink(rootKey: ByteArray): AvatarColor { + return forIndex(rootKey.first().toInt()) + } + + private fun forData(data: ByteArray): AvatarColor { + var hash = 0 + for (value in data) { + hash = hash.rotateLeft(3) xor value.toInt() + } + + return forIndex(hash) + } + + private fun forIndex(index: Int): AvatarColor { + return AvatarColor.RANDOM_OPTIONS[(index.toUInt() % AvatarColor.RANDOM_OPTIONS.size.toUInt()).toInt()] + } +} 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 27fc27e5b6..ecafc788a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -23,6 +23,7 @@ import org.signal.core.util.optionalInt import org.signal.core.util.optionalLong import org.signal.core.util.optionalString import org.signal.core.util.or +import org.signal.core.util.orNull import org.signal.core.util.readToList import org.signal.core.util.readToSet import org.signal.core.util.readToSingleBoolean @@ -48,6 +49,7 @@ import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.color.MaterialColor import org.thoughtcrime.securesms.color.MaterialColor.UnknownColorException import org.thoughtcrime.securesms.conversation.colors.AvatarColor +import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash import org.thoughtcrime.securesms.conversation.colors.ChatColors import org.thoughtcrime.securesms.conversation.colors.ChatColors.Companion.forChatColor import org.thoughtcrime.securesms.conversation.colors.ChatColors.Id.Companion.forLongValue @@ -602,7 +604,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da } else { val values = ContentValues().apply { put(GROUP_ID, groupId.toString()) - put(AVATAR_COLOR, AvatarColor.random().serialize()) + put(AVATAR_COLOR, AvatarColorHash.forGroupId(groupId).serialize()) } val id = writableDatabase.insert(TABLE_NAME, null, values) @@ -3855,12 +3857,13 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da private fun buildContentValuesForNewUser(e164: String?, pni: PNI?, aci: ACI?): ContentValues { check(e164 != null || pni != null || aci != null) { "Must provide some sort of identifier!" } + val serviceId = (aci ?: pni)?.toString() val values = contentValuesOf( PHONE to e164, - SERVICE_ID to (aci ?: pni)?.toString(), + SERVICE_ID to serviceId, PNI_COLUMN to pni?.toString(), STORAGE_SERVICE_ID to Base64.encodeBytes(StorageSyncHelper.generateKey()), - AVATAR_COLOR to AvatarColor.random().serialize() + AVATAR_COLOR to AvatarColorHash.forAddress(serviceId, e164).serialize() ) if (pni != null || aci != null) { @@ -3917,14 +3920,16 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da } if (isInsert) { - put(AVATAR_COLOR, AvatarColor.random().serialize()) + put(AVATAR_COLOR, AvatarColorHash.forAddress(contact.serviceId.toString(), contact.number.orNull()).serialize()) } } } private fun getValuesForStorageGroupV1(groupV1: SignalGroupV1Record, isInsert: Boolean): ContentValues { return ContentValues().apply { - put(GROUP_ID, GroupId.v1orThrow(groupV1.groupId).toString()) + val groupId = GroupId.v1orThrow(groupV1.groupId) + + put(GROUP_ID, groupId.toString()) put(GROUP_TYPE, GroupType.SIGNAL_V1.id) put(PROFILE_SHARING, if (groupV1.isProfileSharingEnabled) "1" else "0") put(BLOCKED, if (groupV1.isBlocked) "1" else "0") @@ -3938,14 +3943,16 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da } if (isInsert) { - put(AVATAR_COLOR, AvatarColor.random().serialize()) + put(AVATAR_COLOR, AvatarColorHash.forGroupId(groupId).serialize()) } } } private fun getValuesForStorageGroupV2(groupV2: SignalGroupV2Record, isInsert: Boolean): ContentValues { return ContentValues().apply { - put(GROUP_ID, GroupId.v2(groupV2.masterKeyOrThrow).toString()) + val groupId = GroupId.v2(groupV2.masterKeyOrThrow) + + put(GROUP_ID, groupId.toString()) put(GROUP_TYPE, GroupType.SIGNAL_V2.id) put(PROFILE_SHARING, if (groupV2.isProfileSharingEnabled) "1" else "0") put(BLOCKED, if (groupV2.isBlocked) "1" else "0") @@ -3960,7 +3967,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da } if (isInsert) { - put(AVATAR_COLOR, AvatarColor.random().serialize()) + put(AVATAR_COLOR, AvatarColorHash.forGroupId(groupId).serialize()) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt index 33b6f35533..1003f36d6a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt @@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment import org.thoughtcrime.securesms.attachments.TombstoneAttachment import org.thoughtcrime.securesms.components.emoji.EmojiUtil import org.thoughtcrime.securesms.contactshare.Contact -import org.thoughtcrime.securesms.conversation.colors.AvatarColor +import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash import org.thoughtcrime.securesms.crypto.SecurityEvent import org.thoughtcrime.securesms.database.CallLinkTable import org.thoughtcrime.securesms.database.CallTable @@ -1206,7 +1206,7 @@ object SyncMessageProcessor { revoked = false, expiration = Instant.MIN ), - avatarColor = AvatarColor.random() + avatarColor = AvatarColorHash.forCallLink(callLinkRootKey.keyBytes) ) ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java index 818a42b6f3..190f4d9505 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java @@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.WebRtcCallActivity; import org.thoughtcrime.securesms.calls.links.CallLinks; import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery; import org.thoughtcrime.securesms.conversation.ConversationIntents; -import org.thoughtcrime.securesms.conversation.colors.AvatarColor; +import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash; import org.thoughtcrime.securesms.database.CallLinkTable; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.model.GroupRecord; @@ -358,7 +358,7 @@ public class CommunicationActions { null ), new SignalCallLinkState("", CallLinkState.Restrictions.UNKNOWN, false, Instant.MIN), - AvatarColor.random() + AvatarColorHash.INSTANCE.forCallLink(rootKey.getKeyBytes()) )); }