Conversation.java revision 54ed5c67919c7a7a3f26d286de91f196d664b1c8
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.Context;
20import android.database.Cursor;
21import android.net.Uri;
22import android.os.Parcel;
23import android.os.Parcelable;
24import android.provider.BaseColumns;
25import android.text.SpannableStringBuilder;
26import android.text.TextUtils;
27
28import com.android.mail.R;
29import com.android.mail.providers.UIProvider.ConversationColumns;
30import com.google.common.collect.ImmutableList;
31
32import java.util.ArrayList;
33import java.util.Collection;
34import java.util.Collections;
35import java.util.List;
36
37public class Conversation implements Parcelable {
38    public static final int NO_POSITION = -1;
39
40    private static final String EMPTY_STRING = "";
41
42    /**
43     * @see BaseColumns#_ID
44     */
45    public long id;
46    /**
47     * @see UIProvider.ConversationColumns#URI
48     */
49    public Uri uri;
50    /**
51     * @see UIProvider.ConversationColumns#SUBJECT
52     */
53    public String subject;
54    /**
55     * @see UIProvider.ConversationColumns#DATE_RECEIVED_MS
56     */
57    public long dateMs;
58    /**
59     * @see UIProvider.ConversationColumns#SNIPPET
60     */
61    @Deprecated
62    public String snippet;
63    /**
64     * @see UIProvider.ConversationColumns#HAS_ATTACHMENTS
65     */
66    public boolean hasAttachments;
67    /**
68     * @see UIProvider.ConversationColumns#MESSAGE_LIST_URI
69     */
70    public Uri messageListUri;
71    /**
72     * @see UIProvider.ConversationColumns#SENDER_INFO
73     */
74    @Deprecated
75    public String senders;
76    /**
77     * @see UIProvider.ConversationColumns#NUM_MESSAGES
78     */
79    private int numMessages;
80    /**
81     * @see UIProvider.ConversationColumns#NUM_DRAFTS
82     */
83    private int numDrafts;
84    /**
85     * @see UIProvider.ConversationColumns#SENDING_STATE
86     */
87    public int sendingState;
88    /**
89     * @see UIProvider.ConversationColumns#PRIORITY
90     */
91    public int priority;
92    /**
93     * @see UIProvider.ConversationColumns#READ
94     */
95    public boolean read;
96    /**
97     * @see UIProvider.ConversationColumns#SEEN
98     */
99    public boolean seen;
100    /**
101     * @see UIProvider.ConversationColumns#STARRED
102     */
103    public boolean starred;
104    /**
105     * @see UIProvider.ConversationColumns#RAW_FOLDERS
106     */
107    private FolderList rawFolders;
108    /**
109     * @see UIProvider.ConversationColumns#FLAGS
110     */
111    public int convFlags;
112    /**
113     * @see UIProvider.ConversationColumns#PERSONAL_LEVEL
114     */
115    public int personalLevel;
116    /**
117     * @see UIProvider.ConversationColumns#SPAM
118     */
119    public boolean spam;
120    /**
121     * @see UIProvider.ConversationColumns#MUTED
122     */
123    public boolean muted;
124    /**
125     * @see UIProvider.ConversationColumns#PHISHING
126     */
127    public boolean phishing;
128    /**
129     * @see UIProvider.ConversationColumns#COLOR
130     */
131    public int color;
132    /**
133     * @see UIProvider.ConversationColumns#ACCOUNT_URI
134     */
135    public Uri accountUri;
136    /**
137     * @see UIProvider.ConversationColumns#CONVERSATION_INFO
138     */
139    public ConversationInfo conversationInfo;
140    /**
141     * @see UIProvider.ConversationColumns#CONVERSATION_BASE_URI
142     */
143    public Uri conversationBaseUri;
144    /**
145     * @see UIProvider.ConversationColumns#REMOTE
146     */
147    public boolean isRemote;
148
149    // Used within the UI to indicate the adapter position of this conversation
150    public transient int position;
151    // Used within the UI to indicate that a Conversation should be removed from
152    // the ConversationCursor when executing an update, e.g. the the
153    // Conversation is no longer in the ConversationList for the current folder,
154    // that is it's now in some other folder(s)
155    public transient boolean localDeleteOnUpdate;
156
157    private transient boolean viewed;
158
159    private ArrayList<Folder> cachedDisplayableFolders;
160
161    private static String sSendersDelimeter;
162
163    private static String sSubjectAndSnippet;
164
165    // Constituents of convFlags below
166    // Flag indicating that the item has been deleted, but will continue being
167    // shown in the list Delete/Archive of a mostly-dead item will NOT propagate
168    // the delete/archive, but WILL remove the item from the cursor
169    public static final int FLAG_MOSTLY_DEAD = 1 << 0;
170
171    /** An immutable, empty conversation list */
172    public static final Collection<Conversation> EMPTY = Collections.emptyList();
173
174    @Override
175    public int describeContents() {
176        return 0;
177    }
178
179    @Override
180    public void writeToParcel(Parcel dest, int flags) {
181        dest.writeLong(id);
182        dest.writeParcelable(uri, flags);
183        dest.writeString(subject);
184        dest.writeLong(dateMs);
185        dest.writeString(snippet);
186        dest.writeInt(hasAttachments ? 1 : 0);
187        dest.writeParcelable(messageListUri, 0);
188        dest.writeString(senders);
189        dest.writeInt(numMessages);
190        dest.writeInt(numDrafts);
191        dest.writeInt(sendingState);
192        dest.writeInt(priority);
193        dest.writeInt(read ? 1 : 0);
194        dest.writeInt(seen ? 1 : 0);
195        dest.writeInt(starred ? 1 : 0);
196        dest.writeParcelable(rawFolders, 0);
197        dest.writeInt(convFlags);
198        dest.writeInt(personalLevel);
199        dest.writeInt(spam ? 1 : 0);
200        dest.writeInt(phishing ? 1 : 0);
201        dest.writeInt(muted ? 1 : 0);
202        dest.writeInt(color);
203        dest.writeParcelable(accountUri, 0);
204        dest.writeParcelable(conversationInfo, 0);
205        dest.writeParcelable(conversationBaseUri, 0);
206        dest.writeInt(isRemote ? 1 : 0);
207    }
208
209    private Conversation(Parcel in, ClassLoader loader) {
210        id = in.readLong();
211        uri = in.readParcelable(null);
212        subject = in.readString();
213        dateMs = in.readLong();
214        snippet = in.readString();
215        hasAttachments = (in.readInt() != 0);
216        messageListUri = in.readParcelable(null);
217        senders = emptyIfNull(in.readString());
218        numMessages = in.readInt();
219        numDrafts = in.readInt();
220        sendingState = in.readInt();
221        priority = in.readInt();
222        read = (in.readInt() != 0);
223        seen = (in.readInt() != 0);
224        starred = (in.readInt() != 0);
225        rawFolders = in.readParcelable(loader);
226        convFlags = in.readInt();
227        personalLevel = in.readInt();
228        spam = in.readInt() != 0;
229        phishing = in.readInt() != 0;
230        muted = in.readInt() != 0;
231        color = in.readInt();
232        accountUri = in.readParcelable(null);
233        position = NO_POSITION;
234        localDeleteOnUpdate = false;
235        conversationInfo = in.readParcelable(loader);
236        conversationBaseUri = in.readParcelable(null);
237        isRemote = in.readInt() != 0;
238    }
239
240    @Override
241    public String toString() {
242        return "[conversation id=" + id + ", subject =" + subject + "]";
243    }
244
245    public static final ClassLoaderCreator<Conversation> CREATOR =
246            new ClassLoaderCreator<Conversation>() {
247
248        @Override
249        public Conversation createFromParcel(Parcel source) {
250            return new Conversation(source, null);
251        }
252
253        @Override
254        public Conversation createFromParcel(Parcel source, ClassLoader loader) {
255            return new Conversation(source, loader);
256        }
257
258        @Override
259        public Conversation[] newArray(int size) {
260            return new Conversation[size];
261        }
262
263    };
264
265    public static final Uri MOVE_CONVERSATIONS_URI = Uri.parse("content://moveconversations");
266
267    /**
268     * The column that needs to be updated to change the folders for a conversation.
269     */
270    public static final String UPDATE_FOLDER_COLUMN = ConversationColumns.RAW_FOLDERS;
271
272    public Conversation(Cursor cursor) {
273        if (cursor != null) {
274            id = cursor.getLong(UIProvider.CONVERSATION_ID_COLUMN);
275            uri = Uri.parse(cursor.getString(UIProvider.CONVERSATION_URI_COLUMN));
276            dateMs = cursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN);
277            subject = cursor.getString(UIProvider.CONVERSATION_SUBJECT_COLUMN);
278            // Don't allow null subject
279            if (subject == null) {
280                subject = "";
281            }
282            hasAttachments = cursor.getInt(UIProvider.CONVERSATION_HAS_ATTACHMENTS_COLUMN) != 0;
283            String messageList = cursor.getString(UIProvider.CONVERSATION_MESSAGE_LIST_URI_COLUMN);
284            messageListUri = !TextUtils.isEmpty(messageList) ? Uri.parse(messageList) : null;
285            sendingState = cursor.getInt(UIProvider.CONVERSATION_SENDING_STATE_COLUMN);
286            priority = cursor.getInt(UIProvider.CONVERSATION_PRIORITY_COLUMN);
287            read = cursor.getInt(UIProvider.CONVERSATION_READ_COLUMN) != 0;
288            seen = cursor.getInt(UIProvider.CONVERSATION_SEEN_COLUMN) != 0;
289            starred = cursor.getInt(UIProvider.CONVERSATION_STARRED_COLUMN) != 0;
290            rawFolders = FolderList.fromBlob(
291                    cursor.getBlob(UIProvider.CONVERSATION_RAW_FOLDERS_COLUMN));
292            convFlags = cursor.getInt(UIProvider.CONVERSATION_FLAGS_COLUMN);
293            personalLevel = cursor.getInt(UIProvider.CONVERSATION_PERSONAL_LEVEL_COLUMN);
294            spam = cursor.getInt(UIProvider.CONVERSATION_IS_SPAM_COLUMN) != 0;
295            phishing = cursor.getInt(UIProvider.CONVERSATION_IS_PHISHING_COLUMN) != 0;
296            muted = cursor.getInt(UIProvider.CONVERSATION_MUTED_COLUMN) != 0;
297            color = cursor.getInt(UIProvider.CONVERSATION_COLOR_COLUMN);
298            String account = cursor.getString(UIProvider.CONVERSATION_ACCOUNT_URI_COLUMN);
299            accountUri = !TextUtils.isEmpty(account) ? Uri.parse(account) : null;
300            position = NO_POSITION;
301            localDeleteOnUpdate = false;
302            conversationInfo = ConversationInfo.fromBlob(
303                    cursor.getBlob(UIProvider.CONVERSATION_INFO_COLUMN));
304            final String conversationBase =
305                    cursor.getString(UIProvider.CONVERSATION_BASE_URI_COLUMN);
306            conversationBaseUri = !TextUtils.isEmpty(conversationBase) ?
307                    Uri.parse(conversationBase) : null;
308            if (conversationInfo == null) {
309                snippet = cursor.getString(UIProvider.CONVERSATION_SNIPPET_COLUMN);
310                senders = emptyIfNull(cursor.getString(UIProvider.CONVERSATION_SENDER_INFO_COLUMN));
311                numMessages = cursor.getInt(UIProvider.CONVERSATION_NUM_MESSAGES_COLUMN);
312                numDrafts = cursor.getInt(UIProvider.CONVERSATION_NUM_DRAFTS_COLUMN);
313            }
314            isRemote = cursor.getInt(UIProvider.CONVERSATION_REMOTE_COLUMN) != 0;
315        }
316    }
317
318    public Conversation() {
319    }
320
321    public static Conversation create(long id, Uri uri, String subject, long dateMs,
322            String snippet, boolean hasAttachment, Uri messageListUri, String senders,
323            int numMessages, int numDrafts, int sendingState, int priority, boolean read,
324            boolean seen, boolean starred, FolderList rawFolders, int convFlags, int personalLevel,
325            boolean spam, boolean phishing, boolean muted, Uri accountUri,
326            ConversationInfo conversationInfo, Uri conversationBase, boolean isRemote) {
327
328        final Conversation conversation = new Conversation();
329
330        conversation.id = id;
331        conversation.uri = uri;
332        conversation.subject = subject;
333        conversation.dateMs = dateMs;
334        conversation.snippet = snippet;
335        conversation.hasAttachments = hasAttachment;
336        conversation.messageListUri = messageListUri;
337        conversation.senders = emptyIfNull(senders);
338        conversation.numMessages = numMessages;
339        conversation.numDrafts = numDrafts;
340        conversation.sendingState = sendingState;
341        conversation.priority = priority;
342        conversation.read = read;
343        conversation.seen = seen;
344        conversation.starred = starred;
345        conversation.rawFolders = rawFolders;
346        conversation.convFlags = convFlags;
347        conversation.personalLevel = personalLevel;
348        conversation.spam = spam;
349        conversation.phishing = phishing;
350        conversation.muted = muted;
351        conversation.color = 0;
352        conversation.accountUri = accountUri;
353        conversation.conversationInfo = conversationInfo;
354        conversation.conversationBaseUri = conversationBase;
355        conversation.isRemote = isRemote;
356        return conversation;
357    }
358
359    /**
360     * Get the <strong>immutable</strong> list of {@link Folder}s for this conversation. To modify
361     * this list, make a new {@link FolderList} and use {@link #setRawFolders(FolderList)}.
362     *
363     * @return <strong>Immutable</strong> list of {@link Folder}s.
364     */
365    public List<Folder> getRawFolders() {
366        return rawFolders.folders;
367    }
368
369    public void setRawFolders(FolderList folders) {
370        clearCachedFolders();
371        rawFolders = folders;
372    }
373
374    private void clearCachedFolders() {
375        cachedDisplayableFolders = null;
376    }
377
378    public ArrayList<Folder> getRawFoldersForDisplay(Folder ignoreFolder) {
379        if (cachedDisplayableFolders == null) {
380            cachedDisplayableFolders = new ArrayList<Folder>();
381            for (Folder folder : rawFolders.folders) {
382                // skip the ignoreFolder
383                if (ignoreFolder != null && ignoreFolder.equals(folder)) {
384                    continue;
385                }
386                cachedDisplayableFolders.add(folder);
387            }
388        }
389        return cachedDisplayableFolders;
390    }
391
392    @Override
393    public boolean equals(Object o) {
394        if (o instanceof Conversation) {
395            Conversation conv = (Conversation) o;
396            return conv.uri.equals(uri);
397        }
398        return false;
399    }
400
401    @Override
402    public int hashCode() {
403        return uri.hashCode();
404    }
405
406    /**
407     * Get if this conversation is marked as high priority.
408     */
409    public boolean isImportant() {
410        return priority == UIProvider.ConversationPriority.IMPORTANT;
411    }
412
413    /**
414     * Get if this conversation is mostly dead
415     */
416    public boolean isMostlyDead() {
417        return (convFlags & FLAG_MOSTLY_DEAD) != 0;
418    }
419
420    /**
421     * Returns true if the URI of the conversation specified as the needle was
422     * found in the collection of conversations specified as the haystack. False
423     * otherwise. This method is safe to call with null arguments.
424     *
425     * @param haystack
426     * @param needle
427     * @return true if the needle was found in the haystack, false otherwise.
428     */
429    public final static boolean contains(Collection<Conversation> haystack, Conversation needle) {
430        // If the haystack is empty, it cannot contain anything.
431        if (haystack == null || haystack.size() <= 0) {
432            return false;
433        }
434        // The null folder exists everywhere.
435        if (needle == null) {
436            return true;
437        }
438        final long toFind = needle.id;
439        for (final Conversation c : haystack) {
440            if (toFind == c.id) {
441                return true;
442            }
443        }
444        return false;
445    }
446
447    /**
448     * Returns a collection of a single conversation. This method always returns
449     * a valid collection even if the input conversation is null.
450     *
451     * @param in a conversation, possibly null.
452     * @return a collection of the conversation.
453     */
454    public static Collection<Conversation> listOf(Conversation in) {
455        final Collection<Conversation> target = (in == null) ? EMPTY : ImmutableList.of(in);
456        return target;
457    }
458
459    /**
460     * Get the snippet for this conversation. Masks that it may come from
461     * conversation info or the original deprecated snippet string.
462     */
463    public String getSnippet() {
464        return conversationInfo != null && !TextUtils.isEmpty(conversationInfo.firstSnippet) ?
465                conversationInfo.firstSnippet : snippet;
466    }
467
468    public String getSenders(Context context) {
469        if (conversationInfo != null) {
470            ArrayList<String> senders = new ArrayList<String>();
471            for (MessageInfo m : this.conversationInfo.messageInfos) {
472                senders.add(m.sender);
473            }
474            return TextUtils.join(getSendersDelimeter(context), senders);
475        } else {
476            return senders;
477        }
478    }
479
480    private String getSendersDelimeter(Context context) {
481        if (sSendersDelimeter == null) {
482            sSendersDelimeter = context.getResources().getString(R.string.senders_split_token);
483        }
484        return sSendersDelimeter;
485    }
486
487    /**
488     * Get the number of messages for this conversation.
489     */
490    public int getNumMessages() {
491        return conversationInfo != null ? conversationInfo.messageCount : numMessages;
492    }
493
494    /**
495     * Get the number of drafts for this conversation.
496     */
497    public int numDrafts() {
498        return conversationInfo != null ? conversationInfo.draftCount : numDrafts;
499    }
500
501    public boolean isViewed() {
502        return viewed;
503    }
504
505    public void markViewed() {
506        viewed = true;
507    }
508
509    public String getBaseUri(String defaultValue) {
510        return conversationBaseUri != null ? conversationBaseUri.toString() : defaultValue;
511    }
512
513    /**
514     * Create a human-readable string of all the conversations
515     * @param collection Any collection of conversations
516     * @return string with a human readable representation of the conversations.
517     */
518    public static String toString(Collection<Conversation> collection) {
519        final StringBuilder out = new StringBuilder(collection.size() + " conversations:");
520        int count = 0;
521        for (final Conversation c : collection) {
522            count++;
523            // Indent the conversations to make them easy to read in debug
524            // output.
525            out.append("      " + count + ": " + c.toString() + "\n");
526        }
527        return out.toString();
528    }
529
530    /**
531     * Returns an empty string if the specified string is null
532     */
533    private static String emptyIfNull(String in) {
534        return in != null ? in : EMPTY_STRING;
535    }
536
537    /**
538     * Get the properly formatted subject and snippet string for display a
539     * conversation.
540     *
541     * @param context
542     * @param filteredSubject
543     * @param snippet
544     */
545    public static String getSubjectAndSnippetForDisplay(Context context,
546            String filteredSubject, String snippet) {
547        if (sSubjectAndSnippet == null) {
548            sSubjectAndSnippet = context.getString(R.string.subject_and_snippet);
549        }
550        return (!TextUtils.isEmpty(snippet)) ?
551                String.format(sSubjectAndSnippet, filteredSubject, snippet)
552                : filteredSubject;
553    }
554}
555