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     public static final Uri MOVE_CONVERSATIONS_URI = Uri.parse("content://moveconversations");
    265 
    266     /**
    267      * The column that needs to be updated to change the folders for a conversation.
    268      */
    269     public static final String UPDATE_FOLDER_COLUMN = ConversationColumns.RAW_FOLDERS;
    270 
    271     public Conversation(Cursor cursor) {
    272         if (cursor == null) {
    273             throw new IllegalArgumentException("Creating conversation from null cursor");
    274         }
    275         id = cursor.getLong(UIProvider.CONVERSATION_ID_COLUMN);
    276         uri = Uri.parse(cursor.getString(UIProvider.CONVERSATION_URI_COLUMN));
    277         dateMs = cursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN);
    278         final String subj = cursor.getString(UIProvider.CONVERSATION_SUBJECT_COLUMN);
    279         // Don't allow null subject
    280         if (subj == null) {
    281             subject = "";
    282         } else {
    283             subject = subj;
    284         }
    285         hasAttachments = cursor.getInt(UIProvider.CONVERSATION_HAS_ATTACHMENTS_COLUMN) != 0;
    286         String messageList = cursor.getString(UIProvider.CONVERSATION_MESSAGE_LIST_URI_COLUMN);
    287         messageListUri = !TextUtils.isEmpty(messageList) ? Uri.parse(messageList) : null;
    288         sendingState = cursor.getInt(UIProvider.CONVERSATION_SENDING_STATE_COLUMN);
    289         priority = cursor.getInt(UIProvider.CONVERSATION_PRIORITY_COLUMN);
    290         read = cursor.getInt(UIProvider.CONVERSATION_READ_COLUMN) != 0;
    291         seen = cursor.getInt(UIProvider.CONVERSATION_SEEN_COLUMN) != 0;
    292         starred = cursor.getInt(UIProvider.CONVERSATION_STARRED_COLUMN) != 0;
    293         rawFolders = readRawFolders(cursor);
    294         convFlags = cursor.getInt(UIProvider.CONVERSATION_FLAGS_COLUMN);
    295         personalLevel = cursor.getInt(UIProvider.CONVERSATION_PERSONAL_LEVEL_COLUMN);
    296         spam = cursor.getInt(UIProvider.CONVERSATION_IS_SPAM_COLUMN) != 0;
    297         phishing = cursor.getInt(UIProvider.CONVERSATION_IS_PHISHING_COLUMN) != 0;
    298         muted = cursor.getInt(UIProvider.CONVERSATION_MUTED_COLUMN) != 0;
    299         color = cursor.getInt(UIProvider.CONVERSATION_COLOR_COLUMN);
    300         String account = cursor.getString(UIProvider.CONVERSATION_ACCOUNT_URI_COLUMN);
    301         accountUri = !TextUtils.isEmpty(account) ? Uri.parse(account) : null;
    302         position = NO_POSITION;
    303         localDeleteOnUpdate = false;
    304         conversationInfo = readConversationInfo(cursor);
    305         if (conversationInfo == null) {
    306             LogUtils.wtf(LOG_TAG, "Null conversation info from cursor");
    307         }
    308         final String conversationBase =
    309                 cursor.getString(UIProvider.CONVERSATION_BASE_URI_COLUMN);
    310         conversationBaseUri = !TextUtils.isEmpty(conversationBase) ?
    311                 Uri.parse(conversationBase) : null;
    312         isRemote = cursor.getInt(UIProvider.CONVERSATION_REMOTE_COLUMN) != 0;
    313         orderKey = cursor.getLong(UIProvider.CONVERSATION_ORDER_KEY_COLUMN);
    314     }
    315 
    316     public Conversation(Conversation other) {
    317         if (other == null) {
    318             throw new IllegalArgumentException("Copying null conversation");
    319         }
    320 
    321         id = other.id;
    322         uri = other.uri;
    323         dateMs = other.dateMs;
    324         subject = other.subject;
    325         hasAttachments = other.hasAttachments;
    326         messageListUri = other.messageListUri;
    327         sendingState = other.sendingState;
    328         priority = other.priority;
    329         read = other.read;
    330         seen = other.seen;
    331         starred = other.starred;
    332         rawFolders = other.rawFolders; // FolderList is immutable, shallow copy is OK
    333         convFlags = other.convFlags;
    334         personalLevel = other.personalLevel;
    335         spam = other.spam;
    336         phishing = other.phishing;
    337         muted = other.muted;
    338         color = other.color;
    339         accountUri = other.accountUri;
    340         position = other.position;
    341         localDeleteOnUpdate = other.localDeleteOnUpdate;
    342         // although ConversationInfo is mutable (see ConversationInfo.markRead), applyCachedValues
    343         // will overwrite this if cached changes exist anyway, so a shallow copy is OK
    344         conversationInfo = other.conversationInfo;
    345         conversationBaseUri = other.conversationBaseUri;
    346         isRemote = other.isRemote;
    347         orderKey = other.orderKey;
    348     }
    349 
    350     private Conversation(long id, Uri uri, String subject, long dateMs,
    351             boolean hasAttachment, Uri messageListUri,
    352             int sendingState, int priority, boolean read,
    353             boolean seen, boolean starred, FolderList rawFolders, int convFlags, int personalLevel,
    354             boolean spam, boolean phishing, boolean muted, Uri accountUri,
    355             ConversationInfo conversationInfo, Uri conversationBase, boolean isRemote,
    356             String permalink, long orderKey) {
    357         if (conversationInfo == null) {
    358             throw new IllegalArgumentException("Null conversationInfo");
    359         }
    360         this.id = id;
    361         this.uri = uri;
    362         this.subject = subject;
    363         this.dateMs = dateMs;
    364         this.hasAttachments = hasAttachment;
    365         this.messageListUri = messageListUri;
    366         this.sendingState = sendingState;
    367         this.priority = priority;
    368         this.read = read;
    369         this.seen = seen;
    370         this.starred = starred;
    371         this.rawFolders = rawFolders;
    372         this.convFlags = convFlags;
    373         this.personalLevel = personalLevel;
    374         this.spam = spam;
    375         this.phishing = phishing;
    376         this.muted = muted;
    377         this.color = 0;
    378         this.accountUri = accountUri;
    379         this.conversationInfo = conversationInfo;
    380         this.conversationBaseUri = conversationBase;
    381         this.isRemote = isRemote;
    382         this.orderKey = orderKey;
    383     }
    384 
    385     public static class Builder {
    386         private long mId;
    387         private Uri mUri;
    388         private String mSubject;
    389         private long mDateMs;
    390         private boolean mHasAttachments;
    391         private Uri mMessageListUri;
    392         private int mSendingState;
    393         private int mPriority;
    394         private boolean mRead;
    395         private boolean mSeen;
    396         private boolean mStarred;
    397         private FolderList mRawFolders;
    398         private int mConvFlags;
    399         private int mPersonalLevel;
    400         private boolean mSpam;
    401         private boolean mPhishing;
    402         private boolean mMuted;
    403         private Uri mAccountUri;
    404         private ConversationInfo mConversationInfo;
    405         private Uri mConversationBaseUri;
    406         private boolean mIsRemote;
    407         private String mPermalink;
    408         private long mOrderKey;
    409 
    410         public Builder setId(long id) {
    411             mId = id;
    412             return this;
    413         }
    414 
    415         public Builder setUri(Uri uri) {
    416             mUri = uri;
    417             return this;
    418         }
    419 
    420         public Builder setSubject(String subject) {
    421             mSubject = subject;
    422             return this;
    423         }
    424 
    425         public Builder setDateMs(long dateMs) {
    426             mDateMs = dateMs;
    427             return this;
    428         }
    429 
    430         public Builder setHasAttachments(boolean hasAttachments) {
    431             mHasAttachments = hasAttachments;
    432             return this;
    433         }
    434 
    435         public Builder setMessageListUri(Uri messageListUri) {
    436             mMessageListUri = messageListUri;
    437             return this;
    438         }
    439 
    440         public Builder setSendingState(int sendingState) {
    441             mSendingState = sendingState;
    442             return this;
    443         }
    444 
    445         public Builder setPriority(int priority) {
    446             mPriority = priority;
    447             return this;
    448         }
    449 
    450         public Builder setRead(boolean read) {
    451             mRead = read;
    452             return this;
    453         }
    454 
    455         public Builder setSeen(boolean seen) {
    456             mSeen = seen;
    457             return this;
    458         }
    459 
    460         public Builder setStarred(boolean starred) {
    461             mStarred = starred;
    462             return this;
    463         }
    464 
    465         public Builder setRawFolders(FolderList rawFolders) {
    466             mRawFolders = rawFolders;
    467             return this;
    468         }
    469 
    470         public Builder setConvFlags(int convFlags) {
    471             mConvFlags = convFlags;
    472             return this;
    473         }
    474 
    475         public Builder setPersonalLevel(int personalLevel) {
    476             mPersonalLevel = personalLevel;
    477             return this;
    478         }
    479 
    480         public Builder setSpam(boolean spam) {
    481             mSpam = spam;
    482             return this;
    483         }
    484 
    485         public Builder setPhishing(boolean phishing) {
    486             mPhishing = phishing;
    487             return this;
    488         }
    489 
    490         public Builder setMuted(boolean muted) {
    491             mMuted = muted;
    492             return this;
    493         }
    494 
    495         public Builder setAccountUri(Uri accountUri) {
    496             mAccountUri = accountUri;
    497             return this;
    498         }
    499 
    500         public Builder setConversationInfo(ConversationInfo conversationInfo) {
    501             if (conversationInfo == null) {
    502                 throw new IllegalArgumentException("Can't set null ConversationInfo");
    503             }
    504             mConversationInfo = conversationInfo;
    505             return this;
    506         }
    507 
    508         public Builder setConversationBaseUri(Uri conversationBaseUri) {
    509             mConversationBaseUri = conversationBaseUri;
    510             return this;
    511         }
    512 
    513         public Builder setIsRemote(boolean isRemote) {
    514             mIsRemote = isRemote;
    515             return this;
    516         }
    517 
    518         public Builder setPermalink(String permalink) {
    519             mPermalink = permalink;
    520             return this;
    521         }
    522 
    523         public Builder setOrderKey(long orderKey) {
    524             mOrderKey = orderKey;
    525             return this;
    526         }
    527 
    528         public Builder() {}
    529 
    530         public Conversation build() {
    531             if (mConversationInfo == null) {
    532                 LogUtils.d(LOG_TAG, "Null conversationInfo in Builder");
    533                 mConversationInfo = new ConversationInfo();
    534             }
    535             return new Conversation(mId, mUri, mSubject, mDateMs, mHasAttachments, mMessageListUri,
    536                     mSendingState, mPriority, mRead, mSeen, mStarred, mRawFolders, mConvFlags,
    537                     mPersonalLevel, mSpam, mPhishing, mMuted, mAccountUri, mConversationInfo,
    538                     mConversationBaseUri, mIsRemote, mPermalink, mOrderKey);
    539         }
    540     }
    541 
    542     private static final Bundle CONVERSATION_INFO_REQUEST;
    543     private static final Bundle RAW_FOLDERS_REQUEST;
    544 
    545     static {
    546         RAW_FOLDERS_REQUEST = new Bundle(2);
    547         RAW_FOLDERS_REQUEST.putBoolean(
    548                 ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS, true);
    549         RAW_FOLDERS_REQUEST.putInt(
    550                 ConversationCursorCommand.COMMAND_KEY_OPTIONS,
    551                 ConversationCursorCommand.OPTION_MOVE_POSITION);
    552 
    553         CONVERSATION_INFO_REQUEST = new Bundle(2);
    554         CONVERSATION_INFO_REQUEST.putBoolean(
    555                 ConversationCursorCommand.COMMAND_GET_CONVERSATION_INFO, true);
    556         CONVERSATION_INFO_REQUEST.putInt(
    557                 ConversationCursorCommand.COMMAND_KEY_OPTIONS,
    558                 ConversationCursorCommand.OPTION_MOVE_POSITION);
    559     }
    560 
    561     private static ConversationInfo readConversationInfo(Cursor cursor) {
    562         final ConversationInfo ci;
    563 
    564         if (cursor instanceof ConversationCursor) {
    565             final byte[] blob = ((ConversationCursor) cursor).getCachedBlob(
    566                     UIProvider.CONVERSATION_INFO_COLUMN);
    567             if (blob != null && blob.length > 0) {
    568                 return ConversationInfo.fromBlob(blob);
    569             }
    570         }
    571 
    572         final Bundle response = cursor.respond(CONVERSATION_INFO_REQUEST);
    573         if (response.containsKey(ConversationCursorCommand.COMMAND_GET_CONVERSATION_INFO)) {
    574             ci = response.getParcelable(ConversationCursorCommand.COMMAND_GET_CONVERSATION_INFO);
    575         } else {
    576             // legacy fallback
    577             ci = ConversationInfo.fromBlob(cursor.getBlob(UIProvider.CONVERSATION_INFO_COLUMN));
    578         }
    579         return ci;
    580     }
    581 
    582     private static FolderList readRawFolders(Cursor cursor) {
    583         final FolderList fl;
    584 
    585         if (cursor instanceof ConversationCursor) {
    586             final byte[] blob = ((ConversationCursor) cursor).getCachedBlob(
    587                     UIProvider.CONVERSATION_RAW_FOLDERS_COLUMN);
    588             if (blob != null && blob.length > 0) {
    589                 return FolderList.fromBlob(blob);
    590             }
    591         }
    592 
    593         final Bundle response = cursor.respond(RAW_FOLDERS_REQUEST);
    594         if (response.containsKey(ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS)) {
    595             fl = response.getParcelable(ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS);
    596         } else {
    597             // legacy fallback
    598             // TODO: delete this once Email supports the respond call
    599             fl = FolderList.fromBlob(
    600                     cursor.getBlob(UIProvider.CONVERSATION_RAW_FOLDERS_COLUMN));
    601         }
    602         return fl;
    603     }
    604 
    605     /**
    606      * Apply any column values from the given {@link ContentValues} (where column names are the
    607      * keys) to this conversation.
    608      *
    609      */
    610     public void applyCachedValues(ContentValues values) {
    611         if (values == null) {
    612             return;
    613         }
    614         for (String key : values.keySet()) {
    615             final Object val = values.get(key);
    616             LogUtils.i(LOG_TAG, "Conversation: applying cached value to col=%s val=%s", key,
    617                     val);
    618             if (ConversationColumns.READ.equals(key)) {
    619                 read = (Integer) val != 0;
    620             } else if (ConversationColumns.CONVERSATION_INFO.equals(key)) {
    621                 final ConversationInfo cachedCi = ConversationInfo.fromBlob((byte[]) val);
    622                 if (cachedCi == null) {
    623                     LogUtils.d(LOG_TAG, "Null ConversationInfo in applyCachedValues");
    624                 } else {
    625                     conversationInfo.overwriteWith(cachedCi);
    626                 }
    627             } else if (ConversationColumns.FLAGS.equals(key)) {
    628                 convFlags = (Integer) val;
    629             } else if (ConversationColumns.STARRED.equals(key)) {
    630                 starred = (Integer) val != 0;
    631             } else if (ConversationColumns.SEEN.equals(key)) {
    632                 seen = (Integer) val != 0;
    633             } else if (ConversationColumns.RAW_FOLDERS.equals(key)) {
    634                 rawFolders = FolderList.fromBlob((byte[]) val);
    635             } else if (ConversationColumns.VIEWED.equals(key)) {
    636                 // ignore. this is not read from the cursor, either.
    637             } else if (ConversationColumns.PRIORITY.equals(key)) {
    638                 priority = (Integer) val;
    639             } else {
    640                 LogUtils.e(LOG_TAG, new UnsupportedOperationException(),
    641                         "unsupported cached conv value in col=%s", key);
    642             }
    643         }
    644     }
    645 
    646     /**
    647      * Get the <strong>immutable</strong> list of {@link Folder}s for this conversation. To modify
    648      * this list, make a new {@link FolderList} and use {@link #setRawFolders(FolderList)}.
    649      *
    650      * @return <strong>Immutable</strong> list of {@link Folder}s.
    651      */
    652     public List<Folder> getRawFolders() {
    653         return rawFolders.folders;
    654     }
    655 
    656     public void setRawFolders(FolderList folders) {
    657         rawFolders = folders;
    658     }
    659 
    660     @Override
    661     public boolean equals(Object o) {
    662         if (o instanceof Conversation) {
    663             Conversation conv = (Conversation) o;
    664             return conv.uri.equals(uri);
    665         }
    666         return false;
    667     }
    668 
    669     @Override
    670     public int hashCode() {
    671         return uri.hashCode();
    672     }
    673 
    674     /**
    675      * Get if this conversation is marked as high priority.
    676      */
    677     public boolean isImportant() {
    678         return priority == UIProvider.ConversationPriority.IMPORTANT;
    679     }
    680 
    681     /**
    682      * Get if this conversation is mostly dead
    683      */
    684     public boolean isMostlyDead() {
    685         return (convFlags & FLAG_MOSTLY_DEAD) != 0;
    686     }
    687 
    688     /**
    689      * Returns true if the URI of the conversation specified as the needle was
    690      * found in the collection of conversations specified as the haystack. False
    691      * otherwise. This method is safe to call with null arguments.
    692      *
    693      * @param haystack
    694      * @param needle
    695      * @return true if the needle was found in the haystack, false otherwise.
    696      */
    697     public final static boolean contains(Collection<Conversation> haystack, Conversation needle) {
    698         // If the haystack is empty, it cannot contain anything.
    699         if (haystack == null || haystack.size() <= 0) {
    700             return false;
    701         }
    702         // The null folder exists everywhere.
    703         if (needle == null) {
    704             return true;
    705         }
    706         final long toFind = needle.id;
    707         for (final Conversation c : haystack) {
    708             if (toFind == c.id) {
    709                 return true;
    710             }
    711         }
    712         return false;
    713     }
    714 
    715     /**
    716      * Returns a collection of a single conversation. This method always returns
    717      * a valid collection even if the input conversation is null.
    718      *
    719      * @param in a conversation, possibly null.
    720      * @return a collection of the conversation.
    721      */
    722     public static Collection<Conversation> listOf(Conversation in) {
    723         final Collection<Conversation> target = (in == null) ? EMPTY : ImmutableList.of(in);
    724         return target;
    725     }
    726 
    727     /**
    728      * Get the snippet for this conversation.
    729      */
    730     public String getSnippet() {
    731         return !TextUtils.isEmpty(conversationInfo.firstSnippet) ?
    732                 conversationInfo.firstSnippet : "";
    733     }
    734 
    735     /**
    736      * Get the number of messages for this conversation.
    737      */
    738     public int getNumMessages() {
    739         return conversationInfo.messageCount;
    740     }
    741 
    742     /**
    743      * Get the number of drafts for this conversation.
    744      */
    745     public int numDrafts() {
    746         return conversationInfo.draftCount;
    747     }
    748 
    749     public boolean isViewed() {
    750         return viewed;
    751     }
    752 
    753     public void markViewed() {
    754         viewed = true;
    755     }
    756 
    757     public String getBaseUri(String defaultValue) {
    758         return conversationBaseUri != null ? conversationBaseUri.toString() : defaultValue;
    759     }
    760 
    761     /**
    762      * Create a human-readable string of all the conversations
    763      * @param collection Any collection of conversations
    764      * @return string with a human readable representation of the conversations.
    765      */
    766     public static String toString(Collection<Conversation> collection) {
    767         final StringBuilder out = new StringBuilder(collection.size() + " conversations:");
    768         int count = 0;
    769         for (final Conversation c : collection) {
    770             count++;
    771             // Indent the conversations to make them easy to read in debug
    772             // output.
    773             out.append("      " + count + ": " + c.toString() + "\n");
    774         }
    775         return out.toString();
    776     }
    777 
    778     /**
    779      * Returns an empty string if the specified string is null
    780      */
    781     private static String emptyIfNull(String in) {
    782         return in != null ? in : EMPTY_STRING;
    783     }
    784 
    785     /**
    786      * Get the properly formatted badge and subject string for displaying a conversation.
    787      */
    788     public static String getSubjectForDisplay(Context context, String badgeText,
    789             String filteredSubject) {
    790         if (TextUtils.isEmpty(filteredSubject)) {
    791             return context.getString(R.string.no_subject);
    792         } else if (!TextUtils.isEmpty(badgeText)) {
    793             if (sBadgeAndSubject == null) {
    794                 sBadgeAndSubject = context.getString(R.string.badge_and_subject);
    795             }
    796             return String.format(sBadgeAndSubject, badgeText, filteredSubject);
    797         }
    798 
    799         return filteredSubject;
    800     }
    801 
    802     /**
    803      * Public object that knows how to construct Conversation given Cursors. This is not used by
    804      * {@link ConversationCursor} or {@link ConversationCursorLoader}.
    805      */
    806     public static final CursorCreator<Conversation> FACTORY = new CursorCreator<Conversation>() {
    807         @Override
    808         public Conversation createFromCursor(final Cursor c) {
    809             return new Conversation(c);
    810         }
    811 
    812         @Override
    813         public String toString() {
    814             return "Conversation CursorCreator";
    815         }
    816     };
    817 }
    818