• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_GENERIC, Mailbox.TYPE_MAIL)76         MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_GENERIC,  Mailbox.TYPE_MAIL);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_INBOX, Mailbox.TYPE_INBOX)77         MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_INBOX,  Mailbox.TYPE_INBOX);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_DRAFTS, Mailbox.TYPE_DRAFTS)78         MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_DRAFTS,  Mailbox.TYPE_DRAFTS);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_DELETED, Mailbox.TYPE_TRASH)79         MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_DELETED,  Mailbox.TYPE_TRASH);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_SENT, Mailbox.TYPE_SENT)80         MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_SENT,  Mailbox.TYPE_SENT);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_OUTBOX, Mailbox.TYPE_OUTBOX)81         MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_OUTBOX,  Mailbox.TYPE_OUTBOX);
82         //MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_TASKS,  Mailbox.TYPE_TASKS);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_CALENDAR, Mailbox.TYPE_CALENDAR)83         MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_CALENDAR,  Mailbox.TYPE_CALENDAR);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_CONTACTS, Mailbox.TYPE_CONTACTS)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);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_MAIL, Mailbox.TYPE_MAIL)87         MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_MAIL, Mailbox.TYPE_MAIL);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_CALENDAR, Mailbox.TYPE_CALENDAR)88         MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_CALENDAR, Mailbox.TYPE_CALENDAR);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_CONTACTS, Mailbox.TYPE_CONTACTS)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 {
UNINITIALIZED_PARENT_KEY.put(MailboxColumns.PARENT_KEY, Mailbox.PARENT_KEY_UNINITIALIZED)173         UNINITIALIZED_PARENT_KEY.put(MailboxColumns.PARENT_KEY, Mailbox.PARENT_KEY_UNINITIALIZED);
174     }
175 
FolderSyncParser(final Context context, final ContentResolver resolver, final InputStream in, final Account account, final boolean statusOnly)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 
FolderSyncParser(InputStream in, AbstractSyncAdapter adapter)185     public FolderSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException {
186         this(in, adapter, false);
187     }
188 
FolderSyncParser(InputStream in, AbstractSyncAdapter adapter, boolean statusOnly)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
parse()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      */
getServerIdCursor(final String serverId)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      */
deleteParser()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 
SyncOptions(int interval, int lookback)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
saveMailboxSyncOptions()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
restoreMailboxSyncOptions()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      */
addMailboxOp(final String name, final String serverId, final String parentServerId, final int mailboxType, final boolean fromServer)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      */
addParser()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      */
updateParser()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      */
changesParser()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      */
flushOperations()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      */
doParentFixups()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
commandsParser()701     public void commandsParser() throws IOException {
702     }
703 
704     @Override
commit()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
responsesParser()762     public void responsesParser() throws IOException {
763     }
764 
765     @Override
wipe()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