diff options
author | Android (Google) Code Review <android-gerrit@google.com> | 2009-12-08 16:35:05 -0800 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2009-12-08 16:35:05 -0800 |
commit | b882b361f123e4ef7870c99e8a9fd9e1be48e396 (patch) | |
tree | c2a862a461ec5c81911663d9ed25e7da43aea8e1 /core/java/android | |
parent | af9788a9d6c238dd97f40dc020e036739daf0811 (diff) | |
parent | 286b191d30dfd8b63efb1fb2c0d403a3b02d4e0f (diff) | |
download | frameworks_base-b882b361f123e4ef7870c99e8a9fd9e1be48e396.zip frameworks_base-b882b361f123e4ef7870c99e8a9fd9e1be48e396.tar.gz frameworks_base-b882b361f123e4ef7870c99e8a9fd9e1be48e396.tar.bz2 |
Merge change Iee7f2457 into eclair-mr2
* changes:
Deleted Gmail.java from frameworks. Previously it had been moved to vendor/google/.../Gmail/...
Diffstat (limited to 'core/java/android')
-rw-r--r-- | core/java/android/provider/Gmail.java | 2468 |
1 files changed, 0 insertions, 2468 deletions
diff --git a/core/java/android/provider/Gmail.java b/core/java/android/provider/Gmail.java deleted file mode 100644 index 8bb7adf..0000000 --- a/core/java/android/provider/Gmail.java +++ /dev/null @@ -1,2468 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.provider; - -import com.google.android.collect.Lists; -import com.google.android.collect.Maps; -import com.google.android.collect.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.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; - -import com.android.common.Patterns; - -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.ArrayList; -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; - -/** - * A thin wrapper over the content resolver for accessing the gmail provider. - * - * @hide - */ -public final 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"; - private static final String AUTHORITY_PLUS_CONVERSATIONS = - "content://" + AUTHORITY + "/conversations/"; - private static final String AUTHORITY_PLUS_LABELS = - "content://" + AUTHORITY + "/labels/"; - private 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); - private 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 = Maps.newHashMap(); - public static final SimpleStringSplitter sSenderListSplitter = - new SimpleStringSplitter(SENDER_LIST_SEPARATOR); - public static String[] sSenderFragments = new String[8]; - - /** - * 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 = Patterns.EMAIL_ADDRESS.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 - */ - 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. - private 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}; - private 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 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 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 int mNameIndex; - private int mNumConversationsIndex; - private 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 - */ - 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 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 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 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 LabelMap mLabelMap; - - private int mConversationIdIndex; - private int mSubjectIndex; - private int mSnippetIndex; - private int mFromIndex; - private int mDateIndex; - private int mPersonalLevelIndex; - private int mLabelIdsIndex; - private int mNumMessagesIndex; - private int mMaxMessageIdIndex; - private int mHasAttachmentsIndex; - private int mHasMessagesWithErrorsIndex; - private int mForceAllUnreadIndex; - - private 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 - */ - 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; - } - } -} |