Home | History | Annotate | Download | only in providers
      1 /**
      2  * Copyright (c) 2012, Google Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *     http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.mail.providers;
     18 
     19 import android.content.ContentValues;
     20 import android.content.Context;
     21 import android.database.Cursor;
     22 import android.net.Uri;
     23 import android.os.Bundle;
     24 import android.os.Parcel;
     25 import android.os.Parcelable;
     26 import android.provider.BaseColumns;
     27 import android.text.TextUtils;
     28 
     29 import com.android.mail.R;
     30 import com.android.mail.browse.ConversationCursor;
     31 import com.android.mail.content.CursorCreator;
     32 import com.android.mail.providers.UIProvider.ConversationColumns;
     33 import com.android.mail.ui.ConversationCursorLoader;
     34 import com.android.mail.utils.LogTag;
     35 import com.android.mail.utils.LogUtils;
     36 import com.google.common.collect.ImmutableList;
     37 import com.google.common.collect.Lists;
     38 
     39 import java.util.ArrayList;
     40 import java.util.Collection;
     41 import java.util.Collections;
     42 import java.util.List;
     43 
     44 public class Conversation implements Parcelable {
     45     public static final int NO_POSITION = -1;
     46 
     47     private static final String LOG_TAG = LogTag.getLogTag();
     48 
     49     private static final String EMPTY_STRING = "";
     50 
     51     /**
     52      * @see BaseColumns#_ID
     53      */
     54     public long id;
     55     /**
     56      * @see UIProvider.ConversationColumns#URI
     57      */
     58     public Uri uri;
     59     /**
     60      * @see UIProvider.ConversationColumns#SUBJECT
     61      */
     62     public String subject;
     63     /**
     64      * @see UIProvider.ConversationColumns#DATE_RECEIVED_MS
     65      */
     66     public long dateMs;
     67     /**
     68      * @see UIProvider.ConversationColumns#SNIPPET
     69      */
     70     @Deprecated
     71     public String snippet;
     72     /**
     73      * @see UIProvider.ConversationColumns#HAS_ATTACHMENTS
     74      */
     75     public boolean hasAttachments;
     76     /**
     77      * Union of attachmentPreviewUri0 and attachmentPreviewUri1
     78      */
     79     public transient ArrayList<String> attachmentPreviews;
     80     /**
     81      * @see UIProvider.ConversationColumns#ATTACHMENT_PREVIEW_URI0
     82      */
     83     public String attachmentPreviewUri0;
     84     /**
     85      * @see UIProvider.ConversationColumns#ATTACHMENT_PREVIEW_URI1
     86      */
     87     public String attachmentPreviewUri1;
     88     /**
     89      * @see UIProvider.ConversationColumns#ATTACHMENT_PREVIEW_STATES
     90      */
     91     public int attachmentPreviewStates;
     92     /**
     93      * @see UIProvider.ConversationColumns#ATTACHMENT_PREVIEWS_COUNT
     94      */
     95     public int attachmentPreviewsCount;
     96     /**
     97      * @see UIProvider.ConversationColumns#MESSAGE_LIST_URI
     98      */
     99     public Uri messageListUri;
    100     /**
    101      * @see UIProvider.ConversationColumns#SENDER_INFO
    102      */
    103     @Deprecated
    104     public String senders;
    105     /**
    106      * @see UIProvider.ConversationColumns#NUM_MESSAGES
    107      */
    108     private int numMessages;
    109     /**
    110      * @see UIProvider.ConversationColumns#NUM_DRAFTS
    111      */
    112     private int numDrafts;
    113     /**
    114      * @see UIProvider.ConversationColumns#SENDING_STATE
    115      */
    116     public int sendingState;
    117     /**
    118      * @see UIProvider.ConversationColumns#PRIORITY
    119      */
    120     public int priority;
    121     /**
    122      * @see UIProvider.ConversationColumns#READ
    123      */
    124     public boolean read;
    125     /**
    126      * @see UIProvider.ConversationColumns#SEEN
    127      */
    128     public boolean seen;
    129     /**
    130      * @see UIProvider.ConversationColumns#STARRED
    131      */
    132     public boolean starred;
    133     /**
    134      * @see UIProvider.ConversationColumns#RAW_FOLDERS
    135      */
    136     private FolderList rawFolders;
    137     /**
    138      * @see UIProvider.ConversationColumns#FLAGS
    139      */
    140     public int convFlags;
    141     /**
    142      * @see UIProvider.ConversationColumns#PERSONAL_LEVEL
    143      */
    144     public int personalLevel;
    145     /**
    146      * @see UIProvider.ConversationColumns#SPAM
    147      */
    148     public boolean spam;
    149     /**
    150      * @see UIProvider.ConversationColumns#MUTED
    151      */
    152     public boolean muted;
    153     /**
    154      * @see UIProvider.ConversationColumns#PHISHING
    155      */
    156     public boolean phishing;
    157     /**
    158      * @see UIProvider.ConversationColumns#COLOR
    159      */
    160     public int color;
    161     /**
    162      * @see UIProvider.ConversationColumns#ACCOUNT_URI
    163      */
    164     public Uri accountUri;
    165     /**
    166      * @see UIProvider.ConversationColumns#CONVERSATION_INFO
    167      */
    168     public ConversationInfo conversationInfo;
    169     /**
    170      * @see UIProvider.ConversationColumns#CONVERSATION_BASE_URI
    171      */
    172     public Uri conversationBaseUri;
    173     /**
    174      * @see UIProvider.ConversationColumns#REMOTE
    175      */
    176     public boolean isRemote;
    177 
    178     /**
    179      * Used within the UI to indicate the adapter position of this conversation
    180      *
    181      * @deprecated Keeping this in sync with the desired value is a not always done properly, is a
    182      *             source of bugs, and is a bad idea in general. Do not trust this value. Try to
    183      *             migrate code away from using it.
    184      */
    185     @Deprecated
    186     public transient int position;
    187     // Used within the UI to indicate that a Conversation should be removed from
    188     // the ConversationCursor when executing an update, e.g. the the
    189     // Conversation is no longer in the ConversationList for the current folder,
    190     // that is it's now in some other folder(s)
    191     public transient boolean localDeleteOnUpdate;
    192 
    193     private transient boolean viewed;
    194 
    195     private static String sSubjectAndSnippet;
    196 
    197     // Constituents of convFlags below
    198     // Flag indicating that the item has been deleted, but will continue being
    199     // shown in the list Delete/Archive of a mostly-dead item will NOT propagate
    200     // the delete/archive, but WILL remove the item from the cursor
    201     public static final int FLAG_MOSTLY_DEAD = 1 << 0;
    202 
    203     /** An immutable, empty conversation list */
    204     public static final Collection<Conversation> EMPTY = Collections.emptyList();
    205 
    206     @Override
    207     public int describeContents() {
    208         return 0;
    209     }
    210 
    211     @Override
    212     public void writeToParcel(Parcel dest, int flags) {
    213         dest.writeLong(id);
    214         dest.writeParcelable(uri, flags);
    215         dest.writeString(subject);
    216         dest.writeLong(dateMs);
    217         dest.writeString(snippet);
    218         dest.writeInt(hasAttachments ? 1 : 0);
    219         dest.writeParcelable(messageListUri, 0);
    220         dest.writeString(senders);
    221         dest.writeInt(numMessages);
    222         dest.writeInt(numDrafts);
    223         dest.writeInt(sendingState);
    224         dest.writeInt(priority);
    225         dest.writeInt(read ? 1 : 0);
    226         dest.writeInt(seen ? 1 : 0);
    227         dest.writeInt(starred ? 1 : 0);
    228         dest.writeParcelable(rawFolders, 0);
    229         dest.writeInt(convFlags);
    230         dest.writeInt(personalLevel);
    231         dest.writeInt(spam ? 1 : 0);
    232         dest.writeInt(phishing ? 1 : 0);
    233         dest.writeInt(muted ? 1 : 0);
    234         dest.writeInt(color);
    235         dest.writeParcelable(accountUri, 0);
    236         dest.writeParcelable(conversationInfo, 0);
    237         dest.writeParcelable(conversationBaseUri, 0);
    238         dest.writeInt(isRemote ? 1 : 0);
    239         dest.writeString(attachmentPreviewUri0);
    240         dest.writeString(attachmentPreviewUri1);
    241         dest.writeInt(attachmentPreviewStates);
    242         dest.writeInt(attachmentPreviewsCount);
    243     }
    244 
    245     private Conversation(Parcel in, ClassLoader loader) {
    246         id = in.readLong();
    247         uri = in.readParcelable(null);
    248         subject = in.readString();
    249         dateMs = in.readLong();
    250         snippet = in.readString();
    251         hasAttachments = (in.readInt() != 0);
    252         messageListUri = in.readParcelable(null);
    253         senders = emptyIfNull(in.readString());
    254         numMessages = in.readInt();
    255         numDrafts = in.readInt();
    256         sendingState = in.readInt();
    257         priority = in.readInt();
    258         read = (in.readInt() != 0);
    259         seen = (in.readInt() != 0);
    260         starred = (in.readInt() != 0);
    261         rawFolders = in.readParcelable(loader);
    262         convFlags = in.readInt();
    263         personalLevel = in.readInt();
    264         spam = in.readInt() != 0;
    265         phishing = in.readInt() != 0;
    266         muted = in.readInt() != 0;
    267         color = in.readInt();
    268         accountUri = in.readParcelable(null);
    269         position = NO_POSITION;
    270         localDeleteOnUpdate = false;
    271         conversationInfo = in.readParcelable(loader);
    272         conversationBaseUri = in.readParcelable(null);
    273         isRemote = in.readInt() != 0;
    274         attachmentPreviews = null;
    275         attachmentPreviewUri0 = in.readString();
    276         attachmentPreviewUri1 = in.readString();
    277         attachmentPreviewStates = in.readInt();
    278         attachmentPreviewsCount = in.readInt();
    279     }
    280 
    281     @Override
    282     public String toString() {
    283         // log extra info at DEBUG level or finer
    284         final StringBuilder sb = new StringBuilder("[conversation id=");
    285         sb.append(id);
    286         if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
    287             sb.append(", subject=");
    288             sb.append(subject);
    289         }
    290         sb.append("]");
    291         return sb.toString();
    292     }
    293 
    294     public static final ClassLoaderCreator<Conversation> CREATOR =
    295             new ClassLoaderCreator<Conversation>() {
    296 
    297         @Override
    298         public Conversation createFromParcel(Parcel source) {
    299             return new Conversation(source, null);
    300         }
    301 
    302         @Override
    303         public Conversation createFromParcel(Parcel source, ClassLoader loader) {
    304             return new Conversation(source, loader);
    305         }
    306 
    307         @Override
    308         public Conversation[] newArray(int size) {
    309             return new Conversation[size];
    310         }
    311 
    312     };
    313 
    314     public static final Uri MOVE_CONVERSATIONS_URI = Uri.parse("content://moveconversations");
    315 
    316     /**
    317      * The column that needs to be updated to change the folders for a conversation.
    318      */
    319     public static final String UPDATE_FOLDER_COLUMN = ConversationColumns.RAW_FOLDERS;
    320 
    321     public Conversation(Cursor cursor) {
    322         if (cursor != null) {
    323             id = cursor.getLong(UIProvider.CONVERSATION_ID_COLUMN);
    324             uri = Uri.parse(cursor.getString(UIProvider.CONVERSATION_URI_COLUMN));
    325             dateMs = cursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN);
    326             subject = cursor.getString(UIProvider.CONVERSATION_SUBJECT_COLUMN);
    327             // Don't allow null subject
    328             if (subject == null) {
    329                 subject = "";
    330             }
    331             hasAttachments = cursor.getInt(UIProvider.CONVERSATION_HAS_ATTACHMENTS_COLUMN) != 0;
    332             String messageList = cursor.getString(UIProvider.CONVERSATION_MESSAGE_LIST_URI_COLUMN);
    333             messageListUri = !TextUtils.isEmpty(messageList) ? Uri.parse(messageList) : null;
    334             sendingState = cursor.getInt(UIProvider.CONVERSATION_SENDING_STATE_COLUMN);
    335             priority = cursor.getInt(UIProvider.CONVERSATION_PRIORITY_COLUMN);
    336             read = cursor.getInt(UIProvider.CONVERSATION_READ_COLUMN) != 0;
    337             seen = cursor.getInt(UIProvider.CONVERSATION_SEEN_COLUMN) != 0;
    338             starred = cursor.getInt(UIProvider.CONVERSATION_STARRED_COLUMN) != 0;
    339             rawFolders = readRawFolders(cursor);
    340             convFlags = cursor.getInt(UIProvider.CONVERSATION_FLAGS_COLUMN);
    341             personalLevel = cursor.getInt(UIProvider.CONVERSATION_PERSONAL_LEVEL_COLUMN);
    342             spam = cursor.getInt(UIProvider.CONVERSATION_IS_SPAM_COLUMN) != 0;
    343             phishing = cursor.getInt(UIProvider.CONVERSATION_IS_PHISHING_COLUMN) != 0;
    344             muted = cursor.getInt(UIProvider.CONVERSATION_MUTED_COLUMN) != 0;
    345             color = cursor.getInt(UIProvider.CONVERSATION_COLOR_COLUMN);
    346             String account = cursor.getString(UIProvider.CONVERSATION_ACCOUNT_URI_COLUMN);
    347             accountUri = !TextUtils.isEmpty(account) ? Uri.parse(account) : null;
    348             position = NO_POSITION;
    349             localDeleteOnUpdate = false;
    350             conversationInfo = readConversationInfo(cursor);
    351             final String conversationBase =
    352                     cursor.getString(UIProvider.CONVERSATION_BASE_URI_COLUMN);
    353             conversationBaseUri = !TextUtils.isEmpty(conversationBase) ?
    354                     Uri.parse(conversationBase) : null;
    355             if (conversationInfo == null) {
    356                 snippet = cursor.getString(UIProvider.CONVERSATION_SNIPPET_COLUMN);
    357                 senders = emptyIfNull(cursor.getString(UIProvider.CONVERSATION_SENDER_INFO_COLUMN));
    358                 numMessages = cursor.getInt(UIProvider.CONVERSATION_NUM_MESSAGES_COLUMN);
    359                 numDrafts = cursor.getInt(UIProvider.CONVERSATION_NUM_DRAFTS_COLUMN);
    360             }
    361             isRemote = cursor.getInt(UIProvider.CONVERSATION_REMOTE_COLUMN) != 0;
    362             attachmentPreviews = null;
    363             attachmentPreviewUri0 = cursor.getString(
    364                     UIProvider.CONVERSATION_ATTACHMENT_PREVIEW_URI0_COLUMN);
    365             attachmentPreviewUri1 = cursor.getString(
    366                     UIProvider.CONVERSATION_ATTACHMENT_PREVIEW_URI1_COLUMN);
    367             attachmentPreviewStates = cursor.getInt(
    368                     UIProvider.CONVERSATION_ATTACHMENT_PREVIEW_STATES_COLUMN);
    369             attachmentPreviewsCount = cursor.getInt(
    370                     UIProvider.CONVERSATION_ATTACHMENT_PREVIEWS_COUNT_COLUMN);
    371         }
    372     }
    373 
    374     public Conversation(Conversation other) {
    375         if (other == null) {
    376             return;
    377         }
    378 
    379         id = other.id;
    380         uri = other.uri;
    381         dateMs = other.dateMs;
    382         subject = other.subject;
    383         hasAttachments = other.hasAttachments;
    384         messageListUri = other.messageListUri;
    385         sendingState = other.sendingState;
    386         priority = other.priority;
    387         read = other.read;
    388         seen = other.seen;
    389         starred = other.starred;
    390         rawFolders = other.rawFolders; // FolderList is immutable, shallow copy is OK
    391         convFlags = other.convFlags;
    392         personalLevel = other.personalLevel;
    393         spam = other.spam;
    394         phishing = other.phishing;
    395         muted = other.muted;
    396         color = other.color;
    397         accountUri = other.accountUri;
    398         position = other.position;
    399         localDeleteOnUpdate = other.localDeleteOnUpdate;
    400         // although ConversationInfo is mutable (see ConversationInfo.markRead), applyCachedValues
    401         // will overwrite this if cached changes exist anyway, so a shallow copy is OK
    402         conversationInfo = other.conversationInfo;
    403         conversationBaseUri = other.conversationBaseUri;
    404         snippet = other.snippet;
    405         senders = other.senders;
    406         numMessages = other.numMessages;
    407         numDrafts = other.numDrafts;
    408         isRemote = other.isRemote;
    409         attachmentPreviews = null;
    410         attachmentPreviewUri0 = other.attachmentPreviewUri0;
    411         attachmentPreviewUri1 = other.attachmentPreviewUri1;
    412         attachmentPreviewStates = other.attachmentPreviewStates;
    413         attachmentPreviewsCount = other.attachmentPreviewsCount;
    414     }
    415 
    416     public Conversation() {
    417     }
    418 
    419     public static Conversation create(long id, Uri uri, String subject, long dateMs, String snippet,
    420             boolean hasAttachment, Uri messageListUri, String senders,
    421             int numMessages, int numDrafts, int sendingState, int priority, boolean read,
    422             boolean seen, boolean starred, FolderList rawFolders, int convFlags, int personalLevel,
    423             boolean spam, boolean phishing, boolean muted, Uri accountUri,
    424             ConversationInfo conversationInfo, Uri conversationBase, boolean isRemote,
    425             String attachmentPreviewUri0, String attachmentPreviewUri1, int attachmentPreviewStates,
    426             int attachmentPreviewsCount) {
    427         final Conversation conversation = new Conversation();
    428 
    429         conversation.id = id;
    430         conversation.uri = uri;
    431         conversation.subject = subject;
    432         conversation.dateMs = dateMs;
    433         conversation.snippet = snippet;
    434         conversation.hasAttachments = hasAttachment;
    435         conversation.messageListUri = messageListUri;
    436         conversation.senders = emptyIfNull(senders);
    437         conversation.numMessages = numMessages;
    438         conversation.numDrafts = numDrafts;
    439         conversation.sendingState = sendingState;
    440         conversation.priority = priority;
    441         conversation.read = read;
    442         conversation.seen = seen;
    443         conversation.starred = starred;
    444         conversation.rawFolders = rawFolders;
    445         conversation.convFlags = convFlags;
    446         conversation.personalLevel = personalLevel;
    447         conversation.spam = spam;
    448         conversation.phishing = phishing;
    449         conversation.muted = muted;
    450         conversation.color = 0;
    451         conversation.accountUri = accountUri;
    452         conversation.conversationInfo = conversationInfo;
    453         conversation.conversationBaseUri = conversationBase;
    454         conversation.isRemote = isRemote;
    455         conversation.attachmentPreviews = null;
    456         conversation.attachmentPreviewUri0 = attachmentPreviewUri0;
    457         conversation.attachmentPreviewUri1 = attachmentPreviewUri1;
    458         conversation.attachmentPreviewStates = attachmentPreviewStates;
    459         conversation.attachmentPreviewsCount = attachmentPreviewsCount;
    460         return conversation;
    461     }
    462 
    463     private static final Bundle sConversationInfoRequest = new Bundle(1);
    464     private static final Bundle sRawFoldersRequest = new Bundle(1);
    465 
    466     private static ConversationInfo readConversationInfo(Cursor cursor) {
    467         final ConversationInfo ci;
    468 
    469         if (cursor instanceof ConversationCursor) {
    470             final byte[] blob = ((ConversationCursor) cursor).getCachedBlob(
    471                     UIProvider.CONVERSATION_INFO_COLUMN);
    472             if (blob != null && blob.length > 0) {
    473                 return ConversationInfo.fromBlob(blob);
    474             }
    475         }
    476 
    477         final String key = UIProvider.ConversationCursorCommand.COMMAND_GET_CONVERSATION_INFO;
    478         if (sConversationInfoRequest.isEmpty()) {
    479             sConversationInfoRequest.putBoolean(key, true);
    480             sConversationInfoRequest.putInt(
    481                     UIProvider.ConversationCursorCommand.COMMAND_KEY_OPTIONS,
    482                     UIProvider.ConversationCursorCommand.OPTION_MOVE_POSITION);
    483         }
    484         final Bundle response = cursor.respond(sConversationInfoRequest);
    485         if (response.containsKey(key)) {
    486             ci = response.getParcelable(key);
    487         } else {
    488             // legacy fallback
    489             ci = ConversationInfo.fromBlob(cursor.getBlob(UIProvider.CONVERSATION_INFO_COLUMN));
    490         }
    491         return ci;
    492     }
    493 
    494     private static FolderList readRawFolders(Cursor cursor) {
    495         final FolderList fl;
    496 
    497         if (cursor instanceof ConversationCursor) {
    498             final byte[] blob = ((ConversationCursor) cursor).getCachedBlob(
    499                     UIProvider.CONVERSATION_RAW_FOLDERS_COLUMN);
    500             if (blob != null && blob.length > 0) {
    501                 return FolderList.fromBlob(blob);
    502             }
    503         }
    504 
    505         final String key = UIProvider.ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS;
    506         if (sRawFoldersRequest.isEmpty()) {
    507             sRawFoldersRequest.putBoolean(key, true);
    508             sRawFoldersRequest.putInt(
    509                     UIProvider.ConversationCursorCommand.COMMAND_KEY_OPTIONS,
    510                     UIProvider.ConversationCursorCommand.OPTION_MOVE_POSITION);
    511         }
    512         final Bundle response = cursor.respond(sRawFoldersRequest);
    513         if (response.containsKey(key)) {
    514             fl = response.getParcelable(key);
    515         } else {
    516             // legacy fallback
    517             // TODO: delete this once Email supports the respond call
    518             fl = FolderList.fromBlob(
    519                     cursor.getBlob(UIProvider.CONVERSATION_RAW_FOLDERS_COLUMN));
    520         }
    521         return fl;
    522     }
    523 
    524     /**
    525      * Apply any column values from the given {@link ContentValues} (where column names are the
    526      * keys) to this conversation.
    527      *
    528      */
    529     public void applyCachedValues(ContentValues values) {
    530         if (values == null) {
    531             return;
    532         }
    533         for (String key : values.keySet()) {
    534             final Object val = values.get(key);
    535             LogUtils.i(LOG_TAG, "Conversation: applying cached value to col=%s val=%s", key,
    536                     val);
    537             if (ConversationColumns.READ.equals(key)) {
    538                 read = (Integer) val != 0;
    539             } else if (ConversationColumns.CONVERSATION_INFO.equals(key)) {
    540                 conversationInfo = ConversationInfo.fromBlob((byte[]) val);
    541             } else if (ConversationColumns.FLAGS.equals(key)) {
    542                 convFlags = (Integer) val;
    543             } else if (ConversationColumns.STARRED.equals(key)) {
    544                 starred = (Integer) val != 0;
    545             } else if (ConversationColumns.SEEN.equals(key)) {
    546                 seen = (Integer) val != 0;
    547             } else if (ConversationColumns.RAW_FOLDERS.equals(key)) {
    548                 rawFolders = FolderList.fromBlob((byte[]) val);
    549             } else if (ConversationColumns.VIEWED.equals(key)) {
    550                 // ignore. this is not read from the cursor, either.
    551             } else {
    552                 LogUtils.e(LOG_TAG, new UnsupportedOperationException(),
    553                         "unsupported cached conv value in col=%s", key);
    554             }
    555         }
    556     }
    557 
    558     /**
    559      * Get the <strong>immutable</strong> list of {@link Folder}s for this conversation. To modify
    560      * this list, make a new {@link FolderList} and use {@link #setRawFolders(FolderList)}.
    561      *
    562      * @return <strong>Immutable</strong> list of {@link Folder}s.
    563      */
    564     public List<Folder> getRawFolders() {
    565         return rawFolders.folders;
    566     }
    567 
    568     public void setRawFolders(FolderList folders) {
    569         rawFolders = folders;
    570     }
    571 
    572     @Override
    573     public boolean equals(Object o) {
    574         if (o instanceof Conversation) {
    575             Conversation conv = (Conversation) o;
    576             return conv.uri.equals(uri);
    577         }
    578         return false;
    579     }
    580 
    581     @Override
    582     public int hashCode() {
    583         return uri.hashCode();
    584     }
    585 
    586     /**
    587      * Get if this conversation is marked as high priority.
    588      */
    589     public boolean isImportant() {
    590         return priority == UIProvider.ConversationPriority.IMPORTANT;
    591     }
    592 
    593     /**
    594      * Get if this conversation is mostly dead
    595      */
    596     public boolean isMostlyDead() {
    597         return (convFlags & FLAG_MOSTLY_DEAD) != 0;
    598     }
    599 
    600     /**
    601      * Returns true if the URI of the conversation specified as the needle was
    602      * found in the collection of conversations specified as the haystack. False
    603      * otherwise. This method is safe to call with null arguments.
    604      *
    605      * @param haystack
    606      * @param needle
    607      * @return true if the needle was found in the haystack, false otherwise.
    608      */
    609     public final static boolean contains(Collection<Conversation> haystack, Conversation needle) {
    610         // If the haystack is empty, it cannot contain anything.
    611         if (haystack == null || haystack.size() <= 0) {
    612             return false;
    613         }
    614         // The null folder exists everywhere.
    615         if (needle == null) {
    616             return true;
    617         }
    618         final long toFind = needle.id;
    619         for (final Conversation c : haystack) {
    620             if (toFind == c.id) {
    621                 return true;
    622             }
    623         }
    624         return false;
    625     }
    626 
    627     /**
    628      * Returns a collection of a single conversation. This method always returns
    629      * a valid collection even if the input conversation is null.
    630      *
    631      * @param in a conversation, possibly null.
    632      * @return a collection of the conversation.
    633      */
    634     public static Collection<Conversation> listOf(Conversation in) {
    635         final Collection<Conversation> target = (in == null) ? EMPTY : ImmutableList.of(in);
    636         return target;
    637     }
    638 
    639     /**
    640      * Get the snippet for this conversation. Masks that it may come from
    641      * conversation info or the original deprecated snippet string.
    642      */
    643     public String getSnippet() {
    644         return conversationInfo != null && !TextUtils.isEmpty(conversationInfo.firstSnippet) ?
    645                 conversationInfo.firstSnippet : snippet;
    646     }
    647 
    648     /**
    649      * Get the number of messages for this conversation.
    650      */
    651     public int getNumMessages() {
    652         return conversationInfo != null ? conversationInfo.messageCount : numMessages;
    653     }
    654 
    655     /**
    656      * Get the number of drafts for this conversation.
    657      */
    658     public int numDrafts() {
    659         return conversationInfo != null ? conversationInfo.draftCount : numDrafts;
    660     }
    661 
    662     public boolean isViewed() {
    663         return viewed;
    664     }
    665 
    666     public void markViewed() {
    667         viewed = true;
    668     }
    669 
    670     public String getBaseUri(String defaultValue) {
    671         return conversationBaseUri != null ? conversationBaseUri.toString() : defaultValue;
    672     }
    673 
    674     public ArrayList<String> getAttachmentPreviewUris() {
    675         if (attachmentPreviews == null) {
    676             attachmentPreviews = Lists.newArrayListWithCapacity(2);
    677             if (!TextUtils.isEmpty(attachmentPreviewUri0)) {
    678                 attachmentPreviews.add(attachmentPreviewUri0);
    679             }
    680             if (!TextUtils.isEmpty(attachmentPreviewUri1)) {
    681                 attachmentPreviews.add(attachmentPreviewUri1);
    682             }
    683         }
    684         return attachmentPreviews;
    685     }
    686 
    687     /**
    688      * Create a human-readable string of all the conversations
    689      * @param collection Any collection of conversations
    690      * @return string with a human readable representation of the conversations.
    691      */
    692     public static String toString(Collection<Conversation> collection) {
    693         final StringBuilder out = new StringBuilder(collection.size() + " conversations:");
    694         int count = 0;
    695         for (final Conversation c : collection) {
    696             count++;
    697             // Indent the conversations to make them easy to read in debug
    698             // output.
    699             out.append("      " + count + ": " + c.toString() + "\n");
    700         }
    701         return out.toString();
    702     }
    703 
    704     /**
    705      * Returns an empty string if the specified string is null
    706      */
    707     private static String emptyIfNull(String in) {
    708         return in != null ? in : EMPTY_STRING;
    709     }
    710 
    711     /**
    712      * Get the properly formatted subject and snippet string for display a
    713      * conversation.
    714      *
    715      * @param context
    716      * @param filteredSubject
    717      * @param snippet
    718      */
    719     public static String getSubjectAndSnippetForDisplay(Context context,
    720             String filteredSubject, String snippet) {
    721         if (sSubjectAndSnippet == null) {
    722             sSubjectAndSnippet = context.getString(R.string.subject_and_snippet);
    723         }
    724         if (TextUtils.isEmpty(filteredSubject) && TextUtils.isEmpty(snippet)) {
    725             return "";
    726         } else if (TextUtils.isEmpty(filteredSubject)) {
    727             return snippet;
    728         } else if (TextUtils.isEmpty(snippet)) {
    729             return filteredSubject;
    730         }
    731 
    732         return String.format(sSubjectAndSnippet, filteredSubject, snippet);
    733     }
    734 
    735     /**
    736      * Public object that knows how to construct Conversation given Cursors. This is not used by
    737      * {@link ConversationCursor} or {@link ConversationCursorLoader}.
    738      */
    739     public static final CursorCreator<Conversation> FACTORY = new CursorCreator<Conversation>() {
    740         @Override
    741         public Conversation createFromCursor(final Cursor c) {
    742             return new Conversation(c);
    743         }
    744 
    745         @Override
    746         public String toString() {
    747             return "Conversation CursorCreator";
    748         }
    749     };
    750 }
    751