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