Conversation.java revision 351ad4e87e0b0b98df9ca88266a8a63541dc5a81
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;
35
36public class Conversation implements Parcelable {
37    public static final int NO_POSITION = -1;
38
39    private static final String EMPTY_STRING = "";
40
41    /**
42     * @see BaseColumns#_ID
43     */
44    public long id;
45    /**
46     * @see UIProvider.ConversationColumns#URI
47     */
48    public Uri uri;
49    /**
50     * @see UIProvider.ConversationColumns#SUBJECT
51     */
52    public String subject;
53    /**
54     * @see UIProvider.ConversationColumns#DATE_RECEIVED_MS
55     */
56    public long dateMs;
57    /**
58     * @see UIProvider.ConversationColumns#SNIPPET
59     */
60    @Deprecated
61    public String snippet;
62    /**
63     * @see UIProvider.ConversationColumns#HAS_ATTACHMENTS
64     */
65    public boolean hasAttachments;
66    /**
67     * @see UIProvider.ConversationColumns#MESSAGE_LIST_URI
68     */
69    public Uri messageListUri;
70    /**
71     * @see UIProvider.ConversationColumns#SENDER_INFO
72     */
73    @Deprecated
74    public String senders;
75    /**
76     * @see UIProvider.ConversationColumns#NUM_MESSAGES
77     */
78    private int numMessages;
79    /**
80     * @see UIProvider.ConversationColumns#NUM_DRAFTS
81     */
82    private int numDrafts;
83    /**
84     * @see UIProvider.ConversationColumns#SENDING_STATE
85     */
86    public int sendingState;
87    /**
88     * @see UIProvider.ConversationColumns#PRIORITY
89     */
90    public int priority;
91    /**
92     * @see UIProvider.ConversationColumns#READ
93     */
94    public boolean read;
95    /**
96     * @see UIProvider.ConversationColumns#STARRED
97     */
98    public boolean starred;
99    /**
100     * @see UIProvider.ConversationColumns#RAW_FOLDERS
101     */
102    private String rawFolders;
103    /**
104     * @see UIProvider.ConversationColumns#FLAGS
105     */
106    public int convFlags;
107    /**
108     * @see UIProvider.ConversationColumns#PERSONAL_LEVEL
109     */
110    public int personalLevel;
111    /**
112     * @see UIProvider.ConversationColumns#SPAM
113     */
114    public boolean spam;
115    /**
116     * @see UIProvider.ConversationColumns#MUTED
117     */
118    public boolean muted;
119    /**
120     * @see UIProvider.ConversationColumns#PHISHING
121     */
122    public boolean phishing;
123    /**
124     * @see UIProvider.ConversationColumns#COLOR
125     */
126    public int color;
127    /**
128     * @see UIProvider.ConversationColumns#ACCOUNT_URI
129     */
130    public Uri accountUri;
131    /**
132     * @see UIProvider.ConversationColumns#CONVERSATION_INFO
133     */
134    public ConversationInfo conversationInfo;
135    /**
136     * @see UIProvider.ConversationColumns#CONVERSATION_BASE_URI
137     */
138    public Uri conversationBaseUri;
139    /**
140     * @see UIProvider.ConversationColumns#REMOTE
141     */
142    public boolean isRemote;
143
144    // Used within the UI to indicate the adapter position of this conversation
145    public transient int position;
146    // Used within the UI to indicate that a Conversation should be removed from
147    // the ConversationCursor when executing an update, e.g. the the
148    // Conversation is no longer in the ConversationList for the current folder,
149    // that is it's now in some other folder(s)
150    public transient boolean localDeleteOnUpdate;
151
152    private transient boolean viewed;
153
154    private ArrayList<Folder> cachedRawFolders;
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.writeString(rawFolders);
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.readString();
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 = cursor.getString(UIProvider.CONVERSATION_RAW_FOLDERS_COLUMN);
284            convFlags = cursor.getInt(UIProvider.CONVERSATION_FLAGS_COLUMN);
285            personalLevel = cursor.getInt(UIProvider.CONVERSATION_PERSONAL_LEVEL_COLUMN);
286            spam = cursor.getInt(UIProvider.CONVERSATION_IS_SPAM_COLUMN) != 0;
287            phishing = cursor.getInt(UIProvider.CONVERSATION_IS_PHISHING_COLUMN) != 0;
288            muted = cursor.getInt(UIProvider.CONVERSATION_MUTED_COLUMN) != 0;
289            color = cursor.getInt(UIProvider.CONVERSATION_COLOR_COLUMN);
290            String account = cursor.getString(UIProvider.CONVERSATION_ACCOUNT_URI_COLUMN);
291            accountUri = !TextUtils.isEmpty(account) ? Uri.parse(account) : null;
292            position = NO_POSITION;
293            localDeleteOnUpdate = false;
294            conversationInfo = ConversationInfo.fromBlob(
295                    cursor.getBlob(UIProvider.CONVERSATION_INFO_COLUMN));
296            final String conversationBase =
297                    cursor.getString(UIProvider.CONVERSATION_BASE_URI_COLUMN);
298            conversationBaseUri = !TextUtils.isEmpty(conversationBase) ?
299                    Uri.parse(conversationBase) : null;
300            if (conversationInfo == null) {
301                snippet = cursor.getString(UIProvider.CONVERSATION_SNIPPET_COLUMN);
302                senders = emptyIfNull(cursor.getString(UIProvider.CONVERSATION_SENDER_INFO_COLUMN));
303                numMessages = cursor.getInt(UIProvider.CONVERSATION_NUM_MESSAGES_COLUMN);
304                numDrafts = cursor.getInt(UIProvider.CONVERSATION_NUM_DRAFTS_COLUMN);
305            }
306            isRemote = cursor.getInt(UIProvider.CONVERSATION_REMOTE_COLUMN) != 0;
307        }
308    }
309
310    public Conversation() {
311    }
312
313    public static Conversation create(long id, Uri uri, String subject, long dateMs,
314            String snippet, boolean hasAttachment, Uri messageListUri, String senders,
315            int numMessages, int numDrafts, int sendingState, int priority, boolean read,
316            boolean starred, String rawFolders, int convFlags, int personalLevel, boolean spam,
317            boolean phishing, boolean muted, Uri accountUri, ConversationInfo conversationInfo,
318            Uri conversationBase, boolean isRemote) {
319
320        final Conversation conversation = new Conversation();
321
322        conversation.id = id;
323        conversation.uri = uri;
324        conversation.subject = subject;
325        conversation.dateMs = dateMs;
326        conversation.snippet = snippet;
327        conversation.hasAttachments = hasAttachment;
328        conversation.messageListUri = messageListUri;
329        conversation.senders = emptyIfNull(senders);
330        conversation.numMessages = numMessages;
331        conversation.numDrafts = numDrafts;
332        conversation.sendingState = sendingState;
333        conversation.priority = priority;
334        conversation.read = read;
335        conversation.starred = starred;
336        conversation.rawFolders = rawFolders;
337        conversation.convFlags = convFlags;
338        conversation.personalLevel = personalLevel;
339        conversation.spam = spam;
340        conversation.phishing = phishing;
341        conversation.muted = muted;
342        conversation.color = 0;
343        conversation.accountUri = accountUri;
344        conversation.conversationInfo = conversationInfo;
345        conversation.conversationBaseUri = conversationBase;
346        conversation.isRemote = isRemote;
347        return conversation;
348    }
349
350    public ArrayList<Folder> getRawFolders() {
351        if (cachedRawFolders == null) {
352            // Create cached folders.
353            if (!TextUtils.isEmpty(rawFolders)) {
354                cachedRawFolders = Folder.getFoldersArray(rawFolders);
355            } else {
356                return new ArrayList<Folder>();
357            }
358        }
359        return cachedRawFolders;
360    }
361
362    public void setRawFolders(String raw) {
363        clearCachedFolders();
364        rawFolders = raw;
365    }
366
367    public String getRawFoldersString() {
368        return rawFolders;
369    }
370
371    private void clearCachedFolders() {
372        cachedRawFolders = null;
373        cachedDisplayableFolders = null;
374    }
375
376    public ArrayList<Folder> getRawFoldersForDisplay(Folder ignoreFolder) {
377        ArrayList<Folder> folders = getRawFolders();
378        if (cachedDisplayableFolders == null) {
379            cachedDisplayableFolders = new ArrayList<Folder>();
380            for (Folder folder : folders) {
381                // skip the ignoreFolder
382                if (ignoreFolder != null && ignoreFolder.equals(folder)) {
383                    continue;
384                }
385                cachedDisplayableFolders.add(folder);
386            }
387        }
388        return cachedDisplayableFolders;
389    }
390
391    @Override
392    public boolean equals(Object o) {
393        if (o instanceof Conversation) {
394            Conversation conv = (Conversation) o;
395            return conv.uri.equals(uri);
396        }
397        return false;
398    }
399
400    @Override
401    public int hashCode() {
402        return uri.hashCode();
403    }
404
405    /**
406     * Get if this conversation is marked as high priority.
407     */
408    public boolean isImportant() {
409        return priority == UIProvider.ConversationPriority.IMPORTANT;
410    }
411
412    /**
413     * Get if this conversation is mostly dead
414     */
415    public boolean isMostlyDead() {
416        return (convFlags & FLAG_MOSTLY_DEAD) != 0;
417    }
418
419    /**
420     * Returns true if the URI of the conversation specified as the needle was
421     * found in the collection of conversations specified as the haystack. False
422     * otherwise. This method is safe to call with null arguments.
423     *
424     * @param haystack
425     * @param needle
426     * @return true if the needle was found in the haystack, false otherwise.
427     */
428    public final static boolean contains(Collection<Conversation> haystack, Conversation needle) {
429        // If the haystack is empty, it cannot contain anything.
430        if (haystack == null || haystack.size() <= 0) {
431            return false;
432        }
433        // The null folder exists everywhere.
434        if (needle == null) {
435            return true;
436        }
437        final long toFind = needle.id;
438        for (final Conversation c : haystack) {
439            if (toFind == c.id) {
440                return true;
441            }
442        }
443        return false;
444    }
445
446    /**
447     * Returns a collection of a single conversation. This method always returns
448     * a valid collection even if the input conversation is null.
449     *
450     * @param in a conversation, possibly null.
451     * @return a collection of the conversation.
452     */
453    public static Collection<Conversation> listOf(Conversation in) {
454        final Collection<Conversation> target = (in == null) ? EMPTY : ImmutableList.of(in);
455        return target;
456    }
457
458    /**
459     * Get the snippet for this conversation. Masks that it may come from
460     * conversation info or the original deprecated snippet string.
461     */
462    public String getSnippet() {
463        return conversationInfo != null && !TextUtils.isEmpty(conversationInfo.firstSnippet) ?
464                conversationInfo.firstSnippet : snippet;
465    }
466
467    public String getSenders(Context context) {
468        if (conversationInfo != null) {
469            ArrayList<String> senders = new ArrayList<String>();
470            for (MessageInfo m : this.conversationInfo.messageInfos) {
471                senders.add(m.sender);
472            }
473            return TextUtils.join(getSendersDelimeter(context), senders);
474        } else {
475            return senders;
476        }
477    }
478
479    private String getSendersDelimeter(Context context) {
480        if (sSendersDelimeter == null) {
481            sSendersDelimeter = context.getResources().getString(R.string.senders_split_token);
482        }
483        return sSendersDelimeter;
484    }
485
486    /**
487     * Get the number of messages for this conversation.
488     */
489    public int getNumMessages() {
490        return conversationInfo != null ? conversationInfo.messageCount : numMessages;
491    }
492
493    /**
494     * Get the number of drafts for this conversation.
495     */
496    public int numDrafts() {
497        return conversationInfo != null ? conversationInfo.draftCount : numDrafts;
498    }
499
500    public boolean isViewed() {
501        return viewed;
502    }
503
504    public void markViewed() {
505        viewed = true;
506    }
507
508    /**
509     * Create a human-readable string of all the conversations
510     * @param collection Any collection of conversations
511     * @return string with a human readable representation of the conversations.
512     */
513    public static String toString(Collection<Conversation> collection) {
514        final StringBuilder out = new StringBuilder(collection.size() + " conversations:");
515        int count = 0;
516        for (final Conversation c : collection) {
517            count++;
518            // Indent the conversations to make them easy to read in debug
519            // output.
520            out.append("      " + count + ": " + c.toString() + "\n");
521        }
522        return out.toString();
523    }
524
525    /**
526     * Returns an empty string if the specified string is null
527     */
528    private static String emptyIfNull(String in) {
529        return in != null ? in : EMPTY_STRING;
530    }
531
532    /**
533     * Get the properly formatted subject and snippet string for display a conversation.
534     */
535    public static SpannableStringBuilder getSubjectAndSnippetForDisplay(Context context,
536            String filteredSubject, String snippet) {
537        if (sSubjectAndSnippet == null) {
538            sSubjectAndSnippet = context.getString(R.string.subject_and_snippet);
539        }
540        return new SpannableStringBuilder((!TextUtils.isEmpty(snippet)) ?
541                String.format(sSubjectAndSnippet, filteredSubject, snippet)
542                : filteredSubject);
543    }
544}
545