Fix folder resync.

Bug: 9226893

Change-Id: I5dc96f13e0c8fc5d5cff9da40d09cd9eb8f50a26
diff --git a/src/com/android/exchange/adapter/FolderSyncParser.java b/src/com/android/exchange/adapter/FolderSyncParser.java
index 302e558..8bd2051 100644
--- a/src/com/android/exchange/adapter/FolderSyncParser.java
+++ b/src/com/android/exchange/adapter/FolderSyncParser.java
@@ -27,13 +27,11 @@
 import android.os.RemoteException;
 import android.text.TextUtils;
 
-import com.android.emailcommon.Logging;
 import com.android.emailcommon.provider.Account;
 import com.android.emailcommon.provider.EmailContent;
 import com.android.emailcommon.provider.EmailContent.AccountColumns;
 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
 import com.android.emailcommon.provider.Mailbox;
-import com.android.emailcommon.provider.MailboxUtilities;
 import com.android.emailcommon.service.SyncWindow;
 import com.android.emailcommon.utility.AttachmentUtilities;
 import com.android.exchange.CommandStatusException;
@@ -74,34 +72,63 @@
     public static final int JOURNAL_TYPE = 11;
     public static final int USER_MAILBOX_TYPE = 12;
 
-    // Chunk size for our mailbox commits
-    private final static int MAILBOX_COMMIT_SIZE = 20;
-    // Max mailboxes per account
-    private final static int MAX_MAILBOXES_PER_ACCOUNT = 1000000;
-
     // EAS types that we are willing to consider valid folders for EAS sync
     private static final List<Integer> VALID_EAS_FOLDER_TYPES = Arrays.asList(INBOX_TYPE,
             DRAFTS_TYPE, DELETED_TYPE, SENT_TYPE, OUTBOX_TYPE, USER_MAILBOX_TYPE, CALENDAR_TYPE,
             CONTACTS_TYPE, USER_GENERIC_TYPE);
 
-    public static final String ALL_BUT_ACCOUNT_MAILBOX = MailboxColumns.ACCOUNT_KEY + "=? and " +
-        MailboxColumns.TYPE + "!=" + Mailbox.TYPE_EAS_ACCOUNT_MAILBOX;
+    /** Content selection for all mailboxes belonging to an account. */
+    private static final String WHERE_ACCOUNT_KEY = MailboxColumns.ACCOUNT_KEY + "=?";
 
+    /**
+     * Content selection to find a specific mailbox by server id. Since server ids aren't unique
+     * across all accounts, this must also check account id.
+     */
     private static final String WHERE_SERVER_ID_AND_ACCOUNT = MailboxColumns.SERVER_ID + "=? and " +
         MailboxColumns.ACCOUNT_KEY + "=?";
 
+    /**
+     * Content selection to find a specific mailbox by display name and account.
+     */
     private static final String WHERE_DISPLAY_NAME_AND_ACCOUNT = MailboxColumns.DISPLAY_NAME +
         "=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
 
+    /**
+     * Content selection to find children by parent's server id. Since server ids aren't unique
+     * across accounts, this must also use account id.
+     */
     private static final String WHERE_PARENT_SERVER_ID_AND_ACCOUNT =
         MailboxColumns.PARENT_SERVER_ID +"=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
 
+    /** Projection used when fetching a Mailbox's ids. */
     private static final String[] MAILBOX_ID_COLUMNS_PROJECTION =
         new String[] {MailboxColumns.ID, MailboxColumns.SERVER_ID, MailboxColumns.PARENT_SERVER_ID};
     private static final int MAILBOX_ID_COLUMNS_ID = 0;
     private static final int MAILBOX_ID_COLUMNS_SERVER_ID = 1;
     private static final int MAILBOX_ID_COLUMNS_PARENT_SERVER_ID = 2;
 
+    /** Projection used for changed parents during parent/child fixup. */
+    private static final String[] FIXUP_PARENT_PROJECTION =
+            { MailboxColumns.ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.HIERARCHICAL_NAME,
+                    MailboxColumns.FLAGS };
+    private static final int FIXUP_PARENT_ID_COLUMN = 0;
+    private static final int FIXUP_PARENT_DISPLAY_NAME_COLUMN = 1;
+    private static final int FIXUP_PARENT_HIERARCHICAL_NAME_COLUMN = 2;
+    private static final int FIXUP_PARENT_FLAGS_COLUMN = 3;
+
+    /** Projection used for changed children during parent/child fixup. */
+    private static final String[] FIXUP_CHILD_PROJECTION =
+            { MailboxColumns.ID, MailboxColumns.DISPLAY_NAME };
+    private static final int FIXUP_CHILD_ID_COLUMN = 0;
+    private static final int FIXUP_CHILD_DISPLAY_NAME_COLUMN = 1;
+
+    /** Flags that are set or cleared when a mailbox's child status changes. */
+    private static final int HAS_CHILDREN_FLAGS =
+            Mailbox.FLAG_HAS_CHILDREN | Mailbox.FLAG_CHILDREN_VISIBLE;
+
+    /** Mailbox.NO_MAILBOX, as a string (convenience since this is used in several places). */
+    private static final String NO_MAILBOX_STRING = Long.toString(Mailbox.NO_MAILBOX);
+
     @VisibleForTesting
     long mAccountId;
     @VisibleForTesting
