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