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