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