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