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