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