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