Gmail.java :  » Widget » android-recent-widget » org » recentwidget » compat » gmail » Android Open Source

Android Open Source » Widget » android recent widget 
android recent widget » org » recentwidget » compat » gmail » Gmail.java
package org.recentwidget.compat.gmail;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.recentwidget.compat.util.Lists;
import org.recentwidget.compat.util.Maps;
import org.recentwidget.compat.util.Sets;

import android.content.AsyncQueryHandler;
import android.content.ContentQueryMap;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.BaseColumns;
import android.text.Html;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextUtils.SimpleStringSplitter;
import android.text.style.CharacterStyle;
import android.util.Log;

/**
 * Taken from
 * http://www.google.co.in/codesearch/p?hl=en#uX1GffpyOZk/core/java/android
 * /provider/Gmail.java&q=gmail.java&d=0
 * 
 */
public class Gmail {
  // Set to true to enable extra debugging.
  private static final boolean DEBUG = false;

  public static final String GMAIL_AUTH_SERVICE = "mail";
  // These constants come from
  // google3/java/com/google/caribou/backend/MailLabel.java.
  public static final String LABEL_SENT = "^f";
  public static final String LABEL_INBOX = "^i";
  public static final String LABEL_DRAFT = "^r";
  public static final String LABEL_UNREAD = "^u";
  public static final String LABEL_TRASH = "^k";
  public static final String LABEL_SPAM = "^s";
  public static final String LABEL_STARRED = "^t";
  public static final String LABEL_CHAT = "^b"; // 'b' for 'buzz'
  public static final String LABEL_VOICEMAIL = "^vm";
  public static final String LABEL_IGNORED = "^g";
  public static final String LABEL_ALL = "^all";
  // These constants (starting with "^^") are only used locally and are not
  // understood by the
  // server.
  public static final String LABEL_VOICEMAIL_INBOX = "^^vmi";
  public static final String LABEL_CACHED = "^^cached";
  public static final String LABEL_OUTBOX = "^^out";

  public static final String AUTHORITY = "gmail-ls";
  private static final String TAG = "Gmail";
  public static final String AUTHORITY_PLUS_CONVERSATIONS = "content://"
      + AUTHORITY + "/conversations/";
  private static final String AUTHORITY_PLUS_LABELS = "content://"
      + AUTHORITY + "/labels/";
  public static final String AUTHORITY_PLUS_MESSAGES = "content://"
      + AUTHORITY + "/messages/";
  private static final String AUTHORITY_PLUS_SETTINGS = "content://"
      + AUTHORITY + "/settings/";

  public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
  private static final Uri LABELS_URI = Uri.parse(AUTHORITY_PLUS_LABELS);
  public static final Uri CONVERSATIONS_URI = Uri
      .parse(AUTHORITY_PLUS_CONVERSATIONS);
  private static final Uri SETTINGS_URI = Uri.parse(AUTHORITY_PLUS_SETTINGS);

  /** Separates email addresses in strings in the database. */
  public static final String EMAIL_SEPARATOR = "\n";
  public static final Pattern EMAIL_SEPARATOR_PATTERN = Pattern
      .compile(EMAIL_SEPARATOR);

  /**
   * Space-separated lists have separators only between items.
   */
  private static final char SPACE_SEPARATOR = ' ';
  public static final Pattern SPACE_SEPARATOR_PATTERN = Pattern.compile(" ");

  /**
   * Comma-separated lists have separators between each item, before the first
   * and after the last item. The empty list is <tt>,</tt>.
   * 
   * <p>
   * This makes them easier to modify with SQL since it is not a special case
   * to add or remove the last item. Having a separator on each side of each
   * value also makes it safe to use SQL's REPLACE to remove an item from a
   * string by using REPLACE(',value,', ',').
   * 
   * <p>
   * We could use the same separator for both lists but this makes it easier
   * to remember which kind of list one is dealing with.
   */
  private static final char COMMA_SEPARATOR = ',';
  public static final Pattern COMMA_SEPARATOR_PATTERN = Pattern.compile(",");

  /** Separates attachment info parts in strings in the database. */
  public static final String ATTACHMENT_INFO_SEPARATOR = "\n";
  public static final Pattern ATTACHMENT_INFO_SEPARATOR_PATTERN = Pattern
      .compile(ATTACHMENT_INFO_SEPARATOR);

  public static final Character SENDER_LIST_SEPARATOR = '\n';
  public static final String SENDER_LIST_TOKEN_ELIDED = "e";
  public static final String SENDER_LIST_TOKEN_NUM_MESSAGES = "n";
  public static final String SENDER_LIST_TOKEN_NUM_DRAFTS = "d";
  public static final String SENDER_LIST_TOKEN_LITERAL = "l";
  public static final String SENDER_LIST_TOKEN_SENDING = "s";
  public static final String SENDER_LIST_TOKEN_SEND_FAILED = "f";

  /** Used for finding status in a cursor's extras. */
  public static final String EXTRA_STATUS = "status";

  public static final String RESPOND_INPUT_COMMAND = "command";
  public static final String COMMAND_RETRY = "retry";
  public static final String COMMAND_ACTIVATE = "activate";
  public static final String COMMAND_SET_VISIBLE = "setVisible";
  public static final String SET_VISIBLE_PARAM_VISIBLE = "visible";
  public static final String RESPOND_OUTPUT_COMMAND_RESPONSE = "commandResponse";
  public static final String COMMAND_RESPONSE_OK = "ok";
  public static final String COMMAND_RESPONSE_UNKNOWN = "unknownCommand";

  public static final String INSERT_PARAM_ATTACHMENT_ORIGIN = "origin";
  public static final String INSERT_PARAM_ATTACHMENT_ORIGIN_EXTRAS = "originExtras";

  private static final Pattern NAME_ADDRESS_PATTERN = Pattern
      .compile("\"(.*)\"");
  private static final Pattern UNNAMED_ADDRESS_PATTERN = Pattern
      .compile("([^<]+)@");

  private static final Map<Integer, Integer> sPriorityToLength = new HashMap<Integer, Integer>();
  public static final SimpleStringSplitter sSenderListSplitter = new SimpleStringSplitter(
      SENDER_LIST_SEPARATOR);
  public static String[] sSenderFragments = new String[8];

  public static final Pattern EMAIL_ADDRESS_PATTERN = Pattern
      .compile("[a-zA-Z0-9\\+\\.\\_\\%\\-]{1,256}" + "\\@"
          + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + "(" + "\\."
          + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + ")+");

  /**
   * Returns the name in an address string
   * 
   * @param addressString
   *            such as &quot;bobby&quot; &lt;bob@example.com&gt;
   * @return returns the quoted name in the addressString, otherwise the
   *         username from the email address
   */
  public static String getNameFromAddressString(String addressString) {
    Matcher namedAddressMatch = NAME_ADDRESS_PATTERN.matcher(addressString);
    if (namedAddressMatch.find()) {
      String name = namedAddressMatch.group(1);
      if (name.length() > 0)
        return name;
      addressString = addressString.substring(namedAddressMatch.end(),
          addressString.length());
    }

    Matcher unnamedAddressMatch = UNNAMED_ADDRESS_PATTERN
        .matcher(addressString);
    if (unnamedAddressMatch.find()) {
      return unnamedAddressMatch.group(1);
    }

    return addressString;
  }

  /**
   * Returns the email address in an address string
   * 
   * @param addressString
   *            such as &quot;bobby&quot; &lt;bob@example.com&gt;
   * @return returns the email address, such as bob@example.com from the
   *         example above
   */
  public static String getEmailFromAddressString(String addressString) {
    String result = addressString;
    Matcher match = EMAIL_ADDRESS_PATTERN.matcher(addressString);
    if (match.find()) {
      result = addressString.substring(match.start(), match.end());
    }

    return result;
  }

  /**
   * Returns whether the label is user-defined (versus system-defined labels
   * such as inbox, whose names start with "^").
   */
  public static boolean isLabelUserDefined(String label) {
    // TODO: label should never be empty so we should be able to say
    // [label.charAt(0) != '^'].
    // However, it's a release week and I'm too scared to make that change.
    return !label.startsWith("^");
  }

  private static final Set<String> USER_SETTABLE_BUILTIN_LABELS = Sets
      .newHashSet(Gmail.LABEL_INBOX, Gmail.LABEL_UNREAD,
          Gmail.LABEL_TRASH, Gmail.LABEL_SPAM, Gmail.LABEL_STARRED,
          Gmail.LABEL_IGNORED);

  /**
   * Returns whether the label is user-settable. For example, labels such as
   * LABEL_DRAFT should only be set internally.
   */
  public static boolean isLabelUserSettable(String label) {
    return USER_SETTABLE_BUILTIN_LABELS.contains(label)
        || isLabelUserDefined(label);
  }

  /**
   * Returns the set of labels using the raw labels from a previous
   * getRawLabels() as input.
   * 
   * @return a copy of the set of labels. To add or remove labels call
   *         MessageCursor.addOrRemoveLabel on each message in the
   *         conversation.
   */
  public static Set<Long> getLabelIdsFromLabelIdsString(
      TextUtils.StringSplitter splitter) {
    Set<Long> labelIds = Sets.newHashSet();
    for (String labelIdString : splitter) {
      labelIds.add(Long.valueOf(labelIdString));
    }
    return labelIds;
  }

  /**
   * @deprecated remove when the activities stop using canonical names to
   *             identify labels
   */
  @Deprecated
  public static Set<String> getCanonicalNamesFromLabelIdsString(
      LabelMap labelMap, TextUtils.StringSplitter splitter) {
    Set<String> canonicalNames = Sets.newHashSet();
    for (long labelId : getLabelIdsFromLabelIdsString(splitter)) {
      final String canonicalName = labelMap.getCanonicalName(labelId);
      // We will sometimes see labels that the label map does not yet know
      // about or that
      // do not have names yet.
      if (!TextUtils.isEmpty(canonicalName)) {
        canonicalNames.add(canonicalName);
      } else {
        Log.w(TAG,
            "getCanonicalNamesFromLabelIdsString skipping label id: "
                + labelId);
      }
    }
    return canonicalNames;
  }

  /**
   * @return a StringSplitter that is configured to split message label id
   *         strings
   */
  public static TextUtils.StringSplitter newMessageLabelIdsSplitter() {
    return new TextUtils.SimpleStringSplitter(SPACE_SEPARATOR);
  }

  /**
   * @return a StringSplitter that is configured to split conversation label
   *         id strings
   */
  public static TextUtils.StringSplitter newConversationLabelIdsSplitter() {
    return new CommaStringSplitter();
  }

  /**
   * A splitter for strings of the form described in the docs for
   * COMMA_SEPARATOR.
   */
  private static class CommaStringSplitter extends
      TextUtils.SimpleStringSplitter {

    public CommaStringSplitter() {
      super(COMMA_SEPARATOR);
    }

    @Override
    public void setString(String string) {
      // The string should always be at least a single comma.
      super.setString(string.substring(1));
    }
  }

