From e7c20499ec66273a844d15d5933296850a7b2eb7 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Tue, 15 Aug 2017 19:23:42 -0700 Subject: [PATCH] Display profile name in when appropriate Display in conversation list, conversation actionbar, group messages, and group members list when address is not in system contacts // FREEBIE --- res/layout/conversation_item_received.xml | 49 +++++++++++++---- res/layout/conversation_item_sent.xml | 19 ++++++- .../securesms/ConversationItem.java | 50 +++++++++++++++-- .../securesms/ConversationTitleView.java | 38 ++++++++----- .../securesms/GroupMembersDialog.java | 8 ++- .../securesms/components/FromTextView.java | 33 ++++++++++-- .../contacts/avatars/ContactPhotoFactory.java | 8 +-- .../securesms/database/DatabaseFactory.java | 7 ++- .../database/RecipientPreferenceDatabase.java | 54 +++++++++++++++---- .../groups/GroupMessageProcessor.java | 2 +- .../securesms/recipients/Recipient.java | 13 ++++- .../recipients/RecipientProvider.java | 4 +- .../securesms/util/DirectoryHelper.java | 19 ++++--- .../spans/CenterAlignedRelativeSizeSpan.java | 25 +++++++++ 14 files changed, 270 insertions(+), 59 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/util/spans/CenterAlignedRelativeSizeSpan.java diff --git a/res/layout/conversation_item_received.xml b/res/layout/conversation_item_received.xml index 5169b4eb01..491e69ca21 100644 --- a/res/layout/conversation_item_received.xml +++ b/res/layout/conversation_item_received.xml @@ -13,16 +13,6 @@ xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto"> - - + + + + + + + + + app:scaleEmojis="true" + tools:text="boop"/> - + + + + + ((ViewStub) findViewById(R.id.audio_view_stub)); this.documentViewStub = new Stub<>((ViewStub) findViewById(R.id.document_view_stub)); this.expirationTimer = (ExpirationTimerView) findViewById(R.id.expiration_indicator); + this.groupSenderHolder = findViewById(R.id.group_sender_holder); setOnClickListener(new ClickListener(null)); @@ -205,6 +210,32 @@ public class ConversationItem extends LinearLayout setExpiration(messageRecord); } + @Override + public void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + if (groupSenderHolder != null && groupSenderHolder.getVisibility() == View.VISIBLE) { + View content = (View) groupSenderHolder.getParent(); + + groupSenderHolder.layout(content.getPaddingLeft(), content.getPaddingTop(), + content.getWidth() - content.getPaddingRight(), + content.getPaddingTop() + groupSenderHolder.getMeasuredHeight()); + + + if (DynamicLanguage.getLayoutDirection(context) == LAYOUT_DIRECTION_RTL) { + groupSenderProfileName.layout(groupSenderHolder.getPaddingLeft(), + groupSenderHolder.getPaddingTop(), + groupSenderHolder.getPaddingLeft() + groupSenderProfileName.getWidth(), + groupSenderHolder.getPaddingTop() + groupSenderProfileName.getHeight()); + } else { + groupSenderProfileName.layout(groupSenderHolder.getWidth() - groupSenderHolder.getPaddingRight() - groupSenderProfileName.getWidth(), + groupSenderHolder.getPaddingTop(), + groupSenderHolder.getWidth() - groupSenderProfileName.getPaddingRight(), + groupSenderHolder.getPaddingTop() + groupSenderProfileName.getHeight()); + } + } + } + private void initializeAttributes() { final int[] attributes = new int[] {R.attr.conversation_item_bubble_background, R.attr.conversation_list_item_background_selected, @@ -499,10 +530,19 @@ public class ConversationItem extends LinearLayout private void setGroupMessageStatus(MessageRecord messageRecord, Recipient recipient) { if (groupThread && !messageRecord.isOutgoing()) { - this.groupStatusText.setText(recipient.toShortString()); - this.groupStatusText.setVisibility(View.VISIBLE); + this.groupSender.setText(recipient.toShortString()); + + if (recipient.getName() == null && recipient.getProfileName() != null) { + this.groupSenderProfileName.setText("~" + recipient.getProfileName()); + this.groupSenderProfileName.setVisibility(View.VISIBLE); + } else { + this.groupSenderProfileName.setText(null); + this.groupSenderProfileName.setVisibility(View.GONE); + } + + this.groupSenderHolder.setVisibility(View.VISIBLE); } else { - this.groupStatusText.setVisibility(View.GONE); + this.groupSenderHolder.setVisibility(View.GONE); } } diff --git a/src/org/thoughtcrime/securesms/ConversationTitleView.java b/src/org/thoughtcrime/securesms/ConversationTitleView.java index ce7740a163..69be4d432c 100644 --- a/src/org/thoughtcrime/securesms/ConversationTitleView.java +++ b/src/org/thoughtcrime/securesms/ConversationTitleView.java @@ -65,23 +65,35 @@ public class ConversationTitleView extends LinearLayout { } private void setRecipientTitle(Recipient recipient) { - if (!recipient.isGroupRecipient()) { - if (TextUtils.isEmpty(recipient.getName())) { - this.title.setText(recipient.getAddress().serialize()); - this.subtitle.setText(null); - this.subtitle.setVisibility(View.GONE); - } else { - this.title.setText(recipient.getName()); + if (recipient.isGroupRecipient()) setGroupRecipientTitle(recipient); + else if (TextUtils.isEmpty(recipient.getName())) setNonContactRecipientTitle(recipient); + else setContactRecipientTitle(recipient); + } - if (recipient.getCustomLabel() != null) this.subtitle.setText(recipient.getCustomLabel()); - else this.subtitle.setText(recipient.getAddress().serialize()); + private void setGroupRecipientTitle(Recipient recipient) { + this.title.setText(recipient.getName()); + this.subtitle.setText(null); + this.subtitle.setVisibility(View.GONE); + } - this.subtitle.setVisibility(View.VISIBLE); - } - } else { - this.title.setText(recipient.getName()); + private void setNonContactRecipientTitle(Recipient recipient) { + this.title.setText(recipient.getAddress().serialize()); + + if (TextUtils.isEmpty(recipient.getProfileName())) { this.subtitle.setText(null); this.subtitle.setVisibility(View.GONE); + } else { + this.subtitle.setText("~" + recipient.getProfileName()); + this.subtitle.setVisibility(View.VISIBLE); } } + + private void setContactRecipientTitle(Recipient recipient) { + this.title.setText(recipient.getName()); + + if (recipient.getCustomLabel() != null) this.subtitle.setText(recipient.getCustomLabel()); + else this.subtitle.setText(recipient.getAddress().serialize()); + + this.subtitle.setVisibility(View.VISIBLE); + } } diff --git a/src/org/thoughtcrime/securesms/GroupMembersDialog.java b/src/org/thoughtcrime/securesms/GroupMembersDialog.java index acf675aecd..fcdd6e4576 100644 --- a/src/org/thoughtcrime/securesms/GroupMembersDialog.java +++ b/src/org/thoughtcrime/securesms/GroupMembersDialog.java @@ -111,7 +111,13 @@ public class GroupMembersDialog extends AsyncTask> { if (isLocalNumber(recipient)) { recipientStrings.add(context.getString(R.string.GroupMembersDialog_me)); } else { - recipientStrings.add(recipient.toShortString()); + String name = recipient.toShortString(); + + if (recipient.getName() == null && recipient.getProfileName() != null) { + name += " ~" + recipient.getProfileName(); + } + + recipientStrings.add(name); } } diff --git a/src/org/thoughtcrime/securesms/components/FromTextView.java b/src/org/thoughtcrime/securesms/components/FromTextView.java index 804fc736a8..1ac461af6d 100644 --- a/src/org/thoughtcrime/securesms/components/FromTextView.java +++ b/src/org/thoughtcrime/securesms/components/FromTextView.java @@ -4,13 +4,21 @@ import android.content.Context; import android.content.res.TypedArray; import android.graphics.Typeface; import android.text.Spannable; +import android.text.SpannableString; import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.AbsoluteSizeSpan; +import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; +import android.text.style.TypefaceSpan; import android.util.AttributeSet; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.emoji.EmojiTextView; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.DynamicLanguage; +import org.thoughtcrime.securesms.util.ResUtil; +import org.thoughtcrime.securesms.util.spans.CenterAlignedRelativeSizeSpan; public class FromTextView extends EmojiTextView { @@ -41,9 +49,28 @@ public class FromTextView extends EmojiTextView { typeface = Typeface.NORMAL; } - SpannableStringBuilder builder = new SpannableStringBuilder(fromString); - builder.setSpan(new StyleSpan(typeface), 0, builder.length(), - Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + SpannableStringBuilder builder = new SpannableStringBuilder(); + + SpannableString fromSpan = new SpannableString(fromString); + fromSpan.setSpan(new StyleSpan(typeface), 0, builder.length(), + Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + + if (recipient.getName() == null && recipient.getProfileName() != null) { + SpannableString profileName = new SpannableString(" (~" + recipient.getProfileName() + ") "); + profileName.setSpan(new CenterAlignedRelativeSizeSpan(0.75f), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + profileName.setSpan(new TypefaceSpan("sans-serif-light"), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + profileName.setSpan(new ForegroundColorSpan(ResUtil.getColor(getContext(), R.attr.conversation_list_item_subject_color)), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + if (DynamicLanguage.getLayoutDirection(getContext()) == LAYOUT_DIRECTION_RTL){ + builder.append(profileName); + builder.append(fromSpan); + } else { + builder.append(fromSpan); + builder.append(profileName); + } + } else { + builder.append(fromSpan); + } colors.recycle(); diff --git a/src/org/thoughtcrime/securesms/contacts/avatars/ContactPhotoFactory.java b/src/org/thoughtcrime/securesms/contacts/avatars/ContactPhotoFactory.java index 94cc521bc5..7a9ad21601 100644 --- a/src/org/thoughtcrime/securesms/contacts/avatars/ContactPhotoFactory.java +++ b/src/org/thoughtcrime/securesms/contacts/avatars/ContactPhotoFactory.java @@ -73,10 +73,10 @@ public class ContactPhotoFactory { return new BitmapContactPhoto(BitmapFactory.decodeByteArray(avatar, 0, avatar.length)); } - private static ContactPhoto getSignalAvatarContactPhoto(@NonNull Context context, - @NonNull Address address, - @Nullable String name, - int targetSize) + public static ContactPhoto getSignalAvatarContactPhoto(@NonNull Context context, + @NonNull Address address, + @Nullable String name, + int targetSize) { try { Bitmap bitmap = Glide.with(context) diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index b1075bd3d4..b7da217334 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -1288,9 +1288,12 @@ public class DatabaseFactory { String address = new NumberMigrator(TextSecurePreferences.getLocalNumber(context)).migrate(cursor.getString(0)); ContentValues contentValues = new ContentValues(1); - contentValues.put("recipient_ids", address); contentValues.put("registered", cursor.getInt(1) == 1); - db.replace("recipient_preferences", null, contentValues); + + if (db.update("recipient_preferences", contentValues, "recipient_ids = ?", new String[] {address}) < 1) { + contentValues.put("recipient_ids", address); + db.insert("recipient_preferences", null, contentValues); + } } } diff --git a/src/org/thoughtcrime/securesms/database/RecipientPreferenceDatabase.java b/src/org/thoughtcrime/securesms/database/RecipientPreferenceDatabase.java index b1b28c2b67..23371d0383 100644 --- a/src/org/thoughtcrime/securesms/database/RecipientPreferenceDatabase.java +++ b/src/org/thoughtcrime/securesms/database/RecipientPreferenceDatabase.java @@ -171,6 +171,18 @@ public class RecipientPreferenceDatabase extends Database { signalProfileAvatar)); } + public BulkOperationsHandle resetAllDisplayNames() { + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + database.beginTransaction(); + + ContentValues contentValues = new ContentValues(1); + contentValues.put(SYSTEM_DISPLAY_NAME, (String)null); + + database.update(TABLE_NAME, contentValues, null, null); + + return new BulkOperationsHandle(database); + } + public void setColor(Recipient recipient, MaterialColor color) { ContentValues values = new ContentValues(); values.put(COLOR, color.serialize()); @@ -266,19 +278,17 @@ public class RecipientPreferenceDatabase extends Database { SQLiteDatabase db = databaseHelper.getWritableDatabase(); for (Address activeAddress : activeAddresses) { - ContentValues contentValues = new ContentValues(2); - contentValues.put(ADDRESS, activeAddress.serialize()); + ContentValues contentValues = new ContentValues(1); contentValues.put(REGISTERED, 1); - db.replace(TABLE_NAME, null, contentValues); + updateOrInsert(activeAddress, contentValues); } for (Address inactiveAddress : inactiveAddresses) { - ContentValues contentValues = new ContentValues(2); - contentValues.put(ADDRESS, inactiveAddress.serialize()); + ContentValues contentValues = new ContentValues(1); contentValues.put(REGISTERED, 0); - db.replace(TABLE_NAME, null, contentValues); + updateOrInsert(inactiveAddress, contentValues); } context.getContentResolver().notifyChange(Uri.parse(RECIPIENT_PREFERENCES_URI), null); @@ -302,6 +312,15 @@ public class RecipientPreferenceDatabase extends Database { database.beginTransaction(); + updateOrInsert(database, address, contentValues); + + database.setTransactionSuccessful(); + database.endTransaction(); + + context.getContentResolver().notifyChange(Uri.parse(RECIPIENT_PREFERENCES_URI), null); + } + + private void updateOrInsert(SQLiteDatabase database, Address address, ContentValues contentValues) { int updated = database.update(TABLE_NAME, contentValues, ADDRESS + " = ?", new String[] {address.serialize()}); @@ -309,11 +328,28 @@ public class RecipientPreferenceDatabase extends Database { contentValues.put(ADDRESS, address.serialize()); database.insert(TABLE_NAME, null, contentValues); } + } - database.setTransactionSuccessful(); - database.endTransaction(); + public class BulkOperationsHandle { - context.getContentResolver().notifyChange(Uri.parse(RECIPIENT_PREFERENCES_URI), null); + private final SQLiteDatabase database; + + public BulkOperationsHandle(SQLiteDatabase database) { + this.database = database; + } + + public void setDisplayName(@NonNull Address address, @Nullable String displayName) { + ContentValues contentValues = new ContentValues(1); + contentValues.put(SYSTEM_DISPLAY_NAME, displayName); + updateOrInsert(address, contentValues); + } + + public void finish() { + database.setTransactionSuccessful(); + database.endTransaction(); + RecipientFactory.clearCache(context); + context.getContentResolver().notifyChange(Uri.parse(RECIPIENT_PREFERENCES_URI), null); + } } public static class RecipientsPreferences { diff --git a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java index 48817c41c7..1e43ebd486 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java @@ -65,7 +65,7 @@ public class GroupMessageProcessor { if (record.isPresent() && group.getType() == Type.UPDATE) { return handleGroupUpdate(context, masterSecret, envelope, group, record.get(), outgoing); - } else if (record.isPresent() && group.getType() == Type.UPDATE) { + } else if (!record.isPresent() && group.getType() == Type.UPDATE) { return handleGroupCreate(context, masterSecret, envelope, group, outgoing); } else if (record.isPresent() && group.getType() == Type.QUIT) { return handleGroupLeave(context, masterSecret, envelope, group, record.get(), outgoing); diff --git a/src/org/thoughtcrime/securesms/recipients/Recipient.java b/src/org/thoughtcrime/securesms/recipients/Recipient.java index d0590efa9f..cd525cf347 100644 --- a/src/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/src/org/thoughtcrime/securesms/recipients/Recipient.java @@ -64,6 +64,7 @@ public class Recipient implements RecipientModifiedListener { private boolean blocked = false; private VibrateState vibrate = VibrateState.DEFAULT; private int expireMessages = 0; + private String profileName = null; @Nullable private MaterialColor color; @@ -88,6 +89,9 @@ public class Recipient implements RecipientModifiedListener { this.blocked = stale.blocked; this.vibrate = stale.vibrate; this.expireMessages = stale.expireMessages; + this.profileName = stale.profileName; + this.participants.clear(); + this.participants.addAll(stale.participants); } if (details.isPresent()) { @@ -99,6 +103,7 @@ public class Recipient implements RecipientModifiedListener { this.blocked = details.get().blocked; this.vibrate = details.get().vibrateState; this.expireMessages = details.get().expireMessages; + this.profileName = details.get().profileName; this.participants.clear(); this.participants.addAll(details.get().participants); } @@ -118,6 +123,7 @@ public class Recipient implements RecipientModifiedListener { Recipient.this.blocked = result.blocked; Recipient.this.vibrate = result.vibrateState; Recipient.this.expireMessages = result.expireMessages; + Recipient.this.profileName = result.profileName; Recipient.this.participants.clear(); Recipient.this.participants.addAll(result.participants); @@ -151,6 +157,7 @@ public class Recipient implements RecipientModifiedListener { this.blocked = details.blocked; this.vibrate = details.vibrateState; this.expireMessages = details.expireMessages; + this.profileName = details.profileName; this.participants.addAll(details.participants); this.resolving = false; } @@ -196,6 +203,10 @@ public class Recipient implements RecipientModifiedListener { return customLabel; } + public @Nullable String getProfileName() { + return profileName; + } + public boolean isGroupRecipient() { return address.isGroup(); } @@ -208,7 +219,7 @@ public class Recipient implements RecipientModifiedListener { return address.isGroup() && !address.isMmsGroup(); } - public List getParticipants() { + public @NonNull List getParticipants() { return participants; } diff --git a/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java b/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java index 5747209e30..ea4196207b 100644 --- a/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java +++ b/src/org/thoughtcrime/securesms/recipients/RecipientProvider.java @@ -154,7 +154,7 @@ class RecipientProvider { } if (STATIC_DETAILS.containsKey(address.serialize())) return STATIC_DETAILS.get(address.serialize()); - else return new RecipientDetails(null, null, null, ContactPhotoFactory.getDefaultContactPhoto(null), preferences.orNull(), null); + else return new RecipientDetails(null, null, null, ContactPhotoFactory.getSignalAvatarContactPhoto(context, address, null, context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size)), preferences.orNull(), null); } private @NonNull RecipientDetails getGroupRecipientDetails(Context context, Address groupId, Optional groupRecord, Optional preferences, boolean asynchronous) { @@ -198,6 +198,7 @@ class RecipientProvider { public final boolean blocked; public final int expireMessages; @NonNull public final List participants; + @Nullable public final String profileName; public RecipientDetails(@Nullable String name, @Nullable String customLabel, @Nullable Uri contactUri, @NonNull ContactPhoto avatar, @@ -214,6 +215,7 @@ class RecipientProvider { this.blocked = preferences != null && preferences.isBlocked(); this.expireMessages = preferences != null ? preferences.getExpireMessages() : 0; this.participants = participants == null ? new LinkedList() : participants; + this.profileName = preferences != null ? preferences.getProfileName() : null; if (name == null && preferences != null) this.name = preferences.getSystemDisplayName(); else this.name = name; diff --git a/src/org/thoughtcrime/securesms/util/DirectoryHelper.java b/src/org/thoughtcrime/securesms/util/DirectoryHelper.java index b178a1176d..22d022bdd2 100644 --- a/src/org/thoughtcrime/securesms/util/DirectoryHelper.java +++ b/src/org/thoughtcrime/securesms/util/DirectoryHelper.java @@ -162,17 +162,22 @@ public class DirectoryHelper { List
newUsers = DatabaseFactory.getContactsDatabase(context) .setRegisteredUsers(account.get().getAccount(), activeAddresses, removeMissing); - Cursor cursor = ContactAccessor.getInstance().getAllSystemContacts(context); + Cursor cursor = ContactAccessor.getInstance().getAllSystemContacts(context); + RecipientPreferenceDatabase.BulkOperationsHandle handle = DatabaseFactory.getRecipientPreferenceDatabase(context).resetAllDisplayNames(); - while (cursor != null && cursor.moveToNext()) { - String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER)); + try { + while (cursor != null && cursor.moveToNext()) { + String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER)); - if (!TextUtils.isEmpty(number)) { - Address address = Address.fromExternal(context, number); - String displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); + if (!TextUtils.isEmpty(number)) { + Address address = Address.fromExternal(context, number); + String displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); - DatabaseFactory.getRecipientPreferenceDatabase(context).setSystemDisplayName(address, displayName); + handle.setDisplayName(address, displayName); + } } + } finally { + handle.finish(); } return new RefreshResult(newUsers, account.get().isFresh()); diff --git a/src/org/thoughtcrime/securesms/util/spans/CenterAlignedRelativeSizeSpan.java b/src/org/thoughtcrime/securesms/util/spans/CenterAlignedRelativeSizeSpan.java new file mode 100644 index 0000000000..b15c96137d --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/spans/CenterAlignedRelativeSizeSpan.java @@ -0,0 +1,25 @@ +package org.thoughtcrime.securesms.util.spans; + + +import android.text.TextPaint; +import android.text.style.MetricAffectingSpan; + +public class CenterAlignedRelativeSizeSpan extends MetricAffectingSpan { + + private final float relativeSize; + + public CenterAlignedRelativeSizeSpan(float relativeSize) { + this.relativeSize = relativeSize; + } + + @Override + public void updateMeasureState(TextPaint p) { + updateDrawState(p); + } + + @Override + public void updateDrawState(TextPaint tp) { + tp.setTextSize(tp.getTextSize() * relativeSize); + tp.baselineShift += (int) (tp.ascent() * relativeSize) / 4; + } +}