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                 String newKey = getValue();
    192                 if (!resetFolders) {
    193                     mAccount.mSyncKey = newKey;
    194                     userLog("New syncKey: ", newKey);
    195                 } else {
    196                     userLog("Ignoring new syncKey: ", newKey);
    197                 }
    198             } else if (tag == Tags.FOLDER_CHANGES) {
    199                 if (mStatusOnly) return res;
    200                 changesParser(mOperations, mInitialSync);
    201             } else
    202                 skipTag();
    203         }
    204         if (mStatusOnly) return res;
    205         synchronized (mService.getSynchronizer()) {
    206             if (!mService.isStopped() || resetFolders) {
    207                 commit();
    208                 userLog("Leaving FolderSyncParser with Account syncKey=", mAccount.mSyncKey);
    209             }
    210         }
    211         return res;
    212     }
    213 
    214     private Cursor getServerIdCursor(String serverId) {
    215         mBindArguments[0] = serverId;
    216         mBindArguments[1] = mAccountIdAsString;
    217         return mContentResolver.query(Mailbox.CONTENT_URI, MAILBOX_ID_COLUMNS_PROJECTION,
    218                 WHERE_SERVER_ID_AND_ACCOUNT, mBindArguments, null);
    219     }
    220 
    221     public void deleteParser(ArrayList<ContentProviderOperation> ops) throws IOException {
    222         while (nextTag(Tags.FOLDER_DELETE) != END) {
    223             switch (tag) {
    224                 case Tags.FOLDER_SERVER_ID:
    225                     String serverId = getValue();
    226                     // Find the mailbox in this account with the given serverId
    227                     Cursor c = getServerIdCursor(serverId);
    228                     try {
    229                         if (c.moveToFirst()) {
    230                             userLog("Deleting ", serverId);
    231                             ops.add(ContentProviderOperation.newDelete(
    232                                     ContentUris.withAppendedId(Mailbox.CONTENT_URI,
    233                                             c.getLong(MAILBOX_ID_COLUMNS_ID))).build());
    234                             AttachmentUtilities.deleteAllMailboxAttachmentFiles(mContext,
    235                                     mAccountId, mMailbox.mId);
    236                             if (!mInitialSync) {
    237                                 String parentId = c.getString(MAILBOX_ID_COLUMNS_PARENT_SERVER_ID);
    238                                 if (!TextUtils.isEmpty(parentId)) {
    239                                     mParentFixupsNeeded.add(parentId);
    240                                 }
    241                             }
    242                         }
    243                     } finally {
    244                         c.close();
    245                     }
    246                     break;
    247                 default:
    248                     skipTag();
    249             }
    250         }
    251     }
    252 
    253     private static class SyncOptions {
    254         private final int mInterval;
    255         private final int mLookback;
    256 
    257         private SyncOptions(int interval, int lookback) {
    258             mInterval = interval;
    259             mLookback = lookback;
    260         }
    261     }
    262 
    263     private static final String MAILBOX_STATE_SELECTION =
    264         MailboxColumns.ACCOUNT_KEY + "=? AND (" + MailboxColumns.SYNC_INTERVAL + "!=" +
    265             Account.CHECK_INTERVAL_NEVER + " OR " + Mailbox.SYNC_LOOKBACK + "!=" +
    266             SyncWindow.SYNC_WINDOW_UNKNOWN + ")";
    267 
    268     private static final String[] MAILBOX_STATE_PROJECTION = new String[] {
    269         MailboxColumns.SERVER_ID, MailboxColumns.SYNC_INTERVAL, MailboxColumns.SYNC_LOOKBACK};
    270     private static final int MAILBOX_STATE_SERVER_ID = 0;
    271     private static final int MAILBOX_STATE_INTERVAL = 1;
    272     private static final int MAILBOX_STATE_LOOKBACK = 2;
    273     @VisibleForTesting
    274     final HashMap<String, SyncOptions> mSyncOptionsMap = new HashMap<String, SyncOptions>();
    275 
    276     /**
    277      * For every mailbox in this account that has a non-default interval or lookback, save those
    278      * values.
    279      */
    280     @VisibleForTesting
    281     void saveMailboxSyncOptions() {
    282         // Shouldn't be necessary, but...
    283         mSyncOptionsMap.clear();
    284         Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, MAILBOX_STATE_PROJECTION,
    285                 MAILBOX_STATE_SELECTION, new String[] {mAccountIdAsString}, null);
    286         if (c != null) {
    287             try {
    288                 while (c.moveToNext()) {
    289                     mSyncOptionsMap.put(c.getString(MAILBOX_STATE_SERVER_ID),
    290                             new SyncOptions(c.getInt(MAILBOX_STATE_INTERVAL),
    291                                     c.getInt(MAILBOX_STATE_LOOKBACK)));
    292                 }
    293             } finally {
    294                 c.close();
    295             }
    296         }
    297     }
    298 
    299     /**
    300      * For every set of saved mailbox sync options, try to find and restore those values
    301      */
    302     @VisibleForTesting
    303     void restoreMailboxSyncOptions() {
    304         try {
    305             ContentValues cv = new ContentValues();
    306             mBindArguments[1] = mAccountIdAsString;
    307             for (String serverId: mSyncOptionsMap.keySet()) {
    308                 SyncOptions options = mSyncOptionsMap.get(serverId);
    309                 cv.put(MailboxColumns.SYNC_INTERVAL, options.mInterval);
    310                 cv.put(MailboxColumns.SYNC_LOOKBACK, options.mLookback);
    311                 mBindArguments[0] = serverId;
    312                 // If we match account and server id, set the sync options
    313                 mContentResolver.update(Mailbox.CONTENT_URI, cv, WHERE_SERVER_ID_AND_ACCOUNT,
    314                         mBindArguments);
    315             }
    316         } finally {
    317             mSyncOptionsMap.clear();
    318         }
    319     }
    320 
    321     public Mailbox addParser() throws IOException {
    322         String name = null;
    323         String serverId = null;
    324         String parentId = null;
    325         int type = 0;
    326 
    327         while (nextTag(Tags.FOLDER_ADD) != END) {
    328             switch (tag) {
    329                 case Tags.FOLDER_DISPLAY_NAME: {
    330                     name = getValue();
    331                     break;
    332                 }
    333                 case Tags.FOLDER_TYPE: {
    334                     type = getValueInt();
    335                     break;
    336                 }
    337                 case Tags.FOLDER_PARENT_ID: {
    338                     parentId = getValue();
    339                     break;
    340                 }
    341                 case Tags.FOLDER_SERVER_ID: {
    342                     serverId = getValue();
    343                     break;
    344                 }
    345                 default:
    346                     skipTag();
    347             }
    348         }
    349 
    350         if (VALID_EAS_FOLDER_TYPES.contains(type)) {
    351             Mailbox mailbox = new Mailbox();
    352             mailbox.mDisplayName = name;
    353             mailbox.mServerId = serverId;
    354             mailbox.mAccountKey = mAccountId;
    355             mailbox.mType = Mailbox.TYPE_MAIL;
    356             // Note that all mailboxes default to checking "never" (i.e. manual sync only)
    357             // We set specific intervals for inbox, contacts, and (eventually) calendar
    358             mailbox.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER;
    359             switch (type) {
    360                 case INBOX_TYPE:
    361                     mailbox.mType = Mailbox.TYPE_INBOX;
    362                     mailbox.mSyncInterval = mAccount.mSyncInterval;
    363                     break;
    364                 case CONTACTS_TYPE:
    365                     mailbox.mType = Mailbox.TYPE_CONTACTS;
    366                     mailbox.mSyncInterval = mAccount.mSyncInterval;
    367                     break;
    368                 case OUTBOX_TYPE:
    369                     // TYPE_OUTBOX mailboxes are known by ExchangeService to sync whenever they
    370                     // aren't empty.  The value of mSyncFrequency is ignored for this kind of
    371                     // mailbox.
    372                     mailbox.mType = Mailbox.TYPE_OUTBOX;
    373                     break;
    374                 case SENT_TYPE:
    375                     mailbox.mType = Mailbox.TYPE_SENT;
    376                     break;
    377                 case DRAFTS_TYPE:
    378                     mailbox.mType = Mailbox.TYPE_DRAFTS;
    379                     break;
    380                 case DELETED_TYPE:
    381                     mailbox.mType = Mailbox.TYPE_TRASH;
    382                     break;
    383                 case CALENDAR_TYPE:
    384                     mailbox.mType = Mailbox.TYPE_CALENDAR;
    385                     mailbox.mSyncInterval = mAccount.mSyncInterval;
    386                     break;
    387                 case USER_GENERIC_TYPE:
    388                     mailbox.mType = Mailbox.TYPE_UNKNOWN;
    389                     break;
    390             }
    391 
    392             // Make boxes like Contacts and Calendar invisible in the folder list
    393             mailbox.mFlagVisible = (mailbox.mType < Mailbox.TYPE_NOT_EMAIL);
    394 
    395             if (!parentId.equals("0")) {
    396                 mailbox.mParentServerId = parentId;
    397                 if (!mInitialSync) {
    398                     mParentFixupsNeeded.add(parentId);
    399                 }
    400             }
    401             // At the least, we'll need to set flags
    402             mFixupUninitializedNeeded = true;
    403 
    404             return mailbox;
    405         }
    406         return null;
    407     }
    408 
    409     /**
    410      * Determine whether a given mailbox holds mail, rather than other data.  We do this by first
    411      * checking the type of the mailbox (if it's a known good type, great; if it's a known bad
    412      * type, return false).  If it's unknown, we check the parent, first by trying to find it in
    413      * the current set of newly synced items, and then by looking it up in EmailProvider.  If
    414      * we can find the parent, we use the same rules to determine if it holds mail; if it does,
    415      * then its children do as well, so that's a go.
    416      *
    417      * @param mailbox the mailbox we're checking
    418      * @param mailboxMap a HashMap relating server id's of mailboxes in the current sync set to
    419      * the corresponding mailbox structures
    420      * @return whether or not the mailbox contains email (rather than PIM or unknown data)
    421      */
    422     /*package*/ boolean isValidMailFolder(Mailbox mailbox, HashMap<String, Mailbox> mailboxMap) {
    423         int folderType = mailbox.mType;
    424         // Automatically accept our email types
    425         if (folderType < Mailbox.TYPE_NOT_EMAIL) return true;
    426         // Automatically reject everything else but "unknown"
    427         if (folderType != Mailbox.TYPE_UNKNOWN) return false;
    428         // If this is TYPE_UNKNOWN, check the parent
    429         Mailbox parent = mailboxMap.get(mailbox.mParentServerId);
    430         // If the parent is in the map, then check it out; if not, it could be an existing saved
    431         // Mailbox, so we'll have to query the database
    432         if (parent == null) {
    433             mBindArguments[0] = Long.toString(mAccount.mId);
    434             long parentId = -1;
    435             if (mailbox.mParentServerId != null) {
    436                 mBindArguments[1] = mailbox.mParentServerId;
    437                 parentId = Utility.getFirstRowInt(mContext, Mailbox.CONTENT_URI,
    438                         EmailContent.ID_PROJECTION,
    439                         MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.SERVER_ID + "=?",
    440                         mBindArguments, null, EmailContent.ID_PROJECTION_COLUMN, -1);
    441             }
    442             if (parentId != -1) {
    443                 // Get the parent from the database
    444                 parent = Mailbox.restoreMailboxWithId(mContext, parentId);
    445                 if (parent == null) return false;
    446             } else {
    447                 return false;
    448             }
    449         }
    450         return isValidMailFolder(parent, mailboxMap);
    451     }
    452 
    453     public void updateParser(ArrayList<ContentProviderOperation> ops) throws IOException {
    454         String serverId = null;
    455         String displayName = null;
    456         String parentId = null;
    457         while (nextTag(Tags.FOLDER_UPDATE) != END) {
    458             switch (tag) {
    459                 case Tags.FOLDER_SERVER_ID:
    460                     serverId = getValue();
    461                     break;
    462                 case Tags.FOLDER_DISPLAY_NAME:
    463                     displayName = getValue();
    464                     break;
    465                 case Tags.FOLDER_PARENT_ID:
    466                     parentId = getValue();
    467                     break;
    468                 default:
    469                     skipTag();
    470                     break;
    471             }
    472         }
    473         // We'll make a change if one of parentId or displayName are specified
    474         // serverId is required, but let's be careful just the same
    475         if (serverId != null && (displayName != null || parentId != null)) {
    476             Cursor c = getServerIdCursor(serverId);
    477             try {
    478                 // If we find the mailbox (using serverId), make the change
    479                 if (c.moveToFirst()) {
    480                     userLog("Updating ", serverId);
    481                     // Fix up old and new parents, as needed
    482                     if (!TextUtils.isEmpty(parentId)) {
    483                         mParentFixupsNeeded.add(parentId);
    484                     }
    485                     String oldParentId = c.getString(MAILBOX_ID_COLUMNS_PARENT_SERVER_ID);
    486                     if (!TextUtils.isEmpty(oldParentId)) {
    487                         mParentFixupsNeeded.add(oldParentId);
    488                     }
    489                     // Set display name if we've got one
    490                     ContentValues cv = new ContentValues();
    491                     if (displayName != null) {
    492                         cv.put(Mailbox.DISPLAY_NAME, displayName);
    493                     }
    494                     // Save away the server id and uninitialize the parent key
    495                     cv.put(Mailbox.PARENT_SERVER_ID, parentId);
    496                     // Clear the parent key; it will be fixed up after the commit
    497                     cv.put(Mailbox.PARENT_KEY, Mailbox.PARENT_KEY_UNINITIALIZED);
    498                     ops.add(ContentProviderOperation.newUpdate(
    499                             ContentUris.withAppendedId(Mailbox.CONTENT_URI,
    500                                     c.getLong(MAILBOX_ID_COLUMNS_ID))).withValues(cv).build());
    501                     // Say we need to fixup uninitialized mailboxes
    502                     mFixupUninitializedNeeded = true;
    503                 }
    504             } finally {
    505                 c.close();
    506             }
    507         }
    508     }
    509 
    510     private boolean commitMailboxes(ArrayList<Mailbox> validMailboxes,
    511             ArrayList<Mailbox> userMailboxes, HashMap<String, Mailbox> mailboxMap,
    512             ArrayList<ContentProviderOperation> ops) {
    513 
    514         // Go through the generic user mailboxes; we'll call them valid if any parent is valid
    515         for (Mailbox m: userMailboxes) {
    516             if (isValidMailFolder(m, mailboxMap)) {
    517                 m.mType = Mailbox.TYPE_MAIL;
    518                 validMailboxes.add(m);
    519             } else {
    520                 userLog("Rejecting unknown type mailbox: " + m.mDisplayName);
    521             }
    522         }
    523 
    524         // Add operations for all valid mailboxes
    525         for (Mailbox m: validMailboxes) {
    526             userLog("Adding mailbox: ", m.mDisplayName);
    527             ops.add(ContentProviderOperation
    528                     .newInsert(Mailbox.CONTENT_URI).withValues(m.toContentValues()).build());
    529         }
    530 
    531         // Commit the mailboxes
    532         userLog("Applying ", mOperations.size(), " mailbox operations.");
    533         // Execute the batch; throw IOExceptions if this fails, hoping the issue isn't repeatable
    534         // If it IS repeatable, there's no good result, since the folder list will be invalid
    535         try {
    536             mContentResolver.applyBatch(EmailContent.AUTHORITY, mOperations);
    537             return true;
    538         } catch (RemoteException e) {
    539             userLog("RemoteException in commitMailboxes");
    540             return false;
    541         } catch (OperationApplicationException e) {
    542             userLog("OperationApplicationException in commitMailboxes");
    543             return false;
    544         }
    545     }
    546 
    547     public void changesParser(final ArrayList<ContentProviderOperation> ops,
    548             final boolean initialSync) throws IOException {
    549 
    550         // Array of added mailboxes
    551         final ArrayList<Mailbox> addMailboxes = new ArrayList<Mailbox>();
    552 
    553         // Indicate start of (potential) mailbox changes
    554         MailboxUtilities.startMailboxChanges(mContext, mAccount.mId);
    555 
    556         while (nextTag(Tags.FOLDER_CHANGES) != END) {
    557             if (tag == Tags.FOLDER_ADD) {
    558                 Mailbox mailbox = addParser();
    559                 if (mailbox != null) {
    560                     addMailboxes.add(mailbox);
    561                 }
    562             } else if (tag == Tags.FOLDER_DELETE) {
    563                 deleteParser(ops);
    564             } else if (tag == Tags.FOLDER_UPDATE) {
    565                 updateParser(ops);
    566             } else if (tag == Tags.FOLDER_COUNT) {
    567                 getValueInt();
    568             } else
    569                 skipTag();
    570         }
    571 
    572         // Synchronize on the parser to prevent this being run concurrently
    573         // (an extremely unlikely event, but nonetheless possible)
    574         synchronized (FolderSyncParser.this) {
    575             // Mailboxes that we known contain email
    576             ArrayList<Mailbox> validMailboxes = new ArrayList<Mailbox>();
    577             // Mailboxes that we're unsure about
    578             ArrayList<Mailbox> userMailboxes = new ArrayList<Mailbox>();
    579 
    580             // Maps folder serverId to mailbox (used to validate user mailboxes)
    581             HashMap<String, Mailbox> mailboxMap = new HashMap<String, Mailbox>();
    582             for (Mailbox mailbox : addMailboxes) {
    583                 mailboxMap.put(mailbox.mServerId, mailbox);
    584             }
    585 
    586             int mailboxCommitCount = 0;
    587             for (Mailbox mailbox : addMailboxes) {
    588                 // And add the mailbox to the proper list
    589                 if (mailbox.mType == Mailbox.TYPE_UNKNOWN) {
    590                     userMailboxes.add(mailbox);
    591                 } else {
    592                     validMailboxes.add(mailbox);
    593                 }
    594                 // On initial sync, we commit what we have every 20 mailboxes
    595                 if (initialSync && (++mailboxCommitCount == MAILBOX_COMMIT_SIZE)) {
    596                     if (!commitMailboxes(validMailboxes, userMailboxes, mailboxMap,
    597                             ops)) {
    598                         mService.stop();
    599                         return;
    600                     }
    601                     // Clear our arrays to prepare for more
    602                     userMailboxes.clear();
    603                     validMailboxes.clear();
    604                     ops.clear();
    605                     mailboxCommitCount = 0;
    606                 }
    607             }
    608             // Commit the sync key and mailboxes
    609             ContentValues cv = new ContentValues();
    610             cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
    611             ops.add(ContentProviderOperation
    612                     .newUpdate(
    613                             ContentUris.withAppendedId(Account.CONTENT_URI,
    614                                     mAccount.mId))
    615                             .withValues(cv).build());
    616             if (!commitMailboxes(validMailboxes, userMailboxes, mailboxMap, ops)) {
    617                 mService.stop();
    618                 return;
    619             }
    620             String accountSelector = Mailbox.ACCOUNT_KEY + "=" + mAccount.mId;
    621             // For new boxes, setup the parent key and flags
    622             if (mFixupUninitializedNeeded) {
    623                 MailboxUtilities.fixupUninitializedParentKeys(mContext,
    624                         accountSelector);
    625             }
    626             // For modified parents, reset the flags (and children's parent key)
    627             for (String parentServerId: mParentFixupsNeeded) {
    628                 Cursor c = mContentResolver.query(Mailbox.CONTENT_URI,
    629                         Mailbox.CONTENT_PROJECTION, Mailbox.PARENT_SERVER_ID + "=?",
    630                         new String[] {parentServerId}, null);
    631                 try {
    632                     if (c.moveToFirst()) {
    633                         MailboxUtilities.setFlagsAndChildrensParentKey(mContext, c,
    634                                 accountSelector);
    635                     }
    636                 } finally {
    637                     c.close();
    638                 }
    639             }
    640 
    641             // Signal completion of mailbox changes
    642             MailboxUtilities.endMailboxChanges(mContext, mAccount.mId);
    643         }
    644     }
    645 
    646     /**
    647      * Not needed for FolderSync parsing; everything is done within changesParser
    648      */
    649     @Override
    650     public void commandsParser() throws IOException {
    651     }
    652 
    653     /**
    654      * Clean up after sync
    655      */
    656     @Override
    657     public void commit() throws IOException {
    658         // Look for sync issues and its children and delete them
    659         // I'm not aware of any other way to deal with this properly
    660         mBindArguments[0] = "Sync Issues";
    661         mBindArguments[1] = mAccountIdAsString;
    662         Cursor c = mContentResolver.query(Mailbox.CONTENT_URI,
    663                 MAILBOX_ID_COLUMNS_PROJECTION, WHERE_DISPLAY_NAME_AND_ACCOUNT,
    664                 mBindArguments, null);
    665         String parentServerId = null;
    666         long id = 0;
    667         try {
    668             if (c.moveToFirst()) {
    669                 id = c.getLong(MAILBOX_ID_COLUMNS_ID);
    670                 parentServerId = c.getString(MAILBOX_ID_COLUMNS_SERVER_ID);
    671             }
    672         } finally {
    673             c.close();
    674         }
    675         if (parentServerId != null) {
    676             mContentResolver.delete(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id),
    677                     null, null);
    678             mBindArguments[0] = parentServerId;
    679             mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_PARENT_SERVER_ID_AND_ACCOUNT,
    680                     mBindArguments);
    681         }
    682 
    683         // If we have saved options, restore them now
    684         if (mInitialSync) {
    685             restoreMailboxSyncOptions();
    686         }
    687     }
    688 
    689     @Override
    690     public void responsesParser() throws IOException {
    691     }
    692 
    693 }
    694