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