• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.provider;
18 
19 import com.google.android.collect.Lists;
20 import com.google.android.collect.Maps;
21 import com.google.android.collect.Sets;
22 
23 import android.content.AsyncQueryHandler;
24 import android.content.ContentQueryMap;
25 import android.content.ContentResolver;
26 import android.content.ContentUris;
27 import android.content.ContentValues;
28 import android.database.ContentObserver;
29 import android.database.Cursor;
30 import android.database.DataSetObserver;
31 import android.net.Uri;
32 import android.os.Bundle;
33 import android.os.Handler;
34 import android.text.Html;
35 import android.text.SpannableStringBuilder;
36 import android.text.Spanned;
37 import android.text.TextUtils;
38 import android.text.TextUtils.SimpleStringSplitter;
39 import android.text.style.CharacterStyle;
40 import android.text.util.Regex;
41 import android.util.Log;
42 
43 import java.io.UnsupportedEncodingException;
44 import java.net.URLEncoder;
45 import java.util.ArrayList;
46 import java.util.HashSet;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.Observable;
50 import java.util.Observer;
51 import java.util.Set;
52 import java.util.SortedSet;
53 import java.util.TreeSet;
54 import java.util.regex.Matcher;
55 import java.util.regex.Pattern;
56 
57 /**
58  * A thin wrapper over the content resolver for accessing the gmail provider.
59  *
60  * @hide
61  */
62 public 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      */
getNameFromAddressString(String addressString)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      */
getEmailFromAddressString(String addressString)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      */
isLabelUserDefined(String label)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      */
isLabelUserSettable(String label)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      */
getLabelIdsFromLabelIdsString( TextUtils.StringSplitter splitter)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      */
getCanonicalNamesFromLabelIdsString( LabelMap labelMap, TextUtils.StringSplitter splitter)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      */
newMessageLabelIdsSplitter()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      */
newConversationLabelIdsSplitter()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 
CommaStringSplitter()282         public CommaStringSplitter() {
283             super(COMMA_SEPARATOR);
284         }
285 
286         @Override
setString(String string)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      */
getLabelIdsStringFromLabelIds(Set<Long> labelIds)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 
ConversationColumns()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 
MessageColumns()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 
LabelColumns()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 {
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 
Gmail(ContentResolver contentResolver)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      */
toNonnullString(String source)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      */
getConversationCursorForCursor(String account, Cursor cursor)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      */
getSelectionArguments( BecomeActiveNetworkCursor becomeActiveNetworkCursor)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      */
runQueryForConversations(String account, AsyncQueryHandler handler, int token, String query, BecomeActiveNetworkCursor becomeActiveNetworkCursor)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      */
getConversationCursorForQuery( String account, String query, BecomeActiveNetworkCursor becomeActiveNetworkCursor)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      */
getMessageCursorForMessageId(String account, long messageId)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      */
getLocalMessageCursorForQuery(String account, String query)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      */
getMessageCursorForConversationId(String account, long conversationId)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      */
expungeMessage(String account, long messageId)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      */
addOrRemoveLabelOnConversation( String account, long conversationId, long maxServerMessageId, String label, boolean add)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      */
addOrRemoveLabelOnMessage(ContentResolver contentResolver, String account, long conversationId, long messageId, String label, boolean add)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      */
getSettings(String account)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      */
setSettings(String account, Settings settings)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      */
getSenderSnippet( String instructions, SpannableStringBuilder sb, int maxChars, CharacterStyle unreadStyle, CharacterStyle draftsStyle, CharSequence meString, CharSequence draftString, CharSequence draftPluralString, CharSequence sendingString, CharSequence sendFailedString, boolean forceAllUnread, boolean forceAllRead)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 
getCursor()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          */
MailCursor(String account, Cursor cursor)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          */
getAccount()1047         public String getAccount() {
1048             return mAccount;
1049         }
1050 
checkThread()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          */
getUpdateValues()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          */
onCursorPositionChanged()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          */
count()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          */
position()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          */
moveTo(int position)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          */
next()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          */
release()1150         final public void release() {
1151             if (mCursor != null) {
1152                 mCursor.unregisterContentObserver(this);
1153                 mCursor.deactivate();
1154             }
1155         }
1156 
registerContentObserver(ContentObserver observer)1157         final public void registerContentObserver(ContentObserver observer) {
1158             mCursor.registerContentObserver(observer);
1159         }
1160 
unregisterContentObserver(ContentObserver observer)1161         final public void unregisterContentObserver(ContentObserver observer) {
1162             mCursor.unregisterContentObserver(observer);
1163         }
1164 
registerDataSetObserver(DataSetObserver observer)1165         final public void registerDataSetObserver(DataSetObserver observer) {
1166             mCursor.registerDataSetObserver(observer);
1167         }
1168 
unregisterDataSetObserver(DataSetObserver observer)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          */
registerObserver(MailCursorObserver observer)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          */
unregisterObserver(MailCursorObserver observer)1187         final public void unregisterObserver(MailCursorObserver observer) {
1188             mObservers.remove(observer);
1189         }
1190 
1191         // ********* ContentObserver
1192 
1193         @Override
deliverSelfNotifications()1194         final public boolean deliverSelfNotifications() {
1195             return false;
1196         }
1197 
1198         @Override
onChange(boolean selfChange)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 
checkCursor()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          */
getStringInColumn(int columnIndex)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          */
onCursorChanged(MailCursor cursor)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 
LabelCursor(String account, Cursor cursor)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          */
getName()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          */
getNumConversations()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          */
getNumUnreadConversations()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 
LabelMap(ContentResolver contentResolver, String account, boolean keepUpdated)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 
LabelMap(Cursor cursor, boolean keepUpdated)1320         public LabelMap(Cursor cursor, boolean keepUpdated) {
1321             init(cursor, keepUpdated);
1322         }
1323 
init(Cursor cursor, boolean keepUpdated)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          */
labelsSynced()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          */
updateDataStructures()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 
getLabelIdSent()1400         public long getLabelIdSent() {
1401             checkLabelsSynced();
1402             return mLabelIdSent;
1403         }
1404 
getLabelIdInbox()1405         public long getLabelIdInbox() {
1406             checkLabelsSynced();
1407             return mLabelIdInbox;
1408         }
1409 
getLabelIdDraft()1410         public long getLabelIdDraft() {
1411             checkLabelsSynced();
1412             return mLabelIdDraft;
1413         }
1414 
getLabelIdUnread()1415         public long getLabelIdUnread() {
1416             checkLabelsSynced();
1417             return mLabelIdUnread;
1418         }
1419 
getLabelIdTrash()1420         public long getLabelIdTrash() {
1421             checkLabelsSynced();
1422             return mLabelIdTrash;
1423         }
1424 
getLabelIdSpam()1425         public long getLabelIdSpam() {
1426             checkLabelsSynced();
1427             return mLabelIdSpam;
1428         }
1429 
getLabelIdStarred()1430         public long getLabelIdStarred() {
1431             checkLabelsSynced();
1432             return mLabelIdStarred;
1433         }
1434 
getLabelIdChat()1435         public long getLabelIdChat() {
1436             checkLabelsSynced();
1437             return mLabelIdChat;
1438         }
1439 
getLabelIdIgnored()1440         public long getLabelIdIgnored() {
1441             checkLabelsSynced();
1442             return mLabelIdIgnored;
1443         }
1444 
getLabelIdVoicemail()1445         public long getLabelIdVoicemail() {
1446             checkLabelsSynced();
1447             return mLabelIdVoicemail;
1448         }
1449 
getLabelIdVoicemailInbox()1450         public long getLabelIdVoicemailInbox() {
1451             checkLabelsSynced();
1452             return mLabelIdVoicemailInbox;
1453         }
1454 
getLabelIdCached()1455         public long getLabelIdCached() {
1456             checkLabelsSynced();
1457             return mLabelIdCached;
1458         }
1459 
getLabelIdOutbox()1460         public long getLabelIdOutbox() {
1461             checkLabelsSynced();
1462             return mLabelIdOutbox;
1463         }
1464 
checkLabelsSynced()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. */
getSortedUserLabels()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 
getSortedUserMeaningfulSystemLabels()1488         public static List<String> getSortedUserMeaningfulSystemLabels() {
1489             return SORTED_USER_MEANINGFUL_SYSTEM_LABELS;
1490         }
1491 
getUserMeaningfulSystemLabelsSet()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 
getForcedIncludedLabels()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 
getForcedIncludedOrPartialLabels()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 
getForcedUnsyncedLabels()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
getNumConversations(String label)1527         public int getNumConversations(String label) {
1528             return getNumConversations(getLabelId(label));
1529         }
1530 
1531         /** Returns the number of conversation with a given label. */
getNumConversations(long labelId)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
getNumUnreadConversations(String label)1541         public int getNumUnreadConversations(String label) {
1542             return getNumUnreadConversations(getLabelId(label));
1543         }
1544 
1545         /** Returns the number of unread conversation with a given label. */
getNumUnreadConversations(long labelId)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