  /**
   * Creates a single string of the form that getLabelIdsFromLabelIdsString
   * can split.
   */
  public static String getLabelIdsStringFromLabelIds(Set<Long> labelIds) {
    StringBuilder sb = new StringBuilder();
    sb.append(COMMA_SEPARATOR);
    for (Long labelId : labelIds) {
      sb.append(labelId);
      sb.append(COMMA_SEPARATOR);
    }
    return sb.toString();
  }

  public static final class ConversationColumns {
    public static final String ID = "_id";
    public static final String SUBJECT = "subject";
    public static final String SNIPPET = "snippet";
    public static final String FROM = "fromAddress";
    public static final String DATE = "date";
    public static final String PERSONAL_LEVEL = "personalLevel";
    /**
     * A list of label names with a space after each one (including the last
     * one). This makes it easier remove individual labels from this list
     * using SQL.
     */
    public static final String LABEL_IDS = "labelIds";
    public static final String NUM_MESSAGES = "numMessages";
    public static final String MAX_MESSAGE_ID = "maxMessageId";
    public static final String HAS_ATTACHMENTS = "hasAttachments";
    public static final String HAS_MESSAGES_WITH_ERRORS = "hasMessagesWithErrors";
    public static final String FORCE_ALL_UNREAD = "forceAllUnread";

    private ConversationColumns() {
    }
  }

  public static final class MessageColumns {

    public static final String ID = "_id";
    public static final String MESSAGE_ID = "messageId";
    public static final String CONVERSATION_ID = "conversation";
    public static final String SUBJECT = "subject";
    public static final String SNIPPET = "snippet";
    public static final String FROM = "fromAddress";
    public static final String TO = "toAddresses";
    public static final String CC = "ccAddresses";
    public static final String BCC = "bccAddresses";
    public static final String REPLY_TO = "replyToAddresses";
    public static final String DATE_SENT_MS = "dateSentMs";
    public static final String DATE_RECEIVED_MS = "dateReceivedMs";
    public static final String LIST_INFO = "listInfo";
    public static final String PERSONAL_LEVEL = "personalLevel";
    public static final String BODY = "body";
    public static final String EMBEDS_EXTERNAL_RESOURCES = "bodyEmbedsExternalResources";
    public static final String LABEL_IDS = "labelIds";
    public static final String JOINED_ATTACHMENT_INFOS = "joinedAttachmentInfos";
    public static final String ERROR = "error";
    // TODO: add a method for accessing this
    public static final String REF_MESSAGE_ID = "refMessageId";

    // Fake columns used only for saving or sending messages.
    public static final String FAKE_SAVE = "save";
    public static final String FAKE_REF_MESSAGE_ID = "refMessageId";

    private MessageColumns() {
    }
  }

  public static final class LabelColumns {
    public static final String CANONICAL_NAME = "canonicalName";
    public static final String NAME = "name";
    public static final String NUM_CONVERSATIONS = "numConversations";
    public static final String NUM_UNREAD_CONVERSATIONS = "numUnreadConversations";

    private LabelColumns() {
    }
  }

  public static final class SettingsColumns {
    public static final String LABELS_INCLUDED = "labelsIncluded";
    public static final String LABELS_PARTIAL = "labelsPartial";
    public static final String CONVERSATION_AGE_DAYS = "conversationAgeDays";
    public static final String MAX_ATTACHMENET_SIZE_MB = "maxAttachmentSize";
  }

  /**
   * These flags can be included as Selection Arguments when querying the
   * provider.
   */
  public static class SelectionArguments {
    private SelectionArguments() {
      // forbid instantiation
    }

    /**
     * Specifies that you do NOT wish the returned cursor to become the
     * Active Network Cursor. If you do not include this flag as a
     * selectionArg, the new cursor will become the Active Network Cursor by
     * default.
     */
    public static final String DO_NOT_BECOME_ACTIVE_NETWORK_CURSOR = "SELECTION_ARGUMENT_DO_NOT_BECOME_ACTIVE_NETWORK_CURSOR";
  }

  // These are the projections that we need when getting cursors from the
  // content provider.
  public static String[] CONVERSATION_PROJECTION = { ConversationColumns.ID,
      ConversationColumns.SUBJECT, ConversationColumns.SNIPPET,
      ConversationColumns.FROM, ConversationColumns.DATE,
      ConversationColumns.PERSONAL_LEVEL, ConversationColumns.LABEL_IDS,
      ConversationColumns.NUM_MESSAGES,
      ConversationColumns.MAX_MESSAGE_ID,
      ConversationColumns.HAS_ATTACHMENTS,
      ConversationColumns.HAS_MESSAGES_WITH_ERRORS,
      ConversationColumns.FORCE_ALL_UNREAD };
  public static String[] MESSAGE_PROJECTION = { MessageColumns.ID,
      MessageColumns.MESSAGE_ID, MessageColumns.CONVERSATION_ID,
      MessageColumns.SUBJECT, MessageColumns.SNIPPET,
      MessageColumns.FROM, MessageColumns.TO, MessageColumns.CC,
      MessageColumns.BCC, MessageColumns.REPLY_TO,
      MessageColumns.DATE_SENT_MS, MessageColumns.DATE_RECEIVED_MS,
      MessageColumns.LIST_INFO, MessageColumns.PERSONAL_LEVEL,
      MessageColumns.BODY, MessageColumns.EMBEDS_EXTERNAL_RESOURCES,
      MessageColumns.LABEL_IDS, MessageColumns.JOINED_ATTACHMENT_INFOS,
      MessageColumns.ERROR };
  private static String[] LABEL_PROJECTION = { BaseColumns._ID,
      LabelColumns.CANONICAL_NAME, LabelColumns.NAME,
      LabelColumns.NUM_CONVERSATIONS,
      LabelColumns.NUM_UNREAD_CONVERSATIONS };
  private static String[] SETTINGS_PROJECTION = {
      SettingsColumns.LABELS_INCLUDED, SettingsColumns.LABELS_PARTIAL,
      SettingsColumns.CONVERSATION_AGE_DAYS,
      SettingsColumns.MAX_ATTACHMENET_SIZE_MB, };

  private final ContentResolver mContentResolver;

  public Gmail(ContentResolver contentResolver) {
    mContentResolver = contentResolver;
  }

  /**
   * Returns source if source is non-null. Returns the empty string otherwise.
   */
  private static String toNonnullString(String source) {
    if (source == null) {
      return "";
    } else {
      return source;
    }
  }

  /**
   * Behavior for a new cursor: should it become the Active Network Cursor?
   * This could potentially lead to bad behavior if someone else is using the
   * Active Network Cursor, since theirs will stop being the Active Network
   * Cursor.
   */
  public static enum BecomeActiveNetworkCursor {
    /**
     * The new cursor should become the one and only Active Network Cursor.
     * Any other cursor that might already be the Active Network Cursor will
     * cease to be so.
     */
    YES,

    /**
     * The new cursor should not become the Active Network Cursor. Any other
     * cursor that might already be the Active Network Cursor will continue
     * to be so.
     */
    NO
  }

  /**
   * Wraps a Cursor in a ConversationCursor
   * 
   * @param account
   *            the account the cursor is associated with
   * @param cursor
   *            The Cursor to wrap
   * @return a new ConversationCursor
   */
  public ConversationCursor getConversationCursorForCursor(String account,
      Cursor cursor) {
    if (TextUtils.isEmpty(account)) {
      throw new IllegalArgumentException("account is empty");
    }
    return new ConversationCursor(this, account, cursor);
  }

  /**
   * Creates an array of SelectionArguments suitable for passing to the
   * provider's query. Currently this only handles one flag, but it could be
   * expanded in the future.
   */
  private static String[] getSelectionArguments(
      BecomeActiveNetworkCursor becomeActiveNetworkCursor) {
    if (BecomeActiveNetworkCursor.NO == becomeActiveNetworkCursor) {
      return new String[] { SelectionArguments.DO_NOT_BECOME_ACTIVE_NETWORK_CURSOR };
    } else {
      // Default behavior; no args required.
      return null;
    }
  }

  /**
   * Asynchronously gets a cursor over all conversations matching a query. The
   * query is in Gmail's query syntax. When the operation is complete the
   * handler's onQueryComplete() method is called with the resulting Cursor.
   * 
   * @param account
   *            run the query on this account
   * @param handler
   *            An AsyncQueryHanlder that will be used to run the query
   * @param token
   *            The token to pass to startQuery, which will be passed back to
   *            onQueryComplete
   * @param query
   *            a query in Gmail's query syntax
   * @param becomeActiveNetworkCursor
   *            whether or not the returned cursor should become the Active
   *            Network Cursor
   */
  public void runQueryForConversations(String account,
      AsyncQueryHandler handler, int token, String query,
      BecomeActiveNetworkCursor becomeActiveNetworkCursor) {
    if (TextUtils.isEmpty(account)) {
      throw new IllegalArgumentException("account is empty");
    }
    String[] selectionArgs = getSelectionArguments(becomeActiveNetworkCursor);
    handler.startQuery(token, null, Uri.withAppendedPath(CONVERSATIONS_URI,
        account), CONVERSATION_PROJECTION, query, selectionArgs, null);
  }

  /**
   * Synchronously gets a cursor over all conversations matching a query. The
   * query is in Gmail's query syntax.
   * 
   * @param account
   *            run the query on this account
   * @param query
   *            a query in Gmail's query syntax
   * @param becomeActiveNetworkCursor
   *            whether or not the returned cursor should become the Active
   *            Network Cursor
   */
  public ConversationCursor getConversationCursorForQuery(String account,
      String query, BecomeActiveNetworkCursor becomeActiveNetworkCursor) {
    String[] selectionArgs = getSelectionArguments(becomeActiveNetworkCursor);
    Cursor cursor = mContentResolver.query(Uri.withAppendedPath(
        CONVERSATIONS_URI, account), CONVERSATION_PROJECTION, query,
        selectionArgs, null);
    return new ConversationCursor(this, account, cursor);
  }

  /**
   * Gets a message cursor over the single message with the given id.
   * 
   * @param account
   *            get the cursor for messages in this account
   * @param messageId
   *            the id of the message
   * @return a cursor over the message
   */
  public MessageCursor getMessageCursorForMessageId(String account,
      long messageId) {
    if (TextUtils.isEmpty(account)) {
      throw new IllegalArgumentException("account is empty");
    }
    Uri uri = Uri
        .parse(AUTHORITY_PLUS_MESSAGES + account + "/" + messageId);
    Cursor cursor = mContentResolver.query(uri, MESSAGE_PROJECTION, null,
        null, null);
    return new MessageCursor(this, mContentResolver, account, cursor);
  }

