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