MessagingController.java revision 0dff228dc769e141ec2a27d951963a0d705ddabb
1/* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.email; 18 19import com.android.email.mail.FetchProfile; 20import com.android.email.mail.Flag; 21import com.android.email.mail.Folder; 22import com.android.email.mail.Message; 23import com.android.email.mail.MessageRetrievalListener; 24import com.android.email.mail.MessagingException; 25import com.android.email.mail.Part; 26import com.android.email.mail.Sender; 27import com.android.email.mail.Store; 28import com.android.email.mail.StoreSynchronizer; 29import com.android.email.mail.Folder.FolderType; 30import com.android.email.mail.Folder.OpenMode; 31import com.android.email.mail.internet.MimeUtility; 32import com.android.email.mail.store.LocalStore; 33import com.android.email.mail.store.LocalStore.LocalFolder; 34import com.android.email.mail.store.LocalStore.LocalMessage; 35import com.android.email.mail.store.LocalStore.PendingCommand; 36 37import android.content.Context; 38import android.os.Process; 39import android.util.Config; 40import android.util.Log; 41 42import java.util.ArrayList; 43import java.util.Collections; 44import java.util.Date; 45import java.util.HashMap; 46import java.util.HashSet; 47import java.util.concurrent.BlockingQueue; 48import java.util.concurrent.LinkedBlockingQueue; 49 50/** 51 * Starts a long running (application) Thread that will run through commands 52 * that require remote mailbox access. This class is used to serialize and 53 * prioritize these commands. Each method that will submit a command requires a 54 * MessagingListener instance to be provided. It is expected that that listener 55 * has also been added as a registered listener using addListener(). When a 56 * command is to be executed, if the listener that was provided with the command 57 * is no longer registered the command is skipped. The design idea for the above 58 * is that when an Activity starts it registers as a listener. When it is paused 59 * it removes itself. Thus, any commands that that activity submitted are 60 * removed from the queue once the activity is no longer active. 61 */ 62public class MessagingController implements Runnable { 63 /** 64 * The maximum message size that we'll consider to be "small". A small message is downloaded 65 * in full immediately instead of in pieces. Anything over this size will be downloaded in 66 * pieces with attachments being left off completely and downloaded on demand. 67 * 68 * 69 * 25k for a "small" message was picked by educated trial and error. 70 * http://answers.google.com/answers/threadview?id=312463 claims that the 71 * average size of an email is 59k, which I feel is too large for our 72 * blind download. The following tests were performed on a download of 73 * 25 random messages. 74 * <pre> 75 * 5k - 61 seconds, 76 * 25k - 51 seconds, 77 * 55k - 53 seconds, 78 * </pre> 79 * So 25k gives good performance and a reasonable data footprint. Sounds good to me. 80 */ 81 private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024); 82 83 private static final String PENDING_COMMAND_TRASH = 84 "com.android.email.MessagingController.trash"; 85 private static final String PENDING_COMMAND_MARK_READ = 86 "com.android.email.MessagingController.markRead"; 87 private static final String PENDING_COMMAND_APPEND = 88 "com.android.email.MessagingController.append"; 89 90 private static MessagingController inst = null; 91 private BlockingQueue<Command> mCommands = new LinkedBlockingQueue<Command>(); 92 private Thread mThread; 93 94 /** 95 * All access to mListeners *must* be synchronized 96 */ 97 private HashSet<MessagingListener> mListeners = new HashSet<MessagingListener>(); 98 private boolean mBusy; 99 private Context mContext; 100 101 protected MessagingController(Context _context) { 102 mContext = _context; 103 mThread = new Thread(this); 104 mThread.start(); 105 } 106 107 /** 108 * Gets or creates the singleton instance of MessagingController. Application is used to 109 * provide a Context to classes that need it. 110 * @param application 111 * @return 112 */ 113 public synchronized static MessagingController getInstance(Context _context) { 114 if (inst == null) { 115 inst = new MessagingController(_context); 116 } 117 return inst; 118 } 119 120 /** 121 * Inject a mock controller. Used only for testing. Affects future calls to getInstance(). 122 */ 123 public static void injectMockController(MessagingController mockController) { 124 inst = mockController; 125 } 126 127 public boolean isBusy() { 128 return mBusy; 129 } 130 131 public void run() { 132 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 133 while (true) { 134 try { 135 Command command = mCommands.take(); 136 if (command.listener == null || isActiveListener(command.listener)) { 137 mBusy = true; 138 command.runnable.run(); 139 synchronized (mListeners) { 140 for (MessagingListener l : mListeners) { 141 l.controllerCommandCompleted(mCommands.size() > 0); 142 } 143 } 144 } 145 } 146 catch (Exception e) { 147 if (Config.LOGD) { 148 Log.d(Email.LOG_TAG, "Error running command", e); 149 } 150 } 151 mBusy = false; 152 } 153 } 154 155 private void put(String description, MessagingListener listener, Runnable runnable) { 156 try { 157 Command command = new Command(); 158 command.listener = listener; 159 command.runnable = runnable; 160 command.description = description; 161 mCommands.add(command); 162 } 163 catch (IllegalStateException ie) { 164 throw new Error(ie); 165 } 166 } 167 168 public void addListener(MessagingListener listener) { 169 synchronized (mListeners) { 170 mListeners.add(listener); 171 } 172 } 173 174 public void removeListener(MessagingListener listener) { 175 synchronized (mListeners) { 176 mListeners.remove(listener); 177 } 178 } 179 180 private boolean isActiveListener(MessagingListener listener) { 181 synchronized (mListeners) { 182 return mListeners.contains(listener); 183 } 184 } 185 186 /** 187 * Lists folders that are available locally and remotely. This method calls 188 * listFoldersCallback for local folders before it returns, and then for 189 * remote folders at some later point. If there are no local folders 190 * includeRemote is forced by this method. This method should be called from 191 * a Thread as it may take several seconds to list the local folders. 192 * 193 * TODO this needs to cache the remote folder list 194 * TODO break out an inner listFoldersSynchronized which could simplify checkMail 195 * 196 * @param account 197 * @param includeRemote 198 * @param listener 199 * @throws MessagingException 200 */ 201 public void listFolders( 202 final Account account, 203 boolean refreshRemote, 204 MessagingListener listener) { 205 synchronized (mListeners) { 206 for (MessagingListener l : mListeners) { 207 l.listFoldersStarted(account); 208 } 209 } 210 try { 211 Store localStore = Store.getInstance(account.getLocalStoreUri(), mContext, null); 212 Folder[] localFolders = localStore.getPersonalNamespaces(); 213 214 if (localFolders == null || localFolders.length == 0) { 215 refreshRemote = true; 216 } else { 217 synchronized (mListeners) { 218 for (MessagingListener l : mListeners) { 219 l.listFolders(account, localFolders); 220 } 221 } 222 } 223 } 224 catch (Exception e) { 225 synchronized (mListeners) { 226 for (MessagingListener l : mListeners) { 227 l.listFoldersFailed(account, e.getMessage()); 228 return; 229 } 230 } 231 } 232 if (refreshRemote) { 233 put("listFolders", listener, new Runnable() { 234 public void run() { 235 try { 236 LocalStore localStore = (LocalStore) Store.getInstance( 237 account.getLocalStoreUri(), mContext, null); 238 Store store = Store.getInstance(account.getStoreUri(), mContext, 239 localStore.getPersistentCallbacks()); 240 241 Folder[] remoteFolders = store.getPersonalNamespaces(); 242 updateAccountFolderNames(account, remoteFolders); 243 244 HashSet<String> remoteFolderNames = new HashSet<String>(); 245 for (int i = 0, count = remoteFolders.length; i < count; i++) { 246 Folder localFolder = localStore.getFolder(remoteFolders[i].getName()); 247 if (!localFolder.exists()) { 248 localFolder.create(FolderType.HOLDS_MESSAGES); 249 } 250 remoteFolderNames.add(remoteFolders[i].getName()); 251 } 252 253 Folder[] localFolders = localStore.getPersonalNamespaces(); 254 255 /* 256 * Clear out any folders that are no longer on the remote store. 257 */ 258 for (Folder localFolder : localFolders) { 259 String localFolderName = localFolder.getName(); 260 if (localFolderName.equalsIgnoreCase(Email.INBOX) || 261 localFolderName.equals(account.getTrashFolderName()) || 262 localFolderName.equals(account.getOutboxFolderName()) || 263 localFolderName.equals(account.getDraftsFolderName()) || 264 localFolderName.equals(account.getSentFolderName())) { 265 continue; 266 } 267 if (!remoteFolderNames.contains(localFolder.getName())) { 268 localFolder.delete(false); 269 } 270 } 271 272 // Signal the remote store so it can used folder-based callbacks 273 for (Folder remoteFolder : remoteFolders) { 274 Folder localFolder = localStore.getFolder(remoteFolder.getName()); 275 remoteFolder.localFolderSetupComplete(localFolder); 276 } 277 278 localFolders = localStore.getPersonalNamespaces(); 279 280 synchronized (mListeners) { 281 for (MessagingListener l : mListeners) { 282 l.listFolders(account, localFolders); 283 } 284 for (MessagingListener l : mListeners) { 285 l.listFoldersFinished(account); 286 } 287 } 288 } 289 catch (Exception e) { 290 synchronized (mListeners) { 291 for (MessagingListener l : mListeners) { 292 l.listFoldersFailed(account, ""); 293 } 294 } 295 } 296 } 297 }); 298 } else { 299 synchronized (mListeners) { 300 for (MessagingListener l : mListeners) { 301 l.listFoldersFinished(account); 302 } 303 } 304 } 305 } 306 307 /** 308 * Asks the store for a list of server-specific folder names and, if provided, updates 309 * the account record for future getFolder() operations. 310 * 311 * NOTE: Inbox is not queried, because we require it to be INBOX, and outbox is not 312 * queried, because outbox is local-only. 313 */ 314 /* package */ void updateAccountFolderNames(Account account, Folder[] remoteFolders) { 315 String trash = null; 316 String sent = null; 317 String drafts = null; 318 319 for (Folder folder : remoteFolders) { 320 Folder.FolderRole role = folder.getRole(); 321 if (role == Folder.FolderRole.TRASH) { 322 trash = folder.getName(); 323 } else if (role == Folder.FolderRole.SENT) { 324 sent = folder.getName(); 325 } else if (role == Folder.FolderRole.DRAFTS) { 326 drafts = folder.getName(); 327 } 328 } 329 330 // Do not update when null (defaults are already in place) 331 boolean commit = false; 332 if (trash != null && !trash.equals(account.getTrashFolderName())) { 333 account.setTrashFolderName(trash); 334 commit = true; 335 } 336 if (sent != null && !sent.equals(account.getSentFolderName())) { 337 account.setSentFolderName(sent); 338 commit = true; 339 } 340 if (drafts != null && !drafts.equals(account.getDraftsFolderName())) { 341 account.setDraftsFolderName(drafts); 342 commit = true; 343 } 344 if (commit) { 345 account.save(Preferences.getPreferences(mContext)); 346 } 347 } 348 349 /** 350 * List the local message store for the given folder. This work is done 351 * synchronously. 352 * 353 * @param account 354 * @param folder 355 * @param listener 356 * @throws MessagingException 357 */ 358 public void listLocalMessages(final Account account, final String folder, 359 MessagingListener listener) { 360 synchronized (mListeners) { 361 for (MessagingListener l : mListeners) { 362 l.listLocalMessagesStarted(account, folder); 363 } 364 } 365 366 try { 367 Store localStore = Store.getInstance(account.getLocalStoreUri(), mContext, null); 368 Folder localFolder = localStore.getFolder(folder); 369 localFolder.open(OpenMode.READ_WRITE, null); 370 Message[] localMessages = localFolder.getMessages(null); 371 ArrayList<Message> messages = new ArrayList<Message>(); 372 for (Message message : localMessages) { 373 if (!message.isSet(Flag.DELETED)) { 374 messages.add(message); 375 } 376 } 377 synchronized (mListeners) { 378 for (MessagingListener l : mListeners) { 379 l.listLocalMessages(account, folder, messages.toArray(new Message[0])); 380 } 381 for (MessagingListener l : mListeners) { 382 l.listLocalMessagesFinished(account, folder); 383 } 384 } 385 } 386 catch (Exception e) { 387 synchronized (mListeners) { 388 for (MessagingListener l : mListeners) { 389 l.listLocalMessagesFailed(account, folder, e.getMessage()); 390 } 391 } 392 } 393 } 394 395 public void loadMoreMessages(Account account, String folder, MessagingListener listener) { 396 try { 397 Store.StoreInfo info = Store.StoreInfo.getStoreInfo(account.getStoreUri(), mContext); 398 LocalStore localStore = (LocalStore) Store.getInstance( 399 account.getLocalStoreUri(), mContext, null); 400 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 401 int oldLimit = localFolder.getVisibleLimit(); 402 if (oldLimit <= 0) { 403 oldLimit = info.mVisibleLimitDefault; 404 } 405 localFolder.setVisibleLimit(oldLimit + info.mVisibleLimitIncrement); 406 synchronizeMailbox(account, folder, listener); 407 } 408 catch (MessagingException me) { 409 throw new RuntimeException("Unable to set visible limit on folder", me); 410 } 411 } 412 413 public void resetVisibleLimits(Account[] accounts) { 414 for (Account account : accounts) { 415 try { 416 Store.StoreInfo info = Store.StoreInfo.getStoreInfo(account.getStoreUri(), 417 mContext); 418 LocalStore localStore = 419 (LocalStore) Store.getInstance(account.getLocalStoreUri(), mContext, null); 420 localStore.resetVisibleLimits(info.mVisibleLimitDefault); 421 } 422 catch (MessagingException e) { 423 Log.e(Email.LOG_TAG, "Unable to reset visible limits", e); 424 } 425 } 426 } 427 428 /** 429 * Start background synchronization of the specified folder. 430 * @param account 431 * @param folder 432 * @param listener 433 */ 434 public void synchronizeMailbox(final Account account, final String folder, 435 MessagingListener listener) { 436 /* 437 * We don't ever sync the Outbox. 438 */ 439 if (folder.equals(account.getOutboxFolderName())) { 440 return; 441 } 442 synchronized (mListeners) { 443 for (MessagingListener l : mListeners) { 444 l.synchronizeMailboxStarted(account, folder); 445 } 446 } 447 put("synchronizeMailbox", listener, new Runnable() { 448 public void run() { 449 synchronizeMailboxSynchronous(account, folder); 450 } 451 }); 452 } 453 454 /** 455 * Start foreground synchronization of the specified folder. This is called by 456 * synchronizeMailbox or checkMail. 457 * @param account 458 * @param folder 459 * @param listener 460 */ 461 private void synchronizeMailboxSynchronous(final Account account, final String folder) { 462 synchronized (mListeners) { 463 for (MessagingListener l : mListeners) { 464 l.synchronizeMailboxStarted(account, folder); 465 } 466 } 467 try { 468 processPendingCommandsSynchronous(account); 469 470 StoreSynchronizer.SyncResults results; 471 472 // Select generic sync or store-specific sync 473 final LocalStore localStore = 474 (LocalStore) Store.getInstance(account.getLocalStoreUri(), mContext, null); 475 Store remoteStore = Store.getInstance(account.getStoreUri(), mContext, 476 localStore.getPersistentCallbacks()); 477 StoreSynchronizer customSync = remoteStore.getMessageSynchronizer(); 478 if (customSync == null) { 479 results = synchronizeMailboxGeneric(account, folder); 480 } else { 481 results = customSync.SynchronizeMessagesSynchronous( 482 account, folder, mListeners, mContext); 483 } 484 485 synchronized (mListeners) { 486 for (MessagingListener l : mListeners) { 487 l.synchronizeMailboxFinished( 488 account, 489 folder, 490 results.mTotalMessages, results.mNewMessages); 491 } 492 } 493 494 } catch (Exception e) { 495 if (Config.LOGV) { 496 Log.v(Email.LOG_TAG, "synchronizeMailbox", e); 497 } 498 synchronized (mListeners) { 499 for (MessagingListener l : mListeners) { 500 l.synchronizeMailboxFailed( 501 account, 502 folder, 503 e); 504 } 505 } 506 } 507 } 508 509 /** 510 * Generic synchronizer - used for POP3 and IMAP. 511 * 512 * TODO Break this method up into smaller chunks. 513 * 514 * @param account 515 * @param folder 516 * @return 517 * @throws MessagingException 518 */ 519 private StoreSynchronizer.SyncResults synchronizeMailboxGeneric(final Account account, 520 final String folder) throws MessagingException { 521 /* 522 * Get the message list from the local store and create an index of 523 * the uids within the list. 524 */ 525 final LocalStore localStore = 526 (LocalStore) Store.getInstance(account.getLocalStoreUri(), mContext, null); 527 final LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 528 localFolder.open(OpenMode.READ_WRITE, null); 529 Message[] localMessages = localFolder.getMessages(null); 530 HashMap<String, Message> localUidMap = new HashMap<String, Message>(); 531 int localUnreadCount = 0; 532 for (Message message : localMessages) { 533 localUidMap.put(message.getUid(), message); 534 if (!message.isSet(Flag.SEEN)) { 535 localUnreadCount++; 536 } 537 } 538 539 Store remoteStore = Store.getInstance(account.getStoreUri(), mContext, 540 localStore.getPersistentCallbacks()); 541 Folder remoteFolder = remoteStore.getFolder(folder); 542 543 /* 544 * If the folder is a "special" folder we need to see if it exists 545 * on the remote server. It if does not exist we'll try to create it. If we 546 * can't create we'll abort. This will happen on every single Pop3 folder as 547 * designed and on Imap folders during error conditions. This allows us 548 * to treat Pop3 and Imap the same in this code. 549 */ 550 if (folder.equals(account.getTrashFolderName()) || 551 folder.equals(account.getSentFolderName()) || 552 folder.equals(account.getDraftsFolderName())) { 553 if (!remoteFolder.exists()) { 554 if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { 555 return new StoreSynchronizer.SyncResults(0, 0); 556 } 557 } 558 } 559 560 /* 561 * Synchronization process: 562 Open the folder 563 Upload any local messages that are marked as PENDING_UPLOAD (Drafts, Sent, Trash) 564 Get the message count 565 Get the list of the newest Email.DEFAULT_VISIBLE_LIMIT messages 566 getMessages(messageCount - Email.DEFAULT_VISIBLE_LIMIT, messageCount) 567 See if we have each message locally, if not fetch it's flags and envelope 568 Get and update the unread count for the folder 569 Update the remote flags of any messages we have locally with an internal date 570 newer than the remote message. 571 Get the current flags for any messages we have locally but did not just download 572 Update local flags 573 For any message we have locally but not remotely, delete the local message to keep 574 cache clean. 575 Download larger parts of any new messages. 576 (Optional) Download small attachments in the background. 577 */ 578 579 /* 580 * Open the remote folder. This pre-loads certain metadata like message count. 581 */ 582 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 583 584 /* 585 * Trash any remote messages that are marked as trashed locally. 586 */ 587 588 /* 589 * Get the remote message count. 590 */ 591 int remoteMessageCount = remoteFolder.getMessageCount(); 592 593 int visibleLimit = localFolder.getVisibleLimit(); 594 if (visibleLimit <= 0) { 595 Store.StoreInfo info = Store.StoreInfo.getStoreInfo(account.getStoreUri(), mContext); 596 visibleLimit = info.mVisibleLimitDefault; 597 localFolder.setVisibleLimit(visibleLimit); 598 } 599 600 Message[] remoteMessages = new Message[0]; 601 final ArrayList<Message> unsyncedMessages = new ArrayList<Message>(); 602 HashMap<String, Message> remoteUidMap = new HashMap<String, Message>(); 603 604 int newMessageCount = 0; 605 if (remoteMessageCount > 0) { 606 /* 607 * Message numbers start at 1. 608 */ 609 int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1; 610 int remoteEnd = remoteMessageCount; 611 remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null); 612 for (Message message : remoteMessages) { 613 remoteUidMap.put(message.getUid(), message); 614 } 615 616 /* 617 * Get a list of the messages that are in the remote list but not on the 618 * local store, or messages that are in the local store but failed to download 619 * on the last sync. These are the new messages that we will download. 620 */ 621 for (Message message : remoteMessages) { 622 Message localMessage = localUidMap.get(message.getUid()); 623 if (localMessage == null) { 624 newMessageCount++; 625 } 626 if (localMessage == null || 627 (!localMessage.isSet(Flag.X_DOWNLOADED_FULL) && 628 !localMessage.isSet(Flag.X_DOWNLOADED_PARTIAL))) { 629 unsyncedMessages.add(message); 630 } 631 } 632 } 633 634 /* 635 * A list of messages that were downloaded and which did not have the Seen flag set. 636 * This will serve to indicate the true "new" message count that will be reported to 637 * the user via notification. 638 */ 639 final ArrayList<Message> newMessages = new ArrayList<Message>(); 640 641 /* 642 * Fetch the flags and envelope only of the new messages. This is intended to get us 643 * critical data as fast as possible, and then we'll fill in the details. 644 */ 645 if (unsyncedMessages.size() > 0) { 646 647 /* 648 * Reverse the order of the messages. Depending on the server this may get us 649 * fetch results for newest to oldest. If not, no harm done. 650 */ 651 Collections.reverse(unsyncedMessages); 652 653 FetchProfile fp = new FetchProfile(); 654 fp.add(FetchProfile.Item.FLAGS); 655 fp.add(FetchProfile.Item.ENVELOPE); 656 remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp, 657 new MessageRetrievalListener() { 658 public void messageFinished(Message message, int number, int ofTotal) { 659 try { 660 // Store the new message locally 661 localFolder.appendMessages(new Message[] { 662 message 663 }); 664 665 // And include it in the view 666 if (message.getSubject() != null && 667 message.getFrom() != null) { 668 /* 669 * We check to make sure that we got something worth 670 * showing (subject and from) because some protocols 671 * (POP) may not be able to give us headers for 672 * ENVELOPE, only size. 673 */ 674 synchronized (mListeners) { 675 for (MessagingListener l : mListeners) { 676 l.synchronizeMailboxNewMessage(account, folder, 677 localFolder.getMessage(message.getUid())); 678 } 679 } 680 } 681 682 if (!message.isSet(Flag.SEEN)) { 683 newMessages.add(message); 684 } 685 } 686 catch (Exception e) { 687 Log.e(Email.LOG_TAG, 688 "Error while storing downloaded message.", 689 e); 690 } 691 } 692 693 public void messageStarted(String uid, int number, int ofTotal) { 694 } 695 }); 696 } 697 698 /* 699 * Refresh the flags for any messages in the local store that we didn't just 700 * download. 701 */ 702 FetchProfile fp = new FetchProfile(); 703 fp.add(FetchProfile.Item.FLAGS); 704 remoteFolder.fetch(remoteMessages, fp, null); 705 boolean remoteSupportsSeenFlag = false; 706 for (Flag flag : remoteFolder.getPermanentFlags()) { 707 if (flag == Flag.SEEN) { 708 remoteSupportsSeenFlag = true; 709 } 710 } 711 for (Message remoteMessage : remoteMessages) { 712 Message localMessage = localFolder.getMessage(remoteMessage.getUid()); 713 if (localMessage == null) { 714 continue; 715 } 716 if (remoteMessage.isSet(Flag.SEEN) != localMessage.isSet(Flag.SEEN) 717 && remoteSupportsSeenFlag) { 718 localMessage.setFlag(Flag.SEEN, remoteMessage.isSet(Flag.SEEN)); 719 synchronized (mListeners) { 720 for (MessagingListener l : mListeners) { 721 l.synchronizeMailboxNewMessage(account, folder, localMessage); 722 } 723 } 724 } 725 } 726 727 /* 728 * Get and store the unread message count. 729 */ 730 int remoteUnreadMessageCount = remoteFolder.getUnreadMessageCount(); 731 if (remoteUnreadMessageCount == -1) { 732 if (remoteSupportsSeenFlag) { 733 /* 734 * If remote folder doesn't supported unread message count but supports 735 * seen flag, use local folder's unread message count and the size of 736 * new messages. 737 * This mode is actually not used but for non-POP3, non-IMAP. 738 */ 739 localFolder.setUnreadMessageCount(localFolder.getUnreadMessageCount() 740 + newMessages.size()); 741 } else { 742 /* 743 * If remote folder doesn't supported unread message count and doesn't 744 * support seen flag, use localUnreadCount and newMessageCount which 745 * don't rely on remote SEEN flag. 746 * This mode is used by POP3. 747 */ 748 localFolder.setUnreadMessageCount(localUnreadCount + newMessageCount); 749 } 750 } else { 751 /* 752 * If remote folder supports unread message count, 753 * use remoteUnreadMessageCount. 754 * This mode is used by IMAP. 755 */ 756 localFolder.setUnreadMessageCount(remoteUnreadMessageCount); 757 } 758 759 /* 760 * Remove any messages that are in the local store but no longer on the remote store. 761 */ 762 for (Message localMessage : localMessages) { 763 if (remoteUidMap.get(localMessage.getUid()) == null) { 764 localMessage.setFlag(Flag.X_DESTROYED, true); 765 synchronized (mListeners) { 766 for (MessagingListener l : mListeners) { 767 l.synchronizeMailboxRemovedMessage(account, folder, localMessage); 768 } 769 } 770 } 771 } 772 773 /* 774 * Now we download the actual content of messages. 775 */ 776 ArrayList<Message> largeMessages = new ArrayList<Message>(); 777 ArrayList<Message> smallMessages = new ArrayList<Message>(); 778 for (Message message : unsyncedMessages) { 779 /* 780 * Sort the messages into two buckets, small and large. Small messages will be 781 * downloaded fully and large messages will be downloaded in parts. By sorting 782 * into two buckets we can pipeline the commands for each set of messages 783 * into a single command to the server saving lots of round trips. 784 */ 785 if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) { 786 largeMessages.add(message); 787 } else { 788 smallMessages.add(message); 789 } 790 } 791 /* 792 * Grab the content of the small messages first. This is going to 793 * be very fast and at very worst will be a single up of a few bytes and a single 794 * download of 625k. 795 */ 796 fp = new FetchProfile(); 797 fp.add(FetchProfile.Item.BODY); 798 remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), 799 fp, new MessageRetrievalListener() { 800 public void messageFinished(Message message, int number, int ofTotal) { 801 try { 802 // Store the updated message locally 803 localFolder.appendMessages(new Message[] { 804 message 805 }); 806 807 Message localMessage = localFolder.getMessage(message.getUid()); 808 809 // Set a flag indicating this message has now be fully downloaded 810 localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); 811 812 // Update the listener with what we've found 813 synchronized (mListeners) { 814 for (MessagingListener l : mListeners) { 815 l.synchronizeMailboxNewMessage( 816 account, 817 folder, 818 localMessage); 819 } 820 } 821 } 822 catch (MessagingException me) { 823 824 } 825 } 826 827 public void messageStarted(String uid, int number, int ofTotal) { 828 } 829 }); 830 831 /* 832 * Now do the large messages that require more round trips. 833 */ 834 fp.clear(); 835 fp.add(FetchProfile.Item.STRUCTURE); 836 remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), 837 fp, null); 838 for (Message message : largeMessages) { 839 if (message.getBody() == null) { 840 /* 841 * The provider was unable to get the structure of the message, so 842 * we'll download a reasonable portion of the messge and mark it as 843 * incomplete so the entire thing can be downloaded later if the user 844 * wishes to download it. 845 */ 846 fp.clear(); 847 fp.add(FetchProfile.Item.BODY_SANE); 848 /* 849 * TODO a good optimization here would be to make sure that all Stores set 850 * the proper size after this fetch and compare the before and after size. If 851 * they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED 852 */ 853 854 remoteFolder.fetch(new Message[] { message }, fp, null); 855 // Store the updated message locally 856 localFolder.appendMessages(new Message[] { 857 message 858 }); 859 860 Message localMessage = localFolder.getMessage(message.getUid()); 861 862 // Set a flag indicating that the message has been partially downloaded and 863 // is ready for view. 864 localMessage.setFlag(Flag.X_DOWNLOADED_PARTIAL, true); 865 } else { 866 /* 867 * We have a structure to deal with, from which 868 * we can pull down the parts we want to actually store. 869 * Build a list of parts we are interested in. Text parts will be downloaded 870 * right now, attachments will be left for later. 871 */ 872 873 ArrayList<Part> viewables = new ArrayList<Part>(); 874 ArrayList<Part> attachments = new ArrayList<Part>(); 875 MimeUtility.collectParts(message, viewables, attachments); 876 877 /* 878 * Now download the parts we're interested in storing. 879 */ 880 for (Part part : viewables) { 881 fp.clear(); 882 fp.add(part); 883 // TODO what happens if the network connection dies? We've got partial 884 // messages with incorrect status stored. 885 remoteFolder.fetch(new Message[] { message }, fp, null); 886 } 887 // Store the updated message locally 888 localFolder.appendMessages(new Message[] { 889 message 890 }); 891 892 Message localMessage = localFolder.getMessage(message.getUid()); 893 894 // Set a flag indicating this message has been fully downloaded and can be 895 // viewed. 896 localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); 897 } 898 899 // Update the listener with what we've found 900 synchronized (mListeners) { 901 for (MessagingListener l : mListeners) { 902 l.synchronizeMailboxNewMessage( 903 account, 904 folder, 905 localFolder.getMessage(message.getUid())); 906 } 907 } 908 } 909 910 911 /* 912 * Report successful sync 913 */ 914 StoreSynchronizer.SyncResults results = new StoreSynchronizer.SyncResults( 915 remoteFolder.getMessageCount(), newMessages.size()); 916 917 remoteFolder.close(false); 918 localFolder.close(false); 919 920 return results; 921 } 922 923 private void queuePendingCommand(Account account, PendingCommand command) { 924 try { 925 LocalStore localStore = (LocalStore) Store.getInstance( 926 account.getLocalStoreUri(), mContext, null); 927 localStore.addPendingCommand(command); 928 } 929 catch (Exception e) { 930 throw new RuntimeException("Unable to enqueue pending command", e); 931 } 932 } 933 934 private void processPendingCommands(final Account account) { 935 put("processPendingCommands", null, new Runnable() { 936 public void run() { 937 try { 938 processPendingCommandsSynchronous(account); 939 } 940 catch (MessagingException me) { 941 if (Config.LOGV) { 942 Log.v(Email.LOG_TAG, "processPendingCommands", me); 943 } 944 /* 945 * Ignore any exceptions from the commands. Commands will be processed 946 * on the next round. 947 */ 948 } 949 } 950 }); 951 } 952 953 private void processPendingCommandsSynchronous(Account account) throws MessagingException { 954 LocalStore localStore = (LocalStore) Store.getInstance( 955 account.getLocalStoreUri(), mContext, null); 956 ArrayList<PendingCommand> commands = localStore.getPendingCommands(); 957 for (PendingCommand command : commands) { 958 /* 959 * We specifically do not catch any exceptions here. If a command fails it is 960 * most likely due to a server or IO error and it must be retried before any 961 * other command processes. This maintains the order of the commands. 962 */ 963 if (PENDING_COMMAND_APPEND.equals(command.command)) { 964 processPendingAppend(command, account); 965 } 966 else if (PENDING_COMMAND_MARK_READ.equals(command.command)) { 967 processPendingMarkRead(command, account); 968 } 969 else if (PENDING_COMMAND_TRASH.equals(command.command)) { 970 processPendingTrash(command, account); 971 } 972 localStore.removePendingCommand(command); 973 } 974 } 975 976 /** 977 * Process a pending append message command. This command uploads a local message to the 978 * server, first checking to be sure that the server message is not newer than 979 * the local message. Once the local message is successfully processed it is deleted so 980 * that the server message will be synchronized down without an additional copy being 981 * created. 982 * TODO update the local message UID instead of deleteing it 983 * 984 * @param command arguments = (String folder, String uid) 985 * @param account 986 * @throws MessagingException 987 */ 988 private void processPendingAppend(PendingCommand command, Account account) 989 throws MessagingException { 990 String folder = command.arguments[0]; 991 String uid = command.arguments[1]; 992 993 LocalStore localStore = (LocalStore) Store.getInstance( 994 account.getLocalStoreUri(), mContext, null); 995 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 996 LocalMessage localMessage = (LocalMessage) localFolder.getMessage(uid); 997 998 if (localMessage == null) { 999 return; 1000 } 1001 1002 Store remoteStore = Store.getInstance(account.getStoreUri(), mContext, 1003 localStore.getPersistentCallbacks()); 1004 Folder remoteFolder = remoteStore.getFolder(folder); 1005 if (!remoteFolder.exists()) { 1006 if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { 1007 return; 1008 } 1009 } 1010 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 1011 if (remoteFolder.getMode() != OpenMode.READ_WRITE) { 1012 return; 1013 } 1014 1015 Message remoteMessage = null; 1016 if (!localMessage.getUid().startsWith("Local") 1017 && !localMessage.getUid().contains("-")) { 1018 remoteMessage = remoteFolder.getMessage(localMessage.getUid()); 1019 } 1020 1021 if (remoteMessage == null) { 1022 /* 1023 * If the message does not exist remotely we just upload it and then 1024 * update our local copy with the new uid. 1025 */ 1026 FetchProfile fp = new FetchProfile(); 1027 fp.add(FetchProfile.Item.BODY); 1028 localFolder.fetch(new Message[] { localMessage }, fp, null); 1029 String oldUid = localMessage.getUid(); 1030 remoteFolder.appendMessages(new Message[] { localMessage }); 1031 localFolder.changeUid(localMessage); 1032 synchronized (mListeners) { 1033 for (MessagingListener l : mListeners) { 1034 l.messageUidChanged(account, folder, oldUid, localMessage.getUid()); 1035 } 1036 } 1037 } 1038 else { 1039 /* 1040 * If the remote message exists we need to determine which copy to keep. 1041 */ 1042 /* 1043 * See if the remote message is newer than ours. 1044 */ 1045 FetchProfile fp = new FetchProfile(); 1046 fp.add(FetchProfile.Item.ENVELOPE); 1047 remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); 1048 Date localDate = localMessage.getInternalDate(); 1049 Date remoteDate = remoteMessage.getInternalDate(); 1050 if (remoteDate.compareTo(localDate) > 0) { 1051 /* 1052 * If the remote message is newer than ours we'll just 1053 * delete ours and move on. A sync will get the server message 1054 * if we need to be able to see it. 1055 */ 1056 localMessage.setFlag(Flag.DELETED, true); 1057 } 1058 else { 1059 /* 1060 * Otherwise we'll upload our message and then delete the remote message. 1061 */ 1062 fp.clear(); 1063 fp = new FetchProfile(); 1064 fp.add(FetchProfile.Item.BODY); 1065 localFolder.fetch(new Message[] { localMessage }, fp, null); 1066 String oldUid = localMessage.getUid(); 1067 remoteFolder.appendMessages(new Message[] { localMessage }); 1068 localFolder.changeUid(localMessage); 1069 synchronized (mListeners) { 1070 for (MessagingListener l : mListeners) { 1071 l.messageUidChanged(account, folder, oldUid, localMessage.getUid()); 1072 } 1073 } 1074 remoteMessage.setFlag(Flag.DELETED, true); 1075 } 1076 } 1077 } 1078 1079 /** 1080 * Process a pending trash message command. 1081 * 1082 * @param command arguments = (String folder, String uid) 1083 * @param account 1084 * @throws MessagingException 1085 */ 1086 private void processPendingTrash(PendingCommand command, final Account account) 1087 throws MessagingException { 1088 String folder = command.arguments[0]; 1089 String uid = command.arguments[1]; 1090 1091 final LocalStore localStore = (LocalStore) Store.getInstance( 1092 account.getLocalStoreUri(), mContext, null); 1093 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 1094 1095 Store remoteStore = Store.getInstance(account.getStoreUri(), mContext, 1096 localStore.getPersistentCallbacks()); 1097 Folder remoteFolder = remoteStore.getFolder(folder); 1098 if (!remoteFolder.exists()) { 1099 return; 1100 } 1101 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 1102 if (remoteFolder.getMode() != OpenMode.READ_WRITE) { 1103 return; 1104 } 1105 1106 Message remoteMessage = null; 1107 if (!uid.startsWith("Local") 1108 && !uid.contains("-")) { 1109 remoteMessage = remoteFolder.getMessage(uid); 1110 } 1111 if (remoteMessage == null) { 1112 return; 1113 } 1114 1115 Folder remoteTrashFolder = remoteStore.getFolder(account.getTrashFolderName()); 1116 /* 1117 * Attempt to copy the remote message to the remote trash folder. 1118 */ 1119 if (!remoteTrashFolder.exists()) { 1120 /* 1121 * If the remote trash folder doesn't exist we try to create it. 1122 */ 1123 remoteTrashFolder.create(FolderType.HOLDS_MESSAGES); 1124 } 1125 1126 if (remoteTrashFolder.exists()) { 1127 /* 1128 * Because remoteTrashFolder may be new, we need to explicitly open it 1129 * and pass in the persistence callbacks. 1130 */ 1131 final LocalFolder localTrashFolder = 1132 (LocalFolder) localStore.getFolder(account.getTrashFolderName()); 1133 remoteTrashFolder.open(OpenMode.READ_WRITE, localTrashFolder.getPersistentCallbacks()); 1134 if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) { 1135 return; 1136 } 1137 1138 remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder, 1139 new Folder.MessageUpdateCallbacks() { 1140 public void onMessageUidChange(Message message, String newUid) 1141 throws MessagingException { 1142 // update the UID in the local trash folder, because some stores will 1143 // have to change it when copying to remoteTrashFolder 1144 LocalMessage localMessage = 1145 (LocalMessage) localTrashFolder.getMessage(message.getUid()); 1146 if(localMessage != null) { 1147 localMessage.setUid(newUid); 1148 localTrashFolder.updateMessage(localMessage); 1149 } 1150 } 1151 1152 /** 1153 * This will be called if the deleted message doesn't exist and can't be 1154 * deleted (e.g. it was already deleted from the server.) In this case, 1155 * attempt to delete the local copy as well. 1156 */ 1157 public void onMessageNotFound(Message message) throws MessagingException { 1158 LocalMessage localMessage = 1159 (LocalMessage) localTrashFolder.getMessage(message.getUid()); 1160 if (localMessage != null) { 1161 localMessage.setFlag(Flag.DELETED, true); 1162 } 1163 } 1164 1165 } 1166 ); 1167 } 1168 1169 remoteMessage.setFlag(Flag.DELETED, true); 1170 remoteFolder.expunge(); 1171 } 1172 1173 /** 1174 * Processes a pending mark read or unread command. 1175 * 1176 * @param command arguments = (String folder, String uid, boolean read) 1177 * @param account 1178 */ 1179 private void processPendingMarkRead(PendingCommand command, Account account) 1180 throws MessagingException { 1181 String folder = command.arguments[0]; 1182 String uid = command.arguments[1]; 1183 boolean read = Boolean.parseBoolean(command.arguments[2]); 1184 1185 LocalStore localStore = (LocalStore) Store.getInstance( 1186 account.getLocalStoreUri(), mContext, null); 1187 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 1188 1189 Store remoteStore = Store.getInstance(account.getStoreUri(), mContext, 1190 localStore.getPersistentCallbacks()); 1191 Folder remoteFolder = remoteStore.getFolder(folder); 1192 if (!remoteFolder.exists()) { 1193 return; 1194 } 1195 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 1196 if (remoteFolder.getMode() != OpenMode.READ_WRITE) { 1197 return; 1198 } 1199 Message remoteMessage = null; 1200 if (!uid.startsWith("Local") 1201 && !uid.contains("-")) { 1202 remoteMessage = remoteFolder.getMessage(uid); 1203 } 1204 if (remoteMessage == null) { 1205 return; 1206 } 1207 remoteMessage.setFlag(Flag.SEEN, read); 1208 } 1209 1210 /** 1211 * Mark the message with the given account, folder and uid either Seen or not Seen. 1212 * @param account 1213 * @param folder 1214 * @param uid 1215 * @param seen 1216 */ 1217 public void markMessageRead( 1218 final Account account, 1219 final String folder, 1220 final String uid, 1221 final boolean seen) { 1222 try { 1223 Store localStore = Store.getInstance(account.getLocalStoreUri(), mContext, null); 1224 Folder localFolder = localStore.getFolder(folder); 1225 localFolder.open(OpenMode.READ_WRITE, null); 1226 1227 Message message = localFolder.getMessage(uid); 1228 message.setFlag(Flag.SEEN, seen); 1229 PendingCommand command = new PendingCommand(); 1230 command.command = PENDING_COMMAND_MARK_READ; 1231 command.arguments = new String[] { folder, uid, Boolean.toString(seen) }; 1232 queuePendingCommand(account, command); 1233 processPendingCommands(account); 1234 } 1235 catch (MessagingException me) { 1236 throw new RuntimeException(me); 1237 } 1238 } 1239 1240 private void loadMessageForViewRemote(final Account account, final String folder, 1241 final String uid, MessagingListener listener) { 1242 put("loadMessageForViewRemote", listener, new Runnable() { 1243 public void run() { 1244 try { 1245 LocalStore localStore = (LocalStore) Store.getInstance( 1246 account.getLocalStoreUri(), mContext, null); 1247 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 1248 localFolder.open(OpenMode.READ_WRITE, null); 1249 1250 Message message = localFolder.getMessage(uid); 1251 1252 if (message.isSet(Flag.X_DOWNLOADED_FULL)) { 1253 /* 1254 * If the message has been synchronized since we were called we'll 1255 * just hand it back cause it's ready to go. 1256 */ 1257 FetchProfile fp = new FetchProfile(); 1258 fp.add(FetchProfile.Item.ENVELOPE); 1259 fp.add(FetchProfile.Item.BODY); 1260 localFolder.fetch(new Message[] { message }, fp, null); 1261 1262 synchronized (mListeners) { 1263 for (MessagingListener l : mListeners) { 1264 l.loadMessageForViewBodyAvailable(account, folder, uid, message); 1265 } 1266 for (MessagingListener l : mListeners) { 1267 l.loadMessageForViewFinished(account, folder, uid, message); 1268 } 1269 } 1270 localFolder.close(false); 1271 return; 1272 } 1273 1274 /* 1275 * At this point the message is not available, so we need to download it 1276 * fully if possible. 1277 */ 1278 1279 Store remoteStore = Store.getInstance(account.getStoreUri(), mContext, 1280 localStore.getPersistentCallbacks()); 1281 Folder remoteFolder = remoteStore.getFolder(folder); 1282 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 1283 1284 // Get the remote message and fully download it (and save into local store) 1285 1286 if (remoteStore.requireStructurePrefetch()) { 1287 // For remote stores that require it, prefetch the message structure. 1288 FetchProfile fp = new FetchProfile(); 1289 fp.add(FetchProfile.Item.STRUCTURE); 1290 localFolder.fetch(new Message[] { message }, fp, null); 1291 1292 ArrayList<Part> viewables = new ArrayList<Part>(); 1293 ArrayList<Part> attachments = new ArrayList<Part>(); 1294 MimeUtility.collectParts(message, viewables, attachments); 1295 fp.clear(); 1296 for (Part part : viewables) { 1297 fp.add(part); 1298 } 1299 1300 remoteFolder.fetch(new Message[] { message }, fp, null); 1301 1302 // Store the updated message locally 1303 localFolder.updateMessage((LocalMessage)message); 1304 1305 } else { 1306 // Most remote stores can directly obtain the message using only uid 1307 Message remoteMessage = remoteFolder.getMessage(uid); 1308 FetchProfile fp = new FetchProfile(); 1309 fp.add(FetchProfile.Item.BODY); 1310 remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); 1311 1312 // Store the message locally 1313 localFolder.appendMessages(new Message[] { remoteMessage }); 1314 } 1315 1316 // Now obtain the local copy for further access & manipulation 1317 message = localFolder.getMessage(uid); 1318 FetchProfile fp = new FetchProfile(); 1319 fp.add(FetchProfile.Item.BODY); 1320 localFolder.fetch(new Message[] { message }, fp, null); 1321 1322 // This is a view message request, so mark it read 1323 if (!message.isSet(Flag.SEEN)) { 1324 markMessageRead(account, folder, uid, true); 1325 } 1326 1327 // Mark that this message is now fully synched 1328 message.setFlag(Flag.X_DOWNLOADED_FULL, true); 1329 1330 synchronized (mListeners) { 1331 for (MessagingListener l : mListeners) { 1332 l.loadMessageForViewBodyAvailable(account, folder, uid, message); 1333 } 1334 for (MessagingListener l : mListeners) { 1335 l.loadMessageForViewFinished(account, folder, uid, message); 1336 } 1337 } 1338 remoteFolder.close(false); 1339 localFolder.close(false); 1340 } 1341 catch (Exception e) { 1342 synchronized (mListeners) { 1343 for (MessagingListener l : mListeners) { 1344 l.loadMessageForViewFailed(account, folder, uid, e.getMessage()); 1345 } 1346 } 1347 } 1348 } 1349 }); 1350 } 1351 1352 private boolean isInlineImage(Part part) throws MessagingException { 1353 String contentId = part.getContentId(); 1354 String mimeType = part.getMimeType(); 1355 return contentId != null && mimeType != null && mimeType.startsWith("image/"); 1356 } 1357 1358 public void loadInlineImagesForView(final Account account, final Message message, 1359 MessagingListener listener) { 1360 synchronized (mListeners) { 1361 for (MessagingListener l : mListeners) { 1362 l.loadInlineImagesForViewStarted(account, message); 1363 } 1364 } 1365 try { 1366 LocalStore localStore = (LocalStore)Store.getInstance(account.getLocalStoreUri(), 1367 mContext, null); 1368 LocalFolder localFolder = (LocalFolder)message.getFolder(); 1369 localFolder.open(OpenMode.READ_WRITE, null); 1370 1371 // Download inline images if necessary. 1372 Folder remoteFolder = null; 1373 ArrayList<Part> viewables = new ArrayList<Part>(); 1374 ArrayList<Part> attachments = new ArrayList<Part>(); 1375 MimeUtility.collectParts(message, viewables, attachments); 1376 FetchProfile fp = new FetchProfile(); 1377 Message[] localMessages = new Message[] { 1378 message 1379 }; 1380 for (Part part : attachments) { 1381 if (isInlineImage(part) && part.getBody() == null) { 1382 if (remoteFolder == null) { 1383 Store remoteStore = Store.getInstance(account.getStoreUri(), mContext, 1384 localStore.getPersistentCallbacks()); 1385 remoteFolder = remoteStore.getFolder(message.getFolder().getName()); 1386 remoteFolder 1387 .open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 1388 } 1389 fp.clear(); 1390 fp.add(part); 1391 remoteFolder.fetch(localMessages, fp, null); 1392 localFolder.updateMessage((LocalMessage)message); 1393 synchronized (mListeners) { 1394 for (MessagingListener l : mListeners) { 1395 l.loadInlineImagesForViewOneAvailable(account, message, part); 1396 } 1397 } 1398 } 1399 } 1400 if (remoteFolder != null) { 1401 remoteFolder.close(false); 1402 } 1403 synchronized (mListeners) { 1404 for (MessagingListener l : mListeners) { 1405 l.loadInlineImagesForViewFinished(account, message); 1406 } 1407 } 1408 } catch (Exception e) { 1409 synchronized (mListeners) { 1410 for (MessagingListener l : mListeners) { 1411 l.loadInlineImagesForViewFailed(account, message); 1412 } 1413 } 1414 } 1415 } 1416 1417 public void loadMessageForView(final Account account, final String folder, final String uid, 1418 MessagingListener listener) { 1419 synchronized (mListeners) { 1420 for (MessagingListener l : mListeners) { 1421 l.loadMessageForViewStarted(account, folder, uid); 1422 } 1423 } 1424 try { 1425 Store localStore = Store.getInstance(account.getLocalStoreUri(), mContext, null); 1426 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 1427 localFolder.open(OpenMode.READ_WRITE, null); 1428 1429 Message message = localFolder.getMessage(uid); 1430 1431 synchronized (mListeners) { 1432 for (MessagingListener l : mListeners) { 1433 l.loadMessageForViewHeadersAvailable(account, folder, uid, message); 1434 } 1435 } 1436 1437 if (!message.isSet(Flag.X_DOWNLOADED_FULL)) { 1438 loadMessageForViewRemote(account, folder, uid, listener); 1439 localFolder.close(false); 1440 return; 1441 } 1442 1443 if (!message.isSet(Flag.SEEN)) { 1444 markMessageRead(account, folder, uid, true); 1445 } 1446 1447 FetchProfile fp = new FetchProfile(); 1448 fp.add(FetchProfile.Item.ENVELOPE); 1449 fp.add(FetchProfile.Item.BODY); 1450 localFolder.fetch(new Message[] { 1451 message 1452 }, fp, null); 1453 1454 synchronized (mListeners) { 1455 for (MessagingListener l : mListeners) { 1456 l.loadMessageForViewBodyAvailable(account, folder, uid, message); 1457 } 1458 for (MessagingListener l : mListeners) { 1459 l.loadMessageForViewFinished(account, folder, uid, message); 1460 } 1461 } 1462 localFolder.close(false); 1463 } 1464 catch (Exception e) { 1465 synchronized (mListeners) { 1466 for (MessagingListener l : mListeners) { 1467 l.loadMessageForViewFailed(account, folder, uid, e.getMessage()); 1468 } 1469 } 1470 } 1471 } 1472 1473 /** 1474 * Attempts to load the attachment specified by part from the given account and message. 1475 * @param account 1476 * @param message 1477 * @param part 1478 * @param listener 1479 */ 1480 public void loadAttachment( 1481 final Account account, 1482 final Message message, 1483 final Part part, 1484 final Object tag, 1485 MessagingListener listener) { 1486 /* 1487 * Check if the attachment has already been downloaded. If it has there's no reason to 1488 * download it, so we just tell the listener that it's ready to go. 1489 */ 1490 try { 1491 if (part.getBody() != null) { 1492 synchronized (mListeners) { 1493 for (MessagingListener l : mListeners) { 1494 l.loadAttachmentStarted(account, message, part, tag, false); 1495 } 1496 for (MessagingListener l : mListeners) { 1497 l.loadAttachmentFinished(account, message, part, tag); 1498 } 1499 } 1500 return; 1501 } 1502 } 1503 catch (MessagingException me) { 1504 /* 1505 * If the header isn't there the attachment isn't downloaded yet, so just continue 1506 * on. 1507 */ 1508 } 1509 1510 synchronized (mListeners) { 1511 for (MessagingListener l : mListeners) { 1512 l.loadAttachmentStarted(account, message, part, tag, true); 1513 } 1514 } 1515 1516 put("loadAttachment", listener, new Runnable() { 1517 public void run() { 1518 try { 1519 LocalStore localStore = (LocalStore) Store.getInstance( 1520 account.getLocalStoreUri(), mContext, null); 1521 /* 1522 * We clear out any attachments already cached in the entire store except 1523 * inline images and then we update the passed in message to reflect there are 1524 * no cached attachments. This is in support of limiting the account to having' 1525 * one attachment downloaded at a time. 1526 */ 1527 localStore.pruneCachedAttachments(); 1528 ArrayList<Part> viewables = new ArrayList<Part>(); 1529 ArrayList<Part> attachments = new ArrayList<Part>(); 1530 MimeUtility.collectParts(message, viewables, attachments); 1531 for (Part attachment : attachments) { 1532 if (!isInlineImage(attachment)) { 1533 attachment.setBody(null); 1534 } 1535 } 1536 Store remoteStore = Store.getInstance(account.getStoreUri(), mContext, 1537 localStore.getPersistentCallbacks()); 1538 LocalFolder localFolder = 1539 (LocalFolder) localStore.getFolder(message.getFolder().getName()); 1540 Folder remoteFolder = remoteStore.getFolder(message.getFolder().getName()); 1541 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 1542 1543 FetchProfile fp = new FetchProfile(); 1544 fp.add(part); 1545 remoteFolder.fetch(new Message[] { message }, fp, null); 1546 localFolder.updateMessage((LocalMessage)message); 1547 localFolder.close(false); 1548 synchronized (mListeners) { 1549 for (MessagingListener l : mListeners) { 1550 l.loadAttachmentFinished(account, message, part, tag); 1551 } 1552 } 1553 } 1554 catch (MessagingException me) { 1555 if (Config.LOGV) { 1556 Log.v(Email.LOG_TAG, "", me); 1557 } 1558 synchronized (mListeners) { 1559 for (MessagingListener l : mListeners) { 1560 l.loadAttachmentFailed(account, message, part, tag, me.getMessage()); 1561 } 1562 } 1563 } 1564 } 1565 }); 1566 } 1567 1568 /** 1569 * Stores the given message in the Outbox and starts a sendPendingMessages command to 1570 * attempt to send the message. 1571 * @param account 1572 * @param message 1573 * @param listener 1574 */ 1575 public void sendMessage(final Account account, 1576 final Message message, 1577 MessagingListener listener) { 1578 try { 1579 Store localStore = Store.getInstance(account.getLocalStoreUri(), mContext, null); 1580 LocalFolder localFolder = 1581 (LocalFolder) localStore.getFolder(account.getOutboxFolderName()); 1582 localFolder.open(OpenMode.READ_WRITE, null); 1583 localFolder.appendMessages(new Message[] { 1584 message 1585 }); 1586 Message localMessage = localFolder.getMessage(message.getUid()); 1587 localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); 1588 localFolder.close(false); 1589 sendPendingMessages(account, null); 1590 } 1591 catch (Exception e) { 1592// synchronized (mListeners) { 1593// for (MessagingListener l : mListeners) { 1594// // TODO general failed 1595// } 1596// } 1597 } 1598 } 1599 1600 /** 1601 * Attempt to send any messages that are sitting in the Outbox. 1602 * @param account 1603 * @param listener 1604 */ 1605 public void sendPendingMessages(final Account account, 1606 MessagingListener listener) { 1607 put("sendPendingMessages", listener, new Runnable() { 1608 public void run() { 1609 sendPendingMessagesSynchronous(account); 1610 } 1611 }); 1612 } 1613 1614 /** 1615 * Attempt to send any messages that are sitting in the Outbox. 1616 * @param account 1617 * @param listener 1618 */ 1619 public void sendPendingMessagesSynchronous(final Account account) { 1620 try { 1621 LocalStore localStore = (LocalStore) Store.getInstance( 1622 account.getLocalStoreUri(), mContext, null); 1623 Folder localFolder = localStore.getFolder(account.getOutboxFolderName()); 1624 if (!localFolder.exists()) { 1625 return; 1626 } 1627 localFolder.open(OpenMode.READ_WRITE, null); 1628 1629 Message[] localMessages = localFolder.getMessages(null); 1630 1631 /* 1632 * The profile we will use to pull all of the content 1633 * for a given local message into memory for sending. 1634 */ 1635 FetchProfile fp = new FetchProfile(); 1636 fp.add(FetchProfile.Item.ENVELOPE); 1637 fp.add(FetchProfile.Item.BODY); 1638 1639 LocalFolder localSentFolder = 1640 (LocalFolder) localStore.getFolder(account.getSentFolderName()); 1641 1642 // Determine if upload to "sent" folder is necessary 1643 Store remoteStore = Store.getInstance( 1644 account.getStoreUri(), mContext, localStore.getPersistentCallbacks()); 1645 boolean requireCopyMessageToSentFolder = remoteStore.requireCopyMessageToSentFolder(); 1646 1647 Sender sender = Sender.getInstance(account.getSenderUri(), mContext); 1648 for (Message message : localMessages) { 1649 try { 1650 localFolder.fetch(new Message[] { message }, fp, null); 1651 try { 1652 // Send message using Sender 1653 message.setFlag(Flag.X_SEND_IN_PROGRESS, true); 1654 sender.sendMessage(message); 1655 message.setFlag(Flag.X_SEND_IN_PROGRESS, false); 1656 1657 // Upload to "sent" folder if not supported server-side 1658 if (requireCopyMessageToSentFolder) { 1659 localFolder.copyMessages( 1660 new Message[] { message },localSentFolder, null); 1661 PendingCommand command = new PendingCommand(); 1662 command.command = PENDING_COMMAND_APPEND; 1663 command.arguments = 1664 new String[] { localSentFolder.getName(), message.getUid() }; 1665 queuePendingCommand(account, command); 1666 processPendingCommands(account); 1667 } 1668 1669 // And delete from outbox 1670 message.setFlag(Flag.X_DESTROYED, true); 1671 } 1672 catch (Exception e) { 1673 message.setFlag(Flag.X_SEND_FAILED, true); 1674 synchronized (mListeners) { 1675 for (MessagingListener l : mListeners) { 1676 l.sendPendingMessageFailed(account, message, e); 1677 } 1678 } 1679 } 1680 } 1681 catch (Exception e) { 1682 synchronized (mListeners) { 1683 for (MessagingListener l : mListeners) { 1684 l.sendPendingMessageFailed(account, message, e); 1685 } 1686 } 1687 } 1688 } 1689 localFolder.expunge(); 1690 synchronized (mListeners) { 1691 for (MessagingListener l : mListeners) { 1692 l.sendPendingMessagesCompleted(account); 1693 } 1694 } 1695 } 1696 catch (Exception e) { 1697 synchronized (mListeners) { 1698 for (MessagingListener l : mListeners) { 1699 l.sendPendingMessagesFailed(account, e); 1700 } 1701 } 1702 } 1703 } 1704 1705 /** 1706 * We do the local portion of this synchronously because other activities may have to make 1707 * updates based on what happens here 1708 * @param account 1709 * @param folder 1710 * @param message 1711 * @param listener 1712 */ 1713 public void deleteMessage(final Account account, final String folder, final Message message, 1714 MessagingListener listener) { 1715 if (folder.equals(account.getTrashFolderName())) { 1716 return; 1717 } 1718 try { 1719 Store localStore = Store.getInstance(account.getLocalStoreUri(), mContext, null); 1720 Folder localFolder = localStore.getFolder(folder); 1721 Folder localTrashFolder = localStore.getFolder(account.getTrashFolderName()); 1722 1723 localFolder.copyMessages(new Message[] { message }, localTrashFolder, null); 1724 message.setFlag(Flag.DELETED, true); 1725 1726 if (account.getDeletePolicy() == Account.DELETE_POLICY_ON_DELETE) { 1727 PendingCommand command = new PendingCommand(); 1728 command.command = PENDING_COMMAND_TRASH; 1729 command.arguments = new String[] { folder, message.getUid() }; 1730 queuePendingCommand(account, command); 1731 processPendingCommands(account); 1732 } 1733 } 1734 catch (MessagingException me) { 1735 throw new RuntimeException("Error deleting message from local store.", me); 1736 } 1737 } 1738 1739 public void emptyTrash(final Account account, MessagingListener listener) { 1740 put("emptyTrash", listener, new Runnable() { 1741 public void run() { 1742 // TODO IMAP 1743 try { 1744 Store localStore = Store.getInstance( 1745 account.getLocalStoreUri(), mContext, null); 1746 Folder localFolder = localStore.getFolder(account.getTrashFolderName()); 1747 localFolder.open(OpenMode.READ_WRITE, null); 1748 Message[] messages = localFolder.getMessages(null); 1749 localFolder.setFlags(messages, new Flag[] { 1750 Flag.DELETED 1751 }, true); 1752 localFolder.close(true); 1753 synchronized (mListeners) { 1754 for (MessagingListener l : mListeners) { 1755 l.emptyTrashCompleted(account); 1756 } 1757 } 1758 } 1759 catch (Exception e) { 1760 // TODO 1761 if (Config.LOGV) { 1762 Log.v(Email.LOG_TAG, "emptyTrash"); 1763 } 1764 } 1765 } 1766 }); 1767 } 1768 1769 /** 1770 * Checks mail for one or multiple accounts. If account is null all accounts 1771 * are checked. 1772 * 1773 * TODO: There is no use case for "check all accounts". Clean up this API to remove 1774 * that case. Callers can supply the appropriate list. 1775 * 1776 * TODO: Better protection against a failure in account n, which should not prevent 1777 * syncing account in accounts n+1 and beyond. 1778 * 1779 * @param context 1780 * @param accounts List of accounts to check, or null to check all accounts 1781 * @param listener 1782 */ 1783 public void checkMail(final Context context, Account[] accounts, 1784 final MessagingListener listener) { 1785 /** 1786 * Note: The somewhat tortured logic here is to guarantee proper ordering of events: 1787 * listeners: checkMailStarted 1788 * account 1: list folders 1789 * account 1: sync messages 1790 * account 2: list folders 1791 * account 2: sync messages 1792 * ... 1793 * account n: list folders 1794 * account n: sync messages 1795 * listeners: checkMailFinished 1796 */ 1797 synchronized (mListeners) { 1798 for (MessagingListener l : mListeners) { 1799 l.checkMailStarted(context, null); // TODO this needs to pass the actual array 1800 } 1801 } 1802 if (accounts == null) { 1803 accounts = Preferences.getPreferences(context).getAccounts(); 1804 } 1805 for (final Account account : accounts) { 1806 listFolders(account, true, null); 1807 1808 put("checkMail", listener, new Runnable() { 1809 public void run() { 1810 sendPendingMessagesSynchronous(account); 1811 synchronizeMailboxSynchronous(account, Email.INBOX); 1812 } 1813 }); 1814 } 1815 put("checkMailFinished", listener, new Runnable() { 1816 public void run() { 1817 synchronized (mListeners) { 1818 for (MessagingListener l : mListeners) { 1819 l.checkMailFinished(context, null); // TODO this needs to pass actual array 1820 } 1821 } 1822 } 1823 }); 1824 } 1825 1826 public void saveDraft(final Account account, final Message message) { 1827 try { 1828 Store localStore = Store.getInstance(account.getLocalStoreUri(), mContext, null); 1829 LocalFolder localFolder = 1830 (LocalFolder) localStore.getFolder(account.getDraftsFolderName()); 1831 localFolder.open(OpenMode.READ_WRITE, null); 1832 localFolder.appendMessages(new Message[] { 1833 message 1834 }); 1835 Message localMessage = localFolder.getMessage(message.getUid()); 1836 localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); 1837 1838 PendingCommand command = new PendingCommand(); 1839 command.command = PENDING_COMMAND_APPEND; 1840 command.arguments = new String[] { 1841 localFolder.getName(), 1842 localMessage.getUid() }; 1843 queuePendingCommand(account, command); 1844 processPendingCommands(account); 1845 } 1846 catch (MessagingException e) { 1847 Log.e(Email.LOG_TAG, "Unable to save message as draft.", e); 1848 } 1849 } 1850 1851 class Command { 1852 public Runnable runnable; 1853 1854 public MessagingListener listener; 1855 1856 public String description; 1857 1858 @Override 1859 public String toString() { 1860 return description; 1861 } 1862 } 1863} 1864