Controller.java revision ed1dc9ee72410e07de24bc4a072d066981611a48
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.emailcommon.Api; 38import com.android.emailcommon.Logging; 39import com.android.emailcommon.mail.AuthenticationFailedException; 40import com.android.emailcommon.mail.Folder.MessageRetrievalListener; 41import com.android.emailcommon.mail.MessagingException; 42import com.android.emailcommon.provider.Account; 43import com.android.emailcommon.provider.EmailContent; 44import com.android.emailcommon.provider.EmailContent.Attachment; 45import com.android.emailcommon.provider.EmailContent.Body; 46import com.android.emailcommon.provider.EmailContent.MailboxColumns; 47import com.android.emailcommon.provider.EmailContent.Message; 48import com.android.emailcommon.provider.EmailContent.MessageColumns; 49import com.android.emailcommon.provider.HostAuth; 50import com.android.emailcommon.provider.Mailbox; 51import com.android.emailcommon.service.EmailServiceStatus; 52import com.android.emailcommon.service.IEmailService; 53import com.android.emailcommon.service.IEmailServiceCallback; 54import com.android.emailcommon.service.SearchParams; 55import com.android.emailcommon.utility.AttachmentUtilities; 56import com.android.emailcommon.utility.EmailAsyncTask; 57import com.android.emailcommon.utility.Utility; 58import com.google.common.annotations.VisibleForTesting; 59 60import java.io.FileNotFoundException; 61import java.io.IOException; 62import java.io.InputStream; 63import java.util.ArrayList; 64import java.util.Collection; 65import java.util.HashMap; 66import java.util.HashSet; 67import java.util.concurrent.ConcurrentHashMap; 68 69/** 70 * New central controller/dispatcher for Email activities that may require remote operations. 71 * Handles disambiguating between legacy MessagingController operations and newer provider/sync 72 * based code. We implement Service to allow loadAttachment calls to be sent in a consistent manner 73 * to IMAP, POP3, and EAS by AttachmentDownloadService 74 */ 75public class Controller { 76 private static final String TAG = "Controller"; 77 private static Controller sInstance; 78 private final Context mContext; 79 private Context mProviderContext; 80 private final MessagingController mLegacyController; 81 private final LegacyListener mLegacyListener = new LegacyListener(); 82 private final ServiceCallback mServiceCallback = new ServiceCallback(); 83 private final HashSet<Result> mListeners = new HashSet<Result>(); 84 /*package*/ final ConcurrentHashMap<Long, Boolean> mLegacyControllerMap = 85 new ConcurrentHashMap<Long, Boolean>(); 86 87 // Note that 0 is a syntactically valid account key; however there can never be an account 88 // with id = 0, so attempts to restore the account will return null. Null values are 89 // handled properly within the code, so this won't cause any issues. 90 private static final long GLOBAL_MAILBOX_ACCOUNT_KEY = 0; 91 /*package*/ static final String ATTACHMENT_MAILBOX_SERVER_ID = "__attachment_mailbox__"; 92 /*package*/ static final String ATTACHMENT_MESSAGE_UID_PREFIX = "__attachment_message__"; 93 /*package*/ static final String SEARCH_MAILBOX_SERVER_ID = "__search_mailbox__"; 94 private static final String WHERE_TYPE_ATTACHMENT = 95 MailboxColumns.TYPE + "=" + Mailbox.TYPE_ATTACHMENT; 96 private static final String WHERE_TYPE_SEARCH = 97 MailboxColumns.TYPE + "=" + Mailbox.TYPE_SEARCH; 98 private static final String WHERE_MAILBOX_KEY = MessageColumns.MAILBOX_KEY + "=?"; 99 100 private static final String[] MESSAGEID_TO_ACCOUNTID_PROJECTION = new String[] { 101 EmailContent.RECORD_ID, 102 EmailContent.MessageColumns.ACCOUNT_KEY 103 }; 104 private static final int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1; 105 106 private static final String[] BODY_SOURCE_KEY_PROJECTION = 107 new String[] {Body.SOURCE_MESSAGE_KEY}; 108 private static final int BODY_SOURCE_KEY_COLUMN = 0; 109 private static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?"; 110 111 private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?"; 112 private static final String MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION = 113 MAILBOXES_FOR_ACCOUNT_SELECTION + " AND " + MailboxColumns.TYPE + "!=" + 114 Mailbox.TYPE_EAS_ACCOUNT_MAILBOX; 115 private static final String MESSAGES_FOR_ACCOUNT_SELECTION = MessageColumns.ACCOUNT_KEY + "=?"; 116 117 // Service callbacks as set up via setCallback 118 private static RemoteCallbackList<IEmailServiceCallback> sCallbackList = 119 new RemoteCallbackList<IEmailServiceCallback>(); 120 121 protected Controller(Context _context) { 122 mContext = _context.getApplicationContext(); 123 mProviderContext = _context; 124 mLegacyController = MessagingController.getInstance(mProviderContext, this); 125 mLegacyController.addListener(mLegacyListener); 126 } 127 128 /** 129 * Cleanup for test. Mustn't be called for the regular {@link Controller}, as it's a 130 * singleton and lives till the process finishes. 131 * 132 * <p>However, this method MUST be called for mock instances. 133 */ 134 public void cleanupForTest() { 135 mLegacyController.removeListener(mLegacyListener); 136 } 137 138 /** 139 * Gets or creates the singleton instance of Controller. 140 */ 141 public synchronized static Controller getInstance(Context _context) { 142 if (sInstance == null) { 143 sInstance = new Controller(_context); 144 } 145 return sInstance; 146 } 147 148 /** 149 * Inject a mock controller. Used only for testing. Affects future calls to getInstance(). 150 * 151 * Tests that use this method MUST clean it up by calling this method again with null. 152 */ 153 public synchronized static void injectMockControllerForTest(Controller mockController) { 154 sInstance = mockController; 155 } 156 157 /** 158 * For testing only: Inject a different context for provider access. This will be 159 * used internally for access the underlying provider (e.g. getContentResolver().query()). 160 * @param providerContext the provider context to be used by this instance 161 */ 162 public void setProviderContext(Context providerContext) { 163 mProviderContext = providerContext; 164 } 165 166 /** 167 * Any UI code that wishes for callback results (on async ops) should register their callback 168 * here (typically from onResume()). Unregistered callbacks will never be called, to prevent 169 * problems when the command completes and the activity has already paused or finished. 170 * @param listener The callback that may be used in action methods 171 */ 172 public void addResultCallback(Result listener) { 173 synchronized (mListeners) { 174 listener.setRegistered(true); 175 mListeners.add(listener); 176 } 177 } 178 179 /** 180 * Any UI code that no longer wishes for callback results (on async ops) should unregister 181 * their callback here (typically from onPause()). Unregistered callbacks will never be called, 182 * to prevent problems when the command completes and the activity has already paused or 183 * finished. 184 * @param listener The callback that may no longer be used 185 */ 186 public void removeResultCallback(Result listener) { 187 synchronized (mListeners) { 188 listener.setRegistered(false); 189 mListeners.remove(listener); 190 } 191 } 192 193 public Collection<Result> getResultCallbacksForTest() { 194 return mListeners; 195 } 196 197 /** 198 * Delete all Messages that live in the attachment mailbox 199 */ 200 public void deleteAttachmentMessages() { 201 // Note: There should only be one attachment mailbox at present 202 ContentResolver resolver = mProviderContext.getContentResolver(); 203 Cursor c = null; 204 try { 205 c = resolver.query(Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION, 206 WHERE_TYPE_ATTACHMENT, null, null); 207 while (c.moveToNext()) { 208 long mailboxId = c.getLong(EmailContent.ID_PROJECTION_COLUMN); 209 // Must delete attachments BEFORE messages 210 AttachmentUtilities.deleteAllMailboxAttachmentFiles(mProviderContext, 0, 211 mailboxId); 212 resolver.delete(Message.CONTENT_URI, WHERE_MAILBOX_KEY, 213 new String[] {Long.toString(mailboxId)}); 214 } 215 } finally { 216 if (c != null) { 217 c.close(); 218 } 219 } 220 } 221 222 /** 223 * Get a mailbox based on a sqlite WHERE clause 224 */ 225 private Mailbox getGlobalMailboxWhere(String where) { 226 Cursor c = mProviderContext.getContentResolver().query(Mailbox.CONTENT_URI, 227 Mailbox.CONTENT_PROJECTION, where, null, null); 228 try { 229 if (c.moveToFirst()) { 230 Mailbox m = new Mailbox(); 231 m.restore(c); 232 return m; 233 } 234 } finally { 235 c.close(); 236 } 237 return null; 238 } 239 240 /** 241 * Returns the attachment mailbox (where we store eml attachment Emails), creating one 242 * if necessary 243 * @return the global attachment mailbox 244 */ 245 public Mailbox getAttachmentMailbox() { 246 Mailbox m = getGlobalMailboxWhere(WHERE_TYPE_ATTACHMENT); 247 if (m == null) { 248 m = new Mailbox(); 249 m.mAccountKey = GLOBAL_MAILBOX_ACCOUNT_KEY; 250 m.mServerId = ATTACHMENT_MAILBOX_SERVER_ID; 251 m.mFlagVisible = false; 252 m.mDisplayName = ATTACHMENT_MAILBOX_SERVER_ID; 253 m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER; 254 m.mType = Mailbox.TYPE_ATTACHMENT; 255 m.save(mProviderContext); 256 } 257 return m; 258 } 259 260 /** 261 * Returns the search mailbox for the specified account, creating one if necessary 262 * @return the search mailbox for the passed in account 263 */ 264 public Mailbox getSearchMailbox(long accountId) { 265 Mailbox m = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_SEARCH); 266 if (m == null) { 267 m = new Mailbox(); 268 m.mAccountKey = accountId; 269 m.mServerId = SEARCH_MAILBOX_SERVER_ID; 270 m.mFlagVisible = true; 271 m.mDisplayName = SEARCH_MAILBOX_SERVER_ID; 272 m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER; 273 m.mType = Mailbox.TYPE_SEARCH; 274 m.mFlags = Mailbox.FLAG_HOLDS_MAIL; 275 m.mParentKey = Mailbox.NO_MAILBOX; 276 m.save(mProviderContext); 277 } 278 return m; 279 } 280 281 /** 282 * Create a Message from the Uri and store it in the attachment mailbox 283 * @param uri the uri containing message content 284 * @return the Message or null 285 */ 286 public Message loadMessageFromUri(Uri uri) { 287 Mailbox mailbox = getAttachmentMailbox(); 288 if (mailbox == null) return null; 289 try { 290 InputStream is = mProviderContext.getContentResolver().openInputStream(uri); 291 try { 292 // First, create a Pop3Message from the attachment and then parse it 293 Pop3Message pop3Message = new Pop3Message( 294 ATTACHMENT_MESSAGE_UID_PREFIX + System.currentTimeMillis(), null); 295 pop3Message.parse(is); 296 // Now, pull out the header fields 297 Message msg = new Message(); 298 LegacyConversions.updateMessageFields(msg, pop3Message, 0, mailbox.mId); 299 // Commit the message to the local store 300 msg.save(mProviderContext); 301 // Setup the rest of the message and mark it completely loaded 302 mLegacyController.copyOneMessageToProvider(pop3Message, msg, 303 Message.FLAG_LOADED_COMPLETE, mProviderContext); 304 // Restore the complete message and return it 305 return Message.restoreMessageWithId(mProviderContext, msg.mId); 306 } catch (MessagingException e) { 307 } catch (IOException e) { 308 } 309 } catch (FileNotFoundException e) { 310 } 311 return null; 312 } 313 314 /** 315 * Set logging flags for external sync services 316 * 317 * Generally this should be called by anybody who changes Email.DEBUG 318 */ 319 public void serviceLogging(int debugFlags) { 320 IEmailService service = ExchangeUtils.getExchangeService(mContext, mServiceCallback); 321 try { 322 service.setLogging(debugFlags); 323 } catch (RemoteException e) { 324 // TODO Change exception handling to be consistent with however this method 325 // is implemented for other protocols 326 Log.d("setLogging", "RemoteException" + e); 327 } 328 } 329 330 /** 331 * Request a remote update of mailboxes for an account. 332 */ 333 public void updateMailboxList(final long accountId) { 334 Utility.runAsync(new Runnable() { 335 @Override 336 public void run() { 337 final IEmailService service = getServiceForAccount(accountId); 338 if (service != null) { 339 // Service implementation 340 try { 341 service.updateFolderList(accountId); 342 } catch (RemoteException e) { 343 // TODO Change exception handling to be consistent with however this method 344 // is implemented for other protocols 345 Log.d("updateMailboxList", "RemoteException" + e); 346 } 347 } else { 348 // MessagingController implementation 349 mLegacyController.listFolders(accountId, mLegacyListener); 350 } 351 } 352 }); 353 } 354 355 /** 356 * Request a remote update of a mailbox. For use by the timed service. 357 * 358 * Functionally this is quite similar to updateMailbox(), but it's a separate API and 359 * separate callback in order to keep UI callbacks from affecting the service loop. 360 */ 361 public void serviceCheckMail(final long accountId, final long mailboxId, final long tag) { 362 IEmailService service = getServiceForAccount(accountId); 363 if (service != null) { 364 // Service implementation 365// try { 366 // TODO this isn't quite going to work, because we're going to get the 367 // generic (UI) callbacks and not the ones we need to restart the ol' service. 368 // service.startSync(mailboxId, tag); 369 mLegacyListener.checkMailFinished(mContext, accountId, mailboxId, tag); 370// } catch (RemoteException e) { 371 // TODO Change exception handling to be consistent with however this method 372 // is implemented for other protocols 373// Log.d("updateMailbox", "RemoteException" + e); 374// } 375 } else { 376 // MessagingController implementation 377 Utility.runAsync(new Runnable() { 378 public void run() { 379 mLegacyController.checkMail(accountId, tag, mLegacyListener); 380 } 381 }); 382 } 383 } 384 385 /** 386 * Request a remote update of a mailbox. 387 * 388 * The contract here should be to try and update the headers ASAP, in order to populate 389 * a simple message list. We should also at this point queue up a background task of 390 * downloading some/all of the messages in this mailbox, but that should be interruptable. 391 */ 392 public void updateMailbox(final long accountId, final long mailboxId, boolean userRequest) { 393 394 IEmailService service = getServiceForAccount(accountId); 395 if (service != null) { 396 try { 397 service.startSync(mailboxId, userRequest); 398 } catch (RemoteException e) { 399 // TODO Change exception handling to be consistent with however this method 400 // is implemented for other protocols 401 Log.d("updateMailbox", "RemoteException" + e); 402 } 403 } else { 404 // MessagingController implementation 405 Utility.runAsync(new Runnable() { 406 public void run() { 407 // TODO shouldn't be passing fully-build accounts & mailboxes into APIs 408 Account account = 409 Account.restoreAccountWithId(mProviderContext, accountId); 410 Mailbox mailbox = 411 Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); 412 if (account == null || mailbox == null || 413 mailbox.mType == Mailbox.TYPE_SEARCH) { 414 return; 415 } 416 mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener); 417 } 418 }); 419 } 420 } 421 422 /** 423 * Request that any final work necessary be done, to load a message. 424 * 425 * Note, this assumes that the caller has already checked message.mFlagLoaded and that 426 * additional work is needed. There is no optimization here for a message which is already 427 * loaded. 428 * 429 * @param messageId the message to load 430 * @param callback the Controller callback by which results will be reported 431 */ 432 public void loadMessageForView(final long messageId) { 433 434 // Split here for target type (Service or MessagingController) 435 IEmailService service = getServiceForMessage(messageId); 436 if (service != null) { 437 // There is no service implementation, so we'll just jam the value, log the error, 438 // and get out of here. 439 Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId); 440 ContentValues cv = new ContentValues(); 441 cv.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE); 442 mProviderContext.getContentResolver().update(uri, cv, null, null); 443 Log.d(Logging.LOG_TAG, "Unexpected loadMessageForView() for service-based message."); 444 final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 445 synchronized (mListeners) { 446 for (Result listener : mListeners) { 447 listener.loadMessageForViewCallback(null, accountId, messageId, 100); 448 } 449 } 450 } else { 451 // MessagingController implementation 452 Utility.runAsync(new Runnable() { 453 public void run() { 454 mLegacyController.loadMessageForView(messageId, mLegacyListener); 455 } 456 }); 457 } 458 } 459 460 461 /** 462 * Saves the message to a mailbox of given type. 463 * This is a synchronous operation taking place in the same thread as the caller. 464 * Upon return the message.mId is set. 465 * @param message the message (must have the mAccountId set). 466 * @param mailboxType the mailbox type (e.g. Mailbox.TYPE_DRAFTS). 467 */ 468 public void saveToMailbox(final EmailContent.Message message, final int mailboxType) { 469 long accountId = message.mAccountKey; 470 long mailboxId = findOrCreateMailboxOfType(accountId, mailboxType); 471 message.mMailboxKey = mailboxId; 472 message.save(mProviderContext); 473 } 474 475 /** 476 * Look for a specific mailbox, creating it if necessary, and return the mailbox id. 477 * This is a blocking operation and should not be called from the UI thread. 478 * 479 * Synchronized so multiple threads can call it (and not risk creating duplicate boxes). 480 * 481 * @param accountId the account id 482 * @param mailboxType the mailbox type (e.g. EmailContent.Mailbox.TYPE_TRASH) 483 * @return the id of the mailbox. The mailbox is created if not existing. 484 * Returns Mailbox.NO_MAILBOX if the accountId or mailboxType are negative. 485 * Does not validate the input in other ways (e.g. does not verify the existence of account). 486 */ 487 public synchronized long findOrCreateMailboxOfType(long accountId, int mailboxType) { 488 if (accountId < 0 || mailboxType < 0) { 489 return Mailbox.NO_MAILBOX; 490 } 491 long mailboxId = 492 Mailbox.findMailboxOfType(mProviderContext, accountId, mailboxType); 493 return mailboxId == Mailbox.NO_MAILBOX ? createMailbox(accountId, mailboxType) : mailboxId; 494 } 495 496 /** 497 * Returns the server-side name for a specific mailbox. 498 * 499 * @param mailboxType the mailbox type 500 * @return the resource string corresponding to the mailbox type, empty if not found. 501 */ 502 /* package */ String getMailboxServerName(int mailboxType) { 503 int resId = -1; 504 switch (mailboxType) { 505 case Mailbox.TYPE_INBOX: 506 resId = R.string.mailbox_name_server_inbox; 507 break; 508 case Mailbox.TYPE_OUTBOX: 509 resId = R.string.mailbox_name_server_outbox; 510 break; 511 case Mailbox.TYPE_DRAFTS: 512 resId = R.string.mailbox_name_server_drafts; 513 break; 514 case Mailbox.TYPE_TRASH: 515 resId = R.string.mailbox_name_server_trash; 516 break; 517 case Mailbox.TYPE_SENT: 518 resId = R.string.mailbox_name_server_sent; 519 break; 520 case Mailbox.TYPE_JUNK: 521 resId = R.string.mailbox_name_server_junk; 522 break; 523 } 524 return resId != -1 ? mContext.getString(resId) : ""; 525 } 526 527 /** 528 * Create a mailbox given the account and mailboxType. 529 * TODO: Does this need to be signaled explicitly to the sync engines? 530 */ 531 @VisibleForTesting 532 long createMailbox(long accountId, int mailboxType) { 533 if (accountId < 0 || mailboxType < 0) { 534 String mes = "Invalid arguments " + accountId + ' ' + mailboxType; 535 Log.e(Logging.LOG_TAG, mes); 536 throw new RuntimeException(mes); 537 } 538 Mailbox box = new Mailbox(); 539 box.mAccountKey = accountId; 540 box.mType = mailboxType; 541 box.mSyncInterval = Account.CHECK_INTERVAL_NEVER; 542 box.mFlagVisible = true; 543 box.mServerId = box.mDisplayName = getMailboxServerName(mailboxType); 544 // All system mailboxes are off the top-level & can hold mail 545 if (mailboxType != Mailbox.TYPE_MAIL) { 546 box.mParentKey = Mailbox.NO_MAILBOX; 547 box.mFlags = Mailbox.FLAG_HOLDS_MAIL; 548 } 549 box.save(mProviderContext); 550 return box.mId; 551 } 552 553 /** 554 * Send a message: 555 * - move the message to Outbox (the message is assumed to be in Drafts). 556 * - EAS service will take it from there 557 * - trigger send for POP/IMAP 558 * @param messageId the id of the message to send 559 */ 560 public void sendMessage(long messageId, long accountId) { 561 ContentResolver resolver = mProviderContext.getContentResolver(); 562 if (accountId == -1) { 563 accountId = lookupAccountForMessage(messageId); 564 } 565 if (accountId == -1) { 566 // probably the message was not found 567 if (Logging.LOGD) { 568 Email.log("no account found for message " + messageId); 569 } 570 return; 571 } 572 573 // Move to Outbox 574 long outboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_OUTBOX); 575 ContentValues cv = new ContentValues(); 576 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, outboxId); 577 578 // does this need to be SYNCED_CONTENT_URI instead? 579 Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId); 580 resolver.update(uri, cv, null, null); 581 582 sendPendingMessages(accountId); 583 } 584 585 private void sendPendingMessagesSmtp(long accountId) { 586 // for IMAP & POP only, (attempt to) send the message now 587 final Account account = 588 Account.restoreAccountWithId(mProviderContext, accountId); 589 if (account == null) { 590 return; 591 } 592 final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT); 593 Utility.runAsync(new Runnable() { 594 public void run() { 595 mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener); 596 } 597 }); 598 } 599 600 /** 601 * Try to send all pending messages for a given account 602 * 603 * @param accountId the account for which to send messages 604 */ 605 public void sendPendingMessages(long accountId) { 606 // 1. make sure we even have an outbox, exit early if not 607 final long outboxId = 608 Mailbox.findMailboxOfType(mProviderContext, accountId, Mailbox.TYPE_OUTBOX); 609 if (outboxId == Mailbox.NO_MAILBOX) { 610 return; 611 } 612 613 // 2. dispatch as necessary 614 IEmailService service = getServiceForAccount(accountId); 615 if (service != null) { 616 // Service implementation 617 try { 618 service.startSync(outboxId, false); 619 } catch (RemoteException e) { 620 // TODO Change exception handling to be consistent with however this method 621 // is implemented for other protocols 622 Log.d("updateMailbox", "RemoteException" + e); 623 } 624 } else { 625 // MessagingController implementation 626 sendPendingMessagesSmtp(accountId); 627 } 628 } 629 630 /** 631 * Reset visible limits for all accounts. 632 * For each account: 633 * look up limit 634 * write limit into all mailboxes for that account 635 */ 636 public void resetVisibleLimits() { 637 Utility.runAsync(new Runnable() { 638 public void run() { 639 ContentResolver resolver = mProviderContext.getContentResolver(); 640 Cursor c = null; 641 try { 642 c = resolver.query( 643 Account.CONTENT_URI, 644 Account.ID_PROJECTION, 645 null, null, null); 646 while (c.moveToNext()) { 647 long accountId = c.getLong(Account.ID_PROJECTION_COLUMN); 648 String protocol = Account.getProtocol(mProviderContext, accountId); 649 if (!HostAuth.SCHEME_EAS.equals(protocol)) { 650 ContentValues cv = new ContentValues(); 651 cv.put(MailboxColumns.VISIBLE_LIMIT, Email.VISIBLE_LIMIT_DEFAULT); 652 resolver.update(Mailbox.CONTENT_URI, cv, 653 MailboxColumns.ACCOUNT_KEY + "=?", 654 new String[] { Long.toString(accountId) }); 655 } 656 } 657 } finally { 658 if (c != null) { 659 c.close(); 660 } 661 } 662 } 663 }); 664 } 665 666 /** 667 * Increase the load count for a given mailbox, and trigger a refresh. Applies only to 668 * IMAP and POP. 669 * 670 * @param mailboxId the mailbox 671 */ 672 public void loadMoreMessages(final long mailboxId) { 673 EmailAsyncTask.runAsyncParallel(new Runnable() { 674 public void run() { 675 Mailbox mailbox = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); 676 if (mailbox == null) { 677 return; 678 } 679 if (mailbox.mType == Mailbox.TYPE_SEARCH) { 680 try { 681 searchMore(mailbox.mAccountKey); 682 } catch (MessagingException e) { 683 // Nothing to be done 684 } 685 return; 686 } 687 Account account = Account.restoreAccountWithId(mProviderContext, 688 mailbox.mAccountKey); 689 if (account == null) { 690 return; 691 } 692 // Use provider math to increment the field 693 ContentValues cv = new ContentValues();; 694 cv.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT); 695 cv.put(EmailContent.ADD_COLUMN_NAME, Email.VISIBLE_LIMIT_INCREMENT); 696 Uri uri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, mailboxId); 697 mProviderContext.getContentResolver().update(uri, cv, null, null); 698 // Trigger a refresh using the new, longer limit 699 mailbox.mVisibleLimit += Email.VISIBLE_LIMIT_INCREMENT; 700 mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener); 701 } 702 }); 703 } 704 705 /** 706 * @param messageId the id of message 707 * @return the accountId corresponding to the given messageId, or -1 if not found. 708 */ 709 private long lookupAccountForMessage(long messageId) { 710 ContentResolver resolver = mProviderContext.getContentResolver(); 711 Cursor c = resolver.query(EmailContent.Message.CONTENT_URI, 712 MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?", 713 new String[] { Long.toString(messageId) }, null); 714 try { 715 return c.moveToFirst() 716 ? c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID) 717 : -1; 718 } finally { 719 c.close(); 720 } 721 } 722 723 /** 724 * Delete a single attachment entry from the DB given its id. 725 * Does not delete any eventual associated files. 726 */ 727 public void deleteAttachment(long attachmentId) { 728 ContentResolver resolver = mProviderContext.getContentResolver(); 729 Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId); 730 resolver.delete(uri, null, null); 731 } 732 733 /** 734 * Async version of {@link #deleteMessageSync}. 735 */ 736 public void deleteMessage(final long messageId) { 737 EmailAsyncTask.runAsyncParallel(new Runnable() { 738 public void run() { 739 deleteMessageSync(messageId); 740 } 741 }); 742 } 743 744 /** 745 * Batch & async version of {@link #deleteMessageSync}. 746 */ 747 public void deleteMessages(final long[] messageIds) { 748 if (messageIds == null || messageIds.length == 0) { 749 throw new IllegalArgumentException(); 750 } 751 EmailAsyncTask.runAsyncParallel(new Runnable() { 752 public void run() { 753 for (long messageId: messageIds) { 754 deleteMessageSync(messageId); 755 } 756 } 757 }); 758 } 759 760 /** 761 * Delete a single message by moving it to the trash, or really delete it if it's already in 762 * trash or a draft message. 763 * 764 * This function has no callback, no result reporting, because the desired outcome 765 * is reflected entirely by changes to one or more cursors. 766 * 767 * @param messageId The id of the message to "delete". 768 */ 769 /* package */ void deleteMessageSync(long messageId) { 770 // 1. Get the message's account 771 Account account = Account.getAccountForMessageId(mProviderContext, messageId); 772 773 if (account == null) return; 774 775 // 2. Confirm that there is a trash mailbox available. If not, create one 776 long trashMailboxId = findOrCreateMailboxOfType(account.mId, Mailbox.TYPE_TRASH); 777 778 // 3. Get the message's original mailbox 779 Mailbox mailbox = Mailbox.getMailboxForMessageId(mProviderContext, messageId); 780 781 if (mailbox == null) return; 782 783 // 4. Drop non-essential data for the message (e.g. attachment files) 784 AttachmentUtilities.deleteAllAttachmentFiles(mProviderContext, account.mId, 785 messageId); 786 787 Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, 788 messageId); 789 ContentResolver resolver = mProviderContext.getContentResolver(); 790 791 // 5. Perform "delete" as appropriate 792 if ((mailbox.mId == trashMailboxId) || (mailbox.mType == Mailbox.TYPE_DRAFTS)) { 793 // 5a. Really delete it 794 resolver.delete(uri, null, null); 795 } else { 796 // 5b. Move to trash 797 ContentValues cv = new ContentValues(); 798 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId); 799 resolver.update(uri, cv, null, null); 800 } 801 802 if (isMessagingController(account)) { 803 mLegacyController.processPendingActions(account.mId); 804 } 805 } 806 807 /** 808 * Moves messages to a new mailbox. 809 * 810 * This function has no callback, no result reporting, because the desired outcome 811 * is reflected entirely by changes to one or more cursors. 812 * 813 * Note this method assumes all of the given message and mailbox IDs belong to the same 814 * account. 815 * 816 * @param messageIds IDs of the messages that are to be moved 817 * @param newMailboxId ID of the new mailbox that the messages will be moved to 818 * @return an asynchronous task that executes the move (for testing only) 819 */ 820 public EmailAsyncTask<Void, Void, Void> moveMessages(final long[] messageIds, 821 final long newMailboxId) { 822 if (messageIds == null || messageIds.length == 0) { 823 throw new IllegalArgumentException(); 824 } 825 return EmailAsyncTask.runAsyncParallel(new Runnable() { 826 public void run() { 827 Account account = Account.getAccountForMessageId(mProviderContext, messageIds[0]); 828 if (account != null) { 829 ContentValues cv = new ContentValues(); 830 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, newMailboxId); 831 ContentResolver resolver = mProviderContext.getContentResolver(); 832 for (long messageId : messageIds) { 833 Uri uri = ContentUris.withAppendedId( 834 EmailContent.Message.SYNCED_CONTENT_URI, messageId); 835 resolver.update(uri, cv, null, null); 836 } 837 if (isMessagingController(account)) { 838 mLegacyController.processPendingActions(account.mId); 839 } 840 } 841 } 842 }); 843 } 844 845 /** 846 * Set/clear the unread status of a message 847 * 848 * @param messageId the message to update 849 * @param isRead the new value for the isRead flag 850 * @return the AsyncTask that will execute the changes (for testing only) 851 */ 852 public AsyncTask<Void, Void, Void> setMessageRead(final long messageId, final boolean isRead) { 853 return setMessageBoolean(messageId, EmailContent.MessageColumns.FLAG_READ, isRead); 854 } 855 856 /** 857 * Set/clear the favorite status of a message 858 * 859 * @param messageId the message to update 860 * @param isFavorite the new value for the isFavorite flag 861 * @return the AsyncTask that will execute the changes (for testing only) 862 */ 863 public AsyncTask<Void, Void, Void> setMessageFavorite(final long messageId, 864 final boolean isFavorite) { 865 return setMessageBoolean(messageId, EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite); 866 } 867 868 /** 869 * Set/clear boolean columns of a message 870 * 871 * @param messageId the message to update 872 * @param columnName the column to update 873 * @param columnValue the new value for the column 874 * @return the AsyncTask that will execute the changes (for testing only) 875 */ 876 private AsyncTask<Void, Void, Void> setMessageBoolean(final long messageId, 877 final String columnName, final boolean columnValue) { 878 return Utility.runAsync(new Runnable() { 879 public void run() { 880 ContentValues cv = new ContentValues(); 881 cv.put(columnName, columnValue); 882 Uri uri = ContentUris.withAppendedId( 883 EmailContent.Message.SYNCED_CONTENT_URI, messageId); 884 mProviderContext.getContentResolver().update(uri, cv, null, null); 885 886 // Service runs automatically, MessagingController needs a kick 887 long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 888 if (accountId == -1) { 889 return; 890 } 891 if (isMessagingController(accountId)) { 892 mLegacyController.processPendingActions(accountId); 893 } 894 } 895 }); 896 } 897 898 private static final HashMap<Long, SearchParams> sSearchParamsMap = 899 new HashMap<Long, SearchParams>(); 900 901 public void searchMore(long accountId) throws MessagingException { 902 SearchParams params = sSearchParamsMap.get(accountId); 903 if (params == null) return; 904 params.mOffset += params.mLimit; 905 searchMessages(accountId, params); 906 } 907 908 /** 909 * Search for messages on the (IMAP) server; do not call this on the UI thread! 910 * @param accountId the id of the account to be searched 911 * @param searchParams the parameters for this search 912 * @throws MessagingException 913 */ 914 public void searchMessages(final long accountId, final SearchParams searchParams) 915 throws MessagingException { 916 // Find/create our search mailbox 917 Mailbox searchMailbox = getSearchMailbox(accountId); 918 if (searchMailbox == null) return; 919 final long searchMailboxId = searchMailbox.mId; 920 // Save this away (per account) 921 sSearchParamsMap.put(accountId, searchParams); 922 IEmailService service = getServiceForAccount(accountId); 923 if (service != null) { 924 // Service implementation 925 try { 926 service.searchMessages(accountId, searchParams, searchMailboxId); 927 } catch (RemoteException e) { 928 // TODO Change exception handling to be consistent with however this method 929 // is implemented for other protocols 930 Log.e("searchMessages", "RemoteException", e); 931 } 932 } else { 933 // This is the actual mailbox we'll be searching 934 Mailbox actualMailbox = Mailbox.restoreMailboxWithId(mContext, searchParams.mMailboxId); 935 if (actualMailbox == null) { 936 Log.e(Logging.LOG_TAG, "Unable to find mailbox " + searchParams.mMailboxId 937 + " to search in with " + searchParams); 938 return; 939 } 940 941 if (searchParams.mOffset == 0) { 942 // Delete existing contents of search mailbox 943 ContentResolver resolver = mContext.getContentResolver(); 944 resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId, 945 null); 946 ContentValues cv = new ContentValues(); 947 // For now, use the actual query as the name of the mailbox 948 cv.put(Mailbox.DISPLAY_NAME, searchParams.mFilter); 949 // But use the server id of the actual mailbox we're searching; this allows full 950 // message loading to work normally (clever, huh?) 951 cv.put(MailboxColumns.SERVER_ID, actualMailbox.mServerId); 952 resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId), 953 cv, null, null); 954 } 955 // Do the search 956 if (Email.DEBUG) { 957 Log.d(Logging.LOG_TAG, "Search: " + searchParams.mFilter); 958 } 959 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 ExchangeUtils.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