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