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