MessagingController.java revision 4836f3c289f728048435ea2b5055e65b167f5a88
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, null); 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 account.getStoreCallbacks()); 215 216 Folder[] remoteFolders = store.getPersonalNamespaces(); 217 218 Store localStore = Store.getInstance( 219 account.getLocalStoreUri(), mApplication, null); 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, null); 288 Folder localFolder = localStore.getFolder(folder); 289 localFolder.open(OpenMode.READ_WRITE, null); 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 Store.StoreInfo info = Store.StoreInfo.getStoreInfo(account.getStoreUri(), 314 mApplication); 315 LocalStore localStore = (LocalStore) Store.getInstance( 316 account.getLocalStoreUri(), mApplication, null); 317 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 318 int oldLimit = localFolder.getVisibleLimit(); 319 if (oldLimit <= 0) { 320 oldLimit = info.mVisibleLimitDefault; 321 } 322 localFolder.setVisibleLimit(oldLimit + info.mVisibleLimitIncrement); 323 synchronizeMailbox(account, folder, listener); 324 } 325 catch (MessagingException me) { 326 throw new RuntimeException("Unable to set visible limit on folder", me); 327 } 328 } 329 330 public void resetVisibleLimits(Account[] accounts) { 331 for (Account account : accounts) { 332 try { 333 Store.StoreInfo info = Store.StoreInfo.getStoreInfo(account.getStoreUri(), 334 mApplication); 335 LocalStore localStore = 336 (LocalStore) Store.getInstance(account.getLocalStoreUri(), mApplication, null); 337 localStore.resetVisibleLimits(info.mVisibleLimitDefault); 338 } 339 catch (MessagingException e) { 340 Log.e(Email.LOG_TAG, "Unable to reset visible limits", e); 341 } 342 } 343 } 344 345 /** 346 * Start background synchronization of the specified folder. 347 * @param account 348 * @param folder 349 * @param numNewestMessagesToKeep Specifies the number of messages that should be 350 * considered as part of the window of available messages. This number effectively limits 351 * the user's view into the mailbox to the newest (numNewestMessagesToKeep) messages. 352 * @param listener 353 */ 354 public void synchronizeMailbox(final Account account, final String folder, 355 MessagingListener listener) { 356 /* 357 * We don't ever sync the Outbox. 358 */ 359 if (folder.equals(account.getOutboxFolderName())) { 360 return; 361 } 362 for (MessagingListener l : mListeners) { 363 l.synchronizeMailboxStarted(account, folder); 364 } 365 put("synchronizeMailbox", listener, new Runnable() { 366 public void run() { 367 synchronizeMailboxSyncronous(account, folder); 368 } 369 }); 370 } 371 372 /** 373 * Start foreground synchronization of the specified folder. This is generally only called 374 * by synchronizeMailbox. 375 * @param account 376 * @param folder 377 * @param numNewestMessagesToKeep Specifies the number of messages that should be 378 * considered as part of the window of available messages. This number effectively limits 379 * the user's view into the mailbox to the newest (numNewestMessagesToKeep) messages. 380 * @param listener 381 * 382 * TODO Break this method up into smaller chunks. 383 */ 384 public void synchronizeMailboxSyncronous(final Account account, final String folder) { 385 for (MessagingListener l : mListeners) { 386 l.synchronizeMailboxStarted(account, folder); 387 } 388 try { 389 processPendingCommandsSynchronous(account); 390 391 /* 392 * Get the message list from the local store and create an index of 393 * the uids within the list. 394 */ 395 final LocalStore localStore = 396 (LocalStore) Store.getInstance(account.getLocalStoreUri(), mApplication, null); 397 final LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 398 localFolder.open(OpenMode.READ_WRITE, null); 399 Message[] localMessages = localFolder.getMessages(null); 400 HashMap<String, Message> localUidMap = new HashMap<String, Message>(); 401 for (Message message : localMessages) { 402 localUidMap.put(message.getUid(), message); 403 } 404 405 Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication, 406 account.getStoreCallbacks()); 407 Folder remoteFolder = remoteStore.getFolder(folder); 408 409 /* 410 * If the folder is a "special" folder we need to see if it exists 411 * on the remote server. It if does not exist we'll try to create it. If we 412 * can't create we'll abort. This will happen on every single Pop3 folder as 413 * designed and on Imap folders during error conditions. This allows us 414 * to treat Pop3 and Imap the same in this code. 415 */ 416 if (folder.equals(account.getTrashFolderName()) || 417 folder.equals(account.getSentFolderName()) || 418 folder.equals(account.getDraftsFolderName())) { 419 if (!remoteFolder.exists()) { 420 if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { 421 for (MessagingListener l : mListeners) { 422 l.synchronizeMailboxFinished(account, folder, 0, 0); 423 } 424 return; 425 } 426 } 427 } 428 429 /* 430 * Synchronization process: 431 Open the folder 432 Upload any local messages that are marked as PENDING_UPLOAD (Drafts, Sent, Trash) 433 Get the message count 434 Get the list of the newest Email.DEFAULT_VISIBLE_LIMIT messages 435 getMessages(messageCount - Email.DEFAULT_VISIBLE_LIMIT, messageCount) 436 See if we have each message locally, if not fetch it's flags and envelope 437 Get and update the unread count for the folder 438 Update the remote flags of any messages we have locally with an internal date 439 newer than the remote message. 440 Get the current flags for any messages we have locally but did not just download 441 Update local flags 442 For any message we have locally but not remotely, delete the local message to keep 443 cache clean. 444 Download larger parts of any new messages. 445 (Optional) Download small attachments in the background. 446 */ 447 448 /* 449 * Open the remote folder. This pre-loads certain metadata like message count. 450 */ 451 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 452 453 /* 454 * Trash any remote messages that are marked as trashed locally. 455 */ 456 457 /* 458 * Get the remote message count. 459 */ 460 int remoteMessageCount = remoteFolder.getMessageCount(); 461 462 int visibleLimit = localFolder.getVisibleLimit(); 463 if (visibleLimit <= 0) { 464 Store.StoreInfo info = Store.StoreInfo.getStoreInfo(account.getStoreUri(), 465 mApplication); 466 visibleLimit = info.mVisibleLimitDefault; 467 localFolder.setVisibleLimit(visibleLimit); 468 } 469 470 Message[] remoteMessages = new Message[0]; 471 final ArrayList<Message> unsyncedMessages = new ArrayList<Message>(); 472 HashMap<String, Message> remoteUidMap = new HashMap<String, Message>(); 473 474 if (remoteMessageCount > 0) { 475 /* 476 * Message numbers start at 1. 477 */ 478 int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1; 479 int remoteEnd = remoteMessageCount; 480 remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null); 481 for (Message message : remoteMessages) { 482 remoteUidMap.put(message.getUid(), message); 483 } 484 485 /* 486 * Get a list of the messages that are in the remote list but not on the 487 * local store, or messages that are in the local store but failed to download 488 * on the last sync. These are the new messages that we will download. 489 */ 490 for (Message message : remoteMessages) { 491 Message localMessage = localUidMap.get(message.getUid()); 492 if (localMessage == null || 493 (!localMessage.isSet(Flag.X_DOWNLOADED_FULL) && 494 !localMessage.isSet(Flag.X_DOWNLOADED_PARTIAL))) { 495 unsyncedMessages.add(message); 496 } 497 } 498 } 499 500 /* 501 * A list of messages that were downloaded and which did not have the Seen flag set. 502 * This will serve to indicate the true "new" message count that will be reported to 503 * the user via notification. 504 */ 505 final ArrayList<Message> newMessages = new ArrayList<Message>(); 506 507 /* 508 * Fetch the flags and envelope only of the new messages. This is intended to get us 509 * critical data as fast as possible, and then we'll fill in the details. 510 */ 511 if (unsyncedMessages.size() > 0) { 512 513 /* 514 * Reverse the order of the messages. Depending on the server this may get us 515 * fetch results for newest to oldest. If not, no harm done. 516 */ 517 Collections.reverse(unsyncedMessages); 518 519 FetchProfile fp = new FetchProfile(); 520 fp.add(FetchProfile.Item.FLAGS); 521 fp.add(FetchProfile.Item.ENVELOPE); 522 remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp, 523 new MessageRetrievalListener() { 524 public void messageFinished(Message message, int number, int ofTotal) { 525 try { 526 // Store the new message locally 527 localFolder.appendMessages(new Message[] { 528 message 529 }); 530 531 // And include it in the view 532 if (message.getSubject() != null && 533 message.getFrom() != null) { 534 /* 535 * We check to make sure that we got something worth 536 * showing (subject and from) because some protocols 537 * (POP) may not be able to give us headers for 538 * ENVELOPE, only size. 539 */ 540 for (MessagingListener l : mListeners) { 541 l.synchronizeMailboxNewMessage(account, folder, 542 localFolder.getMessage(message.getUid())); 543 } 544 } 545 546 if (!message.isSet(Flag.SEEN)) { 547 newMessages.add(message); 548 } 549 } 550 catch (Exception e) { 551 Log.e(Email.LOG_TAG, 552 "Error while storing downloaded message.", 553 e); 554 } 555 } 556 557 public void messageStarted(String uid, int number, int ofTotal) { 558 } 559 }); 560 } 561 562 /* 563 * Refresh the flags for any messages in the local store that we didn't just 564 * download. 565 */ 566 FetchProfile fp = new FetchProfile(); 567 fp.add(FetchProfile.Item.FLAGS); 568 remoteFolder.fetch(remoteMessages, fp, null); 569 for (Message remoteMessage : remoteMessages) { 570 Message localMessage = localFolder.getMessage(remoteMessage.getUid()); 571 if (localMessage == null) { 572 continue; 573 } 574 if (remoteMessage.isSet(Flag.SEEN) != localMessage.isSet(Flag.SEEN)) { 575 localMessage.setFlag(Flag.SEEN, remoteMessage.isSet(Flag.SEEN)); 576 for (MessagingListener l : mListeners) { 577 l.synchronizeMailboxNewMessage(account, folder, localMessage); 578 } 579 } 580 } 581 582 /* 583 * Get and store the unread message count. 584 */ 585 int remoteUnreadMessageCount = remoteFolder.getUnreadMessageCount(); 586 if (remoteUnreadMessageCount == -1) { 587 localFolder.setUnreadMessageCount(localFolder.getUnreadMessageCount() 588 + newMessages.size()); 589 } 590 else { 591 localFolder.setUnreadMessageCount(remoteUnreadMessageCount); 592 } 593 594 /* 595 * Remove any messages that are in the local store but no longer on the remote store. 596 */ 597 for (Message localMessage : localMessages) { 598 if (remoteUidMap.get(localMessage.getUid()) == null) { 599 localMessage.setFlag(Flag.X_DESTROYED, true); 600 for (MessagingListener l : mListeners) { 601 l.synchronizeMailboxRemovedMessage(account, folder, localMessage); 602 } 603 } 604 } 605 606 /* 607 * Now we download the actual content of messages. 608 */ 609 ArrayList<Message> largeMessages = new ArrayList<Message>(); 610 ArrayList<Message> smallMessages = new ArrayList<Message>(); 611 for (Message message : unsyncedMessages) { 612 /* 613 * Sort the messages into two buckets, small and large. Small messages will be 614 * downloaded fully and large messages will be downloaded in parts. By sorting 615 * into two buckets we can pipeline the commands for each set of messages 616 * into a single command to the server saving lots of round trips. 617 */ 618 if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) { 619 largeMessages.add(message); 620 } else { 621 smallMessages.add(message); 622 } 623 } 624 /* 625 * Grab the content of the small messages first. This is going to 626 * be very fast and at very worst will be a single up of a few bytes and a single 627 * download of 625k. 628 */ 629 fp = new FetchProfile(); 630 fp.add(FetchProfile.Item.BODY); 631 remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), 632 fp, new MessageRetrievalListener() { 633 public void messageFinished(Message message, int number, int ofTotal) { 634 try { 635 // Store the updated message locally 636 localFolder.appendMessages(new Message[] { 637 message 638 }); 639 640 Message localMessage = localFolder.getMessage(message.getUid()); 641 642 // Set a flag indicating this message has now be fully downloaded 643 localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); 644 645 // Update the listener with what we've found 646 for (MessagingListener l : mListeners) { 647 l.synchronizeMailboxNewMessage( 648 account, 649 folder, 650 localMessage); 651 } 652 } 653 catch (MessagingException me) { 654 655 } 656 } 657 658 public void messageStarted(String uid, int number, int ofTotal) { 659 } 660 }); 661 662 /* 663 * Now do the large messages that require more round trips. 664 */ 665 fp.clear(); 666 fp.add(FetchProfile.Item.STRUCTURE); 667 remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), 668 fp, null); 669 for (Message message : largeMessages) { 670 if (message.getBody() == null) { 671 /* 672 * The provider was unable to get the structure of the message, so 673 * we'll download a reasonable portion of the messge and mark it as 674 * incomplete so the entire thing can be downloaded later if the user 675 * wishes to download it. 676 */ 677 fp.clear(); 678 fp.add(FetchProfile.Item.BODY_SANE); 679 /* 680 * TODO a good optimization here would be to make sure that all Stores set 681 * the proper size after this fetch and compare the before and after size. If 682 * they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED 683 */ 684 685 remoteFolder.fetch(new Message[] { message }, fp, null); 686 // Store the updated message locally 687 localFolder.appendMessages(new Message[] { 688 message 689 }); 690 691 Message localMessage = localFolder.getMessage(message.getUid()); 692 693 // Set a flag indicating that the message has been partially downloaded and 694 // is ready for view. 695 localMessage.setFlag(Flag.X_DOWNLOADED_PARTIAL, true); 696 } else { 697 /* 698 * We have a structure to deal with, from which 699 * we can pull down the parts we want to actually store. 700 * Build a list of parts we are interested in. Text parts will be downloaded 701 * right now, attachments will be left for later. 702 */ 703 704 ArrayList<Part> viewables = new ArrayList<Part>(); 705 ArrayList<Part> attachments = new ArrayList<Part>(); 706 MimeUtility.collectParts(message, viewables, attachments); 707 708 /* 709 * Now download the parts we're interested in storing. 710 */ 711 for (Part part : viewables) { 712 fp.clear(); 713 fp.add(part); 714 // TODO what happens if the network connection dies? We've got partial 715 // messages with incorrect status stored. 716 remoteFolder.fetch(new Message[] { message }, fp, null); 717 } 718 // Store the updated message locally 719 localFolder.appendMessages(new Message[] { 720 message 721 }); 722 723 Message localMessage = localFolder.getMessage(message.getUid()); 724 725 // Set a flag indicating this message has been fully downloaded and can be 726 // viewed. 727 localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); 728 } 729 730 // Update the listener with what we've found 731 for (MessagingListener l : mListeners) { 732 l.synchronizeMailboxNewMessage( 733 account, 734 folder, 735 localFolder.getMessage(message.getUid())); 736 } 737 } 738 739 740 /* 741 * Notify listeners that we're finally done. 742 */ 743 for (MessagingListener l : mListeners) { 744 l.synchronizeMailboxFinished( 745 account, 746 folder, 747 remoteFolder.getMessageCount(), newMessages.size()); 748 } 749 750 remoteFolder.close(false); 751 localFolder.close(false); 752 } 753 catch (Exception e) { 754 if (Config.LOGV) { 755 Log.v(Email.LOG_TAG, "synchronizeMailbox", e); 756 } 757 for (MessagingListener l : mListeners) { 758 l.synchronizeMailboxFailed( 759 account, 760 folder, 761 e); 762 } 763 } 764 } 765 766 private void queuePendingCommand(Account account, PendingCommand command) { 767 try { 768 LocalStore localStore = (LocalStore) Store.getInstance( 769 account.getLocalStoreUri(), mApplication, null); 770 localStore.addPendingCommand(command); 771 } 772 catch (Exception e) { 773 throw new RuntimeException("Unable to enqueue pending command", e); 774 } 775 } 776 777 private void processPendingCommands(final Account account) { 778 put("processPendingCommands", null, new Runnable() { 779 public void run() { 780 try { 781 processPendingCommandsSynchronous(account); 782 } 783 catch (MessagingException me) { 784 if (Config.LOGV) { 785 Log.v(Email.LOG_TAG, "processPendingCommands", me); 786 } 787 /* 788 * Ignore any exceptions from the commands. Commands will be processed 789 * on the next round. 790 */ 791 } 792 } 793 }); 794 } 795 796 private void processPendingCommandsSynchronous(Account account) throws MessagingException { 797 LocalStore localStore = (LocalStore) Store.getInstance( 798 account.getLocalStoreUri(), mApplication, null); 799 ArrayList<PendingCommand> commands = localStore.getPendingCommands(); 800 for (PendingCommand command : commands) { 801 /* 802 * We specifically do not catch any exceptions here. If a command fails it is 803 * most likely due to a server or IO error and it must be retried before any 804 * other command processes. This maintains the order of the commands. 805 */ 806 if (PENDING_COMMAND_APPEND.equals(command.command)) { 807 processPendingAppend(command, account); 808 } 809 else if (PENDING_COMMAND_MARK_READ.equals(command.command)) { 810 processPendingMarkRead(command, account); 811 } 812 else if (PENDING_COMMAND_TRASH.equals(command.command)) { 813 processPendingTrash(command, account); 814 } 815 localStore.removePendingCommand(command); 816 } 817 } 818 819 /** 820 * Process a pending append message command. This command uploads a local message to the 821 * server, first checking to be sure that the server message is not newer than 822 * the local message. Once the local message is successfully processed it is deleted so 823 * that the server message will be synchronized down without an additional copy being 824 * created. 825 * TODO update the local message UID instead of deleteing it 826 * 827 * @param command arguments = (String folder, String uid) 828 * @param account 829 * @throws MessagingException 830 */ 831 private void processPendingAppend(PendingCommand command, Account account) 832 throws MessagingException { 833 String folder = command.arguments[0]; 834 String uid = command.arguments[1]; 835 836 LocalStore localStore = (LocalStore) Store.getInstance( 837 account.getLocalStoreUri(), mApplication, null); 838 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 839 LocalMessage localMessage = (LocalMessage) localFolder.getMessage(uid); 840 841 if (localMessage == null) { 842 return; 843 } 844 845 Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication, 846 account.getStoreCallbacks()); 847 Folder remoteFolder = remoteStore.getFolder(folder); 848 if (!remoteFolder.exists()) { 849 if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { 850 return; 851 } 852 } 853 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 854 if (remoteFolder.getMode() != OpenMode.READ_WRITE) { 855 return; 856 } 857 858 Message remoteMessage = null; 859 if (!localMessage.getUid().startsWith("Local") 860 && !localMessage.getUid().contains("-")) { 861 remoteMessage = remoteFolder.getMessage(localMessage.getUid()); 862 } 863 864 if (remoteMessage == null) { 865 /* 866 * If the message does not exist remotely we just upload it and then 867 * update our local copy with the new uid. 868 */ 869 FetchProfile fp = new FetchProfile(); 870 fp.add(FetchProfile.Item.BODY); 871 localFolder.fetch(new Message[] { localMessage }, fp, null); 872 String oldUid = localMessage.getUid(); 873 remoteFolder.appendMessages(new Message[] { localMessage }); 874 localFolder.changeUid(localMessage); 875 for (MessagingListener l : mListeners) { 876 l.messageUidChanged(account, folder, oldUid, localMessage.getUid()); 877 } 878 } 879 else { 880 /* 881 * If the remote message exists we need to determine which copy to keep. 882 */ 883 /* 884 * See if the remote message is newer than ours. 885 */ 886 FetchProfile fp = new FetchProfile(); 887 fp.add(FetchProfile.Item.ENVELOPE); 888 remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); 889 Date localDate = localMessage.getInternalDate(); 890 Date remoteDate = remoteMessage.getInternalDate(); 891 if (remoteDate.compareTo(localDate) > 0) { 892 /* 893 * If the remote message is newer than ours we'll just 894 * delete ours and move on. A sync will get the server message 895 * if we need to be able to see it. 896 */ 897 localMessage.setFlag(Flag.DELETED, true); 898 } 899 else { 900 /* 901 * Otherwise we'll upload our message and then delete the remote message. 902 */ 903 fp.clear(); 904 fp = new FetchProfile(); 905 fp.add(FetchProfile.Item.BODY); 906 localFolder.fetch(new Message[] { localMessage }, fp, null); 907 String oldUid = localMessage.getUid(); 908 remoteFolder.appendMessages(new Message[] { localMessage }); 909 localFolder.changeUid(localMessage); 910 for (MessagingListener l : mListeners) { 911 l.messageUidChanged(account, folder, oldUid, localMessage.getUid()); 912 } 913 remoteMessage.setFlag(Flag.DELETED, true); 914 } 915 } 916 } 917 918 /** 919 * Process a pending trash message command. 920 * 921 * @param command arguments = (String folder, String uid) 922 * @param account 923 * @throws MessagingException 924 */ 925 private void processPendingTrash(PendingCommand command, Account account) 926 throws MessagingException { 927 String folder = command.arguments[0]; 928 String uid = command.arguments[1]; 929 930 LocalStore localStore = (LocalStore) Store.getInstance( 931 account.getLocalStoreUri(), mApplication, null); 932 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 933 934 Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication, 935 account.getStoreCallbacks()); 936 Folder remoteFolder = remoteStore.getFolder(folder); 937 if (!remoteFolder.exists()) { 938 return; 939 } 940 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 941 if (remoteFolder.getMode() != OpenMode.READ_WRITE) { 942 return; 943 } 944 945 Message remoteMessage = null; 946 if (!uid.startsWith("Local") 947 && !uid.contains("-")) { 948 remoteMessage = remoteFolder.getMessage(uid); 949 } 950 if (remoteMessage == null) { 951 return; 952 } 953 954 Folder remoteTrashFolder = remoteStore.getFolder(account.getTrashFolderName()); 955 /* 956 * Attempt to copy the remote message to the remote trash folder. 957 */ 958 if (!remoteTrashFolder.exists()) { 959 /* 960 * If the remote trash folder doesn't exist we try to create it. 961 */ 962 remoteTrashFolder.create(FolderType.HOLDS_MESSAGES); 963 } 964 965 if (remoteTrashFolder.exists()) { 966 remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder); 967 } 968 969 remoteMessage.setFlag(Flag.DELETED, true); 970 remoteFolder.expunge(); 971 } 972 973 /** 974 * Processes a pending mark read or unread command. 975 * 976 * @param command arguments = (String folder, String uid, boolean read) 977 * @param account 978 */ 979 private void processPendingMarkRead(PendingCommand command, Account account) 980 throws MessagingException { 981 String folder = command.arguments[0]; 982 String uid = command.arguments[1]; 983 boolean read = Boolean.parseBoolean(command.arguments[2]); 984 985 LocalStore localStore = (LocalStore) Store.getInstance( 986 account.getLocalStoreUri(), mApplication, null); 987 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 988 989 Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication, 990 account.getStoreCallbacks()); 991 Folder remoteFolder = remoteStore.getFolder(folder); 992 if (!remoteFolder.exists()) { 993 return; 994 } 995 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 996 if (remoteFolder.getMode() != OpenMode.READ_WRITE) { 997 return; 998 } 999 Message remoteMessage = null; 1000 if (!uid.startsWith("Local") 1001 && !uid.contains("-")) { 1002 remoteMessage = remoteFolder.getMessage(uid); 1003 } 1004 if (remoteMessage == null) { 1005 return; 1006 } 1007 remoteMessage.setFlag(Flag.SEEN, read); 1008 } 1009 1010 /** 1011 * Mark the message with the given account, folder and uid either Seen or not Seen. 1012 * @param account 1013 * @param folder 1014 * @param uid 1015 * @param seen 1016 */ 1017 public void markMessageRead( 1018 final Account account, 1019 final String folder, 1020 final String uid, 1021 final boolean seen) { 1022 try { 1023 Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication, null); 1024 Folder localFolder = localStore.getFolder(folder); 1025 localFolder.open(OpenMode.READ_WRITE, null); 1026 1027 Message message = localFolder.getMessage(uid); 1028 message.setFlag(Flag.SEEN, seen); 1029 PendingCommand command = new PendingCommand(); 1030 command.command = PENDING_COMMAND_MARK_READ; 1031 command.arguments = new String[] { folder, uid, Boolean.toString(seen) }; 1032 queuePendingCommand(account, command); 1033 processPendingCommands(account); 1034 } 1035 catch (MessagingException me) { 1036 throw new RuntimeException(me); 1037 } 1038 } 1039 1040 private void loadMessageForViewRemote(final Account account, final String folder, 1041 final String uid, MessagingListener listener) { 1042 put("loadMessageForViewRemote", listener, new Runnable() { 1043 public void run() { 1044 try { 1045 Store localStore = Store.getInstance( 1046 account.getLocalStoreUri(), mApplication, null); 1047 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 1048 localFolder.open(OpenMode.READ_WRITE, null); 1049 1050 Message message = localFolder.getMessage(uid); 1051 1052 if (message.isSet(Flag.X_DOWNLOADED_FULL)) { 1053 /* 1054 * If the message has been synchronized since we were called we'll 1055 * just hand it back cause it's ready to go. 1056 */ 1057 FetchProfile fp = new FetchProfile(); 1058 fp.add(FetchProfile.Item.ENVELOPE); 1059 fp.add(FetchProfile.Item.BODY); 1060 localFolder.fetch(new Message[] { message }, fp, null); 1061 1062 for (MessagingListener l : mListeners) { 1063 l.loadMessageForViewBodyAvailable(account, folder, uid, message); 1064 } 1065 for (MessagingListener l : mListeners) { 1066 l.loadMessageForViewFinished(account, folder, uid, message); 1067 } 1068 localFolder.close(false); 1069 return; 1070 } 1071 1072 /* 1073 * At this point the message is not available, so we need to download it 1074 * fully if possible. 1075 */ 1076 1077 Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication, 1078 account.getStoreCallbacks()); 1079 Folder remoteFolder = remoteStore.getFolder(folder); 1080 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 1081 1082 // Get the remote message and fully download it 1083 Message remoteMessage = remoteFolder.getMessage(uid); 1084 FetchProfile fp = new FetchProfile(); 1085 fp.add(FetchProfile.Item.BODY); 1086 remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); 1087 1088 // Store the message locally and load the stored message into memory 1089 localFolder.appendMessages(new Message[] { remoteMessage }); 1090 message = localFolder.getMessage(uid); 1091 localFolder.fetch(new Message[] { message }, fp, null); 1092 1093 // This is a view message request, so mark it read 1094 if (!message.isSet(Flag.SEEN)) { 1095 markMessageRead(account, folder, uid, true); 1096 } 1097 1098 // Mark that this message is now fully synched 1099 message.setFlag(Flag.X_DOWNLOADED_FULL, true); 1100 1101 for (MessagingListener l : mListeners) { 1102 l.loadMessageForViewBodyAvailable(account, folder, uid, message); 1103 } 1104 for (MessagingListener l : mListeners) { 1105 l.loadMessageForViewFinished(account, folder, uid, message); 1106 } 1107 remoteFolder.close(false); 1108 localFolder.close(false); 1109 } 1110 catch (Exception e) { 1111 for (MessagingListener l : mListeners) { 1112 l.loadMessageForViewFailed(account, folder, uid, e.getMessage()); 1113 } 1114 } 1115 } 1116 }); 1117 } 1118 1119 public void loadMessageForView(final Account account, final String folder, final String uid, 1120 MessagingListener listener) { 1121 for (MessagingListener l : mListeners) { 1122 l.loadMessageForViewStarted(account, folder, uid); 1123 } 1124 try { 1125 Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication, null); 1126 LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); 1127 localFolder.open(OpenMode.READ_WRITE, null); 1128 1129 Message message = localFolder.getMessage(uid); 1130 1131 for (MessagingListener l : mListeners) { 1132 l.loadMessageForViewHeadersAvailable(account, folder, uid, message); 1133 } 1134 1135 if (!message.isSet(Flag.X_DOWNLOADED_FULL)) { 1136 loadMessageForViewRemote(account, folder, uid, listener); 1137 localFolder.close(false); 1138 return; 1139 } 1140 1141 if (!message.isSet(Flag.SEEN)) { 1142 markMessageRead(account, folder, uid, true); 1143 } 1144 1145 FetchProfile fp = new FetchProfile(); 1146 fp.add(FetchProfile.Item.ENVELOPE); 1147 fp.add(FetchProfile.Item.BODY); 1148 localFolder.fetch(new Message[] { 1149 message 1150 }, fp, null); 1151 1152 for (MessagingListener l : mListeners) { 1153 l.loadMessageForViewBodyAvailable(account, folder, uid, message); 1154 } 1155 1156 for (MessagingListener l : mListeners) { 1157 l.loadMessageForViewFinished(account, folder, uid, message); 1158 } 1159 localFolder.close(false); 1160 } 1161 catch (Exception e) { 1162 for (MessagingListener l : mListeners) { 1163 l.loadMessageForViewFailed(account, folder, uid, e.getMessage()); 1164 } 1165 } 1166 } 1167 1168 /** 1169 * Attempts to load the attachment specified by part from the given account and message. 1170 * @param account 1171 * @param message 1172 * @param part 1173 * @param listener 1174 */ 1175 public void loadAttachment( 1176 final Account account, 1177 final Message message, 1178 final Part part, 1179 final Object tag, 1180 MessagingListener listener) { 1181 /* 1182 * Check if the attachment has already been downloaded. If it has there's no reason to 1183 * download it, so we just tell the listener that it's ready to go. 1184 */ 1185 try { 1186 if (part.getBody() != null) { 1187 for (MessagingListener l : mListeners) { 1188 l.loadAttachmentStarted(account, message, part, tag, false); 1189 } 1190 1191 for (MessagingListener l : mListeners) { 1192 l.loadAttachmentFinished(account, message, part, tag); 1193 } 1194 return; 1195 } 1196 } 1197 catch (MessagingException me) { 1198 /* 1199 * If the header isn't there the attachment isn't downloaded yet, so just continue 1200 * on. 1201 */ 1202 } 1203 1204 for (MessagingListener l : mListeners) { 1205 l.loadAttachmentStarted(account, message, part, tag, true); 1206 } 1207 1208 put("loadAttachment", listener, new Runnable() { 1209 public void run() { 1210 try { 1211 LocalStore localStore = (LocalStore) Store.getInstance( 1212 account.getLocalStoreUri(), mApplication, null); 1213 /* 1214 * We clear out any attachments already cached in the entire store and then 1215 * we update the passed in message to reflect that there are no cached 1216 * attachments. This is in support of limiting the account to having one 1217 * attachment downloaded at a time. 1218 */ 1219 localStore.pruneCachedAttachments(); 1220 ArrayList<Part> viewables = new ArrayList<Part>(); 1221 ArrayList<Part> attachments = new ArrayList<Part>(); 1222 MimeUtility.collectParts(message, viewables, attachments); 1223 for (Part attachment : attachments) { 1224 attachment.setBody(null); 1225 } 1226 Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication, 1227 account.getStoreCallbacks()); 1228 LocalFolder localFolder = 1229 (LocalFolder) localStore.getFolder(message.getFolder().getName()); 1230 Folder remoteFolder = remoteStore.getFolder(message.getFolder().getName()); 1231 remoteFolder.open(OpenMode.READ_WRITE, localFolder.getPersistentCallbacks()); 1232 1233 FetchProfile fp = new FetchProfile(); 1234 fp.add(part); 1235 remoteFolder.fetch(new Message[] { message }, fp, null); 1236 localFolder.updateMessage((LocalMessage)message); 1237 localFolder.close(false); 1238 for (MessagingListener l : mListeners) { 1239 l.loadAttachmentFinished(account, message, part, tag); 1240 } 1241 } 1242 catch (MessagingException me) { 1243 if (Config.LOGV) { 1244 Log.v(Email.LOG_TAG, "", me); 1245 } 1246 for (MessagingListener l : mListeners) { 1247 l.loadAttachmentFailed(account, message, part, tag, me.getMessage()); 1248 } 1249 } 1250 } 1251 }); 1252 } 1253 1254 /** 1255 * Stores the given message in the Outbox and starts a sendPendingMessages command to 1256 * attempt to send the message. 1257 * @param account 1258 * @param message 1259 * @param listener 1260 */ 1261 public void sendMessage(final Account account, 1262 final Message message, 1263 MessagingListener listener) { 1264 try { 1265 Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication, null); 1266 LocalFolder localFolder = 1267 (LocalFolder) localStore.getFolder(account.getOutboxFolderName()); 1268 localFolder.open(OpenMode.READ_WRITE, null); 1269 localFolder.appendMessages(new Message[] { 1270 message 1271 }); 1272 Message localMessage = localFolder.getMessage(message.getUid()); 1273 localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); 1274 localFolder.close(false); 1275 sendPendingMessages(account, null); 1276 } 1277 catch (Exception e) { 1278 for (MessagingListener l : mListeners) { 1279 // TODO general failed 1280 } 1281 } 1282 } 1283 1284 /** 1285 * Attempt to send any messages that are sitting in the Outbox. 1286 * @param account 1287 * @param listener 1288 */ 1289 public void sendPendingMessages(final Account account, 1290 MessagingListener listener) { 1291 put("sendPendingMessages", listener, new Runnable() { 1292 public void run() { 1293 sendPendingMessagesSynchronous(account); 1294 } 1295 }); 1296 } 1297 1298 /** 1299 * Attempt to send any messages that are sitting in the Outbox. 1300 * @param account 1301 * @param listener 1302 */ 1303 public void sendPendingMessagesSynchronous(final Account account) { 1304 try { 1305 Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication, null); 1306 Folder localFolder = localStore.getFolder( 1307 account.getOutboxFolderName()); 1308 if (!localFolder.exists()) { 1309 return; 1310 } 1311 localFolder.open(OpenMode.READ_WRITE, null); 1312 1313 Message[] localMessages = localFolder.getMessages(null); 1314 1315 /* 1316 * The profile we will use to pull all of the content 1317 * for a given local message into memory for sending. 1318 */ 1319 FetchProfile fp = new FetchProfile(); 1320 fp.add(FetchProfile.Item.ENVELOPE); 1321 fp.add(FetchProfile.Item.BODY); 1322 1323 LocalFolder localSentFolder = 1324 (LocalFolder) localStore.getFolder( 1325 account.getSentFolderName()); 1326 1327 Sender sender = Sender.getInstance(account.getSenderUri(), mApplication); 1328 for (Message message : localMessages) { 1329 try { 1330 localFolder.fetch(new Message[] { message }, fp, null); 1331 try { 1332 message.setFlag(Flag.X_SEND_IN_PROGRESS, true); 1333 sender.sendMessage(message); 1334 message.setFlag(Flag.X_SEND_IN_PROGRESS, false); 1335 localFolder.copyMessages( 1336 new Message[] { message }, 1337 localSentFolder); 1338 1339 PendingCommand command = new PendingCommand(); 1340 command.command = PENDING_COMMAND_APPEND; 1341 command.arguments = 1342 new String[] { 1343 localSentFolder.getName(), 1344 message.getUid() }; 1345 queuePendingCommand(account, command); 1346 processPendingCommands(account); 1347 message.setFlag(Flag.X_DESTROYED, true); 1348 } 1349 catch (Exception e) { 1350 message.setFlag(Flag.X_SEND_FAILED, true); 1351 } 1352 } 1353 catch (Exception e) { 1354 /* 1355 * We ignore this exception because a future refresh will retry this 1356 * message. 1357 */ 1358 } 1359 } 1360 localFolder.expunge(); 1361 if (localFolder.getMessageCount() == 0) { 1362 localFolder.delete(false); 1363 } 1364 for (MessagingListener l : mListeners) { 1365 l.sendPendingMessagesCompleted(account); 1366 } 1367 } 1368 catch (Exception e) { 1369 for (MessagingListener l : mListeners) { 1370 // TODO general failed 1371 } 1372 } 1373 } 1374 1375 /** 1376 * We do the local portion of this synchronously because other activities may have to make 1377 * updates based on what happens here 1378 * @param account 1379 * @param folder 1380 * @param message 1381 * @param listener 1382 */ 1383 public void deleteMessage(final Account account, final String folder, final Message message, 1384 MessagingListener listener) { 1385 if (folder.equals(account.getTrashFolderName())) { 1386 return; 1387 } 1388 try { 1389 Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication, null); 1390 Folder localFolder = localStore.getFolder(folder); 1391 Folder localTrashFolder = localStore.getFolder(account.getTrashFolderName()); 1392 1393 localFolder.copyMessages(new Message[] { message }, localTrashFolder); 1394 message.setFlag(Flag.DELETED, true); 1395 1396 if (account.getDeletePolicy() == Account.DELETE_POLICY_ON_DELETE) { 1397 PendingCommand command = new PendingCommand(); 1398 command.command = PENDING_COMMAND_TRASH; 1399 command.arguments = new String[] { folder, message.getUid() }; 1400 queuePendingCommand(account, command); 1401 processPendingCommands(account); 1402 } 1403 } 1404 catch (MessagingException me) { 1405 throw new RuntimeException("Error deleting message from local store.", me); 1406 } 1407 } 1408 1409 public void emptyTrash(final Account account, MessagingListener listener) { 1410 put("emptyTrash", listener, new Runnable() { 1411 public void run() { 1412 // TODO IMAP 1413 try { 1414 Store localStore = Store.getInstance( 1415 account.getLocalStoreUri(), mApplication, null); 1416 Folder localFolder = localStore.getFolder(account.getTrashFolderName()); 1417 localFolder.open(OpenMode.READ_WRITE, null); 1418 Message[] messages = localFolder.getMessages(null); 1419 localFolder.setFlags(messages, new Flag[] { 1420 Flag.DELETED 1421 }, true); 1422 localFolder.close(true); 1423 for (MessagingListener l : mListeners) { 1424 l.emptyTrashCompleted(account); 1425 } 1426 } 1427 catch (Exception e) { 1428 // TODO 1429 if (Config.LOGV) { 1430 Log.v(Email.LOG_TAG, "emptyTrash"); 1431 } 1432 } 1433 } 1434 }); 1435 } 1436 1437 /** 1438 * Checks mail for one or multiple accounts. If account is null all accounts 1439 * are checked. 1440 * 1441 * TODO: There is no use case for "check all accounts". Clean up this API to remove 1442 * that case. Callers can supply the appropriate list. 1443 * 1444 * @param context 1445 * @param accountsToCheck List of accounts to check, or null to check all accounts 1446 * @param listener 1447 */ 1448 public void checkMail(final Context context, final Account[] accountsToCheck, 1449 final MessagingListener listener) { 1450 for (MessagingListener l : mListeners) { 1451 l.checkMailStarted(context, null); // TODO this needs to pass the actual array 1452 } 1453 put("checkMail", listener, new Runnable() { 1454 public void run() { 1455 Account[] accounts = accountsToCheck; 1456 if (accounts == null) { 1457 accounts = Preferences.getPreferences(context).getAccounts(); 1458 } 1459 for (Account account : accounts) { 1460 sendPendingMessagesSynchronous(account); 1461 synchronizeMailboxSyncronous(account, Email.INBOX); 1462 } 1463 for (MessagingListener l : mListeners) { 1464 l.checkMailFinished(context, null); // TODO this needs to pass the actual array 1465 } 1466 } 1467 }); 1468 } 1469 1470 public void saveDraft(final Account account, final Message message) { 1471 try { 1472 Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication, null); 1473 LocalFolder localFolder = 1474 (LocalFolder) localStore.getFolder(account.getDraftsFolderName()); 1475 localFolder.open(OpenMode.READ_WRITE, null); 1476 localFolder.appendMessages(new Message[] { 1477 message 1478 }); 1479 Message localMessage = localFolder.getMessage(message.getUid()); 1480 localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); 1481 1482 PendingCommand command = new PendingCommand(); 1483 command.command = PENDING_COMMAND_APPEND; 1484 command.arguments = new String[] { 1485 localFolder.getName(), 1486 localMessage.getUid() }; 1487 queuePendingCommand(account, command); 1488 processPendingCommands(account); 1489 } 1490 catch (MessagingException e) { 1491 Log.e(Email.LOG_TAG, "Unable to save message as draft.", e); 1492 } 1493 } 1494 1495 class Command { 1496 public Runnable runnable; 1497 1498 public MessagingListener listener; 1499 1500 public String description; 1501 } 1502} 1503