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