MessagingController.java revision 235609d04e95db2097efcc3c999a8fbd6c1ae23e
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 for (Message message : localMessages) { 532 localUidMap.put(message.getUid(), message); 533 } 534 535 Store remoteStore = Store.getInstance(account.getStoreUri(), mContext, 536 localStore.getPersistentCallbacks()); 537 Folder remoteFolder = remoteStore.getFolder(folder); 538 539 /* 540 * If the folder is a "special" folder we need to see if it exists 541 * on the remote server. It if does not exist we'll try to create it. If we 542 * can't create we'll abort. This will happen on every single Pop3 folder as 543 * designed and on Imap folders during error conditions. This allows us 544 * to treat Pop3 and Imap the same in this code. 545 */ 546 if (folder.equals(account.getTrashFolderName()) || 547 folder.equals(account.getSentFolderName()) || 548 folder.equals(account.getDraftsFolderName())) { 549 if (!remoteFolder.exists()) { 550 if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { 551 return new StoreSynchronizer.SyncResults(0, 0); 552 } 553 } 554 } 555 556 /* 557 * Synchronization process: 558 Open the folder 559 Upload any local messages that are marked as PENDING_UPLOAD (Drafts, Sent, Trash) 560 Get the message count 561 Get the list of the newest Email.DEFAULT_VISIBLE_LIMIT messages 562 getMessages(messageCount - Email.DEFAULT_VISIBLE_LIMIT, messageCount) 563 See if we have each message locally, if not fetch it's flags and envelope 564 Get and update the unread count for the folder 565 Update the remote flags of any messages we have locally with an internal date 566 newer than the remote message. 567 Get the current flags for any messages we have locally but did not just download 568 Update local flags 569 For any message we have locally but not remotely, delete the local message to keep 570 cache clean. 571 Download larger parts of any new messages. 572 (Optional) Download small attachments in the background. 573 */ 574 575 /* 576 * Open the remote folder. This pre-loads certain metadata like message count. 577 */ 578 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 579 580 /* 581 * Trash any remote messages that are marked as trashed locally. 582 */ 583 584 /* 585 * Get the remote message count. 586 */ 587 int remoteMessageCount = remoteFolder.getMessageCount(); 588 589 int visibleLimit = localFolder.getVisibleLimit(); 590 if (visibleLimit <= 0) { 591 Store.StoreInfo info = Store.StoreInfo.getStoreInfo(account.getStoreUri(), mContext); 592 visibleLimit = info.mVisibleLimitDefault; 593 localFolder.setVisibleLimit(visibleLimit); 594 } 595 596 Message[] remoteMessages = new Message[0]; 597 final ArrayList<Message> unsyncedMessages = new ArrayList<Message>(); 598 HashMap<String, Message> remoteUidMap = new HashMap<String, Message>(); 599 600 if (remoteMessageCount > 0) { 601 /* 602 * Message numbers start at 1. 603 */ 604 int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1; 605 int remoteEnd = remoteMessageCount; 606 remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null); 607 for (Message message : remoteMessages) { 608 remoteUidMap.put(message.getUid(), message); 609 } 610 611 /* 612 * Get a list of the messages that are in the remote list but not on the 613 * local store, or messages that are in the local store but failed to download 614 * on the last sync. These are the new messages that we will download. 615 */ 616 for (Message message : remoteMessages) { 617 Message localMessage = localUidMap.get(message.getUid()); 618 if (localMessage == null || 619 (!localMessage.isSet(Flag.X_DOWNLOADED_FULL) && 620 !localMessage.isSet(Flag.X_DOWNLOADED_PARTIAL))) { 621 unsyncedMessages.add(message); 622 } 623 } 624 } 625 626 /* 627 * A list of messages that were downloaded and which did not have the Seen flag set. 628 * This will serve to indicate the true "new" message count that will be reported to 629 * the user via notification. 630 */ 631 final ArrayList<Message> newMessages = new ArrayList<Message>(); 632 633 /* 634 * Fetch the flags and envelope only of the new messages. This is intended to get us 635 * critical data as fast as possible, and then we'll fill in the details. 636 */ 637 if (unsyncedMessages.size() > 0) { 638 639 /* 640 * Reverse the order of the messages. Depending on the server this may get us 641 * fetch results for newest to oldest. If not, no harm done. 642 */ 643 Collections.reverse(unsyncedMessages); 644 645 FetchProfile fp = new FetchProfile(); 646 fp.add(FetchProfile.Item.FLAGS); 647 fp.add(FetchProfile.Item.ENVELOPE); 648 remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp, 649 new MessageRetrievalListener() { 650 public void messageFinished(Message message, int number, int ofTotal) { 651 try { 652 // Store the new message locally 653 localFolder.appendMessages(new Message[] { 654 message 655 }); 656 657 // And include it in the view 658 if (message.getSubject() != null && 659 message.getFrom() != null) { 660 /* 661 * We check to make sure that we got something worth 662 * showing (subject and from) because some protocols 663 * (POP) may not be able to give us headers for 664 * ENVELOPE, only size. 665 */ 666 synchronized (mListeners) { 667 for (MessagingListener l : mListeners) { 668 l.synchronizeMailboxNewMessage(account, folder, 669 localFolder.getMessage(message.getUid())); 670 } 671 } 672 } 673 674 if (!message.isSet(Flag.SEEN)) { 675 newMessages.add(message); 676 } 677 } 678 catch (Exception e) { 679 Log.e(Email.LOG_TAG, 680 "Error while storing downloaded message.", 681 e); 682 } 683 } 684 685 public void messageStarted(String uid, int number, int ofTotal) { 686 } 687 }); 688 } 689 690 /* 691 * Refresh the flags for any messages in the local store that we didn't just 692 * download. 693 */ 694 FetchProfile fp = new FetchProfile(); 695 fp.add(FetchProfile.Item.FLAGS); 696 remoteFolder.fetch(remoteMessages, fp, null); 697 for (Message remoteMessage : remoteMessages) { 698 Message localMessage = localFolder.getMessage(remoteMessage.getUid()); 699 if (localMessage == null) { 700 continue; 701 } 702 if (remoteMessage.isSet(Flag.SEEN) != localMessage.isSet(Flag.SEEN)) { 703 localMessage.setFlag(Flag.SEEN, remoteMessage.isSet(Flag.SEEN)); 704 synchronized (mListeners) { 705 for (MessagingListener l : mListeners) { 706 l.synchronizeMailboxNewMessage(account, folder, localMessage); 707 } 708 } 709 } 710 } 711 712 /* 713 * Get and store the unread message count. 714 */ 715 int remoteUnreadMessageCount = remoteFolder.getUnreadMessageCount(); 716 if (remoteUnreadMessageCount == -1) { 717 localFolder.setUnreadMessageCount(localFolder.getUnreadMessageCount() 718 + newMessages.size()); 719 } 720 else { 721 localFolder.setUnreadMessageCount(remoteUnreadMessageCount); 722 } 723 724 /* 725 * Remove any messages that are in the local store but no longer on the remote store. 726 */ 727 for (Message localMessage : localMessages) { 728 if (remoteUidMap.get(localMessage.getUid()) == null) { 729 localMessage.setFlag(Flag.X_DESTROYED, true); 730 synchronized (mListeners) { 731 for (MessagingListener l : mListeners) { 732 l.synchronizeMailboxRemovedMessage(account, folder, localMessage); 733 } 734 } 735 } 736 } 737 738 /* 739 * Now we download the actual content of messages. 740 */ 741 ArrayList<Message> largeMessages = new ArrayList<Message>(); 742 ArrayList<Message> smallMessages = new ArrayList<Message>(); 743 for (Message message : unsyncedMessages) { 744 /* 745 * Sort the messages into two buckets, small and large. Small messages will be 746 * downloaded fully and large messages will be downloaded in parts. By sorting 747 * into two buckets we can pipeline the commands for each set of messages 748 * into a single command to the server saving lots of round trips. 749 */ 750 if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) { 751 largeMessages.add(message); 752 } else { 753 smallMessages.add(message); 754 } 755 } 756 /* 757 * Grab the content of the small messages first. This is going to 758 * be very fast and at very worst will be a single up of a few bytes and a single 759 * download of 625k. 760 */ 761 fp = new FetchProfile(); 762 fp.add(FetchProfile.Item.BODY); 763 remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), 764 fp, new MessageRetrievalListener() { 765 public void messageFinished(Message message, int number, int ofTotal) { 766 try { 767 // Store the updated message locally 768 localFolder.appendMessages(new Message[] { 769 message 770 }); 771 772 Message localMessage = localFolder.getMessage(message.getUid()); 773 774 // Set a flag indicating this message has now be fully downloaded 775 localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); 776 777 // Update the listener with what we've found 778 synchronized (mListeners) { 779 for (MessagingListener l : mListeners) { 780 l.synchronizeMailboxNewMessage( 781 account, 782 folder, 783 localMessage); 784 } 785 } 786 } 787 catch (MessagingException me) { 788 789 } 790 } 791 792 public void messageStarted(String uid, int number, int ofTotal) { 793 } 794 }); 795 796 /* 797 * Now do the large messages that require more round trips. 798 */ 799 fp.clear(); 800 fp.add(FetchProfile.Item.STRUCTURE); 801 remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), 802 fp, null); 803 for (Message message : largeMessages) { 804 if (message.getBody() == null) { 805 /* 806 * The provider was unable to get the structure of the message, so 807 * we'll download a reasonable portion of the messge and mark it as 808 * incomplete so the entire thing can be downloaded later if the user 809 * wishes to download it. 810 */ 811 fp.clear(); 812 fp.add(FetchProfile.Item.BODY_SANE); 813 /* 814 * TODO a good optimization here would be to make sure that all Stores set 815 * the proper size after this fetch and compare the before and after size. If 816 * they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED 817 */ 818 819 remoteFolder.fetch(new Message[] { message }, fp, null); 820 // Store the updated message locally 821 localFolder.appendMessages(new Message[] { 822 message 823 }); 824 825 Message localMessage = localFolder.getMessage(message.getUid()); 826 827 // Set a flag indicating that the message has been partially downloaded and 828 // is ready for view. 829 localMessage.setFlag(Flag.X_DOWNLOADED_PARTIAL, true); 830 } else { 831 /* 832 * We have a structure to deal with, from which 833 * we can pull down the parts we want to actually store. 834 * Build a list of parts we are interested in. Text parts will be downloaded 835 * right now, attachments will be left for later. 836 */ 837 838 ArrayList<Part> viewables = new ArrayList<Part>(); 839 ArrayList<Part> attachments = new ArrayList<Part>(); 840 MimeUtility.collectParts(message, viewables, attachments); 841 842 /* 843 * Now download the parts we're interested in storing. 844 */ 845 for (Part part : viewables) { 846 fp.clear(); 847 fp.add(part); 848 // TODO what happens if the network connection dies? We've got partial 849 // messages with incorrect status stored. 850 remoteFolder.fetch(new Message[] { message }, fp, null); 851 } 852 // Store the updated message locally 853 localFolder.appendMessages(new Message[] { 854 message 855 }); 856 857 Message localMessage = localFolder.getMessage(message.getUid()); 858 859 // Set a flag indicating this message has been fully downloaded and can be 860 // viewed. 861 localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); 862 } 863 864 // Update the listener with what we've found 865 synchronized (mListeners) { 866 for (MessagingListener l : mListeners) { 867 l.synchronizeMailboxNewMessage( 868 account, 869 folder, 870 localFolder.getMessage(message.getUid())); 871 } 872 } 873 } 874 875 876 /* 877 * Report successful sync 878 */ 879 StoreSynchronizer.SyncResults results = new StoreSynchronizer.SyncResults( 880 remoteFolder.getMessageCount(), newMessages.size()); 881 882 remoteFolder.close(false); 883 localFolder.close(false); 884 885 return results; 886 } 887 888 private void queuePendingCommand(Account account, PendingCommand command) { 889 try { 890 LocalStore localStore = (LocalStore) Store.getInstance( 891 account.getLocalStoreUri(), mContext, null); 892 localStore.addPendingCommand(command); 893 } 894 catch (Exception e) { 895 throw new RuntimeException("Unable to enqueue pending command", e); 896 } 897 } 898 899 private void processPendingCommands(final Account account) { 900 put("processPendingCommands", null, new Runnable() { 901 public void run() { 902 try { 903 processPendingCommandsSynchronous(account); 904 } 905 catch (MessagingException me) { 906 if (Config.LOGV) { 907 Log.v(Email.LOG_TAG, "processPendingCommands", me); 908 } 909 /* 910 * Ignore any exceptions from the commands. Commands will be processed 911 * on the next round. 912 */ 913 } 914 } 915 }); 916 } 917 918 private void processPendingCommandsSynchronous(Account account) throws MessagingException { 919 LocalStore localStore = (LocalStore) Store.getInstance( 920 account.getLocalStoreUri(), mContext, null); 921 ArrayList<PendingCommand> commands = localStore.getPendingCommands(); 922 for (PendingCommand command : commands) { 923 /* 924 * We specifically do not catch any exceptions here. If a command fails it is 925 * most likely due to a server or IO error and it must be retried before any 926 * other command processes. This maintains the order of the commands. 927 */ 928 if (PENDING_COMMAND_APPEND.equals(command.command)) { 929 processPendingAppend(command, account); 930 } 931 else if (PENDING_COMMAND_MARK_READ.equals(command.command)) { 932 processPendingMarkRead(command, account); 933 } 934 else if (PENDING_COMMAND_TRASH.equals(command.command)) { 935 processPendingTrash(command, account); 936 } 937 localStore.removePendingCommand(command); 938 } 939 } 940 941 /** 942 * Process a pending append message command. This command uploads a local message to the 943 * server, first checking to be sure that the server message is not newer than 944 * the local message. Once the local message is successfully processed it is deleted so 945 * that the server message will be synchronized down without an additional copy being 946 * created. 947 * TODO update the local message UID instead of deleteing it 948 * 949 * @param command arguments = (String folder, String uid) 950 * @param account 951 * @throws MessagingException 952 */ 953 private void processPendingAppend(PendingCommand command, Account account) 954 throws MessagingException { 955 String folder = command.arguments[0]; 956 String uid = command.arguments[1]; 957 958 LocalStore localStore = (LocalStore) Store.getInstance( 959 account.getLocalStoreUri(), mContext, null); 960 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 961 LocalMessage localMessage = (LocalMessage) localFolder.getMessage(uid); 962 963 if (localMessage == null) { 964 return; 965 } 966 967 Store remoteStore = Store.getInstance(account.getStoreUri(), mContext, 968 localStore.getPersistentCallbacks()); 969 Folder remoteFolder = remoteStore.getFolder(folder); 970 if (!remoteFolder.exists()) { 971 if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { 972 return; 973 } 974 } 975 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 976 if (remoteFolder.getMode() != OpenMode.READ_WRITE) { 977 return; 978 } 979 980 Message remoteMessage = null; 981 if (!localMessage.getUid().startsWith("Local") 982 && !localMessage.getUid().contains("-")) { 983 remoteMessage = remoteFolder.getMessage(localMessage.getUid()); 984 } 985 986 if (remoteMessage == null) { 987 /* 988 * If the message does not exist remotely we just upload it and then 989 * update our local copy with the new uid. 990 */ 991 FetchProfile fp = new FetchProfile(); 992 fp.add(FetchProfile.Item.BODY); 993 localFolder.fetch(new Message[] { localMessage }, fp, null); 994 String oldUid = localMessage.getUid(); 995 remoteFolder.appendMessages(new Message[] { localMessage }); 996 localFolder.changeUid(localMessage); 997 synchronized (mListeners) { 998 for (MessagingListener l : mListeners) { 999 l.messageUidChanged(account, folder, oldUid, localMessage.getUid()); 1000 } 1001 } 1002 } 1003 else { 1004 /* 1005 * If the remote message exists we need to determine which copy to keep. 1006 */ 1007 /* 1008 * See if the remote message is newer than ours. 1009 */ 1010 FetchProfile fp = new FetchProfile(); 1011 fp.add(FetchProfile.Item.ENVELOPE); 1012 remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); 1013 Date localDate = localMessage.getInternalDate(); 1014 Date remoteDate = remoteMessage.getInternalDate(); 1015 if (remoteDate.compareTo(localDate) > 0) { 1016 /* 1017 * If the remote message is newer than ours we'll just 1018 * delete ours and move on. A sync will get the server message 1019 * if we need to be able to see it. 1020 */ 1021 localMessage.setFlag(Flag.DELETED, true); 1022 } 1023 else { 1024 /* 1025 * Otherwise we'll upload our message and then delete the remote message. 1026 */ 1027 fp.clear(); 1028 fp = new FetchProfile(); 1029 fp.add(FetchProfile.Item.BODY); 1030 localFolder.fetch(new Message[] { localMessage }, fp, null); 1031 String oldUid = localMessage.getUid(); 1032 remoteFolder.appendMessages(new Message[] { localMessage }); 1033 localFolder.changeUid(localMessage); 1034 synchronized (mListeners) { 1035 for (MessagingListener l : mListeners) { 1036 l.messageUidChanged(account, folder, oldUid, localMessage.getUid()); 1037 } 1038 } 1039 remoteMessage.setFlag(Flag.DELETED, true); 1040 } 1041 } 1042 } 1043 1044 /** 1045 * Process a pending trash message command. 1046 * 1047 * @param command arguments = (String folder, String uid) 1048 * @param account 1049 * @throws MessagingException 1050 */ 1051 private void processPendingTrash(PendingCommand command, final Account account) 1052 throws MessagingException { 1053 String folder = command.arguments[0]; 1054 String uid = command.arguments[1]; 1055 1056 final LocalStore localStore = (LocalStore) Store.getInstance( 1057 account.getLocalStoreUri(), mContext, null); 1058 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 1059 1060 Store remoteStore = Store.getInstance(account.getStoreUri(), mContext, 1061 localStore.getPersistentCallbacks()); 1062 Folder remoteFolder = remoteStore.getFolder(folder); 1063 if (!remoteFolder.exists()) { 1064 return; 1065 } 1066 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 1067 if (remoteFolder.getMode() != OpenMode.READ_WRITE) { 1068 return; 1069 } 1070 1071 Message remoteMessage = null; 1072 if (!uid.startsWith("Local") 1073 && !uid.contains("-")) { 1074 remoteMessage = remoteFolder.getMessage(uid); 1075 } 1076 if (remoteMessage == null) { 1077 return; 1078 } 1079 1080 Folder remoteTrashFolder = remoteStore.getFolder(account.getTrashFolderName()); 1081 /* 1082 * Attempt to copy the remote message to the remote trash folder. 1083 */ 1084 if (!remoteTrashFolder.exists()) { 1085 /* 1086 * If the remote trash folder doesn't exist we try to create it. 1087 */ 1088 remoteTrashFolder.create(FolderType.HOLDS_MESSAGES); 1089 } 1090 1091 if (remoteTrashFolder.exists()) { 1092 /* 1093 * Because remoteTrashFolder may be new, we need to explicitly open it 1094 * and pass in the persistence callbacks. 1095 */ 1096 final LocalFolder localTrashFolder = 1097 (LocalFolder) localStore.getFolder(account.getTrashFolderName()); 1098 remoteTrashFolder.open(OpenMode.READ_WRITE, localTrashFolder.getPersistentCallbacks()); 1099 if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) { 1100 return; 1101 } 1102 1103 remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder, 1104 new Folder.MessageUpdateCallbacks() { 1105 public void onMessageUidChange(Message message, String newUid) 1106 throws MessagingException { 1107 // update the UID in the local trash folder, because some stores will 1108 // have to change it when copying to remoteTrashFolder 1109 LocalMessage localMessage = 1110 (LocalMessage) localTrashFolder.getMessage(message.getUid()); 1111 if(localMessage != null) { 1112 localMessage.setUid(newUid); 1113 localTrashFolder.updateMessage(localMessage); 1114 } 1115 } 1116 1117 /** 1118 * This will be called if the deleted message doesn't exist and can't be 1119 * deleted (e.g. it was already deleted from the server.) In this case, 1120 * attempt to delete the local copy as well. 1121 */ 1122 public void onMessageNotFound(Message message) throws MessagingException { 1123 LocalMessage localMessage = 1124 (LocalMessage) localTrashFolder.getMessage(message.getUid()); 1125 if (localMessage != null) { 1126 localMessage.setFlag(Flag.DELETED, true); 1127 } 1128 } 1129 1130 } 1131 ); 1132 } 1133 1134 remoteMessage.setFlag(Flag.DELETED, true); 1135 remoteFolder.expunge(); 1136 } 1137 1138 /** 1139 * Processes a pending mark read or unread command. 1140 * 1141 * @param command arguments = (String folder, String uid, boolean read) 1142 * @param account 1143 */ 1144 private void processPendingMarkRead(PendingCommand command, Account account) 1145 throws MessagingException { 1146 String folder = command.arguments[0]; 1147 String uid = command.arguments[1]; 1148 boolean read = Boolean.parseBoolean(command.arguments[2]); 1149 1150 LocalStore localStore = (LocalStore) Store.getInstance( 1151 account.getLocalStoreUri(), mContext, null); 1152 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 1153 1154 Store remoteStore = Store.getInstance(account.getStoreUri(), mContext, 1155 localStore.getPersistentCallbacks()); 1156 Folder remoteFolder = remoteStore.getFolder(folder); 1157 if (!remoteFolder.exists()) { 1158 return; 1159 } 1160 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 1161 if (remoteFolder.getMode() != OpenMode.READ_WRITE) { 1162 return; 1163 } 1164 Message remoteMessage = null; 1165 if (!uid.startsWith("Local") 1166 && !uid.contains("-")) { 1167 remoteMessage = remoteFolder.getMessage(uid); 1168 } 1169 if (remoteMessage == null) { 1170 return; 1171 } 1172 remoteMessage.setFlag(Flag.SEEN, read); 1173 } 1174 1175 /** 1176 * Mark the message with the given account, folder and uid either Seen or not Seen. 1177 * @param account 1178 * @param folder 1179 * @param uid 1180 * @param seen 1181 */ 1182 public void markMessageRead( 1183 final Account account, 1184 final String folder, 1185 final String uid, 1186 final boolean seen) { 1187 try { 1188 Store localStore = Store.getInstance(account.getLocalStoreUri(), mContext, null); 1189 Folder localFolder = localStore.getFolder(folder); 1190 localFolder.open(OpenMode.READ_WRITE, null); 1191 1192 Message message = localFolder.getMessage(uid); 1193 message.setFlag(Flag.SEEN, seen); 1194 PendingCommand command = new PendingCommand(); 1195 command.command = PENDING_COMMAND_MARK_READ; 1196 command.arguments = new String[] { folder, uid, Boolean.toString(seen) }; 1197 queuePendingCommand(account, command); 1198 processPendingCommands(account); 1199 } 1200 catch (MessagingException me) { 1201 throw new RuntimeException(me); 1202 } 1203 } 1204 1205 private void loadMessageForViewRemote(final Account account, final String folder, 1206 final String uid, MessagingListener listener) { 1207 put("loadMessageForViewRemote", listener, new Runnable() { 1208 public void run() { 1209 try { 1210 LocalStore localStore = (LocalStore) Store.getInstance( 1211 account.getLocalStoreUri(), mContext, null); 1212 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 1213 localFolder.open(OpenMode.READ_WRITE, null); 1214 1215 Message message = localFolder.getMessage(uid); 1216 1217 if (message.isSet(Flag.X_DOWNLOADED_FULL)) { 1218 /* 1219 * If the message has been synchronized since we were called we'll 1220 * just hand it back cause it's ready to go. 1221 */ 1222 FetchProfile fp = new FetchProfile(); 1223 fp.add(FetchProfile.Item.ENVELOPE); 1224 fp.add(FetchProfile.Item.BODY); 1225 localFolder.fetch(new Message[] { message }, fp, null); 1226 1227 synchronized (mListeners) { 1228 for (MessagingListener l : mListeners) { 1229 l.loadMessageForViewBodyAvailable(account, folder, uid, message); 1230 } 1231 for (MessagingListener l : mListeners) { 1232 l.loadMessageForViewFinished(account, folder, uid, message); 1233 } 1234 } 1235 localFolder.close(false); 1236 return; 1237 } 1238 1239 /* 1240 * At this point the message is not available, so we need to download it 1241 * fully if possible. 1242 */ 1243 1244 Store remoteStore = Store.getInstance(account.getStoreUri(), mContext, 1245 localStore.getPersistentCallbacks()); 1246 Folder remoteFolder = remoteStore.getFolder(folder); 1247 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 1248 1249 // Get the remote message and fully download it (and save into local store) 1250 1251 if (remoteStore.requireStructurePrefetch()) { 1252 // For remote stores that require it, prefetch the message structure. 1253 FetchProfile fp = new FetchProfile(); 1254 fp.add(FetchProfile.Item.STRUCTURE); 1255 localFolder.fetch(new Message[] { message }, fp, null); 1256 1257 ArrayList<Part> viewables = new ArrayList<Part>(); 1258 ArrayList<Part> attachments = new ArrayList<Part>(); 1259 MimeUtility.collectParts(message, viewables, attachments); 1260 fp.clear(); 1261 for (Part part : viewables) { 1262 fp.add(part); 1263 } 1264 1265 remoteFolder.fetch(new Message[] { message }, fp, null); 1266 1267 // Store the updated message locally 1268 localFolder.updateMessage((LocalMessage)message); 1269 1270 } else { 1271 // Most remote stores can directly obtain the message using only uid 1272 Message remoteMessage = remoteFolder.getMessage(uid); 1273 FetchProfile fp = new FetchProfile(); 1274 fp.add(FetchProfile.Item.BODY); 1275 remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); 1276 1277 // Store the message locally 1278 localFolder.appendMessages(new Message[] { remoteMessage }); 1279 } 1280 1281 // Now obtain the local copy for further access & manipulation 1282 message = localFolder.getMessage(uid); 1283 FetchProfile fp = new FetchProfile(); 1284 fp.add(FetchProfile.Item.BODY); 1285 localFolder.fetch(new Message[] { message }, fp, null); 1286 1287 // This is a view message request, so mark it read 1288 if (!message.isSet(Flag.SEEN)) { 1289 markMessageRead(account, folder, uid, true); 1290 } 1291 1292 // Mark that this message is now fully synched 1293 message.setFlag(Flag.X_DOWNLOADED_FULL, true); 1294 1295 synchronized (mListeners) { 1296 for (MessagingListener l : mListeners) { 1297 l.loadMessageForViewBodyAvailable(account, folder, uid, message); 1298 } 1299 for (MessagingListener l : mListeners) { 1300 l.loadMessageForViewFinished(account, folder, uid, message); 1301 } 1302 } 1303 remoteFolder.close(false); 1304 localFolder.close(false); 1305 } 1306 catch (Exception e) { 1307 synchronized (mListeners) { 1308 for (MessagingListener l : mListeners) { 1309 l.loadMessageForViewFailed(account, folder, uid, e.getMessage()); 1310 } 1311 } 1312 } 1313 } 1314 }); 1315 } 1316 1317 public void loadMessageForView(final Account account, final String folder, final String uid, 1318 MessagingListener listener) { 1319 synchronized (mListeners) { 1320 for (MessagingListener l : mListeners) { 1321 l.loadMessageForViewStarted(account, folder, uid); 1322 } 1323 } 1324 try { 1325 Store localStore = Store.getInstance(account.getLocalStoreUri(), mContext, null); 1326 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 1327 localFolder.open(OpenMode.READ_WRITE, null); 1328 1329 Message message = localFolder.getMessage(uid); 1330 1331 synchronized (mListeners) { 1332 for (MessagingListener l : mListeners) { 1333 l.loadMessageForViewHeadersAvailable(account, folder, uid, message); 1334 } 1335 } 1336 1337 if (!message.isSet(Flag.X_DOWNLOADED_FULL)) { 1338 loadMessageForViewRemote(account, folder, uid, listener); 1339 localFolder.close(false); 1340 return; 1341 } 1342 1343 if (!message.isSet(Flag.SEEN)) { 1344 markMessageRead(account, folder, uid, true); 1345 } 1346 1347 FetchProfile fp = new FetchProfile(); 1348 fp.add(FetchProfile.Item.ENVELOPE); 1349 fp.add(FetchProfile.Item.BODY); 1350 localFolder.fetch(new Message[] { 1351 message 1352 }, fp, null); 1353 1354 synchronized (mListeners) { 1355 for (MessagingListener l : mListeners) { 1356 l.loadMessageForViewBodyAvailable(account, folder, uid, message); 1357 } 1358 for (MessagingListener l : mListeners) { 1359 l.loadMessageForViewFinished(account, folder, uid, message); 1360 } 1361 } 1362 localFolder.close(false); 1363 } 1364 catch (Exception e) { 1365 synchronized (mListeners) { 1366 for (MessagingListener l : mListeners) { 1367 l.loadMessageForViewFailed(account, folder, uid, e.getMessage()); 1368 } 1369 } 1370 } 1371 } 1372 1373 /** 1374 * Attempts to load the attachment specified by part from the given account and message. 1375 * @param account 1376 * @param message 1377 * @param part 1378 * @param listener 1379 */ 1380 public void loadAttachment( 1381 final Account account, 1382 final Message message, 1383 final Part part, 1384 final Object tag, 1385 MessagingListener listener) { 1386 /* 1387 * Check if the attachment has already been downloaded. If it has there's no reason to 1388 * download it, so we just tell the listener that it's ready to go. 1389 */ 1390 try { 1391 if (part.getBody() != null) { 1392 synchronized (mListeners) { 1393 for (MessagingListener l : mListeners) { 1394 l.loadAttachmentStarted(account, message, part, tag, false); 1395 } 1396 for (MessagingListener l : mListeners) { 1397 l.loadAttachmentFinished(account, message, part, tag); 1398 } 1399 } 1400 return; 1401 } 1402 } 1403 catch (MessagingException me) { 1404 /* 1405 * If the header isn't there the attachment isn't downloaded yet, so just continue 1406 * on. 1407 */ 1408 } 1409 1410 synchronized (mListeners) { 1411 for (MessagingListener l : mListeners) { 1412 l.loadAttachmentStarted(account, message, part, tag, true); 1413 } 1414 } 1415 1416 put("loadAttachment", listener, new Runnable() { 1417 public void run() { 1418 try { 1419 LocalStore localStore = (LocalStore) Store.getInstance( 1420 account.getLocalStoreUri(), mContext, null); 1421 /* 1422 * We clear out any attachments already cached in the entire store and then 1423 * we update the passed in message to reflect that there are no cached 1424 * attachments. This is in support of limiting the account to having one 1425 * attachment downloaded at a time. 1426 */ 1427 localStore.pruneCachedAttachments(); 1428 ArrayList<Part> viewables = new ArrayList<Part>(); 1429 ArrayList<Part> attachments = new ArrayList<Part>(); 1430 MimeUtility.collectParts(message, viewables, attachments); 1431 for (Part attachment : attachments) { 1432 attachment.setBody(null); 1433 } 1434 Store remoteStore = Store.getInstance(account.getStoreUri(), mContext, 1435 localStore.getPersistentCallbacks()); 1436 LocalFolder localFolder = 1437 (LocalFolder) localStore.getFolder(message.getFolder().getName()); 1438 Folder remoteFolder = remoteStore.getFolder(message.getFolder().getName()); 1439 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 1440 1441 FetchProfile fp = new FetchProfile(); 1442 fp.add(part); 1443 remoteFolder.fetch(new Message[] { message }, fp, null); 1444 localFolder.updateMessage((LocalMessage)message); 1445 localFolder.close(false); 1446 synchronized (mListeners) { 1447 for (MessagingListener l : mListeners) { 1448 l.loadAttachmentFinished(account, message, part, tag); 1449 } 1450 } 1451 } 1452 catch (MessagingException me) { 1453 if (Config.LOGV) { 1454 Log.v(Email.LOG_TAG, "", me); 1455 } 1456 synchronized (mListeners) { 1457 for (MessagingListener l : mListeners) { 1458 l.loadAttachmentFailed(account, message, part, tag, me.getMessage()); 1459 } 1460 } 1461 } 1462 } 1463 }); 1464 } 1465 1466 /** 1467 * Stores the given message in the Outbox and starts a sendPendingMessages command to 1468 * attempt to send the message. 1469 * @param account 1470 * @param message 1471 * @param listener 1472 */ 1473 public void sendMessage(final Account account, 1474 final Message message, 1475 MessagingListener listener) { 1476 try { 1477 Store localStore = Store.getInstance(account.getLocalStoreUri(), mContext, null); 1478 LocalFolder localFolder = 1479 (LocalFolder) localStore.getFolder(account.getOutboxFolderName()); 1480 localFolder.open(OpenMode.READ_WRITE, null); 1481 localFolder.appendMessages(new Message[] { 1482 message 1483 }); 1484 Message localMessage = localFolder.getMessage(message.getUid()); 1485 localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); 1486 localFolder.close(false); 1487 sendPendingMessages(account, null); 1488 } 1489 catch (Exception e) { 1490// synchronized (mListeners) { 1491// for (MessagingListener l : mListeners) { 1492// // TODO general failed 1493// } 1494// } 1495 } 1496 } 1497 1498 /** 1499 * Attempt to send any messages that are sitting in the Outbox. 1500 * @param account 1501 * @param listener 1502 */ 1503 public void sendPendingMessages(final Account account, 1504 MessagingListener listener) { 1505 put("sendPendingMessages", listener, new Runnable() { 1506 public void run() { 1507 sendPendingMessagesSynchronous(account); 1508 } 1509 }); 1510 } 1511 1512 /** 1513 * Attempt to send any messages that are sitting in the Outbox. 1514 * @param account 1515 * @param listener 1516 */ 1517 public void sendPendingMessagesSynchronous(final Account account) { 1518 try { 1519 LocalStore localStore = (LocalStore) Store.getInstance( 1520 account.getLocalStoreUri(), mContext, null); 1521 Folder localFolder = localStore.getFolder(account.getOutboxFolderName()); 1522 if (!localFolder.exists()) { 1523 return; 1524 } 1525 localFolder.open(OpenMode.READ_WRITE, null); 1526 1527 Message[] localMessages = localFolder.getMessages(null); 1528 1529 /* 1530 * The profile we will use to pull all of the content 1531 * for a given local message into memory for sending. 1532 */ 1533 FetchProfile fp = new FetchProfile(); 1534 fp.add(FetchProfile.Item.ENVELOPE); 1535 fp.add(FetchProfile.Item.BODY); 1536 1537 LocalFolder localSentFolder = 1538 (LocalFolder) localStore.getFolder(account.getSentFolderName()); 1539 1540 // Determine if upload to "sent" folder is necessary 1541 Store remoteStore = Store.getInstance( 1542 account.getStoreUri(), mContext, localStore.getPersistentCallbacks()); 1543 boolean requireCopyMessageToSentFolder = remoteStore.requireCopyMessageToSentFolder(); 1544 1545 Sender sender = Sender.getInstance(account.getSenderUri(), mContext); 1546 for (Message message : localMessages) { 1547 try { 1548 localFolder.fetch(new Message[] { message }, fp, null); 1549 try { 1550 // Send message using Sender 1551 message.setFlag(Flag.X_SEND_IN_PROGRESS, true); 1552 sender.sendMessage(message); 1553 message.setFlag(Flag.X_SEND_IN_PROGRESS, false); 1554 1555 // Upload to "sent" folder if not supported server-side 1556 if (requireCopyMessageToSentFolder) { 1557 localFolder.copyMessages( 1558 new Message[] { message },localSentFolder, null); 1559 PendingCommand command = new PendingCommand(); 1560 command.command = PENDING_COMMAND_APPEND; 1561 command.arguments = 1562 new String[] { localSentFolder.getName(), message.getUid() }; 1563 queuePendingCommand(account, command); 1564 processPendingCommands(account); 1565 } 1566 1567 // And delete from outbox 1568 message.setFlag(Flag.X_DESTROYED, true); 1569 } 1570 catch (Exception e) { 1571 message.setFlag(Flag.X_SEND_FAILED, true); 1572 } 1573 } 1574 catch (Exception e) { 1575 /* 1576 * We ignore this exception because a future refresh will retry this 1577 * message. 1578 */ 1579 } 1580 } 1581 localFolder.expunge(); 1582 if (localFolder.getMessageCount() == 0) { 1583 localFolder.delete(false); 1584 } 1585 synchronized (mListeners) { 1586 for (MessagingListener l : mListeners) { 1587 l.sendPendingMessagesCompleted(account); 1588 } 1589 } 1590 } 1591 catch (Exception e) { 1592// synchronized (mListeners) { 1593// for (MessagingListener l : mListeners) { 1594// // TODO general failed 1595// } 1596// } 1597 } 1598 } 1599 1600 /** 1601 * We do the local portion of this synchronously because other activities may have to make 1602 * updates based on what happens here 1603 * @param account 1604 * @param folder 1605 * @param message 1606 * @param listener 1607 */ 1608 public void deleteMessage(final Account account, final String folder, final Message message, 1609 MessagingListener listener) { 1610 if (folder.equals(account.getTrashFolderName())) { 1611 return; 1612 } 1613 try { 1614 Store localStore = Store.getInstance(account.getLocalStoreUri(), mContext, null); 1615 Folder localFolder = localStore.getFolder(folder); 1616 Folder localTrashFolder = localStore.getFolder(account.getTrashFolderName()); 1617 1618 localFolder.copyMessages(new Message[] { message }, localTrashFolder, null); 1619 message.setFlag(Flag.DELETED, true); 1620 1621 if (account.getDeletePolicy() == Account.DELETE_POLICY_ON_DELETE) { 1622 PendingCommand command = new PendingCommand(); 1623 command.command = PENDING_COMMAND_TRASH; 1624 command.arguments = new String[] { folder, message.getUid() }; 1625 queuePendingCommand(account, command); 1626 processPendingCommands(account); 1627 } 1628 } 1629 catch (MessagingException me) { 1630 throw new RuntimeException("Error deleting message from local store.", me); 1631 } 1632 } 1633 1634 public void emptyTrash(final Account account, MessagingListener listener) { 1635 put("emptyTrash", listener, new Runnable() { 1636 public void run() { 1637 // TODO IMAP 1638 try { 1639 Store localStore = Store.getInstance( 1640 account.getLocalStoreUri(), mContext, null); 1641 Folder localFolder = localStore.getFolder(account.getTrashFolderName()); 1642 localFolder.open(OpenMode.READ_WRITE, null); 1643 Message[] messages = localFolder.getMessages(null); 1644 localFolder.setFlags(messages, new Flag[] { 1645 Flag.DELETED 1646 }, true); 1647 localFolder.close(true); 1648 synchronized (mListeners) { 1649 for (MessagingListener l : mListeners) { 1650 l.emptyTrashCompleted(account); 1651 } 1652 } 1653 } 1654 catch (Exception e) { 1655 // TODO 1656 if (Config.LOGV) { 1657 Log.v(Email.LOG_TAG, "emptyTrash"); 1658 } 1659 } 1660 } 1661 }); 1662 } 1663 1664 /** 1665 * Checks mail for one or multiple accounts. If account is null all accounts 1666 * are checked. 1667 * 1668 * TODO: There is no use case for "check all accounts". Clean up this API to remove 1669 * that case. Callers can supply the appropriate list. 1670 * 1671 * TODO: Better protection against a failure in account n, which should not prevent 1672 * syncing account in accounts n+1 and beyond. 1673 * 1674 * @param context 1675 * @param accounts List of accounts to check, or null to check all accounts 1676 * @param listener 1677 */ 1678 public void checkMail(final Context context, Account[] accounts, 1679 final MessagingListener listener) { 1680 /** 1681 * Note: The somewhat tortured logic here is to guarantee proper ordering of events: 1682 * listeners: checkMailStarted 1683 * account 1: list folders 1684 * account 1: sync messages 1685 * account 2: list folders 1686 * account 2: sync messages 1687 * ... 1688 * account n: list folders 1689 * account n: sync messages 1690 * listeners: checkMailFinished 1691 */ 1692 synchronized (mListeners) { 1693 for (MessagingListener l : mListeners) { 1694 l.checkMailStarted(context, null); // TODO this needs to pass the actual array 1695 } 1696 } 1697 if (accounts == null) { 1698 accounts = Preferences.getPreferences(context).getAccounts(); 1699 } 1700 for (final Account account : accounts) { 1701 listFolders(account, true, null); 1702 1703 put("checkMail", listener, new Runnable() { 1704 public void run() { 1705 sendPendingMessagesSynchronous(account); 1706 synchronizeMailboxSynchronous(account, Email.INBOX); 1707 } 1708 }); 1709 } 1710 put("checkMailFinished", listener, new Runnable() { 1711 public void run() { 1712 synchronized (mListeners) { 1713 for (MessagingListener l : mListeners) { 1714 l.checkMailFinished(context, null); // TODO this needs to pass actual array 1715 } 1716 } 1717 } 1718 }); 1719 } 1720 1721 public void saveDraft(final Account account, final Message message) { 1722 try { 1723 Store localStore = Store.getInstance(account.getLocalStoreUri(), mContext, null); 1724 LocalFolder localFolder = 1725 (LocalFolder) localStore.getFolder(account.getDraftsFolderName()); 1726 localFolder.open(OpenMode.READ_WRITE, null); 1727 localFolder.appendMessages(new Message[] { 1728 message 1729 }); 1730 Message localMessage = localFolder.getMessage(message.getUid()); 1731 localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); 1732 1733 PendingCommand command = new PendingCommand(); 1734 command.command = PENDING_COMMAND_APPEND; 1735 command.arguments = new String[] { 1736 localFolder.getName(), 1737 localMessage.getUid() }; 1738 queuePendingCommand(account, command); 1739 processPendingCommands(account); 1740 } 1741 catch (MessagingException e) { 1742 Log.e(Email.LOG_TAG, "Unable to save message as draft.", e); 1743 } 1744 } 1745 1746 class Command { 1747 public Runnable runnable; 1748 1749 public MessagingListener listener; 1750 1751 public String description; 1752 1753 @Override 1754 public String toString() { 1755 return description; 1756 } 1757 } 1758} 1759