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