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