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