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