blob: 6db4f6019fbefbe4b42b66d99bfebb0157479d10 [file] [log] [blame]
/*
* Copyright (C) 2008-2009 Marc Blank
* Licensed to The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.exchange.adapter;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.os.RemoteException;
import android.os.TransactionTooLargeException;
import android.text.TextUtils;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.AccountColumns;
import com.android.emailcommon.provider.EmailContent.MailboxColumns;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.SyncWindow;
import com.android.emailcommon.utility.AttachmentUtilities;
import com.android.exchange.CommandStatusException;
import com.android.exchange.CommandStatusException.CommandStatus;
import com.android.exchange.Eas;
import com.android.exchange.eas.EasSyncContacts;
import com.android.exchange.eas.EasSyncCalendar;
import com.android.mail.utils.LogUtils;
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Parse the result of a FolderSync command
*
* Handles the addition, deletion, and changes to folders in the user's Exchange account.
**/
public class FolderSyncParser extends AbstractSyncParser {
public static final String TAG = "FolderSyncParser";
/**
* Mapping from EAS type values to {@link Mailbox} types.
* See http://msdn.microsoft.com/en-us/library/gg650877(v=exchg.80).aspx for the list of EAS
* type values.
* If an EAS type is not in the map, or is inserted with a value of {@link Mailbox#TYPE_NONE},
* then we don't support that type and we should ignore it.
* TODO: Maybe we should store the mailbox anyway, otherwise it'll be annoying to upgrade.
*/
private static final SparseIntArray MAILBOX_TYPE_MAP;
static {
MAILBOX_TYPE_MAP = new SparseIntArray(11);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_GENERIC, Mailbox.TYPE_MAIL);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_INBOX, Mailbox.TYPE_INBOX);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_DRAFTS, Mailbox.TYPE_DRAFTS);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_DELETED, Mailbox.TYPE_TRASH);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_SENT, Mailbox.TYPE_SENT);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_OUTBOX, Mailbox.TYPE_OUTBOX);
//MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_TASKS, Mailbox.TYPE_TASKS);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_CALENDAR, Mailbox.TYPE_CALENDAR);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_CONTACTS, Mailbox.TYPE_CONTACTS);
//MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_NOTES, Mailbox.TYPE_NONE);
//MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_JOURNAL, Mailbox.TYPE_NONE);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_MAIL, Mailbox.TYPE_MAIL);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_CALENDAR, Mailbox.TYPE_CALENDAR);
MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_CONTACTS, Mailbox.TYPE_CONTACTS);
//MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_TASKS, Mailbox.TYPE_TASKS);
//MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_JOURNAL, Mailbox.TYPE_NONE);
//MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_USER_NOTES, Mailbox.TYPE_NONE);
//MAILBOX_TYPE_MAP.put(Eas.MAILBOX_TYPE_UNKNOWN, Mailbox.TYPE_NONE);
//MAILBOX_TYPE_MAP.put(MAILBOX_TYPE_RECIPIENT_INFORMATION_CACHE, Mailbox.TYPE_NONE);
}
/** Content selection for all mailboxes belonging to an account. */
private static final String WHERE_ACCOUNT_KEY = MailboxColumns.ACCOUNT_KEY + "=?";
/**
* Content selection to find a specific mailbox by server id. Since server ids aren't unique
* across all accounts, this must also check account id.
*/
private static final String WHERE_SERVER_ID_AND_ACCOUNT = MailboxColumns.SERVER_ID + "=? and " +
MailboxColumns.ACCOUNT_KEY + "=?";
/**
* Content selection to find a specific mailbox by display name and account.
*/
private static final String WHERE_DISPLAY_NAME_AND_ACCOUNT = MailboxColumns.DISPLAY_NAME +
"=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
/**
* Content selection to find children by parent's server id. Since server ids aren't unique
* across accounts, this must also use account id.
*/
private static final String WHERE_PARENT_SERVER_ID_AND_ACCOUNT =
MailboxColumns.PARENT_SERVER_ID +"=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
/** Projection used when fetching a Mailbox's ids. */
private static final String[] MAILBOX_ID_COLUMNS_PROJECTION =
new String[] {MailboxColumns.ID, MailboxColumns.SERVER_ID, MailboxColumns.PARENT_SERVER_ID};
private static final int MAILBOX_ID_COLUMNS_ID = 0;
private static final int MAILBOX_ID_COLUMNS_SERVER_ID = 1;
private static final int MAILBOX_ID_COLUMNS_PARENT_SERVER_ID = 2;
/** Projection used for changed parents during parent/child fixup. */
private static final String[] FIXUP_PARENT_PROJECTION =
{ MailboxColumns.ID, MailboxColumns.FLAGS };
private static final int FIXUP_PARENT_ID_COLUMN = 0;
private static final int FIXUP_PARENT_FLAGS_COLUMN = 1;
/** Projection used for changed children during parent/child fixup. */
private static final String[] FIXUP_CHILD_PROJECTION =
{ MailboxColumns.ID };
private static final int FIXUP_CHILD_ID_COLUMN = 0;
/** Flags that are set or cleared when a mailbox's child status changes. */
private static final int HAS_CHILDREN_FLAGS =
Mailbox.FLAG_HAS_CHILDREN | Mailbox.FLAG_CHILDREN_VISIBLE;
/** Mailbox.NO_MAILBOX, as a string (convenience since this is used in several places). */
private static final String NO_MAILBOX_STRING = Long.toString(Mailbox.NO_MAILBOX);
@VisibleForTesting
long mAccountId;
@VisibleForTesting
String mAccountIdAsString;
private final String[] mBindArguments = new String[2];
/** List of pending operations to send as a batch to the content provider. */
private final ArrayList<ContentProviderOperation> mOperations =
new ArrayList<ContentProviderOperation>();
/** Indicates whether this sync is an initial FolderSync. */
private boolean mInitialSync;
/** List of folder server ids whose children changed with this sync. */
private final Set<String> mParentFixupsNeeded = new LinkedHashSet<String>();
/** Indicates whether the sync response provided a different sync key than we had. */
private boolean mSyncKeyChanged = false;
// If true, we only care about status (this is true when validating an account) and ignore
// other data
private final boolean mStatusOnly;
/** Map of folder types that have been created during this sync. */
private final SparseBooleanArray mCreatedFolderTypes =
new SparseBooleanArray(Mailbox.REQUIRED_FOLDER_TYPES.length);
private static final ContentValues UNINITIALIZED_PARENT_KEY = new ContentValues();
static {
UNINITIALIZED_PARENT_KEY.put(MailboxColumns.PARENT_KEY, Mailbox.PARENT_KEY_UNINITIALIZED);
}
public FolderSyncParser(final Context context, final ContentResolver resolver,
final InputStream in, final Account account, final boolean statusOnly)
throws IOException {
super(context, resolver, in, null, account);
mAccountId = mAccount.mId;
mAccountIdAsString = Long.toString(mAccountId);
mStatusOnly = statusOnly;
}
public FolderSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException {
this(in, adapter, false);
}
public FolderSyncParser(InputStream in, AbstractSyncAdapter adapter, boolean statusOnly)
throws IOException {
super(in, adapter);
mAccountId = mAccount.mId;
mAccountIdAsString = Long.toString(mAccountId);
mStatusOnly = statusOnly;
}
@Override
public boolean parse() throws IOException, CommandStatusException {
int status;
boolean res = false;
boolean resetFolders = false;
mInitialSync = (mAccount.mSyncKey == null) || "0".equals(mAccount.mSyncKey);
if (mInitialSync) {
// We're resyncing all folders for this account, so nuke any existing ones.
mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_ACCOUNT_KEY,
new String[] {mAccountIdAsString});
}
if (nextTag(START_DOCUMENT) != Tags.FOLDER_FOLDER_SYNC)
throw new EasParserException();
while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
if (tag == Tags.FOLDER_STATUS) {
status = getValueInt();
// Do a sanity check on the account here; if we have any duplicated folders, we'll
// act as though we have a bad folder sync key (wipe/reload mailboxes)
// Note: The ContentValues isn't used, but no point creating a new one
int dupes = 0;
if (mAccountId > 0) {
dupes = mContentResolver.update(
ContentUris.withAppendedId(EmailContent.ACCOUNT_CHECK_URI, mAccountId),
UNINITIALIZED_PARENT_KEY, null, null);
}
if (dupes > 0) {
LogUtils.w(TAG, "Duplicate mailboxes found for account %d: %d", mAccountId,
dupes);
status = Eas.FOLDER_STATUS_INVALID_KEY;
}
if (status != Eas.FOLDER_STATUS_OK) {
// If the account hasn't been saved, this is a validation attempt, so we don't
// try reloading the folder list...
if (CommandStatus.isDeniedAccess(status) ||
CommandStatus.isNeedsProvisioning(status) ||
(mAccount.mId == Account.NOT_SAVED)) {
LogUtils.e(LogUtils.TAG, "FolderSync: Unknown status: " + status);
throw new CommandStatusException(status);
// Note that we need to catch both old-style (Eas.FOLDER_STATUS_INVALID_KEY)
// and EAS 14 style command status
} else if (status == Eas.FOLDER_STATUS_INVALID_KEY ||
CommandStatus.isBadSyncKey(status)) {
wipe();
// Reconstruct _main
res = true;
resetFolders = true;
} else {
// Other errors are at the server, so let's throw an error that will
// cause this sync to be retried at a later time
throw new EasParserException("Folder status error");
}
}
} else if (tag == Tags.FOLDER_SYNC_KEY) {
final String newKey = getValue();
if (newKey != null && !resetFolders) {
mSyncKeyChanged = !newKey.equals(mAccount.mSyncKey);
mAccount.mSyncKey = newKey;
}
} else if (tag == Tags.FOLDER_CHANGES) {
if (mStatusOnly) return res;
changesParser();
} else
skipTag();
}
if (!mStatusOnly) {
commit();
}
return res;
}
/**
* Get a cursor with folder ids for a specific folder.
* @param serverId The server id for the folder we are interested in.
* @return A cursor for the folder specified by serverId for this account.
*/
private Cursor getServerIdCursor(final String serverId) {
mBindArguments[0] = serverId;
mBindArguments[1] = mAccountIdAsString;
return mContentResolver.query(Mailbox.CONTENT_URI, MAILBOX_ID_COLUMNS_PROJECTION,
WHERE_SERVER_ID_AND_ACCOUNT, mBindArguments, null);
}
/**
* Add the appropriate {@link ContentProviderOperation} to {@link #mOperations} for a Delete
* change in the FolderSync response.
* @throws IOException
*/
private void deleteParser() throws IOException {
while (nextTag(Tags.FOLDER_DELETE) != END) {
switch (tag) {
case Tags.FOLDER_SERVER_ID:
final String serverId = getValue();
// Find the mailbox in this account with the given serverId
final Cursor c = getServerIdCursor(serverId);
try {
if (c.moveToFirst()) {
LogUtils.d(TAG, "Deleting %s", serverId);
final long mailboxId = c.getLong(MAILBOX_ID_COLUMNS_ID);
mOperations.add(ContentProviderOperation.newDelete(
ContentUris.withAppendedId(Mailbox.CONTENT_URI,
mailboxId)).build());
AttachmentUtilities.deleteAllMailboxAttachmentFiles(mContext,
mAccountId, mailboxId);
final String parentId =
c.getString(MAILBOX_ID_COLUMNS_PARENT_SERVER_ID);
if (!TextUtils.isEmpty(parentId)) {
mParentFixupsNeeded.add(parentId);
}
}
} finally {
c.close();
}
break;
default:
skipTag();
}
}
}
private static class SyncOptions {
private final int mInterval;
private final int mLookback;
private SyncOptions(int interval, int lookback) {
mInterval = interval;
mLookback = lookback;
}
}
private static final String MAILBOX_STATE_SELECTION =
MailboxColumns.ACCOUNT_KEY + "=? AND (" + MailboxColumns.SYNC_INTERVAL + "!=" +
Account.CHECK_INTERVAL_NEVER + " OR " + Mailbox.SYNC_LOOKBACK + "!=" +
SyncWindow.SYNC_WINDOW_ACCOUNT + ")";
private static final String[] MAILBOX_STATE_PROJECTION = new String[] {
MailboxColumns.SERVER_ID, MailboxColumns.SYNC_INTERVAL, MailboxColumns.SYNC_LOOKBACK};
private static final int MAILBOX_STATE_SERVER_ID = 0;
private static final int MAILBOX_STATE_INTERVAL = 1;
private static final int MAILBOX_STATE_LOOKBACK = 2;
@VisibleForTesting
final HashMap<String, SyncOptions> mSyncOptionsMap = new HashMap<String, SyncOptions>();
/**
* For every mailbox in this account that has a non-default interval or lookback, save those
* values.
*/
@VisibleForTesting
void saveMailboxSyncOptions() {
// Shouldn't be necessary, but...
mSyncOptionsMap.clear();
Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, MAILBOX_STATE_PROJECTION,
MAILBOX_STATE_SELECTION, new String[] {mAccountIdAsString}, null);
if (c != null) {
try {
while (c.moveToNext()) {
mSyncOptionsMap.put(c.getString(MAILBOX_STATE_SERVER_ID),
new SyncOptions(c.getInt(MAILBOX_STATE_INTERVAL),
c.getInt(MAILBOX_STATE_LOOKBACK)));
}
} finally {
c.close();
}
}
}
/**
* For every set of saved mailbox sync options, try to find and restore those values
*/
@VisibleForTesting
void restoreMailboxSyncOptions() {
try {
ContentValues cv = new ContentValues();
mBindArguments[1] = mAccountIdAsString;
for (String serverId: mSyncOptionsMap.keySet()) {
SyncOptions options = mSyncOptionsMap.get(serverId);
cv.put(MailboxColumns.SYNC_INTERVAL, options.mInterval);
cv.put(MailboxColumns.SYNC_LOOKBACK, options.mLookback);
mBindArguments[0] = serverId;
// If we match account and server id, set the sync options
mContentResolver.update(Mailbox.CONTENT_URI, cv, WHERE_SERVER_ID_AND_ACCOUNT,
mBindArguments);
}
} finally {
mSyncOptionsMap.clear();
}
}
/**
* Add a {@link ContentProviderOperation} to {@link #mOperations} to add a mailbox.
* @param name The new mailbox's name.
* @param serverId The new mailbox's server id.
* @param parentServerId The server id of the new mailbox's parent ("0" if none).
* @param mailboxType The mailbox's type, which is one of the values defined in {@link Mailbox}.
* @param fromServer Whether this mailbox was synced from server (as opposed to local-only).
* @throws IOException
*/
private void addMailboxOp(final String name, final String serverId,
final String parentServerId, final int mailboxType, final boolean fromServer)
throws IOException {
final ContentValues cv = new ContentValues(10);
cv.put(MailboxColumns.DISPLAY_NAME, name);
if (fromServer) {
cv.put(MailboxColumns.SERVER_ID, serverId);
final String parentId;
if (parentServerId.equals("0")) {
parentId = NO_MAILBOX_STRING;
cv.put(MailboxColumns.PARENT_KEY, Mailbox.NO_MAILBOX);
} else {
parentId = parentServerId;
mParentFixupsNeeded.add(parentId);
}
cv.put(MailboxColumns.PARENT_SERVER_ID, parentId);
} else {
cv.put(MailboxColumns.SERVER_ID, "");
cv.put(MailboxColumns.PARENT_KEY, Mailbox.NO_MAILBOX);
cv.put(MailboxColumns.PARENT_SERVER_ID, NO_MAILBOX_STRING);
cv.put(MailboxColumns.TOTAL_COUNT, -1);
}
cv.put(MailboxColumns.ACCOUNT_KEY, mAccountId);
cv.put(MailboxColumns.TYPE, mailboxType);
final boolean shouldSync = fromServer && Mailbox.getDefaultSyncStateForType(mailboxType);
cv.put(MailboxColumns.SYNC_INTERVAL, shouldSync ? 1 : 0);
// Set basic flags
int flags = 0;
if (mailboxType <= Mailbox.TYPE_NOT_EMAIL) {
flags |= Mailbox.FLAG_HOLDS_MAIL + Mailbox.FLAG_SUPPORTS_SETTINGS;
}
// Outbox, Drafts, and Sent don't allow mail to be moved to them
if (mailboxType == Mailbox.TYPE_MAIL || mailboxType == Mailbox.TYPE_TRASH ||
mailboxType == Mailbox.TYPE_JUNK || mailboxType == Mailbox.TYPE_INBOX) {
flags |= Mailbox.FLAG_ACCEPTS_MOVED_MAIL;
}
cv.put(MailboxColumns.FLAGS, flags);
// Make boxes like Contacts and Calendar invisible in the folder list
cv.put(MailboxColumns.FLAG_VISIBLE, (mailboxType < Mailbox.TYPE_NOT_EMAIL));
mOperations.add(
ContentProviderOperation.newInsert(Mailbox.CONTENT_URI).withValues(cv).build());
mCreatedFolderTypes.put(mailboxType, true);
}
/**
* Add the appropriate {@link ContentProviderOperation} to {@link #mOperations} for an Add
* change in the FolderSync response.
* @throws IOException
*/
private void addParser() throws IOException {
String name = null;
String serverId = null;
String parentId = null;
int type = 0;
while (nextTag(Tags.FOLDER_ADD) != END) {
switch (tag) {
case Tags.FOLDER_DISPLAY_NAME: {
name = getValue();
break;
}
case Tags.FOLDER_TYPE: {
type = getValueInt();
break;
}
case Tags.FOLDER_PARENT_ID: {
parentId = getValue();
break;
}
case Tags.FOLDER_SERVER_ID: {
serverId = getValue();
break;
}
default:
skipTag();
}
}
if (name != null && serverId != null && parentId != null) {
final int mailboxType = MAILBOX_TYPE_MAP.get(type, Mailbox.TYPE_NONE);
if (mailboxType != Mailbox.TYPE_NONE) {
if (type == Eas.MAILBOX_TYPE_CALENDAR && !name.contains(mAccount.mEmailAddress)) {
name = mAccount.mEmailAddress;
}
addMailboxOp(name, serverId, parentId, mailboxType, true);
}
}
}
/**
* Add the appropriate {@link ContentProviderOperation} to {@link #mOperations} for an Update
* change in the FolderSync response.
* @throws IOException
*/
private void updateParser() throws IOException {
String serverId = null;
String displayName = null;
String parentId = null;
while (nextTag(Tags.FOLDER_UPDATE) != END) {
switch (tag) {
case Tags.FOLDER_SERVER_ID:
serverId = getValue();
break;
case Tags.FOLDER_DISPLAY_NAME:
displayName = getValue();
break;
case Tags.FOLDER_PARENT_ID:
parentId = getValue();
break;
default:
skipTag();
break;
}
}
// We'll make a change if one of parentId or displayName are specified
// serverId is required, but let's be careful just the same
if (serverId != null && (displayName != null || parentId != null)) {
final Cursor c = getServerIdCursor(serverId);
try {
// If we find the mailbox (using serverId), make the change
if (c.moveToFirst()) {
LogUtils.d(TAG, "Updating %s", serverId);
final ContentValues cv = new ContentValues();
// Store the new parent key.
cv.put(Mailbox.PARENT_SERVER_ID, parentId);
// Fix up old and new parents, as needed
if (!TextUtils.isEmpty(parentId)) {
mParentFixupsNeeded.add(parentId);
} else {
cv.put(Mailbox.PARENT_KEY, Mailbox.NO_MAILBOX);
}
final String oldParentId = c.getString(MAILBOX_ID_COLUMNS_PARENT_SERVER_ID);
if (!TextUtils.isEmpty(oldParentId)) {
mParentFixupsNeeded.add(oldParentId);
}
// Set display name if we've got one
if (displayName != null) {
cv.put(Mailbox.DISPLAY_NAME, displayName);
}
mOperations.add(ContentProviderOperation.newUpdate(
ContentUris.withAppendedId(Mailbox.CONTENT_URI,
c.getLong(MAILBOX_ID_COLUMNS_ID))).withValues(cv).build());
}
} finally {
c.close();
}
}
}
/**
* Handle the Changes element of the FolderSync response. This is the container for Add, Delete,
* and Update elements.
* @throws IOException
*/
private void changesParser() throws IOException {
while (nextTag(Tags.FOLDER_CHANGES) != END) {
if (tag == Tags.FOLDER_ADD) {
addParser();
} else if (tag == Tags.FOLDER_DELETE) {
deleteParser();
} else if (tag == Tags.FOLDER_UPDATE) {
updateParser();
} else if (tag == Tags.FOLDER_COUNT) {
// TODO: Maybe we can make use of this count somehow.
getValueInt();
} else
skipTag();
}
}
/**
* Commit the contents of {@link #mOperations} to the content provider.
* @throws IOException
*/
private void flushOperations() throws IOException {
if (mOperations.isEmpty()) {
return;
}
int transactionSize = mOperations.size();
final ArrayList<ContentProviderOperation> subOps =
new ArrayList<ContentProviderOperation>(transactionSize);
while (!mOperations.isEmpty()) {
subOps.clear();
// If the original transaction is split into smaller transactions,
// need to ensure the final transaction doesn't overrun the array.
if (transactionSize > mOperations.size()) {
transactionSize = mOperations.size();
}
subOps.addAll(mOperations.subList(0, transactionSize));
// Try to apply the ops. If the transaction is too large, split it in half and try again
// If some other error happens then throw an IOException up the stack.
try {
mContentResolver.applyBatch(EmailContent.AUTHORITY, subOps);
mOperations.removeAll(subOps);
} catch (final TransactionTooLargeException e) {
// If the transaction is too large, try splitting it.
if (transactionSize == 1) {
LogUtils.e(TAG, "Single operation transaction too large");
throw new IOException("Single operation transaction too large");
}
LogUtils.d(TAG, "Transaction operation count %d too large, halving...",
transactionSize);
transactionSize = transactionSize / 2;
if (transactionSize < 1) {
transactionSize = 1;
}
} catch (final RemoteException e) {
LogUtils.e(TAG, "RemoteException in commit");
throw new IOException("RemoteException in commit");
} catch (final OperationApplicationException e) {
LogUtils.e(TAG, "OperationApplicationException in commit");
throw new IOException("OperationApplicationException in commit");
}
}
mOperations.clear();
}
/**
* Fix folder data for any folders whose parent or children changed during this sync.
* Unfortunately this cannot be done in the same pass as the actual sync: newly synced folders
* lack ids until they're committed to the content provider, so we can't set the parentKey
* for their children.
* During parsing, we only track the parents who have changed. We need to do a query for
* children anyway (to determine whether a parent still has any) so it's simpler to not bother
* tracking which folders have had their parents changed.
* TODO: Figure out if we can avoid the two-pass.
* @throws IOException
*/
private void doParentFixups() throws IOException {
if (mParentFixupsNeeded.isEmpty()) {
return;
}
// These objects will be used in every loop iteration, so create them here for efficiency
// and just reset the values inside the loop as necessary.
final String[] bindArguments = new String[2];
bindArguments[1] = mAccountIdAsString;
final ContentValues cv = new ContentValues(1);
for (final String parentServerId : mParentFixupsNeeded) {
// Get info about this parent.
bindArguments[0] = parentServerId;
final Cursor parentCursor = mContentResolver.query(Mailbox.CONTENT_URI,
FIXUP_PARENT_PROJECTION, WHERE_SERVER_ID_AND_ACCOUNT, bindArguments, null);
if (parentCursor == null) {
// TODO: Error handling.
continue;
}
final long parentId;
final int parentFlags;
try {
if (parentCursor.moveToFirst()) {
parentId = parentCursor.getLong(FIXUP_PARENT_ID_COLUMN);
parentFlags = parentCursor.getInt(FIXUP_PARENT_FLAGS_COLUMN);
} else {
// TODO: Error handling.
continue;
}
} finally {
parentCursor.close();
}
// Fix any children for this parent.
final Cursor childCursor = mContentResolver.query(Mailbox.CONTENT_URI,
FIXUP_CHILD_PROJECTION, WHERE_PARENT_SERVER_ID_AND_ACCOUNT, bindArguments,
null);
boolean hasChildren = false;
if (childCursor != null) {
try {
// Clear the results of the last iteration.
cv.clear();
// All children in this loop share the same parentId.
cv.put(MailboxColumns.PARENT_KEY, parentId);
while (childCursor.moveToNext()) {
final long childId = childCursor.getLong(FIXUP_CHILD_ID_COLUMN);
mOperations.add(ContentProviderOperation.newUpdate(
ContentUris.withAppendedId(Mailbox.CONTENT_URI, childId)).
withValues(cv).build());
hasChildren = true;
}
} finally {
childCursor.close();
}
}
// Fix the parent's flags based on whether it now has children.
final int newFlags;
if (hasChildren) {
newFlags = parentFlags | HAS_CHILDREN_FLAGS;
} else {
newFlags = parentFlags & ~HAS_CHILDREN_FLAGS;
}
if (newFlags != parentFlags) {
cv.clear();
cv.put(MailboxColumns.FLAGS, newFlags);
mOperations.add(ContentProviderOperation.newUpdate(ContentUris.withAppendedId(
Mailbox.CONTENT_URI, parentId)).withValues(cv).build());
}
flushOperations();
}
}
@Override
public void commandsParser() throws IOException {
}
@Override
public void commit() throws IOException {
// Set the account sync key.
if (mSyncKeyChanged) {
final ContentValues cv = new ContentValues(1);
cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
mOperations.add(
ContentProviderOperation.newUpdate(mAccount.getUri()).withValues(cv).build());
}
// If this is the initial sync, make sure we have all the required folder types.
if (mInitialSync) {
for (final int requiredType : Mailbox.REQUIRED_FOLDER_TYPES) {
if (!mCreatedFolderTypes.get(requiredType)) {
addMailboxOp(Mailbox.getSystemMailboxName(mContext, requiredType),
null, null, requiredType, false);
}
}
}
// Send all operations so far.
flushOperations();
// Now that new mailboxes are committed, let's do parent fixups.
doParentFixups();
// Look for sync issues and its children and delete them
// I'm not aware of any other way to deal with this properly
mBindArguments[0] = "Sync Issues";
mBindArguments[1] = mAccountIdAsString;
Cursor c = mContentResolver.query(Mailbox.CONTENT_URI,
MAILBOX_ID_COLUMNS_PROJECTION, WHERE_DISPLAY_NAME_AND_ACCOUNT,
mBindArguments, null);
String parentServerId = null;
long id = 0;
try {
if (c.moveToFirst()) {
id = c.getLong(MAILBOX_ID_COLUMNS_ID);
parentServerId = c.getString(MAILBOX_ID_COLUMNS_SERVER_ID);
}
} finally {
c.close();
}
if (parentServerId != null) {
mContentResolver.delete(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id),
null, null);
mBindArguments[0] = parentServerId;
mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_PARENT_SERVER_ID_AND_ACCOUNT,
mBindArguments);
}
// If we have saved options, restore them now
if (mInitialSync) {
restoreMailboxSyncOptions();
}
}
@Override
public void responsesParser() throws IOException {
}
@Override
protected void wipe() {
EasSyncCalendar.wipeAccountFromContentProvider(mContext,
mAccount.mEmailAddress);
EasSyncContacts.wipeAccountFromContentProvider(mContext,
mAccount.mEmailAddress);
// Save away any mailbox sync information that is NOT default
saveMailboxSyncOptions();
// And only then, delete mailboxes
mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_ACCOUNT_KEY,
new String[] {mAccountIdAsString});
// Reset the sync key and save.
mAccount.mSyncKey = "0";
ContentValues cv = new ContentValues();
cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
mContentResolver.update(ContentUris.withAppendedId(Account.CONTENT_URI,
mAccount.mId), cv, null, null);
}
}