From ed556fbd3a5e2c9a4ce841c1a8f12b82529e6784 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Sat, 13 Dec 2014 18:10:59 -0800 Subject: [PATCH] Support for multi-select in the conversation list. // FREEBIE Closes #1601 Closes #2214 Fixes #2188 Fixes #786 --- res/values/strings.xml | 4 +- .../securesms/ConversationAdapter.java | 37 +++- .../securesms/ConversationFragment.java | 165 +++++++++++------- .../securesms/ConversationItem.java | 124 ++++++++----- .../database/model/MessageRecord.java | 11 ++ 5 files changed, 227 insertions(+), 114 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 612befadf..d4850af9e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -118,7 +118,7 @@ Transport: %1$s\nSent/Received: %2$s Sender: %1$s\nTransport: %2$s\nSent: %3$s\nReceived: %4$s Confirm message delete - Are you sure that you want to permanently delete this message? + Are you sure that you want to permanently delete all selected messages? Save to storage? Saving this media to storage will allow any other apps on your phone to access it.\n\nContinue? Error while saving attachment to storage! @@ -130,6 +130,8 @@ PUSH MMS SMS + Deleting... + Deleting messages... Delete threads? diff --git a/src/org/thoughtcrime/securesms/ConversationAdapter.java b/src/org/thoughtcrime/securesms/ConversationAdapter.java index 291f4a64e..4ab6830e4 100644 --- a/src/org/thoughtcrime/securesms/ConversationAdapter.java +++ b/src/org/thoughtcrime/securesms/ConversationAdapter.java @@ -35,7 +35,11 @@ import org.thoughtcrime.securesms.util.LRUCache; import java.lang.ref.SoftReference; import java.util.Collections; +import java.util.HashSet; import java.util.Map; +import java.util.Set; + +import org.thoughtcrime.securesms.ConversationFragment.SelectionClickListener; /** * A cursor adapter for a conversation thread. Ultimately @@ -55,19 +59,23 @@ public class ConversationAdapter extends CursorAdapter implements AbsListView.Re public static final int MESSAGE_TYPE_INCOMING = 1; public static final int MESSAGE_TYPE_GROUP_ACTION = 2; - private final Handler failedIconClickHandler; - private final Context context; - private final MasterSecret masterSecret; - private final boolean groupThread; - private final boolean pushDestination; - private final LayoutInflater inflater; + private final Set batchSelected = Collections.synchronizedSet(new HashSet()); - public ConversationAdapter(Context context, MasterSecret masterSecret, + private final SelectionClickListener selectionClickListener; + private final Handler failedIconClickHandler; + private final Context context; + private final MasterSecret masterSecret; + private final boolean groupThread; + private final boolean pushDestination; + private final LayoutInflater inflater; + + public ConversationAdapter(Context context, MasterSecret masterSecret, SelectionClickListener selectionClickListener, Handler failedIconClickHandler, boolean groupThread, boolean pushDestination) { super(context, null, 0); this.context = context; this.masterSecret = masterSecret; + this.selectionClickListener = selectionClickListener; this.failedIconClickHandler = failedIconClickHandler; this.groupThread = groupThread; this.pushDestination = pushDestination; @@ -81,7 +89,8 @@ public class ConversationAdapter extends CursorAdapter implements AbsListView.Re String type = cursor.getString(cursor.getColumnIndexOrThrow(MmsSmsDatabase.TRANSPORT)); MessageRecord messageRecord = getMessageRecord(id, cursor, type); - item.set(masterSecret, messageRecord, failedIconClickHandler, groupThread, pushDestination); + item.set(masterSecret, messageRecord, batchSelected, selectionClickListener, + failedIconClickHandler, groupThread, pushDestination); } @Override @@ -158,6 +167,18 @@ public class ConversationAdapter extends CursorAdapter implements AbsListView.Re this.getCursor().close(); } + public void toggleBatchSelected(MessageRecord messageRecord) { + if (batchSelected.contains(messageRecord)) { + batchSelected.remove(messageRecord); + } else { + batchSelected.add(messageRecord); + } + } + + public Set getBatchSelected() { + return batchSelected; + } + @Override public void onMovedToScrapHeap(View view) { ((ConversationItem)view).unbind(); diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java index 64d8652ae..a87b2e628 100644 --- a/src/org/thoughtcrime/securesms/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationFragment.java @@ -40,17 +40,23 @@ import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.util.Dialogs; import org.thoughtcrime.securesms.util.DirectoryHelper; import org.thoughtcrime.securesms.util.FutureTaskListener; +import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask; import org.thoughtcrime.securesms.util.SaveAttachmentTask; import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment; import java.sql.Date; import java.text.SimpleDateFormat; +import java.util.LinkedList; +import java.util.List; public class ConversationFragment extends ListFragment implements LoaderManager.LoaderCallbacks { private static final String TAG = ConversationFragment.class.getSimpleName(); + private final ActionModeCallback actionModeCallback = new ActionModeCallback(); + private final SelectionClickListener selectionClickListener = new SelectionClickListener(); + private ConversationFragmentListener listener; private MasterSecret masterSecret; @@ -96,7 +102,7 @@ public class ConversationFragment extends ListFragment private void initializeListAdapter() { if (this.recipients != null && this.threadId != -1) { - this.setListAdapter(new ConversationAdapter(getActivity(), masterSecret, + this.setListAdapter(new ConversationAdapter(getActivity(), masterSecret, selectionClickListener, new FailedIconClickHandler(), (!this.recipients.isSingleRecipient()) || this.recipients.isGroupRecipient(), DirectoryHelper.isPushDestination(getActivity(), this.recipients))); @@ -106,49 +112,50 @@ public class ConversationFragment extends ListFragment } private void initializeContextualActionBar() { - getListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { - @Override - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { - if (actionMode != null) { - view.setSelected(true); - return false; - } - - actionMode = ((ActionBarActivity)getActivity()).startSupportActionMode(actionModeCallback); - view.setSelected(true); - return true; - } - }); - - getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - if (actionMode != null) { - view.setSelected(true); - setCorrectMenuVisibility(getMessageRecord(), actionMode.getMenu()); - } - } - }); + getListView().setOnItemClickListener(selectionClickListener); + getListView().setOnItemLongClickListener(selectionClickListener); } - private void setCorrectMenuVisibility(MessageRecord messageRecord, Menu menu) { - MenuItem resend = menu.findItem(R.id.menu_context_resend); - MenuItem saveAttachment = menu.findItem(R.id.menu_context_save_attachment); + private void setCorrectMenuVisibility(Menu menu) { + ConversationAdapter adapter = (ConversationAdapter) getListAdapter(); + List messageRecords = getSelectedMessageRecords(); - if (messageRecord.isFailed()) resend.setVisible(true); - else resend.setVisible(false); + if (actionMode != null && messageRecords.size() == 0) { + adapter.getBatchSelected().clear(); + adapter.notifyDataSetChanged(); + actionMode.finish(); + return; + } - if (messageRecord.isMms() && !messageRecord.isMmsNotification()) { - saveAttachment.setVisible(((MediaMmsMessageRecord)messageRecord).containsMediaSlide()); + if (messageRecords.size() > 1) { + menu.findItem(R.id.menu_context_forward).setVisible(false); + menu.findItem(R.id.menu_context_copy).setVisible(false); + menu.findItem(R.id.menu_context_details).setVisible(false); + menu.findItem(R.id.menu_context_save_attachment).setVisible(false); + menu.findItem(R.id.menu_context_resend).setVisible(false); } else { - saveAttachment.setVisible(false); + MessageRecord messageRecord = messageRecords.get(0); + + menu.findItem(R.id.menu_context_resend).setVisible(messageRecord.isFailed()); + menu.findItem(R.id.menu_context_save_attachment).setVisible(messageRecord.isMms() && + !messageRecord.isMmsNotification() && + ((MediaMmsMessageRecord)messageRecord).containsMediaSlide()); + + menu.findItem(R.id.menu_context_forward).setVisible(true); + menu.findItem(R.id.menu_context_details).setVisible(true); + menu.findItem(R.id.menu_context_copy).setVisible(true); } } - private MessageRecord getMessageRecord() { - Cursor cursor = ((CursorAdapter)getListAdapter()).getCursor(); - ConversationItem conversationItem = (ConversationItem)(((ConversationAdapter)getListAdapter()).newView(getActivity(), cursor, null)); - return conversationItem.getMessageRecord(); + private MessageRecord getSelectedMessageRecord() { + List messageRecords = getSelectedMessageRecords(); + + if (messageRecords.size() == 1) return messageRecords.get(0); + else throw new AssertionError(); + } + + private List getSelectedMessageRecords() { + return new LinkedList<>(((ConversationAdapter)getListAdapter()).getBatchSelected()); } public void reload(Recipients recipients, long threadId) { @@ -177,23 +184,32 @@ public class ConversationFragment extends ListFragment clipboard.setText(body); } - private void handleDeleteMessage(final MessageRecord message) { - final long messageId = message.getId(); - + private void handleDeleteMessages(final List messageRecords) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(R.string.ConversationFragment_confirm_message_delete); builder.setIcon(Dialogs.resolveIcon(getActivity(), R.attr.dialog_alert_icon)); builder.setCancelable(true); - builder.setMessage(R.string.ConversationFragment_are_you_sure_you_want_to_permanently_delete_this_message); - + builder.setMessage(R.string.ConversationFragment_are_you_sure_you_want_to_permanently_delete_all_selected_messages); builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - if (message.isMms()) { - DatabaseFactory.getMmsDatabase(getActivity()).delete(messageId); - } else { - DatabaseFactory.getSmsDatabase(getActivity()).deleteMessage(messageId); - } + new ProgressDialogAsyncTask(getActivity(), + R.string.ConversationFragment_deleting, + R.string.ConversationFragment_deleting_messages) + { + @Override + protected Void doInBackground(MessageRecord... messageRecords) { + for (MessageRecord messageRecord : messageRecords) { + if (messageRecord.isMms()) { + DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId()); + } else { + DatabaseFactory.getSmsDatabase(getActivity()).deleteMessage(messageRecord.getId()); + } + } + + return null; + } + }.execute(messageRecords.toArray(new MessageRecord[messageRecords.size()])); } }); @@ -312,16 +328,43 @@ public class ConversationFragment extends ListFragment public void setComposeText(String text); } - private ActionMode.Callback actionModeCallback = new ActionMode.Callback() { + public class SelectionClickListener + implements AdapterView.OnItemLongClickListener, AdapterView.OnItemClickListener + { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (actionMode != null && view instanceof ConversationItem) { + MessageRecord messageRecord = ((ConversationItem)view).getMessageRecord(); + ((ConversationAdapter) getListAdapter()).toggleBatchSelected(messageRecord); + ((ConversationAdapter) getListAdapter()).notifyDataSetChanged(); + + setCorrectMenuVisibility(actionMode.getMenu()); + } + } + + @Override + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + if (actionMode == null && view instanceof ConversationItem) { + MessageRecord messageRecord = ((ConversationItem)view).getMessageRecord(); + ((ConversationAdapter) getListAdapter()).toggleBatchSelected(messageRecord); + ((ConversationAdapter) getListAdapter()).notifyDataSetChanged(); + + actionMode = ((ActionBarActivity)getActivity()).startSupportActionMode(actionModeCallback); + return true; + } + + return false; + } + } + + private class ActionModeCallback implements ActionMode.Callback { @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.conversation_context, menu); - MessageRecord messageRecord = getMessageRecord(); - setCorrectMenuVisibility(messageRecord, menu); - + setCorrectMenuVisibility(menu); return true; } @@ -332,41 +375,37 @@ public class ConversationFragment extends ListFragment @Override public void onDestroyActionMode(ActionMode mode) { - if (getListView() != null && getListView().getChildCount() > 0) { - for (int i = 0; i < getListView().getChildCount(); i++){ - getListView().getChildAt(i).setSelected(false); - } - } + ((ConversationAdapter)getListAdapter()).getBatchSelected().clear(); + ((ConversationAdapter)getListAdapter()).notifyDataSetChanged(); + actionMode = null; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - MessageRecord messageRecord = getMessageRecord(); - switch(item.getItemId()) { case R.id.menu_context_copy: - handleCopyMessage(messageRecord); + handleCopyMessage(getSelectedMessageRecord()); actionMode.finish(); return true; case R.id.menu_context_delete_message: - handleDeleteMessage(messageRecord); + handleDeleteMessages(getSelectedMessageRecords()); actionMode.finish(); return true; case R.id.menu_context_details: - handleDisplayDetails(messageRecord); + handleDisplayDetails(getSelectedMessageRecord()); actionMode.finish(); return true; case R.id.menu_context_forward: - handleForwardMessage(messageRecord); + handleForwardMessage(getSelectedMessageRecord()); actionMode.finish(); return true; case R.id.menu_context_resend: - handleResendMessage(messageRecord); + handleResendMessage(getSelectedMessageRecord()); actionMode.finish(); return true; case R.id.menu_context_save_attachment: - handleSaveAttachment((MediaMmsMessageRecord)messageRecord); + handleSaveAttachment((MediaMmsMessageRecord)getSelectedMessageRecord()); actionMode.finish(); return true; } diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java index 27bef7c54..4ad253a11 100644 --- a/src/org/thoughtcrime/securesms/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/ConversationItem.java @@ -25,10 +25,8 @@ import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; -import android.net.Uri; import android.os.Handler; import android.os.Message; -import android.provider.Contacts.Intents; import android.provider.ContactsContract; import android.provider.ContactsContract.QuickContact; import android.util.AttributeSet; @@ -40,9 +38,10 @@ import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; +import org.thoughtcrime.securesms.ConversationFragment.SelectionClickListener; +import org.thoughtcrime.securesms.contacts.ContactPhotoFactory; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.contacts.ContactPhotoFactory; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; @@ -61,6 +60,8 @@ import org.thoughtcrime.securesms.util.Emoji; import org.thoughtcrime.securesms.util.FutureTaskListener; import org.thoughtcrime.securesms.util.ListenableFutureTask; +import java.util.Set; + /** * A view that displays an individual conversation item within a conversation * thread. Used by ComposeMessageActivity's ListActivity via a ConversationAdapter. @@ -81,13 +82,13 @@ public class ConversationItem extends LinearLayout { R.attr.conversation_item_sent_push_pending_background, R.attr.conversation_item_sent_push_pending_triangle_background}; - private final static int SENT_PUSH = 0; - private final static int SENT_PUSH_TRIANGLE = 1; - private final static int SENT_SMS = 2; - private final static int SENT_SMS_TRIANGLE = 3; - private final static int SENT_SMS_PENDING = 4; - private final static int SENT_SMS_PENDING_TRIANGLE = 5; - private final static int SENT_PUSH_PENDING = 6; + private final static int SENT_PUSH = 0; + private final static int SENT_PUSH_TRIANGLE = 1; + private final static int SENT_SMS = 2; + private final static int SENT_SMS_TRIANGLE = 3; + private final static int SENT_SMS_PENDING = 4; + private final static int SENT_SMS_PENDING_TRIANGLE = 5; + private final static int SENT_PUSH_PENDING = 6; private final static int SENT_PUSH_PENDING_TRIANGLE = 7; private Handler failedIconHandler; @@ -108,19 +109,21 @@ public class ConversationItem extends LinearLayout { private View triangleTick; private ImageView pendingIndicator; - private View mmsContainer; - private ImageView mmsThumbnail; - private Button mmsDownloadButton; - private TextView mmsDownloadingLabel; - private ListenableFutureTask slideDeck; - private FutureTaskListener slideDeckListener; - private TypedArray backgroundDrawables; + private Set batchSelected; + private SelectionClickListener selectionClickListener; + private View mmsContainer; + private ImageView mmsThumbnail; + private Button mmsDownloadButton; + private TextView mmsDownloadingLabel; + private ListenableFutureTask slideDeck; + private FutureTaskListener slideDeckListener; + private TypedArray backgroundDrawables; - private final FailedIconClickListener failedIconClickListener = new FailedIconClickListener(); - private final MmsDownloadClickListener mmsDownloadClickListener = new MmsDownloadClickListener(); + private final FailedIconClickListener failedIconClickListener = new FailedIconClickListener(); + private final MmsDownloadClickListener mmsDownloadClickListener = new MmsDownloadClickListener(); private final MmsPreferencesClickListener mmsPreferencesClickListener = new MmsPreferencesClickListener(); - private final ClickListener clickListener = new ClickListener(); - private final Handler handler = new Handler(); + private final ClickListener clickListener = new ClickListener(); + private final Handler handler = new Handler(); private final Context context; public ConversationItem(Context context) { @@ -157,19 +160,23 @@ public class ConversationItem extends LinearLayout { setOnClickListener(clickListener); if (failedImage != null) failedImage.setOnClickListener(failedIconClickListener); if (mmsDownloadButton != null) mmsDownloadButton.setOnClickListener(mmsDownloadClickListener); + if (mmsThumbnail != null) mmsThumbnail.setOnLongClickListener(new MultiSelectLongClickListener()); } public void set(MasterSecret masterSecret, MessageRecord messageRecord, + Set batchSelected, SelectionClickListener selectionClickListener, Handler failedIconHandler, boolean groupThread, boolean pushDestination) { + this.masterSecret = masterSecret; + this.messageRecord = messageRecord; + this.batchSelected = batchSelected; + this.selectionClickListener = selectionClickListener; + this.failedIconHandler = failedIconHandler; + this.groupThread = groupThread; + this.pushDestination = pushDestination; - this.messageRecord = messageRecord; - this.masterSecret = masterSecret; - this.failedIconHandler = failedIconHandler; - this.groupThread = groupThread; - this.pushDestination = pushDestination; - - setBackgroundDrawables(messageRecord); + setConversationBackgroundDrawables(messageRecord); + setSelectionBackgroundDrawables(messageRecord); setBodyText(messageRecord); if (!messageRecord.isGroupAction()) { @@ -211,33 +218,57 @@ public class ConversationItem extends LinearLayout { /// MessageRecord Attribute Parsers - private void setBackgroundDrawables(MessageRecord messageRecord) { + private void setConversationBackgroundDrawables(MessageRecord messageRecord) { if (conversationParent != null && backgroundDrawables != null) { if (messageRecord.isOutgoing()) { final int background; final int triangleBackground; if (messageRecord.isPending() && pushDestination && !messageRecord.isForcedSms()) { - background = SENT_PUSH_PENDING; + background = SENT_PUSH_PENDING; triangleBackground = SENT_PUSH_PENDING_TRIANGLE; } else if (messageRecord.isPending() || messageRecord.isPendingSmsFallback()) { - background = SENT_SMS_PENDING; + background = SENT_SMS_PENDING; triangleBackground = SENT_SMS_PENDING_TRIANGLE; } else if (messageRecord.isPush()) { - background = SENT_PUSH; + background = SENT_PUSH; triangleBackground = SENT_PUSH_TRIANGLE; } else { - background = SENT_SMS; + background = SENT_SMS; triangleBackground = SENT_SMS_TRIANGLE; } + setViewBackgroundWithoutResettingPadding(conversationParent, backgroundDrawables.getResourceId(background, -1)); setViewBackgroundWithoutResettingPadding(triangleTick, backgroundDrawables.getResourceId(triangleBackground, -1)); } } } + private void setSelectionBackgroundDrawables(MessageRecord messageRecord) { + int[] attributes = new int[]{R.attr.conversation_list_item_background_selected, + R.attr.conversation_item_background}; + + TypedArray drawables = context.obtainStyledAttributes(attributes); + + if (batchSelected.contains(messageRecord)) { + setBackgroundDrawable(drawables.getDrawable(0)); + } else { + setBackgroundDrawable(drawables.getDrawable(1)); + } + + drawables.recycle(); + } + private void setBodyText(MessageRecord messageRecord) { - bodyText.setText(Emoji.getInstance(context).emojify(messageRecord.getDisplayBody(), new Emoji.InvalidatingPageLoadedListener(bodyText)), - TextView.BufferType.SPANNABLE); + bodyText.setClickable(false); + bodyText.setFocusable(false); + bodyText.setText(Emoji.getInstance(context).emojify(messageRecord.getDisplayBody(), + new Emoji.InvalidatingPageLoadedListener(bodyText)), + TextView.BufferType.SPANNABLE); + + if (bodyText.isClickable() && bodyText.isFocusable()) { + bodyText.setOnLongClickListener(new MultiSelectLongClickListener()); + bodyText.setOnClickListener(new MultiSelectLongClickListener()); + } } private void setContactPhoto(MessageRecord messageRecord) { @@ -365,12 +396,6 @@ public class ConversationItem extends LinearLayout { if (slide.hasImage()) { slide.setThumbnailOn(mmsThumbnail); mmsThumbnail.setOnClickListener(new ThumbnailClickListener(slide)); - mmsThumbnail.setOnLongClickListener(new OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - return false; - } - }); mmsThumbnail.setVisibility(View.VISIBLE); return; } @@ -474,7 +499,9 @@ public class ConversationItem extends LinearLayout { } public void onClick(View v) { - if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType())) { + if (!batchSelected.isEmpty()) { + selectionClickListener.onItemClick(null, ConversationItem.this, -1, -1); + } else if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType())) { Intent intent = new Intent(context, MediaPreviewActivity.class); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setDataAndType(slide.getUri(), slide.getContentType()); @@ -545,6 +572,19 @@ public class ConversationItem extends LinearLayout { } } + private class MultiSelectLongClickListener implements OnLongClickListener, OnClickListener { + @Override + public boolean onLongClick(View view) { + selectionClickListener.onItemLongClick(null, ConversationItem.this, -1, -1); + return true; + } + + @Override + public void onClick(View view) { + selectionClickListener.onItemClick(null, ConversationItem.this, -1, -1); + } + } + private void handleMessageApproval() { final int title; final int message; diff --git a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java index 491644e86..f8f9a0fc8 100644 --- a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -182,4 +182,15 @@ public abstract class MessageRecord extends DisplayRecord { return spannable; } + + public boolean equals(Object other) { + return other != null && + other instanceof MessageRecord && + ((MessageRecord) other).getId() == getId() && + ((MessageRecord) other).isMms() == isMms(); + } + + public int hashCode() { + return (int)getId(); + } }