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