Controller.java revision 7ac38af790f272a92d18dc858d0e0bba124849be
1/* 2 * Copyright (C) 2009 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 17package com.android.email; 18 19import android.content.ContentResolver; 20import android.content.ContentUris; 21import android.content.ContentValues; 22import android.content.Context; 23import android.database.Cursor; 24import android.net.Uri; 25import android.os.RemoteCallbackList; 26import android.os.RemoteException; 27import android.util.Log; 28 29import com.android.email.mail.store.Pop3Store.Pop3Message; 30import com.android.email.provider.AccountBackupRestore; 31import com.android.email.provider.Utilities; 32import com.android.email.service.EmailServiceUtils; 33import com.android.emailcommon.Logging; 34import com.android.emailcommon.mail.AuthenticationFailedException; 35import com.android.emailcommon.mail.MessagingException; 36import com.android.emailcommon.provider.Account; 37import com.android.emailcommon.provider.EmailContent; 38import com.android.emailcommon.provider.EmailContent.Attachment; 39import com.android.emailcommon.provider.EmailContent.MailboxColumns; 40import com.android.emailcommon.provider.EmailContent.Message; 41import com.android.emailcommon.provider.EmailContent.MessageColumns; 42import com.android.emailcommon.provider.HostAuth; 43import com.android.emailcommon.provider.Mailbox; 44import com.android.emailcommon.service.EmailServiceProxy; 45import com.android.emailcommon.service.EmailServiceStatus; 46import com.android.emailcommon.service.IEmailService; 47import com.android.emailcommon.service.IEmailServiceCallback; 48import com.android.emailcommon.service.SearchParams; 49import com.android.emailcommon.utility.AttachmentUtilities; 50import com.android.emailcommon.utility.EmailAsyncTask; 51import com.android.emailcommon.utility.Utility; 52import com.google.common.annotations.VisibleForTesting; 53 54import java.io.FileNotFoundException; 55import java.io.IOException; 56import java.io.InputStream; 57import java.util.ArrayList; 58import java.util.Collection; 59import java.util.HashMap; 60import java.util.HashSet; 61import java.util.concurrent.ConcurrentHashMap; 62 63/** 64 * New central controller/dispatcher for Email activities that may require remote operations. 65 * Handles disambiguating between legacy MessagingController operations and newer provider/sync 66 * based code. We implement Service to allow loadAttachment calls to be sent in a consistent manner 67 * to IMAP, POP3, and EAS by AttachmentDownloadService 68 */ 69public class Controller { 70 private static Controller sInstance; 71 private final Context mContext; 72 private Context mProviderContext; 73 private final ServiceCallback mServiceCallback = new ServiceCallback(); 74 private final HashSet<Result> mListeners = new HashSet<Result>(); 75 /*package*/ final ConcurrentHashMap<Long, Boolean> mLegacyControllerMap = 76 new ConcurrentHashMap<Long, Boolean>(); 77 78 // Note that 0 is a syntactically valid account key; however there can never be an account 79 // with id = 0, so attempts to restore the account will return null. Null values are 80 // handled properly within the code, so this won't cause any issues. 81 private static final long GLOBAL_MAILBOX_ACCOUNT_KEY = 0; 82 /*package*/ static final String ATTACHMENT_MAILBOX_SERVER_ID = "__attachment_mailbox__"; 83 /*package*/ static final String ATTACHMENT_MESSAGE_UID_PREFIX = "__attachment_message__"; 84 /*package*/ static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__"; 85 private static final String WHERE_TYPE_ATTACHMENT = 86 MailboxColumns.TYPE + "=" + Mailbox.TYPE_ATTACHMENT; 87 private static final String WHERE_MAILBOX_KEY = MessageColumns.MAILBOX_KEY + "=?"; 88 89 private static final String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] { 90 EmailContent.RECORD_ID, 91 EmailContent.MessageColumns.ACCOUNT_KEY 92 }; 93 private static final int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1; 94 95 private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?"; 96 private static final String MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION = 97 MAILBOXES_FOR_ACCOUNT_SELECTION + " AND " + MailboxColumns.TYPE + "!=" + 98 Mailbox.TYPE_EAS_ACCOUNT_MAILBOX; 99 private static final String MESSAGES_FOR_ACCOUNT_SELECTION = MessageColumns.ACCOUNT_KEY + "=?"; 100 101 // Service callbacks as set up via setCallback 102 private static RemoteCallbackList<IEmailServiceCallback> sCallbackList = 103 new RemoteCallbackList<IEmailServiceCallback>(); 104 105 private volatile boolean mInUnitTests = false; 106 107 protected Controller(Context _context) { 108 mContext = _context.getApplicationContext(); 109 mProviderContext = _context; 110 } 111 112 /** 113 * Mark this controller as being in use in a unit test. 114 * This is a kludge vs having proper mocks and dependency injection; since the Controller is a 115 * global singleton there isn't much else we can do. 116 */ 117 public void markForTest(boolean inUnitTests) { 118 mInUnitTests = inUnitTests; 119 } 120 121 /** 122 * Gets or creates the singleton instance of Controller. 123 */ 124 public synchronized static Controller getInstance(Context _context) { 125 if (sInstance == null) { 126 sInstance = new Controller(_context); 127 } 128 return sInstance; 129 } 130 131 /** 132 * Inject a mock controller. Used only for testing. Affects future calls to getInstance(). 133 * 134 * Tests that use this method MUST clean it up by calling this method again with null. 135 */ 136 public synchronized static void injectMockControllerForTest(Controller mockController) { 137 sInstance = mockController; 138 } 139 140 /** 141 * For testing only: Inject a different context for provider access. This will be 142 * used internally for access the underlying provider (e.g. getContentResolver().query()). 143 * @param providerContext the provider context to be used by this instance 144 */ 145 public void setProviderContext(Context providerContext) { 146 mProviderContext = providerContext; 147 } 148 149 /** 150 * Any UI code that wishes for callback results (on async ops) should register their callback 151 * here (typically from onResume()). Unregistered callbacks will never be called, to prevent 152 * problems when the command completes and the activity has already paused or finished. 153 * @param listener The callback that may be used in action methods 154 */ 155 public void addResultCallback(Result listener) { 156 synchronized (mListeners) { 157 listener.setRegistered(true); 158 mListeners.add(listener); 159 } 160 } 161 162 /** 163 * Any UI code that no longer wishes for callback results (on async ops) should unregister 164 * their callback here (typically from onPause()). Unregistered callbacks will never be called, 165 * to prevent problems when the command completes and the activity has already paused or 166 * finished. 167 * @param listener The callback that may no longer be used 168 */ 169 public void removeResultCallback(Result listener) { 170 synchronized (mListeners) { 171 listener.setRegistered(false); 172 mListeners.remove(listener); 173 } 174 } 175 176 public Collection<Result> getResultCallbacksForTest() { 177 return mListeners; 178 } 179 180 /** 181 * Delete all Messages that live in the attachment mailbox 182 */ 183 public void deleteAttachmentMessages() { 184 // Note: There should only be one attachment mailbox at present 185 ContentResolver resolver = mProviderContext.getContentResolver(); 186 Cursor c = null; 187 try { 188 c = resolver.query(Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION, 189 WHERE_TYPE_ATTACHMENT, null, null); 190 while (c.moveToNext()) { 191 long mailboxId = c.getLong(EmailContent.ID_PROJECTION_COLUMN); 192 // Must delete attachments BEFORE messages 193 AttachmentUtilities.deleteAllMailboxAttachmentFiles(mProviderContext, 0, 194 mailboxId); 195 resolver.delete(Message.CONTENT_URI, WHERE_MAILBOX_KEY, 196 new String[] {Long.toString(mailboxId)}); 197 } 198 } finally { 199 if (c != null) { 200 c.close(); 201 } 202 } 203 } 204 205 /** 206 * Get a mailbox based on a sqlite WHERE clause 207 */ 208 private Mailbox getGlobalMailboxWhere(String where) { 209 Cursor c = mProviderContext.getContentResolver().query(Mailbox.CONTENT_URI, 210 Mailbox.CONTENT_PROJECTION, where, null, null); 211 try { 212 if (c.moveToFirst()) { 213 Mailbox m = new Mailbox(); 214 m.restore(c); 215 return m; 216 } 217 } finally { 218 c.close(); 219 } 220 return null; 221 } 222 223 /** 224 * Returns the attachment mailbox (where we store eml attachment Emails), creating one 225 * if necessary 226 * @return the global attachment mailbox 227 */ 228 public Mailbox getAttachmentMailbox() { 229 Mailbox m = getGlobalMailboxWhere(WHERE_TYPE_ATTACHMENT); 230 if (m == null) { 231 m = new Mailbox(); 232 m.mAccountKey = GLOBAL_MAILBOX_ACCOUNT_KEY; 233 m.mServerId = ATTACHMENT_MAILBOX_SERVER_ID; 234 m.mFlagVisible = false; 235 m.mDisplayName = ATTACHMENT_MAILBOX_SERVER_ID; 236 m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER; 237 m.mType = Mailbox.TYPE_ATTACHMENT; 238 m.save(mProviderContext); 239 } 240 return m; 241 } 242 243 /** 244 * Returns the search mailbox for the specified account, creating one if necessary 245 * @return the search mailbox for the passed in account 246 */ 247 public Mailbox getSearchMailbox(long accountId) { 248 Mailbox m = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_SEARCH); 249 if (m == null) { 250 m = new Mailbox(); 251 m.mAccountKey = accountId; 252 m.mServerId = SEARCH_MAILBOX_SERVER_ID; 253 m.mFlagVisible = false; 254 m.mDisplayName = SEARCH_MAILBOX_SERVER_ID; 255 m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER; 256 m.mType = Mailbox.TYPE_SEARCH; 257 m.mFlags = Mailbox.FLAG_HOLDS_MAIL; 258 m.mParentKey = Mailbox.NO_MAILBOX; 259 m.save(mProviderContext); 260 } 261 return m; 262 } 263 264 /** 265 * Create a Message from the Uri and store it in the attachment mailbox 266 * @param uri the uri containing message content 267 * @return the Message or null 268 */ 269 public Message loadMessageFromUri(Uri uri) { 270 Mailbox mailbox = getAttachmentMailbox(); 271 if (mailbox == null) return null; 272 try { 273 InputStream is = mProviderContext.getContentResolver().openInputStream(uri); 274 try { 275 // First, create a Pop3Message from the attachment and then parse it 276 Pop3Message pop3Message = new Pop3Message( 277 ATTACHMENT_MESSAGE_UID_PREFIX + System.currentTimeMillis(), null); 278 pop3Message.parse(is); 279 // Now, pull out the header fields 280 Message msg = new Message(); 281 LegacyConversions.updateMessageFields(msg, pop3Message, 0, mailbox.mId); 282 // Commit the message to the local store 283 msg.save(mProviderContext); 284 // Setup the rest of the message and mark it completely loaded 285 Utilities.copyOneMessageToProvider(mProviderContext, pop3Message, msg, 286 Message.FLAG_LOADED_COMPLETE); 287 // Restore the complete message and return it 288 return Message.restoreMessageWithId(mProviderContext, msg.mId); 289 } catch (MessagingException e) { 290 } catch (IOException e) { 291 } 292 } catch (FileNotFoundException e) { 293 } 294 return null; 295 } 296 297 /** 298 * Set logging flags for external sync services 299 * 300 * Generally this should be called by anybody who changes Email.DEBUG 301 */ 302 public void serviceLogging(int debugFlags) { 303 IEmailService service = EmailServiceUtils.getExchangeService(mContext, mServiceCallback); 304 try { 305 service.setLogging(debugFlags); 306 } catch (RemoteException e) { 307 // TODO Change exception handling to be consistent with however this method 308 // is implemented for other protocols 309 Log.d("setLogging", "RemoteException" + e); 310 } 311 } 312 313 /** 314 * Request a remote update of mailboxes for an account. 315 */ 316 @SuppressWarnings("deprecation") 317 public void updateMailboxList(final long accountId) { 318 Utility.runAsync(new Runnable() { 319 @Override 320 public void run() { 321 final IEmailService service = getServiceForAccount(accountId); 322 if (service != null) { 323 // Service implementation 324 try { 325 service.updateFolderList(accountId); 326 } catch (RemoteException e) { 327 // TODO Change exception handling to be consistent with however this method 328 // is implemented for other protocols 329 Log.d("updateMailboxList", "RemoteException" + e); 330 } 331 } else { 332 throw new IllegalStateException("No service for updateMailboxList?"); 333 } 334 } 335 }); 336 } 337 338 /** 339 * Request a remote update of a mailbox. 340 * 341 * The contract here should be to try and update the headers ASAP, in order to populate 342 * a simple message list. We should also at this point queue up a background task of 343 * downloading some/all of the messages in this mailbox, but that should be interruptable. 344 */ 345 public void updateMailbox(final long accountId, final long mailboxId, boolean userRequest) { 346 347 IEmailService service = getServiceForAccount(accountId); 348 if (service != null) { 349 try { 350 service.startSync(mailboxId, userRequest); 351 } catch (RemoteException e) { 352 // TODO Change exception handling to be consistent with however this method 353 // is implemented for other protocols 354 Log.d("updateMailbox", "RemoteException" + e); 355 } 356 } else { 357 throw new IllegalStateException("No service for loadMessageForView?"); 358 } 359 } 360 361 /** 362 * Request that any final work necessary be done, to load a message. 363 * 364 * Note, this assumes that the caller has already checked message.mFlagLoaded and that 365 * additional work is needed. There is no optimization here for a message which is already 366 * loaded. 367 * 368 * @param messageId the message to load 369 * @param callback the Controller callback by which results will be reported 370 */ 371 public void loadMessageForView(final long messageId) { 372 373 // Split here for target type (Service or MessagingController) 374 EmailServiceProxy service = getServiceForMessage(messageId); 375 if (service.isRemote()) { 376 // There is no service implementation, so we'll just jam the value, log the error, 377 // and get out of here. 378 Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId); 379 ContentValues cv = new ContentValues(); 380 cv.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE); 381 mProviderContext.getContentResolver().update(uri, cv, null, null); 382 Log.d(Logging.LOG_TAG, "Unexpected loadMessageForView() for remote service message."); 383 final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 384 synchronized (mListeners) { 385 for (Result listener : mListeners) { 386 listener.loadMessageForViewCallback(null, accountId, messageId, 100); 387 } 388 } 389 } else { 390 try { 391 service.loadMore(messageId); 392 } catch (RemoteException e) { 393 } 394 } 395 } 396 397 398 /** 399 * Saves the message to a mailbox of given type. 400 * This is a synchronous operation taking place in the same thread as the caller. 401 * Upon return the message.mId is set. 402 * @param message the message (must have the mAccountId set). 403 * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS). 404 */ 405 public void saveToMailbox(final EmailContent.Message message, final int mailboxType) { 406 long accountId = message.mAccountKey; 407 long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType); 408 message.mMailboxKey = mailboxId; 409 message.save(mProviderContext); 410 } 411 412 /** 413 * Look for a specific system mailbox, creating it if necessary, and return the mailbox id. 414 * This is a blocking operation and should not be called from the UI thread. 415 * 416 * Synchronized so multiple threads can call it (and not risk creating duplicate boxes). 417 * 418 * @param accountId the account id 419 * @param mailboxType the mailbox type (e.g. EmailContent.Mailbox.TYPE_TRASH) 420 * @return the id of the mailbox. The mailbox is created if not existing. 421 * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative. 422 * Does not validate the input in other ways (e.g. does not verify the existence of account). 423 */ 424 public synchronized long findOrCreateMailboxOfType(long accountId, int mailboxType) { 425 if (accountId < 0 || mailboxType < 0) { 426 return Mailbox.NO_MAILBOX; 427 } 428 long mailboxId = 429 Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType); 430 return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId; 431 } 432 433 /** 434 * Returns the server-side name for a specific mailbox. 435 * 436 * @return the resource string corresponding to the mailbox type, empty if not found. 437 */ 438 public static String getMailboxServerName(Context context, int mailboxType) { 439 int resId = -1; 440 switch (mailboxType) { 441 case Mailbox.TYPE_INBOX: 442 resId = R.string.mailbox_name_server_inbox; 443 break; 444 case Mailbox.TYPE_OUTBOX: 445 resId = R.string.mailbox_name_server_outbox; 446 break; 447 case Mailbox.TYPE_DRAFTS: 448 resId = R.string.mailbox_name_server_drafts; 449 break; 450 case Mailbox.TYPE_TRASH: 451 resId = R.string.mailbox_name_server_trash; 452 break; 453 case Mailbox.TYPE_SENT: 454 resId = R.string.mailbox_name_server_sent; 455 break; 456 case Mailbox.TYPE_JUNK: 457 resId = R.string.mailbox_name_server_junk; 458 break; 459 } 460 return resId != -1 ? context.getString(resId) : ""; 461 } 462 463 /** 464 * Create a mailbox given the account and mailboxType. 465 * TODO: Does this need to be signaled explicitly to the sync engines? 466 */ 467 @VisibleForTesting 468 long createMailbox(long accountId, int mailboxType) { 469 if (accountId < 0 || mailboxType < 0) { 470 String mes = "Invalid arguments " + accountId + ' ' + mailboxType; 471 Log.e(Logging.LOG_TAG, mes); 472 throw new RuntimeException(mes); 473 } 474 Mailbox box = Mailbox.newSystemMailbox( 475 accountId, mailboxType, getMailboxServerName(mContext, mailboxType)); 476 box.save(mProviderContext); 477 return box.mId; 478 } 479 480 /** 481 * Send a message: 482 * - move the message to Outbox (the message is assumed to be in Drafts). 483 * - EAS service will take it from there 484 * - mark reply/forward state in source message (if any) 485 * - trigger send for POP/IMAP 486 * @param message the fully populated Message (usually retrieved from the Draft box). Note that 487 * all transient fields (e.g. Body related fields) are also expected to be fully loaded 488 */ 489 public void sendMessage(Message message) { 490 ContentResolver resolver = mProviderContext.getContentResolver(); 491 long accountId = message.mAccountKey; 492 long messageId = message.mId; 493 if (accountId == Account.NO_ACCOUNT) { 494 accountId = lookupAccountForMessage(messageId); 495 } 496 if (accountId == Account.NO_ACCOUNT) { 497 // probably the message was not found 498 if (Logging.LOGD) { 499 Email.log("no account found for message " + messageId); 500 } 501 return; 502 } 503 504 // Move to Outbox 505 long outboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_OUTBOX); 506 ContentValues cv = new ContentValues(); 507 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, outboxId); 508 509 // does this need to be SYNCED_CONTENT_URI instead? 510 Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId); 511 resolver.update(uri, cv, null, null); 512 513 // If this is a reply/forward, indicate it as such on the source. 514 long sourceKey = message.mSourceKey; 515 if (sourceKey != Message.NO_MESSAGE) { 516 boolean isReply = (message.mFlags & Message.FLAG_TYPE_REPLY) != 0; 517 int flagUpdate = isReply ? Message.FLAG_REPLIED_TO : Message.FLAG_FORWARDED; 518 setMessageAnsweredOrForwarded(sourceKey, flagUpdate); 519 } 520 521 sendPendingMessages(accountId); 522 } 523 524 public void sendPendingMessages(long accountId) { 525 EmailServiceProxy service = 526 EmailServiceUtils.getServiceForAccount(mContext, null, accountId); 527 try { 528 service.sendMail(accountId); 529 } catch (RemoteException e) { 530 } 531 } 532 533 /** 534 * Reset visible limits for all accounts. 535 * For each account: 536 * look up limit 537 * write limit into all mailboxes for that account 538 */ 539 @SuppressWarnings("deprecation") 540 public void resetVisibleLimits() { 541 Utility.runAsync(new Runnable() { 542 @Override 543 public void run() { 544 ContentResolver resolver = mProviderContext.getContentResolver(); 545 Cursor c = null; 546 try { 547 c = resolver.query( 548 Account.CONTENT_URI, 549 Account.ID_PROJECTION, 550 null, null, null); 551 while (c.moveToNext()) { 552 long accountId = c.getLong(Account.ID_PROJECTION_COLUMN); 553 String protocol = Account.getProtocol(mProviderContext, accountId); 554 if (!HostAuth.SCHEME_EAS.equals(protocol)) { 555 ContentValues cv = new ContentValues(); 556 cv.put(MailboxColumns.VISIBLE_LIMIT, Email.VISIBLE_LIMIT_DEFAULT); 557 resolver.update(Mailbox.CONTENT_URI, cv, 558 MailboxColumns.ACCOUNT_KEY + "=?", 559 new String[] { Long.toString(accountId) }); 560 } 561 } 562 } finally { 563 if (c != null) { 564 c.close(); 565 } 566 } 567 } 568 }); 569 } 570 571 /** 572 * Increase the load count for a given mailbox, and trigger a refresh. Applies only to 573 * IMAP and POP mailboxes, with the exception of the EAS search mailbox. 574 * 575 * @param mailboxId the mailbox 576 */ 577 public void loadMoreMessages(final long mailboxId) { 578 EmailAsyncTask.runAsyncParallel(new Runnable() { 579 @Override 580 public void run() { 581 Mailbox mailbox = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); 582 if (mailbox == null) { 583 return; 584 } 585 if (mailbox.mType == Mailbox.TYPE_SEARCH) { 586 try { 587 searchMore(mailbox.mAccountKey); 588 } catch (MessagingException e) { 589 // Nothing to be done 590 } 591 return; 592 } 593 Account account = Account.restoreAccountWithId(mProviderContext, 594 mailbox.mAccountKey); 595 if (account == null) { 596 return; 597 } 598 // Use provider math to increment the field 599 ContentValues cv = new ContentValues();; 600 cv.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT); 601 cv.put(EmailContent.ADD_COLUMN_NAME, Email.VISIBLE_LIMIT_INCREMENT); 602 Uri uri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, mailboxId); 603 mProviderContext.getContentResolver().update(uri, cv, null, null); 604 // Trigger a refresh using the new, longer limit 605 mailbox.mVisibleLimit += Email.VISIBLE_LIMIT_INCREMENT; 606 //mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener); 607 } 608 }); 609 } 610 611 /** 612 * @param messageId the id of message 613 * @return the accountId corresponding to the given messageId, or -1 if not found. 614 */ 615 private long lookupAccountForMessage(long messageId) { 616 ContentResolver resolver = mProviderContext.getContentResolver(); 617 Cursor c = resolver.query(EmailContent.Message.CONTENT_URI, 618 MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?", 619 new String[] { Long.toString(messageId) }, null); 620 try { 621 return c.moveToFirst() 622 ? c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID) 623 : -1; 624 } finally { 625 c.close(); 626 } 627 } 628 629 /** 630 * Delete a single attachment entry from the DB given its id. 631 * Does not delete any eventual associated files. 632 */ 633 public void deleteAttachment(long attachmentId) { 634 ContentResolver resolver = mProviderContext.getContentResolver(); 635 Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId); 636 resolver.delete(uri, null, null); 637 } 638 639 /** 640 * Async version of {@link #deleteMessageSync}. 641 */ 642 public void deleteMessage(final long messageId) { 643 EmailAsyncTask.runAsyncParallel(new Runnable() { 644 @Override 645 public void run() { 646 deleteMessageSync(messageId); 647 } 648 }); 649 } 650 651 /** 652 * Batch & async version of {@link #deleteMessageSync}. 653 */ 654 public void deleteMessages(final long[] messageIds) { 655 if (messageIds == null || messageIds.length == 0) { 656 throw new IllegalArgumentException(); 657 } 658 EmailAsyncTask.runAsyncParallel(new Runnable() { 659 @Override 660 public void run() { 661 for (long messageId: messageIds) { 662 deleteMessageSync(messageId); 663 } 664 } 665 }); 666 } 667 668 /** 669 * Delete a single message by moving it to the trash, or really delete it if it's already in 670 * trash or a draft message. 671 * 672 * This function has no callback, no result reporting, because the desired outcome 673 * is reflected entirely by changes to one or more cursors. 674 * 675 * @param messageId The id of the message to "delete". 676 */ 677 /* package */ void deleteMessageSync(long messageId) { 678 // 1. Get the message's account 679 Account account = Account.getAccountForMessageId(mProviderContext, messageId); 680 681 if (account == null) return; 682 683 // 2. Confirm that there is a trash mailbox available. If not, create one 684 long trashMailboxId = findOrCreateMailboxOfType(account.mId, Mailbox.TYPE_TRASH); 685 686 // 3. Get the message's original mailbox 687 Mailbox mailbox = Mailbox.getMailboxForMessageId(mProviderContext, messageId); 688 689 if (mailbox == null) return; 690 691 // 4. Drop non-essential data for the message (e.g. attachment files) 692 AttachmentUtilities.deleteAllAttachmentFiles(mProviderContext, account.mId, 693 messageId); 694 695 Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, 696 messageId); 697 ContentResolver resolver = mProviderContext.getContentResolver(); 698 699 // 5. Perform "delete" as appropriate 700 if ((mailbox.mId == trashMailboxId) || (mailbox.mType == Mailbox.TYPE_DRAFTS)) { 701 // 5a. Really delete it 702 resolver.delete(uri, null, null); 703 } else { 704 // 5b. Move to trash 705 ContentValues cv = new ContentValues(); 706 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId); 707 resolver.update(uri, cv, null, null); 708 } 709 } 710 711 /** 712 * Moves messages to a new mailbox. 713 * 714 * This function has no callback, no result reporting, because the desired outcome 715 * is reflected entirely by changes to one or more cursors. 716 * 717 * Note this method assumes all of the given message and mailbox IDs belong to the same 718 * account. 719 * 720 * @param messageIds IDs of the messages that are to be moved 721 * @param newMailboxId ID of the new mailbox that the messages will be moved to 722 * @return an asynchronous task that executes the move (for testing only) 723 */ 724 public EmailAsyncTask<Void, Void, Void> moveMessages(final long[] messageIds, 725 final long newMailboxId) { 726 if (messageIds == null || messageIds.length == 0) { 727 throw new IllegalArgumentException(); 728 } 729 return EmailAsyncTask.runAsyncParallel(new Runnable() { 730 @Override 731 public void run() { 732 Account account = Account.getAccountForMessageId(mProviderContext, messageIds[0]); 733 if (account != null) { 734 ContentValues cv = new ContentValues(); 735 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, newMailboxId); 736 ContentResolver resolver = mProviderContext.getContentResolver(); 737 for (long messageId : messageIds) { 738 Uri uri = ContentUris.withAppendedId( 739 EmailContent.Message.SYNCED_CONTENT_URI, messageId); 740 resolver.update(uri, cv, null, null); 741 } 742 } 743 } 744 }); 745 } 746 747 /** 748 * Set/clear the unread status of a message 749 * 750 * @param messageId the message to update 751 * @param isRead the new value for the isRead flag 752 */ 753 public void setMessageReadSync(long messageId, boolean isRead) { 754 setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_READ, isRead); 755 } 756 757 /** 758 * Set/clear the unread status of a message from UI thread 759 * 760 * @param messageId the message to update 761 * @param isRead the new value for the isRead flag 762 * @return the EmailAsyncTask created 763 */ 764 public EmailAsyncTask<Void, Void, Void> setMessageRead(final long messageId, 765 final boolean isRead) { 766 return EmailAsyncTask.runAsyncParallel(new Runnable() { 767 @Override 768 public void run() { 769 setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_READ, isRead); 770 }}); 771 } 772 773 /** 774 * Update a message record and ping MessagingController, if necessary 775 * 776 * @param messageId the message to update 777 * @param cv the ContentValues used in the update 778 */ 779 private void updateMessageSync(long messageId, ContentValues cv) { 780 Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId); 781 mProviderContext.getContentResolver().update(uri, cv, null, null); 782 } 783 784 /** 785 * Set the answered status of a message 786 * 787 * @param messageId the message to update 788 * @return the AsyncTask that will execute the changes (for testing only) 789 */ 790 public void setMessageAnsweredOrForwarded(final long messageId, 791 final int flag) { 792 EmailAsyncTask.runAsyncParallel(new Runnable() { 793 @Override 794 public void run() { 795 Message msg = Message.restoreMessageWithId(mProviderContext, messageId); 796 if (msg == null) { 797 Log.w(Logging.LOG_TAG, "Unable to find source message for a reply/forward"); 798 return; 799 } 800 ContentValues cv = new ContentValues(); 801 cv.put(MessageColumns.FLAGS, msg.mFlags | flag); 802 updateMessageSync(messageId, cv); 803 } 804 }); 805 } 806 807 /** 808 * Set/clear the favorite status of a message from UI thread 809 * 810 * @param messageId the message to update 811 * @param isFavorite the new value for the isFavorite flag 812 * @return the EmailAsyncTask created 813 */ 814 public EmailAsyncTask<Void, Void, Void> setMessageFavorite(final long messageId, 815 final boolean isFavorite) { 816 return EmailAsyncTask.runAsyncParallel(new Runnable() { 817 @Override 818 public void run() { 819 setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_FAVORITE, 820 isFavorite); 821 }}); 822 } 823 /** 824 * Set/clear the favorite status of a message 825 * 826 * @param messageId the message to update 827 * @param isFavorite the new value for the isFavorite flag 828 */ 829 public void setMessageFavoriteSync(long messageId, boolean isFavorite) { 830 setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite); 831 } 832 833 /** 834 * Set/clear boolean columns of a message 835 * 836 * @param messageId the message to update 837 * @param columnName the column to update 838 * @param columnValue the new value for the column 839 */ 840 private void setMessageBooleanSync(long messageId, String columnName, boolean columnValue) { 841 ContentValues cv = new ContentValues(); 842 cv.put(columnName, columnValue); 843 updateMessageSync(messageId, cv); 844 } 845 846 847 private static final HashMap<Long, SearchParams> sSearchParamsMap = 848 new HashMap<Long, SearchParams>(); 849 850 public void searchMore(long accountId) throws MessagingException { 851 SearchParams params = sSearchParamsMap.get(accountId); 852 if (params == null) return; 853 params.mOffset += params.mLimit; 854 searchMessages(accountId, params); 855 } 856 857 /** 858 * Search for messages on the (IMAP) server; do not call this on the UI thread! 859 * @param accountId the id of the account to be searched 860 * @param searchParams the parameters for this search 861 * @throws MessagingException 862 */ 863 public int searchMessages(final long accountId, final SearchParams searchParams) 864 throws MessagingException { 865 // Find/create our search mailbox 866 Mailbox searchMailbox = getSearchMailbox(accountId); 867 if (searchMailbox == null) return 0; 868 final long searchMailboxId = searchMailbox.mId; 869 // Save this away (per account) 870 sSearchParamsMap.put(accountId, searchParams); 871 872 if (searchParams.mOffset == 0) { 873 // Delete existing contents of search mailbox 874 ContentResolver resolver = mContext.getContentResolver(); 875 resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId, 876 null); 877 ContentValues cv = new ContentValues(); 878 // For now, use the actual query as the name of the mailbox 879 cv.put(Mailbox.DISPLAY_NAME, searchParams.mFilter); 880 resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId), 881 cv, null, null); 882 } 883 884 IEmailService service = getServiceForAccount(accountId); 885 if (service != null) { 886 // Service implementation 887 try { 888 return service.searchMessages(accountId, searchParams, searchMailboxId); 889 } catch (RemoteException e) { 890 // TODO Change exception handling to be consistent with however this method 891 // is implemented for other protocols 892 Log.e("searchMessages", "RemoteException", e); 893 } 894 } 895 return 0; 896 } 897 898 private EmailServiceProxy getServiceForAccount(long accountId) { 899 return EmailServiceUtils.getServiceForAccount(mContext, mServiceCallback, accountId); 900 } 901 902 /** 903 * Respond to a meeting invitation. 904 * 905 * @param messageId the id of the invitation being responded to 906 * @param response the code representing the response to the invitation 907 */ 908 public void sendMeetingResponse(final long messageId, final int response) { 909 // Split here for target type (Service or MessagingController) 910 IEmailService service = getServiceForMessage(messageId); 911 if (service != null) { 912 // Service implementation 913 try { 914 service.sendMeetingResponse(messageId, response); 915 } catch (RemoteException e) { 916 // TODO Change exception handling to be consistent with however this method 917 // is implemented for other protocols 918 Log.e("onDownloadAttachment", "RemoteException", e); 919 } 920 } 921 } 922 923 /** 924 * Request that an attachment be loaded. It will be stored at a location controlled 925 * by the AttachmentProvider. 926 * 927 * @param attachmentId the attachment to load 928 * @param messageId the owner message 929 * @param accountId the owner account 930 */ 931 public void loadAttachment(final long attachmentId, final long messageId, 932 final long accountId) { 933 Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId); 934 if (attachInfo == null) { 935 return; 936 } 937 938 if (Utility.attachmentExists(mProviderContext, attachInfo)) { 939 // The attachment has already been downloaded, so we will just "pretend" to download it 940 // This presumably is for POP3 messages 941 synchronized (mListeners) { 942 for (Result listener : mListeners) { 943 listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 0); 944 } 945 for (Result listener : mListeners) { 946 listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 100); 947 } 948 } 949 return; 950 } 951 952 // Flag the attachment as needing download at the user's request 953 ContentValues cv = new ContentValues(); 954 cv.put(Attachment.FLAGS, attachInfo.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST); 955 attachInfo.update(mProviderContext, cv); 956 } 957 958 /** 959 * For a given message id, return a service proxy if applicable, or null. 960 * 961 * @param messageId the message of interest 962 * @result service proxy, or null if n/a 963 */ 964 private EmailServiceProxy getServiceForMessage(long messageId) { 965 // TODO make this more efficient, caching the account, smaller lookup here, etc. 966 Message message = Message.restoreMessageWithId(mProviderContext, messageId); 967 if (message == null) { 968 return null; 969 } 970 return getServiceForAccount(message.mAccountKey); 971 } 972 973 /** 974 * Delete an account. 975 */ 976 public void deleteAccount(final long accountId) { 977 EmailAsyncTask.runAsyncParallel(new Runnable() { 978 @Override 979 public void run() { 980 deleteAccountSync(accountId, mProviderContext); 981 } 982 }); 983 } 984 985 /** 986 * Delete an account synchronously. 987 */ 988 public void deleteAccountSync(long accountId, Context context) { 989 try { 990 mLegacyControllerMap.remove(accountId); 991 // Get the account URI. 992 final Account account = Account.restoreAccountWithId(context, accountId); 993 if (account == null) { 994 return; // Already deleted? 995 } 996 997 // Delete account data, attachments, PIM data, etc. 998 deleteSyncedDataSync(accountId); 999 1000 // Now delete the account itself 1001 Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); 1002 context.getContentResolver().delete(uri, null, null); 1003 1004 // For unit tests, don't run backup, security, and ui pieces. 1005 if (mInUnitTests) { 1006 return; 1007 } 1008 1009 // Clean up 1010 AccountBackupRestore.backup(context); 1011 SecurityPolicy.getInstance(context).reducePolicies(); 1012 Email.setServicesEnabledSync(context); 1013 Email.setNotifyUiAccountsChanged(true); 1014 } catch (Exception e) { 1015 Log.w(Logging.LOG_TAG, "Exception while deleting account", e); 1016 } 1017 } 1018 1019 /** 1020 * Delete all synced data, but don't delete the actual account. This is used when security 1021 * policy requirements are not met, and we don't want to reveal any synced data, but we do 1022 * wish to keep the account configured (e.g. to accept remote wipe commands). 1023 * 1024 * The only mailbox not deleted is the account mailbox (if any) 1025 * Also, clear the sync keys on the remaining account, since the data is gone. 1026 * 1027 * SYNCHRONOUS - do not call from UI thread. 1028 * 1029 * @param accountId The account to wipe. 1030 */ 1031 public void deleteSyncedDataSync(long accountId) { 1032 try { 1033 // Delete synced attachments 1034 AttachmentUtilities.deleteAllAccountAttachmentFiles(mProviderContext, 1035 accountId); 1036 1037 // Delete synced email, leaving only an empty inbox. We do this in two phases: 1038 // 1. Delete all non-inbox mailboxes (which will delete all of their messages) 1039 // 2. Delete all remaining messages (which will be the inbox messages) 1040 ContentResolver resolver = mProviderContext.getContentResolver(); 1041 String[] accountIdArgs = new String[] { Long.toString(accountId) }; 1042 resolver.delete(Mailbox.CONTENT_URI, 1043 MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION, 1044 accountIdArgs); 1045 resolver.delete(Message.CONTENT_URI, MESSAGES_FOR_ACCOUNT_SELECTION, accountIdArgs); 1046 1047 // Delete sync keys on remaining items 1048 ContentValues cv = new ContentValues(); 1049 cv.putNull(Account.SYNC_KEY); 1050 resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs); 1051 cv.clear(); 1052 cv.putNull(Mailbox.SYNC_KEY); 1053 resolver.update(Mailbox.CONTENT_URI, cv, 1054 MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs); 1055 1056 // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable 1057 IEmailService service = getServiceForAccount(accountId); 1058 if (service != null) { 1059 service.deleteAccountPIMData(accountId); 1060 } 1061 } catch (Exception e) { 1062 Log.w(Logging.LOG_TAG, "Exception while deleting account synced data", e); 1063 } 1064 } 1065 1066 /** 1067 * Simple callback for synchronous commands. For many commands, this can be largely ignored 1068 * and the result is observed via provider cursors. The callback will *not* necessarily be 1069 * made from the UI thread, so you may need further handlers to safely make UI updates. 1070 */ 1071 public static abstract class Result { 1072 private volatile boolean mRegistered; 1073 1074 protected void setRegistered(boolean registered) { 1075 mRegistered = registered; 1076 } 1077 1078 protected final boolean isRegistered() { 1079 return mRegistered; 1080 } 1081 1082 /** 1083 * Callback for updateMailboxList 1084 * 1085 * @param result If null, the operation completed without error 1086 * @param accountId The account being operated on 1087 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1088 */ 1089 public void updateMailboxListCallback(MessagingException result, long accountId, 1090 int progress) { 1091 } 1092 1093 /** 1094 * Callback for updateMailbox. Note: This looks a lot like checkMailCallback, but 1095 * it's a separate call used only by UI's, so we can keep things separate. 1096 * 1097 * @param result If null, the operation completed without error 1098 * @param accountId The account being operated on 1099 * @param mailboxId The mailbox being operated on 1100 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1101 * @param numNewMessages the number of new messages delivered 1102 */ 1103 public void updateMailboxCallback(MessagingException result, long accountId, 1104 long mailboxId, int progress, int numNewMessages, ArrayList<Long> addedMessages) { 1105 } 1106 1107 /** 1108 * Callback for loadMessageForView 1109 * 1110 * @param result if null, the attachment completed - if non-null, terminating with failure 1111 * @param messageId the message which contains the attachment 1112 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1113 */ 1114 public void loadMessageForViewCallback(MessagingException result, long accountId, 1115 long messageId, int progress) { 1116 } 1117 1118 /** 1119 * Callback for loadAttachment 1120 * 1121 * @param result if null, the attachment completed - if non-null, terminating with failure 1122 * @param messageId the message which contains the attachment 1123 * @param attachmentId the attachment being loaded 1124 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1125 */ 1126 public void loadAttachmentCallback(MessagingException result, long accountId, 1127 long messageId, long attachmentId, int progress) { 1128 } 1129 1130 /** 1131 * Callback for checkmail. Note: This looks a lot like updateMailboxCallback, but 1132 * it's a separate call used only by the automatic checker service, so we can keep 1133 * things separate. 1134 * 1135 * @param result If null, the operation completed without error 1136 * @param accountId The account being operated on 1137 * @param mailboxId The mailbox being operated on (may be unknown at start) 1138 * @param progress 0 for "starting", no updates, 100 for complete 1139 * @param tag the same tag that was passed to serviceCheckMail() 1140 */ 1141 public void serviceCheckMailCallback(MessagingException result, long accountId, 1142 long mailboxId, int progress, long tag) { 1143 } 1144 1145 /** 1146 * Callback for sending pending messages. This will be called once to start the 1147 * group, multiple times for messages, and once to complete the group. 1148 * 1149 * Unfortunately this callback works differently on SMTP and EAS. 1150 * 1151 * On SMTP: 1152 * 1153 * First, we get this. 1154 * result == null, messageId == -1, progress == 0: start batch send 1155 * 1156 * Then we get these callbacks per message. 1157 * (Exchange backend may skip "start sending one message".) 1158 * result == null, messageId == xx, progress == 0: start sending one message 1159 * result == xxxx, messageId == xx, progress == 0; failed sending one message 1160 * 1161 * Finally we get this. 1162 * result == null, messageId == -1, progres == 100; finish sending batch 1163 * 1164 * On EAS: Almost same as above, except: 1165 * 1166 * - There's no first ("start batch send") callback. 1167 * - accountId is always -1. 1168 * 1169 * @param result If null, the operation completed without error 1170 * @param accountId The account being operated on 1171 * @param messageId The being sent (may be unknown at start) 1172 * @param progress 0 for "starting", 100 for complete 1173 */ 1174 public void sendMailCallback(MessagingException result, long accountId, 1175 long messageId, int progress) { 1176 } 1177 } 1178 1179 /** 1180 * Service callback for service operations 1181 */ 1182 private class ServiceCallback extends IEmailServiceCallback.Stub { 1183 1184 @Override 1185 public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, 1186 int progress) { 1187 MessagingException result = mapStatusToException(statusCode); 1188 switch (statusCode) { 1189 case EmailServiceStatus.SUCCESS: 1190 progress = 100; 1191 break; 1192 case EmailServiceStatus.IN_PROGRESS: 1193 // discard progress reports that look like sentinels 1194 if (progress < 0 || progress >= 100) { 1195 return; 1196 } 1197 break; 1198 } 1199 final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 1200 synchronized (mListeners) { 1201 for (Result listener : mListeners) { 1202 listener.loadAttachmentCallback(result, accountId, messageId, attachmentId, 1203 progress); 1204 } 1205 } 1206 } 1207 1208 /** 1209 * Note, this is an incomplete implementation of this callback, because we are 1210 * not getting things back from Service in quite the same way as from MessagingController. 1211 * However, this is sufficient for basic "progress=100" notification that message send 1212 * has just completed. 1213 */ 1214 @Override 1215 public void sendMessageStatus(long messageId, String subject, int statusCode, 1216 int progress) { 1217 long accountId = -1; // This should be in the callback 1218 MessagingException result = mapStatusToException(statusCode); 1219 switch (statusCode) { 1220 case EmailServiceStatus.SUCCESS: 1221 progress = 100; 1222 break; 1223 case EmailServiceStatus.IN_PROGRESS: 1224 // discard progress reports that look like sentinels 1225 if (progress < 0 || progress >= 100) { 1226 return; 1227 } 1228 break; 1229 } 1230 synchronized(mListeners) { 1231 for (Result listener : mListeners) { 1232 listener.sendMailCallback(result, accountId, messageId, progress); 1233 } 1234 } 1235 } 1236 1237 /** 1238 * Note, this is an incomplete implementation of this callback, because we are 1239 * not getting things back from Service in quite the same way as from MessagingController. 1240 * However, this is sufficient for basic "progress=100" notification that message send 1241 * has just completed. 1242 */ 1243 @Override 1244 public void loadMessageStatus(long messageId, int statusCode, int progress) { 1245 long accountId = -1; // This should be in the callback 1246 MessagingException result = mapStatusToException(statusCode); 1247 switch (statusCode) { 1248 case EmailServiceStatus.SUCCESS: 1249 progress = 100; 1250 break; 1251 case EmailServiceStatus.IN_PROGRESS: 1252 // discard progress reports that look like sentinels 1253 if (progress < 0 || progress >= 100) { 1254 return; 1255 } 1256 break; 1257 } 1258 synchronized(mListeners) { 1259 for (Result listener : mListeners) { 1260 listener.loadMessageForViewCallback(result, accountId, messageId, progress); 1261 } 1262 } 1263 } 1264 1265 @Override 1266 public void syncMailboxListStatus(long accountId, int statusCode, int progress) { 1267 MessagingException result = mapStatusToException(statusCode); 1268 switch (statusCode) { 1269 case EmailServiceStatus.SUCCESS: 1270 progress = 100; 1271 break; 1272 case EmailServiceStatus.IN_PROGRESS: 1273 // discard progress reports that look like sentinels 1274 if (progress < 0 || progress >= 100) { 1275 return; 1276 } 1277 break; 1278 } 1279 synchronized(mListeners) { 1280 for (Result listener : mListeners) { 1281 listener.updateMailboxListCallback(result, accountId, progress); 1282 } 1283 } 1284 } 1285 1286 @Override 1287 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) { 1288 MessagingException result = mapStatusToException(statusCode); 1289 switch (statusCode) { 1290 case EmailServiceStatus.SUCCESS: 1291 progress = 100; 1292 break; 1293 case EmailServiceStatus.IN_PROGRESS: 1294 // discard progress reports that look like sentinels 1295 if (progress < 0 || progress >= 100) { 1296 return; 1297 } 1298 break; 1299 } 1300 // TODO should pass this back instead of looking it up here 1301 Mailbox mbx = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); 1302 // The mailbox could have disappeared if the server commanded it 1303 if (mbx == null) return; 1304 long accountId = mbx.mAccountKey; 1305 synchronized(mListeners) { 1306 for (Result listener : mListeners) { 1307 listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0, null); 1308 } 1309 } 1310 } 1311 1312 private MessagingException mapStatusToException(int statusCode) { 1313 switch (statusCode) { 1314 case EmailServiceStatus.SUCCESS: 1315 case EmailServiceStatus.IN_PROGRESS: 1316 // Don't generate error if the account is uninitialized 1317 case EmailServiceStatus.ACCOUNT_UNINITIALIZED: 1318 return null; 1319 1320 case EmailServiceStatus.LOGIN_FAILED: 1321 return new AuthenticationFailedException(""); 1322 1323 case EmailServiceStatus.CONNECTION_ERROR: 1324 return new MessagingException(MessagingException.IOERROR); 1325 1326 case EmailServiceStatus.SECURITY_FAILURE: 1327 return new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED); 1328 1329 case EmailServiceStatus.ACCESS_DENIED: 1330 return new MessagingException(MessagingException.ACCESS_DENIED); 1331 1332 case EmailServiceStatus.ATTACHMENT_NOT_FOUND: 1333 return new MessagingException(MessagingException.ATTACHMENT_NOT_FOUND); 1334 1335 case EmailServiceStatus.CLIENT_CERTIFICATE_ERROR: 1336 return new MessagingException(MessagingException.CLIENT_CERTIFICATE_ERROR); 1337 1338 case EmailServiceStatus.MESSAGE_NOT_FOUND: 1339 case EmailServiceStatus.FOLDER_NOT_DELETED: 1340 case EmailServiceStatus.FOLDER_NOT_RENAMED: 1341 case EmailServiceStatus.FOLDER_NOT_CREATED: 1342 case EmailServiceStatus.REMOTE_EXCEPTION: 1343 // TODO: define exception code(s) & UI string(s) for server-side errors 1344 default: 1345 return new MessagingException(String.valueOf(statusCode)); 1346 } 1347 } 1348 } 1349 1350 private interface ServiceCallbackWrapper { 1351 public void call(IEmailServiceCallback cb) throws RemoteException; 1352 } 1353 1354 /** 1355 * Proxy that can be used to broadcast service callbacks; we currently use this only for 1356 * loadAttachment callbacks 1357 */ 1358 private final IEmailServiceCallback.Stub mCallbackProxy = new IEmailServiceCallback.Stub() { 1359 1360 /** 1361 * Broadcast a callback to the everyone that's registered 1362 * 1363 * @param wrapper the ServiceCallbackWrapper used in the broadcast 1364 */ 1365 private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) { 1366 if (sCallbackList != null) { 1367 // Call everyone on our callback list 1368 // Exceptions can be safely ignored 1369 int count = sCallbackList.beginBroadcast(); 1370 for (int i = 0; i < count; i++) { 1371 try { 1372 wrapper.call(sCallbackList.getBroadcastItem(i)); 1373 } catch (RemoteException e) { 1374 } 1375 } 1376 sCallbackList.finishBroadcast(); 1377 } 1378 } 1379 1380 @Override 1381 public void loadAttachmentStatus(final long messageId, final long attachmentId, 1382 final int status, final int progress) { 1383 broadcastCallback(new ServiceCallbackWrapper() { 1384 @Override 1385 public void call(IEmailServiceCallback cb) throws RemoteException { 1386 cb.loadAttachmentStatus(messageId, attachmentId, status, progress); 1387 } 1388 }); 1389 } 1390 1391 @Override 1392 public void syncMailboxListStatus(long accountId, int statusCode, int progress) 1393 throws RemoteException { 1394 } 1395 1396 @Override 1397 public void syncMailboxStatus(final long mailboxId, final int statusCode, 1398 final int progress) throws RemoteException { 1399 } 1400 1401 @Override 1402 public void sendMessageStatus(long messageId, String subject, int statusCode, int progress) 1403 throws RemoteException { 1404 } 1405 1406 @Override 1407 public void loadMessageStatus(long messageId, int statusCode, int progress) 1408 throws RemoteException { 1409 } 1410 }; 1411} 1412