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