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