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 "bobby" <bob@example.com>
* @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 "bobby" <bob@example.com>
* @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 <= position <= 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;
}
}
}
|