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.ContentResolver;
     22 import android.content.ContentUris;
     23 import android.content.ContentValues;
     24 import android.content.Context;
     25 import android.content.OperationApplicationException;
     26 import android.database.Cursor;
     27 import android.os.RemoteException;
     28 import android.os.TransactionTooLargeException;
     29 import android.text.TextUtils;
     30 import android.util.SparseBooleanArray;
     31 import android.util.SparseIntArray;
     32 
     33 import com.android.emailcommon.provider.Account;
     34 import com.android.emailcommon.provider.EmailContent;
     35 import com.android.emailcommon.provider.EmailContent.AccountColumns;
     36 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
     37 import com.android.emailcommon.provider.Mailbox;
     38 import com.android.emailcommon.service.SyncWindow;
     39 import com.android.emailcommon.utility.AttachmentUtilities;
     40 import com.android.exchange.CommandStatusException;
     41 import com.android.exchange.CommandStatusException.CommandStatus;
     42 import com.android.exchange.Eas;
     43 import com.android.exchange.eas.EasSyncContacts;
     44 import com.android.exchange.eas.EasSyncCalendar;
     45 import com.android.mail.utils.LogUtils;
     46 import com.google.common.annotations.VisibleForTesting;
     47 
     48 import java.io.IOException;
     49 import java.io.InputStream;
     50 import java.util.ArrayList;
     51 import java.util.HashMap;
     52 import java.util.LinkedHashSet;
     53 import java.util.Set;
     54 
     55 /**
     56  * Parse the result of a FolderSync command
     57  *
     58  * Handles the addition, deletion, and changes to folders in the user's Exchange account.
     59  **/
     60 
     61 public class FolderSyncParser extends AbstractSyncParser {
     62 
     63     public static final String TAG = "FolderSyncParser";
     64 
     65     /**
     66      * Mapping from EAS type values to {@link Mailbox} types.
     67      * See http://msdn.microsoft.com/en-us/library/gg650877(v=exchg.80).aspx for the list of EAS
     68      * type values.
     69      * If an EAS type is not in the map, or is inserted with a value of {@link Mailbox#TYPE_NONE},
     70      * then we don't support that type and we should ignore it.
     71      * TODO: Maybe we should store the mailbox anyway, otherwise it'll be annoying to upgrade.
     72      */
     73     private static final SparseIntArray MAILBOX_TYPE_MAP;
     74     static {
     75         MAILBOX_TYPE_MAP = new SparseIntArray(11);
     76         MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_GENERIC,  Mailbox.TYPE_MAIL);
     77         MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_INBOX,  Mailbox.TYPE_INBOX);
     78         MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_DRAFTS,  Mailbox.TYPE_DRAFTS);
     79         MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_DELETED,  Mailbox.TYPE_TRASH);
     80         MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_SENT,  Mailbox.TYPE_SENT);
     81         MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_OUTBOX,  Mailbox.TYPE_OUTBOX);
     82         //MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_TASKS,  Mailbox.TYPE_TASKS);
     83         MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_CALENDAR,  Mailbox.TYPE_CALENDAR);
     84         MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_CONTACTS,  Mailbox.TYPE_CONTACTS);
     85         //MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_NOTES, Mailbox.TYPE_NONE);
     86         //MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_JOURNAL, Mailbox.TYPE_NONE);
     87         MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_MAIL, Mailbox.TYPE_MAIL);
     88         MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_CALENDAR, Mailbox.TYPE_CALENDAR);
     89         MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_CONTACTS, Mailbox.TYPE_CONTACTS);
     90         //MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_TASKS, Mailbox.TYPE_TASKS);
     91         //MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_JOURNAL, Mailbox.TYPE_NONE);
     92         //MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_NOTES, Mailbox.TYPE_NONE);
     93         //MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_UNKNOWN, Mailbox.TYPE_NONE);
     94         //MAILBOX_TYPE_MAP.put(MAILBOX_TYPE_RECIPIENT_INFORMATION_CACHE, Mailbox.TYPE_NONE);
     95     }
     96 
     97     /** Content selection for all mailboxes belonging to an account. */
     98     private static final String WHERE_ACCOUNT_KEY = MailboxColumns.ACCOUNT_KEY + "=?";
     99 
    100     /**
    101      * Content selection to find a specific mailbox by server id. Since server ids aren't unique
    102      * across all accounts, this must also check account id.
    103      */
    104     private static final String WHERE_SERVER_ID_AND_ACCOUNT = MailboxColumns.SERVER_ID + "=? and " +
    105         MailboxColumns.ACCOUNT_KEY + "=?";
    106 
    107     /**
    108      * Content selection to find a specific mailbox by display name and account.
    109      */
    110     private static final String WHERE_DISPLAY_NAME_AND_ACCOUNT = MailboxColumns.DISPLAY_NAME +
    111         "=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
    112 
    113     /**
    114      * Content selection to find children by parent's server id. Since server ids aren't unique
    115      * across accounts, this must also use account id.
    116      */
    117     private static final String WHERE_PARENT_SERVER_ID_AND_ACCOUNT =
    118         MailboxColumns.PARENT_SERVER_ID +"=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
    119 
    120     /** Projection used when fetching a Mailbox's ids. */
    121     private static final String[] MAILBOX_ID_COLUMNS_PROJECTION =
    122         new String[] {MailboxColumns.ID, MailboxColumns.SERVER_ID, MailboxColumns.PARENT_SERVER_ID};
    123     private static final int MAILBOX_ID_COLUMNS_ID = 0;
    124     private static final int MAILBOX_ID_COLUMNS_SERVER_ID = 1;
    125     private static final int MAILBOX_ID_COLUMNS_PARENT_SERVER_ID = 2;
    126 
    127     /** Projection used for changed parents during parent/child fixup. */
    128     private static final String[] FIXUP_PARENT_PROJECTION =
    129             { MailboxColumns.ID, MailboxColumns.FLAGS };
    130     private static final int FIXUP_PARENT_ID_COLUMN = 0;
    131     private static final int FIXUP_PARENT_FLAGS_COLUMN = 1;
    132 
    133     /** Projection used for changed children during parent/child fixup. */
    134     private static final String[] FIXUP_CHILD_PROJECTION =
    135             { MailboxColumns.ID };
    136     private static final int FIXUP_CHILD_ID_COLUMN = 0;
    137 
    138     /** Flags that are set or cleared when a mailbox's child status changes. */
    139     private static final int HAS_CHILDREN_FLAGS =
    140             Mailbox.FLAG_HAS_CHILDREN | Mailbox.FLAG_CHILDREN_VISIBLE;
    141 
    142     /** Mailbox.NO_MAILBOX, as a string (convenience since this is used in several places). */
    143     private static final String NO_MAILBOX_STRING = Long.toString(Mailbox.NO_MAILBOX);
    144 
    145     @VisibleForTesting
    146     long mAccountId;
    147     @VisibleForTesting
    148     String mAccountIdAsString;
    149 
    150     private final String[] mBindArguments = new String[2];
    151 
    152     /** List of pending operations to send as a batch to the content provider. */
    153     private final ArrayList<ContentProviderOperation> mOperations =
    154             new ArrayList<ContentProviderOperation>();
    155     /** Indicates whether this sync is an initial FolderSync. */
    156     private boolean mInitialSync;
    157     /** List of folder server ids whose children changed with this sync. */
    158     private final Set<String> mParentFixupsNeeded = new LinkedHashSet<String>();
    159     /** Indicates whether the sync response provided a different sync key than we had. */
    160     private boolean mSyncKeyChanged = false;
    161 
    162     // If true, we only care about status (this is true when validating an account) and ignore
    163     // other data
    164     private final boolean mStatusOnly;
    165 
    166     /** Map of folder types that have been created during this sync. */
    167     private final SparseBooleanArray mCreatedFolderTypes =
    168             new SparseBooleanArray(Mailbox.REQUIRED_FOLDER_TYPES.length);
    169 
    170     private static final ContentValues UNINITIALIZED_PARENT_KEY = new ContentValues();
    171 
    172     static {
    173         UNINITIALIZED_PARENT_KEY.put(MailboxColumns.PARENT_KEY, Mailbox.PARENT_KEY_UNINITIALIZED);
    174     }
    175 
    176     public FolderSyncParser(final Context context, final ContentResolver resolver,
    177             final InputStream in, final Account account, final boolean statusOnly)
    178                     throws IOException {
    179         super(context, resolver, in, null, account);
    180         mAccountId = mAccount.mId;
    181         mAccountIdAsString = Long.toString(mAccountId);
    182         mStatusOnly = statusOnly;
    183     }
    184 
    185     public FolderSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException {
    186         this(in, adapter, false);
    187     }
    188 
    189     public FolderSyncParser(InputStream in, AbstractSyncAdapter adapter, boolean statusOnly)
    190             throws IOException {
    191         super(in, adapter);
    192         mAccountId = mAccount.mId;
    193         mAccountIdAsString = Long.toString(mAccountId);
    194         mStatusOnly = statusOnly;
    195     }
    196 
    197     @Override
    198     public boolean parse() throws IOException, CommandStatusException {
    199         int status;
    200         boolean res = false;
    201         boolean resetFolders = false;
    202         mInitialSync = (mAccount.mSyncKey == null) || "0".equals(mAccount.mSyncKey);
    203         if (mInitialSync) {
    204             // We're resyncing all folders for this account, so nuke any existing ones.
    205             mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_ACCOUNT_KEY,
    206                     new String[] {mAccountIdAsString});
    207         }
    208         if (nextTag(START_DOCUMENT) != Tags.FOLDER_FOLDER_SYNC)
    209             throw new EasParserException();
    210         while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
    211             if (tag == Tags.FOLDER_STATUS) {
    212                 status = getValueInt();
    213                 // Do a sanity check on the account here; if we have any duplicated folders, we'll
    214                 // act as though we have a bad folder sync key (wipe/reload mailboxes)
    215                 // Note: The ContentValues isn't used, but no point creating a new one
    216                 int dupes = 0;
    217                 if (mAccountId > 0) {
    218                     dupes = mContentResolver.update(
    219                             ContentUris.withAppendedId(EmailContent.ACCOUNT_CHECK_URI, mAccountId),
    220                             UNINITIALIZED_PARENT_KEY, null, null);
    221                 }
    222                 if (dupes > 0) {
    223                     LogUtils.w(TAG, "Duplicate mailboxes found for account %d: %d", mAccountId,
    224                             dupes);
    225                     status = Eas.FOLDER_STATUS_INVALID_KEY;
    226                 }
    227                 if (status != Eas.FOLDER_STATUS_OK) {
    228                     // If the account hasn't been saved, this is a validation attempt, so we don't
    229                     // try reloading the folder list...
    230                     if (CommandStatus.isDeniedAccess(status) ||
    231                             CommandStatus.isNeedsProvisioning(status) ||
    232                             (mAccount.mId == Account.NOT_SAVED)) {
    233                         LogUtils.e(LogUtils.TAG, "FolderSync: Unknown status: " + status);
    234                         throw new CommandStatusException(status);
    235                     // Note that we need to catch both old-style (Eas.FOLDER_STATUS_INVALID_KEY)
    236                     // and EAS 14 style command status
    237                     } else if (status == Eas.FOLDER_STATUS_INVALID_KEY ||
    238                             CommandStatus.isBadSyncKey(status)) {
    239                         wipe();
    240                         // Reconstruct _main
    241                         res = true;
    242                         resetFolders = true;
    243                     } else {
    244                         // Other errors are at the server, so let's throw an error that will
    245                         // cause this sync to be retried at a later time
    246                         throw new EasParserException("Folder status error");
    247                     }
    248                 }
    249             } else if (tag == Tags.FOLDER_SYNC_KEY) {
    250                 final String newKey = getValue();
    251                 if (newKey != null && !resetFolders) {
    252                     mSyncKeyChanged = !newKey.equals(mAccount.mSyncKey);
    253                     mAccount.mSyncKey = newKey;
    254                 }
    255             } else if (tag == Tags.FOLDER_CHANGES) {
    256                 if (mStatusOnly) return res;
    257                 changesParser();
    258             } else
    259                 skipTag();
    260         }
    261         if (!mStatusOnly) {
    262             commit();
    263         }
    264         return res;
    265     }
    266 
    267     /**
    268      * Get a cursor with folder ids for a specific folder.
    269      * @param serverId The server id for the folder we are interested in.
    270      * @return A cursor for the folder specified by serverId for this account.
    271      */
    272     private Cursor getServerIdCursor(final String serverId) {
    273         mBindArguments[0] = serverId;
    274         mBindArguments[1] = mAccountIdAsString;
    275         return mContentResolver.query(Mailbox.CONTENT_URI, MAILBOX_ID_COLUMNS_PROJECTION,
    276                 WHERE_SERVER_ID_AND_ACCOUNT, mBindArguments, null);
    277     }
    278 
    279     /**
    280      * Add the appropriate {@link ContentProviderOperation} to {@link #mOperations} for a Delete
    281      * change in the FolderSync response.
    282      * @throws IOException
    283      */
    284     private void deleteParser() throws IOException {
    285         while (nextTag(Tags.FOLDER_DELETE) != END) {
    286             switch (tag) {
    287                 case Tags.FOLDER_SERVER_ID:
    288                     final String serverId = getValue();
    289                     // Find the mailbox in this account with the given serverId
    290                     final Cursor c = getServerIdCursor(serverId);
    291                     try {
    292                         if (c.moveToFirst()) {
    293                             LogUtils.d(TAG, "Deleting %s", serverId);
    294                             final long mailboxId = c.getLong(MAILBOX_ID_COLUMNS_ID);
    295                             mOperations.add(ContentProviderOperation.newDelete(
    296                                     ContentUris.withAppendedId(Mailbox.CONTENT_URI,
    297                                             mailboxId)).build());
    298                             AttachmentUtilities.deleteAllMailboxAttachmentFiles(mContext,
    299                                     mAccountId, mailboxId);
    300                             final String parentId =
    301                                     c.getString(MAILBOX_ID_COLUMNS_PARENT_SERVER_ID);
    302                             if (!TextUtils.isEmpty(parentId)) {
    303                                 mParentFixupsNeeded.add(parentId);
    304                             }
    305                         }
    306                     } finally {
    307                         c.close();
    308                     }
    309                     break;
    310                 default:
    311                     skipTag();
    312             }
    313         }
    314     }
    315 
    316     private static class SyncOptions {
    317         private final int mInterval;
    318         private final int mLookback;
    319 
    320         private SyncOptions(int interval, int lookback) {
    321             mInterval = interval;
    322             mLookback = lookback;
    323         }
    324     }
    325 
    326     private static final String MAILBOX_STATE_SELECTION =
    327         MailboxColumns.ACCOUNT_KEY + "=? AND (" + MailboxColumns.SYNC_INTERVAL + "!=" +
    328             Account.CHECK_INTERVAL_NEVER + " OR " + Mailbox.SYNC_LOOKBACK + "!=" +
    329             SyncWindow.SYNC_WINDOW_ACCOUNT + ")";
    330 
    331     private static final String[] MAILBOX_STATE_PROJECTION = new String[] {
    332         MailboxColumns.SERVER_ID, MailboxColumns.SYNC_INTERVAL, MailboxColumns.SYNC_LOOKBACK};
    333     private static final int MAILBOX_STATE_SERVER_ID = 0;
    334     private static final int MAILBOX_STATE_INTERVAL = 1;
    335     private static final int MAILBOX_STATE_LOOKBACK = 2;
    336     @VisibleForTesting
    337     final HashMap<String, SyncOptions> mSyncOptionsMap = new HashMap<String, SyncOptions>();
    338 
    339     /**
    340      * For every mailbox in this account that has a non-default interval or lookback, save those
    341      * values.
    342      */
    343     @VisibleForTesting
    344     void saveMailboxSyncOptions() {
    345         // Shouldn't be necessary, but...
    346         mSyncOptionsMap.clear();
    347         Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, MAILBOX_STATE_PROJECTION,
    348                 MAILBOX_STATE_SELECTION, new String[] {mAccountIdAsString}, null);
    349         if (c != null) {
    350             try {
    351                 while (c.moveToNext()) {
    352                     mSyncOptionsMap.put(c.getString(MAILBOX_STATE_SERVER_ID),
    353                             new SyncOptions(c.getInt(MAILBOX_STATE_INTERVAL),
    354                                     c.getInt(MAILBOX_STATE_LOOKBACK)));
    355                 }
    356             } finally {
    357                 c.close();
    358             }
    359         }
    360     }
    361 
    362     /**
    363      * For every set of saved mailbox sync options, try to find and restore those values
    364      */
    365     @VisibleForTesting
    366     void restoreMailboxSyncOptions() {
    367         try {
    368             ContentValues cv = new ContentValues();
    369             mBindArguments[1] = mAccountIdAsString;
    370             for (String serverId: mSyncOptionsMap.keySet()) {
    371                 SyncOptions options = mSyncOptionsMap.get(serverId);
    372                 cv.put(MailboxColumns.SYNC_INTERVAL, options.mInterval);
    373                 cv.put(MailboxColumns.SYNC_LOOKBACK, options.mLookback);
    374                 mBindArguments[0] = serverId;
    375                 // If we match account and server id, set the sync options
    376                 mContentResolver.update(Mailbox.CONTENT_URI, cv, WHERE_SERVER_ID_AND_ACCOUNT,
    377                         mBindArguments);
    378             }
    379         } finally {
    380             mSyncOptionsMap.clear();
    381         }
    382     }
    383 
    384     /**
    385      * Add a {@link ContentProviderOperation} to {@link #mOperations} to add a mailbox.
    386      * @param name The new mailbox's name.
    387      * @param serverId The new mailbox's server id.
    388      * @param parentServerId The server id of the new mailbox's parent ("0" if none).
    389      * @param mailboxType The mailbox's type, which is one of the values defined in {@link Mailbox}.
    390      * @param fromServer Whether this mailbox was synced from server (as opposed to local-only).
    391      * @throws IOException
    392      */
    393     private void addMailboxOp(final String name, final String serverId,
    394             final String parentServerId, final int mailboxType, final boolean fromServer)
    395             throws IOException {
    396         final ContentValues cv = new ContentValues(10);
    397         cv.put(MailboxColumns.DISPLAY_NAME, name);
    398         if (fromServer) {
    399             cv.put(MailboxColumns.SERVER_ID, serverId);
    400             final String parentId;
    401             if (parentServerId.equals("0")) {
    402                 parentId = NO_MAILBOX_STRING;
    403                 cv.put(MailboxColumns.PARENT_KEY, Mailbox.NO_MAILBOX);
    404             } else {
    405                 parentId = parentServerId;
    406                 mParentFixupsNeeded.add(parentId);
    407             }
    408             cv.put(MailboxColumns.PARENT_SERVER_ID, parentId);
    409         } else {
    410             cv.put(MailboxColumns.SERVER_ID, "");
    411             cv.put(MailboxColumns.PARENT_KEY, Mailbox.NO_MAILBOX);
    412             cv.put(MailboxColumns.PARENT_SERVER_ID, NO_MAILBOX_STRING);
    413             cv.put(MailboxColumns.TOTAL_COUNT, -1);
    414         }
    415         cv.put(MailboxColumns.ACCOUNT_KEY, mAccountId);
    416         cv.put(MailboxColumns.TYPE, mailboxType);
    417 
    418         final boolean shouldSync = fromServer && Mailbox.getDefaultSyncStateForType(mailboxType);
    419         cv.put(MailboxColumns.SYNC_INTERVAL, shouldSync ? 1 : 0);
    420 
    421         // Set basic flags
    422         int flags = 0;
    423         if (mailboxType <= Mailbox.TYPE_NOT_EMAIL) {
    424             flags |= Mailbox.FLAG_HOLDS_MAIL + Mailbox.FLAG_SUPPORTS_SETTINGS;
    425         }
    426         // Outbox, Drafts, and Sent don't allow mail to be moved to them
    427         if (mailboxType == Mailbox.TYPE_MAIL || mailboxType == Mailbox.TYPE_TRASH ||
    428                 mailboxType == Mailbox.TYPE_JUNK || mailboxType == Mailbox.TYPE_INBOX) {
    429             flags |= Mailbox.FLAG_ACCEPTS_MOVED_MAIL;
    430         }
    431         cv.put(MailboxColumns.FLAGS, flags);
    432 
    433         // Make boxes like Contacts and Calendar invisible in the folder list
    434         cv.put(MailboxColumns.FLAG_VISIBLE, (mailboxType < Mailbox.TYPE_NOT_EMAIL));
    435 
    436         mOperations.add(
    437                 ContentProviderOperation.newInsert(Mailbox.CONTENT_URI).withValues(cv).build());
    438 
    439         mCreatedFolderTypes.put(mailboxType, true);
    440     }
    441 
    442     /**
    443      * Add the appropriate {@link ContentProviderOperation} to {@link #mOperations} for an Add
    444      * change in the FolderSync response.
    445      * @throws IOException
    446      */
    447     private void addParser() throws IOException {
    448         String name = null;
    449         String serverId = null;
    450         String parentId = null;
    451         int type = 0;
    452 
    453         while (nextTag(Tags.FOLDER_ADD) != END) {
    454             switch (tag) {
    455                 case Tags.FOLDER_DISPLAY_NAME: {
    456                     name = getValue();
    457                     break;
    458                 }
    459                 case Tags.FOLDER_TYPE: {
    460                     type = getValueInt();
    461                     break;
    462                 }
    463                 case Tags.FOLDER_PARENT_ID: {
    464                     parentId = getValue();
    465                     break;
    466                 }
    467                 case Tags.FOLDER_SERVER_ID: {
    468                     serverId = getValue();
    469                     break;
    470                 }
    471                 default:
    472                     skipTag();
    473             }
    474         }
    475         if (name != null && serverId != null && parentId != null) {
    476             final int mailboxType = MAILBOX_TYPE_MAP.get(type, Mailbox.TYPE_NONE);
    477             if (mailboxType != Mailbox.TYPE_NONE) {
    478                 if (type == Eas.MAILBOX_TYPE_CALENDAR && !name.contains(mAccount.mEmailAddress)) {
    479                     name = mAccount.mEmailAddress;
    480                 }
    481                 addMailboxOp(name, serverId, parentId, mailboxType, true);
    482             }
    483         }
    484     }
    485 
    486     /**
    487      * Add the appropriate {@link ContentProviderOperation} to {@link #mOperations} for an Update
    488      * change in the FolderSync response.
    489      * @throws IOException
    490      */
    491     private void updateParser() throws IOException {
    492         String serverId = null;
    493         String displayName = null;
    494         String parentId = null;
    495         while (nextTag(Tags.FOLDER_UPDATE) != END) {
    496             switch (tag) {
    497                 case Tags.FOLDER_SERVER_ID:
    498                     serverId = getValue();
    499                     break;
    500                 case Tags.FOLDER_DISPLAY_NAME:
    501                     displayName = getValue();
    502                     break;
    503                 case Tags.FOLDER_PARENT_ID:
    504                     parentId = getValue();
    505                     break;
    506                 default:
    507                     skipTag();
    508                     break;
    509             }
    510         }
    511         // We'll make a change if one of parentId or displayName are specified
    512         // serverId is required, but let's be careful just the same
    513         if (serverId != null && (displayName != null || parentId != null)) {
    514             final Cursor c = getServerIdCursor(serverId);
    515             try {
    516                 // If we find the mailbox (using serverId), make the change
    517                 if (c.moveToFirst()) {
    518                     LogUtils.d(TAG, "Updating %s", serverId);
    519                     final ContentValues cv = new ContentValues();
    520                     // Store the new parent key.
    521                     cv.put(Mailbox.PARENT_SERVER_ID, parentId);
    522                     // Fix up old and new parents, as needed
    523                     if (!TextUtils.isEmpty(parentId)) {
    524                         mParentFixupsNeeded.add(parentId);
    525                     } else {
    526                         cv.put(Mailbox.PARENT_KEY, Mailbox.NO_MAILBOX);
    527                     }
    528                     final String oldParentId = c.getString(MAILBOX_ID_COLUMNS_PARENT_SERVER_ID);
    529                     if (!TextUtils.isEmpty(oldParentId)) {
    530                         mParentFixupsNeeded.add(oldParentId);
    531                     }
    532                     // Set display name if we've got one
    533                     if (displayName != null) {
    534                         cv.put(Mailbox.DISPLAY_NAME, displayName);
    535                     }
    536                     mOperations.add(ContentProviderOperation.newUpdate(
    537                             ContentUris.withAppendedId(Mailbox.CONTENT_URI,
    538                                     c.getLong(MAILBOX_ID_COLUMNS_ID))).withValues(cv).build());
    539                 }
    540             } finally {
    541                 c.close();
    542             }
    543         }
    544     }
    545 
    546     /**
    547      * Handle the Changes element of the FolderSync response. This is the container for Add, Delete,
    548      * and Update elements.
    549      * @throws IOException
    550      */
    551     private void changesParser() throws IOException {
    552         while (nextTag(Tags.FOLDER_CHANGES) != END) {
    553             if (tag == Tags.FOLDER_ADD) {
    554                 addParser();
    555             } else if (tag == Tags.FOLDER_DELETE) {
    556                 deleteParser();
    557             } else if (tag == Tags.FOLDER_UPDATE) {
    558                 updateParser();
    559             } else if (tag == Tags.FOLDER_COUNT) {
    560                 // TODO: Maybe we can make use of this count somehow.
    561                 getValueInt();
    562             } else
    563                 skipTag();
    564         }
    565     }
    566 
    567     /**
    568      * Commit the contents of {@link #mOperations} to the content provider.
    569      * @throws IOException
    570      */
    571     private void flushOperations() throws IOException {
    572         if (mOperations.isEmpty()) {
    573             return;
    574         }
    575         int transactionSize = mOperations.size();
    576         final ArrayList<ContentProviderOperation> subOps =
    577                 new ArrayList<ContentProviderOperation>(transactionSize);
    578         while (!mOperations.isEmpty()) {
    579             subOps.clear();
    580             // If the original transaction is split into smaller transactions,
    581             // need to ensure the final transaction doesn't overrun the array.
    582             if (transactionSize > mOperations.size()) {
    583                 transactionSize = mOperations.size();
    584             }
    585             subOps.addAll(mOperations.subList(0, transactionSize));
    586             // Try to apply the ops. If the transaction is too large, split it in half and try again
    587             // If some other error happens then throw an IOException up the stack.
    588             try {
    589                 mContentResolver.applyBatch(EmailContent.AUTHORITY, subOps);
    590                 mOperations.removeAll(subOps);
    591             } catch (final TransactionTooLargeException e) {
    592                 // If the transaction is too large, try splitting it.
    593                 if (transactionSize == 1) {
    594                     LogUtils.e(TAG, "Single operation transaction too large");
    595                     throw new IOException("Single operation transaction too large");
    596                 }
    597                 LogUtils.d(TAG, "Transaction operation count %d too large, halving...",
    598                         transactionSize);
    599                 transactionSize = transactionSize / 2;
    600                 if (transactionSize < 1) {
    601                     transactionSize = 1;
    602                 }
    603             } catch (final RemoteException e) {
    604                 LogUtils.e(TAG, "RemoteException in commit");
    605                 throw new IOException("RemoteException in commit");
    606             } catch (final OperationApplicationException e) {
    607                 LogUtils.e(TAG, "OperationApplicationException in commit");
    608                 throw new IOException("OperationApplicationException in commit");
    609             }
    610         }
    611         mOperations.clear();
    612     }
    613 
    614     /**
    615      * Fix folder data for any folders whose parent or children changed during this sync.
    616      * Unfortunately this cannot be done in the same pass as the actual sync: newly synced folders
    617      * lack ids until they're committed to the content provider, so we can't set the parentKey
    618      * for their children.
    619      * During parsing, we only track the parents who have changed. We need to do a query for
    620      * children anyway (to determine whether a parent still has any) so it's simpler to not bother
    621      * tracking which folders have had their parents changed.
    622      * TODO: Figure out if we can avoid the two-pass.
    623      * @throws IOException
    624      */
    625     private void doParentFixups() throws IOException {
    626         if (mParentFixupsNeeded.isEmpty()) {
    627             return;
    628         }
    629 
    630         // These objects will be used in every loop iteration, so create them here for efficiency
    631         // and just reset the values inside the loop as necessary.
    632         final String[] bindArguments = new String[2];
    633         bindArguments[1] = mAccountIdAsString;
    634         final ContentValues cv = new ContentValues(1);
    635 
    636         for (final String parentServerId : mParentFixupsNeeded) {
    637             // Get info about this parent.
    638             bindArguments[0] = parentServerId;
    639             final Cursor parentCursor = mContentResolver.query(Mailbox.CONTENT_URI,
    640                     FIXUP_PARENT_PROJECTION, WHERE_SERVER_ID_AND_ACCOUNT, bindArguments, null);
    641             if (parentCursor == null) {
    642                 // TODO: Error handling.
    643                 continue;
    644             }
    645             final long parentId;
    646             final int parentFlags;
    647             try {
    648                 if (parentCursor.moveToFirst()) {
    649                     parentId = parentCursor.getLong(FIXUP_PARENT_ID_COLUMN);
    650                     parentFlags = parentCursor.getInt(FIXUP_PARENT_FLAGS_COLUMN);
    651                 } else {
    652                     // TODO: Error handling.
    653                     continue;
    654                 }
    655             } finally {
    656                 parentCursor.close();
    657             }
    658 
    659             // Fix any children for this parent.
    660             final Cursor childCursor = mContentResolver.query(Mailbox.CONTENT_URI,
    661                     FIXUP_CHILD_PROJECTION, WHERE_PARENT_SERVER_ID_AND_ACCOUNT, bindArguments,
    662                     null);
    663             boolean hasChildren = false;
    664             if (childCursor != null) {
    665                 try {
    666                     // Clear the results of the last iteration.
    667                     cv.clear();
    668                     // All children in this loop share the same parentId.
    669                     cv.put(MailboxColumns.PARENT_KEY, parentId);
    670                     while (childCursor.moveToNext()) {
    671                         final long childId = childCursor.getLong(FIXUP_CHILD_ID_COLUMN);
    672                         mOperations.add(ContentProviderOperation.newUpdate(
    673                                 ContentUris.withAppendedId(Mailbox.CONTENT_URI, childId)).
    674                                 withValues(cv).build());
    675                         hasChildren = true;
    676                     }
    677                 } finally {
    678                     childCursor.close();
    679                 }
    680             }
    681 
    682             // Fix the parent's flags based on whether it now has children.
    683             final int newFlags;
    684 
    685             if (hasChildren) {
    686                 newFlags = parentFlags | HAS_CHILDREN_FLAGS;
    687             } else {
    688                 newFlags = parentFlags & ~HAS_CHILDREN_FLAGS;
    689             }
    690             if (newFlags != parentFlags) {
    691                 cv.clear();
    692                 cv.put(MailboxColumns.FLAGS, newFlags);
    693                 mOperations.add(ContentProviderOperation.newUpdate(ContentUris.withAppendedId(
    694                         Mailbox.CONTENT_URI, parentId)).withValues(cv).build());
    695             }
    696             flushOperations();
    697         }
    698     }
    699 
    700     @Override
    701     public void commandsParser() throws IOException {
    702     }
    703 
    704     @Override
    705     public void commit() throws IOException {
    706         // Set the account sync key.
    707         if (mSyncKeyChanged) {
    708             final ContentValues cv = new ContentValues(1);
    709             cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
    710             mOperations.add(
    711                     ContentProviderOperation.newUpdate(mAccount.getUri()).withValues(cv).build());
    712         }
    713 
    714         // If this is the initial sync, make sure we have all the required folder types.
    715         if (mInitialSync) {
    716             for (final int requiredType : Mailbox.REQUIRED_FOLDER_TYPES) {
    717                 if (!mCreatedFolderTypes.get(requiredType)) {
    718                     addMailboxOp(Mailbox.getSystemMailboxName(mContext, requiredType),
    719                             null, null, requiredType, false);
    720                 }
    721             }
    722         }
    723 
    724         // Send all operations so far.
    725         flushOperations();
    726 
    727         // Now that new mailboxes are committed, let's do parent fixups.
    728         doParentFixups();
    729 
    730         // Look for sync issues and its children and delete them
    731         // I'm not aware of any other way to deal with this properly
    732         mBindArguments[0] = "Sync Issues";
    733         mBindArguments[1] = mAccountIdAsString;
    734         Cursor c = mContentResolver.query(Mailbox.CONTENT_URI,
    735                 MAILBOX_ID_COLUMNS_PROJECTION, WHERE_DISPLAY_NAME_AND_ACCOUNT,
    736                 mBindArguments, null);
    737         String parentServerId = null;
    738         long id = 0;
    739         try {
    740             if (c.moveToFirst()) {
    741                 id = c.getLong(MAILBOX_ID_COLUMNS_ID);
    742                 parentServerId = c.getString(MAILBOX_ID_COLUMNS_SERVER_ID);
    743             }
    744         } finally {
    745             c.close();
    746         }
    747         if (parentServerId != null) {
    748             mContentResolver.delete(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id),
    749                     null, null);
    750             mBindArguments[0] = parentServerId;
    751             mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_PARENT_SERVER_ID_AND_ACCOUNT,
    752                     mBindArguments);
    753         }
    754 
    755         // If we have saved options, restore them now
    756         if (mInitialSync) {
    757             restoreMailboxSyncOptions();
    758         }
    759     }
    760 
    761     @Override
    762     public void responsesParser() throws IOException {
    763     }
    764 
    765     @Override
    766     protected void wipe() {
    767         EasSyncCalendar.wipeAccountFromContentProvider(mContext,
    768                 mAccount.mEmailAddress);
    769         EasSyncContacts.wipeAccountFromContentProvider(mContext,
    770                 mAccount.mEmailAddress);
    771 
    772         // Save away any mailbox sync information that is NOT default
    773         saveMailboxSyncOptions();
    774         // And only then, delete mailboxes
    775         mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_ACCOUNT_KEY,
    776                 new String[] {mAccountIdAsString});
    777         // Reset the sync key and save.
    778         mAccount.mSyncKey = "0";
    779         ContentValues cv = new ContentValues();
    780         cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
    781         mContentResolver.update(ContentUris.withAppendedId(Account.CONTENT_URI,
    782                 mAccount.mId), cv, null, null);
    783     }
    784 }
    785