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