diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java index 42c4d4bbdd..ddd3ad3dff 100644 --- a/src/org/thoughtcrime/securesms/ConversationListActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationListActivity.java @@ -37,8 +37,6 @@ import android.view.ViewTreeObserver; import android.widget.ImageView; import android.widget.Toast; -import com.lelloman.identicon.drawable.ClassicIdenticonDrawable; - import org.thoughtcrime.securesms.components.RatingManager; import org.thoughtcrime.securesms.components.SearchToolbar; import org.thoughtcrime.securesms.conversation.ConversationActivity; @@ -46,6 +44,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.JazzIdenticonDrawable; import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.permissions.Permissions; @@ -204,7 +203,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..5a3db4bbce 100644 --- a/src/org/thoughtcrime/securesms/components/AvatarImageView.java +++ b/src/org/thoughtcrime/securesms/components/AvatarImageView.java @@ -6,21 +6,28 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Outline; import android.graphics.Paint; +import android.graphics.drawable.Drawable; import android.provider.ContactsContract; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.widget.AppCompatImageView; +import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; import android.view.ViewOutlineProvider; -import com.lelloman.identicon.drawable.ClassicIdenticonDrawable; - -import network.loki.messenger.R; +import org.thoughtcrime.securesms.color.MaterialColor; +import org.thoughtcrime.securesms.contacts.avatars.ContactColors; +import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto; +import org.thoughtcrime.securesms.loki.JazzIdenticonDrawable; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientExporter; +import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.ThemeUtil; +import org.whispersystems.libsignal.util.guava.Optional; + +import network.loki.messenger.R; public class AvatarImageView extends AppCompatImageView { @@ -97,8 +104,23 @@ 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()); - setImageDrawable(identicon); + + Drawable image; + if (recipient.isGroupRecipient()) { + Context context = this.getContext(); + + String name = Optional.fromNullable(recipient.getName()).or(Optional.fromNullable(TextSecurePreferences.getProfileName(context))).or(""); + MaterialColor fallbackColor = recipient.getColor(); + + if (fallbackColor == ContactColors.UNKNOWN_COLOR && !TextUtils.isEmpty(name)) { + fallbackColor = ContactColors.generateFor(name); + } + + image = new GeneratedContactPhoto(name, R.drawable.ic_profile_default).asDrawable(context, fallbackColor.toAvatarColor(context)); + } else { + image = new JazzIdenticonDrawable(w, h, recipient.getAddress().serialize()); + } + setImageDrawable(image); } public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) { diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java index c089c50f19..6dc4cf67c5 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -99,15 +99,7 @@ import org.thoughtcrime.securesms.mms.TextSlide; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; import org.thoughtcrime.securesms.stickers.StickerUrl; -import org.thoughtcrime.securesms.util.DateUtils; -import org.thoughtcrime.securesms.util.DynamicTheme; -import org.thoughtcrime.securesms.util.LongClickCopySpan; -import org.thoughtcrime.securesms.util.LongClickMovementMethod; -import org.thoughtcrime.securesms.util.SearchUtil; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.util.ThemeUtil; -import org.thoughtcrime.securesms.util.Util; -import org.thoughtcrime.securesms.util.ViewUtil; +import org.thoughtcrime.securesms.util.*; import org.thoughtcrime.securesms.util.views.Stub; import org.whispersystems.libsignal.util.guava.Optional; @@ -883,7 +875,17 @@ public class ConversationItem extends LinearLayout @SuppressLint("SetTextI18n") private void setGroupMessageStatus(MessageRecord messageRecord, Recipient recipient) { if (groupThread && !messageRecord.isOutgoing()) { - this.groupSender.setText(recipient.toShortString()); + // Show custom display names for group chats + String displayName = recipient.toShortString(); + try { + String serverId = GroupUtil.getDecodedStringId(conversationRecipient.getAddress().serialize()); + String senderDisplayName = DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(serverId, recipient.getAddress().serialize()); + if (senderDisplayName != null) { displayName = senderDisplayName; } + } catch (Exception e) { + // Do nothing + } + + this.groupSender.setText(displayName); if (recipient.getName() == null && !TextUtils.isEmpty(recipient.getProfileName())) { this.groupSenderProfileName.setText("~" + recipient.getProfileName()); diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 5c54f7d13b..31ca0310ec 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -73,8 +73,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int JOBMANAGER_STRIKES_BACK = 20; private static final int STICKERS = 21; private static final int lokiV1 = 22; + private static final int lokiV2 = 23; - private static final int DATABASE_VERSION = lokiV1; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes + private static final int DATABASE_VERSION = lokiV2; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -134,7 +135,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(LokiMessageDatabase.getCreateTableCommand()); db.execSQL(LokiThreadDatabase.getCreateFriendRequestTableCommand()); db.execSQL(LokiThreadDatabase.getCreateSessionResetTableCommand()); - db.execSQL(LokiUserDatabase.getCreateTableCommand()); + db.execSQL(LokiUserDatabase.getCreateDisplayNameTableCommand()); + db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand()); executeStatements(db, SmsDatabase.CREATE_INDEXS); executeStatements(db, MmsDatabase.CREATE_INDEXS); @@ -493,6 +495,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDTableCommand()); } + if (oldVersion < lokiV2) { + db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand()); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/src/org/thoughtcrime/securesms/loki/IdenticonDrawable.kt b/src/org/thoughtcrime/securesms/loki/IdenticonDrawable.kt new file mode 100644 index 0000000000..86d69ce990 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/IdenticonDrawable.kt @@ -0,0 +1,49 @@ +package org.thoughtcrime.securesms.loki + +import android.graphics.* +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/JazzIdenticonDrawable.kt b/src/org/thoughtcrime/securesms/loki/JazzIdenticonDrawable.kt new file mode 100644 index 0000000000..ff678db54c --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/JazzIdenticonDrawable.kt @@ -0,0 +1,132 @@ +package org.thoughtcrime.securesms.loki + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +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/LokiGroupChatPoller.kt b/src/org/thoughtcrime/securesms/loki/LokiGroupChatPoller.kt index 6db0b7a8a6..4b6afca3fc 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiGroupChatPoller.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiGroupChatPoller.kt @@ -103,8 +103,9 @@ class LokiGroupChatPoller(private val context: Context, private val group: LokiG val id = group.id.toByteArray() val x1 = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, null, null, null) val x2 = SignalServiceDataMessage(message.timestamp, x1, null, message.body) + val x3 = SignalServiceContent(x2, message.hexEncodedPublicKey, SignalServiceAddress.DEFAULT_DEVICE_ID, message.timestamp, false) val senderDisplayName = "${message.displayName} (...${message.hexEncodedPublicKey.takeLast(8)})" - val x3 = SignalServiceContent(x2, senderDisplayName, SignalServiceAddress.DEFAULT_DEVICE_ID, message.timestamp, false) + DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, message.hexEncodedPublicKey, senderDisplayName) PushDecryptJob(context).handleTextMessage(x3, x2, Optional.absent(), Optional.of(message.serverID)) } fun processOutgoingMessage(message: LokiGroupMessage) { diff --git a/src/org/thoughtcrime/securesms/loki/LokiUserDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiUserDatabase.kt index 50a2faee8f..5b907f8be7 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiUserDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiUserDatabase.kt @@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.loki import android.content.ContentValues import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.util.Log import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper @@ -12,10 +14,16 @@ import org.whispersystems.signalservice.loki.messaging.LokiUserDatabaseProtocol class LokiUserDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiUserDatabaseProtocol { companion object { + // Shared + private val displayName = "display_name" + // Display name cache private val displayNameTable = "loki_user_display_name_database" private val hexEncodedPublicKey = "hex_encoded_public_key" - private val displayName = "display_name" - @JvmStatic val createTableCommand = "CREATE TABLE $displayNameTable ($hexEncodedPublicKey TEXT PRIMARY KEY, $displayName TEXT);" + @JvmStatic val createDisplayNameTableCommand = "CREATE TABLE $displayNameTable ($hexEncodedPublicKey TEXT PRIMARY KEY, $displayName TEXT);" + // Server display name cache + private val serverDisplayNameTable = "loki_user_server_display_name_database" + private val serverID = "server_id" + @JvmStatic val createServerDisplayNameTableCommand = "CREATE TABLE $serverDisplayNameTable ($hexEncodedPublicKey TEXT, $serverID TEXT, $displayName TEXT, PRIMARY KEY ($hexEncodedPublicKey, $serverID));" } override fun getDisplayName(hexEncodedPublicKey: String): String? { @@ -37,4 +45,25 @@ class LokiUserDatabase(context: Context, helper: SQLCipherOpenHelper) : Database database.insertOrUpdate(displayNameTable, row, "${Companion.hexEncodedPublicKey} = ?", arrayOf( hexEncodedPublicKey )) Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false).notifyListeners() } + + fun getServerDisplayName(serverID: String, hexEncodedPublicKey: String): String? { + val database = databaseHelper.readableDatabase + return database.get(serverDisplayNameTable, "${Companion.hexEncodedPublicKey} = ? AND ${Companion.serverID} = ?", arrayOf(hexEncodedPublicKey, serverID)) { cursor -> + cursor.getString(cursor.getColumnIndexOrThrow(displayName)) + } + } + + fun setServerDisplayName(serverID: String, hexEncodedPublicKey: String, displayName: String) { + val database = databaseHelper.writableDatabase + val values = ContentValues(3) + values.put(Companion.serverID, serverID) + values.put(Companion.hexEncodedPublicKey, hexEncodedPublicKey) + values.put(Companion.displayName, displayName) + try { + database.insertWithOnConflict(serverDisplayNameTable, null, values, SQLiteDatabase.CONFLICT_REPLACE) + Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false).notifyListeners() + } catch (e: Exception) { + Log.d("Loki", "Couldn't save server display name due to exception: $e.") + } + } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/RNG.kt b/src/org/thoughtcrime/securesms/loki/RNG.kt new file mode 100644 index 0000000000..208414785a --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/RNG.kt @@ -0,0 +1,28 @@ +package org.thoughtcrime.securesms.loki + +class RNG(hash: Long) { + private var seed: Long + private val initial: Long + + init { + seed = hash % 2147483647 + if (seed <= 0) { + seed = 2147483646 + } + initial = seed + } + + fun next(): Long { + val newSeed = (seed * 16807) % 2147483647 + seed = newSeed + return seed + } + + fun nextFloat(): Float { + return (next() - 1).toFloat() / 2147483646 + } + + 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..49e60e1f97 100644 --- a/src/org/thoughtcrime/securesms/preferences/widgets/ProfilePreference.java +++ b/src/org/thoughtcrime/securesms/preferences/widgets/ProfilePreference.java @@ -18,9 +18,8 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; -import com.lelloman.identicon.drawable.ClassicIdenticonDrawable; - import org.thoughtcrime.securesms.database.Address; +import org.thoughtcrime.securesms.loki.JazzIdenticonDrawable; import org.thoughtcrime.securesms.util.TextSecurePreferences; import network.loki.messenger.R; @@ -101,7 +100,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; } diff --git a/src/org/thoughtcrime/securesms/util/GroupUtil.java b/src/org/thoughtcrime/securesms/util/GroupUtil.java index 6b83304ee1..224887fb0a 100644 --- a/src/org/thoughtcrime/securesms/util/GroupUtil.java +++ b/src/org/thoughtcrime/securesms/util/GroupUtil.java @@ -15,7 +15,6 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; -import org.thoughtcrime.securesms.sms.MessageSender; import org.whispersystems.libsignal.util.guava.Optional; import java.io.IOException; @@ -43,6 +42,11 @@ public class GroupUtil { return Hex.fromStringCondensed(groupId.split("!", 2)[1]); } + public static String getDecodedStringId(String groupId) throws IOException { + byte[] id = getDecodedId(groupId); + return new String(id); + } + public static boolean isEncodedGroup(@NonNull String groupId) { return groupId.startsWith(ENCODED_SIGNAL_GROUP_PREFIX) || groupId.startsWith(ENCODED_MMS_GROUP_PREFIX); }