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