From 9e3a6ce97707f185ba183db346614a03de35cb04 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 6 Sep 2019 11:49:02 +1000 Subject: [PATCH] Added Identicon --- .../securesms/ConversationListActivity.java | 3 +- .../securesms/components/AvatarImageView.java | 3 +- .../loki/identicon/IdenticonDrawable.kt | 58 ++++++++ .../loki/identicon/JazzIdenticonDrawable.kt | 130 ++++++++++++++++++ .../securesms/loki/identicon/RNG.kt | 29 ++++ .../widgets/ProfilePreference.java | 3 +- 6 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/loki/identicon/IdenticonDrawable.kt create mode 100644 src/org/thoughtcrime/securesms/loki/identicon/JazzIdenticonDrawable.kt create mode 100644 src/org/thoughtcrime/securesms/loki/identicon/RNG.kt diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java index 42c4d4bbdd..1b70956137 100644 --- a/src/org/thoughtcrime/securesms/ConversationListActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationListActivity.java @@ -46,6 +46,7 @@ import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; import org.thoughtcrime.securesms.lock.RegistrationLockDialog; +import org.thoughtcrime.securesms.loki.identicon.JazzIdenticonDrawable; import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.permissions.Permissions; @@ -204,7 +205,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit int height = profilePictureImageView.getHeight(); if (width == 0 || height == 0) return true; profilePictureImageView.getViewTreeObserver().removeOnPreDrawListener(this); - ClassicIdenticonDrawable identicon = new ClassicIdenticonDrawable(width, height, recipient.getAddress().serialize().hashCode()); + JazzIdenticonDrawable identicon = new JazzIdenticonDrawable(width, height, recipient.getAddress().serialize()); profilePictureImageView.setImageDrawable(identicon); return true; } diff --git a/src/org/thoughtcrime/securesms/components/AvatarImageView.java b/src/org/thoughtcrime/securesms/components/AvatarImageView.java index ce0fffb72a..b9a7627269 100644 --- a/src/org/thoughtcrime/securesms/components/AvatarImageView.java +++ b/src/org/thoughtcrime/securesms/components/AvatarImageView.java @@ -17,6 +17,7 @@ import android.view.ViewOutlineProvider; import com.lelloman.identicon.drawable.ClassicIdenticonDrawable; import network.loki.messenger.R; +import org.thoughtcrime.securesms.loki.identicon.JazzIdenticonDrawable; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientExporter; @@ -97,7 +98,7 @@ public class AvatarImageView extends AppCompatImageView { protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (w == 0 || h == 0 || recipient == null) { return; } - ClassicIdenticonDrawable identicon = new ClassicIdenticonDrawable(w, h, recipient.getAddress().serialize().hashCode()); + JazzIdenticonDrawable identicon = new JazzIdenticonDrawable(w, h, recipient.getAddress().serialize()); setImageDrawable(identicon); } diff --git a/src/org/thoughtcrime/securesms/loki/identicon/IdenticonDrawable.kt b/src/org/thoughtcrime/securesms/loki/identicon/IdenticonDrawable.kt new file mode 100644 index 0000000000..257b4bf40c --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/identicon/IdenticonDrawable.kt @@ -0,0 +1,58 @@ +package org.thoughtcrime.securesms.loki.identicon + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.ColorFilter +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.drawable.Drawable + +/** + * Basically a [Bitmap] wrapper, the [Bitmap] size must be known when instantiating it + * but when drawing it will draw the [Bitmap] to fit the canvas. + */ +abstract class IdenticonDrawable( + width: Int, + height: Int, + hash: Long +) : Drawable() { + + private val bitmapRect: Rect = Rect(0, 0, width, height) + private val destinationRect: Rect = Rect(0, 0, width, height) + private val bitmap: Bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + private val canvas: Canvas = Canvas(bitmap) + private val bitmapPaint = Paint(Paint.ANTI_ALIAS_FLAG) + + var hash: Long = hash + set(value) { + field = value + onSetHash(value) + invalidateBitmap() + } + + protected fun invalidateBitmap() { + drawBitmap(canvas) + invalidateSelf() + } + + protected abstract fun drawBitmap(canvas: Canvas) + + protected open fun onSetHash(newHash: Long) = Unit + + override fun draw(canvas: Canvas) { + destinationRect.set(0, 0, canvas.width, canvas.height) + canvas.drawBitmap(bitmap, bitmapRect, destinationRect, bitmapPaint) + } + + override fun setAlpha(i: Int) { + bitmapPaint.alpha = i + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + bitmapPaint.colorFilter = colorFilter + } + + override fun getOpacity(): Int { + return bitmapPaint.alpha + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/identicon/JazzIdenticonDrawable.kt b/src/org/thoughtcrime/securesms/loki/identicon/JazzIdenticonDrawable.kt new file mode 100644 index 0000000000..06c5fc618d --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/identicon/JazzIdenticonDrawable.kt @@ -0,0 +1,130 @@ +package org.thoughtcrime.securesms.loki.identicon + +import android.graphics.* +import kotlin.math.* + +class JazzIdenticonDrawable(width: Int, height: Int, hash: Long) : IdenticonDrawable(width, height, hash) { + + constructor(width: Int, height: Int, hashString: String): this(width, height, 0) { + val hexRegex = Regex("^[0-9A-Fa-f]+\$") + if (hashString.length >= 12 && hashString.matches(hexRegex)) { + hash = hashString.substring(0 until 12).toLong(16) + } + } + + companion object { + var colors = listOf( + "#01888c", // teal + "#fc7500", // bright orange + "#034f5d", // dark teal + "#E784BA", // light pink + "#81C8B6", // bright green + "#c7144c", // raspberry + "#f3c100", // goldenrod + "#1598f2", // lightning blue + "#2465e1", // sail blue + "#f19e02" // gold + ).map{ Color.parseColor(it) } + } + + private var generator: RNG = RNG(hash) + private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL } + + // Settings + private val wobble: Float = 30f + private val shapeCount = 4 + + init { + invalidateBitmap() + } + + override fun onSetHash(newHash: Long) { + super.onSetHash(newHash) + generator = RNG(newHash) + invalidateBitmap() + } + + override fun drawBitmap(canvas: Canvas) { + generator.reset() + + val newColors = hueShift(colors) + val shuffled = shuffleList(newColors) + + canvas.drawColor(shuffled[0]) + for (i in 0 until shapeCount) { + drawSquare(canvas, shuffled[i + 1], i, shapeCount - 1) + } + } + + private fun drawSquare(canvas: Canvas, color: Int, index: Int, total: Int) { + val size = min(canvas.width, canvas.height) + val center = (size / 2).toFloat() + val firstRotation = generator.nextFloat() + val angle = PI * 2 * firstRotation + + val a = size / total.toFloat() + val b = generator.nextFloat() + val c = index.toFloat() * a + val velocity = a * b + c + + val tx = cos(angle) * velocity + val ty = sin(angle) * velocity + + // Third random is a shape rotation on top of all that + val secondRotation = generator.nextFloat() + val rotation = (firstRotation * 360f) + (secondRotation * 180f) + + // Paint it! + canvas.save() + + paint.color = color + canvas.translate(tx.toFloat(), ty.toFloat()) + canvas.rotate(rotation.round(1), center, center) + canvas.drawRect(0f, 0f, canvas.width.toFloat(), canvas.height.toFloat(), paint) + + canvas.restore() + } + + private fun hueShift(colors: List): List { + val amount = generator.nextFloat() * 30 - wobble / 2 + + return colors.map { color -> + val red = Color.red(color) + val green = Color.green(color) + val blue = Color.blue(color) + + val hsv = FloatArray(3) + Color.RGBToHSV(red, green, blue, hsv) + + // Normalise between 0 and 360 + var newHue = hsv[0] + round(amount) + if (newHue < 0) { newHue += 360 } + if (newHue > 360) { newHue -= 360 } + + hsv[0] = newHue + Color.HSVToColor(hsv) + } + } + + private fun shuffleList(list: List): List { + var currentIndex = list.count() + val newList = list.toMutableList() + while (currentIndex > 0) { + val randomIndex = generator.next().toInt() % currentIndex + currentIndex -= 1 + + // Swap + val temp = newList[currentIndex] + newList[currentIndex] = newList[randomIndex] + newList[randomIndex] = temp + } + + return newList + } +} + +private fun Float.round(decimals: Int): Float { + var multiplier = 1f + repeat(decimals) { multiplier *= 10 } + return round(this * multiplier) / multiplier +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/identicon/RNG.kt b/src/org/thoughtcrime/securesms/loki/identicon/RNG.kt new file mode 100644 index 0000000000..6e43ecaeb5 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/identicon/RNG.kt @@ -0,0 +1,29 @@ +package org.thoughtcrime.securesms.loki.identicon + +class RNG(hash: Long) { + + private var seed: Long + private val initial: Long + + init { + seed = hash % 2147483647 + if (seed <= 0) { + seed = 2147483646 + } + initial = seed + } + + public fun next(): Long { + val newSeed = (seed * 16807) % 2147483647 + seed = newSeed + return seed + } + + public fun nextFloat(): Float { + return (next() - 1).toFloat() / 2147483646 + } + + public fun reset() { + seed = initial + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/preferences/widgets/ProfilePreference.java b/src/org/thoughtcrime/securesms/preferences/widgets/ProfilePreference.java index c67d6c0d1e..2838bfc0b3 100644 --- a/src/org/thoughtcrime/securesms/preferences/widgets/ProfilePreference.java +++ b/src/org/thoughtcrime/securesms/preferences/widgets/ProfilePreference.java @@ -21,6 +21,7 @@ import android.widget.Toast; import com.lelloman.identicon.drawable.ClassicIdenticonDrawable; import org.thoughtcrime.securesms.database.Address; +import org.thoughtcrime.securesms.loki.identicon.JazzIdenticonDrawable; import org.thoughtcrime.securesms.util.TextSecurePreferences; import network.loki.messenger.R; @@ -101,7 +102,7 @@ public class ProfilePreference extends Preference { int height = avatarView.getHeight(); if (width == 0 || height == 0) return true; avatarView.getViewTreeObserver().removeOnPreDrawListener(this); - ClassicIdenticonDrawable identicon = new ClassicIdenticonDrawable(width, height, userHexEncodedPublicKey.hashCode()); + JazzIdenticonDrawable identicon = new JazzIdenticonDrawable(width, height, userHexEncodedPublicKey); avatarView.setImageDrawable(identicon); return true; }