Listen for recipient changes in conversations and group updates.

Closes #4079
// FREEBIE
master
Moxie Marlinspike 2015-09-16 17:31:24 -07:00
parent a7e05c4cd6
commit 0794380ca8
13 changed files with 222 additions and 95 deletions

View File

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.ConversationItem
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/conversation_item"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView android:id="@+id/conversation_item_body"
android:autoLink="all"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:linksClickable="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:gravity="center"
android:textStyle="italic"
android:textColor="?attr/conversation_group_member_name"
android:textSize="14sp" />
<TextView android:id="@+id/conversation_item_date"
android:autoLink="all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:linksClickable="false"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_gravity="right"
android:fontFamily="sans-serif-light"
android:textColor="?conversation_item_sent_text_secondary_color"
android:text="date"
android:layout_marginBottom="8dp"
android:visibility="gone"/>
</org.thoughtcrime.securesms.ConversationItem>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.ConversationUpdateItem
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/conversation_update_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="20dp">
<LinearLayout android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView android:id="@+id/conversation_update_icon"
android:layout_marginRight="7dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView android:id="@+id/conversation_update_body"
android:autoLink="all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:linksClickable="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textStyle="italic"
android:textColor="?attr/conversation_group_member_name"/>
</LinearLayout>
</org.thoughtcrime.securesms.ConversationUpdateItem>

View File

@ -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<MessageRecord> batchSelected = Collections.synchronizedSet(new HashSet<MessageRecord>());
@ -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();
}
}

View File

@ -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());

View File

@ -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);
}
}
}

View File

@ -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 {

View File

@ -0,0 +1,5 @@
package org.thoughtcrime.securesms;
public interface Unbindable {
public void unbind();
}

View File

@ -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()) {

View File

@ -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()) {

View File

@ -179,7 +179,7 @@ public class Recipient {
}
for (RecipientModifiedListener listener : localListeners)
listener.onModified(Recipient.this);
listener.onModified(this);
}
public interface RecipientModifiedListener {

View File

@ -185,9 +185,7 @@ public class Recipients implements Iterable<Recipient>, RecipientModifiedListene
}
}
synchronized (this) {
listeners.add(listener);
}
listeners.add(listener);
}
public synchronized void removeListener(RecipientsModifiedListener listener) {

View File

@ -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, "");
// }
}

View File

@ -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<String> 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);
}
}
}
}