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