| /* |
| * 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.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.ExchangeService; |
| 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; |
| |
| /** |
| * 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(1, Mailbox.TYPE_MAIL); // User-created folder (generic) |
| MAILBOX_TYPE_MAP.put(2, Mailbox.TYPE_INBOX); // Default Inbox folder |
| MAILBOX_TYPE_MAP.put(3, Mailbox.TYPE_DRAFTS); // Default Drafts folder |
| MAILBOX_TYPE_MAP.put(4, Mailbox.TYPE_TRASH); // Default Deleted Items folder |
| MAILBOX_TYPE_MAP.put(5, Mailbox.TYPE_SENT); // Default Sent Items folder |
| MAILBOX_TYPE_MAP.put(6, Mailbox.TYPE_OUTBOX); // Default Outbox folder |
| //MAILBOX_TYPE_MAP.put(7, Mailbox.TYPE_TASKS); // Default Tasks folder |
| MAILBOX_TYPE_MAP.put(8, Mailbox.TYPE_CALENDAR); // Default Calendar folder |
| MAILBOX_TYPE_MAP.put(9, Mailbox.TYPE_CONTACTS); // Default Contacts folder |
| //MAILBOX_TYPE_MAP.put(10, Mailbox.TYPE_NONE); // Default Notes folder |
| //MAILBOX_TYPE_MAP.put(11, Mailbox.TYPE_NONE); // Default Journal folder |
| MAILBOX_TYPE_MAP.put(12, Mailbox.TYPE_MAIL); // User-created Mail folder |
| MAILBOX_TYPE_MAP.put(13, Mailbox.TYPE_CALENDAR); // User-created Calendar folder |
| MAILBOX_TYPE_MAP.put(14, Mailbox.TYPE_CONTACTS); // User-created Contacts folder |
| //MAILBOX_TYPE_MAP.put(15, Mailbox.TYPE_TASKS); // User-created Tasks folder |
| //MAILBOX_TYPE_MAP.put(16, Mailbox.TYPE_NONE); // User-created Journal folder |
| //MAILBOX_TYPE_MAP.put(17, Mailbox.TYPE_NONE); // User-created Notes folder |
| //MAILBOX_TYPE_MAP.put(18, Mailbox.TYPE_NONE); // Unknown folder type |
| //MAILBOX_TYPE_MAP.put(19, Mailbox.TYPE_NONE); // Recipient information cache |
| } |
| |
| /** 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.DISPLAY_NAME, MailboxColumns.HIERARCHICAL_NAME, |
| MailboxColumns.FLAGS }; |
| private static final int FIXUP_PARENT_ID_COLUMN = 0; |
| private static final int FIXUP_PARENT_DISPLAY_NAME_COLUMN = 1; |
| private static final int FIXUP_PARENT_HIERARCHICAL_NAME_COLUMN = 2; |
| private static final int FIXUP_PARENT_FLAGS_COLUMN = 3; |
| |
| /** Projection used for changed children during parent/child fixup. */ |
| private static final String[] FIXUP_CHILD_PROJECTION = |
| { MailboxColumns.ID, MailboxColumns.DISPLAY_NAME }; |
| private static final int FIXUP_CHILD_ID_COLUMN = 0; |
| private static final int FIXUP_CHILD_DISPLAY_NAME_COLUMN = 1; |
| |
| /** 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; |
| @VisibleForTesting |
| boolean mInUnitTest = false; |
| |
| private String[] mBindArguments = new String[2]; |
| |
| /** List of pending operations to send as a batch to the content provider. */ |
| private 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 ArrayList<String> mParentFixupsNeeded = new ArrayList<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(); |
| |
| { |
| 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)) { |
| 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)) { |
| // Delete PIM data |
| ExchangeService.deleteAccountPIMData(mContext, mAccountId); |
| // 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}); |
| // Reconstruct _main |
| res = true; |
| resetFolders = true; |
| // Reset the sync key and save (this should trigger the AccountObserver |
| // in ExchangeService, which will recreate the account mailbox, which |
| // will then start syncing folders, etc.) |
| 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); |
| } 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.i(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) { |
| 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.i(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; |
| } |
| // Execute the batch; throw IOExceptions if this fails, hoping the issue isn't repeatable |
| // If it IS repeatable, there's no good result, since the folder list will be invalid |
| try { |
| mContentResolver.applyBatch(EmailContent.AUTHORITY, mOperations); |
| } 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(2); |
| |
| 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 String parentHierarchicalName; |
| final int parentFlags; |
| try { |
| if (parentCursor.moveToFirst()) { |
| parentId = parentCursor.getLong(FIXUP_PARENT_ID_COLUMN); |
| final String hierarchicalName = parentCursor.getString( |
| FIXUP_PARENT_HIERARCHICAL_NAME_COLUMN); |
| if (hierarchicalName != null) { |
| parentHierarchicalName = hierarchicalName; |
| } else { |
| parentHierarchicalName = parentCursor.getString( |
| FIXUP_PARENT_DISPLAY_NAME_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); |
| final String childName = |
| childCursor.getString(FIXUP_CHILD_DISPLAY_NAME_COLUMN); |
| cv.put(MailboxColumns.HIERARCHICAL_NAME, |
| parentHierarchicalName + "/" + childName); |
| 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 { |
| } |
| |
| } |