Conversation.java revision 1bca265b7a8a3f9ea08e0ae51eeb145f0883a266
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.VIEWED.equals(key)) {
430                // ignore. this is not read from the cursor, either.
431            } else {
432                LogUtils.e(LOG_TAG, new UnsupportedOperationException(),
433                        "unsupported cached conv value in col=%s", key);
434            }
435        }
436    }
437
438    /**
439     * Get the <strong>immutable</strong> list of {@link Folder}s for this conversation. To modify
440     * this list, make a new {@link FolderList} and use {@link #setRawFolders(FolderList)}.
441     *
442     * @return <strong>Immutable</strong> list of {@link Folder}s.
443     */
444    public List<Folder> getRawFolders() {
445        return rawFolders.folders;
446    }
447
448    public void setRawFolders(FolderList folders) {
449        clearCachedFolders();
450        rawFolders = folders;
451    }
452
453    private void clearCachedFolders() {
454        cachedDisplayableFolders = null;
455    }
456
457    public ArrayList<Folder> getRawFoldersForDisplay(Folder ignoreFolder) {
458        if (cachedDisplayableFolders == null) {
459            cachedDisplayableFolders = new ArrayList<Folder>();
460            for (Folder folder : rawFolders.folders) {
461                // skip the ignoreFolder
462                if (ignoreFolder != null && ignoreFolder.equals(folder)) {
463                    continue;
464                }
465                cachedDisplayableFolders.add(folder);
466            }
467        }
468        return cachedDisplayableFolders;
469    }
470
471    @Override
472    public boolean equals(Object o) {
473        if (o instanceof Conversation) {
474            Conversation conv = (Conversation) o;
475            return conv.uri.equals(uri);
476        }
477        return false;
478    }
479
480    @Override
481    public int hashCode() {
482        return uri.hashCode();
483    }
484
485    /**
486     * Get if this conversation is marked as high priority.
487     */
488    public boolean isImportant() {
489        return priority == UIProvider.ConversationPriority.IMPORTANT;
490    }
491
492    /**
493     * Get if this conversation is mostly dead
494     */
495    public boolean isMostlyDead() {
496        return (convFlags & FLAG_MOSTLY_DEAD) != 0;
497    }
498
499    /**
500     * Returns true if the URI of the conversation specified as the needle was
501     * found in the collection of conversations specified as the haystack. False
502     * otherwise. This method is safe to call with null arguments.
503     *
504     * @param haystack
505     * @param needle
506     * @return true if the needle was found in the haystack, false otherwise.
507     */
508    public final static boolean contains(Collection<Conversation> haystack, Conversation needle) {
509        // If the haystack is empty, it cannot contain anything.
510        if (haystack == null || haystack.size() <= 0) {
511            return false;
512        }
513        // The null folder exists everywhere.
514        if (needle == null) {
515            return true;
516        }
517        final long toFind = needle.id;
518        for (final Conversation c : haystack) {
519            if (toFind == c.id) {
520                return true;
521            }
522        }
523        return false;
524    }
525
526    /**
527     * Returns a collection of a single conversation. This method always returns
528     * a valid collection even if the input conversation is null.
529     *
530     * @param in a conversation, possibly null.
531     * @return a collection of the conversation.
532     */
533    public static Collection<Conversation> listOf(Conversation in) {
534        final Collection<Conversation> target = (in == null) ? EMPTY : ImmutableList.of(in);
535        return target;
536    }
537
538    /**
539     * Get the snippet for this conversation. Masks that it may come from
540     * conversation info or the original deprecated snippet string.
541     */
542    public String getSnippet() {
543        return conversationInfo != null && !TextUtils.isEmpty(conversationInfo.firstSnippet) ?
544                conversationInfo.firstSnippet : snippet;
545    }
546
547    public String getSenders(Context context) {
548        if (conversationInfo != null) {
549            ArrayList<String> senders = new ArrayList<String>();
550            for (MessageInfo m : this.conversationInfo.messageInfos) {
551                senders.add(m.sender);
552            }
553            return TextUtils.join(getSendersDelimeter(context), senders);
554        } else {
555            return senders;
556        }
557    }
558
559    private static String getSendersDelimeter(Context context) {
560        if (sSendersDelimeter == null) {
561            sSendersDelimeter = context.getResources().getString(R.string.senders_split_token);
562        }
563        return sSendersDelimeter;
564    }
565
566    /**
567     * Get the number of messages for this conversation.
568     */
569    public int getNumMessages() {
570        return conversationInfo != null ? conversationInfo.messageCount : numMessages;
571    }
572
573    /**
574     * Get the number of drafts for this conversation.
575     */
576    public int numDrafts() {
577        return conversationInfo != null ? conversationInfo.draftCount : numDrafts;
578    }
579
580    public boolean isViewed() {
581        return viewed;
582    }
583
584    public void markViewed() {
585        viewed = true;
586    }
587
588    public String getBaseUri(String defaultValue) {
589        return conversationBaseUri != null ? conversationBaseUri.toString() : defaultValue;
590    }
591
592    /**
593     * Create a human-readable string of all the conversations
594     * @param collection Any collection of conversations
595     * @return string with a human readable representation of the conversations.
596     */
597    public static String toString(Collection<Conversation> collection) {
598        final StringBuilder out = new StringBuilder(collection.size() + " conversations:");
599        int count = 0;
600        for (final Conversation c : collection) {
601            count++;
602            // Indent the conversations to make them easy to read in debug
603            // output.
604            out.append("      " + count + ": " + c.toString() + "\n");
605        }
606        return out.toString();
607    }
608
609    /**
610     * Returns an empty string if the specified string is null
611     */
612    private static String emptyIfNull(String in) {
613        return in != null ? in : EMPTY_STRING;
614    }
615
616    /**
617     * Get the properly formatted subject and snippet string for display a
618     * conversation.
619     *
620     * @param context
621     * @param filteredSubject
622     * @param snippet
623     */
624    public static String getSubjectAndSnippetForDisplay(Context context,
625            String filteredSubject, String snippet) {
626        if (sSubjectAndSnippet == null) {
627            sSubjectAndSnippet = context.getString(R.string.subject_and_snippet);
628        }
629        if (TextUtils.isEmpty(filteredSubject) && TextUtils.isEmpty(snippet)) {
630            return "";
631        } else if (TextUtils.isEmpty(filteredSubject)) {
632            return snippet;
633        } else if (TextUtils.isEmpty(snippet)) {
634            return filteredSubject;
635        }
636
637        return String.format(sSubjectAndSnippet, filteredSubject, snippet);
638    }
639}
640