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