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