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