Conversation.java revision f8b613c1103952ae5cc1f8a5e6ae60aa62d73dec
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        if (cursor instanceof ConversationCursor) {
423            final byte[] blob = ((ConversationCursor) cursor).getCachedBlob(
424                    UIProvider.CONVERSATION_INFO_COLUMN);
425            if (blob != null && blob.length > 0) {
426                return ConversationInfo.fromBlob(blob);
427            }
428        }
429
430        final String key = UIProvider.ConversationCursorCommand.COMMAND_GET_CONVERSATION_INFO;
431        if (sConversationInfoRequest.isEmpty()) {
432            sConversationInfoRequest.putBoolean(key, true);
433            sConversationInfoRequest.putInt(
434                    UIProvider.ConversationCursorCommand.COMMAND_KEY_OPTIONS,
435                    UIProvider.ConversationCursorCommand.OPTION_MOVE_POSITION);
436        }
437        final Bundle response = cursor.respond(sConversationInfoRequest);
438        if (response.containsKey(key)) {
439            ci = response.getParcelable(key);
440        } else {
441            // legacy fallback
442            ci = ConversationInfo.fromBlob(cursor.getBlob(UIProvider.CONVERSATION_INFO_COLUMN));
443        }
444        return ci;
445    }
446
447    private static FolderList readRawFolders(Cursor cursor) {
448        final FolderList fl;
449
450        if (cursor instanceof ConversationCursor) {
451            final byte[] blob = ((ConversationCursor) cursor).getCachedBlob(
452                    UIProvider.CONVERSATION_RAW_FOLDERS_COLUMN);
453            if (blob != null && blob.length > 0) {
454                return FolderList.fromBlob(blob);
455            }
456        }
457
458        final String key = UIProvider.ConversationCursorCommand.COMMAND_GET_RAW_FOLDERS;
459        if (sRawFoldersRequest.isEmpty()) {
460            sRawFoldersRequest.putBoolean(key, true);
461            sRawFoldersRequest.putInt(
462                    UIProvider.ConversationCursorCommand.COMMAND_KEY_OPTIONS,
463                    UIProvider.ConversationCursorCommand.OPTION_MOVE_POSITION);
464        }
465        final Bundle response = cursor.respond(sRawFoldersRequest);
466        if (response.containsKey(key)) {
467            fl = response.getParcelable(key);
468        } else {
469            // legacy fallback
470            fl = FolderList.fromBlob(
471                    cursor.getBlob(UIProvider.CONVERSATION_RAW_FOLDERS_COLUMN));
472        }
473        return fl;
474    }
475
476    /**
477     * Apply any column values from the given {@link ContentValues} (where column names are the
478     * keys) to this conversation.
479     *
480     */
481    public void applyCachedValues(ContentValues values) {
482        if (values == null) {
483            return;
484        }
485        for (String key : values.keySet()) {
486            final Object val = values.get(key);
487            LogUtils.i(LOG_TAG, "Conversation: applying cached value to col=%s val=%s", key,
488                    val);
489            if (ConversationColumns.READ.equals(key)) {
490                read = (Integer) val != 0;
491            } else if (ConversationColumns.CONVERSATION_INFO.equals(key)) {
492                conversationInfo = ConversationInfo.fromBlob((byte[]) val);
493            } else if (ConversationColumns.FLAGS.equals(key)) {
494                convFlags = (Integer) val;
495            } else if (ConversationColumns.STARRED.equals(key)) {
496                starred = (Integer) val != 0;
497            } else if (ConversationColumns.SEEN.equals(key)) {
498                seen = (Integer) val != 0;
499            } else if (ConversationColumns.RAW_FOLDERS.equals(key)) {
500                rawFolders = FolderList.fromBlob((byte[]) val);
501            } else if (ConversationColumns.VIEWED.equals(key)) {
502                // ignore. this is not read from the cursor, either.
503            } else {
504                LogUtils.e(LOG_TAG, new UnsupportedOperationException(),
505                        "unsupported cached conv value in col=%s", key);
506            }
507        }
508    }
509
510    /**
511     * Get the <strong>immutable</strong> list of {@link Folder}s for this conversation. To modify
512     * this list, make a new {@link FolderList} and use {@link #setRawFolders(FolderList)}.
513     *
514     * @return <strong>Immutable</strong> list of {@link Folder}s.
515     */
516    public List<Folder> getRawFolders() {
517        return rawFolders.folders;
518    }
519
520    public void setRawFolders(FolderList folders) {
521        clearCachedFolders();
522        rawFolders = folders;
523    }
524
525    private void clearCachedFolders() {
526        cachedDisplayableFolders = null;
527    }
528
529    public ArrayList<Folder> getRawFoldersForDisplay(final Uri ignoreFolderUri,
530            final int ignoreFolderType) {
531        if (cachedDisplayableFolders == null) {
532            cachedDisplayableFolders = new ArrayList<Folder>();
533            for (Folder folder : rawFolders.folders) {
534                // skip the ignoreFolder
535                if (ignoreFolderUri != null && ignoreFolderUri.equals(folder.uri)) {
536                    continue;
537                }
538                // Skip the ignoreFolderType
539                if (ignoreFolderType >= 0 && folder.isType(ignoreFolderType)) {
540                    continue;
541                }
542                cachedDisplayableFolders.add(folder);
543            }
544        }
545        return cachedDisplayableFolders;
546    }
547
548    @Override
549    public boolean equals(Object o) {
550        if (o instanceof Conversation) {
551            Conversation conv = (Conversation) o;
552            return conv.uri.equals(uri);
553        }
554        return false;
555    }
556
557    @Override
558    public int hashCode() {
559        return uri.hashCode();
560    }
561
562    /**
563     * Get if this conversation is marked as high priority.
564     */
565    public boolean isImportant() {
566        return priority == UIProvider.ConversationPriority.IMPORTANT;
567    }
568
569    /**
570     * Get if this conversation is mostly dead
571     */
572    public boolean isMostlyDead() {
573        return (convFlags & FLAG_MOSTLY_DEAD) != 0;
574    }
575
576    /**
577     * Returns true if the URI of the conversation specified as the needle was
578     * found in the collection of conversations specified as the haystack. False
579     * otherwise. This method is safe to call with null arguments.
580     *
581     * @param haystack
582     * @param needle
583     * @return true if the needle was found in the haystack, false otherwise.
584     */
585    public final static boolean contains(Collection<Conversation> haystack, Conversation needle) {
586        // If the haystack is empty, it cannot contain anything.
587        if (haystack == null || haystack.size() <= 0) {
588            return false;
589        }
590        // The null folder exists everywhere.
591        if (needle == null) {
592            return true;
593        }
594        final long toFind = needle.id;
595        for (final Conversation c : haystack) {
596            if (toFind == c.id) {
597                return true;
598            }
599        }
600        return false;
601    }
602
603    /**
604     * Returns a collection of a single conversation. This method always returns
605     * a valid collection even if the input conversation is null.
606     *
607     * @param in a conversation, possibly null.
608     * @return a collection of the conversation.
609     */
610    public static Collection<Conversation> listOf(Conversation in) {
611        final Collection<Conversation> target = (in == null) ? EMPTY : ImmutableList.of(in);
612        return target;
613    }
614
615    /**
616     * Get the snippet for this conversation. Masks that it may come from
617     * conversation info or the original deprecated snippet string.
618     */
619    public String getSnippet() {
620        return conversationInfo != null && !TextUtils.isEmpty(conversationInfo.firstSnippet) ?
621                conversationInfo.firstSnippet : snippet;
622    }
623
624    /**
625     * Get the number of messages for this conversation.
626     */
627    public int getNumMessages() {
628        return conversationInfo != null ? conversationInfo.messageCount : numMessages;
629    }
630
631    /**
632     * Get the number of drafts for this conversation.
633     */
634    public int numDrafts() {
635        return conversationInfo != null ? conversationInfo.draftCount : numDrafts;
636    }
637
638    public boolean isViewed() {
639        return viewed;
640    }
641
642    public void markViewed() {
643        viewed = true;
644    }
645
646    public String getBaseUri(String defaultValue) {
647        return conversationBaseUri != null ? conversationBaseUri.toString() : defaultValue;
648    }
649
650    public int getAttachmentsCount() {
651        return getAttachments().size();
652    }
653
654    public ArrayList<String> getAttachments() {
655        return Lists.newArrayList();
656    }
657
658    /**
659     * Create a human-readable string of all the conversations
660     * @param collection Any collection of conversations
661     * @return string with a human readable representation of the conversations.
662     */
663    public static String toString(Collection<Conversation> collection) {
664        final StringBuilder out = new StringBuilder(collection.size() + " conversations:");
665        int count = 0;
666        for (final Conversation c : collection) {
667            count++;
668            // Indent the conversations to make them easy to read in debug
669            // output.
670            out.append("      " + count + ": " + c.toString() + "\n");
671        }
672        return out.toString();
673    }
674
675    /**
676     * Returns an empty string if the specified string is null
677     */
678    private static String emptyIfNull(String in) {
679        return in != null ? in : EMPTY_STRING;
680    }
681
682    /**
683     * Get the properly formatted subject and snippet string for display a
684     * conversation.
685     *
686     * @param context
687     * @param filteredSubject
688     * @param snippet
689     */
690    public static String getSubjectAndSnippetForDisplay(Context context,
691            String filteredSubject, String snippet) {
692        if (sSubjectAndSnippet == null) {
693            sSubjectAndSnippet = context.getString(R.string.subject_and_snippet);
694        }
695        if (TextUtils.isEmpty(filteredSubject) && TextUtils.isEmpty(snippet)) {
696            return "";
697        } else if (TextUtils.isEmpty(filteredSubject)) {
698            return snippet;
699        } else if (TextUtils.isEmpty(snippet)) {
700            return filteredSubject;
701        }
702
703        return String.format(sSubjectAndSnippet, filteredSubject, snippet);
704    }
705
706    /**
707     * Public object that knows how to construct Conversation given Cursors. This is not used by
708     * {@link ConversationCursor} or {@link ConversationCursorLoader}.
709     */
710    public static final CursorCreator<Conversation> FACTORY = new CursorCreator<Conversation>() {
711        @Override
712        public Conversation createFromCursor(final Cursor c) {
713            return new Conversation(c);
714        }
715
716        @Override
717        public String toString() {
718            return "Conversation CursorCreator";
719        }
720    };
721}
722