Conversation.java revision 167bddc283a66f210e0484eeca609f4e7f35dac4
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.TextPaint;
27import android.text.TextUtils;
28import android.text.TextUtils.TruncateAt;
29
30import com.android.mail.R;
31import com.android.mail.providers.UIProvider.ConversationColumns;
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 EMPTY_STRING = "";
43
44    /**
45     * @see BaseColumns#_ID
46     */
47    public long id;
48    /**
49     * @see UIProvider.ConversationColumns#URI
50     */
51    public Uri uri;
52    /**
53     * @see UIProvider.ConversationColumns#SUBJECT
54     */
55    public String subject;
56    /**
57     * @see UIProvider.ConversationColumns#DATE_RECEIVED_MS
58     */
59    public long dateMs;
60    /**
61     * @see UIProvider.ConversationColumns#SNIPPET
62     */
63    @Deprecated
64    public String snippet;
65    /**
66     * @see UIProvider.ConversationColumns#HAS_ATTACHMENTS
67     */
68    public boolean hasAttachments;
69    /**
70     * @see UIProvider.ConversationColumns#MESSAGE_LIST_URI
71     */
72    public Uri messageListUri;
73    /**
74     * @see UIProvider.ConversationColumns#SENDER_INFO
75     */
76    @Deprecated
77    public String senders;
78    /**
79     * @see UIProvider.ConversationColumns#NUM_MESSAGES
80     */
81    private int numMessages;
82    /**
83     * @see UIProvider.ConversationColumns#NUM_DRAFTS
84     */
85    private int numDrafts;
86    /**
87     * @see UIProvider.ConversationColumns#SENDING_STATE
88     */
89    public int sendingState;
90    /**
91     * @see UIProvider.ConversationColumns#PRIORITY
92     */
93    public int priority;
94    /**
95     * @see UIProvider.ConversationColumns#READ
96     */
97    public boolean read;
98    /**
99     * @see UIProvider.ConversationColumns#STARRED
100     */
101    public boolean starred;
102    /**
103     * @see UIProvider.ConversationColumns#RAW_FOLDERS
104     */
105    private FolderList rawFolders;
106    /**
107     * @see UIProvider.ConversationColumns#FLAGS
108     */
109    public int convFlags;
110    /**
111     * @see UIProvider.ConversationColumns#PERSONAL_LEVEL
112     */
113    public int personalLevel;
114    /**
115     * @see UIProvider.ConversationColumns#SPAM
116     */
117    public boolean spam;
118    /**
119     * @see UIProvider.ConversationColumns#MUTED
120     */
121    public boolean muted;
122    /**
123     * @see UIProvider.ConversationColumns#PHISHING
124     */
125    public boolean phishing;
126    /**
127     * @see UIProvider.ConversationColumns#COLOR
128     */
129    public int color;
130    /**
131     * @see UIProvider.ConversationColumns#ACCOUNT_URI
132     */
133    public Uri accountUri;
134    /**
135     * @see UIProvider.ConversationColumns#CONVERSATION_INFO
136     */
137    public ConversationInfo conversationInfo;
138    /**
139     * @see UIProvider.ConversationColumns#CONVERSATION_BASE_URI
140     */
141    public Uri conversationBaseUri;
142    /**
143     * @see UIProvider.ConversationColumns#REMOTE
144     */
145    public boolean isRemote;
146
147    // Used within the UI to indicate the adapter position of this conversation
148    public transient int position;
149    // Used within the UI to indicate that a Conversation should be removed from
150    // the ConversationCursor when executing an update, e.g. the the
151    // Conversation is no longer in the ConversationList for the current folder,
152    // that is it's now in some other folder(s)
153    public transient boolean localDeleteOnUpdate;
154
155    private transient boolean viewed;
156
157    private ArrayList<Folder> cachedDisplayableFolders;
158
159    private static String sSendersDelimeter;
160
161    private static String sSubjectAndSnippet;
162
163    // Constituents of convFlags below
164    // Flag indicating that the item has been deleted, but will continue being
165    // shown in the list Delete/Archive of a mostly-dead item will NOT propagate
166    // the delete/archive, but WILL remove the item from the cursor
167    public static final int FLAG_MOSTLY_DEAD = 1 << 0;
168
169    /** An immutable, empty conversation list */
170    public static final Collection<Conversation> EMPTY = Collections.emptyList();
171
172    @Override
173    public int describeContents() {
174        return 0;
175    }
176
177    @Override
178    public void writeToParcel(Parcel dest, int flags) {
179        dest.writeLong(id);
180        dest.writeParcelable(uri, flags);
181        dest.writeString(subject);
182        dest.writeLong(dateMs);
183        dest.writeString(snippet);
184        dest.writeInt(hasAttachments ? 1 : 0);
185        dest.writeParcelable(messageListUri, 0);
186        dest.writeString(senders);
187        dest.writeInt(numMessages);
188        dest.writeInt(numDrafts);
189        dest.writeInt(sendingState);
190        dest.writeInt(priority);
191        dest.writeInt(read ? 1 : 0);
192        dest.writeInt(starred ? 1 : 0);
193        dest.writeParcelable(rawFolders, 0);
194        dest.writeInt(convFlags);
195        dest.writeInt(personalLevel);
196        dest.writeInt(spam ? 1 : 0);
197        dest.writeInt(phishing ? 1 : 0);
198        dest.writeInt(muted ? 1 : 0);
199        dest.writeInt(color);
200        dest.writeParcelable(accountUri, 0);
201        dest.writeParcelable(conversationInfo, 0);
202        dest.writeParcelable(conversationBaseUri, 0);
203        dest.writeInt(isRemote ? 1 : 0);
204    }
205
206    private Conversation(Parcel in, ClassLoader loader) {
207        id = in.readLong();
208        uri = in.readParcelable(null);
209        subject = in.readString();
210        dateMs = in.readLong();
211        snippet = in.readString();
212        hasAttachments = (in.readInt() != 0);
213        messageListUri = in.readParcelable(null);
214        senders = emptyIfNull(in.readString());
215        numMessages = in.readInt();
216        numDrafts = in.readInt();
217        sendingState = in.readInt();
218        priority = in.readInt();
219        read = (in.readInt() != 0);
220        starred = (in.readInt() != 0);
221        rawFolders = in.readParcelable(loader);
222        convFlags = in.readInt();
223        personalLevel = in.readInt();
224        spam = in.readInt() != 0;
225        phishing = in.readInt() != 0;
226        muted = in.readInt() != 0;
227        color = in.readInt();
228        accountUri = in.readParcelable(null);
229        position = NO_POSITION;
230        localDeleteOnUpdate = false;
231        conversationInfo = in.readParcelable(loader);
232        conversationBaseUri = in.readParcelable(null);
233        isRemote = in.readInt() != 0;
234    }
235
236    @Override
237    public String toString() {
238        return "[conversation id=" + id + ", subject =" + subject + "]";
239    }
240
241    public static final ClassLoaderCreator<Conversation> CREATOR =
242            new ClassLoaderCreator<Conversation>() {
243
244        @Override
245        public Conversation createFromParcel(Parcel source) {
246            return new Conversation(source, null);
247        }
248
249        @Override
250        public Conversation createFromParcel(Parcel source, ClassLoader loader) {
251            return new Conversation(source, loader);
252        }
253
254        @Override
255        public Conversation[] newArray(int size) {
256            return new Conversation[size];
257        }
258
259    };
260
261    public static final Uri MOVE_CONVERSATIONS_URI = Uri.parse("content://moveconversations");
262
263    /**
264     * The column that needs to be updated to change the folders for a conversation.
265     */
266    public static final String UPDATE_FOLDER_COLUMN = ConversationColumns.RAW_FOLDERS;
267
268    public Conversation(Cursor cursor) {
269        if (cursor != null) {
270            id = cursor.getLong(UIProvider.CONVERSATION_ID_COLUMN);
271            uri = Uri.parse(cursor.getString(UIProvider.CONVERSATION_URI_COLUMN));
272            dateMs = cursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN);
273            subject = cursor.getString(UIProvider.CONVERSATION_SUBJECT_COLUMN);
274            // Don't allow null subject
275            if (subject == null) {
276                subject = "";
277            }
278            hasAttachments = cursor.getInt(UIProvider.CONVERSATION_HAS_ATTACHMENTS_COLUMN) != 0;
279            String messageList = cursor.getString(UIProvider.CONVERSATION_MESSAGE_LIST_URI_COLUMN);
280            messageListUri = !TextUtils.isEmpty(messageList) ? Uri.parse(messageList) : null;
281            sendingState = cursor.getInt(UIProvider.CONVERSATION_SENDING_STATE_COLUMN);
282            priority = cursor.getInt(UIProvider.CONVERSATION_PRIORITY_COLUMN);
283            read = cursor.getInt(UIProvider.CONVERSATION_READ_COLUMN) != 0;
284            starred = cursor.getInt(UIProvider.CONVERSATION_STARRED_COLUMN) != 0;
285            rawFolders = FolderList.fromBlob(
286                    cursor.getBlob(UIProvider.CONVERSATION_RAW_FOLDERS_COLUMN));
287            convFlags = cursor.getInt(UIProvider.CONVERSATION_FLAGS_COLUMN);
288            personalLevel = cursor.getInt(UIProvider.CONVERSATION_PERSONAL_LEVEL_COLUMN);
289            spam = cursor.getInt(UIProvider.CONVERSATION_IS_SPAM_COLUMN) != 0;
290            phishing = cursor.getInt(UIProvider.CONVERSATION_IS_PHISHING_COLUMN) != 0;
291            muted = cursor.getInt(UIProvider.CONVERSATION_MUTED_COLUMN) != 0;
292            color = cursor.getInt(UIProvider.CONVERSATION_COLOR_COLUMN);
293            String account = cursor.getString(UIProvider.CONVERSATION_ACCOUNT_URI_COLUMN);
294            accountUri = !TextUtils.isEmpty(account) ? Uri.parse(account) : null;
295            position = NO_POSITION;
296            localDeleteOnUpdate = false;
297            conversationInfo = ConversationInfo.fromBlob(
298                    cursor.getBlob(UIProvider.CONVERSATION_INFO_COLUMN));
299            final String conversationBase =
300                    cursor.getString(UIProvider.CONVERSATION_BASE_URI_COLUMN);
301            conversationBaseUri = !TextUtils.isEmpty(conversationBase) ?
302                    Uri.parse(conversationBase) : null;
303            if (conversationInfo == null) {
304                snippet = cursor.getString(UIProvider.CONVERSATION_SNIPPET_COLUMN);
305                senders = emptyIfNull(cursor.getString(UIProvider.CONVERSATION_SENDER_INFO_COLUMN));
306                numMessages = cursor.getInt(UIProvider.CONVERSATION_NUM_MESSAGES_COLUMN);
307                numDrafts = cursor.getInt(UIProvider.CONVERSATION_NUM_DRAFTS_COLUMN);
308            }
309            isRemote = cursor.getInt(UIProvider.CONVERSATION_REMOTE_COLUMN) != 0;
310        }
311    }
312
313    public Conversation() {
314    }
315
316    public static Conversation create(long id, Uri uri, String subject, long dateMs,
317            String snippet, boolean hasAttachment, Uri messageListUri, String senders,
318            int numMessages, int numDrafts, int sendingState, int priority, boolean read,
319            boolean starred, FolderList rawFolders, int convFlags, int personalLevel, boolean spam,
320            boolean phishing, boolean muted, Uri accountUri, ConversationInfo conversationInfo,
321            Uri conversationBase, boolean isRemote) {
322
323        final Conversation conversation = new Conversation();
324
325        conversation.id = id;
326        conversation.uri = uri;
327        conversation.subject = subject;
328        conversation.dateMs = dateMs;
329        conversation.snippet = snippet;
330        conversation.hasAttachments = hasAttachment;
331        conversation.messageListUri = messageListUri;
332        conversation.senders = emptyIfNull(senders);
333        conversation.numMessages = numMessages;
334        conversation.numDrafts = numDrafts;
335        conversation.sendingState = sendingState;
336        conversation.priority = priority;
337        conversation.read = read;
338        conversation.starred = starred;
339        conversation.rawFolders = rawFolders;
340        conversation.convFlags = convFlags;
341        conversation.personalLevel = personalLevel;
342        conversation.spam = spam;
343        conversation.phishing = phishing;
344        conversation.muted = muted;
345        conversation.color = 0;
346        conversation.accountUri = accountUri;
347        conversation.conversationInfo = conversationInfo;
348        conversation.conversationBaseUri = conversationBase;
349        conversation.isRemote = isRemote;
350        return conversation;
351    }
352
353    /**
354     * Get the <strong>immutable</strong> list of {@link Folder}s for this conversation. To modify
355     * this list, make a new {@link FolderList} and use {@link #setRawFolders(FolderList)}.
356     *
357     * @return <strong>Immutable</strong> list of {@link Folder}s.
358     */
359    public List<Folder> getRawFolders() {
360        return rawFolders.folders;
361    }
362
363    public void setRawFolders(FolderList folders) {
364        clearCachedFolders();
365        rawFolders = folders;
366    }
367
368    private void clearCachedFolders() {
369        cachedDisplayableFolders = null;
370    }
371
372    public ArrayList<Folder> getRawFoldersForDisplay(Folder ignoreFolder) {
373        if (cachedDisplayableFolders == null) {
374            cachedDisplayableFolders = new ArrayList<Folder>();
375            for (Folder folder : rawFolders.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
529     * conversation.
530     *
531     * @param context
532     * @param filteredSubject
533     * @param snippet
534     * @param maxChars Supply max characters the returned string can have, or -1
535     *            if there is no limit
536     */
537    public static SpannableStringBuilder getSubjectAndSnippetForDisplay(Context context,
538            String filteredSubject, String snippet, int avail, TextPaint paint) {
539        if (sSubjectAndSnippet == null) {
540            sSubjectAndSnippet = context.getString(R.string.subject_and_snippet);
541        }
542        String subjectText = (!TextUtils.isEmpty(snippet)) ?
543                String.format(sSubjectAndSnippet, filteredSubject, snippet)
544                : filteredSubject;
545        SpannableStringBuilder builder = new SpannableStringBuilder();
546        if (avail != -1) {
547            CharSequence ellipsizedText = TextUtils.ellipsize(subjectText, paint, avail,
548                    TruncateAt.END);
549            builder.append(ellipsizedText, 0, ellipsizedText.length() - 1);
550        } else {
551            builder.append(subjectText);
552        }
553        return builder;
554    }
555}
556