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