FolderSyncParser.java revision 35b892c7570b06e72c97c0915d0e41abc83ed6b0
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.ContentResolver;
22import android.content.ContentUris;
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.OperationApplicationException;
26import android.database.Cursor;
27import android.os.RemoteException;
28import android.text.TextUtils;
29
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.exchange.CommandStatusException;
38import com.android.exchange.CommandStatusException.CommandStatus;
39import com.android.exchange.Eas;
40import com.android.exchange.ExchangeService;
41import com.android.mail.utils.LogUtils;
42import com.google.common.annotations.VisibleForTesting;
43
44import java.io.IOException;
45import java.io.InputStream;
46import java.util.ArrayList;
47import java.util.Arrays;
48import java.util.HashMap;
49import java.util.List;
50
51/**
52 * Parse the result of a FolderSync command
53 *
54 * Handles the addition, deletion, and changes to folders in the user's Exchange account.
55 **/
56
57public class FolderSyncParser extends AbstractSyncParser {
58
59    public static final String TAG = "FolderSyncParser";
60
61    // These are defined by the EAS protocol
62    public static final int USER_GENERIC_TYPE = 1;
63    public static final int INBOX_TYPE = 2;
64    public static final int DRAFTS_TYPE = 3;
65    public static final int DELETED_TYPE = 4;
66    public static final int SENT_TYPE = 5;
67    public static final int OUTBOX_TYPE = 6;
68    public static final int TASKS_TYPE = 7;
69    public static final int CALENDAR_TYPE = 8;
70    public static final int CONTACTS_TYPE = 9;
71    public static final int NOTES_TYPE = 10;
72    public static final int JOURNAL_TYPE = 11;
73    public static final int USER_MAILBOX_TYPE = 12;
74
75    // EAS types that we are willing to consider valid folders for EAS sync
76    private static final List<Integer> VALID_EAS_FOLDER_TYPES = Arrays.asList(INBOX_TYPE,
77            DRAFTS_TYPE, DELETED_TYPE, SENT_TYPE, OUTBOX_TYPE, USER_MAILBOX_TYPE, CALENDAR_TYPE,
78            CONTACTS_TYPE, USER_GENERIC_TYPE);
79
80    /** Content selection for all mailboxes belonging to an account. */
81    private static final String WHERE_ACCOUNT_KEY = MailboxColumns.ACCOUNT_KEY + "=?";
82
83    /**
84     * Content selection to find a specific mailbox by server id. Since server ids aren't unique
85     * across all accounts, this must also check account id.
86     */
87    private static final String WHERE_SERVER_ID_AND_ACCOUNT = MailboxColumns.SERVER_ID + "=? and " +
88        MailboxColumns.ACCOUNT_KEY + "=?";
89
90    /**
91     * Content selection to find a specific mailbox by display name and account.
92     */
93    private static final String WHERE_DISPLAY_NAME_AND_ACCOUNT = MailboxColumns.DISPLAY_NAME +
94        "=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
95
96    /**
97     * Content selection to find children by parent's server id. Since server ids aren't unique
98     * across accounts, this must also use account id.
99     */
100    private static final String WHERE_PARENT_SERVER_ID_AND_ACCOUNT =
101        MailboxColumns.PARENT_SERVER_ID +"=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
102
103    /** Projection used when fetching a Mailbox's ids. */
104    private static final String[] MAILBOX_ID_COLUMNS_PROJECTION =
105        new String[] {MailboxColumns.ID, MailboxColumns.SERVER_ID, MailboxColumns.PARENT_SERVER_ID};
106    private static final int MAILBOX_ID_COLUMNS_ID = 0;
107    private static final int MAILBOX_ID_COLUMNS_SERVER_ID = 1;
108    private static final int MAILBOX_ID_COLUMNS_PARENT_SERVER_ID = 2;
109
110    /** Projection used for changed parents during parent/child fixup. */
111    private static final String[] FIXUP_PARENT_PROJECTION =
112            { MailboxColumns.ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.HIERARCHICAL_NAME,
113                    MailboxColumns.FLAGS };
114    private static final int FIXUP_PARENT_ID_COLUMN = 0;
115    private static final int FIXUP_PARENT_DISPLAY_NAME_COLUMN = 1;
116    private static final int FIXUP_PARENT_HIERARCHICAL_NAME_COLUMN = 2;
117    private static final int FIXUP_PARENT_FLAGS_COLUMN = 3;
118
119    /** Projection used for changed children during parent/child fixup. */
120    private static final String[] FIXUP_CHILD_PROJECTION =
121            { MailboxColumns.ID, MailboxColumns.DISPLAY_NAME };
122    private static final int FIXUP_CHILD_ID_COLUMN = 0;
123    private static final int FIXUP_CHILD_DISPLAY_NAME_COLUMN = 1;
124
125    /** Flags that are set or cleared when a mailbox's child status changes. */
126    private static final int HAS_CHILDREN_FLAGS =
127            Mailbox.FLAG_HAS_CHILDREN | Mailbox.FLAG_CHILDREN_VISIBLE;
128
129    /** Mailbox.NO_MAILBOX, as a string (convenience since this is used in several places). */
130    private static final String NO_MAILBOX_STRING = Long.toString(Mailbox.NO_MAILBOX);
131
132    @VisibleForTesting
133    long mAccountId;
134    @VisibleForTesting
135    String mAccountIdAsString;
136    @VisibleForTesting
137    boolean mInUnitTest = false;
138
139    private String[] mBindArguments = new String[2];
140
141    /** List of pending operations to send as a batch to the content provider. */
142    private ArrayList<ContentProviderOperation> mOperations =
143            new ArrayList<ContentProviderOperation>();
144    /** Indicates whether this sync is an initial FolderSync. */
145    private boolean mInitialSync;
146    /** List of folder server ids whose children changed with this sync. */
147    private ArrayList<String> mParentFixupsNeeded = new ArrayList<String>();
148    /** Indicates whether the sync response provided a different sync key than we had. */
149    private boolean mSyncKeyChanged = false;
150
151    // If true, we only care about status (this is true when validating an account) and ignore
152    // other data
153    private final boolean mStatusOnly;
154
155    private boolean mOutboxCreated = false;
156
157    private static final ContentValues UNINITIALIZED_PARENT_KEY = new ContentValues();
158
159    {
160        UNINITIALIZED_PARENT_KEY.put(MailboxColumns.PARENT_KEY, Mailbox.PARENT_KEY_UNINITIALIZED);
161    }
162
163    public FolderSyncParser(final Context context, final ContentResolver resolver,
164            final InputStream in, final Account account, final boolean statusOnly)
165                    throws IOException {
166        super(context, resolver, in, null, account);
167        mAccountId = mAccount.mId;
168        mAccountIdAsString = Long.toString(mAccountId);
169        mStatusOnly = statusOnly;
170    }
171
172    public FolderSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException {
173        this(in, adapter, false);
174    }
175
176    public FolderSyncParser(InputStream in, AbstractSyncAdapter adapter, boolean statusOnly)
177            throws IOException {
178        super(in, adapter);
179        mAccountId = mAccount.mId;
180        mAccountIdAsString = Long.toString(mAccountId);
181        mStatusOnly = statusOnly;
182    }
183
184    @Override
185    public boolean parse() throws IOException, CommandStatusException {
186        int status;
187        boolean res = false;
188        boolean resetFolders = false;
189        mInitialSync = (mAccount.mSyncKey == null) || "0".equals(mAccount.mSyncKey);
190        if (mInitialSync) {
191            // We're resyncing all folders for this account, so nuke any existing ones.
192            mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_ACCOUNT_KEY,
193                    new String[] {mAccountIdAsString});
194        }
195        if (nextTag(START_DOCUMENT) != Tags.FOLDER_FOLDER_SYNC)
196            throw new EasParserException();
197        while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
198            if (tag == Tags.FOLDER_STATUS) {
199                status = getValueInt();
200                // Do a sanity check on the account here; if we have any duplicated folders, we'll
201                // act as though we have a bad folder sync key (wipe/reload mailboxes)
202                // Note: The ContentValues isn't used, but no point creating a new one
203                int dupes = 0;
204                if (mAccountId > 0) {
205                    dupes = mContentResolver.update(
206                            ContentUris.withAppendedId(EmailContent.ACCOUNT_CHECK_URI, mAccountId),
207                            UNINITIALIZED_PARENT_KEY, null, null);
208                }
209                if (dupes > 0) {
210                    LogUtils.w(TAG, "Duplicate mailboxes found for account %d: %d", mAccountId,
211                            dupes);
212                    status = Eas.FOLDER_STATUS_INVALID_KEY;
213                }
214                if (status != Eas.FOLDER_STATUS_OK) {
215                    // If the account hasn't been saved, this is a validation attempt, so we don't
216                    // try reloading the folder list...
217                    if (CommandStatus.isDeniedAccess(status) ||
218                            CommandStatus.isNeedsProvisioning(status) ||
219                            (mAccount.mId == Account.NOT_SAVED)) {
220                        throw new CommandStatusException(status);
221                    // Note that we need to catch both old-style (Eas.FOLDER_STATUS_INVALID_KEY)
222                    // and EAS 14 style command status
223                    } else if (status == Eas.FOLDER_STATUS_INVALID_KEY ||
224                            CommandStatus.isBadSyncKey(status)) {
225                        // Delete PIM data
226                        ExchangeService.deleteAccountPIMData(mContext, mAccountId);
227                        // Save away any mailbox sync information that is NOT default
228                        saveMailboxSyncOptions();
229                        // And only then, delete mailboxes
230                        mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_ACCOUNT_KEY,
231                                new String[] {mAccountIdAsString});
232                        // Reconstruct _main
233                        res = true;
234                        resetFolders = true;
235                        // Reset the sync key and save (this should trigger the AccountObserver
236                        // in ExchangeService, which will recreate the account mailbox, which
237                        // will then start syncing folders, etc.)
238                        mAccount.mSyncKey = "0";
239                        ContentValues cv = new ContentValues();
240                        cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
241                        mContentResolver.update(ContentUris.withAppendedId(Account.CONTENT_URI,
242                                mAccount.mId), cv, null, null);
243                    } else {
244                        // Other errors are at the server, so let's throw an error that will
245                        // cause this sync to be retried at a later time
246                        throw new EasParserException("Folder status error");
247                    }
248                }
249            } else if (tag == Tags.FOLDER_SYNC_KEY) {
250                final String newKey = getValue();
251                if (newKey != null && !resetFolders) {
252                    mSyncKeyChanged = !newKey.equals(mAccount.mSyncKey);
253                    mAccount.mSyncKey = newKey;
254                }
255            } else if (tag == Tags.FOLDER_CHANGES) {
256                if (mStatusOnly) return res;
257                changesParser();
258            } else
259                skipTag();
260        }
261        if (!mStatusOnly) {
262            commit();
263        }
264        return res;
265    }
266
267    /**
268     * Get a cursor with folder ids for a specific folder.
269     * @param serverId The server id for the folder we are interested in.
270     * @return A cursor for the folder specified by serverId for this account.
271     */
272    private Cursor getServerIdCursor(final String serverId) {
273        mBindArguments[0] = serverId;
274        mBindArguments[1] = mAccountIdAsString;
275        return mContentResolver.query(Mailbox.CONTENT_URI, MAILBOX_ID_COLUMNS_PROJECTION,
276                WHERE_SERVER_ID_AND_ACCOUNT, mBindArguments, null);
277    }
278
279    /**
280     * Add the appropriate {@link ContentProviderOperation} to {@link #mOperations} for a Delete
281     * change in the FolderSync response.
282     * @throws IOException
283     */
284    private void deleteParser() throws IOException {
285        while (nextTag(Tags.FOLDER_DELETE) != END) {
286            switch (tag) {
287                case Tags.FOLDER_SERVER_ID:
288                    final String serverId = getValue();
289                    // Find the mailbox in this account with the given serverId
290                    final Cursor c = getServerIdCursor(serverId);
291                    try {
292                        if (c.moveToFirst()) {
293                            LogUtils.i(TAG, "Deleting %s", serverId);
294                            final long mailboxId = c.getLong(MAILBOX_ID_COLUMNS_ID);
295                            mOperations.add(ContentProviderOperation.newDelete(
296                                    ContentUris.withAppendedId(Mailbox.CONTENT_URI,
297                                            mailboxId)).build());
298                            AttachmentUtilities.deleteAllMailboxAttachmentFiles(mContext,
299                                    mAccountId, mailboxId);
300                            final String parentId =
301                                    c.getString(MAILBOX_ID_COLUMNS_PARENT_SERVER_ID);
302                            if (!TextUtils.isEmpty(parentId)) {
303                                mParentFixupsNeeded.add(parentId);
304                            }
305                        }
306                    } finally {
307                        c.close();
308                    }
309                    break;
310                default:
311                    skipTag();
312            }
313        }
314    }
315
316    private static class SyncOptions {
317        private final int mInterval;
318        private final int mLookback;
319
320        private SyncOptions(int interval, int lookback) {
321            mInterval = interval;
322            mLookback = lookback;
323        }
324    }
325
326    private static final String MAILBOX_STATE_SELECTION =
327        MailboxColumns.ACCOUNT_KEY + "=? AND (" + MailboxColumns.SYNC_INTERVAL + "!=" +
328            Account.CHECK_INTERVAL_NEVER + " OR " + Mailbox.SYNC_LOOKBACK + "!=" +
329            SyncWindow.SYNC_WINDOW_ACCOUNT + ")";
330
331    private static final String[] MAILBOX_STATE_PROJECTION = new String[] {
332        MailboxColumns.SERVER_ID, MailboxColumns.SYNC_INTERVAL, MailboxColumns.SYNC_LOOKBACK};
333    private static final int MAILBOX_STATE_SERVER_ID = 0;
334    private static final int MAILBOX_STATE_INTERVAL = 1;
335    private static final int MAILBOX_STATE_LOOKBACK = 2;
336    @VisibleForTesting
337    final HashMap<String, SyncOptions> mSyncOptionsMap = new HashMap<String, SyncOptions>();
338
339    /**
340     * For every mailbox in this account that has a non-default interval or lookback, save those
341     * values.
342     */
343    @VisibleForTesting
344    void saveMailboxSyncOptions() {
345        // Shouldn't be necessary, but...
346        mSyncOptionsMap.clear();
347        Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, MAILBOX_STATE_PROJECTION,
348                MAILBOX_STATE_SELECTION, new String[] {mAccountIdAsString}, null);
349        if (c != null) {
350            try {
351                while (c.moveToNext()) {
352                    mSyncOptionsMap.put(c.getString(MAILBOX_STATE_SERVER_ID),
353                            new SyncOptions(c.getInt(MAILBOX_STATE_INTERVAL),
354                                    c.getInt(MAILBOX_STATE_LOOKBACK)));
355                }
356            } finally {
357                c.close();
358            }
359        }
360    }
361
362    /**
363     * For every set of saved mailbox sync options, try to find and restore those values
364     */
365    @VisibleForTesting
366    void restoreMailboxSyncOptions() {
367        try {
368            ContentValues cv = new ContentValues();
369            mBindArguments[1] = mAccountIdAsString;
370            for (String serverId: mSyncOptionsMap.keySet()) {
371                SyncOptions options = mSyncOptionsMap.get(serverId);
372                cv.put(MailboxColumns.SYNC_INTERVAL, options.mInterval);
373                cv.put(MailboxColumns.SYNC_LOOKBACK, options.mLookback);
374                mBindArguments[0] = serverId;
375                // If we match account and server id, set the sync options
376                mContentResolver.update(Mailbox.CONTENT_URI, cv, WHERE_SERVER_ID_AND_ACCOUNT,
377                        mBindArguments);
378            }
379        } finally {
380            mSyncOptionsMap.clear();
381        }
382    }
383
384    /**
385     * Add a {@link ContentProviderOperation} to {@link #mOperations} to add a mailbox.
386     * @param name The new mailbox's name.
387     * @param serverId The new mailbox's server id.
388     * @param parentServerId The server id of the new mailbox's parent ("0" if none).
389     * @param easType The mailbox's type, in terms of the EAS values (NOT {@link Mailbox}'s type
390     *                values).
391     * @throws IOException
392     */
393    private void addMailboxOp(final String name, final String serverId,
394            final String parentServerId, final int easType) throws IOException {
395        final ContentValues cv = new ContentValues();
396        cv.put(MailboxColumns.DISPLAY_NAME, name);
397        cv.put(MailboxColumns.SERVER_ID, serverId);
398        final String parentId;
399        if (parentServerId.equals("0")) {
400            parentId = NO_MAILBOX_STRING;
401            cv.put(Mailbox.PARENT_KEY, Mailbox.NO_MAILBOX);
402        } else {
403            parentId = parentServerId;
404            mParentFixupsNeeded.add(parentId);
405        }
406        cv.put(MailboxColumns.PARENT_SERVER_ID, parentId);
407        cv.put(MailboxColumns.ACCOUNT_KEY, mAccountId);
408        final int mailboxType;
409        final boolean shouldSync;
410        switch (easType) {
411            case INBOX_TYPE:
412                mailboxType = Mailbox.TYPE_INBOX;
413                shouldSync = true;
414                break;
415            case CONTACTS_TYPE:
416                mailboxType = Mailbox.TYPE_CONTACTS;
417                shouldSync = true;
418                break;
419            case OUTBOX_TYPE:
420                // TYPE_OUTBOX mailboxes are known by ExchangeService to sync whenever they
421                // aren't empty.  The value of mSyncFrequency is ignored for this kind of
422                // mailbox.
423                mailboxType = Mailbox.TYPE_OUTBOX;
424                shouldSync = false;
425                mOutboxCreated = true;
426                break;
427            case SENT_TYPE:
428                mailboxType = Mailbox.TYPE_SENT;
429                shouldSync = false;
430                break;
431            case DRAFTS_TYPE:
432                mailboxType = Mailbox.TYPE_DRAFTS;
433                shouldSync = false;
434                break;
435            case DELETED_TYPE:
436                mailboxType = Mailbox.TYPE_TRASH;
437                shouldSync = false;
438                break;
439            case CALENDAR_TYPE:
440                mailboxType = Mailbox.TYPE_CALENDAR;
441                shouldSync = true;
442                break;
443            default:
444                mailboxType = Mailbox.TYPE_MAIL;
445                shouldSync = false;
446        }
447        cv.put(MailboxColumns.TYPE, mailboxType);
448        cv.put(MailboxColumns.SYNC_INTERVAL, shouldSync ? 1 : 0);
449
450        // Set basic flags
451        int flags = 0;
452        if (mailboxType <= Mailbox.TYPE_NOT_EMAIL) {
453            flags |= Mailbox.FLAG_HOLDS_MAIL + Mailbox.FLAG_SUPPORTS_SETTINGS;
454        }
455        // Outbox, Drafts, and Sent don't allow mail to be moved to them
456        if (mailboxType == Mailbox.TYPE_MAIL || mailboxType == Mailbox.TYPE_TRASH ||
457                mailboxType == Mailbox.TYPE_JUNK || mailboxType == Mailbox.TYPE_INBOX) {
458            flags |= Mailbox.FLAG_ACCEPTS_MOVED_MAIL;
459        }
460        cv.put(MailboxColumns.FLAGS, flags);
461
462        // Make boxes like Contacts and Calendar invisible in the folder list
463        cv.put(MailboxColumns.FLAG_VISIBLE, (mailboxType < Mailbox.TYPE_NOT_EMAIL));
464
465        mOperations.add(
466                ContentProviderOperation.newInsert(Mailbox.CONTENT_URI).withValues(cv).build());
467    }
468
469    /**
470     * Add the appropriate {@link ContentProviderOperation} to {@link #mOperations} for an Add
471     * change in the FolderSync response.
472     * @throws IOException
473     */
474    private void addParser() throws IOException {
475        String name = null;
476        String serverId = null;
477        String parentId = null;
478        int type = 0;
479
480        while (nextTag(Tags.FOLDER_ADD) != END) {
481            switch (tag) {
482                case Tags.FOLDER_DISPLAY_NAME: {
483                    name = getValue();
484                    break;
485                }
486                case Tags.FOLDER_TYPE: {
487                    type = getValueInt();
488                    break;
489                }
490                case Tags.FOLDER_PARENT_ID: {
491                    parentId = getValue();
492                    break;
493                }
494                case Tags.FOLDER_SERVER_ID: {
495                    serverId = getValue();
496                    break;
497                }
498                default:
499                    skipTag();
500            }
501        }
502
503        if (VALID_EAS_FOLDER_TYPES.contains(type)) {
504            addMailboxOp(name, serverId, parentId, type);
505        }
506    }
507
508    /**
509     * Add the appropriate {@link ContentProviderOperation} to {@link #mOperations} for an Update
510     * change in the FolderSync response.
511     * @throws IOException
512     */
513    private void updateParser() throws IOException {
514        String serverId = null;
515        String displayName = null;
516        String parentId = null;
517        while (nextTag(Tags.FOLDER_UPDATE) != END) {
518            switch (tag) {
519                case Tags.FOLDER_SERVER_ID:
520                    serverId = getValue();
521                    break;
522                case Tags.FOLDER_DISPLAY_NAME:
523                    displayName = getValue();
524                    break;
525                case Tags.FOLDER_PARENT_ID:
526                    parentId = getValue();
527                    break;
528                default:
529                    skipTag();
530                    break;
531            }
532        }
533        // We'll make a change if one of parentId or displayName are specified
534        // serverId is required, but let's be careful just the same
535        if (serverId != null && (displayName != null || parentId != null)) {
536            final Cursor c = getServerIdCursor(serverId);
537            try {
538                // If we find the mailbox (using serverId), make the change
539                if (c.moveToFirst()) {
540                    LogUtils.i(TAG, "Updating %s", serverId);
541                    final ContentValues cv = new ContentValues();
542                    // Store the new parent key.
543                    cv.put(Mailbox.PARENT_SERVER_ID, parentId);
544                    // Fix up old and new parents, as needed
545                    if (!TextUtils.isEmpty(parentId)) {
546                        mParentFixupsNeeded.add(parentId);
547                    } else {
548                        cv.put(Mailbox.PARENT_KEY, Mailbox.NO_MAILBOX);
549                    }
550                    final String oldParentId = c.getString(MAILBOX_ID_COLUMNS_PARENT_SERVER_ID);
551                    if (!TextUtils.isEmpty(oldParentId)) {
552                        mParentFixupsNeeded.add(oldParentId);
553                    }
554                    // Set display name if we've got one
555                    if (displayName != null) {
556                        cv.put(Mailbox.DISPLAY_NAME, displayName);
557                    }
558                    mOperations.add(ContentProviderOperation.newUpdate(
559                            ContentUris.withAppendedId(Mailbox.CONTENT_URI,
560                                    c.getLong(MAILBOX_ID_COLUMNS_ID))).withValues(cv).build());
561                }
562            } finally {
563                c.close();
564            }
565        }
566    }
567
568    /**
569     * Handle the Changes element of the FolderSync response. This is the container for Add, Delete,
570     * and Update elements.
571     * @throws IOException
572     */
573    private void changesParser() throws IOException {
574        while (nextTag(Tags.FOLDER_CHANGES) != END) {
575            if (tag == Tags.FOLDER_ADD) {
576                addParser();
577            } else if (tag == Tags.FOLDER_DELETE) {
578                deleteParser();
579            } else if (tag == Tags.FOLDER_UPDATE) {
580                updateParser();
581            } else if (tag == Tags.FOLDER_COUNT) {
582                // TODO: Maybe we can make use of this count somehow.
583                getValueInt();
584            } else
585                skipTag();
586        }
587    }
588
589    /**
590     * Commit the contents of {@link #mOperations} to the content provider.
591     * @throws IOException
592     */
593    private void flushOperations() throws IOException {
594        if (mOperations.isEmpty()) {
595            return;
596        }
597        // Execute the batch; throw IOExceptions if this fails, hoping the issue isn't repeatable
598        // If it IS repeatable, there's no good result, since the folder list will be invalid
599        try {
600            mContentResolver.applyBatch(EmailContent.AUTHORITY, mOperations);
601        } catch (final RemoteException e) {
602            LogUtils.e(TAG, "RemoteException in commit");
603            throw new IOException("RemoteException in commit");
604        } catch (final OperationApplicationException e) {
605            LogUtils.e(TAG, "OperationApplicationException in commit");
606            throw new IOException("OperationApplicationException in commit");
607        }
608        mOperations.clear();
609    }
610
611    /**
612     * Fix folder data for any folders whose parent or children changed during this sync.
613     * Unfortunately this cannot be done in the same pass as the actual sync: newly synced folders
614     * lack ids until they're committed to the content provider, so we can't set the parentKey
615     * for their children.
616     * During parsing, we only track the parents who have changed. We need to do a query for
617     * children anyway (to determine whether a parent still has any) so it's simpler to not bother
618     * tracking which folders have had their parents changed.
619     * TODO: Figure out if we can avoid the two-pass.
620     * @throws IOException
621     */
622    private void doParentFixups() throws IOException {
623        if (mParentFixupsNeeded.isEmpty()) {
624            return;
625        }
626
627        // These objects will be used in every loop iteration, so create them here for efficiency
628        // and just reset the values inside the loop as necessary.
629        final String[] bindArguments = new String[2];
630        bindArguments[1] = mAccountIdAsString;
631        final ContentValues cv = new ContentValues(2);
632
633        for (final String parentServerId : mParentFixupsNeeded) {
634            // Get info about this parent.
635            bindArguments[0] = parentServerId;
636            final Cursor parentCursor = mContentResolver.query(Mailbox.CONTENT_URI,
637                    FIXUP_PARENT_PROJECTION, WHERE_SERVER_ID_AND_ACCOUNT, bindArguments, null);
638            if (parentCursor == null) {
639                // TODO: Error handling.
640                continue;
641            }
642            final long parentId;
643            final String parentHierarchicalName;
644            final int parentFlags;
645            try {
646                if (parentCursor.moveToFirst()) {
647                    parentId = parentCursor.getLong(FIXUP_PARENT_ID_COLUMN);
648                    final String hierarchicalName = parentCursor.getString(
649                            FIXUP_PARENT_HIERARCHICAL_NAME_COLUMN);
650                    if (hierarchicalName != null) {
651                        parentHierarchicalName = hierarchicalName;
652                    } else {
653                        parentHierarchicalName = parentCursor.getString(
654                                FIXUP_PARENT_DISPLAY_NAME_COLUMN);
655                    }
656                    parentFlags = parentCursor.getInt(FIXUP_PARENT_FLAGS_COLUMN);
657                } else {
658                    // TODO: Error handling.
659                    continue;
660                }
661            } finally {
662                parentCursor.close();
663            }
664
665            // Fix any children for this parent.
666            final Cursor childCursor = mContentResolver.query(Mailbox.CONTENT_URI,
667                    FIXUP_CHILD_PROJECTION, WHERE_PARENT_SERVER_ID_AND_ACCOUNT, bindArguments,
668                    null);
669            boolean hasChildren = false;
670            if (childCursor != null) {
671                try {
672                    // Clear the results of the last iteration.
673                    cv.clear();
674                    // All children in this loop share the same parentId.
675                    cv.put(MailboxColumns.PARENT_KEY, parentId);
676                    while (childCursor.moveToNext()) {
677                        final long childId = childCursor.getLong(FIXUP_CHILD_ID_COLUMN);
678                        final String childName =
679                                childCursor.getString(FIXUP_CHILD_DISPLAY_NAME_COLUMN);
680                        cv.put(MailboxColumns.HIERARCHICAL_NAME,
681                                parentHierarchicalName + "/" + childName);
682                        mOperations.add(ContentProviderOperation.newUpdate(
683                                ContentUris.withAppendedId(Mailbox.CONTENT_URI, childId)).
684                                withValues(cv).build());
685                        hasChildren = true;
686                    }
687                } finally {
688                    childCursor.close();
689                }
690            }
691
692            // Fix the parent's flags based on whether it now has children.
693            final int newFlags;
694
695            if (hasChildren) {
696                newFlags = parentFlags | HAS_CHILDREN_FLAGS;
697            } else {
698                newFlags = parentFlags & ~HAS_CHILDREN_FLAGS;
699            }
700            if (newFlags != parentFlags) {
701                cv.clear();
702                cv.put(MailboxColumns.FLAGS, newFlags);
703                mOperations.add(ContentProviderOperation.newUpdate(ContentUris.withAppendedId(
704                        Mailbox.CONTENT_URI, parentId)).withValues(cv).build());
705            }
706        }
707
708        flushOperations();
709    }
710
711    @Override
712    public void commandsParser() throws IOException {
713    }
714
715    @Override
716    public void commit() throws IOException {
717        // Set the account sync key.
718        if (mSyncKeyChanged) {
719            final ContentValues cv = new ContentValues(1);
720            cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
721            mOperations.add(
722                    ContentProviderOperation.newUpdate(mAccount.getUri()).withValues(cv).build());
723        }
724
725        // If this is the initial sync, and we didn't get an Outbox, make one.
726        if (mInitialSync && !mOutboxCreated) {
727            addMailboxOp(Mailbox.getSystemMailboxName(mContext, Mailbox.TYPE_OUTBOX), "0", "0",
728                    OUTBOX_TYPE);
729        }
730
731        // Send all operations so far.
732        flushOperations();
733
734        // Now that new mailboxes are committed, let's do parent fixups.
735        doParentFixups();
736
737        // Look for sync issues and its children and delete them
738        // I'm not aware of any other way to deal with this properly
739        mBindArguments[0] = "Sync Issues";
740        mBindArguments[1] = mAccountIdAsString;
741        Cursor c = mContentResolver.query(Mailbox.CONTENT_URI,
742                MAILBOX_ID_COLUMNS_PROJECTION, WHERE_DISPLAY_NAME_AND_ACCOUNT,
743                mBindArguments, null);
744        String parentServerId = null;
745        long id = 0;
746        try {
747            if (c.moveToFirst()) {
748                id = c.getLong(MAILBOX_ID_COLUMNS_ID);
749                parentServerId = c.getString(MAILBOX_ID_COLUMNS_SERVER_ID);
750            }
751        } finally {
752            c.close();
753        }
754        if (parentServerId != null) {
755            mContentResolver.delete(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id),
756                    null, null);
757            mBindArguments[0] = parentServerId;
758            mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_PARENT_SERVER_ID_AND_ACCOUNT,
759                    mBindArguments);
760        }
761
762        // If we have saved options, restore them now
763        if (mInitialSync) {
764            restoreMailboxSyncOptions();
765        }
766    }
767
768    @Override
769    public void responsesParser() throws IOException {
770    }
771
772}
773