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