Controller.java revision 6c21942ec45f561d711b3d74ecca8e62afb735c4
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.MailboxColumns; 27import com.android.email.provider.EmailContent.Message; 28import com.android.email.provider.EmailContent.MessageColumns; 29import com.android.email.service.EmailServiceProxy; 30import com.android.exchange.EmailServiceStatus; 31import com.android.exchange.IEmailService; 32import com.android.exchange.IEmailServiceCallback; 33import com.android.exchange.SyncManager; 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.util.HashSet; 46 47/** 48 * New central controller/dispatcher for Email activities that may require remote operations. 49 * Handles disambiguating between legacy MessagingController operations and newer provider/sync 50 * based code. 51 */ 52public class Controller { 53 54 static Controller sInstance; 55 private Context mContext; 56 private Context mProviderContext; 57 private MessagingController mLegacyController; 58 private LegacyListener mLegacyListener = new LegacyListener(); 59 private ServiceCallback mServiceCallback = new ServiceCallback(); 60 private HashSet<Result> mListeners = new HashSet<Result>(); 61 62 private static String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] { 63 EmailContent.RECORD_ID, 64 EmailContent.MessageColumns.ACCOUNT_KEY 65 }; 66 private static int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1; 67 68 protected Controller(Context _context) { 69 mContext = _context; 70 mProviderContext = _context; 71 mLegacyController = MessagingController.getInstance(mContext); 72 mLegacyController.addListener(mLegacyListener); 73 } 74 75 /** 76 * Gets or creates the singleton instance of Controller. 77 * @param _context The context that will be used for all underlying system access 78 */ 79 public synchronized static Controller getInstance(Context _context) { 80 if (sInstance == null) { 81 sInstance = new Controller(_context); 82 } 83 return sInstance; 84 } 85 86 /** 87 * For testing only: Inject a different context for provider access. This will be 88 * used internally for access the underlying provider (e.g. getContentResolver().query()). 89 * @param providerContext the provider context to be used by this instance 90 */ 91 public void setProviderContext(Context providerContext) { 92 mProviderContext = providerContext; 93 } 94 95 /** 96 * Any UI code that wishes for callback results (on async ops) should register their callback 97 * here (typically from onResume()). Unregistered callbacks will never be called, to prevent 98 * problems when the command completes and the activity has already paused or finished. 99 * @param listener The callback that may be used in action methods 100 */ 101 public void addResultCallback(Result listener) { 102 synchronized (mListeners) { 103 mListeners.add(listener); 104 } 105 } 106 107 /** 108 * Any UI code that no longer wishes for callback results (on async ops) should unregister 109 * their callback here (typically from onPause()). Unregistered callbacks will never be called, 110 * to prevent problems when the command completes and the activity has already paused or 111 * finished. 112 * @param listener The callback that may no longer be used 113 */ 114 public void removeResultCallback(Result listener) { 115 synchronized (mListeners) { 116 mListeners.remove(listener); 117 } 118 } 119 120 private boolean isActiveResultCallback(Result listener) { 121 synchronized (mListeners) { 122 return mListeners.contains(listener); 123 } 124 } 125 126 /** 127 * Enable/disable logging for external sync services 128 * 129 * Generally this should be called by anybody who changes Email.DEBUG 130 */ 131 public void serviceLogging(int debugEnabled) { 132 IEmailService service = 133 new EmailServiceProxy(mContext, SyncManager.class, mServiceCallback); 134 try { 135 service.setLogging(debugEnabled); 136 } catch (RemoteException e) { 137 // TODO Change exception handling to be consistent with however this method 138 // is implemented for other protocols 139 Log.d("updateMailboxList", "RemoteException" + e); 140 } 141 } 142 143 /** 144 * Request a remote update of mailboxes for an account. 145 * 146 * TODO: Clean up threading in MessagingController cases (or perhaps here in Controller) 147 */ 148 public void updateMailboxList(final long accountId, final Result callback) { 149 150 IEmailService service = getServiceForAccount(accountId); 151 if (service != null) { 152 // Service implementation 153 try { 154 service.updateFolderList(accountId); 155 } catch (RemoteException e) { 156 // TODO Change exception handling to be consistent with however this method 157 // is implemented for other protocols 158 Log.d("updateMailboxList", "RemoteException" + e); 159 } 160 } else { 161 // MessagingController implementation 162 new Thread() { 163 @Override 164 public void run() { 165 mLegacyController.listFolders(accountId, mLegacyListener); 166 } 167 }.start(); 168 } 169 } 170 171 /** 172 * Request a remote update of a mailbox. For use by the timed service. 173 * 174 * Functionally this is quite similar to updateMailbox(), but it's a separate API and 175 * separate callback in order to keep UI callbacks from affecting the service loop. 176 */ 177 public void serviceCheckMail(final long accountId, final long mailboxId, final long tag, 178 final Result callback) { 179 IEmailService service = getServiceForAccount(accountId); 180 if (service != null) { 181 // Service implementation 182// try { 183 // TODO this isn't quite going to work, because we're going to get the 184 // generic (UI) callbacks and not the ones we need to restart the ol' service. 185 // service.startSync(mailboxId, tag); 186 callback.serviceCheckMailCallback(null, accountId, mailboxId, 100, tag); 187// } catch (RemoteException e) { 188 // TODO Change exception handling to be consistent with however this method 189 // is implemented for other protocols 190// Log.d("updateMailbox", "RemoteException" + e); 191// } 192 } else { 193 // MessagingController implementation 194 new Thread() { 195 @Override 196 public void run() { 197 mLegacyController.checkMail(accountId, tag, mLegacyListener); 198 } 199 }.start(); 200 } 201 } 202 203 /** 204 * Request a remote update of a mailbox. 205 * 206 * The contract here should be to try and update the headers ASAP, in order to populate 207 * a simple message list. We should also at this point queue up a background task of 208 * downloading some/all of the messages in this mailbox, but that should be interruptable. 209 */ 210 public void updateMailbox(final long accountId, final long mailboxId, final Result callback) { 211 212 IEmailService service = getServiceForAccount(accountId); 213 if (service != null) { 214 // Service implementation 215 try { 216 service.startSync(mailboxId); 217 } catch (RemoteException e) { 218 // TODO Change exception handling to be consistent with however this method 219 // is implemented for other protocols 220 Log.d("updateMailbox", "RemoteException" + e); 221 } 222 } else { 223 // MessagingController implementation 224 new Thread() { 225 @Override 226 public void run() { 227 // TODO shouldn't be passing fully-build accounts & mailboxes into APIs 228 Account account = 229 EmailContent.Account.restoreAccountWithId(mProviderContext, accountId); 230 Mailbox mailbox = 231 EmailContent.Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); 232 mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener); 233 } 234 }.start(); 235 } 236 } 237 238 /** 239 * Request that any final work necessary be done, to load a message. 240 * 241 * Note, this assumes that the caller has already checked message.mFlagLoaded and that 242 * additional work is needed. There is no optimization here for a message which is already 243 * loaded. 244 * 245 * @param messageId the message to load 246 * @param callback the Controller callback by which results will be reported 247 */ 248 public void loadMessageForView(final long messageId, final Result callback) { 249 250 // Split here for target type (Service or MessagingController) 251 IEmailService service = getServiceForMessage(messageId); 252 if (service != null) { 253 // There is no service implementation, so we'll just jam the value, log the error, 254 // and get out of here. 255 Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId); 256 ContentValues cv = new ContentValues(); 257 cv.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE); 258 mContext.getContentResolver().update(uri, cv, null, null); 259 Log.d(Email.LOG_TAG, "Unexpected loadMessageForView() for service-based message."); 260 synchronized (mListeners) { 261 for (Result listener : mListeners) { 262 listener.loadMessageForViewCallback(null, messageId, 100); 263 } 264 } 265 } else { 266 // MessagingController implementation 267 new Thread() { 268 @Override 269 public void run() { 270 mLegacyController.loadMessageForView(messageId, mLegacyListener); 271 } 272 }.start(); 273 } 274 } 275 276 277 /** 278 * Saves the message to a mailbox of given type. 279 * This is a synchronous operation taking place in the same thread as the caller. 280 * Upon return the message.mId is set. 281 * @param message the message (must have the mAccountId set). 282 * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS). 283 */ 284 public void saveToMailbox(final EmailContent.Message message, final int mailboxType) { 285 long accountId = message.mAccountKey; 286 long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType); 287 message.mMailboxKey = mailboxId; 288 message.save(mContext); 289 } 290 291 /** 292 * @param accountId the account id 293 * @param mailboxType the mailbox type (e.g. EmailContent.Mailbox.TYPE_TRASH) 294 * @return the id of the mailbox. The mailbox is created if not existing. 295 * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative. 296 * Does not validate the input in other ways (e.g. does not verify the existence of account). 297 */ 298 public long findOrCreateMailboxOfType(long accountId, int mailboxType) { 299 if (accountId < 0 || mailboxType < 0) { 300 return Mailbox.NO_MAILBOX; 301 } 302 long mailboxId = 303 Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType); 304 return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId; 305 } 306 307 /** 308 * @param mailboxType the mailbox type 309 * @return the resource string corresponding to the mailbox type, empty if not found. 310 */ 311 /* package */ String getSpecialMailboxDisplayName(int mailboxType) { 312 int resId = -1; 313 switch (mailboxType) { 314 case Mailbox.TYPE_INBOX: 315 // TODO: there is no special_mailbox_display_name_inbox; why? 316 resId = R.string.special_mailbox_name_inbox; 317 break; 318 case Mailbox.TYPE_OUTBOX: 319 resId = R.string.special_mailbox_display_name_outbox; 320 break; 321 case Mailbox.TYPE_DRAFTS: 322 resId = R.string.special_mailbox_display_name_drafts; 323 break; 324 case Mailbox.TYPE_TRASH: 325 resId = R.string.special_mailbox_display_name_trash; 326 break; 327 case Mailbox.TYPE_SENT: 328 resId = R.string.special_mailbox_display_name_sent; 329 break; 330 } 331 return resId != -1 ? mContext.getString(resId) : ""; 332 } 333 334 /** 335 * Create a mailbox given the account and mailboxType. 336 * TODO: Does this need to be signaled explicitly to the sync engines? 337 * As this method is only used internally ('private'), it does not 338 * validate its inputs (accountId and mailboxType). 339 */ 340 /* package */ long createMailbox(long accountId, int mailboxType) { 341 if (accountId < 0 || mailboxType < 0) { 342 String mes = "Invalid arguments " + accountId + ' ' + mailboxType; 343 Log.e(Email.LOG_TAG, mes); 344 throw new RuntimeException(mes); 345 } 346 Mailbox box = new Mailbox(); 347 box.mAccountKey = accountId; 348 box.mType = mailboxType; 349 box.mSyncInterval = EmailContent.Account.CHECK_INTERVAL_NEVER; 350 box.mFlagVisible = true; 351 box.mDisplayName = getSpecialMailboxDisplayName(mailboxType); 352 box.save(mProviderContext); 353 return box.mId; 354 } 355 356 /** 357 * Send a message: 358 * - move the message to Outbox (the message is assumed to be in Drafts). 359 * - EAS service will take it from there 360 * - trigger send for POP/IMAP 361 * @param messageId the id of the message to send 362 */ 363 public void sendMessage(long messageId, long accountId) { 364 ContentResolver resolver = mProviderContext.getContentResolver(); 365 if (accountId == -1) { 366 accountId = lookupAccountForMessage(messageId); 367 } 368 if (accountId == -1) { 369 // probably the message was not found 370 if (Email.LOGD) { 371 Email.log("no account found for message " + messageId); 372 } 373 return; 374 } 375 376 // Move to Outbox 377 long outboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_OUTBOX); 378 ContentValues cv = new ContentValues(); 379 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, outboxId); 380 381 // does this need to be SYNCED_CONTENT_URI instead? 382 Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId); 383 resolver.update(uri, cv, null, null); 384 385 // for IMAP & POP only, (attempt to) send the message now 386 final EmailContent.Account account = 387 EmailContent.Account.restoreAccountWithId(mContext, accountId); 388 if (this.isMessagingController(account)) { 389 final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT); 390 new Thread() { 391 @Override 392 public void run() { 393 mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener); 394 } 395 }.start(); 396 } 397 } 398 399 /** 400 * Try to send all pending messages for a given account 401 * 402 * @param accountId the account for which to send messages (-1 for all accounts) 403 * @param callback 404 */ 405 public void sendPendingMessages(long accountId, Result callback) { 406 // 1. make sure we even have an outbox, exit early if not 407 final long outboxId = 408 Mailbox.findMailboxOfType(mProviderContext, accountId, Mailbox.TYPE_OUTBOX); 409 if (outboxId == Mailbox.NO_MAILBOX) { 410 return; 411 } 412 413 // 2. dispatch as necessary 414 IEmailService service = getServiceForAccount(accountId); 415 if (service != null) { 416 // Service implementation 417 try { 418 service.startSync(outboxId); 419 } catch (RemoteException e) { 420 // TODO Change exception handling to be consistent with however this method 421 // is implemented for other protocols 422 Log.d("updateMailbox", "RemoteException" + e); 423 } 424 } else { 425 // MessagingController implementation 426 final EmailContent.Account account = 427 EmailContent.Account.restoreAccountWithId(mContext, accountId); 428 final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT); 429 new Thread() { 430 @Override 431 public void run() { 432 mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener); 433 } 434 }.start(); 435 } 436 } 437 438 /** 439 * Reset visible limits for all accounts. 440 * For each account: 441 * look up limit 442 * write limit into all mailboxes for that account 443 */ 444 public void resetVisibleLimits() { 445 new Thread() { 446 @Override 447 public void run() { 448 ContentResolver resolver = mContext.getContentResolver(); 449 Cursor c = null; 450 try { 451 c = resolver.query( 452 Account.CONTENT_URI, 453 Account.ID_PROJECTION, 454 null, null, null); 455 while (c.moveToNext()) { 456 long accountId = c.getLong(Account.ID_PROJECTION_COLUMN); 457 Account account = Account.restoreAccountWithId(mContext, accountId); 458 if (account != null) { 459 Store.StoreInfo info = Store.StoreInfo.getStoreInfo( 460 account.getStoreUri(mContext), mContext); 461 if (info != null && info.mVisibleLimitDefault > 0) { 462 int limit = info.mVisibleLimitDefault; 463 ContentValues cv = new ContentValues(); 464 cv.put(MailboxColumns.VISIBLE_LIMIT, limit); 465 resolver.update(Mailbox.CONTENT_URI, cv, 466 MailboxColumns.ACCOUNT_KEY + "=?", 467 new String[] { Long.toString(accountId) }); 468 } 469 } 470 } 471 } finally { 472 c.close(); 473 } 474 } 475 }.start(); 476 } 477 478 /** 479 * Increase the load count for a given mailbox, and trigger a refresh. Applies only to 480 * IMAP and POP. 481 * 482 * @param mailboxId the mailbox 483 * @param callback 484 */ 485 public void loadMoreMessages(final long mailboxId, Result callback) { 486 new Thread() { 487 @Override 488 public void run() { 489 Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); 490 if (mailbox == null) { 491 return; 492 } 493 Account account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey); 494 if (account == null) { 495 return; 496 } 497 Store.StoreInfo info = Store.StoreInfo.getStoreInfo( 498 account.getStoreUri(mContext), mContext); 499 if (info != null && info.mVisibleLimitIncrement > 0) { 500 // Use provider math to increment the field 501 ContentValues cv = new ContentValues();; 502 cv.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT); 503 cv.put(EmailContent.ADD_COLUMN_NAME, info.mVisibleLimitIncrement); 504 Uri uri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, mailboxId); 505 mContext.getContentResolver().update(uri, cv, null, null); 506 // Trigger a refresh using the new, longer limit 507 mailbox.mVisibleLimit += info.mVisibleLimitIncrement; 508 mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener); 509 } 510 } 511 }.start(); 512 } 513 514 /** 515 * @param messageId the id of message 516 * @return the accountId corresponding to the given messageId, or -1 if not found. 517 */ 518 private long lookupAccountForMessage(long messageId) { 519 ContentResolver resolver = mProviderContext.getContentResolver(); 520 Cursor c = resolver.query(EmailContent.Message.CONTENT_URI, 521 MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?", 522 new String[] { Long.toString(messageId) }, null); 523 try { 524 return c.moveToFirst() 525 ? c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID) 526 : -1; 527 } finally { 528 c.close(); 529 } 530 } 531 532 /** 533 * Delete a single message by moving it to the trash. 534 * 535 * This function has no callback, no result reporting, because the desired outcome 536 * is reflected entirely by changes to one or more cursors. 537 * 538 * @param messageId The id of the message to "delete". 539 * @param accountId The id of the message's account, or -1 if not known by caller 540 * 541 * TODO: Move out of UI thread 542 * TODO: "get account a for message m" should be a utility 543 * TODO: "get mailbox of type n for account a" should be a utility 544 */ 545 public void deleteMessage(long messageId, long accountId) { 546 ContentResolver resolver = mProviderContext.getContentResolver(); 547 548 // 1. Look up acct# for message we're deleting 549 Cursor c = null; 550 if (accountId == -1) { 551 accountId = lookupAccountForMessage(messageId); 552 } 553 if (accountId == -1) { 554 return; 555 } 556 557 // 2. Confirm that there is a trash mailbox available 558 // 3. If there's no trash mailbox, create one 559 // TODO: Does this need to be signaled explicitly to the sync engines? 560 long trashMailboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_TRASH); 561 562 // 4. Change the mailbox key for the message we're "deleting" 563 ContentValues cv = new ContentValues(); 564 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId); 565 Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId); 566 resolver.update(uri, cv, null, null); 567 568 // 5. Drop non-essential data for the message (e.g. attachment files) 569 AttachmentProvider.deleteAllAttachmentFiles(mContext, accountId, messageId); 570 571 // 6. Service runs automatically, MessagingController needs a kick 572 final Message message = Message.restoreMessageWithId(mContext, messageId); 573 Account account = Account.restoreAccountWithId(mContext, message.mAccountKey); 574 if (isMessagingController(account)) { 575 new Thread() { 576 @Override 577 public void run() { 578 mLegacyController.processPendingCommands(message.mAccountKey); 579 } 580 }.start(); 581 } 582 } 583 584 /** 585 * Set/clear the unread status of a message 586 * 587 * TODO db ops should not be in this thread. queue it up. 588 * 589 * @param messageId the message to update 590 * @param isRead the new value for the isRead flag 591 */ 592 public void setMessageRead(final long messageId, boolean isRead) { 593 ContentValues cv = new ContentValues(); 594 cv.put(EmailContent.MessageColumns.FLAG_READ, isRead); 595 Uri uri = ContentUris.withAppendedId( 596 EmailContent.Message.SYNCED_CONTENT_URI, messageId); 597 mProviderContext.getContentResolver().update(uri, cv, null, null); 598 599 // Service runs automatically, MessagingController needs a kick 600 final Message message = Message.restoreMessageWithId(mContext, messageId); 601 Account account = Account.restoreAccountWithId(mContext, message.mAccountKey); 602 if (isMessagingController(account)) { 603 new Thread() { 604 @Override 605 public void run() { 606 mLegacyController.processPendingCommands(message.mAccountKey); 607 } 608 }.start(); 609 } 610 } 611 612 /** 613 * Set/clear the favorite status of a message 614 * 615 * TODO db ops should not be in this thread. queue it up. 616 * 617 * @param messageId the message to update 618 * @param isFavorite the new value for the isFavorite flag 619 */ 620 public void setMessageFavorite(final long messageId, boolean isFavorite) { 621 ContentValues cv = new ContentValues(); 622 cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite); 623 Uri uri = ContentUris.withAppendedId( 624 EmailContent.Message.SYNCED_CONTENT_URI, messageId); 625 mProviderContext.getContentResolver().update(uri, cv, null, null); 626 627 // Service runs automatically, MessagingController needs a kick 628 final Message message = Message.restoreMessageWithId(mContext, messageId); 629 Account account = Account.restoreAccountWithId(mContext, message.mAccountKey); 630 if (isMessagingController(account)) { 631 new Thread() { 632 @Override 633 public void run() { 634 mLegacyController.processPendingCommands(message.mAccountKey); 635 } 636 }.start(); 637 } 638 } 639 640 /** 641 * Request that an attachment be loaded. It will be stored at a location controlled 642 * by the AttachmentProvider. 643 * 644 * @param attachmentId the attachment to load 645 * @param messageId the owner message 646 * @param mailboxId the owner mailbox 647 * @param accountId the owner account 648 * @param callback the Controller callback by which results will be reported 649 */ 650 public void loadAttachment(final long attachmentId, final long messageId, final long mailboxId, 651 final long accountId, final Result callback) { 652 653 File saveToFile = AttachmentProvider.getAttachmentFilename(mContext, 654 accountId, attachmentId); 655 if (saveToFile.exists()) { 656 // The attachment has already been downloaded, so we will just "pretend" to download it 657 synchronized (mListeners) { 658 for (Result listener : mListeners) { 659 listener.loadAttachmentCallback(null, messageId, attachmentId, 0); 660 } 661 for (Result listener : mListeners) { 662 listener.loadAttachmentCallback(null, messageId, attachmentId, 100); 663 } 664 } 665 return; 666 } 667 668 Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId); 669 670 // Split here for target type (Service or MessagingController) 671 IEmailService service = getServiceForMessage(messageId); 672 if (service != null) { 673 // Service implementation 674 try { 675 service.loadAttachment(attachInfo.mId, saveToFile.getAbsolutePath(), 676 AttachmentProvider.getAttachmentUri(accountId, attachmentId).toString()); 677 } catch (RemoteException e) { 678 // TODO Change exception handling to be consistent with however this method 679 // is implemented for other protocols 680 Log.e("onDownloadAttachment", "RemoteException", e); 681 } 682 } else { 683 // MessagingController implementation 684 new Thread() { 685 @Override 686 public void run() { 687 mLegacyController.loadAttachment(accountId, messageId, mailboxId, attachmentId, 688 mLegacyListener); 689 } 690 }.start(); 691 } 692 } 693 694 /** 695 * For a given message id, return a service proxy if applicable, or null. 696 * 697 * @param messageId the message of interest 698 * @result service proxy, or null if n/a 699 */ 700 private IEmailService getServiceForMessage(long messageId) { 701 // TODO make this more efficient, caching the account, smaller lookup here, etc. 702 Message message = Message.restoreMessageWithId(mProviderContext, messageId); 703 return getServiceForAccount(message.mAccountKey); 704 } 705 706 /** 707 * For a given account id, return a service proxy if applicable, or null. 708 * 709 * TODO this should use a cache because we'll be doing this a lot 710 * 711 * @param accountId the message of interest 712 * @result service proxy, or null if n/a 713 */ 714 private IEmailService getServiceForAccount(long accountId) { 715 // TODO make this more efficient, caching the account, MUCH smaller lookup here, etc. 716 Account account = EmailContent.Account.restoreAccountWithId(mProviderContext, accountId); 717 if (isMessagingController(account)) { 718 return null; 719 } else { 720 return new EmailServiceProxy(mContext, SyncManager.class, mServiceCallback); 721 } 722 } 723 724 /** 725 * Simple helper to determine if legacy MessagingController should be used 726 * 727 * TODO this should not require a full account, just an accountId 728 * TODO this should use a cache because we'll be doing this a lot 729 */ 730 public boolean isMessagingController(EmailContent.Account account) { 731 Store.StoreInfo info = 732 Store.StoreInfo.getStoreInfo(account.getStoreUri(mContext), mContext); 733 String scheme = info.mScheme; 734 735 return ("pop3".equals(scheme) || "imap".equals(scheme)); 736 } 737 738 /** 739 * Simple callback for synchronous commands. For many commands, this can be largely ignored 740 * and the result is observed via provider cursors. The callback will *not* necessarily be 741 * made from the UI thread, so you may need further handlers to safely make UI updates. 742 */ 743 public interface Result { 744 /** 745 * Callback for updateMailboxList 746 * 747 * @param result If null, the operation completed without error 748 * @param accountId The account being operated on 749 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 750 */ 751 public void updateMailboxListCallback(MessagingException result, long accountId, 752 int progress); 753 754 /** 755 * Callback for updateMailbox. Note: This looks a lot like checkMailCallback, but 756 * it's a separate call used only by UI's, so we can keep things separate. 757 * 758 * @param result If null, the operation completed without error 759 * @param accountId The account being operated on 760 * @param mailboxId The mailbox being operated on 761 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 762 * @param numNewMessages the number of new messages delivered 763 */ 764 public void updateMailboxCallback(MessagingException result, long accountId, 765 long mailboxId, int progress, int numNewMessages); 766 767 /** 768 * Callback for loadMessageForView 769 * 770 * @param result if null, the attachment completed - if non-null, terminating with failure 771 * @param messageId the message which contains the attachment 772 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 773 */ 774 public void loadMessageForViewCallback(MessagingException result, long messageId, 775 int progress); 776 777 /** 778 * Callback for loadAttachment 779 * 780 * @param result if null, the attachment completed - if non-null, terminating with failure 781 * @param messageId the message which contains the attachment 782 * @param attachmentId the attachment being loaded 783 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 784 */ 785 public void loadAttachmentCallback(MessagingException result, long messageId, 786 long attachmentId, int progress); 787 788 /** 789 * Callback for checkmail. Note: This looks a lot like updateMailboxCallback, but 790 * it's a separate call used only by the automatic checker service, so we can keep 791 * things separate. 792 * 793 * @param result If null, the operation completed without error 794 * @param accountId The account being operated on 795 * @param mailboxId The mailbox being operated on (may be unknown at start) 796 * @param progress 0 for "starting", no updates, 100 for complete 797 * @param tag the same tag that was passed to serviceCheckMail() 798 */ 799 public void serviceCheckMailCallback(MessagingException result, long accountId, 800 long mailboxId, int progress, long tag); 801 802 /** 803 * Callback for sending pending messages. This will be called once to start the 804 * group, multiple times for messages, and once to complete the group. 805 * 806 * @param result If null, the operation completed without error 807 * @param accountId The account being operated on 808 * @param messageId The being sent (may be unknown at start) 809 * @param progress 0 for "starting", 100 for complete 810 */ 811 public void sendMailCallback(MessagingException result, long accountId, 812 long messageId, int progress); 813 } 814 815 /** 816 * Support for receiving callbacks from MessagingController and dealing with UI going 817 * out of scope. 818 */ 819 private class LegacyListener extends MessagingListener { 820 821 @Override 822 public void listFoldersStarted(EmailContent.Account account) { 823 synchronized (mListeners) { 824 for (Result l : mListeners) { 825 l.updateMailboxListCallback(null, account.mId, 0); 826 } 827 } 828 } 829 830 @Override 831 public void listFoldersFailed(EmailContent.Account account, String message) { 832 synchronized (mListeners) { 833 for (Result l : mListeners) { 834 l.updateMailboxListCallback(new MessagingException(message), account.mId, 0); 835 } 836 } 837 } 838 839 @Override 840 public void listFoldersFinished(EmailContent.Account account) { 841 synchronized (mListeners) { 842 for (Result l : mListeners) { 843 l.updateMailboxListCallback(null, account.mId, 100); 844 } 845 } 846 } 847 848 @Override 849 public void synchronizeMailboxStarted(EmailContent.Account account, 850 EmailContent.Mailbox folder) { 851 synchronized (mListeners) { 852 for (Result l : mListeners) { 853 l.updateMailboxCallback(null, account.mId, folder.mId, 0, 0); 854 } 855 } 856 } 857 858 @Override 859 public void synchronizeMailboxFinished(EmailContent.Account account, 860 EmailContent.Mailbox folder, int totalMessagesInMailbox, int numNewMessages) { 861 synchronized (mListeners) { 862 for (Result l : mListeners) { 863 l.updateMailboxCallback(null, account.mId, folder.mId, 100, numNewMessages); 864 } 865 } 866 } 867 868 @Override 869 public void synchronizeMailboxFailed(EmailContent.Account account, 870 EmailContent.Mailbox folder, Exception e) { 871 MessagingException me; 872 if (e instanceof MessagingException) { 873 me = (MessagingException) e; 874 } else { 875 me = new MessagingException(e.toString()); 876 } 877 synchronized (mListeners) { 878 for (Result l : mListeners) { 879 l.updateMailboxCallback(me, account.mId, folder.mId, 0, 0); 880 } 881 } 882 } 883 884 @Override 885 public void checkMailStarted(Context context, long accountId, long tag) { 886 synchronized (mListeners) { 887 for (Result l : mListeners) { 888 l.serviceCheckMailCallback(null, accountId, -1, 0, tag); 889 } 890 } 891 } 892 893 @Override 894 public void checkMailFinished(Context context, long accountId, long folderId, long tag) { 895 synchronized (mListeners) { 896 for (Result l : mListeners) { 897 l.serviceCheckMailCallback(null, accountId, folderId, 100, tag); 898 } 899 } 900 } 901 902 @Override 903 public void loadMessageForViewStarted(long messageId) { 904 synchronized (mListeners) { 905 for (Result listener : mListeners) { 906 listener.loadMessageForViewCallback(null, messageId, 0); 907 } 908 } 909 } 910 911 @Override 912 public void loadMessageForViewFinished(long messageId) { 913 synchronized (mListeners) { 914 for (Result listener : mListeners) { 915 listener.loadMessageForViewCallback(null, messageId, 100); 916 } 917 } 918 } 919 920 @Override 921 public void loadMessageForViewFailed(long messageId, String message) { 922 synchronized (mListeners) { 923 for (Result listener : mListeners) { 924 listener.loadMessageForViewCallback(new MessagingException(message), 925 messageId, 0); 926 } 927 } 928 } 929 930 @Override 931 public void loadAttachmentStarted(long accountId, long messageId, long attachmentId, 932 boolean requiresDownload) { 933 synchronized (mListeners) { 934 for (Result listener : mListeners) { 935 listener.loadAttachmentCallback(null, messageId, attachmentId, 0); 936 } 937 } 938 } 939 940 @Override 941 public void loadAttachmentFinished(long accountId, long messageId, long attachmentId) { 942 synchronized (mListeners) { 943 for (Result listener : mListeners) { 944 listener.loadAttachmentCallback(null, messageId, attachmentId, 100); 945 } 946 } 947 } 948 949 @Override 950 public void loadAttachmentFailed(long accountId, long messageId, long attachmentId, 951 String reason) { 952 synchronized (mListeners) { 953 for (Result listener : mListeners) { 954 listener.loadAttachmentCallback(new MessagingException(reason), 955 messageId, attachmentId, 0); 956 } 957 } 958 } 959 960 @Override 961 synchronized public void sendPendingMessagesStarted(long accountId, long messageId) { 962 synchronized (mListeners) { 963 for (Result listener : mListeners) { 964 listener.sendMailCallback(null, accountId, messageId, 0); 965 } 966 } 967 } 968 969 @Override 970 synchronized public void sendPendingMessagesCompleted(long accountId) { 971 synchronized (mListeners) { 972 for (Result listener : mListeners) { 973 listener.sendMailCallback(null, accountId, -1, 100); 974 } 975 } 976 } 977 978 @Override 979 synchronized public void sendPendingMessagesFailed(long accountId, long messageId, 980 Exception reason) { 981 MessagingException me; 982 if (reason instanceof MessagingException) { 983 me = (MessagingException) reason; 984 } else { 985 me = new MessagingException(reason.toString()); 986 } 987 synchronized (mListeners) { 988 for (Result listener : mListeners) { 989 listener.sendMailCallback(me, accountId, messageId, 0); 990 } 991 } 992 } 993 } 994 995 /** 996 * Service callback for service operations 997 */ 998 private class ServiceCallback extends IEmailServiceCallback.Stub { 999 1000 private final static boolean DEBUG_FAIL_DOWNLOADS = false; // do not check in "true" 1001 1002 public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, 1003 int progress) { 1004 MessagingException result = null; 1005 switch (statusCode) { 1006 case EmailServiceStatus.SUCCESS: 1007 progress = 100; 1008 break; 1009 case EmailServiceStatus.IN_PROGRESS: 1010 if (DEBUG_FAIL_DOWNLOADS && progress > 75) { 1011 result = new MessagingException( 1012 String.valueOf(EmailServiceStatus.CONNECTION_ERROR)); 1013 } 1014 // discard progress reports that look like sentinels 1015 if (progress < 0 || progress >= 100) { 1016 return; 1017 } 1018 break; 1019 default: 1020 result = new MessagingException(String.valueOf(statusCode)); 1021 break; 1022 } 1023 synchronized (mListeners) { 1024 for (Result listener : mListeners) { 1025 listener.loadAttachmentCallback(result, messageId, attachmentId, progress); 1026 } 1027 } 1028 } 1029 1030 public void sendMessageStatus(long messageId, String subject, int statusCode, 1031 int progress) { 1032 // TODO Auto-generated method stub 1033 1034 } 1035 1036 public void syncMailboxListStatus(long accountId, int statusCode, int progress) { 1037 MessagingException result= null; 1038 switch (statusCode) { 1039 case EmailServiceStatus.SUCCESS: 1040 progress = 100; 1041 break; 1042 case EmailServiceStatus.IN_PROGRESS: 1043 // discard progress reports that look like sentinels 1044 if (progress < 0 || progress >= 100) { 1045 return; 1046 } 1047 break; 1048 default: 1049 result = new MessagingException(String.valueOf(statusCode)); 1050 break; 1051 } 1052 synchronized(mListeners) { 1053 for (Result listener : mListeners) { 1054 listener.updateMailboxListCallback(result, accountId, progress); 1055 } 1056 } 1057 } 1058 1059 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) { 1060 MessagingException result= null; 1061 switch (statusCode) { 1062 case EmailServiceStatus.SUCCESS: 1063 progress = 100; 1064 break; 1065 case EmailServiceStatus.IN_PROGRESS: 1066 // discard progress reports that look like sentinels 1067 if (progress < 0 || progress >= 100) { 1068 return; 1069 } 1070 break; 1071 default: 1072 result = new MessagingException(String.valueOf(statusCode)); 1073 break; 1074 } 1075 // TODO where do we get "number of new messages" as well? 1076 // TODO should pass this back instead of looking it up here 1077 // TODO smaller projection 1078 Mailbox mbx = Mailbox.restoreMailboxWithId(mContext, mailboxId); 1079 // The mailbox could have disappeared if the server commanded it 1080 if (mbx == null) return; 1081 long accountId = mbx.mAccountKey; 1082 synchronized(mListeners) { 1083 for (Result listener : mListeners) { 1084 listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0); 1085 } 1086 } 1087 } 1088 } 1089} 1090