Generate placeholder avatars from two characters, re-fetch missed avatars (#856)

* feat: splitting names in the avatar generation

* fix: re-fetch avatars if initial downloads fail

* fix: remove shadowed name, add tests for common labels
This commit is contained in:
Harris 2022-03-15 09:24:15 +11:00 committed by GitHub
parent 11d49426d3
commit 6649a9a745
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 66 additions and 18 deletions

View File

@ -254,7 +254,7 @@ public class JobManager implements ConstraintObserver.Notifier {
public static class Builder { public static class Builder {
private ExecutorFactory executorFactory = new DefaultExecutorFactory(); private ExecutorFactory executorFactory = new DefaultExecutorFactory();
private int jobThreadCount = Math.max(2, Math.min(Runtime.getRuntime().availableProcessors() - 1, 4)); private int jobThreadCount = 1;
private Map<String, Job.Factory> jobFactories = new HashMap<>(); private Map<String, Job.Factory> jobFactories = new HashMap<>();
private Map<String, Constraint.Factory> constraintFactories = new HashMap<>(); private Map<String, Constraint.Factory> constraintFactories = new HashMap<>();
private List<ConstraintObserver> constraintObservers = new ArrayList<>(); private List<ConstraintObserver> constraintObservers = new ArrayList<>();

View File

@ -84,7 +84,7 @@ public class RetrieveProfileAvatarJob extends BaseJob {
return; return;
} }
if (Util.equals(profileAvatar, recipient.resolve().getProfileAvatar())) { if (AvatarHelper.avatarFileExists(context, recipient.resolve().getAddress()) && Util.equals(profileAvatar, recipient.resolve().getProfileAvatar())) {
Log.w(TAG, "Already retrieved profile avatar: " + profileAvatar); Log.w(TAG, "Already retrieved profile avatar: " + profileAvatar);
return; return;
} }

View File

@ -1,14 +1,20 @@
package org.thoughtcrime.securesms.util package org.thoughtcrime.securesms.util
import android.content.Context import android.content.Context
import android.graphics.* import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Typeface
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.text.TextPaint import android.text.TextPaint
import android.text.TextUtils import android.text.TextUtils
import network.loki.messenger.R import network.loki.messenger.R
import java.math.BigInteger import java.math.BigInteger
import java.security.MessageDigest import java.security.MessageDigest
import java.util.* import java.util.Locale
object AvatarPlaceholderGenerator { object AvatarPlaceholderGenerator {
@ -28,7 +34,7 @@ object AvatarPlaceholderGenerator {
val colorPrimary = colorArray[(hash % colorArray.size).toInt()] val colorPrimary = colorArray[(hash % colorArray.size).toInt()]
val labelText = when { val labelText = when {
!TextUtils.isEmpty(displayName) -> extractLabel(displayName!!.capitalize()) !TextUtils.isEmpty(displayName) -> extractLabel(displayName!!.capitalize(Locale.ROOT))
!TextUtils.isEmpty(hashString) -> extractLabel(hashString) !TextUtils.isEmpty(hashString) -> extractLabel(hashString)
else -> EMPTY_LABEL else -> EMPTY_LABEL
} }
@ -57,14 +63,19 @@ object AvatarPlaceholderGenerator {
return BitmapDrawable(context.resources, bitmap) return BitmapDrawable(context.resources, bitmap)
} }
private fun extractLabel(content: String): String { fun extractLabel(content: String): String {
var content = content.trim() val trimmedContent = content.trim()
if (content.isEmpty()) return EMPTY_LABEL if (trimmedContent.isEmpty()) return EMPTY_LABEL
return if (content.length > 2 && content.startsWith("05")) { return if (trimmedContent.length > 2 && trimmedContent.startsWith("05")) {
content[2].toString().toUpperCase(Locale.ROOT) trimmedContent[2].toString()
} else { } else {
content.first().toString().toUpperCase(Locale.ROOT) val splitWords = trimmedContent.split(Regex("\\W"))
if (splitWords.size < 2) {
trimmedContent.take(2)
} else {
splitWords.filter { word -> word.isNotEmpty() }.take(2).map { it.first() }.joinToString("")
} }
}.uppercase()
} }
private fun getSha512(input: String): String { private fun getSha512(input: String): String {

View File

@ -0,0 +1,26 @@
package org.thoughtcrime.securesms.recipients
import org.junit.Assert.assertEquals
import org.junit.Test
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator
class AvatarGeneratorTest {
@Test
fun testCommonAvatarFormats() {
val testNamesAndResults = mapOf(
"H " to "H",
"Test Name" to "TN",
"test name" to "TN",
"howdy partner" to "HP",
"testname" to "TE", //
"05aaapubkey" to "A", // pubkey values only return first non-05 character
"Test" to "TE"
)
testNamesAndResults.forEach { (test, expected) ->
val processed = AvatarPlaceholderGenerator.extractLabel(test)
assertEquals(expected, processed)
}
}
}

View File

@ -46,6 +46,11 @@ public class AvatarHelper {
return new File(avatarDirectory, new File(address.serialize()).getName()); return new File(avatarDirectory, new File(address.serialize()).getName());
} }
public static boolean avatarFileExists(@NonNull Context context , @NonNull Address address) {
File avatarFile = getAvatarFile(context, address);
return avatarFile.exists();
}
public static void setAvatar(@NonNull Context context, @NonNull Address address, @Nullable byte[] data) public static void setAvatar(@NonNull Context context, @NonNull Address address, @Nullable byte[] data)
throws IOException throws IOException
{ {

View File

@ -1,6 +1,7 @@
package org.session.libsession.messaging.sending_receiving package org.session.libsession.messaging.sending_receiving
import android.text.TextUtils import android.text.TextUtils
import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.AttachmentDownloadJob import org.session.libsession.messaging.jobs.AttachmentDownloadJob
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
@ -188,28 +189,33 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val context = MessagingModuleConfiguration.shared.context val context = MessagingModuleConfiguration.shared.context
val userPublicKey = storage.getUserPublicKey() val userPublicKey = storage.getUserPublicKey()
val messageSender: String? = message.sender
// Get or create thread // Get or create thread
// FIXME: In case this is an open group this actually * doesn't * create the thread if it doesn't yet // FIXME: In case this is an open group this actually * doesn't * create the thread if it doesn't yet
// exist. This is intentional, but it's very non-obvious. // exist. This is intentional, but it's very non-obvious.
val threadID = storage.getOrCreateThreadIdFor(message.syncTarget val threadID = storage.getOrCreateThreadIdFor(message.syncTarget
?: message.sender!!, message.groupPublicKey, openGroupID) ?: messageSender!!, message.groupPublicKey, openGroupID)
if (threadID < 0) { if (threadID < 0) {
// Thread doesn't exist; should only be reached in a case where we are processing open group messages for a no longer existent thread // Thread doesn't exist; should only be reached in a case where we are processing open group messages for a no longer existent thread
throw MessageReceiver.Error.NoThread throw MessageReceiver.Error.NoThread
} }
// Update profile if needed // Update profile if needed
val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false) val recipient = Recipient.from(context, Address.fromSerialized(messageSender!!), false)
val profile = message.profile val profile = message.profile
if (profile != null && userPublicKey != message.sender) { if (profile != null && userPublicKey != messageSender) {
val profileManager = SSKEnvironment.shared.profileManager val profileManager = SSKEnvironment.shared.profileManager
val name = profile.displayName!! val name = profile.displayName!!
if (name.isNotEmpty()) { if (name.isNotEmpty()) {
profileManager.setName(context, recipient, name) profileManager.setName(context, recipient, name)
} }
val newProfileKey = profile.profileKey val newProfileKey = profile.profileKey
if (newProfileKey?.isNotEmpty() == true && (newProfileKey.size == 16 || newProfileKey.size == 32) && profile.profilePictureURL?.isNotEmpty() == true
&& (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, newProfileKey))) { val needsProfilePicture = !AvatarHelper.avatarFileExists(context, Address.fromSerialized(messageSender))
profileManager.setProfileKey(context, recipient, newProfileKey) val profileKeyValid = newProfileKey?.isNotEmpty() == true && (newProfileKey.size == 16 || newProfileKey.size == 32) && profile.profilePictureURL?.isNotEmpty() == true
val profileKeyChanged = (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, newProfileKey))
if ((profileKeyValid && profileKeyChanged) || (profileKeyValid && needsProfilePicture)) {
profileManager.setProfileKey(context, recipient, newProfileKey!!)
profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN) profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN)
profileManager.setProfilePictureURL(context, recipient, profile.profilePictureURL!!) profileManager.setProfilePictureURL(context, recipient, profile.profilePictureURL!!)
} }