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