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