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