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