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