/* * 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 java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import com.android.email.provider.EmailProvider; import com.android.exchange.Eas; import com.android.exchange.EasSyncService; import com.android.exchange.MockParserStream; import com.android.exchange.SyncManager; import com.android.exchange.EmailContent.Account; import com.android.exchange.EmailContent.Mailbox; import com.android.exchange.EmailContent.MailboxColumns; import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.OperationApplicationException; import android.database.Cursor; import android.os.RemoteException; import android.util.Log; /** * Parse the result of a FolderSync command * * Handles the addition, deletion, and changes to folders in the user's Exchange account. **/ public class EasFolderSyncParser extends EasParser { public static final String TAG = "FolderSyncParser"; // These are defined by the EAS protocol public static final int USER_FOLDER_TYPE = 1; public static final int INBOX_TYPE = 2; public static final int DRAFTS_TYPE = 3; public static final int DELETED_TYPE = 4; public static final int SENT_TYPE = 5; public static final int OUTBOX_TYPE = 6; public static final int TASKS_TYPE = 7; public static final int CALENDAR_TYPE = 8; public static final int CONTACTS_TYPE = 9; public static final int NOTES_TYPE = 10; public static final int JOURNAL_TYPE = 11; public static final int USER_MAILBOX_TYPE = 12; public static final List mValidFolderTypes = Arrays.asList(INBOX_TYPE, DRAFTS_TYPE, DELETED_TYPE, SENT_TYPE, OUTBOX_TYPE, USER_MAILBOX_TYPE, CALENDAR_TYPE, CONTACTS_TYPE); private static final String WHERE_SERVER_ID_AND_ACCOUNT = MailboxColumns.SERVER_ID + "=? and " + MailboxColumns.ACCOUNT_KEY + "=?"; private static final String WHERE_DISPLAY_NAME_AND_ACCOUNT = MailboxColumns.DISPLAY_NAME + "=? and " + MailboxColumns.ACCOUNT_KEY + "=?"; private static final String WHERE_PARENT_SERVER_ID_AND_ACCOUNT = MailboxColumns.PARENT_SERVER_ID +"=? and " + MailboxColumns.ACCOUNT_KEY + "=?"; private static final String[] MAILBOX_ID_COLUMNS_PROJECTION = new String[] {MailboxColumns.ID, MailboxColumns.SERVER_ID}; private Account mAccount; private long mAccountId; private String mAccountIdAsString; private EasSyncService mService; private Context mContext; private ContentResolver mContentResolver; private MockParserStream mMock = null; private String[] mBindArguments = new String[2]; public EasFolderSyncParser(InputStream in, EasSyncService service) throws IOException { super(in); mService = service; mAccount = service.mAccount; mAccountId = mAccount.mId; mAccountIdAsString = Long.toString(mAccountId); mContext = service.mContext; mContentResolver = mContext.getContentResolver(); if (in instanceof MockParserStream) { mMock = (MockParserStream)in; } setDebug(true); } public boolean parse() throws IOException { int status; boolean res = false; if (nextTag(START_DOCUMENT) != EasTags.FOLDER_FOLDER_SYNC) throw new IOException(); while (nextTag(START_DOCUMENT) != END_DOCUMENT) { if (tag == EasTags.FOLDER_STATUS) { status = getValueInt(); if (status != Eas.FOLDER_STATUS_OK) { mService.errorLog("FolderSync failed: " + status); if (status == Eas.FOLDER_STATUS_INVALID_KEY) { mAccount.mSyncKey = "0"; mService.errorLog("Bad sync key; RESET and delete all folders"); mContentResolver.delete(Mailbox.CONTENT_URI, MailboxColumns.ACCOUNT_KEY + '=' + mAccountId, null); // Stop existing syncs and reconstruct _main SyncManager.folderListReloaded(mAccountId); res = 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 mService.errorLog("Throwing IOException; will retry later"); throw new IOException(); } } } else if (tag == EasTags.FOLDER_SYNC_KEY) { mAccount.mSyncKey = getValue(); mService.userLog("New Account SyncKey: " + mAccount.mSyncKey); } else if (tag == EasTags.FOLDER_CHANGES) { changesParser(); } else skipTag(); } mAccount.saveOrUpdate(mContext); return res; } private Cursor getServerIdCursor(String serverId) { mBindArguments[0] = serverId; mBindArguments[1] = mAccountIdAsString; return mContentResolver.query(Mailbox.CONTENT_URI, new String[] {MailboxColumns.ID}, WHERE_SERVER_ID_AND_ACCOUNT, mBindArguments, null); } public void deleteParser(ArrayList ops) throws IOException { while (nextTag(EasTags.SYNC_DELETE) != END) { switch (tag) { case EasTags.FOLDER_SERVER_ID: String serverId = getValue(); // Find the mailbox in this account with the given serverId Cursor c = getServerIdCursor(serverId); try { if (c.moveToFirst()) { mService.userLog("Deleting " + serverId); ops.add(ContentProviderOperation.newDelete( ContentUris.withAppendedId(Mailbox.CONTENT_URI, c.getLong(0))).build()); } } finally { c.close(); } break; default: skipTag(); } } } public void addParser(ArrayList ops) throws IOException { String name = null; String serverId = null; String parentId = null; int type = 0; while (nextTag(EasTags.FOLDER_ADD) != END) { switch (tag) { case EasTags.FOLDER_DISPLAY_NAME: { name = getValue(); break; } case EasTags.FOLDER_TYPE: { type = getValueInt(); break; } case EasTags.FOLDER_PARENT_ID: { parentId = getValue(); break; } case EasTags.FOLDER_SERVER_ID: { serverId = getValue(); break; } default: skipTag(); } } if (mValidFolderTypes.contains(type)) { Mailbox m = new Mailbox(); m.mDisplayName = name; m.mServerId = serverId; m.mAccountKey = mAccountId; m.mType = Mailbox.TYPE_MAIL; m.mSyncFrequency = Account.CHECK_INTERVAL_NEVER; switch (type) { case INBOX_TYPE: m.mSyncFrequency = Account.CHECK_INTERVAL_PUSH; m.mType = Mailbox.TYPE_INBOX; break; case OUTBOX_TYPE: m.mSyncFrequency = Account.CHECK_INTERVAL_NEVER; // TYPE_OUTBOX mailboxes are known by SyncManager to sync whenever they aren't // empty. The value of mSyncFrequency is ignored for this kind of mailbox. m.mType = Mailbox.TYPE_OUTBOX; break; case SENT_TYPE: m.mType = Mailbox.TYPE_SENT; break; case DRAFTS_TYPE: m.mType = Mailbox.TYPE_DRAFTS; break; case DELETED_TYPE: m.mType = Mailbox.TYPE_TRASH; break; case CALENDAR_TYPE: m.mType = Mailbox.TYPE_CALENDAR; // TODO This could be push, depending on settings // For now, no sync, since it's not yet implemented break; case CONTACTS_TYPE: m.mType = Mailbox.TYPE_CONTACTS; // TODO Frequency below should depend on settings m.mSyncFrequency = Account.CHECK_INTERVAL_PUSH; break; } // Make boxes like Contacts and Calendar invisible in the folder list m.mFlagVisible = (m.mType < Mailbox.TYPE_NOT_EMAIL); if (!parentId.equals("0")) { m.mParentServerId = parentId; } Log.v(TAG, "Adding mailbox: " + m.mDisplayName); ops.add(ContentProviderOperation .newInsert(Mailbox.CONTENT_URI).withValues(m.toContentValues()).build()); } return; } public void changesParser() throws IOException { // Keep track of new boxes, deleted boxes, updated boxes ArrayList ops = new ArrayList(); while (nextTag(EasTags.FOLDER_CHANGES) != END) { // TODO Handle FOLDER_CHANGE and FOLDER_DELETE if (tag == EasTags.FOLDER_ADD) { addParser(ops); } else if (tag == EasTags.FOLDER_DELETE) { deleteParser(ops); } else if (tag == EasTags.FOLDER_COUNT) { getValueInt(); } else skipTag(); } // The mock stream is used for junit tests, so that the parsing code can be tested // separately from the provider code. // TODO Change tests to not require this; remove references to the mock stream if (mMock != null) { mMock.setResult(null); return; } // Create the new mailboxes in a single batch operation if (!ops.isEmpty()) { mService.userLog("Applying " + ops.size() + " mailbox operations."); // Then, we create an update for the account (most importantly, updating the syncKey) ops.add(ContentProviderOperation.newUpdate( ContentUris.withAppendedId(Account.CONTENT_URI, mAccountId)).withValues( mAccount.toContentValues()).build()); // Finally, we execute the batch try { mService.mContext.getContentResolver() .applyBatch(EmailProvider.EMAIL_AUTHORITY, ops); mService.userLog("New syncKey: " + mAccount.mSyncKey); } catch (RemoteException e) { // There is nothing to be done here; fail by returning null } catch (OperationApplicationException e) { // There is nothing to be done here; fail by returning null } // 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(0); parentServerId = c.getString(1); } } 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); } } } }