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