MessagingController.java revision c84467afe1b5e0a657ed7d6a9fa1e3fe1ff259a0
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.TrafficStats; 25import android.net.Uri; 26import android.os.Process; 27import android.text.TextUtils; 28import android.util.Log; 29 30import com.android.email.mail.Sender; 31import com.android.email.mail.Store; 32import com.android.emailcommon.Logging; 33import com.android.emailcommon.TrafficFlags; 34import com.android.emailcommon.internet.MimeBodyPart; 35import com.android.emailcommon.internet.MimeHeader; 36import com.android.emailcommon.internet.MimeMultipart; 37import com.android.emailcommon.internet.MimeUtility; 38import com.android.emailcommon.mail.AuthenticationFailedException; 39import com.android.emailcommon.mail.FetchProfile; 40import com.android.emailcommon.mail.Flag; 41import com.android.emailcommon.mail.Folder; 42import com.android.emailcommon.mail.Folder.FolderType; 43import com.android.emailcommon.mail.Folder.MessageRetrievalListener; 44import com.android.emailcommon.mail.Folder.OpenMode; 45import com.android.emailcommon.mail.Message; 46import com.android.emailcommon.mail.MessagingException; 47import com.android.emailcommon.mail.Part; 48import com.android.emailcommon.provider.Account; 49import com.android.emailcommon.provider.EmailContent; 50import com.android.emailcommon.provider.EmailContent.Attachment; 51import com.android.emailcommon.provider.EmailContent.AttachmentColumns; 52import com.android.emailcommon.provider.EmailContent.MailboxColumns; 53import com.android.emailcommon.provider.EmailContent.MessageColumns; 54import com.android.emailcommon.provider.EmailContent.SyncColumns; 55import com.android.emailcommon.provider.Mailbox; 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.Date; 63import java.util.HashMap; 64import java.util.HashSet; 65import java.util.concurrent.BlockingQueue; 66import java.util.concurrent.LinkedBlockingQueue; 67 68/** 69 * Starts a long running (application) Thread that will run through commands 70 * that require remote mailbox access. This class is used to serialize and 71 * prioritize these commands. Each method that will submit a command requires a 72 * MessagingListener instance to be provided. It is expected that that listener 73 * has also been added as a registered listener using addListener(). When a 74 * command is to be executed, if the listener that was provided with the command 75 * is no longer registered the command is skipped. The design idea for the above 76 * is that when an Activity starts it registers as a listener. When it is paused 77 * it removes itself. Thus, any commands that that activity submitted are 78 * removed from the queue once the activity is no longer active. 79 */ 80public class MessagingController implements Runnable { 81 82 /** 83 * The maximum message size that we'll consider to be "small". A small message is downloaded 84 * in full immediately instead of in pieces. Anything over this size will be downloaded in 85 * pieces with attachments being left off completely and downloaded on demand. 86 * 87 * 88 * 25k for a "small" message was picked by educated trial and error. 89 * http://answers.google.com/answers/threadview?id=312463 claims that the 90 * average size of an email is 59k, which I feel is too large for our 91 * blind download. The following tests were performed on a download of 92 * 25 random messages. 93 * <pre> 94 * 5k - 61 seconds, 95 * 25k - 51 seconds, 96 * 55k - 53 seconds, 97 * </pre> 98 * So 25k gives good performance and a reasonable data footprint. Sounds good to me. 99 */ 100 private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024); 101 102 /** 103 * We write this into the serverId field of messages that will never be upsynced. 104 */ 105 private static final String LOCAL_SERVERID_PREFIX = "Local-"; 106 107 private static final ContentValues PRUNE_ATTACHMENT_CV = new ContentValues(); 108 static { 109 PRUNE_ATTACHMENT_CV.putNull(AttachmentColumns.CONTENT_URI); 110 } 111 112 private static MessagingController sInstance = null; 113 private final BlockingQueue<Command> mCommands = new LinkedBlockingQueue<Command>(); 114 private final Thread mThread; 115 116 /** 117 * All access to mListeners *must* be synchronized 118 */ 119 private final GroupMessagingListener mListeners = new GroupMessagingListener(); 120 private boolean mBusy; 121 private final Context mContext; 122 private final Controller mController; 123 124 /** 125 * Simple cache for last search result mailbox by account and serverId, since the most common 126 * case will be repeated use of the same mailbox 127 */ 128 private long mLastSearchAccountKey = Account.NO_ACCOUNT; 129 private String mLastSearchServerId = null; 130 private Mailbox mLastSearchRemoteMailbox = null; 131 132 protected MessagingController(Context _context, Controller _controller) { 133 mContext = _context.getApplicationContext(); 134 mController = _controller; 135 mThread = new Thread(this); 136 mThread.start(); 137 } 138 139 /** 140 * Gets or creates the singleton instance of MessagingController. Application is used to 141 * provide a Context to classes that need it. 142 */ 143 public synchronized static MessagingController getInstance(Context _context, 144 Controller _controller) { 145 if (sInstance == null) { 146 sInstance = new MessagingController(_context, _controller); 147 } 148 return sInstance; 149 } 150 151 /** 152 * Inject a mock controller. Used only for testing. Affects future calls to getInstance(). 153 */ 154 public static void injectMockController(MessagingController mockController) { 155 sInstance = mockController; 156 } 157 158 // TODO: seems that this reading of mBusy isn't thread-safe 159 public boolean isBusy() { 160 return mBusy; 161 } 162 163 @Override 164 public void run() { 165 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 166 // TODO: add an end test to this infinite loop 167 while (true) { 168 Command command; 169 try { 170 command = mCommands.take(); 171 } catch (InterruptedException e) { 172 continue; //re-test the condition on the eclosing while 173 } 174 if (command.listener == null || isActiveListener(command.listener)) { 175 mBusy = true; 176 command.runnable.run(); 177 mListeners.controllerCommandCompleted(mCommands.size() > 0); 178 } 179 mBusy = false; 180 } 181 } 182 183 private void put(String description, MessagingListener listener, Runnable runnable) { 184 try { 185 Command command = new Command(); 186 command.listener = listener; 187 command.runnable = runnable; 188 command.description = description; 189 mCommands.add(command); 190 } 191 catch (IllegalStateException ie) { 192 throw new Error(ie); 193 } 194 } 195 196 public void addListener(MessagingListener listener) { 197 mListeners.addListener(listener); 198 } 199 200 public void removeListener(MessagingListener listener) { 201 mListeners.removeListener(listener); 202 } 203 204 private boolean isActiveListener(MessagingListener listener) { 205 return mListeners.isActiveListener(listener); 206 } 207 208 private static final int MAILBOX_COLUMN_ID = 0; 209 private static final int MAILBOX_COLUMN_SERVER_ID = 1; 210 private static final int MAILBOX_COLUMN_TYPE = 2; 211 212 /** Small projection for just the columns required for a sync. */ 213 private static final String[] MAILBOX_PROJECTION = new String[] { 214 MailboxColumns.ID, 215 MailboxColumns.SERVER_ID, 216 MailboxColumns.TYPE, 217 }; 218 219 /** 220 * Synchronize the folder list with the remote server. Synchronization occurs in the 221 * background and results are passed through the {@link MessagingListener}. If the 222 * given listener is not {@code null}, it must have been previously added to the set 223 * of listeners using the {@link #addListener(MessagingListener)}. Otherwise, no 224 * actions will be performed. 225 * 226 * TODO this needs to cache the remote folder list 227 * TODO break out an inner listFoldersSynchronized which could simplify checkMail 228 * 229 * @param accountId ID of the account for which to list the folders 230 * @param listener A listener to notify 231 */ 232 void listFolders(final long accountId, MessagingListener listener) { 233 final Account account = Account.restoreAccountWithId(mContext, accountId); 234 if (account == null) { 235 Log.i(Logging.LOG_TAG, "Could not load account id " + accountId 236 + ". Has it been removed?"); 237 return; 238 } 239 mListeners.listFoldersStarted(accountId); 240 put("listFolders", listener, new Runnable() { 241 // TODO For now, mailbox addition occurs in the server-dependent store implementation, 242 // but, mailbox removal occurs here. Instead, each store should be responsible for 243 // content synchronization (addition AND removal) since each store will likely need 244 // to implement it's own, unique synchronization methodology. 245 @Override 246 public void run() { 247 TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account)); 248 Cursor localFolderCursor = null; 249 try { 250 // Step 1: Get remote mailboxes 251 Store store = Store.getInstance(account, mContext); 252 Folder[] remoteFolders = store.updateFolders(); 253 HashSet<String> remoteFolderNames = new HashSet<String>(); 254 for (int i = 0, count = remoteFolders.length; i < count; i++) { 255 remoteFolderNames.add(remoteFolders[i].getName()); 256 } 257 258 // Step 2: Get local mailboxes 259 localFolderCursor = mContext.getContentResolver().query( 260 Mailbox.CONTENT_URI, 261 MAILBOX_PROJECTION, 262 EmailContent.MailboxColumns.ACCOUNT_KEY + "=?", 263 new String[] { String.valueOf(account.mId) }, 264 null); 265 266 // Step 3: Remove any local mailbox not on the remote list 267 while (localFolderCursor.moveToNext()) { 268 String mailboxPath = localFolderCursor.getString(MAILBOX_COLUMN_SERVER_ID); 269 // Short circuit if we have a remote mailbox with the same name 270 if (remoteFolderNames.contains(mailboxPath)) { 271 continue; 272 } 273 274 int mailboxType = localFolderCursor.getInt(MAILBOX_COLUMN_TYPE); 275 long mailboxId = localFolderCursor.getLong(MAILBOX_COLUMN_ID); 276 switch (mailboxType) { 277 case Mailbox.TYPE_INBOX: 278 case Mailbox.TYPE_DRAFTS: 279 case Mailbox.TYPE_OUTBOX: 280 case Mailbox.TYPE_SENT: 281 case Mailbox.TYPE_TRASH: 282 case Mailbox.TYPE_SEARCH: 283 // Never, ever delete special mailboxes 284 break; 285 default: 286 // Drop all attachment files related to this mailbox 287 AttachmentUtilities.deleteAllMailboxAttachmentFiles( 288 mContext, accountId, mailboxId); 289 // Delete the mailbox; database triggers take care of related 290 // Message, Body and Attachment records 291 Uri uri = ContentUris.withAppendedId( 292 Mailbox.CONTENT_URI, mailboxId); 293 mContext.getContentResolver().delete(uri, null, null); 294 break; 295 } 296 } 297 mListeners.listFoldersFinished(accountId); 298 } catch (Exception e) { 299 mListeners.listFoldersFailed(accountId, e.toString()); 300 } finally { 301 if (localFolderCursor != null) { 302 localFolderCursor.close(); 303 } 304 } 305 } 306 }); 307 } 308 309 /** 310 * Start background synchronization of the specified folder. 311 * @param account 312 * @param folder 313 * @param listener 314 */ 315 public void synchronizeMailbox(final Account account, 316 final Mailbox folder, MessagingListener listener) { 317 /* 318 * We don't ever sync the Outbox. 319 */ 320 if (folder.mType == Mailbox.TYPE_OUTBOX) { 321 return; 322 } 323 mListeners.synchronizeMailboxStarted(account.mId, folder.mId); 324 put("synchronizeMailbox", listener, new Runnable() { 325 @Override 326 public void run() { 327 synchronizeMailboxSynchronous(account, folder); 328 } 329 }); 330 } 331 332 /** 333 * Start foreground synchronization of the specified folder. This is called by 334 * synchronizeMailbox or checkMail. 335 * TODO this should use ID's instead of fully-restored objects 336 * @param account 337 * @param folder 338 */ 339 private void synchronizeMailboxSynchronous(final Account account, 340 final Mailbox folder) { 341 TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account)); 342 mListeners.synchronizeMailboxStarted(account.mId, folder.mId); 343 if ((folder.mFlags & Mailbox.FLAG_HOLDS_MAIL) == 0) { 344 // We don't hold messages, so, nothing to synchronize 345 mListeners.synchronizeMailboxFinished(account.mId, folder.mId, 0, 0, null); 346 return; 347 } 348 NotificationController nc = NotificationController.getInstance(mContext); 349 try { 350 351 // Select generic sync or store-specific sync 352 SyncResults results = synchronizeMailboxGeneric(account, folder); 353 // The account might have been deleted 354 if (results == null) return; 355 mListeners.synchronizeMailboxFinished(account.mId, folder.mId, 356 results.mTotalMessages, 357 results.mAddedMessages.size(), 358 results.mAddedMessages); 359 // Clear authentication notification for this account 360 nc.cancelLoginFailedNotification(account.mId); 361 } catch (MessagingException e) { 362 if (Logging.LOGD) { 363 Log.v(Logging.LOG_TAG, "synchronizeMailbox", e); 364 } 365 if (e instanceof AuthenticationFailedException) { 366 // Generate authentication notification 367 nc.showLoginFailedNotification(account.mId); 368 } 369 mListeners.synchronizeMailboxFailed(account.mId, folder.mId, e); 370 } 371 } 372 373 /** 374 * Lightweight record for the first pass of message sync, where I'm just seeing if 375 * the local message requires sync. Later (for messages that need syncing) we'll do a full 376 * readout from the DB. 377 */ 378 private static class LocalMessageInfo { 379 private static final int COLUMN_ID = 0; 380 private static final int COLUMN_FLAG_READ = 1; 381 private static final int COLUMN_FLAG_FAVORITE = 2; 382 private static final int COLUMN_FLAG_LOADED = 3; 383 private static final int COLUMN_SERVER_ID = 4; 384 private static final int COLUMN_FLAGS = 7; 385 private static final String[] PROJECTION = new String[] { 386 EmailContent.RECORD_ID, 387 MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_LOADED, 388 SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY, 389 MessageColumns.FLAGS 390 }; 391 392 final long mId; 393 final boolean mFlagRead; 394 final boolean mFlagFavorite; 395 final int mFlagLoaded; 396 final String mServerId; 397 final int mFlags; 398 399 public LocalMessageInfo(Cursor c) { 400 mId = c.getLong(COLUMN_ID); 401 mFlagRead = c.getInt(COLUMN_FLAG_READ) != 0; 402 mFlagFavorite = c.getInt(COLUMN_FLAG_FAVORITE) != 0; 403 mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED); 404 mServerId = c.getString(COLUMN_SERVER_ID); 405 mFlags = c.getInt(COLUMN_FLAGS); 406 // Note: mailbox key and account key not needed - they are projected for the SELECT 407 } 408 } 409 410 private void saveOrUpdate(EmailContent content, Context context) { 411 if (content.isSaved()) { 412 content.update(context, content.toContentValues()); 413 } else { 414 content.save(context); 415 } 416 } 417 418 /** 419 * Load the structure and body of messages not yet synced 420 * @param account the account we're syncing 421 * @param remoteFolder the (open) Folder we're working on 422 * @param unsyncedMessages an array of Message's we've got headers for 423 * @param toMailbox the destination mailbox we're syncing 424 * @throws MessagingException 425 */ 426 void loadUnsyncedMessages(final Account account, Folder remoteFolder, 427 ArrayList<Message> unsyncedMessages, final Mailbox toMailbox) 428 throws MessagingException { 429 430 // 1. Divide the unsynced messages into small & large (by size) 431 432 // TODO doing this work here (synchronously) is problematic because it prevents the UI 433 // from affecting the order (e.g. download a message because the user requested it.) Much 434 // of this logic should move out to a different sync loop that attempts to update small 435 // groups of messages at a time, as a background task. However, we can't just return 436 // (yet) because POP messages don't have an envelope yet.... 437 438 ArrayList<Message> largeMessages = new ArrayList<Message>(); 439 ArrayList<Message> smallMessages = new ArrayList<Message>(); 440 for (Message message : unsyncedMessages) { 441 if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) { 442 largeMessages.add(message); 443 } else { 444 smallMessages.add(message); 445 } 446 } 447 448 // 2. Download small messages 449 450 // TODO Problems with this implementation. 1. For IMAP, where we get a real envelope, 451 // this is going to be inefficient and duplicate work we've already done. 2. It's going 452 // back to the DB for a local message that we already had (and discarded). 453 454 // For small messages, we specify "body", which returns everything (incl. attachments) 455 FetchProfile fp = new FetchProfile(); 456 fp.add(FetchProfile.Item.BODY); 457 remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), fp, 458 new MessageRetrievalListener() { 459 @Override 460 public void messageRetrieved(Message message) { 461 // Store the updated message locally and mark it fully loaded 462 copyOneMessageToProvider(message, account, toMailbox, 463 EmailContent.Message.FLAG_LOADED_COMPLETE); 464 } 465 466 @Override 467 public void loadAttachmentProgress(int progress) { 468 } 469 }); 470 471 // 3. Download large messages. We ask the server to give us the message structure, 472 // but not all of the attachments. 473 fp.clear(); 474 fp.add(FetchProfile.Item.STRUCTURE); 475 remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), fp, null); 476 for (Message message : largeMessages) { 477 if (message.getBody() == null) { 478 // POP doesn't support STRUCTURE mode, so we'll just do a partial download 479 // (hopefully enough to see some/all of the body) and mark the message for 480 // further download. 481 fp.clear(); 482 fp.add(FetchProfile.Item.BODY_SANE); 483 // TODO a good optimization here would be to make sure that all Stores set 484 // the proper size after this fetch and compare the before and after size. If 485 // they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED 486 remoteFolder.fetch(new Message[] { message }, fp, null); 487 488 // Store the partially-loaded message and mark it partially loaded 489 copyOneMessageToProvider(message, account, toMailbox, 490 EmailContent.Message.FLAG_LOADED_PARTIAL); 491 } else { 492 // We have a structure to deal with, from which 493 // we can pull down the parts we want to actually store. 494 // Build a list of parts we are interested in. Text parts will be downloaded 495 // right now, attachments will be left for later. 496 ArrayList<Part> viewables = new ArrayList<Part>(); 497 ArrayList<Part> attachments = new ArrayList<Part>(); 498 MimeUtility.collectParts(message, viewables, attachments); 499 // Download the viewables immediately 500 for (Part part : viewables) { 501 fp.clear(); 502 fp.add(part); 503 // TODO what happens if the network connection dies? We've got partial 504 // messages with incorrect status stored. 505 remoteFolder.fetch(new Message[] { message }, fp, null); 506 } 507 // Store the updated message locally and mark it fully loaded 508 copyOneMessageToProvider(message, account, toMailbox, 509 EmailContent.Message.FLAG_LOADED_COMPLETE); 510 } 511 } 512 513 } 514 515 public void downloadFlagAndEnvelope(final Account account, final Mailbox mailbox, 516 Folder remoteFolder, ArrayList<Message> unsyncedMessages, 517 HashMap<String, LocalMessageInfo> localMessageMap, final ArrayList<Long> unseenMessages) 518 throws MessagingException { 519 FetchProfile fp = new FetchProfile(); 520 fp.add(FetchProfile.Item.FLAGS); 521 fp.add(FetchProfile.Item.ENVELOPE); 522 523 final HashMap<String, LocalMessageInfo> localMapCopy; 524 if (localMessageMap != null) 525 localMapCopy = new HashMap<String, LocalMessageInfo>(localMessageMap); 526 else { 527 localMapCopy = new HashMap<String, LocalMessageInfo>(); 528 } 529 530 remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp, 531 new MessageRetrievalListener() { 532 @Override 533 public void messageRetrieved(Message message) { 534 try { 535 // Determine if the new message was already known (e.g. partial) 536 // And create or reload the full message info 537 LocalMessageInfo localMessageInfo = 538 localMapCopy.get(message.getUid()); 539 EmailContent.Message localMessage = null; 540 if (localMessageInfo == null) { 541 localMessage = new EmailContent.Message(); 542 } else { 543 localMessage = EmailContent.Message.restoreMessageWithId( 544 mContext, localMessageInfo.mId); 545 } 546 547 if (localMessage != null) { 548 try { 549 // Copy the fields that are available into the message 550 LegacyConversions.updateMessageFields(localMessage, 551 message, account.mId, mailbox.mId); 552 // Commit the message to the local store 553 saveOrUpdate(localMessage, mContext); 554 // Track the "new" ness of the downloaded message 555 if (!message.isSet(Flag.SEEN) && unseenMessages != null) { 556 unseenMessages.add(localMessage.mId); 557 } 558 } catch (MessagingException me) { 559 Log.e(Logging.LOG_TAG, 560 "Error while copying downloaded message." + me); 561 } 562 563 } 564 } 565 catch (Exception e) { 566 Log.e(Logging.LOG_TAG, 567 "Error while storing downloaded message." + e.toString()); 568 } 569 } 570 571 @Override 572 public void loadAttachmentProgress(int progress) { 573 } 574 }); 575 576 } 577 578 /** 579 * Generic synchronizer - used for POP3 and IMAP. 580 * 581 * TODO Break this method up into smaller chunks. 582 * 583 * @param account the account to sync 584 * @param mailbox the mailbox to sync 585 * @return results of the sync pass 586 * @throws MessagingException 587 */ 588 private SyncResults synchronizeMailboxGeneric(final Account account, final Mailbox mailbox) 589 throws MessagingException { 590 591 /* 592 * A list of IDs for messages that were downloaded and did not have the seen flag set. 593 * This serves as the "true" new message count reported to the user via notification. 594 */ 595 final ArrayList<Long> unseenMessages = new ArrayList<Long>(); 596 597 if (Email.DEBUG) { 598 Log.d(Logging.LOG_TAG, "*** synchronizeMailboxGeneric ***"); 599 } 600 ContentResolver resolver = mContext.getContentResolver(); 601 602 // 0. We do not ever sync DRAFTS or OUTBOX (down or up) 603 if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) { 604 int totalMessages = EmailContent.count(mContext, mailbox.getUri(), null, null); 605 return new SyncResults(totalMessages, unseenMessages); 606 } 607 608 // 1. Get the message list from the local store and create an index of the uids 609 610 Cursor localUidCursor = null; 611 HashMap<String, LocalMessageInfo> localMessageMap = new HashMap<String, LocalMessageInfo>(); 612 613 try { 614 localUidCursor = resolver.query( 615 EmailContent.Message.CONTENT_URI, 616 LocalMessageInfo.PROJECTION, 617 EmailContent.MessageColumns.ACCOUNT_KEY + "=?" + 618 " AND " + MessageColumns.MAILBOX_KEY + "=?", 619 new String[] { 620 String.valueOf(account.mId), 621 String.valueOf(mailbox.mId) 622 }, 623 null); 624 while (localUidCursor.moveToNext()) { 625 LocalMessageInfo info = new LocalMessageInfo(localUidCursor); 626 localMessageMap.put(info.mServerId, info); 627 } 628 } finally { 629 if (localUidCursor != null) { 630 localUidCursor.close(); 631 } 632 } 633 634 // 2. Open the remote folder and create the remote folder if necessary 635 636 Store remoteStore = Store.getInstance(account, mContext); 637 // The account might have been deleted 638 if (remoteStore == null) return null; 639 Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); 640 641 /* 642 * If the folder is a "special" folder we need to see if it exists 643 * on the remote server. It if does not exist we'll try to create it. If we 644 * can't create we'll abort. This will happen on every single Pop3 folder as 645 * designed and on Imap folders during error conditions. This allows us 646 * to treat Pop3 and Imap the same in this code. 647 */ 648 if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_SENT 649 || mailbox.mType == Mailbox.TYPE_DRAFTS) { 650 if (!remoteFolder.exists()) { 651 if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { 652 return new SyncResults(0, unseenMessages); 653 } 654 } 655 } 656 657 // 3, Open the remote folder. This pre-loads certain metadata like message count. 658 remoteFolder.open(OpenMode.READ_WRITE); 659 660 // 4. Trash any remote messages that are marked as trashed locally. 661 // TODO - this comment was here, but no code was here. 662 663 // 5. Get the remote message count. 664 int remoteMessageCount = remoteFolder.getMessageCount(); 665 666 // 6. Determine the limit # of messages to download 667 int visibleLimit = mailbox.mVisibleLimit; 668 if (visibleLimit <= 0) { 669 visibleLimit = Email.VISIBLE_LIMIT_DEFAULT; 670 } 671 672 // 7. Create a list of messages to download 673 Message[] remoteMessages = new Message[0]; 674 final ArrayList<Message> unsyncedMessages = new ArrayList<Message>(); 675 HashMap<String, Message> remoteUidMap = new HashMap<String, Message>(); 676 677 int newMessageCount = 0; 678 if (remoteMessageCount > 0) { 679 /* 680 * Message numbers start at 1. 681 */ 682 int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1; 683 int remoteEnd = remoteMessageCount; 684 remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null); 685 // TODO Why are we running through the list twice? Combine w/ for loop below 686 for (Message message : remoteMessages) { 687 remoteUidMap.put(message.getUid(), message); 688 } 689 690 /* 691 * Get a list of the messages that are in the remote list but not on the 692 * local store, or messages that are in the local store but failed to download 693 * on the last sync. These are the new messages that we will download. 694 * Note, we also skip syncing messages which are flagged as "deleted message" sentinels, 695 * because they are locally deleted and we don't need or want the old message from 696 * the server. 697 */ 698 for (Message message : remoteMessages) { 699 LocalMessageInfo localMessage = localMessageMap.get(message.getUid()); 700 if (localMessage == null) { 701 newMessageCount++; 702 } 703 // localMessage == null -> message has never been created (not even headers) 704 // mFlagLoaded = UNLOADED -> message created, but none of body loaded 705 // mFlagLoaded = PARTIAL -> message created, a "sane" amt of body has been loaded 706 // mFlagLoaded = COMPLETE -> message body has been completely loaded 707 // mFlagLoaded = DELETED -> message has been deleted 708 // Only the first two of these are "unsynced", so let's retrieve them 709 if (localMessage == null || 710 (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_UNLOADED)) { 711 unsyncedMessages.add(message); 712 } 713 } 714 } 715 716 // 8. Download basic info about the new/unloaded messages (if any) 717 /* 718 * Fetch the flags and envelope only of the new messages. This is intended to get us 719 * critical data as fast as possible, and then we'll fill in the details. 720 */ 721 if (unsyncedMessages.size() > 0) { 722 downloadFlagAndEnvelope(account, mailbox, remoteFolder, unsyncedMessages, 723 localMessageMap, unseenMessages); 724 } 725 726 // 9. Refresh the flags for any messages in the local store that we didn't just download. 727 FetchProfile fp = new FetchProfile(); 728 fp.add(FetchProfile.Item.FLAGS); 729 remoteFolder.fetch(remoteMessages, fp, null); 730 boolean remoteSupportsSeen = false; 731 boolean remoteSupportsFlagged = false; 732 boolean remoteSupportsAnswered = false; 733 for (Flag flag : remoteFolder.getPermanentFlags()) { 734 if (flag == Flag.SEEN) { 735 remoteSupportsSeen = true; 736 } 737 if (flag == Flag.FLAGGED) { 738 remoteSupportsFlagged = true; 739 } 740 if (flag == Flag.ANSWERED) { 741 remoteSupportsAnswered = true; 742 } 743 } 744 // Update SEEN/FLAGGED/ANSWERED (star) flags (if supported remotely - e.g. not for POP3) 745 if (remoteSupportsSeen || remoteSupportsFlagged || remoteSupportsAnswered) { 746 for (Message remoteMessage : remoteMessages) { 747 LocalMessageInfo localMessageInfo = localMessageMap.get(remoteMessage.getUid()); 748 if (localMessageInfo == null) { 749 continue; 750 } 751 boolean localSeen = localMessageInfo.mFlagRead; 752 boolean remoteSeen = remoteMessage.isSet(Flag.SEEN); 753 boolean newSeen = (remoteSupportsSeen && (remoteSeen != localSeen)); 754 boolean localFlagged = localMessageInfo.mFlagFavorite; 755 boolean remoteFlagged = remoteMessage.isSet(Flag.FLAGGED); 756 boolean newFlagged = (remoteSupportsFlagged && (localFlagged != remoteFlagged)); 757 int localFlags = localMessageInfo.mFlags; 758 boolean localAnswered = (localFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0; 759 boolean remoteAnswered = remoteMessage.isSet(Flag.ANSWERED); 760 boolean newAnswered = (remoteSupportsAnswered && (localAnswered != remoteAnswered)); 761 if (newSeen || newFlagged || newAnswered) { 762 Uri uri = ContentUris.withAppendedId( 763 EmailContent.Message.CONTENT_URI, localMessageInfo.mId); 764 ContentValues updateValues = new ContentValues(); 765 updateValues.put(MessageColumns.FLAG_READ, remoteSeen); 766 updateValues.put(MessageColumns.FLAG_FAVORITE, remoteFlagged); 767 if (remoteAnswered) { 768 localFlags |= EmailContent.Message.FLAG_REPLIED_TO; 769 } else { 770 localFlags &= ~EmailContent.Message.FLAG_REPLIED_TO; 771 } 772 updateValues.put(MessageColumns.FLAGS, localFlags); 773 resolver.update(uri, updateValues, null, null); 774 } 775 } 776 } 777 778 // 10. Remove any messages that are in the local store but no longer on the remote store. 779 HashSet<String> localUidsToDelete = new HashSet<String>(localMessageMap.keySet()); 780 localUidsToDelete.removeAll(remoteUidMap.keySet()); 781 for (String uidToDelete : localUidsToDelete) { 782 LocalMessageInfo infoToDelete = localMessageMap.get(uidToDelete); 783 784 // Delete associated data (attachment files) 785 // Attachment & Body records are auto-deleted when we delete the Message record 786 AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId, 787 infoToDelete.mId); 788 789 // Delete the message itself 790 Uri uriToDelete = ContentUris.withAppendedId( 791 EmailContent.Message.CONTENT_URI, infoToDelete.mId); 792 resolver.delete(uriToDelete, null, null); 793 794 // Delete extra rows (e.g. synced or deleted) 795 Uri syncRowToDelete = ContentUris.withAppendedId( 796 EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId); 797 resolver.delete(syncRowToDelete, null, null); 798 Uri deletERowToDelete = ContentUris.withAppendedId( 799 EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId); 800 resolver.delete(deletERowToDelete, null, null); 801 } 802 803 loadUnsyncedMessages(account, remoteFolder, unsyncedMessages, mailbox); 804 805 // 14. Clean up and report results 806 remoteFolder.close(false); 807 808 return new SyncResults(remoteMessageCount, unseenMessages); 809 } 810 811 /** 812 * Copy one downloaded message (which may have partially-loaded sections) 813 * into a newly created EmailProvider Message, given the account and mailbox 814 * 815 * @param message the remote message we've just downloaded 816 * @param account the account it will be stored into 817 * @param folder the mailbox it will be stored into 818 * @param loadStatus when complete, the message will be marked with this status (e.g. 819 * EmailContent.Message.LOADED) 820 */ 821 public void copyOneMessageToProvider(Message message, Account account, 822 Mailbox folder, int loadStatus) { 823 EmailContent.Message localMessage = null; 824 Cursor c = null; 825 try { 826 c = mContext.getContentResolver().query( 827 EmailContent.Message.CONTENT_URI, 828 EmailContent.Message.CONTENT_PROJECTION, 829 EmailContent.MessageColumns.ACCOUNT_KEY + "=?" + 830 " AND " + MessageColumns.MAILBOX_KEY + "=?" + 831 " AND " + SyncColumns.SERVER_ID + "=?", 832 new String[] { 833 String.valueOf(account.mId), 834 String.valueOf(folder.mId), 835 String.valueOf(message.getUid()) 836 }, 837 null); 838 if (c.moveToNext()) { 839 localMessage = EmailContent.getContent(c, EmailContent.Message.class); 840 localMessage.mMailboxKey = folder.mId; 841 localMessage.mAccountKey = account.mId; 842 copyOneMessageToProvider(message, localMessage, loadStatus, mContext); 843 } 844 } finally { 845 if (c != null) { 846 c.close(); 847 } 848 } 849 } 850 851 /** 852 * Copy one downloaded message (which may have partially-loaded sections) 853 * into an already-created EmailProvider Message 854 * 855 * @param message the remote message we've just downloaded 856 * @param localMessage the EmailProvider Message, already created 857 * @param loadStatus when complete, the message will be marked with this status (e.g. 858 * EmailContent.Message.LOADED) 859 * @param context the context to be used for EmailProvider 860 */ 861 public void copyOneMessageToProvider(Message message, EmailContent.Message localMessage, 862 int loadStatus, Context context) { 863 try { 864 865 EmailContent.Body body = EmailContent.Body.restoreBodyWithMessageId(context, 866 localMessage.mId); 867 if (body == null) { 868 body = new EmailContent.Body(); 869 } 870 try { 871 // Copy the fields that are available into the message object 872 LegacyConversions.updateMessageFields(localMessage, message, 873 localMessage.mAccountKey, localMessage.mMailboxKey); 874 875 // Now process body parts & attachments 876 ArrayList<Part> viewables = new ArrayList<Part>(); 877 ArrayList<Part> attachments = new ArrayList<Part>(); 878 MimeUtility.collectParts(message, viewables, attachments); 879 880 ConversionUtilities.updateBodyFields(body, localMessage, viewables); 881 882 // Commit the message & body to the local store immediately 883 saveOrUpdate(localMessage, context); 884 saveOrUpdate(body, context); 885 886 // process (and save) attachments 887 LegacyConversions.updateAttachments(context, localMessage, attachments); 888 889 // One last update of message with two updated flags 890 localMessage.mFlagLoaded = loadStatus; 891 892 ContentValues cv = new ContentValues(); 893 cv.put(EmailContent.MessageColumns.FLAG_ATTACHMENT, localMessage.mFlagAttachment); 894 cv.put(EmailContent.MessageColumns.FLAG_LOADED, localMessage.mFlagLoaded); 895 Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, 896 localMessage.mId); 897 context.getContentResolver().update(uri, cv, null, null); 898 899 } catch (MessagingException me) { 900 Log.e(Logging.LOG_TAG, "Error while copying downloaded message." + me); 901 } 902 903 } catch (RuntimeException rte) { 904 Log.e(Logging.LOG_TAG, "Error while storing downloaded message." + rte.toString()); 905 } catch (IOException ioe) { 906 Log.e(Logging.LOG_TAG, "Error while storing attachment." + ioe.toString()); 907 } 908 } 909 910 /** 911 * Process a pending append message command. This command uploads a local message to the 912 * server, first checking to be sure that the server message is not newer than 913 * the local message. 914 * 915 * @param remoteStore the remote store we're working in 916 * @param account The account in which we are working 917 * @param newMailbox The mailbox we're appending to 918 * @param message The message we're appending 919 * @return true if successfully uploaded 920 */ 921 private boolean processPendingAppend(Store remoteStore, Account account, 922 Mailbox newMailbox, EmailContent.Message message) 923 throws MessagingException { 924 925 boolean updateInternalDate = false; 926 boolean updateMessage = false; 927 boolean deleteMessage = false; 928 929 // 1. Find the remote folder that we're appending to and create and/or open it 930 Folder remoteFolder = remoteStore.getFolder(newMailbox.mServerId); 931 if (!remoteFolder.exists()) { 932 if (!remoteFolder.canCreate(FolderType.HOLDS_MESSAGES)) { 933 // This is POP3, we cannot actually upload. Instead, we'll update the message 934 // locally with a fake serverId (so we don't keep trying here) and return. 935 if (message.mServerId == null || message.mServerId.length() == 0) { 936 message.mServerId = LOCAL_SERVERID_PREFIX + message.mId; 937 Uri uri = 938 ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, message.mId); 939 ContentValues cv = new ContentValues(); 940 cv.put(EmailContent.Message.SERVER_ID, message.mServerId); 941 mContext.getContentResolver().update(uri, cv, null, null); 942 } 943 return true; 944 } 945 if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { 946 // This is a (hopefully) transient error and we return false to try again later 947 return false; 948 } 949 } 950 remoteFolder.open(OpenMode.READ_WRITE); 951 if (remoteFolder.getMode() != OpenMode.READ_WRITE) { 952 return false; 953 } 954 955 // 2. If possible, load a remote message with the matching UID 956 Message remoteMessage = null; 957 if (message.mServerId != null && message.mServerId.length() > 0) { 958 remoteMessage = remoteFolder.getMessage(message.mServerId); 959 } 960 961 // 3. If a remote message could not be found, upload our local message 962 if (remoteMessage == null) { 963 // 3a. Create a legacy message to upload 964 Message localMessage = LegacyConversions.makeMessage(mContext, message); 965 966 // 3b. Upload it 967 FetchProfile fp = new FetchProfile(); 968 fp.add(FetchProfile.Item.BODY); 969 remoteFolder.appendMessages(new Message[] { localMessage }); 970 971 // 3b. And record the UID from the server 972 message.mServerId = localMessage.getUid(); 973 updateInternalDate = true; 974 updateMessage = true; 975 } else { 976 // 4. If the remote message exists we need to determine which copy to keep. 977 FetchProfile fp = new FetchProfile(); 978 fp.add(FetchProfile.Item.ENVELOPE); 979 remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); 980 Date localDate = new Date(message.mServerTimeStamp); 981 Date remoteDate = remoteMessage.getInternalDate(); 982 if (remoteDate != null && remoteDate.compareTo(localDate) > 0) { 983 // 4a. If the remote message is newer than ours we'll just 984 // delete ours and move on. A sync will get the server message 985 // if we need to be able to see it. 986 deleteMessage = true; 987 } else { 988 // 4b. Otherwise we'll upload our message and then delete the remote message. 989 990 // Create a legacy message to upload 991 Message localMessage = LegacyConversions.makeMessage(mContext, message); 992 993 // 4c. Upload it 994 fp.clear(); 995 fp = new FetchProfile(); 996 fp.add(FetchProfile.Item.BODY); 997 remoteFolder.appendMessages(new Message[] { localMessage }); 998 999 // 4d. Record the UID and new internalDate from the server 1000 message.mServerId = localMessage.getUid(); 1001 updateInternalDate = true; 1002 updateMessage = true; 1003 1004 // 4e. And delete the old copy of the message from the server 1005 remoteMessage.setFlag(Flag.DELETED, true); 1006 } 1007 } 1008 1009 // 5. If requested, Best-effort to capture new "internaldate" from the server 1010 if (updateInternalDate && message.mServerId != null) { 1011 try { 1012 Message remoteMessage2 = remoteFolder.getMessage(message.mServerId); 1013 if (remoteMessage2 != null) { 1014 FetchProfile fp2 = new FetchProfile(); 1015 fp2.add(FetchProfile.Item.ENVELOPE); 1016 remoteFolder.fetch(new Message[] { remoteMessage2 }, fp2, null); 1017 message.mServerTimeStamp = remoteMessage2.getInternalDate().getTime(); 1018 updateMessage = true; 1019 } 1020 } catch (MessagingException me) { 1021 // skip it - we can live without this 1022 } 1023 } 1024 1025 // 6. Perform required edits to local copy of message 1026 if (deleteMessage || updateMessage) { 1027 Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, message.mId); 1028 ContentResolver resolver = mContext.getContentResolver(); 1029 if (deleteMessage) { 1030 resolver.delete(uri, null, null); 1031 } else if (updateMessage) { 1032 ContentValues cv = new ContentValues(); 1033 cv.put(EmailContent.Message.SERVER_ID, message.mServerId); 1034 cv.put(EmailContent.Message.SERVER_TIMESTAMP, message.mServerTimeStamp); 1035 resolver.update(uri, cv, null, null); 1036 } 1037 } 1038 1039 return true; 1040 } 1041 1042 /** 1043 * Finish loading a message that have been partially downloaded. 1044 * 1045 * @param messageId the message to load 1046 * @param listener the callback by which results will be reported 1047 */ 1048 public void loadMessageForView(final long messageId, MessagingListener listener) { 1049 mListeners.loadMessageForViewStarted(messageId); 1050 put("loadMessageForViewRemote", listener, new Runnable() { 1051 @Override 1052 public void run() { 1053 try { 1054 // 1. Resample the message, in case it disappeared or synced while 1055 // this command was in queue 1056 EmailContent.Message message = 1057 EmailContent.Message.restoreMessageWithId(mContext, messageId); 1058 if (message == null) { 1059 mListeners.loadMessageForViewFailed(messageId, "Unknown message"); 1060 return; 1061 } 1062 if (message.mFlagLoaded == EmailContent.Message.FLAG_LOADED_COMPLETE) { 1063 mListeners.loadMessageForViewFinished(messageId); 1064 return; 1065 } 1066 1067 // 2. Open the remote folder. 1068 // TODO combine with common code in loadAttachment 1069 Account account = Account.restoreAccountWithId(mContext, message.mAccountKey); 1070 Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey); 1071 if (account == null || mailbox == null) { 1072 mListeners.loadMessageForViewFailed(messageId, "null account or mailbox"); 1073 return; 1074 } 1075 TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account)); 1076 1077 Store remoteStore = Store.getInstance(account, mContext); 1078 String remoteServerId = mailbox.mServerId; 1079 // If this is a search result, use the protocolSearchInfo field to get the 1080 // correct remote location 1081 if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) { 1082 remoteServerId = message.mProtocolSearchInfo; 1083 } 1084 Folder remoteFolder = remoteStore.getFolder(remoteServerId); 1085 remoteFolder.open(OpenMode.READ_WRITE); 1086 1087 // 3. Set up to download the entire message 1088 Message remoteMessage = remoteFolder.getMessage(message.mServerId); 1089 FetchProfile fp = new FetchProfile(); 1090 fp.add(FetchProfile.Item.BODY); 1091 remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); 1092 1093 // 4. Write to provider 1094 copyOneMessageToProvider(remoteMessage, account, mailbox, 1095 EmailContent.Message.FLAG_LOADED_COMPLETE); 1096 1097 // 5. Notify UI 1098 mListeners.loadMessageForViewFinished(messageId); 1099 1100 } catch (MessagingException me) { 1101 if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me); 1102 mListeners.loadMessageForViewFailed(messageId, me.getMessage()); 1103 } catch (RuntimeException rte) { 1104 mListeners.loadMessageForViewFailed(messageId, rte.getMessage()); 1105 } 1106 } 1107 }); 1108 } 1109 1110 /** 1111 * Attempts to load the attachment specified by id from the given account and message. 1112 */ 1113 public void loadAttachment(final long accountId, final long messageId, final long mailboxId, 1114 final long attachmentId, MessagingListener listener, final boolean background) { 1115 mListeners.loadAttachmentStarted(accountId, messageId, attachmentId, true); 1116 1117 put("loadAttachment", listener, new Runnable() { 1118 @Override 1119 public void run() { 1120 try { 1121 //1. Check if the attachment is already here and return early in that case 1122 Attachment attachment = 1123 Attachment.restoreAttachmentWithId(mContext, attachmentId); 1124 if (attachment == null) { 1125 mListeners.loadAttachmentFailed(accountId, messageId, attachmentId, 1126 new MessagingException("The attachment is null"), 1127 background); 1128 return; 1129 } 1130 if (Utility.attachmentExists(mContext, attachment)) { 1131 mListeners.loadAttachmentFinished(accountId, messageId, attachmentId); 1132 return; 1133 } 1134 1135 // 2. Open the remote folder. 1136 // TODO all of these could be narrower projections 1137 Account account = Account.restoreAccountWithId(mContext, accountId); 1138 Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); 1139 EmailContent.Message message = 1140 EmailContent.Message.restoreMessageWithId(mContext, messageId); 1141 1142 if (account == null || mailbox == null || message == null) { 1143 mListeners.loadAttachmentFailed(accountId, messageId, attachmentId, 1144 new MessagingException( 1145 "Account, mailbox, message or attachment are null"), 1146 background); 1147 return; 1148 } 1149 TrafficStats.setThreadStatsTag( 1150 TrafficFlags.getAttachmentFlags(mContext, account)); 1151 1152 Store remoteStore = Store.getInstance(account, mContext); 1153 Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); 1154 remoteFolder.open(OpenMode.READ_WRITE); 1155 1156 // 3. Generate a shell message in which to retrieve the attachment, 1157 // and a shell BodyPart for the attachment. Then glue them together. 1158 Message storeMessage = remoteFolder.createMessage(message.mServerId); 1159 MimeBodyPart storePart = new MimeBodyPart(); 1160 storePart.setSize((int)attachment.mSize); 1161 storePart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, 1162 attachment.mLocation); 1163 storePart.setHeader(MimeHeader.HEADER_CONTENT_TYPE, 1164 String.format("%s;\n name=\"%s\"", 1165 attachment.mMimeType, 1166 attachment.mFileName)); 1167 // TODO is this always true for attachments? I think we dropped the 1168 // true encoding along the way 1169 storePart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); 1170 1171 MimeMultipart multipart = new MimeMultipart(); 1172 multipart.setSubType("mixed"); 1173 multipart.addBodyPart(storePart); 1174 1175 storeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed"); 1176 storeMessage.setBody(multipart); 1177 1178 // 4. Now ask for the attachment to be fetched 1179 FetchProfile fp = new FetchProfile(); 1180 fp.add(storePart); 1181 remoteFolder.fetch(new Message[] { storeMessage }, fp, 1182 mController.new MessageRetrievalListenerBridge( 1183 messageId, attachmentId)); 1184 1185 // If we failed to load the attachment, throw an Exception here, so that 1186 // AttachmentDownloadService knows that we failed 1187 if (storePart.getBody() == null) { 1188 throw new MessagingException("Attachment not loaded."); 1189 } 1190 1191 // 5. Save the downloaded file and update the attachment as necessary 1192 LegacyConversions.saveAttachmentBody(mContext, storePart, attachment, 1193 accountId); 1194 1195 // 6. Report success 1196 mListeners.loadAttachmentFinished(accountId, messageId, attachmentId); 1197 } 1198 catch (MessagingException me) { 1199 if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me); 1200 mListeners.loadAttachmentFailed( 1201 accountId, messageId, attachmentId, me, background); 1202 } catch (IOException ioe) { 1203 Log.e(Logging.LOG_TAG, "Error while storing attachment." + ioe.toString()); 1204 } 1205 }}); 1206 } 1207 1208 /** 1209 * Attempt to send any messages that are sitting in the Outbox. 1210 * @param account 1211 * @param listener 1212 */ 1213 public void sendPendingMessages(final Account account, final long sentFolderId, 1214 MessagingListener listener) { 1215 put("sendPendingMessages", listener, new Runnable() { 1216 @Override 1217 public void run() { 1218 sendPendingMessagesSynchronous(account, sentFolderId); 1219 } 1220 }); 1221 } 1222 1223 /** 1224 * Attempt to send all messages sitting in the given account's outbox. Optionally, 1225 * if the server requires it, the message will be moved to the given sent folder. 1226 */ 1227 public void sendPendingMessagesSynchronous(final Account account, 1228 long sentFolderId) { 1229 TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(mContext, account)); 1230 NotificationController nc = NotificationController.getInstance(mContext); 1231 // 1. Loop through all messages in the account's outbox 1232 long outboxId = Mailbox.findMailboxOfType(mContext, account.mId, Mailbox.TYPE_OUTBOX); 1233 if (outboxId == Mailbox.NO_MAILBOX) { 1234 return; 1235 } 1236 ContentResolver resolver = mContext.getContentResolver(); 1237 Cursor c = resolver.query(EmailContent.Message.CONTENT_URI, 1238 EmailContent.Message.ID_COLUMN_PROJECTION, 1239 EmailContent.Message.MAILBOX_KEY + "=?", new String[] { Long.toString(outboxId) }, 1240 null); 1241 try { 1242 // 2. exit early 1243 if (c.getCount() <= 0) { 1244 return; 1245 } 1246 // 3. do one-time setup of the Sender & other stuff 1247 mListeners.sendPendingMessagesStarted(account.mId, -1); 1248 1249 Sender sender = Sender.getInstance(mContext, account); 1250 Store remoteStore = Store.getInstance(account, mContext); 1251 boolean requireMoveMessageToSentFolder = remoteStore.requireCopyMessageToSentFolder(); 1252 ContentValues moveToSentValues = null; 1253 if (requireMoveMessageToSentFolder) { 1254 moveToSentValues = new ContentValues(); 1255 moveToSentValues.put(MessageColumns.MAILBOX_KEY, sentFolderId); 1256 } 1257 1258 // 4. loop through the available messages and send them 1259 while (c.moveToNext()) { 1260 long messageId = -1; 1261 try { 1262 messageId = c.getLong(0); 1263 mListeners.sendPendingMessagesStarted(account.mId, messageId); 1264 // Don't send messages with unloaded attachments 1265 if (Utility.hasUnloadedAttachments(mContext, messageId)) { 1266 if (Email.DEBUG) { 1267 Log.d(Logging.LOG_TAG, "Can't send #" + messageId + 1268 "; unloaded attachments"); 1269 } 1270 continue; 1271 } 1272 sender.sendMessage(messageId); 1273 } catch (MessagingException me) { 1274 // report error for this message, but keep trying others 1275 if (me instanceof AuthenticationFailedException) { 1276 nc.showLoginFailedNotification(account.mId); 1277 } 1278 mListeners.sendPendingMessagesFailed(account.mId, messageId, me); 1279 continue; 1280 } 1281 // 5. move to sent, or delete 1282 Uri syncedUri = 1283 ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId); 1284 if (requireMoveMessageToSentFolder) { 1285 // If this is a forwarded message and it has attachments, delete them, as they 1286 // duplicate information found elsewhere (on the server). This saves storage. 1287 EmailContent.Message msg = 1288 EmailContent.Message.restoreMessageWithId(mContext, messageId); 1289 if (msg != null && 1290 ((msg.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0)) { 1291 AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId, 1292 messageId); 1293 } 1294 resolver.update(syncedUri, moveToSentValues, null, null); 1295 } else { 1296 AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId, 1297 messageId); 1298 Uri uri = 1299 ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId); 1300 resolver.delete(uri, null, null); 1301 resolver.delete(syncedUri, null, null); 1302 } 1303 } 1304 // 6. report completion/success 1305 mListeners.sendPendingMessagesCompleted(account.mId); 1306 nc.cancelLoginFailedNotification(account.mId); 1307 } catch (MessagingException me) { 1308 if (me instanceof AuthenticationFailedException) { 1309 nc.showLoginFailedNotification(account.mId); 1310 } 1311 mListeners.sendPendingMessagesFailed(account.mId, -1, me); 1312 } finally { 1313 c.close(); 1314 } 1315 } 1316 1317 /** 1318 * Checks mail for an account. 1319 * This entry point is for use by the mail checking service only, because it 1320 * gives slightly different callbacks (so the service doesn't get confused by callbacks 1321 * triggered by/for the foreground UI. 1322 * 1323 * TODO clean up the execution model which is unnecessarily threaded due to legacy code 1324 * 1325 * @param accountId the account to check 1326 * @param listener 1327 */ 1328 public void checkMail(final long accountId, final long tag, final MessagingListener listener) { 1329 mListeners.checkMailStarted(mContext, accountId, tag); 1330 1331 // This puts the command on the queue (not synchronous) 1332 listFolders(accountId, null); 1333 1334 // Put this on the queue as well so it follows listFolders 1335 put("checkMail", listener, new Runnable() { 1336 @Override 1337 public void run() { 1338 // send any pending outbound messages. note, there is a slight race condition 1339 // here if we somehow don't have a sent folder, but this should never happen 1340 // because the call to sendMessage() would have built one previously. 1341 long inboxId = -1; 1342 Account account = Account.restoreAccountWithId(mContext, accountId); 1343 if (account != null) { 1344 long sentboxId = Mailbox.findMailboxOfType(mContext, accountId, 1345 Mailbox.TYPE_SENT); 1346 if (sentboxId != Mailbox.NO_MAILBOX) { 1347 sendPendingMessagesSynchronous(account, sentboxId); 1348 } 1349 // find mailbox # for inbox and sync it. 1350 // TODO we already know this in Controller, can we pass it in? 1351 inboxId = Mailbox.findMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX); 1352 if (inboxId != Mailbox.NO_MAILBOX) { 1353 Mailbox mailbox = 1354 Mailbox.restoreMailboxWithId(mContext, inboxId); 1355 if (mailbox != null) { 1356 synchronizeMailboxSynchronous(account, mailbox); 1357 } 1358 } 1359 } 1360 mListeners.checkMailFinished(mContext, accountId, inboxId, tag); 1361 } 1362 }); 1363 } 1364 1365 private static class Command { 1366 public Runnable runnable; 1367 1368 public MessagingListener listener; 1369 1370 public String description; 1371 1372 @Override 1373 public String toString() { 1374 return description; 1375 } 1376 } 1377 1378 /** Results of the latest synchronization. */ 1379 private static class SyncResults { 1380 /** The total # of messages in the folder */ 1381 public final int mTotalMessages; 1382 /** A list of new message IDs; must not be {@code null} */ 1383 public final ArrayList<Long> mAddedMessages; 1384 1385 public SyncResults(int totalMessages, ArrayList<Long> addedMessages) { 1386 if (addedMessages == null) { 1387 throw new IllegalArgumentException("addedMessages must not be null"); 1388 } 1389 mTotalMessages = totalMessages; 1390 mAddedMessages = addedMessages; 1391 } 1392 } 1393} 1394