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
describeContents()172     public int describeContents() {
173         return 0;
174     }
175 
176     @Override
writeToParcel(Parcel dest, int flags)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 
Conversation(Parcel in, ClassLoader loader)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
toString()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 
Conversation(Cursor cursor)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 
Conversation(Conversation other)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 
Conversation(long id, Uri uri, String subject, long dateMs, boolean hasAttachment, Uri messageListUri, int sendingState, int priority, boolean read, boolean seen, boolean starred, FolderList rawFolders, int convFlags, int personalLevel, boolean spam, boolean phishing, boolean muted, Uri accountUri, ConversationInfo conversationInfo, Uri conversationBase, boolean isRemote, String permalink, long orderKey)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 
setId(long id)408         public Builder setId(long id) {
409             mId = id;
410             return this;
411         }
412 
setUri(Uri uri)413         public Builder setUri(Uri uri) {
414             mUri = uri;
415             return this;
416         }
417 
setSubject(String subject)418         public Builder setSubject(String subject) {
419             mSubject = subject;
420             return this;
421         }
422 
setDateMs(long dateMs)423         public Builder setDateMs(long dateMs) {
424             mDateMs = dateMs;
425             return this;
426         }
427 
setHasAttachments(boolean hasAttachments)428         public Builder setHasAttachments(boolean hasAttachments) {
429             mHasAttachments = hasAttachments;
430             return this;
431         }
432 
setMessageListUri(Uri messageListUri)433         public Builder setMessageListUri(Uri messageListUri) {
434             mMessageListUri = messageListUri;
435             return this;
436         }
437 
setSendingState(int sendingState)438         public Builder setSendingState(int sendingState) {
439             mSendingState = sendingState;
440             return this;
441         }
442 
setPriority(int priority)443         public Builder setPriority(int priority) {
444             mPriority = priority;
445             return this;
446         }
447 
setRead(boolean read)448         public Builder setRead(boolean read) {
449             mRead = read;
450             return this;
451         }
452 
setSeen(boolean seen)453         public Builder setSeen(boolean seen) {
454             mSeen = seen;
455             return this;
456         }
457 
setStarred(boolean starred)458         public Builder setStarred(boolean starred) {
459             mStarred = starred;
460             return this;
461         }
462 
setRawFolders(FolderList rawFolders)463         public Builder setRawFolders(FolderList rawFolders) {
464             mRawFolders = rawFolders;
465             return this;
466         }
467 
setConvFlags(int convFlags)468         public Builder setConvFlags(int convFlags) {
469             mConvFlags = convFlags;
470             return this;
471         }
472 
setPersonalLevel(int personalLevel)473         public Builder setPersonalLevel(int personalLevel) {
474             mPersonalLevel = personalLevel;
475             return this;
476         }
477 
setSpam(boolean spam)478         public Builder setSpam(boolean spam) {
479             mSpam = spam;
480             return this;
481         }
482 
setPhishing(boolean phishing)483         public Builder setPhishing(boolean phishing) {
484             mPhishing = phishing;
485             return this;
486         }
487 
setMuted(boolean muted)488         public Builder setMuted(boolean muted) {
489             mMuted = muted;
490             return this;
491         }
492 
setAccountUri(Uri accountUri)493         public Builder setAccountUri(Uri accountUri) {
494             mAccountUri = accountUri;
495             return this;
496         }
497 
setConversationInfo(ConversationInfo conversationInfo)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 
setConversationBaseUri(Uri conversationBaseUri)506         public Builder setConversationBaseUri(Uri conversationBaseUri) {
507             mConversationBaseUri = conversationBaseUri;
508             return this;
509         }
510 
setIsRemote(boolean isRemote)511         public Builder setIsRemote(boolean isRemote) {
512             mIsRemote = isRemote;
513             return this;
514         }
515 
setPermalink(String permalink)516         public Builder setPermalink(String permalink) {
517             mPermalink = permalink;
518             return this;
519         }
520 
setOrderKey(long orderKey)521         public Builder setOrderKey(long orderKey) {
522             mOrderKey = orderKey;
523             return this;
524         }
525 
Builder()526         public Builder() {}
527 
build()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);
RAW_FOLDERS_REQUEST.putBoolean( ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS, true)545         RAW_FOLDERS_REQUEST.putBoolean(
546                 ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS, true);
RAW_FOLDERS_REQUEST.putInt( ConversationCursorCommand.COMMAND_KEY_OPTIONS, ConversationCursorCommand.OPTION_MOVE_POSITION)547         RAW_FOLDERS_REQUEST.putInt(
548                 ConversationCursorCommand.COMMAND_KEY_OPTIONS,
549                 ConversationCursorCommand.OPTION_MOVE_POSITION);
550 
551         CONVERSATION_INFO_REQUEST = new Bundle(2);
CONVERSATION_INFO_REQUEST.putBoolean( ConversationCursorCommand.COMMAND_GET_CONVERSATION_INFO, true)552         CONVERSATION_INFO_REQUEST.putBoolean(
553                 ConversationCursorCommand.COMMAND_GET_CONVERSATION_INFO, true);
CONVERSATION_INFO_REQUEST.putInt( ConversationCursorCommand.COMMAND_KEY_OPTIONS, ConversationCursorCommand.OPTION_MOVE_POSITION)554         CONVERSATION_INFO_REQUEST.putInt(
555                 ConversationCursorCommand.COMMAND_KEY_OPTIONS,
556                 ConversationCursorCommand.OPTION_MOVE_POSITION);
557     }
558 
readConversationInfo(Cursor cursor)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 
readRawFolders(Cursor cursor)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      */
applyCachedValues(ContentValues values)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      */
getRawFolders()650     public List<Folder> getRawFolders() {
651         return rawFolders.folders;
652     }
653 
setRawFolders(FolderList folders)654     public void setRawFolders(FolderList folders) {
655         rawFolders = folders;
656     }
657 
658     @Override
equals(Object o)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
hashCode()668     public int hashCode() {
669         return uri.hashCode();
670     }
671 
672     /**
673      * Get if this conversation is marked as high priority.
674      */
isImportant()675     public boolean isImportant() {
676         return priority == UIProvider.ConversationPriority.IMPORTANT;
677     }
678 
679     /**
680      * Get if this conversation is mostly dead
681      */
isMostlyDead()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      */
contains(Collection<Conversation> haystack, Conversation needle)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      */
listOf(Conversation in)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      */
getSnippet()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      */
getNumMessages()736     public int getNumMessages() {
737         return conversationInfo.messageCount;
738     }
739 
740     /**
741      * Get the number of drafts for this conversation.
742      */
numDrafts()743     public int numDrafts() {
744         return conversationInfo.draftCount;
745     }
746 
isViewed()747     public boolean isViewed() {
748         return viewed;
749     }
750 
markViewed()751     public void markViewed() {
752         viewed = true;
753     }
754 
getBaseUri(String defaultValue)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      */
isInTrash()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      */
toString(Collection<Conversation> collection)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      */
emptyIfNull(String in)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      */
getSubjectForDisplay(Context context, String badgeText, String filteredSubject)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