MessagingController.java revision ae8ca3fbd1545c3a94011d7d70bcadac99e7779f
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 java.util.ArrayList; 20import java.util.Collections; 21import java.util.Date; 22import java.util.HashMap; 23import java.util.HashSet; 24import java.util.List; 25import java.util.concurrent.BlockingQueue; 26import java.util.concurrent.LinkedBlockingQueue; 27 28import android.app.Application; 29import android.content.Context; 30import android.os.Process; 31import android.util.Config; 32import android.util.Log; 33 34import com.android.email.mail.FetchProfile; 35import com.android.email.mail.Flag; 36import com.android.email.mail.Folder; 37import com.android.email.mail.Message; 38import com.android.email.mail.MessageRetrievalListener; 39import com.android.email.mail.MessagingException; 40import com.android.email.mail.Part; 41import com.android.email.mail.Sender; 42import com.android.email.mail.Store; 43import com.android.email.mail.Folder.FolderType; 44import com.android.email.mail.Folder.OpenMode; 45import com.android.email.mail.internet.MimeUtility; 46import com.android.email.mail.store.LocalStore; 47import com.android.email.mail.store.LocalStore.LocalFolder; 48import com.android.email.mail.store.LocalStore.LocalMessage; 49import com.android.email.mail.store.LocalStore.PendingCommand; 50 51/** 52 * Starts a long running (application) Thread that will run through commands 53 * that require remote mailbox access. This class is used to serialize and 54 * prioritize these commands. Each method that will submit a command requires a 55 * MessagingListener instance to be provided. It is expected that that listener 56 * has also been added as a registered listener using addListener(). When a 57 * command is to be executed, if the listener that was provided with the command 58 * is no longer registered the command is skipped. The design idea for the above 59 * is that when an Activity starts it registers as a listener. When it is paused 60 * it removes itself. Thus, any commands that that activity submitted are 61 * removed from the queue once the activity is no longer active. 62 */ 63public class MessagingController implements Runnable { 64 /** 65 * The maximum message size that we'll consider to be "small". A small message is downloaded 66 * in full immediately instead of in pieces. Anything over this size will be downloaded in 67 * pieces with attachments being left off completely and downloaded on demand. 68 * 69 * 70 * 25k for a "small" message was picked by educated trial and error. 71 * http://answers.google.com/answers/threadview?id=312463 claims that the 72 * average size of an email is 59k, which I feel is too large for our 73 * blind download. The following tests were performed on a download of 74 * 25 random messages. 75 * <pre> 76 * 5k - 61 seconds, 77 * 25k - 51 seconds, 78 * 55k - 53 seconds, 79 * </pre> 80 * So 25k gives good performance and a reasonable data footprint. Sounds good to me. 81 */ 82 private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024); 83 84 private static final String PENDING_COMMAND_TRASH = 85 "com.android.email.MessagingController.trash"; 86 private static final String PENDING_COMMAND_MARK_READ = 87 "com.android.email.MessagingController.markRead"; 88 private static final String PENDING_COMMAND_APPEND = 89 "com.android.email.MessagingController.append"; 90 91 private static MessagingController inst = null; 92 private BlockingQueue<Command> mCommands = new LinkedBlockingQueue<Command>(); 93 private Thread mThread; 94 private HashSet<MessagingListener> mListeners = new HashSet<MessagingListener>(); 95 private boolean mBusy; 96 private Application mApplication; 97 98 protected MessagingController(Application application) { 99 mApplication = application; 100 mThread = new Thread(this); 101 mThread.start(); 102 } 103 104 /** 105 * Gets or creates the singleton instance of MessagingController. Application is used to 106 * provide a Context to classes that need it. 107 * @param application 108 * @return 109 */ 110 public synchronized static MessagingController getInstance(Application application) { 111 if (inst == null) { 112 inst = new MessagingController(application); 113 } 114 return inst; 115 } 116 117 /** 118 * Inject a mock controller. Used only for testing. Affects future calls to getInstance(). 119 */ 120 public static void injectMockController(MessagingController mockController) { 121 inst = mockController; 122 } 123 124 public boolean isBusy() { 125 return mBusy; 126 } 127 128 public void run() { 129 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 130 while (true) { 131 try { 132 Command command = mCommands.take(); 133 if (command.listener == null || mListeners.contains(command.listener)) { 134 mBusy = true; 135 command.runnable.run(); 136 for (MessagingListener l : mListeners) { 137 l.controllerCommandCompleted(mCommands.size() > 0); 138 } 139 } 140 } 141 catch (Exception e) { 142 if (Config.LOGD) { 143 Log.d(Email.LOG_TAG, "Error running command", e); 144 } 145 } 146 mBusy = false; 147 } 148 } 149 150 private void put(String description, MessagingListener listener, Runnable runnable) { 151 try { 152 Command command = new Command(); 153 command.listener = listener; 154 command.runnable = runnable; 155 command.description = description; 156 mCommands.put(command); 157 } 158 catch (InterruptedException ie) { 159 throw new Error(ie); 160 } 161 } 162 163 public void addListener(MessagingListener listener) { 164 mListeners.add(listener); 165 } 166 167 public void removeListener(MessagingListener listener) { 168 mListeners.remove(listener); 169 } 170 171 /** 172 * Lists folders that are available locally and remotely. This method calls 173 * listFoldersCallback for local folders before it returns, and then for 174 * remote folders at some later point. If there are no local folders 175 * includeRemote is forced by this method. This method should be called from 176 * a Thread as it may take several seconds to list the local folders. TODO 177 * this needs to cache the remote folder list 178 * 179 * @param account 180 * @param includeRemote 181 * @param listener 182 * @throws MessagingException 183 */ 184 public void listFolders( 185 final Account account, 186 boolean refreshRemote, 187 MessagingListener listener) { 188 for (MessagingListener l : mListeners) { 189 l.listFoldersStarted(account); 190 } 191 try { 192 Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); 193 Folder[] localFolders = localStore.getPersonalNamespaces(); 194 195 if (localFolders == null || localFolders.length == 0) { 196 refreshRemote = true; 197 } else { 198 for (MessagingListener l : mListeners) { 199 l.listFolders(account, localFolders); 200 } 201 } 202 } 203 catch (Exception e) { 204 for (MessagingListener l : mListeners) { 205 l.listFoldersFailed(account, e.getMessage()); 206 return; 207 } 208 } 209 if (refreshRemote) { 210 put("listFolders", listener, new Runnable() { 211 public void run() { 212 try { 213 Store store = Store.getInstance(account.getStoreUri(), mApplication); 214 215 Folder[] remoteFolders = store.getPersonalNamespaces(); 216 217 Store localStore = Store.getInstance( 218 account.getLocalStoreUri(), 219 mApplication); 220 HashSet<String> remoteFolderNames = new HashSet<String>(); 221 for (int i = 0, count = remoteFolders.length; i < count; i++) { 222 Folder localFolder = localStore.getFolder(remoteFolders[i].getName()); 223 if (!localFolder.exists()) { 224 localFolder.create(FolderType.HOLDS_MESSAGES); 225 } 226 remoteFolderNames.add(remoteFolders[i].getName()); 227 } 228 229 Folder[] localFolders = localStore.getPersonalNamespaces(); 230 231 /* 232 * Clear out any folders that are no longer on the remote store. 233 */ 234 for (Folder localFolder : localFolders) { 235 String localFolderName = localFolder.getName(); 236 if (localFolderName.equalsIgnoreCase(Email.INBOX) || 237 localFolderName.equals(account.getTrashFolderName()) || 238 localFolderName.equals(account.getOutboxFolderName()) || 239 localFolderName.equals(account.getDraftsFolderName()) || 240 localFolderName.equals(account.getSentFolderName())) { 241 continue; 242 } 243 if (!remoteFolderNames.contains(localFolder.getName())) { 244 localFolder.delete(false); 245 } 246 } 247 248 localFolders = localStore.getPersonalNamespaces(); 249 250 for (MessagingListener l : mListeners) { 251 l.listFolders(account, localFolders); 252 } 253 for (MessagingListener l : mListeners) { 254 l.listFoldersFinished(account); 255 } 256 } 257 catch (Exception e) { 258 for (MessagingListener l : mListeners) { 259 l.listFoldersFailed(account, ""); 260 } 261 } 262 } 263 }); 264 } else { 265 for (MessagingListener l : mListeners) { 266 l.listFoldersFinished(account); 267 } 268 } 269 } 270 271 /** 272 * List the local message store for the given folder. This work is done 273 * synchronously. 274 * 275 * @param account 276 * @param folder 277 * @param listener 278 * @throws MessagingException 279 */ 280 public void listLocalMessages(final Account account, final String folder, 281 MessagingListener listener) { 282 for (MessagingListener l : mListeners) { 283 l.listLocalMessagesStarted(account, folder); 284 } 285 286 try { 287 Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); 288 Folder localFolder = localStore.getFolder(folder); 289 localFolder.open(OpenMode.READ_WRITE); 290 Message[] localMessages = localFolder.getMessages(null); 291 ArrayList<Message> messages = new ArrayList<Message>(); 292 for (Message message : localMessages) { 293 if (!message.isSet(Flag.DELETED)) { 294 messages.add(message); 295 } 296 } 297 for (MessagingListener l : mListeners) { 298 l.listLocalMessages(account, folder, messages.toArray(new Message[0])); 299 } 300 for (MessagingListener l : mListeners) { 301 l.listLocalMessagesFinished(account, folder); 302 } 303 } 304 catch (Exception e) { 305 for (MessagingListener l : mListeners) { 306 l.listLocalMessagesFailed(account, folder, e.getMessage()); 307 } 308 } 309 } 310 311 public void loadMoreMessages(Account account, String folder, MessagingListener listener) { 312 try { 313 LocalStore localStore = (LocalStore) Store.getInstance( 314 account.getLocalStoreUri(), 315 mApplication); 316 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 317 localFolder.setVisibleLimit(localFolder.getVisibleLimit() 318 + Email.VISIBLE_LIMIT_INCREMENT); 319 synchronizeMailbox(account, folder, listener); 320 } 321 catch (MessagingException me) { 322 throw new RuntimeException("Unable to set visible limit on folder", me); 323 } 324 } 325 326 public void resetVisibleLimits(Account[] accounts) { 327 for (Account account : accounts) { 328 try { 329 LocalStore localStore = 330 (LocalStore) Store.getInstance(account.getLocalStoreUri(), mApplication); 331 localStore.resetVisibleLimits(); 332 } 333 catch (MessagingException e) { 334 Log.e(Email.LOG_TAG, "Unable to reset visible limits", e); 335 } 336 } 337 } 338 339 /** 340 * Start background synchronization of the specified folder. 341 * @param account 342 * @param folder 343 * @param numNewestMessagesToKeep Specifies the number of messages that should be 344 * considered as part of the window of available messages. This number effectively limits 345 * the user's view into the mailbox to the newest (numNewestMessagesToKeep) messages. 346 * @param listener 347 */ 348 public void synchronizeMailbox(final Account account, final String folder, 349 MessagingListener listener) { 350 /* 351 * We don't ever sync the Outbox. 352 */ 353 if (folder.equals(account.getOutboxFolderName())) { 354 return; 355 } 356 for (MessagingListener l : mListeners) { 357 l.synchronizeMailboxStarted(account, folder); 358 } 359 put("synchronizeMailbox", listener, new Runnable() { 360 public void run() { 361 synchronizeMailboxSyncronous(account, folder); 362 } 363 }); 364 } 365 366 /** 367 * Start foreground synchronization of the specified folder. This is generally only called 368 * by synchronizeMailbox. 369 * @param account 370 * @param folder 371 * @param numNewestMessagesToKeep Specifies the number of messages that should be 372 * considered as part of the window of available messages. This number effectively limits 373 * the user's view into the mailbox to the newest (numNewestMessagesToKeep) messages. 374 * @param listener 375 * 376 * TODO Break this method up into smaller chunks. 377 */ 378 public void synchronizeMailboxSyncronous(final Account account, final String folder) { 379 for (MessagingListener l : mListeners) { 380 l.synchronizeMailboxStarted(account, folder); 381 } 382 try { 383 processPendingCommandsSynchronous(account); 384 385 /* 386 * Get the message list from the local store and create an index of 387 * the uids within the list. 388 */ 389 final LocalStore localStore = 390 (LocalStore) Store.getInstance(account.getLocalStoreUri(), mApplication); 391 final LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 392 localFolder.open(OpenMode.READ_WRITE); 393 Message[] localMessages = localFolder.getMessages(null); 394 HashMap<String, Message> localUidMap = new HashMap<String, Message>(); 395 for (Message message : localMessages) { 396 localUidMap.put(message.getUid(), message); 397 } 398 399 Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication); 400 Folder remoteFolder = remoteStore.getFolder(folder); 401 402 /* 403 * If the folder is a "special" folder we need to see if it exists 404 * on the remote server. It if does not exist we'll try to create it. If we 405 * can't create we'll abort. This will happen on every single Pop3 folder as 406 * designed and on Imap folders during error conditions. This allows us 407 * to treat Pop3 and Imap the same in this code. 408 */ 409 if (folder.equals(account.getTrashFolderName()) || 410 folder.equals(account.getSentFolderName()) || 411 folder.equals(account.getDraftsFolderName())) { 412 if (!remoteFolder.exists()) { 413 if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { 414 for (MessagingListener l : mListeners) { 415 l.synchronizeMailboxFinished(account, folder, 0, 0); 416 } 417 return; 418 } 419 } 420 } 421 422 /* 423 * Synchronization process: 424 Open the folder 425 Upload any local messages that are marked as PENDING_UPLOAD (Drafts, Sent, Trash) 426 Get the message count 427 Get the list of the newest Email.DEFAULT_VISIBLE_LIMIT messages 428 getMessages(messageCount - Email.DEFAULT_VISIBLE_LIMIT, messageCount) 429 See if we have each message locally, if not fetch it's flags and envelope 430 Get and update the unread count for the folder 431 Update the remote flags of any messages we have locally with an internal date 432 newer than the remote message. 433 Get the current flags for any messages we have locally but did not just download 434 Update local flags 435 For any message we have locally but not remotely, delete the local message to keep 436 cache clean. 437 Download larger parts of any new messages. 438 (Optional) Download small attachments in the background. 439 */ 440 441 /* 442 * Open the remote folder. This pre-loads certain metadata like message count. 443 */ 444 remoteFolder.open(OpenMode.READ_WRITE); 445 446 /* 447 * Trash any remote messages that are marked as trashed locally. 448 */ 449 450 /* 451 * Get the remote message count. 452 */ 453 int remoteMessageCount = remoteFolder.getMessageCount(); 454 455 int visibleLimit = localFolder.getVisibleLimit(); 456 457 Message[] remoteMessages = new Message[0]; 458 final ArrayList<Message> unsyncedMessages = new ArrayList<Message>(); 459 HashMap<String, Message> remoteUidMap = new HashMap<String, Message>(); 460 461 if (remoteMessageCount > 0) { 462 /* 463 * Message numbers start at 1. 464 */ 465 int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1; 466 int remoteEnd = remoteMessageCount; 467 remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null); 468 for (Message message : remoteMessages) { 469 remoteUidMap.put(message.getUid(), message); 470 } 471 472 /* 473 * Get a list of the messages that are in the remote list but not on the 474 * local store, or messages that are in the local store but failed to download 475 * on the last sync. These are the new messages that we will download. 476 */ 477 for (Message message : remoteMessages) { 478 Message localMessage = localUidMap.get(message.getUid()); 479 if (localMessage == null || 480 (!localMessage.isSet(Flag.X_DOWNLOADED_FULL) && 481 !localMessage.isSet(Flag.X_DOWNLOADED_PARTIAL))) { 482 unsyncedMessages.add(message); 483 } 484 } 485 } 486 487 /* 488 * A list of messages that were downloaded and which did not have the Seen flag set. 489 * This will serve to indicate the true "new" message count that will be reported to 490 * the user via notification. 491 */ 492 final ArrayList<Message> newMessages = new ArrayList<Message>(); 493 494 /* 495 * Fetch the flags and envelope only of the new messages. This is intended to get us 496 * critical data as fast as possible, and then we'll fill in the details. 497 */ 498 if (unsyncedMessages.size() > 0) { 499 500 /* 501 * Reverse the order of the messages. Depending on the server this may get us 502 * fetch results for newest to oldest. If not, no harm done. 503 */ 504 Collections.reverse(unsyncedMessages); 505 506 FetchProfile fp = new FetchProfile(); 507 fp.add(FetchProfile.Item.FLAGS); 508 fp.add(FetchProfile.Item.ENVELOPE); 509 remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp, 510 new MessageRetrievalListener() { 511 public void messageFinished(Message message, int number, int ofTotal) { 512 try { 513 // Store the new message locally 514 localFolder.appendMessages(new Message[] { 515 message 516 }); 517 518 // And include it in the view 519 if (message.getSubject() != null && 520 message.getFrom() != null) { 521 /* 522 * We check to make sure that we got something worth 523 * showing (subject and from) because some protocols 524 * (POP) may not be able to give us headers for 525 * ENVELOPE, only size. 526 */ 527 for (MessagingListener l : mListeners) { 528 l.synchronizeMailboxNewMessage(account, folder, 529 localFolder.getMessage(message.getUid())); 530 } 531 } 532 533 if (!message.isSet(Flag.SEEN)) { 534 newMessages.add(message); 535 } 536 } 537 catch (Exception e) { 538 Log.e(Email.LOG_TAG, 539 "Error while storing downloaded message.", 540 e); 541 } 542 } 543 544 public void messageStarted(String uid, int number, int ofTotal) { 545 } 546 }); 547 } 548 549 /* 550 * Refresh the flags for any messages in the local store that we didn't just 551 * download. 552 */ 553 FetchProfile fp = new FetchProfile(); 554 fp.add(FetchProfile.Item.FLAGS); 555 remoteFolder.fetch(remoteMessages, fp, null); 556 for (Message remoteMessage : remoteMessages) { 557 Message localMessage = localFolder.getMessage(remoteMessage.getUid()); 558 if (localMessage == null) { 559 continue; 560 } 561 if (remoteMessage.isSet(Flag.SEEN) != localMessage.isSet(Flag.SEEN)) { 562 localMessage.setFlag(Flag.SEEN, remoteMessage.isSet(Flag.SEEN)); 563 for (MessagingListener l : mListeners) { 564 l.synchronizeMailboxNewMessage(account, folder, localMessage); 565 } 566 } 567 } 568 569 /* 570 * Get and store the unread message count. 571 */ 572 int remoteUnreadMessageCount = remoteFolder.getUnreadMessageCount(); 573 if (remoteUnreadMessageCount == -1) { 574 localFolder.setUnreadMessageCount(localFolder.getUnreadMessageCount() 575 + newMessages.size()); 576 } 577 else { 578 localFolder.setUnreadMessageCount(remoteUnreadMessageCount); 579 } 580 581 /* 582 * Remove any messages that are in the local store but no longer on the remote store. 583 */ 584 for (Message localMessage : localMessages) { 585 if (remoteUidMap.get(localMessage.getUid()) == null) { 586 localMessage.setFlag(Flag.X_DESTROYED, true); 587 for (MessagingListener l : mListeners) { 588 l.synchronizeMailboxRemovedMessage(account, folder, localMessage); 589 } 590 } 591 } 592 593 /* 594 * Now we download the actual content of messages. 595 */ 596 ArrayList<Message> largeMessages = new ArrayList<Message>(); 597 ArrayList<Message> smallMessages = new ArrayList<Message>(); 598 for (Message message : unsyncedMessages) { 599 /* 600 * Sort the messages into two buckets, small and large. Small messages will be 601 * downloaded fully and large messages will be downloaded in parts. By sorting 602 * into two buckets we can pipeline the commands for each set of messages 603 * into a single command to the server saving lots of round trips. 604 */ 605 if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) { 606 largeMessages.add(message); 607 } else { 608 smallMessages.add(message); 609 } 610 } 611 /* 612 * Grab the content of the small messages first. This is going to 613 * be very fast and at very worst will be a single up of a few bytes and a single 614 * download of 625k. 615 */ 616 fp = new FetchProfile(); 617 fp.add(FetchProfile.Item.BODY); 618 remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), 619 fp, new MessageRetrievalListener() { 620 public void messageFinished(Message message, int number, int ofTotal) { 621 try { 622 // Store the updated message locally 623 localFolder.appendMessages(new Message[] { 624 message 625 }); 626 627 Message localMessage = localFolder.getMessage(message.getUid()); 628 629 // Set a flag indicating this message has now be fully downloaded 630 localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); 631 632 // Update the listener with what we've found 633 for (MessagingListener l : mListeners) { 634 l.synchronizeMailboxNewMessage( 635 account, 636 folder, 637 localMessage); 638 } 639 } 640 catch (MessagingException me) { 641 642 } 643 } 644 645 public void messageStarted(String uid, int number, int ofTotal) { 646 } 647 }); 648 649 /* 650 * Now do the large messages that require more round trips. 651 */ 652 fp.clear(); 653 fp.add(FetchProfile.Item.STRUCTURE); 654 remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), 655 fp, null); 656 for (Message message : largeMessages) { 657 if (message.getBody() == null) { 658 /* 659 * The provider was unable to get the structure of the message, so 660 * we'll download a reasonable portion of the messge and mark it as 661 * incomplete so the entire thing can be downloaded later if the user 662 * wishes to download it. 663 */ 664 fp.clear(); 665 fp.add(FetchProfile.Item.BODY_SANE); 666 /* 667 * TODO a good optimization here would be to make sure that all Stores set 668 * the proper size after this fetch and compare the before and after size. If 669 * they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED 670 */ 671 672 remoteFolder.fetch(new Message[] { message }, fp, null); 673 // Store the updated message locally 674 localFolder.appendMessages(new Message[] { 675 message 676 }); 677 678 Message localMessage = localFolder.getMessage(message.getUid()); 679 680 // Set a flag indicating that the message has been partially downloaded and 681 // is ready for view. 682 localMessage.setFlag(Flag.X_DOWNLOADED_PARTIAL, true); 683 } else { 684 /* 685 * We have a structure to deal with, from which 686 * we can pull down the parts we want to actually store. 687 * Build a list of parts we are interested in. Text parts will be downloaded 688 * right now, attachments will be left for later. 689 */ 690 691 ArrayList<Part> viewables = new ArrayList<Part>(); 692 ArrayList<Part> attachments = new ArrayList<Part>(); 693 MimeUtility.collectParts(message, viewables, attachments); 694 695 /* 696 * Now download the parts we're interested in storing. 697 */ 698 for (Part part : viewables) { 699 fp.clear(); 700 fp.add(part); 701 // TODO what happens if the network connection dies? We've got partial 702 // messages with incorrect status stored. 703 remoteFolder.fetch(new Message[] { message }, fp, null); 704 } 705 // Store the updated message locally 706 localFolder.appendMessages(new Message[] { 707 message 708 }); 709 710 Message localMessage = localFolder.getMessage(message.getUid()); 711 712 // Set a flag indicating this message has been fully downloaded and can be 713 // viewed. 714 localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); 715 } 716 717 // Update the listener with what we've found 718 for (MessagingListener l : mListeners) { 719 l.synchronizeMailboxNewMessage( 720 account, 721 folder, 722 localFolder.getMessage(message.getUid())); 723 } 724 } 725 726 727 /* 728 * Notify listeners that we're finally done. 729 */ 730 for (MessagingListener l : mListeners) { 731 l.synchronizeMailboxFinished( 732 account, 733 folder, 734 remoteFolder.getMessageCount(), newMessages.size()); 735 } 736 737 remoteFolder.close(false); 738 localFolder.close(false); 739 } 740 catch (Exception e) { 741 if (Config.LOGV) { 742 Log.v(Email.LOG_TAG, "synchronizeMailbox", e); 743 } 744 for (MessagingListener l : mListeners) { 745 l.synchronizeMailboxFailed( 746 account, 747 folder, 748 e); 749 } 750 } 751 } 752 753 private void queuePendingCommand(Account account, PendingCommand command) { 754 try { 755 LocalStore localStore = (LocalStore) Store.getInstance( 756 account.getLocalStoreUri(), 757 mApplication); 758 localStore.addPendingCommand(command); 759 } 760 catch (Exception e) { 761 throw new RuntimeException("Unable to enqueue pending command", e); 762 } 763 } 764 765 private void processPendingCommands(final Account account) { 766 put("processPendingCommands", null, new Runnable() { 767 public void run() { 768 try { 769 processPendingCommandsSynchronous(account); 770 } 771 catch (MessagingException me) { 772 if (Config.LOGV) { 773 Log.v(Email.LOG_TAG, "processPendingCommands", me); 774 } 775 /* 776 * Ignore any exceptions from the commands. Commands will be processed 777 * on the next round. 778 */ 779 } 780 } 781 }); 782 } 783 784 private void processPendingCommandsSynchronous(Account account) throws MessagingException { 785 LocalStore localStore = (LocalStore) Store.getInstance( 786 account.getLocalStoreUri(), 787 mApplication); 788 ArrayList<PendingCommand> commands = localStore.getPendingCommands(); 789 for (PendingCommand command : commands) { 790 /* 791 * We specifically do not catch any exceptions here. If a command fails it is 792 * most likely due to a server or IO error and it must be retried before any 793 * other command processes. This maintains the order of the commands. 794 */ 795 if (PENDING_COMMAND_APPEND.equals(command.command)) { 796 processPendingAppend(command, account); 797 } 798 else if (PENDING_COMMAND_MARK_READ.equals(command.command)) { 799 processPendingMarkRead(command, account); 800 } 801 else if (PENDING_COMMAND_TRASH.equals(command.command)) { 802 processPendingTrash(command, account); 803 } 804 localStore.removePendingCommand(command); 805 } 806 } 807 808 /** 809 * Process a pending append message command. This command uploads a local message to the 810 * server, first checking to be sure that the server message is not newer than 811 * the local message. Once the local message is successfully processed it is deleted so 812 * that the server message will be synchronized down without an additional copy being 813 * created. 814 * TODO update the local message UID instead of deleteing it 815 * 816 * @param command arguments = (String folder, String uid) 817 * @param account 818 * @throws MessagingException 819 */ 820 private void processPendingAppend(PendingCommand command, Account account) 821 throws MessagingException { 822 String folder = command.arguments[0]; 823 String uid = command.arguments[1]; 824 825 LocalStore localStore = (LocalStore) Store.getInstance( 826 account.getLocalStoreUri(), 827 mApplication); 828 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 829 LocalMessage localMessage = (LocalMessage) localFolder.getMessage(uid); 830 831 if (localMessage == null) { 832 return; 833 } 834 835 Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication); 836 Folder remoteFolder = remoteStore.getFolder(folder); 837 if (!remoteFolder.exists()) { 838 if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { 839 return; 840 } 841 } 842 remoteFolder.open(OpenMode.READ_WRITE); 843 if (remoteFolder.getMode() != OpenMode.READ_WRITE) { 844 return; 845 } 846 847 Message remoteMessage = null; 848 if (!localMessage.getUid().startsWith("Local") 849 && !localMessage.getUid().contains("-")) { 850 remoteMessage = remoteFolder.getMessage(localMessage.getUid()); 851 } 852 853 if (remoteMessage == null) { 854 /* 855 * If the message does not exist remotely we just upload it and then 856 * update our local copy with the new uid. 857 */ 858 FetchProfile fp = new FetchProfile(); 859 fp.add(FetchProfile.Item.BODY); 860 localFolder.fetch(new Message[] { localMessage }, fp, null); 861 String oldUid = localMessage.getUid(); 862 remoteFolder.appendMessages(new Message[] { localMessage }); 863 localFolder.changeUid(localMessage); 864 for (MessagingListener l : mListeners) { 865 l.messageUidChanged(account, folder, oldUid, localMessage.getUid()); 866 } 867 } 868 else { 869 /* 870 * If the remote message exists we need to determine which copy to keep. 871 */ 872 /* 873 * See if the remote message is newer than ours. 874 */ 875 FetchProfile fp = new FetchProfile(); 876 fp.add(FetchProfile.Item.ENVELOPE); 877 remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); 878 Date localDate = localMessage.getInternalDate(); 879 Date remoteDate = remoteMessage.getInternalDate(); 880 if (remoteDate.compareTo(localDate) > 0) { 881 /* 882 * If the remote message is newer than ours we'll just 883 * delete ours and move on. A sync will get the server message 884 * if we need to be able to see it. 885 */ 886 localMessage.setFlag(Flag.DELETED, true); 887 } 888 else { 889 /* 890 * Otherwise we'll upload our message and then delete the remote message. 891 */ 892 fp.clear(); 893 fp = new FetchProfile(); 894 fp.add(FetchProfile.Item.BODY); 895 localFolder.fetch(new Message[] { localMessage }, fp, null); 896 String oldUid = localMessage.getUid(); 897 remoteFolder.appendMessages(new Message[] { localMessage }); 898 localFolder.changeUid(localMessage); 899 for (MessagingListener l : mListeners) { 900 l.messageUidChanged(account, folder, oldUid, localMessage.getUid()); 901 } 902 remoteMessage.setFlag(Flag.DELETED, true); 903 } 904 } 905 } 906 907 /** 908 * Process a pending trash message command. 909 * 910 * @param command arguments = (String folder, String uid) 911 * @param account 912 * @throws MessagingException 913 */ 914 private void processPendingTrash(PendingCommand command, Account account) 915 throws MessagingException { 916 String folder = command.arguments[0]; 917 String uid = command.arguments[1]; 918 919 Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication); 920 Folder remoteFolder = remoteStore.getFolder(folder); 921 if (!remoteFolder.exists()) { 922 return; 923 } 924 remoteFolder.open(OpenMode.READ_WRITE); 925 if (remoteFolder.getMode() != OpenMode.READ_WRITE) { 926 return; 927 } 928 929 Message remoteMessage = null; 930 if (!uid.startsWith("Local") 931 && !uid.contains("-")) { 932 remoteMessage = remoteFolder.getMessage(uid); 933 } 934 if (remoteMessage == null) { 935 return; 936 } 937 938 Folder remoteTrashFolder = remoteStore.getFolder(account.getTrashFolderName()); 939 /* 940 * Attempt to copy the remote message to the remote trash folder. 941 */ 942 if (!remoteTrashFolder.exists()) { 943 /* 944 * If the remote trash folder doesn't exist we try to create it. 945 */ 946 remoteTrashFolder.create(FolderType.HOLDS_MESSAGES); 947 } 948 949 if (remoteTrashFolder.exists()) { 950 remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder); 951 } 952 953 remoteMessage.setFlag(Flag.DELETED, true); 954 remoteFolder.expunge(); 955 } 956 957 /** 958 * Processes a pending mark read or unread command. 959 * 960 * @param command arguments = (String folder, String uid, boolean read) 961 * @param account 962 */ 963 private void processPendingMarkRead(PendingCommand command, Account account) 964 throws MessagingException { 965 String folder = command.arguments[0]; 966 String uid = command.arguments[1]; 967 boolean read = Boolean.parseBoolean(command.arguments[2]); 968 969 Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication); 970 Folder remoteFolder = remoteStore.getFolder(folder); 971 if (!remoteFolder.exists()) { 972 return; 973 } 974 remoteFolder.open(OpenMode.READ_WRITE); 975 if (remoteFolder.getMode() != OpenMode.READ_WRITE) { 976 return; 977 } 978 Message remoteMessage = null; 979 if (!uid.startsWith("Local") 980 && !uid.contains("-")) { 981 remoteMessage = remoteFolder.getMessage(uid); 982 } 983 if (remoteMessage == null) { 984 return; 985 } 986 remoteMessage.setFlag(Flag.SEEN, read); 987 } 988 989 /** 990 * Mark the message with the given account, folder and uid either Seen or not Seen. 991 * @param account 992 * @param folder 993 * @param uid 994 * @param seen 995 */ 996 public void markMessageRead( 997 final Account account, 998 final String folder, 999 final String uid, 1000 final boolean seen) { 1001 try { 1002 Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); 1003 Folder localFolder = localStore.getFolder(folder); 1004 localFolder.open(OpenMode.READ_WRITE); 1005 1006 Message message = localFolder.getMessage(uid); 1007 message.setFlag(Flag.SEEN, seen); 1008 PendingCommand command = new PendingCommand(); 1009 command.command = PENDING_COMMAND_MARK_READ; 1010 command.arguments = new String[] { folder, uid, Boolean.toString(seen) }; 1011 queuePendingCommand(account, command); 1012 processPendingCommands(account); 1013 } 1014 catch (MessagingException me) { 1015 throw new RuntimeException(me); 1016 } 1017 } 1018 1019 private void loadMessageForViewRemote(final Account account, final String folder, 1020 final String uid, MessagingListener listener) { 1021 put("loadMessageForViewRemote", listener, new Runnable() { 1022 public void run() { 1023 try { 1024 Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); 1025 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 1026 localFolder.open(OpenMode.READ_WRITE); 1027 1028 Message message = localFolder.getMessage(uid); 1029 1030 if (message.isSet(Flag.X_DOWNLOADED_FULL)) { 1031 /* 1032 * If the message has been synchronized since we were called we'll 1033 * just hand it back cause it's ready to go. 1034 */ 1035 FetchProfile fp = new FetchProfile(); 1036 fp.add(FetchProfile.Item.ENVELOPE); 1037 fp.add(FetchProfile.Item.BODY); 1038 localFolder.fetch(new Message[] { message }, fp, null); 1039 1040 for (MessagingListener l : mListeners) { 1041 l.loadMessageForViewBodyAvailable(account, folder, uid, message); 1042 } 1043 for (MessagingListener l : mListeners) { 1044 l.loadMessageForViewFinished(account, folder, uid, message); 1045 } 1046 localFolder.close(false); 1047 return; 1048 } 1049 1050 /* 1051 * At this point the message is not available, so we need to download it 1052 * fully if possible. 1053 */ 1054 1055 Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication); 1056 Folder remoteFolder = remoteStore.getFolder(folder); 1057 remoteFolder.open(OpenMode.READ_WRITE); 1058 1059 // Get the remote message and fully download it 1060 Message remoteMessage = remoteFolder.getMessage(uid); 1061 FetchProfile fp = new FetchProfile(); 1062 fp.add(FetchProfile.Item.BODY); 1063 remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); 1064 1065 // Store the message locally and load the stored message into memory 1066 localFolder.appendMessages(new Message[] { remoteMessage }); 1067 message = localFolder.getMessage(uid); 1068 localFolder.fetch(new Message[] { message }, fp, null); 1069 1070 // This is a view message request, so mark it read 1071 if (!message.isSet(Flag.SEEN)) { 1072 markMessageRead(account, folder, uid, true); 1073 } 1074 1075 // Mark that this message is now fully synched 1076 message.setFlag(Flag.X_DOWNLOADED_FULL, true); 1077 1078 for (MessagingListener l : mListeners) { 1079 l.loadMessageForViewBodyAvailable(account, folder, uid, message); 1080 } 1081 for (MessagingListener l : mListeners) { 1082 l.loadMessageForViewFinished(account, folder, uid, message); 1083 } 1084 remoteFolder.close(false); 1085 localFolder.close(false); 1086 } 1087 catch (Exception e) { 1088 for (MessagingListener l : mListeners) { 1089 l.loadMessageForViewFailed(account, folder, uid, e.getMessage()); 1090 } 1091 } 1092 } 1093 }); 1094 } 1095 1096 public void loadMessageForView(final Account account, final String folder, final String uid, 1097 MessagingListener listener) { 1098 for (MessagingListener l : mListeners) { 1099 l.loadMessageForViewStarted(account, folder, uid); 1100 } 1101 try { 1102 Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); 1103 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 1104 localFolder.open(OpenMode.READ_WRITE); 1105 1106 Message message = localFolder.getMessage(uid); 1107 1108 for (MessagingListener l : mListeners) { 1109 l.loadMessageForViewHeadersAvailable(account, folder, uid, message); 1110 } 1111 1112 if (!message.isSet(Flag.X_DOWNLOADED_FULL)) { 1113 loadMessageForViewRemote(account, folder, uid, listener); 1114 localFolder.close(false); 1115 return; 1116 } 1117 1118 if (!message.isSet(Flag.SEEN)) { 1119 markMessageRead(account, folder, uid, true); 1120 } 1121 1122 FetchProfile fp = new FetchProfile(); 1123 fp.add(FetchProfile.Item.ENVELOPE); 1124 fp.add(FetchProfile.Item.BODY); 1125 localFolder.fetch(new Message[] { 1126 message 1127 }, fp, null); 1128 1129 for (MessagingListener l : mListeners) { 1130 l.loadMessageForViewBodyAvailable(account, folder, uid, message); 1131 } 1132 1133 for (MessagingListener l : mListeners) { 1134 l.loadMessageForViewFinished(account, folder, uid, message); 1135 } 1136 localFolder.close(false); 1137 } 1138 catch (Exception e) { 1139 for (MessagingListener l : mListeners) { 1140 l.loadMessageForViewFailed(account, folder, uid, e.getMessage()); 1141 } 1142 } 1143 } 1144 1145 /** 1146 * Attempts to load the attachment specified by part from the given account and message. 1147 * @param account 1148 * @param message 1149 * @param part 1150 * @param listener 1151 */ 1152 public void loadAttachment( 1153 final Account account, 1154 final Message message, 1155 final Part part, 1156 final Object tag, 1157 MessagingListener listener) { 1158 /* 1159 * Check if the attachment has already been downloaded. If it has there's no reason to 1160 * download it, so we just tell the listener that it's ready to go. 1161 */ 1162 try { 1163 if (part.getBody() != null) { 1164 for (MessagingListener l : mListeners) { 1165 l.loadAttachmentStarted(account, message, part, tag, false); 1166 } 1167 1168 for (MessagingListener l : mListeners) { 1169 l.loadAttachmentFinished(account, message, part, tag); 1170 } 1171 return; 1172 } 1173 } 1174 catch (MessagingException me) { 1175 /* 1176 * If the header isn't there the attachment isn't downloaded yet, so just continue 1177 * on. 1178 */ 1179 } 1180 1181 for (MessagingListener l : mListeners) { 1182 l.loadAttachmentStarted(account, message, part, tag, true); 1183 } 1184 1185 put("loadAttachment", listener, new Runnable() { 1186 public void run() { 1187 try { 1188 LocalStore localStore = 1189 (LocalStore) Store.getInstance(account.getLocalStoreUri(), mApplication); 1190 /* 1191 * We clear out any attachments already cached in the entire store and then 1192 * we update the passed in message to reflect that there are no cached 1193 * attachments. This is in support of limiting the account to having one 1194 * attachment downloaded at a time. 1195 */ 1196 localStore.pruneCachedAttachments(); 1197 ArrayList<Part> viewables = new ArrayList<Part>(); 1198 ArrayList<Part> attachments = new ArrayList<Part>(); 1199 MimeUtility.collectParts(message, viewables, attachments); 1200 for (Part attachment : attachments) { 1201 attachment.setBody(null); 1202 } 1203 Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication); 1204 LocalFolder localFolder = 1205 (LocalFolder) localStore.getFolder(message.getFolder().getName()); 1206 Folder remoteFolder = remoteStore.getFolder(message.getFolder().getName()); 1207 remoteFolder.open(OpenMode.READ_WRITE); 1208 1209 FetchProfile fp = new FetchProfile(); 1210 fp.add(part); 1211 remoteFolder.fetch(new Message[] { message }, fp, null); 1212 localFolder.updateMessage((LocalMessage)message); 1213 localFolder.close(false); 1214 for (MessagingListener l : mListeners) { 1215 l.loadAttachmentFinished(account, message, part, tag); 1216 } 1217 } 1218 catch (MessagingException me) { 1219 if (Config.LOGV) { 1220 Log.v(Email.LOG_TAG, "", me); 1221 } 1222 for (MessagingListener l : mListeners) { 1223 l.loadAttachmentFailed(account, message, part, tag, me.getMessage()); 1224 } 1225 } 1226 } 1227 }); 1228 } 1229 1230 /** 1231 * Stores the given message in the Outbox and starts a sendPendingMessages command to 1232 * attempt to send the message. 1233 * @param account 1234 * @param message 1235 * @param listener 1236 */ 1237 public void sendMessage(final Account account, 1238 final Message message, 1239 MessagingListener listener) { 1240 try { 1241 Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); 1242 LocalFolder localFolder = 1243 (LocalFolder) localStore.getFolder(account.getOutboxFolderName()); 1244 localFolder.open(OpenMode.READ_WRITE); 1245 localFolder.appendMessages(new Message[] { 1246 message 1247 }); 1248 Message localMessage = localFolder.getMessage(message.getUid()); 1249 localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); 1250 localFolder.close(false); 1251 sendPendingMessages(account, null); 1252 } 1253 catch (Exception e) { 1254 for (MessagingListener l : mListeners) { 1255 // TODO general failed 1256 } 1257 } 1258 } 1259 1260 /** 1261 * Attempt to send any messages that are sitting in the Outbox. 1262 * @param account 1263 * @param listener 1264 */ 1265 public void sendPendingMessages(final Account account, 1266 MessagingListener listener) { 1267 put("sendPendingMessages", listener, new Runnable() { 1268 public void run() { 1269 sendPendingMessagesSynchronous(account); 1270 } 1271 }); 1272 } 1273 1274 /** 1275 * Attempt to send any messages that are sitting in the Outbox. 1276 * @param account 1277 * @param listener 1278 */ 1279 public void sendPendingMessagesSynchronous(final Account account) { 1280 try { 1281 Store localStore = Store.getInstance( 1282 account.getLocalStoreUri(), 1283 mApplication); 1284 Folder localFolder = localStore.getFolder( 1285 account.getOutboxFolderName()); 1286 if (!localFolder.exists()) { 1287 return; 1288 } 1289 localFolder.open(OpenMode.READ_WRITE); 1290 1291 Message[] localMessages = localFolder.getMessages(null); 1292 1293 /* 1294 * The profile we will use to pull all of the content 1295 * for a given local message into memory for sending. 1296 */ 1297 FetchProfile fp = new FetchProfile(); 1298 fp.add(FetchProfile.Item.ENVELOPE); 1299 fp.add(FetchProfile.Item.BODY); 1300 1301 LocalFolder localSentFolder = 1302 (LocalFolder) localStore.getFolder( 1303 account.getSentFolderName()); 1304 1305 Sender sender = Sender.getInstance(account.getSenderUri(), mApplication); 1306 for (Message message : localMessages) { 1307 try { 1308 localFolder.fetch(new Message[] { message }, fp, null); 1309 try { 1310 message.setFlag(Flag.X_SEND_IN_PROGRESS, true); 1311 sender.sendMessage(message); 1312 message.setFlag(Flag.X_SEND_IN_PROGRESS, false); 1313 localFolder.copyMessages( 1314 new Message[] { message }, 1315 localSentFolder); 1316 1317 PendingCommand command = new PendingCommand(); 1318 command.command = PENDING_COMMAND_APPEND; 1319 command.arguments = 1320 new String[] { 1321 localSentFolder.getName(), 1322 message.getUid() }; 1323 queuePendingCommand(account, command); 1324 processPendingCommands(account); 1325 message.setFlag(Flag.X_DESTROYED, true); 1326 } 1327 catch (Exception e) { 1328 message.setFlag(Flag.X_SEND_FAILED, true); 1329 } 1330 } 1331 catch (Exception e) { 1332 /* 1333 * We ignore this exception because a future refresh will retry this 1334 * message. 1335 */ 1336 } 1337 } 1338 localFolder.expunge(); 1339 if (localFolder.getMessageCount() == 0) { 1340 localFolder.delete(false); 1341 } 1342 for (MessagingListener l : mListeners) { 1343 l.sendPendingMessagesCompleted(account); 1344 } 1345 } 1346 catch (Exception e) { 1347 for (MessagingListener l : mListeners) { 1348 // TODO general failed 1349 } 1350 } 1351 } 1352 1353 /** 1354 * We do the local portion of this synchronously because other activities may have to make 1355 * updates based on what happens here 1356 * @param account 1357 * @param folder 1358 * @param message 1359 * @param listener 1360 */ 1361 public void deleteMessage(final Account account, final String folder, final Message message, 1362 MessagingListener listener) { 1363 if (folder.equals(account.getTrashFolderName())) { 1364 return; 1365 } 1366 try { 1367 Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); 1368 Folder localFolder = localStore.getFolder(folder); 1369 Folder localTrashFolder = localStore.getFolder(account.getTrashFolderName()); 1370 1371 localFolder.copyMessages(new Message[] { message }, localTrashFolder); 1372 message.setFlag(Flag.DELETED, true); 1373 1374 if (account.getDeletePolicy() == Account.DELETE_POLICY_ON_DELETE) { 1375 PendingCommand command = new PendingCommand(); 1376 command.command = PENDING_COMMAND_TRASH; 1377 command.arguments = new String[] { folder, message.getUid() }; 1378 queuePendingCommand(account, command); 1379 processPendingCommands(account); 1380 } 1381 } 1382 catch (MessagingException me) { 1383 throw new RuntimeException("Error deleting message from local store.", me); 1384 } 1385 } 1386 1387 public void emptyTrash(final Account account, MessagingListener listener) { 1388 put("emptyTrash", listener, new Runnable() { 1389 public void run() { 1390 // TODO IMAP 1391 try { 1392 Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); 1393 Folder localFolder = localStore.getFolder(account.getTrashFolderName()); 1394 localFolder.open(OpenMode.READ_WRITE); 1395 Message[] messages = localFolder.getMessages(null); 1396 localFolder.setFlags(messages, new Flag[] { 1397 Flag.DELETED 1398 }, true); 1399 localFolder.close(true); 1400 for (MessagingListener l : mListeners) { 1401 l.emptyTrashCompleted(account); 1402 } 1403 } 1404 catch (Exception e) { 1405 // TODO 1406 if (Config.LOGV) { 1407 Log.v(Email.LOG_TAG, "emptyTrash"); 1408 } 1409 } 1410 } 1411 }); 1412 } 1413 1414 /** 1415 * Checks mail for one or multiple accounts. If account is null all accounts 1416 * are checked. 1417 * 1418 * TODO: There is no use case for "check all accounts". Clean up this API to remove 1419 * that case. Callers can supply the appropriate list. 1420 * 1421 * @param context 1422 * @param accountsToCheck List of accounts to check, or null to check all accounts 1423 * @param listener 1424 */ 1425 public void checkMail(final Context context, final Account[] accountsToCheck, 1426 final MessagingListener listener) { 1427 for (MessagingListener l : mListeners) { 1428 l.checkMailStarted(context, null); // TODO this needs to pass the actual array 1429 } 1430 put("checkMail", listener, new Runnable() { 1431 public void run() { 1432 Account[] accounts = accountsToCheck; 1433 if (accounts == null) { 1434 accounts = Preferences.getPreferences(context).getAccounts(); 1435 } 1436 for (Account account : accounts) { 1437 sendPendingMessagesSynchronous(account); 1438 synchronizeMailboxSyncronous(account, Email.INBOX); 1439 } 1440 for (MessagingListener l : mListeners) { 1441 l.checkMailFinished(context, null); // TODO this needs to pass the actual array 1442 } 1443 } 1444 }); 1445 } 1446 1447 public void saveDraft(final Account account, final Message message) { 1448 try { 1449 Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); 1450 LocalFolder localFolder = 1451 (LocalFolder) localStore.getFolder(account.getDraftsFolderName()); 1452 localFolder.open(OpenMode.READ_WRITE); 1453 localFolder.appendMessages(new Message[] { 1454 message 1455 }); 1456 Message localMessage = localFolder.getMessage(message.getUid()); 1457 localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); 1458 1459 PendingCommand command = new PendingCommand(); 1460 command.command = PENDING_COMMAND_APPEND; 1461 command.arguments = new String[] { 1462 localFolder.getName(), 1463 localMessage.getUid() }; 1464 queuePendingCommand(account, command); 1465 processPendingCommands(account); 1466 } 1467 catch (MessagingException e) { 1468 Log.e(Email.LOG_TAG, "Unable to save message as draft.", e); 1469 } 1470 } 1471 1472 class Command { 1473 public Runnable runnable; 1474 1475 public MessagingListener listener; 1476 1477 public String description; 1478 } 1479} 1480