From 0794380ca8f28503c08b252dd38c2b354b10e8dc Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Wed, 16 Sep 2015 17:31:24 -0700 Subject: [PATCH] Listen for recipient changes in conversations and group updates. Closes #4079 // FREEBIE --- res/layout/conversation_item_activity.xml | 33 ------- res/layout/conversation_item_update.xml | 30 ++++++ .../securesms/ConversationAdapter.java | 25 +++-- .../securesms/ConversationItem.java | 41 ++++---- .../securesms/ConversationUpdateItem.java | 98 +++++++++++++++++++ .../securesms/MessageDetailsActivity.java | 2 +- .../thoughtcrime/securesms/Unbindable.java | 5 + .../database/model/MessageRecord.java | 2 +- .../database/model/ThreadRecord.java | 2 +- .../securesms/recipients/Recipient.java | 2 +- .../securesms/recipients/Recipients.java | 4 +- .../securesms/sms/IncomingGroupMessage.java | 10 -- .../securesms/util/GroupUtil.java | 63 +++++++++--- 13 files changed, 222 insertions(+), 95 deletions(-) delete mode 100644 res/layout/conversation_item_activity.xml create mode 100644 res/layout/conversation_item_update.xml create mode 100644 src/org/thoughtcrime/securesms/ConversationUpdateItem.java create mode 100644 src/org/thoughtcrime/securesms/Unbindable.java diff --git a/res/layout/conversation_item_activity.xml b/res/layout/conversation_item_activity.xml deleted file mode 100644 index 79adc8789b..0000000000 --- a/res/layout/conversation_item_activity.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - diff --git a/res/layout/conversation_item_update.xml b/res/layout/conversation_item_update.xml new file mode 100644 index 0000000000..a3c927e65b --- /dev/null +++ b/res/layout/conversation_item_update.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + diff --git a/src/org/thoughtcrime/securesms/ConversationAdapter.java b/src/org/thoughtcrime/securesms/ConversationAdapter.java index e89ed27af3..c6cc4cb44b 100644 --- a/src/org/thoughtcrime/securesms/ConversationAdapter.java +++ b/src/org/thoughtcrime/securesms/ConversationAdapter.java @@ -57,7 +57,7 @@ public class ConversationAdapter extends CursorAdapter implements AbsListView.Re public static final int MESSAGE_TYPE_OUTGOING = 0; public static final int MESSAGE_TYPE_INCOMING = 1; - public static final int MESSAGE_TYPE_GROUP_ACTION = 2; + public static final int MESSAGE_TYPE_UPDATE = 2; private final Set batchSelected = Collections.synchronizedSet(new HashSet()); @@ -85,13 +85,22 @@ public class ConversationAdapter extends CursorAdapter implements AbsListView.Re @Override public void bindView(View view, Context context, Cursor cursor) { - ConversationItem item = (ConversationItem)view; long id = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID)); String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT)); MessageRecord messageRecord = getMessageRecord(id, cursor, type); - item.set(masterSecret, messageRecord, locale, batchSelected, selectionClickListener, - groupThread, pushDestination); + switch (getItemViewType(cursor)) { + case MESSAGE_TYPE_INCOMING: + case MESSAGE_TYPE_OUTGOING: + ((ConversationItem) view).set(masterSecret, messageRecord, locale, batchSelected, + selectionClickListener, groupThread, pushDestination); + break; + case MESSAGE_TYPE_UPDATE: + ((ConversationUpdateItem)view).set(messageRecord); + break; + default: + throw new AssertionError("Unknown type!"); + } } @Override @@ -113,8 +122,8 @@ public class ConversationAdapter extends CursorAdapter implements AbsListView.Re case ConversationAdapter.MESSAGE_TYPE_INCOMING: view = inflater.inflate(R.layout.conversation_item_received, parent, false); break; - case ConversationAdapter.MESSAGE_TYPE_GROUP_ACTION: - view = inflater.inflate(R.layout.conversation_item_activity, parent, false); + case ConversationAdapter.MESSAGE_TYPE_UPDATE: + view = inflater.inflate(R.layout.conversation_item_update, parent, false); break; default: throw new IllegalArgumentException("unsupported item view type given to ConversationAdapter"); } @@ -138,7 +147,7 @@ public class ConversationAdapter extends CursorAdapter implements AbsListView.Re String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT)); MessageRecord messageRecord = getMessageRecord(id, cursor, type); - if (messageRecord.isGroupAction()) return MESSAGE_TYPE_GROUP_ACTION; + if (messageRecord.isGroupAction()) return MESSAGE_TYPE_UPDATE; else if (messageRecord.isOutgoing()) return MESSAGE_TYPE_OUTGOING; else return MESSAGE_TYPE_INCOMING; } @@ -181,6 +190,6 @@ public class ConversationAdapter extends CursorAdapter implements AbsListView.Re @Override public void onMovedToScrapHeap(View view) { - ((ConversationItem)view).unbind(); + ((Unbindable) view).unbind(); } } diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java index 408fbcd1df..44201532b1 100644 --- a/src/org/thoughtcrime/securesms/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/ConversationItem.java @@ -60,6 +60,7 @@ import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.DateUtils; +import org.thoughtcrime.securesms.util.Util; import java.util.List; import java.util.Locale; @@ -73,7 +74,7 @@ import java.util.Set; * */ -public class ConversationItem extends LinearLayout implements Recipient.RecipientModifiedListener { +public class ConversationItem extends LinearLayout implements Recipient.RecipientModifiedListener, Unbindable { private final static String TAG = ConversationItem.class.getSimpleName(); private MessageRecord messageRecord; @@ -181,16 +182,13 @@ public class ConversationItem extends LinearLayout implements Recipient.Recipien setSelectionBackgroundDrawables(messageRecord); setBodyText(messageRecord); - - if (hasConversationBubble(messageRecord)) { - setBubbleState(messageRecord, recipient); - setStatusIcons(messageRecord); - setContactPhoto(recipient); - setGroupMessageStatus(messageRecord, recipient); - setEvents(messageRecord); - setMinimumWidth(); - setMediaAttributes(messageRecord); - } + setBubbleState(messageRecord, recipient); + setStatusIcons(messageRecord); + setContactPhoto(recipient); + setGroupMessageStatus(messageRecord, recipient); + setEvents(messageRecord); + setMinimumWidth(); + setMediaAttributes(messageRecord); } private void initializeAttributes() { @@ -205,6 +203,7 @@ public class ConversationItem extends LinearLayout implements Recipient.Recipien attrs.recycle(); } + @Override public void unbind() { if (recipient != null) { recipient.removeListener(this); @@ -236,10 +235,6 @@ public class ConversationItem extends LinearLayout implements Recipient.Recipien } } - private boolean hasConversationBubble(MessageRecord messageRecord) { - return !messageRecord.isGroupAction(); - } - private boolean isCaptionlessMms(MessageRecord messageRecord) { return TextUtils.isEmpty(messageRecord.getDisplayBody()) && messageRecord.isMms(); } @@ -403,12 +398,15 @@ public class ConversationItem extends LinearLayout implements Recipient.Recipien } @Override - public void onModified(Recipient recipient) { - if (hasConversationBubble(messageRecord)) { - setBubbleState(messageRecord, recipient); - setContactPhoto(recipient); - setGroupMessageStatus(messageRecord, recipient); - } + public void onModified(final Recipient recipient) { + Util.runOnMain(new Runnable() { + @Override + public void run() { + setBubbleState(messageRecord, recipient); + setContactPhoto(recipient); + setGroupMessageStatus(messageRecord, recipient); + } + }); } private class ThumbnailDownloadClickListener implements ThumbnailView.ThumbnailClickListener { @@ -416,6 +414,7 @@ public class ConversationItem extends LinearLayout implements Recipient.Recipien DatabaseFactory.getPartDatabase(context).setTransferState(messageRecord.getId(), slide.getPart().getPartId(), PartDatabase.TRANSFER_PROGRESS_STARTED); } } + private class ThumbnailClickListener implements ThumbnailView.ThumbnailClickListener { private void fireIntent(Slide slide) { Log.w(TAG, "Clicked: " + slide.getUri() + " , " + slide.getContentType()); diff --git a/src/org/thoughtcrime/securesms/ConversationUpdateItem.java b/src/org/thoughtcrime/securesms/ConversationUpdateItem.java new file mode 100644 index 0000000000..f3939909ed --- /dev/null +++ b/src/org/thoughtcrime/securesms/ConversationUpdateItem.java @@ -0,0 +1,98 @@ +package org.thoughtcrime.securesms; + +import android.content.Context; +import android.content.Intent; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.util.GroupUtil; +import org.thoughtcrime.securesms.util.Util; + +public class ConversationUpdateItem extends LinearLayout + implements Recipients.RecipientsModifiedListener, Recipient.RecipientModifiedListener, Unbindable, View.OnClickListener +{ + private static final String TAG = ConversationUpdateItem.class.getSimpleName(); + + private ImageView icon; + private TextView body; + private Recipient sender; + private MessageRecord messageRecord; + + public ConversationUpdateItem(Context context) { + super(context); + } + + public ConversationUpdateItem(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onFinishInflate() { + super.onFinishInflate(); + + this.icon = (ImageView)findViewById(R.id.conversation_update_icon); + this.body = (TextView)findViewById(R.id.conversation_update_body); + + setOnClickListener(this); + } + + public void set(MessageRecord messageRecord) { + this.messageRecord = messageRecord; + this.sender = messageRecord.getIndividualRecipient(); + + this.sender.addListener(this); + + if (messageRecord.isGroupAction()) { + icon.setImageDrawable(getContext().getResources().getDrawable(R.drawable.ic_group_grey600_24dp)); + + if (messageRecord.isGroupQuit() && messageRecord.isOutgoing()) { + body.setText(R.string.MessageRecord_left_group); + } else if (messageRecord.isGroupQuit()) { + body.setText(getContext().getString(R.string.ConversationItem_group_action_left, sender.toShortString())); + } else { + GroupUtil.GroupDescription description = GroupUtil.getDescription(getContext(), messageRecord.getBody().getBody()); + description.addListener(this); + body.setText(description.toString()); + } + } + } + + @Override + public void onModified(Recipients recipients) { + onModified(recipients.getPrimaryRecipient()); + } + + @Override + public void onModified(Recipient recipient) { + Util.runOnMain(new Runnable() { + @Override + public void run() { + set(messageRecord); + } + }); + } + + @Override + public void onClick(View v) { + if (messageRecord.isIdentityUpdate()) { + Intent intent = new Intent(getContext(), RecipientPreferenceActivity.class); + intent.putExtra(RecipientPreferenceActivity.RECIPIENTS_EXTRA, + new long[] {messageRecord.getIndividualRecipient().getRecipientId()}); + + getContext().startActivity(intent); + } + } + + @Override + public void unbind() { + if (sender != null) { + sender.removeListener(this); + } + } +} diff --git a/src/org/thoughtcrime/securesms/MessageDetailsActivity.java b/src/org/thoughtcrime/securesms/MessageDetailsActivity.java index 764365f722..dad593a094 100644 --- a/src/org/thoughtcrime/securesms/MessageDetailsActivity.java +++ b/src/org/thoughtcrime/securesms/MessageDetailsActivity.java @@ -184,7 +184,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity private void inflateMessageViewIfAbsent(MessageRecord messageRecord) { if (conversationItem == null) { if (messageRecord.isGroupAction()) { - conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_activity, itemParent, false); + conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_update, itemParent, false); } else if (messageRecord.isOutgoing()) { conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_sent, itemParent, false); } else { diff --git a/src/org/thoughtcrime/securesms/Unbindable.java b/src/org/thoughtcrime/securesms/Unbindable.java new file mode 100644 index 0000000000..3dd5cd8cc0 --- /dev/null +++ b/src/org/thoughtcrime/securesms/Unbindable.java @@ -0,0 +1,5 @@ +package org.thoughtcrime.securesms; + +public interface Unbindable { + public void unbind(); +} diff --git a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java index 161a4ad883..c58a7ab870 100644 --- a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -110,7 +110,7 @@ public abstract class MessageRecord extends DisplayRecord { if (isGroupUpdate() && isOutgoing()) { return emphasisAdded(context.getString(R.string.MessageRecord_updated_group)); } else if (isGroupUpdate()) { - return emphasisAdded(GroupUtil.getDescription(context, getBody().getBody())); + return emphasisAdded(GroupUtil.getDescription(context, getBody().getBody()).toString()); } else if (isGroupQuit() && isOutgoing()) { return emphasisAdded(context.getString(R.string.MessageRecord_left_group)); } else if (isGroupQuit()) { diff --git a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java index e2fe05f43d..def8f3ece8 100644 --- a/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -57,7 +57,7 @@ public class ThreadRecord extends DisplayRecord { if (SmsDatabase.Types.isDecryptInProgressType(type)) { return emphasisAdded(context.getString(R.string.MessageDisplayHelper_decrypting_please_wait)); } else if (isGroupUpdate()) { - return emphasisAdded(GroupUtil.getDescription(context, getBody().getBody())); + return emphasisAdded(GroupUtil.getDescription(context, getBody().getBody()).toString()); } else if (isGroupQuit()) { return emphasisAdded(context.getString(R.string.ThreadRecord_left_the_group)); } else if (isKeyExchange()) { diff --git a/src/org/thoughtcrime/securesms/recipients/Recipient.java b/src/org/thoughtcrime/securesms/recipients/Recipient.java index 09628ebabd..7c8a06a208 100644 --- a/src/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/src/org/thoughtcrime/securesms/recipients/Recipient.java @@ -179,7 +179,7 @@ public class Recipient { } for (RecipientModifiedListener listener : localListeners) - listener.onModified(Recipient.this); + listener.onModified(this); } public interface RecipientModifiedListener { diff --git a/src/org/thoughtcrime/securesms/recipients/Recipients.java b/src/org/thoughtcrime/securesms/recipients/Recipients.java index 15a23fcc49..4408cecb7f 100644 --- a/src/org/thoughtcrime/securesms/recipients/Recipients.java +++ b/src/org/thoughtcrime/securesms/recipients/Recipients.java @@ -185,9 +185,7 @@ public class Recipients implements Iterable, RecipientModifiedListene } } - synchronized (this) { - listeners.add(listener); - } + listeners.add(listener); } public synchronized void removeListener(RecipientsModifiedListener listener) { diff --git a/src/org/thoughtcrime/securesms/sms/IncomingGroupMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingGroupMessage.java index 5fdcf9dcca..29e940adb2 100644 --- a/src/org/thoughtcrime/securesms/sms/IncomingGroupMessage.java +++ b/src/org/thoughtcrime/securesms/sms/IncomingGroupMessage.java @@ -29,14 +29,4 @@ public class IncomingGroupMessage extends IncomingTextMessage { return groupContext.getType().getNumber() == GroupContext.Type.QUIT_VALUE; } -// public static IncomingGroupMessage createForQuit(String groupId, String user) throws IOException { -// IncomingTextMessage base = new IncomingTextMessage(user, groupId); -// GroupContext context = GroupContext.newBuilder() -// .setType(GroupContext.Type.QUIT) -// .setId(ByteString.copyFrom(GroupUtil.getDecodedId(groupId))) -// .build(); -// -// return new IncomingGroupMessage(base, context, ""); -// } - } diff --git a/src/org/thoughtcrime/securesms/util/GroupUtil.java b/src/org/thoughtcrime/securesms/util/GroupUtil.java index 465e887f92..cc27c7d4c3 100644 --- a/src/org/thoughtcrime/securesms/util/GroupUtil.java +++ b/src/org/thoughtcrime/securesms/util/GroupUtil.java @@ -1,19 +1,22 @@ package org.thoughtcrime.securesms.util; import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; -import com.google.protobuf.InvalidProtocolBufferException; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.recipients.RecipientFactory; +import org.thoughtcrime.securesms.recipients.Recipients; import java.io.IOException; -import java.util.List; -import org.thoughtcrime.securesms.R; import static org.whispersystems.textsecure.internal.push.TextSecureProtos.GroupContext; public class GroupUtil { private static final String ENCODED_GROUP_PREFIX = "__textsecure_group__!"; + private static final String TAG = GroupUtil.class.getSimpleName(); public static String getEncodedId(byte[] groupId) { return ENCODED_GROUP_PREFIX + Hex.toStringCondensed(groupId); @@ -31,19 +34,47 @@ public class GroupUtil { return groupId.startsWith(ENCODED_GROUP_PREFIX); } - public static String getDescription(Context context, String encodedGroup) { + public static @NonNull GroupDescription getDescription(@NonNull Context context, @Nullable String encodedGroup) { if (encodedGroup == null) { - return context.getString(R.string.GroupUtil_group_updated); + return new GroupDescription(context, null); } try { - StringBuilder description = new StringBuilder(); GroupContext groupContext = GroupContext.parseFrom(Base64.decode(encodedGroup)); - List members = groupContext.getMembersList(); - String title = groupContext.getName(); + return new GroupDescription(context, groupContext); + } catch (IOException e) { + Log.w(TAG, e); + return new GroupDescription(context, null); + } + } - if (!members.isEmpty()) { - description.append(context.getString(R.string.GroupUtil_joined_the_group, Util.join(members, ", "))); + public static class GroupDescription { + + @NonNull private final Context context; + @Nullable private final GroupContext groupContext; + @Nullable private final Recipients members; + + public GroupDescription(@NonNull Context context, @Nullable GroupContext groupContext) { + this.context = context.getApplicationContext(); + this.groupContext = groupContext; + + if (groupContext == null || groupContext.getMembersList().isEmpty()) { + this.members = null; + } else { + this.members = RecipientFactory.getRecipientsFromString(context, Util.join(groupContext.getMembersList(), ", "), true); + } + } + + public String toString() { + if (groupContext == null) { + return context.getString(R.string.GroupUtil_group_updated); + } + + StringBuilder description = new StringBuilder(); + String title = groupContext.getName(); + + if (members != null) { + description.append(context.getString(R.string.GroupUtil_joined_the_group, members.toShortString())); } if (title != null && !title.trim().isEmpty()) { @@ -56,12 +87,12 @@ public class GroupUtil { } else { return context.getString(R.string.GroupUtil_group_updated); } - } catch (InvalidProtocolBufferException e) { - Log.w("GroupUtil", e); - return context.getString(R.string.GroupUtil_group_updated); - } catch (IOException e) { - Log.w("GroupUtil", e); - return context.getString(R.string.GroupUtil_group_updated); + } + + public void addListener(Recipients.RecipientsModifiedListener listener) { + if (this.members != null) { + this.members.addListener(listener); + } } } }