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 Controller(Context _context)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 */ getInstance(Context _context)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 */ setProviderContext(Context providerContext)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 */ addResultCallback(Result listener)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 */ removeResultCallback(Result listener)119 public void removeResultCallback(Result listener) { 120 synchronized (mListeners) { 121 mListeners.remove(listener); 122 } 123 } 124 isActiveResultCallback(Result listener)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 */ serviceLogging(int debugEnabled)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 */ updateMailboxList(final long accountId, final Result callback)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 */ serviceCheckMail(final long accountId, final long mailboxId, final long tag, final Result callback)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 */ updateMailbox(final long accountId, final long mailboxId, final Result callback)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 */ loadMessageForView(final long messageId, final Result callback)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 */ saveToMailbox(final EmailContent.Message message, final int mailboxType)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 */ findOrCreateMailboxOfType(long accountId, int mailboxType)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 */ getMailboxServerName(int mailboxType)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 */ createMailbox(long accountId, int mailboxType)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 */ sendMessage(long messageId, long accountId)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 */ sendPendingMessages(long accountId, Result callback)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 */ resetVisibleLimits()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 */ loadMoreMessages(final long mailboxId, Result callback)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 */ lookupAccountForMessage(long messageId)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 */ deleteAttachment(long attachmentId)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 */ deleteMessage(long messageId, long accountId)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 */ setMessageRead(final long messageId, boolean isRead)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 */ setMessageFavorite(final long messageId, boolean isFavorite)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 */ sendMeetingResponse(final long messageId, final int response, final Result callback)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 */ loadAttachment(final long attachmentId, final long messageId, final long mailboxId, final long accountId, final Result callback)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 if (attachInfo == null) { 750 return; 751 } 752 753 if (saveToFile.exists() && attachInfo.mContentUri != null) { 754 // The attachment has already been downloaded, so we will just "pretend" to download it 755 synchronized (mListeners) { 756 for (Result listener : mListeners) { 757 listener.loadAttachmentCallback(null, messageId, attachmentId, 0); 758 } 759 for (Result listener : mListeners) { 760 listener.loadAttachmentCallback(null, messageId, attachmentId, 100); 761 } 762 } 763 return; 764 } 765 766 // Split here for target type (Service or MessagingController) 767 IEmailService service = getServiceForMessage(messageId); 768 if (service != null) { 769 // Service implementation 770 try { 771 service.loadAttachment(attachInfo.mId, saveToFile.getAbsolutePath(), 772 AttachmentProvider.getAttachmentUri(accountId, attachmentId).toString()); 773 } catch (RemoteException e) { 774 // TODO Change exception handling to be consistent with however this method 775 // is implemented for other protocols 776 Log.e("onDownloadAttachment", "RemoteException", e); 777 } 778 } else { 779 // MessagingController implementation 780 new Thread() { 781 @Override 782 public void run() { 783 mLegacyController.loadAttachment(accountId, messageId, mailboxId, attachmentId, 784 mLegacyListener); 785 } 786 }.start(); 787 } 788 } 789 790 /** 791 * For a given message id, return a service proxy if applicable, or null. 792 * 793 * @param messageId the message of interest 794 * @result service proxy, or null if n/a 795 */ getServiceForMessage(long messageId)796 private IEmailService getServiceForMessage(long messageId) { 797 // TODO make this more efficient, caching the account, smaller lookup here, etc. 798 Message message = Message.restoreMessageWithId(mProviderContext, messageId); 799 if (message == null) { 800 return null; 801 } 802 return getServiceForAccount(message.mAccountKey); 803 } 804 805 /** 806 * For a given account id, return a service proxy if applicable, or null. 807 * 808 * TODO this should use a cache because we'll be doing this a lot 809 * 810 * @param accountId the message of interest 811 * @result service proxy, or null if n/a 812 */ getServiceForAccount(long accountId)813 private IEmailService getServiceForAccount(long accountId) { 814 // TODO make this more efficient, caching the account, MUCH smaller lookup here, etc. 815 Account account = EmailContent.Account.restoreAccountWithId(mProviderContext, accountId); 816 if (account == null || isMessagingController(account)) { 817 return null; 818 } else { 819 return ExchangeUtils.getExchangeEmailService(mContext, mServiceCallback); 820 } 821 } 822 823 /** 824 * Simple helper to determine if legacy MessagingController should be used 825 * 826 * TODO this should not require a full account, just an accountId 827 * TODO this should use a cache because we'll be doing this a lot 828 */ isMessagingController(EmailContent.Account account)829 public boolean isMessagingController(EmailContent.Account account) { 830 if (account == null) return false; 831 Store.StoreInfo info = 832 Store.StoreInfo.getStoreInfo(account.getStoreUri(mProviderContext), mContext); 833 // This null happens in testing. 834 if (info == null) { 835 return false; 836 } 837 String scheme = info.mScheme; 838 839 return ("pop3".equals(scheme) || "imap".equals(scheme)); 840 } 841 842 /** 843 * Simple callback for synchronous commands. For many commands, this can be largely ignored 844 * and the result is observed via provider cursors. The callback will *not* necessarily be 845 * made from the UI thread, so you may need further handlers to safely make UI updates. 846 */ 847 public interface Result { 848 /** 849 * Callback for updateMailboxList 850 * 851 * @param result If null, the operation completed without error 852 * @param accountId The account being operated on 853 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 854 */ updateMailboxListCallback(MessagingException result, long accountId, int progress)855 public void updateMailboxListCallback(MessagingException result, long accountId, 856 int progress); 857 858 /** 859 * Callback for updateMailbox. Note: This looks a lot like checkMailCallback, but 860 * it's a separate call used only by UI's, so we can keep things separate. 861 * 862 * @param result If null, the operation completed without error 863 * @param accountId The account being operated on 864 * @param mailboxId The mailbox being operated on 865 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 866 * @param numNewMessages the number of new messages delivered 867 */ updateMailboxCallback(MessagingException result, long accountId, long mailboxId, int progress, int numNewMessages)868 public void updateMailboxCallback(MessagingException result, long accountId, 869 long mailboxId, int progress, int numNewMessages); 870 871 /** 872 * Callback for loadMessageForView 873 * 874 * @param result if null, the attachment completed - if non-null, terminating with failure 875 * @param messageId the message which contains the attachment 876 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 877 */ loadMessageForViewCallback(MessagingException result, long messageId, int progress)878 public void loadMessageForViewCallback(MessagingException result, long messageId, 879 int progress); 880 881 /** 882 * Callback for loadAttachment 883 * 884 * @param result if null, the attachment completed - if non-null, terminating with failure 885 * @param messageId the message which contains the attachment 886 * @param attachmentId the attachment being loaded 887 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 888 */ loadAttachmentCallback(MessagingException result, long messageId, long attachmentId, int progress)889 public void loadAttachmentCallback(MessagingException result, long messageId, 890 long attachmentId, int progress); 891 892 /** 893 * Callback for checkmail. Note: This looks a lot like updateMailboxCallback, but 894 * it's a separate call used only by the automatic checker service, so we can keep 895 * things separate. 896 * 897 * @param result If null, the operation completed without error 898 * @param accountId The account being operated on 899 * @param mailboxId The mailbox being operated on (may be unknown at start) 900 * @param progress 0 for "starting", no updates, 100 for complete 901 * @param tag the same tag that was passed to serviceCheckMail() 902 */ serviceCheckMailCallback(MessagingException result, long accountId, long mailboxId, int progress, long tag)903 public void serviceCheckMailCallback(MessagingException result, long accountId, 904 long mailboxId, int progress, long tag); 905 906 /** 907 * Callback for sending pending messages. This will be called once to start the 908 * group, multiple times for messages, and once to complete the group. 909 * 910 * @param result If null, the operation completed without error 911 * @param accountId The account being operated on 912 * @param messageId The being sent (may be unknown at start) 913 * @param progress 0 for "starting", 100 for complete 914 */ sendMailCallback(MessagingException result, long accountId, long messageId, int progress)915 public void sendMailCallback(MessagingException result, long accountId, 916 long messageId, int progress); 917 } 918 919 /** 920 * Support for receiving callbacks from MessagingController and dealing with UI going 921 * out of scope. 922 */ 923 private class LegacyListener extends MessagingListener { 924 925 @Override listFoldersStarted(long accountId)926 public void listFoldersStarted(long accountId) { 927 synchronized (mListeners) { 928 for (Result l : mListeners) { 929 l.updateMailboxListCallback(null, accountId, 0); 930 } 931 } 932 } 933 934 @Override listFoldersFailed(long accountId, String message)935 public void listFoldersFailed(long accountId, String message) { 936 synchronized (mListeners) { 937 for (Result l : mListeners) { 938 l.updateMailboxListCallback(new MessagingException(message), accountId, 0); 939 } 940 } 941 } 942 943 @Override listFoldersFinished(long accountId)944 public void listFoldersFinished(long accountId) { 945 synchronized (mListeners) { 946 for (Result l : mListeners) { 947 l.updateMailboxListCallback(null, accountId, 100); 948 } 949 } 950 } 951 952 @Override synchronizeMailboxStarted(long accountId, long mailboxId)953 public void synchronizeMailboxStarted(long accountId, long mailboxId) { 954 synchronized (mListeners) { 955 for (Result l : mListeners) { 956 l.updateMailboxCallback(null, accountId, mailboxId, 0, 0); 957 } 958 } 959 } 960 961 @Override synchronizeMailboxFinished(long accountId, long mailboxId, int totalMessagesInMailbox, int numNewMessages)962 public void synchronizeMailboxFinished(long accountId, long mailboxId, 963 int totalMessagesInMailbox, int numNewMessages) { 964 synchronized (mListeners) { 965 for (Result l : mListeners) { 966 l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages); 967 } 968 } 969 } 970 971 @Override synchronizeMailboxFailed(long accountId, long mailboxId, Exception e)972 public void synchronizeMailboxFailed(long accountId, long mailboxId, Exception e) { 973 MessagingException me; 974 if (e instanceof MessagingException) { 975 me = (MessagingException) e; 976 } else { 977 me = new MessagingException(e.toString()); 978 } 979 synchronized (mListeners) { 980 for (Result l : mListeners) { 981 l.updateMailboxCallback(me, accountId, mailboxId, 0, 0); 982 } 983 } 984 } 985 986 @Override checkMailStarted(Context context, long accountId, long tag)987 public void checkMailStarted(Context context, long accountId, long tag) { 988 synchronized (mListeners) { 989 for (Result l : mListeners) { 990 l.serviceCheckMailCallback(null, accountId, -1, 0, tag); 991 } 992 } 993 } 994 995 @Override checkMailFinished(Context context, long accountId, long folderId, long tag)996 public void checkMailFinished(Context context, long accountId, long folderId, long tag) { 997 synchronized (mListeners) { 998 for (Result l : mListeners) { 999 l.serviceCheckMailCallback(null, accountId, folderId, 100, tag); 1000 } 1001 } 1002 } 1003 1004 @Override loadMessageForViewStarted(long messageId)1005 public void loadMessageForViewStarted(long messageId) { 1006 synchronized (mListeners) { 1007 for (Result listener : mListeners) { 1008 listener.loadMessageForViewCallback(null, messageId, 0); 1009 } 1010 } 1011 } 1012 1013 @Override loadMessageForViewFinished(long messageId)1014 public void loadMessageForViewFinished(long messageId) { 1015 synchronized (mListeners) { 1016 for (Result listener : mListeners) { 1017 listener.loadMessageForViewCallback(null, messageId, 100); 1018 } 1019 } 1020 } 1021 1022 @Override loadMessageForViewFailed(long messageId, String message)1023 public void loadMessageForViewFailed(long messageId, String message) { 1024 synchronized (mListeners) { 1025 for (Result listener : mListeners) { 1026 listener.loadMessageForViewCallback(new MessagingException(message), 1027 messageId, 0); 1028 } 1029 } 1030 } 1031 1032 @Override loadAttachmentStarted(long accountId, long messageId, long attachmentId, boolean requiresDownload)1033 public void loadAttachmentStarted(long accountId, long messageId, long attachmentId, 1034 boolean requiresDownload) { 1035 synchronized (mListeners) { 1036 for (Result listener : mListeners) { 1037 listener.loadAttachmentCallback(null, messageId, attachmentId, 0); 1038 } 1039 } 1040 } 1041 1042 @Override loadAttachmentFinished(long accountId, long messageId, long attachmentId)1043 public void loadAttachmentFinished(long accountId, long messageId, long attachmentId) { 1044 synchronized (mListeners) { 1045 for (Result listener : mListeners) { 1046 listener.loadAttachmentCallback(null, messageId, attachmentId, 100); 1047 } 1048 } 1049 } 1050 1051 @Override loadAttachmentFailed(long accountId, long messageId, long attachmentId, String reason)1052 public void loadAttachmentFailed(long accountId, long messageId, long attachmentId, 1053 String reason) { 1054 synchronized (mListeners) { 1055 for (Result listener : mListeners) { 1056 listener.loadAttachmentCallback(new MessagingException(reason), 1057 messageId, attachmentId, 0); 1058 } 1059 } 1060 } 1061 1062 @Override sendPendingMessagesStarted(long accountId, long messageId)1063 synchronized public void sendPendingMessagesStarted(long accountId, long messageId) { 1064 synchronized (mListeners) { 1065 for (Result listener : mListeners) { 1066 listener.sendMailCallback(null, accountId, messageId, 0); 1067 } 1068 } 1069 } 1070 1071 @Override sendPendingMessagesCompleted(long accountId)1072 synchronized public void sendPendingMessagesCompleted(long accountId) { 1073 synchronized (mListeners) { 1074 for (Result listener : mListeners) { 1075 listener.sendMailCallback(null, accountId, -1, 100); 1076 } 1077 } 1078 } 1079 1080 @Override sendPendingMessagesFailed(long accountId, long messageId, Exception reason)1081 synchronized public void sendPendingMessagesFailed(long accountId, long messageId, 1082 Exception reason) { 1083 MessagingException me; 1084 if (reason instanceof MessagingException) { 1085 me = (MessagingException) reason; 1086 } else { 1087 me = new MessagingException(reason.toString()); 1088 } 1089 synchronized (mListeners) { 1090 for (Result listener : mListeners) { 1091 listener.sendMailCallback(me, accountId, messageId, 0); 1092 } 1093 } 1094 } 1095 } 1096 1097 /** 1098 * Service callback for service operations 1099 */ 1100 private class ServiceCallback extends IEmailServiceCallback.Stub { 1101 1102 private final static boolean DEBUG_FAIL_DOWNLOADS = false; // do not check in "true" 1103 loadAttachmentStatus(long messageId, long attachmentId, int statusCode, int progress)1104 public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, 1105 int progress) { 1106 MessagingException result = mapStatusToException(statusCode); 1107 switch (statusCode) { 1108 case EmailServiceStatus.SUCCESS: 1109 progress = 100; 1110 break; 1111 case EmailServiceStatus.IN_PROGRESS: 1112 if (DEBUG_FAIL_DOWNLOADS && progress > 75) { 1113 result = new MessagingException( 1114 String.valueOf(EmailServiceStatus.CONNECTION_ERROR)); 1115 } 1116 // discard progress reports that look like sentinels 1117 if (progress < 0 || progress >= 100) { 1118 return; 1119 } 1120 break; 1121 } 1122 synchronized (mListeners) { 1123 for (Result listener : mListeners) { 1124 listener.loadAttachmentCallback(result, messageId, attachmentId, progress); 1125 } 1126 } 1127 } 1128 1129 /** 1130 * Note, this is an incomplete implementation of this callback, because we are 1131 * not getting things back from Service in quite the same way as from MessagingController. 1132 * However, this is sufficient for basic "progress=100" notification that message send 1133 * has just completed. 1134 */ sendMessageStatus(long messageId, String subject, int statusCode, int progress)1135 public void sendMessageStatus(long messageId, String subject, int statusCode, 1136 int progress) { 1137 // Log.d(Email.LOG_TAG, "sendMessageStatus: messageId=" + messageId 1138 // + " statusCode=" + statusCode + " progress=" + progress); 1139 // Log.d(Email.LOG_TAG, "sendMessageStatus: subject=" + subject); 1140 long accountId = -1; // This should be in the callback 1141 MessagingException result = mapStatusToException(statusCode); 1142 switch (statusCode) { 1143 case EmailServiceStatus.SUCCESS: 1144 progress = 100; 1145 break; 1146 case EmailServiceStatus.IN_PROGRESS: 1147 // discard progress reports that look like sentinels 1148 if (progress < 0 || progress >= 100) { 1149 return; 1150 } 1151 break; 1152 } 1153 // Log.d(Email.LOG_TAG, "result=" + result + " messageId=" + messageId 1154 // + " progress=" + progress); 1155 synchronized(mListeners) { 1156 for (Result listener : mListeners) { 1157 listener.sendMailCallback(result, accountId, messageId, progress); 1158 } 1159 } 1160 } 1161 syncMailboxListStatus(long accountId, int statusCode, int progress)1162 public void syncMailboxListStatus(long accountId, int statusCode, int progress) { 1163 MessagingException result = mapStatusToException(statusCode); 1164 switch (statusCode) { 1165 case EmailServiceStatus.SUCCESS: 1166 progress = 100; 1167 break; 1168 case EmailServiceStatus.IN_PROGRESS: 1169 // discard progress reports that look like sentinels 1170 if (progress < 0 || progress >= 100) { 1171 return; 1172 } 1173 break; 1174 } 1175 synchronized(mListeners) { 1176 for (Result listener : mListeners) { 1177 listener.updateMailboxListCallback(result, accountId, progress); 1178 } 1179 } 1180 } 1181 syncMailboxStatus(long mailboxId, int statusCode, int progress)1182 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) { 1183 MessagingException result = mapStatusToException(statusCode); 1184 switch (statusCode) { 1185 case EmailServiceStatus.SUCCESS: 1186 progress = 100; 1187 break; 1188 case EmailServiceStatus.IN_PROGRESS: 1189 // discard progress reports that look like sentinels 1190 if (progress < 0 || progress >= 100) { 1191 return; 1192 } 1193 break; 1194 } 1195 // TODO where do we get "number of new messages" as well? 1196 // TODO should pass this back instead of looking it up here 1197 // TODO smaller projection 1198 Mailbox mbx = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); 1199 // The mailbox could have disappeared if the server commanded it 1200 if (mbx == null) return; 1201 long accountId = mbx.mAccountKey; 1202 synchronized(mListeners) { 1203 for (Result listener : mListeners) { 1204 listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0); 1205 } 1206 } 1207 } 1208 mapStatusToException(int statusCode)1209 private MessagingException mapStatusToException(int statusCode) { 1210 switch (statusCode) { 1211 case EmailServiceStatus.SUCCESS: 1212 case EmailServiceStatus.IN_PROGRESS: 1213 return null; 1214 1215 case EmailServiceStatus.LOGIN_FAILED: 1216 return new AuthenticationFailedException(""); 1217 1218 case EmailServiceStatus.CONNECTION_ERROR: 1219 return new MessagingException(MessagingException.IOERROR); 1220 1221 case EmailServiceStatus.SECURITY_FAILURE: 1222 return new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED); 1223 1224 case EmailServiceStatus.MESSAGE_NOT_FOUND: 1225 case EmailServiceStatus.ATTACHMENT_NOT_FOUND: 1226 case EmailServiceStatus.FOLDER_NOT_DELETED: 1227 case EmailServiceStatus.FOLDER_NOT_RENAMED: 1228 case EmailServiceStatus.FOLDER_NOT_CREATED: 1229 case EmailServiceStatus.REMOTE_EXCEPTION: 1230 // TODO: define exception code(s) & UI string(s) for server-side errors 1231 default: 1232 return new MessagingException(String.valueOf(statusCode)); 1233 } 1234 } 1235 } 1236 } 1237