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