  /**
   * Gets a message cursor over the messages that match the query. Note that
   * this simply finds all of the messages that match and returns them. It
   * does not return all messages in conversations where any message matches.
   * 
   * @param account
   *            get the cursor for messages in this account
   * @param query
   *            a query in GMail's query syntax. Currently only queries of the
   *            form [label:<label>] are supported
   * @return a cursor over the messages
   */
  public MessageCursor getLocalMessageCursorForQuery(String account,
      String query) {
    if (TextUtils.isEmpty(account)) {
      throw new IllegalArgumentException("account is empty");
    }
    Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/");
    Cursor cursor = mContentResolver.query(uri, MESSAGE_PROJECTION, query,
        null, null);
    return new MessageCursor(this, mContentResolver, account, cursor);
  }

  /**
   * Gets a cursor over all of the messages in a conversation.
   * 
   * @param account
   *            get the cursor for messages in this account
   * @param conversationId
   *            the id of the converstion to fetch messages for
   * @return a cursor over messages in the conversation
   */
  public MessageCursor getMessageCursorForConversationId(String account,
      long conversationId) {
    if (TextUtils.isEmpty(account)) {
      throw new IllegalArgumentException("account is empty");
    }
    Uri uri = Uri.parse(AUTHORITY_PLUS_CONVERSATIONS + account + "/"
        + conversationId + "/messages");
    Cursor cursor = mContentResolver.query(uri, MESSAGE_PROJECTION, null,
        null, null);
    return new MessageCursor(this, mContentResolver, account, cursor);
  }

  /**
   * Expunge the indicated message. One use of this is to discard drafts.
   * 
   * @param account
   *            the account of the message id
   * @param messageId
   *            the id of the message to expunge
   */
  public void expungeMessage(String account, long messageId) {
    if (TextUtils.isEmpty(account)) {
      throw new IllegalArgumentException("account is empty");
    }
    Uri uri = Uri
        .parse(AUTHORITY_PLUS_MESSAGES + account + "/" + messageId);
    mContentResolver.delete(uri, null, null);
  }

  /**
   * Adds or removes the label on the conversation.
   * 
   * @param account
   *            the account of the conversation
   * @param conversationId
   *            the conversation
   * @param maxServerMessageId
   *            the highest message id to whose labels should be changed. Note
   *            that everywhere else in this file messageId means local
   *            message id but here you need to use a server message id.
   * @param label
   *            the label to add or remove
   * @param add
   *            true to add the label, false to remove it
   */
  public void addOrRemoveLabelOnConversation(String account,
      long conversationId, long maxServerMessageId, String label,
      boolean add) {
    if (TextUtils.isEmpty(account)) {
      throw new IllegalArgumentException("account is empty");
    }
    if (add) {
      Uri uri = Uri.parse(AUTHORITY_PLUS_CONVERSATIONS + account + "/"
          + conversationId + "/labels");
      ContentValues values = new ContentValues();
      values.put(LabelColumns.CANONICAL_NAME, label);
      values.put(ConversationColumns.MAX_MESSAGE_ID, maxServerMessageId);
      mContentResolver.insert(uri, values);
    } else {
      String encodedLabel;
      try {
        encodedLabel = URLEncoder.encode(label, "utf-8");
      } catch (UnsupportedEncodingException e) {
        throw new RuntimeException(e);
      }
      Uri uri = Uri.parse(AUTHORITY_PLUS_CONVERSATIONS + account + "/"
          + conversationId + "/labels/" + encodedLabel);
      mContentResolver.delete(uri, ConversationColumns.MAX_MESSAGE_ID,
          new String[] { "" + maxServerMessageId });
    }
  }

