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