Add initial support for uploading new Contacts to Exchange server

* Refactor the sync adapters to separate out parsing from commit
* Use ContactsProvider to save the SyncKey for contacts
* Fixes #2072664 and #2072456

Change-Id: I1e85c498496e83d9523489636a75f366f7fbd106
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index 62e770b..4f70c44 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -29,6 +29,7 @@
 import com.android.email.provider.EmailContent.MailboxColumns;
 import com.android.email.provider.EmailContent.Message;
 import com.android.exchange.adapter.AbstractSyncAdapter;
+import com.android.exchange.adapter.AccountSyncAdapter;
 import com.android.exchange.adapter.ContactsSyncAdapter;
 import com.android.exchange.adapter.EmailSyncAdapter;
 import com.android.exchange.adapter.FolderSyncParser;
@@ -536,7 +537,8 @@
                          InputStream is = entity.getContent();
                          // Returns true if we need to sync again
                          userLog("FolderSync, deviceId = ", mDeviceId);
-                         if (new FolderSyncParser(is, this).parse()) {
+                         if (new FolderSyncParser(is, new AccountSyncAdapter(mMailbox, this))
+                                 .parse()) {
                              continue;
                          }
                      }
@@ -901,22 +903,19 @@
             }
 
             Serializer s = new Serializer();
-            if (mailbox.mSyncKey == null) {
-                userLog("Mailbox syncKey RESET");
-                mailbox.mSyncKey = "0";
-            }
             String className = target.getCollectionName();
-            userLog("Sending ", className, " syncKey: ", mailbox.mSyncKey);
+            String syncKey = target.getSyncKey();
+            userLog("Sending ", className, " syncKey: ", syncKey);
             s.start(Tags.SYNC_SYNC)
                 .start(Tags.SYNC_COLLECTIONS)
                 .start(Tags.SYNC_COLLECTION)
                 .data(Tags.SYNC_CLASS, className)
-                .data(Tags.SYNC_SYNC_KEY, mailbox.mSyncKey)
+                .data(Tags.SYNC_SYNC_KEY, syncKey)
                 .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId)
                 .tag(Tags.SYNC_DELETES_AS_MOVES);
 
             // EAS doesn't like GetChanges if the syncKey is "0"; not documented
