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