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