Conversation.java revision 817d6658562f7a2bbbc8255b2e10ea3ff1864e20
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.Context;
20import android.database.Cursor;
21import android.net.Uri;
22import android.os.Parcel;
23import android.os.Parcelable;
24import android.provider.BaseColumns;
25import android.text.TextUtils;
26
27import com.android.mail.R;
28import com.android.mail.providers.UIProvider.ConversationColumns;
29import com.android.mail.utils.LogTag;
30import com.android.mail.utils.LogUtils;
31import com.google.common.collect.ImmutableList;
32
33import java.util.ArrayList;
34import java.util.Collection;
35import java.util.Collections;
36import java.util.List;
37
38public class Conversation implements Parcelable {
39    public static final int NO_POSITION = -1;
40
41    private static final String LOG_TAG = LogTag.getLogTag();
42
43    private static final String EMPTY_STRING = "";
44
45    /**
46     * @see BaseColumns#_ID
47     */
48    public long id;
49    /**
50     * @see UIProvider.ConversationColumns#URI
51     */
52    public Uri uri;
53    /**
54     * @see UIProvider.ConversationColumns#SUBJECT
55     */
56    public String subject;
57    /**
58     * @see UIProvider.ConversationColumns#DATE_RECEIVED_MS
59     */
60    public long dateMs;
61    /**
62     * @see UIProvider.ConversationColumns#SNIPPET
63     */
64    @Deprecated
65    public String snippet;
66    /**
67     * @see UIProvider.ConversationColumns#HAS_ATTACHMENTS
68     */
69    public boolean hasAttachments;
70    /**
71     * @see UIProvider.ConversationColumns#MESSAGE_LIST_URI
72     */
73    public Uri messageListUri;
74    /**
75     * @see UIProvider.ConversationColumns#SENDER_INFO
76     */
77    @Deprecated
78    public String senders;
79    /**
80     * @see UIProvider.ConversationColumns#NUM_MESSAGES
81     */
82    private int numMessages;
83    /**
84     * @see UIProvider.ConversationColumns#NUM_DRAFTS
85     */
86    private int numDrafts;
87    /**
88     * @see UIProvider.ConversationColumns#SENDING_STATE
89     */
90    public int sendingState;
91    /**
92     * @see UIProvider.ConversationColumns#PRIORITY
93     */
94    public int priority;
95    /**
96     * @see UIProvider.ConversationColumns#READ
97     */
98    public boolean read;
99    /**
100     * @see UIProvider.ConversationColumns#SEEN
101     */
102    public boolean seen;
103    /**
104     * @see UIProvider.ConversationColumns#STARRED
105     */
106    public boolean starred;
107    /**
108     * @see UIProvider.ConversationColumns#RAW_FOLDERS
109     */
110    private FolderList rawFolders;
111    /**
112     * @see UIProvider.ConversationColumns#FLAGS
113     */
114    public int convFlags;
115    /**
116     * @see UIProvider.ConversationColumns#PERSONAL_LEVEL
117     */
118    public int personalLevel;
119    /**
120     * @see UIProvider.ConversationColumns#SPAM
121     */
122    public boolean spam;
123    /**
124     * @see UIProvider.ConversationColumns#MUTED
125     */
126    public boolean muted;
127    /**
128     * @see UIProvider.ConversationColumns#PHISHING
129     */
130    public boolean phishing;
131    /**
132     * @see UIProvider.ConversationColumns#COLOR
133     */
134    public int color;
135    /**
136     * @see UIProvider.ConversationColumns#ACCOUNT_URI
137     */
138    public Uri accountUri;
139    /**
140     * @see UIProvider.ConversationColumns#CONVERSATION_INFO
141     */
142    public ConversationInfo conversationInfo;
143    /**
144     * @see UIProvider.ConversationColumns#CONVERSATION_BASE_URI
145     */
146    public Uri conversationBaseUri;
147    /**
148     * @see UIProvider.ConversationColumns#REMOTE
149     */
150    public boolean isRemote;
151
152    // Used within the UI to indicate the adapter position of this conversation
153    public transient int position;
154    // Used within the UI to indicate that a Conversation should be removed from
155    // the ConversationCursor when executing an update, e.g. the the
156    // Conversation is no longer in the ConversationList for the current folder,
157    // that is it's now in some other folder(s)
158    public transient boolean localDeleteOnUpdate;
159
160    private transient boolean viewed;
161
162    private ArrayList<Folder> cachedDisplayableFolders;
163
164    private static String sSendersDelimeter;
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() {
330    }
331
332    public static Conversation create(long id, Uri uri, String subject, long dateMs,
333            String snippet, boolean hasAttachment, Uri messageListUri, String senders,
334            int numMessages, int numDrafts, int sendingState, int priority, boolean read,
335            boolean seen, boolean starred, FolderList rawFolders, int convFlags, int personalLevel,
336            boolean spam, boolean phishing, boolean muted, Uri accountUri,
337            ConversationInfo conversationInfo, Uri conversationBase, boolean isRemote) {
338
339        final Conversation conversation = new Conversation();
340
341        conversation.id = id;
342        conversation.uri = uri;
343        conversation.subject = subject;
344        conversation.dateMs = dateMs;
345        conversation.snippet = snippet;
346        conversation.hasAttachments = hasAttachment;
347        conversation.messageListUri = messageListUri;
348        conversation.senders = emptyIfNull(senders);
349        conversation.numMessages = numMessages;
350        conversation.numDrafts = numDrafts;
351        conversation.sendingState = sendingState;
352        conversation.priority = priority;
353        conversation.read = read;
354        conversation.seen = seen;
355        conversation.starred = starred;
356        conversation.rawFolders = rawFolders;
357        conversation.convFlags = convFlags;
358        conversation.personalLevel = personalLevel;
359        conversation.spam = spam;
360        conversation.phishing = phishing;
361        conversation.muted = muted;
362        conversation.color = 0;
363        conversation.accountUri = accountUri;
364        conversation.conversationInfo = conversationInfo;
365        conversation.conversationBaseUri = conversationBase;
366        conversation.isRemote = isRemote;
367        return conversation;
368    }
369
370    /**
371     * Get the <strong>immutable</strong> list of {@link Folder}s for this conversation. To modify
372     * this list, make a new {@link FolderList} and use {@link #setRawFolders(FolderList)}.
373     *
374     * @return <strong>Immutable</strong> list of {@link Folder}s.
375     */
376    public List<Folder> getRawFolders() {
377        return rawFolders.folders;
378    }
379
380    public void setRawFolders(FolderList folders) {
381        clearCachedFolders();
382        rawFolders = folders;
383    }
384
385    private void clearCachedFolders() {
386        cachedDisplayableFolders = null;
387    }
388
389    public ArrayList<Folder> getRawFoldersForDisplay(Folder ignoreFolder) {
390        if (cachedDisplayableFolders == null) {
391            cachedDisplayableFolders = new ArrayList<Folder>();
392            for (Folder folder : rawFolders.folders) {
393                // skip the ignoreFolder
394                if (ignoreFolder != null && ignoreFolder.equals(folder)) {
395                    continue;
396                }
397                cachedDisplayableFolders.add(folder);
398            }
399        }
400        return cachedDisplayableFolders;
401    }
402
403    @Override
404    public boolean equals(Object o) {
405        if (o instanceof Conversation) {
406            Conversation conv = (Conversation) o;
407            return conv.uri.equals(uri);
408        }
409        return false;
410    }
411
412    @Override
413    public int hashCode() {
414        return uri.hashCode();
415    }
416
417    /**
418     * Get if this conversation is marked as high priority.
419     */
420    public boolean isImportant() {
421        return priority == UIProvider.ConversationPriority.IMPORTANT;
422    }
423
424    /**
425     * Get if this conversation is mostly dead
426     */
427    public boolean isMostlyDead() {
428        return (convFlags & FLAG_MOSTLY_DEAD) != 0;
429    }
430
431    /**
432     * Returns true if the URI of the conversation specified as the needle was
433     * found in the collection of conversations specified as the haystack. False
434     * otherwise. This method is safe to call with null arguments.
435     *
436     * @param haystack
437     * @param needle
438     * @return true if the needle was found in the haystack, false otherwise.
439     */
440    public final static boolean contains(Collection<Conversation> haystack, Conversation needle) {
441        // If the haystack is empty, it cannot contain anything.
442        if (haystack == null || haystack.size() <= 0) {
443            return false;
444        }
445        // The null folder exists everywhere.
446        if (needle == null) {
447            return true;
448        }
449        final long toFind = needle.id;
450        for (final Conversation c : haystack) {
451            if (toFind == c.id) {
452                return true;
453            }
454        }
455        return false;
456    }
457
458    /**
459     * Returns a collection of a single conversation. This method always returns
460     * a valid collection even if the input conversation is null.
461     *
462     * @param in a conversation, possibly null.
463     * @return a collection of the conversation.
464     */
465    public static Collection<Conversation> listOf(Conversation in) {
466        final Collection<Conversation> target = (in == null) ? EMPTY : ImmutableList.of(in);
467        return target;
468    }
469
470    /**
471     * Get the snippet for this conversation. Masks that it may come from
472     * conversation info or the original deprecated snippet string.
473     */
474    public String getSnippet() {
475        return conversationInfo != null && !TextUtils.isEmpty(conversationInfo.firstSnippet) ?
476                conversationInfo.firstSnippet : snippet;
477    }
478
479    public String getSenders(Context context) {
480        if (conversationInfo != null) {
481            ArrayList<String> senders = new ArrayList<String>();
482            for (MessageInfo m : this.conversationInfo.messageInfos) {
483                senders.add(m.sender);
484            }
485            return TextUtils.join(getSendersDelimeter(context), senders);
486        } else {
487            return senders;
488        }
489    }
490
491    private String getSendersDelimeter(Context context) {
492        if (sSendersDelimeter == null) {
493            sSendersDelimeter = context.getResources().getString(R.string.senders_split_token);
494        }
495        return sSendersDelimeter;
496    }
497
498    /**
499     * Get the number of messages for this conversation.
500     */
501    public int getNumMessages() {
502        return conversationInfo != null ? conversationInfo.messageCount : numMessages;
503    }
504
505    /**
506     * Get the number of drafts for this conversation.
507     */
508    public int numDrafts() {
509        return conversationInfo != null ? conversationInfo.draftCount : numDrafts;
510    }
511
512    public boolean isViewed() {
513        return viewed;
514    }
515
516    public void markViewed() {
517        viewed = true;
518    }
519
520    public String getBaseUri(String defaultValue) {
521        return conversationBaseUri != null ? conversationBaseUri.toString() : defaultValue;
522    }
523
524    /**
525     * Create a human-readable string of all the conversations
526     * @param collection Any collection of conversations
527     * @return string with a human readable representation of the conversations.
528     */
529    public static String toString(Collection<Conversation> collection) {
530        final StringBuilder out = new StringBuilder(collection.size() + " conversations:");
531        int count = 0;
532        for (final Conversation c : collection) {
533            count++;
534            // Indent the conversations to make them easy to read in debug
535            // output.
536            out.append("      " + count + ": " + c.toString() + "\n");
537        }
538        return out.toString();
539    }
540
541    /**
542     * Returns an empty string if the specified string is null
543     */
544    private static String emptyIfNull(String in) {
545        return in != null ? in : EMPTY_STRING;
546    }
547
548    /**
549     * Get the properly formatted subject and snippet string for display a
550     * conversation.
551     *
552     * @param context
553     * @param filteredSubject
554     * @param snippet
555     */
556    public static String getSubjectAndSnippetForDisplay(Context context,
557            String filteredSubject, String snippet) {
558        if (sSubjectAndSnippet == null) {
559            sSubjectAndSnippet = context.getString(R.string.subject_and_snippet);
560        }
561        return (!TextUtils.isEmpty(snippet)) ?
562                String.format(sSubjectAndSnippet, filteredSubject, snippet)
563                : filteredSubject;
564    }
565}
566