@@ -110,11 +137,17 @@
     boolean mInUnitTest = false;
 
     private String[] mBindArguments = new String[2];
+
+    /** List of pending operations to send as a batch to the content provider. */
     private ArrayList<ContentProviderOperation> mOperations =
-        new ArrayList<ContentProviderOperation>();
+            new ArrayList<ContentProviderOperation>();
+    /** Indicates whether this sync is an initial FolderSync. */
     private boolean mInitialSync;
+    /** List of folder server ids whose children changed with this sync. */
     private ArrayList<String> mParentFixupsNeeded = new ArrayList<String>();
-    private boolean mFixupUninitializedNeeded = false;
+    /** Indicates whether the sync response provided a different sync key than we had. */
+    private boolean mSyncKeyChanged = false;
+
     // If true, we only care about status (this is true when validating an account) and ignore
     // other data
     private final boolean mStatusOnly;
@@ -151,13 +184,11 @@
         int status;
         boolean res = false;
         boolean resetFolders = false;
-        // Since we're now (potentially) committing mailboxes in chunks, ensure that we start with
-        // only the account mailbox
-        String key = mAccount.mSyncKey;
-        mInitialSync = (key == null) || "0".equals(key);
+        mInitialSync = (mAccount.mSyncKey == null) || "0".equals(mAccount.mSyncKey);
         if (mInitialSync) {
-            mContentResolver.delete(Mailbox.CONTENT_URI, ALL_BUT_ACCOUNT_MAILBOX,
-                    new String[] {Long.toString(mAccountId)});
+            // We're resyncing all folders for this account, so nuke any existing ones.
+            mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_ACCOUNT_KEY,
+                    new String[] {mAccountIdAsString});
         }
         if (nextTag(START_DOCUMENT) != Tags.FOLDER_FOLDER_SYNC)
             throw new EasParserException();
@@ -174,11 +205,8 @@
                             UNINITIALIZED_PARENT_KEY, null, null);
                 }
                 if (dupes > 0) {
-                    String e = "Duplicate mailboxes found for account " + mAccountId + ": " + dupes;
-                    // For verbose logging, make sure this is in emaillog.txt
-                    userLog(e);
-                    // Worthy of logging, regardless
-                    LogUtils.w(Logging.LOG_TAG, e);
+                    LogUtils.w(TAG, "Duplicate mailboxes found for account %d: %d", mAccountId,
+                            dupes);
                     status = Eas.FOLDER_STATUS_INVALID_KEY;
                 }
                 if (status != Eas.FOLDER_STATUS_OK) {
@@ -197,9 +225,8 @@
                         // Save away any mailbox sync information that is NOT default
                         saveMailboxSyncOptions();
                         // And only then, delete mailboxes
-                        mContentResolver.delete(Mailbox.CONTENT_URI,
-                                MailboxColumns.ACCOUNT_KEY + "=?",
-                                new String[] {Long.toString(mAccountId)});
+                        mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_ACCOUNT_KEY,
+                                new String[] {mAccountIdAsString});
                         // Reconstruct _main
                         res = true;
                         resetFolders = true;
@@ -218,13 +245,14 @@
                     }
                 }
             } else if (tag == Tags.FOLDER_SYNC_KEY) {
-                String newKey = getValue();
-                if (!resetFolders) {
+                final String newKey = getValue();
+                if (newKey != null && !resetFolders) {
+                    mSyncKeyChanged = !newKey.equals(mAccount.mSyncKey);
                     mAccount.mSyncKey = newKey;
                 }
             } else if (tag == Tags.FOLDER_CHANGES) {
                 if (mStatusOnly) return res;
-                changesParser(mOperations, mInitialSync);
+                changesParser();
             } else
                 skipTag();
         }
@@ -234,14 +262,24 @@
         return res;
     }
 
