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