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