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