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