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