-            if (!mailbox.mSyncKey.equals("0")) {
+            if (!syncKey.equals("0")) {
                 s.tag(Tags.SYNC_GET_CHANGES);
             }
             s.data(Tags.SYNC_WINDOW_SIZE,
@@ -943,7 +942,7 @@
             }
 
             // Send our changes up to the server
-            target.sendLocalChanges(s, this);
+            target.sendLocalChanges(s);
 
             s.end().end().end().done();
             userLog("Sync, deviceId = ", mDeviceId);
@@ -952,8 +951,8 @@
             if (code == HttpStatus.SC_OK) {
                  InputStream is = resp.getEntity().getContent();
                 if (is != null) {
-                    moreAvailable = target.parse(is, this);
-                    target.cleanup(this);
+                    moreAvailable = target.parse(is);
+                    target.cleanup();
                 }
             } else {
                 userLog("Sync response error: ", code);
diff --git a/src/com/android/exchange/adapter/AbstractSyncAdapter.java b/src/com/android/exchange/adapter/AbstractSyncAdapter.java
index 8d9fb58..d6ad165 100644
--- a/src/com/android/exchange/adapter/AbstractSyncAdapter.java
+++ b/src/com/android/exchange/adapter/AbstractSyncAdapter.java
@@ -44,15 +44,15 @@
     public Account mAccount;
 
     // Create the data for local changes that need to be sent up to the server
-    public abstract boolean sendLocalChanges(Serializer s, EasSyncService service)
+    public abstract boolean sendLocalChanges(Serializer s)
         throws IOException;
     // Parse incoming data from the EAS server, creating, modifying, and deleting objects as
     // required through the EmailProvider
-    public abstract boolean parse(InputStream is, EasSyncService service)
+    public abstract boolean parse(InputStream is)
         throws IOException;
     // The name used to specify the collection type of the target (Email, Calendar, or Contacts)
     public abstract String getCollectionName();
-    public abstract void cleanup(EasSyncService service);
+    public abstract void cleanup();
 
     public AbstractSyncAdapter(Mailbox mailbox, EasSyncService service) {
         mMailbox = mailbox;
@@ -61,12 +61,29 @@
         mAccount = service.mAccount;
     }
 
-    void userLog(String ...strings) {
+    public void userLog(String ...strings) {
         mService.userLog(strings);
     }
 
-    void incrementChangeCount() {
+    public void incrementChangeCount() {
         mService.mChangeCount++;
     }
+
+    /**
+     * Returns the current SyncKey; override if the SyncKey is stored elsewhere (as for Contacts)
+     * @return the current SyncKey for the Mailbox
+     * @throws IOException
+     */
+    public String getSyncKey() throws IOException {
+        if (mMailbox.mSyncKey == null) {
+            userLog("Reset SyncKey to 0");
+            mMailbox.mSyncKey = "0";
+        }
+        return mMailbox.mSyncKey;
+    }
+
+    public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
+        mMailbox.mSyncKey = syncKey;
+    }
 }
 
diff --git a/src/com/android/exchange/adapter/AbstractSyncParser.java b/src/com/android/exchange/adapter/AbstractSyncParser.java
index d598c22..66d8aba 100644
--- a/src/com/android/exchange/adapter/AbstractSyncParser.java
+++ b/src/com/android/exchange/adapter/AbstractSyncParser.java
@@ -43,10 +43,12 @@
     protected Account mAccount;
     protected Context mContext;
     protected ContentResolver mContentResolver;
+    protected AbstractSyncAdapter mAdapter;
 
-    public AbstractSyncParser(InputStream in, EasSyncService _service) throws IOException {
+    public AbstractSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException {
         super(in);
-        mService = _service;
+        mAdapter = adapter;
+        mService = adapter.mService;
         mContext = mService.mContext;
         mContentResolver = mContext.getContentResolver();
         mMailbox = mService.mMailbox;
@@ -61,13 +63,15 @@
 
     /**
      * Read, parse, and act on server responses
-     * Email doesn't have any, so this isn't yet implemented anywhere.  It will become abstract,
-     * in the near future, however.
      * @throws IOException
      */
-    public void responsesParser() throws IOException {
-        // Placeholder until needed; will become an abstract method
-    }
+    public abstract void responsesParser() throws IOException;
+
+    /**
+     * Commit any changes found during parsing
+     * @throws IOException
+     */
+    public abstract void commit() throws IOException;
 
     /**
      * Delete all records of this class in this account
@@ -101,7 +105,7 @@
                     // Status = 3 means invalid sync key
                     if (status == 3) {
                         // Must delete all of the data and start over with syncKey of "0"
-                        mMailbox.mSyncKey = "0";
+                        mAdapter.setSyncKey("0", false);
                         // Make this a push box through the first sync
                         // TODO Make frequency conditional on user settings!
                         mMailbox.mSyncInterval = Mailbox.CHECK_INTERVAL_PUSH;
@@ -123,12 +127,12 @@
             } else if (tag == Tags.SYNC_MORE_AVAILABLE) {
                 moreAvailable = true;
             } else if (tag == Tags.SYNC_SYNC_KEY) {
-                if (mMailbox.mSyncKey.equals("0")) {
+                if (mAdapter.getSyncKey().equals("0")) {
                     moreAvailable = true;
                 }
                 String newKey = getValue();
                 userLog("Parsed key for ", mMailbox.mDisplayName, ": ", newKey);
-                mMailbox.mSyncKey = newKey;
+                mAdapter.setSyncKey(newKey, true);
                 // If we were pushing (i.e. auto-start), now we'll become ping-triggered
                 if (mMailbox.mSyncInterval == Mailbox.CHECK_INTERVAL_PUSH) {
                     mMailbox.mSyncInterval = Mailbox.CHECK_INTERVAL_PING;
@@ -138,15 +142,15 @@
            }
         }
 
+        // Commit any changes
+        commit();
+
         // If the sync interval has changed, or if no commands were parsed save the change
-        if (mMailbox.mSyncInterval != interval || mService.mChangeCount == 0) {
+        if (mMailbox.mSyncInterval != interval) {
             synchronized (mService.getSynchronizer()) {
                 if (!mService.isStopped()) {
                     // Make sure we save away the new syncFrequency
                     ContentValues cv = new ContentValues();
-                    if (mService.mChangeCount == 0) {
-                        cv.put(MailboxColumns.SYNC_KEY, mMailbox.mSyncKey);
-                    }
                     cv.put(MailboxColumns.SYNC_INTERVAL, mMailbox.mSyncInterval);
                     mMailbox.update(mContext, cv);
                 }
diff --git a/src/com/android/exchange/adapter/AccountSyncAdapter.java b/src/com/android/exchange/adapter/AccountSyncAdapter.java
new file mode 100644
index 0000000..2a1c7be
--- /dev/null
+++ b/src/com/android/exchange/adapter/AccountSyncAdapter.java
@@ -0,0 +1,34 @@
+package com.android.exchange.adapter;
+
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.EasSyncService;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class AccountSyncAdapter extends AbstractSyncAdapter {
+
+    public AccountSyncAdapter(Mailbox mailbox, EasSyncService service) {
+        super(mailbox, service);
+     }
+
+    @Override
+    public void cleanup() {
+    }
+
+    @Override
+    public String getCollectionName() {
+        return null;
+    }
+
+    @Override
+    public boolean parse(InputStream is) throws IOException {
+        return false;
+    }
+
+    @Override
+    public boolean sendLocalChanges(Serializer s) throws IOException {
+        return false;
+    }
+
+}
diff --git a/src/com/android/exchange/adapter/CalendarSyncAdapter.java b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
index 2c716fb..739c9e2 100644
--- a/src/com/android/exchange/adapter/CalendarSyncAdapter.java
+++ b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
@@ -39,18 +39,18 @@
     }
 
     @Override
-    public boolean sendLocalChanges(Serializer s, EasSyncService service) throws IOException {
+    public boolean sendLocalChanges(Serializer s) throws IOException {
         // TODO Auto-generated method stub
         return false;
     }
 
     @Override
-    public void cleanup(EasSyncService service) {
+    public void cleanup() {
         // TODO Auto-generated method stub
     }
 
     @Override
-    public boolean parse(InputStream is, EasSyncService service) throws IOException {
+    public boolean parse(InputStream is) throws IOException {
         // TODO Auto-generated method stub
         return false;
     }
diff --git a/src/com/android/exchange/adapter/ContactsSyncAdapter.java b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
index cb24810..f55adf0 100644
--- a/src/com/android/exchange/adapter/ContactsSyncAdapter.java
+++ b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
@@ -19,10 +19,10 @@
 
 import com.android.email.codec.binary.Base64;
 import com.android.email.provider.EmailContent.Mailbox;
-import com.android.email.provider.EmailContent.MailboxColumns;
 import com.android.exchange.Eas;
 import com.android.exchange.EasSyncService;
 
+import android.content.ContentProviderClient;
 import android.content.ContentProviderOperation;
 import android.content.ContentProviderResult;
 import android.content.ContentResolver;
@@ -37,9 +37,11 @@
 import android.net.Uri;
 import android.os.RemoteException;
 import android.provider.ContactsContract;
+import android.provider.SyncStateContract;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.Groups;
 import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.SyncState;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.CommonDataKinds.Im;
@@ -66,6 +68,7 @@
 
     private static final String TAG = "EasContactsSyncAdapter";
     private static final String SERVER_ID_SELECTION = RawContacts.SOURCE_ID + "=?";
+    private static final String CLIENT_ID_SELECTION = RawContacts.SYNC1 + "=?";
     private static final String[] ID_PROJECTION = new String[] {RawContacts._ID};
     private static final String[] GROUP_PROJECTION = new String[] {Groups.SOURCE_ID};
 
@@ -112,16 +115,71 @@
     ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
     ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
 
+    android.accounts.Account mAccountManagerAccount;
+
     public ContactsSyncAdapter(Mailbox mailbox, EasSyncService service) {
         super(mailbox, service);
     }
 
     @Override
-    public boolean parse(InputStream is, EasSyncService service) throws IOException {
-        EasContactsSyncParser p = new EasContactsSyncParser(is, service);
+    public boolean parse(InputStream is) throws IOException {
+        EasContactsSyncParser p = new EasContactsSyncParser(is, this);
         return p.parse();
     }
 
+    /**
+     * We get our SyncKey from ContactsProvider.  If there's not one, we set it to "0" (the reset
+     * state) and save that away.
+     */
+    @Override
+    public String getSyncKey() throws IOException {
+        ContentProviderClient client =
+            mService.mContentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
+        try {
+            byte[] data = SyncStateContract.Helpers.get(client,
+                    ContactsContract.SyncState.CONTENT_URI, getAccountManagerAccount());
+            if (data == null || data.length == 0) {
+                setSyncKey("0", false);
+                return "0";
+            } else {
+                String syncKey = new String(data);
+                userLog("SyncKey retrieved from ContactsProvider: " + syncKey);
+                return syncKey;
+            }
+        } catch (RemoteException e) {
+            throw new IOException("Can't get SyncKey from ContactsProvider");
+        }
+    }
+
+    /**
+     * We only need to set this when we're forced to make the SyncKey "0" (a reset).  In all other
+     * cases, the SyncKey is set within ContactOperations
+     */
+    @Override
+    public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
+        if ("0".equals(syncKey) || !inCommands) {
+            ContentProviderClient client =
+                mService.mContentResolver
+                    .acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
+            try {
+                SyncStateContract.Helpers.set(client, ContactsContract.SyncState.CONTENT_URI,
+                        getAccountManagerAccount(), syncKey.getBytes());
+                userLog("SyncKey set to ", syncKey, " in ContactsProvider");
+           } catch (RemoteException e) {
+                throw new IOException("Can't set SyncKey in ContactsProvider");
+            }
+        }
+        mMailbox.mSyncKey = syncKey;
+    }
+
+    public android.accounts.Account getAccountManagerAccount() {
+        if (mAccountManagerAccount == null) {
+            mAccountManagerAccount =
+                new android.accounts.Account(mAccount.mEmailAddress, Eas.ACCOUNT_MANAGER_TYPE);
+        }
+        return mAccountManagerAccount;
+    }
+
     // YomiFirstName, YomiLastName, and YomiCompanyName are the names of EAS fields
     // Yomi is a shortened form of yomigana, which is a Japanese phonetic rendering.
     public static final class Yomi {
@@ -211,9 +269,10 @@
         String[] mBindArgument = new String[1];
         String mMailboxIdAsString;
         Uri mAccountUri;
+        ContactOperations ops = new ContactOperations();
 
-        public EasContactsSyncParser(InputStream in, EasSyncService service) throws IOException {
-            super(in, service);
+        public EasContactsSyncParser(InputStream in, ContactsSyncAdapter adapter) throws IOException {
+            super(in, adapter);
             mAccountUri = uriWithAccount(RawContacts.CONTENT_URI);
         }
 
@@ -588,6 +647,12 @@
                     mBindArgument, null);
         }
 
+        private Cursor getClientIdCursor(String clientId) {
+            mBindArgument[0] = clientId;
+            return mContentResolver.query(mAccountUri, ID_PROJECTION, CLIENT_ID_SELECTION,
+                    mBindArgument, null);
+        }
+
         public void deleteParser(ContactOperations ops) throws IOException {
             while (nextTag(Tags.SYNC_DELETE) != END) {
                 switch (tag) {
@@ -663,7 +728,6 @@
 
         @Override
         public void commandsParser() throws IOException {
-            ContactOperations ops = new ContactOperations();
             while (nextTag(Tags.SYNC_COMMANDS) != END) {
                 if (tag == Tags.SYNC_ADD) {
                     addParser(ops);
@@ -677,6 +741,14 @@
                 } else
                     skipTag();
             }
+        }
+
+        @Override
+        public void commit() throws IOException {
+           // Save the syncKey here, using the Helper provider by Contacts provider
+            userLog("Contacts SyncKey saved as: ", mMailbox.mSyncKey);
+            ops.add(SyncStateContract.Helpers.newSetOperation(SyncState.CONTENT_URI,
+                    getAccountManagerAccount(), mMailbox.mSyncKey.getBytes()));
 
             // Execute these all at once...
             ops.execute();
@@ -694,12 +766,57 @@
                     }
                 }
             }
+        }
 
-            // Update the sync key in the database
-            userLog("Contacts SyncKey saved as: ", mMailbox.mSyncKey);
+        public void addResponsesParser() throws IOException {
+            String serverId = null;
+            String clientId = null;
             ContentValues cv = new ContentValues();
-            cv.put(MailboxColumns.SYNC_KEY, mMailbox.mSyncKey);
-            Mailbox.update(mContext, Mailbox.CONTENT_URI, mMailbox.mId, cv);
+            while (nextTag(Tags.SYNC_ADD) != END) {
+                switch (tag) {
+                    case Tags.SYNC_SERVER_ID:
+                        serverId = getValue();
+                        break;
+                    case Tags.SYNC_CLIENT_ID:
+                        clientId = getValue();
+                        break;
+                    case Tags.SYNC_STATUS:
+                        getValue();
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+
+            // This is theoretically impossible, but...
+            if (clientId == null || serverId == null) return;
+
+            Cursor c = getClientIdCursor(clientId);
+            try {
+                if (c.moveToFirst()) {
+                    cv.put(RawContacts.SOURCE_ID, serverId);
+                    cv.put(RawContacts.DIRTY, 0);
+                    ops.add(ContentProviderOperation.newUpdate(ContentUris
+                            .withAppendedId(RawContacts.CONTENT_URI, c.getLong(0)))
+                            .withValues(cv)
+                            .build());
+                    userLog("New contact " + clientId + " was given serverId: " + serverId);
+                }
+            } finally {
+                c.close();
+            }
+        }
+        @Override
+        public void responsesParser() throws IOException {
+            // Handle server responses here (for Add and Change)
+            while (nextTag(Tags.SYNC_RESPONSES) != END) {
+                if (tag == Tags.SYNC_ADD) {
+                    addResponsesParser();
+                } else if (tag == Tags.SYNC_CHANGE) {
+                    //changeResponsesParser();
+                } else
+                    skipTag();
+            }
         }
     }
 
@@ -1142,7 +1259,7 @@
     }
 
     @Override
-    public void cleanup(EasSyncService service) {
+    public void cleanup() {
         // Mark the changed contacts dirty = 0
         // TODO Put this in a single batch
         ContactOperations ops = new ContactOperations();
@@ -1190,8 +1307,9 @@
     }
 
     private void sendIm(Serializer s, ContentValues cv) throws IOException {
-        String value = cv.getAsString(Email.DATA);
-        switch (cv.getAsInteger(Email.TYPE)) {
+        String value = cv.getAsString(Im.DATA);
+        if (value == null) return;
+        switch (cv.getAsInteger(Im.TYPE)) {
             case TYPE_IM1:
                 s.data(Tags.CONTACTS2_IM_ADDRESS, value);
                 break;
@@ -1411,21 +1529,22 @@
     }
 
     @Override
-    public boolean sendLocalChanges(Serializer s, EasSyncService service) throws IOException {
+    public boolean sendLocalChanges(Serializer s) throws IOException {
         // First, let's find Contacts that have changed.
-        ContentResolver cr = service.mContentResolver;
+        ContentResolver cr = mService.mContentResolver;
         Uri uri = RawContacts.CONTENT_URI.buildUpon()
-                .appendQueryParameter(RawContacts.ACCOUNT_NAME, service.mAccount.mEmailAddress)
+                .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress)
                 .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
                 .build();
 
-        if (service.mMailbox.mSyncKey.equals("0")) {
+        if (getSyncKey().equals("0")) {
             return false;
         }
 
         try {
             // Get them all atomically
             EntityIterator ei = cr.queryEntities(uri, RawContacts.DIRTY + "=1", null, null);
+            ContentValues cidValues = new ContentValues();
             try {
                 boolean first = true;
                 while (ei.hasNext()) {
@@ -1433,17 +1552,25 @@
                     // For each of these entities, create the change commands
                     ContentValues entityValues = entity.getEntityValues();
                     String serverId = entityValues.getAsString(RawContacts.SOURCE_ID);
-                    if (serverId == null) {
-                        // TODO Handle upload of new contacts
-                        continue;
-                    }
                     ArrayList<Integer> groupIds = new ArrayList<Integer>();
                     if (first) {
                         s.start(Tags.SYNC_COMMANDS);
                         first = false;
                     }
-                    s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId)
-                        .start(Tags.SYNC_APPLICATION_DATA);
+                    if (serverId == null) {
+                        // This is a new contact; create a clientId
+                        String clientId = "new_" + mMailbox.mId + '_' + System.currentTimeMillis();
+                        s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
+                        // And save it in the raw contact
+                        cidValues.put(ContactsContract.RawContacts.SYNC1, clientId);
+                        cr.update(ContentUris.
+                                withAppendedId(ContactsContract.RawContacts.CONTENT_URI,
+                                        entityValues.getAsLong(ContactsContract.RawContacts._ID)),
+                                        cidValues, null, null);
+                    } else {
+                        s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
+                    }
+                    s.start(Tags.SYNC_APPLICATION_DATA);
                     // Write out the data here
                     for (NamedContentValues ncv: entity.getSubValues()) {
                         ContentValues cv = ncv.values;
diff --git a/src/com/android/exchange/adapter/EmailSyncAdapter.java b/src/com/android/exchange/adapter/EmailSyncAdapter.java
index 6762d4b..2cd43c2 100644
--- a/src/com/android/exchange/adapter/EmailSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EmailSyncAdapter.java
@@ -72,8 +72,8 @@
     }
 
     @Override
-    public boolean parse(InputStream is, EasSyncService service) throws IOException {
-        EasEmailSyncParser p = new EasEmailSyncParser(is, service);
+    public boolean parse(InputStream is) throws IOException {
+        EasEmailSyncParser p = new EasEmailSyncParser(is, this);
         return p.parse();
     }
 
@@ -84,8 +84,12 @@
 
         private String mMailboxIdAsString;
 
-        public EasEmailSyncParser(InputStream in, EasSyncService service) throws IOException {
-            super(in, service);
+        ArrayList<Message> newEmails = new ArrayList<Message>();
+        ArrayList<Long> deletedEmails = new ArrayList<Long>();
+        ArrayList<ServerChange> changedEmails = new ArrayList<ServerChange>();
+
+        public EasEmailSyncParser(InputStream in, EmailSyncAdapter adapter) throws IOException {
+            super(in, adapter);
             mMailboxIdAsString = Long.toString(mMailbox.mId);
         }
 
@@ -399,11 +403,6 @@
          */
         @Override
         public void commandsParser() throws IOException {
-            ArrayList<Message> newEmails = new ArrayList<Message>();
-            ArrayList<Long> deletedEmails = new ArrayList<Long>();
-            ArrayList<ServerChange> changedEmails = new ArrayList<ServerChange>();
-            int notifyCount = 0;
-
             while (nextTag(Tags.SYNC_COMMANDS) != END) {
                 if (tag == Tags.SYNC_ADD) {
                     addParser(newEmails);
@@ -417,6 +416,15 @@
                 } else
                     skipTag();
             }
+        }
+
+        @Override
+        public void responsesParser() throws IOException {
+        }
+
+        @Override
+        public void commit() throws IOException {
+            int notifyCount = 0;
 
             // Use a batch operation to handle the changes
             // TODO New mail notifications?  Who looks for these?
@@ -500,12 +508,12 @@
     }
 
     @Override
-    public void cleanup(EasSyncService service) {
+    public void cleanup() {
         if (!mDeletedIdList.isEmpty() || !mUpdatedIdList.isEmpty()) {
             ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
             addCleanupOps(ops);
             try {
-                service.mContext.getContentResolver()
+                mContext.getContentResolver()
                     .applyBatch(EmailProvider.EMAIL_AUTHORITY, ops);
             } catch (RemoteException e) {
                 // There is nothing to be done here; fail by returning null
@@ -546,7 +554,7 @@
     }
 
     @Override
-    public boolean sendLocalChanges(Serializer s, EasSyncService service) throws IOException {
+    public boolean sendLocalChanges(Serializer s) throws IOException {
         ContentResolver cr = mContext.getContentResolver();
 
         // Find any of our deleted items
diff --git a/src/com/android/exchange/adapter/FolderSyncParser.java b/src/com/android/exchange/adapter/FolderSyncParser.java
index 379a80f..646c357 100644
--- a/src/com/android/exchange/adapter/FolderSyncParser.java
+++ b/src/com/android/exchange/adapter/FolderSyncParser.java
@@ -24,7 +24,6 @@
 import com.android.email.provider.EmailContent.Mailbox;
 import com.android.email.provider.EmailContent.MailboxColumns;
 import com.android.exchange.Eas;
-import com.android.exchange.EasSyncService;
 import com.android.exchange.MockParserStream;
 import com.android.exchange.SyncManager;
 
@@ -88,8 +87,8 @@
     private MockParserStream mMock = null;
     private String[] mBindArguments = new String[2];
 
-    public FolderSyncParser(InputStream in, EasSyncService service) throws IOException {
-        super(in, service);
+    public FolderSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException {
+        super(in, adapter);
         mAccountId = mAccount.mId;
         mAccountIdAsString = Long.toString(mAccountId);
         if (in instanceof MockParserStream) {
@@ -330,12 +329,27 @@
         }
     }
 
+    /**
+     * Not needed for FolderSync parsing; everything is done within changesParser
+     */
     @Override
     public void commandsParser() throws IOException {
     }
 
+    /**
+     * We don't need to implement commit() because all operations take place atomically within
+     * changesParser
+     */
+    @Override
+    public void commit() throws IOException {
+    }
+
     @Override
     public void wipe() {
     }
 
+    @Override
+    public void responsesParser() throws IOException {
+    }
+
 }
diff --git a/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java b/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java
index 6d7ba5e..e6f7353 100644
--- a/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java
+++ b/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java
@@ -65,7 +65,7 @@
     public void testGetMimeTypeFromFileName() throws IOException {
         EasSyncService service = getTestService();
         EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service);
-        EasEmailSyncParser p = adapter.new EasEmailSyncParser(getTestInputStream(), service);
+        EasEmailSyncParser p = adapter.new EasEmailSyncParser(getTestInputStream(), adapter);
         // Test a few known types
         String mimeType = p.getMimeTypeFromFileName("foo.jpg");
         assertEquals("image/jpeg", mimeType);