Controller.java revision b81a31b29b22b1b11e8ad636638d2b8213e9f199
1/* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.email; 18 19import android.app.Service; 20import android.content.ContentResolver; 21import android.content.ContentUris; 22import android.content.ContentValues; 23import android.content.Context; 24import android.content.Intent; 25import android.database.Cursor; 26import android.net.Uri; 27import android.os.Bundle; 28import android.os.IBinder; 29import android.os.RemoteCallbackList; 30import android.os.RemoteException; 31import android.util.Log; 32 33import com.android.email.mail.store.Pop3Store.Pop3Message; 34import com.android.email.provider.AccountBackupRestore; 35import com.android.email.service.EmailServiceUtils; 36import com.android.emailcommon.Api; 37import com.android.emailcommon.Logging; 38import com.android.emailcommon.mail.AuthenticationFailedException; 39import com.android.emailcommon.mail.Folder.MessageRetrievalListener; 40import com.android.emailcommon.mail.MessagingException; 41import com.android.emailcommon.provider.Account; 42import com.android.emailcommon.provider.EmailContent; 43import com.android.emailcommon.provider.EmailContent.Attachment; 44import com.android.emailcommon.provider.EmailContent.Body; 45import com.android.emailcommon.provider.EmailContent.MailboxColumns; 46import com.android.emailcommon.provider.EmailContent.Message; 47import com.android.emailcommon.provider.EmailContent.MessageColumns; 48import com.android.emailcommon.provider.HostAuth; 49import com.android.emailcommon.provider.Mailbox; 50import com.android.emailcommon.service.EmailServiceStatus; 51import com.android.emailcommon.service.IEmailService; 52import com.android.emailcommon.service.IEmailServiceCallback; 53import com.android.emailcommon.service.SearchParams; 54import com.android.emailcommon.utility.AttachmentUtilities; 55import com.android.emailcommon.utility.EmailAsyncTask; 56import com.android.emailcommon.utility.Utility; 57import com.google.common.annotations.VisibleForTesting; 58 59import java.io.FileNotFoundException; 60import java.io.IOException; 61import java.io.InputStream; 62import java.util.ArrayList; 63import java.util.Collection; 64import java.util.HashMap; 65import java.util.HashSet; 66import java.util.concurrent.ConcurrentHashMap; 67 68/** 69 * New central controller/dispatcher for Email activities that may require remote operations. 70 * Handles disambiguating between legacy MessagingController operations and newer provider/sync 71 * based code. We implement Service to allow loadAttachment calls to be sent in a consistent manner 72 * to IMAP, POP3, and EAS by AttachmentDownloadService 73 */ 74public class Controller { 75 private static final String TAG = "Controller"; 76 private static Controller sInstance; 77 private final Context mContext; 78 private Context mProviderContext; 79 private final MessagingController mLegacyController; 80 private final LegacyListener mLegacyListener = new LegacyListener(); 81 private final ServiceCallback mServiceCallback = new ServiceCallback(); 82 private final HashSet<Result> mListeners = new HashSet<Result>(); 83 /*package*/ final ConcurrentHashMap<Long, Boolean> mLegacyControllerMap = 84 new ConcurrentHashMap<Long, Boolean>(); 85 86 // Note that 0 is a syntactically valid account key; however there can never be an account 87 // with id = 0, so attempts to restore the account will return null. Null values are 88 // handled properly within the code, so this won't cause any issues. 89 private static final long GLOBAL_MAILBOX_ACCOUNT_KEY = 0; 90 /*package*/ static final String ATTACHMENT_MAILBOX_SERVER_ID = "__attachment_mailbox__"; 91 /*package*/ static final String ATTACHMENT_MESSAGE_UID_PREFIX = "__attachment_message__"; 92 /*package*/ static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__"; 93 private static final String WHERE_TYPE_ATTACHMENT = 94 MailboxColumns.TYPE + "=" + Mailbox.TYPE_ATTACHMENT; 95 private static final String WHERE_MAILBOX_KEY = MessageColumns.MAILBOX_KEY + "=?"; 96 97 private static final String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] { 98 EmailContent.RECORD_ID, 99 EmailContent.MessageColumns.ACCOUNT_KEY 100 }; 101 private static final int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1; 102 103 private static final String[] BODY_SOURCE_KEY_PROJECTION = 104 new String[] {Body.SOURCE_MESSAGE_KEY}; 105 private static final int BODY_SOURCE_KEY_COLUMN = 0; 106 private static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?"; 107 108 private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?"; 109 private static final String MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION = 110 MAILBOXES_FOR_ACCOUNT_SELECTION + " AND " + MailboxColumns.TYPE + "!=" + 111 Mailbox.TYPE_EAS_ACCOUNT_MAILBOX; 112 private static final String MESSAGES_FOR_ACCOUNT_SELECTION = MessageColumns.ACCOUNT_KEY + "=?"; 113 114 // Service callbacks as set up via setCallback 115 private static RemoteCallbackList<IEmailServiceCallback> sCallbackList = 116 new RemoteCallbackList<IEmailServiceCallback>(); 117 118 protected Controller(Context _context) { 119 mContext = _context.getApplicationContext(); 120 mProviderContext = _context; 121 mLegacyController = MessagingController.getInstance(mProviderContext, this); 122 mLegacyController.addListener(mLegacyListener); 123 } 124 125 /** 126 * Cleanup for test. Mustn't be called for the regular {@link Controller}, as it's a 127 * singleton and lives till the process finishes. 128 * 129 * <p>However, this method MUST be called for mock instances. 130 */ 131 public void cleanupForTest() { 132 mLegacyController.removeListener(mLegacyListener); 133 } 134 135 /** 136 * Gets or creates the singleton instance of Controller. 137 */ 138 public synchronized static Controller getInstance(Context _context) { 139 if (sInstance == null) { 140 sInstance = new Controller(_context); 141 } 142 return sInstance; 143 } 144 145 /** 146 * Inject a mock controller. Used only for testing. Affects future calls to getInstance(). 147 * 148 * Tests that use this method MUST clean it up by calling this method again with null. 149 */ 150 public synchronized static void injectMockControllerForTest(Controller mockController) { 151 sInstance = mockController; 152 } 153 154 /** 155 * For testing only: Inject a different context for provider access. This will be 156 * used internally for access the underlying provider (e.g. getContentResolver().query()). 157 * @param providerContext the provider context to be used by this instance 158 */ 159 public void setProviderContext(Context providerContext) { 160 mProviderContext = providerContext; 161 } 162 163 /** 164 * Any UI code that wishes for callback results (on async ops) should register their callback 165 * here (typically from onResume()). Unregistered callbacks will never be called, to prevent 166 * problems when the command completes and the activity has already paused or finished. 167 * @param listener The callback that may be used in action methods 168 */ 169 public void addResultCallback(Result listener) { 170 synchronized (mListeners) { 171 listener.setRegistered(true); 172 mListeners.add(listener); 173 } 174 } 175 176 /** 177 * Any UI code that no longer wishes for callback results (on async ops) should unregister 178 * their callback here (typically from onPause()). Unregistered callbacks will never be called, 179 * to prevent problems when the command completes and the activity has already paused or 180 * finished. 181 * @param listener The callback that may no longer be used 182 */ 183 public void removeResultCallback(Result listener) { 184 synchronized (mListeners) { 185 listener.setRegistered(false); 186 mListeners.remove(listener); 187 } 188 } 189 190 public Collection<Result> getResultCallbacksForTest() { 191 return mListeners; 192 } 193 194 /** 195 * Delete all Messages that live in the attachment mailbox 196 */ 197 public void deleteAttachmentMessages() { 198 // Note: There should only be one attachment mailbox at present 199 ContentResolver resolver = mProviderContext.getContentResolver(); 200 Cursor c = null; 201 try { 202 c = resolver.query(Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION, 203 WHERE_TYPE_ATTACHMENT, null, null); 204 while (c.moveToNext()) { 205 long mailboxId = c.getLong(EmailContent.ID_PROJECTION_COLUMN); 206 // Must delete attachments BEFORE messages 207 AttachmentUtilities.deleteAllMailboxAttachmentFiles(mProviderContext, 0, 208 mailboxId); 209 resolver.delete(Message.CONTENT_URI, WHERE_MAILBOX_KEY, 210 new String[] {Long.toString(mailboxId)}); 211 } 212 } finally { 213 if (c != null) { 214 c.close(); 215 } 216 } 217 } 218 219 /** 220 * Get a mailbox based on a sqlite WHERE clause 221 */ 222 private Mailbox getGlobalMailboxWhere(String where) { 223 Cursor c = mProviderContext.getContentResolver().query(Mailbox.CONTENT_URI, 224 Mailbox.CONTENT_PROJECTION, where, null, null); 225 try { 226 if (c.moveToFirst()) { 227 Mailbox m = new Mailbox(); 228 m.restore(c); 229 return m; 230 } 231 } finally { 232 c.close(); 233 } 234 return null; 235 } 236 237 /** 238 * Returns the attachment mailbox (where we store eml attachment Emails), creating one 239 * if necessary 240 * @return the global attachment mailbox 241 */ 242 public Mailbox getAttachmentMailbox() { 243 Mailbox m = getGlobalMailboxWhere(WHERE_TYPE_ATTACHMENT); 244 if (m == null) { 245 m = new Mailbox(); 246 m.mAccountKey = GLOBAL_MAILBOX_ACCOUNT_KEY; 247 m.mServerId = ATTACHMENT_MAILBOX_SERVER_ID; 248 m.mFlagVisible = false; 249 m.mDisplayName = ATTACHMENT_MAILBOX_SERVER_ID; 250 m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER; 251 m.mType = Mailbox.TYPE_ATTACHMENT; 252 m.save(mProviderContext); 253 } 254 return m; 255 } 256 257 /** 258 * Returns the search mailbox for the specified account, creating one if necessary 259 * @return the search mailbox for the passed in account 260 */ 261 public Mailbox getSearchMailbox(long accountId) { 262 Mailbox m = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_SEARCH); 263 if (m == null) { 264 m = new Mailbox(); 265 m.mAccountKey = accountId; 266 m.mServerId = SEARCH_MAILBOX_SERVER_ID; 267 m.mFlagVisible = false; 268 m.mDisplayName = SEARCH_MAILBOX_SERVER_ID; 269 m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER; 270 m.mType = Mailbox.TYPE_SEARCH; 271 m.mFlags = Mailbox.FLAG_HOLDS_MAIL; 272 m.mParentKey = Mailbox.NO_MAILBOX; 273 m.save(mProviderContext); 274 } 275 return m; 276 } 277 278 /** 279 * Create a Message from the Uri and store it in the attachment mailbox 280 * @param uri the uri containing message content 281 * @return the Message or null 282 */ 283 public Message loadMessageFromUri(Uri uri) { 284 Mailbox mailbox = getAttachmentMailbox(); 285 if (mailbox == null) return null; 286 try { 287 InputStream is = mProviderContext.getContentResolver().openInputStream(uri); 288 try { 289 // First, create a Pop3Message from the attachment and then parse it 290 Pop3Message pop3Message = new Pop3Message( 291 ATTACHMENT_MESSAGE_UID_PREFIX + System.currentTimeMillis(), null); 292 pop3Message.parse(is); 293 // Now, pull out the header fields 294 Message msg = new Message(); 295 LegacyConversions.updateMessageFields(msg, pop3Message, 0, mailbox.mId); 296 // Commit the message to the local store 297 msg.save(mProviderContext); 298 // Setup the rest of the message and mark it completely loaded 299 mLegacyController.copyOneMessageToProvider(pop3Message, msg, 300 Message.FLAG_LOADED_COMPLETE, mProviderContext); 301 // Restore the complete message and return it 302 return Message.restoreMessageWithId(mProviderContext, msg.mId); 303 } catch (MessagingException e) { 304 } catch (IOException e) { 305 } 306 } catch (FileNotFoundException e) { 307 } 308 return null; 309 } 310 311 /** 312 * Set logging flags for external sync services 313 * 314 * Generally this should be called by anybody who changes Email.DEBUG 315 */ 316 public void serviceLogging(int debugFlags) { 317 IEmailService service = EmailServiceUtils.getExchangeService(mContext, mServiceCallback); 318 try { 319 service.setLogging(debugFlags); 320 } catch (RemoteException e) { 321 // TODO Change exception handling to be consistent with however this method 322 // is implemented for other protocols 323 Log.d("setLogging", "RemoteException" + e); 324 } 325 } 326 327 /** 328 * Request a remote update of mailboxes for an account. 329 */ 330 public void updateMailboxList(final long accountId) { 331 Utility.runAsync(new Runnable() { 332 @Override 333 public void run() { 334 final IEmailService service = getServiceForAccount(accountId); 335 if (service != null) { 336 // Service implementation 337 try { 338 service.updateFolderList(accountId); 339 } catch (RemoteException e) { 340 // TODO Change exception handling to be consistent with however this method 341 // is implemented for other protocols 342 Log.d("updateMailboxList", "RemoteException" + e); 343 } 344 } else { 345 // MessagingController implementation 346 mLegacyController.listFolders(accountId, mLegacyListener); 347 } 348 } 349 }); 350 } 351 352 /** 353 * Request a remote update of a mailbox. For use by the timed service. 354 * 355 * Functionally this is quite similar to updateMailbox(), but it's a separate API and 356 * separate callback in order to keep UI callbacks from affecting the service loop. 357 */ 358 public void serviceCheckMail(final long accountId, final long mailboxId, final long tag) { 359 IEmailService service = getServiceForAccount(accountId); 360 if (service != null) { 361 // Service implementation 362// try { 363 // TODO this isn't quite going to work, because we're going to get the 364 // generic (UI) callbacks and not the ones we need to restart the ol' service. 365 // service.startSync(mailboxId, tag); 366 mLegacyListener.checkMailFinished(mContext, accountId, mailboxId, tag); 367// } catch (RemoteException e) { 368 // TODO Change exception handling to be consistent with however this method 369 // is implemented for other protocols 370// Log.d("updateMailbox", "RemoteException" + e); 371// } 372 } else { 373 // MessagingController implementation 374 Utility.runAsync(new Runnable() { 375 public void run() { 376 mLegacyController.checkMail(accountId, tag, mLegacyListener); 377 } 378 }); 379 } 380 } 381 382 /** 383 * Request a remote update of a mailbox. 384 * 385 * The contract here should be to try and update the headers ASAP, in order to populate 386 * a simple message list. We should also at this point queue up a background task of 387 * downloading some/all of the messages in this mailbox, but that should be interruptable. 388 */ 389 public void updateMailbox(final long accountId, final long mailboxId, boolean userRequest) { 390 391 IEmailService service = getServiceForAccount(accountId); 392 if (service != null) { 393 try { 394 service.startSync(mailboxId, userRequest); 395 } catch (RemoteException e) { 396 // TODO Change exception handling to be consistent with however this method 397 // is implemented for other protocols 398 Log.d("updateMailbox", "RemoteException" + e); 399 } 400 } else { 401 // MessagingController implementation 402 Utility.runAsync(new Runnable() { 403 public void run() { 404 // TODO shouldn't be passing fully-build accounts & mailboxes into APIs 405 Account account = 406 Account.restoreAccountWithId(mProviderContext, accountId); 407 Mailbox mailbox = 408 Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); 409 if (account == null || mailbox == null || 410 mailbox.mType == Mailbox.TYPE_SEARCH) { 411 return; 412 } 413 mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener); 414 } 415 }); 416 } 417 } 418 419 /** 420 * Request that any final work necessary be done, to load a message. 421 * 422 * Note, this assumes that the caller has already checked message.mFlagLoaded and that 423 * additional work is needed. There is no optimization here for a message which is already 424 * loaded. 425 * 426 * @param messageId the message to load 427 * @param callback the Controller callback by which results will be reported 428 */ 429 public void loadMessageForView(final long messageId) { 430 431 // Split here for target type (Service or MessagingController) 432 IEmailService service = getServiceForMessage(messageId); 433 if (service != null) { 434 // There is no service implementation, so we'll just jam the value, log the error, 435 // and get out of here. 436 Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId); 437 ContentValues cv = new ContentValues(); 438 cv.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE); 439 mProviderContext.getContentResolver().update(uri, cv, null, null); 440 Log.d(Logging.LOG_TAG, "Unexpected loadMessageForView() for service-based message."); 441 final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 442 synchronized (mListeners) { 443 for (Result listener : mListeners) { 444 listener.loadMessageForViewCallback(null, accountId, messageId, 100); 445 } 446 } 447 } else { 448 // MessagingController implementation 449 Utility.runAsync(new Runnable() { 450 public void run() { 451 mLegacyController.loadMessageForView(messageId, mLegacyListener); 452 } 453 }); 454 } 455 } 456 457 458 /** 459 * Saves the message to a mailbox of given type. 460 * This is a synchronous operation taking place in the same thread as the caller. 461 * Upon return the message.mId is set. 462 * @param message the message (must have the mAccountId set). 463 * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS). 464 */ 465 public void saveToMailbox(final EmailContent.Message message, final int mailboxType) { 466 long accountId = message.mAccountKey; 467 long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType); 468 message.mMailboxKey = mailboxId; 469 message.save(mProviderContext); 470 } 471 472 /** 473 * Look for a specific mailbox, creating it if necessary, and return the mailbox id. 474 * This is a blocking operation and should not be called from the UI thread. 475 * 476 * Synchronized so multiple threads can call it (and not risk creating duplicate boxes). 477 * 478 * @param accountId the account id 479 * @param mailboxType the mailbox type (e.g. EmailContent.Mailbox.TYPE_TRASH) 480 * @return the id of the mailbox. The mailbox is created if not existing. 481 * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative. 482 * Does not validate the input in other ways (e.g. does not verify the existence of account). 483 */ 484 public synchronized long findOrCreateMailboxOfType(long accountId, int mailboxType) { 485 if (accountId < 0 || mailboxType < 0) { 486 return Mailbox.NO_MAILBOX; 487 } 488 long mailboxId = 489 Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType); 490 return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId; 491 } 492 493 /** 494 * Returns the server-side name for a specific mailbox. 495 * 496 * @param mailboxType the mailbox type 497 * @return the resource string corresponding to the mailbox type, empty if not found. 498 */ 499 /* package */ String getMailboxServerName(int mailboxType) { 500 int resId = -1; 501 switch (mailboxType) { 502 case Mailbox.TYPE_INBOX: 503 resId = R.string.mailbox_name_server_inbox; 504 break; 505 case Mailbox.TYPE_OUTBOX: 506 resId = R.string.mailbox_name_server_outbox; 507 break; 508 case Mailbox.TYPE_DRAFTS: 509 resId = R.string.mailbox_name_server_drafts; 510 break; 511 case Mailbox.TYPE_TRASH: 512 resId = R.string.mailbox_name_server_trash; 513 break; 514 case Mailbox.TYPE_SENT: 515 resId = R.string.mailbox_name_server_sent; 516 break; 517 case Mailbox.TYPE_JUNK: 518 resId = R.string.mailbox_name_server_junk; 519 break; 520 } 521 return resId != -1 ? mContext.getString(resId) : ""; 522 } 523 524 /** 525 * Create a mailbox given the account and mailboxType. 526 * TODO: Does this need to be signaled explicitly to the sync engines? 527 */ 528 @VisibleForTesting 529 long createMailbox(long accountId, int mailboxType) { 530 if (accountId < 0 || mailboxType < 0) { 531 String mes = "Invalid arguments " + accountId + ' ' + mailboxType; 532 Log.e(Logging.LOG_TAG, mes); 533 throw new RuntimeException(mes); 534 } 535 Mailbox box = new Mailbox(); 536 box.mAccountKey = accountId; 537 box.mType = mailboxType; 538 box.mSyncInterval = Account.CHECK_INTERVAL_NEVER; 539 box.mFlagVisible = true; 540 box.mServerId = box.mDisplayName = getMailboxServerName(mailboxType); 541 // All system mailboxes are off the top-level & can hold mail 542 if (mailboxType != Mailbox.TYPE_MAIL) { 543 box.mParentKey = Mailbox.NO_MAILBOX; 544 box.mFlags = Mailbox.FLAG_HOLDS_MAIL; 545 } 546 box.save(mProviderContext); 547 return box.mId; 548 } 549 550 /** 551 * Send a message: 552 * - move the message to Outbox (the message is assumed to be in Drafts). 553 * - EAS service will take it from there 554 * - mark reply/forward state in source message (if any) 555 * - trigger send for POP/IMAP 556 * @param message the fully populated Message (usually retrieved from the Draft box). Note that 557 * all transient fields (e.g. Body related fields) are also expected to be fully loaded 558 */ 559 public void sendMessage(Message message) { 560 ContentResolver resolver = mProviderContext.getContentResolver(); 561 long accountId = message.mAccountKey; 562 long messageId = message.mId; 563 if (accountId == Account.NO_ACCOUNT) { 564 accountId = lookupAccountForMessage(messageId); 565 } 566 if (accountId == Account.NO_ACCOUNT) { 567 // probably the message was not found 568 if (Logging.LOGD) { 569 Email.log("no account found for message " + messageId); 570 } 571 return; 572 } 573 574 // Move to Outbox 575 long outboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_OUTBOX); 576 ContentValues cv = new ContentValues(); 577 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, outboxId); 578 579 // does this need to be SYNCED_CONTENT_URI instead? 580 Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId); 581 resolver.update(uri, cv, null, null); 582 583 // If this is a reply/forward, indicate it as such on the source. 584 long sourceKey = message.mSourceKey; 585 if (sourceKey != Message.NO_MESSAGE) { 586 boolean isReply = (message.mFlags & Message.FLAG_TYPE_REPLY) != 0; 587 int flagUpdate = isReply ? Message.FLAG_REPLIED_TO : Message.FLAG_FORWARDED; 588 setMessageAnsweredOrForwarded(sourceKey, flagUpdate); 589 } 590 591 sendPendingMessages(accountId); 592 } 593 594 private void sendPendingMessagesSmtp(long accountId) { 595 // for IMAP & POP only, (attempt to) send the message now 596 final Account account = 597 Account.restoreAccountWithId(mProviderContext, accountId); 598 if (account == null) { 599 return; 600 } 601 final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT); 602 Utility.runAsync(new Runnable() { 603 public void run() { 604 mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener); 605 } 606 }); 607 } 608 609 /** 610 * Try to send all pending messages for a given account 611 * 612 * @param accountId the account for which to send messages 613 */ 614 public void sendPendingMessages(long accountId) { 615 // 1. make sure we even have an outbox, exit early if not 616 final long outboxId = 617 Mailbox.findMailboxOfType(mProviderContext, accountId, Mailbox.TYPE_OUTBOX); 618 if (outboxId == Mailbox.NO_MAILBOX) { 619 return; 620 } 621 622 // 2. dispatch as necessary 623 IEmailService service = getServiceForAccount(accountId); 624 if (service != null) { 625 // Service implementation 626 try { 627 service.startSync(outboxId, false); 628 } catch (RemoteException e) { 629 // TODO Change exception handling to be consistent with however this method 630 // is implemented for other protocols 631 Log.d("updateMailbox", "RemoteException" + e); 632 } 633 } else { 634 // MessagingController implementation 635 sendPendingMessagesSmtp(accountId); 636 } 637 } 638 639 /** 640 * Reset visible limits for all accounts. 641 * For each account: 642 * look up limit 643 * write limit into all mailboxes for that account 644 */ 645 public void resetVisibleLimits() { 646 Utility.runAsync(new Runnable() { 647 public void run() { 648 ContentResolver resolver = mProviderContext.getContentResolver(); 649 Cursor c = null; 650 try { 651 c = resolver.query( 652 Account.CONTENT_URI, 653 Account.ID_PROJECTION, 654 null, null, null); 655 while (c.moveToNext()) { 656 long accountId = c.getLong(Account.ID_PROJECTION_COLUMN); 657 String protocol = Account.getProtocol(mProviderContext, accountId); 658 if (!HostAuth.SCHEME_EAS.equals(protocol)) { 659 ContentValues cv = new ContentValues(); 660 cv.put(MailboxColumns.VISIBLE_LIMIT, Email.VISIBLE_LIMIT_DEFAULT); 661 resolver.update(Mailbox.CONTENT_URI, cv, 662 MailboxColumns.ACCOUNT_KEY + "=?", 663 new String[] { Long.toString(accountId) }); 664 } 665 } 666 } finally { 667 if (c != null) { 668 c.close(); 669 } 670 } 671 } 672 }); 673 } 674 675 /** 676 * Increase the load count for a given mailbox, and trigger a refresh. Applies only to 677 * IMAP and POP mailboxes, with the exception of the EAS search mailbox. 678 * 679 * @param mailboxId the mailbox 680 */ 681 public void loadMoreMessages(final long mailboxId) { 682 EmailAsyncTask.runAsyncParallel(new Runnable() { 683 public void run() { 684 Mailbox mailbox = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); 685 if (mailbox == null) { 686 return; 687 } 688 if (mailbox.mType == Mailbox.TYPE_SEARCH) { 689 try { 690 searchMore(mailbox.mAccountKey); 691 } catch (MessagingException e) { 692 // Nothing to be done 693 } 694 return; 695 } 696 Account account = Account.restoreAccountWithId(mProviderContext, 697 mailbox.mAccountKey); 698 if (account == null) { 699 return; 700 } 701 // Use provider math to increment the field 702 ContentValues cv = new ContentValues();; 703 cv.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT); 704 cv.put(EmailContent.ADD_COLUMN_NAME, Email.VISIBLE_LIMIT_INCREMENT); 705 Uri uri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, mailboxId); 706 mProviderContext.getContentResolver().update(uri, cv, null, null); 707 // Trigger a refresh using the new, longer limit 708 mailbox.mVisibleLimit += Email.VISIBLE_LIMIT_INCREMENT; 709 mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener); 710 } 711 }); 712 } 713 714 /** 715 * @param messageId the id of message 716 * @return the accountId corresponding to the given messageId, or -1 if not found. 717 */ 718 private long lookupAccountForMessage(long messageId) { 719 ContentResolver resolver = mProviderContext.getContentResolver(); 720 Cursor c = resolver.query(EmailContent.Message.CONTENT_URI, 721 MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?", 722 new String[] { Long.toString(messageId) }, null); 723 try { 724 return c.moveToFirst() 725 ? c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID) 726 : -1; 727 } finally { 728 c.close(); 729 } 730 } 731 732 /** 733 * Delete a single attachment entry from the DB given its id. 734 * Does not delete any eventual associated files. 735 */ 736 public void deleteAttachment(long attachmentId) { 737 ContentResolver resolver = mProviderContext.getContentResolver(); 738 Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId); 739 resolver.delete(uri, null, null); 740 } 741 742 /** 743 * Async version of {@link #deleteMessageSync}. 744 */ 745 public void deleteMessage(final long messageId) { 746 EmailAsyncTask.runAsyncParallel(new Runnable() { 747 public void run() { 748 deleteMessageSync(messageId); 749 } 750 }); 751 } 752 753 /** 754 * Batch & async version of {@link #deleteMessageSync}. 755 */ 756 public void deleteMessages(final long[] messageIds) { 757 if (messageIds == null || messageIds.length == 0) { 758 throw new IllegalArgumentException(); 759 } 760 EmailAsyncTask.runAsyncParallel(new Runnable() { 761 public void run() { 762 for (long messageId: messageIds) { 763 deleteMessageSync(messageId); 764 } 765 } 766 }); 767 } 768 769 /** 770 * Delete a single message by moving it to the trash, or really delete it if it's already in 771 * trash or a draft message. 772 * 773 * This function has no callback, no result reporting, because the desired outcome 774 * is reflected entirely by changes to one or more cursors. 775 * 776 * @param messageId The id of the message to "delete". 777 */ 778 /* package */ void deleteMessageSync(long messageId) { 779 // 1. Get the message's account 780 Account account = Account.getAccountForMessageId(mProviderContext, messageId); 781 782 if (account == null) return; 783 784 // 2. Confirm that there is a trash mailbox available. If not, create one 785 long trashMailboxId = findOrCreateMailboxOfType(account.mId, Mailbox.TYPE_TRASH); 786 787 // 3. Get the message's original mailbox 788 Mailbox mailbox = Mailbox.getMailboxForMessageId(mProviderContext, messageId); 789 790 if (mailbox == null) return; 791 792 // 4. Drop non-essential data for the message (e.g. attachment files) 793 AttachmentUtilities.deleteAllAttachmentFiles(mProviderContext, account.mId, 794 messageId); 795 796 Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, 797 messageId); 798 ContentResolver resolver = mProviderContext.getContentResolver(); 799 800 // 5. Perform "delete" as appropriate 801 if ((mailbox.mId == trashMailboxId) || (mailbox.mType == Mailbox.TYPE_DRAFTS)) { 802 // 5a. Really delete it 803 resolver.delete(uri, null, null); 804 } else { 805 // 5b. Move to trash 806 ContentValues cv = new ContentValues(); 807 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId); 808 resolver.update(uri, cv, null, null); 809 } 810 811 if (isMessagingController(account)) { 812 mLegacyController.processPendingActions(account.mId); 813 } 814 } 815 816 /** 817 * Moves messages to a new mailbox. 818 * 819 * This function has no callback, no result reporting, because the desired outcome 820 * is reflected entirely by changes to one or more cursors. 821 * 822 * Note this method assumes all of the given message and mailbox IDs belong to the same 823 * account. 824 * 825 * @param messageIds IDs of the messages that are to be moved 826 * @param newMailboxId ID of the new mailbox that the messages will be moved to 827 * @return an asynchronous task that executes the move (for testing only) 828 */ 829 public EmailAsyncTask<Void, Void, Void> moveMessages(final long[] messageIds, 830 final long newMailboxId) { 831 if (messageIds == null || messageIds.length == 0) { 832 throw new IllegalArgumentException(); 833 } 834 return EmailAsyncTask.runAsyncParallel(new Runnable() { 835 public void run() { 836 Account account = Account.getAccountForMessageId(mProviderContext, messageIds[0]); 837 if (account != null) { 838 ContentValues cv = new ContentValues(); 839 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, newMailboxId); 840 ContentResolver resolver = mProviderContext.getContentResolver(); 841 for (long messageId : messageIds) { 842 Uri uri = ContentUris.withAppendedId( 843 EmailContent.Message.SYNCED_CONTENT_URI, messageId); 844 resolver.update(uri, cv, null, null); 845 } 846 if (isMessagingController(account)) { 847 mLegacyController.processPendingActions(account.mId); 848 } 849 } 850 } 851 }); 852 } 853 854 /** 855 * Set/clear the unread status of a message 856 * 857 * @param messageId the message to update 858 * @param isRead the new value for the isRead flag 859 */ 860 public void setMessageReadSync(long messageId, boolean isRead) { 861 setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_READ, isRead); 862 } 863 864 /** 865 * Set/clear the unread status of a message from UI thread 866 * 867 * @param messageId the message to update 868 * @param isRead the new value for the isRead flag 869 * @return the EmailAsyncTask created 870 */ 871 public EmailAsyncTask<Void, Void, Void> setMessageRead(final long messageId, 872 final boolean isRead) { 873 return EmailAsyncTask.runAsyncParallel(new Runnable() { 874 @Override 875 public void run() { 876 setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_READ, isRead); 877 }}); 878 } 879 880 /** 881 * Update a message record and ping MessagingController, if necessary 882 * 883 * @param messageId the message to update 884 * @param cv the ContentValues used in the update 885 */ 886 private void updateMessageSync(long messageId, ContentValues cv) { 887 Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId); 888 mProviderContext.getContentResolver().update(uri, cv, null, null); 889 890 // Service runs automatically, MessagingController needs a kick 891 long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 892 if (accountId == Account.NO_ACCOUNT) return; 893 if (isMessagingController(accountId)) { 894 mLegacyController.processPendingActions(accountId); 895 } 896 } 897 898 /** 899 * Set the answered status of a message 900 * 901 * @param messageId the message to update 902 * @return the AsyncTask that will execute the changes (for testing only) 903 */ 904 public void setMessageAnsweredOrForwarded(final long messageId, 905 final int flag) { 906 EmailAsyncTask.runAsyncParallel(new Runnable() { 907 public void run() { 908 Message msg = Message.restoreMessageWithId(mProviderContext, messageId); 909 if (msg == null) { 910 Log.w(Logging.LOG_TAG, "Unable to find source message for a reply/forward"); 911 return; 912 } 913 ContentValues cv = new ContentValues(); 914 cv.put(MessageColumns.FLAGS, msg.mFlags | flag); 915 updateMessageSync(messageId, cv); 916 } 917 }); 918 } 919 920 /** 921 * Set/clear the favorite status of a message from UI thread 922 * 923 * @param messageId the message to update 924 * @param isFavorite the new value for the isFavorite flag 925 * @return the EmailAsyncTask created 926 */ 927 public EmailAsyncTask<Void, Void, Void> setMessageFavorite(final long messageId, 928 final boolean isFavorite) { 929 return EmailAsyncTask.runAsyncParallel(new Runnable() { 930 @Override 931 public void run() { 932 setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_FAVORITE, 933 isFavorite); 934 }}); 935 } 936 /** 937 * Set/clear the favorite status of a message 938 * 939 * @param messageId the message to update 940 * @param isFavorite the new value for the isFavorite flag 941 */ 942 public void setMessageFavoriteSync(long messageId, boolean isFavorite) { 943 setMessageBooleanSync(messageId, EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite); 944 } 945 946 /** 947 * Set/clear boolean columns of a message 948 * 949 * @param messageId the message to update 950 * @param columnName the column to update 951 * @param columnValue the new value for the column 952 */ 953 private void setMessageBooleanSync(long messageId, String columnName, boolean columnValue) { 954 ContentValues cv = new ContentValues(); 955 cv.put(columnName, columnValue); 956 updateMessageSync(messageId, cv); 957 } 958 959 960 private static final HashMap<Long, SearchParams> sSearchParamsMap = 961 new HashMap<Long, SearchParams>(); 962 963 public void searchMore(long accountId) throws MessagingException { 964 SearchParams params = sSearchParamsMap.get(accountId); 965 if (params == null) return; 966 params.mOffset += params.mLimit; 967 searchMessages(accountId, params); 968 } 969 970 /** 971 * Search for messages on the (IMAP) server; do not call this on the UI thread! 972 * @param accountId the id of the account to be searched 973 * @param searchParams the parameters for this search 974 * @throws MessagingException 975 */ 976 public int searchMessages(final long accountId, final SearchParams searchParams) 977 throws MessagingException { 978 // Find/create our search mailbox 979 Mailbox searchMailbox = getSearchMailbox(accountId); 980 if (searchMailbox == null) return 0; 981 final long searchMailboxId = searchMailbox.mId; 982 // Save this away (per account) 983 sSearchParamsMap.put(accountId, searchParams); 984 985 if (searchParams.mOffset == 0) { 986 // Delete existing contents of search mailbox 987 ContentResolver resolver = mContext.getContentResolver(); 988 resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId, 989 null); 990 ContentValues cv = new ContentValues(); 991 // For now, use the actual query as the name of the mailbox 992 cv.put(Mailbox.DISPLAY_NAME, searchParams.mFilter); 993 resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId), 994 cv, null, null); 995 } 996 997 IEmailService service = getServiceForAccount(accountId); 998 if (service != null) { 999 // Service implementation 1000 try { 1001 return service.searchMessages(accountId, searchParams, searchMailboxId); 1002 } catch (RemoteException e) { 1003 // TODO Change exception handling to be consistent with however this method 1004 // is implemented for other protocols 1005 Log.e("searchMessages", "RemoteException", e); 1006 return 0; 1007 } 1008 } else { 1009 // This is the actual mailbox we'll be searching 1010 Mailbox actualMailbox = Mailbox.restoreMailboxWithId(mContext, searchParams.mMailboxId); 1011 if (actualMailbox == null) { 1012 Log.e(Logging.LOG_TAG, "Unable to find mailbox " + searchParams.mMailboxId 1013 + " to search in with " + searchParams); 1014 return 0; 1015 } 1016 // Do the search 1017 if (Email.DEBUG) { 1018 Log.d(Logging.LOG_TAG, "Search: " + searchParams.mFilter); 1019 } 1020 return mLegacyController.searchMailbox(accountId, searchParams, searchMailboxId); 1021 } 1022 } 1023 1024 /** 1025 * Respond to a meeting invitation. 1026 * 1027 * @param messageId the id of the invitation being responded to 1028 * @param response the code representing the response to the invitation 1029 */ 1030 public void sendMeetingResponse(final long messageId, final int response) { 1031 // Split here for target type (Service or MessagingController) 1032 IEmailService service = getServiceForMessage(messageId); 1033 if (service != null) { 1034 // Service implementation 1035 try { 1036 service.sendMeetingResponse(messageId, response); 1037 } catch (RemoteException e) { 1038 // TODO Change exception handling to be consistent with however this method 1039 // is implemented for other protocols 1040 Log.e("onDownloadAttachment", "RemoteException", e); 1041 } 1042 } 1043 } 1044 1045 /** 1046 * Request that an attachment be loaded. It will be stored at a location controlled 1047 * by the AttachmentProvider. 1048 * 1049 * @param attachmentId the attachment to load 1050 * @param messageId the owner message 1051 * @param accountId the owner account 1052 */ 1053 public void loadAttachment(final long attachmentId, final long messageId, 1054 final long accountId) { 1055 Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId); 1056 if (attachInfo == null) { 1057 return; 1058 } 1059 1060 if (Utility.attachmentExists(mProviderContext, attachInfo)) { 1061 // The attachment has already been downloaded, so we will just "pretend" to download it 1062 // This presumably is for POP3 messages 1063 synchronized (mListeners) { 1064 for (Result listener : mListeners) { 1065 listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 0); 1066 } 1067 for (Result listener : mListeners) { 1068 listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 100); 1069 } 1070 } 1071 return; 1072 } 1073 1074 // Flag the attachment as needing download at the user's request 1075 ContentValues cv = new ContentValues(); 1076 cv.put(Attachment.FLAGS, attachInfo.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST); 1077 attachInfo.update(mProviderContext, cv); 1078 } 1079 1080 /** 1081 * For a given message id, return a service proxy if applicable, or null. 1082 * 1083 * @param messageId the message of interest 1084 * @result service proxy, or null if n/a 1085 */ 1086 private IEmailService getServiceForMessage(long messageId) { 1087 // TODO make this more efficient, caching the account, smaller lookup here, etc. 1088 Message message = Message.restoreMessageWithId(mProviderContext, messageId); 1089 if (message == null) { 1090 return null; 1091 } 1092 return getServiceForAccount(message.mAccountKey); 1093 } 1094 1095 /** 1096 * For a given account id, return a service proxy if applicable, or null. 1097 * 1098 * @param accountId the message of interest 1099 * @result service proxy, or null if n/a 1100 */ 1101 private IEmailService getServiceForAccount(long accountId) { 1102 if (isMessagingController(accountId)) return null; 1103 return getExchangeEmailService(); 1104 } 1105 1106 private IEmailService getExchangeEmailService() { 1107 return EmailServiceUtils.getExchangeService(mContext, mServiceCallback); 1108 } 1109 1110 /** 1111 * Simple helper to determine if legacy MessagingController should be used 1112 */ 1113 public boolean isMessagingController(Account account) { 1114 if (account == null) return false; 1115 return isMessagingController(account.mId); 1116 } 1117 1118 public boolean isMessagingController(long accountId) { 1119 Boolean isLegacyController = mLegacyControllerMap.get(accountId); 1120 if (isLegacyController == null) { 1121 String protocol = Account.getProtocol(mProviderContext, accountId); 1122 isLegacyController = ("pop3".equals(protocol) || "imap".equals(protocol)); 1123 mLegacyControllerMap.put(accountId, isLegacyController); 1124 } 1125 return isLegacyController; 1126 } 1127 1128 /** 1129 * Delete an account. 1130 */ 1131 public void deleteAccount(final long accountId) { 1132 Utility.runAsync(new Runnable() { 1133 public void run() { 1134 deleteAccountSync(accountId, mProviderContext); 1135 } 1136 }); 1137 } 1138 1139 /** 1140 * Backup our accounts; define this here so that unit tests can override the behavior 1141 * @param context the caller's context 1142 */ 1143 @VisibleForTesting 1144 protected void backupAccounts(Context context) { 1145 AccountBackupRestore.backup(context); 1146 } 1147 1148 /** 1149 * Delete an account synchronously. 1150 */ 1151 public void deleteAccountSync(long accountId, Context context) { 1152 try { 1153 mLegacyControllerMap.remove(accountId); 1154 // Get the account URI. 1155 final Account account = Account.restoreAccountWithId(context, accountId); 1156 if (account == null) { 1157 return; // Already deleted? 1158 } 1159 1160 // Delete account data, attachments, PIM data, etc. 1161 deleteSyncedDataSync(accountId); 1162 1163 // Now delete the account itself 1164 Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); 1165 context.getContentResolver().delete(uri, null, null); 1166 1167 backupAccounts(context); 1168 1169 // Release or relax device administration, if relevant 1170 SecurityPolicy.getInstance(context).reducePolicies(); 1171 1172 Email.setServicesEnabledSync(context); 1173 } catch (Exception e) { 1174 Log.w(Logging.LOG_TAG, "Exception while deleting account", e); 1175 } finally { 1176 synchronized (mListeners) { 1177 for (Result l : mListeners) { 1178 l.deleteAccountCallback(accountId); 1179 } 1180 } 1181 } 1182 } 1183 1184 /** 1185 * Delete all synced data, but don't delete the actual account. This is used when security 1186 * policy requirements are not met, and we don't want to reveal any synced data, but we do 1187 * wish to keep the account configured (e.g. to accept remote wipe commands). 1188 * 1189 * The only mailbox not deleted is the account mailbox (if any) 1190 * Also, clear the sync keys on the remaining account, since the data is gone. 1191 * 1192 * SYNCHRONOUS - do not call from UI thread. 1193 * 1194 * @param accountId The account to wipe. 1195 */ 1196 public void deleteSyncedDataSync(long accountId) { 1197 try { 1198 // Delete synced attachments 1199 AttachmentUtilities.deleteAllAccountAttachmentFiles(mProviderContext, 1200 accountId); 1201 1202 // Delete synced email, leaving only an empty inbox. We do this in two phases: 1203 // 1. Delete all non-inbox mailboxes (which will delete all of their messages) 1204 // 2. Delete all remaining messages (which will be the inbox messages) 1205 ContentResolver resolver = mProviderContext.getContentResolver(); 1206 String[] accountIdArgs = new String[] { Long.toString(accountId) }; 1207 resolver.delete(Mailbox.CONTENT_URI, 1208 MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION, 1209 accountIdArgs); 1210 resolver.delete(Message.CONTENT_URI, MESSAGES_FOR_ACCOUNT_SELECTION, accountIdArgs); 1211 1212 // Delete sync keys on remaining items 1213 ContentValues cv = new ContentValues(); 1214 cv.putNull(Account.SYNC_KEY); 1215 resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs); 1216 cv.clear(); 1217 cv.putNull(Mailbox.SYNC_KEY); 1218 resolver.update(Mailbox.CONTENT_URI, cv, 1219 MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs); 1220 1221 // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable 1222 IEmailService service = getServiceForAccount(accountId); 1223 if (service != null) { 1224 service.deleteAccountPIMData(accountId); 1225 } 1226 } catch (Exception e) { 1227 Log.w(Logging.LOG_TAG, "Exception while deleting account synced data", e); 1228 } 1229 } 1230 1231 /** 1232 * Simple callback for synchronous commands. For many commands, this can be largely ignored 1233 * and the result is observed via provider cursors. The callback will *not* necessarily be 1234 * made from the UI thread, so you may need further handlers to safely make UI updates. 1235 */ 1236 public static abstract class Result { 1237 private volatile boolean mRegistered; 1238 1239 protected void setRegistered(boolean registered) { 1240 mRegistered = registered; 1241 } 1242 1243 protected final boolean isRegistered() { 1244 return mRegistered; 1245 } 1246 1247 /** 1248 * Callback for updateMailboxList 1249 * 1250 * @param result If null, the operation completed without error 1251 * @param accountId The account being operated on 1252 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1253 */ 1254 public void updateMailboxListCallback(MessagingException result, long accountId, 1255 int progress) { 1256 } 1257 1258 /** 1259 * Callback for updateMailbox. Note: This looks a lot like checkMailCallback, but 1260 * it's a separate call used only by UI's, so we can keep things separate. 1261 * 1262 * @param result If null, the operation completed without error 1263 * @param accountId The account being operated on 1264 * @param mailboxId The mailbox being operated on 1265 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1266 * @param numNewMessages the number of new messages delivered 1267 */ 1268 public void updateMailboxCallback(MessagingException result, long accountId, 1269 long mailboxId, int progress, int numNewMessages, ArrayList<Long> addedMessages) { 1270 } 1271 1272 /** 1273 * Callback for loadMessageForView 1274 * 1275 * @param result if null, the attachment completed - if non-null, terminating with failure 1276 * @param messageId the message which contains the attachment 1277 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1278 */ 1279 public void loadMessageForViewCallback(MessagingException result, long accountId, 1280 long messageId, int progress) { 1281 } 1282 1283 /** 1284 * Callback for loadAttachment 1285 * 1286 * @param result if null, the attachment completed - if non-null, terminating with failure 1287 * @param messageId the message which contains the attachment 1288 * @param attachmentId the attachment being loaded 1289 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1290 */ 1291 public void loadAttachmentCallback(MessagingException result, long accountId, 1292 long messageId, long attachmentId, int progress) { 1293 } 1294 1295 /** 1296 * Callback for checkmail. Note: This looks a lot like updateMailboxCallback, but 1297 * it's a separate call used only by the automatic checker service, so we can keep 1298 * things separate. 1299 * 1300 * @param result If null, the operation completed without error 1301 * @param accountId The account being operated on 1302 * @param mailboxId The mailbox being operated on (may be unknown at start) 1303 * @param progress 0 for "starting", no updates, 100 for complete 1304 * @param tag the same tag that was passed to serviceCheckMail() 1305 */ 1306 public void serviceCheckMailCallback(MessagingException result, long accountId, 1307 long mailboxId, int progress, long tag) { 1308 } 1309 1310 /** 1311 * Callback for sending pending messages. This will be called once to start the 1312 * group, multiple times for messages, and once to complete the group. 1313 * 1314 * Unfortunately this callback works differently on SMTP and EAS. 1315 * 1316 * On SMTP: 1317 * 1318 * First, we get this. 1319 * result == null, messageId == -1, progress == 0: start batch send 1320 * 1321 * Then we get these callbacks per message. 1322 * (Exchange backend may skip "start sending one message".) 1323 * result == null, messageId == xx, progress == 0: start sending one message 1324 * result == xxxx, messageId == xx, progress == 0; failed sending one message 1325 * 1326 * Finally we get this. 1327 * result == null, messageId == -1, progres == 100; finish sending batch 1328 * 1329 * On EAS: Almost same as above, except: 1330 * 1331 * - There's no first ("start batch send") callback. 1332 * - accountId is always -1. 1333 * 1334 * @param result If null, the operation completed without error 1335 * @param accountId The account being operated on 1336 * @param messageId The being sent (may be unknown at start) 1337 * @param progress 0 for "starting", 100 for complete 1338 */ 1339 public void sendMailCallback(MessagingException result, long accountId, 1340 long messageId, int progress) { 1341 } 1342 1343 /** 1344 * Callback from {@link Controller#deleteAccount}. 1345 */ 1346 public void deleteAccountCallback(long accountId) { 1347 } 1348 } 1349 1350 /** 1351 * Bridge to intercept {@link MessageRetrievalListener#loadAttachmentProgress} and 1352 * pass down to {@link Result}. 1353 */ 1354 public class MessageRetrievalListenerBridge implements MessageRetrievalListener { 1355 private final long mMessageId; 1356 private final long mAttachmentId; 1357 private final long mAccountId; 1358 1359 public MessageRetrievalListenerBridge(long messageId, long attachmentId) { 1360 mMessageId = messageId; 1361 mAttachmentId = attachmentId; 1362 mAccountId = Account.getAccountIdForMessageId(mProviderContext, mMessageId); 1363 } 1364 1365 @Override 1366 public void loadAttachmentProgress(int progress) { 1367 synchronized (mListeners) { 1368 for (Result listener : mListeners) { 1369 listener.loadAttachmentCallback(null, mAccountId, mMessageId, mAttachmentId, 1370 progress); 1371 } 1372 } 1373 } 1374 1375 @Override 1376 public void messageRetrieved(com.android.emailcommon.mail.Message message) { 1377 } 1378 } 1379 1380 /** 1381 * Support for receiving callbacks from MessagingController and dealing with UI going 1382 * out of scope. 1383 */ 1384 public class LegacyListener extends MessagingListener { 1385 public LegacyListener() { 1386 } 1387 1388 @Override 1389 public void listFoldersStarted(long accountId) { 1390 synchronized (mListeners) { 1391 for (Result l : mListeners) { 1392 l.updateMailboxListCallback(null, accountId, 0); 1393 } 1394 } 1395 } 1396 1397 @Override 1398 public void listFoldersFailed(long accountId, String message) { 1399 synchronized (mListeners) { 1400 for (Result l : mListeners) { 1401 l.updateMailboxListCallback(new MessagingException(message), accountId, 0); 1402 } 1403 } 1404 } 1405 1406 @Override 1407 public void listFoldersFinished(long accountId) { 1408 synchronized (mListeners) { 1409 for (Result l : mListeners) { 1410 l.updateMailboxListCallback(null, accountId, 100); 1411 } 1412 } 1413 } 1414 1415 @Override 1416 public void synchronizeMailboxStarted(long accountId, long mailboxId) { 1417 synchronized (mListeners) { 1418 for (Result l : mListeners) { 1419 l.updateMailboxCallback(null, accountId, mailboxId, 0, 0, null); 1420 } 1421 } 1422 } 1423 1424 @Override 1425 public void synchronizeMailboxFinished(long accountId, long mailboxId, 1426 int totalMessagesInMailbox, int numNewMessages, ArrayList<Long> addedMessages) { 1427 synchronized (mListeners) { 1428 for (Result l : mListeners) { 1429 l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages, 1430 addedMessages); 1431 } 1432 } 1433 } 1434 1435 @Override 1436 public void synchronizeMailboxFailed(long accountId, long mailboxId, Exception e) { 1437 MessagingException me; 1438 if (e instanceof MessagingException) { 1439 me = (MessagingException) e; 1440 } else { 1441 me = new MessagingException(e.toString()); 1442 } 1443 synchronized (mListeners) { 1444 for (Result l : mListeners) { 1445 l.updateMailboxCallback(me, accountId, mailboxId, 0, 0, null); 1446 } 1447 } 1448 } 1449 1450 @Override 1451 public void checkMailStarted(Context context, long accountId, long tag) { 1452 synchronized (mListeners) { 1453 for (Result l : mListeners) { 1454 l.serviceCheckMailCallback(null, accountId, -1, 0, tag); 1455 } 1456 } 1457 } 1458 1459 @Override 1460 public void checkMailFinished(Context context, long accountId, long folderId, long tag) { 1461 synchronized (mListeners) { 1462 for (Result l : mListeners) { 1463 l.serviceCheckMailCallback(null, accountId, folderId, 100, tag); 1464 } 1465 } 1466 } 1467 1468 @Override 1469 public void loadMessageForViewStarted(long messageId) { 1470 final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 1471 synchronized (mListeners) { 1472 for (Result listener : mListeners) { 1473 listener.loadMessageForViewCallback(null, accountId, messageId, 0); 1474 } 1475 } 1476 } 1477 1478 @Override 1479 public void loadMessageForViewFinished(long messageId) { 1480 final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 1481 synchronized (mListeners) { 1482 for (Result listener : mListeners) { 1483 listener.loadMessageForViewCallback(null, accountId, messageId, 100); 1484 } 1485 } 1486 } 1487 1488 @Override 1489 public void loadMessageForViewFailed(long messageId, String message) { 1490 final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 1491 synchronized (mListeners) { 1492 for (Result listener : mListeners) { 1493 listener.loadMessageForViewCallback(new MessagingException(message), 1494 accountId, messageId, 0); 1495 } 1496 } 1497 } 1498 1499 @Override 1500 public void loadAttachmentStarted(long accountId, long messageId, long attachmentId, 1501 boolean requiresDownload) { 1502 try { 1503 mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, 1504 EmailServiceStatus.IN_PROGRESS, 0); 1505 } catch (RemoteException e) { 1506 } 1507 synchronized (mListeners) { 1508 for (Result listener : mListeners) { 1509 listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 0); 1510 } 1511 } 1512 } 1513 1514 @Override 1515 public void loadAttachmentFinished(long accountId, long messageId, long attachmentId) { 1516 try { 1517 mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, 1518 EmailServiceStatus.SUCCESS, 100); 1519 } catch (RemoteException e) { 1520 } 1521 synchronized (mListeners) { 1522 for (Result listener : mListeners) { 1523 listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 100); 1524 } 1525 } 1526 } 1527 1528 @Override 1529 public void loadAttachmentFailed(long accountId, long messageId, long attachmentId, 1530 MessagingException me, boolean background) { 1531 try { 1532 // If the cause of the MessagingException is an IOException, we send a status of 1533 // CONNECTION_ERROR; in this case, AttachmentDownloadService will try again to 1534 // download the attachment. Otherwise, the error is considered non-recoverable. 1535 int status = EmailServiceStatus.ATTACHMENT_NOT_FOUND; 1536 if (me != null && me.getCause() instanceof IOException) { 1537 status = EmailServiceStatus.CONNECTION_ERROR; 1538 } 1539 mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, status, 0); 1540 } catch (RemoteException e) { 1541 } 1542 synchronized (mListeners) { 1543 for (Result listener : mListeners) { 1544 // TODO We are overloading the exception here. The UI listens for this 1545 // callback and displays a toast if the exception is not null. Since we 1546 // want to avoid displaying toast for background operations, we force 1547 // the exception to be null. This needs to be re-worked so the UI will 1548 // only receive (or at least pays attention to) responses for requests 1549 // it explicitly cares about. Then we would not need to overload the 1550 // exception parameter. 1551 listener.loadAttachmentCallback(background ? null : me, accountId, messageId, 1552 attachmentId, 0); 1553 } 1554 } 1555 } 1556 1557 @Override 1558 synchronized public void sendPendingMessagesStarted(long accountId, long messageId) { 1559 synchronized (mListeners) { 1560 for (Result listener : mListeners) { 1561 listener.sendMailCallback(null, accountId, messageId, 0); 1562 } 1563 } 1564 } 1565 1566 @Override 1567 synchronized public void sendPendingMessagesCompleted(long accountId) { 1568 synchronized (mListeners) { 1569 for (Result listener : mListeners) { 1570 listener.sendMailCallback(null, accountId, -1, 100); 1571 } 1572 } 1573 } 1574 1575 @Override 1576 synchronized public void sendPendingMessagesFailed(long accountId, long messageId, 1577 Exception reason) { 1578 MessagingException me; 1579 if (reason instanceof MessagingException) { 1580 me = (MessagingException) reason; 1581 } else { 1582 me = new MessagingException(reason.toString()); 1583 } 1584 synchronized (mListeners) { 1585 for (Result listener : mListeners) { 1586 listener.sendMailCallback(me, accountId, messageId, 0); 1587 } 1588 } 1589 } 1590 } 1591 1592 /** 1593 * Service callback for service operations 1594 */ 1595 private class ServiceCallback extends IEmailServiceCallback.Stub { 1596 1597 private final static boolean DEBUG_FAIL_DOWNLOADS = false; // do not check in "true" 1598 1599 public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, 1600 int progress) { 1601 MessagingException result = mapStatusToException(statusCode); 1602 switch (statusCode) { 1603 case EmailServiceStatus.SUCCESS: 1604 progress = 100; 1605 break; 1606 case EmailServiceStatus.IN_PROGRESS: 1607 if (DEBUG_FAIL_DOWNLOADS && progress > 75) { 1608 result = new MessagingException( 1609 String.valueOf(EmailServiceStatus.CONNECTION_ERROR)); 1610 } 1611 // discard progress reports that look like sentinels 1612 if (progress < 0 || progress >= 100) { 1613 return; 1614 } 1615 break; 1616 } 1617 final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 1618 synchronized (mListeners) { 1619 for (Result listener : mListeners) { 1620 listener.loadAttachmentCallback(result, accountId, messageId, attachmentId, 1621 progress); 1622 } 1623 } 1624 } 1625 1626 /** 1627 * Note, this is an incomplete implementation of this callback, because we are 1628 * not getting things back from Service in quite the same way as from MessagingController. 1629 * However, this is sufficient for basic "progress=100" notification that message send 1630 * has just completed. 1631 */ 1632 public void sendMessageStatus(long messageId, String subject, int statusCode, 1633 int progress) { 1634 long accountId = -1; // This should be in the callback 1635 MessagingException result = mapStatusToException(statusCode); 1636 switch (statusCode) { 1637 case EmailServiceStatus.SUCCESS: 1638 progress = 100; 1639 break; 1640 case EmailServiceStatus.IN_PROGRESS: 1641 // discard progress reports that look like sentinels 1642 if (progress < 0 || progress >= 100) { 1643 return; 1644 } 1645 break; 1646 } 1647 synchronized(mListeners) { 1648 for (Result listener : mListeners) { 1649 listener.sendMailCallback(result, accountId, messageId, progress); 1650 } 1651 } 1652 } 1653 1654 public void syncMailboxListStatus(long accountId, int statusCode, int progress) { 1655 MessagingException result = mapStatusToException(statusCode); 1656 switch (statusCode) { 1657 case EmailServiceStatus.SUCCESS: 1658 progress = 100; 1659 break; 1660 case EmailServiceStatus.IN_PROGRESS: 1661 // discard progress reports that look like sentinels 1662 if (progress < 0 || progress >= 100) { 1663 return; 1664 } 1665 break; 1666 } 1667 synchronized(mListeners) { 1668 for (Result listener : mListeners) { 1669 listener.updateMailboxListCallback(result, accountId, progress); 1670 } 1671 } 1672 } 1673 1674 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) { 1675 MessagingException result = mapStatusToException(statusCode); 1676 switch (statusCode) { 1677 case EmailServiceStatus.SUCCESS: 1678 progress = 100; 1679 break; 1680 case EmailServiceStatus.IN_PROGRESS: 1681 // discard progress reports that look like sentinels 1682 if (progress < 0 || progress >= 100) { 1683 return; 1684 } 1685 break; 1686 } 1687 // TODO should pass this back instead of looking it up here 1688 Mailbox mbx = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); 1689 // The mailbox could have disappeared if the server commanded it 1690 if (mbx == null) return; 1691 long accountId = mbx.mAccountKey; 1692 synchronized(mListeners) { 1693 for (Result listener : mListeners) { 1694 listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0, null); 1695 } 1696 } 1697 } 1698 1699 private MessagingException mapStatusToException(int statusCode) { 1700 switch (statusCode) { 1701 case EmailServiceStatus.SUCCESS: 1702 case EmailServiceStatus.IN_PROGRESS: 1703 // Don't generate error if the account is uninitialized 1704 case EmailServiceStatus.ACCOUNT_UNINITIALIZED: 1705 return null; 1706 1707 case EmailServiceStatus.LOGIN_FAILED: 1708 return new AuthenticationFailedException(""); 1709 1710 case EmailServiceStatus.CONNECTION_ERROR: 1711 return new MessagingException(MessagingException.IOERROR); 1712 1713 case EmailServiceStatus.SECURITY_FAILURE: 1714 return new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED); 1715 1716 case EmailServiceStatus.ACCESS_DENIED: 1717 return new MessagingException(MessagingException.ACCESS_DENIED); 1718 1719 case EmailServiceStatus.ATTACHMENT_NOT_FOUND: 1720 return new MessagingException(MessagingException.ATTACHMENT_NOT_FOUND); 1721 1722 case EmailServiceStatus.CLIENT_CERTIFICATE_ERROR: 1723 return new MessagingException(MessagingException.CLIENT_CERTIFICATE_ERROR); 1724 1725 case EmailServiceStatus.MESSAGE_NOT_FOUND: 1726 case EmailServiceStatus.FOLDER_NOT_DELETED: 1727 case EmailServiceStatus.FOLDER_NOT_RENAMED: 1728 case EmailServiceStatus.FOLDER_NOT_CREATED: 1729 case EmailServiceStatus.REMOTE_EXCEPTION: 1730 // TODO: define exception code(s) & UI string(s) for server-side errors 1731 default: 1732 return new MessagingException(String.valueOf(statusCode)); 1733 } 1734 } 1735 } 1736 1737 private interface ServiceCallbackWrapper { 1738 public void call(IEmailServiceCallback cb) throws RemoteException; 1739 } 1740 1741 /** 1742 * Proxy that can be used to broadcast service callbacks; we currently use this only for 1743 * loadAttachment callbacks 1744 */ 1745 private final IEmailServiceCallback.Stub mCallbackProxy = 1746 new IEmailServiceCallback.Stub() { 1747 1748 /** 1749 * Broadcast a callback to the everyone that's registered 1750 * 1751 * @param wrapper the ServiceCallbackWrapper used in the broadcast 1752 */ 1753 private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) { 1754 if (sCallbackList != null) { 1755 // Call everyone on our callback list 1756 // Exceptions can be safely ignored 1757 int count = sCallbackList.beginBroadcast(); 1758 for (int i = 0; i < count; i++) { 1759 try { 1760 wrapper.call(sCallbackList.getBroadcastItem(i)); 1761 } catch (RemoteException e) { 1762 } 1763 } 1764 sCallbackList.finishBroadcast(); 1765 } 1766 } 1767 1768 public void loadAttachmentStatus(final long messageId, final long attachmentId, 1769 final int status, final int progress) { 1770 broadcastCallback(new ServiceCallbackWrapper() { 1771 @Override 1772 public void call(IEmailServiceCallback cb) throws RemoteException { 1773 cb.loadAttachmentStatus(messageId, attachmentId, status, progress); 1774 } 1775 }); 1776 } 1777 1778 @Override 1779 public void sendMessageStatus(long messageId, String subject, int statusCode, int progress){ 1780 } 1781 1782 @Override 1783 public void syncMailboxListStatus(long accountId, int statusCode, int progress) { 1784 } 1785 1786 @Override 1787 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) { 1788 } 1789 }; 1790 1791 public static class ControllerService extends Service { 1792 /** 1793 * Create our EmailService implementation here. For now, only loadAttachment is supported; 1794 * the intention, however, is to move more functionality to the service interface 1795 */ 1796 private final IEmailService.Stub mBinder = new IEmailService.Stub() { 1797 1798 public Bundle validate(HostAuth hostAuth) { 1799 return null; 1800 } 1801 1802 public Bundle autoDiscover(String userName, String password) { 1803 return null; 1804 } 1805 1806 public void startSync(long mailboxId, boolean userRequest) { 1807 } 1808 1809 public void stopSync(long mailboxId) { 1810 } 1811 1812 public void loadAttachment(long attachmentId, boolean background) 1813 throws RemoteException { 1814 Attachment att = Attachment.restoreAttachmentWithId(ControllerService.this, 1815 attachmentId); 1816 if (att != null) { 1817 if (Email.DEBUG) { 1818 Log.d(TAG, "loadAttachment " + attachmentId + ": " + att.mFileName); 1819 } 1820 Message msg = Message.restoreMessageWithId(ControllerService.this, 1821 att.mMessageKey); 1822 if (msg != null) { 1823 // If the message is a forward and the attachment needs downloading, we need 1824 // to retrieve the message from the source, rather than from the message 1825 // itself 1826 if ((msg.mFlags & Message.FLAG_TYPE_FORWARD) != 0) { 1827 String[] cols = Utility.getRowColumns(ControllerService.this, 1828 Body.CONTENT_URI, BODY_SOURCE_KEY_PROJECTION, WHERE_MESSAGE_KEY, 1829 new String[] {Long.toString(msg.mId)}); 1830 if (cols != null) { 1831 msg = Message.restoreMessageWithId(ControllerService.this, 1832 Long.parseLong(cols[BODY_SOURCE_KEY_COLUMN])); 1833 if (msg == null) { 1834 // TODO: We can try restoring from the deleted table here... 1835 return; 1836 } 1837 } 1838 } 1839 MessagingController legacyController = sInstance.mLegacyController; 1840 LegacyListener legacyListener = sInstance.mLegacyListener; 1841 legacyController.loadAttachment(msg.mAccountKey, msg.mId, msg.mMailboxKey, 1842 attachmentId, legacyListener, background); 1843 } else { 1844 // Send back the specific error status for this case 1845 sInstance.mCallbackProxy.loadAttachmentStatus(att.mMessageKey, attachmentId, 1846 EmailServiceStatus.MESSAGE_NOT_FOUND, 0); 1847 } 1848 } 1849 } 1850 1851 public void updateFolderList(long accountId) { 1852 } 1853 1854 public void hostChanged(long accountId) { 1855 } 1856 1857 public void setLogging(int flags) { 1858 } 1859 1860 public void sendMeetingResponse(long messageId, int response) { 1861 } 1862 1863 public void loadMore(long messageId) { 1864 } 1865 1866 // The following three methods are not implemented in this version 1867 public boolean createFolder(long accountId, String name) { 1868 return false; 1869 } 1870 1871 public boolean deleteFolder(long accountId, String name) { 1872 return false; 1873 } 1874 1875 public boolean renameFolder(long accountId, String oldName, String newName) { 1876 return false; 1877 } 1878 1879 public void setCallback(IEmailServiceCallback cb) { 1880 sCallbackList.register(cb); 1881 } 1882 1883 public void deleteAccountPIMData(long accountId) { 1884 } 1885 1886 public int searchMessages(long accountId, SearchParams searchParams, 1887 long destMailboxId) { 1888 return 0; 1889 } 1890 1891 @Override 1892 public int getApiLevel() { 1893 return Api.LEVEL; 1894 } 1895 }; 1896 1897 @Override 1898 public IBinder onBind(Intent intent) { 1899 return mBinder; 1900 } 1901 } 1902} 1903