Home | History | Annotate | Download | only in adapter
      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 com.android.email.provider.AttachmentProvider;
     21 import com.android.email.provider.EmailContent;
     22 import com.android.email.provider.EmailProvider;
     23 import com.android.email.provider.EmailContent.AccountColumns;
     24 import com.android.email.provider.EmailContent.Mailbox;
     25 import com.android.email.provider.EmailContent.MailboxColumns;
     26 import com.android.exchange.Eas;
     27 import com.android.exchange.MockParserStream;
     28 import com.android.exchange.SyncManager;
     29 
     30 import android.content.ContentProviderOperation;
     31 import android.content.ContentUris;
     32 import android.content.ContentValues;
     33 import android.content.OperationApplicationException;
     34 import android.database.Cursor;
     35 import android.os.RemoteException;
     36 
     37 import java.io.IOException;
     38 import java.io.InputStream;
     39 import java.util.ArrayList;
     40 import java.util.Arrays;
     41 import java.util.List;
     42 
     43 /**
     44  * Parse the result of a FolderSync command
     45  *
     46  * Handles the addition, deletion, and changes to folders in the user's Exchange account.
     47  **/
     48 
     49 public class FolderSyncParser extends AbstractSyncParser {
     50 
     51     public static final String TAG = "FolderSyncParser";
     52 
     53     // These are defined by the EAS protocol
     54     public static final int USER_FOLDER_TYPE = 1;
     55     public static final int INBOX_TYPE = 2;
     56     public static final int DRAFTS_TYPE = 3;
     57     public static final int DELETED_TYPE = 4;
     58     public static final int SENT_TYPE = 5;
     59     public static final int OUTBOX_TYPE = 6;
     60     public static final int TASKS_TYPE = 7;
     61     public static final int CALENDAR_TYPE = 8;
     62     public static final int CONTACTS_TYPE = 9;
     63     public static final int NOTES_TYPE = 10;
     64     public static final int JOURNAL_TYPE = 11;
     65     public static final int USER_MAILBOX_TYPE = 12;
     66 
     67     public static final List<Integer> mValidFolderTypes = Arrays.asList(INBOX_TYPE, DRAFTS_TYPE,
     68             DELETED_TYPE, SENT_TYPE, OUTBOX_TYPE, USER_MAILBOX_TYPE, CALENDAR_TYPE, CONTACTS_TYPE);
     69 
     70     public static final String ALL_BUT_ACCOUNT_MAILBOX = MailboxColumns.ACCOUNT_KEY + "=? and " +
     71         MailboxColumns.TYPE + "!=" + Mailbox.TYPE_EAS_ACCOUNT_MAILBOX;
     72 
     73    private static final String WHERE_SERVER_ID_AND_ACCOUNT = MailboxColumns.SERVER_ID + "=? and " +
     74         MailboxColumns.ACCOUNT_KEY + "=?";
     75 
     76     private static final String WHERE_DISPLAY_NAME_AND_ACCOUNT = MailboxColumns.DISPLAY_NAME +
     77         "=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
     78 
     79     private static final String WHERE_PARENT_SERVER_ID_AND_ACCOUNT =
     80         MailboxColumns.PARENT_SERVER_ID +"=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
     81 
     82     private static final String[] MAILBOX_ID_COLUMNS_PROJECTION =
     83         new String[] {MailboxColumns.ID, MailboxColumns.SERVER_ID};
     84 
     85     private long mAccountId;
     86     private String mAccountIdAsString;
     87     private MockParserStream mMock = null;
     88     private String[] mBindArguments = new String[2];
     89 
     90     public FolderSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException {
     91         super(in, adapter);
     92         mAccountId = mAccount.mId;
     93         mAccountIdAsString = Long.toString(mAccountId);
     94         if (in instanceof MockParserStream) {
     95             mMock = (MockParserStream)in;
     96         }
     97     }
     98 
     99     @Override
    100     public boolean parse() throws IOException {
    101         int status;
    102         boolean res = false;
    103         boolean resetFolders = false;
    104         if (nextTag(START_DOCUMENT) != Tags.FOLDER_FOLDER_SYNC)
    105             throw new EasParserException();
    106         while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
    107             if (tag == Tags.FOLDER_STATUS) {
    108                 status = getValueInt();
    109                 if (status != Eas.FOLDER_STATUS_OK) {
    110                     mService.errorLog("FolderSync failed: " + status);
    111                     if (status == Eas.FOLDER_STATUS_INVALID_KEY) {
    112                         mAccount.mSyncKey = "0";
    113                         mService.errorLog("Bad sync key; RESET and delete all folders");
    114                         mContentResolver.delete(Mailbox.CONTENT_URI, ALL_BUT_ACCOUNT_MAILBOX,
    115                                 new String[] {Long.toString(mAccountId)});
    116                         // Stop existing syncs and reconstruct _main
    117                         SyncManager.stopNonAccountMailboxSyncsForAccount(mAccountId);
    118                         res = true;
    119                         resetFolders = true;
    120                     } else {
    121                         // Other errors are at the server, so let's throw an error that will
    122                         // cause this sync to be retried at a later time
    123                         mService.errorLog("Throwing IOException; will retry later");
    124                         throw new EasParserException("Folder status error");
    125                     }
    126                 }
    127             } else if (tag == Tags.FOLDER_SYNC_KEY) {
    128                 mAccount.mSyncKey = getValue();
    129                 userLog("New Account SyncKey: ", mAccount.mSyncKey);
    130             } else if (tag == Tags.FOLDER_CHANGES) {
    131                 changesParser();
    132             } else
    133                 skipTag();
    134         }
    135         synchronized (mService.getSynchronizer()) {
    136             if (!mService.isStopped() || resetFolders) {
    137                 ContentValues cv = new ContentValues();
    138                 cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
    139                 mAccount.update(mContext, cv);
    140                 userLog("Leaving FolderSyncParser with Account syncKey=", mAccount.mSyncKey);
    141             }
    142         }
    143         return res;
    144     }
    145 
    146     private Cursor getServerIdCursor(String serverId) {
    147         mBindArguments[0] = serverId;
    148         mBindArguments[1] = mAccountIdAsString;
    149         return mContentResolver.query(Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION,
    150                 WHERE_SERVER_ID_AND_ACCOUNT, mBindArguments, null);
    151     }
    152 
    153     public void deleteParser(ArrayList<ContentProviderOperation> ops) throws IOException {
    154         while (nextTag(Tags.FOLDER_DELETE) != END) {
    155             switch (tag) {
    156                 case Tags.FOLDER_SERVER_ID:
    157                     String serverId = getValue();
    158                     // Find the mailbox in this account with the given serverId
    159                     Cursor c = getServerIdCursor(serverId);
    160                     try {
    161                         if (c.moveToFirst()) {
    162                             userLog("Deleting ", serverId);
    163                             ops.add(ContentProviderOperation.newDelete(
    164                                     ContentUris.withAppendedId(Mailbox.CONTENT_URI,
    165                                             c.getLong(0))).build());
    166                             AttachmentProvider.deleteAllMailboxAttachmentFiles(mContext,
    167                                     mAccountId, mMailbox.mId);
    168                         }
    169                     } finally {
    170                         c.close();
    171                     }
    172                     break;
    173                 default:
    174                     skipTag();
    175             }
    176         }
    177     }
    178 
    179     public void addParser(ArrayList<ContentProviderOperation> ops) throws IOException {
    180         String name = null;
    181         String serverId = null;
    182         String parentId = null;
    183         int type = 0;
    184 
    185         while (nextTag(Tags.FOLDER_ADD) != END) {
    186             switch (tag) {
    187                 case Tags.FOLDER_DISPLAY_NAME: {
    188                     name = getValue();
    189                     break;
    190                 }
    191                 case Tags.FOLDER_TYPE: {
    192                     type = getValueInt();
    193                     break;
    194                 }
    195                 case Tags.FOLDER_PARENT_ID: {
    196                     parentId = getValue();
    197                     break;
    198                 }
    199                 case Tags.FOLDER_SERVER_ID: {
    200                     serverId = getValue();
    201                     break;
    202                 }
    203                 default:
    204                     skipTag();
    205             }
    206         }
    207         if (mValidFolderTypes.contains(type)) {
    208             Mailbox m = new Mailbox();
    209             m.mDisplayName = name;
    210             m.mServerId = serverId;
    211             m.mAccountKey = mAccountId;
    212             m.mType = Mailbox.TYPE_MAIL;
    213             // Note that all mailboxes default to checking "never" (i.e. manual sync only)
    214             // We set specific intervals for inbox, contacts, and (eventually) calendar
    215             m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER;
    216             switch (type) {
    217                 case INBOX_TYPE:
    218                     m.mType = Mailbox.TYPE_INBOX;
    219                     m.mSyncInterval = mAccount.mSyncInterval;
    220                     break;
    221                 case CONTACTS_TYPE:
    222                     m.mType = Mailbox.TYPE_CONTACTS;
    223                     m.mSyncInterval = mAccount.mSyncInterval;
    224                     break;
    225                 case OUTBOX_TYPE:
    226                     // TYPE_OUTBOX mailboxes are known by SyncManager to sync whenever they aren't
    227                     // empty.  The value of mSyncFrequency is ignored for this kind of mailbox.
    228                     m.mType = Mailbox.TYPE_OUTBOX;
    229                     break;
    230                 case SENT_TYPE:
    231                     m.mType = Mailbox.TYPE_SENT;
    232                     break;
    233                 case DRAFTS_TYPE:
    234                     m.mType = Mailbox.TYPE_DRAFTS;
    235                     break;
    236                 case DELETED_TYPE:
    237                     m.mType = Mailbox.TYPE_TRASH;
    238                     break;
    239                 case CALENDAR_TYPE:
    240                     m.mType = Mailbox.TYPE_CALENDAR;
    241                     m.mSyncInterval = mAccount.mSyncInterval;
    242                     break;
    243             }
    244 
    245             // Make boxes like Contacts and Calendar invisible in the folder list
    246             m.mFlagVisible = (m.mType < Mailbox.TYPE_NOT_EMAIL);
    247 
    248             if (!parentId.equals("0")) {
    249                 m.mParentServerId = parentId;
    250             }
    251 
    252             userLog("Adding mailbox: ", m.mDisplayName);
    253             ops.add(ContentProviderOperation
    254                     .newInsert(Mailbox.CONTENT_URI).withValues(m.toContentValues()).build());
    255         }
    256 
    257         return;
    258     }
    259 
    260     public void updateParser(ArrayList<ContentProviderOperation> ops) throws IOException {
    261         String serverId = null;
    262         String displayName = null;
    263         String parentId = null;
    264         while (nextTag(Tags.FOLDER_UPDATE) != END) {
    265             switch (tag) {
    266                 case Tags.FOLDER_SERVER_ID:
    267                     serverId = getValue();
    268                     break;
    269                 case Tags.FOLDER_DISPLAY_NAME:
    270                     displayName = getValue();
    271                     break;
    272                 case Tags.FOLDER_PARENT_ID:
    273                     parentId = getValue();
    274                     break;
    275                 default:
    276                     skipTag();
    277                     break;
    278             }
    279         }
    280         // We'll make a change if one of parentId or displayName are specified
    281         // serverId is required, but let's be careful just the same
    282         if (serverId != null && (displayName != null || parentId != null)) {
    283             Cursor c = getServerIdCursor(serverId);
    284             try {
    285                 // If we find the mailbox (using serverId), make the change
    286                 if (c.moveToFirst()) {
    287                     userLog("Updating ", serverId);
    288                     ContentValues cv = new ContentValues();
    289                     if (displayName != null) {
    290                         cv.put(Mailbox.DISPLAY_NAME, displayName);
    291                     }
    292                     if (parentId != null) {
    293                         cv.put(Mailbox.PARENT_SERVER_ID, parentId);
    294                     }
    295                     ops.add(ContentProviderOperation.newUpdate(
    296                             ContentUris.withAppendedId(Mailbox.CONTENT_URI,
    297                                     c.getLong(0))).withValues(cv).build());
    298                 }
    299             } finally {
    300                 c.close();
    301             }
    302         }
    303     }
    304 
    305     public void changesParser() throws IOException {
    306         // Keep track of new boxes, deleted boxes, updated boxes
    307         ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
    308 
    309         while (nextTag(Tags.FOLDER_CHANGES) != END) {
    310             if (tag == Tags.FOLDER_ADD) {
    311                 addParser(ops);
    312             } else if (tag == Tags.FOLDER_DELETE) {
    313                 deleteParser(ops);
    314             } else if (tag == Tags.FOLDER_UPDATE) {
    315                 updateParser(ops);
    316             } else if (tag == Tags.FOLDER_COUNT) {
    317                 getValueInt();
    318             } else
    319                 skipTag();
    320         }
    321 
    322         // The mock stream is used for junit tests, so that the parsing code can be tested
    323         // separately from the provider code.
    324         // TODO Change tests to not require this; remove references to the mock stream
    325         if (mMock != null) {
    326             mMock.setResult(null);
    327             return;
    328         }
    329 
    330         // Create the new mailboxes in a single batch operation
    331         // Don't save any data if the service has been stopped
    332         synchronized (mService.getSynchronizer()) {
    333             if (!ops.isEmpty() && !mService.isStopped()) {
    334                 userLog("Applying ", ops.size(), " mailbox operations.");
    335 
    336                 // Execute the batch
    337                 try {
    338                     mContentResolver.applyBatch(EmailProvider.EMAIL_AUTHORITY, ops);
    339                     userLog("New Account SyncKey: ", mAccount.mSyncKey);
    340                 } catch (RemoteException e) {
    341                     // There is nothing to be done here; fail by returning null
    342                 } catch (OperationApplicationException e) {
    343                     // There is nothing to be done here; fail by returning null
    344                 }
    345 
    346                 // Look for sync issues and its children and delete them
    347                 // I'm not aware of any other way to deal with this properly
    348                 mBindArguments[0] = "Sync Issues";
    349                 mBindArguments[1] = mAccountIdAsString;
    350                 Cursor c = mContentResolver.query(Mailbox.CONTENT_URI,
    351                         MAILBOX_ID_COLUMNS_PROJECTION, WHERE_DISPLAY_NAME_AND_ACCOUNT,
    352                         mBindArguments, null);
    353                 String parentServerId = null;
    354                 long id = 0;
    355                 try {
    356                     if (c.moveToFirst()) {
    357                         id = c.getLong(0);
    358                         parentServerId = c.getString(1);
    359                     }
    360                 } finally {
    361                     c.close();
    362                 }
    363                 if (parentServerId != null) {
    364                     mContentResolver.delete(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id),
    365                             null, null);
    366                     mBindArguments[0] = parentServerId;
    367                     mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_PARENT_SERVER_ID_AND_ACCOUNT,
    368                             mBindArguments);
    369                 }
    370             }
    371         }
    372     }
    373 
    374     /**
    375      * Not needed for FolderSync parsing; everything is done within changesParser
    376      */
    377     @Override
    378     public void commandsParser() throws IOException {
    379     }
    380 
    381     /**
    382      * We don't need to implement commit() because all operations take place atomically within
    383      * changesParser
    384      */
    385     @Override
    386     public void commit() throws IOException {
    387     }
    388 
    389     @Override
    390     public void wipe() {
    391     }
    392 
    393     @Override
    394     public void responsesParser() throws IOException {
    395     }
    396 
    397 }
    398