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