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