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