FolderSyncParser.java revision e6c2456aa6c00ef78c6d1d1621511d7ef8507f83
1/* 2 * Copyright (C) 2008-2009 Marc Blank 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.android.exchange.adapter; 19 20import android.content.ContentProviderOperation; 21import android.content.ContentUris; 22import android.content.ContentValues; 23import android.content.OperationApplicationException; 24import android.database.Cursor; 25import android.os.RemoteException; 26import android.text.TextUtils; 27import android.util.Log; 28 29import com.android.emailcommon.Logging; 30import com.android.emailcommon.provider.Account; 31import com.android.emailcommon.provider.EmailContent; 32import com.android.emailcommon.provider.EmailContent.AccountColumns; 33import com.android.emailcommon.provider.EmailContent.MailboxColumns; 34import com.android.emailcommon.provider.Mailbox; 35import com.android.emailcommon.service.SyncWindow; 36import com.android.emailcommon.utility.AttachmentUtilities; 37import com.android.emailcommon.utility.Utility; 38import com.android.exchange.CommandStatusException; 39import com.android.exchange.CommandStatusException.CommandStatus; 40import com.android.exchange.Eas; 41import com.android.exchange.ExchangeService; 42import com.android.exchange.provider.MailboxUtilities; 43import com.google.common.annotations.VisibleForTesting; 44 45import java.io.IOException; 46import java.io.InputStream; 47import java.util.ArrayList; 48import java.util.Arrays; 49import java.util.HashMap; 50import java.util.List; 51 52/** 53 * Parse the result of a FolderSync command 54 * 55 * Handles the addition, deletion, and changes to folders in the user's Exchange account. 56 **/ 57 58public class FolderSyncParser extends AbstractSyncParser { 59 60 public static final String TAG = "FolderSyncParser"; 61 62 // These are defined by the EAS protocol 63 public static final int USER_GENERIC_TYPE = 1; 64 public static final int INBOX_TYPE = 2; 65 public static final int DRAFTS_TYPE = 3; 66 public static final int DELETED_TYPE = 4; 67 public static final int SENT_TYPE = 5; 68 public static final int OUTBOX_TYPE = 6; 69 public static final int TASKS_TYPE = 7; 70 public static final int CALENDAR_TYPE = 8; 71 public static final int CONTACTS_TYPE = 9; 72 public static final int NOTES_TYPE = 10; 73 public static final int JOURNAL_TYPE = 11; 74 public static final int USER_MAILBOX_TYPE = 12; 75 76 // Chunk size for our mailbox commits 77 public final static int MAILBOX_COMMIT_SIZE = 20; 78 79 // EAS types that we are willing to consider valid folders for EAS sync 80 public static final List<Integer> VALID_EAS_FOLDER_TYPES = Arrays.asList(INBOX_TYPE, 81 DRAFTS_TYPE, DELETED_TYPE, SENT_TYPE, OUTBOX_TYPE, USER_MAILBOX_TYPE, CALENDAR_TYPE, 82 CONTACTS_TYPE, USER_GENERIC_TYPE); 83 84 public static final String ALL_BUT_ACCOUNT_MAILBOX = MailboxColumns.ACCOUNT_KEY + "=? and " + 85 MailboxColumns.TYPE + "!=" + Mailbox.TYPE_EAS_ACCOUNT_MAILBOX; 86 87 private static final String WHERE_SERVER_ID_AND_ACCOUNT = MailboxColumns.SERVER_ID + "=? and " + 88 MailboxColumns.ACCOUNT_KEY + "=?"; 89 90 private static final String WHERE_DISPLAY_NAME_AND_ACCOUNT = MailboxColumns.DISPLAY_NAME + 91 "=? and " + MailboxColumns.ACCOUNT_KEY + "=?"; 92 93 private static final String WHERE_PARENT_SERVER_ID_AND_ACCOUNT = 94 MailboxColumns.PARENT_SERVER_ID +"=? and " + MailboxColumns.ACCOUNT_KEY + "=?"; 95 96 private static final String[] MAILBOX_ID_COLUMNS_PROJECTION = 97 new String[] {MailboxColumns.ID, MailboxColumns.SERVER_ID, MailboxColumns.PARENT_SERVER_ID}; 98 private static final int MAILBOX_ID_COLUMNS_ID = 0; 99 private static final int MAILBOX_ID_COLUMNS_SERVER_ID = 1; 100 private static final int MAILBOX_ID_COLUMNS_PARENT_SERVER_ID = 2; 101 102 @VisibleForTesting 103 long mAccountId; 104 @VisibleForTesting 105 String mAccountIdAsString; 106 @VisibleForTesting 107 boolean mInUnitTest = false; 108 109 private String[] mBindArguments = new String[2]; 110 private ArrayList<ContentProviderOperation> mOperations = 111 new ArrayList<ContentProviderOperation>(); 112 private boolean mInitialSync; 113 private ArrayList<String> mParentFixupsNeeded = new ArrayList<String>(); 114 private boolean mFixupUninitializedNeeded = false; 115 // If true, we only care about status (this is true when validating an account) and ignore 116 // other data 117 private final boolean mStatusOnly; 118 119 private static final ContentValues UNINITIALIZED_PARENT_KEY = new ContentValues(); 120 121 { 122 UNINITIALIZED_PARENT_KEY.put(MailboxColumns.PARENT_KEY, Mailbox.PARENT_KEY_UNINITIALIZED); 123 } 124 125 public FolderSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException { 126 this(in, adapter, false); 127 } 128 129 public FolderSyncParser(InputStream in, AbstractSyncAdapter adapter, boolean statusOnly) 130 throws IOException { 131 super(in, adapter); 132 mAccountId = mAccount.mId; 133 mAccountIdAsString = Long.toString(mAccountId); 134 mStatusOnly = statusOnly; 135 } 136 137 @Override 138 public boolean parse() throws IOException, CommandStatusException { 139 int status; 140 boolean res = false; 141 boolean resetFolders = false; 142 // Since we're now (potentially) committing mailboxes in chunks, ensure that we start with 143 // only the account mailbox 144 String key = mAccount.mSyncKey; 145 mInitialSync = (key == null) || "0".equals(key); 146 if (mInitialSync) { 147 mContentResolver.delete(Mailbox.CONTENT_URI, ALL_BUT_ACCOUNT_MAILBOX, 148 new String[] {Long.toString(mAccountId)}); 149 } 150 if (nextTag(START_DOCUMENT) != Tags.FOLDER_FOLDER_SYNC) 151 throw new EasParserException(); 152 while (nextTag(START_DOCUMENT) != END_DOCUMENT) { 153 if (tag == Tags.FOLDER_STATUS) { 154 status = getValueInt(); 155 // Do a sanity check on the account here; if we have any duplicated folders, we'll 156 // act as though we have a bad folder sync key (wipe/reload mailboxes) 157 // Note: The ContentValues isn't used, but no point creating a new one 158 int dupes = 0; 159 if (mAccountId > 0) { 160 dupes = mContentResolver.update( 161 ContentUris.withAppendedId(EmailContent.ACCOUNT_CHECK_URI, mAccountId), 162 UNINITIALIZED_PARENT_KEY, null, null); 163 } 164 if (dupes > 0) { 165 String e = "Duplicate mailboxes found for account " + mAccountId + ": " + dupes; 166 // For verbose logging, make sure this is in emaillog.txt 167 userLog(e); 168 // Worthy of logging, regardless 169 Log.w(Logging.LOG_TAG, e); 170 status = Eas.FOLDER_STATUS_INVALID_KEY; 171 } 172 if (status != Eas.FOLDER_STATUS_OK) { 173 mService.errorLog("FolderSync failed: " + CommandStatus.toString(status)); 174 // If the account hasn't been saved, this is a validation attempt, so we don't 175 // try reloading the folder list... 176 if (CommandStatus.isDeniedAccess(status) || 177 CommandStatus.isNeedsProvisioning(status) || 178 (mAccount.mId == Account.NOT_SAVED)) { 179 throw new CommandStatusException(status); 180 // Note that we need to catch both old-style (Eas.FOLDER_STATUS_INVALID_KEY) 181 // and EAS 14 style command status 182 } else if (status == Eas.FOLDER_STATUS_INVALID_KEY || 183 CommandStatus.isBadSyncKey(status)) { 184 mService.errorLog("Bad sync key; RESET and delete all folders"); 185 // Delete PIM data 186 ExchangeService.deleteAccountPIMData(mAccountId); 187 // Save away any mailbox sync information that is NOT default 188 saveMailboxSyncOptions(); 189 // And only then, delete mailboxes 190 mContentResolver.delete(Mailbox.CONTENT_URI, 191 MailboxColumns.ACCOUNT_KEY + "=?", 192 new String[] {Long.toString(mAccountId)}); 193 // Stop existing syncs and reconstruct _main 194 ExchangeService.stopNonAccountMailboxSyncsForAccount(mAccountId); 195 res = true; 196 resetFolders = true; 197 // Reset the sync key and save (this should trigger the AccountObserver 198 // in ExchangeService, which will recreate the account mailbox, which 199 // will then start syncing folders, etc.) 200 mAccount.mSyncKey = "0"; 201 ContentValues cv = new ContentValues(); 202 cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey); 203 mContentResolver.update(ContentUris.withAppendedId(Account.CONTENT_URI, 204 mAccount.mId), cv, null, null); 205 } else { 206 // Other errors are at the server, so let's throw an error that will 207 // cause this sync to be retried at a later time 208 mService.errorLog("Throwing IOException; will retry later"); 209 throw new EasParserException("Folder status error"); 210 } 211 } 212 } else if (tag == Tags.FOLDER_SYNC_KEY) { 213 String newKey = getValue(); 214 if (!resetFolders) { 215 mAccount.mSyncKey = newKey; 216 userLog("New syncKey: ", newKey); 217 } else { 218 userLog("Ignoring new syncKey: ", newKey); 219 } 220 } else if (tag == Tags.FOLDER_CHANGES) { 221 if (mStatusOnly) return res; 222 changesParser(mOperations, mInitialSync); 223 } else 224 skipTag(); 225 } 226 if (mStatusOnly) return res; 227 synchronized (mService.getSynchronizer()) { 228 if (!mService.isStopped() || resetFolders) { 229 commit(); 230 userLog("Leaving FolderSyncParser with Account syncKey=", mAccount.mSyncKey); 231 } 232 } 233 return res; 234 } 235 236 private Cursor getServerIdCursor(String serverId) { 237 mBindArguments[0] = serverId; 238 mBindArguments[1] = mAccountIdAsString; 239 return mContentResolver.query(Mailbox.CONTENT_URI, MAILBOX_ID_COLUMNS_PROJECTION, 240 WHERE_SERVER_ID_AND_ACCOUNT, mBindArguments, null); 241 } 242 243 public void deleteParser(ArrayList<ContentProviderOperation> ops) throws IOException { 244 while (nextTag(Tags.FOLDER_DELETE) != END) { 245 switch (tag) { 246 case Tags.FOLDER_SERVER_ID: 247 String serverId = getValue(); 248 // Find the mailbox in this account with the given serverId 249 Cursor c = getServerIdCursor(serverId); 250 try { 251 if (c.moveToFirst()) { 252 userLog("Deleting ", serverId); 253 ops.add(ContentProviderOperation.newDelete( 254 ContentUris.withAppendedId(Mailbox.CONTENT_URI, 255 c.getLong(MAILBOX_ID_COLUMNS_ID))).build()); 256 AttachmentUtilities.deleteAllMailboxAttachmentFiles(mContext, 257 mAccountId, mMailbox.mId); 258 if (!mInitialSync) { 259 String parentId = c.getString(MAILBOX_ID_COLUMNS_PARENT_SERVER_ID); 260 if (!TextUtils.isEmpty(parentId)) { 261 mParentFixupsNeeded.add(parentId); 262 } 263 } 264 } 265 } finally { 266 c.close(); 267 } 268 break; 269 default: 270 skipTag(); 271 } 272 } 273 } 274 275 private static class SyncOptions { 276 private final int mInterval; 277 private final int mLookback; 278 279 private SyncOptions(int interval, int lookback) { 280 mInterval = interval; 281 mLookback = lookback; 282 } 283 } 284 285 private static final String MAILBOX_STATE_SELECTION = 286 MailboxColumns.ACCOUNT_KEY + "=? AND (" + MailboxColumns.SYNC_INTERVAL + "!=" + 287 Account.CHECK_INTERVAL_NEVER + " OR " + Mailbox.SYNC_LOOKBACK + "!=" + 288 SyncWindow.SYNC_WINDOW_UNKNOWN + ")"; 289 290 private static final String[] MAILBOX_STATE_PROJECTION = new String[] { 291 MailboxColumns.SERVER_ID, MailboxColumns.SYNC_INTERVAL, MailboxColumns.SYNC_LOOKBACK}; 292 private static final int MAILBOX_STATE_SERVER_ID = 0; 293 private static final int MAILBOX_STATE_INTERVAL = 1; 294 private static final int MAILBOX_STATE_LOOKBACK = 2; 295 @VisibleForTesting 296 final HashMap<String, SyncOptions> mSyncOptionsMap = new HashMap<String, SyncOptions>(); 297 298 /** 299 * For every mailbox in this account that has a non-default interval or lookback, save those 300 * values. 301 */ 302 @VisibleForTesting 303 void saveMailboxSyncOptions() { 304 // Shouldn't be necessary, but... 305 mSyncOptionsMap.clear(); 306 Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, MAILBOX_STATE_PROJECTION, 307 MAILBOX_STATE_SELECTION, new String[] {mAccountIdAsString}, null); 308 if (c != null) { 309 try { 310 while (c.moveToNext()) { 311 mSyncOptionsMap.put(c.getString(MAILBOX_STATE_SERVER_ID), 312 new SyncOptions(c.getInt(MAILBOX_STATE_INTERVAL), 313 c.getInt(MAILBOX_STATE_LOOKBACK))); 314 } 315 } finally { 316 c.close(); 317 } 318 } 319 } 320 321 /** 322 * For every set of saved mailbox sync options, try to find and restore those values 323 */ 324 @VisibleForTesting 325 void restoreMailboxSyncOptions() { 326 try { 327 ContentValues cv = new ContentValues(); 328 mBindArguments[1] = mAccountIdAsString; 329 for (String serverId: mSyncOptionsMap.keySet()) { 330 SyncOptions options = mSyncOptionsMap.get(serverId); 331 cv.put(MailboxColumns.SYNC_INTERVAL, options.mInterval); 332 cv.put(MailboxColumns.SYNC_LOOKBACK, options.mLookback); 333 mBindArguments[0] = serverId; 334 // If we match account and server id, set the sync options 335 mContentResolver.update(Mailbox.CONTENT_URI, cv, WHERE_SERVER_ID_AND_ACCOUNT, 336 mBindArguments); 337 } 338 } finally { 339 mSyncOptionsMap.clear(); 340 } 341 } 342 343 public Mailbox addParser() throws IOException { 344 String name = null; 345 String serverId = null; 346 String parentId = null; 347 int type = 0; 348 349 while (nextTag(Tags.FOLDER_ADD) != END) { 350 switch (tag) { 351 case Tags.FOLDER_DISPLAY_NAME: { 352 name = getValue(); 353 break; 354 } 355 case Tags.FOLDER_TYPE: { 356 type = getValueInt(); 357 break; 358 } 359 case Tags.FOLDER_PARENT_ID: { 360 parentId = getValue(); 361 break; 362 } 363 case Tags.FOLDER_SERVER_ID: { 364 serverId = getValue(); 365 break; 366 } 367 default: 368 skipTag(); 369 } 370 } 371 372 if (VALID_EAS_FOLDER_TYPES.contains(type)) { 373 Mailbox mailbox = new Mailbox(); 374 mailbox.mDisplayName = name; 375 mailbox.mServerId = serverId; 376 mailbox.mAccountKey = mAccountId; 377 mailbox.mType = Mailbox.TYPE_MAIL; 378 // Note that all mailboxes default to checking "never" (i.e. manual sync only) 379 // We set specific intervals for inbox, contacts, and (eventually) calendar 380 mailbox.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER; 381 switch (type) { 382 case INBOX_TYPE: 383 mailbox.mType = Mailbox.TYPE_INBOX; 384 mailbox.mSyncInterval = mAccount.mSyncInterval; 385 break; 386 case CONTACTS_TYPE: 387 mailbox.mType = Mailbox.TYPE_CONTACTS; 388 mailbox.mSyncInterval = mAccount.mSyncInterval; 389 break; 390 case OUTBOX_TYPE: 391 // TYPE_OUTBOX mailboxes are known by ExchangeService to sync whenever they 392 // aren't empty. The value of mSyncFrequency is ignored for this kind of 393 // mailbox. 394 mailbox.mType = Mailbox.TYPE_OUTBOX; 395 break; 396 case SENT_TYPE: 397 mailbox.mType = Mailbox.TYPE_SENT; 398 break; 399 case DRAFTS_TYPE: 400 mailbox.mType = Mailbox.TYPE_DRAFTS; 401 break; 402 case DELETED_TYPE: 403 mailbox.mType = Mailbox.TYPE_TRASH; 404 break; 405 case CALENDAR_TYPE: 406 mailbox.mType = Mailbox.TYPE_CALENDAR; 407 mailbox.mSyncInterval = mAccount.mSyncInterval; 408 break; 409 case USER_GENERIC_TYPE: 410 mailbox.mType = Mailbox.TYPE_UNKNOWN; 411 break; 412 } 413 414 // Make boxes like Contacts and Calendar invisible in the folder list 415 mailbox.mFlagVisible = (mailbox.mType < Mailbox.TYPE_NOT_EMAIL); 416 417 if (!parentId.equals("0")) { 418 mailbox.mParentServerId = parentId; 419 if (!mInitialSync) { 420 mParentFixupsNeeded.add(parentId); 421 } 422 } 423 // At the least, we'll need to set flags 424 mFixupUninitializedNeeded = true; 425 426 return mailbox; 427 } 428 return null; 429 } 430 431 /** 432 * Determine whether a given mailbox holds mail, rather than other data. We do this by first 433 * checking the type of the mailbox (if it's a known good type, great; if it's a known bad 434 * type, return false). If it's unknown, we check the parent, first by trying to find it in 435 * the current set of newly synced items, and then by looking it up in EmailProvider. If 436 * we can find the parent, we use the same rules to determine if it holds mail; if it does, 437 * then its children do as well, so that's a go. 438 * 439 * @param mailbox the mailbox we're checking 440 * @param mailboxMap a HashMap relating server id's of mailboxes in the current sync set to 441 * the corresponding mailbox structures 442 * @return whether or not the mailbox contains email (rather than PIM or unknown data) 443 */ 444 /*package*/ boolean isValidMailFolder(Mailbox mailbox, HashMap<String, Mailbox> mailboxMap) { 445 int folderType = mailbox.mType; 446 // Automatically accept our email types 447 if (folderType < Mailbox.TYPE_NOT_EMAIL) return true; 448 // Automatically reject everything else but "unknown" 449 if (folderType != Mailbox.TYPE_UNKNOWN) return false; 450 // If this is TYPE_UNKNOWN, check the parent 451 Mailbox parent = mailboxMap.get(mailbox.mParentServerId); 452 // If the parent is in the map, then check it out; if not, it could be an existing saved 453 // Mailbox, so we'll have to query the database 454 if (parent == null) { 455 mBindArguments[0] = Long.toString(mAccount.mId); 456 long parentId = -1; 457 if (mailbox.mParentServerId != null) { 458 mBindArguments[1] = mailbox.mParentServerId; 459 parentId = Utility.getFirstRowInt(mContext, Mailbox.CONTENT_URI, 460 EmailContent.ID_PROJECTION, 461 MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.SERVER_ID + "=?", 462 mBindArguments, null, EmailContent.ID_PROJECTION_COLUMN, -1); 463 } 464 if (parentId != -1) { 465 // Get the parent from the database 466 parent = Mailbox.restoreMailboxWithId(mContext, parentId); 467 if (parent == null) return false; 468 } else { 469 return false; 470 } 471 } 472 return isValidMailFolder(parent, mailboxMap); 473 } 474 475 public void updateParser(ArrayList<ContentProviderOperation> ops) throws IOException { 476 String serverId = null; 477 String displayName = null; 478 String parentId = null; 479 while (nextTag(Tags.FOLDER_UPDATE) != END) { 480 switch (tag) { 481 case Tags.FOLDER_SERVER_ID: 482 serverId = getValue(); 483 break; 484 case Tags.FOLDER_DISPLAY_NAME: 485 displayName = getValue(); 486 break; 487 case Tags.FOLDER_PARENT_ID: 488 parentId = getValue(); 489 break; 490 default: 491 skipTag(); 492 break; 493 } 494 } 495 // We'll make a change if one of parentId or displayName are specified 496 // serverId is required, but let's be careful just the same 497 if (serverId != null && (displayName != null || parentId != null)) { 498 Cursor c = getServerIdCursor(serverId); 499 try { 500 // If we find the mailbox (using serverId), make the change 501 if (c.moveToFirst()) { 502 userLog("Updating ", serverId); 503 // Fix up old and new parents, as needed 504 if (!TextUtils.isEmpty(parentId)) { 505 mParentFixupsNeeded.add(parentId); 506 } 507 String oldParentId = c.getString(MAILBOX_ID_COLUMNS_PARENT_SERVER_ID); 508 if (!TextUtils.isEmpty(oldParentId)) { 509 mParentFixupsNeeded.add(oldParentId); 510 } 511 // Set display name if we've got one 512 ContentValues cv = new ContentValues(); 513 if (displayName != null) { 514 cv.put(Mailbox.DISPLAY_NAME, displayName); 515 } 516 // Save away the server id and uninitialize the parent key 517 cv.put(Mailbox.PARENT_SERVER_ID, parentId); 518 // Clear the parent key; it will be fixed up after the commit 519 cv.put(Mailbox.PARENT_KEY, Mailbox.PARENT_KEY_UNINITIALIZED); 520 ops.add(ContentProviderOperation.newUpdate( 521 ContentUris.withAppendedId(Mailbox.CONTENT_URI, 522 c.getLong(MAILBOX_ID_COLUMNS_ID))).withValues(cv).build()); 523 // Say we need to fixup uninitialized mailboxes 524 mFixupUninitializedNeeded = true; 525 } 526 } finally { 527 c.close(); 528 } 529 } 530 } 531 532 private boolean commitMailboxes(ArrayList<Mailbox> validMailboxes, 533 ArrayList<Mailbox> userMailboxes, HashMap<String, Mailbox> mailboxMap, 534 ArrayList<ContentProviderOperation> ops) { 535 536 // Go through the generic user mailboxes; we'll call them valid if any parent is valid 537 for (Mailbox m: userMailboxes) { 538 if (isValidMailFolder(m, mailboxMap)) { 539 m.mType = Mailbox.TYPE_MAIL; 540 validMailboxes.add(m); 541 } else { 542 userLog("Rejecting unknown type mailbox: " + m.mDisplayName); 543 } 544 } 545 546 // Add operations for all valid mailboxes 547 for (Mailbox m: validMailboxes) { 548 userLog("Adding mailbox: ", m.mDisplayName); 549 ops.add(ContentProviderOperation 550 .newInsert(Mailbox.CONTENT_URI).withValues(m.toContentValues()).build()); 551 } 552 553 // Commit the mailboxes 554 userLog("Applying ", mOperations.size(), " mailbox operations."); 555 // Execute the batch; throw IOExceptions if this fails, hoping the issue isn't repeatable 556 // If it IS repeatable, there's no good result, since the folder list will be invalid 557 try { 558 mContentResolver.applyBatch(EmailContent.AUTHORITY, mOperations); 559 return true; 560 } catch (RemoteException e) { 561 userLog("RemoteException in commitMailboxes"); 562 return false; 563 } catch (OperationApplicationException e) { 564 userLog("OperationApplicationException in commitMailboxes"); 565 return false; 566 } 567 } 568 569 public void changesParser(final ArrayList<ContentProviderOperation> ops, 570 final boolean initialSync) throws IOException { 571 572 // Array of added mailboxes 573 final ArrayList<Mailbox> addMailboxes = new ArrayList<Mailbox>(); 574 575 // Indicate start of (potential) mailbox changes 576 MailboxUtilities.startMailboxChanges(mContext, mAccount.mId); 577 578 while (nextTag(Tags.FOLDER_CHANGES) != END) { 579 if (tag == Tags.FOLDER_ADD) { 580 Mailbox mailbox = addParser(); 581 if (mailbox != null) { 582 addMailboxes.add(mailbox); 583 } 584 } else if (tag == Tags.FOLDER_DELETE) { 585 deleteParser(ops); 586 } else if (tag == Tags.FOLDER_UPDATE) { 587 updateParser(ops); 588 } else if (tag == Tags.FOLDER_COUNT) { 589 getValueInt(); 590 } else 591 skipTag(); 592 } 593 594 // Synchronize on the parser to prevent this being run concurrently 595 // (an extremely unlikely event, but nonetheless possible) 596 synchronized (FolderSyncParser.this) { 597 // Mailboxes that we known contain email 598 ArrayList<Mailbox> validMailboxes = new ArrayList<Mailbox>(); 599 // Mailboxes that we're unsure about 600 ArrayList<Mailbox> userMailboxes = new ArrayList<Mailbox>(); 601 602 // Maps folder serverId to mailbox (used to validate user mailboxes) 603 HashMap<String, Mailbox> mailboxMap = new HashMap<String, Mailbox>(); 604 for (Mailbox mailbox : addMailboxes) { 605 mailboxMap.put(mailbox.mServerId, mailbox); 606 } 607 608 int mailboxCommitCount = 0; 609 for (Mailbox mailbox : addMailboxes) { 610 // And add the mailbox to the proper list 611 if (mailbox.mType == Mailbox.TYPE_UNKNOWN) { 612 userMailboxes.add(mailbox); 613 } else { 614 validMailboxes.add(mailbox); 615 } 616 // On initial sync, we commit what we have every 20 mailboxes 617 if (initialSync && (++mailboxCommitCount == MAILBOX_COMMIT_SIZE)) { 618 if (!commitMailboxes(validMailboxes, userMailboxes, mailboxMap, 619 ops)) { 620 mService.stop(); 621 return; 622 } 623 // Clear our arrays to prepare for more 624 userMailboxes.clear(); 625 validMailboxes.clear(); 626 ops.clear(); 627 mailboxCommitCount = 0; 628 } 629 } 630 // Commit the sync key and mailboxes 631 ContentValues cv = new ContentValues(); 632 cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey); 633 ops.add(ContentProviderOperation 634 .newUpdate( 635 ContentUris.withAppendedId(Account.CONTENT_URI, 636 mAccount.mId)) 637 .withValues(cv).build()); 638 if (!commitMailboxes(validMailboxes, userMailboxes, mailboxMap, ops)) { 639 mService.stop(); 640 return; 641 } 642 String accountSelector = Mailbox.ACCOUNT_KEY + "=" + mAccount.mId; 643 // For new boxes, setup the parent key and flags 644 if (mFixupUninitializedNeeded) { 645 MailboxUtilities.fixupUninitializedParentKeys(mContext, 646 accountSelector); 647 } 648 // For modified parents, reset the flags (and children's parent key) 649 for (String parentServerId: mParentFixupsNeeded) { 650 Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, 651 Mailbox.CONTENT_PROJECTION, Mailbox.PARENT_SERVER_ID + "=?", 652 new String[] {parentServerId}, null); 653 try { 654 if (c.moveToFirst()) { 655 MailboxUtilities.setFlagsAndChildrensParentKey(mContext, c, 656 accountSelector); 657 } 658 } finally { 659 c.close(); 660 } 661 } 662 663 MailboxUtilities.setupHierarchicalNames(mContext, mAccount.mId); 664 665 // Signal completion of mailbox changes 666 MailboxUtilities.endMailboxChanges(mContext, mAccount.mId); 667 } 668 } 669 670 /** 671 * Not needed for FolderSync parsing; everything is done within changesParser 672 */ 673 @Override 674 public void commandsParser() throws IOException { 675 } 676 677 /** 678 * Clean up after sync 679 */ 680 @Override 681 public void commit() throws IOException { 682 // Look for sync issues and its children and delete them 683 // I'm not aware of any other way to deal with this properly 684 mBindArguments[0] = "Sync Issues"; 685 mBindArguments[1] = mAccountIdAsString; 686 Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, 687 MAILBOX_ID_COLUMNS_PROJECTION, WHERE_DISPLAY_NAME_AND_ACCOUNT, 688 mBindArguments, null); 689 String parentServerId = null; 690 long id = 0; 691 try { 692 if (c.moveToFirst()) { 693 id = c.getLong(MAILBOX_ID_COLUMNS_ID); 694 parentServerId = c.getString(MAILBOX_ID_COLUMNS_SERVER_ID); 695 } 696 } finally { 697 c.close(); 698 } 699 if (parentServerId != null) { 700 mContentResolver.delete(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id), 701 null, null); 702 mBindArguments[0] = parentServerId; 703 mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_PARENT_SERVER_ID_AND_ACCOUNT, 704 mBindArguments); 705 } 706 707 // If we have saved options, restore them now 708 if (mInitialSync) { 709 restoreMailboxSyncOptions(); 710 } 711 } 712 713 @Override 714 public void responsesParser() throws IOException { 715 } 716 717} 718