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