  /**
   * Adds or removes the label on the message.
   * 
   * @param contentResolver
   *            the content resolver.
   * @param account
   *            the account of the message
   * @param conversationId
   *            the conversation containing the message
   * @param messageId
   *            the id of the message to whose labels should be changed
   * @param label
   *            the label to add or remove
   * @param add
   *            true to add the label, false to remove it
   */
  public static void addOrRemoveLabelOnMessage(
      ContentResolver contentResolver, String account,
      long conversationId, long messageId, String label, boolean add) {

    // conversationId is unused but we want to start passing it whereever we
    // pass a message id.
    if (add) {
      Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/"
          + messageId + "/labels");
      ContentValues values = new ContentValues();
      values.put(LabelColumns.CANONICAL_NAME, label);
      contentResolver.insert(uri, values);
    } else {
      String encodedLabel;
      try {
        encodedLabel = URLEncoder.encode(label, "utf-8");
      } catch (UnsupportedEncodingException e) {
        throw new RuntimeException(e);
      }
      Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/"
          + messageId + "/labels/" + encodedLabel);
      contentResolver.delete(uri, null, null);
    }
  }

  /**
   * The mail provider will send an intent when certain changes happen in
   * certain labels. Currently those labels are inbox and voicemail.
   * 
   * <p>
   * The intent will have the action ACTION_PROVIDER_CHANGED and the extras
   * mentioned below. The data for the intent will be
   * content://gmail-ls/unread/<name of label>.
   * 
   * <p>
   * The goal is to support the following user experience:
   * <ul>
   * <li>When present the new mail indicator reports the number of unread
   * conversations in the inbox (or some other label).</li>
   * <li>When the user views the inbox the indicator is removed immediately.
   * They do not have to read all of the conversations.</li>
   * <li>If more mail arrives the indicator reappears and shows the total
   * number of unread conversations in the inbox.</li>
   * <li>If the user reads the new conversations on the web the indicator
   * disappears on the phone since there is no unread mail in the inbox that
   * the user hasn't seen.</li>
   * <li>The phone should vibrate/etc when it transitions from having no
   * unseen unread inbox mail to having some.</li>
   */

  /** The account in which the change occurred. */
  static public final String PROVIDER_CHANGED_EXTRA_ACCOUNT = "account";

  /** The number of unread conversations matching the label. */
  static public final String PROVIDER_CHANGED_EXTRA_COUNT = "count";

  /** Whether to get the user's attention, perhaps by vibrating. */
  static public final String PROVIDER_CHANGED_EXTRA_GET_ATTENTION = "getAttention";

  /**
   * A label that is attached to all of the conversations being notified
   * about. This enables the receiver of a notification to get a list of
   * matching conversations.
   */
  static public final String PROVIDER_CHANGED_EXTRA_TAG_LABEL = "tagLabel";

  /**
   * Settings for which conversations should be synced to the phone.
   * Conversations are synced if any message matches any of the following
   * criteria:
   * 
   * <ul>
   * <li>the message has a label in the include set</li>
   * <li>the message is no older than conversationAgeDays and has a label in
   * the partial set.</li>
   * <li>also, pending changes on the server: the message has no
   * user-controllable labels.</li>
   * </ul>
   * 
   * <p>
   * A user-controllable label is a user-defined label or star, inbox, trash,
   * spam, etc. LABEL_UNREAD is not considered user-controllable.
   */
  public static class Settings {
    public long conversationAgeDays;
    public long maxAttachmentSizeMb;
    public String[] labelsIncluded;
    public String[] labelsPartial;
  }

  /**
   * Returns the settings.
   * 
   * @param account
   *            the account whose setting should be retrieved
   */
  public Settings getSettings(String account) {
    if (TextUtils.isEmpty(account)) {
      throw new IllegalArgumentException("account is empty");
    }
    Settings settings = new Settings();
    Cursor cursor = mContentResolver.query(Uri.withAppendedPath(
        SETTINGS_URI, account), SETTINGS_PROJECTION, null, null, null);
    cursor.moveToNext();
    settings.labelsIncluded = TextUtils.split(cursor.getString(0),
        SPACE_SEPARATOR_PATTERN);
    settings.labelsPartial = TextUtils.split(cursor.getString(1),
        SPACE_SEPARATOR_PATTERN);
    settings.conversationAgeDays = Long.parseLong(cursor.getString(2));
    settings.maxAttachmentSizeMb = Long.parseLong(cursor.getString(3));
    cursor.close();
    return settings;
  }

  /**
   * Sets the settings. A sync will be scheduled automatically.
   */
  public void setSettings(String account, Settings settings) {
    if (TextUtils.isEmpty(account)) {
      throw new IllegalArgumentException("account is empty");
    }
    ContentValues values = new ContentValues();
    values.put(SettingsColumns.LABELS_INCLUDED, TextUtils.join(" ",
        settings.labelsIncluded));
    values.put(SettingsColumns.LABELS_PARTIAL, TextUtils.join(" ",
        settings.labelsPartial));
    values.put(SettingsColumns.CONVERSATION_AGE_DAYS,
        settings.conversationAgeDays);
    values.put(SettingsColumns.MAX_ATTACHMENET_SIZE_MB,
        settings.maxAttachmentSizeMb);
    mContentResolver.update(Uri.withAppendedPath(SETTINGS_URI, account),
        values, null, null);
  }

  /**
   * Uses sender instructions to build a formatted string.
   * 
   * <p>
   * Sender list instructions contain compact information about the sender
   * list. Most work that can be done without knowing how much room will be
   * availble for the sender list is done when creating the instructions.
   * 
   * <p>
   * The instructions string consists of tokens separated by
   * SENDER_LIST_SEPARATOR. Here are the tokens, one per line:
   * <ul>
   * <li><tt>n</tt></li>
   * <li><em>int</em>, the number of non-draft messages in the conversation</li>
   * <li><tt>d</tt</li>
   * <li><em>int</em>, the number of drafts in the conversation</li>
   * <li><tt>l</tt></li>
   * <li><em>literal html to be included in the output</em></li>
   * <li><tt>s</tt> indicates that the message is sending (in the outbox
   * without errors)</li>
   * <li><tt>f</tt> indicates that the message failed to send (in the outbox
   * with errors)</li>
   * <li><em>for each message</em>
   * <ul>
   * <li><em>int</em>, 0 for read, 1 for unread</li>
   * <li><em>int</em>, the priority of the message. Zero is the most important
   * </li>
   * <li><em>text</em>, the sender text or blank for messages from 'me'</li>
   * </ul>
   * </li>
   * <li><tt>e</tt> to indicate that one or more messages have been elided</li>
   * 
   * <p>
   * The instructions indicate how many messages and drafts are in the
   * conversation and then describe the most important messages in order,
   * indicating the priority of each message and whether the message is
   * unread.
   * 
   * @param instructions
   *            instructions as described above
   * @param sb
   *            the SpannableStringBuilder to append to
   * @param maxChars
   *            the number of characters available to display the text
   * @param unreadStyle
   *            the CharacterStyle for unread messages, or null
   * @param draftsStyle
   *            the CharacterStyle for draft messages, or null
   * @param sendingString
   *            the string to use when there are messages scheduled to be sent
   * @param sendFailedString
   *            the string to use when there are messages that mailed to send
   * @param meString
   *            the string to use for messages sent by this user
   * @param draftString
   *            the string to use for "Draft"
   * @param draftPluralString
   *            the string to use for "Drafts"
   */
  public static void 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) {
    assert !(forceAllUnread && forceAllRead);
    boolean unreadStatusIsForced = forceAllUnread || forceAllRead;
    boolean forcedUnreadStatus = forceAllUnread;

    // Measure each fragment. It's ok to iterate over the entire set of
    // fragments because it is
    // never a long list, even if there are many senders.
    final Map<Integer, Integer> priorityToLength = sPriorityToLength;
    priorityToLength.clear();

    int maxFoundPriority = Integer.MIN_VALUE;
    int numMessages = 0;
    int numDrafts = 0;
    CharSequence draftsFragment = "";
    CharSequence sendingFragment = "";
    CharSequence sendFailedFragment = "";

    sSenderListSplitter.setString(instructions);
    int numFragments = 0;
    String[] fragments = sSenderFragments;
    int currentSize = fragments.length;
    while (sSenderListSplitter.hasNext()) {
      fragments[numFragments++] = sSenderListSplitter.next();
      if (numFragments == currentSize) {
        sSenderFragments = new String[2 * currentSize];
        System
            .arraycopy(fragments, 0, sSenderFragments, 0,
                currentSize);
        currentSize *= 2;
        fragments = sSenderFragments;
      }
    }

    for (int i = 0; i < numFragments;) {
      String fragment0 = fragments[i++];
      if ("".equals(fragment0)) {
        // This should be the final fragment.
      } else if (Gmail.SENDER_LIST_TOKEN_ELIDED.equals(fragment0)) {
        // ignore
      } else if (Gmail.SENDER_LIST_TOKEN_NUM_MESSAGES.equals(fragment0)) {
        numMessages = Integer.valueOf(fragments[i++]);
      } else if (Gmail.SENDER_LIST_TOKEN_NUM_DRAFTS.equals(fragment0)) {
        String numDraftsString = fragments[i++];
        numDrafts = Integer.parseInt(numDraftsString);
        draftsFragment = numDrafts == 1 ? draftString
            : draftPluralString + " (" + numDraftsString + ")";
      } else if (Gmail.SENDER_LIST_TOKEN_LITERAL.equals(fragment0)) {
        sb.append(Html.fromHtml(fragments[i++]));
        return;
      } else if (Gmail.SENDER_LIST_TOKEN_SENDING.equals(fragment0)) {
        sendingFragment = sendingString;
      } else if (Gmail.SENDER_LIST_TOKEN_SEND_FAILED.equals(fragment0)) {
        sendFailedFragment = sendFailedString;
      } else {
        String priorityString = fragments[i++];
        CharSequence nameString = fragments[i++];
        if (nameString.length() == 0)
          nameString = meString;
        int priority = Integer.parseInt(priorityString);
        priorityToLength.put(priority, nameString.length());
        maxFoundPriority = Math.max(maxFoundPriority, priority);
      }
    }
    String numMessagesFragment = (numMessages != 0) ? " ("
        + Integer.toString(numMessages + numDrafts) + ")" : "";

    // Don't allocate fixedFragment unless we need it
    SpannableStringBuilder fixedFragment = null;
    int fixedFragmentLength = 0;
    if (draftsFragment.length() != 0) {
      if (fixedFragment == null) {
        fixedFragment = new SpannableStringBuilder();
      }
      fixedFragment.append(draftsFragment);
      if (draftsStyle != null) {
        fixedFragment.setSpan(CharacterStyle.wrap(draftsStyle), 0,
            fixedFragment.length(),
            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
      }
    }
    if (sendingFragment.length() != 0) {
      if (fixedFragment == null) {
        fixedFragment = new SpannableStringBuilder();
      }
      if (fixedFragment.length() != 0)
        fixedFragment.append(", ");
      fixedFragment.append(sendingFragment);
    }
    if (sendFailedFragment.length() != 0) {
      if (fixedFragment == null) {
        fixedFragment = new SpannableStringBuilder();
      }
      if (fixedFragment.length() != 0)
        fixedFragment.append(", ");
      fixedFragment.append(sendFailedFragment);
    }

    if (fixedFragment != null) {
      fixedFragmentLength = fixedFragment.length();
    }

    final boolean normalMessagesExist = numMessagesFragment.length() != 0
        || maxFoundPriority != Integer.MIN_VALUE;
    String preFixedFragement = "";
    if (normalMessagesExist && fixedFragmentLength != 0) {
      preFixedFragement = ", ";
    }
    int maxPriorityToInclude = -1; // inclusive
    int numCharsUsed = numMessagesFragment.length()
        + preFixedFragement.length() + fixedFragmentLength;
    int numSendersUsed = 0;
    while (maxPriorityToInclude < maxFoundPriority) {
      if (priorityToLength.containsKey(maxPriorityToInclude + 1)) {
        int length = numCharsUsed
            + priorityToLength.get(maxPriorityToInclude + 1);
        if (numCharsUsed > 0)
          length += 2;
        // We must show at least two senders if they exist. If we don't
        // have space for both
        // then we will truncate names.
        if (length > maxChars && numSendersUsed >= 2) {
          break;
        }
        numCharsUsed = length;
        numSendersUsed++;
      }
      maxPriorityToInclude++;
    }

    int numCharsToRemovePerWord = 0;
    if (numCharsUsed > maxChars) {
      numCharsToRemovePerWord = (numCharsUsed - maxChars)
          / numSendersUsed;
    }

    boolean elided = false;
    for (int i = 0; i < numFragments;) {
      String fragment0 = fragments[i++];
      if ("".equals(fragment0)) {
        // This should be the final fragment.
      } else if (SENDER_LIST_TOKEN_ELIDED.equals(fragment0)) {
        elided = true;
      } else if (SENDER_LIST_TOKEN_NUM_MESSAGES.equals(fragment0)) {
        i++;
      } else if (SENDER_LIST_TOKEN_NUM_DRAFTS.equals(fragment0)) {
        i++;
      } else if (SENDER_LIST_TOKEN_SENDING.equals(fragment0)) {
      } else if (SENDER_LIST_TOKEN_SEND_FAILED.equals(fragment0)) {
      } else {
        final String unreadString = fragment0;
        final String priorityString = fragments[i++];
        String nameString = fragments[i++];
        if (nameString.length() == 0)
          nameString = meString.toString();
        if (numCharsToRemovePerWord != 0) {
          nameString = nameString.substring(0, Math.max(nameString
              .length()
              - numCharsToRemovePerWord, 0));
        }
        final boolean unread = unreadStatusIsForced ? forcedUnreadStatus
            : Integer.parseInt(unreadString) != 0;
        final int priority = Integer.parseInt(priorityString);
        if (priority <= maxPriorityToInclude) {
          if (sb.length() != 0) {
            sb.append(elided ? " .. " : ", ");
          }
          elided = false;
          int pos = sb.length();
          sb.append(nameString);
          if (unread && unreadStyle != null) {
            sb.setSpan(CharacterStyle.wrap(unreadStyle), pos, sb
                .length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
          }
        } else {
          elided = true;
        }
      }
    }
    sb.append(numMessagesFragment);
    if (fixedFragmentLength != 0) {
      sb.append(preFixedFragement);
      sb.append(fixedFragment);
    }
  }

  /**
   * This is a cursor that only defines methods to move throught the results
   * and register to hear about changes. All access to the data is left to
   * subinterfaces.
   */
  public static class MailCursor extends ContentObserver {

    // A list of observers of this cursor.
    private final Set<MailCursorObserver> mObservers;

    // Updated values are accumulated here before being written out if the
    // cursor is asked to persist the changes.
    private ContentValues mUpdateValues;

    protected Cursor mCursor;
    protected String mAccount;

    public Cursor getCursor() {
      return mCursor;
    }

    /**
     * Constructs the MailCursor given a regular cursor, registering as a
     * change observer of the cursor.
     * 
     * @param account
     *            the account the cursor is associated with
     * @param cursor
     *            the underlying cursor
     */
    protected MailCursor(String account, Cursor cursor) {
      super(new Handler());
      mObservers = new HashSet<MailCursorObserver>();
      mCursor = cursor;
      mAccount = account;
      if (mCursor != null)
        mCursor.registerContentObserver(this);
    }

    /**
     * Gets the account associated with this cursor.
     * 
     * @return the account.
     */
    public String getAccount() {
      return mAccount;
    }

    protected void checkThread() {
      // Turn this on when activity code no longer runs in the sync thread
      // after notifications of changes.
      // Thread currentThread = Thread.currentThread();
      // if (currentThread != mThread) {
      // throw new RuntimeException("Accessed from the wrong thread");
      // }
    }

    /**
     * Lazily constructs a map of update values to apply to the database if
     * requested. This map is cleared out when we move to a different item
     * in the result set.
     * 
     * @return a map of values to be applied by an update.
     */
    protected ContentValues getUpdateValues() {
      if (mUpdateValues == null) {
        mUpdateValues = new ContentValues();
      }
      return mUpdateValues;
    }

    /**
     * Called whenever mCursor is changed to point to a different row.
     * Subclasses should override this if they need to clear out state when
     * this happens.
     * 
     * Subclasses must call the inherited version if they override this.
     */
    protected void onCursorPositionChanged() {
      mUpdateValues = null;
    }

    // ********* MailCursor

    /**
     * Returns the numbers of rows in the cursor.
     * 
     * @return the number of rows in the cursor.
     */
    final public int count() {
      if (mCursor != null) {
        return mCursor.getCount();
      } else {
        return 0;
      }
    }

    /**
     * @return the current position of this cursor, or -1 if this cursor has
     *         not been initialized.
     */
    final public int position() {
      if (mCursor != null) {
        return mCursor.getPosition();
      } else {
        return -1;
      }
    }

    /**
     * Move the cursor to an absolute position. The valid range of vaues is
     * -1 &lt;= position &lt;= count.
     * 
     * <p>
     * This method will return true if the request destination was
     * reachable, otherwise it returns false.
     * 
     * @param position
     *            the zero-based position to move to.
     * @return whether the requested move fully succeeded.
     */
    final public boolean moveTo(int position) {
      checkCursor();
      checkThread();
      boolean moved = mCursor.moveToPosition(position);
      if (moved)
        onCursorPositionChanged();
      return moved;
    }

    /**
     * Move the cursor to the next row.
     * 
     * <p>
     * This method will return false if the cursor is already past the last
     * entry in the result set.
     * 
     * @return whether the move succeeded.
     */
    final public boolean next() {
      checkCursor();
      checkThread();
      boolean moved = mCursor.moveToNext();
      if (moved)
        onCursorPositionChanged();
      return moved;
    }

    /**
     * Release all resources and locks associated with the cursor. The
     * cursor will not be valid after this function is called.
     */
    final public void release() {
      if (mCursor != null) {
        mCursor.unregisterContentObserver(this);
        mCursor.deactivate();
      }
    }

    final public void registerContentObserver(ContentObserver observer) {
      mCursor.registerContentObserver(observer);
    }

    final public void unregisterContentObserver(ContentObserver observer) {
      mCursor.unregisterContentObserver(observer);
    }

    final public void registerDataSetObserver(DataSetObserver observer) {
      mCursor.registerDataSetObserver(observer);
    }

    final public void unregisterDataSetObserver(DataSetObserver observer) {
      mCursor.unregisterDataSetObserver(observer);
    }

    /**
     * Register an observer to hear about changes to the cursor.
     * 
     * @param observer
     *            the observer to register
     */
    final public void registerObserver(MailCursorObserver observer) {
      mObservers.add(observer);
    }

    /**
     * Unregister an observer.
     * 
     * @param observer
     *            the observer to unregister
     */
    final public void unregisterObserver(MailCursorObserver observer) {
      mObservers.remove(observer);
    }

    // ********* ContentObserver

    @Override
    final public boolean deliverSelfNotifications() {
      return false;
    }

    @Override
    public void onChange(boolean selfChange) {
      if (DEBUG) {
        Log.d(TAG, "MailCursor is notifying " + mObservers.size()
            + " observers");
      }
      for (MailCursorObserver o : mObservers) {
        o.onCursorChanged(this);
      }
    }

    protected void checkCursor() {
      if (mCursor == null) {
        throw new IllegalStateException(
            "cannot read from an insertion cursor");
      }
    }

    /**
     * Returns the string value of the column, or "" if the value is null.
     */
    protected String getStringInColumn(int columnIndex) {
      checkCursor();
      return toNonnullString(mCursor.getString(columnIndex));
    }
  }

  /**
   * A MailCursor observer is notified of changes to the result set of a
   * cursor.
   */
  public interface MailCursorObserver {

    /**
     * Called when the result set of a cursor has changed.
     * 
     * @param cursor
     *            the cursor whose result set has changed.
     */
    void onCursorChanged(MailCursor cursor);
  }

  /**
   * A cursor over labels.
   */
  public final class LabelCursor extends MailCursor {

    private final int mNameIndex;
    private final int mNumConversationsIndex;
    private final int mNumUnreadConversationsIndex;

    private LabelCursor(String account, Cursor cursor) {
      super(account, cursor);

      mNameIndex = mCursor
          .getColumnIndexOrThrow(LabelColumns.CANONICAL_NAME);
      mNumConversationsIndex = mCursor
          .getColumnIndexOrThrow(LabelColumns.NUM_CONVERSATIONS);
      mNumUnreadConversationsIndex = mCursor
          .getColumnIndexOrThrow(LabelColumns.NUM_UNREAD_CONVERSATIONS);
    }

    /**
     * Gets the canonical name of the current label.
     * 
     * @return the current label's name.
     */
    public String getName() {
      return getStringInColumn(mNameIndex);
    }

    /**
     * Gets the number of conversations with this label.
     * 
     * @return the number of conversations with this label.
     */
    public int getNumConversations() {
      return mCursor.getInt(mNumConversationsIndex);
    }

    /**
     * Gets the number of unread conversations with this label.
     * 
     * @return the number of unread conversations with this label.
     */
    public int getNumUnreadConversations() {
      return mCursor.getInt(mNumUnreadConversationsIndex);
    }
  }

  /**
   * This is a map of labels. TODO: make it observable.
   */
  public static final class LabelMap extends Observable {
    private final static ContentValues EMPTY_CONTENT_VALUES = new ContentValues();

    private ContentQueryMap mQueryMap;
    private SortedSet<String> mSortedUserLabels;
    private Map<String, Long> mCanonicalNameToId;

    private long mLabelIdSent;
    private long mLabelIdInbox;
    private long mLabelIdDraft;
    private long mLabelIdUnread;
    private long mLabelIdTrash;
    private long mLabelIdSpam;
    private long mLabelIdStarred;
    private long mLabelIdChat;
    private long mLabelIdVoicemail;
    private long mLabelIdIgnored;
    private long mLabelIdVoicemailInbox;
    private long mLabelIdCached;
    private long mLabelIdOutbox;

    private boolean mLabelsSynced = false;

    public LabelMap(ContentResolver contentResolver, String account,
        boolean keepUpdated) {
      if (TextUtils.isEmpty(account)) {
        throw new IllegalArgumentException("account is empty");
      }
      Cursor cursor = contentResolver.query(Uri.withAppendedPath(
          LABELS_URI, account), LABEL_PROJECTION, null, null, null);
      init(cursor, keepUpdated);
    }

    public LabelMap(Cursor cursor, boolean keepUpdated) {
      init(cursor, keepUpdated);
    }

    private void init(Cursor cursor, boolean keepUpdated) {
      mQueryMap = new ContentQueryMap(cursor, BaseColumns._ID,
          keepUpdated, null);
      mSortedUserLabels = new TreeSet<String>(java.text.Collator
          .getInstance());
      mCanonicalNameToId = Maps.newHashMap();
      updateDataStructures();
      mQueryMap.addObserver(new Observer() {
        public void update(Observable observable, Object data) {
          updateDataStructures();
          setChanged();
          notifyObservers();
        }
      });
    }

    /**
     * @return whether at least some labels have been synced.
     */
    public boolean labelsSynced() {
      return mLabelsSynced;
    }

    /**
     * Updates the data structures that are maintained separately from
     * mQueryMap after the query map has changed.
     */
    private void updateDataStructures() {
      mSortedUserLabels.clear();
      mCanonicalNameToId.clear();
      for (Map.Entry<String, ContentValues> row : mQueryMap.getRows()
          .entrySet()) {
        long labelId = Long.valueOf(row.getKey());
        String canonicalName = row.getValue().getAsString(
            LabelColumns.CANONICAL_NAME);
        if (isLabelUserDefined(canonicalName)) {
          mSortedUserLabels.add(canonicalName);
        }
        mCanonicalNameToId.put(canonicalName, labelId);

        if (LABEL_SENT.equals(canonicalName)) {
          mLabelIdSent = labelId;
        } else if (LABEL_INBOX.equals(canonicalName)) {
          mLabelIdInbox = labelId;
        } else if (LABEL_DRAFT.equals(canonicalName)) {
          mLabelIdDraft = labelId;
        } else if (LABEL_UNREAD.equals(canonicalName)) {
          mLabelIdUnread = labelId;
        } else if (LABEL_TRASH.equals(canonicalName)) {
          mLabelIdTrash = labelId;
        } else if (LABEL_SPAM.equals(canonicalName)) {
          mLabelIdSpam = labelId;
        } else if (LABEL_STARRED.equals(canonicalName)) {
          mLabelIdStarred = labelId;
        } else if (LABEL_CHAT.equals(canonicalName)) {
          mLabelIdChat = labelId;
        } else if (LABEL_IGNORED.equals(canonicalName)) {
          mLabelIdIgnored = labelId;
        } else if (LABEL_VOICEMAIL.equals(canonicalName)) {
          mLabelIdVoicemail = labelId;
        } else if (LABEL_VOICEMAIL_INBOX.equals(canonicalName)) {
          mLabelIdVoicemailInbox = labelId;
        } else if (LABEL_CACHED.equals(canonicalName)) {
          mLabelIdCached = labelId;
        } else if (LABEL_OUTBOX.equals(canonicalName)) {
          mLabelIdOutbox = labelId;
        }
        mLabelsSynced = mLabelIdSent != 0 && mLabelIdInbox != 0
            && mLabelIdDraft != 0 && mLabelIdUnread != 0
            && mLabelIdTrash != 0 && mLabelIdSpam != 0
            && mLabelIdStarred != 0 && mLabelIdChat != 0
            && mLabelIdIgnored != 0 && mLabelIdVoicemail != 0;
      }
    }

    public long getLabelIdSent() {
      checkLabelsSynced();
      return mLabelIdSent;
    }

    public long getLabelIdInbox() {
      checkLabelsSynced();
      return mLabelIdInbox;
    }

    public long getLabelIdDraft() {
      checkLabelsSynced();
      return mLabelIdDraft;
    }

    public long getLabelIdUnread() {
      checkLabelsSynced();
      return mLabelIdUnread;
    }

    public long getLabelIdTrash() {
      checkLabelsSynced();
      return mLabelIdTrash;
    }

    public long getLabelIdSpam() {
      checkLabelsSynced();
      return mLabelIdSpam;
    }

    public long getLabelIdStarred() {
      checkLabelsSynced();
      return mLabelIdStarred;
    }

    public long getLabelIdChat() {
      checkLabelsSynced();
      return mLabelIdChat;
    }

    public long getLabelIdIgnored() {
      checkLabelsSynced();
      return mLabelIdIgnored;
    }

    public long getLabelIdVoicemail() {
      checkLabelsSynced();
      return mLabelIdVoicemail;
    }

    public long getLabelIdVoicemailInbox() {
      checkLabelsSynced();
      return mLabelIdVoicemailInbox;
    }

    public long getLabelIdCached() {
      checkLabelsSynced();
      return mLabelIdCached;
    }

    public long getLabelIdOutbox() {
      checkLabelsSynced();
      return mLabelIdOutbox;
    }

    private void checkLabelsSynced() {
      if (!labelsSynced()) {
        throw new IllegalStateException("LabelMap not initalized");
      }
    }

    /** Returns the list of user-defined labels in alphabetical order. */
    public SortedSet<String> getSortedUserLabels() {
      return mSortedUserLabels;
    }

    private static final List<String> SORTED_USER_MEANINGFUL_SYSTEM_LABELS = Lists
        .newArrayList(LABEL_INBOX, LABEL_STARRED, LABEL_CHAT,
            LABEL_SENT, LABEL_OUTBOX, LABEL_DRAFT, LABEL_ALL,
            LABEL_SPAM, LABEL_TRASH);

    private static final Set<String> USER_MEANINGFUL_SYSTEM_LABELS_SET = Sets
        .newHashSet(SORTED_USER_MEANINGFUL_SYSTEM_LABELS
            .toArray(new String[] {}));

    public static List<String> getSortedUserMeaningfulSystemLabels() {
      return SORTED_USER_MEANINGFUL_SYSTEM_LABELS;
    }

    public static Set<String> getUserMeaningfulSystemLabelsSet() {
      return USER_MEANINGFUL_SYSTEM_LABELS_SET;
    }

    /**
     * If you are ever tempted to remove outbox or draft from this set make
     * sure you have a way to stop draft and outbox messages from getting
     * purged before they are sent to the server.
     */
    private static final Set<String> FORCED_INCLUDED_LABELS = Sets
        .newHashSet(LABEL_OUTBOX, LABEL_DRAFT);

    public static Set<String> getForcedIncludedLabels() {
      return FORCED_INCLUDED_LABELS;
    }

    private static final Set<String> FORCED_INCLUDED_OR_PARTIAL_LABELS = Sets
        .newHashSet(LABEL_INBOX);

    public static Set<String> getForcedIncludedOrPartialLabels() {
      return FORCED_INCLUDED_OR_PARTIAL_LABELS;
    }

    private static final Set<String> FORCED_UNSYNCED_LABELS = Sets
        .newHashSet(LABEL_ALL, LABEL_CHAT, LABEL_SPAM, LABEL_TRASH);

    public static Set<String> getForcedUnsyncedLabels() {
      return FORCED_UNSYNCED_LABELS;
    }

    /**
     * Returns the number of conversation with a given label.
     * 
     * @deprecated Use {@link #getLabelId} instead.
     */
    @Deprecated
    public int getNumConversations(String label) {
      return getNumConversations(getLabelId(label));
    }

    /** Returns the number of conversation with a given label. */
    public int getNumConversations(long labelId) {
      return getLabelIdValues(labelId).getAsInteger(
          LabelColumns.NUM_CONVERSATIONS);
    }

    /**
     * Returns the number of unread conversation with a given label.
     * 
     * @deprecated Use {@link #getLabelId} instead.
     */
    @Deprecated
    public int getNumUnreadConversations(String label) {
      return getNumUnreadConversations(getLabelId(label));
    }

    /** Returns the number of unread conversation with a given label. */
    public int getNumUnreadConversations(long labelId) {
      Integer unreadConversations = getLabelIdValues(labelId)
          .getAsInteger(LabelColumns.NUM_UNREAD_CONVERSATIONS);
      // There seems to be a race condition here that can get the label
      // maps into a bad
      // state and lose state on a particular label.
      int result = 0;
      if (unreadConversations != null) {
        result = unreadConversations < 0 ? 0 : unreadConversations;
      }

      return result;
    }

    /**
     * @return the canonical name for a label
     */
    public String getCanonicalName(long labelId) {
      return getLabelIdValues(labelId).getAsString(
          LabelColumns.CANONICAL_NAME);
    }

    /**
     * @return the human name for a label
     */
    public String getName(long labelId) {
      return getLabelIdValues(labelId).getAsString(LabelColumns.NAME);
    }

    /**
     * @return whether a given label is known
     */
    public boolean hasLabel(long labelId) {
      return mQueryMap.getRows().containsKey(Long.toString(labelId));
    }

    /**
     * @return returns the id of a label given the canonical name
     * @deprecated this is only needed because most of the UI uses label
     *             names instead of ids
     */
    @Deprecated
    public long getLabelId(String canonicalName) {
      if (mCanonicalNameToId.containsKey(canonicalName)) {
        return mCanonicalNameToId.get(canonicalName);
      } else {
        throw new IllegalArgumentException("Unknown canonical name: "
            + canonicalName);
      }
    }

    private ContentValues getLabelIdValues(long labelId) {
      final ContentValues values = mQueryMap.getValues(Long
          .toString(labelId));
      if (values != null) {
        return values;
      } else {
        return EMPTY_CONTENT_VALUES;
      }
    }

    /**
     * Force the map to requery. This should not be necessary outside tests.
     */
    public void requery() {
      mQueryMap.requery();
    }

    public void close() {
      mQueryMap.close();
    }
  }

  private final Map<String, Gmail.LabelMap> mLabelMaps = Maps.newHashMap();

  public LabelMap getLabelMap(String account) {
    Gmail.LabelMap labelMap = mLabelMaps.get(account);
    if (labelMap == null) {
      labelMap = new Gmail.LabelMap(mContentResolver, account, true /* keepUpdated */);
      mLabelMaps.put(account, labelMap);
    }
    return labelMap;
  }

  public enum PersonalLevel {
    NOT_TO_ME(0), TO_ME_AND_OTHERS(1), ONLY_TO_ME(2);

    private int mLevel;

    PersonalLevel(int level) {
      mLevel = level;
    }

    public int toInt() {
      return mLevel;
    }

    public static PersonalLevel fromInt(int level) {
      switch (level) {
      case 0:
        return NOT_TO_ME;
      case 1:
        return TO_ME_AND_OTHERS;
      case 2:
        return ONLY_TO_ME;
      default:
        throw new IllegalArgumentException(level
            + " is not a personal level");
      }
    }
  }

  /**
   * Indicates a version of an attachment.
   */
  public enum AttachmentRendition {
    /**
     * The full version of an attachment if it can be handled on the device,
     * otherwise the preview.
     */
    BEST,

    /**
     * A smaller or simpler version of the attachment, such as a scaled-down
     * image or an HTML version of a document. Not always available.
     */
    SIMPLE,
  }

  /**
   * The columns that can be requested when querying an attachment's download
   * URI. See getAttachmentDownloadUri.
   */
  public static final class AttachmentColumns implements BaseColumns {

    /** Contains a STATUS value from {@link android.provider.Downloads} */
    public static final String STATUS = "status";

    /**
     * The name of the file to open (with ContentProvider.open). If this is
     * empty then continue to use the attachment's URI.
     * 
     * TODO: I'm not sure that we need this. See the note in CL 66853-p9.
     */
    public static final String FILENAME = "filename";
  }

  /**
   * We track where an attachment came from so that we know how to download it
   * and include it in new messages.
   */
  public enum AttachmentOrigin {
    /** Extras are "<conversationId>-<messageId>-<partId>". */
    SERVER_ATTACHMENT,
    /** Extras are "<path>". */
    LOCAL_FILE;

    private static final String SERVER_EXTRAS_SEPARATOR = "_";

    public static String serverExtras(long conversationId, long messageId,
        String partId) {
      return conversationId + SERVER_EXTRAS_SEPARATOR + messageId
          + SERVER_EXTRAS_SEPARATOR + partId;
    }

    /**
     * @param extras
     *            extras as returned by serverExtras
     * @return an array of conversationId, messageId, partId (all as
     *         strings)
     */
    public static String[] splitServerExtras(String extras) {
      return TextUtils.split(extras, SERVER_EXTRAS_SEPARATOR);
    }

    public static String localFileExtras(Uri path) {
      return path.toString();
    }
  }

  public static final class Attachment {
    /** Identifies the attachment uniquely when combined wih a message id. */
    public String partId;

    /** The intended filename of the attachment. */
    public String name;

    /** The native content type. */
    public String contentType;

    /** The size of the attachment in its native form. */
    public int size;

    /**
     * The content type of the simple version of the attachment. Blank if no
     * simple version is available.
     */
    public String simpleContentType;

    public AttachmentOrigin origin;

    public String originExtras;

    public String toJoinedString() {
      return TextUtils.join("|", Lists.newArrayList(partId == null ? ""
          : partId, name.replace("|", ""), contentType, size,
          simpleContentType, origin.toString(), originExtras));
    }

    public static Attachment parseJoinedString(String joinedString) {
      String[] fragments = TextUtils.split(joinedString, "\\|");
      int i = 0;
      Attachment attachment = new Attachment();
      attachment.partId = fragments[i++];
      if (TextUtils.isEmpty(attachment.partId)) {
        attachment.partId = null;
      }
      attachment.name = fragments[i++];
      attachment.contentType = fragments[i++];
      attachment.size = Integer.parseInt(fragments[i++]);
      attachment.simpleContentType = fragments[i++];
      attachment.origin = AttachmentOrigin.valueOf(fragments[i++]);
      attachment.originExtras = fragments[i++];
      return attachment;
    }
  }

  /**
   * Any given attachment can come in two different renditions (see
   * {@link android.provider.Gmail.AttachmentRendition}) and can be saved to
   * the sd card or to a cache. The gmail provider automatically syncs some
   * attachments to the cache. Other attachments can be downloaded on demand.
   * Attachments in the cache will be purged as needed to save space.
   * Attachments on the SD card must be managed by the user or other software.
   * 
   * @param account
   *            which account to use
   * @param messageId
   *            the id of the mesage with the attachment
   * @param attachment
   *            the attachment
   * @param rendition
   *            the desired rendition
   * @param saveToSd
   *            whether the attachment should be saved to (or loaded from) the
   *            sd card or
   * @return the URI to ask the content provider to open in order to open an
   *         attachment.
   */
  public static Uri getAttachmentUri(String account, long messageId,
      Attachment attachment, AttachmentRendition rendition,
      boolean saveToSd) {
    if (TextUtils.isEmpty(account)) {
      throw new IllegalArgumentException("account is empty");
    }
    if (attachment.origin == AttachmentOrigin.LOCAL_FILE) {
      return Uri.parse(attachment.originExtras);
    } else {
      return Uri.parse(AUTHORITY_PLUS_MESSAGES).buildUpon().appendPath(
          account).appendPath(Long.toString(messageId)).appendPath(
          "attachments").appendPath(attachment.partId).appendPath(
          rendition.toString())
          .appendPath(Boolean.toString(saveToSd)).build();
    }
  }

  /**
   * Return the URI to query in order to find out whether an attachment is
   * downloaded.
   * 
   * <p>
   * Querying this will also start a download if necessary. The cursor
   * returned by querying this URI can contain the columns in
   * {@link android.provider.Gmail.AttachmentColumns}.
   * 
   * <p>
   * Deleting this URI will cancel the download if it was not started
   * automatically by the provider. It will also remove bookkeeping for
   * saveToSd downloads.
   * 
   * @param attachmentUri
   *            the attachment URI as returned by getAttachmentUri. The URI's
   *            authority Gmail.AUTHORITY. If it is not then you should open
   *            the file directly.
   */
  public static Uri getAttachmentDownloadUri(Uri attachmentUri) {
    if (!"content".equals(attachmentUri.getScheme())) {
      throw new IllegalArgumentException(
          "Uri's scheme must be 'content': " + attachmentUri);
    }
    return attachmentUri.buildUpon().appendPath("download").build();
  }

  public enum CursorStatus {
    LOADED, LOADING, ERROR, // A network error occurred.
  }

  /**
   * A cursor over messages.
   */
  public static final class MessageCursor extends MailCursor {

    private LabelMap mLabelMap;

    private final ContentResolver mContentResolver;

    /**
     * Only valid if mCursor == null, in which case we are inserting a new
     * message.
     */
    long mInReplyToLocalMessageId;
    boolean mPreserveAttachments;

    private int mIdIndex;
    private int mConversationIdIndex;
    private int mSubjectIndex;
    private int mSnippetIndex;
    private int mFromIndex;
    private int mToIndex;
    private int mCcIndex;
    private int mBccIndex;
    private int mReplyToIndex;
    private int mDateSentMsIndex;
    private int mDateReceivedMsIndex;
    private int mListInfoIndex;
    private int mPersonalLevelIndex;
    private int mBodyIndex;
    private int mBodyEmbedsExternalResourcesIndex;
    private int mLabelIdsIndex;
    private int mJoinedAttachmentInfosIndex;
    private int mErrorIndex;

    private final TextUtils.StringSplitter mLabelIdsSplitter = newMessageLabelIdsSplitter();

    public MessageCursor(Gmail gmail, ContentResolver cr, String account,
        Cursor cursor) {
      super(account, cursor);
      mLabelMap = gmail.getLabelMap(account);
      if (cursor == null) {
        throw new IllegalArgumentException(
            "null cursor passed to MessageCursor()");
      }

      mContentResolver = cr;

      mIdIndex = mCursor.getColumnIndexOrThrow(MessageColumns.ID);
      mConversationIdIndex = mCursor
          .getColumnIndexOrThrow(MessageColumns.CONVERSATION_ID);
      mSubjectIndex = mCursor
          .getColumnIndexOrThrow(MessageColumns.SUBJECT);
      mSnippetIndex = mCursor
          .getColumnIndexOrThrow(MessageColumns.SNIPPET);
      mFromIndex = mCursor.getColumnIndexOrThrow(MessageColumns.FROM);
      mToIndex = mCursor.getColumnIndexOrThrow(MessageColumns.TO);
      mCcIndex = mCursor.getColumnIndexOrThrow(MessageColumns.CC);
      mBccIndex = mCursor.getColumnIndexOrThrow(MessageColumns.BCC);
      mReplyToIndex = mCursor
          .getColumnIndexOrThrow(MessageColumns.REPLY_TO);
      mDateSentMsIndex = mCursor
          .getColumnIndexOrThrow(MessageColumns.DATE_SENT_MS);
      mDateReceivedMsIndex = mCursor
          .getColumnIndexOrThrow(MessageColumns.DATE_RECEIVED_MS);
      mListInfoIndex = mCursor
          .getColumnIndexOrThrow(MessageColumns.LIST_INFO);
      mPersonalLevelIndex = mCursor
          .getColumnIndexOrThrow(MessageColumns.PERSONAL_LEVEL);
      mBodyIndex = mCursor.getColumnIndexOrThrow(MessageColumns.BODY);
      mBodyEmbedsExternalResourcesIndex = mCursor
          .getColumnIndexOrThrow(MessageColumns.EMBEDS_EXTERNAL_RESOURCES);
      mLabelIdsIndex = mCursor
          .getColumnIndexOrThrow(MessageColumns.LABEL_IDS);
      mJoinedAttachmentInfosIndex = mCursor
          .getColumnIndexOrThrow(MessageColumns.JOINED_ATTACHMENT_INFOS);
      mErrorIndex = mCursor.getColumnIndexOrThrow(MessageColumns.ERROR);

      mInReplyToLocalMessageId = 0;
      mPreserveAttachments = false;
    }

    protected MessageCursor(ContentResolver cr, String account,
        long inReplyToMessageId, boolean preserveAttachments) {
      super(account, null);
      mContentResolver = cr;
      mInReplyToLocalMessageId = inReplyToMessageId;
      mPreserveAttachments = preserveAttachments;
    }

    @Override
    protected void onCursorPositionChanged() {
      super.onCursorPositionChanged();
    }

    public CursorStatus getStatus() {
      Bundle extras = mCursor.getExtras();
      String stringStatus = extras.getString(EXTRA_STATUS);
      return CursorStatus.valueOf(stringStatus);
    }

    /** Retry a network request after errors. */
    public void retry() {
      Bundle input = new Bundle();
      input.putString(RESPOND_INPUT_COMMAND, COMMAND_RETRY);
      Bundle output = mCursor.respond(input);
      String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE);
      assert COMMAND_RESPONSE_OK.equals(response);
    }

    /**
     * Gets the message id of the current message. Note that this is an
     * immutable local message (not, for example, GMail's message id, which
     * is immutable).
     * 
     * @return the message's id
     */
    public long getMessageId() {
      checkCursor();
      return mCursor.getLong(mIdIndex);
    }

    /**
     * Gets the message's conversation id. This must be immutable. (For
     * example, with GMail this should be the original conversation id
     * rather than the default notion of converation id.)
     * 
     * @return the message's conversation id
     */
    public long getConversationId() {
      checkCursor();
      return mCursor.getLong(mConversationIdIndex);
    }

    /**
     * Gets the message's subject.
     * 
     * @return the message's subject
     */
    public String getSubject() {
      return getStringInColumn(mSubjectIndex);
    }

    /**
     * Gets the message's snippet (the short piece of the body). The snippet
     * is generated from the body and cannot be set directly.
     * 
     * @return the message's snippet
     */
    public String getSnippet() {
      return getStringInColumn(mSnippetIndex);
    }

    /**
     * Gets the message's from address.
     * 
     * @return the message's from address
     */
    public String getFromAddress() {
      return getStringInColumn(mFromIndex);
    }

    /**
     * Returns the addresses for the key, if it has been updated, or index
     * otherwise.
     */
    private String[] getAddresses(String key, int index) {
      ContentValues updated = getUpdateValues();
      String addresses;
      if (updated.containsKey(key)) {
        addresses = (String) getUpdateValues().get(key);
      } else {
        addresses = getStringInColumn(index);
      }

      return TextUtils.split(addresses, EMAIL_SEPARATOR_PATTERN);
    }

    /**
     * Gets the message's to addresses.
     * 
     * @return the message's to addresses
     */
    public String[] getToAddresses() {
      return getAddresses(MessageColumns.TO, mToIndex);
    }

    /**
     * Gets the message's cc addresses.
     * 
     * @return the message's cc addresses
     */
    public String[] getCcAddresses() {
      return getAddresses(MessageColumns.CC, mCcIndex);
    }

    /**
     * Gets the message's bcc addresses.
     * 
     * @return the message's bcc addresses
     */
    public String[] getBccAddresses() {
      return getAddresses(MessageColumns.BCC, mBccIndex);
    }

    /**
     * Gets the message's replyTo address.
     * 
     * @return the message's replyTo address
     */
    public String[] getReplyToAddress() {
      return TextUtils.split(getStringInColumn(mReplyToIndex),
          EMAIL_SEPARATOR_PATTERN);
    }

    public long getDateSentMs() {
      checkCursor();
      return mCursor.getLong(mDateSentMsIndex);
    }

    public long getDateReceivedMs() {
      checkCursor();
      return mCursor.getLong(mDateReceivedMsIndex);
    }

    public String getListInfo() {
      return getStringInColumn(mListInfoIndex);
    }

    public PersonalLevel getPersonalLevel() {
      checkCursor();
      int personalLevelInt = mCursor.getInt(mPersonalLevelIndex);
      return PersonalLevel.fromInt(personalLevelInt);
    }

    /**
     * @deprecated Always returns true.
     */
    @Deprecated
    public boolean getExpanded() {
      return true;
    }

    /**
     * Gets the message's body.
     * 
     * @return the message's body
     */
    public String getBody() {
      return getStringInColumn(mBodyIndex);
    }

    /**
     * @return whether the message's body contains embedded references to
     *         external resources. In that case the resources should only be
     *         displayed if the user explicitly asks for them to be
     */
    public boolean getBodyEmbedsExternalResources() {
      checkCursor();
      return mCursor.getInt(mBodyEmbedsExternalResourcesIndex) != 0;
    }

    /**
     * @return a copy of the set of label ids
     */
    public Set<Long> getLabelIds() {
      String labelNames = mCursor.getString(mLabelIdsIndex);
      mLabelIdsSplitter.setString(labelNames);
      return getLabelIdsFromLabelIdsString(mLabelIdsSplitter);
    }

    /**
     * @return a joined string of labels separated by spaces.
     */
    public String getRawLabelIds() {
      return mCursor.getString(mLabelIdsIndex);
    }

    /**
     * Adds a label to a message (if add is true) or removes it (if add is
     * false).
     * 
     * @param label
     *            the label to add or remove
     * @param add
     *            whether to add or remove the label
     */
    public void addOrRemoveLabel(String label, boolean add) {
      addOrRemoveLabelOnMessage(mContentResolver, mAccount,
          getConversationId(), getMessageId(), label, add);
    }

    public ArrayList<Attachment> getAttachmentInfos() {
      ArrayList<Attachment> attachments = Lists.newArrayList();

      String joinedAttachmentInfos = mCursor
          .getString(mJoinedAttachmentInfosIndex);
      if (joinedAttachmentInfos != null) {
        for (String joinedAttachmentInfo : TextUtils.split(
            joinedAttachmentInfos,
            ATTACHMENT_INFO_SEPARATOR_PATTERN)) {

          Attachment attachment = Attachment
              .parseJoinedString(joinedAttachmentInfo);
          attachments.add(attachment);
        }
      }
      return attachments;
    }

    /**
     * @return the error text for the message. Error text gets set if the
     *         server rejects a message that we try to save or send. If
     *         there is error text then the message is no longer scheduled
     *         to be saved or sent. Calling save() or send() will clear any
     *         error as well as scheduling another atempt to save or send
     *         the message.
     */
    public String getErrorText() {
      return mCursor.getString(mErrorIndex);
    }
  }

  /**
   * A helper class for creating or updating messags. Use the putXxx methods
   * to provide initial or new values for the message. Then save or send the
   * message. To save or send an existing message without making other changes
   * to it simply provide an emty ContentValues.
   */
  public static class MessageModification {

    /**
     * Sets the message's subject. Only valid for drafts.
     * 
     * @param values
     *            the ContentValues that will be used to create or update
     *            the message
     * @param subject
     *            the new subject
     */
    public static void putSubject(ContentValues values, String subject) {
      values.put(MessageColumns.SUBJECT, subject);
    }

    /**
     * Sets the message's to address. Only valid for drafts.
     * 
     * @param values
     *            the ContentValues that will be used to create or update
     *            the message
     * @param toAddresses
     *            the new to addresses
     */
    public static void putToAddresses(ContentValues values,
        String[] toAddresses) {
      values.put(MessageColumns.TO, TextUtils.join(EMAIL_SEPARATOR,
          toAddresses));
    }

    /**
     * Sets the message's cc address. Only valid for drafts.
     * 
     * @param values
     *            the ContentValues that will be used to create or update
     *            the message
     * @param ccAddresses
     *            the new cc addresses
     */
    public static void putCcAddresses(ContentValues values,
        String[] ccAddresses) {
      values.put(MessageColumns.CC, TextUtils.join(EMAIL_SEPARATOR,
          ccAddresses));
    }

    /**
     * Sets the message's bcc address. Only valid for drafts.
     * 
     * @param values
     *            the ContentValues that will be used to create or update
     *            the message
     * @param bccAddresses
     *            the new bcc addresses
     */
    public static void putBccAddresses(ContentValues values,
        String[] bccAddresses) {
      values.put(MessageColumns.BCC, TextUtils.join(EMAIL_SEPARATOR,
          bccAddresses));
    }

    /**
     * Saves a new body for the message. Only valid for drafts.
     * 
     * @param values
     *            the ContentValues that will be used to create or update
     *            the message
     * @param body
     *            the new body of the message
     */
    public static void putBody(ContentValues values, String body) {
      values.put(MessageColumns.BODY, body);
    }

    /**
     * Sets the attachments on a message. Only valid for drafts.
     * 
     * @param values
     *            the ContentValues that will be used to create or update
     *            the message
     * @param attachments
     */
    public static void putAttachments(ContentValues values,
        List<Attachment> attachments) {
      values.put(MessageColumns.JOINED_ATTACHMENT_INFOS,
          joinedAttachmentsString(attachments));
    }

    /**
     * Create a new message and save it as a draft or send it.
     * 
     * @param contentResolver
     *            the content resolver to use
     * @param account
     *            the account to use
     * @param values
     *            the values for the new message
     * @param refMessageId
     *            the message that is being replied to or forwarded
     * @param save
     *            whether to save or send the message
     * @return the id of the new message
     */
    public static long sendOrSaveNewMessage(
        ContentResolver contentResolver, String account,
        ContentValues values, long refMessageId, boolean save) {
      values.put(MessageColumns.FAKE_SAVE, save);
      values.put(MessageColumns.FAKE_REF_MESSAGE_ID, refMessageId);
      Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/");
      Uri result = contentResolver.insert(uri, values);
      return ContentUris.parseId(result);
    }

    /**
     * Update an existing draft and save it as a new draft or send it.
     * 
     * @param contentResolver
     *            the content resolver to use
     * @param account
     *            the account to use
     * @param messageId
     *            the id of the message to update
     * @param updateValues
     *            the values to change. Unspecified fields will not be
     *            altered
     * @param save
     *            whether to resave the message as a draft or send it
     */
    public static void sendOrSaveExistingMessage(
        ContentResolver contentResolver, String account,
        long messageId, ContentValues updateValues, boolean save) {
      updateValues.put(MessageColumns.FAKE_SAVE, save);
      updateValues.put(MessageColumns.FAKE_REF_MESSAGE_ID, 0);
      Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/"
          + messageId);
      contentResolver.update(uri, updateValues, null, null);
    }

    /**
     * The string produced here is parsed by
     * Gmail.MessageCursor#getAttachmentInfos.
     */
    public static String joinedAttachmentsString(
        List<Gmail.Attachment> attachments) {
      StringBuilder attachmentsSb = new StringBuilder();
      for (Gmail.Attachment attachment : attachments) {
        if (attachmentsSb.length() != 0) {
          attachmentsSb.append(Gmail.ATTACHMENT_INFO_SEPARATOR);
        }
        attachmentsSb.append(attachment.toJoinedString());
      }
      return attachmentsSb.toString();
    }

  }

  /**
   * A cursor over conversations.
   * 
   * "Conversation" refers to the information needed to populate a list of
   * conversations, not all of the messages in a conversation.
   */
  public static final class ConversationCursor extends MailCursor {

    private final LabelMap mLabelMap;

    private final int mConversationIdIndex;
    private final int mSubjectIndex;
    private final int mSnippetIndex;
    private final int mFromIndex;
    private final int mDateIndex;
    private final int mPersonalLevelIndex;
    private final int mLabelIdsIndex;
    private final int mNumMessagesIndex;
    private final int mMaxMessageIdIndex;
    private final int mHasAttachmentsIndex;
    private final int mHasMessagesWithErrorsIndex;
    private final int mForceAllUnreadIndex;

    private final TextUtils.StringSplitter mLabelIdsSplitter = newConversationLabelIdsSplitter();

    private ConversationCursor(Gmail gmail, String account, Cursor cursor) {
      super(account, cursor);
      mLabelMap = gmail.getLabelMap(account);

      mConversationIdIndex = mCursor
          .getColumnIndexOrThrow(ConversationColumns.ID);
      mSubjectIndex = mCursor
          .getColumnIndexOrThrow(ConversationColumns.SUBJECT);
      mSnippetIndex = mCursor
          .getColumnIndexOrThrow(ConversationColumns.SNIPPET);
      mFromIndex = mCursor
          .getColumnIndexOrThrow(ConversationColumns.FROM);
      mDateIndex = mCursor
          .getColumnIndexOrThrow(ConversationColumns.DATE);
      mPersonalLevelIndex = mCursor
          .getColumnIndexOrThrow(ConversationColumns.PERSONAL_LEVEL);
      mLabelIdsIndex = mCursor
          .getColumnIndexOrThrow(ConversationColumns.LABEL_IDS);
      mNumMessagesIndex = mCursor
          .getColumnIndexOrThrow(ConversationColumns.NUM_MESSAGES);
      mMaxMessageIdIndex = mCursor
          .getColumnIndexOrThrow(ConversationColumns.MAX_MESSAGE_ID);
      mHasAttachmentsIndex = mCursor
          .getColumnIndexOrThrow(ConversationColumns.HAS_ATTACHMENTS);
      mHasMessagesWithErrorsIndex = mCursor
          .getColumnIndexOrThrow(ConversationColumns.HAS_MESSAGES_WITH_ERRORS);
      mForceAllUnreadIndex = mCursor
          .getColumnIndexOrThrow(ConversationColumns.FORCE_ALL_UNREAD);
    }

    @Override
    protected void onCursorPositionChanged() {
      super.onCursorPositionChanged();
    }

    public CursorStatus getStatus() {
      Bundle extras = mCursor.getExtras();
      String stringStatus = extras.getString(EXTRA_STATUS);
      return CursorStatus.valueOf(stringStatus);
    }

    /** Retry a network request after errors. */
    public void retry() {
      Bundle input = new Bundle();
      input.putString(RESPOND_INPUT_COMMAND, COMMAND_RETRY);
      Bundle output = mCursor.respond(input);
      String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE);
      assert COMMAND_RESPONSE_OK.equals(response);
    }

    /**
     * When a conversation cursor is created it becomes the active network
     * cursor, which means that it will fetch results from the network if it
     * needs to in order to show all mail that matches its query. If you
     * later want to requery an older cursor and would like that cursor to
     * be the active cursor you need to call this method before requerying.
     */
    public void becomeActiveNetworkCursor() {
      Bundle input = new Bundle();
      input.putString(RESPOND_INPUT_COMMAND, COMMAND_ACTIVATE);
      Bundle output = mCursor.respond(input);
      String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE);
      assert COMMAND_RESPONSE_OK.equals(response);
    }

    /**
     * Tells the cursor whether its contents are visible to the user. The
     * cursor will automatically broadcast intents to remove any matching
     * new-mail notifications when this cursor's results become visible and,
     * if they are visible, when the cursor is requeried.
     * 
     * Note that contents shown in an activity that is resumed but not
     * focused (onWindowFocusChanged/hasWindowFocus) then results shown in
     * that activity do not count as visible. (This happens when the
     * activity is behind the lock screen or a dialog.)
     * 
     * @param visible
     *            whether the contents of this cursor are visible to the
     *            user.
     */
    public void setContentsVisibleToUser(boolean visible) {
      Bundle input = new Bundle();
      input.putString(RESPOND_INPUT_COMMAND, COMMAND_SET_VISIBLE);
      input.putBoolean(SET_VISIBLE_PARAM_VISIBLE, visible);
      Bundle output = mCursor.respond(input);
      String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE);
      assert COMMAND_RESPONSE_OK.equals(response);
    }

    /**
     * Gets the conversation id. This is immutable. (The server calls it the
     * original conversation id.)
     * 
     * @return the conversation id
     */
    public long getConversationId() {
      return mCursor.getLong(mConversationIdIndex);
    }

    /**
     * Returns the instructions for building from snippets. Pass this to
     * getFromSnippetHtml in order to actually build the snippets.
     * 
     * @return snippet instructions for use by getFromSnippetHtml()
     */
    public String getFromSnippetInstructions() {
      return getStringInColumn(mFromIndex);
    }

    /**
     * Gets the conversation's subject.
     * 
     * @return the subject
     */
    public String getSubject() {
      return getStringInColumn(mSubjectIndex);
    }

    /**
     * Gets the conversation's snippet.
     * 
     * @return the snippet
     */
    public String getSnippet() {
      return getStringInColumn(mSnippetIndex);
    }

    /**
     * Get's the conversation's personal level.
     * 
     * @return the personal level.
     */
    public PersonalLevel getPersonalLevel() {
      int personalLevelInt = mCursor.getInt(mPersonalLevelIndex);
      return PersonalLevel.fromInt(personalLevelInt);
    }

    /**
     * @return a copy of the set of labels. To add or remove labels call
     *         MessageCursor.addOrRemoveLabel on each message in the
     *         conversation.
     * @deprecated use getLabelIds
     */
    @Deprecated
    public Set<String> getLabels() {
      return getLabels(getRawLabelIds(), mLabelMap);
    }

    /**
     * @return a copy of the set of labels. To add or remove labels call
     *         MessageCursor.addOrRemoveLabel on each message in the
     *         conversation.
     */
    public Set<Long> getLabelIds() {
      mLabelIdsSplitter.setString(getRawLabelIds());
      return getLabelIdsFromLabelIdsString(mLabelIdsSplitter);
    }

    /**
     * Returns the set of labels using the raw labels from a previous
     * getRawLabels() as input.
     * 
     * @return a copy of the set of labels. To add or remove labels call
     *         MessageCursor.addOrRemoveLabel on each message in the
     *         conversation.
     */
    public Set<String> getLabels(String rawLabelIds, LabelMap labelMap) {
      mLabelIdsSplitter.setString(rawLabelIds);
      return getCanonicalNamesFromLabelIdsString(labelMap,
          mLabelIdsSplitter);
    }

    /**
     * @return a joined string of labels separated by spaces. Use
     *         getLabels(rawLabels) to convert this to a Set of labels.
     */
    public String getRawLabelIds() {
      return mCursor.getString(mLabelIdsIndex);
    }

    /**
     * @return the number of messages in the conversation
     */
    public int getNumMessages() {
      return mCursor.getInt(mNumMessagesIndex);
    }

    /**
     * @return the max message id in the conversation
     */
    public long getMaxServerMessageId() {
      return mCursor.getLong(mMaxMessageIdIndex);
    }

    public long getDateMs() {
      return mCursor.getLong(mDateIndex);
    }

    public boolean hasAttachments() {
      return mCursor.getInt(mHasAttachmentsIndex) != 0;
    }

    public boolean hasMessagesWithErrors() {
      return mCursor.getInt(mHasMessagesWithErrorsIndex) != 0;
    }

    public boolean getForceAllUnread() {
      return !mCursor.isNull(mForceAllUnreadIndex)
          && mCursor.getInt(mForceAllUnreadIndex) != 0;
    }
  }
}
java2s.com  | Contact Us | Privacy Policy
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.