-    private Cursor getServerIdCursor(String serverId) {
+    /**
+     * Get a cursor with folder ids for a specific folder.
+     * @param serverId The server id for the folder we are interested in.
+     * @return A cursor for the folder specified by serverId for this account.
+     */
+    private Cursor getServerIdCursor(final String serverId) {
         mBindArguments[0] = serverId;
         mBindArguments[1] = mAccountIdAsString;
         return mContentResolver.query(Mailbox.CONTENT_URI, MAILBOX_ID_COLUMNS_PROJECTION,
                 WHERE_SERVER_ID_AND_ACCOUNT, mBindArguments, null);
     }
 
-    private void deleteParser(ArrayList<ContentProviderOperation> ops) throws IOException {
+    /**
+     * Add the appropriate {@link ContentProviderOperation} to {@link #mOperations} for a Delete
+     * change in the FolderSync response.
+     * @throws IOException
+     */
+    private void deleteParser() throws IOException {
         while (nextTag(Tags.FOLDER_DELETE) != END) {
             switch (tag) {
                 case Tags.FOLDER_SERVER_ID:
@@ -250,18 +288,17 @@
                     final Cursor c = getServerIdCursor(serverId);
                     try {
                         if (c.moveToFirst()) {
-                            userLog("Deleting ", serverId);
+                            LogUtils.i(TAG, "Deleting %s", serverId);
                             final long mailboxId = c.getLong(MAILBOX_ID_COLUMNS_ID);
-                            ops.add(ContentProviderOperation.newDelete(
+                            mOperations.add(ContentProviderOperation.newDelete(
                                     ContentUris.withAppendedId(Mailbox.CONTENT_URI,
                                             mailboxId)).build());
                             AttachmentUtilities.deleteAllMailboxAttachmentFiles(mContext,
                                     mAccountId, mailboxId);
-                            if (!mInitialSync) {
-                                String parentId = c.getString(MAILBOX_ID_COLUMNS_PARENT_SERVER_ID);
-                                if (!TextUtils.isEmpty(parentId)) {
-                                    mParentFixupsNeeded.add(parentId);
-                                }
+                            final String parentId =
+                                    c.getString(MAILBOX_ID_COLUMNS_PARENT_SERVER_ID);
+                            if (!TextUtils.isEmpty(parentId)) {
+                                mParentFixupsNeeded.add(parentId);
                             }
                         }
                     } finally {
@@ -342,7 +379,96 @@
         }
     }
 
-    private Mailbox addParser() throws IOException {
+    /**
+     * Add a {@link ContentProviderOperation} to {@link #mOperations} to add a mailbox.
+     * @param name The new mailbox's name.
+     * @param serverId The new mailbox's server id.
+     * @param parentServerId The server id of the new mailbox's parent ("0" if none).
+     * @param easType The mailbox's type, in terms of the EAS values (NOT {@link Mailbox}'s type
+     *                values).
+     * @throws IOException
+     */
+    private void addMailboxOp(final String name, final String serverId,
+            final String parentServerId, final int easType) throws IOException {
+        final ContentValues cv = new ContentValues();
+        cv.put(MailboxColumns.DISPLAY_NAME, name);
+        cv.put(MailboxColumns.SERVER_ID, serverId);
+        final String parentId;
+        if (parentServerId.equals("0")) {
+            parentId = NO_MAILBOX_STRING;
+            cv.put(Mailbox.PARENT_KEY, Mailbox.NO_MAILBOX);
+        } else {
+            parentId = parentServerId;
+            mParentFixupsNeeded.add(parentId);
+        }
+        cv.put(MailboxColumns.PARENT_SERVER_ID, parentId);
+        cv.put(MailboxColumns.ACCOUNT_KEY, mAccountId);
+        final int mailboxType;
+        final long syncInterval;
+        switch (easType) {
+            case INBOX_TYPE:
+                mailboxType = Mailbox.TYPE_INBOX;
+                syncInterval = mAccount.mSyncInterval;
+                break;
+            case CONTACTS_TYPE:
+                mailboxType = Mailbox.TYPE_CONTACTS;
+                syncInterval = mAccount.mSyncInterval;
+                break;
+            case OUTBOX_TYPE:
+                // TYPE_OUTBOX mailboxes are known by ExchangeService to sync whenever they
+                // aren't empty.  The value of mSyncFrequency is ignored for this kind of
+                // mailbox.
+                mailboxType = Mailbox.TYPE_OUTBOX;
+                syncInterval = Mailbox.CHECK_INTERVAL_NEVER;
+                break;
+            case SENT_TYPE:
+                mailboxType = Mailbox.TYPE_SENT;
+                syncInterval = Mailbox.CHECK_INTERVAL_NEVER;
+                break;
+            case DRAFTS_TYPE:
+                mailboxType = Mailbox.TYPE_DRAFTS;
+                syncInterval = Mailbox.CHECK_INTERVAL_NEVER;
+                break;
+            case DELETED_TYPE:
+                mailboxType = Mailbox.TYPE_TRASH;
+                syncInterval = Mailbox.CHECK_INTERVAL_NEVER;
+                break;
+            case CALENDAR_TYPE:
+                mailboxType = Mailbox.TYPE_CALENDAR;
+                syncInterval = mAccount.mSyncInterval;
+                break;
+            default:
+                mailboxType = Mailbox.TYPE_MAIL;
+                syncInterval = Mailbox.CHECK_INTERVAL_NEVER;
+        }
+        cv.put(MailboxColumns.TYPE, mailboxType);
+        cv.put(MailboxColumns.SYNC_INTERVAL, syncInterval);
+
+        // Set basic flags
+        int flags = 0;
+        if (mailboxType <= Mailbox.TYPE_NOT_EMAIL) {
+            flags |= Mailbox.FLAG_HOLDS_MAIL + Mailbox.FLAG_SUPPORTS_SETTINGS;
+        }
+        // Outbox, Drafts, and Sent don't allow mail to be moved to them
+        if (mailboxType == Mailbox.TYPE_MAIL || mailboxType == Mailbox.TYPE_TRASH ||
+                mailboxType == Mailbox.TYPE_JUNK || mailboxType == Mailbox.TYPE_INBOX) {
+            flags |= Mailbox.FLAG_ACCEPTS_MOVED_MAIL;
+        }
+        cv.put(MailboxColumns.FLAGS, flags);
+
+        // Make boxes like Contacts and Calendar invisible in the folder list
+        cv.put(MailboxColumns.FLAG_VISIBLE, (mailboxType < Mailbox.TYPE_NOT_EMAIL));
+
+        mOperations.add(
+                ContentProviderOperation.newInsert(Mailbox.CONTENT_URI).withValues(cv).build());
+    }
+
+    /**
+     * Add the appropriate {@link ContentProviderOperation} to {@link #mOperations} for an Add
+     * change in the FolderSync response.
+     * @throws IOException
+     */
+    private void addParser() throws IOException {
         String name = null;
         String serverId = null;
         String parentId = null;
@@ -372,62 +498,16 @@
         }
 
         if (VALID_EAS_FOLDER_TYPES.contains(type)) {
-            Mailbox mailbox = new Mailbox();
-            mailbox.mDisplayName = name;
-            mailbox.mServerId = serverId;
-            mailbox.mAccountKey = mAccountId;
-            mailbox.mType = Mailbox.TYPE_MAIL;
-            // Note that all mailboxes default to checking "never" (i.e. manual sync only)
-            // We set specific intervals for inbox, contacts, and (eventually) calendar
-            mailbox.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER;
-            switch (type) {
-                case INBOX_TYPE:
-                    mailbox.mType = Mailbox.TYPE_INBOX;
-                    mailbox.mSyncInterval = mAccount.mSyncInterval;
-                    break;
-                case CONTACTS_TYPE:
-                    mailbox.mType = Mailbox.TYPE_CONTACTS;
-                    mailbox.mSyncInterval = mAccount.mSyncInterval;
-                    break;
-                case OUTBOX_TYPE:
-                    // TYPE_OUTBOX mailboxes are known by ExchangeService to sync whenever they
-                    // aren't empty.  The value of mSyncFrequency is ignored for this kind of
-                    // mailbox.
-                    mailbox.mType = Mailbox.TYPE_OUTBOX;
-                    break;
-                case SENT_TYPE:
-                    mailbox.mType = Mailbox.TYPE_SENT;
-                    break;
-                case DRAFTS_TYPE:
-                    mailbox.mType = Mailbox.TYPE_DRAFTS;
-                    break;
-                case DELETED_TYPE:
-                    mailbox.mType = Mailbox.TYPE_TRASH;
-                    break;
-                case CALENDAR_TYPE:
-                    mailbox.mType = Mailbox.TYPE_CALENDAR;
-                    mailbox.mSyncInterval = mAccount.mSyncInterval;
-                    break;
-            }
-
-            // Make boxes like Contacts and Calendar invisible in the folder list
-            mailbox.mFlagVisible = (mailbox.mType < Mailbox.TYPE_NOT_EMAIL);
-
-            if (!parentId.equals("0")) {
-                mailbox.mParentServerId = parentId;
-                if (!mInitialSync) {
-                    mParentFixupsNeeded.add(parentId);
-                }
-            }
-            // At the least, we'll need to set flags
-            mFixupUninitializedNeeded = true;
-
-            return mailbox;
+            addMailboxOp(name, serverId, parentId, type);
         }
-        return null;
     }
 
-    private void updateParser(ArrayList<ContentProviderOperation> ops) throws IOException {
+    /**
+     * Add the appropriate {@link ContentProviderOperation} to {@link #mOperations} for an Update
+     * change in the FolderSync response.
+     * @throws IOException
+     */
+    private void updateParser() throws IOException {
         String serverId = null;
         String displayName = null;
         String parentId = null;
@@ -450,33 +530,31 @@
         // We'll make a change if one of parentId or displayName are specified
         // serverId is required, but let's be careful just the same
         if (serverId != null && (displayName != null || parentId != null)) {
-            Cursor c = getServerIdCursor(serverId);
+            final Cursor c = getServerIdCursor(serverId);
             try {
                 // If we find the mailbox (using serverId), make the change
                 if (c.moveToFirst()) {
-                    userLog("Updating ", serverId);
+                    LogUtils.i(TAG, "Updating %s", serverId);
+                    final ContentValues cv = new ContentValues();
+                    // Store the new parent key.
+                    cv.put(Mailbox.PARENT_SERVER_ID, parentId);
                     // Fix up old and new parents, as needed
                     if (!TextUtils.isEmpty(parentId)) {
                         mParentFixupsNeeded.add(parentId);
+                    } else {
+                        cv.put(Mailbox.PARENT_KEY, Mailbox.NO_MAILBOX);
                     }
-                    String oldParentId = c.getString(MAILBOX_ID_COLUMNS_PARENT_SERVER_ID);
+                    final String oldParentId = c.getString(MAILBOX_ID_COLUMNS_PARENT_SERVER_ID);
                     if (!TextUtils.isEmpty(oldParentId)) {
                         mParentFixupsNeeded.add(oldParentId);
                     }
                     // Set display name if we've got one
-                    ContentValues cv = new ContentValues();
                     if (displayName != null) {
                         cv.put(Mailbox.DISPLAY_NAME, displayName);
                     }
-                    // Save away the server id and uninitialize the parent key
-                    cv.put(Mailbox.PARENT_SERVER_ID, parentId);
-                    // Clear the parent key; it will be fixed up after the commit
-                    cv.put(Mailbox.PARENT_KEY, Mailbox.PARENT_KEY_UNINITIALIZED);
-                    ops.add(ContentProviderOperation.newUpdate(
+                    mOperations.add(ContentProviderOperation.newUpdate(
                             ContentUris.withAppendedId(Mailbox.CONTENT_URI,
                                     c.getLong(MAILBOX_ID_COLUMNS_ID))).withValues(cv).build());
-                    // Say we need to fixup uninitialized mailboxes
-                    mFixupUninitializedNeeded = true;
                 }
             } finally {
                 c.close();
@@ -484,188 +562,175 @@
         }
     }
 
-    private boolean commitMailboxes(ArrayList<ContentProviderOperation> ops) {
-        // Commit the mailboxes
-        userLog("Applying ", mOperations.size(), " mailbox operations.");
-        // Execute the batch; throw IOExceptions if this fails, hoping the issue isn't repeatable
-        // If it IS repeatable, there's no good result, since the folder list will be invalid
-        try {
-            mContentResolver.applyBatch(EmailContent.AUTHORITY, mOperations);
-            return true;
-        } catch (RemoteException e) {
-            userLog("RemoteException in commitMailboxes");
-            return false;
-        } catch (OperationApplicationException e) {
-            userLog("OperationApplicationException in commitMailboxes");
-            return false;
-        }
-    }
-
-    private void changesParser(final ArrayList<ContentProviderOperation> ops,
-            final boolean initialSync) throws IOException {
-
-        // Array of added mailboxes
-        final ArrayList<Mailbox> addMailboxes = new ArrayList<Mailbox>();
-
-        // Indicate start of (potential) mailbox changes
-        MailboxUtilities.startMailboxChanges(mContext, mAccount.mId);
-
+    /**
+     * Handle the Changes element of the FolderSync response. This is the container for Add, Delete,
+     * and Update elements.
+     * @throws IOException
+     */
+    private void changesParser() throws IOException {
         while (nextTag(Tags.FOLDER_CHANGES) != END) {
             if (tag == Tags.FOLDER_ADD) {
-                final Mailbox mailbox = addParser();
-                if (mailbox != null) {
-                    addMailboxes.add(mailbox);
-                }
+                addParser();
             } else if (tag == Tags.FOLDER_DELETE) {
-                deleteParser(ops);
+                deleteParser();
             } else if (tag == Tags.FOLDER_UPDATE) {
-                updateParser(ops);
+                updateParser();
             } else if (tag == Tags.FOLDER_COUNT) {
+                // TODO: Maybe we can make use of this count somehow.
                 getValueInt();
             } else
                 skipTag();
         }
-
-        // Map folder serverId to mailbox (used to validate user mailboxes)
-        final HashMap<String, Mailbox> mailboxMap = new HashMap<String, Mailbox>();
-        for (final Mailbox mailbox : addMailboxes) {
-            mailboxMap.put(mailbox.mServerId, mailbox);
-        }
-        userLog("Total of " + addMailboxes.size() + " mailboxes parsed");
-
-        // Synchronize on the parser to prevent this being run concurrently
-        // (an extremely unlikely event, but nonetheless possible)
-        if (mInitialSync)  {
-            synchronized (FolderSyncParser.this) {
-                // Assign unique sequential ids and set appropriate mailbox flags; it's safe to
-                // assume that there won't be more than one million mailboxes defined for an
-                // account.  I use millions for ease in debugging (i.e. associating an int in
-                // the debugger with an account)
-                long mailboxId = (mAccount.mId * MAX_MAILBOXES_PER_ACCOUNT) + 1;
-                // Set basic flags
-                for (Mailbox mailbox : addMailboxes) {
-                    int type = mailbox.mType;
-                    if (type <= Mailbox.TYPE_NOT_EMAIL) {
-                        mailbox.mFlags |= Mailbox.FLAG_HOLDS_MAIL + Mailbox.FLAG_SUPPORTS_SETTINGS;
-                    }
-                    // Outbox, Drafts, and Sent don't allow mail to be moved to them
-                    if (type == Mailbox.TYPE_MAIL || type == Mailbox.TYPE_TRASH ||
-                            type == Mailbox.TYPE_JUNK || type == Mailbox.TYPE_INBOX) {
-                        mailbox.mFlags |= Mailbox.FLAG_ACCEPTS_MOVED_MAIL;
-                    }
-                    mailbox.mId = mailboxId++;
-                }
-                // Set parent mailbox key and hierarchical name; set parent flags on parents
-                for (Mailbox mailbox: addMailboxes) {
-                    String parentServerId = mailbox.mParentServerId;
-                    if (parentServerId == null || parentServerId.equals("0")) {
-                        mailbox.mParentKey = Mailbox.NO_MAILBOX;
-                    } else {
-                        Mailbox parentMailbox = mailboxMap.get(parentServerId);
-                        if (parentMailbox != null) {
-                            mailbox.mParentKey = parentMailbox.mId;
-                            parentMailbox.mFlags |=
-                                    Mailbox.FLAG_HAS_CHILDREN + Mailbox.FLAG_CHILDREN_VISIBLE;
-                            String hierarchicalName = mailbox.mDisplayName;
-                            while (parentMailbox != null) {
-                                hierarchicalName = parentMailbox.mDisplayName + "/" +
-                                        hierarchicalName;
-                                if (parentMailbox.mParentServerId != null &&
-                                        !parentMailbox.mParentServerId.equals("0")) {
-                                    parentMailbox = mailboxMap.get(parentMailbox.mParentServerId);
-                                } else {
-                                    break;
-                                }
-                            }
-                            mailbox.mHierarchicalName = hierarchicalName;
-                        } else {
-                            userLog("Parent not found with serverId = " + parentServerId);
-                        }
-                    }
-                }
-            }
-
-            // Save all the new mailboxes away in groups of 20
-            int batchCount = 0;
-            for (Mailbox mailbox: addMailboxes) {
-                if (mailbox.mId == Mailbox.NO_MAILBOX) {
-                    userLog("Skipping mailbox: ", mailbox.mDisplayName);
-                    continue;
-                }
-                if (++batchCount == MAILBOX_COMMIT_SIZE) {
-                    if (!commitMailboxes(ops)) {
-                        //mService.stop();
-                        return;
-                    }
-                    ops.clear();
-                    batchCount = 0;
-                }
-                userLog("Adding mailbox: ", mailbox.mDisplayName);
-                ContentValues initialValues = mailbox.toContentValues();
-                // We already have an id if this is the initial sync
-                if (mInitialSync) {
-                    initialValues.put(MailboxColumns.ID, mailbox.mId);
-                }
-                ops.add(ContentProviderOperation.newInsert(
-                        Mailbox.CONTENT_URI).withValues(initialValues).build());
-            }
-
-            // Save away the new sync key with the last batch
-            ContentValues cv = new ContentValues();
-            cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
-            ops.add(ContentProviderOperation
-                    .newUpdate(
-                            ContentUris.withAppendedId(Account.CONTENT_URI,
-                                    mAccount.mId))
-                                    .withValues(cv).build());
-            if (!commitMailboxes(ops)) {
-                //mService.stop();
-                return;
-            }
-        }
-
-        // If this isn't the initial sync, we need to fix up the hierarchy
-        if (!mInitialSync) {
-            String accountSelector = Mailbox.ACCOUNT_KEY + "=" + mAccount.mId;
-            // For new boxes, setup the parent key and flags
-            if (mFixupUninitializedNeeded) {
-                MailboxUtilities.fixupUninitializedParentKeys(mContext,
-                        accountSelector);
-            }
-            // For modified parents, reset the flags (and children's parent key)
-            for (String parentServerId: mParentFixupsNeeded) {
-                Cursor c = mContentResolver.query(Mailbox.CONTENT_URI,
-                        Mailbox.CONTENT_PROJECTION, Mailbox.PARENT_SERVER_ID + "=?",
-                        new String[] {parentServerId}, null);
-                try {
-                    if (c.moveToFirst()) {
-                        MailboxUtilities.setFlagsAndChildrensParentKey(mContext, c,
-                                accountSelector);
-                    }
-                } finally {
-                    c.close();
-                }
-            }
-
-            MailboxUtilities.setupHierarchicalNames(mContext, mAccount.mId);
-        }
-
-        // Signal completion of mailbox changes
-        MailboxUtilities.endMailboxChanges(mContext, mAccount.mId);
     }
 
     /**
-     * Not needed for FolderSync parsing; everything is done within changesParser
+     * Commit the contents of {@link #mOperations} to the content provider.
+     * @throws IOException
      */
+    private void flushOperations() throws IOException {
+        if (mOperations.isEmpty()) {
+            return;
+        }
+        // Execute the batch; throw IOExceptions if this fails, hoping the issue isn't repeatable
+        // If it IS repeatable, there's no good result, since the folder list will be invalid
+        try {
+            mContentResolver.applyBatch(EmailContent.AUTHORITY, mOperations);
+        } catch (final RemoteException e) {
+            LogUtils.e(TAG, "RemoteException in commit");
+            throw new IOException("RemoteException in commit");
+        } catch (final OperationApplicationException e) {
+            LogUtils.e(TAG, "OperationApplicationException in commit");
+            throw new IOException("OperationApplicationException in commit");
+        }
+        mOperations.clear();
+    }
+
+    /**
+     * Fix folder data for any folders whose parent or children changed during this sync.
+     * Unfortunately this cannot be done in the same pass as the actual sync: newly synced folders
+     * lack ids until they're committed to the content provider, so we can't set the parentKey
+     * for their children.
+     * During parsing, we only track the parents who have changed. We need to do a query for
+     * children anyway (to determine whether a parent still has any) so it's simpler to not bother
+     * tracking which folders have had their parents changed.
+     * TODO: Figure out if we can avoid the two-pass.
+     * @throws IOException
+     */
+    private void doParentFixups() throws IOException {
+        if (mParentFixupsNeeded.isEmpty()) {
+            return;
+        }
+
+        // These objects will be used in every loop iteration, so create them here for efficiency
+        // and just reset the values inside the loop as necessary.
+        final String[] bindArguments = new String[2];
+        bindArguments[1] = mAccountIdAsString;
+        final ContentValues cv = new ContentValues(2);
+
+        for (final String parentServerId : mParentFixupsNeeded) {
+            // Get info about this parent.
+            bindArguments[0] = parentServerId;
+            final Cursor parentCursor = mContentResolver.query(Mailbox.CONTENT_URI,
+                    FIXUP_PARENT_PROJECTION, WHERE_SERVER_ID_AND_ACCOUNT, bindArguments, null);
+            if (parentCursor == null) {
+                // TODO: Error handling.
+                continue;
+            }
+            final long parentId;
+            final String parentHierarchicalName;
+            final int parentFlags;
+            try {
+                if (parentCursor.moveToFirst()) {
+                    parentId = parentCursor.getLong(FIXUP_PARENT_ID_COLUMN);
+                    final String hierarchicalName = parentCursor.getString(
+                            FIXUP_PARENT_HIERARCHICAL_NAME_COLUMN);
+                    if (hierarchicalName != null) {
+                        parentHierarchicalName = hierarchicalName;
+                    } else {
+                        parentHierarchicalName = parentCursor.getString(
+                                FIXUP_PARENT_DISPLAY_NAME_COLUMN);
+                    }
+                    parentFlags = parentCursor.getInt(FIXUP_PARENT_FLAGS_COLUMN);
+                } else {
+                    // TODO: Error handling.
+                    continue;
+                }
+            } finally {
+                parentCursor.close();
+            }
+
+            // Fix any children for this parent.
+            final Cursor childCursor = mContentResolver.query(Mailbox.CONTENT_URI,
+                    FIXUP_CHILD_PROJECTION, WHERE_PARENT_SERVER_ID_AND_ACCOUNT, bindArguments,
+                    null);
+            boolean hasChildren = false;
+            if (childCursor != null) {
+                try {
+                    // Clear the results of the last iteration.
+                    cv.clear();
+                    // All children in this loop share the same parentId.
+                    cv.put(MailboxColumns.PARENT_KEY, parentId);
+                    while (childCursor.moveToNext()) {
+                        final long childId = childCursor.getLong(FIXUP_CHILD_ID_COLUMN);
+                        final String childName =
+                                childCursor.getString(FIXUP_CHILD_DISPLAY_NAME_COLUMN);
+                        cv.put(MailboxColumns.HIERARCHICAL_NAME,
+                                parentHierarchicalName + "/" + childName);
+                        mOperations.add(ContentProviderOperation.newUpdate(
+                                ContentUris.withAppendedId(Mailbox.CONTENT_URI, childId)).
+                                withValues(cv).build());
+                        hasChildren = true;
+                    }
+                } finally {
+                    childCursor.close();
+                }
+            }
+
+            // Fix the parent's flags based on whether it now has children.
+            final int newFlags;
+
+            if (hasChildren) {
+                newFlags = parentFlags | HAS_CHILDREN_FLAGS;
+            } else {
+                newFlags = parentFlags & ~HAS_CHILDREN_FLAGS;
+            }
+            if (newFlags != parentFlags) {
+                cv.clear();
+                cv.put(MailboxColumns.FLAGS, newFlags);
+                mOperations.add(ContentProviderOperation.newUpdate(ContentUris.withAppendedId(
+                        Mailbox.CONTENT_URI, parentId)).withValues(cv).build());
+            }
+        }
+
+        flushOperations();
+    }
+
     @Override
     public void commandsParser() throws IOException {
     }
 
-    /**
-     * Clean up after sync
-     */
     @Override
     public void commit() throws IOException {
+        // Set the account sync key.
+        if (mSyncKeyChanged) {
+            final ContentValues cv = new ContentValues(1);
+            cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
+            mOperations.add(
+                    ContentProviderOperation.newUpdate(mAccount.getUri()).withValues(cv).build());
+        }
+
+        // If this is the initial sync, add the outbox.
+        if (mInitialSync) {
+            addMailboxOp(Mailbox.getSystemMailboxName(mContext, Mailbox.TYPE_OUTBOX), "0", "0",
+                    OUTBOX_TYPE);
+        }
+
+        // Send all operations so far.
+        flushOperations();
+
+        // Now that new mailboxes are committed, let's do parent fixups.
+        doParentFixups();
+
         // Look for sync issues and its children and delete them
         // I'm not aware of any other way to deal with this properly
         mBindArguments[0] = "Sync Issues";
diff --git a/src/com/android/exchange/service/EasAccountValidator.java b/src/com/android/exchange/service/EasAccountValidator.java
index ea12599..9be2c01 100644
--- a/src/com/android/exchange/service/EasAccountValidator.java
+++ b/src/com/android/exchange/service/EasAccountValidator.java
@@ -9,7 +9,6 @@
 import com.android.emailcommon.provider.Account;
 import com.android.emailcommon.provider.EmailContent.AccountColumns;
 import com.android.emailcommon.provider.HostAuth;
-import com.android.emailcommon.provider.Mailbox;
 import com.android.emailcommon.provider.Policy;
 import com.android.emailcommon.service.EmailServiceProxy;
 import com.android.emailcommon.service.PolicyServiceProxy;
@@ -220,7 +219,6 @@
                             resp.getInputStream(), mAccount, isStatusOnly).parse();
                 }
                 resultCode = MessagingException.NO_ERROR;
-                // TODO: Save mAccount (at least syncKey should be different).
             } else if (code == HttpStatus.SC_FORBIDDEN) {
                 // For validation only, we take 403 as ACCESS_DENIED (the account isn't
                 // authorized, possibly due to device type)
@@ -611,13 +609,6 @@
 
         if (bundle != null) {
             bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, resultCode);
-        } else if (resultCode == MessagingException.NO_ERROR) {
-            // This is an actual sync which succeeded. Let's force the outbox to exist as well.
-            if (Mailbox.findMailboxOfType(mContext, mAccount.mId, Mailbox.TYPE_OUTBOX) ==
-                    Mailbox.NO_MAILBOX) {
-                Mailbox.newSystemMailbox(mContext, mAccount.mId, Mailbox.TYPE_OUTBOX)
-                        .save(mContext);
-            }
         }
     }