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