Controller.java revision 0b8e04c84def3a06ef45126b48efc485fa0a7628
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.Pop3Store.Pop3Message; 35import com.android.email.provider.AccountBackupRestore; 36import com.android.email.service.EmailServiceUtils; 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 = false; 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 = EmailServiceUtils.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 mailboxes, with the exception of the EAS search mailbox. 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 int 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 0; 919 final long searchMailboxId = searchMailbox.mId; 920 // Save this away (per account) 921 sSearchParamsMap.put(accountId, searchParams); 922 923 if (searchParams.mOffset == 0) { 924 // Delete existing contents of search mailbox 925 ContentResolver resolver = mContext.getContentResolver(); 926 resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId, 927 null); 928 ContentValues cv = new ContentValues(); 929 // For now, use the actual query as the name of the mailbox 930 cv.put(Mailbox.DISPLAY_NAME, searchParams.mFilter); 931 resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId), 932 cv, null, null); 933 } 934 935 IEmailService service = getServiceForAccount(accountId); 936 if (service != null) { 937 // Service implementation 938 try { 939 return service.searchMessages(accountId, searchParams, searchMailboxId); 940 } catch (RemoteException e) { 941 // TODO Change exception handling to be consistent with however this method 942 // is implemented for other protocols 943 Log.e("searchMessages", "RemoteException", e); 944 return 0; 945 } 946 } else { 947 // This is the actual mailbox we'll be searching 948 Mailbox actualMailbox = Mailbox.restoreMailboxWithId(mContext, searchParams.mMailboxId); 949 if (actualMailbox == null) { 950 Log.e(Logging.LOG_TAG, "Unable to find mailbox " + searchParams.mMailboxId 951 + " to search in with " + searchParams); 952 return 0; 953 } 954 // Do the search 955 if (Email.DEBUG) { 956 Log.d(Logging.LOG_TAG, "Search: " + searchParams.mFilter); 957 } 958 return mLegacyController.searchMailbox(accountId, searchParams, searchMailboxId); 959 } 960 } 961 962 /** 963 * Respond to a meeting invitation. 964 * 965 * @param messageId the id of the invitation being responded to 966 * @param response the code representing the response to the invitation 967 */ 968 public void sendMeetingResponse(final long messageId, final int response) { 969 // Split here for target type (Service or MessagingController) 970 IEmailService service = getServiceForMessage(messageId); 971 if (service != null) { 972 // Service implementation 973 try { 974 service.sendMeetingResponse(messageId, response); 975 } catch (RemoteException e) { 976 // TODO Change exception handling to be consistent with however this method 977 // is implemented for other protocols 978 Log.e("onDownloadAttachment", "RemoteException", e); 979 } 980 } 981 } 982 983 /** 984 * Request that an attachment be loaded. It will be stored at a location controlled 985 * by the AttachmentProvider. 986 * 987 * @param attachmentId the attachment to load 988 * @param messageId the owner message 989 * @param accountId the owner account 990 */ 991 public void loadAttachment(final long attachmentId, final long messageId, 992 final long accountId) { 993 Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId); 994 if (attachInfo == null) { 995 return; 996 } 997 998 if (Utility.attachmentExists(mProviderContext, attachInfo)) { 999 // The attachment has already been downloaded, so we will just "pretend" to download it 1000 // This presumably is for POP3 messages 1001 synchronized (mListeners) { 1002 for (Result listener : mListeners) { 1003 listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 0); 1004 } 1005 for (Result listener : mListeners) { 1006 listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 100); 1007 } 1008 } 1009 return; 1010 } 1011 1012 // Flag the attachment as needing download at the user's request 1013 ContentValues cv = new ContentValues(); 1014 cv.put(Attachment.FLAGS, attachInfo.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST); 1015 attachInfo.update(mProviderContext, cv); 1016 } 1017 1018 /** 1019 * For a given message id, return a service proxy if applicable, or null. 1020 * 1021 * @param messageId the message of interest 1022 * @result service proxy, or null if n/a 1023 */ 1024 private IEmailService getServiceForMessage(long messageId) { 1025 // TODO make this more efficient, caching the account, smaller lookup here, etc. 1026 Message message = Message.restoreMessageWithId(mProviderContext, messageId); 1027 if (message == null) { 1028 return null; 1029 } 1030 return getServiceForAccount(message.mAccountKey); 1031 } 1032 1033 /** 1034 * For a given account id, return a service proxy if applicable, or null. 1035 * 1036 * @param accountId the message of interest 1037 * @result service proxy, or null if n/a 1038 */ 1039 private IEmailService getServiceForAccount(long accountId) { 1040 if (isMessagingController(accountId)) return null; 1041 return getExchangeEmailService(); 1042 } 1043 1044 private IEmailService getExchangeEmailService() { 1045 return EmailServiceUtils.getExchangeService(mContext, mServiceCallback); 1046 } 1047 1048 /** 1049 * Simple helper to determine if legacy MessagingController should be used 1050 */ 1051 public boolean isMessagingController(Account account) { 1052 if (account == null) return false; 1053 return isMessagingController(account.mId); 1054 } 1055 1056 public boolean isMessagingController(long accountId) { 1057 Boolean isLegacyController = mLegacyControllerMap.get(accountId); 1058 if (isLegacyController == null) { 1059 String protocol = Account.getProtocol(mProviderContext, accountId); 1060 isLegacyController = ("pop3".equals(protocol) || "imap".equals(protocol)); 1061 mLegacyControllerMap.put(accountId, isLegacyController); 1062 } 1063 return isLegacyController; 1064 } 1065 1066 /** 1067 * Delete an account. 1068 */ 1069 public void deleteAccount(final long accountId) { 1070 Utility.runAsync(new Runnable() { 1071 public void run() { 1072 deleteAccountSync(accountId, mProviderContext); 1073 } 1074 }); 1075 } 1076 1077 /** 1078 * Backup our accounts; define this here so that unit tests can override the behavior 1079 * @param context the caller's context 1080 */ 1081 @VisibleForTesting 1082 protected void backupAccounts(Context context) { 1083 AccountBackupRestore.backup(context); 1084 } 1085 1086 /** 1087 * Delete an account synchronously. 1088 */ 1089 public void deleteAccountSync(long accountId, Context context) { 1090 try { 1091 mLegacyControllerMap.remove(accountId); 1092 // Get the account URI. 1093 final Account account = Account.restoreAccountWithId(context, accountId); 1094 if (account == null) { 1095 return; // Already deleted? 1096 } 1097 1098 Uri uri = ContentUris.withAppendedId( 1099 Account.CONTENT_URI, accountId); 1100 context.getContentResolver().delete(uri, null, null); 1101 1102 backupAccounts(context); 1103 1104 // Release or relax device administration, if relevant 1105 SecurityPolicy.getInstance(context).reducePolicies(); 1106 1107 Email.setServicesEnabledSync(context); 1108 } catch (Exception e) { 1109 Log.w(Logging.LOG_TAG, "Exception while deleting account", e); 1110 } finally { 1111 synchronized (mListeners) { 1112 for (Result l : mListeners) { 1113 l.deleteAccountCallback(accountId); 1114 } 1115 } 1116 } 1117 } 1118 1119 /** 1120 * Delete all synced data, but don't delete the actual account. This is used when security 1121 * policy requirements are not met, and we don't want to reveal any synced data, but we do 1122 * wish to keep the account configured (e.g. to accept remote wipe commands). 1123 * 1124 * The only mailbox not deleted is the account mailbox (if any) 1125 * Also, clear the sync keys on the remaining account, since the data is gone. 1126 * 1127 * SYNCHRONOUS - do not call from UI thread. 1128 * 1129 * @param accountId The account to wipe. 1130 */ 1131 public void deleteSyncedDataSync(long accountId) { 1132 try { 1133 // Delete synced attachments 1134 AttachmentUtilities.deleteAllAccountAttachmentFiles(mProviderContext, 1135 accountId); 1136 1137 // Delete synced email, leaving only an empty inbox. We do this in two phases: 1138 // 1. Delete all non-inbox mailboxes (which will delete all of their messages) 1139 // 2. Delete all remaining messages (which will be the inbox messages) 1140 ContentResolver resolver = mProviderContext.getContentResolver(); 1141 String[] accountIdArgs = new String[] { Long.toString(accountId) }; 1142 resolver.delete(Mailbox.CONTENT_URI, 1143 MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION, 1144 accountIdArgs); 1145 resolver.delete(Message.CONTENT_URI, MESSAGES_FOR_ACCOUNT_SELECTION, accountIdArgs); 1146 1147 // Delete sync keys on remaining items 1148 ContentValues cv = new ContentValues(); 1149 cv.putNull(Account.SYNC_KEY); 1150 resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs); 1151 cv.clear(); 1152 cv.putNull(Mailbox.SYNC_KEY); 1153 resolver.update(Mailbox.CONTENT_URI, cv, 1154 MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs); 1155 1156 // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable 1157 IEmailService service = getServiceForAccount(accountId); 1158 if (service != null) { 1159 service.deleteAccountPIMData(accountId); 1160 } 1161 } catch (Exception e) { 1162 Log.w(Logging.LOG_TAG, "Exception while deleting account synced data", e); 1163 } 1164 } 1165 1166 /** 1167 * Simple callback for synchronous commands. For many commands, this can be largely ignored 1168 * and the result is observed via provider cursors. The callback will *not* necessarily be 1169 * made from the UI thread, so you may need further handlers to safely make UI updates. 1170 */ 1171 public static abstract class Result { 1172 private volatile boolean mRegistered; 1173 1174 protected void setRegistered(boolean registered) { 1175 mRegistered = registered; 1176 } 1177 1178 protected final boolean isRegistered() { 1179 return mRegistered; 1180 } 1181 1182 /** 1183 * Callback for updateMailboxList 1184 * 1185 * @param result If null, the operation completed without error 1186 * @param accountId The account being operated on 1187 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1188 */ 1189 public void updateMailboxListCallback(MessagingException result, long accountId, 1190 int progress) { 1191 } 1192 1193 /** 1194 * Callback for updateMailbox. Note: This looks a lot like checkMailCallback, but 1195 * it's a separate call used only by UI's, so we can keep things separate. 1196 * 1197 * @param result If null, the operation completed without error 1198 * @param accountId The account being operated on 1199 * @param mailboxId The mailbox being operated on 1200 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1201 * @param numNewMessages the number of new messages delivered 1202 */ 1203 public void updateMailboxCallback(MessagingException result, long accountId, 1204 long mailboxId, int progress, int numNewMessages, ArrayList<Long> addedMessages) { 1205 } 1206 1207 /** 1208 * Callback for loadMessageForView 1209 * 1210 * @param result if null, the attachment completed - if non-null, terminating with failure 1211 * @param messageId the message which contains the attachment 1212 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1213 */ 1214 public void loadMessageForViewCallback(MessagingException result, long accountId, 1215 long messageId, int progress) { 1216 } 1217 1218 /** 1219 * Callback for loadAttachment 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 attachmentId the attachment being loaded 1224 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1225 */ 1226 public void loadAttachmentCallback(MessagingException result, long accountId, 1227 long messageId, long attachmentId, int progress) { 1228 } 1229 1230 /** 1231 * Callback for checkmail. Note: This looks a lot like updateMailboxCallback, but 1232 * it's a separate call used only by the automatic checker service, so we can keep 1233 * things separate. 1234 * 1235 * @param result If null, the operation completed without error 1236 * @param accountId The account being operated on 1237 * @param mailboxId The mailbox being operated on (may be unknown at start) 1238 * @param progress 0 for "starting", no updates, 100 for complete 1239 * @param tag the same tag that was passed to serviceCheckMail() 1240 */ 1241 public void serviceCheckMailCallback(MessagingException result, long accountId, 1242 long mailboxId, int progress, long tag) { 1243 } 1244 1245 /** 1246 * Callback for sending pending messages. This will be called once to start the 1247 * group, multiple times for messages, and once to complete the group. 1248 * 1249 * Unfortunately this callback works differently on SMTP and EAS. 1250 * 1251 * On SMTP: 1252 * 1253 * First, we get this. 1254 * result == null, messageId == -1, progress == 0: start batch send 1255 * 1256 * Then we get these callbacks per message. 1257 * (Exchange backend may skip "start sending one message".) 1258 * result == null, messageId == xx, progress == 0: start sending one message 1259 * result == xxxx, messageId == xx, progress == 0; failed sending one message 1260 * 1261 * Finally we get this. 1262 * result == null, messageId == -1, progres == 100; finish sending batch 1263 * 1264 * On EAS: Almost same as above, except: 1265 * 1266 * - There's no first ("start batch send") callback. 1267 * - accountId is always -1. 1268 * 1269 * @param result If null, the operation completed without error 1270 * @param accountId The account being operated on 1271 * @param messageId The being sent (may be unknown at start) 1272 * @param progress 0 for "starting", 100 for complete 1273 */ 1274 public void sendMailCallback(MessagingException result, long accountId, 1275 long messageId, int progress) { 1276 } 1277 1278 /** 1279 * Callback from {@link Controller#deleteAccount}. 1280 */ 1281 public void deleteAccountCallback(long accountId) { 1282 } 1283 } 1284 1285 /** 1286 * Bridge to intercept {@link MessageRetrievalListener#loadAttachmentProgress} and 1287 * pass down to {@link Result}. 1288 */ 1289 public class MessageRetrievalListenerBridge implements MessageRetrievalListener { 1290 private final long mMessageId; 1291 private final long mAttachmentId; 1292 private final long mAccountId; 1293 1294 public MessageRetrievalListenerBridge(long messageId, long attachmentId) { 1295 mMessageId = messageId; 1296 mAttachmentId = attachmentId; 1297 mAccountId = Account.getAccountIdForMessageId(mProviderContext, mMessageId); 1298 } 1299 1300 @Override 1301 public void loadAttachmentProgress(int progress) { 1302 synchronized (mListeners) { 1303 for (Result listener : mListeners) { 1304 listener.loadAttachmentCallback(null, mAccountId, mMessageId, mAttachmentId, 1305 progress); 1306 } 1307 } 1308 } 1309 1310 @Override 1311 public void messageRetrieved(com.android.emailcommon.mail.Message message) { 1312 } 1313 } 1314 1315 /** 1316 * Support for receiving callbacks from MessagingController and dealing with UI going 1317 * out of scope. 1318 */ 1319 public class LegacyListener extends MessagingListener { 1320 public LegacyListener() { 1321 } 1322 1323 @Override 1324 public void listFoldersStarted(long accountId) { 1325 synchronized (mListeners) { 1326 for (Result l : mListeners) { 1327 l.updateMailboxListCallback(null, accountId, 0); 1328 } 1329 } 1330 } 1331 1332 @Override 1333 public void listFoldersFailed(long accountId, String message) { 1334 synchronized (mListeners) { 1335 for (Result l : mListeners) { 1336 l.updateMailboxListCallback(new MessagingException(message), accountId, 0); 1337 } 1338 } 1339 } 1340 1341 @Override 1342 public void listFoldersFinished(long accountId) { 1343 synchronized (mListeners) { 1344 for (Result l : mListeners) { 1345 l.updateMailboxListCallback(null, accountId, 100); 1346 } 1347 } 1348 } 1349 1350 @Override 1351 public void synchronizeMailboxStarted(long accountId, long mailboxId) { 1352 synchronized (mListeners) { 1353 for (Result l : mListeners) { 1354 l.updateMailboxCallback(null, accountId, mailboxId, 0, 0, null); 1355 } 1356 } 1357 } 1358 1359 @Override 1360 public void synchronizeMailboxFinished(long accountId, long mailboxId, 1361 int totalMessagesInMailbox, int numNewMessages, ArrayList<Long> addedMessages) { 1362 synchronized (mListeners) { 1363 for (Result l : mListeners) { 1364 l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages, 1365 addedMessages); 1366 } 1367 } 1368 } 1369 1370 @Override 1371 public void synchronizeMailboxFailed(long accountId, long mailboxId, Exception e) { 1372 MessagingException me; 1373 if (e instanceof MessagingException) { 1374 me = (MessagingException) e; 1375 } else { 1376 me = new MessagingException(e.toString()); 1377 } 1378 synchronized (mListeners) { 1379 for (Result l : mListeners) { 1380 l.updateMailboxCallback(me, accountId, mailboxId, 0, 0, null); 1381 } 1382 } 1383 } 1384 1385 @Override 1386 public void checkMailStarted(Context context, long accountId, long tag) { 1387 synchronized (mListeners) { 1388 for (Result l : mListeners) { 1389 l.serviceCheckMailCallback(null, accountId, -1, 0, tag); 1390 } 1391 } 1392 } 1393 1394 @Override 1395 public void checkMailFinished(Context context, long accountId, long folderId, long tag) { 1396 synchronized (mListeners) { 1397 for (Result l : mListeners) { 1398 l.serviceCheckMailCallback(null, accountId, folderId, 100, tag); 1399 } 1400 } 1401 } 1402 1403 @Override 1404 public void loadMessageForViewStarted(long messageId) { 1405 final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 1406 synchronized (mListeners) { 1407 for (Result listener : mListeners) { 1408 listener.loadMessageForViewCallback(null, accountId, messageId, 0); 1409 } 1410 } 1411 } 1412 1413 @Override 1414 public void loadMessageForViewFinished(long messageId) { 1415 final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 1416 synchronized (mListeners) { 1417 for (Result listener : mListeners) { 1418 listener.loadMessageForViewCallback(null, accountId, messageId, 100); 1419 } 1420 } 1421 } 1422 1423 @Override 1424 public void loadMessageForViewFailed(long messageId, String message) { 1425 final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 1426 synchronized (mListeners) { 1427 for (Result listener : mListeners) { 1428 listener.loadMessageForViewCallback(new MessagingException(message), 1429 accountId, messageId, 0); 1430 } 1431 } 1432 } 1433 1434 @Override 1435 public void loadAttachmentStarted(long accountId, long messageId, long attachmentId, 1436 boolean requiresDownload) { 1437 try { 1438 mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, 1439 EmailServiceStatus.IN_PROGRESS, 0); 1440 } catch (RemoteException e) { 1441 } 1442 synchronized (mListeners) { 1443 for (Result listener : mListeners) { 1444 listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 0); 1445 } 1446 } 1447 } 1448 1449 @Override 1450 public void loadAttachmentFinished(long accountId, long messageId, long attachmentId) { 1451 try { 1452 mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, 1453 EmailServiceStatus.SUCCESS, 100); 1454 } catch (RemoteException e) { 1455 } 1456 synchronized (mListeners) { 1457 for (Result listener : mListeners) { 1458 listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 100); 1459 } 1460 } 1461 } 1462 1463 @Override 1464 public void loadAttachmentFailed(long accountId, long messageId, long attachmentId, 1465 MessagingException me, boolean background) { 1466 try { 1467 // If the cause of the MessagingException is an IOException, we send a status of 1468 // CONNECTION_ERROR; in this case, AttachmentDownloadService will try again to 1469 // download the attachment. Otherwise, the error is considered non-recoverable. 1470 int status = EmailServiceStatus.ATTACHMENT_NOT_FOUND; 1471 if (me != null && me.getCause() instanceof IOException) { 1472 status = EmailServiceStatus.CONNECTION_ERROR; 1473 } 1474 mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, status, 0); 1475 } catch (RemoteException e) { 1476 } 1477 synchronized (mListeners) { 1478 for (Result listener : mListeners) { 1479 // TODO We are overloading the exception here. The UI listens for this 1480 // callback and displays a toast if the exception is not null. Since we 1481 // want to avoid displaying toast for background operations, we force 1482 // the exception to be null. This needs to be re-worked so the UI will 1483 // only receive (or at least pays attention to) responses for requests 1484 // it explicitly cares about. Then we would not need to overload the 1485 // exception parameter. 1486 listener.loadAttachmentCallback(background ? null : me, accountId, messageId, 1487 attachmentId, 0); 1488 } 1489 } 1490 } 1491 1492 @Override 1493 synchronized public void sendPendingMessagesStarted(long accountId, long messageId) { 1494 synchronized (mListeners) { 1495 for (Result listener : mListeners) { 1496 listener.sendMailCallback(null, accountId, messageId, 0); 1497 } 1498 } 1499 } 1500 1501 @Override 1502 synchronized public void sendPendingMessagesCompleted(long accountId) { 1503 synchronized (mListeners) { 1504 for (Result listener : mListeners) { 1505 listener.sendMailCallback(null, accountId, -1, 100); 1506 } 1507 } 1508 } 1509 1510 @Override 1511 synchronized public void sendPendingMessagesFailed(long accountId, long messageId, 1512 Exception reason) { 1513 MessagingException me; 1514 if (reason instanceof MessagingException) { 1515 me = (MessagingException) reason; 1516 } else { 1517 me = new MessagingException(reason.toString()); 1518 } 1519 synchronized (mListeners) { 1520 for (Result listener : mListeners) { 1521 listener.sendMailCallback(me, accountId, messageId, 0); 1522 } 1523 } 1524 } 1525 } 1526 1527 /** 1528 * Service callback for service operations 1529 */ 1530 private class ServiceCallback extends IEmailServiceCallback.Stub { 1531 1532 private final static boolean DEBUG_FAIL_DOWNLOADS = false; // do not check in "true" 1533 1534 public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, 1535 int progress) { 1536 MessagingException result = mapStatusToException(statusCode); 1537 switch (statusCode) { 1538 case EmailServiceStatus.SUCCESS: 1539 progress = 100; 1540 break; 1541 case EmailServiceStatus.IN_PROGRESS: 1542 if (DEBUG_FAIL_DOWNLOADS && progress > 75) { 1543 result = new MessagingException( 1544 String.valueOf(EmailServiceStatus.CONNECTION_ERROR)); 1545 } 1546 // discard progress reports that look like sentinels 1547 if (progress < 0 || progress >= 100) { 1548 return; 1549 } 1550 break; 1551 } 1552 final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 1553 synchronized (mListeners) { 1554 for (Result listener : mListeners) { 1555 listener.loadAttachmentCallback(result, accountId, messageId, attachmentId, 1556 progress); 1557 } 1558 } 1559 } 1560 1561 /** 1562 * Note, this is an incomplete implementation of this callback, because we are 1563 * not getting things back from Service in quite the same way as from MessagingController. 1564 * However, this is sufficient for basic "progress=100" notification that message send 1565 * has just completed. 1566 */ 1567 public void sendMessageStatus(long messageId, String subject, int statusCode, 1568 int progress) { 1569 long accountId = -1; // This should be in the callback 1570 MessagingException result = mapStatusToException(statusCode); 1571 switch (statusCode) { 1572 case EmailServiceStatus.SUCCESS: 1573 progress = 100; 1574 break; 1575 case EmailServiceStatus.IN_PROGRESS: 1576 // discard progress reports that look like sentinels 1577 if (progress < 0 || progress >= 100) { 1578 return; 1579 } 1580 break; 1581 } 1582 synchronized(mListeners) { 1583 for (Result listener : mListeners) { 1584 listener.sendMailCallback(result, accountId, messageId, progress); 1585 } 1586 } 1587 } 1588 1589 public void syncMailboxListStatus(long accountId, int statusCode, int progress) { 1590 MessagingException result = mapStatusToException(statusCode); 1591 switch (statusCode) { 1592 case EmailServiceStatus.SUCCESS: 1593 progress = 100; 1594 break; 1595 case EmailServiceStatus.IN_PROGRESS: 1596 // discard progress reports that look like sentinels 1597 if (progress < 0 || progress >= 100) { 1598 return; 1599 } 1600 break; 1601 } 1602 synchronized(mListeners) { 1603 for (Result listener : mListeners) { 1604 listener.updateMailboxListCallback(result, accountId, progress); 1605 } 1606 } 1607 } 1608 1609 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) { 1610 MessagingException result = mapStatusToException(statusCode); 1611 switch (statusCode) { 1612 case EmailServiceStatus.SUCCESS: 1613 progress = 100; 1614 break; 1615 case EmailServiceStatus.IN_PROGRESS: 1616 // discard progress reports that look like sentinels 1617 if (progress < 0 || progress >= 100) { 1618 return; 1619 } 1620 break; 1621 } 1622 // TODO should pass this back instead of looking it up here 1623 Mailbox mbx = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); 1624 // The mailbox could have disappeared if the server commanded it 1625 if (mbx == null) return; 1626 long accountId = mbx.mAccountKey; 1627 synchronized(mListeners) { 1628 for (Result listener : mListeners) { 1629 listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0, null); 1630 } 1631 } 1632 } 1633 1634 private MessagingException mapStatusToException(int statusCode) { 1635 switch (statusCode) { 1636 case EmailServiceStatus.SUCCESS: 1637 case EmailServiceStatus.IN_PROGRESS: 1638 // Don't generate error if the account is uninitialized 1639 case EmailServiceStatus.ACCOUNT_UNINITIALIZED: 1640 return null; 1641 1642 case EmailServiceStatus.LOGIN_FAILED: 1643 return new AuthenticationFailedException(""); 1644 1645 case EmailServiceStatus.CONNECTION_ERROR: 1646 return new MessagingException(MessagingException.IOERROR); 1647 1648 case EmailServiceStatus.SECURITY_FAILURE: 1649 return new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED); 1650 1651 case EmailServiceStatus.ACCESS_DENIED: 1652 return new MessagingException(MessagingException.ACCESS_DENIED); 1653 1654 case EmailServiceStatus.ATTACHMENT_NOT_FOUND: 1655 return new MessagingException(MessagingException.ATTACHMENT_NOT_FOUND); 1656 1657 case EmailServiceStatus.CLIENT_CERTIFICATE_ERROR: 1658 return new MessagingException(MessagingException.CLIENT_CERTIFICATE_ERROR); 1659 1660 case EmailServiceStatus.MESSAGE_NOT_FOUND: 1661 case EmailServiceStatus.FOLDER_NOT_DELETED: 1662 case EmailServiceStatus.FOLDER_NOT_RENAMED: 1663 case EmailServiceStatus.FOLDER_NOT_CREATED: 1664 case EmailServiceStatus.REMOTE_EXCEPTION: 1665 // TODO: define exception code(s) & UI string(s) for server-side errors 1666 default: 1667 return new MessagingException(String.valueOf(statusCode)); 1668 } 1669 } 1670 } 1671 1672 private interface ServiceCallbackWrapper { 1673 public void call(IEmailServiceCallback cb) throws RemoteException; 1674 } 1675 1676 /** 1677 * Proxy that can be used to broadcast service callbacks; we currently use this only for 1678 * loadAttachment callbacks 1679 */ 1680 private final IEmailServiceCallback.Stub mCallbackProxy = 1681 new IEmailServiceCallback.Stub() { 1682 1683 /** 1684 * Broadcast a callback to the everyone that's registered 1685 * 1686 * @param wrapper the ServiceCallbackWrapper used in the broadcast 1687 */ 1688 private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) { 1689 if (sCallbackList != null) { 1690 // Call everyone on our callback list 1691 // Exceptions can be safely ignored 1692 int count = sCallbackList.beginBroadcast(); 1693 for (int i = 0; i < count; i++) { 1694 try { 1695 wrapper.call(sCallbackList.getBroadcastItem(i)); 1696 } catch (RemoteException e) { 1697 } 1698 } 1699 sCallbackList.finishBroadcast(); 1700 } 1701 } 1702 1703 public void loadAttachmentStatus(final long messageId, final long attachmentId, 1704 final int status, final int progress) { 1705 broadcastCallback(new ServiceCallbackWrapper() { 1706 @Override 1707 public void call(IEmailServiceCallback cb) throws RemoteException { 1708 cb.loadAttachmentStatus(messageId, attachmentId, status, progress); 1709 } 1710 }); 1711 } 1712 1713 @Override 1714 public void sendMessageStatus(long messageId, String subject, int statusCode, int progress){ 1715 } 1716 1717 @Override 1718 public void syncMailboxListStatus(long accountId, int statusCode, int progress) { 1719 } 1720 1721 @Override 1722 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) { 1723 } 1724 }; 1725 1726 public static class ControllerService extends Service { 1727 /** 1728 * Create our EmailService implementation here. For now, only loadAttachment is supported; 1729 * the intention, however, is to move more functionality to the service interface 1730 */ 1731 private final IEmailService.Stub mBinder = new IEmailService.Stub() { 1732 1733 public Bundle validate(HostAuth hostAuth) { 1734 return null; 1735 } 1736 1737 public Bundle autoDiscover(String userName, String password) { 1738 return null; 1739 } 1740 1741 public void startSync(long mailboxId, boolean userRequest) { 1742 } 1743 1744 public void stopSync(long mailboxId) { 1745 } 1746 1747 public void loadAttachment(long attachmentId, boolean background) 1748 throws RemoteException { 1749 Attachment att = Attachment.restoreAttachmentWithId(ControllerService.this, 1750 attachmentId); 1751 if (att != null) { 1752 if (Email.DEBUG) { 1753 Log.d(TAG, "loadAttachment " + attachmentId + ": " + att.mFileName); 1754 } 1755 Message msg = Message.restoreMessageWithId(ControllerService.this, 1756 att.mMessageKey); 1757 if (msg != null) { 1758 // If the message is a forward and the attachment needs downloading, we need 1759 // to retrieve the message from the source, rather than from the message 1760 // itself 1761 if ((msg.mFlags & Message.FLAG_TYPE_FORWARD) != 0) { 1762 String[] cols = Utility.getRowColumns(ControllerService.this, 1763 Body.CONTENT_URI, BODY_SOURCE_KEY_PROJECTION, WHERE_MESSAGE_KEY, 1764 new String[] {Long.toString(msg.mId)}); 1765 if (cols != null) { 1766 msg = Message.restoreMessageWithId(ControllerService.this, 1767 Long.parseLong(cols[BODY_SOURCE_KEY_COLUMN])); 1768 if (msg == null) { 1769 // TODO: We can try restoring from the deleted table here... 1770 return; 1771 } 1772 } 1773 } 1774 MessagingController legacyController = sInstance.mLegacyController; 1775 LegacyListener legacyListener = sInstance.mLegacyListener; 1776 legacyController.loadAttachment(msg.mAccountKey, msg.mId, msg.mMailboxKey, 1777 attachmentId, legacyListener, background); 1778 } else { 1779 // Send back the specific error status for this case 1780 sInstance.mCallbackProxy.loadAttachmentStatus(att.mMessageKey, attachmentId, 1781 EmailServiceStatus.MESSAGE_NOT_FOUND, 0); 1782 } 1783 } 1784 } 1785 1786 public void updateFolderList(long accountId) { 1787 } 1788 1789 public void hostChanged(long accountId) { 1790 } 1791 1792 public void setLogging(int flags) { 1793 } 1794 1795 public void sendMeetingResponse(long messageId, int response) { 1796 } 1797 1798 public void loadMore(long messageId) { 1799 } 1800 1801 // The following three methods are not implemented in this version 1802 public boolean createFolder(long accountId, String name) { 1803 return false; 1804 } 1805 1806 public boolean deleteFolder(long accountId, String name) { 1807 return false; 1808 } 1809 1810 public boolean renameFolder(long accountId, String oldName, String newName) { 1811 return false; 1812 } 1813 1814 public void setCallback(IEmailServiceCallback cb) { 1815 sCallbackList.register(cb); 1816 } 1817 1818 public void deleteAccountPIMData(long accountId) { 1819 } 1820 1821 public int searchMessages(long accountId, SearchParams searchParams, 1822 long destMailboxId) { 1823 return 0; 1824 } 1825 1826 @Override 1827 public int getApiLevel() { 1828 return Api.LEVEL; 1829 } 1830 }; 1831 1832 @Override 1833 public IBinder onBind(Intent intent) { 1834 return mBinder; 1835 } 1836 } 1837} 1838