1/*
2 * Copyright (C) 2007 The Android Open Source Project
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 android.provider;
18
19import com.google.android.collect.Lists;
20import com.google.android.collect.Maps;
21import com.google.android.collect.Sets;
22
23import android.content.AsyncQueryHandler;
24import android.content.ContentQueryMap;
25import android.content.ContentResolver;
26import android.content.ContentUris;
27import android.content.ContentValues;
28import android.database.ContentObserver;
29import android.database.Cursor;
30import android.database.DataSetObserver;
31import android.net.Uri;
32import android.os.Bundle;
33import android.os.Handler;
34import android.text.Html;
35import android.text.SpannableStringBuilder;
36import android.text.Spanned;
37import android.text.TextUtils;
38import android.text.TextUtils.SimpleStringSplitter;
39import android.text.style.CharacterStyle;
40import android.text.util.Regex;
41import android.util.Log;
42
43import java.io.UnsupportedEncodingException;
44import java.net.URLEncoder;
45import java.util.ArrayList;
46import java.util.HashSet;
47import java.util.List;
48import java.util.Map;
49import java.util.Observable;
50import java.util.Observer;
51import java.util.Set;
52import java.util.SortedSet;
53import java.util.TreeSet;
54import java.util.regex.Matcher;
55import java.util.regex.Pattern;
56
57/**
58 * A thin wrapper over the content resolver for accessing the gmail provider.
59 *
60 * @hide
61 */
62public final class Gmail {
63    // Set to true to enable extra debugging.
64    private static final boolean DEBUG = false;
65
66    public static final String GMAIL_AUTH_SERVICE = "mail";
67    // These constants come from google3/java/com/google/caribou/backend/MailLabel.java.
68    public static final String LABEL_SENT = "^f";
69    public static final String LABEL_INBOX = "^i";
70    public static final String LABEL_DRAFT = "^r";
71    public static final String LABEL_UNREAD = "^u";
72    public static final String LABEL_TRASH = "^k";
73    public static final String LABEL_SPAM = "^s";
74    public static final String LABEL_STARRED = "^t";
75    public static final String LABEL_CHAT = "^b"; // 'b' for 'buzz'
76    public static final String LABEL_VOICEMAIL = "^vm";
77    public static final String LABEL_IGNORED = "^g";
78    public static final String LABEL_ALL = "^all";
79    // These constants (starting with "^^") are only used locally and are not understood by the
80    // server.
81    public static final String LABEL_VOICEMAIL_INBOX = "^^vmi";
82    public static final String LABEL_CACHED = "^^cached";
83    public static final String LABEL_OUTBOX = "^^out";
84
85    public static final String AUTHORITY = "gmail-ls";
86    private static final String TAG = "Gmail";
87    private static final String AUTHORITY_PLUS_CONVERSATIONS =
88            "content://" + AUTHORITY + "/conversations/";
89    private static final String AUTHORITY_PLUS_LABELS =
90            "content://" + AUTHORITY + "/labels/";
91    private static final String AUTHORITY_PLUS_MESSAGES =
92            "content://" + AUTHORITY + "/messages/";
93    private static final String AUTHORITY_PLUS_SETTINGS =
94            "content://" + AUTHORITY + "/settings/";
95
96    public static final Uri BASE_URI = Uri.parse(
97            "content://" + AUTHORITY);
98    private static final Uri LABELS_URI =
99            Uri.parse(AUTHORITY_PLUS_LABELS);
100    private static final Uri CONVERSATIONS_URI =
101            Uri.parse(AUTHORITY_PLUS_CONVERSATIONS);
102    private static final Uri SETTINGS_URI =
103            Uri.parse(AUTHORITY_PLUS_SETTINGS);
104
105    /** Separates email addresses in strings in the database. */
106    public static final String EMAIL_SEPARATOR = "\n";
107    public static final Pattern EMAIL_SEPARATOR_PATTERN = Pattern.compile(EMAIL_SEPARATOR);
108
109    /**
110     * Space-separated lists have separators only between items.
111     */
112    private static final char SPACE_SEPARATOR = ' ';
113    public static final Pattern SPACE_SEPARATOR_PATTERN = Pattern.compile(" ");
114
115    /**
116     * Comma-separated lists have separators between each item, before the first and after the last
117     * item. The empty list is <tt>,</tt>.
118     *
119     * <p>This makes them easier to modify with SQL since it is not a special case to add or
120     * remove the last item. Having a separator on each side of each value also makes it safe to use
121     * SQL's REPLACE to remove an item from a string by using REPLACE(',value,', ',').
122     *
123     * <p>We could use the same separator for both lists but this makes it easier to remember which
124     * kind of list one is dealing with.
125     */
126    private static final char COMMA_SEPARATOR = ',';
127    public static final Pattern COMMA_SEPARATOR_PATTERN = Pattern.compile(",");
128
129    /** Separates attachment info parts in strings in the database. */
130    public static final String ATTACHMENT_INFO_SEPARATOR = "\n";
131    public static final Pattern ATTACHMENT_INFO_SEPARATOR_PATTERN =
132            Pattern.compile(ATTACHMENT_INFO_SEPARATOR);
133
134    public static final Character SENDER_LIST_SEPARATOR = '\n';
135    public static final String SENDER_LIST_TOKEN_ELIDED = "e";
136    public static final String SENDER_LIST_TOKEN_NUM_MESSAGES = "n";
137    public static final String SENDER_LIST_TOKEN_NUM_DRAFTS = "d";
138    public static final String SENDER_LIST_TOKEN_LITERAL = "l";
139    public static final String SENDER_LIST_TOKEN_SENDING = "s";
140    public static final String SENDER_LIST_TOKEN_SEND_FAILED = "f";
141
142    /** Used for finding status in a cursor's extras. */
143    public static final String EXTRA_STATUS = "status";
144
145    public static final String RESPOND_INPUT_COMMAND = "command";
146    public static final String COMMAND_RETRY = "retry";
147    public static final String COMMAND_ACTIVATE = "activate";
148    public static final String COMMAND_SET_VISIBLE = "setVisible";
149    public static final String SET_VISIBLE_PARAM_VISIBLE = "visible";
150    public static final String RESPOND_OUTPUT_COMMAND_RESPONSE = "commandResponse";
151    public static final String COMMAND_RESPONSE_OK =  "ok";
152    public static final String COMMAND_RESPONSE_UNKNOWN =  "unknownCommand";
153
154    public static final String INSERT_PARAM_ATTACHMENT_ORIGIN = "origin";
155    public static final String INSERT_PARAM_ATTACHMENT_ORIGIN_EXTRAS = "originExtras";
156
157    private static final Pattern NAME_ADDRESS_PATTERN = Pattern.compile("\"(.*)\"");
158    private static final Pattern UNNAMED_ADDRESS_PATTERN = Pattern.compile("([^<]+)@");
159
160    private static final Map<Integer, Integer> sPriorityToLength = Maps.newHashMap();
161    public static final SimpleStringSplitter sSenderListSplitter =
162            new SimpleStringSplitter(SENDER_LIST_SEPARATOR);
163    public static String[] sSenderFragments = new String[8];
164
165    /**
166     * Returns the name in an address string
167     * @param addressString such as &quot;bobby&quot; &lt;bob@example.com&gt;
168     * @return returns the quoted name in the addressString, otherwise the username from the email
169     *   address
170     */
171    public static String getNameFromAddressString(String addressString) {
172        Matcher namedAddressMatch = NAME_ADDRESS_PATTERN.matcher(addressString);
173        if (namedAddressMatch.find()) {
174            String name = namedAddressMatch.group(1);
175            if (name.length() > 0) return name;
176            addressString =
177                    addressString.substring(namedAddressMatch.end(), addressString.length());
178        }
179
180        Matcher unnamedAddressMatch = UNNAMED_ADDRESS_PATTERN.matcher(addressString);
181        if (unnamedAddressMatch.find()) {
182            return unnamedAddressMatch.group(1);
183        }
184
185        return addressString;
186    }
187
188    /**
189     * Returns the email address in an address string
190     * @param addressString such as &quot;bobby&quot; &lt;bob@example.com&gt;
191     * @return returns the email address, such as bob@example.com from the example above
192     */
193    public static String getEmailFromAddressString(String addressString) {
194        String result = addressString;
195        Matcher match = Regex.EMAIL_ADDRESS_PATTERN.matcher(addressString);
196        if (match.find()) {
197            result = addressString.substring(match.start(), match.end());
198        }
199
200        return result;
201    }
202
203    /**
204     * Returns whether the label is user-defined (versus system-defined labels such as inbox, whose
205     * names start with "^").
206     */
207    public static boolean isLabelUserDefined(String label) {
208        // TODO: label should never be empty so we should be able to say [label.charAt(0) != '^'].
209        // However, it's a release week and I'm too scared to make that change.
210        return !label.startsWith("^");
211    }
212
213    private static final Set<String> USER_SETTABLE_BUILTIN_LABELS = Sets.newHashSet(
214            Gmail.LABEL_INBOX,
215            Gmail.LABEL_UNREAD,
216            Gmail.LABEL_TRASH,
217            Gmail.LABEL_SPAM,
218            Gmail.LABEL_STARRED,
219            Gmail.LABEL_IGNORED);
220
221    /**
222     * Returns whether the label is user-settable. For example, labels such as LABEL_DRAFT should
223     * only be set internally.
224     */
225    public static boolean isLabelUserSettable(String label) {
226        return USER_SETTABLE_BUILTIN_LABELS.contains(label) || isLabelUserDefined(label);
227    }
228
229    /**
230     * Returns the set of labels using the raw labels from a previous getRawLabels()
231     * as input.
232     * @return a copy of the set of labels. To add or remove labels call
233     * MessageCursor.addOrRemoveLabel on each message in the conversation.
234     */
235    public static Set<Long> getLabelIdsFromLabelIdsString(
236            TextUtils.StringSplitter splitter) {
237        Set<Long> labelIds = Sets.newHashSet();
238        for (String labelIdString : splitter) {
239            labelIds.add(Long.valueOf(labelIdString));
240        }
241        return labelIds;
242    }
243
244    /**
245     * @deprecated remove when the activities stop using canonical names to identify labels
246     */
247    public static Set<String> getCanonicalNamesFromLabelIdsString(
248            LabelMap labelMap, TextUtils.StringSplitter splitter) {
249        Set<String> canonicalNames = Sets.newHashSet();
250        for (long labelId : getLabelIdsFromLabelIdsString(splitter)) {
251            final String canonicalName = labelMap.getCanonicalName(labelId);
252            // We will sometimes see labels that the label map does not yet know about or that
253            // do not have names yet.
254            if (!TextUtils.isEmpty(canonicalName)) {
255                canonicalNames.add(canonicalName);
256            } else {
257                Log.w(TAG, "getCanonicalNamesFromLabelIdsString skipping label id: " + labelId);
258            }
259        }
260        return canonicalNames;
261    }
262
263    /**
264     * @return a StringSplitter that is configured to split message label id strings
265     */
266    public static TextUtils.StringSplitter newMessageLabelIdsSplitter() {
267        return new TextUtils.SimpleStringSplitter(SPACE_SEPARATOR);
268    }
269
270    /**
271     * @return a StringSplitter that is configured to split conversation label id strings
272     */
273    public static TextUtils.StringSplitter newConversationLabelIdsSplitter() {
274        return new CommaStringSplitter();
275    }
276
277    /**
278     * A splitter for strings of the form described in the docs for COMMA_SEPARATOR.
279     */
280    private static class CommaStringSplitter extends TextUtils.SimpleStringSplitter {
281
282        public CommaStringSplitter() {
283            super(COMMA_SEPARATOR);
284        }
285
286        @Override
287        public void setString(String string) {
288            // The string should always be at least a single comma.
289            super.setString(string.substring(1));
290        }
291    }
292
293    /**
294     * Creates a single string of the form that getLabelIdsFromLabelIdsString can split.
295     */
296    public static String getLabelIdsStringFromLabelIds(Set<Long> labelIds) {
297        StringBuilder sb = new StringBuilder();
298        sb.append(COMMA_SEPARATOR);
299        for (Long labelId : labelIds) {
300            sb.append(labelId);
301            sb.append(COMMA_SEPARATOR);
302        }
303        return sb.toString();
304    }
305
306    public static final class ConversationColumns {
307        public static final String ID = "_id";
308        public static final String SUBJECT = "subject";
309        public static final String SNIPPET = "snippet";
310        public static final String FROM = "fromAddress";
311        public static final String DATE = "date";
312        public static final String PERSONAL_LEVEL = "personalLevel";
313        /** A list of label names with a space after each one (including the last one). This makes
314         * it easier remove individual labels from this list using SQL. */
315        public static final String LABEL_IDS = "labelIds";
316        public static final String NUM_MESSAGES = "numMessages";
317        public static final String MAX_MESSAGE_ID = "maxMessageId";
318        public static final String HAS_ATTACHMENTS = "hasAttachments";
319        public static final String HAS_MESSAGES_WITH_ERRORS = "hasMessagesWithErrors";
320        public static final String FORCE_ALL_UNREAD = "forceAllUnread";
321
322        private ConversationColumns() {}
323    }
324
325    public static final class MessageColumns {
326
327        public static final String ID = "_id";
328        public static final String MESSAGE_ID = "messageId";
329        public static final String CONVERSATION_ID = "conversation";
330        public static final String SUBJECT = "subject";
331        public static final String SNIPPET = "snippet";
332        public static final String FROM = "fromAddress";
333        public static final String TO = "toAddresses";
334        public static final String CC = "ccAddresses";
335        public static final String BCC = "bccAddresses";
336        public static final String REPLY_TO = "replyToAddresses";
337        public static final String DATE_SENT_MS = "dateSentMs";
338        public static final String DATE_RECEIVED_MS = "dateReceivedMs";
339        public static final String LIST_INFO = "listInfo";
340        public static final String PERSONAL_LEVEL = "personalLevel";
341        public static final String BODY = "body";
342        public static final String EMBEDS_EXTERNAL_RESOURCES = "bodyEmbedsExternalResources";
343        public static final String LABEL_IDS = "labelIds";
344        public static final String JOINED_ATTACHMENT_INFOS = "joinedAttachmentInfos";
345        public static final String ERROR = "error";
346        // TODO: add a method for accessing this
347        public static final String REF_MESSAGE_ID = "refMessageId";
348
349        // Fake columns used only for saving or sending messages.
350        public static final String FAKE_SAVE = "save";
351        public static final String FAKE_REF_MESSAGE_ID = "refMessageId";
352
353        private MessageColumns() {}
354    }
355
356    public static final class LabelColumns {
357        public static final String CANONICAL_NAME = "canonicalName";
358        public static final String NAME = "name";
359        public static final String NUM_CONVERSATIONS = "numConversations";
360        public static final String NUM_UNREAD_CONVERSATIONS =
361                "numUnreadConversations";
362
363        private LabelColumns() {}
364    }
365
366    public static final class SettingsColumns {
367        public static final String LABELS_INCLUDED = "labelsIncluded";
368        public static final String LABELS_PARTIAL = "labelsPartial";
369        public static final String CONVERSATION_AGE_DAYS =
370                "conversationAgeDays";
371        public static final String MAX_ATTACHMENET_SIZE_MB =
372                "maxAttachmentSize";
373    }
374
375    /**
376     * These flags can be included as Selection Arguments when
377     * querying the provider.
378     */
379    public static class SelectionArguments {
380        private SelectionArguments() {
381            // forbid instantiation
382        }
383
384        /**
385         * Specifies that you do NOT wish the returned cursor to
386         * become the Active Network Cursor.  If you do not include
387         * this flag as a selectionArg, the new cursor will become the
388         * Active Network Cursor by default.
389         */
390        public static final String DO_NOT_BECOME_ACTIVE_NETWORK_CURSOR =
391                "SELECTION_ARGUMENT_DO_NOT_BECOME_ACTIVE_NETWORK_CURSOR";
392    }
393
394    // These are the projections that we need when getting cursors from the
395    // content provider.
396    private static String[] CONVERSATION_PROJECTION = {
397            ConversationColumns.ID,
398            ConversationColumns.SUBJECT,
399            ConversationColumns.SNIPPET,
400            ConversationColumns.FROM,
401            ConversationColumns.DATE,
402            ConversationColumns.PERSONAL_LEVEL,
403            ConversationColumns.LABEL_IDS,
404            ConversationColumns.NUM_MESSAGES,
405            ConversationColumns.MAX_MESSAGE_ID,
406            ConversationColumns.HAS_ATTACHMENTS,
407            ConversationColumns.HAS_MESSAGES_WITH_ERRORS,
408            ConversationColumns.FORCE_ALL_UNREAD};
409    private static String[] MESSAGE_PROJECTION = {
410            MessageColumns.ID,
411            MessageColumns.MESSAGE_ID,
412            MessageColumns.CONVERSATION_ID,
413            MessageColumns.SUBJECT,
414            MessageColumns.SNIPPET,
415            MessageColumns.FROM,
416            MessageColumns.TO,
417            MessageColumns.CC,
418            MessageColumns.BCC,
419            MessageColumns.REPLY_TO,
420            MessageColumns.DATE_SENT_MS,
421            MessageColumns.DATE_RECEIVED_MS,
422            MessageColumns.LIST_INFO,
423            MessageColumns.PERSONAL_LEVEL,
424            MessageColumns.BODY,
425            MessageColumns.EMBEDS_EXTERNAL_RESOURCES,
426            MessageColumns.LABEL_IDS,
427            MessageColumns.JOINED_ATTACHMENT_INFOS,
428            MessageColumns.ERROR};
429    private static String[] LABEL_PROJECTION = {
430            BaseColumns._ID,
431            LabelColumns.CANONICAL_NAME,
432            LabelColumns.NAME,
433            LabelColumns.NUM_CONVERSATIONS,
434            LabelColumns.NUM_UNREAD_CONVERSATIONS};
435    private static String[] SETTINGS_PROJECTION = {
436            SettingsColumns.LABELS_INCLUDED,
437            SettingsColumns.LABELS_PARTIAL,
438            SettingsColumns.CONVERSATION_AGE_DAYS,
439            SettingsColumns.MAX_ATTACHMENET_SIZE_MB,
440    };
441
442    private ContentResolver mContentResolver;
443
444    public Gmail(ContentResolver contentResolver) {
445        mContentResolver = contentResolver;
446    }
447
448    /**
449     * Returns source if source is non-null. Returns the empty string otherwise.
450     */
451    private static String toNonnullString(String source) {
452        if (source == null) {
453            return "";
454        } else {
455            return source;
456        }
457    }
458
459    /**
460     * Behavior for a new cursor: should it become the Active Network
461     * Cursor?  This could potentially lead to bad behavior if someone
462     * else is using the Active Network Cursor, since theirs will stop
463     * being the Active Network Cursor.
464     */
465    public static enum BecomeActiveNetworkCursor {
466        /**
467         * The new cursor should become the one and only Active
468         * Network Cursor.  Any other cursor that might already be the
469         * Active Network Cursor will cease to be so.
470         */
471        YES,
472
473        /**
474         * The new cursor should not become the Active Network
475         * Cursor. Any other cursor that might already be the Active
476         * Network Cursor will continue to be so.
477         */
478        NO
479    }
480
481    /**
482     * Wraps a Cursor in a ConversationCursor
483     *
484     * @param account the account the cursor is associated with
485     * @param cursor The Cursor to wrap
486     * @return a new ConversationCursor
487     */
488    public ConversationCursor getConversationCursorForCursor(String account, Cursor cursor) {
489        if (TextUtils.isEmpty(account)) {
490            throw new IllegalArgumentException("account is empty");
491        }
492        return new ConversationCursor(this, account, cursor);
493    }
494
495    /**
496     * Creates an array of SelectionArguments suitable for passing to the provider's query.
497     * Currently this only handles one flag, but it could be expanded in the future.
498     */
499    private static String[] getSelectionArguments(
500            BecomeActiveNetworkCursor becomeActiveNetworkCursor) {
501        if (BecomeActiveNetworkCursor.NO == becomeActiveNetworkCursor) {
502            return new String[] {SelectionArguments.DO_NOT_BECOME_ACTIVE_NETWORK_CURSOR};
503        } else {
504            // Default behavior; no args required.
505            return null;
506        }
507    }
508
509    /**
510     * Asynchronously gets a cursor over all conversations matching a query. The
511     * query is in Gmail's query syntax. When the operation is complete the handler's
512     * onQueryComplete() method is called with the resulting Cursor.
513     *
514     * @param account run the query on this account
515     * @param handler An AsyncQueryHanlder that will be used to run the query
516     * @param token The token to pass to startQuery, which will be passed back to onQueryComplete
517     * @param query a query in Gmail's query syntax
518     * @param becomeActiveNetworkCursor whether or not the returned
519     * cursor should become the Active Network Cursor
520     */
521    public void runQueryForConversations(String account, AsyncQueryHandler handler, int token,
522            String query, BecomeActiveNetworkCursor becomeActiveNetworkCursor) {
523        if (TextUtils.isEmpty(account)) {
524            throw new IllegalArgumentException("account is empty");
525        }
526        String[] selectionArgs = getSelectionArguments(becomeActiveNetworkCursor);
527        handler.startQuery(token, null, Uri.withAppendedPath(CONVERSATIONS_URI, account),
528                CONVERSATION_PROJECTION, query, selectionArgs, null);
529    }
530
531    /**
532     * Synchronously gets a cursor over all conversations matching a query. The
533     * query is in Gmail's query syntax.
534     *
535     * @param account run the query on this account
536     * @param query a query in Gmail's query syntax
537     * @param becomeActiveNetworkCursor whether or not the returned
538     * cursor should become the Active Network Cursor
539     */
540    public ConversationCursor getConversationCursorForQuery(
541            String account, String query, BecomeActiveNetworkCursor becomeActiveNetworkCursor) {
542        String[] selectionArgs = getSelectionArguments(becomeActiveNetworkCursor);
543        Cursor cursor = mContentResolver.query(
544                Uri.withAppendedPath(CONVERSATIONS_URI, account), CONVERSATION_PROJECTION,
545                query, selectionArgs, null);
546        return new ConversationCursor(this, account, cursor);
547    }
548
549    /**
550     * Gets a message cursor over the single message with the given id.
551     *
552     * @param account get the cursor for messages in this account
553     * @param messageId the id of the message
554     * @return a cursor over the message
555     */
556    public MessageCursor getMessageCursorForMessageId(String account, long messageId) {
557        if (TextUtils.isEmpty(account)) {
558            throw new IllegalArgumentException("account is empty");
559        }
560        Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/" + messageId);
561        Cursor cursor = mContentResolver.query(uri, MESSAGE_PROJECTION, null, null, null);
562        return new MessageCursor(this, mContentResolver, account, cursor);
563    }
564
565    /**
566     * Gets a message cursor over the messages that match the query. Note that
567     * this simply finds all of the messages that match and returns them. It
568     * does not return all messages in conversations where any message matches.
569     *
570     * @param account get the cursor for messages in this account
571     * @param query a query in GMail's query syntax. Currently only queries of
572     *     the form [label:<label>] are supported
573     * @return a cursor over the messages
574     */
575    public MessageCursor getLocalMessageCursorForQuery(String account, String query) {
576        if (TextUtils.isEmpty(account)) {
577            throw new IllegalArgumentException("account is empty");
578        }
579        Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/");
580        Cursor cursor = mContentResolver.query(uri, MESSAGE_PROJECTION, query, null, null);
581        return new MessageCursor(this, mContentResolver, account, cursor);
582    }
583
584    /**
585     * Gets a cursor over all of the messages in a conversation.
586     *
587     * @param account get the cursor for messages in this account
588     * @param conversationId the id of the converstion to fetch messages for
589     * @return a cursor over messages in the conversation
590     */
591    public MessageCursor getMessageCursorForConversationId(String account, long conversationId) {
592        if (TextUtils.isEmpty(account)) {
593            throw new IllegalArgumentException("account is empty");
594        }
595        Uri uri = Uri.parse(
596                AUTHORITY_PLUS_CONVERSATIONS + account + "/" + conversationId + "/messages");
597        Cursor cursor = mContentResolver.query(
598                uri, MESSAGE_PROJECTION, null, null, null);
599        return new MessageCursor(this, mContentResolver, account, cursor);
600    }
601
602    /**
603     * Expunge the indicated message. One use of this is to discard drafts.
604     *
605     * @param account the account of the message id
606     * @param messageId the id of the message to expunge
607     */
608    public void expungeMessage(String account, long messageId) {
609        if (TextUtils.isEmpty(account)) {
610            throw new IllegalArgumentException("account is empty");
611        }
612        Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/" + messageId);
613        mContentResolver.delete(uri, null, null);
614    }
615
616    /**
617     * Adds or removes the label on the conversation.
618     *
619     * @param account the account of the conversation
620     * @param conversationId the conversation
621     * @param maxServerMessageId the highest message id to whose labels should be changed. Note that
622     *   everywhere else in this file messageId means local message id but here you need to use a
623     *   server message id.
624     * @param label the label to add or remove
625     * @param add true to add the label, false to remove it
626     */
627    public void addOrRemoveLabelOnConversation(
628            String account, long conversationId, long maxServerMessageId, String label,
629            boolean add) {
630        if (TextUtils.isEmpty(account)) {
631            throw new IllegalArgumentException("account is empty");
632        }
633        if (add) {
634            Uri uri = Uri.parse(
635                    AUTHORITY_PLUS_CONVERSATIONS + account + "/" + conversationId + "/labels");
636            ContentValues values = new ContentValues();
637            values.put(LabelColumns.CANONICAL_NAME, label);
638            values.put(ConversationColumns.MAX_MESSAGE_ID, maxServerMessageId);
639            mContentResolver.insert(uri, values);
640        } else {
641            String encodedLabel;
642            try {
643                encodedLabel = URLEncoder.encode(label, "utf-8");
644            } catch (UnsupportedEncodingException e) {
645                throw new RuntimeException(e);
646            }
647            Uri uri = Uri.parse(
648                    AUTHORITY_PLUS_CONVERSATIONS + account + "/"
649                            + conversationId + "/labels/" + encodedLabel);
650            mContentResolver.delete(
651                    uri, ConversationColumns.MAX_MESSAGE_ID, new String[]{"" + maxServerMessageId});
652        }
653    }
654
655    /**
656     * Adds or removes the label on the message.
657     *
658     * @param contentResolver the content resolver.
659     * @param account the account of the message
660     * @param conversationId the conversation containing the message
661     * @param messageId the id of the message to whose labels should be changed
662     * @param label the label to add or remove
663     * @param add true to add the label, false to remove it
664     */
665    public static void addOrRemoveLabelOnMessage(ContentResolver contentResolver, String account,
666            long conversationId, long messageId, String label, boolean add) {
667
668        // conversationId is unused but we want to start passing it whereever we pass a message id.
669        if (add) {
670            Uri uri = Uri.parse(
671                    AUTHORITY_PLUS_MESSAGES + account + "/" + messageId + "/labels");
672            ContentValues values = new ContentValues();
673            values.put(LabelColumns.CANONICAL_NAME, label);
674            contentResolver.insert(uri, values);
675        } else {
676            String encodedLabel;
677            try {
678                encodedLabel = URLEncoder.encode(label, "utf-8");
679            } catch (UnsupportedEncodingException e) {
680                throw new RuntimeException(e);
681            }
682            Uri uri = Uri.parse(
683                    AUTHORITY_PLUS_MESSAGES + account + "/" + messageId
684                    + "/labels/" + encodedLabel);
685            contentResolver.delete(uri, null, null);
686        }
687    }
688
689    /**
690     * The mail provider will send an intent when certain changes happen in certain labels.
691     * Currently those labels are inbox and voicemail.
692     *
693     * <p>The intent will have the action ACTION_PROVIDER_CHANGED and the extras mentioned below.
694     * The data for the intent will be content://gmail-ls/unread/<name of label>.
695     *
696     * <p>The goal is to support the following user experience:<ul>
697     *   <li>When present the new mail indicator reports the number of unread conversations in the
698     *   inbox (or some other label).</li>
699     *   <li>When the user views the inbox the indicator is removed immediately. They do not have to
700     *   read all of the conversations.</li>
701     *   <li>If more mail arrives the indicator reappears and shows the total number of unread
702     *   conversations in the inbox.</li>
703     *   <li>If the user reads the new conversations on the web the indicator disappears on the
704     *   phone since there is no unread mail in the inbox that the user hasn't seen.</li>
705     *   <li>The phone should vibrate/etc when it transitions from having no unseen unread inbox
706     *   mail to having some.</li>
707     */
708
709    /** The account in which the change occurred. */
710    static public final String PROVIDER_CHANGED_EXTRA_ACCOUNT = "account";
711
712    /** The number of unread conversations matching the label. */
713    static public final String PROVIDER_CHANGED_EXTRA_COUNT = "count";
714
715    /** Whether to get the user's attention, perhaps by vibrating. */
716    static public final String PROVIDER_CHANGED_EXTRA_GET_ATTENTION = "getAttention";
717
718    /**
719     * A label that is attached to all of the conversations being notified about. This enables the
720     * receiver of a notification to get a list of matching conversations.
721     */
722    static public final String PROVIDER_CHANGED_EXTRA_TAG_LABEL = "tagLabel";
723
724    /**
725     * Settings for which conversations should be synced to the phone.
726     * Conversations are synced if any message matches any of the following
727     * criteria:
728     *
729     * <ul>
730     *   <li>the message has a label in the include set</li>
731     *   <li>the message is no older than conversationAgeDays and has a label in the partial set.
732     *   </li>
733     *   <li>also, pending changes on the server: the message has no user-controllable labels.</li>
734     * </ul>
735     *
736     * <p>A user-controllable label is a user-defined label or star, inbox,
737     * trash, spam, etc. LABEL_UNREAD is not considered user-controllable.
738     */
739    public static class Settings {
740        public long conversationAgeDays;
741        public long maxAttachmentSizeMb;
742        public String[] labelsIncluded;
743        public String[] labelsPartial;
744    }
745
746    /**
747     * Returns the settings.
748     * @param account the account whose setting should be retrieved
749     */
750    public Settings getSettings(String account) {
751        if (TextUtils.isEmpty(account)) {
752            throw new IllegalArgumentException("account is empty");
753        }
754        Settings settings = new Settings();
755        Cursor cursor = mContentResolver.query(
756                Uri.withAppendedPath(SETTINGS_URI, account), SETTINGS_PROJECTION, null, null, null);
757        cursor.moveToNext();
758        settings.labelsIncluded = TextUtils.split(cursor.getString(0), SPACE_SEPARATOR_PATTERN);
759        settings.labelsPartial = TextUtils.split(cursor.getString(1), SPACE_SEPARATOR_PATTERN);
760        settings.conversationAgeDays = Long.parseLong(cursor.getString(2));
761        settings.maxAttachmentSizeMb = Long.parseLong(cursor.getString(3));
762        cursor.close();
763        return settings;
764    }
765
766    /**
767     * Sets the settings. A sync will be scheduled automatically.
768     */
769    public void setSettings(String account, Settings settings) {
770        if (TextUtils.isEmpty(account)) {
771            throw new IllegalArgumentException("account is empty");
772        }
773        ContentValues values = new ContentValues();
774        values.put(
775                SettingsColumns.LABELS_INCLUDED,
776                TextUtils.join(" ", settings.labelsIncluded));
777        values.put(
778                SettingsColumns.LABELS_PARTIAL,
779                TextUtils.join(" ", settings.labelsPartial));
780        values.put(
781                SettingsColumns.CONVERSATION_AGE_DAYS,
782                settings.conversationAgeDays);
783        values.put(
784                SettingsColumns.MAX_ATTACHMENET_SIZE_MB,
785                settings.maxAttachmentSizeMb);
786        mContentResolver.update(Uri.withAppendedPath(SETTINGS_URI, account), values, null, null);
787    }
788
789    /**
790     * Uses sender instructions to build a formatted string.
791     *
792     * <p>Sender list instructions contain compact information about the sender list. Most work that
793     * can be done without knowing how much room will be availble for the sender list is done when
794     * creating the instructions.
795     *
796     * <p>The instructions string consists of tokens separated by SENDER_LIST_SEPARATOR. Here are
797     * the tokens, one per line:<ul>
798     * <li><tt>n</tt></li>
799     * <li><em>int</em>, the number of non-draft messages in the conversation</li>
800     * <li><tt>d</tt</li>
801     * <li><em>int</em>, the number of drafts in the conversation</li>
802     * <li><tt>l</tt></li>
803     * <li><em>literal html to be included in the output</em></li>
804     * <li><tt>s</tt> indicates that the message is sending (in the outbox without errors)</li>
805     * <li><tt>f</tt> indicates that the message failed to send (in the outbox with errors)</li>
806     * <li><em>for each message</em><ul>
807     *   <li><em>int</em>, 0 for read, 1 for unread</li>
808     *   <li><em>int</em>, the priority of the message. Zero is the most important</li>
809     *   <li><em>text</em>, the sender text or blank for messages from 'me'</li>
810     * </ul></li>
811     * <li><tt>e</tt> to indicate that one or more messages have been elided</li>
812     *
813     * <p>The instructions indicate how many messages and drafts are in the conversation and then
814     * describe the most important messages in order, indicating the priority of each message and
815     * whether the message is unread.
816     *
817     * @param instructions instructions as described above
818     * @param sb the SpannableStringBuilder to append to
819     * @param maxChars the number of characters available to display the text
820     * @param unreadStyle the CharacterStyle for unread messages, or null
821     * @param draftsStyle the CharacterStyle for draft messages, or null
822     * @param sendingString the string to use when there are messages scheduled to be sent
823     * @param sendFailedString the string to use when there are messages that mailed to send
824     * @param meString the string to use for messages sent by this user
825     * @param draftString the string to use for "Draft"
826     * @param draftPluralString the string to use for "Drafts"
827     */
828    public static void getSenderSnippet(
829            String instructions, SpannableStringBuilder sb, int maxChars,
830            CharacterStyle unreadStyle,
831            CharacterStyle draftsStyle,
832            CharSequence meString, CharSequence draftString, CharSequence draftPluralString,
833            CharSequence sendingString, CharSequence sendFailedString,
834            boolean forceAllUnread, boolean forceAllRead) {
835        assert !(forceAllUnread && forceAllRead);
836        boolean unreadStatusIsForced = forceAllUnread || forceAllRead;
837        boolean forcedUnreadStatus = forceAllUnread;
838
839        // Measure each fragment. It's ok to iterate over the entire set of fragments because it is
840        // never a long list, even if there are many senders.
841        final Map<Integer, Integer> priorityToLength = sPriorityToLength;
842        priorityToLength.clear();
843
844        int maxFoundPriority = Integer.MIN_VALUE;
845        int numMessages = 0;
846        int numDrafts = 0;
847        CharSequence draftsFragment = "";
848        CharSequence sendingFragment = "";
849        CharSequence sendFailedFragment = "";
850
851        sSenderListSplitter.setString(instructions);
852        int numFragments = 0;
853        String[] fragments = sSenderFragments;
854        int currentSize = fragments.length;
855        while (sSenderListSplitter.hasNext()) {
856            fragments[numFragments++] = sSenderListSplitter.next();
857            if (numFragments == currentSize) {
858                sSenderFragments = new String[2 * currentSize];
859                System.arraycopy(fragments, 0, sSenderFragments, 0, currentSize);
860                currentSize *= 2;
861                fragments = sSenderFragments;
862            }
863        }
864
865        for (int i = 0; i < numFragments;) {
866            String fragment0 = fragments[i++];
867            if ("".equals(fragment0)) {
868                // This should be the final fragment.
869            } else if (Gmail.SENDER_LIST_TOKEN_ELIDED.equals(fragment0)) {
870                // ignore
871            } else if (Gmail.SENDER_LIST_TOKEN_NUM_MESSAGES.equals(fragment0)) {
872                numMessages = Integer.valueOf(fragments[i++]);
873            } else if (Gmail.SENDER_LIST_TOKEN_NUM_DRAFTS.equals(fragment0)) {
874                String numDraftsString = fragments[i++];
875                numDrafts = Integer.parseInt(numDraftsString);
876                draftsFragment = numDrafts == 1 ? draftString :
877                        draftPluralString + " (" + numDraftsString + ")";
878            } else if (Gmail.SENDER_LIST_TOKEN_LITERAL.equals(fragment0)) {
879                sb.append(Html.fromHtml(fragments[i++]));
880                return;
881            } else if (Gmail.SENDER_LIST_TOKEN_SENDING.equals(fragment0)) {
882                sendingFragment = sendingString;
883            } else if (Gmail.SENDER_LIST_TOKEN_SEND_FAILED.equals(fragment0)) {
884                sendFailedFragment = sendFailedString;
885            } else {
886                String priorityString = fragments[i++];
887                CharSequence nameString = fragments[i++];
888                if (nameString.length() == 0) nameString = meString;
889                int priority = Integer.parseInt(priorityString);
890                priorityToLength.put(priority, nameString.length());
891                maxFoundPriority = Math.max(maxFoundPriority, priority);
892            }
893        }
894        String numMessagesFragment =
895                (numMessages != 0) ? " (" + Integer.toString(numMessages + numDrafts) + ")" : "";
896
897        // Don't allocate fixedFragment unless we need it
898        SpannableStringBuilder fixedFragment = null;
899        int fixedFragmentLength = 0;
900        if (draftsFragment.length() != 0) {
901            if (fixedFragment == null) {
902                fixedFragment = new SpannableStringBuilder();
903            }
904            fixedFragment.append(draftsFragment);
905            if (draftsStyle != null) {
906                fixedFragment.setSpan(
907                        CharacterStyle.wrap(draftsStyle),
908                        0, fixedFragment.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
909            }
910        }
911        if (sendingFragment.length() != 0) {
912            if (fixedFragment == null) {
913                fixedFragment = new SpannableStringBuilder();
914            }
915            if (fixedFragment.length() != 0) fixedFragment.append(", ");
916            fixedFragment.append(sendingFragment);
917        }
918        if (sendFailedFragment.length() != 0) {
919            if (fixedFragment == null) {
920                fixedFragment = new SpannableStringBuilder();
921            }
922            if (fixedFragment.length() != 0) fixedFragment.append(", ");
923            fixedFragment.append(sendFailedFragment);
924        }
925
926        if (fixedFragment != null) {
927            fixedFragmentLength = fixedFragment.length();
928        }
929
930        final boolean normalMessagesExist =
931                numMessagesFragment.length() != 0 || maxFoundPriority != Integer.MIN_VALUE;
932        String preFixedFragement = "";
933        if (normalMessagesExist && fixedFragmentLength != 0) {
934            preFixedFragement = ", ";
935        }
936        int maxPriorityToInclude = -1; // inclusive
937        int numCharsUsed =
938                numMessagesFragment.length() + preFixedFragement.length() + fixedFragmentLength;
939        int numSendersUsed = 0;
940        while (maxPriorityToInclude < maxFoundPriority) {
941            if (priorityToLength.containsKey(maxPriorityToInclude + 1)) {
942                int length = numCharsUsed + priorityToLength.get(maxPriorityToInclude + 1);
943                if (numCharsUsed > 0) length += 2;
944                // We must show at least two senders if they exist. If we don't have space for both
945                // then we will truncate names.
946                if (length > maxChars && numSendersUsed >= 2) {
947                    break;
948                }
949                numCharsUsed = length;
950                numSendersUsed++;
951            }
952            maxPriorityToInclude++;
953        }
954
955        int numCharsToRemovePerWord = 0;
956        if (numCharsUsed > maxChars) {
957            numCharsToRemovePerWord = (numCharsUsed - maxChars) / numSendersUsed;
958        }
959
960        boolean elided = false;
961        for (int i = 0; i < numFragments;) {
962            String fragment0 = fragments[i++];
963            if ("".equals(fragment0)) {
964                // This should be the final fragment.
965            } else if (SENDER_LIST_TOKEN_ELIDED.equals(fragment0)) {
966                elided = true;
967            } else if (SENDER_LIST_TOKEN_NUM_MESSAGES.equals(fragment0)) {
968                i++;
969            } else if (SENDER_LIST_TOKEN_NUM_DRAFTS.equals(fragment0)) {
970                i++;
971            } else if (SENDER_LIST_TOKEN_SENDING.equals(fragment0)) {
972            } else if (SENDER_LIST_TOKEN_SEND_FAILED.equals(fragment0)) {
973            } else {
974                final String unreadString = fragment0;
975                final String priorityString = fragments[i++];
976                String nameString = fragments[i++];
977                if (nameString.length() == 0) nameString = meString.toString();
978                if (numCharsToRemovePerWord != 0) {
979                    nameString = nameString.substring(
980                            0, Math.max(nameString.length() - numCharsToRemovePerWord, 0));
981                }
982                final boolean unread = unreadStatusIsForced
983                        ? forcedUnreadStatus : Integer.parseInt(unreadString) != 0;
984                final int priority = Integer.parseInt(priorityString);
985                if (priority <= maxPriorityToInclude) {
986                    if (sb.length() != 0) {
987                        sb.append(elided ? " .. " : ", ");
988                    }
989                    elided = false;
990                    int pos = sb.length();
991                    sb.append(nameString);
992                    if (unread && unreadStyle != null) {
993                        sb.setSpan(CharacterStyle.wrap(unreadStyle),
994                                pos, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
995                    }
996                } else {
997                    elided = true;
998                }
999            }
1000        }
1001        sb.append(numMessagesFragment);
1002        if (fixedFragmentLength != 0) {
1003            sb.append(preFixedFragement);
1004            sb.append(fixedFragment);
1005        }
1006    }
1007
1008    /**
1009     * This is a cursor that only defines methods to move throught the results
1010     * and register to hear about changes. All access to the data is left to
1011     * subinterfaces.
1012     */
1013    public static class MailCursor extends ContentObserver {
1014
1015        // A list of observers of this cursor.
1016        private Set<MailCursorObserver> mObservers;
1017
1018        // Updated values are accumulated here before being written out if the
1019        // cursor is asked to persist the changes.
1020        private ContentValues mUpdateValues;
1021
1022        protected Cursor mCursor;
1023        protected String mAccount;
1024
1025        public Cursor getCursor() {
1026            return mCursor;
1027        }
1028
1029        /**
1030         * Constructs the MailCursor given a regular cursor, registering as a
1031         * change observer of the cursor.
1032         * @param account the account the cursor is associated with
1033         * @param cursor the underlying cursor
1034         */
1035        protected MailCursor(String account, Cursor cursor) {
1036            super(new Handler());
1037            mObservers = new HashSet<MailCursorObserver>();
1038            mCursor = cursor;
1039            mAccount = account;
1040            if (mCursor != null) mCursor.registerContentObserver(this);
1041        }
1042
1043        /**
1044         * Gets the account associated with this cursor.
1045         * @return the account.
1046         */
1047        public String getAccount() {
1048            return mAccount;
1049        }
1050
1051        protected void checkThread() {
1052            // Turn this on when activity code no longer runs in the sync thread
1053            // after notifications of changes.
1054//            Thread currentThread = Thread.currentThread();
1055//            if (currentThread != mThread) {
1056//                throw new RuntimeException("Accessed from the wrong thread");
1057//            }
1058        }
1059
1060        /**
1061         * Lazily constructs a map of update values to apply to the database
1062         * if requested. This map is cleared out when we move to a different
1063         * item in the result set.
1064         *
1065         * @return a map of values to be applied by an update.
1066         */
1067        protected ContentValues getUpdateValues() {
1068            if (mUpdateValues == null) {
1069                mUpdateValues = new ContentValues();
1070            }
1071            return mUpdateValues;
1072        }
1073
1074        /**
1075         * Called whenever mCursor is changed to point to a different row.
1076         * Subclasses should override this if they need to clear out state
1077         * when this happens.
1078         *
1079         * Subclasses must call the inherited version if they override this.
1080         */
1081        protected void onCursorPositionChanged() {
1082            mUpdateValues = null;
1083        }
1084
1085        // ********* MailCursor
1086
1087        /**
1088         * Returns the numbers of rows in the cursor.
1089         *
1090         * @return the number of rows in the cursor.
1091         */
1092        final public int count() {
1093            if (mCursor != null) {
1094                return mCursor.getCount();
1095            } else {
1096                return 0;
1097            }
1098        }
1099
1100        /**
1101         * @return the current position of this cursor, or -1 if this cursor
1102         * has not been initialized.
1103         */
1104        final public int position() {
1105            if (mCursor != null) {
1106                return mCursor.getPosition();
1107            } else {
1108                return -1;
1109            }
1110        }
1111
1112        /**
1113         * Move the cursor to an absolute position. The valid
1114         * range of vaues is -1 &lt;= position &lt;= count.
1115         *
1116         * <p>This method will return true if the request destination was
1117         * reachable, otherwise it returns false.
1118         *
1119         * @param position the zero-based position to move to.
1120         * @return whether the requested move fully succeeded.
1121         */
1122        final public boolean moveTo(int position) {
1123            checkCursor();
1124            checkThread();
1125            boolean moved = mCursor.moveToPosition(position);
1126            if (moved) onCursorPositionChanged();
1127            return moved;
1128        }
1129
1130        /**
1131         * Move the cursor to the next row.
1132         *
1133         * <p>This method will return false if the cursor is already past the
1134         * last entry in the result set.
1135         *
1136         * @return whether the move succeeded.
1137         */
1138        final public boolean next() {
1139            checkCursor();
1140            checkThread();
1141            boolean moved = mCursor.moveToNext();
1142            if (moved) onCursorPositionChanged();
1143            return moved;
1144        }
1145
1146        /**
1147         * Release all resources and locks associated with the cursor. The
1148         * cursor will not be valid after this function is called.
1149         */
1150        final public void release() {
1151            if (mCursor != null) {
1152                mCursor.unregisterContentObserver(this);
1153                mCursor.deactivate();
1154            }
1155        }
1156
1157        final public void registerContentObserver(ContentObserver observer) {
1158            mCursor.registerContentObserver(observer);
1159        }
1160
1161        final public void unregisterContentObserver(ContentObserver observer) {
1162            mCursor.unregisterContentObserver(observer);
1163        }
1164
1165        final public void registerDataSetObserver(DataSetObserver observer) {
1166            mCursor.registerDataSetObserver(observer);
1167        }
1168
1169        final public void unregisterDataSetObserver(DataSetObserver observer) {
1170            mCursor.unregisterDataSetObserver(observer);
1171        }
1172
1173        /**
1174         * Register an observer to hear about changes to the cursor.
1175         *
1176         * @param observer the observer to register
1177         */
1178        final public void registerObserver(MailCursorObserver observer) {
1179            mObservers.add(observer);
1180        }
1181
1182        /**
1183         * Unregister an observer.
1184         *
1185         * @param observer the observer to unregister
1186         */
1187        final public void unregisterObserver(MailCursorObserver observer) {
1188            mObservers.remove(observer);
1189        }
1190
1191        // ********* ContentObserver
1192
1193        @Override
1194        final public boolean deliverSelfNotifications() {
1195            return false;
1196        }
1197
1198        @Override
1199        public void onChange(boolean selfChange) {
1200            if (DEBUG) {
1201                Log.d(TAG, "MailCursor is notifying " + mObservers.size() + " observers");
1202            }
1203            for (MailCursorObserver o: mObservers) {
1204                o.onCursorChanged(this);
1205            }
1206        }
1207
1208        protected void checkCursor() {
1209            if (mCursor == null) {
1210                throw new IllegalStateException(
1211                        "cannot read from an insertion cursor");
1212            }
1213        }
1214
1215        /**
1216         * Returns the string value of the column, or "" if the value is null.
1217         */
1218        protected String getStringInColumn(int columnIndex) {
1219            checkCursor();
1220            return toNonnullString(mCursor.getString(columnIndex));
1221        }
1222    }
1223
1224    /**
1225     * A MailCursor observer is notified of changes to the result set of a
1226     * cursor.
1227     */
1228    public interface MailCursorObserver {
1229
1230        /**
1231         * Called when the result set of a cursor has changed.
1232         *
1233         * @param cursor the cursor whose result set has changed.
1234         */
1235        void onCursorChanged(MailCursor cursor);
1236    }
1237
1238    /**
1239     * A cursor over labels.
1240     */
1241    public final class LabelCursor extends MailCursor {
1242
1243        private int mNameIndex;
1244        private int mNumConversationsIndex;
1245        private int mNumUnreadConversationsIndex;
1246
1247        private LabelCursor(String account, Cursor cursor) {
1248            super(account, cursor);
1249
1250            mNameIndex = mCursor.getColumnIndexOrThrow(LabelColumns.CANONICAL_NAME);
1251            mNumConversationsIndex =
1252                    mCursor.getColumnIndexOrThrow(LabelColumns.NUM_CONVERSATIONS);
1253            mNumUnreadConversationsIndex = mCursor.getColumnIndexOrThrow(
1254                    LabelColumns.NUM_UNREAD_CONVERSATIONS);
1255        }
1256
1257        /**
1258         * Gets the canonical name of the current label.
1259         *
1260         * @return the current label's name.
1261         */
1262        public String getName() {
1263            return getStringInColumn(mNameIndex);
1264        }
1265
1266        /**
1267         * Gets the number of conversations with this label.
1268         *
1269         * @return the number of conversations with this label.
1270         */
1271        public int getNumConversations() {
1272            return mCursor.getInt(mNumConversationsIndex);
1273        }
1274
1275        /**
1276         * Gets the number of unread conversations with this label.
1277         *
1278         * @return the number of unread conversations with this label.
1279         */
1280        public int getNumUnreadConversations() {
1281            return mCursor.getInt(mNumUnreadConversationsIndex);
1282        }
1283    }
1284
1285    /**
1286     * This is a map of labels. TODO: make it observable.
1287     */
1288    public static final class LabelMap extends Observable {
1289        private final static ContentValues EMPTY_CONTENT_VALUES = new ContentValues();
1290
1291        private ContentQueryMap mQueryMap;
1292        private SortedSet<String> mSortedUserLabels;
1293        private Map<String, Long> mCanonicalNameToId;
1294
1295        private long mLabelIdSent;
1296        private long mLabelIdInbox;
1297        private long mLabelIdDraft;
1298        private long mLabelIdUnread;
1299        private long mLabelIdTrash;
1300        private long mLabelIdSpam;
1301        private long mLabelIdStarred;
1302        private long mLabelIdChat;
1303        private long mLabelIdVoicemail;
1304        private long mLabelIdIgnored;
1305        private long mLabelIdVoicemailInbox;
1306        private long mLabelIdCached;
1307        private long mLabelIdOutbox;
1308
1309        private boolean mLabelsSynced = false;
1310
1311        public LabelMap(ContentResolver contentResolver, String account, boolean keepUpdated) {
1312            if (TextUtils.isEmpty(account)) {
1313                throw new IllegalArgumentException("account is empty");
1314            }
1315            Cursor cursor = contentResolver.query(
1316                    Uri.withAppendedPath(LABELS_URI, account), LABEL_PROJECTION, null, null, null);
1317            init(cursor, keepUpdated);
1318        }
1319
1320        public LabelMap(Cursor cursor, boolean keepUpdated) {
1321            init(cursor, keepUpdated);
1322        }
1323
1324        private void init(Cursor cursor, boolean keepUpdated) {
1325            mQueryMap = new ContentQueryMap(cursor, BaseColumns._ID, keepUpdated, null);
1326            mSortedUserLabels = new TreeSet<String>(java.text.Collator.getInstance());
1327            mCanonicalNameToId = Maps.newHashMap();
1328            updateDataStructures();
1329            mQueryMap.addObserver(new Observer() {
1330                public void update(Observable observable, Object data) {
1331                    updateDataStructures();
1332                    setChanged();
1333                    notifyObservers();
1334                }
1335            });
1336        }
1337
1338        /**
1339         * @return whether at least some labels have been synced.
1340         */
1341        public boolean labelsSynced() {
1342            return mLabelsSynced;
1343        }
1344
1345        /**
1346         * Updates the data structures that are maintained separately from mQueryMap after the query
1347         * map has changed.
1348         */
1349        private void updateDataStructures() {
1350            mSortedUserLabels.clear();
1351            mCanonicalNameToId.clear();
1352            for (Map.Entry<String, ContentValues> row : mQueryMap.getRows().entrySet()) {
1353                long labelId = Long.valueOf(row.getKey());
1354                String canonicalName = row.getValue().getAsString(LabelColumns.CANONICAL_NAME);
1355                if (isLabelUserDefined(canonicalName)) {
1356                    mSortedUserLabels.add(canonicalName);
1357                }
1358                mCanonicalNameToId.put(canonicalName, labelId);
1359
1360                if (LABEL_SENT.equals(canonicalName)) {
1361                    mLabelIdSent = labelId;
1362                } else if (LABEL_INBOX.equals(canonicalName)) {
1363                    mLabelIdInbox = labelId;
1364                } else if (LABEL_DRAFT.equals(canonicalName)) {
1365                    mLabelIdDraft = labelId;
1366                } else if (LABEL_UNREAD.equals(canonicalName)) {
1367                    mLabelIdUnread = labelId;
1368                } else if (LABEL_TRASH.equals(canonicalName)) {
1369                    mLabelIdTrash = labelId;
1370                } else if (LABEL_SPAM.equals(canonicalName)) {
1371                    mLabelIdSpam = labelId;
1372                } else if (LABEL_STARRED.equals(canonicalName)) {
1373                    mLabelIdStarred = labelId;
1374                } else if (LABEL_CHAT.equals(canonicalName)) {
1375                    mLabelIdChat = labelId;
1376                } else if (LABEL_IGNORED.equals(canonicalName)) {
1377                    mLabelIdIgnored = labelId;
1378                } else if (LABEL_VOICEMAIL.equals(canonicalName)) {
1379                    mLabelIdVoicemail = labelId;
1380                } else if (LABEL_VOICEMAIL_INBOX.equals(canonicalName)) {
1381                    mLabelIdVoicemailInbox = labelId;
1382                } else if (LABEL_CACHED.equals(canonicalName)) {
1383                    mLabelIdCached = labelId;
1384                } else if (LABEL_OUTBOX.equals(canonicalName)) {
1385                    mLabelIdOutbox = labelId;
1386                }
1387                mLabelsSynced = mLabelIdSent != 0
1388                    && mLabelIdInbox != 0
1389                    && mLabelIdDraft != 0
1390                    && mLabelIdUnread != 0
1391                    && mLabelIdTrash != 0
1392                    && mLabelIdSpam != 0
1393                    && mLabelIdStarred != 0
1394                    && mLabelIdChat != 0
1395                    && mLabelIdIgnored != 0
1396                    && mLabelIdVoicemail != 0;
1397            }
1398        }
1399
1400        public long getLabelIdSent() {
1401            checkLabelsSynced();
1402            return mLabelIdSent;
1403        }
1404
1405        public long getLabelIdInbox() {
1406            checkLabelsSynced();
1407            return mLabelIdInbox;
1408        }
1409
1410        public long getLabelIdDraft() {
1411            checkLabelsSynced();
1412            return mLabelIdDraft;
1413        }
1414
1415        public long getLabelIdUnread() {
1416            checkLabelsSynced();
1417            return mLabelIdUnread;
1418        }
1419
1420        public long getLabelIdTrash() {
1421            checkLabelsSynced();
1422            return mLabelIdTrash;
1423        }
1424
1425        public long getLabelIdSpam() {
1426            checkLabelsSynced();
1427            return mLabelIdSpam;
1428        }
1429
1430        public long getLabelIdStarred() {
1431            checkLabelsSynced();
1432            return mLabelIdStarred;
1433        }
1434
1435        public long getLabelIdChat() {
1436            checkLabelsSynced();
1437            return mLabelIdChat;
1438        }
1439
1440        public long getLabelIdIgnored() {
1441            checkLabelsSynced();
1442            return mLabelIdIgnored;
1443        }
1444
1445        public long getLabelIdVoicemail() {
1446            checkLabelsSynced();
1447            return mLabelIdVoicemail;
1448        }
1449
1450        public long getLabelIdVoicemailInbox() {
1451            checkLabelsSynced();
1452            return mLabelIdVoicemailInbox;
1453        }
1454
1455        public long getLabelIdCached() {
1456            checkLabelsSynced();
1457            return mLabelIdCached;
1458        }
1459
1460        public long getLabelIdOutbox() {
1461            checkLabelsSynced();
1462            return mLabelIdOutbox;
1463        }
1464
1465        private void checkLabelsSynced() {
1466            if (!labelsSynced()) {
1467                throw new IllegalStateException("LabelMap not initalized");
1468            }
1469        }
1470
1471        /** Returns the list of user-defined labels in alphabetical order. */
1472        public SortedSet<String> getSortedUserLabels() {
1473            return mSortedUserLabels;
1474        }
1475
1476        private static final List<String> SORTED_USER_MEANINGFUL_SYSTEM_LABELS =
1477                Lists.newArrayList(
1478                        LABEL_INBOX, LABEL_STARRED, LABEL_CHAT, LABEL_SENT,
1479                        LABEL_OUTBOX, LABEL_DRAFT, LABEL_ALL,
1480                        LABEL_SPAM, LABEL_TRASH);
1481
1482
1483        private static final Set<String> USER_MEANINGFUL_SYSTEM_LABELS_SET =
1484                Sets.newHashSet(
1485                        SORTED_USER_MEANINGFUL_SYSTEM_LABELS.toArray(
1486                                new String[]{}));
1487
1488        public static List<String> getSortedUserMeaningfulSystemLabels() {
1489            return SORTED_USER_MEANINGFUL_SYSTEM_LABELS;
1490        }
1491
1492        public static Set<String> getUserMeaningfulSystemLabelsSet() {
1493            return USER_MEANINGFUL_SYSTEM_LABELS_SET;
1494        }
1495
1496        /**
1497         * If you are ever tempted to remove outbox or draft from this set make sure you have a
1498         * way to stop draft and outbox messages from getting purged before they are sent to the
1499         * server.
1500         */
1501        private static final Set<String> FORCED_INCLUDED_LABELS =
1502                Sets.newHashSet(LABEL_OUTBOX, LABEL_DRAFT);
1503
1504        public static Set<String> getForcedIncludedLabels() {
1505            return FORCED_INCLUDED_LABELS;
1506        }
1507
1508        private static final Set<String> FORCED_INCLUDED_OR_PARTIAL_LABELS =
1509                Sets.newHashSet(LABEL_INBOX);
1510
1511        public static Set<String> getForcedIncludedOrPartialLabels() {
1512            return FORCED_INCLUDED_OR_PARTIAL_LABELS;
1513        }
1514
1515        private static final Set<String> FORCED_UNSYNCED_LABELS =
1516                Sets.newHashSet(LABEL_ALL, LABEL_CHAT, LABEL_SPAM, LABEL_TRASH);
1517
1518        public static Set<String> getForcedUnsyncedLabels() {
1519            return FORCED_UNSYNCED_LABELS;
1520        }
1521
1522        /**
1523         * Returns the number of conversation with a given label.
1524         * @deprecated Use {@link #getLabelId} instead.
1525         */
1526        @Deprecated
1527        public int getNumConversations(String label) {
1528            return getNumConversations(getLabelId(label));
1529        }
1530
1531        /** Returns the number of conversation with a given label. */
1532        public int getNumConversations(long labelId) {
1533            return getLabelIdValues(labelId).getAsInteger(LabelColumns.NUM_CONVERSATIONS);
1534        }
1535
1536        /**
1537         * Returns the number of unread conversation with a given label.
1538         * @deprecated Use {@link #getLabelId} instead.
1539         */
1540        @Deprecated
1541        public int getNumUnreadConversations(String label) {
1542            return getNumUnreadConversations(getLabelId(label));
1543        }
1544
1545        /** Returns the number of unread conversation with a given label. */
1546        public int getNumUnreadConversations(long labelId) {
1547            Integer unreadConversations =
1548                    getLabelIdValues(labelId).getAsInteger(LabelColumns.NUM_UNREAD_CONVERSATIONS);
1549            // There seems to be a race condition here that can get the label maps into a bad
1550            // state and lose state on a particular label.
1551            int result = 0;
1552            if (unreadConversations != null) {
1553                result = unreadConversations < 0 ? 0 : unreadConversations;
1554            }
1555
1556            return result;
1557        }
1558
1559        /**
1560         * @return the canonical name for a label
1561         */
1562        public String getCanonicalName(long labelId) {
1563            return getLabelIdValues(labelId).getAsString(LabelColumns.CANONICAL_NAME);
1564        }
1565
1566        /**
1567         * @return the human name for a label
1568         */
1569        public String getName(long labelId) {
1570            return getLabelIdValues(labelId).getAsString(LabelColumns.NAME);
1571        }
1572
1573        /**
1574         * @return whether a given label is known
1575         */
1576        public boolean hasLabel(long labelId) {
1577            return mQueryMap.getRows().containsKey(Long.toString(labelId));
1578        }
1579
1580        /**
1581         * @return returns the id of a label given the canonical name
1582         * @deprecated this is only needed because most of the UI uses label names instead of ids
1583         */
1584        public long getLabelId(String canonicalName) {
1585            if (mCanonicalNameToId.containsKey(canonicalName)) {
1586                return mCanonicalNameToId.get(canonicalName);
1587            } else {
1588                throw new IllegalArgumentException("Unknown canonical name: " + canonicalName);
1589            }
1590        }
1591
1592        private ContentValues getLabelIdValues(long labelId) {
1593            final ContentValues values = mQueryMap.getValues(Long.toString(labelId));
1594            if (values != null) {
1595                return values;
1596            } else {
1597                return EMPTY_CONTENT_VALUES;
1598            }
1599        }
1600
1601        /** Force the map to requery. This should not be necessary outside tests. */
1602        public void requery() {
1603            mQueryMap.requery();
1604        }
1605
1606        public void close() {
1607            mQueryMap.close();
1608        }
1609    }
1610
1611    private Map<String, Gmail.LabelMap> mLabelMaps = Maps.newHashMap();
1612
1613    public LabelMap getLabelMap(String account) {
1614        Gmail.LabelMap labelMap = mLabelMaps.get(account);
1615        if (labelMap == null) {
1616            labelMap = new Gmail.LabelMap(mContentResolver, account, true /* keepUpdated */);
1617            mLabelMaps.put(account, labelMap);
1618        }
1619        return labelMap;
1620    }
1621
1622    public enum PersonalLevel {
1623        NOT_TO_ME(0),
1624        TO_ME_AND_OTHERS(1),
1625        ONLY_TO_ME(2);
1626
1627        private int mLevel;
1628
1629        PersonalLevel(int level) {
1630            mLevel = level;
1631        }
1632
1633        public int toInt() {
1634            return mLevel;
1635        }
1636
1637        public static PersonalLevel fromInt(int level) {
1638            switch (level) {
1639                case 0: return NOT_TO_ME;
1640                case 1: return TO_ME_AND_OTHERS;
1641                case 2: return ONLY_TO_ME;
1642                default:
1643                    throw new IllegalArgumentException(
1644                            level + " is not a personal level");
1645            }
1646        }
1647    }
1648
1649    /**
1650     * Indicates a version of an attachment.
1651     */
1652    public enum AttachmentRendition {
1653        /**
1654         * The full version of an attachment if it can be handled on the device, otherwise the
1655         * preview.
1656         */
1657        BEST,
1658
1659        /** A smaller or simpler version of the attachment, such as a scaled-down image or an HTML
1660         * version of a document. Not always available.
1661         */
1662        SIMPLE,
1663    }
1664
1665    /**
1666     * The columns that can be requested when querying an attachment's download URI. See
1667     * getAttachmentDownloadUri.
1668     */
1669    public static final class AttachmentColumns implements BaseColumns {
1670
1671        /** Contains a STATUS value from {@link android.provider.Downloads} */
1672        public static final String STATUS = "status";
1673
1674        /**
1675         * The name of the file to open (with ContentProvider.open). If this is empty then continue
1676         * to use the attachment's URI.
1677         *
1678         * TODO: I'm not sure that we need this. See the note in CL 66853-p9.
1679         */
1680        public static final String FILENAME = "filename";
1681    }
1682
1683    /**
1684     * We track where an attachment came from so that we know how to download it and include it
1685     * in new messages.
1686     */
1687    public enum AttachmentOrigin {
1688        /** Extras are "<conversationId>-<messageId>-<partId>". */
1689        SERVER_ATTACHMENT,
1690        /** Extras are "<path>". */
1691        LOCAL_FILE;
1692
1693        private static final String SERVER_EXTRAS_SEPARATOR = "_";
1694
1695        public static String serverExtras(
1696                long conversationId, long messageId, String partId) {
1697            return conversationId + SERVER_EXTRAS_SEPARATOR
1698                    + messageId + SERVER_EXTRAS_SEPARATOR + partId;
1699        }
1700
1701        /**
1702         * @param extras extras as returned by serverExtras
1703         * @return an array of conversationId, messageId, partId (all as strings)
1704         */
1705        public static String[] splitServerExtras(String extras) {
1706            return TextUtils.split(extras, SERVER_EXTRAS_SEPARATOR);
1707        }
1708
1709        public static String localFileExtras(Uri path) {
1710            return path.toString();
1711        }
1712    }
1713
1714    public static final class Attachment {
1715        /** Identifies the attachment uniquely when combined wih a message id.*/
1716        public String partId;
1717
1718        /** The intended filename of the attachment.*/
1719        public String name;
1720
1721        /** The native content type.*/
1722        public String contentType;
1723
1724        /** The size of the attachment in its native form.*/
1725        public int size;
1726
1727        /**
1728         * The content type of the simple version of the attachment. Blank if no simple version is
1729         * available.
1730         */
1731        public String simpleContentType;
1732
1733        public AttachmentOrigin origin;
1734
1735        public String originExtras;
1736
1737        public String toJoinedString() {
1738            return TextUtils.join(
1739                "|", Lists.newArrayList(partId == null ? "" : partId,
1740                                        name.replace("|", ""), contentType,
1741                                        size, simpleContentType,
1742                                        origin.toString(), originExtras));
1743        }
1744
1745        public static Attachment parseJoinedString(String joinedString) {
1746            String[] fragments = TextUtils.split(joinedString, "\\|");
1747            int i = 0;
1748            Attachment attachment = new Attachment();
1749            attachment.partId = fragments[i++];
1750            if (TextUtils.isEmpty(attachment.partId)) {
1751                attachment.partId = null;
1752            }
1753            attachment.name = fragments[i++];
1754            attachment.contentType = fragments[i++];
1755            attachment.size = Integer.parseInt(fragments[i++]);
1756            attachment.simpleContentType = fragments[i++];
1757            attachment.origin = AttachmentOrigin.valueOf(fragments[i++]);
1758            attachment.originExtras = fragments[i++];
1759            return attachment;
1760        }
1761    }
1762
1763    /**
1764     * Any given attachment can come in two different renditions (see
1765     * {@link android.provider.Gmail.AttachmentRendition}) and can be saved to the sd card or to a
1766     * cache. The gmail provider automatically syncs some attachments to the cache. Other
1767     * attachments can be downloaded on demand. Attachments in the cache will be purged as needed to
1768     * save space. Attachments on the SD card must be managed by the user or other software.
1769     *
1770     * @param account which account to use
1771     * @param messageId the id of the mesage with the attachment
1772     * @param attachment the attachment
1773     * @param rendition the desired rendition
1774     * @param saveToSd whether the attachment should be saved to (or loaded from) the sd card or
1775     * @return the URI to ask the content provider to open in order to open an attachment.
1776     */
1777    public static Uri getAttachmentUri(
1778            String account, long messageId, Attachment attachment,
1779            AttachmentRendition rendition, boolean saveToSd) {
1780        if (TextUtils.isEmpty(account)) {
1781            throw new IllegalArgumentException("account is empty");
1782        }
1783        if (attachment.origin == AttachmentOrigin.LOCAL_FILE) {
1784            return Uri.parse(attachment.originExtras);
1785        } else {
1786            return Uri.parse(
1787                    AUTHORITY_PLUS_MESSAGES).buildUpon()
1788                    .appendPath(account).appendPath(Long.toString(messageId))
1789                    .appendPath("attachments").appendPath(attachment.partId)
1790                    .appendPath(rendition.toString())
1791                    .appendPath(Boolean.toString(saveToSd))
1792                    .build();
1793        }
1794    }
1795
1796    /**
1797     * Return the URI to query in order to find out whether an attachment is downloaded.
1798     *
1799     * <p>Querying this will also start a download if necessary. The cursor returned by querying
1800     * this URI can contain the columns in {@link android.provider.Gmail.AttachmentColumns}.
1801     *
1802     * <p>Deleting this URI will cancel the download if it was not started automatically by the
1803     * provider. It will also remove bookkeeping for saveToSd downloads.
1804     *
1805     * @param attachmentUri the attachment URI as returned by getAttachmentUri. The URI's authority
1806     *   Gmail.AUTHORITY. If it is not then you should open the file directly.
1807     */
1808    public static Uri getAttachmentDownloadUri(Uri attachmentUri) {
1809        if (!"content".equals(attachmentUri.getScheme())) {
1810            throw new IllegalArgumentException("Uri's scheme must be 'content': " + attachmentUri);
1811        }
1812        return attachmentUri.buildUpon().appendPath("download").build();
1813    }
1814
1815    public enum CursorStatus {
1816        LOADED,
1817        LOADING,
1818        ERROR, // A network error occurred.
1819    }
1820
1821    /**
1822     * A cursor over messages.
1823     */
1824    public static final class MessageCursor extends MailCursor {
1825
1826        private LabelMap mLabelMap;
1827
1828        private ContentResolver mContentResolver;
1829
1830        /**
1831         * Only valid if mCursor == null, in which case we are inserting a new
1832         * message.
1833         */
1834        long mInReplyToLocalMessageId;
1835        boolean mPreserveAttachments;
1836
1837        private int mIdIndex;
1838        private int mConversationIdIndex;
1839        private int mSubjectIndex;
1840        private int mSnippetIndex;
1841        private int mFromIndex;
1842        private int mToIndex;
1843        private int mCcIndex;
1844        private int mBccIndex;
1845        private int mReplyToIndex;
1846        private int mDateSentMsIndex;
1847        private int mDateReceivedMsIndex;
1848        private int mListInfoIndex;
1849        private int mPersonalLevelIndex;
1850        private int mBodyIndex;
1851        private int mBodyEmbedsExternalResourcesIndex;
1852        private int mLabelIdsIndex;
1853        private int mJoinedAttachmentInfosIndex;
1854        private int mErrorIndex;
1855
1856        private TextUtils.StringSplitter mLabelIdsSplitter = newMessageLabelIdsSplitter();
1857
1858        public MessageCursor(Gmail gmail, ContentResolver cr, String account, Cursor cursor) {
1859            super(account, cursor);
1860            mLabelMap = gmail.getLabelMap(account);
1861            if (cursor == null) {
1862                throw new IllegalArgumentException(
1863                        "null cursor passed to MessageCursor()");
1864            }
1865
1866            mContentResolver = cr;
1867
1868            mIdIndex = mCursor.getColumnIndexOrThrow(MessageColumns.ID);
1869            mConversationIdIndex =
1870                    mCursor.getColumnIndexOrThrow(MessageColumns.CONVERSATION_ID);
1871            mSubjectIndex = mCursor.getColumnIndexOrThrow(MessageColumns.SUBJECT);
1872            mSnippetIndex = mCursor.getColumnIndexOrThrow(MessageColumns.SNIPPET);
1873            mFromIndex = mCursor.getColumnIndexOrThrow(MessageColumns.FROM);
1874            mToIndex = mCursor.getColumnIndexOrThrow(MessageColumns.TO);
1875            mCcIndex = mCursor.getColumnIndexOrThrow(MessageColumns.CC);
1876            mBccIndex = mCursor.getColumnIndexOrThrow(MessageColumns.BCC);
1877            mReplyToIndex = mCursor.getColumnIndexOrThrow(MessageColumns.REPLY_TO);
1878            mDateSentMsIndex =
1879                    mCursor.getColumnIndexOrThrow(MessageColumns.DATE_SENT_MS);
1880            mDateReceivedMsIndex =
1881                    mCursor.getColumnIndexOrThrow(MessageColumns.DATE_RECEIVED_MS);
1882            mListInfoIndex = mCursor.getColumnIndexOrThrow(MessageColumns.LIST_INFO);
1883            mPersonalLevelIndex =
1884                    mCursor.getColumnIndexOrThrow(MessageColumns.PERSONAL_LEVEL);
1885            mBodyIndex = mCursor.getColumnIndexOrThrow(MessageColumns.BODY);
1886            mBodyEmbedsExternalResourcesIndex =
1887                    mCursor.getColumnIndexOrThrow(MessageColumns.EMBEDS_EXTERNAL_RESOURCES);
1888            mLabelIdsIndex = mCursor.getColumnIndexOrThrow(MessageColumns.LABEL_IDS);
1889            mJoinedAttachmentInfosIndex =
1890                    mCursor.getColumnIndexOrThrow(MessageColumns.JOINED_ATTACHMENT_INFOS);
1891            mErrorIndex = mCursor.getColumnIndexOrThrow(MessageColumns.ERROR);
1892
1893            mInReplyToLocalMessageId = 0;
1894            mPreserveAttachments = false;
1895        }
1896
1897        protected MessageCursor(ContentResolver cr, String account, long inReplyToMessageId,
1898                boolean preserveAttachments) {
1899            super(account, null);
1900            mContentResolver = cr;
1901            mInReplyToLocalMessageId = inReplyToMessageId;
1902            mPreserveAttachments = preserveAttachments;
1903        }
1904
1905        @Override
1906        protected void onCursorPositionChanged() {
1907            super.onCursorPositionChanged();
1908        }
1909
1910        public CursorStatus getStatus() {
1911            Bundle extras = mCursor.getExtras();
1912            String stringStatus = extras.getString(EXTRA_STATUS);
1913            return CursorStatus.valueOf(stringStatus);
1914        }
1915
1916        /** Retry a network request after errors. */
1917        public void retry() {
1918            Bundle input = new Bundle();
1919            input.putString(RESPOND_INPUT_COMMAND, COMMAND_RETRY);
1920            Bundle output = mCursor.respond(input);
1921            String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE);
1922            assert COMMAND_RESPONSE_OK.equals(response);
1923        }
1924
1925        /**
1926         * Gets the message id of the current message. Note that this is an
1927         * immutable local message (not, for example, GMail's message id, which
1928         * is immutable).
1929         *
1930         * @return the message's id
1931         */
1932        public long getMessageId() {
1933            checkCursor();
1934            return mCursor.getLong(mIdIndex);
1935        }
1936
1937        /**
1938         * Gets the message's conversation id. This must be immutable. (For
1939         * example, with GMail this should be the original conversation id
1940         * rather than the default notion of converation id.)
1941         *
1942         * @return the message's conversation id
1943         */
1944        public long getConversationId() {
1945            checkCursor();
1946            return mCursor.getLong(mConversationIdIndex);
1947        }
1948
1949        /**
1950         * Gets the message's subject.
1951         *
1952         * @return the message's subject
1953         */
1954        public String getSubject() {
1955            return getStringInColumn(mSubjectIndex);
1956        }
1957
1958        /**
1959         * Gets the message's snippet (the short piece of the body). The snippet
1960         * is generated from the body and cannot be set directly.
1961         *
1962         * @return the message's snippet
1963         */
1964        public String getSnippet() {
1965            return getStringInColumn(mSnippetIndex);
1966        }
1967
1968        /**
1969         * Gets the message's from address.
1970         *
1971         * @return the message's from address
1972         */
1973        public String getFromAddress() {
1974            return getStringInColumn(mFromIndex);
1975        }
1976
1977        /**
1978         * Returns the addresses for the key, if it has been updated, or index otherwise.
1979         */
1980        private String[] getAddresses(String key, int index) {
1981            ContentValues updated = getUpdateValues();
1982            String addresses;
1983            if (updated.containsKey(key)) {
1984                addresses = (String)getUpdateValues().get(key);
1985            } else {
1986                addresses = getStringInColumn(index);
1987            }
1988
1989            return TextUtils.split(addresses, EMAIL_SEPARATOR_PATTERN);
1990        }
1991
1992        /**
1993         * Gets the message's to addresses.
1994         * @return the message's to addresses
1995         */
1996        public String[] getToAddresses() {
1997           return getAddresses(MessageColumns.TO, mToIndex);
1998        }
1999
2000        /**
2001         * Gets the message's cc addresses.
2002         * @return the message's cc addresses
2003         */
2004        public String[] getCcAddresses() {
2005            return getAddresses(MessageColumns.CC, mCcIndex);
2006        }
2007
2008        /**
2009         * Gets the message's bcc addresses.
2010         * @return the message's bcc addresses
2011         */
2012        public String[] getBccAddresses() {
2013            return getAddresses(MessageColumns.BCC, mBccIndex);
2014        }
2015
2016        /**
2017         * Gets the message's replyTo address.
2018         *
2019         * @return the message's replyTo address
2020         */
2021        public String[] getReplyToAddress() {
2022            return TextUtils.split(getStringInColumn(mReplyToIndex), EMAIL_SEPARATOR_PATTERN);
2023        }
2024
2025        public long getDateSentMs() {
2026            checkCursor();
2027            return mCursor.getLong(mDateSentMsIndex);
2028        }
2029
2030        public long getDateReceivedMs() {
2031            checkCursor();
2032            return mCursor.getLong(mDateReceivedMsIndex);
2033        }
2034
2035        public String getListInfo() {
2036            return getStringInColumn(mListInfoIndex);
2037        }
2038
2039        public PersonalLevel getPersonalLevel() {
2040            checkCursor();
2041            int personalLevelInt = mCursor.getInt(mPersonalLevelIndex);
2042            return PersonalLevel.fromInt(personalLevelInt);
2043        }
2044
2045        /**
2046         * @deprecated Always returns true.
2047         */
2048        @Deprecated
2049        public boolean getExpanded() {
2050            return true;
2051        }
2052
2053        /**
2054         * Gets the message's body.
2055         *
2056         * @return the message's body
2057         */
2058        public String getBody() {
2059            return getStringInColumn(mBodyIndex);
2060        }
2061
2062        /**
2063         * @return whether the message's body contains embedded references to external resources. In
2064         * that case the resources should only be displayed if the user explicitly asks for them to
2065         * be
2066         */
2067        public boolean getBodyEmbedsExternalResources() {
2068            checkCursor();
2069            return mCursor.getInt(mBodyEmbedsExternalResourcesIndex) != 0;
2070        }
2071
2072        /**
2073         * @return a copy of the set of label ids
2074         */
2075        public Set<Long> getLabelIds() {
2076            String labelNames = mCursor.getString(mLabelIdsIndex);
2077            mLabelIdsSplitter.setString(labelNames);
2078            return getLabelIdsFromLabelIdsString(mLabelIdsSplitter);
2079        }
2080
2081        /**
2082         * @return a joined string of labels separated by spaces.
2083         */
2084        public String getRawLabelIds() {
2085            return mCursor.getString(mLabelIdsIndex);
2086        }
2087
2088        /**
2089         * Adds a label to a message (if add is true) or removes it (if add is
2090         * false).
2091         *
2092         * @param label the label to add or remove
2093         * @param add whether to add or remove the label
2094         */
2095        public void addOrRemoveLabel(String label, boolean add) {
2096            addOrRemoveLabelOnMessage(mContentResolver, mAccount, getConversationId(),
2097                    getMessageId(), label, add);
2098        }
2099
2100        public ArrayList<Attachment> getAttachmentInfos() {
2101            ArrayList<Attachment> attachments = Lists.newArrayList();
2102
2103            String joinedAttachmentInfos = mCursor.getString(mJoinedAttachmentInfosIndex);
2104            if (joinedAttachmentInfos != null) {
2105                for (String joinedAttachmentInfo :
2106                        TextUtils.split(joinedAttachmentInfos, ATTACHMENT_INFO_SEPARATOR_PATTERN)) {
2107
2108                    Attachment attachment = Attachment.parseJoinedString(joinedAttachmentInfo);
2109                    attachments.add(attachment);
2110                }
2111            }
2112            return attachments;
2113        }
2114
2115        /**
2116         * @return the error text for the message. Error text gets set if the server rejects a
2117         * message that we try to save or send. If there is error text then the message is no longer
2118         * scheduled to be saved or sent. Calling save() or send() will clear any error as well as
2119         * scheduling another atempt to save or send the message.
2120         */
2121        public String getErrorText() {
2122            return mCursor.getString(mErrorIndex);
2123        }
2124    }
2125
2126    /**
2127     * A helper class for creating or updating messags. Use the putXxx methods to provide initial or
2128     * new values for the message. Then save or send the message. To save or send an existing
2129     * message without making other changes to it simply provide an emty ContentValues.
2130     */
2131    public static class MessageModification {
2132
2133        /**
2134         * Sets the message's subject. Only valid for drafts.
2135         *
2136         * @param values the ContentValues that will be used to create or update the message
2137         * @param subject the new subject
2138         */
2139        public static void putSubject(ContentValues values, String subject) {
2140            values.put(MessageColumns.SUBJECT, subject);
2141        }
2142
2143        /**
2144         * Sets the message's to address. Only valid for drafts.
2145         *
2146         * @param values the ContentValues that will be used to create or update the message
2147         * @param toAddresses the new to addresses
2148         */
2149        public static void putToAddresses(ContentValues values, String[] toAddresses) {
2150            values.put(MessageColumns.TO, TextUtils.join(EMAIL_SEPARATOR, toAddresses));
2151        }
2152
2153        /**
2154         * Sets the message's cc address. Only valid for drafts.
2155         *
2156         * @param values the ContentValues that will be used to create or update the message
2157         * @param ccAddresses the new cc addresses
2158         */
2159        public static void putCcAddresses(ContentValues values, String[] ccAddresses) {
2160            values.put(MessageColumns.CC, TextUtils.join(EMAIL_SEPARATOR, ccAddresses));
2161        }
2162
2163        /**
2164         * Sets the message's bcc address. Only valid for drafts.
2165         *
2166         * @param values the ContentValues that will be used to create or update the message
2167         * @param bccAddresses the new bcc addresses
2168         */
2169        public static void putBccAddresses(ContentValues values, String[] bccAddresses) {
2170            values.put(MessageColumns.BCC, TextUtils.join(EMAIL_SEPARATOR, bccAddresses));
2171        }
2172
2173        /**
2174         * Saves a new body for the message. Only valid for drafts.
2175         *
2176         * @param values the ContentValues that will be used to create or update the message
2177         * @param body the new body of the message
2178         */
2179        public static void putBody(ContentValues values, String body) {
2180            values.put(MessageColumns.BODY, body);
2181        }
2182
2183        /**
2184         * Sets the attachments on a message. Only valid for drafts.
2185         *
2186         * @param values the ContentValues that will be used to create or update the message
2187         * @param attachments
2188         */
2189        public static void putAttachments(ContentValues values, List<Attachment> attachments) {
2190            values.put(
2191                    MessageColumns.JOINED_ATTACHMENT_INFOS, joinedAttachmentsString(attachments));
2192        }
2193
2194        /**
2195         * Create a new message and save it as a draft or send it.
2196         *
2197         * @param contentResolver the content resolver to use
2198         * @param account the account to use
2199         * @param values the values for the new message
2200         * @param refMessageId the message that is being replied to or forwarded
2201         * @param save whether to save or send the message
2202         * @return the id of the new message
2203         */
2204        public static long sendOrSaveNewMessage(
2205                ContentResolver contentResolver, String account,
2206                ContentValues values, long refMessageId, boolean save) {
2207            values.put(MessageColumns.FAKE_SAVE, save);
2208            values.put(MessageColumns.FAKE_REF_MESSAGE_ID, refMessageId);
2209            Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/");
2210            Uri result = contentResolver.insert(uri, values);
2211            return ContentUris.parseId(result);
2212        }
2213
2214        /**
2215         * Update an existing draft and save it as a new draft or send it.
2216         *
2217         * @param contentResolver the content resolver to use
2218         * @param account the account to use
2219         * @param messageId the id of the message to update
2220         * @param updateValues the values to change. Unspecified fields will not be altered
2221         * @param save whether to resave the message as a draft or send it
2222         */
2223        public static void sendOrSaveExistingMessage(
2224                ContentResolver contentResolver, String account, long messageId,
2225                ContentValues updateValues, boolean save) {
2226            updateValues.put(MessageColumns.FAKE_SAVE, save);
2227            updateValues.put(MessageColumns.FAKE_REF_MESSAGE_ID, 0);
2228            Uri uri = Uri.parse(
2229                    AUTHORITY_PLUS_MESSAGES + account + "/" + messageId);
2230            contentResolver.update(uri, updateValues, null, null);
2231        }
2232
2233        /**
2234         * The string produced here is parsed by Gmail.MessageCursor#getAttachmentInfos.
2235         */
2236        public static String joinedAttachmentsString(List<Gmail.Attachment> attachments) {
2237            StringBuilder attachmentsSb = new StringBuilder();
2238            for (Gmail.Attachment attachment : attachments) {
2239                if (attachmentsSb.length() != 0) {
2240                    attachmentsSb.append(Gmail.ATTACHMENT_INFO_SEPARATOR);
2241                }
2242                attachmentsSb.append(attachment.toJoinedString());
2243            }
2244            return attachmentsSb.toString();
2245        }
2246
2247    }
2248
2249    /**
2250     * A cursor over conversations.
2251     *
2252     * "Conversation" refers to the information needed to populate a list of
2253     * conversations, not all of the messages in a conversation.
2254     */
2255    public static final class ConversationCursor extends MailCursor {
2256
2257        private LabelMap mLabelMap;
2258
2259        private int mConversationIdIndex;
2260        private int mSubjectIndex;
2261        private int mSnippetIndex;
2262        private int mFromIndex;
2263        private int mDateIndex;
2264        private int mPersonalLevelIndex;
2265        private int mLabelIdsIndex;
2266        private int mNumMessagesIndex;
2267        private int mMaxMessageIdIndex;
2268        private int mHasAttachmentsIndex;
2269        private int mHasMessagesWithErrorsIndex;
2270        private int mForceAllUnreadIndex;
2271
2272        private TextUtils.StringSplitter mLabelIdsSplitter = newConversationLabelIdsSplitter();
2273
2274        private ConversationCursor(Gmail gmail, String account, Cursor cursor) {
2275            super(account, cursor);
2276            mLabelMap = gmail.getLabelMap(account);
2277
2278            mConversationIdIndex =
2279                    mCursor.getColumnIndexOrThrow(ConversationColumns.ID);
2280            mSubjectIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.SUBJECT);
2281            mSnippetIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.SNIPPET);
2282            mFromIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.FROM);
2283            mDateIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.DATE);
2284            mPersonalLevelIndex =
2285                    mCursor.getColumnIndexOrThrow(ConversationColumns.PERSONAL_LEVEL);
2286            mLabelIdsIndex =
2287                    mCursor.getColumnIndexOrThrow(ConversationColumns.LABEL_IDS);
2288            mNumMessagesIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.NUM_MESSAGES);
2289            mMaxMessageIdIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.MAX_MESSAGE_ID);
2290            mHasAttachmentsIndex =
2291                    mCursor.getColumnIndexOrThrow(ConversationColumns.HAS_ATTACHMENTS);
2292            mHasMessagesWithErrorsIndex =
2293                    mCursor.getColumnIndexOrThrow(ConversationColumns.HAS_MESSAGES_WITH_ERRORS);
2294            mForceAllUnreadIndex =
2295                    mCursor.getColumnIndexOrThrow(ConversationColumns.FORCE_ALL_UNREAD);
2296        }
2297
2298        @Override
2299        protected void onCursorPositionChanged() {
2300            super.onCursorPositionChanged();
2301        }
2302
2303        public CursorStatus getStatus() {
2304            Bundle extras = mCursor.getExtras();
2305            String stringStatus = extras.getString(EXTRA_STATUS);
2306            return CursorStatus.valueOf(stringStatus);
2307        }
2308
2309        /** Retry a network request after errors. */
2310        public void retry() {
2311            Bundle input = new Bundle();
2312            input.putString(RESPOND_INPUT_COMMAND, COMMAND_RETRY);
2313            Bundle output = mCursor.respond(input);
2314            String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE);
2315            assert COMMAND_RESPONSE_OK.equals(response);
2316        }
2317
2318        /**
2319         * When a conversation cursor is created it becomes the active network cursor, which means
2320         * that it will fetch results from the network if it needs to in order to show all mail that
2321         * matches its query. If you later want to requery an older cursor and would like that
2322         * cursor to be the active cursor you need to call this method before requerying.
2323         */
2324        public void becomeActiveNetworkCursor() {
2325            Bundle input = new Bundle();
2326            input.putString(RESPOND_INPUT_COMMAND, COMMAND_ACTIVATE);
2327            Bundle output = mCursor.respond(input);
2328            String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE);
2329            assert COMMAND_RESPONSE_OK.equals(response);
2330        }
2331
2332        /**
2333         * Tells the cursor whether its contents are visible to the user. The cursor will
2334         * automatically broadcast intents to remove any matching new-mail notifications when this
2335         * cursor's results become visible and, if they are visible, when the cursor is requeried.
2336         *
2337         * Note that contents shown in an activity that is resumed but not focused
2338         * (onWindowFocusChanged/hasWindowFocus) then results shown in that activity do not count
2339         * as visible. (This happens when the activity is behind the lock screen or a dialog.)
2340         *
2341         * @param visible whether the contents of this cursor are visible to the user.
2342         */
2343        public void setContentsVisibleToUser(boolean visible) {
2344            Bundle input = new Bundle();
2345            input.putString(RESPOND_INPUT_COMMAND, COMMAND_SET_VISIBLE);
2346            input.putBoolean(SET_VISIBLE_PARAM_VISIBLE, visible);
2347            Bundle output = mCursor.respond(input);
2348            String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE);
2349            assert COMMAND_RESPONSE_OK.equals(response);
2350        }
2351
2352        /**
2353         * Gets the conversation id. This is immutable. (The server calls it the original
2354         * conversation id.)
2355         *
2356         * @return the conversation id
2357         */
2358        public long getConversationId() {
2359            return mCursor.getLong(mConversationIdIndex);
2360        }
2361
2362        /**
2363         * Returns the instructions for building from snippets. Pass this to getFromSnippetHtml
2364         * in order to actually build the snippets.
2365         * @return snippet instructions for use by getFromSnippetHtml()
2366         */
2367        public String getFromSnippetInstructions() {
2368            return getStringInColumn(mFromIndex);
2369        }
2370
2371        /**
2372         * Gets the conversation's subject.
2373         *
2374         * @return the subject
2375         */
2376        public String getSubject() {
2377            return getStringInColumn(mSubjectIndex);
2378        }
2379
2380        /**
2381         * Gets the conversation's snippet.
2382         *
2383         * @return the snippet
2384         */
2385        public String getSnippet() {
2386            return getStringInColumn(mSnippetIndex);
2387        }
2388
2389        /**
2390         * Get's the conversation's personal level.
2391         *
2392         * @return the personal level.
2393         */
2394        public PersonalLevel getPersonalLevel() {
2395            int personalLevelInt = mCursor.getInt(mPersonalLevelIndex);
2396            return PersonalLevel.fromInt(personalLevelInt);
2397        }
2398
2399        /**
2400         * @return a copy of the set of labels. To add or remove labels call
2401         *         MessageCursor.addOrRemoveLabel on each message in the conversation.
2402         * @deprecated use getLabelIds
2403         */
2404        public Set<String> getLabels() {
2405            return getLabels(getRawLabelIds(), mLabelMap);
2406        }
2407
2408        /**
2409         * @return a copy of the set of labels. To add or remove labels call
2410         *         MessageCursor.addOrRemoveLabel on each message in the conversation.
2411         */
2412        public Set<Long> getLabelIds() {
2413            mLabelIdsSplitter.setString(getRawLabelIds());
2414            return getLabelIdsFromLabelIdsString(mLabelIdsSplitter);
2415        }
2416
2417        /**
2418         * Returns the set of labels using the raw labels from a previous getRawLabels()
2419         * as input.
2420         * @return a copy of the set of labels. To add or remove labels call
2421         * MessageCursor.addOrRemoveLabel on each message in the conversation.
2422         */
2423        public Set<String> getLabels(String rawLabelIds, LabelMap labelMap) {
2424            mLabelIdsSplitter.setString(rawLabelIds);
2425            return getCanonicalNamesFromLabelIdsString(labelMap, mLabelIdsSplitter);
2426        }
2427
2428        /**
2429         * @return a joined string of labels separated by spaces. Use
2430         * getLabels(rawLabels) to convert this to a Set of labels.
2431         */
2432        public String getRawLabelIds() {
2433            return mCursor.getString(mLabelIdsIndex);
2434        }
2435
2436        /**
2437         * @return the number of messages in the conversation
2438         */
2439        public int getNumMessages() {
2440            return mCursor.getInt(mNumMessagesIndex);
2441        }
2442
2443        /**
2444         * @return the max message id in the conversation
2445         */
2446        public long getMaxServerMessageId() {
2447            return mCursor.getLong(mMaxMessageIdIndex);
2448        }
2449
2450        public long getDateMs() {
2451            return mCursor.getLong(mDateIndex);
2452        }
2453
2454        public boolean hasAttachments() {
2455            return mCursor.getInt(mHasAttachmentsIndex) != 0;
2456        }
2457
2458        public boolean hasMessagesWithErrors() {
2459            return mCursor.getInt(mHasMessagesWithErrorsIndex) != 0;
2460        }
2461
2462        public boolean getForceAllUnread() {
2463            return !mCursor.isNull(mForceAllUnreadIndex)
2464                    && mCursor.getInt(mForceAllUnreadIndex) != 0;
2465        }
2466    }
2467}
2468