Implement simple avatar color picking algorithm to align with iOS.

This commit is contained in:
Alex Hart
2023-06-05 10:07:10 -03:00
committed by Cody Henthorne
parent bf7aaddbf9
commit 290c107698
6 changed files with 78 additions and 15 deletions

View File

@@ -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<CallLinkTable.CallLink> = mutableStateOf(
CallLinkTable.CallLink(
recipientId = RecipientId.UNKNOWN,

View File

@@ -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,

View File

@@ -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()]
}
}

View File

@@ -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())
}
}
}

View File

@@ -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)
)
)

View File

@@ -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())
));
}