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