Controller.java revision 0993190cafebc107bd27a26996b5d63d4a4ede10
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.Mailbox; 32import com.android.emailcommon.provider.EmailContent.MailboxColumns; 33import com.android.emailcommon.provider.EmailContent.Message; 34import com.android.emailcommon.provider.EmailContent.MessageColumns; 35import com.android.emailcommon.service.EmailServiceStatus; 36import com.android.emailcommon.service.IEmailService; 37import com.android.emailcommon.service.IEmailServiceCallback; 38import com.android.emailcommon.utility.AttachmentUtilities; 39import com.android.emailcommon.utility.EmailAsyncTask; 40import com.android.emailcommon.utility.Utility; 41import com.google.common.annotations.VisibleForTesting; 42 43import android.app.Service; 44import android.content.ContentResolver; 45import android.content.ContentUris; 46import android.content.ContentValues; 47import android.content.Context; 48import android.content.Intent; 49import android.database.Cursor; 50import android.net.Uri; 51import android.os.AsyncTask; 52import android.os.Bundle; 53import android.os.IBinder; 54import android.os.RemoteCallbackList; 55import android.os.RemoteException; 56import android.util.Log; 57 58import java.io.FileNotFoundException; 59import java.io.IOException; 60import java.io.InputStream; 61import java.security.InvalidParameterException; 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 EmailContent.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 /* package */ long createMailbox(long accountId, int mailboxType) { 529 if (accountId < 0 || mailboxType < 0) { 530 String mes = "Invalid arguments " + accountId + ' ' + mailboxType; 531 Log.e(Logging.LOG_TAG, mes); 532 throw new RuntimeException(mes); 533 } 534 Mailbox box = new Mailbox(); 535 box.mAccountKey = accountId; 536 box.mType = mailboxType; 537 box.mSyncInterval = EmailContent.Account.CHECK_INTERVAL_NEVER; 538 box.mFlagVisible = true; 539 box.mServerId = box.mDisplayName = getMailboxServerName(mailboxType); 540 box.save(mProviderContext); 541 return box.mId; 542 } 543 544 /** 545 * Send a message: 546 * - move the message to Outbox (the message is assumed to be in Drafts). 547 * - EAS service will take it from there 548 * - trigger send for POP/IMAP 549 * @param messageId the id of the message to send 550 */ 551 public void sendMessage(long messageId, long accountId) { 552 ContentResolver resolver = mProviderContext.getContentResolver(); 553 if (accountId == -1) { 554 accountId = lookupAccountForMessage(messageId); 555 } 556 if (accountId == -1) { 557 // probably the message was not found 558 if (Email.LOGD) { 559 Email.log("no account found for message " + messageId); 560 } 561 return; 562 } 563 564 // Move to Outbox 565 long outboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_OUTBOX); 566 ContentValues cv = new ContentValues(); 567 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, outboxId); 568 569 // does this need to be SYNCED_CONTENT_URI instead? 570 Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId); 571 resolver.update(uri, cv, null, null); 572 573 sendPendingMessages(accountId); 574 } 575 576 private void sendPendingMessagesSmtp(long accountId) { 577 // for IMAP & POP only, (attempt to) send the message now 578 final EmailContent.Account account = 579 EmailContent.Account.restoreAccountWithId(mProviderContext, accountId); 580 if (account == null) { 581 return; 582 } 583 final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT); 584 Utility.runAsync(new Runnable() { 585 public void run() { 586 mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener); 587 } 588 }); 589 } 590 591 /** 592 * Try to send all pending messages for a given account 593 * 594 * @param accountId the account for which to send messages 595 */ 596 public void sendPendingMessages(long accountId) { 597 // 1. make sure we even have an outbox, exit early if not 598 final long outboxId = 599 Mailbox.findMailboxOfType(mProviderContext, accountId, Mailbox.TYPE_OUTBOX); 600 if (outboxId == Mailbox.NO_MAILBOX) { 601 return; 602 } 603 604 // 2. dispatch as necessary 605 IEmailService service = getServiceForAccount(accountId); 606 if (service != null) { 607 // Service implementation 608 try { 609 service.startSync(outboxId, false); 610 } catch (RemoteException e) { 611 // TODO Change exception handling to be consistent with however this method 612 // is implemented for other protocols 613 Log.d("updateMailbox", "RemoteException" + e); 614 } 615 } else { 616 // MessagingController implementation 617 sendPendingMessagesSmtp(accountId); 618 } 619 } 620 621 /** 622 * Reset visible limits for all accounts. 623 * For each account: 624 * look up limit 625 * write limit into all mailboxes for that account 626 */ 627 public void resetVisibleLimits() { 628 Utility.runAsync(new Runnable() { 629 public void run() { 630 ContentResolver resolver = mProviderContext.getContentResolver(); 631 Cursor c = null; 632 try { 633 c = resolver.query( 634 Account.CONTENT_URI, 635 Account.ID_PROJECTION, 636 null, null, null); 637 while (c.moveToNext()) { 638 long accountId = c.getLong(Account.ID_PROJECTION_COLUMN); 639 Account account = Account.restoreAccountWithId(mProviderContext, accountId); 640 if (account != null) { 641 Store.StoreInfo info = Store.StoreInfo.getStoreInfo( 642 account.getStoreUri(mProviderContext), mContext); 643 if (info != null && info.mVisibleLimitDefault > 0) { 644 int limit = info.mVisibleLimitDefault; 645 ContentValues cv = new ContentValues(); 646 cv.put(MailboxColumns.VISIBLE_LIMIT, limit); 647 resolver.update(Mailbox.CONTENT_URI, cv, 648 MailboxColumns.ACCOUNT_KEY + "=?", 649 new String[] { Long.toString(accountId) }); 650 } 651 } 652 } 653 } finally { 654 if (c != null) { 655 c.close(); 656 } 657 } 658 } 659 }); 660 } 661 662 /** 663 * Increase the load count for a given mailbox, and trigger a refresh. Applies only to 664 * IMAP and POP. 665 * 666 * @param mailboxId the mailbox 667 */ 668 public void loadMoreMessages(final long mailboxId) { 669 Utility.runAsync(new Runnable() { 670 public void run() { 671 Mailbox mailbox = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); 672 if (mailbox == null) { 673 return; 674 } 675 Account account = Account.restoreAccountWithId(mProviderContext, 676 mailbox.mAccountKey); 677 if (account == null) { 678 return; 679 } 680 Store.StoreInfo info = Store.StoreInfo.getStoreInfo( 681 account.getStoreUri(mProviderContext), mContext); 682 if (info != null && info.mVisibleLimitIncrement > 0) { 683 // Use provider math to increment the field 684 ContentValues cv = new ContentValues();; 685 cv.put(EmailContent.FIELD_COLUMN_NAME, MailboxColumns.VISIBLE_LIMIT); 686 cv.put(EmailContent.ADD_COLUMN_NAME, info.mVisibleLimitIncrement); 687 Uri uri = ContentUris.withAppendedId(Mailbox.ADD_TO_FIELD_URI, mailboxId); 688 mProviderContext.getContentResolver().update(uri, cv, null, null); 689 // Trigger a refresh using the new, longer limit 690 mailbox.mVisibleLimit += info.mVisibleLimitIncrement; 691 mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener); 692 } 693 } 694 }); 695 } 696 697 /** 698 * @param messageId the id of message 699 * @return the accountId corresponding to the given messageId, or -1 if not found. 700 */ 701 private long lookupAccountForMessage(long messageId) { 702 ContentResolver resolver = mProviderContext.getContentResolver(); 703 Cursor c = resolver.query(EmailContent.Message.CONTENT_URI, 704 MESSAGEID_TO_ACCOUNTID_PROJECTION, EmailContent.RECORD_ID + "=?", 705 new String[] { Long.toString(messageId) }, null); 706 try { 707 return c.moveToFirst() 708 ? c.getLong(MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID) 709 : -1; 710 } finally { 711 c.close(); 712 } 713 } 714 715 /** 716 * Delete a single attachment entry from the DB given its id. 717 * Does not delete any eventual associated files. 718 */ 719 public void deleteAttachment(long attachmentId) { 720 ContentResolver resolver = mProviderContext.getContentResolver(); 721 Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId); 722 resolver.delete(uri, null, null); 723 } 724 725 /** 726 * Delete a single message by moving it to the trash, or really delete it if it's already in 727 * trash or a draft message. 728 * 729 * This function has no callback, no result reporting, because the desired outcome 730 * is reflected entirely by changes to one or more cursors. 731 * 732 * @param messageId The id of the message to "delete". 733 * @param accountId The id of the message's account, or -1 if not known by caller 734 */ 735 public void deleteMessage(final long messageId, final long accountId) { 736 Utility.runAsync(new Runnable() { 737 public void run() { 738 deleteMessageSync(messageId, accountId); 739 } 740 }); 741 } 742 743 /** 744 * Synchronous version of {@link #deleteMessage} for tests. 745 */ 746 /* package */ void deleteMessageSync(long messageId, long accountId) { 747 // 1. Get the message's account 748 Account account = Account.getAccountForMessageId(mProviderContext, messageId); 749 750 if (account == null) return; 751 752 // 2. Confirm that there is a trash mailbox available. If not, create one 753 long trashMailboxId = findOrCreateMailboxOfType(account.mId, Mailbox.TYPE_TRASH); 754 755 // 3. Get the message's original mailbox 756 Mailbox mailbox = Mailbox.getMailboxForMessageId(mProviderContext, messageId); 757 758 if (mailbox == null) return; 759 760 // 4. Drop non-essential data for the message (e.g. attachment files) 761 AttachmentUtilities.deleteAllAttachmentFiles(mProviderContext, account.mId, 762 messageId); 763 764 Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, 765 messageId); 766 ContentResolver resolver = mProviderContext.getContentResolver(); 767 768 // 5. Perform "delete" as appropriate 769 if ((mailbox.mId == trashMailboxId) || (mailbox.mType == Mailbox.TYPE_DRAFTS)) { 770 // 5a. Really delete it 771 resolver.delete(uri, null, null); 772 } else { 773 // 5b. Move to trash 774 ContentValues cv = new ContentValues(); 775 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId); 776 resolver.update(uri, cv, null, null); 777 } 778 779 if (isMessagingController(account)) { 780 mLegacyController.processPendingActions(account.mId); 781 } 782 } 783 784 /** 785 * Moves messages to a new mailbox. 786 * 787 * This function has no callback, no result reporting, because the desired outcome 788 * is reflected entirely by changes to one or more cursors. 789 * 790 * Note this method assumes all of the given message and mailbox IDs belong to the same 791 * account. 792 * 793 * @param messageIds IDs of the messages that are to be moved 794 * @param newMailboxId ID of the new mailbox that the messages will be moved to 795 * @return an asynchronous task that executes the move (for testing only) 796 */ 797 public EmailAsyncTask<Void, Void, Void> moveMessages(final long[] messageIds, 798 final long newMailboxId) { 799 if (messageIds == null || messageIds.length == 0) { 800 throw new InvalidParameterException(); 801 } 802 return EmailAsyncTask.runAsyncParallel(new Runnable() { 803 public void run() { 804 Account account = Account.getAccountForMessageId(mProviderContext, messageIds[0]); 805 if (account != null) { 806 ContentValues cv = new ContentValues(); 807 cv.put(EmailContent.MessageColumns.MAILBOX_KEY, newMailboxId); 808 ContentResolver resolver = mProviderContext.getContentResolver(); 809 for (long messageId : messageIds) { 810 Uri uri = ContentUris.withAppendedId( 811 EmailContent.Message.SYNCED_CONTENT_URI, messageId); 812 resolver.update(uri, cv, null, null); 813 } 814 if (isMessagingController(account)) { 815 mLegacyController.processPendingActions(account.mId); 816 } 817 } 818 } 819 }); 820 } 821 822 /** 823 * Set/clear the unread status of a message 824 * 825 * @param messageId the message to update 826 * @param isRead the new value for the isRead flag 827 * @return the AsyncTask that will execute the changes (for testing only) 828 */ 829 public AsyncTask<Void, Void, Void> setMessageRead(final long messageId, final boolean isRead) { 830 return setMessageBoolean(messageId, EmailContent.MessageColumns.FLAG_READ, isRead); 831 } 832 833 /** 834 * Set/clear the favorite status of a message 835 * 836 * @param messageId the message to update 837 * @param isFavorite the new value for the isFavorite flag 838 * @return the AsyncTask that will execute the changes (for testing only) 839 */ 840 public AsyncTask<Void, Void, Void> setMessageFavorite(final long messageId, 841 final boolean isFavorite) { 842 return setMessageBoolean(messageId, EmailContent.MessageColumns.FLAG_FAVORITE, isFavorite); 843 } 844 845 /** 846 * Set/clear boolean columns of a message 847 * 848 * @param messageId the message to update 849 * @param columnName the column to update 850 * @param columnValue the new value for the column 851 * @return the AsyncTask that will execute the changes (for testing only) 852 */ 853 private AsyncTask<Void, Void, Void> setMessageBoolean(final long messageId, 854 final String columnName, final boolean columnValue) { 855 return Utility.runAsync(new Runnable() { 856 public void run() { 857 ContentValues cv = new ContentValues(); 858 cv.put(columnName, columnValue); 859 Uri uri = ContentUris.withAppendedId( 860 EmailContent.Message.SYNCED_CONTENT_URI, messageId); 861 mProviderContext.getContentResolver().update(uri, cv, null, null); 862 863 // Service runs automatically, MessagingController needs a kick 864 long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 865 if (accountId == -1) { 866 return; 867 } 868 if (isMessagingController(accountId)) { 869 mLegacyController.processPendingActions(accountId); 870 } 871 } 872 }); 873 } 874 875 /** 876 * Search for messages on the server; see {@Link EmailServiceProxy#searchMessages(long, long, 877 * boolean, String, int, int, long)} for a complete description of this method's arguments. 878 */ 879 public void searchMessages(final long accountId, final long mailboxId, 880 final boolean includeSubfolders, final String query, final int numResults, 881 final int firstResult, final long destMailboxId) { 882 IEmailService service = getServiceForAccount(accountId); 883 if (service != null) { 884 // Service implementation 885 try { 886 service.searchMessages(accountId, mailboxId, includeSubfolders, query, numResults, 887 firstResult, destMailboxId); 888 } catch (RemoteException e) { 889 // TODO Change exception handling to be consistent with however this method 890 // is implemented for other protocols 891 Log.e("searchMessages", "RemoteException", e); 892 } 893 } 894 } 895 896 /** 897 * Respond to a meeting invitation. 898 * 899 * @param messageId the id of the invitation being responded to 900 * @param response the code representing the response to the invitation 901 */ 902 public void sendMeetingResponse(final long messageId, final int response) { 903 // Split here for target type (Service or MessagingController) 904 IEmailService service = getServiceForMessage(messageId); 905 if (service != null) { 906 // Service implementation 907 try { 908 service.sendMeetingResponse(messageId, response); 909 } catch (RemoteException e) { 910 // TODO Change exception handling to be consistent with however this method 911 // is implemented for other protocols 912 Log.e("onDownloadAttachment", "RemoteException", e); 913 } 914 } 915 } 916 917 /** 918 * Request that an attachment be loaded. It will be stored at a location controlled 919 * by the AttachmentProvider. 920 * 921 * @param attachmentId the attachment to load 922 * @param messageId the owner message 923 * @param accountId the owner account 924 */ 925 public void loadAttachment(final long attachmentId, final long messageId, 926 final long accountId) { 927 Attachment attachInfo = Attachment.restoreAttachmentWithId(mProviderContext, attachmentId); 928 if (attachInfo == null) { 929 return; 930 } 931 932 if (Utility.attachmentExists(mProviderContext, attachInfo)) { 933 // The attachment has already been downloaded, so we will just "pretend" to download it 934 // This presumably is for POP3 messages 935 synchronized (mListeners) { 936 for (Result listener : mListeners) { 937 listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 0); 938 } 939 for (Result listener : mListeners) { 940 listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 100); 941 } 942 } 943 return; 944 } 945 946 // Flag the attachment as needing download at the user's request 947 ContentValues cv = new ContentValues(); 948 cv.put(Attachment.FLAGS, attachInfo.mFlags | Attachment.FLAG_DOWNLOAD_USER_REQUEST); 949 attachInfo.update(mProviderContext, cv); 950 } 951 952 /** 953 * For a given message id, return a service proxy if applicable, or null. 954 * 955 * @param messageId the message of interest 956 * @result service proxy, or null if n/a 957 */ 958 private IEmailService getServiceForMessage(long messageId) { 959 // TODO make this more efficient, caching the account, smaller lookup here, etc. 960 Message message = Message.restoreMessageWithId(mProviderContext, messageId); 961 if (message == null) { 962 return null; 963 } 964 return getServiceForAccount(message.mAccountKey); 965 } 966 967 /** 968 * For a given account id, return a service proxy if applicable, or null. 969 * 970 * @param accountId the message of interest 971 * @result service proxy, or null if n/a 972 */ 973 private IEmailService getServiceForAccount(long accountId) { 974 if (isMessagingController(accountId)) return null; 975 return getExchangeEmailService(); 976 } 977 978 private IEmailService getExchangeEmailService() { 979 return ExchangeUtils.getExchangeService(mContext, mServiceCallback); 980 } 981 982 /** 983 * Simple helper to determine if legacy MessagingController should be used 984 */ 985 public boolean isMessagingController(EmailContent.Account account) { 986 if (account == null) return false; 987 return isMessagingController(account.mId); 988 } 989 990 public boolean isMessagingController(long accountId) { 991 Boolean isLegacyController = mLegacyControllerMap.get(accountId); 992 if (isLegacyController == null) { 993 String protocol = Account.getProtocol(mProviderContext, accountId); 994 isLegacyController = ("pop3".equals(protocol) || "imap".equals(protocol)); 995 mLegacyControllerMap.put(accountId, isLegacyController); 996 } 997 return isLegacyController; 998 } 999 1000 /** 1001 * Delete an account. 1002 */ 1003 public void deleteAccount(final long accountId) { 1004 Utility.runAsync(new Runnable() { 1005 public void run() { 1006 deleteAccountSync(accountId, mProviderContext); 1007 } 1008 }); 1009 } 1010 1011 /** 1012 * Backup our accounts; define this here so that unit tests can override the behavior 1013 * @param context the caller's context 1014 */ 1015 @VisibleForTesting 1016 protected void backupAccounts(Context context) { 1017 AccountBackupRestore.backup(context); 1018 } 1019 1020 /** 1021 * Delete an account synchronously. 1022 */ 1023 public void deleteAccountSync(long accountId, Context context) { 1024 try { 1025 mLegacyControllerMap.remove(accountId); 1026 // Get the account URI. 1027 final Account account = Account.restoreAccountWithId(context, accountId); 1028 if (account == null) { 1029 return; // Already deleted? 1030 } 1031 1032 try { 1033 // Delete Remote store at first. 1034 Store.getInstance(account, context, null).delete(); 1035 // Remove the Store instance from cache. 1036 Store.removeInstance(account, context); 1037 } catch (MessagingException e) { 1038 Log.w(Logging.LOG_TAG, "Failed to delete store", e); 1039 } 1040 1041 Uri uri = ContentUris.withAppendedId( 1042 EmailContent.Account.CONTENT_URI, accountId); 1043 context.getContentResolver().delete(uri, null, null); 1044 1045 backupAccounts(context); 1046 1047 // Release or relax device administration, if relevant 1048 SecurityPolicy.getInstance(context).reducePolicies(); 1049 1050 Email.setServicesEnabledSync(context); 1051 } catch (Exception e) { 1052 Log.w(Logging.LOG_TAG, "Exception while deleting account", e); 1053 } finally { 1054 synchronized (mListeners) { 1055 for (Result l : mListeners) { 1056 l.deleteAccountCallback(accountId); 1057 } 1058 } 1059 } 1060 } 1061 1062 /** 1063 * Delete all synced data, but don't delete the actual account. This is used when security 1064 * policy requirements are not met, and we don't want to reveal any synced data, but we do 1065 * wish to keep the account configured (e.g. to accept remote wipe commands). 1066 * 1067 * The only mailbox not deleted is the account mailbox (if any) 1068 * Also, clear the sync keys on the remaining account, since the data is gone. 1069 * 1070 * SYNCHRONOUS - do not call from UI thread. 1071 * 1072 * @param accountId The account to wipe. 1073 */ 1074 public void deleteSyncedDataSync(long accountId) { 1075 try { 1076 // Delete synced attachments 1077 AttachmentUtilities.deleteAllAccountAttachmentFiles(mProviderContext, 1078 accountId); 1079 1080 // Delete synced email, leaving only an empty inbox. We do this in two phases: 1081 // 1. Delete all non-inbox mailboxes (which will delete all of their messages) 1082 // 2. Delete all remaining messages (which will be the inbox messages) 1083 ContentResolver resolver = mProviderContext.getContentResolver(); 1084 String[] accountIdArgs = new String[] { Long.toString(accountId) }; 1085 resolver.delete(Mailbox.CONTENT_URI, 1086 MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION, 1087 accountIdArgs); 1088 resolver.delete(Message.CONTENT_URI, MESSAGES_FOR_ACCOUNT_SELECTION, accountIdArgs); 1089 1090 // Delete sync keys on remaining items 1091 ContentValues cv = new ContentValues(); 1092 cv.putNull(Account.SYNC_KEY); 1093 resolver.update(Account.CONTENT_URI, cv, Account.ID_SELECTION, accountIdArgs); 1094 cv.clear(); 1095 cv.putNull(Mailbox.SYNC_KEY); 1096 resolver.update(Mailbox.CONTENT_URI, cv, 1097 MAILBOXES_FOR_ACCOUNT_SELECTION, accountIdArgs); 1098 1099 // Delete PIM data (contacts, calendar), stop syncs, etc. if applicable 1100 IEmailService service = getServiceForAccount(accountId); 1101 if (service != null) { 1102 service.deleteAccountPIMData(accountId); 1103 } 1104 } catch (Exception e) { 1105 Log.w(Logging.LOG_TAG, "Exception while deleting account synced data", e); 1106 } 1107 } 1108 1109 /** 1110 * Simple callback for synchronous commands. For many commands, this can be largely ignored 1111 * and the result is observed via provider cursors. The callback will *not* necessarily be 1112 * made from the UI thread, so you may need further handlers to safely make UI updates. 1113 */ 1114 public static abstract class Result { 1115 private volatile boolean mRegistered; 1116 1117 protected void setRegistered(boolean registered) { 1118 mRegistered = registered; 1119 } 1120 1121 protected final boolean isRegistered() { 1122 return mRegistered; 1123 } 1124 1125 /** 1126 * Callback for updateMailboxList 1127 * 1128 * @param result If null, the operation completed without error 1129 * @param accountId The account being operated on 1130 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1131 */ 1132 public void updateMailboxListCallback(MessagingException result, long accountId, 1133 int progress) { 1134 } 1135 1136 /** 1137 * Callback for updateMailbox. Note: This looks a lot like checkMailCallback, but 1138 * it's a separate call used only by UI's, so we can keep things separate. 1139 * 1140 * @param result If null, the operation completed without error 1141 * @param accountId The account being operated on 1142 * @param mailboxId The mailbox being operated on 1143 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1144 * @param numNewMessages the number of new messages delivered 1145 */ 1146 public void updateMailboxCallback(MessagingException result, long accountId, 1147 long mailboxId, int progress, int numNewMessages, ArrayList<Long> addedMessages) { 1148 } 1149 1150 /** 1151 * Callback for loadMessageForView 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 progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1156 */ 1157 public void loadMessageForViewCallback(MessagingException result, long accountId, 1158 long messageId, int progress) { 1159 } 1160 1161 /** 1162 * Callback for loadAttachment 1163 * 1164 * @param result if null, the attachment completed - if non-null, terminating with failure 1165 * @param messageId the message which contains the attachment 1166 * @param attachmentId the attachment being loaded 1167 * @param progress 0 for "starting", 1..99 for updates (if needed in UI), 100 for complete 1168 */ 1169 public void loadAttachmentCallback(MessagingException result, long accountId, 1170 long messageId, long attachmentId, int progress) { 1171 } 1172 1173 /** 1174 * Callback for checkmail. Note: This looks a lot like updateMailboxCallback, but 1175 * it's a separate call used only by the automatic checker service, so we can keep 1176 * things separate. 1177 * 1178 * @param result If null, the operation completed without error 1179 * @param accountId The account being operated on 1180 * @param mailboxId The mailbox being operated on (may be unknown at start) 1181 * @param progress 0 for "starting", no updates, 100 for complete 1182 * @param tag the same tag that was passed to serviceCheckMail() 1183 */ 1184 public void serviceCheckMailCallback(MessagingException result, long accountId, 1185 long mailboxId, int progress, long tag) { 1186 } 1187 1188 /** 1189 * Callback for sending pending messages. This will be called once to start the 1190 * group, multiple times for messages, and once to complete the group. 1191 * 1192 * Unfortunately this callback works differently on SMTP and EAS. 1193 * 1194 * On SMTP: 1195 * 1196 * First, we get this. 1197 * result == null, messageId == -1, progress == 0: start batch send 1198 * 1199 * Then we get these callbacks per message. 1200 * (Exchange backend may skip "start sending one message".) 1201 * result == null, messageId == xx, progress == 0: start sending one message 1202 * result == xxxx, messageId == xx, progress == 0; failed sending one message 1203 * 1204 * Finally we get this. 1205 * result == null, messageId == -1, progres == 100; finish sending batch 1206 * 1207 * On EAS: Almost same as above, except: 1208 * 1209 * - There's no first ("start batch send") callback. 1210 * - accountId is always -1. 1211 * 1212 * @param result If null, the operation completed without error 1213 * @param accountId The account being operated on 1214 * @param messageId The being sent (may be unknown at start) 1215 * @param progress 0 for "starting", 100 for complete 1216 */ 1217 public void sendMailCallback(MessagingException result, long accountId, 1218 long messageId, int progress) { 1219 } 1220 1221 /** 1222 * Callback from {@link Controller#deleteAccount}. 1223 */ 1224 public void deleteAccountCallback(long accountId) { 1225 } 1226 } 1227 1228 /** 1229 * Bridge to intercept {@link MessageRetrievalListener#loadAttachmentProgress} and 1230 * pass down to {@link Result}. 1231 */ 1232 public class MessageRetrievalListenerBridge implements MessageRetrievalListener { 1233 private final long mMessageId; 1234 private final long mAttachmentId; 1235 private final long mAccountId; 1236 1237 public MessageRetrievalListenerBridge(long messageId, long attachmentId) { 1238 mMessageId = messageId; 1239 mAttachmentId = attachmentId; 1240 mAccountId = Account.getAccountIdForMessageId(mProviderContext, mMessageId); 1241 } 1242 1243 @Override 1244 public void loadAttachmentProgress(int progress) { 1245 synchronized (mListeners) { 1246 for (Result listener : mListeners) { 1247 listener.loadAttachmentCallback(null, mAccountId, mMessageId, mAttachmentId, 1248 progress); 1249 } 1250 } 1251 } 1252 1253 @Override 1254 public void messageRetrieved(com.android.emailcommon.mail.Message message) { 1255 } 1256 } 1257 1258 /** 1259 * Support for receiving callbacks from MessagingController and dealing with UI going 1260 * out of scope. 1261 */ 1262 public class LegacyListener extends MessagingListener { 1263 public LegacyListener() { 1264 } 1265 1266 @Override 1267 public void listFoldersStarted(long accountId) { 1268 synchronized (mListeners) { 1269 for (Result l : mListeners) { 1270 l.updateMailboxListCallback(null, accountId, 0); 1271 } 1272 } 1273 } 1274 1275 @Override 1276 public void listFoldersFailed(long accountId, String message) { 1277 synchronized (mListeners) { 1278 for (Result l : mListeners) { 1279 l.updateMailboxListCallback(new MessagingException(message), accountId, 0); 1280 } 1281 } 1282 } 1283 1284 @Override 1285 public void listFoldersFinished(long accountId) { 1286 synchronized (mListeners) { 1287 for (Result l : mListeners) { 1288 l.updateMailboxListCallback(null, accountId, 100); 1289 } 1290 } 1291 } 1292 1293 @Override 1294 public void synchronizeMailboxStarted(long accountId, long mailboxId) { 1295 synchronized (mListeners) { 1296 for (Result l : mListeners) { 1297 l.updateMailboxCallback(null, accountId, mailboxId, 0, 0, null); 1298 } 1299 } 1300 } 1301 1302 @Override 1303 public void synchronizeMailboxFinished(long accountId, long mailboxId, 1304 int totalMessagesInMailbox, int numNewMessages, ArrayList<Long> addedMessages) { 1305 synchronized (mListeners) { 1306 for (Result l : mListeners) { 1307 l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages, 1308 addedMessages); 1309 } 1310 } 1311 } 1312 1313 @Override 1314 public void synchronizeMailboxFailed(long accountId, long mailboxId, Exception e) { 1315 MessagingException me; 1316 if (e instanceof MessagingException) { 1317 me = (MessagingException) e; 1318 } else { 1319 me = new MessagingException(e.toString()); 1320 } 1321 synchronized (mListeners) { 1322 for (Result l : mListeners) { 1323 l.updateMailboxCallback(me, accountId, mailboxId, 0, 0, null); 1324 } 1325 } 1326 } 1327 1328 @Override 1329 public void checkMailStarted(Context context, long accountId, long tag) { 1330 synchronized (mListeners) { 1331 for (Result l : mListeners) { 1332 l.serviceCheckMailCallback(null, accountId, -1, 0, tag); 1333 } 1334 } 1335 } 1336 1337 @Override 1338 public void checkMailFinished(Context context, long accountId, long folderId, long tag) { 1339 synchronized (mListeners) { 1340 for (Result l : mListeners) { 1341 l.serviceCheckMailCallback(null, accountId, folderId, 100, tag); 1342 } 1343 } 1344 } 1345 1346 @Override 1347 public void loadMessageForViewStarted(long messageId) { 1348 final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 1349 synchronized (mListeners) { 1350 for (Result listener : mListeners) { 1351 listener.loadMessageForViewCallback(null, accountId, messageId, 0); 1352 } 1353 } 1354 } 1355 1356 @Override 1357 public void loadMessageForViewFinished(long messageId) { 1358 final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 1359 synchronized (mListeners) { 1360 for (Result listener : mListeners) { 1361 listener.loadMessageForViewCallback(null, accountId, messageId, 100); 1362 } 1363 } 1364 } 1365 1366 @Override 1367 public void loadMessageForViewFailed(long messageId, String message) { 1368 final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 1369 synchronized (mListeners) { 1370 for (Result listener : mListeners) { 1371 listener.loadMessageForViewCallback(new MessagingException(message), 1372 accountId, messageId, 0); 1373 } 1374 } 1375 } 1376 1377 @Override 1378 public void loadAttachmentStarted(long accountId, long messageId, long attachmentId, 1379 boolean requiresDownload) { 1380 try { 1381 mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, 1382 EmailServiceStatus.IN_PROGRESS, 0); 1383 } catch (RemoteException e) { 1384 } 1385 synchronized (mListeners) { 1386 for (Result listener : mListeners) { 1387 listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 0); 1388 } 1389 } 1390 } 1391 1392 @Override 1393 public void loadAttachmentFinished(long accountId, long messageId, long attachmentId) { 1394 try { 1395 mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, 1396 EmailServiceStatus.SUCCESS, 100); 1397 } catch (RemoteException e) { 1398 } 1399 synchronized (mListeners) { 1400 for (Result listener : mListeners) { 1401 listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 100); 1402 } 1403 } 1404 } 1405 1406 @Override 1407 public void loadAttachmentFailed(long accountId, long messageId, long attachmentId, 1408 MessagingException me, boolean background) { 1409 try { 1410 // If the cause of the MessagingException is an IOException, we send a status of 1411 // CONNECTION_ERROR; in this case, AttachmentDownloadService will try again to 1412 // download the attachment. Otherwise, the error is considered non-recoverable. 1413 int status = EmailServiceStatus.ATTACHMENT_NOT_FOUND; 1414 if (me != null && me.getCause() instanceof IOException) { 1415 status = EmailServiceStatus.CONNECTION_ERROR; 1416 } 1417 mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, status, 0); 1418 } catch (RemoteException e) { 1419 } 1420 synchronized (mListeners) { 1421 for (Result listener : mListeners) { 1422 // TODO We are overloading the exception here. The UI listens for this 1423 // callback and displays a toast if the exception is not null. Since we 1424 // want to avoid displaying toast for background operations, we force 1425 // the exception to be null. This needs to be re-worked so the UI will 1426 // only receive (or at least pays attention to) responses for requests 1427 // it explicitly cares about. Then we would not need to overload the 1428 // exception parameter. 1429 listener.loadAttachmentCallback(background ? null : me, accountId, messageId, 1430 attachmentId, 0); 1431 } 1432 } 1433 } 1434 1435 @Override 1436 synchronized public void sendPendingMessagesStarted(long accountId, long messageId) { 1437 synchronized (mListeners) { 1438 for (Result listener : mListeners) { 1439 listener.sendMailCallback(null, accountId, messageId, 0); 1440 } 1441 } 1442 } 1443 1444 @Override 1445 synchronized public void sendPendingMessagesCompleted(long accountId) { 1446 synchronized (mListeners) { 1447 for (Result listener : mListeners) { 1448 listener.sendMailCallback(null, accountId, -1, 100); 1449 } 1450 } 1451 } 1452 1453 @Override 1454 synchronized public void sendPendingMessagesFailed(long accountId, long messageId, 1455 Exception reason) { 1456 MessagingException me; 1457 if (reason instanceof MessagingException) { 1458 me = (MessagingException) reason; 1459 } else { 1460 me = new MessagingException(reason.toString()); 1461 } 1462 synchronized (mListeners) { 1463 for (Result listener : mListeners) { 1464 listener.sendMailCallback(me, accountId, messageId, 0); 1465 } 1466 } 1467 } 1468 } 1469 1470 /** 1471 * Service callback for service operations 1472 */ 1473 private class ServiceCallback extends IEmailServiceCallback.Stub { 1474 1475 private final static boolean DEBUG_FAIL_DOWNLOADS = false; // do not check in "true" 1476 1477 public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, 1478 int progress) { 1479 MessagingException result = mapStatusToException(statusCode); 1480 switch (statusCode) { 1481 case EmailServiceStatus.SUCCESS: 1482 progress = 100; 1483 break; 1484 case EmailServiceStatus.IN_PROGRESS: 1485 if (DEBUG_FAIL_DOWNLOADS && progress > 75) { 1486 result = new MessagingException( 1487 String.valueOf(EmailServiceStatus.CONNECTION_ERROR)); 1488 } 1489 // discard progress reports that look like sentinels 1490 if (progress < 0 || progress >= 100) { 1491 return; 1492 } 1493 break; 1494 } 1495 final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); 1496 synchronized (mListeners) { 1497 for (Result listener : mListeners) { 1498 listener.loadAttachmentCallback(result, accountId, messageId, attachmentId, 1499 progress); 1500 } 1501 } 1502 } 1503 1504 /** 1505 * Note, this is an incomplete implementation of this callback, because we are 1506 * not getting things back from Service in quite the same way as from MessagingController. 1507 * However, this is sufficient for basic "progress=100" notification that message send 1508 * has just completed. 1509 */ 1510 public void sendMessageStatus(long messageId, String subject, int statusCode, 1511 int progress) { 1512 long accountId = -1; // This should be in the callback 1513 MessagingException result = mapStatusToException(statusCode); 1514 switch (statusCode) { 1515 case EmailServiceStatus.SUCCESS: 1516 progress = 100; 1517 break; 1518 case EmailServiceStatus.IN_PROGRESS: 1519 // discard progress reports that look like sentinels 1520 if (progress < 0 || progress >= 100) { 1521 return; 1522 } 1523 break; 1524 } 1525 synchronized(mListeners) { 1526 for (Result listener : mListeners) { 1527 listener.sendMailCallback(result, accountId, messageId, progress); 1528 } 1529 } 1530 } 1531 1532 public void syncMailboxListStatus(long accountId, int statusCode, int progress) { 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.updateMailboxListCallback(result, accountId, progress); 1548 } 1549 } 1550 } 1551 1552 public void syncMailboxStatus(long mailboxId, 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 // TODO should pass this back instead of looking it up here 1566 Mailbox mbx = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); 1567 // The mailbox could have disappeared if the server commanded it 1568 if (mbx == null) return; 1569 long accountId = mbx.mAccountKey; 1570 synchronized(mListeners) { 1571 for (Result listener : mListeners) { 1572 listener.updateMailboxCallback(result, accountId, mailboxId, progress, 0, null); 1573 } 1574 } 1575 } 1576 1577 private MessagingException mapStatusToException(int statusCode) { 1578 switch (statusCode) { 1579 case EmailServiceStatus.SUCCESS: 1580 case EmailServiceStatus.IN_PROGRESS: 1581 // Don't generate error if the account is uninitialized 1582 case EmailServiceStatus.ACCOUNT_UNINITIALIZED: 1583 return null; 1584 1585 case EmailServiceStatus.LOGIN_FAILED: 1586 return new AuthenticationFailedException(""); 1587 1588 case EmailServiceStatus.CONNECTION_ERROR: 1589 return new MessagingException(MessagingException.IOERROR); 1590 1591 case EmailServiceStatus.SECURITY_FAILURE: 1592 return new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED); 1593 1594 case EmailServiceStatus.ACCESS_DENIED: 1595 return new MessagingException(MessagingException.ACCESS_DENIED); 1596 1597 case EmailServiceStatus.MESSAGE_NOT_FOUND: 1598 case EmailServiceStatus.ATTACHMENT_NOT_FOUND: 1599 case EmailServiceStatus.FOLDER_NOT_DELETED: 1600 case EmailServiceStatus.FOLDER_NOT_RENAMED: 1601 case EmailServiceStatus.FOLDER_NOT_CREATED: 1602 case EmailServiceStatus.REMOTE_EXCEPTION: 1603 // TODO: define exception code(s) & UI string(s) for server-side errors 1604 default: 1605 return new MessagingException(String.valueOf(statusCode)); 1606 } 1607 } 1608 } 1609 1610 private interface ServiceCallbackWrapper { 1611 public void call(IEmailServiceCallback cb) throws RemoteException; 1612 } 1613 1614 /** 1615 * Proxy that can be used to broadcast service callbacks; we currently use this only for 1616 * loadAttachment callbacks 1617 */ 1618 private final IEmailServiceCallback.Stub mCallbackProxy = 1619 new IEmailServiceCallback.Stub() { 1620 1621 /** 1622 * Broadcast a callback to the everyone that's registered 1623 * 1624 * @param wrapper the ServiceCallbackWrapper used in the broadcast 1625 */ 1626 private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) { 1627 if (sCallbackList != null) { 1628 // Call everyone on our callback list 1629 // Exceptions can be safely ignored 1630 int count = sCallbackList.beginBroadcast(); 1631 for (int i = 0; i < count; i++) { 1632 try { 1633 wrapper.call(sCallbackList.getBroadcastItem(i)); 1634 } catch (RemoteException e) { 1635 } 1636 } 1637 sCallbackList.finishBroadcast(); 1638 } 1639 } 1640 1641 public void loadAttachmentStatus(final long messageId, final long attachmentId, 1642 final int status, final int progress) { 1643 broadcastCallback(new ServiceCallbackWrapper() { 1644 @Override 1645 public void call(IEmailServiceCallback cb) throws RemoteException { 1646 cb.loadAttachmentStatus(messageId, attachmentId, status, progress); 1647 } 1648 }); 1649 } 1650 1651 @Override 1652 public void sendMessageStatus(long messageId, String subject, int statusCode, int progress){ 1653 } 1654 1655 @Override 1656 public void syncMailboxListStatus(long accountId, int statusCode, int progress) { 1657 } 1658 1659 @Override 1660 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) { 1661 } 1662 }; 1663 1664 public static class ControllerService extends Service { 1665 /** 1666 * Create our EmailService implementation here. For now, only loadAttachment is supported; 1667 * the intention, however, is to move more functionality to the service interface 1668 */ 1669 private final IEmailService.Stub mBinder = new IEmailService.Stub() { 1670 1671 public Bundle validate(String protocol, String host, String userName, String password, 1672 int port, boolean ssl, boolean trustCertificates) { 1673 return null; 1674 } 1675 1676 public Bundle autoDiscover(String userName, String password) { 1677 return null; 1678 } 1679 1680 public void startSync(long mailboxId, boolean userRequest) { 1681 } 1682 1683 public void stopSync(long mailboxId) { 1684 } 1685 1686 public void loadAttachment(long attachmentId, boolean background) 1687 throws RemoteException { 1688 Attachment att = Attachment.restoreAttachmentWithId(ControllerService.this, 1689 attachmentId); 1690 if (att != null) { 1691 if (Email.DEBUG) { 1692 Log.d(TAG, "loadAttachment " + attachmentId + ": " + att.mFileName); 1693 } 1694 Message msg = Message.restoreMessageWithId(ControllerService.this, 1695 att.mMessageKey); 1696 if (msg != null) { 1697 // If the message is a forward and the attachment needs downloading, we need 1698 // to retrieve the message from the source, rather than from the message 1699 // itself 1700 if ((msg.mFlags & Message.FLAG_TYPE_FORWARD) != 0) { 1701 String[] cols = Utility.getRowColumns(ControllerService.this, 1702 Body.CONTENT_URI, BODY_SOURCE_KEY_PROJECTION, WHERE_MESSAGE_KEY, 1703 new String[] {Long.toString(msg.mId)}); 1704 if (cols != null) { 1705 msg = Message.restoreMessageWithId(ControllerService.this, 1706 Long.parseLong(cols[BODY_SOURCE_KEY_COLUMN])); 1707 if (msg == null) { 1708 // TODO: We can try restoring from the deleted table here... 1709 return; 1710 } 1711 } 1712 } 1713 MessagingController legacyController = sInstance.mLegacyController; 1714 LegacyListener legacyListener = sInstance.mLegacyListener; 1715 legacyController.loadAttachment(msg.mAccountKey, msg.mId, msg.mMailboxKey, 1716 attachmentId, legacyListener, background); 1717 } else { 1718 // Send back the specific error status for this case 1719 sInstance.mCallbackProxy.loadAttachmentStatus(att.mMessageKey, attachmentId, 1720 EmailServiceStatus.MESSAGE_NOT_FOUND, 0); 1721 } 1722 } 1723 } 1724 1725 public void updateFolderList(long accountId) { 1726 } 1727 1728 public void hostChanged(long accountId) { 1729 } 1730 1731 public void setLogging(int flags) { 1732 } 1733 1734 public void sendMeetingResponse(long messageId, int response) { 1735 } 1736 1737 public void loadMore(long messageId) { 1738 } 1739 1740 // The following three methods are not implemented in this version 1741 public boolean createFolder(long accountId, String name) { 1742 return false; 1743 } 1744 1745 public boolean deleteFolder(long accountId, String name) { 1746 return false; 1747 } 1748 1749 public boolean renameFolder(long accountId, String oldName, String newName) { 1750 return false; 1751 } 1752 1753 public void setCallback(IEmailServiceCallback cb) { 1754 sCallbackList.register(cb); 1755 } 1756 1757 public void deleteAccountPIMData(long accountId) { 1758 } 1759 1760 public int searchMessages(long accountId, long mailboxId, boolean includeSubfolders, 1761 String query, int numResults, int firstResult, long destMailboxId) { 1762 return 0; 1763 } 1764 1765 @Override 1766 public int getApiLevel() { 1767 return Api.LEVEL; 1768 } 1769 }; 1770 1771 @Override 1772 public IBinder onBind(Intent intent) { 1773 return mBinder; 1774 } 1775 } 1776} 1777