FolderSyncParser.java revision 35b892c7570b06e72c97c0915d0e41abc83ed6b0
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 18package com.android.exchange.adapter; 19 20import android.content.ContentProviderOperation; 21import android.content.ContentResolver; 22import android.content.ContentUris; 23import android.content.ContentValues; 24import android.content.Context; 25import android.content.OperationApplicationException; 26import android.database.Cursor; 27import android.os.RemoteException; 28import android.text.TextUtils; 29 30import com.android.emailcommon.provider.Account; 31import com.android.emailcommon.provider.EmailContent; 32import com.android.emailcommon.provider.EmailContent.AccountColumns; 33import com.android.emailcommon.provider.EmailContent.MailboxColumns; 34import com.android.emailcommon.provider.Mailbox; 35import com.android.emailcommon.service.SyncWindow; 36import com.android.emailcommon.utility.AttachmentUtilities; 37import com.android.exchange.CommandStatusException; 38import com.android.exchange.CommandStatusException.CommandStatus; 39import com.android.exchange.Eas; 40import com.android.exchange.ExchangeService; 41import com.android.mail.utils.LogUtils; 42import com.google.common.annotations.VisibleForTesting; 43 44import java.io.IOException; 45import java.io.InputStream; 46import java.util.ArrayList; 47import java.util.Arrays; 48import java.util.HashMap; 49import java.util.List; 50 51/** 52 * Parse the result of a FolderSync command 53 * 54 * Handles the addition, deletion, and changes to folders in the user's Exchange account. 55 **/ 56 57public class FolderSyncParser extends AbstractSyncParser { 58 59 public static final String TAG = "FolderSyncParser"; 60 61 // These are defined by the EAS protocol 62 public static final int USER_GENERIC_TYPE = 1; 63 public static final int INBOX_TYPE = 2; 64 public static final int DRAFTS_TYPE = 3; 65 public static final int DELETED_TYPE = 4; 66 public static final int SENT_TYPE = 5; 67 public static final int OUTBOX_TYPE = 6; 68 public static final int TASKS_TYPE = 7; 69 public static final int CALENDAR_TYPE = 8; 70 public static final int CONTACTS_TYPE = 9; 71 public static final int NOTES_TYPE = 10; 72 public static final int JOURNAL_TYPE = 11; 73 public static final int USER_MAILBOX_TYPE = 12; 74 75 // EAS types that we are willing to consider valid folders for EAS sync 76 private static final List<Integer> VALID_EAS_FOLDER_TYPES = Arrays.asList(INBOX_TYPE, 77 DRAFTS_TYPE, DELETED_TYPE, SENT_TYPE, OUTBOX_TYPE, USER_MAILBOX_TYPE, CALENDAR_TYPE, 78 CONTACTS_TYPE, USER_GENERIC_TYPE); 79 80 /** Content selection for all mailboxes belonging to an account. */ 81 private static final String WHERE_ACCOUNT_KEY = MailboxColumns.ACCOUNT_KEY + "=?"; 82 83 /** 84 * Content selection to find a specific mailbox by server id. Since server ids aren't unique 85 * across all accounts, this must also check account id. 86 */ 87 private static final String WHERE_SERVER_ID_AND_ACCOUNT = MailboxColumns.SERVER_ID + "=? and " + 88 MailboxColumns.ACCOUNT_KEY + "=?"; 89 90 /** 91 * Content selection to find a specific mailbox by display name and account. 92 */ 93 private static final String WHERE_DISPLAY_NAME_AND_ACCOUNT = MailboxColumns.DISPLAY_NAME + 94 "=? and " + MailboxColumns.ACCOUNT_KEY + "=?"; 95 96 /** 97 * Content selection to find children by parent's server id. Since server ids aren't unique 98 * across accounts, this must also use account id. 99 */ 100 private static final String WHERE_PARENT_SERVER_ID_AND_ACCOUNT = 101 MailboxColumns.PARENT_SERVER_ID +"=? and " + MailboxColumns.ACCOUNT_KEY + "=?"; 102 103 /** Projection used when fetching a Mailbox's ids. */ 104 private static final String[] MAILBOX_ID_COLUMNS_PROJECTION = 105 new String[] {MailboxColumns.ID, MailboxColumns.SERVER_ID, MailboxColumns.PARENT_SERVER_ID}; 106 private static final int MAILBOX_ID_COLUMNS_ID = 0; 107 private static final int MAILBOX_ID_COLUMNS_SERVER_ID = 1; 108 private static final int MAILBOX_ID_COLUMNS_PARENT_SERVER_ID = 2; 109 110 /** Projection used for changed parents during parent/child fixup. */ 111 private static final String[] FIXUP_PARENT_PROJECTION = 112 { MailboxColumns.ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.HIERARCHICAL_NAME, 113 MailboxColumns.FLAGS }; 114 private static final int FIXUP_PARENT_ID_COLUMN = 0; 115 private static final int FIXUP_PARENT_DISPLAY_NAME_COLUMN = 1; 116 private static final int FIXUP_PARENT_HIERARCHICAL_NAME_COLUMN = 2; 117 private static final int FIXUP_PARENT_FLAGS_COLUMN = 3; 118 119 /** Projection used for changed children during parent/child fixup. */ 120 private static final String[] FIXUP_CHILD_PROJECTION = 121 { MailboxColumns.ID, MailboxColumns.DISPLAY_NAME }; 122 private static final int FIXUP_CHILD_ID_COLUMN = 0; 123 private static final int FIXUP_CHILD_DISPLAY_NAME_COLUMN = 1; 124 125 /** Flags that are set or cleared when a mailbox's child status changes. */ 126 private static final int HAS_CHILDREN_FLAGS = 127 Mailbox.FLAG_HAS_CHILDREN | Mailbox.FLAG_CHILDREN_VISIBLE; 128 129 /** Mailbox.NO_MAILBOX, as a string (convenience since this is used in several places). */ 130 private static final String NO_MAILBOX_STRING = Long.toString(Mailbox.NO_MAILBOX); 131 132 @VisibleForTesting 133 long mAccountId; 134 @VisibleForTesting 135 String mAccountIdAsString; 136 @VisibleForTesting 137 boolean mInUnitTest = false; 138 139 private String[] mBindArguments = new String[2]; 140 141 /** List of pending operations to send as a batch to the content provider. */ 142 private ArrayList<ContentProviderOperation> mOperations = 143 new ArrayList<ContentProviderOperation>(); 144 /** Indicates whether this sync is an initial FolderSync. */ 145 private boolean mInitialSync; 146 /** List of folder server ids whose children changed with this sync. */ 147 private ArrayList<String> mParentFixupsNeeded = new ArrayList<String>(); 148 /** Indicates whether the sync response provided a different sync key than we had. */ 149 private boolean mSyncKeyChanged = false; 150 151 // If true, we only care about status (this is true when validating an account) and ignore 152 // other data 153 private final boolean mStatusOnly; 154 155 private boolean mOutboxCreated = false; 156 157 private static final ContentValues UNINITIALIZED_PARENT_KEY = new ContentValues(); 158 159 { 160 UNINITIALIZED_PARENT_KEY.put(MailboxColumns.PARENT_KEY, Mailbox.PARENT_KEY_UNINITIALIZED); 161 } 162 163 public FolderSyncParser(final Context context, final ContentResolver resolver, 164 final InputStream in, final Account account, final boolean statusOnly) 165 throws IOException { 166 super(context, resolver, in, null, account); 167 mAccountId = mAccount.mId; 168 mAccountIdAsString = Long.toString(mAccountId); 169 mStatusOnly = statusOnly; 170 } 171 172 public FolderSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException { 173 this(in, adapter, false); 174 } 175 176 public FolderSyncParser(InputStream in, AbstractSyncAdapter adapter, boolean statusOnly) 177 throws IOException { 178 super(in, adapter); 179 mAccountId = mAccount.mId; 180 mAccountIdAsString = Long.toString(mAccountId); 181 mStatusOnly = statusOnly; 182 } 183 184 @Override 185 public boolean parse() throws IOException, CommandStatusException { 186 int status; 187 boolean res = false; 188 boolean resetFolders = false; 189 mInitialSync = (mAccount.mSyncKey == null) || "0".equals(mAccount.mSyncKey); 190 if (mInitialSync) { 191 // We're resyncing all folders for this account, so nuke any existing ones. 192 mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_ACCOUNT_KEY, 193 new String[] {mAccountIdAsString}); 194 } 195 if (nextTag(START_DOCUMENT) != Tags.FOLDER_FOLDER_SYNC) 196 throw new EasParserException(); 197 while (nextTag(START_DOCUMENT) != END_DOCUMENT) { 198 if (tag == Tags.FOLDER_STATUS) { 199 status = getValueInt(); 200 // Do a sanity check on the account here; if we have any duplicated folders, we'll 201 // act as though we have a bad folder sync key (wipe/reload mailboxes) 202 // Note: The ContentValues isn't used, but no point creating a new one 203 int dupes = 0; 204 if (mAccountId > 0) { 205 dupes = mContentResolver.update( 206 ContentUris.withAppendedId(EmailContent.ACCOUNT_CHECK_URI, mAccountId), 207 UNINITIALIZED_PARENT_KEY, null, null); 208 } 209 if (dupes > 0) { 210 LogUtils.w(TAG, "Duplicate mailboxes found for account %d: %d", mAccountId, 211 dupes); 212 status = Eas.FOLDER_STATUS_INVALID_KEY; 213 } 214 if (status != Eas.FOLDER_STATUS_OK) { 215 // If the account hasn't been saved, this is a validation attempt, so we don't 216 // try reloading the folder list... 217 if (CommandStatus.isDeniedAccess(status) || 218 CommandStatus.isNeedsProvisioning(status) || 219 (mAccount.mId == Account.NOT_SAVED)) { 220 throw new CommandStatusException(status); 221 // Note that we need to catch both old-style (Eas.FOLDER_STATUS_INVALID_KEY) 222 // and EAS 14 style command status 223 } else if (status == Eas.FOLDER_STATUS_INVALID_KEY || 224 CommandStatus.isBadSyncKey(status)) { 225 // Delete PIM data 226 ExchangeService.deleteAccountPIMData(mContext, mAccountId); 227 // Save away any mailbox sync information that is NOT default 228 saveMailboxSyncOptions(); 229 // And only then, delete mailboxes 230 mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_ACCOUNT_KEY, 231 new String[] {mAccountIdAsString}); 232 // Reconstruct _main 233 res = true; 234 resetFolders = true; 235 // Reset the sync key and save (this should trigger the AccountObserver 236 // in ExchangeService, which will recreate the account mailbox, which 237 // will then start syncing folders, etc.) 238 mAccount.mSyncKey = "0"; 239 ContentValues cv = new ContentValues(); 240 cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey); 241 mContentResolver.update(ContentUris.withAppendedId(Account.CONTENT_URI, 242 mAccount.mId), cv, null, null); 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.i(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 easType The mailbox's type, in terms of the EAS values (NOT {@link Mailbox}'s type 390 * values). 391 * @throws IOException 392 */ 393 private void addMailboxOp(final String name, final String serverId, 394 final String parentServerId, final int easType) throws IOException { 395 final ContentValues cv = new ContentValues(); 396 cv.put(MailboxColumns.DISPLAY_NAME, name); 397 cv.put(MailboxColumns.SERVER_ID, serverId); 398 final String parentId; 399 if (parentServerId.equals("0")) { 400 parentId = NO_MAILBOX_STRING; 401 cv.put(Mailbox.PARENT_KEY, Mailbox.NO_MAILBOX); 402 } else { 403 parentId = parentServerId; 404 mParentFixupsNeeded.add(parentId); 405 } 406 cv.put(MailboxColumns.PARENT_SERVER_ID, parentId); 407 cv.put(MailboxColumns.ACCOUNT_KEY, mAccountId); 408 final int mailboxType; 409 final boolean shouldSync; 410 switch (easType) { 411 case INBOX_TYPE: 412 mailboxType = Mailbox.TYPE_INBOX; 413 shouldSync = true; 414 break; 415 case CONTACTS_TYPE: 416 mailboxType = Mailbox.TYPE_CONTACTS; 417 shouldSync = true; 418 break; 419 case OUTBOX_TYPE: 420 // TYPE_OUTBOX mailboxes are known by ExchangeService to sync whenever they 421 // aren't empty. The value of mSyncFrequency is ignored for this kind of 422 // mailbox. 423 mailboxType = Mailbox.TYPE_OUTBOX; 424 shouldSync = false; 425 mOutboxCreated = true; 426 break; 427 case SENT_TYPE: 428 mailboxType = Mailbox.TYPE_SENT; 429 shouldSync = false; 430 break; 431 case DRAFTS_TYPE: 432 mailboxType = Mailbox.TYPE_DRAFTS; 433 shouldSync = false; 434 break; 435 case DELETED_TYPE: 436 mailboxType = Mailbox.TYPE_TRASH; 437 shouldSync = false; 438 break; 439 case CALENDAR_TYPE: 440 mailboxType = Mailbox.TYPE_CALENDAR; 441 shouldSync = true; 442 break; 443 default: 444 mailboxType = Mailbox.TYPE_MAIL; 445 shouldSync = false; 446 } 447 cv.put(MailboxColumns.TYPE, mailboxType); 448 cv.put(MailboxColumns.SYNC_INTERVAL, shouldSync ? 1 : 0); 449 450 // Set basic flags 451 int flags = 0; 452 if (mailboxType <= Mailbox.TYPE_NOT_EMAIL) { 453 flags |= Mailbox.FLAG_HOLDS_MAIL + Mailbox.FLAG_SUPPORTS_SETTINGS; 454 } 455 // Outbox, Drafts, and Sent don't allow mail to be moved to them 456 if (mailboxType == Mailbox.TYPE_MAIL || mailboxType == Mailbox.TYPE_TRASH || 457 mailboxType == Mailbox.TYPE_JUNK || mailboxType == Mailbox.TYPE_INBOX) { 458 flags |= Mailbox.FLAG_ACCEPTS_MOVED_MAIL; 459 } 460 cv.put(MailboxColumns.FLAGS, flags); 461 462 // Make boxes like Contacts and Calendar invisible in the folder list 463 cv.put(MailboxColumns.FLAG_VISIBLE, (mailboxType < Mailbox.TYPE_NOT_EMAIL)); 464 465 mOperations.add( 466 ContentProviderOperation.newInsert(Mailbox.CONTENT_URI).withValues(cv).build()); 467 } 468 469 /** 470 * Add the appropriate {@link ContentProviderOperation} to {@link #mOperations} for an Add 471 * change in the FolderSync response. 472 * @throws IOException 473 */ 474 private void addParser() throws IOException { 475 String name = null; 476 String serverId = null; 477 String parentId = null; 478 int type = 0; 479 480 while (nextTag(Tags.FOLDER_ADD) != END) { 481 switch (tag) { 482 case Tags.FOLDER_DISPLAY_NAME: { 483 name = getValue(); 484 break; 485 } 486 case Tags.FOLDER_TYPE: { 487 type = getValueInt(); 488 break; 489 } 490 case Tags.FOLDER_PARENT_ID: { 491 parentId = getValue(); 492 break; 493 } 494 case Tags.FOLDER_SERVER_ID: { 495 serverId = getValue(); 496 break; 497 } 498 default: 499 skipTag(); 500 } 501 } 502 503 if (VALID_EAS_FOLDER_TYPES.contains(type)) { 504 addMailboxOp(name, serverId, parentId, type); 505 } 506 } 507 508 /** 509 * Add the appropriate {@link ContentProviderOperation} to {@link #mOperations} for an Update 510 * change in the FolderSync response. 511 * @throws IOException 512 */ 513 private void updateParser() throws IOException { 514 String serverId = null; 515 String displayName = null; 516 String parentId = null; 517 while (nextTag(Tags.FOLDER_UPDATE) != END) { 518 switch (tag) { 519 case Tags.FOLDER_SERVER_ID: 520 serverId = getValue(); 521 break; 522 case Tags.FOLDER_DISPLAY_NAME: 523 displayName = getValue(); 524 break; 525 case Tags.FOLDER_PARENT_ID: 526 parentId = getValue(); 527 break; 528 default: 529 skipTag(); 530 break; 531 } 532 } 533 // We'll make a change if one of parentId or displayName are specified 534 // serverId is required, but let's be careful just the same 535 if (serverId != null && (displayName != null || parentId != null)) { 536 final Cursor c = getServerIdCursor(serverId); 537 try { 538 // If we find the mailbox (using serverId), make the change 539 if (c.moveToFirst()) { 540 LogUtils.i(TAG, "Updating %s", serverId); 541 final ContentValues cv = new ContentValues(); 542 // Store the new parent key. 543 cv.put(Mailbox.PARENT_SERVER_ID, parentId); 544 // Fix up old and new parents, as needed 545 if (!TextUtils.isEmpty(parentId)) { 546 mParentFixupsNeeded.add(parentId); 547 } else { 548 cv.put(Mailbox.PARENT_KEY, Mailbox.NO_MAILBOX); 549 } 550 final String oldParentId = c.getString(MAILBOX_ID_COLUMNS_PARENT_SERVER_ID); 551 if (!TextUtils.isEmpty(oldParentId)) { 552 mParentFixupsNeeded.add(oldParentId); 553 } 554 // Set display name if we've got one 555 if (displayName != null) { 556 cv.put(Mailbox.DISPLAY_NAME, displayName); 557 } 558 mOperations.add(ContentProviderOperation.newUpdate( 559 ContentUris.withAppendedId(Mailbox.CONTENT_URI, 560 c.getLong(MAILBOX_ID_COLUMNS_ID))).withValues(cv).build()); 561 } 562 } finally { 563 c.close(); 564 } 565 } 566 } 567 568 /** 569 * Handle the Changes element of the FolderSync response. This is the container for Add, Delete, 570 * and Update elements. 571 * @throws IOException 572 */ 573 private void changesParser() throws IOException { 574 while (nextTag(Tags.FOLDER_CHANGES) != END) { 575 if (tag == Tags.FOLDER_ADD) { 576 addParser(); 577 } else if (tag == Tags.FOLDER_DELETE) { 578 deleteParser(); 579 } else if (tag == Tags.FOLDER_UPDATE) { 580 updateParser(); 581 } else if (tag == Tags.FOLDER_COUNT) { 582 // TODO: Maybe we can make use of this count somehow. 583 getValueInt(); 584 } else 585 skipTag(); 586 } 587 } 588 589 /** 590 * Commit the contents of {@link #mOperations} to the content provider. 591 * @throws IOException 592 */ 593 private void flushOperations() throws IOException { 594 if (mOperations.isEmpty()) { 595 return; 596 } 597 // Execute the batch; throw IOExceptions if this fails, hoping the issue isn't repeatable 598 // If it IS repeatable, there's no good result, since the folder list will be invalid 599 try { 600 mContentResolver.applyBatch(EmailContent.AUTHORITY, mOperations); 601 } catch (final RemoteException e) { 602 LogUtils.e(TAG, "RemoteException in commit"); 603 throw new IOException("RemoteException in commit"); 604 } catch (final OperationApplicationException e) { 605 LogUtils.e(TAG, "OperationApplicationException in commit"); 606 throw new IOException("OperationApplicationException in commit"); 607 } 608 mOperations.clear(); 609 } 610 611 /** 612 * Fix folder data for any folders whose parent or children changed during this sync. 613 * Unfortunately this cannot be done in the same pass as the actual sync: newly synced folders 614 * lack ids until they're committed to the content provider, so we can't set the parentKey 615 * for their children. 616 * During parsing, we only track the parents who have changed. We need to do a query for 617 * children anyway (to determine whether a parent still has any) so it's simpler to not bother 618 * tracking which folders have had their parents changed. 619 * TODO: Figure out if we can avoid the two-pass. 620 * @throws IOException 621 */ 622 private void doParentFixups() throws IOException { 623 if (mParentFixupsNeeded.isEmpty()) { 624 return; 625 } 626 627 // These objects will be used in every loop iteration, so create them here for efficiency 628 // and just reset the values inside the loop as necessary. 629 final String[] bindArguments = new String[2]; 630 bindArguments[1] = mAccountIdAsString; 631 final ContentValues cv = new ContentValues(2); 632 633 for (final String parentServerId : mParentFixupsNeeded) { 634 // Get info about this parent. 635 bindArguments[0] = parentServerId; 636 final Cursor parentCursor = mContentResolver.query(Mailbox.CONTENT_URI, 637 FIXUP_PARENT_PROJECTION, WHERE_SERVER_ID_AND_ACCOUNT, bindArguments, null); 638 if (parentCursor == null) { 639 // TODO: Error handling. 640 continue; 641 } 642 final long parentId; 643 final String parentHierarchicalName; 644 final int parentFlags; 645 try { 646 if (parentCursor.moveToFirst()) { 647 parentId = parentCursor.getLong(FIXUP_PARENT_ID_COLUMN); 648 final String hierarchicalName = parentCursor.getString( 649 FIXUP_PARENT_HIERARCHICAL_NAME_COLUMN); 650 if (hierarchicalName != null) { 651 parentHierarchicalName = hierarchicalName; 652 } else { 653 parentHierarchicalName = parentCursor.getString( 654 FIXUP_PARENT_DISPLAY_NAME_COLUMN); 655 } 656 parentFlags = parentCursor.getInt(FIXUP_PARENT_FLAGS_COLUMN); 657 } else { 658 // TODO: Error handling. 659 continue; 660 } 661 } finally { 662 parentCursor.close(); 663 } 664 665 // Fix any children for this parent. 666 final Cursor childCursor = mContentResolver.query(Mailbox.CONTENT_URI, 667 FIXUP_CHILD_PROJECTION, WHERE_PARENT_SERVER_ID_AND_ACCOUNT, bindArguments, 668 null); 669 boolean hasChildren = false; 670 if (childCursor != null) { 671 try { 672 // Clear the results of the last iteration. 673 cv.clear(); 674 // All children in this loop share the same parentId. 675 cv.put(MailboxColumns.PARENT_KEY, parentId); 676 while (childCursor.moveToNext()) { 677 final long childId = childCursor.getLong(FIXUP_CHILD_ID_COLUMN); 678 final String childName = 679 childCursor.getString(FIXUP_CHILD_DISPLAY_NAME_COLUMN); 680 cv.put(MailboxColumns.HIERARCHICAL_NAME, 681 parentHierarchicalName + "/" + childName); 682 mOperations.add(ContentProviderOperation.newUpdate( 683 ContentUris.withAppendedId(Mailbox.CONTENT_URI, childId)). 684 withValues(cv).build()); 685 hasChildren = true; 686 } 687 } finally { 688 childCursor.close(); 689 } 690 } 691 692 // Fix the parent's flags based on whether it now has children. 693 final int newFlags; 694 695 if (hasChildren) { 696 newFlags = parentFlags | HAS_CHILDREN_FLAGS; 697 } else { 698 newFlags = parentFlags & ~HAS_CHILDREN_FLAGS; 699 } 700 if (newFlags != parentFlags) { 701 cv.clear(); 702 cv.put(MailboxColumns.FLAGS, newFlags); 703 mOperations.add(ContentProviderOperation.newUpdate(ContentUris.withAppendedId( 704 Mailbox.CONTENT_URI, parentId)).withValues(cv).build()); 705 } 706 } 707 708 flushOperations(); 709 } 710 711 @Override 712 public void commandsParser() throws IOException { 713 } 714 715 @Override 716 public void commit() throws IOException { 717 // Set the account sync key. 718 if (mSyncKeyChanged) { 719 final ContentValues cv = new ContentValues(1); 720 cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey); 721 mOperations.add( 722 ContentProviderOperation.newUpdate(mAccount.getUri()).withValues(cv).build()); 723 } 724 725 // If this is the initial sync, and we didn't get an Outbox, make one. 726 if (mInitialSync && !mOutboxCreated) { 727 addMailboxOp(Mailbox.getSystemMailboxName(mContext, Mailbox.TYPE_OUTBOX), "0", "0", 728 OUTBOX_TYPE); 729 } 730 731 // Send all operations so far. 732 flushOperations(); 733 734 // Now that new mailboxes are committed, let's do parent fixups. 735 doParentFixups(); 736 737 // Look for sync issues and its children and delete them 738 // I'm not aware of any other way to deal with this properly 739 mBindArguments[0] = "Sync Issues"; 740 mBindArguments[1] = mAccountIdAsString; 741 Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, 742 MAILBOX_ID_COLUMNS_PROJECTION, WHERE_DISPLAY_NAME_AND_ACCOUNT, 743 mBindArguments, null); 744 String parentServerId = null; 745 long id = 0; 746 try { 747 if (c.moveToFirst()) { 748 id = c.getLong(MAILBOX_ID_COLUMNS_ID); 749 parentServerId = c.getString(MAILBOX_ID_COLUMNS_SERVER_ID); 750 } 751 } finally { 752 c.close(); 753 } 754 if (parentServerId != null) { 755 mContentResolver.delete(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id), 756 null, null); 757 mBindArguments[0] = parentServerId; 758 mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_PARENT_SERVER_ID_AND_ACCOUNT, 759 mBindArguments); 760 } 761 762 // If we have saved options, restore them now 763 if (mInitialSync) { 764 restoreMailboxSyncOptions(); 765 } 766 } 767 768 @Override 769 public void responsesParser() throws IOException { 770 } 771 772} 773