Conversation.java revision cf164d64bcb1da92b427bda99b97f7ec310ef704
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.ContentProviderClient;
20import android.content.ContentValues;
21import android.content.Context;
22import android.database.Cursor;
23import android.net.Uri;
24import android.os.Parcel;
25import android.os.Parcelable;
26import android.text.TextUtils;
27
28import com.android.mail.browse.ConversationCursor.ConversationOperation;
29import com.android.mail.browse.ConversationCursor.ConversationProvider;
30import com.android.mail.providers.UIProvider.ConversationColumns;
31import com.google.common.collect.Lists;
32
33import java.util.ArrayList;
34import java.util.Arrays;
35import java.util.Collection;
36
37public class Conversation implements Parcelable {
38    public static final int NO_POSITION = -1;
39
40    public long id;
41    public Uri uri;
42    public String subject;
43    public long dateMs;
44    public String snippet;
45    public boolean hasAttachments;
46    public Uri messageListUri;
47    public String senders;
48    public int numMessages;
49    public int numDrafts;
50    public int sendingState;
51    public int priority;
52    public boolean read;
53    public boolean starred;
54    public String folderList;
55    public String rawFolders;
56    public int convFlags;
57    public int personalLevel;
58    public boolean spam;
59    public boolean muted;
60
61    // Used within the UI to indicate the adapter position of this conversation
62    public transient int position;
63    // Used within the UI to indicate that a Conversation should be removed from the
64    // ConversationCursor when executing an update, e.g. the the Conversation is no longer
65    // in the ConversationList for the current folder, that is it's now in some other folder(s)
66    public transient boolean localDeleteOnUpdate;
67
68    // Constituents of convFlags below
69    // Flag indicating that the item has been deleted, but will continue being shown in the list
70    // Delete/Archive of a mostly-dead item will NOT propagate the delete/archive, but WILL remove
71    // the item from the cursor
72    public static final int FLAG_MOSTLY_DEAD = 1 << 0;
73
74    @Override
75    public int describeContents() {
76        return 0;
77    }
78
79    @Override
80    public void writeToParcel(Parcel dest, int flags) {
81        dest.writeLong(id);
82        dest.writeParcelable(uri, flags);
83        dest.writeString(subject);
84        dest.writeLong(dateMs);
85        dest.writeString(snippet);
86        dest.writeByte(hasAttachments ? (byte) 1 : 0);
87        dest.writeParcelable(messageListUri, 0);
88        dest.writeString(senders);
89        dest.writeInt(numMessages);
90        dest.writeInt(numDrafts);
91        dest.writeInt(sendingState);
92        dest.writeInt(priority);
93        dest.writeByte(read ? (byte) 1 : 0);
94        dest.writeByte(starred ? (byte) 1 : 0);
95        dest.writeString(folderList);
96        dest.writeString(rawFolders);
97        dest.writeInt(convFlags);
98        dest.writeInt(personalLevel);
99        dest.writeInt(spam ? 1 : 0);
100        dest.writeInt(muted ? 1 : 0);
101    }
102
103    private Conversation(Parcel in) {
104        id = in.readLong();
105        uri = in.readParcelable(null);
106        subject = in.readString();
107        dateMs = in.readLong();
108        snippet = in.readString();
109        hasAttachments = (in.readByte() != 0);
110        messageListUri = in.readParcelable(null);
111        senders = in.readString();
112        numMessages = in.readInt();
113        numDrafts = in.readInt();
114        sendingState = in.readInt();
115        priority = in.readInt();
116        read = (in.readByte() != 0);
117        starred = (in.readByte() != 0);
118        folderList = in.readString();
119        rawFolders = in.readString();
120        convFlags = in.readInt();
121        personalLevel = in.readInt();
122        spam = in.readInt() != 0;
123        muted = in.readInt() != 0;
124        position = NO_POSITION;
125        localDeleteOnUpdate = false;
126    }
127
128    @Override
129    public String toString() {
130        return "[conversation id=" + id + "]";
131    }
132
133    public static final Creator<Conversation> CREATOR = new Creator<Conversation>() {
134
135        @Override
136        public Conversation createFromParcel(Parcel source) {
137            return new Conversation(source);
138        }
139
140        @Override
141        public Conversation[] newArray(int size) {
142            return new Conversation[size];
143        }
144
145    };
146
147    public static final Uri MOVE_CONVERSATIONS_URI = Uri.parse("content://moveconversations");
148
149    public Conversation(Cursor cursor) {
150        if (cursor != null) {
151            id = cursor.getLong(UIProvider.CONVERSATION_ID_COLUMN);
152            uri = Uri.parse(cursor.getString(UIProvider.CONVERSATION_URI_COLUMN));
153            dateMs = cursor.getLong(UIProvider.CONVERSATION_DATE_RECEIVED_MS_COLUMN);
154            subject = cursor.getString(UIProvider.CONVERSATION_SUBJECT_COLUMN);
155            // Don't allow null subject
156            if (subject == null) {
157                subject = "";
158            }
159            snippet = cursor.getString(UIProvider.CONVERSATION_SNIPPET_COLUMN);
160            hasAttachments = cursor.getInt(UIProvider.CONVERSATION_HAS_ATTACHMENTS_COLUMN) != 0;
161            String messageList = cursor
162                    .getString(UIProvider.CONVERSATION_MESSAGE_LIST_URI_COLUMN);
163            messageListUri = !TextUtils.isEmpty(messageList) ? Uri.parse(messageList) : null;
164            senders = cursor.getString(UIProvider.CONVERSATION_SENDER_INFO_COLUMN);
165            numMessages = cursor.getInt(UIProvider.CONVERSATION_NUM_MESSAGES_COLUMN);
166            numDrafts = cursor.getInt(UIProvider.CONVERSATION_NUM_DRAFTS_COLUMN);
167            sendingState = cursor.getInt(UIProvider.CONVERSATION_SENDING_STATE_COLUMN);
168            priority = cursor.getInt(UIProvider.CONVERSATION_PRIORITY_COLUMN);
169            read = cursor.getInt(UIProvider.CONVERSATION_READ_COLUMN) != 0;
170            starred = cursor.getInt(UIProvider.CONVERSATION_STARRED_COLUMN) != 0;
171            folderList = cursor.getString(UIProvider.CONVERSATION_FOLDER_LIST_COLUMN);
172            rawFolders = cursor.getString(UIProvider.CONVERSATION_RAW_FOLDERS_COLUMN);
173            convFlags = cursor.getInt(UIProvider.CONVERSATION_FLAGS_COLUMN);
174            personalLevel = cursor.getInt(UIProvider.CONVERSATION_PERSONAL_LEVEL_COLUMN);
175            spam = cursor.getInt(UIProvider.CONVERSATION_IS_SPAM_COLUMN) != 0;
176            muted = cursor.getInt(UIProvider.CONVERSATION_MUTED_COLUMN) != 0;
177            position = NO_POSITION;
178            localDeleteOnUpdate = false;
179        }
180    }
181
182    private Conversation() {
183    }
184
185    public static Conversation create(long id, Uri uri, String subject, long dateMs,
186            String snippet, boolean hasAttachment, Uri messageListUri, String senders,
187            int numMessages, int numDrafts, int sendingState, int priority, boolean read,
188            boolean starred, String folderList, String rawFolders, int convFlags,
189            int personalLevel, boolean spam, boolean muted) {
190
191        final Conversation conversation = new Conversation();
192
193        conversation.id = id;
194        conversation.uri = uri;
195        conversation.subject = subject;
196        conversation.dateMs = dateMs;
197        conversation.snippet = snippet;
198        conversation.hasAttachments = hasAttachment;
199        conversation.messageListUri = messageListUri;
200        conversation.senders = senders;
201        conversation.numMessages = numMessages;
202        conversation.numDrafts = numDrafts;
203        conversation.sendingState = sendingState;
204        conversation.priority = priority;
205        conversation.read = read;
206        conversation.starred = starred;
207        conversation.folderList = folderList;
208        conversation.rawFolders = rawFolders;
209        conversation.convFlags = convFlags;
210        conversation.personalLevel = personalLevel;
211        conversation.spam = spam;
212        conversation.muted = muted;
213        return conversation;
214    }
215
216    @Override
217    public boolean equals(Object o) {
218        return uri.equals(o);
219    }
220
221    @Override
222    public int hashCode() {
223        return uri.hashCode();
224    }
225
226    /**
227     * Get if this conversation is marked as high priority.
228     */
229    public boolean isImportant() {
230        return priority == UIProvider.ConversationPriority.IMPORTANT;
231    }
232
233    /**
234     * Get if this conversation is mostly dead
235     */
236    public boolean isMostlyDead() {
237        return (convFlags & FLAG_MOSTLY_DEAD) != 0;
238    }
239
240    // Below are methods that update Conversation data (update/delete)
241
242    /**
243     * Update an integer column for a single conversation (see updateBoolean below)
244     */
245    public int updateInt(Context context, String columnName, int value) {
246        return updateInt(context, Arrays.asList(this), columnName, value);
247    }
248
249    /**
250     * Update an integer column for a group of conversations (see updateValues below)
251     */
252    public static int updateInt(Context context, Collection<Conversation> conversations,
253            String columnName, int value) {
254        ContentValues cv = new ContentValues();
255        cv.put(columnName, value);
256        return updateValues(context, conversations, cv);
257    }
258
259    /**
260     * Update a boolean column for a single conversation (see updateBoolean below)
261     */
262    public int updateBoolean(Context context, String columnName, boolean value) {
263        return updateBoolean(context, Arrays.asList(this), columnName, value);
264    }
265
266    /**
267     * Update a string column for a group of conversations (see updateValues below)
268     */
269    public static int updateBoolean(Context context, Collection<Conversation> conversations,
270            String columnName, boolean value) {
271        ContentValues cv = new ContentValues();
272        cv.put(columnName, value);
273        return updateValues(context, conversations, cv);
274    }
275
276    /**
277     * Update a string column for a single conversation (see updateString below)
278     */
279    public int updateString(Context context, String columnName, String value) {
280        return updateString(context, Arrays.asList(this), columnName, value);
281    }
282
283    /**
284     * Update a string column for a group of conversations (see updateValues below)
285     */
286    public static int updateString(Context context, Collection<Conversation> conversations,
287            String columnName, String value) {
288        ContentValues cv = new ContentValues();
289        cv.put(columnName, value);
290        return updateValues(context, conversations, cv);
291    }
292
293    /**
294     * Update a boolean column for a group of conversations, immediately in the UI and in a single
295     * transaction in the underlying provider
296     * @param conversations a collection of conversations
297     * @param context the caller's context
298     * @param columnName the column to update
299     * @param value the new value
300     * @return the sequence number of the operation (for undo)
301     */
302    private static int updateValues(Context context, Collection<Conversation> conversations,
303            ContentValues values) {
304        return apply(context,
305                getOperationsForConversations(conversations, ConversationOperation.UPDATE, values));
306    }
307
308    private static ArrayList<ConversationOperation> getOperationsForConversations(
309            Collection<Conversation> conversations, int op, ContentValues values) {
310        return getOperationsForConversations(conversations, op, values, false /* autoNotify */);
311    }
312
313    private static ArrayList<ConversationOperation> getOperationsForConversations(
314            Collection<Conversation> conversations, int type, ContentValues values,
315            boolean autoNotify) {
316        final ArrayList<ConversationOperation> ops = Lists.newArrayList();
317        for (Conversation conv: conversations) {
318            ConversationOperation op = new ConversationOperation(type, conv, values, autoNotify);
319            ops.add(op);
320        }
321        return ops;
322    }
323
324    /**
325     * Delete a single conversation
326     * @param context the caller's context
327     * @return the sequence number of the operation (for undo)
328     */
329    public int delete(Context context) {
330        ArrayList<Conversation> conversations = new ArrayList<Conversation>();
331        conversations.add(this);
332        return delete(context, conversations);
333    }
334
335    /**
336     * Delete a single conversation
337     * @param context the caller's context
338     * @return the sequence number of the operation (for undo)
339     */
340    public int mostlyArchive(Context context) {
341        ArrayList<Conversation> conversations = new ArrayList<Conversation>();
342        conversations.add(this);
343        return archive(context, conversations);
344    }
345
346    /**
347     * Delete a single conversation
348     * @param context the caller's context
349     * @return the sequence number of the operation (for undo)
350     */
351    public int mostlyDelete(Context context) {
352        ArrayList<Conversation> conversations = new ArrayList<Conversation>();
353        conversations.add(this);
354        return delete(context, conversations);
355    }
356
357    /**
358     * Mark a single conversation read/unread.
359     * @param context the caller's context
360     * @param read true for read, false for unread
361     * @return the sequence number of the operation (for undo)
362     */
363    public int markRead(Context context, boolean read) {
364        ContentValues values = new ContentValues();
365        values.put(ConversationColumns.READ, read);
366
367        return apply(
368                context,
369                getOperationsForConversations(Arrays.asList(this), ConversationOperation.UPDATE,
370                        values, true /* autoNotify */));
371    }
372
373    // Convenience methods
374    private static int apply(Context context, ArrayList<ConversationOperation> operations) {
375        ContentProviderClient client =
376                context.getContentResolver().acquireContentProviderClient(
377                        ConversationProvider.AUTHORITY);
378        try {
379            ConversationProvider cp = (ConversationProvider)client.getLocalContentProvider();
380            return cp.apply(operations);
381        } finally {
382            client.release();
383        }
384    }
385
386    private static void undoLocal(Context context) {
387        ContentProviderClient client =
388                context.getContentResolver().acquireContentProviderClient(
389                        ConversationProvider.AUTHORITY);
390        try {
391            ConversationProvider cp = (ConversationProvider)client.getLocalContentProvider();
392            cp.undo();
393        } finally {
394            client.release();
395        }
396    }
397
398    public static void undo(final Context context, final Uri undoUri) {
399        new Thread(new Runnable() {
400            @Override
401            public void run() {
402                Cursor c = context.getContentResolver().query(undoUri, UIProvider.UNDO_PROJECTION,
403                        null, null, null);
404                if (c != null) {
405                    c.close();
406                }
407            }
408        }).start();
409        undoLocal(context);
410    }
411
412    /**
413     * Delete a group of conversations immediately in the UI and in a single transaction in the
414     * underlying provider. See applyAction for argument descriptions
415     */
416    public static int delete(Context context, Collection<Conversation> conversations) {
417        return applyAction(context, conversations, ConversationOperation.DELETE);
418    }
419
420    /**
421     * As above, for archive
422     */
423    public static int archive(Context context, Collection<Conversation> conversations) {
424        return applyAction(context, conversations, ConversationOperation.ARCHIVE);
425    }
426
427    /**
428     * As above, for mute
429     */
430    public static int mute(Context context, Collection<Conversation> conversations) {
431        return applyAction(context, conversations, ConversationOperation.MUTE);
432    }
433
434    /**
435     * As above, for report spam
436     */
437    public static int reportSpam(Context context, Collection<Conversation> conversations) {
438        return applyAction(context, conversations, ConversationOperation.REPORT_SPAM);
439    }
440
441    /**
442     * As above, for mostly archive
443     */
444    public static int mostlyArchive(Context context, Collection<Conversation> conversations) {
445        return applyAction(context, conversations, ConversationOperation.MOSTLY_ARCHIVE);
446    }
447
448    /**
449     * As above, for mostly delete
450     */
451    public static int mostlyDelete(Context context, Collection<Conversation> conversations) {
452        return applyAction(context, conversations, ConversationOperation.MOSTLY_DELETE);
453    }
454
455    /**
456     * Convenience method for performing an operation on a group of conversations
457     * @param context the caller's context
458     * @param conversations the conversations to be affected
459     * @param opAction the action to take
460     * @return the sequence number of the operation applied in CC
461     */
462    private static int applyAction(Context context, Collection<Conversation> conversations,
463            int opAction) {
464        ArrayList<ConversationOperation> ops = Lists.newArrayList();
465        for (Conversation conv: conversations) {
466            ConversationOperation op =
467                    new ConversationOperation(opAction, conv);
468            ops.add(op);
469        }
470        return apply(context, ops);
471    }
472}