Conversation.java revision 2909b7442130800d6c6c20c1227d65295262f03e
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    private static String sSubjectAndSnippet;
160
161    // Constituents of convFlags below
162    // Flag indicating that the item has been deleted, but will continue being
163    // shown in the list Delete/Archive of a mostly-dead item will NOT propagate
164    // the delete/archive, but WILL remove the item from the cursor
165    public static final int FLAG_MOSTLY_DEAD = 1 << 0;
166
167    /** An immutable, empty conversation list */
168    public static final Collection<Conversation> EMPTY = Collections.emptyList();
169
170    @Override
171    public int describeContents() {
172        return 0;
173    }
174
175    @Override
176    public void writeToParcel(Parcel dest, int flags) {
177        dest.writeLong(id);
178        dest.writeParcelable(uri, flags);
179        dest.writeString(subject);
180        dest.writeLong(dateMs);
181        dest.writeString(snippet);
182        dest.writeInt(hasAttachments ? 1 : 0);
183        dest.writeParcelable(messageListUri, 0);
184        dest.writeString(senders);
185        dest.writeInt(numMessages);
186        dest.writeInt(numDrafts);
187        dest.writeInt(sendingState);
188        dest.writeInt(priority);
189        dest.writeInt(read ? 1 : 0);
190        dest.writeInt(starred ? 1 : 0);
191        dest.writeString(rawFolders);
192        dest.writeInt(convFlags);
193        dest.writeInt(personalLevel);
194        dest.writeInt(spam ? 1 : 0);
195        dest.writeInt(phishing ? 1 : 0);
196        dest.writeInt(muted ? 1 : 0);
197        dest.writeInt(color);
198        dest.writeParcelable(accountUri, 0);
199        dest.writeString(ConversationInfo.toString(conversationInfo));
200        dest.writeParcelable(conversationBaseUri, 0);
201        dest.writeInt(isRemote ? 1 : 0);
202    }
203
204    private Conversation(Parcel in) {
205        id = in.readLong();
206        uri = in.readParcelable(null);
207        subject = in.readString();
208        dateMs = in.readLong();
209        snippet = in.readString();
210        hasAttachments = (in.readInt() != 0);
211        messageListUri = in.readParcelable(null);
212        senders = emptyIfNull(in.readString());
213        numMessages = in.readInt();
214        numDrafts = in.readInt();
215        sendingState = in.readInt();
216        priority = in.readInt();
217        read = (in.readInt() != 0);
218        starred = (in.readInt() != 0);
219        rawFolders = in.readString();
220        convFlags = in.readInt();
221        personalLevel = in.readInt();
222        spam = in.readInt() != 0;
223        phishing = in.readInt() != 0;
224        muted = in.readInt() != 0;
225        color = in.readInt();
226        accountUri = in.readParcelable(null);
227        position = NO_POSITION;
228        localDeleteOnUpdate = false;
229        conversationInfo = ConversationInfo.fromString(in.readString());
230        conversationBaseUri = in.readParcelable(null);
231        isRemote = in.readInt() != 0;
232    }
233
234    @Override
235    public String toString() {
236        return "[conversation id=" + id + ", subject =" + subject + "]";
237    }
238
239    public static final Creator<Conversation> CREATOR = new Creator<Conversation>() {
240
241        @Override
242        public Conversation createFromParcel(Parcel source) {
243            return new Conversation(source);
244        }
245
246        @Override
247        public Conversation[] newArray(int size) {
248            return new Conversation[size];
249        }
250
251    };
252
253    public static final Uri MOVE_CONVERSATIONS_URI = Uri.parse("content://moveconversations");
254
255    /**
256     * The column that needs to be updated to change the read state of a
257     * conversation.
258     */
259    public static final String UPDATE_FOLDER_COLUMN = ConversationColumns.RAW_FOLDERS;
260
261    public Conversation(Cursor cursor) {
262        if (cursor != null) {
263            id = cursor.getLong(UIProvider.CONVERSATION_ID_COLUMN);
264            uri = Uri.parse(cursor.getString(UIProvider.CONVERSATION_URI_COLUMN));
265            dateMs = cursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN);
266            subject = cursor.getString(UIProvider.CONVERSATION_SUBJECT_COLUMN);
267            // Don't allow null subject
268            if (subject == null) {
269                subject = "";
270            }
271            hasAttachments = cursor.getInt(UIProvider.CONVERSATION_HAS_ATTACHMENTS_COLUMN) != 0;
272            String messageList = cursor.getString(UIProvider.CONVERSATION_MESSAGE_LIST_URI_COLUMN);
273            messageListUri = !TextUtils.isEmpty(messageList) ? Uri.parse(messageList) : null;
274            sendingState = cursor.getInt(UIProvider.CONVERSATION_SENDING_STATE_COLUMN);
275            priority = cursor.getInt(UIProvider.CONVERSATION_PRIORITY_COLUMN);
276            read = cursor.getInt(UIProvider.CONVERSATION_READ_COLUMN) != 0;
277            starred = cursor.getInt(UIProvider.CONVERSATION_STARRED_COLUMN) != 0;
278            rawFolders = cursor.getString(UIProvider.CONVERSATION_RAW_FOLDERS_COLUMN);
279            convFlags = cursor.getInt(UIProvider.CONVERSATION_FLAGS_COLUMN);
280            personalLevel = cursor.getInt(UIProvider.CONVERSATION_PERSONAL_LEVEL_COLUMN);
281            spam = cursor.getInt(UIProvider.CONVERSATION_IS_SPAM_COLUMN) != 0;
282            phishing = cursor.getInt(UIProvider.CONVERSATION_IS_PHISHING_COLUMN) != 0;
283            muted = cursor.getInt(UIProvider.CONVERSATION_MUTED_COLUMN) != 0;
284            color = cursor.getInt(UIProvider.CONVERSATION_COLOR_COLUMN);
285            String account = cursor.getString(UIProvider.CONVERSATION_ACCOUNT_URI_COLUMN);
286            accountUri = !TextUtils.isEmpty(account) ? Uri.parse(account) : null;
287            position = NO_POSITION;
288            localDeleteOnUpdate = false;
289            conversationInfo = ConversationInfo.fromString(cursor
290                    .getString(UIProvider.CONVERSATION_INFO_COLUMN));
291            final String conversationBase =
292                    cursor.getString(UIProvider.CONVERSATION_BASE_URI_COLUMN);
293            conversationBaseUri = !TextUtils.isEmpty(conversationBase) ?
294                    Uri.parse(conversationBase) : null;
295            if (conversationInfo == null) {
296                snippet = cursor.getString(UIProvider.CONVERSATION_SNIPPET_COLUMN);
297                senders = emptyIfNull(cursor.getString(UIProvider.CONVERSATION_SENDER_INFO_COLUMN));
298                numMessages = cursor.getInt(UIProvider.CONVERSATION_NUM_MESSAGES_COLUMN);
299                numDrafts = cursor.getInt(UIProvider.CONVERSATION_NUM_DRAFTS_COLUMN);
300            }
301            isRemote = cursor.getInt(UIProvider.CONVERSATION_REMOTE_COLUMN) != 0;
302        }
303    }
304
305    public Conversation() {
306    }
307
308    public static Conversation create(long id, Uri uri, String subject, long dateMs,
309            String snippet, boolean hasAttachment, Uri messageListUri, String senders,
310            int numMessages, int numDrafts, int sendingState, int priority, boolean read,
311            boolean starred, String rawFolders, int convFlags, int personalLevel, boolean spam,
312            boolean phishing, boolean muted, Uri accountUri, ConversationInfo conversationInfo,
313            Uri conversationBase, boolean isRemote) {
314
315        final Conversation conversation = new Conversation();
316
317        conversation.id = id;
318        conversation.uri = uri;
319        conversation.subject = subject;
320        conversation.dateMs = dateMs;
321        conversation.snippet = snippet;
322        conversation.hasAttachments = hasAttachment;
323        conversation.messageListUri = messageListUri;
324        conversation.senders = emptyIfNull(senders);
325        conversation.numMessages = numMessages;
326        conversation.numDrafts = numDrafts;
327        conversation.sendingState = sendingState;
328        conversation.priority = priority;
329        conversation.read = read;
330        conversation.starred = starred;
331        conversation.rawFolders = rawFolders;
332        conversation.convFlags = convFlags;
333        conversation.personalLevel = personalLevel;
334        conversation.spam = spam;
335        conversation.phishing = phishing;
336        conversation.muted = muted;
337        conversation.color = 0;
338        conversation.accountUri = accountUri;
339        conversation.conversationInfo = conversationInfo;
340        conversation.conversationBaseUri = conversationBase;
341        conversation.isRemote = isRemote;
342        return conversation;
343    }
344
345    public ArrayList<Folder> getRawFolders() {
346        if (cachedRawFolders == null) {
347            // Create cached folders.
348            if (!TextUtils.isEmpty(rawFolders)) {
349                cachedRawFolders = Folder.getFoldersArray(rawFolders);
350            } else {
351                return new ArrayList<Folder>();
352            }
353        }
354        return cachedRawFolders;
355    }
356
357    public void setRawFolders(String raw) {
358        clearCachedFolders();
359        rawFolders = raw;
360    }
361
362    public String getRawFoldersString() {
363        return rawFolders;
364    }
365
366    private void clearCachedFolders() {
367        cachedRawFolders = null;
368        cachedDisplayableFolders = null;
369    }
370
371    public ArrayList<Folder> getRawFoldersForDisplay(Folder ignoreFolder) {
372        ArrayList<Folder> folders = getRawFolders();
373        if (cachedDisplayableFolders == null) {
374            cachedDisplayableFolders = new ArrayList<Folder>();
375            for (Folder folder : folders) {
376                // skip the ignoreFolder
377                if (ignoreFolder != null && ignoreFolder.equals(folder)) {
378                    continue;
379                }
380                cachedDisplayableFolders.add(folder);
381            }
382        }
383        return cachedDisplayableFolders;
384    }
385
386    @Override
387    public boolean equals(Object o) {
388        if (o instanceof Conversation) {
389            Conversation conv = (Conversation) o;
390            return conv.uri.equals(uri);
391        }
392        return false;
393    }
394
395    @Override
396    public int hashCode() {
397        return uri.hashCode();
398    }
399
400    /**
401     * Get if this conversation is marked as high priority.
402     */
403    public boolean isImportant() {
404        return priority == UIProvider.ConversationPriority.IMPORTANT;
405    }
406
407    /**
408     * Get if this conversation is mostly dead
409     */
410    public boolean isMostlyDead() {
411        return (convFlags & FLAG_MOSTLY_DEAD) != 0;
412    }
413
414    /**
415     * Returns true if the URI of the conversation specified as the needle was
416     * found in the collection of conversations specified as the haystack. False
417     * otherwise. This method is safe to call with null arguments.
418     *
419     * @param haystack
420     * @param needle
421     * @return true if the needle was found in the haystack, false otherwise.
422     */
423    public final static boolean contains(Collection<Conversation> haystack, Conversation needle) {
424        // If the haystack is empty, it cannot contain anything.
425        if (haystack == null || haystack.size() <= 0) {
426            return false;
427        }
428        // The null folder exists everywhere.
429        if (needle == null) {
430            return true;
431        }
432        final long toFind = needle.id;
433        for (final Conversation c : haystack) {
434            if (toFind == c.id) {
435                return true;
436            }
437        }
438        return false;
439    }
440
441    /**
442     * Returns a collection of a single conversation. This method always returns
443     * a valid collection even if the input conversation is null.
444     *
445     * @param in a conversation, possibly null.
446     * @return a collection of the conversation.
447     */
448    public static Collection<Conversation> listOf(Conversation in) {
449        final Collection<Conversation> target = (in == null) ? EMPTY : ImmutableList.of(in);
450        return target;
451    }
452
453    /**
454     * Get the snippet for this conversation. Masks that it may come from
455     * conversation info or the original deprecated snippet string.
456     */
457    public String getSnippet() {
458        return conversationInfo != null && !TextUtils.isEmpty(conversationInfo.firstSnippet) ?
459                conversationInfo.firstSnippet : snippet;
460    }
461
462    public String getSenders(Context context) {
463        if (conversationInfo != null) {
464            ArrayList<String> senders = new ArrayList<String>();
465            for (MessageInfo m : this.conversationInfo.messageInfos) {
466                senders.add(m.sender);
467            }
468            return TextUtils.join(getSendersDelimeter(context), senders);
469        } else {
470            return senders;
471        }
472    }
473
474    private String getSendersDelimeter(Context context) {
475        if (sSendersDelimeter == null) {
476            sSendersDelimeter = context.getResources().getString(R.string.senders_split_token);
477        }
478        return sSendersDelimeter;
479    }
480
481    /**
482     * Get the number of messages for this conversation.
483     */
484    public int getNumMessages() {
485        return conversationInfo != null ? conversationInfo.messageCount : numMessages;
486    }
487
488    /**
489     * Get the number of drafts for this conversation.
490     */
491    public int numDrafts() {
492        return conversationInfo != null ? conversationInfo.draftCount : numDrafts;
493    }
494
495    public boolean isViewed() {
496        return viewed;
497    }
498
499    public void markViewed() {
500        viewed = true;
501    }
502
503    /**
504     * Create a human-readable string of all the conversations
505     * @param collection Any collection of conversations
506     * @return string with a human readable representation of the conversations.
507     */
508    public static String toString(Collection<Conversation> collection) {
509        final StringBuilder out = new StringBuilder(collection.size() + " conversations:");
510        int count = 0;
511        for (final Conversation c : collection) {
512            count++;
513            // Indent the conversations to make them easy to read in debug
514            // output.
515            out.append("      " + count + ": " + c.toString() + "\n");
516        }
517        return out.toString();
518    }
519
520    /**
521     * Returns an empty string if the specified string is null
522     */
523    private static String emptyIfNull(String in) {
524        return in != null ? in : EMPTY_STRING;
525    }
526
527    /**
528     * Get the properly formatted subject and snippet string for display a conversation.
529     */
530    public static SpannableStringBuilder getSubjectAndSnippetForDisplay(Context context,
531            String filteredSubject, String snippet) {
532        if (sSubjectAndSnippet == null) {
533            sSubjectAndSnippet = context.getString(R.string.subject_and_snippet);
534        }
535        return new SpannableStringBuilder((!TextUtils.isEmpty(snippet)) ?
536                String.format(sSubjectAndSnippet, filteredSubject, snippet)
537                : filteredSubject);
538    }
539}
540