Controller.java revision 77898e14e96feb5d107c4e69b5241169e7425058
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.MessagingException; 20import com.android.email.mail.Store; 21import com.android.email.provider.AttachmentProvider; 22import com.android.email.provider.EmailContent; 23import com.android.email.provider.EmailContent.Account; 24import com.android.email.provider.EmailContent.Attachment; 25import com.android.email.provider.EmailContent.Mailbox; 26import com.android.email.provider.EmailContent.Message; 27import com.android.email.service.EmailServiceProxy; 28import com.android.exchange.EmailServiceStatus; 29import com.android.exchange.IEmailService; 30import com.android.exchange.IEmailServiceCallback; 31import com.android.exchange.SyncManager; 32 33import android.content.ContentResolver; 34import android.content.ContentUris; 35import android.content.ContentValues; 36import android.content.Context; 37import android.database.Cursor; 38import android.net.Uri; 39import android.os.RemoteException; 40import android.util.Log; 41 42import java.io.File; 43import java.util.HashSet; 44 45/** 46 * New central controller/dispatcher for Email activities that may require remote operations. 47 * Handles disambiguating between legacy MessagingController operations and newer provider/sync 48 * based code. 49 */ 50public class Controller { 51 52 static Controller sInstance; 53 private Context mContext; 54 private Context mProviderContext; 55 private MessagingController mLegacyController; 56 private ServiceCallback mServiceCallback = new ServiceCallback(); 57 private HashSet<Result> mListeners = new HashSet<Result>(); 58 59 private static String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] { 60 EmailContent.RECORD_ID, 61 EmailContent.MessageColumns.ACCOUNT_KEY 62 }; 63 private static int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1; 64 65 protected Controller(Context _context) { 66 mContext = _context; 67 mProviderContext = _context; 68 mLegacyController = MessagingController.getInstance(mContext); 69 } 70 71 /** 72 * Gets or creates the singleton instance of Controller. 73 * @param _context The context that will be used for all underlying system access 74 */ 75 public synchronized static Controller getInstance(Context _context) { 76 if (sInstance == null) { 77 sInstance = new Controller(_context); 78 } 79 return sInstance; 80 } 81 82 /** 83 * For testing only: Inject a different context for provider access. This will be 84 * used internally for access the underlying provider (e.g. getContentResolver().query()). 85 * @param providerContext the provider context to be used by this instance 86 */ 87 public void setProviderContext(Context providerContext) { 88 mProviderContext = providerContext; 89 } 90 91 /** 92 * Any UI code that wishes for callback results (on async ops) should register their callback 93 * here (typically from onResume()). Unregistered callbacks will never be called, to prevent 94 * problems when the command completes and the activity has already paused or finished. 95 * @param listener The callback that may be used in action methods 96 */ 97 public void addResultCallback(Result listener) { 98 synchronized (mListeners) { 99 mListeners.add(listener); 100 } 101 } 102 103 /** 104 * Any UI code that no longer wishes for callback results (on async ops) should unregister 105 * their callback here (typically from onPause()). Unregistered callbacks will never be called, 106 * to prevent problems when the command completes and the activity has already paused or 107 * finished. 108 * @param listener The callback that may no longer be used 109 */ 110 public void removeResultCallback(Result listener) { 111 synchronized (mListeners) { 112 mListeners.remove(listener); 113 } 114 } 115 116 private boolean isActiveResultCallback(Result listener) { 117 synchronized (mListeners) { 118 return mListeners.contains(listener); 119 } 120 } 121 122 /** 123 * Enable/disable logging for external sync services 124 * 125 * Generally this should be called by anybody who changes Email.DEBUG 126 */ 127 public void serviceLogging(boolean debugEnabled) { 128 IEmailService service = 129 new EmailServiceProxy(mContext, SyncManager.class, mServiceCallback); 130 try { 131 service.setLogging(debugEnabled); 132 } catch (RemoteException e) { 133 // TODO Change exception handling to be consistent with however this method 134 // is implemented for other protocols 135 Log.d("updateMailboxList", "RemoteException" + e); 136 } 137 } 138 139 /** 140 * Request a remote update of mailboxes for an account. 141 * 142 * TODO: Clean up threading in MessagingController cases (or perhaps here in Controller) 143 */ 144 public void updateMailboxList(final long accountId, final Result callback) { 145 146 IEmailService service = getServiceForAccount(accountId); 147 if (service != null) { 148 // Service implementation 149 try { 150 service.updateFolderList(accountId); 151 } catch (RemoteException e) { 152 // TODO Change exception handling to be consistent with however this method 153 // is implemented for other protocols 154 Log.d("updateMailboxList", "RemoteException" + e); 155 } 156 } else { 157 // MessagingController implementation 158 new Thread() { 159 @Override 160 public void run() { 161 Account account = 162 EmailContent.Account.restoreAccountWithId(mProviderContext, accountId); 163 MessagingListener listener = new LegacyListener(callback); 164 mLegacyController.addListener(listener); 165 mLegacyController.listFolders(account, listener); 166 } 167 }.start(); 168 } 169 } 170 171 /** 172 * Request a remote update of a mailbox. 173 * 174 * The contract here should be to try and update the headers ASAP, in order to populate 175 * a simple message list. We should also at this point queue up a background task of 176 * downloading some/all of the messages in this mailbox, but that should be interruptable. 177 */ 178 public void updateMailbox(final long accountId, 179 final EmailContent.Mailbox mailbox, final Result callback) { 180 181 IEmailService service = getServiceForAccount(accountId); 182 if (service != null) { 183 // Service implementation 184 try { 185 service.startSync(mailbox.mId); 186 } catch (RemoteException e) { 187 // TODO Change exception handling to be consistent with however this method 188 // is implemented for other protocols 189 Log.d("updateMailbox", "RemoteException" + e); 190 } 191 } else { 192 // MessagingController implementation 193 new Thread() { 194 @Override 195 public void run() { 196 Account account = 197 EmailContent.Account.restoreAccountWithId(mProviderContext, accountId); 198 MessagingListener listener = new LegacyListener(callback); 199 mLegacyController.addListener(listener); 200 mLegacyController.synchronizeMailbox(account, mailbox, listener); 201 } 202 }.start(); 203 } 204 } 205 206 /** 207 * Saves the message to a mailbox of given type. 208 * @param message the message (must have the mAccountId set). 209 * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS). 210 * TODO: UI feedback. 211 * TODO: use AsyncTask instead of Thread 212 */ 213 public void saveToMailbox(final EmailContent.Message message, final int mailboxType) { 214 new Thread() { 215 @Override 216 public void run() { 217 long accountId = message.mAccountKey; 218 long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType); 219 message.mMailboxKey = mailboxId; 220 message.save(mContext); 221 } 222 }.start(); 223 } 224 225 /** 226 * @param accountId the account id 227 * @param mailboxType the mailbox type (e.g. EmailContent.Mailbox.TYPE_TRASH) 228 * @return the id of the mailbox. The mailbox is created if not existing. 229 * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative. 230 * Does not validate the input in other ways (e.g. does not verify the existence of account). 231 */ 232 public long findOrCreateMailboxOfType(long accountId, int mailboxType) { 233 if (accountId < 0 || mailboxType < 0) { 234 return Mailbox.NO_MAILBOX; 235 } 236 long mailboxId = 237 Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType); 238 return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId; 239 } 240 241 /** 242 * @param mailboxType the mailbox type 243 * @return the resource string corresponding to the mailbox type, empty if not found. 244 */ 245 /* package */ String getSpecialMailboxDisplayName(int mailboxType) { 246 int resId = -1; 247 switch (mailboxType) { 248 case Mailbox.TYPE_INBOX: 249 // TODO: there is no special_mailbox_display_name_inbox; why? 250 resId = R.string.special_mailbox_name_inbox; 251 break; 252 case Mailbox.TYPE_OUTBOX: 253 resId = R.string.special_mailbox_display_name_outbox; 254 break; 255 case Mailbox.TYPE_DRAFTS: 256 resId = R.string.special_mailbox_display_name_drafts; 257 break; 258 case Mailbox.TYPE_TRASH: 259 resId = R.string.special_mailbox_display_name_trash; 260 break; 261 case Mailbox.TYPE_SENT: 262 resId = R.string.special_mailbox_display_name_sent; 263 break; 264 } 265 return resId != -1 ? mContext.getString(resId) : ""; 266 } 267 268 /** 269 * Create a mailbox given the account and mailboxType. 270 * TODO: Does this need to be signaled explicitly to the sync engines? 271 * As this method is only used internally ('private'), it does not 272 * validate its inputs (accountId and mailboxType). 273 */ 274 /* package */ long createMailbox(long accountId, int mailboxType) { 275 if (accountId < 0 || mailboxType < 0) { 276 String mes = "Invalid arguments " + accountId + ' ' + mailboxType; 277 Log.e(Email.LOG_TAG, mes); 278 throw new RuntimeException(mes); 279 } 280 Mailbox box = new Mailbox(); 281 box.mAccountKey = accountId; 282 box.mType = mailboxType; 283 box.mSyncInterval = EmailContent.Account.CHECK_INTERVAL_NEVER; 284 box.mFlagVisible = true; 285 box.mDisplayName = getSpecialMailboxDisplayName(mailboxType); 286 box.save(mProviderContext); 287 return box.mId; 288 } 289 290 /** 291 * Send a message: 292 * - move the message to Outbox (the message is assumed to be in Drafts). 293 * - perform any necessary notification 294 * - do the work in a separate (non-UI) thread 295 * @param messageId the id of the message to send 296 */ 297 public void sendMessage(long messageId, long accountId) { 298 ContentResolver resolver = mProviderContext.getContentResolver(); 299 if (accountId == -1) { 300 accountId = lookupAccountForMessage(messageId); 301 } 302 if (accountId == -1) { 303 // probably the message was not found 304 if (Email.LOGD) { 305 Email.log("no account found for message " + messageId); 306 } 307 return; 308 } 309 310 // Move to Outbox 311 long outboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_OUTBOX); 312 ContentValues cv = new ContentValues(); 313 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, outboxId); 314 315 // does this need to be SYNCED_CONTENT_URI instead? 316 Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId); 317 resolver.update(uri, cv, null, null); 318 319 // TODO: notifications 320 } 321 322 /** 323 * @param messageId the id of message 324 * @return the accountId corresponding to the given messageId, or -1 if not found. 325 */ 326 private long lookupAccountForMessage(long messageId) { 327 ContentResolver resolver = mProviderContext.getContentResolver(); 328 Cursor c = resolver.query(EmailContent.Message.CONTENT_URI, 329 MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?", 330 new String[] { Long.toString(messageId) }, null); 331 try { 332 return c.moveToFirst() 333 ? c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID) 334 : -1; 335 } finally { 336 c.close(); 337 } 338 } 339 340 /** 341 * Delete a single message by moving it to the trash. 342 * 343 * This function has no callback, no result reporting, because the desired outcome 344 * is reflected entirely by changes to one or more cursors. 345 * 346 * @param messageId The id of the message to "delete". 347 * @param accountId The id of the message's account, or -1 if not known by caller 348 * 349 * TODO: Move out of UI thread 350 * TODO: "get account a for message m" should be a utility 351 * TODO: "get mailbox of type n for account a" should be a utility 352 */ 353 public void deleteMessage(long messageId, long accountId) { 354 ContentResolver resolver = mProviderContext.getContentResolver(); 355 356 // 1. Look up acct# for message we're deleting 357 Cursor c = null; 358 if (accountId == -1) { 359 accountId = lookupAccountForMessage(messageId); 360 } 361 if (accountId == -1) { 362 return; 363 } 364 365 // 2. Confirm that there is a trash mailbox available 366 // 3. If there's no trash mailbox, create one 367 // TODO: Does this need to be signaled explicitly to the sync engines? 368 long trashMailboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_TRASH); 369 370 // 4. Change the mailbox key for the message we're "deleting" 371 ContentValues cv = new ContentValues(); 372 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId); 373 Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId); 374 resolver.update(uri, cv, null, null); 375 376 // 5. Drop non-essential data for the message (e.g. attachments) 377 // TODO: find the actual files (if any, if loaded) & delete them 378 c = null; 379 try { 380 c = resolver.query(EmailContent.Attachment.CONTENT_URI, 381 EmailContent.Attachment.CONTENT_PROJECTION, 382 EmailContent.AttachmentColumns.MESSAGE_KEY + "=?", 383 new String[] { Long.toString(messageId) }, null); 384 while (c.moveToNext()) { 385 // delete any associated storage 386 // delete row? 387 } 388 } finally { 389 if (c != null) c.close(); 390 } 391 392 // 6. For IMAP/POP3 we may need to kick off an immediate delete (depends on acct settings) 393 // TODO write this 394 } 395 396 /** 397 * Set/clear the unread status of a message 398 * 399 * @param messageId the message to update 400 * @param isRead the new value for the isRead flag 401 */ 402 public void setMessageRead(long messageId, boolean isRead) { 403 // TODO this should not be in this thread. queue it up. 404 // TODO Also, it needs to update the read/unread count in the mailbox 405 // TODO kick off service/messagingcontroller actions 406 407 ContentValues cv = new ContentValues(); 408 cv.put(EmailContent.MessageColumns.FLAG_READ, isRead); 409 Uri uri = ContentUris.withAppendedId( 410 EmailContent.Message.SYNCED_CONTENT_URI, messageId); 411 mProviderContext.getContentResolver().update(uri, cv, null, null); 412 } 413 414 /** 415 * Set/clear the favorite status of a message 416 * 417 * @param messageId the message to update 418 * @param isFavorite the new value for the isFavorite flag 419 */ 420 public void setMessageFavorite(long messageId, boolean isFavorite) { 421 // TODO this should not be in this thread. queue it up. 422 // TODO kick off service/messagingcontroller actions 423 424 ContentValues cv = new ContentValues(); 425 cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite); 426 Uri uri = ContentUris.withAppendedId( 427 EmailContent.Message.SYNCED_CONTENT_URI, messageId); 428 mProviderContext.getContentResolver().update(uri, cv, null, null); 429 } 430 431 /** 432 * Request that an attachment be loaded. It will be stored at a location controlled 433 * by the AttachmentProvider. 434 * 435 * @param attachmentId the attachment to load 436 * @param messageId the owner message 437 * @param accountId the owner account 438 * @param callback the Controller callback by which results will be reported 439 */ 440 public void loadAttachment(long attachmentId, long messageId, long accountId, 441 final Result callback) { 442 443 Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId); 444 445 File saveToFile = AttachmentProvider.getAttachmentFilename(mContext, 446 accountId, attachmentId); 447 448 // Split here for target type (Service or MessagingController) 449 IEmailService service = getServiceForMessage(messageId); 450 if (service != null) { 451 // Service implementation 452 try { 453 service.loadAttachment(attachInfo.mId, saveToFile.getAbsolutePath(), 454 AttachmentProvider.getAttachmentUri(accountId, attachmentId).toString()); 455 } catch (RemoteException e) { 456 // TODO Change exception handling to be consistent with however this method 457 // is implemented for other protocols 458 Log.e("onDownloadAttachment", "RemoteException", e); 459 } 460 } else { 461 // MessagingController implementation 462 } 463 } 464 465 /** 466 * For a given message id, return a service proxy if applicable, or null. 467 * 468 * @param messageId the message of interest 469 * @result service proxy, or null if n/a 470 */ 471 private IEmailService getServiceForMessage(long messageId) { 472 // TODO make this more efficient, caching the account, smaller lookup here, etc. 473 Message message = Message.restoreMessageWithId(mProviderContext, messageId); 474 return getServiceForAccount(message.mAccountKey); 475 } 476 477 /** 478 * For a given account id, return a service proxy if applicable, or null. 479 * 480 * @param accountId the message of interest 481 * @result service proxy, or null if n/a 482 */ 483 private IEmailService getServiceForAccount(long accountId) { 484 // TODO make this more efficient, caching the account, MUCH smaller lookup here, etc. 485 Account account = EmailContent.Account.restoreAccountWithId(mProviderContext, accountId); 486 if (isMessagingController(account)) { 487 return null; 488 } else { 489 return new EmailServiceProxy(mContext, SyncManager.class, mServiceCallback); 490 } 491 } 492 493 /** 494 * Simple helper to determine if legacy MessagingController should be used 495 * 496 * TODO this should not require a full account, just an accountId 497 * TODO this should use a cache because we'll be doing this a lot 498 */ 499 private boolean isMessagingController(EmailContent.Account account) { 500 Store.StoreInfo info = 501 Store.StoreInfo.getStoreInfo(account.getStoreUri(mContext), mContext); 502 String scheme = info.mScheme; 503 504 return ("pop3".equals(scheme) || "imap".equals(scheme)); 505 } 506 507 /** 508 * Simple callback for synchronous commands. For many commands, this can be largely ignored 509 * and the result is observed via provider cursors. The callback will *not* necessarily be 510 * made from the UI thread, so you may need further handlers to safely make UI updates. 511 */ 512 public interface Result { 513 514 /** 515 * Callback for updateMailboxList 516 * 517 * @param result If null, the operation completed without error 518 * @param accountId The account being operated on 519 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 520 */ 521 public void updateMailboxListCallback(MessagingException result, long accountId, 522 int progress); 523 524 /** 525 * Callback for updateMailbox 526 * 527 * @param result If null, the operation completed without error 528 * @param accountId The account being operated on 529 * @param mailboxId The mailbox being operated on 530 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 531 */ 532 public void updateMailboxCallback(MessagingException result, long accountId, 533 long mailboxId, int progress, int totalMessagesInMailbox, int numNewMessages); 534 535 /** 536 * Callback for loadAttachment 537 * 538 * @param result if null, the attachment completed - if non-null, terminating with failure 539 * @param messageId the message which contains the attachment 540 * @param attachmentId the attachment being loaded 541 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 542 */ 543 public void loadAttachmentCallback(MessagingException result, long messageId, 544 long attachmentId, int progress); 545 } 546 547 /** 548 * Support for receiving callbacks from MessagingController and dealing with UI going 549 * out of scope. 550 */ 551 private class LegacyListener extends MessagingListener { 552 Result mResultCallback; 553 554 public LegacyListener(Result callback) { 555 mResultCallback = callback; 556 } 557 558 @Override 559 public void listFoldersStarted(EmailContent.Account account) { 560 if (mResultCallback != null && isActiveResultCallback(mResultCallback)) { 561 mResultCallback.updateMailboxListCallback(null, account.mId, 0); 562 } 563 } 564 565 @Override 566 public void listFoldersFailed(EmailContent.Account account, String message) { 567 if (mResultCallback != null && isActiveResultCallback(mResultCallback)) { 568 mResultCallback.updateMailboxListCallback(new MessagingException(message), 569 account.mId, 0); 570 } 571 mLegacyController.removeListener(this); 572 } 573 574 @Override 575 public void listFoldersFinished(EmailContent.Account account) { 576 if (mResultCallback != null && isActiveResultCallback(mResultCallback)) { 577 mResultCallback.updateMailboxListCallback(null, account.mId, 100); 578 } 579 mLegacyController.removeListener(this); 580 } 581 582 @Override 583 public void synchronizeMailboxStarted(EmailContent.Account account, 584 EmailContent.Mailbox folder) { 585 if (mResultCallback != null && isActiveResultCallback(mResultCallback)) { 586 mResultCallback.updateMailboxCallback(null, account.mId, folder.mId, 0, -1, -1); 587 } 588 } 589 590 @Override 591 public void synchronizeMailboxFinished(EmailContent.Account account, 592 EmailContent.Mailbox folder, int totalMessagesInMailbox, int numNewMessages) { 593 if (mResultCallback != null && isActiveResultCallback(mResultCallback)) { 594 mResultCallback.updateMailboxCallback(null, account.mId, folder.mId, 100, 595 totalMessagesInMailbox, numNewMessages); 596 } 597 mLegacyController.removeListener(this); 598 } 599 600 @Override 601 public void synchronizeMailboxFailed(EmailContent.Account account, 602 EmailContent.Mailbox folder, Exception e) { 603 if (mResultCallback != null && isActiveResultCallback(mResultCallback)) { 604 MessagingException me; 605 if (e instanceof MessagingException) { 606 me = (MessagingException) e; 607 } else { 608 me = new MessagingException(e.toString()); 609 } 610 mResultCallback.updateMailboxCallback(me, account.mId, folder.mId, 0, -1, -1); 611 } 612 mLegacyController.removeListener(this); 613 } 614 615 616 } 617 618 /** 619 * Service callback for service operations 620 */ 621 private class ServiceCallback extends IEmailServiceCallback.Stub { 622 623 private final static boolean DEBUG_FAIL_DOWNLOADS = false; // do not check in "true" 624 625 public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, 626 int progress) { 627 MessagingException result = null; 628 switch (statusCode) { 629 case EmailServiceStatus.SUCCESS: 630 progress = 100; 631 break; 632 case EmailServiceStatus.IN_PROGRESS: 633 if (DEBUG_FAIL_DOWNLOADS && progress > 75) { 634 result = new MessagingException( 635 String.valueOf(EmailServiceStatus.CONNECTION_ERROR)); 636 } 637 // discard progress reports that look like sentinels 638 if (progress < 0 || progress >= 100) { 639 return; 640 } 641 break; 642 default: 643 result = new MessagingException(String.valueOf(statusCode)); 644 break; 645 } 646 synchronized (mListeners) { 647 for (Result listener : mListeners) { 648 listener.loadAttachmentCallback(result, messageId, attachmentId, progress); 649 } 650 } 651 } 652 653 public void sendMessageStatus(long messageId, int statusCode, int progress) { 654 // TODO Auto-generated method stub 655 656 } 657 658 public void syncMailboxListStatus(long accountId, int statusCode, int progress) { 659 MessagingException result= null; 660 switch (statusCode) { 661 case EmailServiceStatus.SUCCESS: 662 progress = 100; 663 break; 664 case EmailServiceStatus.IN_PROGRESS: 665 // discard progress reports that look like sentinels 666 if (progress < 0 || progress >= 100) { 667 return; 668 } 669 break; 670 default: 671 result = new MessagingException(String.valueOf(statusCode)); 672 break; 673 } 674 synchronized(mListeners) { 675 for (Result listener : mListeners) { 676 listener.updateMailboxListCallback(result, accountId, progress); 677 } 678 } 679 } 680 681 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) { 682 MessagingException result= null; 683 switch (statusCode) { 684 case EmailServiceStatus.SUCCESS: 685 progress = 100; 686 break; 687 case EmailServiceStatus.IN_PROGRESS: 688 // discard progress reports that look like sentinels 689 if (progress < 0 || progress >= 100) { 690 return; 691 } 692 break; 693 default: 694 result = new MessagingException(String.valueOf(statusCode)); 695 break; 696 } 697 // TODO can we get "number of new messages" back as well? 698 // TODO remove "total num messages" which can be looked up if needed 699 // TODO should pass this back instead of looking it up here 700 // TODO smaller projection 701 Mailbox mbx = Mailbox.restoreMailboxWithId(mContext, mailboxId); 702 long accountId = mbx.mAccountKey; 703 synchronized(mListeners) { 704 for (Result listener : mListeners) { 705 listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0, 0); 706 } 707 } 708 } 709 } 710} 711