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