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